ミドルウェアにおけるOSリソースの有効活用　メモリ・ディスク編

# はじめに

これまでデータベースや検索エンジンといったミドルウェアをチューニングする機会にたびたび遭遇してきました。
どのようなポイントでチューニングをすれば良いのかを理解するには、ミドルウェアの仕組みを理解する必要があり、
なぜミドルウェアがそのような仕組みになっているかを理解するには、基盤の部分つまりOS（Linux）の仕組みを理解する必要があります。

個人的には、結局どのようなミドルウェアを使用するにも、

- いかに効率よく、メモリを使ってディスク上のファイルのデータを扱うか
- いかに効率よく、プロセスやスレッドで処理をさばくか

の２点だと思います。

本記事では、上記２点のうち、

- いかに効率よく、メモリを使ってディスク上のファイルのデータを扱うか

について、Linuxとミドルウェアがどのように工夫しているのかを、自分なりにまとめたものとなります。

**※この記事は都度ブラッシュアップしていきます**
**※勉強中のため間違っている箇所があるかもしれません、ツッコんでいただけると幸いです**

# 先に要約
- なるべくディスク上のファイルのデータをメモリに載せて読み書きしたい（**ディスクI/Oを減らす**）
- そのかわりメモリ上のデータを更新したらファイルにも反映する仕組みが必要（**データの永続化**）
- また更新内容を確実に担保するための仕組みも必要（**クラッシュリカバリ**）

# Linuxではどう工夫しているのか
## ディスクI/Oを減らす
### ページキャッシュ
`TODO 図`

Linuxでは、一度ディスクより読み込んだデータはメモリ上にキャッシュされます。
そして次回以降の読み込み処理ではディスクへアクセスせずにキャッシュに対して行われます。
そうすることで読み込みが高速に行われます。
#### メモリマップトファイル
`TODO 図`

~~ディスク上のファイルのデータを仮想アドレス空間上にメモリマップする機能。
システムコール`mmap`によってメモリマップ処理を行います。
マップされたファイルのデータはメモリへのアクセスと同じように行えるため、ディスクI/Oの削減に効果があります。~~
`ページキャッシュ`上のデータへのアクセスについては、システムコール`read`や`write`にてアクセスする他に、`mmap`により仮想アドレス空間上にメモリマップする方法があります。
マップされたファイルのデータはメモリへのアクセスと同じように行える。

## データの永続化
`TODO 図`

### ダーティーページ
`ページキャッシュ`上で修正されたデータはすぐにはディスク上へ書き込まれません。
書き込みが行われたタイミングで、その都度ディスクへの書き込みが発生するのはパフォーマンス的によろしくないからです。
修正されたデータは`ダーティーページ`とよばれ、後述する`ライトバック`の仕組みにより、非同期でディスク上へと書き込まれます。
### ライトバック（遅延書き込み）
ページキャッシュ上の修正されたデータである`ダーティーページ`はバックグラウンドプロセスにより、後ほどディスク上のファイルへとフラッシュされます。
なお、明示的にフラッシュさせたい場合は、`fsync`や`fdatasync`といったシステムコールを呼び出します。

# MySQL（InnoDB）ではどう工夫しているのか
![mysql_all.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22736/2d32967e-e020-47dd-d497-8371ae7dac0a.png)

## ディスクI/Oを減らす
MySQL（InnoDB）ではOSのキャッシュ機構（ページキャッシュ）を使用せずに、独自のキャッシュ機構（`バッファプール`）を使用することでディスクI/Oの削減を実現しています。
### バッファプール
テーブルやインデックスといった主要な情報は、ディスク上の`テーブルスペース`で管理されています。
クライアントからそれらデータへアクセスする場合は、直接`テーブルスペース`にいくのではなく、キャッシュ領域である`バッファプール`に対して行われます。
仮に欲しいデータがまだ`バッファプール`上にキャッシュされていない場合は、いったん`テーブルスペース`より取得（フェッチ）して`バッファプール`へ格納します。
更新処理についても同様に`バッファプール`に対して行われます。
あくまでもメモリ上の`バッファプール`に対してアクセスを行うことで、ディスクへのI/Oを減らしています。
### ページキャッシュの無効化
ディスクI/Oを減らす手段として、そもそもOSにはページキャッシュが存在します。
しかしながらInnoDBでは、データをキャッシュする手段としては`バッファプール`を使用したほうが効率がよい。
そこでページキャッシュを無効化する設定を行うことで、より効率的なデータアクセスを実現しています。
### 関連パラメータ
- innodb_buffer_pool_size
  - `バッファプール`のサイズを指定します
