現実世界のマイクロサービス:サービスに陰りが見え始め、いよいよ本気になるとき

マイクロサービスを用いれば、エンジニアリングチームは迅速にプロダクトを拡大することができます……もちろん、彼らが分散システム運用の複雑さのせいで泥沼にはまっていなければの話です。本記事では、マイクロサービスの運用に関わる非常に厳しい問題―例えば大規模なサービスのステージングやカナリアデプロイなどの問題―が、RPC層にルーティングの考え方を導入することにより、どう解決できるのかを説明します。

私は、Twitterでインフラのエンジニアを務めていた時代(2010年から2015年まで)を振り返ってみました。すると、当時はそういった言葉がなかったというだけで、私たちは「マイクロサービスを使っていた」のだということが分かります(当時は、今思えば分かりにくい言葉、SOA<サービス指向アーキテクチャ>と呼んでいました)。

バズワードはさておき、当時も、現在私たちがマイクロサービスを使おうとする動機と同じ動機がありました。私たちは、エンジニアリングチームが独立して運用できるように、言い換えれば、デプロイスケジュールやオンコールのローテーション、可用性やスケールを自分たちでコントロールできるようにする必要があったのです。こうしたチームには、サイトを停止することなく、迅速かつ独立的に工程を繰り返したりスケールしたりできるという柔軟性が求められていました。

私はマイクロサービスの形成期に、世界最大レベルに属するマイクロサービスのアプリケーションに取り組んできました。そんな私が確信を持って言えるのは、マイクロサービスとはスケーリングにかける魔法ではないということです。すなわち、それは柔軟性もなければ、セキュリティも信頼性もないものなのです。実は、運用するのはモノリシックなサービスよりもずっと難しいというのが、経験に基づく私の見解です。私がよく使っていたという意味で信頼できる真のツール、例えばコンフィギュレーション管理、ログ処理、strace、tcpdumpなどですが、これらをマイクロサービスに当てはめてみると、まだ粗削りでとてもスマートとは言えない手段だと分かります。マイクロサービスとは、1つのリクエストのために何百ものサービスに手を出す可能性があるという世界です。そんな世界では、何百というインスタンスに対し、どこでtcpdumpを実行したらいいのでしょうか? どのログを読めばいいのでしょうか? そこに時間がかかっていたら、どうやってその原因を突き止めることができるでしょうか? 何かを変えたいときには、その変更は安全に行えるということをどうやって確信できるのでしょうか?


訳:私たちはモノリスをマイクロサービスに切り替えたから、サービス停止の度に殺人ミステリーのような状況になるかもしれない。

Twitterがマイクロサービスに移行したときは、操作性を取り戻すためだけに何百もの(数千もの?)スタッフによる何年にもわたる取り組みが必要となりました。マイクロサービスには、どんな組織でもこのレベルの投資が必要になるということならば、そうしたプロジェクトは大半があっさり失敗に終わるでしょう。しかし、ありがたいことに、ここ数年、マイクロサービス運用の負担を一部軽くする目的でオープンソースプロジェクトが登場しています。それは、データセンターやクラウドの詳細を簡潔にまとめるプロジェクト、システムの実行時間のステータスに可視性を加えるプロジェクト、あるいはサービスの記述を単純化してくれるプロジェクトなどです。しかし、これはまだ、マイクロサービスの規模を拡大していくために必要となるものとしては完全ではありません。チームにとっては、ソースコードから製品、クラウドに至るまで、いいお助けツールはいろいろとありますが、オペレータとしては、これらのサービスが一度実行されるとそれがどうインタラクトするかを制御するには、まだ到底十分とは言えません。Twitterの例で、私たちがサービス間の通信、すなわちRPCを制御できるツールが必要だということが分かりました。

こうした経験を基に、linkerd(”リンカーディー”と発音します)が生まれました。linkerdは、オペレータがサービス間のトラフィックをコマンド&コントロールできるように設計されたプロキシです。トランスポート・セキュリティロードバランシング、多重化、タイムアウト、リトライ、ルーティングといった、あらゆる機能を有します。

