Riotのオンラインサービス運用:第2部

Riotのインフラチームに所属しているKyle AllanとCarl Quinnです。本稿は連載しているブログの第2部です。この連載では、私たち、Riotが世界中でどのようにバックエンド機能をデプロイし運用しているのかを詳しく説明しています。なお、本稿では、デプロイメントのエコシステムの初めの主要コンポーネントとなる、コンテナのスケジューリングについて論じていきます。

Jonathanの執筆した連載第1部では、Riotがデプロイメントに取り組んできた歴史と、私たちが直面した困難について説明しました。具体的に言うと、ソフトウェアをデプロイする際に私たちが経験したさまざまな問題がどのように大きくなっていったかについて述べました。主な原因は、League of Legendsをサポートするためにインフラをどんどん追加したため、 “アプリケーションごとに手作業のサーバプロビジョニング” が必要になったことでした。Dockerと呼ばれるツールが、私たちのサーバデプロイメントのやり方を変えました(他のものもありますが、中でもDockerが一番影響を与えました)。その結果、Admiralが誕生しました。これは私たちが開発したソフトウェアで、クラスタのスケジューリングと管理を行う内部ツールです(ちょうど昨日知ったことなのですが、VMwareに同じ名前の似たようなソフトウェアがありました。賢人は皆同じように考えるものなのですね)。

ここで注意しておきたいのは、アプリケーションのデブロイメントの開発には、まだまだ終わりがないということです。常に進化し続けることなので、私たちも次の段階の準備をしなければなりません(後述しますが、DC/OSの導入などの可能性もあります)。本稿では、どのような経緯で現時点に至ったか、また、なぜ私たちが現在のような選択をしているかをお話しします。この話から、みなさんにも何か学ぶことがあれば幸いです。

スケジューリングとは何か、なぜスケジューリングか

Dockerがリリースされ、Linuxのコンテナ化の技術がより広く知られるようになると、私たちは、インフラのコンテナ実装を行うことで、利益が得られると考えました。Dockerコンテナのイメージは、一度で作成でき、開発、テスト、そして本番環境にデプロイされる、不変的でデプロイ可能な成果物を提供します。さらに、本番環境に実行されるイメージの依存性は、テスト中の状態がそのまま保証されます。

本稿の趣旨においては、もう1つのメリットがとりわけ重要です。Dockerは、スケジューラを利用してコンテナをホストに(うまくいけば賢い方法で)割り当てることで、デプロイメントのユニット(コンテナ)と、コンピュートのユニット(ホスト)の分離を可能にします。これで、サーバとアプリケーションの結合が解消されます。つまり、既定のコンテナは、何台のサーバ上でも実行可能なのです。

Dockerのイメージにパッケージされ、いつでも、何台のサーバ上でもデプロイ可能な私たちのバックエンドのサービスは、変化にすぐに対応していく必要があります。例えば、新しいプレイヤ機能を追加したり、トラフィックを重くする機能をスケールアウトさせたり、更新や修正を素早く公開することです。コンテナ内のサービスを本番環境へデプロイすることを考える際に、解決しなければならない以下の3つの大きな課題があります。この記事ではそれらを取り上げていきます。

  1. ホストのクラスタが与えられると、一連のコンテナを受け取るための特定のホストのセットはどのように選ばれるのか。
  2. この一連のコンテナは、実際にリモートホストでどのように起動されるのか。
  3. コンテナが停止した場合、何が起こるのか。

これら3つの問いに対する答えは、スケジューラ、つまりクラスタ層で動作し、コンテナストラテジを実行するようなサービスが必要だということです。スケジューラはクラスタを維持する際のキーコンポーネントで、コンテナが正しい場所で動作することを保証し、コンテナが停止した時には再起動します。例えば、ロードを管理するために6つのコンテナインスタンスを必要とするHextech Craftingのようなサービスを起動したいと思うかもしれません。スケジューラはこれらのコンテナをサポートするために十分なメモリとCPUリソースを有するホストを見つけ、コンテナを実行し続けるのに必要な動作を全て実行する役割を担います。これらのサーバのうちの1台が停止すると、スケジューラは影響を受けたコンテナのために代わりのホストを見つける担当でもあります。

スケジューラを利用しようと決めた時には、素早くプロトタイプする能力を必要としていました。これはコンテナ化されたサービスが本番でうまく動くかどうかを知るためです。さらに、私たちの環境で既存のオープンソースのオプションが動作することや、メンテナーが改造を受け入れてくれることを確保する必要がありました。

なぜ独自のものを作るのか

Admiralとして誕生したスケジューラを作成する前に、私たちは既存のクラスタマネージャとスケジューラの状況を調べました。Dockerホストのクラスタにまたがるコンテナのスケジューリングをしていたのは誰なのでしょうか。またどのようにそれを実行していたのでしょう。その技術が私たちの問題も解決してくれるのでしょうか。

