Edited at
Z LabDay 2

Istio入門 その4 -基礎から振り返る-

2017年のZ Lab Advent CalendarでもIstio入門シリーズについて書きました。あれからはや1年。Istioのバージョンもv0.2からv1.0.4まで11バージョンもリリースされています。またIstioで使われているEnvoyは、Kubernetesなどと同じご卒業フェーズ1になりました。

もちろんコンセプトは変わっていませんが、v0.8あたりからv1にむけてコンポーネント名や設定方法などは大幅に変更されています。これらの変更点に注意しつつ、Istioを基本から振り返って見ましょう。2


マイクロサービスとその問題点

マイクロサービスというシステムの設計パターンは2012年ごろから言われていましたが、世界的にバズったのは2014年のJames Lewis & Martin FowlerによるMicroservicesについてのブログ記事がきっかけです。

マイクロサービスとは、次の図のように用途や目的ごとに小さなアプリケーションを作るシステム設計パターンのことを言います。分かりやすさのために、従来のシステムに多いモノリシック(日本語で一枚板という意味)なシステムと比較されます。

では、マイクロサービスは従来のモノリシックなシステムと比べて何が良いのでしょうか?大まかには、次の3点が利点としてあげられます。



  1. スケールアウトしやすい


  2. 変更しやすい


  3. 大人数で開発しやすい

1つめのスケールアウトしやすいというのは、機能や目的ごとにシステムのコンポーネントが分かれているので、「人気が出てアクセスが増えた機能だけ増やしたい」ということが、モノリシックなシステムと比べてやり易い3という意味です。モノリシックなシステムで同じことをしたい場合、1機能であってもシステム全体を複製したり、スケールアップしたりとリソースの無駄がでやすいという欠点があります。

同じような理由で、マイクロサービスでは変更がしやすくなっています。マイクロサービスではコンポーネントごとに独立してデプロイを実施できるように設計するので、デプロイフローの対象となるのは、実際に変更を行ったコンポーネントだけです。変更範囲が小さければ小さいほど、変更に掛かる時間は短くなるので頻繁なアプリケーションの改善が行いやすくなります。

一方、モノリシックなシステムでは、ソースコードレベルではモジュールなどを用いて機能分割を行なっていても、デプロイ時にはシステム全体を結合した後にデプロイするという流れになります。よってシステム全体の再デプロイでは、時間が掛かり頻繁に実施できないという問題点があります。

3点目は大規模開発がしやすいという点です。もちろん、少人数のチームでマイクロサービスを採用すると、複数のコンポーネントを開発する分時間がかかり管理も面倒と旨みがありません。しかし、数百人などの大人数のチームの場合には話が違います。この違いには、コミュニケーションコストが深く関わります。

大人数チームでモノリシックなシステムを開発すると、一つの機能変更はシステム全体に影響を与えるため、デプロイの合意を取るまでにミーティングなどを重ねることになるので、コミュニケーションコストが高くなります。

対してマイクロサービスでは、機能や目的ごとにコンポーネントを分割し、そレを開発するための小規模なチームを作ります。各コンポーネントでは、アプリケーションインタフェース(API)を定義するので、依存関係はAPIのみに留めることができます。よって、コンポーネント内部の変更は独立して実施し、APIを変更するときのみチーム間で合意を取ることで、コミュニケーションコストを軽減できます。

利点ばかりを話してきましたが、マイクロサービスにも欠点はあります。システムに機能変更や追加はつきものです。よって、システム規模は年を重ねるごとにより大きく、そしてより複雑になっていきます。

大規模かつ複雑になったマイクロサービスによってもたらされる問題点には、次のようなものがあります。("あります。"というより、実際にマイクロサービスな作ってきて感じた問題点です。)

1. システムのコンポーネント間通信を制御しきれない

2. 障害時に何が起こるか分からない
3. 鍵と証明書を管理しきれない
4. システムの全体像が把握できない

前置きが長くなりましたが、これらの問題解決を助けてくれるのがIstioです。この後の章では、Istioを使ってどのようにこれらの問題を解決できるのかを解説していきます。


大規模マイクロサービスの問題を解決するIstioとは?

問題の解決編に入る前に、まずはIstioの概要を掴んでいきましょう。