- innodb_buffer_pool_instances
  - `バッファプール`のインスタンス数を指定します
  - インスタンス数が複数あることで、バックグラウンドスレッドによる`バッファプール`へのI/Oの競合を抑えられます
- innodb_flush_method
  - フラッシュする方法を指定します
  - `O_DIRECT`にすることでページキャッシュをバイパスさせることができます

## データの永続化
キャッシュ上の更新データをディスク上へと反映する仕組みはLinuxと似たような仕組みとなっています。
都度反映ではなくバックグラウンドで行われます。
### ダーティーページ
`バッファプール`に対して書き込まれた情報は即座に`テーブルスペース`へと反映（フラッシュ）されません。
都度フラッシュをするとディスクI/Oが頻発し、結果としてパフォーマンスに影響が出てしまうためです。
したがって、`テーブルスペース`へまだ書き込まれていない、`バッファプール`上にのみ存在する更新情報（`ダーティーページ`）が存在することになります。
`ダーティーページ`はバックグラウンドスレッドにより非同期で`テーブルスペース`へと書き込まれます。
### チェックポイント
`バッファプール`上の`ダーティーページ`はあるタイミングで`テーブルスペース`へフラッシュされます。
`チェックポイント`とは、あるタイミングでディスクへフラッシュするプロセスで、2種類の`チェックポイント`が存在します。
通常は定期的に少しずつフラッシュする、`ファジーチェックポイント`という仕組みを使います。
`ファジーチェックポイント`では`ダーティーページ`の割合や`InnoDBログ`のサイズなどを考慮して、とても"良い塩梅"に少しずつフラッシュしてくれるので、
一度に全ての`ダーティーページ`をフラッシュする場合に比べて、ディスクI/Oのオーバーヘッドを分散させることができます。
一方、`ダーティーページ`の割合や`InnoDBログ`のサイズが閾値を超えた場合に発生する`シャープチェックポイント`は、一度に全ての`ダーティーページ`をフラッシュしてしまうので、
パフォーマンスの低下につながります。
### 関連パラメータ
- innodb_log_file_size
  - `InnoDBログ`のファイルサイズを指定します
- innodb_log_files_in_group
  - `InnoDBログ`のファイル数を指定します
  - `InnoDBログ`のサイズの合計は`innodb_log_file_size` * `innodb_log_files_in_group`となります
    - この値があまりにも小さいと頻繁に`ダーティーページ`のフラッシュが発生してしまいます
    - ただしあまりにも大きいとクラッシュリカバリに時間がかかってしまうケースもあります
- innodb_max_dirty_pages_pct
  - `バッファプール`内における`ダーティーページ`の比率の上限を設定します
  - この上限に達した場合、強制的にチェックポイント処理（`シャープチェックポイント`）が発生します
- innodb_io_capacity
  - バックグラウンドで実行されるI/O処理の上限値を設定します
- innodb_write_io_threads
  - バックグラウンドスレッド（書き込みI/O）のスレッド数を設定します