本記事では、linkerdによるルーティングへのアプローチを説明しようと思います。古くから、ルーティングはレイヤ3と4、すなわちTCP/IPに属する問題の1つであり、ハードウェアのロードバランサ、BGP、DNS、iptablesなどに絡む問題でした。これらのツールはこうした世界でまだまだ使われてはいるものの、現代のマルチサービスソフトウェアシステムへの拡張はしづらいものです。コネクションやパケットよりもリクエストとレスポンスを、IPアドレスとポートよりもサービスやインスタンスを、それぞれ操作したいのです。

実は、リクエストルーティングは、汎用性が高く、強い影響力を持つツールであることが分かりました。つまり、リクエストルーティングを用いることにより、マイクロサービスに生じる非常に困難な問題の一部を解決することができます。本番環境を安全に、インクリメンタルに、そして制御可能な状態を保って変更していくことができるのです。

linkerdにおけるルーティング

linkerdは、クライアントのリストを使用して設定する必要がありません。そうではなく、必要に応じてリクエストを動的にルーティングし、クライアント情報を供給します。ルーティングの仕組みは基本的に以下の3つによって成り立っています。

  • 論理名、リクエストを表す。
  • 具体名、サービスを表す(つまり、サービスディスカバリという手法で)。
  • および委譲テーブル(dtab)、具体名に対し論理名のマッピングを表す。

