【インターンレポート】KubernetesのOperator Patternを用いた効率的なHypervisorの更新システムの構築

概要

LINEのインフラは50,000台以上の物理マシンによって構築され、インフラエンジニアはそれらマシンの能力を最大限発揮すべく日々開発を行っています。LINEの各種サービスを支えるプライベートクラウドのVerdaも同様に規模を拡大しています。規模の拡大に伴って、規模に応じたオペレーションの問題が出てきました。その問題の一つとして、既存の管理システムによるデプロイは4時間を超えるオペレーションになりました。

この問題を解決するために、今回の技術職 就業型コースのインターンシップで、私は効率的なVerdaのHypervisoの更新システムを構築しました。その設計にはKubernetesのOperator Patternを採用し、比較的複雑になりうる多数の物理マシンに対して並列に行われるオペレーションを、より効率的で堅牢に行えるようにしました。さらに、この設計に基づいた実装を行い、実際に利用されている850台のHypervisorに対するチェックデプロイを通して、その効率を確かめました。

背景

LINEはプライベートクラウドVerdaを開発しています。LINEのサービスはこのプライベートクラウドの上で動作しており、サービスの拡大とともに規模・機能は増大の一途を辿っています。Verdaの実体はいくつかのデータセンターにある大量のHypervisor(物理マシン)であり、その上で動作するVMや仮想ネットワーク、認証をOpenStackを用いて構築しています。そして、これらの機能をLINE内のニーズや事情に特化するために、OpenStackの各コンポーネントへの機能追加や改善、OpenStackと連携するインハウスアプリケーションの開発を推進しています。

このような複雑で巨大なインフラを正しく管理するために、現在はAnsibleを用いて全てのconfigurationを自動化し管理しています。Ansibleのconfigurationは明快で汎用性の高いツールです。しかし、運用上の経験として、デプロイの並列化性能はそれほど高くなく、扱う必要があるホストが増えると線形にデプロイ時間が増えるようになりました。

例えば現在Verdaの一つのOpenStackクラスタには1,300を超えるHypervisorがあります。実験の節にて後述するように、単一ノードで動作するAnsibleの並列化機構ではもはやこの数のデプロイには対処しきれず、現在の運用では、これらのHypervisorを10個のグループに分け、10個のjenkins jobを発行することで並列化による高速化をはかっていますが、それでも2〜3時間程度の時間がかかります。もちろんjenkins slaveを多数用意し、jobを分割していくことで高速化ができると見込めますが、オペレーションも増大し、全体としてあまり良い解決策でないことは確かです。

つまり、自動で適切な数の並列化が可能なデプロイメントシステムを新しく構築することが不可欠だと言えます。

設計


今回の設計では、KubernetesのCRD1を作り、KubernetesのOperator Patternの手法を取り入れることで、より拡張性が高く安定した更新システムを作ることを目指しました。
全体の構成は以下の図のようになっています。

動作の概要について簡単に説明します。Verdaのadminがリソースとして次のような情報をクラスターにデプロイします。

  • Group X1
  • Version: Y1

すると、controllerはこれを検知し、Hypervisor”X1″が指定されたバージョンのplaybookにより管理された状態になるようにKubernetes jobを発行し、対応するHypervisorが更新されます。Kubernetes jobでは、実際にはAnsible playbookとして定義された手続きが実行されます。

次に、例えば状態を以下のように変更したとします。

  • Group X1
  • Version: Y2

このとき、Versionに変更があることが検知できるので、この差を埋めるべく、controllerはKubernetes jobを同様に発行します。このようにバージョンなどリソースに変更があると、その状態に実体が近づくようにcontrollerが動作するという処理、すなわちReconciliation Loopを繰り返すのが、典型的なHypervisor Operatorの動作になります。

ここまでの説明では、単に新しいバージョンに変更を加えるたびに、jobを並列に発行するようなスクリプトを書くだけで済むと感じるかもしれません。しかし、 Operator Patternを採用することは、いくつか利点があります。

