ベンチマークを自動化すると何がうれしいのか

田口雄規氏(以下、田口):私からは、LINEで行っている「ネットワーク機能のベンチマーク自動化」の取り組みに関して紹介したいと思います。

まず簡単に自己紹介します。私は田口雄規と申します。新卒2年目でVerdaのネットワーク室に所属しています。2019年の3月までは、大学で学部と修士として、汎用サーバーをターゲットとしたNFVノードのパケット転送性能を向上させる研究をしていました。そして2019年4月にLINEに新卒入社しています。

携わっている主な業務としては、今日紹介するようなプライベートクラウドのデータプレーンのベンチマークを行って、性能問題の検証と今後の改善案を出すことをメインに行ってきました。そして今年に入ってからは、ロードバランサーのコントロールプレーンの改善プロジェクトや、データセンターのネットワークのオーケストレーター開発といった、データプレーン以外にも関わっています。

それでは本題に入る前に、今日お話することの概要を先に説明したいと思います。今日は、以下の3点についてお話しします。まず、LINEではソフトウェアロードバランサーを自社で開発・運用していますが、その開発フローに、CIのようなかたちで自動ベンチマークを組み込んで結果を開発者に提示しています。この例では、どうやってベンチマークを自動化しているのかというシステム的な話と、ベンチマークを自動化すると何がうれしいのかに関して紹介したいと思います。

次にベンチマークを行うために構築したテストベッド環境を紹介して、LINEのネットワーク検証を行うのに適したテストベッドがどういうものか。また、チームで限りあるリソースをどうやって共有しているのか、そういうことに関してお話しします。

そして最後に自動化されたベンチマークが役に立った事例として、SRv6の性能問題に対してベンチマークを使って問題を切り分け、最終的に解決策を提示するところにまで至った事例を紹介したいと思います。

ロードバランサーの事例

それでは、まず1つ目のロードバランサーの事例に移りたいと思います。先ほども言いましたが、LINEではレイヤー4のロードバランサーをデータプレーンから開発して運用しています。ロードバランサーには、以下のような特徴があります。

まず、ソフトウェアベースで開発されていて、普通のx86アーキテクチャのLinuxのサーバー上で動作します。またデータプレーンは、パケット転送性能の向上のためにXDPベースでフルスクラッチで実装されています。今回はロードバランサーの細かい説明は割愛しますので、詳しく知りたい場合は、2019年のLINE DEVELOPER DAYの資料などを参考にしてもらえればと思います。

現在、LINEのメッセンジャーや広告配信プラットフォームなどさまざまなコアサービス、ファミリーサービスがロードバランサーを利用しているので、高いパフォーマンスと安定性を維持し続けることが不可欠となっています。

では、私がチームに配属された2019年の時点で、ロードバランサーのテストがどう行われていたかを説明したいと思います。テストには、大きく分けて2種類があります。まず意図した通りに動作するかを確かめる機能テストと、あとは意図した通りの性能かを確かめる性能テストです。

機能テストは多くの場合、仮想環境で一通り行えるので、自動化は簡単です。ロードバランサーも例外ではなくて、既にCI上で簡単に自動化できるような状態になっていました。

一方で、性能テストは仮想環境でやっても意味がないので、実機を用意する必要があって自動化に手間がかかります。このような背景もあり、私がチームに参加した際には、開発者が本番環境へのデプロイ前などに手動で性能テストを実行しているというような状況で、データプレーンのテストが開発フローにうまく組み込まれているとは言えないような状況でした。

では、このように手動でテストを実行することの問題点を、ざっくりとまとめてみたいと思います。まずどんなマシンで、どんなコンフィグで、どんなテストをしているかという情報は、基本的にはテストの実行者しか知らない情報なので、単に他の人が同じテストを再現するのが難しいという問題があります。しっかりドキュメント化すればある程度解消できますが、やはり限界があると思います。

また設定ミスやそのときどきで用意できるリソースによって、テスト環境に違いが出てしまう可能性があります。このようなことが起きると、過去に行ったテスト結果と現在の性能の比較がどうしても難しくなってしまいます。新しく何か機能を実装した際に、それが性能にどれほど影響するかというのを確かめるためには、過去の結果と比較することが重要です。