linkerdは、処理する全てのリクエストに対し論理名を割り当てます。例えば、/http/1.1/GET/users/addあるいは/thrift/userService/addUserです。論理名が表現する情報はアプリケーションに関連するもので、インフラに関するものではありません。そのため、論理名は通常、サービスディスカバリ(例えばetcd、consul、ZooKeeper)や環境(例えば本番環境やステージング環境)、地域(例えばus-central-1b<アメリカ中央部1b>、us-east-1<アメリカ東部1>に関する詳細は表しません。

こうした情報の詳細は、具体名でコード化されます。具体名は、通常、ZooKeeper、etcd、consul、DNSなどのようなサービスディスカバリのバックエンドを表します。例えば以下のようなものです。

  • /$/inet/users.example.com/8080にはinetアドレスが付けられる。
  • /io.l5d.k8s/default/thrift/usersにはkubernetesサービスが付けられる。
  • /io.l5d.serversets/users/prod/thriftにはZooKeeperのサーバセットが付けられる。

この「namer」サブシステムは、任意のサービスディスカバリの構想をサポートするための拡張ができるよう、プラグインが可能です。

委譲

論理名および具体名を区別することで、以下の2つのメリットがあります。

  1. アプリケーションコードは運用の詳細ではなく、ビジネスロジック、すなわちユーザ、写真、ツイートなどに焦点を当てられる。
  2. バックエンドは文脈的に、かつnamerdのヘルプを借りて、動的に決定することができる。

論理名から具体名へのマッピングは、委譲テーブル、すなわちDtabによって記述されています。例えば、linkerdは/http/1.1/<METHOD>/<HOST>の形式でHTTPリクエストに名前を割り当てることができます。

linkerdを以下のように設定すると仮定します。

namers:
- kind: io.l5d.experimental.k8s
 authTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token

routers:
- protocol: http
 servers:
 - port: 4140
 baseDtab: |
   /srv         => /io.l5d.k8s/default/http ;
   /host        => /srv ;
   /http/1.1/*  => /host ;

このコンフィギュレーションでは、/http/1.1/GET/usersのような論理名が/io.l5d.k8s/default/http/usersのような具体名へと書き換えられることにより委譲されます。

委譲前 委譲 委譲後
/http/1.1/GET/users /http/1.1/*=>/host /host/users
/host/users /host=>/srv /srv/users
/srv/users /srv=>/io.l5d/default/http /io.l5d.k8s/default/http/users

最終的に、具体名の/io.l5d.k8s/default/http/usersはサービスディスカバリシステムをアドレス指定します。この場合は、KubernetesのマスターAPIに対応しています。io.l5d.k8sのnamerは、「名前空間ポートサービス」という形式の名前を期待しますから、つまり、defaultという名前空間のhttpポートのusersサービスに対するアドレスのlinkerdロードバランサです。

ZooKeeperの中でこのサービスを探し、見つからなければローカルのファイルシステムにフォールバックする」といったロジックを表現するために、複数のnamerが組み合わされることもあります。

namers:
- kind: io.l5d.fs
 rootDir: /path/to/services
- kind: io.l5d.serversets
 zkAddrs:
 - host: 127.0.0.1
   port: 2181

routers:
- protocol: http
 servers:
 - port: 4140
 baseDtab: |
   /srv         => /io.l5d.fs ;
   /srv         => /io.l5d.serversets/path/to/services ;
   /host        => /srv ;
   /http/1.1/*  => /host ;

サーバセットが見つからない場合には、ファイルシステムのnamerに対して検索が行われるよう、/srvの委譲を組み合わせてフォールバックを構成しています。

リクエスト前のオーバーライド

コンテキストに沿って解決するというこの考え方は、個々のリクエストのルーティング方法を変えることにまで拡張できます。

仮に、「あるサービスの新バージョンをステージして、新バージョンでのアプリケーションの振る舞いについてイメージをつかみたい」という状況を考えてみましょう。このサービスは、ユーザに直接的には関わらないものの、このサービスを呼び出す他のサービスが存在するという想定です。例としては、「users」サービスが一般的でしょう。ここでは以下の選択肢があります。

  1. 単に本番環境へデプロイする。#YOLO(訳注:You only live once<人生一度きり>の意味)
  2. このサービスを呼び出す全サービスのステージングバージョンをデプロイする。

どちらのやり方も特に処理しやすいわけではありません。前者はユーザに関わる問題を引き起こします。後者は複雑で扱いにくくなります。というのも、このサービスを呼び出す全サービスの新たな設定をデプロイするのに必要な、アクセスやツーリングを持っていないかもしれないのです……。

幸い、linkerdで得られるルーティング能力のおかげで、アドホックなステージングが可能となります。個々のリクエストに対して前述の委譲システムを拡張すれば、どの呼び出し元も変えることなくusersサービスの新バージョンをステージすることができます。例えば以下のように実行します。

$ curl -H 'Dtab-local: /host/users=>/srv/users-v2' https://example.com/

これは、通常は/srv/usersにリクエストを送信する全てのサービスが、代わりに/srv/users-v2にリクエストを送信するようにするものです。このリクエストに対してのみ適用されます。カバーされるのは全サービスです。

そしてこれはcurlコマンドに限った話ではありません。同じようなことがブラウザのプラグインでも簡単にサポートできます。

上記のアプローチは、複雑なマイクロサービスにおいてサービスの新バージョンをステージする際のオーバーヘッドを大いに減らしてくれます。

namerdを使ったダイナミックルーティング

ここまで、静的な委譲テーブルを用いてlinkerdを設定する方法をお話ししてきました。ですが、実行時にルーティングポリシーを変更したい場合はどうでしょうか? 「カナリア」や「ブルーグリーン」のデプロイをサポートするためにステージングで用いた方法と同様のアプローチを取りたい場合は? namerdの出番です。

namerdは、オペレータが委譲を管理できるようにするサービスです。namerdはサービスディスカバリシステムに対するフロントエンドとして機能するので、linkerdがサービスディスカバリシステムと直接通信しなくてもよくなります。つまり、linkerdインスタンスがnamerdを介して名前を解決することで、サービスディスカバリ用バックエンドのビューがメンテナンスされるのです。

namerd topology
注釈
application RPC:アプリケーションのRPC
proxied RPC:プロキシされたRPC
routing policy:ルーティングポリシー
pluggable data store:接続可能なデータストア

namerdは以下のように設定します。

  • (接続可能な)ストレージのバックエンド。例えばZooKeeperやetcd。
  • namerdにサービスディスカバリの実行方法を知らせる「namer」。
  • 外部インターフェース。通常は、オペレータが委譲を更新するための制御インターフェースと、linkerdインスタンスのための同期インターフェース。

次に、linkerdの設定は、簡略化して示すと以下のようになります。

routers:
- protocol: http
  servers:
  - port: 4180
  interpreter:
    kind: io.l5d.namerd
    namespace: web
    dst: /$/inet/namerd.example.com/4290

そして、namerdの設定はこうなります。

# pluggable dtab storage -- for this example we'll just use an in-memory version.
storage:
  kind: io.buoyant.namerd.storage.inMemory

# pluggable namers (for service discovery)
namers:
  - kind: io.l5d.fs
    ...
  - kind: io.l5d.serversets
    ...

interfaces:
  # used by linkerds to receive updates
  - kind: thriftNameInterpreter
    ip: 0.0.0.0
    port: 4100

  # used by `namerctl` to manage configuration
  - kind: httpController
    ip: 0.0.0.0
    port: 4180

いったんnamerdが実行され、linkerdがnamerdを介して名前を解決するよう設定されると、コマンドラインユーティリティnamerctlを使ってルーティングを動的に更新できるようになります。

namerdを最初に開始する際には、(webという名前の)基本的なdtabを次のように生成します。

$ namerctl dtab create web - <<EOF
/srv         => /io.l5d.fs ;
/srv         => /io.l5d.serversets/path/to/services ;
/host        => /srv ;
/http/1.1/*  => /host ;
EOF

例えば下記は、users-v2サービスの「カナリアテスト」を実行するため、実際の本番環境におけるトラフィックの1%を送信するものです。

$ namerctl dtab update web - <<EOF
/srv         => /io.l5d.fs ;
/srv         => /io.l5d.serversets/path/to/services ;
/host        => /srv ;
/http/1.1/*  => /host ;
/host/users  => 1 * /srv/users-v2 & 99 * /srv/users ;
EOF

新バージョンが受け取るトラフィックの量は、重みを変えることで制御できます。例として、usersのトラフィックの25%をusers-v2に送信するには、namerdを以下のように更新します。

$ namerctl dtab update web - <<EOF
/srv         => /io.l5d.fs ;
/srv         => /io.l5d.serversets/path/to/services ;
/host        => /srv ;
/http/1.1/*  => /host ;
/host/users  => 1 * /srv/users-v2 & 3 * /srv/users ;
EOF

ついに新しいサービスのパフォーマンスに満足できるようになったときには、新バージョンが実際に用意できているのであれば、それを指すようにnamerdを更新できます。もし新バージョンがなくなるのであれば、元のバージョンにフォールバックするよう更新することになります。

$ namerctl dtab update web - <<EOF
/srv         => /io.l5d.fs ;
/srv         => /io.l5d.serversets/path/to/services ;
/host        => /srv ;
/http/1.1/*  => /host ;
/host/users  => /srv/users-v2 | /srv/users ;
EOF

linkerdとは違って、namerdはまだまだ新しいプロジェクトです。私たちは、namerdが運用とデバッグのしやすいツールであるようにするため、開発の工程を迅速に繰り返しています。プロジェクトが成熟してくれば、オペレータにとって実行時にサービスを制御するための強力なツールとなるでしょう。新しい機能を安全に少しずつ管理された形でロールアウト(およびロールバック)する、デプロイツールと統合される可能性もあります。namerdは、チームがモノリスからマイクロサービスへと機能を移行させるのに役立ち、システムのデバッグをやりやすくしてくれるでしょう。私は、RPCのツーリングがいかに強力なものとなる可能性があるかを直に見てきたので、その機能をオープンソースコミュニティに持ち込むことができて大変うれしいです。

linkerdと同様に、namerdはApache License v2のもとで提供されているオープンソースソフトウェアです。namerdをオープンソースコミュニティにリリースできたのは本当に喜ばしいことですし、私たちがBuoyantで構築しているツールの開発に皆さんも関わってくださることを期待しています。すごい経験ができますよ。

自分で試してみよう

私たちは、linkerdとnamerdをKubernetesMesos + Marathonで実行するやり方の例をlinkerd-examplesリポジトリで公開しています。これらのリポジトリには、環境の立ち上げとルーティングに必要なものをできるだけたくさん登録していきたいと考えています。

もし、作業をしていて分からない点があれば、ご遠慮なくslack.linkerd.ioまでお尋ねください。