Linux ディスク暗号化の高速化

Data encryption at rest は、現代のインターネット企業にとって必要不可欠なものです。 しかし、多くの企業は、暗号化のオーバーヘッドによる潜在的なパフォーマンス低下を恐れて、ディスクを暗号化していません。

静止データの暗号化は、世界中に 200 以上のデータ センタを持つ Cloudflare にとって不可欠なものです。 この投稿では、Linux でのディスク暗号化のパフォーマンスを調査し、私たち自身と顧客のためにそれを少なくとも 2 倍速くした方法を説明します!

Encrypting data at rest

静止データの暗号化に関しては、最新のオペレーティング システム (OS) で実装できる方法がいくつかあります。 利用可能な技術は、典型的な OS のストレージ スタックと緊密に結合されています。 ストレージ スタックと暗号化ソリューションの簡略版を以下の図に示します。

スタックの最上部には、ファイル (またはストリーム) でデータを読み取り、書き込むアプリケーションがあります。 OS カーネルのファイル システムは、基礎となるブロック デバイスのどのブロックがどのファイルに属しているかを追跡し、これらのファイルの読み取りと書き込みをブロックの読み取りと書き込みに変換しますが、基礎となるストレージ デバイスのハードウェア仕様はファイルシステムから抽象化されています。 最後に、ブロック サブシステムは、適切なデバイス ドライバーを使用して、ブロックの読み取りと書き込みを基礎となるハードウェアに実際に渡します。

ストレージ スタックのコンセプトは、実際にはよく知られたネットワーク OSI モデルに似ており、各層には情報のよりハイレベルなビューがあり、下位層の実装詳細は上位層から抽象化されています。 また、OSI モデルと同様に、異なる層で暗号化を適用できます (TLS 対 IPsec または VPN について考えてみてください)。

静止データについては、ブロック層 (ハードウェアまたはソフトウェア) またはファイル レベル (アプリケーションまたはファイルシステムで直接) で暗号化を適用することができます。 アプリケーション レベルの暗号化では、アプリケーションの保守者は、必要な特定のデータに必要な任意の暗号化コードを適用することができます。 このアプローチの欠点は、実際に自分たちで実装しなければならないことで、一般に暗号化はあまり開発者に優しいものではありません。 アプリケーションがデータを使用する必要があるたびに、データを再び復号化して CPU サイクルを浪費するか、復号化された独自の「キャッシュ」を実装しなければならず、コードにさらなる複雑さをもたらします。 また、ファイルシステムは、特定のディレクトリだけを暗号化したり、ファイルごとに異なるキーを持つように設定することができます。 しかし、この柔軟性は、より複雑な設定という代償を払うことになります。 ファイルシステムの暗号化は、ファイルのコンテンツのみが暗号化されるため、ブロックデバイスの暗号化よりも安全性が低いと考えられています。 ファイルには、ファイル サイズ、ファイル数、ディレクトリ ツリーのレイアウトなど、関連するメタデータもあり、潜在的な敵にはまだ見えています。

ブロック層での暗号化 (ディスク暗号化またはフルディスク暗号化とも呼ばれる) は、アプリケーションやファイル システム全体に対してデータの暗号化を透過的にします。 ファイル システム レベルの暗号化とは異なり、ファイルのメタデータや空き領域も含めて、ディスク上のすべてのデータを暗号化します。 しかし、柔軟性に欠け、1つの鍵でディスク全体を暗号化することしかできないため、ディレクトリ単位、ファイル単位、ユーザー単位での設定ができません。 暗号の観点からは、ブロック層がデータの上位の概要を把握できなくなり、各ブロックを独立して処理する必要があるため、すべての暗号アルゴリズムが使用できるわけではありません。 一般的なアルゴリズムの多くは、安全性を確保するために何らかのブロックチェインを必要とするため、ディスク暗号化には適用できない。 その代わり、この特殊な用途のためだけに特別なモードが開発されました。

