BEACHSIDE BLOG

Azure と GitHub と C# が好きなエンジニアの個人メモ ( ・ㅂ・)و ̑̑

Bot Channel のコールドスタート対策( Azure Bot Service )

Azure Bot Service では、WebChat とか外からの接続は Bot Channel 経由でなります。 WebChatとかだとここが Cold Start するので、たまーにアクセスすると初回のみ遅くて残念(一昔前よりはだいぶ早くなったけど!)。

ならば定期的にヘルスチェックして Cold Start を解決したいなーって試した内容のメモです。

余談ですが、こないだまで 「Bot Connector のコールドスタート対策」ってタイトルだった...自分で見返して「え?」ってなって修正しました(jsonのコードだけは修正してないけど、こんど気が向いて全部GitHub更新するタイミングで...)。

ASP.NET Core 2.2 のHealthCheck機能が気になる昨今でしたが、ここでは Bot Channel 経由にはならないので使えないですね。 いうても簡単な話なので独自に実装すれば良いだけです。

ただ、気になる点として、そもそも Azure 中で Bot Channel のインスタンスってどう管理されているのか知らんので、ヘルスチェックするだけで確実にコールドスタート問題が解決できるかはわかってないです。最近1ヶ月くらい運用してモニタリングしてる限りは解決できていますが。今月シアトルに行く用事があるので覚えてたら中の人に聞いてみます...覚えてたらブログも更新します。

Echo Bot の作成

去年のV4 アップデートでプロジェクトのテンプレートがわけわからんくなるので整理した方が良いって話はさておき、まずHealthCheck される側のボットを作ります。
Visual Studio 2017 で EchoBot を作りましょう。

f:id:beachside:20190308170206p:plain

特に必須ではないですが、2.2にあげたいところなので、Nuget のバージョン上げたりします。現時点ではまだ依存関係がドタバタしてるしすぐかわるだろうから手順は省略しますが、変更したらデバッグで正しく動くことを確認しておきます。

チャットボット側で HealthCheck を受け取るAPIを作る

単純にチャットボットへ適当なメッセージを投げるだけだとさすがにしょぼいで、ActivityType の Event を利用して処理してみます。いきなりプログラム的な話になりますが、具体的には以下の条件だったらヘルスチェックのリクエストと判断してレスポンスを返すって仕様です。

  • ITurnContextActivity.TypeEvent
  • ITurnContextActivity.Valuehealthcheck

作成したEchoBotのボットのメインのクラス(IBot インターフェースが実装されているクラス)の OnTurnAsync メソッドを変更します。変更箇所は12-17行目、31-40行目。

ざっくりな解説ですが、

  • 12行目turnContext.Activity.Type を判断して、14行目turnContext.Activity.Value の値をチェックしています。この値が healthcheck だったらヘルスチェックの処理をしますが、これは後述のヘルスチェックを送信するクライアント側と合わせる必要がありますので要注意です。
    余談ですが、ユーザーの入力は Activity.Text、Event のタイプの判別に Activity.Value と使い分けてるってのが私個人的な使い分けです。Activity.Valueは object 型なので、より複雑な値の格納可能です。

  • 31行目のメソッドでヘルスチェックのための適当な処理をして値を返すだけです。ガチでチェックするなら Bot で利用しているデータストアとか外部サービスとかのヘルスチェックもして結果を返すイメージでしょうかね。

  • 36行目は、レスポンスとして返すテキストをセットしています。ここでは適当に返しています。

  • 37行目は、ヘルスチェックのレスポンスだと判別するための値をセットしています。この値も後述のヘルスチェックを送信するクライアント側と合わせる必要がありますので要注意です。

Azure Bot Service の作成

Azure で Bot Service ( Web App Bot )を作成して、先ほど作成したボットをデプロイしておきましょう。今回の本題からそれるの、手順で割愛します。

ヘルスチェックを送信するクライアント作成

チャットボットへ定期的にヘルスチェックを実行するので、Azure Functions のタイマートリガーを作ってみます。
単純な定期実行なので Durable Functions の Monitor パターンを使うのもありかなーとか妄想しつつToo Matchかと思いやめました。

Visual Studio 2017で Azure Functions のプロジェクトを作ります。言語はもちろんC#、Functions は V2、 TimerTrigger を選びます。

f:id:beachside:20190228153601p:plain

Nuget のインストール

DirectLine SDK でサクッと Bot Channel に接続できますので、Nuget を Install します。
画面右上の クイック起動 から「nuget」と入力して、「....ソリューションの Nuget パッケージの管理..」を開きます。

f:id:beachside:20190228153609p:plain

以下2つをインストールします。

  • Microsoft.Bot.Connector.DirectLine : version は2019/2時点で最新の3.0.2
  • Microsoft.Rest.ClientRuntime : version は2019/2時点で最新 2.3.20