最初の調査で、いくつかのプロジェクトに注目しました。

Mesos + Marathon

  • この2つはかなり完成度の高い、大規模向けの技術だが、インストールするには複雑で扱いにくいものだった。このことが、この技術の試行や評価を難しくしていた。
  • その時点では、とても限られたコンテナサポートしかなく、Dockerのめまぐるしい進化についていけず、Dockerのエコシステムでは適切に機能しなかった。
  • 私たちのサービスの多くで、サイドカーコンテナにバンドリングするために必要と思われるコンテナグループ(pod)をサポートしていなかった。

LMCTFY => Kubernetes

  • KubernetesはLMCTFYから進化したものだ。見込みがあるように思われたが、将来の進化が、私たちの要求するものと一致するかどうかが明確ではなかった。
  • Kubernetesは私たちが求めるようなコンテナの配置を実行する制約システムをまだ持っていなかった。

Fleet

  • Fleetは最近、オープンソース化され、今ほど完成度は高くなかった。
  • Fleetは汎用アプリケーションサービスとは対照的に、システムサービスのデプロイに対してより専門化されているようだった。

私たちはRESTを介してDocker APIと通信するような小規模のコマンドラインツールのプロトタイプを作成しました。そして、デプロイメントをうまく組み合わせるために、このツールがどのように使えるのかを実証することに成功しました。その後、独自のスケジューラを作る方針を決めたのです。調査したシステムの優れた部分のいくつかを取り入れました。この中にはKubernetesのpodとMarathonの制約システムの背景にある核となる概念も含みます。私たちのビジョンは、これらシステムの構造と機能性を手掛かりとし、可能であれば影響を与え、将来、この中の1つと最終的には収束しようと思っています。

Admiralの概要

私たちは、基本デプロイのためのJSONベースのメタデータ言語CUDLを作成したあとで、Admiralを書き始めました。CUDLとは「ClUster Description Language」(クラスタ記述言語)の意味です。これはAdmiralがRESTful APIで使用する言語になりました。CUDLの主なコンポーネントは以下の2つです。

  • クラスタ – Dockerホストのセット。
  • パック – 1つ以上のコンテナから成るセットを起動するのに必要なメタデータ。Kubernetesのpod + レプリケーションコントローラに似ている。

クラスタとパックは、specとliveという2つの異なるアスペクトを持ちます。それぞれのアスペクトは、コンテナライフサイクルの異なる段階について表現しています。

specは、期待される状態を表します。

  • ソースコントロールのような信頼できる外部ソースからAdmiralに送信される
  • Admiralに到達したあとは変更不能
  • specのクラスタとホストは、クラスタ内で利用可能なリソースを記述する
  • specのパックは、サービスを実行するのに必要なリソース、制約、メタデータを記述する

liveは、実現された状態を表します。

  • 実際に稼働中のオブジェクトをミラーリングする
    • liveのクラスタとホストは、稼働中のDockerデーモンをミラーリングする
    • liveのパックは、稼働中のDockerコンテナグループをミラーリングする
  • Dockerデーモンと通信することで修復可能

AdmiralはGoで書かれており、本番環境のデータセンタで実行される際に、コンパイルされDockerコンテナにパッケージ化されます。Admiralには内部にサブシステムがいくつかあり、その大部分は以下の図に示されています。


ユーザ側から見ると、Admiralとのインタラクションはコマンドラインツールadmiralctlを使って行われます。このツールは、REST APIを介してAdmiralと通信します。ユーザはadmiralctlを用いてAdmiralの全ての機能にアクセスすることができます。その際に使う標準的なコマンドは、スケジュールすべき新しいspecのパックを送信するPOST、古いパックを削除するDELETE、現在の状態を入手するGETです。

本番環境では、AdmiralはHashiCorpのConsulを使ってspecの状態を格納します。重大な障害に備えて、Consulのバックアップは定期的に取っています。万一データが完全に失われた場合には、Admiralは個々のDockerデーモンから取得したliveの状態の情報を使って、specの状態を部分的に再構築することも可能です。

リコンサイラは、Admiralの中心に位置づけられ、スケジューリングのワークフローを進める際の鍵となるサブシステムです。リコンサイラは周期的に実際のliveの状態と期待されるspecの状態を比較して、相違があった場合には、liveの状態をspecの状態に再び一致させるために必要な動作をスケジュールします。

liveの状態とそのドライバパッケージは、liveのホストとコンテナの状態をキャッシュすることでリコンサイラをサポートし、REST APIを介してクラスタホストにおける全てのDockerデーモンとの通信を提供します。

スケジューリングの詳細