では、どのレイヤーを選択すればよいのでしょうか。 いつものように、それは依存します… アプリケーションおよびファイル システム レベルの暗号化は、柔軟性があるため、通常、クライアント システムに適した選択肢です。 たとえば、マルチユーザーデスクトップの各ユーザーは、自分が所有するキーでホームディレクトリを暗号化し、いくつかの共有ディレクトリは暗号化しないようにしたいかもしれません。 逆に、SaaS/PaaS/IaaS企業(クラウドフレアを含む)が管理するサーバーシステムでは、構成の簡素化とセキュリティの確保が望ましい選択です。フルディスク暗号化を有効にすると、どのアプリケーションからのデータも例外なく自動的に暗号化され、上書きされることはありません。 私たちは、すべてのデータは「重要」または「重要でない」バケットに分類されることなく保護される必要があると考えています。 そうすることで、通常、読み取り/書き込みのパフォーマンスが向上し、ホストからのリソース消費も少なくなります。 しかし、ほとんどのハードウェアファームウェアはプロプライエタリであるため、セキュリティコミュニティからの注目度やレビュー度はそれほど高くありません。 過去には、このことが、ハードウェア・ディスク暗号化の一部の実装に欠陥を生じさせ、セキュリティ・モデル全体を無意味なものにしてしまったことがあります。 たとえば、Microsoft は、それ以来、ソフトウェア ベースのディスク暗号化を好むようになりました。

私たちは、潜在的に安全でないソリューションを使用するリスクをデータや顧客のデータに負わせたくなく、オープンソースを強く信じています。 そのため、オープンであり、世界中の多くのセキュリティ専門家によって監査されている Linux カーネルのソフトウェア ディスク暗号化だけに頼っています。

Linux ディスク暗号化のパフォーマンス

私たちの目標は、顧客の帯域幅コストを削減するだけではなく、インターネット ユーザーにできるだけ速くコンテンツを提供することです。 いくつかのプロファイリングと簡単な A/B テストにより、Linux ディスクの暗号化を指摘しました。 データを暗号化しないことは (たとえそれが公共のインターネット キャッシュであっても) 持続可能な選択肢ではないため、Linux ディスク暗号化のパフォーマンスを詳しく調べることにしました。

Device Mapper and dm-crypt

Linux は dm-crypt モジュールと dm-crypt 自体は device mapper カーネル フレームワークによって透過ディスク暗号を実装しています。 簡単に言うと、デバイスマッパーは、ファイルシステムと基礎となるブロックデバイス間を移動する IO 要求を事前/事後処理することができます。

dm-crypt は特に、「書き込み」 IO 要求を実際のブロックデバイスに送る前に暗号化し、「読み取り」 IO 要求をファイルシステムドライバに送る前に復号化します。 シンプルで簡単ですね。

Benchmarking setup

記録として、この投稿の数字は、稼働していないアイドル状態の Cloudflare G9 サーバーで指定のコマンドを実行して得られたものです。 しかし、セットアップは、最新の x86 ラップトップで簡単に再現できるはずです。

一般に、ストレージ スタック周辺の何かをベンチマークすることは、ストレージ ハードウェア自体によってもたらされるノイズのために困難です。 すべてのディスクが同じように作られているわけではないので、この投稿の目的では、利用可能な最速ディスク、つまりディスクなしを使用します。 RAM は永続的なストレージよりもはるかに高速であるため、結果にほとんど影響を与えないはずです。

次のコマンドで 4GB の RAM ディスクを作成します:

$ sudo modprobe brd rd_nr=1 rd_size=4194304$ ls /dev/ram0

ここで、その上に dm-crypt インスタンスをセットアップし、ディスクを暗号化できるようにします。

$ fallocate -l 2M crypthdr.img$ sudo cryptsetup luksFormat /dev/ram0 --header crypthdr.imgWARNING!========This will overwrite data on crypthdr.img irrevocably.Are you sure? (Type uppercase yes): YESEnter passphrase:Verify passphrase:

LUKS/dm-crypt に詳しい方は、ここで LUKS 分離ヘッダーを使用していることにお気づきかもしれません。 通常、LUKS はパスワードで暗号化されたディスク暗号化キーをデータと同じディスクに保存しますが、暗号化デバイスと非暗号化デバイスの読み取り/書き込みパフォーマンスを比較したいので、後でベンチマークを行う際に誤って暗号化キーを上書きしてしまうかもしれません。

さて、テスト用に暗号化されたデバイスを実際に「アンロック」することができます。

$ sudo cryptsetup open --header crypthdr.img /dev/ram0 encrypted-ram0Enter passphrase for /dev/ram0:$ ls /dev/mapper/encrypted-ram0/dev/mapper/encrypted-ram0

この時点で、暗号化されたラムディスクと暗号化されていないラムディスクの性能を比較することができます。 同様に、/dev/mapper/encrypted-ram0 にデータを読み書きする場合、それは dm-crypt によって解読/暗号化され、暗号文として保存されます。

注目すべきは、ファイルシステムのオーバーヘッドによって結果に偏りが出ないように、ブロック デバイス上にいかなるファイル システムも作成していないことです。 暗号化されていない RAM ディスクに 4K ブロック サイズで単純なシーケンシャル読み取り/書き込みの負荷をシミュレートしてみましょう。 統計情報からわかるように、1126 MB/sあたりでだいたい同じスループットで読み書きができていることがわかります。 暗号化されたラムディスクでテストを繰り返してみましょう:

$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=cryptcrypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1fio-2.16Starting 1 process...Run status group 0 (all jobs): READ: io=1693.7MB, aggrb=150874KB/s, minb=150874KB/s, maxb=150874KB/s, mint=11491msec, maxt=11491msec WRITE: io=1696.4MB, aggrb=151170KB/s, minb=151170KB/s, maxb=151170KB/s, mint=11491msec, maxt=11491msec

おっと、これは低下していますね! 現在では ~147 MB/s となり、7倍以上の遅さになっています! そして、これは完全にアイドル状態のマシン上です!

Maybe, crypto is just slow

私たちが最初に考えたことは、最速の暗号を使用することを確実にすることです。 cryptsetup により、システム上で利用可能なすべての暗号化実装をベンチマークして、最適なものを選択できます。

$ sudo cryptsetup benchmark# Tests are approximate using memory only (no storage IO).PBKDF2-sha1 1340890 iterations per second for 256-bit keyPBKDF2-sha256 1539759 iterations per second for 256-bit keyPBKDF2-sha512 1205259 iterations per second for 256-bit keyPBKDF2-ripemd160 967321 iterations per second for 256-bit keyPBKDF2-whirlpool 720175 iterations per second for 256-bit key# Algorithm | Key | Encryption | Decryption aes-cbc 128b 969.7 MiB/s 3110.0 MiB/s serpent-cbc 128b N/A N/A twofish-cbc 128b N/A N/A aes-cbc 256b 756.1 MiB/s 2474.7 MiB/s serpent-cbc 256b N/A N/A twofish-cbc 256b N/A N/A aes-xts 256b 1823.1 MiB/s 1900.3 MiB/s serpent-xts 256b N/A N/A twofish-xts 256b N/A N/A aes-xts 512b 1724.4 MiB/s 1765.8 MiB/s serpent-xts 512b N/A N/A twofish-xts 512b N/A N/A

256 ビットのデータ暗号化キーを持つ aes-xts がここでは最速のようです。

$ sudo dmsetup table /dev/mapper/encrypted-ram00 8388608 crypt aes-xts-plain64 0000000000000000000000000000000000000000000000000000000000000000 0 1:0 0