サービスメッシュとは、アプリケーション間でメッシュ状に通信するネットワークのことをいいます。前章は大規模かつ複雑になったマイクロサービスによって問題が発生すると述べましたが、マイクロサービスが大規模かつ複雑になるということは、このサービスメッシュが大規模かつ複雑になったということと同義です。

Istioは、この「複雑なサービスメッシュの管理が追いつかない」という問題を解決するためにLyft, IBM, Googleといった会社によって開発されはじめたオープンソースです。

では、Istioはサービスメッシュをどうやって管理しているのでしょうか?ポイントは、アプリケーションごとにセットになっているプロキシサーバにあります。Istioは司令塔の役割を持つコントロールプレーンと、指令に基づいて通信制御等をする実行部隊なデータプレーンに分かれています。また、コントロールプレーンのコンポーネントは、API経由でデータプレーンにあるプロキシサーバの設定を制御しています。


コントロールプレーンの役割

コントロールプレーンの主な登場人物は次の3つです。

* Pilot: プロキシサーバのトラフィックを管理するコンポーネントで、基盤(Kubernetesなど)に応じたサービスディスカバリを担当しています。

* Mixer: 収集したデータをもとに、アクセス制御を行うコンポーネントです。プラグインモデルを取っているので、Prometheusなどのメトリクスサービスと連携したりと、柔軟なカスタマイズが可能になっています。

*Citadel4: エンドユーザとアプリケーション間の認証や、コンポーネント間の相互認証など、サービスメッシュのセキュリティを担当するコンポーネントです。


サービスメッシュ・ネイティブなプロキシ Envoy

データプレーンのプロキシサーバには、Lyftによって開発されたEnvoyというL4/L7プロキシが使われています。開発言語はCNCFのプロダクトの中ではちょっと珍しくC++です。

プロキシサーバというとNginx5が有名ですが、EnvoyではAPI経由の設定変更に対応しているので、設定変更時のサーバの再起動が不要です。よって、設定を頻繁に変更するマイクロサービスのサービシュメッシュ管理用のプロキシサーバとして使いやすいものになっています。

ホットリスタートの仕組みなど面白い話が詰まっているので、詳細がきになる方はEnvoyブログがおすすめです。

* Blog: Envoy Threading Model

* Blog: Envoy Hot Restart


:bulb: Info

ちなみに、アプリケーションでセットでデプロイするコンポーネントのことを、その姿がサイドカーに似ているのでアプリケーションのサイドカーと呼びます。



問題1 システムのコンポーネント間通信を制御しきれない

ついに問題解決編です! システムのコンポーネント間通信(サービスメッシュ)を制御しきれないという問題は、アプリケーションコード内にサービスメッシュの設定が埋め込まれている場合にある問題です。どういうことかというと、システムの運用をしていると、次第にカナリア・リリースしたい、携帯とPCでコンポーネントの接続先を振り分けたい、本番のリクエストをミラーしてステージング環境に流したいなどと、様々な通信制御の要望があがってきます。こんな時に、アプリケーションのコードにサービスメッシュの設定が埋め込まれていると、修正ごとに再デプロイが必要になるので、運用・開発チームの双方に負荷がかかってしまいます。

この問題に対して、Istioでは設定をコードから分離するというアプローチを取っています。これにより設定がコードに依存しないので、開発者と運用者が分業しやすいというメリットがあります。また、Istioで使われているEnvoyはL7のロードバランシングができるため、リクエストヘッダを見てテストユーザIDを持つXXXグループだけテスト環境を表示するということや、User-agentがAndroidのリクエストだけ別のコンポーネントに振り分けるなど、柔軟なトラフィック管理が可能です。


トラフィック制御例: カナリア・リリース

サービスのデプロイ方式はいくつかの種類がありますが、クラウドと共に流行りだしたのがBlue/Greenデプロイメントです。これは、Blue(現在動いているバージョン)とGreen(新しいバージョン)の2環境を用意されている場合、ロードバランサの向き先を変えるだけで新バージョンへのアップグレードもロールバックも最小のダウンタイムで実現できるというものです。

単純に全てのアクセスをGreenに切り替えるという方法もありますが、新しいバージョンにバグがあった場合にユーザへ大きな影響を与えてしまいます。そのため、一度に新しいバージョンに切り替えるのではなく、先行して一部のみを切り替えて問題がなければ新バージョンの割合を徐々に増やしていく手法が考えられました。このような、バグによるユーザ影響を狭める手法をカナリア・リリース6といいます。