他にも手動テストでは開発→テスト→修正のループを定常的に回すのが大変なので、テストの回数がどうしても減ってしまいます。結果的に開発途中のバージョンの性能がわからず、気付かないうちに性能低下が起こっている可能性があります。

それでは、今挙げたような問題点を私たちはどういうアプローチで解決したのかを説明します。まずベンチマークを誰でもいつでも再現可能にするために、宣言的なベンチマーク環境のプロビジョニングを行っています。これはAnsibleを使うことで実現していて、カーネルパラメータやNICの設定など、なるべく細かく自動化して再現性を高く保つようにしています。

また過去のテスト結果と比較できるように専用のテストベッド環境を用意して、毎回同じマシンを利用してベンチマークをするようにしています。このテストベッドに関しては、後ほど詳しく説明します。

最後に、開発者が意識をしなくてもフィードバックを得られるようにするため、GitHubやCIシステムとシームレスに連携できることを目指しました。また、現在の性能を過去の結果と比較できるようにするためのシステムも作成することにしました。

これまでの経緯から、下の図のようなシステムを作成しました。順に説明しますと、ロードバランサーの開発者は開発途中のバージョンのプルリクエストをGitHub上で作成します。するとCIシステムのDroneがフックされて、テストベッドのプロビジョニングが開始されます。

プロビジョニングにはAnsibleを使っていて、ルーティングの設定や開発途中のロードバランサーのデプロイなどが、自動で行われます。そして指定された複数のベンチマークが走ってグラフの生成までが自動的に行われます。ベンチマーク結果はプルリクエスト画面にポストされるので、開発者は継続的に性能のフィードバックを受け取りながら開発を進められます。

またベンチマーク結果は外部ストレージにも保存されていて、必要に応じて任意のバージョンとの性能比較ができます。

実施しているベンチマークシナリオ

次に、実施しているベンチマークシナリオの説明です。初めの段階ではパケットロスを許容した最大レートテストと、パケットロスを許容しないZero-Packet-Lossテストの2つを用意しました。最大レートテストでは、ワイヤーレートでトラフィックを送信して、ロードバランサーの最大性能を調べていて、Zero-Packet-Lossテストでは、送信レートを調整しながらドロップしないで転送できる最大性能を調べています。

その後、ロードバランサーに新たにステートフルな機能が追加され、その機能はSYN Flooding攻撃に弱いという機能をもっていたことがありました。このようなときには、もしSYN Flooding攻撃があったらどれくらい性能の低下が起こるのかを知っておく必要があります。そこで、その状況を模した特殊なトラフィック生成プログラムを新たに追加しました。

私たちのテストベッドでは、TRexというDPDKベースのトラフィックジェネレータを使用していますので、Pythonで簡単に新しいベンチマークシナリオを作ることが可能でした。

ベンチマークを行う上で、ロードバランサー固有の計測の難しさもありました。まず前提として、VerdaのロードバランサーではIPIPトンネリングを利用して、パケットをバックエンドサーバーまで届けています。そのためロードバランサーに入力されたパケットは、出力されると新たにIPヘッダが付与されます。これはトラフィックジェネレータから見ると、それぞれが別のフローのパケットであるように見えてしまいます。ここに問題がありました。

DPDKベースのトラフィックジェネレータでは、このような非対称トラフィックに対してはフローごとの統計情報が使えません。フローごとの統計が使えないと、関係ないARPやLLDPといったパケットもカウントされてしまうグローバルな統計を利用するしかなく、結果的に正確なZero-Packet-Lossテストが行えない、という問題に直面しました。

そこでこのベンチマークでは、99.9999パーセントのパケットが受信できていればロスなしとみなす、といった対策をとっています。この問題はハードウェアベースのトラフィックジェネレータを導入すれば解決するかもしれませんが、LINEではソフトウェアベースのトラフィックジェネレータの利点のほうが大きいと判断して、このような対策を取っています。

最後に、実際に稼働している自動ベンチマークのイメージを掲載しました。左側の部分が自動化を行っているDrone CIの画面で、イメージのビルドなどの通常のCIパイプラインが終了したあとに、ベンチマークが実行されるようになっています。