256 ビットのデータ暗号化キーで aes-xts を使用します (dmsetup ツールによって都合よく隠されたすべてのゼロを数えます。実際のバイト数を確認するには、上のコマンドに --showkeys オプションを追加してください)。 しかし、この数字は加算されない。 cryptsetup benchmark は、「テストはメモリのみを使用した概算値です (ストレージ IO はありません)」として、この結果を当てにしないようにと述べていますが、これはまさに私たちがラムディスクを使用して実験をセットアップした方法そのものです。 もう少し悪いケース(全データを読み込み、並列処理なしで順次暗号化・復号化すると仮定)では、裏計算で(1126 * 1823) / (1126 + 1823) =~696 MB/s程度となり、実際の147 * 2 = 294 MB/s(読み込みと書き込みの合計)にはまだかなり遠いことがわかります。

dm-crypt performance flags

cryptsetup man ページを読んでいて、おそらくパフォーマンスチューニングに関連する --perf- という接頭辞のついたオプションが 2 つあることに気づきました。 最初のものは --perf-same_cpu_crypt で、かなり不可解な説明があります:

Perform encryption using the same cpu that IO was submitted on. The default is to use an unbound workqueue so that encryption work is automatically balanced between available CPUs. This option is only relevant for open action.

そこで、オプションを有効にします

$ sudo cryptsetup close encrypted-ram0$ sudo cryptsetup open --header crypthdr.img --perf-same_cpu_crypt /dev/ram0 encrypted-ram0

注意: 最新のマニュアル ページによると、cryptsetup refresh コマンドもあり、これを使用すると、暗号化デバイスを「閉じて」「再び開く」必要なしにこれらのオプションを有効化できます。

オプションが本当に有効になっているかどうかを確認する:

$ sudo dmsetup table encrypted-ram00 8388608 crypt aes-xts-plain64 0000000000000000000000000000000000000000000000000000000000000000 0 1:0 0 1 same_cpu_crypt

はい、出力に same_cpu_crypt が表示され、これは私たちが望んでいたものでした。 ベンチマークを再実行しましょう:

$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=cryptcrypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1fio-2.16Starting 1 process...Run status group 0 (all jobs): READ: io=1596.6MB, aggrb=139811KB/s, minb=139811KB/s, maxb=139811KB/s, mint=11693msec, maxt=11693msec WRITE: io=1600.9MB, aggrb=140192KB/s, minb=140192KB/s, maxb=140192KB/s, mint=11693msec, maxt=11693msec

うーん、今度は~136 MB/sで、前より少し悪くなっています、これはだめですね。 第二の選択肢--perf-submit_from_crypt_cpusはどうでしょう:

Disable offloading writes to a separate thread after encryption. There are some situations where offloading write bios from the encryption threads to a single thread degrades performance significantly. The default is to offload write bios to the same thread. This option is only relevant for open action.

もしかしたら、ここは「ある状況」なので、試してみましょう:

$ sudo cryptsetup close encrypted-ram0$ sudo cryptsetup open --header crypthdr.img --perf-submit_from_crypt_cpus /dev/ram0 encrypted-ram0Enter passphrase for /dev/ram0:$ sudo dmsetup table encrypted-ram00 8388608 crypt aes-xts-plain64 0000000000000000000000000000000000000000000000000000000000000000 0 1:0 0 1 submit_from_crypt_cpus

そして今度はベンチマーク:

$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=cryptcrypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1fio-2.16Starting 1 process...Run status group 0 (all jobs): READ: io=2066.6MB, aggrb=169835KB/s, minb=169835KB/s, maxb=169835KB/s, mint=12457msec, maxt=12457msec WRITE: io=2067.7MB, aggrb=169965KB/s, minb=169965KB/s, maxb=169965KB/s, mint=12457msec, maxt=12457msec

~166 MB/sこれは少し良くなりましたが、まだダメですね…。

Asking the community

絶望的だったので、インターネットにサポートを求めることにし、dm-crypt メーリングリストにこの結果を投稿しましたが、得られた反応はあまり励みになりませんでした:

この数字が気になるとしたら、それはあなたの側の理解不足からです。 おそらく、暗号化が重い操作であることをご存じないのでしょう…