では、実際にどのようにトラフィック制御できるのかを見ていきましょう。下のサンプルコードは、Istioのトラフィック制御を行うKubernetes用の設定ファイルです。

このVirtualService7はトラフィックのルーティングルールを定義するためのリソースで、spec.hostsを宛先として持つリクエストを、該当するプロトコルのルールに基づいてルーティングします。この例ではspecにhttpが指定されていますが、他にもtlstcpに対応しています。8 また、DestinationRuleはその名の通りルーティングした後のトラフィックの宛先を定義するためのリソースです。ここでは使用していませんが、ロードバランシングのアルゴリズムも選択できます。9


canary-bar-all.yaml

apiVersion: networking.istio.io/v1alpha3

kind: VirtualService
metadata:
name: bar
spec:
# リクエストの宛先を指定
hosts:
- bar.default.svc.cluster.local # フルパスを書かなくてもbarでもOK
http:
  # hostsに対するhttpリクエストのルーティングルール
- route:
- destination:
host: bar.default.svc.cluster.local
subset: current
weight: 95
- destination:
host: bar.default.svc.cluster.local
subset: canary
weight: 5
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: bar-destination
spec:
host: bar.default.svc.cluster.local
# 上のVirtualServiceのdestination.subsetと対応している
subsets:
- name: current
labels:
version: v1
- name: canary
labels:
version: v2

このように、spec.http.route.destination.weightで簡単にトラフィックの割合を設定できるので、95%:5%で問題が発生しなければ90%:10%というようにこの設定を書き換えて適用するだけでカナリア・リリースを実現することができます。

カナリアリリースの醍醐味と言えば、メトリクスサーバと連携して新バージョンに問題が問題がないかのチェックを自動化し、問題がなければトラフィックの割合を自動的に変えていくというところです。10OSSではSpinnakerに組み込まれているkayentaが有名ですが、Istioにはここら辺を自動化する機能まではないので自前で構築していく必要があります。


:warning: Attention

Kubernetesでカナリア・リリースを実施する場合には、Istioのようにトラフィックを制御するのではなく、「PodのReplica数を新旧のバージョンで徐々に変えていく」という方法が使われることもあります。しかし、その場合にはReplica数が変更可能な割合を決めてしまうことに注意してください。

トラフィック制御の場合には、トラフィックの1%のみを新バージョンに流すという制御ができますが、旧バージョンのレプリカ数3、新バージョンのレプリカ数1しかなかった場合、新バージョンにいきなり25%のトラフィックが流れることになります。



Pod内のトラフィックをEnvoyに流す仕組み

設定ファイルによるトラフィックの制御方法がわかったところで、そろそろ内部的にどのように動いているのかが気になってきたのではないでしょうか?ということで、まずはアプリケーションをデプロイした場合、そのアプリケーションへのリクエストはどのようにしてEnvoyを経由することになるかについて解説していきます。

まず、KubernetesのAPIを使ってアプリケーションを通常通り作成すると、KubernetesのMutatingAdmissionWebhookという機能によって、Istio用にアプリケーションの設定を書き換えるためのWebhookが実行されます。11 istio-sidecar-injectorがこのフック先になっているので、サイドカーのプロキシなどIstio用の設定を追加し、KubernetesのAPIサーバにレスポンスを返します。このように内部的にIstioの設定をやってくれるので、ユーザからは通常のアプリケーション作成と変わらない体験を得ることができます。

次のコードは、実際にアプリケーションがデプロイされた後の設定ファイルの抜粋です。spec.containersにistio-proxy、spec.initContainersにistio-initが追加されていることがわかります。


istio-injected-pod.yaml

spec:

containers:
# 元からあるアプリケーションコンテナの設定
- image: istio/examples-bookinfo-details-v1:1.8.0
name: details
ports:
- containerPort: 9080
...
# 追加されたプロキシコンテナの設定
- image: docker.io/istio/proxyv2:1.0.4
name: istio-proxy
ports:
- containerPort: 15090
name: http-envoy-prom
protocol: TCP
...
securityContext:
readOnlyRootFilesystem: true
runAsUser: 1337
...
# 追加された初期化コンテナの設定
initContainers:
image: docker.io/istio/proxy_init:1.0.4
name: istio-init
- args: # iptablesを書き換えるスクリプトの引数を指定している
- -p
- "15001" # Envoyのリッスンポート
- -u
- "1337" # isitio-proxyのUID
- -m
- REDIRECT
- -i
- '*'
- -x
- ""
- -b
- "9080" # アプリケーションのリッスンポート
- -d
- ""
...

