レガシーガジェット研究所

気になったことのあれこれ。

Redisのセットアップについて考えてみる ~ Transparent Huge Pages ~

概要

Redisの設定について書かれた以下リンクに気になる点があったので考察してみる。

https://redis.io/topics/admin

上記のリンク内でヒントとして挙げられているのは以下の項目。

  1. RedisをOS上での使用。
  2. オーバーコミットの有効化。
  3. Transparent Huge Pagesの無効化
  4. スワップの設定
  5. 設定項目であるmaxmemoryの調整
  6. 書き込みの多いアプリケーションではRDBの保存やAOFの書き換え時にメモリ使用量が増加する(<= 200%)
  7. daemontools下で動作していない場合はdaemonizeを使用する
  8. Redisが使用しているメモリ量と同等のバックログを準備する(レプリカとの再同期のため)
  9. 永続化を無効化していても、レプリケーションを使用する場合はRDBの保存が必要となる(ディスクレスレプリケーションを使用しない場合は)
  10. レプリケーション有効時、マスターで永続化の有効化及びクラッシュ時の再起動無効化を行う
  11. デフォルトでは認証を必要とせずインターネット上や攻撃者がアクセス可能な場所に置くのはセキュリティ上の問題となる。
  12. LATENCY DOCTOR及びMEMORY DOCTORの使用

上記のうちいくつかは自明で、オーバーコミット(2)やスワップの設定(4)はメモリ不足でのクラッシュやOOM Killerでの強制終了を回避する目的がある(これは記載されている)。

maxmemory(5)もわかりやすく、データ処理以外やフラグメンテーションを考慮した値の設定が必要であることを言及している。

7~12は永続化やレプリケーション、監視などについて書かれていてRedis固有のものだったりするので従うのが良いのはわかる。

個人的に一番気になったのはTransparent Huge Pages(THP)の無効化でDBやKVSではよく言われていること(無効化の推奨)のようだけど、一見有効化する方がページテーブルのサイズが小さくなったり、TLBのヒット率の向上など、メモリ効率やパフォーマンスなどに良い影響を与えるようにも思える。今回はこれを調べてみる。

ページ変換

Linuxでは4KBのページサイズをデフォルトとして使用しており、カーネルは4KB毎に仮想アドレスを物理アドレスに変換する。変換(ページウォーク)にはページテーブルという対応表を用いていて(各エントリが4KBのメモリ領域に対応する)、CPUのMMU(Memory Management Unit)がその変換を行っている。変換結果はTLB(Traslation Lookaside Buffer)と呼ばれるキャッシュが保持し、変換時にTLBのキャッシュミスが発生した場合にページウォークが行われる。

Huge Pages

Huge Pagesはそのページサイズを2MBや1GBにするもので、結果的にページサイズが大きい分ページテーブルのエントリ数は少なくてすみ且つTLBのヒット率も向上する。しかし事前のアロケーション(/proc/sys/vm/nr_hugepages)とHuge Pageを指定した明示的なmmapシステムコールもしくはhugetlbfsという専用のファイルシステムを通じたメモリ確保が必要だったり、Huge Pageとしてマッピングされたメモリ領域はスワップ領域に退避されなかったりと、いくつか設定が必要だったりや制限が存在する。

ただ実際にはOracle Databaseなんかでも最適化の要素として挙げられており実際に使用されているのがわかる。

https://docs.oracle.com/cd/E57425_01/121/CWLIN/memry.htm

自分のVPSでは設定が入っていなかった。

$ cat /proc/sys/vm/nr_hugepages
0

以下はmmapシステムコールのmanと、Huge Pageのためのフラグ。

MMAP(2)

Linux Programmer's Manual MMAP(2)

NAME
    mmap, munmap - map or unmap files or devices into memory

SYNOPSIS
    #include <sys/mman.h>

    void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);

:

MAP_HUGETLB (since Linux 2.6.32)

    Allocate the mapping using "huge pages."  
    See the Linux kernel source file Documentation/vm/hugetlbpage.txt 
    for further information, as well as NOTES, below.

Transparent Huge Pages

これが実際にRedisのドキュメント内で無効化するように言われているTHP。

THPは名前の通りそれを透過的に行うもので、有効になっている場合malloc()などでメモリを確保する際に状況に応じてHuge Pageとしてメモリを確保しマッピングを行ってくれる。以下で設定を確認できる。

$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never # ここでは"always"が選択されている

上記にもあるがオプションは3つ。

  • always: THPが各プロセスで有効で大抵の場合パフォーマンスは向上する(malloc時などに状況に応じて透過的にHuge Pageが確保される)。
  • madvise: madviseシステムコールによって明示的にリクエストされたメモリ領域でのみHuge Pagesが有効になる。
  • never: madviseシステムコールでリクエストされた場合でもHuge Pagesは有効とならない。

オプションの変更は以下のように行う(リブートするとデフォルトに戻る)

echo always >/sys/kernel/mm/transparent_hugepage/enabled

上記に加えてアプリケーション内でmadviseシステムコールを使用することでも設定でき、開始アドレスとサイズそしてMADV_HUGEPAGEをフラグを指定し呼び出すことで、指定の範囲内でTHPが有効となる。

MADVISE(2)                                                                       Linux Programmer's Manual                                                                       MADVISE(2)

NAME
       madvise - give advice about use of memory

SYNOPSIS
       #include <sys/mman.h>

       int madvise(void *addr, size_t length, int advice);

:

まとめるとTHPは小難しい設定やコーディングを必要とせず透過的にHuge Pagesを使用する機構として導入されたもので、一見無効化する必要のないもののように思える。しかしTHPはほぼ全てのDBやKVSで無効化が推奨されている、なぜなのか。

調べるとTHPで使用するページを確保するためのカーネルスレッドであるkhugepagedが問題となるらしいことがわかった。

khugepaged

khugepagedはTHPのHuge Pagesの管理を行うカーネルスレッドで、従来のHuge Pageでは/proc/sys/vm/nr_hugepagesなどを通じて事前にページの確保を行ったが、THPではそのページ割り当てをkhugepagedが動的に行う。しかしTHPの割り当てにはkswapddefragkcompactdといった複数のカーネルスレッドを呼び出す可能性があり、これが負荷の原因となる。

しかも従来のHuge Pagesとは違い、THPで確保されたHuge Pageはスワップ領域に退避される可能性があるため、ページサイズが大きいほどスワップ時のスパイクが起こりやすくなる。

ベンチマークは以下のサイトで行われていて、やっぱり性能劣化が少しばかり見られる模様。

https://www.percona.com/blog/2019/03/06/settling-the-myth-of-transparent-hugepages-for-databases/#

まとめ

最初に思った通りHuge Pages自体はページテーブルのサイズを小さくしたり、TLBのヒット率を向上させたりとパフォーマンスに良い影響を及ぼすようだった。しかしTHPはユーザフレンドリーなインターフェースを提供することを目的にロジックを隠蔽することで思わぬところでパフォーマンス劣化を生む結果となっていた。

余談ではあるけどRedisをソースコードにさらっと目を通した際に、malloc()関数にMAP_HUGETLBフラグは指定されておらずそもそもHuge Pagesを使用しない作りになっていたことが気になった。Redisのように大量のメモリを使用することを想定して作られたミドルウェアがHuge Pagesを使用しないのはなぜなのかという疑問が残った。

参考