私たちは、Google 検索に「暗号化は高価か」と入力して、このトピックについて科学的研究を行うことに決めました。 これはそれだけでも魅力的な読み物ですが、要点は、最新のハードウェア上の最新の暗号は、Cloudflareの規模(1秒間に数百万の暗号化されたHTTPリクエストを行う)でも非常に安価である、ということです。 実際、Cloudflare は、すべての人に無料の SSL/TLS を提供した最初のプロバイダーです。

Digging into the source code

上述のカスタム dm-crypt オプションを使用しようとしたとき、なぜそれらがそもそも存在し、その「オフロード」とは何であるかに興味を持ちました。 当初、私たちは dm-crypt が単純な「プロキシ」であり、スタックを通過するデータを暗号化/復号化するだけだと考えていました。 dm-crypt はメモリ バッファを暗号化するだけではなく、以下に示すような (単純化した) IO トラバース パスのダイアグラムを実行します:

ファイル システムが書き込み要求を発行すると、dm-crypt はそれをすぐに処理せずに “kcryptd” というワークキューに格納します。 一言で言えば、カーネルの作業キューは、ある作業 (この場合は暗号化) を、より都合のよい後の時間に実行するようにスケジュールするだけです。 その時」が来たら、dm-cryptはLinux Crypto APIに実際の暗号化のためのリクエストを送ります。 しかし、最近のLinux Crypto APIは非同期式なので、システムが使用する特定の実装によっては、すぐに処理されず、「後で」再びキューに入れられることがほとんどです。 Linux Crypto APIが最終的に暗号化を行うとき、dm-cryptは保留中の書き込み要求を赤黒いツリーに各要求を置くことによってソートしようとするかもしれません。

次に読み込み要求についてですが、今回は暗号化されたデータをハードウェアから最初に取得する必要がありますが、dm-crypt は単にデータのドライバを要求するのではなく、「kcryptd_io」という別の作業キューに要求をキューイングします。 後のある時点で、実際に暗号化されたデータを手に入れたら、今ではおなじみの “kcryptd” ワークキューを使用して復号化のためのスケジュールを立てます。 「kcryptd” は Linux Crypto API にリクエストを送信し、同様に非同期にデータを復号化することができます。

公平にするために、リクエストは常にこれらのキューのすべてを通過するわけではありませんが、ここで重要なのは、書き込みリクエストが dm-crypt で最大 4 回、読み込みリクエストが最大 3 回キューに入るかもしれないという点です。 この時点で、このような余分なキューイングがパフォーマンス上の問題を引き起こす可能性があるかどうかが気になりました。 例えば、キューイングとテールレイテンシーの関係についてのGoogleの素晴らしいプレゼンテーションがあります。

A significant amount of tail latency is due to queueing effects

So, why are all these queues there and can we remove them?

Git archeology

None wrote more complex code just for fun, especially for the OS kernel.All are all these queues is due to queues. ですから、これらすべてのキューは理由があってそこに置かれたに違いありません。 幸運なことに、Linux カーネル ソースは git で管理されているので、変更とその周辺の決定をたどることができます。

「kcryptd」ワークキューは、利用可能な歴史の最初から、以下のコメントとともにソースにありました。

つまり、読み取り専用だったわけですが、それでも、Linux Crypto API が暗号化に専用のスレッド/キューを使用するとしたら、なぜ割り込みコンテキストであるかどうかを気にするのでしょうか。

2006 年に dm-crypt は、暗号化だけでなく IO リクエストの送信にも “kcryptd” ワークキューを使用し始めました:

This patch is designed to help dm-crypt comply with the new constraints imposed by the following patch in -mm: md-dm-reduce-stack-usage-with-stacked-block-devices.Net を参照。patch

ここでの目標は、並行処理を増やすことではなく、むしろカーネルのスタック使用量を減らすことであったようです。 しかし、Linux カーネル スタックは 2014 年に x86 プラットフォーム向けに拡張されたので、これはもう問題ではないかもしれません。

