ブラウザレンダリングの仕組みについてメモ(Part1)

ブラウザ内部の処理、とくにレンダリングエンジンの処理の流れについて自分用に整理した単なるメモです。

gihyo.jp

レンダリングエンジンの処理の流れ

大まかには次のようなプロセスをたどるのかなと自分は整理しています。

  • リソースのダウンロード
  • リソースのパース
  • レンダリングツリーの構築
  • 描画

ここから各工程についてメモっていきますが、今回はリソースのパースまで書こうと思います。

リソースのダウンロード

まずそもそもここで言ってるリソースというのは、ブラウザが扱うリソースのことで、HTML、CSSJavaScript、画像ファイルのことを指しています。

リソースのダウンロードはサーバからHTMLなどのリソースを取得する工程で、ネットワークプロトコル(例えばHTTP)を使用してリソースを取得します。

(まあこのあたりはイメージしやすいのかな。)

より詳しく言及するとなると、HTTPの話になりそうで、レンダリングエンジンの話とは少し離れていく気がするので、これぐらいにしときます。

リソースのパース

リソースのパースはダウンロードしたリソースを構文解析(parse)して、レンダリングエンジンの内部表現に変換する処理のことです。

各リソースはレンダリングの実装に沿って内部表現に変換されていきます。

例えば、HTMLはDOMツリー、CSSはCSSOMツリーに変換されます。

DOM(Document Object Model)ツリーの構築

DOMとはHTMLのドキュメントを表現するオブジェクトのことで、 DOMツリーとはDOMを木構造として表現したもの。

詳しい説明はこのあたりでしょうか。

ダウンロードしたHTMLは以下の流れでDOMツリーへと変換されていきます。

トークンとは意味的に1つの塊になっている文字列のことで、 トークンを列にしたあとは構文木(木構造のデータ)を構築していきます。

構文木を構築したあとにDOMツリーを構築していくわけですが、構文木内にJavaScriptが含まれている場合は、その都度JavaScriptが実行されます。

JavaScriptの実行

JavsScriptの実行の流れとしては主に以下の流れ。

  1. 字句解析
  2. 構文解析
  3. コンパイル
  4. 実行

コンパイラの一般的な処理の流れと同じですね。

字句解析によりJavaScriptのコードをトーク列に変換し、 構文解析によりコンパイル可能な形式である抽象構文木に変換します。

どのようにコンパイルされるかはJavaScriptエンジンに依存しています。

JavaScriptエンジン - Wikipedia

JavaScriptエンジンで多いのはJITコンパイル型の実装のようです。

最後に、コンパイルされたコードは内部の仮想マシンやCPUで実行されます。

まとめ

とりあえずレンダリングエンジンの処理の流れをインプットして、レンダリングツリーの形式へ構築されるそれまでの前段工程をまとめてみました。

高度なチューニングやデバックなどはレンダリングエンジンの基本的な処理を抑えないと手が進まないので、深堀して整理したいと思いました。

とりあえず今回はここまで。

インデックスについてゆるくまとめる

アプリケーションエンジニアとしてデータベースと仲良くなろうということで、

このあたりの本を読みました。

gihyo.jp

gihyo.jp

gihyo.jp

そこで、インデックスについて何も知らんな、ということでゆるくまとめることにしました。

インデックスについて

検索を高速化するために用いられるもの。 索引のイメージに近くて、キーワードを検索すると、そのキーワードの場所がわかる感じ。

RDBで使われるインデックス

RDBで使われているインデックスだいたい以下のとおり。

  • B-tree(B+tree)インデックス
  • ビットマップインデックス
  • ハッシュインデックス

ここでは、B+treeインデックスについて少し取りあげる。

B+treeインデックスについて

多くのデータベース製品で採用されている検索アルゴリズム

B-treeの修正バージョン。OraclePostgreSQLMySQLなどで採用されている。

B+treeの検索性能が優れているところ

  • ルートとリーフの距離を一定に保つようにされているため、検索性能が安定
  • 木の深さが約3~4レベルくらいで一定していて、データもソートして保持しているため、2分探索によって検索コストを小さく抑えることができる
  • データがソートされていることから、集約関数などで必要になるソートをスキップできることがある。

