Statusを使用した機能の グレイスフルデグラデーション

以前の記事で、 Statusのライブラリを使用した、アプリケーションの堅牢なヘルスチェックを行う方法を説明しました。本記事では、

  • アプリケーションから主要でない機能を削除
  • データセンターのロードバランサから一件のアプリケーションのインスタンスを削除
  • DNSレベルでローテーションから全データセンターを削除

と言った作業を行ったことで停止(outage)した際に、アプリケーションを確認しデグレデーションを行う方法を書いていきたいと思います。

アプリケーションの動作状態の確認

Status ライブラリを使用すると、単一の依存性チェックと、システム全体の評価という、二種類のチェックをシステムに対して行うことが可能です。依存性というのは、システムが機能するために必要なシステムやサービスを指します。

単一の依存性チェックの間、DependencyManager は、依存性のIDを取得し、CheckResultに返す評価方法を使用します。

CheckResultは以下を含みます。

  • 依存関係の状態
  • 依存関係に関する一部の基本的な情報
  • 依存関係の状態を評価するのにかかった時間

CheckResultはJavaのenum(列挙型)で、OK, MINOR, MAJOR, OUTAGE などの一つです。OUTAGE のステータスは依存関係が使用できないことを示します。

final CheckResult checkResult = dependencyManager.evaluate("dependencyId");
final CheckStatus status = checkResult.getStatus();

アプリケーションの状態を評価する二つ目のアプローチには、システム全体を見るやり方があります。これは、高いレベルで全体のシステムがどう処理しているかという全体図を見せてくれます。一つのシステムが OUTAGE の状態にある場合、これはあるアプリケーションのインスタンスが使用不可能であることを示します。

final CheckResultSet checkResultSet = dependencyManager.evaluate();
final CheckStatus systemStatus = checkResultSet.getSystemStatus();

システムが正常でない場合には、システムに送ったリクエストを短絡させ、HTTP ステータスコード500 (“Internal Server Error”) を返すのが、多くの場合、最良とされます。下記の例では、Springでインターセプターを使用し、リクエストをキャプチャし、システムの状態を評価し、アプリケーションが停止している場合にはエラーの応答をします。

public class SystemHealthInterceptor extends HandlerInterceptorAdapter {
    private final DependencyManager dependencyManager;

    @Override
    public boolean preHandle(
            final HttpServletRequest request,
            final HttpServletResponse response,
            final Object handler
    ) throws Exception {
        final CheckResultSet checkResultSet = dependencyManager.evaluate();
        final CheckStatus systemStatus = checkResultSet.getSystemStatus();
        
        switch (systemStatus) {
            case OUTAGE:
                response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
                return false;
            default:
                break;
        }

        return true;
    }
}

依存関係の状態を比較

CheckResultSetCheckResult は、それぞれ現在のシステムの動作状態や、依存関係の状態を返す方法を持っています。CheckStatus を手に入れさえすれば、結果の比較ができる方法もいくつか出てきます。

isBetterThan() は、現在の状態が与えられた状態よりも良いかどうか判断します。これは他を含まない排他的な比較です。

CheckStatus.OK.isBetterThan(CheckStatus.OK)              // evaluates to false
CheckStatus.OK.isBetterThan(/* any other CheckStatus */) // evaluates to true

isWorseThan() は、現在の状態が与えられた状態よりも悪いかどうか判断します。この操作も、他を含まない排他的な比較になります。

CheckStatus.OUTAGE.isWorseThan(CheckStatus.OUTAGE)          // evaluates to false
CheckStatus.OUTAGE.isWorseThan(/* any other CheckStatus */) // evaluates to true

isBetterThan() と isWorseThan() の方法は、評価した依存関係が望ましい状態にあるかを確認できる、優れたツールです。しかし、残念ながらこれらの方法はグレイスフルデグレデーションを行うには、十分なコントロールが利きません。システムは、正常か、停止してしまっているか、のどちらかしかないのです。システムのグレイスフルデグレデーションを、さらにコントロールするために、新たに別の二つの方法が必要となりました。

noBetterThan() は、二つの状態のうち、正常でない方を返します。

CheckStatus.MINOR.noBetterThan(CheckStatus.MAJOR) // returns CheckStatus.MAJOR
CheckStatus.MINOR.noBetterThan(CheckStatus.OK)    // returns CheckStatus.MINOR

noWorseThan() は二つの状態のうち、正常な方を返します。