そしてベンチマークが終了すると、テスト結果が自動的にDrone CI経由でプルリクエスト上にポストされます。その様子が右側の画像で、最大スループットとZero-Packet-Lossテストの結果が掲載されています。この取り組みによって、開発者は自分のコードで性能低下が起きていないかどうかが即座にわかりますし、逆に性能が上がるような変更を入れた際の確認にも使えるようになっています。

限られた物理リソースをどうやって共有しているか

それでは次に、テストベッドの設計と限られた物理リソースをどうやって共有しているかに関して、紹介したいと思います。まず、専用のテストベッドを整備することになった背景から説明したいと思います。ネットワーク開発チームでは、ロードバランサーに限らず、さまざまなネットワーク機能の評価・検証を日々行う必要があります。

しかしテストベッドがない状況では、機材の在庫の状況や機材の設置などを毎回行う必要があって、検証に入れるまでかなり時間がかかってしまいます。そこで、プライベートクラウドを構成するスイッチやハイパーバイザー、NICなどの機器が一通り揃ったテストベッド環境があると便利そうだ、ということになり、構築することになりました。

そこでまず、LINEのインフラ検証に適した、テストベッドのネットワーク設計を行いました。LINEのデータセンターネットワークは、ハイパーバイザー上までフルL3の構成になっているので、テストベッドでも同様にハイパーバイザーやトラフィックジェネレータなどは、すべてCumulus LinuxベースのL3スイッチに最大100Gで接続されています。

L3スイッチを利用することで、基本的なテストトラフィックの制御にはスタティックルートで対応できますし、VRFやポリシーベースルーティングなどのような複雑な機能を使ったテストも可能になります。加えて、各機器にはマネジメント用のインターフェースをもたせており、テスト用のプロビジョニングはこちらから行えます。

1人でテストベッドを利用する場合は、このような環境を作るだけで十分なのですが、実際にはチーム内で複数人が同時に検証やベンチマークを行いたいという要求があるため、もう少し共有するための工夫をしました。

まず、物理サーバーを複数人で扱うために行った工夫です。テストや検証をする際に、自分の前に使っていた人のconfigが残ってしまっていて、おかしな状態になってしまうことがよくあるため、サーバーを利用する前には、毎回まっさらな環境になっていることが好ましいです。また、XDPなどのLinuxカーネルを基にしたデータプレーンでは、さまざまなバージョンのカーネルを試したいというケースもよくあります。

このような要求を満たすために、テストベッドではディスクレスブートが可能な構成にするためにマネジメント用のサーバーを用意しました。マネジメントサーバーはPXEとDHCP、ファイルサーバーの機能になっていて、あらかじめ設定しておくことで、サーバーはブート時にカーネルイメージとrootファイルシステムをマウントできます。

これによってユーザーは、自分の好きなカーネルイメージとファイルシステムで起動できて、他人の環境に影響することがありません。

次に、L3スイッチを共有するための工夫です。システムやルールが何もない状況では、ある人が新しく入れたルーティング設定によって、ベンチマークトラフィックが別のサーバーに誤って転送される可能性があります。そこで私たちがとった解決策は、VRF (Virtual Router and Forwarding) 機能を使うことでした。

独立したルーティングテーブルを利用者ごとに作成することをテストベッド利用上のルールにすることで、他のテストに影響を与えず、かつIPアドレスの割り当ての自由度も上げられました。

このような仕組みを導入して、他人に極力影響を与えずテストベッドを運用することが可能になったのですが、まだどのサーバーを誰が使っているかという情報が誰にもわからない状態ですし、自分が利用するポートを毎回メモしてVRFを作成する、という作業を個人がやるのは、さすがに面倒だと感じたので、この辺りを自動化できるサービスを作成することにしました。

作成したTestbed Managerというサービスの実際の画面がこちらです。左上には、現在誰がどのマシンを利用中かのリストが表示されています。そして新たにマシンを予約したい場合には、右側のフォームにサーバー名を入力することで、空いているサーバーが確保できます。Testbed Managerでは、マシンの確保だけではなく、L3スイッチの設定の自動化ができるようになっています。