インデックスの有効活用

どのような列に対してインデックスを作成するべかの基準となるのが、列のカーディナリティと選択率。

カーディナリティとは値のばらつき具合を示す概念。

選択率とは特定の列の値を指定したときに行をテーブル全体の母集合からどの程度絞り込めるかを示す概念。

基準となる2つの指標

  • カーディナリティが高いこと(値がよくばらついていること)
  • 選択率が低いこと(少ない行にしぼりこめること)
    • 5~10%前後が目安
      • 具体的な閾値DBMSやストレージ性能などの条件によって異なる

インデックスが使用できる構文

インデックスを使える主なSQLの構文は以下の通り。

  • WHERE句
  • JOIN
  • 相関サブクエリ
  • ソート

インデックスによる性能向上が難しいケース

大規模なデータベースになればなるほど、インデックス設計が重要になってくる。

注意として、インデックス設計はテーブル定義とSQLだけをみれば完結するものではないこと。

絞り込み条件を見極める必要があり、SQL文・検索キー列のカーディナリティを知る必要がある。

例えば以下のケースでは性能向上が難しい。

  • SQL文に絞り込み条件が存在しない
  • ほとんどレコードを絞り込めない
  • インデックスが使えない検索条件
    • 中間一致または後方一致のLIKE述語
    • 索引列で演算を行っている
      • (例) where col_1 + 1 > 100
    • IS NULL述語を使っている
    • 牽引列に対して関数を使用している
    • 否定形を用いている

インデックスによる悪影響

インデックスが増えるほど、以下の悪影響を及ぼす。

  • テーブル更新時にオーバヘッドが大きくなる
  • 必要なディスクスペースも大きくなる
  • データサイズが増えることでバッファプールのヒット率も悪化

インデックスが使えない場合の対処

大きく分けて3通り。

  • 外部設計による対処
  • データマートによる対処
  • インデックスオンリースキャン

外部設計による対処

外部設計レベルでパフォーマンスを意識した調整を行うこと。

例えばUI設計でクエリが実行されないようにアプリケーション側で制御する。

とくにアプリケーションとデータベースの設計がそれぞれ専門で実施されていると、 コミュニケーションの断絶が起きがち。 システムで重要機能要件と性能のためによるトレードオフについて妥協点を探す議論が必要。

データマートによる対処

データマートとは、 特定のクエリで必要とされるデータだけを保持する、相対的な小さなサイズのテーブルこと。

サマリテーブルと呼んだりする場合も。

目的はアクセス対象のテーブルサイズを小さくすることでI/O量を減らすこと。

データマート採用の注意観点

  • データの鮮度

    • データ同期のタイミングが短いほど鮮度は新しい。
      • 頻繁に更新処理実行されればパフォーマンス劣化の危険性がある
      • 伝統的には夜間バッチ実行で鮮度は最低1日前
  • データマートのサイズ

    • オリジナルテーブルのサイズと同じであれば意味がない
  • データマートの数
    • 機能要件に即したエンティティではないため管理が比較的難しい
    • ストレージ容量が逼迫
    • パックアップをストレージのスナップショット機能などで取得すると無駄にバックアップウィンドウを圧迫
  • バッチウィンドウ
    • データマート作成に時間がかかるため、バッチウィンドウを圧迫
    • 些細な差分更新でない限り統計情報も収集する必要がある
    • こうした処理を余裕を持って収めるためのパッチウィンドウとジョブネットの考慮が必要

インデックスオンリースキャンによる対処

インデックスオンリースキャンとは、 クエリ実行に必要なカラムがインデックスに含まれてあれば、テーブル全体にアクセスせず、インデックスだけにアクセスするクエリのこと。 このようなインデックスをカヴァリングインデックスという。

利点はI/Oを削減すること。

インデックスはテーブル列のサブセットしか保持していないため、そのサイズはテーブルに比べるとかなり小さい。

また、データマートにはアプリケーション側の改修が必要だが、インデックスの場合はそうした改修が不要。