CheckStatus.MINOR.noWorseThan(CheckStatus.MAJOR) // returns CheckStatus.MINOR
CheckStatus.MINOR.noWorseThan(CheckStatus.OK)    // returns CheckStatus.OK

完全にシステム評価をする間、私たちはこれらの方法の組み合わせと、Urgency#downgradeWith() という方法を使用して、アプリケーションの動作状態のグレイスフルデグレデーションを行います。

停止した状態を調査できる機能を持つことで、依存関係の状態に基づいて、エンジニアは機能を表示するかどうかを動的に切り替えることが可能です。仮に、企業情報を提供する私たちのサービスがデータベースに到達できなかったとします。このサービスのヘルスチェックの状態はMAJOROUTAGE に変更します。私たちの求人検索のプロダクトは検索結果ページの右列にある、企業ページのウィジェットを外します。求職者と企業を結ぶ、核の部分となる機能には影響しません。

正常

正常でない(グレイスフルデグレデーション後)

サービスの動作状態に基づいて機能をコントロールできることだけがStatusの全てではありません。私たちは、フロントエンドのWebアプリケーションのインスタンスへのアクセスをコントロールするのにもStatusを使用しています。インスタンスがリクエストを処理できない場合、再び正常化するまで、ロードバランサからインスタンスを削除します。

インスタンスレベルでのフェイルオーバー

一般的に、本番環境にあるアプリケーションの複数のインスタンスを実行するのが広く推奨されています。これは、万一アプリケーション内の一件のインスタンスが停止した場合にも、リクエストを処理し続けるのを可能にすることで、システムを障害から復旧しやすくします。アプリケーションのこれらインスタンスは一つのマシン内にも、複数のマシンにも、そして複数のデータセンターにも、存在することが可能です。

Status ライブラリは、インスタンスが正常でなくなった際に、それを削除するようにロードバランサを設定することができます。下記の、一つのデータセンター内を表した基本例をご覧下さい。

  一つのデータセンター内のアプリケーションが全て正常に動作している場合、ロードバランサは、リクエストを均等に分散する。アプリケーションが正常な状態かどうかを判断するために、ロードバランサはリクエストをヘルスチェックのエンドポイントに送り、応答するコードを評価する。
インスタンスが正常でなくなった際には、
ヘルスチェックのエンドポイントは200番台以外のHTTPステータスコードを返し、トラフィックに応答しないように指示する。その後ロードバランサは正常でないインスタンスをローテーションから削除し、リクエストの受信を防ぐ。
インスタンス1がローテーションから削除され、データセンター内の他のインスタンスがインスタンス1のトラフィックに応答を開始する。各データセンター内では、複数のインスタスが停止した場合にも、トラフィックを処理できるように十分なインスタンスを設定している。

データセンターレベルでのフェイルオーバー

リクエストがデータセンターにすら送信される前、私たちのドメイン(例:www.indeed.com )はDNSを使用し、IPアドレスを解決します。私たちは、データセンター間で、トラフィックを地理的に分散する、グローバルサーバー・ロードバランサ(GSLB)を使用しています。GSLBがドメインを最も近くで利用可能なデータセンターのIPアドレスに解決し、その後、データセンターのロードバランサは上記で説明したように、トラフィックを転送し、フェイルオーバーさせます。

あるデータセンター全体がリクエストを処理できなくなったら、どうすればいいでしょうか?一件のインスタンスの場合のアプローチと同様に、GSLBは常に各データセンターの状態を(同じヘルスチェックエンドポイントを使用し)確認しています。GSLBが一つのデータセンターがリクエストを処理してないことを検知した場合、別のデータセンターにリクエストをフェイルオーバーさせ、ローテーションから正常でないデータセンターを削除します。繰り返しますが、こうして、停止状態の間ですら、リクエストの処理を確かにすることで、サイトを利用可能の状態に維持できるのです。

一つのデータセンターが正常でいてくれる限り、サイトはリクエストを処理し続けることが出来ます。正常でなくなったデータセンターに当たるユーザーには、この状態は、単にページの読み込みが遅くなったようにしか見えません。理想的でありませんが、処理されないリクエストにくらべれば、遅くてもエクスペリエンスを提供できる方がましでしょう。

最後の想定シナリオは、完全にシステムが停止した場合です。これは全データセンターが正常でなくなり、リクエストを処理できなくなった場合です。エンジニアというのは、こうした最悪の状況は回避しようとします。