“kcryptd_io” ワークキューの最初のバージョンは、メモリ割り当てを待つ多くの要求による飢餓を回避する目的で 2007 年に追加されました。 2011 年に、読み取り要求のキューを条件付きで一部戻す変更が導入されました。

十分なメモリがある場合、コードは別のスレッドでこの操作をキューに入れる代わりに、直接バイオを送信できます。

残念ながら、当時の Linux カーネルのコミット メッセージは今日ほど冗長ではなかったため、利用可能なパフォーマンス データはありません。

2015年に dm-crypt は、書き込みをスタック下に送信する前に別の「dmcrypt_write」スレッドでソートを開始しました。 その結果、書き込み要求は異なる順序で提出され、深刻なパフォーマンス低下を引き起こす可能性があります。

かつてシーケンシャル ディスク アクセスはランダムよりはるかに速く、dm-cryptはそのパターンを壊していたので、これは理にかなったことです。 しかし、これはほとんど回転ディスクに適用され、2015 年にはまだ支配的でした。 最近の高速 SSD (NVME SSD を含む) ではそれほど重要ではないかもしれません。

コミット メッセージの別の部分にも言及する価値があります。

…in particular it enables IO schedulers like CFQ to sort more effectively.

CFQ IO scheduler に対するパフォーマンス利点に言及していますが、Linux スケジューラは、2018 年に CFQ スケジューラがカーネルから除去されているというところまでそれ以降向上しています。

同じパッチセットでは、ソート リストを赤黒いツリーに置き換えています:

理論的には、ソートは基礎となるディスク スケジューラーによって実行されるべきですが、実際にはディスク スケジューラーは有限の要求のみを受け入れてソートしています。 すべての要求のソートを可能にするために、dm-crypt は独自のソートを実装する必要があります。

rbtree ベースのソートに関連するオーバーヘッドは無視できると考えられるので、条件付きで使用されません。

興味深いことに、同じパッチセットで、おなじみの “submit_from_crypt_cpus” オプションが導入されています。

暗号化スレッドから単一スレッドに書き込み BIOS をオフロードすると、パフォーマンスが大幅に低下する状況がいくつかあります

全体として、すべての変更は妥当かつ必要でしたが、その後の状況は変化しています。

  • ハードウェアはより速く、より賢くなった
  • Linux のリソース割り当てが見直された
  • 結合した Linux サブシステムが再構築された

また、上記の設計選択の多くは現代の Linux に適用できない可能性があります。

「クリーンアップ」

上記の調査に基づいて、すべての余分なキューと非同期動作を削除し、dm-crypt をその本来の目的、すなわち通過する IO 要求を単に暗号化/復号化して戻すことを試みることを決定しました。 しかし、安定性とさらなるベンチマークのために、結局実際のコードは削除せず、有効になっていればすべてのキューやスレッドをバイパスする、さらに別の dm-crypt オプションを追加することにしました。 このフラグにより、完全な実稼働負荷の下で実行時に現在の動作と新しい動作を切り替えることができるため、何らかの副作用が見られた場合に簡単に変更を元に戻すことができるようになりました。 このパッチは Cloudflare GitHub Linux リポジトリにあります。

Synchronous Linux Crypto API

上の図から、すべてのキューが dm-crypt で実装されているわけではないことが分かります。 最近のLinux Crypto APIは非同期である可能性もあり、この実験のために、そこでもキューを排除したいと思います。 とはいえ、「かもしれない」とはどういう意味でしょうか。 OSは同じアルゴリズムの異なる実装を含んでいるかもしれません(例えば、x86プラットフォーム上のハードウェアアクセラレーションを用いたAES-NIと一般的なCコードによるAESの実装など)。 デフォルトでは、システムは設定されたアルゴリズムの優先順位に基づいて「最適」なものを選択します。 dm-crypt はこの挙動を上書きし、capi: 接頭辞を使用して特定の暗号実装を要求することができます。 しかし、1つ問題があります。