インデックスオンリースキャン採用の注意観点

  • DBMS(とくに古いバージョン)によっては使えないことがある
  • 1つのインデックスに含められる列数には限度がある
  • 更新のオーバヘッドを増やす
  • 定期的なインデックスのリビルドが必要
    • 検索性能はインデックスのサイズに依存するので、サイズのモニタリングやリビルドが必要
  • SQL文に新たな列が追加されたら使えない

テーブルスキャンを選択した方がよい場合

インデックスを使わずにテーブルスキャンを選択した方がいいケース

  • あるインデックスを使うクエリの実行頻度がきわめて低い1日に1回など)
  • テーブルのサイズが非常に小さい
    • インデックスの対象は数万~数十万が目安
  • 検索結果が非常に多くの行にヒットする
    • インデックスには10%未満を指標とした方がよい

(最後に)本読んだ感想

3冊読み通してそれぞれの難易度をいうと、 SQL実践入門と理論から学ぶデータベースの本が同じぐらいで、 失敗から学ぶRDB本はやや易しい、という感じ。

違いをあげるとすると、SQL実践入門ではインデックスの対処法について比較的幅広く紹介されてました。 理論から学ぶデータベース実践入門はB+tree以外のインデックスについても説明がありましたね。 失敗から学ぶRDB本はわかりやすく端的に説明されてある印象をうけました。

大規模なデータベースを扱ったことがない経験がないので、どこまで通用するのかなー、実際やってみてどうなるんだろうか、というのは気になりましたね。

まあとりあえず基本的な常識(会話できるレベルの知識)は抑えておければいいかな。

コネクションプールって何?パフォーマンス観点から気にすること

バックエンドを勉強し始めて最近はデータベース周りに興味がでてきました。

最近読んでいるJavaパフォーマンスでデータベースに関するパフォーマンスの内容があったので、その個人的なゆるいメモです。

www.oreilly.co.jp

Javaパフォーマンスめっちゃいい本だと思う。)

今回はJDBCのコネクションプールに関して。

そもそもコネクションプールって何?

アプリケーションからデータベースへ接続するためのオブジェクトを生成するには時間がかかってしまいます。

接続回数が多くてその度に多くの初期化コストがかかってしまう、という場合は パフォーマンスがよくないので1度作った接続オブジェクトを再利用することでパフォーマンスをあげましょうと。

「データベースの呼び出しが数回しかない場合、 JDBCの呼び出しにはStatementを使った方が全体最適化されるけど、 何度も呼び出す場合はPreparedStatementを使った方がよい」って話と関連していると思いました。

んで、再利用する接続オブジェクトはどこから提供されるの?って話で、提供しているのがコネクションプール。

コネクションプールのライブラリはたくさんあって、SpringBootを触っていたらHikariCPとかが身近なのかな。

github.com

プール自体はフレームワーク内に用意されることが多いっぽい。

プリペアードステートメントを使ってパフォーマンスを向上させるには、 接続オブジェクトがプールに置かれて再利用されることが必要。

そしてプールを適切に用意するにはコネクションプールとJDBCドライバーの設定に注意が必要。

なのでコネクションプールは大事。

パフォーマンス観点からのコネクションプールの考え方

細かなサイジングとかの話はここでは取り上げなくて、気を付けないといけないことや大きな原則をまとめてみます。

気を付けること

まず、コネクションプールの利用によりヒープの消費が激しくなるという問題を持っています。

コネクションプールには、接続オブジェクトだけでなく、接続ごとに用意されるプリペアードステートメントもキャッシュされる。

これらの接続オブジェクトやプリペアードステートメントのプールが占めるメモリ量がGCへの悪影響を及ぼさないように設定することが大事。

あと、データベース側とのバランスも大事。

データベースに接続するたびにデータベースのリソースをより多く消費してしまう。

アプリケーションサーバが多くの接続を開いたままだとデータベースのパフォーマンスが悪化してしまう。

ではどうするか

では原則としてどうするかの話。

  • コネクションプールについてはアプリケーション内のスレッドごとに接続を1つ用意すること。
  • アプリケーションサーバではスレッドプールとコネクションプールのサイズを同じにすること。

ただし、データベースがボトルネックになっている状況では逆効果になる可能性があります。 その場合は、コネクションプールを使って同時アクセスを制限することでパフォーマンスを改善できる可能性があります。

今回はここまで。