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


Riotのインフラチームに所属しているJonathan McCaffreyです。これから連載でRiotが世界中でどのようにバックエンド機能をデプロイし運用しているのかを詳しく説明していきますが、本稿はその第1回です。技術的な詳細に入る前に、Rioters(Riotで働いている人たち)が機能開発に対してどう考えているか、を理解することが重要となります。Riotで最優先されるのがプレイヤバリューで、開発チームはプレイヤコミュニティと密に連携し機能や改善に関する情報を提供しています。可能な限り最高のプレイヤエクスペリエンスを提供するためには俊敏に行動しながらもフィードバックを基に迅速に計画を変更できる能力を保持する必要があります。インフラチームの使命は、開発者がこれを実現するべく道筋を整備することです。Riotのチームを強化することで、少しでも早く機能を届けることができ、プレイヤに楽しんでもらうことができます。

もちろん、言うは易く行うは難しです。デプロイメントが多岐に渡るため、多くの課題が持ち上がります。弊社のサーバはパブリッククラウドやプライベートデータセンタ、TencentやGarenaのような提携会社の環境など、地理的にも技術的にも多様な場所に置かれています。この複雑さは出荷の準備ができた時に、機能を担当するチームに負担をかけます。このような場面で活躍するのがインフラチームです。最近になって、Riotが独自に開発した “rCluster” というコンテナ型のインターナルクラウド環境で、デプロイする際の障害を取り除くことができました。この記事では、Riotの手動でのデプロイメントから現在のrClusterを使用した機能の起動に至るまでの経緯をお話しします。rClusterが提供するものや技術の実例として、Hextech Craftingシステムの立ち上げを見ていきます。

簡単な歴史

Riotで働き始めた7年前にはデプロイメントやサーバ管理プロセスはほとんどありませんでした。限られた予算の中、迅速な対応が求められる、壮大なアイデアを持ったスタートアップ会社でした。League of Legendsの開発基盤を構築していく上で、ゲームの需要や開発者が要求するさらなる機能のサポート、地域チームからの、世界各国での新規立ち上げの要求などにつまずきながらも対応しました。新手順や戦略的計画などはお構いなしに、手作業でサーバやアプリケーションを立ち上げました。

ここに至るまで、Chefを多くの一般的な展開やインフラ作業に活用する方向で進めてきました。さらに、ビッグデータやWebへの取り組みのためにパブリッククラウドを利用する機会を増やしていきました。このような進展がきっかけとなり、ネットワーク設計やベンダ選び、チーム構成を何度も変えることになりました。

弊社のデータセンタは数千台ものサーバを収容しており、新しいアプリケーションごとに新たなサーバを追加しています。新しいサーバには、ネットワーク間の安全な接続を実現するために、手作りのルーティングとファイヤウォールのルールを用いた、それぞれ個別のVLANが設定されています。これによりセキュリティが確保でき、困難で時間のかかっていた障害ドメインを明確に定義できるようになりました。この設計の苦労を和らげるため、当時の新しい機能は小規模のWebサービスとして設計されていました。これにより弊社の製作したLeague of Legendのエコシステム特有のアプリケーションの数が急増しました。

その上、開発チームには、アプリケーションを検証する能力に対する自信が欠如していました。特にデプロイの時点で判明する設定やネットワークの接続性の問題に関して自信がありませんでした。アプリを物理的なインフラに密接に結びつけることは、生産環境のデータセンタの間で生じた相違をQA(品質保証)やステージング環境、PBE(Riotのテストサーバ)で再現することができないことを意味しました。それぞれの環境は手作りで独自に作成されているため、一貫性がありません。

アプリケーションの増え続けるエコシステムでネットワークのプロビジョニングやサーバを手作業で増設する課題と格闘している中、設定の整合性の問題や開発環境にまつわる苦悩を解決してくれる手段としてDockerが少しずつ開発チームに支持されるようになっていきました。Dockerを使い始めてみると使い道が広がること、インフラの取り組み方の重要な役割を果たすことに気が付きました。

2016シーズン以降

