LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

プライベートクラウドにおけるVM Live Migrationの通信暗号化の実装と検証(インターンレポート)

こんにちは。慶應義塾大学大学院工学研究科人間・社会システム情報科学専攻の菱田快成です。私はIaaSチームのインターン生としてプライベートクラウド「Flava」におけるOpenStackを中心とした仮想マシン基盤の開発業務に携わりました。インターン中に取り組んだタスクの1つであるVM Live Migrationの通信の暗号化について紹介します。本記事ではこの実現のために必要となったコンポーネントの開発や、その検証、性能評価についてまとめます。

仮想マシンのLive Migration

LINEヤフーのプライベートクラウドでは数万台もの仮想マシンが動作しており、当然その仮想マシンをホストする物理マシンの数も膨大です。このような環境では、物理ホスト上で動作中の仮想マシンを止めることなく物理ホストのメンテナンスをしたい状況も多々あります。動作中の仮想マシンを止めることなく、別の物理ホストへ移動させる技術がLive Migrationです。仮想マシンのLive Migrationは以下の手順で行われます。

  1. 移動先物理ホストへ仮想マシンを止めずに、メモリとディスクを全て転送する。
  2. 転送した後に送信元仮想マシンによって書き換えられた部分を再度転送する。
  3. 再び書き換えられた部分を転送する。
  4. 手順3を繰り返し、書き換えられた部分が十分小さくなったら、一瞬だけ仮想マシンを止めて残りを転送する。
  5. 転送先の物理ホストで仮想マシンを動かす。

この他にもネットワークを切断させることなく移動させるための工夫もありますが、本記事では省略します。ここで重要なのはLive Migration時にはメモリとディスクの転送が行われるということです。

Live Migrationの具体的な実装を理解するには、OpenStackがどのようにして仮想マシンを作成しているのかを理解する必要があります。OpenStackは仮想基盤ソフトウェアですが、OpenStack自身が仮想マシンを直接作成しているわけではありません。OpenStackでは、仮想マシンを実際に作成するソフトウェアをドライバと呼び、さまざまなドライバを組み合わせて使うことができます。例えばWindowsではHyper-Vが、Linuxではlibvirtがよく使われています。FlavaではホストのほとんどがLinuxであり、libvirtをドライバとして使用しています。libvirtもさらに抽象化レイヤーであり、バックエンドとしては主にQEMUが使われます。Live Migration時には、このQEMUが移動元、移動先物理ホスト間でTCPのセッションを張ることでメモリとディスクの転送を行います。

Live Migration時の各コンポーネントのレイヤー構造とTCP Sessionを張っている様子

Live Migration通信の暗号化

まず、Live Migrationの通信を暗号化する理由を説明します。Live Migrationでは、メモリ、ディスク、レジスタなど仮想マシンに関するすべての情報が転送されることになります。もし悪意を持った人がその通信を傍受することができる場合、仮想マシンに関するすべての情報を得ることができてしまいます。通信を暗号化することで、送信元と送信先の物理ホストを信頼できればよく、信頼すべき対象を減らせます。

TLSによるLive Migrationの暗号化

Live Migration通信の暗号化は、QEMUが確立するTCPセッションをTLS化することで行います。暗号化のためにTLS化する実装はOpenStack、libvirt、QEMUすべてのレイヤに入っており、OpenStackとlibvirtの設定ファイルを編集することで実現することができます。

TLSは、TCP通信を保護する仕組みで、接続先が本物かどうかを認証し、通信内容を暗号化し、完全性保護も行います。TLSのハンドシェイクでは各物理ホストがCA証明書と自らの秘密鍵と証明書を用いて安全にセッションを確立する必要があります。

FlavaのOpenStackでは各物理ホストに秘密鍵を用意して証明書を署名する仕組みがありませんでした。そのため、このタスクはFlavaのOpenStack内にCAを実装するところから始まりました。

CAの実装

CAは各物理ホストが秘密鍵から生成した署名リクエストを受け取り、リクエスト元を信頼できるならCAの秘密鍵を用いて署名を行います。TLSのセッション確立時には、接続するマシンが相手の証明書から証明書チェインを辿り、CAの証明書まで到達できるかをチェックし、身元の確認を行います。

今回のCAの実装において、特殊だったのはリクエスト元の確認で、Keystoneを用いています。KeystoneはOpenStackのコンポーネントの1つで、クライアントの認証を行うことができます。今回開発したCAでは、有効なKeystoneのトークンを持っているリクエストは信頼できるとして証明書への署名を行うようにしています。