## クラッシュリカバリ
### InnoDBログ
`InnoDBログ`とはデータの更新情報が記録されるファイルです。
InnoDBでは、更新処理が発生した際は、更新情報を先に`InnoDBログバッファ`へ書き込んだあとに、`バッファプール`へ書き込まれます。
そして`InnoDBログバッファ`に書き込まれたオペレーション情報は、コミットのたびに`InnoDBログ`へとフラッシュされます。（※）
なぜわざわざ`InnoDBログ`でオペレーション情報を保持しているかというと、`ダーティーページ`のフラッシュ漏れを防ぐためです。
仮に`ダーティーページ`がフラッシュされていない状態でサーバがダウンしても、`InnoDBログ`のオペレーションを元に更新内容を反映（リカバリ）することができます。

※innodb_flush_log_at_trx_commit=1の場合に限る
### 関連パラメータ
- innodb_log_buffer_size
  - `InnoDBログバッファ`のサイズを指定します
- innodb_log_file_size
  - `InnoDBログ`のファイルサイズを指定します
- innodb_log_files_in_group
  - `InnoDBログ`のファイル数を指定します
  - `InnoDBログ`のサイズの合計は`innodb_log_file_size` * `innodb_log_files_in_group`となります
- innodb_flush_log_at_trx_commit
  - `InnoDBログ`へのフラッシュのタイミングを指定します
  - `1`を指定することでCOMMITのタイミングで`InnoDBログ`へのフラッシュが行われます

# PostgreSQLではどう工夫しているのか
![postgresql_all.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22736/26520bfe-a402-87c2-57f9-b3ea2dffd432.png)

## ディスクI/Oを減らす
### 共有バッファ
`TODO`
### 関連パラメータ
`TODO`
## データの永続化
### ダーティーページ
`TODO`
### チェックポイント
`TODO`
### 関連パラメータ
`TODO`
## クラッシュリカバリ
### WAL
`TODO`
### 関連パラメータ
`TODO`

# Elasticsearchではどう工夫しているのか
`TODO 図`

## ディスクI/Oを減らす
Elasticsearchでは独自のキャッシュ機構（`Indexing Buffer`）とOSのキャッシュ機構（ページキャッシュ）をうまく利用することでディスクI/Oの削減を実現しています。
### Indexing Buffer
データの登録（インデクシング）を行う際は、`Indexing Buffer`と呼ばれるメモリ領域に対して書き込みが行われます。
ディスクに直接アクセスせずにメモリに対して処理を行うことで、ディスクI/Oの軽減を実現しています。
### （ページキャッシュ上の）Segment
Elasticsearchでは、`Index`や`Shard`と呼ばれる論理的な概念でデータを管理しています。
さらに内部的にはluceneの`Segment`とよばれるファイル単位でデータを物理的に保持しています。
そして`Segment`はページキャッシュ上にキャッシュされます。

前述の`Indexing Buffer`へと書き込まれた情報は、そのままの状態では検索対象としてヒットしません。
検索処理はページキャッシュ上の`Segment`に対して行われるため、キャッシュ上の`Segment`への書き込みが必要となります。
ではどうするのかというと、リフレッシュ処理を行う必要があります。
リフレッシュ処理を行うことで、ページキャッシュ上の`Segment`へと更新情報が書き込まれます。

データ検索時も、ディスクに直接アクセスせずに、あくまでもメモリに対して処理が行われるので、ディスクI/Oの軽減が期待できます。
### 関連パラメータ
- ES_HEAP_SIZE
  - ヒープサイズを指定します
  - 物理メモリの半分を指定することが推奨されていますが、32GBを超えてはいけません
- indices.memory.index_buffer_size
  - `Indexing Buffer`のサイズを指定します
  - ヒープのうちどのくらいの割合を確保（%もしくはバイト）するかを指定します
- index.refresh_interval
  - リフレッシュ処理を行う間隔を指定します
  - `-1`を指定することで自動的なリフレッシュを無効化できます
- index.number_of_shards
  - `Shard`の数を指定します
- index.number_of_replicas
  - `Shard`の複製（レプリカ）の数を指定します