その際の設定ログが、この下の四角の部分に表示されています。テストの実行者が直接スイッチにログインして、ルーティングやインターフェースの設定を変更しなくてもいいように、Testbed ManagerはGitHubを連携して、configファイルの設定とその適用を自動化してくれます。

テストの実行者は、まずUI画面から先ほどのようにマシンの確保をします。すると裏側で自動的にCumulus Linuxのルーティング設定ファイルであるFRRのテンプレートファイルが作成されます。同時に、インターフェースの設定ファイルも、ここに書いてあるinterfaces.dのディレクトリ下に作成されます。

configファイルのテンプレートには、VRFデバイスの名前の候補や所属させるインターフェース名が既に記載されているので、VRFの作成ミスを防げます。テストの実行者は、このファイルを必要に応じて編集してmasterブランチにプッシュします。これも全部GitHub上で行えます。準備ができたら、UI画面でSyncボタンを押すと、Testbed Managerは自動的に設定をまとめてL3スイッチに投入します。

以上がTestbed Managerの動作で、これによって物理マシンやルーティングの設定の予期せぬ競合が削減できています。

自動化したベンチマークの事例

次に、自動化という観点からは少し外れてしまうのですが、自動化したベンチマークによって、データプレーンの性能問題の切り分けがスムーズに行えた事例を紹介します。私がチームに配属された昨年(2019年)、プライベートネットワークのVerdaではマルチテナンシー機能の提供が開始される時期でした。

LINEでは、SRv6という最新のルーティング技術を利用して、マルチテナンシーを実現しています。SRv6は、パケットにIPv6ヘッダーとSRヘッダーを付与してルーティングするプロトコルで、Full-L3の既存ネットワークを活かしたマルチテナンシーが実現できています。またデータプレーンには、Linuxカーネルの実装を利用しています。

しかし実際には、SRv6は最新技術ということもあり、何か問題が出た際も、ある程度自分たちで検証や問題解決する必要がありました。

SRv6のマルチテナンシー環境がデプロイされた際に、問題になっていたのは転送性能が低いというものでした。実際に計測してみると、SRv6を使っていないシングルテナント環境の10分の1程度の1Gbpsほどしか性能が出ておらず、ネットワークヘビーなアプリケーションをマルチテナント環境で動作させるのは、難しいような状況でした。

この問題に対して、当時は単純にSRv6のEncap/Decap処理のオーバーヘッドが原因ではないかと考えていました。このような状況で、SRv6高速化の候補の検証を行うことになりました。

その時期に、ちょうどNICベンダーから新たにSRv6のパケットのTCPセグメンテーションオフロード機能、いわゆるTSO機能が追加されたという連絡を受けて、性能向上が期待できそうだったので、このTSO機能の動作検証をチームで行うことになりました。

まずはじめに、この左の図のような構成で、TSOの動作を確認してみました。SRv6は、カプセル化を行うencapノードと、カプセル化を解除するdecapノードの2台利用しています。しかし、当初想定していた設定を適用してもTSOの効果が確認できず、NICのオフロード自体が動作しているのかどうかすらわからないような状況となってしまいました。

そこでまず、テストノードの各インターフェースでさまざまなオフロード設定のパターンを試してみました。実際には、パターンの数膨大で、これを手動でやるのは現実的ではなく、すべてAnsibleを使ってテストを自動化しました。

しかし残念ながら、得られたベンチマーク結果からはどんなパターンを試してもTSOの効果は確認できず、SRv6ノードをブラックボックス的にテストすることに限界を感じました。そこで、もっと細かい設定のベンチマークを行うことにしました。次に行ったのが、マルチテナンシー環境のハイパーバイザー構成を正確に再現した環境で、より詳細なテストをすることでした。

ハイパーバイザー内部の構成を軽く説明します。このHVと書いてある四角の中を見てほしいのですが、物理NICから入ってきたSRv6パケットは、End.DX4アクションが適用されてカプセル化が解除された後、VMの転送用のVFRに送られVMまで転送されます。逆にVMから送信されたパケットは同じ経路を戻り、今度はSRv6でカプセル化されて出ていきます。

このテストでは、受信性能と送信性能のそれぞれを別々に計測するので、どちらがボトルネックになっているかを判断できます。もしも先ほど検討していたTSOがちゃんと有効になっていれば、送信性能は比較的早いはずで、逆に受信性能がボトルネックになっていると想定すると、先ほどのテストでTSOの効果が確認できなかった説明がつきそうです。