設定ファイルのチェックが完了すると、Kubernetes内でいろいろ12な処理が実施され、コンテナが作成されます。作成されるコンテナは、アプリケーション作成時に定義していたコンテナだけではなく、istio-sidecar-injectorによって追加されたistio-initと、istio-proxyの2つも追加で作成されます。istio-initはその名の通り前処理をするコンテナで、iptablesのルールを書き換えてインバウンドとアウトバウンドのトラフィックをistio-proxyにリダイレクトします。

このようにiptablesのルールを書き換えることで、アプリケーションへのトラフィックを全てIstioの管理下に置かれたプロキシに通しているのです。


問題2: 障害時に何が起こるか分からない

だいぶ脇道に逸れましたが、問題解決編その2まできました。続いての問題は、障害時に何が起こるか分からないです。人間である以上、システムの実装時にバグなく作り込むことは不可能です。よって、システムのテストをする必要があります。

ハードウェア障害やネットワーク障害などの異常系の試験について考えると、モノリシックなシステムの場合にはシステムは1つなので、頑張れば網羅的に試験できるかもしれません。しかし、マイクロサービスならどうでしょうか?たったの4コンポーネントだったとしても、Fooのハードディスクに障害が発生した時にBar/Baz/Quxの挙動は問題ないか?など、一気に試験パターンが増えていきます。よって、現代の大規模かつ複雑なマイクロサービスにおいて、全パターンをテストすることは現実的に不可能です。

そんなこんなで、テストのみでシステムの可用性を保つことに無理がでてきたため、近年ではケイオス・エンジニアリングという方法が流行ってきました。これは、ハードウェア障害やネットワーク障害、ユーザから大きなアクセスが突発的にきた状況など起こりそうな障害を実際にシミュレートして、バグが発覚したら直していくという手法です。テストは仕様に基づいて入力に対する出力が正しいことを確認する方法ですが、ケイオス・エンジニアリングは実際に障害という新たな状況を引き起こすことによって、その経過を観測し、対応するという別のアプローチになります。

詳しい話をし始めると終わらなくなるのでここら辺で一旦止めるとして、詳しい話はNetflixのChaos Engineering本がお勧めなので読んでみてください。


事前に障害をシミュレートすることで大規模障害に備える

おきて欲しくないときに限って頻繁におきることもある障害ですが、障害を計画的に起こすというのは意外と面倒です。障害を起こすコードをアプリケーションへ実際に組み込むわけにはいきませんし、物理的にケーブルを引き抜くというのは有効ではありますが、自動化して継続的に実行するには面倒です。ネットワーク系の障害であるとtcコマンドで帯域制限などが行われることもありますが、影響範囲を最小限に抑えるためにノードレベルではなくコンテナレベルで制限するには準備に時間がかかります。

そんな面倒な計画的障害も、Istioを使うとサービスティスカバリとアプリケーション毎のL7ロードバランサの利点を生かして比較的簡単かつコントローラブルに実施することができます。また、対応している機能は任意の時間だけリクエストを遅らせるdelayと、該当のステータスコードをアプリケーションを介さずに返却するabortの2つです。


failure-injection.yaml

apiVersion: networking.istio.io/v1alpha3

kind: VirtualService
metadata:
name: foo
spec:
hosts:
- foo
http:
- fault:
# 10%のリクエストを5秒遅延させる
delay:
percent: 10
fixedDelay: 5s
match:
- headers: # Cookieにuser=testerを持つリクエストのみを対象とする
cookie:
user: tester
...
- fault:
# 30%のリクエストに対して400エラーを返却する
abort:
percent: 30
httpStatus: 400
...

上のコードはVirtualServiceリソースを使って、障害発生の設定を行なっているファイルです。このように、リクエストに対する障害発生率と発生させる障害内容を定義するだけでコントロールすることができます。また、このようにシステムに障害を発生させることをFailure Injectionと呼びます。


サーキット・ブレーカで障害の影響範囲を最小限にとどめる