Indeedが、完全なシステム停止に陥った場合には、私たちは各データセンターと各インスタンスにトラフィックを転送します。このポリシーは、オープンフェイルとして知られ、システムのグレイスフルデグレデーションを可能にします。各インスタンスは正常でない状態を通知してくるかもしれませんが、アプリケーションが何らかの形で作動する可能性があります。私たちは、どうにか何かが動いてくれる方が、全く動かなくなるよりも、ましだと考えるからです。

Indeedにもあなたにも役立つStatus

Statusライブラリは、Indeedで開発し、実行するシステムにとって、不可欠な部分です。私たちは以下の事にStatusを使用しています。

  • 迅速なアプリケーション・インスタンスとデータセンターのフェイルオーバー
  • コードが、トラフィックの大きなデータセンターに到達する前にデプロイの失敗を検知
  • 失敗するとわかっている作業をするのではなく、リクエストを素早くfailさせることで、アプリケーションの速さを維持
  • アプリケーションのサービスリクエストが、正常なインスタンスだけになっていることを確認し、サイトが利用可能であるように維持

Statusを始めるには、こちらのクイックスタートガイドを読み、サンプルを見てみてください。ご質問、ご不明な点は、Indeed GitHub Twitter までご連絡ください。

Indeed の新しいエンジニアリング・ マネージャーになったって? -まずは、コードを書いてもらおうか。

2016年の三月、ソフトウェアエンジニアのための「中途採用」のマネージャーとして、私は Indeed に入社しました。Indeed のエンジニアリング・マネージャーは、管理職としてのマネージャー業務に就く前に一般社員( individual contributor 以下本文では IC )として業務を始めます。私も、IC としてチームと仕事をすることで、より効率よくマネジメントをする準備ができました。

入社前には、どんな心構えをしておくべきかを数人のエンジニアリング・マネージャーと話しました。彼らは私に、入社後三ヶ月から六ヶ月間は、一般社員としての業務に就くとよいとアドバイスをくれました。それは、ユニットテストやコードを書き、変更の実行をし、コードレビューを行い、バグを修正し、ドキュメンテーションを書く、等を行う、ということでした。

このアプローチを聞いて、私はわくわくしました。なぜならエンジニアリング・マネージャーとして働いていたここ数年は、コードを書くなどの業務に残念ながらも貢献する機会がなかったからです。代わりに、私はコードレビューを行い、テクニカルデザインのレビューに参加し、チームの生産性を上げる機能やツールを作ることで、生き延びていました。

Indeed のエンジニア組織で、 IC として新しいマネージャーが就業を開始する際には、四半期の間、複数の異なるチームに参加するか、一つのチームに加わります。私の場合は後者で、収益の管理について取り組むチームに参加しました。

IC としての新入社員研修

私のマネージャーは、入社研修や、社内の wiki を使った自習スタイルの学習を行えるよう手助けしてくれました。新しい社員が、 Indeed で使用されるツールや技術について慣れ親しむために提供されたコンテンツ量に、私は感心しました。私の経験からですが、多くの企業は、便利なドキュメンテーションの作成や維持に対して投資していません。同僚のインディーディアン(Indeedian。社員のことをこう呼びます)[YI1] 達が快く質問に答えてくれたり、技術的な障壁にぶつかった際には助けてくれたりしたのも、同じくらい有益でした。新入社員にとって、こうした助けは本当にありがたいものでした。

そして、IC でいる間管理業務に関する責任はありませんでした。これも私には新鮮で… そして素晴らしかった!コードを書くのに集中できたからです。技術的な能力も積み上げられたし、しばらく使う必要のなかった、なまっていた思考プロセスの勘を取り戻すことができました。チームで使用されている手法やプロセスを観察し、どうやったら自分も同じように生産的になれるかを学びました。Git の使用についても、さらに深く学ぶ機会がありました。ユニットテストや DAO テストを書き、コードの範囲も増やしました。久々に、プロダクトの中の新機能のために、本番へ行くコードを書きました。

自分のチームが持つ 20 件の異なるプロジェクトにもっと早く自分を慣らすために、全てのコードレビューに自分を入れてもらうように頼みました。全てのプロジェクトには貢献できないとわかっていましたが、できる限り多くに触れたかったのです。私がこのお願いをする前には、開発者は通常数人をコードレビューに選んで、メインのレビュー担当者を一人選出していました。私は、全てのレビューに入れてもらっていたので、コードの変更とコードの改善方法を書いたチームメンバーのコメントを見ていました。コードレビューに書かれていたこと全てを理解したとは言いませんが、変更の種類への理解が深まりました。このアプローチは、マネージャーだけでなく、チームの新しいメンバー皆におすすめです。

