Chaos Toolkitによるカオス挿入
これで、AKSクラスタ上にサンプルのアプリケーションが動きましたので、Chaos Toolkitを使ってこのシステムにカオスを挿入します。
実験の環境構築
次のコマンドを実行し、Chaos Toolkitの環境を作成します。詳細な手順については、Chaos Toolkitでカオスエンジニアリングを体験しよう~Azure上のVMにカオス挿入を参考にしてください。
ここでは、追加でChaos ToolkitのKubernetes拡張機能をインストールします。また、環境変数KUBECONFIGにKubernetesクラスタへの接続情報ファイルのパスを設定します。
pip install chaostoolkit-kubernetes export KUBECONFIG=~/.kube/config
1. Podの削除
Kubernetesには自己修復機能が備わっています。そのためPodが強制終了されても自動で復旧します。これを実際に確認してみましょう。
まず、動作しているfrontendアプリのPodを削除し、システムの状態を確認します。
次の実験ファイル(experiment.pod.json)を作成します。
{
"title": "System is resilient to backend failures",
"description": "Can our frontend service gracefully a backend failure?",
...
"steady-state-hypothesis": {
"title": "Services are all available and healthy",
"probes": [
{
"type": "probe",
"name": "all-services-are-healthy",
"tolerance": true,
"provider": {
"type": "python",
"module": "chaosk8s.probes",
"func": "all_pods_healthy"
}
},
{
"type": "probe",
"name": "front-service-must-still-respond",
"tolerance": 200,
"provider": {
"type": "http",
"url": "http://<your frontend Extarnal IP>/"
}
}
]
},
"method": [
{
"type": "action",
"name": "stop-frontend-pod",
"provider": {
"type": "python",
"module": "chaosk8s.pod.actions",
"func": "delete_pods",
"arguments": {
"name": "front"
}
},
...
}
ここで、Chaos ToolkitのKubernetes拡張機能のchaosk8s.pod.actionsのdelete_podsを使います。ここではfrontendアプリのPodを削除したあと、エンドポイントにアクセスして問題なく稼働していれば、仮説通りの動きになります。
次のコマンドで実験を実行します。
chaos run experiment.pod.json
[2021-10-25 10:15:24 INFO] Validating the experiment's syntax [2021-10-25 10:15:44 INFO] Experiment looks valid .... [2021-10-25 10:15:47 INFO] Playing your experiment's method now... [2021-10-25 10:15:47 INFO] Action: stop-backend-service [2021-10-25 10:15:47 INFO] Pausing after activity for 10s... [2021-10-25 10:15:57 INFO] Probe: all-services-are-healthy [2021-10-25 10:15:57 INFO] Steady state hypothesis: Services are all available and healthy [2021-10-25 10:15:57 INFO] Probe: all-services-are-healthy [2021-10-25 10:15:58 INFO] Probe: front-service-must-still-respond [2021-10-25 10:15:58 INFO] Steady state hypothesis is met! [2021-10-25 10:15:58 INFO] Let's rollback... [2021-10-25 10:15:58 INFO] No declared rollbacks, let's move on. [2021-10-25 10:15:58 INFO] Experiment ended with status: completed
ここでPodの状態を確認します。
ChaosToolkitによって、動作しているfront-54758ddbc9-vxk7qが削除されていますが、それを検知したKubernetesのReplicaSet Controllerが新たなPodであるfront-54758ddbc9-rj2c9を起動しています。
front-54758ddbc9-vxk7q 1/1 Terminating 0 44s front-54758ddbc9-rj2c9 0/1 Pending 0 0s front-54758ddbc9-rj2c9 0/1 Pending 0 0s front-54758ddbc9-rj2c9 0/1 ContainerCreating 0 0s front-54758ddbc9-vxk7q 0/1 Terminating 0 47s front-54758ddbc9-rj2c9 1/1 Running 0 3s front-54758ddbc9-vxk7q 0/1 Terminating 0 49s front-54758ddbc9-vxk7q 0/1 Terminating 0 49s
このようにKubernetesのもつ自己修復機能によりPodがただちに復旧し、frontendアプリケーションがエラーなくサービスを継続できていることが分かります。
2. Deploymentの削除
次にChaos Toolkitを使ってDeploymentを削除して、システムの状態を確認します。Deploymentが喪失するケースとしては、CI/CD Pipeline実行時のトラブルや、オペレーションミスなどが想定されます。
次の実験ファイル(experiment.deployment.json)を作成します。
"method": [
{
"type": "action",
"name": "stop-backend-service",
"provider": {
"type": "python",
"module": "chaosk8s.deployment.actions",
"func": "delete_deployment",
"arguments": {
"name": "back"
}
},
moduleはchaosk8s.deployment.actionsを使います。Deploymentを削除したいので、funcにはdelete_deploymentを指定します。今回はbackendアプリがデータを返せなくなるケースを想定しています。
サーキットブレーカーのないケース
Chaos Toolkitを使ってDeploymentリソースを削除して、システムの状態がどうなるかを確認します。今回のサンプルではfrontendアプリが内部でbackendアプリを呼び出しデータを取得していますが、例えばfrontendアプリがクラスタ外で提供されている外部APIを呼び出しているものの、対向サービスのネットワークエラー/システム障害/メンテナンスなどで、一時的に応答を返せなくなったケースなどもあてはまります。
public Mono<String> getApiData() {
return webClient.get()
.uri("http://backend:8081/message")
.retrieve()
.bodyToMono(String.class);
まず、現在のPodの状態を確認します。
kubectl get pod NAME READY STATUS RESTARTS AGE back-6cc87c6f6f-g2ck4 1/1 Running 0 19m front-54758ddbc9-rj2c9 1/1 Running 0 18m
次のコマンドを実行し、Chaos Toolkitを使って動作しているfrontend Deploymentを削除し、システムの状態を確認します。
chaos run experiment.deployment.json
[2021-10-25 10:37:52 INFO] Validating the experiment's syntax [2021-10-25 10:38:10 INFO] Experiment looks valid [2021-10-25 10:38:10 INFO] Running experiment: System is resilient to backend failures .... [2021-10-25 10:38:10 INFO] Playing your experiment's method now... [2021-10-25 10:38:10 INFO] Action: stop-backend-service [2021-10-25 10:38:10 INFO] Pausing after activity for 10s... [2021-10-25 10:38:20 INFO] Probe: all-services-are-healthy [2021-10-25 10:38:21 INFO] Steady state hypothesis: Services are all available and healthy [2021-10-25 10:38:21 INFO] Probe: all-services-are-healthy [2021-10-25 10:38:21 INFO] Probe: front-service-must-still-respond [2021-10-25 10:38:22 CRITICAL] Steady state probe 'front-service-must-still-respond' is not in the given tolerance so failing this experiment [2021-10-25 10:38:22 INFO] Experiment ended with status: deviated [2021-10-25 10:38:22 INFO] The steady-state has deviated, a weakness may have been discovered
ここでPodを確認すると、backend Deploymentが削除されたため、現在frontendのPodのみが動作している状態となります。
kubectl get pod NAME READY STATUS RESTARTS AGE front-54758ddbc9-rj2c9 1/1 Running 0 23m
実験の結果をみても、frontendアプリが通信先のbackendサービスからデータを取得できなくなり、クライアントにステータスコード200の応答を返せず、サービスが継続できていないことが分かります。
[2021-10-25 10:38:22 CRITICAL] Steady state probe 'front-service-must-still-respond' is not in the given tolerance so failing this experiment
サーキットブレーカーのあるケース
このサンプルの実装では、Deploymentリソースの喪失でシステムが停止することがわかりました。これを是正するためサーキットブレーカーを導入します。
今回のサンプルは2つのサービスからなるシンプルな構成ですが、本番環境で稼働するマイクロサービスでは、システムは自律的に動作する複数のサブシステムによって構成されます。そのため、あるサービスに障害が発生してもそれ以外のシステムは正常に動作する必要があります。
まず、実験を続けるためbackendアプリのDeploymentをいったん復旧します。
kubectl apply -f manifest/backend/deployment.yaml
kubectl get po NAME READY STATUS RESTARTS AGE back-6cc87c6f6f-x27fd 1/1 Running 0 4s front-54758ddbc9-rj2c9 1/1 Running 0 39m
frontendアプリのバージョン2を確認します。サーキットブレーカーの実装にはResilience4jを使います。これはJavaによるフォールトトレランスライブラリで、サーキットブレーカーだけでなくリトライ処理やハルクヘッドなどをサポートしています。
サーキットブレーカーは、最近発生した障害の数を監視して、処理を続行するか、例外を返すだけにするかを決定します。
具体的にはサーキットブレーカーは CLOSED/OPEN/HALF-OPENという3つの状態があります。
システムが正常な時はCLOSEDで、一定数以上処理が失敗した場合には、OPENになりアクセスを遮断します。OPENから一定時間経過するとHALF-OPENとなり、さらにHALF-OPENで一定数以上処理が失敗しなければ、正常状態であるCLOSEDに戻ります。
サーキットブレーカーについての詳細はMicrosoft Azureのサーキット ブレーカー パターンにまとまっています。
Resilience4jの設定は、Javaコードまたはプロパティファイルでできます。例えばslidingWindowで設定したcallの成功/失敗を保存し、失敗の確率がfailureRateThresholdに達すると、サーキットブレーカーをOPENにし、アプリケーションに例外を返します。
この例では、10回中の80%のリクエストに失敗すると、例外を返します。
@Configuration
public class CircuitConfig {
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
return CircuitBreakerRegistry.of(
CircuitBreakerConfig.custom()
.slidingWindowSize(10)
.failureRateThreshold(80)
.build()
);
}
}
circuitBreakerメソッドは、引数で指定したサーキットブレーカーが存在しない場合は新しく作成し、既に存在する場合はそのオブジェクトを返します。
ここではレジストリを介してfrontbreakerという名前のサーキットブレーカーを作成します。
@Bean
public CircuitBreaker circuitBreaker(CircuitBreakerRegistry circuitBreakerRegistry) {
return circuitBreakerRegistry.circuitBreaker("frontbreaker");
}
backendのサービスを呼び出すときにサーキットブレーカーを有効にするには、@CircuitBreakerアノテーションを使用するか、Operetorを使って指定します。
Operetorの場合、transformメソッドチェーンを利用してCircuitBreakerOperator.of(circuitBreaker)を利用すると、サーキットブレーカーが機能します。
このサンプルコードではapi.getApiDataの処理でサーキットブレーカーがOPENになると、onErrorResumeで指定したfallbackメソッドを呼び出します。
public Mono<String> getData() {
CircuitBreaker circuit = circuitBreakerRegistry.circuitBreaker("frontbreaker");
return api.getApiData()
.transform(CircuitBreakerOperator.of(circuit))
.onErrorResume(this::fallback);
}
public Mono<String> fallback(Throwable t) {
log.error("Fallback : " + t.getMessage());
return Mono.just("Sorry...I'm frontend.");
}
このサーキットブレーカーを実装したアプリケーションをKubernetesクラスタにデプロイします。frontend Deploymentのコンテナイメージのバージョンをv1からv2に変更してバージョンアップします。
code manifest/frontend/deployment.yaml
# 変更前 containers: - image: <your container registry name>/chaos-frontend:v1 # 変更後 containers: - image: <your container registry name>/chaos-frontend:v2
kubectl apply -f manifest/frontend/deployment.yaml
次のコマンドを実行し、Chaos Toolkitを使って動作しているfrontend Deploymentを削除し、システムの状態を確認します。
chaos run experiment.deployment.json
chaos run experiment.deployment.json [2021-10-25 11:08:07 INFO] Validating the experiment's syntax [2021-10-25 11:08:15 INFO] Experiment looks valid .... [2021-10-25 11:08:16 INFO] Playing your experiment's method now... [2021-10-25 11:08:16 INFO] Action: stop-backend-service [2021-10-25 11:08:17 INFO] Pausing after activity for 10s... [2021-10-25 11:08:27 INFO] Probe: all-services-are-healthy [2021-10-25 11:08:27 INFO] Steady state hypothesis: Services are all available and healthy [2021-10-25 11:08:27 INFO] Probe: all-services-are-healthy [2021-10-25 11:08:27 INFO] Probe: front-service-must-still-respond [2021-10-25 11:08:28 INFO] Steady state hypothesis is met! [2021-10-25 11:08:28 INFO] Let's rollback... [2021-10-25 11:08:28 INFO] No declared rollbacks, let's move on. [2021-10-25 11:08:28 INFO] Experiment ended with status: completed
Podを確認すると、backend Deploymentが削除されたため、frontendのみ動作している状態ですが、frontendがリクエストをクライアントに返しているため、サービスが提供され続けていることが分かります。
kubectl get pod NAME READY STATUS RESTARTS AGE front-55bd94d9c8-8dkjz 1/1 Running 0 3m38s
Webブラウザから次のURLにアクセスします。
http://<frontend External IP>/
backendアプリからデータは取得できないものの、frontendアプリがfallbackメソッドを呼び出し、frontendアプリ自身がデータを返していることが分かります。
このように、サーキットブレーカーを入れることにより一部のサービス障害を他に伝播させることなく、システムを閉塞させることができました。
クリーンアップ
検証が終わりクラスタが不要になったらクラスタのリソースグループを削除します。
RG_NAME=aks-sample az group delete --name $RG_NAME
AKSクラスタを稼働し続けると、課金が発生しますので注意してください。
まとめ
本記事では、Kubernetesクラスタで動くアプリケーションに対してカオス挿入と、障害でアプリケーションにアクセスできなくなったときに、サーキットブレーカーパターンを用いて閉塞させることで、システム全停止を防ぐにはどうすればよいかを説明しました。
クラウドのような分散システムでは個々のサービスが適切に機能している場合でも、それらのサービス間の相互作用によって予測不可能な障害を引き起こす可能性があり、これらを完全にゼロにするということは不可能です。そのため、システムのどこか障害が発生しているという前提に立ち、いかに早く障害を検知・復旧させるかに重点を置いて設計開発をすることが重要です。そして、正確に予測不能な障害による不安定な状況にも耐えられることができるという自信をつけるために、カオスエンジニアリングを検討するとよいでしょう。