インフラチームは、2016シーズンに向けてプレイヤ、開発者、Riotのため、これらの問題を解決することを目標として定めました。2015年後半までには、Riot全体でHextech Craftingのような機能を、手動でデプロイするのではなく、自動化して一貫したやり方でデプロイするようにしました。これを解決したのがrClusterという、マイクロサービスアーキテクチャでDockerとSoftware Defined Networkingを活用した最新のシステムです。rClusterへの切り替えにより、環境とデプロイのプロセスにおける不整合を正し、プロダクトチームがしっかりと製品に集中することが出来るようになります。

rClusterが裏でどのようにHextech Craftingのような機能をサポートするのかを調べるために、少し技術的な内容に踏み込んでみましょう。コンテキストにとって、Hextech Craftingは、ゲーム内のコンテンツのカギを開ける新しい方法をプレイヤに提供するLeague of Legends内の機能です。

その機能はゲーム内で「戦利品(Loot)」として知られていて、以下のような3つの核となるコンポーネントで構成されています。

  • Loot Service-HTTP/JSON REST API上でLootリクエストを発行するJavaアプリケーション。
  • Loot Cache-モニタリングや設定、起動・停止のオペレーションのためにMemcachedと小規模のgolangや、sidecarを使ったキャッシングクラスタ。
  • Loot DB-マスタと複数のスレーブを有するMySQL DBのクラスタ。

クラフト画面を開くと、以下のようになります。


1. プレイヤはクライアントのクラフト画面を開く。
2. クライアントはリモートプロシージャコール(RPC)を使ってフロントエンドアプリケーション、別名 “feapp” を呼び出します。このアプリケーションのプロキシはプレイヤと内部のバックエンドサービスとの間での呼び出しを行います。
3. feappは、Loot Serverに呼び出しを行います。
1. feappは、そのIPやポートの情報を探すために、 “Service Discovery” でLoot Serviceを調べます。
2. feappはHTTPのGETの呼び出しをLoot Serviceに対して送信します。
3. Loot Serviceはそのプレイヤのインベントリが存在するかどうかLoot Cacheを調べます。
4. キャッシュにはインベントリが無いので、Loot ServiceはLoot DBを呼び出し、プレイヤが現在所持しているものを確認し、その結果をキャッシュに取り込みます。
5. Loot ServiceはGETの呼び出しに応えます。
4. feappはRPCの返答をクライアントに返します。

Lootチームと一緒に働くことで、Server層とCache層をDockerコンテナにビルドすることが出来ました。そしてデプロイの設定は以下の様にJSONファイルで定義されました。

Loot ServerのJSONの例:

{
    "name": "euw1.loot.lootserver",
    "service": {
        "appname": "loot.lootserver",
        "location": "lolriot.ams1.euw1_loot"
    },
    "containers": [
        {
            "image": "compet/lootserver",
            "version": "0.1.10-20160511-1746",
            "ports": []
        }
    ],
    "env": [
        "LOOT_SERVER_OPTIONS=-Dloot.regions=EUW1",
        "LOG_FORWARDING=true"
    ],
    "count": 12,
    "cpu": 4,
    "memory": 6144
}

Loot CacheのJSONの例:

{
    "name": "euw1.loot.memcached",
    "service": {
        "appname": "loot.memcached",
        "location": "lolriot.ams1.euw1_loot"
    },
    "containers": [
        {
            "name": "loot.memcached_sidecar",
            "image": "rcluster/memcached-sidecar",
            "version": "0.0.65",
            "ports": [],
            "env": [
                "LOG_FORWARDING=true",
                "RC_GROUP=loot",
                "RC_APP=memcached"
            ]
        },
        {
            "name": "loot.memcached",
            "image": "rcluster/memcached",
            "version": "0.0.65",
            "ports": [],
            "env": ["LOG_FORWARDING=true"]
        }
    ],
    "count": 12,
    "cpu": 1,
    "memory": 2048
}

しかし、この機能を実際にデプロイするために、そして、これまでに概要を述べた苦労を本当に軽減するために、私たちは北米や南米、欧州、アジアといった世界各国で、クラスタを作成し、Dockerをサポートできるようにしなければなりませんでした。これには、以下のような多くの困難な問題を解決する必要がありました。

  • コンテナのスケジューリング
  • Dockerとのネットワーキング
  • 継続的デリバリ
  • 動的アプリケーションの実行