ここで、実際の計測結果を掲載します。たくさんパターンがあって少々わかりにくいのですが、左側の緑色の輪で囲んだデータがマルチテナント環境のデータを表しています。そして左上の図から順番にTSOを有効化した場合の受信性能、その下の図がTSOを無効化した場合の受信性能、そして右上の図がVMからの送信性能を示しています。

まず受信性能と送信性能、上の2つの図を比べてみると先ほどの推測が当たっていて、送信性能は20Gbpsあるのに対して、受信性能は4Gbpsしかないことがわかりました。これは実は、TSOはちゃんと動作していて、性能が十分向上していることを示しています。そして下側の受信インターフェースでTSOを無効化した際の性能と上の図を比較してみると、性能は同じで4Gbpsでした。

このことから、どうやら受信側のTSOがうまく働いておらず、どこかでパケットがセグメンテーションされて分解されている可能性があります。

次に、先ほどのテスト環境を使って、どの部分でパケットが分解されているかを調査しました。調査では、tcpdumpを使ってパケットサイズを確認しています。すると、受信時に複数のTCPセグメントが集約されていることは確認できたものの、その後のEnd.DX4アクションが適用されてカプセル化が解除されたパケットが、ルーティングされる際にセグメンテーションされていて、小さいパケットに分解されてしまっていることが確認できました。

本来、TSOが有効に働くことで、この図の一番右側のVMまで、集約された大きなパケットのまま転送されるはずなんですが、SRv6とVFRを組み合わせると、パケットが途中で分解されてしまい、ハイパーバイザー内でパケットごとの転送オーバーヘッドが増加するようでした。受信性能が低いのは、このオーバーヘッドが原因だと考えました。

問題の原因を調査するには、最終的にはカーネル内部を調査する必要があります。カーネルネットワークの調査には、私と同じネットワーク開発チームの早川が開発したipftrace2を利用しました。ipftrace2を利用することで、SRv6パケットがカーネルのどの関数で処理され、その際にどのようなオフロードフラグが付いているかまで確認できます。

調査の結果、SRv6の処理のあとに、SKB_GSO_IPXIP6というフラグが残り続けてしまっていることに気づきました。このフラグは、IPv6でカプセル化されたパケットに対するTSOフラグで、SRv6パケット自体に付与されているには問題がないのですが、カプセル化が解除されたあとのパケットに付与されていることが、この問題を引き起こしていることがわかりました。

実際に、カーネルのソースコードを見てみても、このフラグを削除するような処理は特に入っていませんでした。見つけたバグを修正するのは、意外とスムーズにいきました。修正したのはSRv6のEnd.DX4の処理で、この左側の図のように、SRv6のカプセル化を解除した際にSKB_GSO_IPXIP6フラグを削除する処理を入れただけです。

実際に、この変更を適用してみると、物理NICからVMまでセグメンテーションされずに大きいパケットのまま転送されることが確認できました。このパッチは、カーネルにアップストリームしていて既に取り込まれています。以上の取り組みによって、結果的に性能は改善され、受信側も送信側も10Gbpsを超えられました。

この図の青い囲みの部分が、SRv6を使っていないシングルテナントの環境なのですが、それと比べてもほぼ同等の性能まで改善できました。結論として、たくさんの条件やパラメータを自動化して、相互に比較可能な状態でベンチマークをした結果、問題解決がスムーズにできたと思っています。

自動ベンチマークで通信のボトルネックを解消

最後に、簡単に話をまとめます。まずロードバランサーの開発フローにCIのようなかたちでベンチマークを自動化して、開発者が特別なアクションをしなくても、開発途中での性能低下に気づけるようになりました。また、サーバーの確保やL3スイッチの設定を自動化できるようなサービスを作って、複数人で同時にテストができるような環境を用意しました。

そして最後に、SRv6の性能問題に対して細かい粒度の自動ベンチマークを行い、カーネルのバグを発見しました。その結果、通信のボトルネックを解消できました。

ということで、私からの発表を終わりにしたいと思います。ありがとうございました。