CA自身と、各物理ホストでCAへ署名リクエストを行い鍵と証明書を管理するクライアントを実装してデプロイし、TLSのハンドシェイクに必要な要素が揃いました。このあと、各コンポーネントでTLSを行うために必要な設定ファイルを書き換えるよう、Ansibleにも変更を加えれば準備は完了です。

TLS化したLive Migrationの検証とバグ

手始めに転送量が数百MBと小さいVMで検証を行いました。小さいVMでは無事にLive Migrationに成功し、パケットキャプチャしたところ、TLSハンドシェイクをした形跡であるClient HelloとServer Helloを確認できました。

WiresharkでTLSのハンドシェイクを確認する様子

その後、大きなVMでも検証を行いました。メモリとディスクともに100GiBのフットプリントで転送量と転送にかかる時間も増えるケースです。結果として、大きいVMでLive Migrationをすると失敗し、ロールバックもされずVMがERROR状態になりました。OpenStackの各コンポーネントのログや物理ホストのログなどを照合して調査した結果、移動元のQEMUがSIGSEGVでプロセスが終了していたことが判明しました。

SIGSEGVの根本原因を突き止める

SIGSEGVは、nullポインタを参照するなどの不正なメモリアクセスによってOSにプロセスを終了させられたことを表しており、具体的な発生箇所はログには何も残りません。そこで、gdbを用いて具体的な発生箇所とバックトレースを見てみることにしました。

幸い、このバグは大きいVMで必ず再現し、無事バックトレースを取ることに成功しました。

Thread 123 "return path" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7f9a657fa640 (LWP 3716251)]
get_total_headers2 (params=0x55c951ecaa00, session=0x55c950df8c90) at /usr/src/debug/gnutls-3.8.3-4.el9_4.x86_64/lib/algorithms.h:270
270             return e->explicit_iv;
(gdb) bt
#0  get_total_headers2 (params=0x55c951ecaa00, session=0x55c950df8c90) at /usr/src/debug/gnutls-3.8.3-4.el9_4.x86_64/lib/algorithms.h:270
#1  recv_headers (ms=<optimized out>, record=0x7f9a657f92c0, htype=4294967295, type=GNUTLS_APPLICATION_DATA, record_params=0x55c951ecaa00, session=0x55c950df8c90) at /usr/src/debug/gnutls-3.8.3-4.el9_4.x86_64/lib/record.c:1172
#2  _gnutls_recv_in_buffers (session=session@entry=0x55c950df8c90, type=type@entry=GNUTLS_APPLICATION_DATA, htype=htype@entry=4294967295, ms=<optimized out>, ms@entry=0) at /usr/src/debug/gnutls-3.8.3-4.el9_4.x86_64/lib/record.c:1288
(省略)
#15 0x00007fb993c89c02 in start_thread () from target:/lib64/libc.so.6
#16 0x00007fb993d0ded4 in clone () from target:/lib64/libc.so.6

バックトレースでは、QEMUがリンクしているTLS実装であるGnuTLS内部でSIGSEGVしていて、変数eを参照する部分で落ちていることが分かります。

これを含むレジスタや周辺のコードをもとにCodexと相談しながら原因を探ったところ、最終的にGnuTLSのバグということが判明しました。また、QEMUサイドのIssueの存在も確認しました。Issueは2023年から存在していてGnuTLS側のfixがマージされたのが2025年だったので数年間存在していたようです。

このGnuTLSのバグはTLS v1.3の鍵更新の処理に存在していました。RFC 8446 4.6.3で鍵更新時には送信、受信の片方の鍵のみを更新するべきであるとしているのにも関わらず、修正前のGnuTLSでは同時に更新してしまっていたため、一部のメッセージの復号に失敗していたのが原因でした。AESでは、TLSレコードを2²⁴個送信すると自動で鍵更新を行います。QEMUのIssueによれば、32GBの転送あたり1回鍵更新が起きるようです。そのため、100GiBのVMでは必ずこのバグを踏んでいたのでした。

無事このパッチを適用して大きいVMで検証を行ったところ、TLSを有効化した状態でもLive Migrationに成功しました。

TLSを使ったLive Migrationの性能評価

TLSを使うと通信は暗号化されます。この暗号化には計算が必要で、現在の仕組みではCPUでこの計算を行っています。そこで、暗号化のスループットがボトルネックになるのか、ボトルネックになるならどの程度の速度が出るのかを計測する必要がありました。この検証の結果を踏まえて、Live Migrationのおおよその終了時間を予想したり、それをもとにオペレーションを行ったりすることができるようになります。