$ grep -A 11 'xts(aes)' /proc/cryptoname : xts(aes)driver : xts(ecb(aes-generic))module : kernelpriority : 100refcnt : 7selftest : passedinternal : notype : skcipherasync : noblocksize : 16min keysize : 32max keysize : 64--name : __xts(aes)driver : cryptd(__xts-aes-aesni)module : cryptdpriority : 451refcnt : 1selftest : passedinternal : yestype : skcipherasync : yesblocksize : 16min keysize : 32max keysize : 64--name : xts(aes)driver : xts-aes-aesnimodule : aesni_intelpriority : 401refcnt : 1selftest : passedinternal : notype : skcipherasync : yesblocksize : 16min keysize : 32max keysize : 64--name : __xts(aes)driver : __xts-aes-aesnimodule : aesni_intelpriority : 401refcnt : 7selftest : passedinternal : yestype : skcipherasync : noblocksize : 16min keysize : 32max keysize : 64

スレッドでのキュー効果を避けるために、上記のリストから同期暗号を明示的に選択したいのですが、サポートされているのは xts(ecb(aes-generic)) (汎用 C 実装) と __xts-aes-aesni (x86 ハードウェア アクセラレーション実装) だけなのです。 後者の方がはるかに高速なのでぜひ欲しいところですが (ここではパフォーマンスを重視しています)、 不審なことに internal とマークされています (internal: yes を参照)。 ソース コードを確認すると、

Mark a cipher as a service implementation only usable by another cipher and never by a normal user of the kernel crypto API

つまり、この暗号は Crypto API 内の他のラッパー コードによってのみ使用でき、外部には使用できないように意図されていることがわかります。 実際には、Crypto API の呼び出し元は、特定の暗号実装を要求するときに、このフラグを明示的に指定する必要がありますが、dm-crypt は設計上 Linux Crypto API の一部ではなく、むしろ「外部」ユーザーであるため、これを行いません。 私たちはすでにdm-cryptモジュールにパッチを当てているので、関連するフラグを追加するだけでよいのです。 しかし、特にAES-NIにはもう一つ問題があります:x86 FPUです。 「浮動小数点 “と言いますか? ビットシフトとXOR演算だけで済むはずの対称型暗号に、なぜ浮動小数点演算が必要なのでしょうか? 浮動小数点演算は必要ありませんが、AES-NI命令はFPU専用のCPUレジスタのいくつかを使用します。 残念ながら、Linuxカーネルはパフォーマンス上の理由から、割り込みコンテキストでこれらのレジスタを常に保持するわけではありません(FPUの保存/復元にはコストがかかります)。 しかし、dm-crypt は割り込みコンテキストでコードを実行する可能性があるため、他のプロセスのデータを破損するリスクがあり、元のコードの「割り込みコンテキストで復号化を行うのは非常に賢明ではない」声明に戻ることになります。 このモジュールは同期で、独自の暗号をロールバックしませんが、暗号化要求の単なる「ルーター」です。

  • 現在の実行コンテキストで FPU (したがって AES-NI) を使用できる場合、暗号化要求をより高速で「内部」の __xts-aes-aesni 実装に転送するだけです (そしてここでそれを使用できます)。 なぜなら、私たちは今 Crypto API の一部だからです)
  • さもなければ、暗号化要求をより低速で一般的な C ベースの xts(ecb(aes-generic)) 実装に転送します

Using the whole lot

それでは、これを使用するプロセスを一緒に歩いていきましょう。 最初のステップはパッチを取得してカーネルを再コンパイルすることです (または dm-crypt と私たちの xtsproxy モジュールをコンパイルするだけです)。

次に、別のターミナルで IO ワークロードを再起動し、負荷がかかった状態で実行時にカーネルを再設定できることを確認します。

$ sudo modprobe xtsproxy$ grep -A 11 'xtsproxy' /proc/cryptodriver : xts-aes-xtsproxymodule : xtsproxypriority : 0refcnt : 0selftest : passedinternal : notype : skcipherasync : noblocksize : 16min keysize : 32max keysize : 64ivsize : 16chunksize : 16