この後の投稿で、rClusterシステムのこれらの構成要素についてさらに詳細に踏み込むつもりなので、ここでは軽く触れるだけにします。

スケジューリング

Admiralと呼ばれる私たちが書いたソフトウェアを使用し、rClusterエコシステムでコンテナのスケジューリングを実装しました。Admiralは、一連の物理マシン上のDockerのデーモンにデータを送り、現在の状態を把握します。ユーザはHTTPSに対して上述のJSONを送ってリクエストを行います。Admiralはそのリクエストを使い、関連したコンテナの期待される状態を把握し、内容を更新します。そして、どのような処理が必要なのかを知るために、絶えずクラスタの現在の状態と期待される状態を注視します。最終的に、Admiralは、期待される状態に近づけるようにコンテナの起動や停止を行うようにDockerデーモンに対して追加の呼び出しを行います。

もしコンテナがクラッシュしたら、Admiralは現在の状態と期待される状態との相違を確認します。そして、それを修正するために別のホスト上でコンテナを開始します。この柔軟性のおかげでサーバの管理が非常に簡単になります。というのも途切れることなく処理を”流し”、メンテナンスをし、ワークロードを取り込んで再び有効にすることができるからです。

AdmiralはオープンソースのツールMarathonとよく似ています。そこで、現在私たちは、MesosとMarathon、DC/OSを活用しようと、私たちの処理内容を移植するための調査を行っています。それが功を奏せば、今後の投稿でお話ししようと思います。

Dockerとのネットワーキング

コンテナが実行されると、私たちはLootアプリケーションやエコシステムの他の部分との間のネットワーク接続を提供する必要があります。その手段として、私たちは、各アプリケーションにプライベートネットワークを付与するため、また、開発チームがGitHubにあるJSONファイルを使ってポリシー自体を管理できるようにするためにOpenContrailを活用しました。

Loot Server Network:

{
    "inbound": [
        {
            "source": "loot.loadbalancer:lolriot.ams1.euw1_loot",
            "ports": [
                "main"
            ]
        },
        {
            "source": "riot.offices:globalriot.earth.alloffices",
            "ports": [
                "main",
                "jmx",
                "jmx_rmi",
                "bproxy"
            ]
        },
        {
            "source": "hmp.metricsd:globalriot.ams1.ams1",
            "ports": [
                "main",
                "logasaurous"
            ]
        },
        {
            "source": "platform.gsm:lolriot.ams1.euw1",
            "ports": [
                "main"
            ]
        },
        {
            "source": "platform.feapp:lolriot.ams1.euw1",
            "ports": [
                "main"
            ]
        },
        {
            "source": "platform.beapp:lolriot.ams1.euw1",
            "ports": [
                "main"
            ]
        },
        {
            "source": "store.purchase:lolriot.ams1.euw1",
            "ports": [
                "main"
            ]
        },
        {
            "source": "pss.psstool:lolriot.ams1.euw1",
            "ports": [
                "main"
            ]
        },
        {
            "source": "championmastery.server:lolriot.ams1.euw1",
            "ports": [
                "main"
            ]
        },
        {
            "source": "rama.server:lolriot.ams1.euw1",
            "ports": [
                "main"
            ]
        }
    ],
    "ports": {
        "bproxy": [
            "1301"
        ],
        "jmx": [
            "23050"
        ],
        "jmx_rmi": [
            "23051"
        ],
        "logasaurous": [
            "1065"
        ],
        "main": [
            "22050"
        ]
    }
}

Loot Cache Network:

{
    "inbound": [
        {
            "source": "loot.lootserver:lolriot.ams1.euw1_loot",
            "ports": [
                "memcached"
            ]
        },
        {
            "source": "riot.offices:globalriot.earth.alloffices",
            "ports": [
                "sidecar",
                "memcached",
                "bproxy"
            ]
        },
        {
            "source": "hmp.metricsd:globalriot.ams1.ams1",
            "ports": [
                "sidecar"
            ]
        },
        {
            "source": "riot.las1build:globalriot.las1.buildfarm",
            "ports": [
                "sidecar"
            ]
        }
    ],
    "ports": {
        "sidecar": 8080,
        "memcached": 11211,
        "bproxy": 1301
    }
}