Admiralのリコンサイラは、specのパックに対して操作を行い、実質的にliveのパックに変換します。specのパックがAdmiralに提出されると、リコンサイラが動作し、Dockerデーモンを使ってコンテナを生成、起動します。このメカニズムを通じて、リコンサイラは、前述した大きなスケジューリング目的のうち初めの2つを達成するのです。リコンサイラがspecのパックを受け取ると、以下のように動作します。

  1. クラスタのリソースとパックの制約を評価して、コンテナに適したホストを見つける。
  2. specから得たデータを使って、リモートホスト上のコンテナを起動する方法を把握する。

Dockerホストでコンテナを起動する例をざっと見てみましょう。この例では、私のローカルのDockerデーモンをDockerホストとして使い、Admiralサーバのローカルインスタンスとやり取りしてみます。

まず、admiral pack create <cluster name> <pack file>コマンドを使って、パックを起動します。このコマンドは特定のクラスタをターゲットとして、specのパックのJSONをAdmiralサーバに提出します。


お気付きでしょうが、コマンドを実行するとほぼ即時に、コンテナが私のマシン上で起動されています。このコンテナは、以下に示されているパックファイルのパラメータを使って起動されました。

{
     "name": "dat.blog_scout",
     "description": "A pack of the Scout service for usage in our engineering blog post.",
     "service": {
          "location": "dev.local.test",
          "discovery": {},
          "appname": "dat.scout"
     },
     "containers": [
          {
               "image": "datd/scout",
               "version": "1.0.0",
               "ports": [{
                    "internal": 8080,
                    "external": 8080
               }]
          }
     ],
     "count": 1
}

次に、admiral pack createを呼び出したあと、showコマンドを使って、Admiralによって生成されたliveのパックを見ることができます。ここでは、コマンドはadmiral pack show <cluster name> <pack name>となります。


最後に、コンテナ内のサービスに接続すれば、パックが機能していることを確認できます。admiral pack showコマンドで得た情報を使い、簡単なcurlコマンドを組み立ててサービスに接続することができます。


Admiral内では、リコンサイラが常に稼働しており、クラスタのliveの状態が期待されるspecの状態に常に一致するようにしてくれます。これによって、クラッシュが原因でコンテナが動作しなくなって終了したり、ハードウェア障害が原因でサーバ全体が使用不能になったりした時に、復旧することが可能となります。リコンサイラは状態が確実に一致するように動作するので、プレイヤのプレイが中断されることは決してありません。この機能によって、前述した最後の3番目の問題が解決されます。つまり、コンテナが予期せず終了した時に素早く復旧できるので、その影響が最小限に抑えられるのです。

以下の画面では、admiral pack createコマンドで起動された既存のコンテナを示しています。次に、そのコンテナの実行を強制終了しています。すると、liveの状態がspecの状態と一致しないことをリコンサイラが認識するので、数秒以内に(別のIDを持った)新しいコンテナがリコンサイラによって起動されています。

リソースと制約

コンテナを最適に割り当てるため、スケジューラはホストのクラスタを認識する必要があります。この問題の解決に際しては、鍵となる2つのコンポーネントがあります。

リソース – サーバが使えるリソースの表現。メモリ、CPU、I/O、ネットワーキングなど。

制約 – パックに伴う条件のセット。パックを配置できる場所に関して、制限の詳細をスケジューラに伝える。例えば、パックのインスタンスを以下の場所に配置した方がよいかもしれない。

  • クラスタ全体の各ホスト上
  • 「myhost.riotgames.com」という名前の特定ホスト上
  • クラスタ内でラベルの付いた各ゾーン内

ホスト上のリソースを定義することで、スケジューラがコンテナを配置する場所を柔軟に決められるようにしています。また、パックの制約を定義することで、クラスタに対する特定パターンを強制できるようにスケジューラの選択肢を制限しています。

まとめ

Riotにとって、Admiralはデプロイ技術を進化させ続けるために不可欠なソフトウェアです。Dockerとスケジューリングシステムのパワーを活用することで、バックエンド機能を以前よりずっと素早くプレイヤに提供できるようになりました。

本稿では、Admiralの機能の一部を詳しく見て、マシンのクラスタ全体にわたってコンテナをスケジュールしている方法をご紹介しました。Jonathanが第1部の記事で言及しているように、オープンソースの世界は、これによく似たモデルへと急速に移行しました。弊社は今後、Admiralで行っている作業を移行し、DC/OSのデプロイに重点を置いていく予定です。DC/OSは、コンテナのワークロードをスケジュールするための主要なオープンソースアプリケーションとなっています。

似たような開発の経験がある方や、この話題に補足したい点のある方は、ぜひ本稿のコメント欄にご記入ください。

  • 第1部:はじめに
  • 第2部:スケジューリング(本稿)
  • 第3部:Dockerとのネットワーキング(次回)
  • 第4部:動的なアプリケーションの実行