新しくロードしたモジュールを使用するように暗号化ディスクを再構成し、パッチを適用した dm-crypt フラグを有効にします (cryptsetup は明らかに我々の修正に気づいていないため、低レベルの dmsetup ツールを使用しなければなりません):

$ sudo dmsetup table encrypted-ram0 --showkeys | sed 's/aes-xts-plain64/capi:xts-aes-xtsproxy-plain64/' | sed 's/$/ 1 force_inline/' | sudo dmsetup reload encrypted-ram0

我々は新しい構成を「ロード」しましたが、それを有効にするためには、暗号化デバイスを中断/再開する必要があります:

$ sudo dmsetup suspend encrypted-ram0 && sudo dmsetup resume encrypted-ram0

そして結果を確認してください。 fio ジョブを実行している別のターミナルに戻って出力を見ることもできますが、より良いものにするために、Grafana で観察された読み取り/書き込みスループットのスナップショットを示します:


なんと、スループットが 2 倍以上になりました! 総スループットは~640 MB/sとなり、上記で予想した~696 MB/sにかなり近づきました。 IOレイテンシはどうでしょうか? (iostat レポート ツールの await 統計値):

待ち時間も半分になりました!

To production

これまで、ファイル システムや実際のハードウェア、最も重要な実ワークロードなど、実環境のスタックの一部が欠けている合成セットアップを使用しています。 想像上のものを最適化していないことを確認するために、これらの変更がスタックのキャッシュ部分にもたらす実稼働環境への影響のスナップショットを以下に示します。 緑色の線は、暗号化されていないディスクを持つサーバーからのもので、これをベースラインとして使用します。 赤い線は、Linuxのデフォルトのディスク暗号化実装でディスクを暗号化したサーバーのもので、青い線は、暗号化ディスクと当社の最適化機能を有効にしたサーバーのものです。 見ての通り、デフォルトのLinuxディスク暗号化実装は、最悪のケースにおいてキャッシュのレイテンシに大きな影響を与えますが、パッチを適用した実装は暗号化を全く使用しない場合と区別がつきません。 言い換えれば、改善された暗号化実装はキャッシュの応答速度に全く影響を与えないので、基本的に無料で手に入れることができるのです。 これは勝利です!

私たちはまだ始まったばかりです

この投稿では、アーキテクチャーの見直しによってシステムのパフォーマンスが倍増することを示しました。 また、最新の暗号は高価ではなく、データを保護しない言い訳は通常ないことを再確認しました。

私たちはこの作業をメインのカーネル ソース ツリーに含めるために提出するつもりですが、おそらく現在の形ではありません。 強力なサーバーや、リソースに制約のある小さな IoT デバイス、その他多くの CPU アーキテクチャで実行されます。 現在のバージョンのパッチは、特定のアーキテクチャ上の特定のワークロードに対してディスク暗号化を最適化するだけですが、Linux にはどこでもスムーズに動作するソリューションが必要です。 実行時フラグにより、機能をその場で簡単に切り替えられ、特定のケースやセットアップにメリットがあるかどうか、簡単な A/B テストが実行されます。 これらのパッチは、5世代のハードウェアと200以上のデータセンターからなる当社の幅広いネットワークで実行されており、安定していると考えてよいでしょう。 Cloudflare のパフォーマンスとセキュリティをお楽しみください!

更新(2020 年 10 月 11 日)

このブログのメインパッチ(少し更新した形)は、メインラインの Linux カーネルにマージされ、バージョン 5.9 以降で利用可能です。 主な違いは、メインライン版では 1 つのフラグではなく 2 つのフラグが公開され、読み取りと書き込みのための dm-crypt ワークキューを独立してバイパスする機能が提供されることです。 詳細は dm-crypt の公式ドキュメントをご覧ください

コメントを残す

メールアドレスが公開されることはありません。