Blog
本記事は、PFN2025 夏季インターンシッププログラムで勤務された中根 敦久さんによる寄稿です。
はじめに
東京大学大学院修士 1 年の中根敦久です。PFN 2025 夏季インターンシップで、Matlantisの Web システムのパフォーマンス改善を行いました。本記事ではその過程と結果をご紹介します。
背景・課題
PFN では、クラウド上で動作する汎⽤原⼦シミュレータである「Matlantis」の開発を行っています。Matlantis では Web ブラウザ上で Python ベースの対話型開発環境である JupyterLab を用いてインテラクティブに量子化学計算を行うことができます。
Matlantis の Web アプリの抱える課題の一つとして、立ち上げに時間がかかるという点がありました。具体的には、ユーザーがダッシュボード上でスタートボタンをクリックしてから JupyterLab が表示されて利用可能になるまでに 30 秒 ~ 数分程度の時間を要しており、場合によっては大きくユーザー体験に影響するケースがありました。そこで本インターンでは、この起動時間を短縮するパフォーマンス改善に取り組みました。
ボトルネックの特定
Matlantis の起動のプロセスは、その間に起こるリダイレクトを境にして、2 つに大別されます。前半のスタートボタンがクリックされてからリダイレクトされるまでの間では主にサーバー上でのリソースの作成が行われており、後半のリダイレクトから起動完了までの間では、主にブラウザ上での JupyterLab のレンダリングが行われています。はじめにこのそれぞれの所要時間をフロントエンド上で計測しました。
その結果、後半の所要時間の中央値は 10 秒未満であるのに対し、前半に関しては 30 秒以上を要していることが分かりました。さらに、前半に関してはテールレイテンシも悪く、90 % 分位点が 80 秒以上にも及んでいました。これを踏まえ、前半のリソース作成にかかる時間の短縮に優先的に取り掛かることにしました。
そこで、まずこのリソース作成のプロセスを細分化して分析しました。ここでのリソース作成の主な処理は Kubernetes クラスター内での Pod 作成です。Kubernetes のイベントログからこの各ステップの所要時間を集計したところ、Pod の Node へのスケジュールから初期化完了までの一連のプロセスの中で、幾つかのボトルネックが見つかりました(図 3)。
本インターンでは特に、コンテナイメージのプルに関する 2 つのボトルネックに注目しました。Pod の中では 3 つのコンテナが起動します。istio-init, istio-proxy コンテナは、サービス間通信を制御するためのサービスメッシュである Istio に関するイメージを、main コンテナは JupyterLab 本体が動作するイメージを要します。このそれぞれのプルに要する時間を短縮します。
従来の仕様
実は、既にこのイメージプルによるレイテンシは解消が試みられていました。Matlantis では、ユーザーから作成をリクエストされた Pod に対して Kubernetes のスケジューラが Node を割り当てる際に、 StandbyPod と呼んでいるキャッシュシステムを採用しています(図 4)。
このシステムでは、あらかじめ用意した Node 上に準備用の Pod (StandbyPod) を立て、そこにメインイメージからなるコンテナをスリープ状態で置いておきます(この時、Node にはメインイメージのキャッシュが保存されます)。そして、ユーザーから実際に Pod 作成をリクエストされた際には、スケジューラが優先度に基づくプリエンプションを起こし、StandbyPod を退避させてこの Node を割り当てます。Node はイメージキャッシュを保持しているため、素早くユーザー用の Pod を起動できます。
さらに具体的には、次のようになっています。StandbyPod には ユーザー用の Pod に対する PodAntiAffinity が定義されており、同一の Node にこの 2 種の Pod が共存できない設定となっています。また、ユーザー用の Pod には StandbyPod よりも高い PriorityClass が定められています。スケジューラがユーザー用の Pod をスケジュールする際、スケジュール可能な Node が見つからなければ、PriorityClass の低いStandbyPod を退避させることでスケジュールができるかを試し、このプリエンプションを発生させます。
このように、ユーザー用の Pod をスケジュールするための Node が、StandbyPod を常駐させる形で、メインイメージのキャッシュを保持した状態で常に用意されています。しかし以下に述べるようにこのシステムには不足が見つかったため、これを改良する形で高速化を行いました。
ボトルネックの解消
改善1. Istio 初期化コンテナの起動におけるイメージのプルについて
まず Istio のイメージのプルのボトルネックに関しては、StandbyPod 内でこのコンテナを起動することで解消されました。前述の通り従来の StandbyPod はメインイメージのコンテナを起動するため Node にそのキャッシュを保存させますが、Istio のイメージに関してはこれを行いません。その為ユーザーの Pod 起動時には、Istio のイメージを軽量ながらも毎回プルしており一定の時間を要していました。そこで、StandbyPod 内で新たに Istio のイメージからなるコンテナもスリープ状態で置いておき、Node にこのイメージのキャッシュも保存させることで高速化が実現されました。
実際、Kubernetes のイベントログから算出した Volume が Node にアタッチされてから Istio イメージのプル完了までの所要時間のヒストグラムは、図 6 のようになり、平均 8 秒程度の時間短縮に寄与しました。
改善 2. メインコンテナの起動におけるイメージのプルについて
メインのイメージのプルのボトルネックに関しては、図 3 に示した通り中央値は 0 秒 (小数点以下四捨五入) と、大半の場合は StandbyPod による Node 内のキャッシュが効いて素早く完了していますが、90 % 分位点が 33 秒となっているように、このキャッシュが効かずにプルをし直しているケースが一定数存在することがわかりました。
調査の結果、これは Kubernetes のスケジューラの仕様によるものと判明しました。何らかの原因で StandbyPod の乗った Node とは別に利用可能な Node がイメージキャッシュ未保持のまま存在する時、Kubernetes のデフォルトスケジューラは、ユーザーの Pod をこちらに割り当てます。これは、前述した通りプリエンプションは最終手段であって、プリエンプションをせずに済むスケジューリングの方法があればそちらが採用されるからです。この場合、割り当てられた Node にはイメージキャッシュがない為、Pod 内でのコンテナの起動はイメージのプルからやり直す必要があります。
この対策としては、Kubernetes スケジューラの標準機能である、ラベルによるフィルタリングを用いました。まず、StandbyPod の起動により Node へのイメージのキャッシュが済んだら、別のコンテナから Kubernetes API にリクエストを送ってその Node 自身にラベルを付与します(その為に StandbyPod には適切な権限を持った ServiceAccount も予め与えておきます)。さらにユーザー用の Pod には NodeAffinity としてこのラベルを要求するよう定義します。これにより、ユーザーの Pod には必ずこのラベルを持つ(即ちイメージキャッシュを保持した)Node が割り当てられるようになり、再度イメージプルがなされるケースはほとんどなくなりました。
なお、StandbyPod はユーザーの Pod と入れ替わる形で退避される必要があるため、PodAntiAffinityの条件を外すことは困難でした(StandbyPod とユーザー用の Pod の共存を許可し、むしろ PodAffinity でその共存を誘導する場合、確かにキャッシュ済みの Node にスケジュールされやすくはなるが、その場合 StandbyPod は停止しない。Deployment で StandbyPod の個数を指定していた都合上、不要になった StandbyPod は停止する必要があった)。また他の案として、スケジューラのカスタムやコントローラの作成なども考えられましたが、既存のコードベースに破壊的変更を注入することへの躊躇などから、これも採用は見送りました。
結果
以上の変更をプロダクション環境に反映させて変化を確認しました。Start ボタンのクリックからリダイレクトまでの所要時間は、修正前後で 図 9 のように変化しました。改善 1 を加えた 9/3 頃に 10 % 分位点及び中央値が、改善 2 を加えた 9/17 頃に 90 % 分位点が、それぞれ短縮されたことが確認できます。定量的には、改善前の 8/26 と改善後の 9/17 とで、中央値が 37 秒 から 29 秒へ、90 % 分位点が 85 秒 から 37 秒 へ、それぞれ短縮されました。Istio のイメージキャッシュの保持が全体的な改善に、ラベリングとアフィニティの修正がテールレイテンシの改善に寄与したと言えます。
おわりに
本インターンシップでは、Matlantis の Web アプリにおける JupyterLab の起動時間の測定および短縮を行いました。結果として、毎度の起動に対して 8 秒程度の起動時間の短縮、及びテールレイテンシの大幅な改善に成功しました。一方で、依然としてユーザー体験に十分優れた起動時間とは言えないというのも現状です。
2 ヶ月弱の本インターンシップは非常に有意義なものでした。技術的には、これまで経験の浅かった Kubernetes のキャッチアップに始まり、本番運用されているシステムの改修までを行わせていただきました。その過程でこれまでの開発ではあまり触れてこなかったインフラ業務への理解が増し、関心が深まりました。また、保守性・可用性の高いプロジェクトをチーム開発する上での作法や姿勢にも多くの学びがありました。業務外の交流イベントなどにおいても、個性豊かな社員・インターン同期の方々との議論や会話は非常に刺激的でした。
最後に、いつも適切な助言を頂きサポートくださったメンターの三浦さんをはじめとする Matlantis Service Dev チームの皆さん、そしてこの非常に充実したインターンシップを体験させていただいた PFN の皆さんにこの場を借りて感謝申し上げます。
出典・ライセンス
本記事の図 4, 5, 7 で使用しているKubernetesアイコンは、The Kubernetes Authors による著作物であり、CC-BY-4.0ライセンスに基づき利用しています。
出典: https://github.com/kubernetes/community/tree/master/icons