他のアクティビティでも、自分のチーム以外の人と交流しやすくなりました。例えば、私は自分と面接をした人全員とランチ・ミーティングをいれました。これは、だいたい他のエンジニアリング・マネージャーとでしたが、プログラムマネージメントやテクニカルライティングのメンバーにも会いました。これらのランチ・ミーティングは、異なる職務の雰囲気をつかむことができました。各々がどう仕事を計画して優先順位を作っているかや、IC からマネージャーへ移行することへのそれぞれの考えなどを話しました。オンサイトで昼食(ところで、これが美味しいんです)を取ることで、エンジニアリング・チームの先輩たちや、他の部署の人とも出会うことができました。

マネージメント業務への移行

最初の四半期の終わりにさしかかる頃には、いくつかのプロジェクトに関わっていました。私のチームが所有する、重要なシステムのいくつかにも馴染んだところで、この頃、私は自分のマネージャーと管理業務への移行について相談しました。足場となっていく、しっかりした基礎を作れたね、と意見が一致したので、一対一の個人面談、四半期の評価、キャリア育成の相談を引き継ぎました。

技術にフォーカスし続けること

管理職を選ぶソフトウェアエンジニアの多くは、コードを書くのを諦めなければ、という考えに悩みます。しかし、リーダーシップを執る役職で、より重要なことは技術レベルでのチームへの参加です。この参加は、様々な形で行えます。Indeed のエンジニアリング・マネージャーは、抽象的なスキルと技術的な決定について、チームを指導します。マネージャー陣が、さらに深い技術への造詣を持っていれれば、彼らの役目もより効果的になります。

私も、ICとして働き始められたことで、チームの信用と尊敬を得られたのも嬉しいです。特に、Money チームのアクバー、ベン、チェン、エリカ、ケヴィン、リー、そしてリチャードには、感謝を述べたいと思います。

Python を使用した ユーザー動向の異常検出

anomaly_detection_banner_cropped

毎月 2 億人のユニークビジターの職探しをお手伝いする中で、私たちは、沢山のデータを手にします。集めたデータからユーザーの動向について多くの情報を得ることができますが、殆どの場合、この動向には予測可能なパターンが見受けられます。しかし、予期せぬ変化はシステムの不具合の兆候や、ユーザーの動向が実際に変化してきている事を表している可能性があります。データが何か変わったことを示していたら、私たちはその原因の解明に努めます。

そんな、ユーザー動向の異常を発見し対応する、というのは入り組んだ問題です。これらの異常が検知しやすくなるように、私たちは複数のオープンソース・ソフトウェアを活用しています。その中で最も重要なものは、 Twitter のAnomalyDetection ライブラリです。

ユーザー動向の異常を観察

大量のデータの中で異常を検知するというのは、時系列に沿って一定の予測可能なパターンがみられるデータの場合には、簡単な話です。図1の例のように、掲載された求人情報のPV 件数が一定の範囲に収まっていたならば、外れ値を見つけるのは簡単です。

anomaly_1

図 1  単一の外れ値

集めるデータのほとんどはユーザー動向によって決定されますが、そのデータのパターンは私たちが簡単に観測できるようなものではありません。様々な要因がユーザー動向に影響を与えているのです。例えば、以下に示す各要因は、ユーザーの所在地によっては、私たちが「通常」範囲と考える PV 件数に影響する可能性があります。

  • 何曜日か
  • 何時か
  • 祝日かどうか

私たちは、いくつかの要因に関しては事前に理解できているかもしれません。または、データ分析後に理解するかもしれません。もしくはよく理解できないまま終わるものもあるでしょう。

私たちの異常検知アルゴリズムは可能な限り多くの変化を把握しなければいけませんが、統計的に有意な外れ値を検出できるくらい、正確であるべきなのです。単純に、「月曜の朝のトラフィックはだいたい他よりも高め」と言うのでは曖昧すぎます。どれくらい他より高いのでしょうか。そして、どれくらいの時間それが続くのでしょうか。

図 2 は、予想されるデータの変動範囲を示し、図 3 は実際のデータの範囲を示しています。実際のデータ内の異常値は、すぐには目に見えません。