いずれかのコンポーネントで障害が発生した場合、そのレスポンスがタイムアウトするまで待機することになりますが、この場合そのレスポンスを待つコンポーネントもタイムアウトまで待機することになります。このように、マイクロサービスでは1コンポーネントの障害が依存するコンポーネントの障害を引き起こしてしまうことがあります。これを防ぐための仕組みをサーキット・ブレーカといいます。この仕組みがどのように機能するかというと、継続的に障害が発生している場合にはタイムアウトを待たずにエラーを返すという方法をとります。

サーキット・ブレーカを実現するための実装は意外と面倒です。NetflixのHystrixなどオープンソースのライブラリもありますが、言語ごとに実装も必要になるので複数の言語で書かれることの多いマイクロサービスのコンポーネントで実現するにはやはりコストが高くなります。

Istioでは、先ほどのFailure Injectionと同じく、サイドカープロキシを利用することでアプリケーションコードへの実装なしにサーキット・ブレーカを実現することができます。次のコードは、この設定を行うためのDestinationRuleを示しています。


circuit-breaker.yaml

apiVersion: networking.istio.io/v1alpha3

kind: DestinationRule
...
trafficPolicy:
# コネクションの制限を定義している
connectionPool:
tcp:
maxConnections: 100
http:
http2MaxRequests: 1000
maxRequestPerConnection: 10
# 異常値の閾値とその時の挙動を定義
outlinerDetection:
http:
consecutiveErrors: 7 # 継続した障害だと判定される閾値
Interval: 5m # 解析を行う間隔
baseEjectionTime: 10 # consecutiveErrorsの上限を越したときに、ホストが即時エラーを返す最小期間

アルゴリズムなどの詳細はEnvoyのOutlier Detectionを参照してください。


以下省略

疲れたのでこれくらいにしておきます。きになる方は元資料をどうぞ。

(需要があれば続きを書くかもしれません。)

https://speakerdeck.com/ladicle/istiotogong-nimaikurosabisunili-tixiang-kae


まとめ

Istioがどのように大規模かつ複雑になったマイクロサービスの問題を解決できるのかを紹介してきました。最後に各問題に対してどのようなアプローチを取っていたのかを振り返ってみます。

1. システムのコンポーネント間通信を制御しきれない

"サービスメッシュの設定とアプリケーションのコードを分離する"
2. 障害時に何が起こるか分からない
"Fault Injection と サーキット・ブレーカで障害に備える"
3. 鍵と証明書を管理しきれない
"鍵と証明書の管理はCitadelに任せて自動化する"
4. システムの全体像が把握できない
"Mixerの機能を使って可視化ツールを導入し、システムの全体像を把握する"

以上、このエントリは Z Lab のメンバーによる Z Lab Advent Calendar 2018 の2日目として業務時間中に書きました。3日目は @tkusumi の担当です





  1. CNCFプロジェクトの成熟度は3つに分けれており、ご卒業は一番上のレベル https://www.cncf.io/projects/ 



  2. なんか見たことある! と思った方、ありがとうございます。Japan Container Days v18.04でやった発表を文字起こしつつ、Istioの最新バージョンに合わせてアップデートしてます。 



  3. やり易いだけで、必ずできることではありません。他の機能との依存関係を増やしすぎたために、結局モノリシックなシステムと変わらないというミスはよく聞く話です。 



  4. Istio v0.8からIstio-Auth, Istio-CAがCitadel(日本語での意味)に変わったことに注意してください。 



  5. Nginxと違ってWebサーバの機能は持っていません 



  6. 一酸化炭素を検出するために鉱山へカナリアを連れて行ったことから、異常を検知するために先行して動作させるものをカナリアと呼びます。 



  7. 去年のカナリア・リリースの紹介ではRouteRuleを使った方法を紹介しましたが、v1alpha1のこのリソースはすでに廃止されたことに注意してください。 



  8. 詳しくはこちらのドキュメントをご参照ください。 



  9. 詳細はこちら 



  10. 人間にカナリア・リリースをやらせるとバグを見逃したという話もあるので、メトリクスの細かな変化を検知するのは、やはりそれが得意な機械にやらせるべきです。ここら辺の話はGoogleやNetflixのリリースエンジニアリング周りの書籍やドキュメントに詳しく書かれているのでオススメです。 



  11. 実際にIstio Sidecar Injectorが動作するのは、このコンポーネントがデプロイされているかつ、該当の名前空間でistio-injection=enabledになっている必要があるのですが、ここでは細かいことを省いています。 



  12. いろいろの内容は弊社Z LabのQiita記事に書かれているので参照してください。