エンジニアがGitHubの中でこの設定を変更すると、アプリケーションのプライベートネットワーク用にポリシーを作成及び更新するために変換ジョブが実行され、Contrailの中でAPIが呼び出されます。

Contrailは、オーバレイ・ネットワークと呼ばれる技術を使って、このようなプライベートネットワークを実現しています。私たちの場合、Contrailは、計算ホストの間でGREトンネルを使用し、ゲートウェイルータは、オーバレイトンネルを出入りしたり、その他のネットワークに向かったりするトラフィックを管理します。OpenContrailシステムは、標準MPLS L3VPNにインスパイアされたもので、概念的に非常によく似ています。詳しいアーキテクチャをさらに知りたい方はこちらを参照してください。


私たちがこのシステムを実装した際、次に挙げるいくつかの重要な課題に取り組む必要がありました。

  • ContrailとDockerの間の統合
  • その他のネットワーク(rClusterの外部)がシームレスに私たちの新しいオーバレイ・ネットワークにアクセスできるようにすること
  • 1つのクラスタのアプリケーションが別のクラスタと通信できるようにすること
  • AWS上でオーバレイ・ネットワークを実行すること
  • オーバレイの中で高可用性に向いたアプリケーションをビルドすること

継続的デリバリ

Max Stewartが以前、Riotが継続的デリバリにDockerを利用している件について記事を投稿していますが、rClusterも活用しています。

Lootアプリケーションでは、継続的インテグレーションのフローは次のような感じになります。


ここでの一般的な目標は、マスタレポジトリが変更された場合、新しいアプリケーションコンテナが作成され、QA環境にデプロイされることです。いつでも使えるこのワークフローを用いれば、チームはすぐに自分たちのコードにも同じ変更を繰り返し、稼働中のゲームに反映された変更を確認することができます。その厳格なフィードバックループによって、エクスペリエンスを迅速に改良することができます。それが、Riotにおけるプレイヤを重視したエンジニアリングの重要な目標です。

動的なアプリケーションの実行

ここまでは、私たちがどのようにHextech Craftingのような機能をビルドしデプロイするかをお話ししてきましたが、このようなコンテナ環境での作業に多くの時間を費やしてきた方なら、それが問題の全てではないことはご存知でしょう。

rClusterモデルでは、コンテナは、動的IPアドレスを持ち、絶えずスピンアップとスピンダウンをしています。これは、私たちの以前の静的サーバおよびデプロイ方法とは全く異なるパラダイムであるため、有効にするには、新しいツールと手順が必要です。

発生した幾つかの重要な問題は次のとおりです。

  • アプリケーションの能力とエンドポイントが常に変化している場合、どのようにアプリケーションを監視するのか?
  • エンドポイントが常に変化している場合、1つのアプリは別のアプリのエンドポイントをどのように知るのか?
  • 新しいコンテナが起動されるたびに、コンテナにsshで入ることができずにログがリセットされる場合、アプリケーションの問題の優先順位をどのように決定するのか?
  • ビルド時に自分のコンテナを焼いている場合、DBのパスワードなどをどのように設定するのか?または、トルコと北アメリカではどのようなオプションが切り替えられるのか?

これらを解決するには、私たちはService Discovery、Configuration Management、Monitoringといったものを処理するためにMicroservices Platformをビルドする必要がありました。このシステムと、それが解決する問題については、この連載の最終回で詳しく説明する予定です。

まとめ

私はこの投稿で、Riotがプレイヤにもっと容易に価値を届けるために解決に取り組んでいるような問題について、概要をお伝えできたのならうれしいです。前に述べたように、私たちは、rClusterが利用しているスケジューリング、Dockerとのネットワーキング、動的なアプリケーションの実行に焦点を当てて、引き続き記事を連載していく予定です。これらの記事がリリースされると、下のリンクが更新されます。

同じような開発をしている方や、会話に参加したい方は、ぜひ下のコメント欄にご記入ください。

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