※ Microsoft.Rest.ClientRuntimeは本来必要ないかと思いますが、2019/2末時点で私がこれを試したときは、以前にもあった依存関係のバグが再発してる(??めんどいので調べてもいない...)。その回避のためにMicrosoft.Rest.ClientRuntimeもインストールしています。

ヘルスチェッカーの実装

とりあえずヘルスチェックする部分を雑に実装。

  • 28行目は、前述のチャットボット側の実装の14行目で設定した値「healthcheck」をセットします。(チャットボット側の実装では小文字で評価してるので大文字小文字は読みやすいよう適当にしてます。)
  • 29行目は、仕様通り Event をセットします。

ボットとのやり取りは、32-33行目です。普通のボットと違って(?) REST のレスポンスを待ってやり取りをしている点くらいです。 (普通とは?感がありますが、DirectLine SDK の仕様の話なのでここでは詳しく説明しません)

  • 35-36行目で、チャットボットのレスポンスの中で、ヘルスチェックのレスポンスを取得します。Activityの値から判断しています。

  • 38-46行目 でヘルスチェックの結果として必要なログをかくところですが、今回は適当にログを書いておしまいです。
    余談ですが、Bot がタイムアウトした場合、DilectLine SDK では 30秒おきに数回リトライしてくれます。


次に、タイマートリガーの Functions のエントリーポイントの実装をサンプルらしく手抜き実装。先ほど作ったヘルスチェッカーのクラスを実行しただけです。
22行目の RunOnStartup は、デバッグ用に true にしてますが、Azure にデプロイする際は false にしておく必要がありますね。

環境変数について

環境変数で以下3つを必要とします。

  • ConnectorSecretKey: Azure Portal で取得した Channel の接続に必要なシークレットキー
  • BotId: Azure にデプロイしたボットのID(handle?)
  • TimerSchedule: 起動タイミングを書く

Azure Functions には、この3つを Application settings に設定します(値の取得方法は後述します)。

また、Azure Functions のデバッグ用に local.settings.json に以下のように定義して値を入れます(今回追加したのは下3つ)。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "ConnectorSecretKey": "",
    "BotId": "",
    "TimerSchedule": ""
  }
}

3つの環境変数の値の取得方法の話に進みます。

ConnectorSecretKeyは、Bot Channel の接続するためのキーです。取得方法は、Azure Portal で、今回作成した Web App Bot を開き、Channels > Edit を開きます。(Azure Portal を日本語に戻さずスクショとっちゃったので英語のまま進めます。)

f:id:beachside:20190308175526p:plain

Secret Keys ひとつをShow して取得します。

f:id:beachside:20190308175738p:plain

次に BotId です。Azure Portal で、今回作成した Web App Bot でサイドメニューの Settings > Bot handle の値を取得しましょう。

f:id:beachside:20190308191306p:plain

TimerSchedule は、Azure Functions のタイマートリガーの起動タイミングです。CRON 式で書きます。詳しくは公式ドキュメントにて。

今回は、10分置きに起動なら 0 */10 * * * * ですね。

コールドスタートの検証

2月に5分間隔~30分間隔とかで数週間試しました結果をメモしておきます。ちなみに全てのリソースはWestUS2リージョンで検証しました。
まず、30分間隔だとレスポンスが7-10秒くらいになります。去年は20秒くらいだった気がするのでだいぶ早くなったけど、流石に辛い。

f:id:beachside:20190310213420p:plain

15分間隔だと、たまに3秒くらいかかるのか...。

f:id:beachside:20190309020535p:plain

長く運用してるとたまにAzure Monitor のアラートが上がって謎事象があったり...

f:id:beachside:20190314104854p:plain

10分間隔だと平均1.5秒くらい。ちなみに5分間隔でも似たような結果でした。今のとここの設定くらいがベターということか。

f:id:beachside:20190310112311p:plain

おわりに

数分おきではなく、数秒間隔でアクセスするとおおよそ1秒以下で返ってきます(そりゃそーだ)。

この検証をする前の私の予想は「特定の期間以上だと遅い、特定期間以内だと早い」という2択の単純な結果になると思っていたのですが、予想に反してコールドスタートの実装は複雑のようですね。どういう仕組みなのか気になる(調べないけど)。

あとは Azure Monitor でアラート設定してモニタリングしておく必要はありますね。 この調査してからブログ書くまでのタイミングが遅かったせいで結局一ヶ月くらいモニタリングしてしまったのですが、WebChat の Bot Channel の Cold start は(おおよそ?)改善してます。たまにあかんの?とか今後どうなるかはわからんので継続的にモニタリング必要ですが。

もうちょいちゃんとしたコードにしてから GitHub に公開しておこうと思ってはいます。