今回の計測では試験環境下で、TLSの有無でどの程度の性能低下が見られるのかを計測しました。具体的な計測結果を以下に示します。

TLSの有無によるLive Migrationのメモリとディスクの転送速度の比較グラフ

TLSを有効化することで、ディスクとメモリともに予想以上に大きいオーバーヘッドがあることが分かりました。ディスクでは平均して25.9%、メモリでは78.9%の速度低下が見られました。今回これだけの性能低下が見られたのはQEMUがTLSのための処理をシングルコアで行っているためだと疑っています。注意深く調べることでより高速化できる可能性はありますが、QEMUやGnuTLSのレイヤへの変更が必要であり、upstreamの改善が待たれます。

感想

この4週間は刺激の多いものとなりました。今回のインターンはなにか1つのインターンタスクに取り組むというよりは実際の業務に入り込んで他の社員の方々と同じように現実のチケットから選んで、メンターの方々と相談しながら進めていくという形でした。本記事で紹介したLive Migrationの暗号化タスクの他にもいくつかのタスクに取り組みました。

今回の調査には様々なログを参照する必要がありました。OpenStackの各物理ホストに存在しているnova-computeというコンポーネントのログ、そのバックエンドであるlibvirtのログ、そのバックエンドであるQEMUのログ。それに加えてカーネルログも参照しました。LINEヤフーのIaaSチームではこのようなログをログ収集基盤に集めていますが、最近ではそれをAIが分析できるような仕組みづくりが整備されてきています。最初にmigrationが失敗したときから、CodexはOpenStackの複数積み重なるレイヤーを1つ1つ丁寧に見ていき、GnuTLSが怪しいということは既に指摘していました。その後、実際にgdbをアタッチして操作したのは私ですが、Codexと協力して調査とログ分析を行い、CodexによるロバストなWeb検索能力があったからこそすぐに原因となるIssueを見つけ、根本原因を特定することができました。

Coding Agentを使えばモノはできてしまう最近の状況を鑑みると、一層このインターンは良い経験となりました。IaaSチームでは全社のインフラ基盤を作っているだけあり、手厚いレビュー体制が整っていました。コーディングはほぼCodexが行いましたが、私自身によるレビューでコードの品質を担保したり、チームメンバーによるレビューを受けてより品質を上げたりする作業に時間のほとんどを使いました。このような経験は個人開発をしている上では得られない良い機会だったと認識しています。また、実装だけでなく、その後のステージング環境での検証や、QAのフローにも携わったり、目にすることができて、とても楽しみながら参加できました。

技術的にも楽しいインターン期間となりました。大学では仮想化環境におけるOSによるパフォーマンスの最適化を扱っていることもあり、研究で目にしている技術が実際のサービス開発で使われ、開発されているところを目撃し、そのサイクルに参加することができたのは楽しかったです。また、OpenStackはQEMUなどのプリミティブなコンポーネントの上のレイヤに位置するものの、直面する問題によっては、実際にその低いレイヤーまで降りていき、問題解決をすることもあるという体験ができたのは非常に興味深く良い経験となりました。

おわりに

本記事では、インターン中に取り組んだVM Live Migrationの暗号化タスクで行った実装、直面した問題、性能評価について見ていきました。

4週間という短い期間でしたが、メンターの方やチームの方々などたくさんの社員の方々にお世話になりました。技術力の高い方々に囲まれながら開発に携われたことは、大変刺激的な経験でした。

本当にありがとうございました!

メンターからひとこと

菱田さんのインターンシップのメンターを担当した花川です。

今回のインターンシップでは、インターン向けに準備したお題というよりは、IaaSチームのバックログに積まれていた、純粋なチームのタスクに実際に取り組んでいただきました。そのため、本記事で紹介したものを含め、菱田さんが担当したのはFlavaの新機能に関するタスクや運用中に発生した問題への対応などです。いずれもOpenStackだけでなく、Flava固有の背景知識が求められました。

そんななか、菱田さんはAIもうまく活用しタスクの背景をすぐにキャッチアップして、実装や検証を行っていたのがとても印象的でした。本記事で触れたGnuTLSとQEMUの問題に対する真因の絞り込みや、パフォーマンスの測定と分析なども、AIを活用しながら丁寧かつスピーディーにこなしており、プロのソフトウェアエンジニアにも引けを取らない成果を上げていただきました。

インターンお疲れ様でした。今後の菱田さんの更なる活躍が楽しみです。