Operator Patternの最大の魅力は、Statefulなリソースに対するオペレーションを簡単かつ高精度に実装可能であるということです。監視やトリガー、スケールといった必須の枠組みをKubernetesに任せることにより、我々は「何が起きた時、どういう状態ならば何をするか」というオペレーションの本質的な機能にのみ注力することができます。
この状態を正しく追従しているという事実は非常に大切で、これにより、本来人間が状況判断をしていたオペレーションを容易に自動化することができます。今回のHypervisor Operatorの例で言えば次のようなオペレーションを自動化できるのは魅力的です。

  • バージョンAを多数のHypervisorにデプロイしたが、そのうちいくつかにエラーが出ることが分かったので、進行中のものを除きそのバージョンのデプロイをやめて、全てのHypervisorを元のバージョンでデプロイし直したい
  • バージョンAをデプロイしたが、直ちに変更が必要と分かったので、そのデプロイが終わり次第ただちにバージョンBをデプロイしたい
  • デプロイ状況に応じて必要となるワーキングノードのサイズを変化させたい

こういった機能を、KubernetesのOperator Patternを用いずに実装しようと思うと、各デプロイの状態監視やデプロイが同時に走らないためのロック機構など煩雑な処理を自前で設計し、実装しないといけません。こういった処理から解放されることは、実装コストを下げることができると同時に、しばしば複雑でバグを生みやすい処理をより信頼できる実装に委ねることができ、プログラム全体としての信頼性の向上にも繋がります。

二つ目に、統一的なインターフェースを与えることができます。Hypervisorの状態は、本来デプロイ処理の実装に依存せず定義できるものであり、この意味でAPIの宣言と実装を切り離すことができるのは良いことであると考えられます。これにより、具体的なオペレーションを変えても、外から見えるインターフェース自体は同じになり利用者がデプロイの実装が変わったからといって、使用法の変更を気にする必要がなくなります。また、自作ツールを作ると、不思議なオプションを使用しなければならない場合など往々にしてあります。”オレオレツール”は一般に保守コストや専門的知識が増えることに繋がり好ましくありません。もちろんwrapperとなる保守ツール群を開発するにせよ、その背後にあるインターフェースに、KubernetesのAPIが存在することは、Hypervisor Operatorの利便性を向上させるものだと考えています。

Auto Pilotへの展望

ここまでは、私のインターンシップ内容としての観点です。しかし、Verda開発チームとしてはより広い展望の一部として、Hypervisor Operatorを見ています。
今回私がOperator Patternを用いて自動化したのは、Hypervisorに関連したオペレーションのうち、Hypervisorの更新を行うものだけです。しかし、現在Verdaには、OpenStackで管理されるHypervisorに対して次のようなオペレーションを行なっています。

・Hypervisorのインストール
・Hypervisorのアップグレード
・Hypervisorとして利用するためのOpenStackへの登録・削除
・ログ・Alert管理基盤との連携
・メンテナンス時のユーザへの通知・VM退避

こういったオペレーションの自動化を図ることで、Hypervisorに関連した通常のオペレーションを完全に無くすことが一つの目標となります。

SREという言葉を生み出したGoogleのエンジニアの一人であるCarla Geisserは、「If a human operator needs to touch your system during normal operations, you have a bug. (通常運用中のシステムに人手が必要なら、それはバグだ)」と言っています3。Hypervisor Operatorの一つの終着点は、Operator SDKの言葉を借りれば、Auto Pilot、自動操縦、です。この段階4まで成熟したOperatorは人間の手を借りずとも、リソースの生成・更新・削除、状態監視、異常検知、そして状況に応じたスケーリングといった「通常オペレーション」を自動で行うことが可能になります。
こういった処理を行う際、人間のオペレーターは必ずしも”人間的”判断を下しているわけではなく、何かしらの指標・マニュアルに従って機械的にシステムに操作を加えていることがほとんどだと思います。すなわち、こういったことは上手に自動化するのがしばしば難しい場合があるだけで、本来ならばコンピュータが行なっていてもおかしくないわけです。
そして、それらの自動化を達成するのに、良い枠組みを与えるのがKubernetesであり、多くの処理を自動化するコストは、その後の運用コストの低下により必ずや償却することができることでしょう。

実装

