システムの複雑性と戦う方法

こんにちは。Zucksでエンジニアをやっています@karahiyo_nです。

先日社内向けに「Zucksで働き学んだ成果に繋がるプラクティス」という発表を行いました。今回はその一部を紹介したいと思います。

発表では6年間でシステム構成がどう変わってきたのかと実際にやってきたタスクを紹介しつつ

  • より妥当な意思決定をするために
  • より早く価値を提供できるように
  • システムの複雑性と戦う方法

などいくつかプラクティスを紹介しました。 今回はその中のひとつ「システムの複雑性と戦う方法」について書きたいと思います。

対象のシステム像

元の発表ではZucksのシステムを取り上げて解説したのですが、ここでは次のようなシステムをイメージしてください。

  • 非常に高いサービスレベルが求められるシステム(例えばAmazon Compute SLA相当)
  • 低レイテンシ、高トラフィック(で、さらに増加傾向)
  • 機能要望は尽きず毎時リリースされるシステム(同時にリリースが走っているのが日常)
  • 複数の外部サービス、クラウドベンダを使っておりリソースの数も多い(AWSやGCPなど。台数の上限緩和を3回くらいやった規模感)

生き物のように複雑かつすごい速度で変化し続けるシステムで、直接コントロールはできない外部環境によってその動きが変わるようなシステムです。

このようなシステムを開発、保守するには様々な万が一の問題、予期せぬ副作用の心配があります。 完璧だと思ったリリースでも思わぬバグがあったり、ユーザ数の増加などによって負荷に耐えられなくなったり、外部サービスやクラウドベンダ起因の問題などなど。

これら万が一の問題、予期せぬ副作用がある中で継続して開発を行いかつ安定して運用することがミッションです。

我々の仕事の難しさは、飛んでいる飛行機を飛ばしたまま改修するようなもの                              by さいないぷ

システムの複雑性と戦う方法

本番環境で安全にトライする

ローカル環境やステージング環境での動作確認だけでは改修内容の正しさを保証できない問題があります。 例えばロジックには問題なかったが本番環境の負荷に耐えられなかったり、権限不足で動かなかったり、結合テストでカバーしていない問題があったり、ライブラリのバージョンアップやデータベースのメンテナンスに伴う瞬断など影響範囲がわかりづらく広範囲に及ぶようなケースなどがあります。 このような事前のテストだけでは改修内容の正しさが保証できないような問題に対しては、本番環境で安全にトライすることを考えます。 完全なテスト・テスト環境を作るのはとても難しいので本番環境で確認するのが有効という考えです。

その際には安全に、サービスに影響しないようトライする方法を確認します。 サービスへの影響がありうるならなるべく影響を小さくしてトライすることを考えます。

サービスへの影響を小さくするために、例えばカナリアデプロイやブルーグリーンデプロイメントを行い全トラフィックの数%だけデプロイしたコードで処理します。 また影響の発生期間を短くするために、ロールバック方法を用意しロールバックにかかる時間を短くします。 とくに、穴となるのが"ロールバックが必要と判断するまでの時間"の長さです。サービスが意図通り動いてるのかどうかの監視とアラーティングも重要です。 もしも、やろうとしている変更がロールバックが難しいような場合は、そのデプロイ内容や方法自体から見直します。

サービスメトリクスを監視する

サービスが意図通り動いているのかどうかの監視とアラーティングが重要と書きましたが、それを実現するためにはどうすればいいか。 ひとつ効果的なのが、サービスメトリクスの監視です。

EC2インスタンスの起動失敗や、Lambdaの起動失敗、外部サービスとの通信に失敗などなど、これらは正常な動きではありませんが想定内の動きと考えます。その一つ一つの振る舞いの検知にのみ注力するのではなくサービスの状態、機能が動いた結果が正常なのかどうかを監視するべくサービスメトリクスの監視にも注力します。 EC2インスタンスの起動に失敗し続けていてサーバー負荷が上昇しその結果レスポンスが遅れていることを検知する、外部サービスとの通信に失敗し続けていてその結果、より最適なサービス提供ができていないことを検知する、なぜだかわからないが収益性が下がっていることを検知する、などなど。サービスに直結するものをみることで予期せぬ副作用や外部環境依存の何かしらの問題が発生していることに気づけるかもしれません。

難しい問題を簡単にする

ここでの難しい問題とは、人が十分に注意しなければいけないことが多い問題のことです。 人類には早すぎる問題に真っ正面から戦わずに、人間でも扱える程度の問題に落とし込めないかを考えます。

安全にトライするためには影響範囲を把握する必要があると先に書きましたが、もしその影響範囲の把握が難しいものになっているならば影響範囲を把握できるようにすることが最初の仕事かもしれません。例えば、誰がこのAPIサーバを使っているのかをロギングして確認する、SecurityGroupのallowルールを見直して実際に必要な最小限の権限にする、複数の機能が動いているサーバを機能ごとに別サーバで動かすよう分割するなどなど。

例外発生時に人が対応しなければいけないことを減らすよう自己復旧の仕組みを整える。データベースの瞬断時には自動で再接続をする、外部サービスへのリクエストの失敗は時間を置いて自動でリトライする。

正常に動くための条件がテストや何かしらの制約で守られておらず脆い。例えばcronスケジュールで繊細な依存解決をしている場合など。(〇〇までには15分も待てば十分でしょう、みたいな意思決定) テストや、コードで依存関係を明示的に書き正常に動くことを保証することを考えます。もしくはまずは監視の仕組みを用意してよくない状態の発生に気づけるようにすることを一手目として考えます。

再発防止する

残念ながら、万が一の問題や予期せぬ副作用が発生しないよう気をつけていても事は起きるときには起きます。 そのときにできることは起きた事から学び、次同じことが起きないように対策を打つことです。根本原因を理解し正しく機能する対策を打ちます。 チームで振り返り、障害内容の共有と今後の対応の確認を行います(ポストモーテム)。 弊チームではChaos Engineeringの一歩として避難訓練を行ったりしています。

まとめ

今回は先日社内向けに発表した「Zucksで働き学んだ成果に繋がるプラクティス」のひとつ"システムの複雑性と戦う方法"について書かせていただきました。 システムの複雑性とうまいこと戦って、毎日夜は安心して寝られることが一つ大切にしていることです。