anomaly_2

図 2 予想されるデータ

anomaly_3

図 3 実際のデータ

図 4 は、実際のデータと予想データを重ねて示したものです。図 5 は、二つの時系列データの差異です。こうやってデータを見ることで、この一連のデータの最後のデータ点が、著しく異なっていることが目立ってくるのです。

anomaly_4

図 4 実際のデータと予想データ

anomaly_5

図 5  実際のデータと予想データの差異

こうした異常値を素早く識別し報告できるような洗練された方法を、私たちは必要としていました。その方法は、未来のデータを予測するために、既存のデータを分析できるものである必要がありました。そして、対応しなければいけない異常を見落とさないように、正確である必要もありました。

ステップ  1 :統計的な問題を解決

難しい数学的な部分は、実際には簡単でした。なぜなら Twitter が既に問題を解いてくれており、彼らの AnomalyDetection library をオープンソース化してくれていたからです。

以下はプロジェクトの説明からの引用です。

「AnomalyDetection は異常検出のためのオープンソース R パッケージです。このパッケージは、統計的な観点から見て、季節性やトレンドなどが存在するデータに対しても頑健性があります。AnomalyDetection のパッケージは様々な種類の文脈で使用が可能です。例えば、新しいソフトウェアをリリースした後のシステム指標や、A/B テスト後のユーザーエンゲージメントにおける異常値の検出の他、計量経済学、金融工学、政治学、社会学などの分野の問題に利用できます。」

このライブラリを作成するために Twitter は、Grubbs 検定としても知られている、ESD( extreme studentized deviateの頭文字)検定をベースにして、改良を加え、ユーザー動向のデータを処理できるようにしたのです。本来は、このテストは外れ値を識別するために、データセットの平均値を使用していました。Twitter の開発者達は、中央値を使用する方が、Webで使用する場合にはより正確だと気づいたのです。なぜならユーザー動向は、時間の流れと共に変動する可能性があるからです。

結果的に、トレンドが変動するような時系列データにおける異常値を、素早く検出することのできるツールを作ることができました。Twitter のデータサイエンティスト達は、このデータを使用して、内部分析とリポートを行っています。例えば、毎秒のツイートと内部サーバーの CPU 使用量について、彼らはリポートするのです。

Twitter のライブラリは、私たちが、過去のデータを使用し、様々な非常に複雑なユーザー動向を推測し、素早く異常な動向を見つけ出せるようにしてくれました。ただ、そこには一つ問題がありました。Twitter は R でライブラリを作成していたのですが、私たちの内部のアラートシステムは、Python で実装されているのです。

私たちは、Twitter のライブラリが自分たちのコードで直接動作するように Python  に移植することを決めました。

ステップ  2 :ライブラリの移植

もちろん、一つのプログラミング言語から、別言語へコードを移植することは、常にリファクタリングと問題の解決を伴います。AnomalyDetection ライブラリを移植する作業の殆どで、R と Python で提供されている数学関数の違いに対して取り組みました。

Twitter のコードは Python では元々対応していない複数の数学関数に依存しています。

最も重要なものはSTL(Seasonal and trend decomposition using loess の頭文字。)と呼ばれるアルゴリズムで、これは時系列データを季節変動、長期変動(トレンド)、不規則変動に分解します。

私たちは、オープンソースの pyloess ライブラリ からSTL を組み込むことができました。Twitter のコードに使用されている数学関数の多くは、 numpyscipy といったライブラリに用意されていました。このおかげで、未対応の数学関数はわずかになったため、R で書かれたコードを読んで、Python でその機能を再現することで、直接私たちのライブラリに移植しました。

オープンソースのコミュニティの仲間達が貢献してくれた、優秀な仕事を活用することで、コードの移植に必要とされる労力をぐっと抑えることができました。pyloess や numpy、scipy などのライブラリを使用して、Twitter で使用している R の数学関数を再現することで、この殆どの作業を、一人の開発者が約一週間で終えることができました。

Python 用 AnomalyDetection
オープンソース化

私たちがオープンソースのコミュニティに参加するのは、エンジニアとして、第三者の仕事から学び応用していく価値を理解しているからです。なので、 Python 用 AnomalyDetection をオープンソースとして公開できて本当に嬉しいです。是非ダウンロードして、試していただければと思います。ご質問、ご不明な点は GitHubTwitter 上にてご連絡ください。