実装にはkubebuilderにより生成される足掛かりをベースに行いました。Kubernetesのrbacの設定やDeploymentの設定、Kubernetesのcontroller runtimeに関連した処理は、ボイラープレートが多いので基本的に省略可能で、必要に応じた変更を加えれば十分です。

そして、必要なCRDをGolangの構造体として次のように定義すると、

type HypervisorSpec struct {
    Group string `json:"group,omitempty"`
    Version string `json:"version,omitempty"`
    Playbooks []string `json:"playbooks,omitempty"`
}
type HypervisorStatus struct {
    Version string `json:"version,omitempty"`
    Playbooks []string `json:"playbooks,omitempty"`
    Status DeployStatus `json:"status,omitempty"`
}

kubebuilderはそれに応じて、自動でKubernetesのCRDのmanifestを以下のように生成してくれます。

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.2.5
  creationTimestamp: null
  name: hypervisors.hypervisor.hypervisor-operator.linecorp.com
spec:
  group: hypervisor.hypervisor-operator.linecorp.com
  names:
    kind: Hypervisor
    listKind: HypervisorList
    plural: hypervisors
    singular: hypervisor

CRDに対するcontrollerの実装については、Kubernetesのcontroller-runtimeというライブラリを利用して行いました。controller-runtimeは、KubernetesのReconciliation Loopを実現するために必要な部分の機能、すなわち、controllerのreconciliation処理を呼び出すタイミングの制御を担います。これは、どのリソースを監視して、どういうタイミングでreconciliationを行うかを管理してくれる、ということです。

Kubernetesの仕組み自体が強力なものであることは前述した通りですが、実装をする上でも、本質的な処理の実装にのみ注力することができたので、良い選択だったと感じています。実際、Hypervisor Operatorの実装をすることができる時間はインターン期間中それほど多くなかったですが、一定の完成を迎えることができ、実際に実験等を行うことができたのは、こういったライブラリ群のおかげだったと言えます。

実験

まずは、13台のHypervisorに対して予備実験を行いました。この程度の数であれば、全てのデプロイを並列に動作させることが可能で、一つのAnsible taskは3~5分程度で終了するので、全体で高々5分程度で全てのHypervisorを更新することができました。さらに予備実験として、115台のHypervisorに対してもデプロイを行い、並列性を51に設定2 して実行すると、15分以内に実行が終わりました。

そして、本実験として、850台のHypervisorに対するテストデプロイを行いました。今回知りたいのは、結局のところ「現実のオペレーション」を考えたとき、Hypervisor Operatorを用いれば、今まで行っていたオペレーションに比べて、どれくらい効率化を行えそうか、という期待度合いです。そこで、性能的に厳密な比較を行うというよりは、現実のオペレーションをシミュレーションした形での比較実験を行いました。

既存のデプロイでは、Hypervisorを10個にわけた上でそれぞれに対してjenkins jobを発行し、ノードレベルでの並列化を行っています。これを、「既存の手法」と考えデプロイを行ったところ、76分かかりました。一方Hypervisor Operatorを用いて、並列性を200に設定してデプロイを行うと25分かかりました。

設定やワーカーの大きさが違うので、機能そのものとしての比較は難しいですが、今まで行っていた手法に比べて、適切な設定さえすれば3倍程度速くデプロイすることができる、というのは大きな進歩です。この比較は、現実的に行われるだろうオペレーションのシミュレーションとしての比較であり、設定には実際に運用する際に利用するだろう値を使いました。したがって、この結果通りの高速化が実際に起こることが期待されます。

以上の結果は、jenkins jobの分割による並列化とAnsibleによる並列化の二つが混ざったものでした。そこで、Ansibleのみの並列化性能を取り出す意味で、デプロイするHypervisorのクラスターを10個に分けて、それぞれに対するjobを直列に動作させてかかる時間を計測しました。Ansibleは、設定により85個のforkを作り並列にそれぞれのplaybookを実行するようにしておきます。つまり、この実験で、Ansible自体の並列化性能を確認することができます。
結果としては、10個のjobの平均実行時間は34.4分でした。1個あたりのjobの実行時間は3~5分程度であることから、完全に並列に動作していれば高々5分程度で動作が終わることが期待されますが、実際にはそうはなりません。単一ノードでのAnsible実行はインフラ規模の増大とともにコストが増大していくことが現実として分かると思います。Ansible自体には多数のノードに分散して実行する機能が備え付けられているわけではないので、この事実がそのような仕組みづくりの必要性を裏付けるものです5