## データの永続化
### （ディスク上の）Segment
ページキャッシュ上の`Segment`は、フラッシュのタイミングでディスク上の`Segment`として作成されます。
フラッシュが行われる契機については、明示的にフラッシュ処理が行われるか、後述する`Translog`がいっぱいになった場合のどちらかとなります。
なお、ディスク上の`Segment`はいったん作成されると更新不可（Immutable）となります。
そのため、データの更新もしくは削除が発生した場合は、新たな`Segment`を作成し、古い`Segment`に削除フラグ（`.del`）が付与されます。
その後`Segment`をマージすることで、更新や削除の情報が反映され、最新の`Segment`として永続化されます。
### 関連パラメータ

## クラッシュリカバリ
### Translog
`Translog`とはデータの登録および削除処理のオペレーションが記録されるファイルです。
そして`Translogバッファ`に書き込まれたオペレーション情報は、都度`Translog`へとフラッシュされます。（※）
なぜわざわざ`Translog`でオペレーション情報を保持しているかというと、ページキャッシュ上の`Segment`のフラッシュ漏れを防ぐためです。
仮に`Segment`がフラッシュされていない状態でサーバがダウンしても、`Translog`のオペレーションを元に更新内容を反映（リカバリ）することができます。

※index.translog.durability=requestの場合に限る

### 関連パラメータ
- index.translog.sync_interval
  - `Translogバッファ`から`Translog`へフラッシュする間隔を指定します
- index.translog.durability
  - `Translogバッファ`から`Translog`へフラッシュする契機を指定します
  - デフォルト（`request`）の場合はリクエストの都度`Translog`へと書き込まれます
- index.translog.flush_threshold_size
  - フラッシュが発生する`Translog`のサイズを指定します
  - `Translog`のサイズが当該値に達するとフラッシュが発生します
- index.translog.retention.size
  - `Translog`のファイル合計サイズを指定します

# おわりに
`TODO`

# 参考書籍・サイト
- [[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識](https://www.amazon.co.jp/%E8%A9%A6%E3%81%97%E3%81%A6%E7%90%86%E8%A7%A3-Linux%E3%81%AE%E3%81%97%E3%81%8F%E3%81%BF-%E5%AE%9F%E9%A8%93%E3%81%A8%E5%9B%B3%E8%A7%A3%E3%81%A7%E5%AD%A6%E3%81%B6OS%E3%81%A8%E3%83%8F%E3%83%BC%E3%83%89%E3%82%A6%E3%82%A7%E3%82%A2%E3%81%AE%E5%9F%BA%E7%A4%8E%E7%9F%A5%E8%AD%98-%E6%AD%A6%E5%86%85-%E8%A6%9A/dp/477419607X)
- [詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド](https://www.amazon.co.jp/%E8%A9%B3%E8%A7%A3MySQL-5-7-%E6%AD%A2%E3%81%BE%E3%82%89%E3%81%AC%E9%80%B2%E5%8C%96%E3%81%AB%E4%B9%97%E3%82%8A%E9%81%85%E3%82%8C%E3%81%AA%E3%81%84%E3%81%9F%E3%82%81%E3%81%AE%E3%83%86%E3%82%AF%E3%83%8B%E3%82%AB%E3%83%AB%E3%82%AC%E3%82%A4%E3%83%89-NEXT-ONE/dp/4798147400)
- [［改訂新版］内部構造から学ぶPostgreSQL 設計・運用計画の鉄則](https://www.amazon.co.jp/o/ASIN/4297100894/gihyojp-22)
- [Elasticsearch The Definitive Guide](https://www.amazon.co.jp/Elasticsearch-Definitive-Distributed-Real-Time-Analytics/dp/1449358543)
- [検索結果の品質向上 Elasticsearch入門](https://speakerdeck.com/rtechkouhou/jian-suo-jie-guo-falsepin-zhi-xiang-shang-elasticsearchru-men)
- [64ビットプラットフォームで絶対お勧めのLuceneのMMapDirectory （Uwe氏のブログより）](http://lucene.jugem.jp/?eid=474)