まとめ

既存のjenkins手法でも、slaveの数を増やしていけば、インフラ規模の増大に対処できなくはないものの、オペレーションのコストを考えるとそれは現実的ではありません。この問題に対処するために、私は今回のインターンシップにおいて、KubernetesのOperator PatternをベースにHypervisorの更新システムを設計・実装しました。

また、実際のデプロイの性能としても、その設計のおかげで、容易にスケールし、インフラの規模に応じてワーカーを適切に投入さえすれば、追加のオペレーションなしで、十分に高速なデプロイが可能であることは、実験からも確かめられました。

Operator Patternに従えば、特にstatefulなオペレーションに関する知識を自動化することが容易となります。従って、Hypervisor OperatorはHypervisorの更新管理のみに留まらず、LINEのインフラオペレーションの効率的で信頼性の高い自動化の足掛かりになることが期待され、別のオペレーションにも応用・拡張されていくことが今後の展望となります。

LINE インターンについて

今回は、Covid-19の感染拡大の影響で、ほぼ完全にリモート(一度だけ出社)でのインターンになりましたが、お忙しい中でもメンターである室井さん、谷野さんとほぼ毎日ZoomやSlackでのコミュニケーションをしていただき、技術的な点はもちろん、会社でエンジニアをするということ全体について勉強をさせて頂きました。またdaily meetingやアイデア共有を共にしてくださったVerdaチームのメンバーの方々や、最後実際に運用されている850台のHypervisorに対する実験をする際には、萬治さんマンスールさんに大変お世話になりました。直接会う機会はありませんでしたが、ここにお礼を書き残します。 難しい情勢でしたが、インターン中お世話になった方々、インターン受け入れに尽力してくださった方々には大変感謝しています。ありがとうございました。

注釈

  1. Custom Resource Definition https://kubernetes.io/ja/docs/concepts/extend-kubernetes/api-extension/custom-resources/
  2. 並列性は、同時に実行して良いJobの数を表します
  3. Besty Beyerら著”Site Reliability Engineering” https://landing.google.com/sre/sre-book/chapters/eliminating-toil/より引用。日本語訳は、澤田武男監訳の「SRE サイトリライアビリティエンジニアリング」(オライリージャパン)  。正確には、”If a human operator needs to touch your system during normal operations, you have a bug. The definition of normal changes as your systems grow.”まで引用した方がいいかもしれません。
  4. Operatorによる自動化を推進するRedHatらによる、OperatorSDKにおいて掲げられたKubernetes Operatorの成熟度の基準 “Operator Capacity Levels”(https://sdk.operatorframework.io/docs/advanced-topics/operator-capabilities/operator-capabilities/)の最終段階。Operatorの成熟度は、およそ以下の5段階に分類されるといいます。
    1. Basic Install: リソースが簡単にデプロイできる状態
    2. Seamless Upgrades: リソースの更新が容易に行える状態
    3. Full Lifecycle: バックアップや障害回復など、アプリケーションの機能を補完する処理も含めたオペレーションを自動で行える状態
    4. Deep Insights: ログやメトリクス等による状態監視・異常検知ができる状態
    5. Auto Pilot: アプリケーションの水平・垂直な自動スケーリングのような、アプリケーションの状態を完全に監視しメトリクスに基づいたあらゆる判断と必要な措置が自動化された状態

 既存のオープンなOperatorをまとめたレジストリである、OperatorHub.ioには、この基準に基づいて、登録されているOperatorがどれくらい成熟しているかの指標が併記されています。なお、Hypervisor Operatorは更新が自動化されている段階であり上の基準に当てはめるのは難しいです。

 5. 実際のところ、Ansibleが並列にデプロイを管理する実装自体にも問題があるのではないかと考えていますが、これについては、今回は有効な形では確かめられていません。

Related Post