今回はどこを探してもあまり載っていない Azure Functions の Linux Consumption Plan のアーキテクチャについて、マネージャの許可を得て公開しようと思っています。
Azure Functions のバックエンドは、Inside the Azure App Service Architecture-日本語訳の情報がありますが、現在のFunctionsも似た感じだけど進化したアーキテクチャが使われています。
今回は、Linux Consumption Plan のアーキテクチャの概要を、クラウドを使う皆さんに有用になりそうな知識を共有するために、そのアーキテクチャ含めて説明してみたいと思います。
Linux Consumption Plan とは
使っただけ課金される、従量課金型でかつ、必要な分だけスケールするアーキテクチャを持つサービスです。2018/9/24 のプレビューを経て、2019/8/21 にGAされています。
Linux Consumption Plan の使いどころ
今後はわかりませんが、dotnet を使うときは Linux でも動作しますが、やはり Windows との相性がよさげでパフォーマンスも今のところは良好です。C# 以外の言語、Python, JavaScript, TypeScript, Javaなど、サポートされている言語だと、開発者の人も Macで開発環境をつくっている人も多いでしょうし、Winのエコシステムより、Linux が好みという人も多いでしょう。Linux だけで動くライブラリを使いたいかもしれません。
他にあまり知られていないことをいうと、コールドスタートの時間は、Win より、Linux Consumption の方が、dotnet を除いて一般的に速くなる傾向にあります。
他にも、Azure Arcが発表されて、Azure Functions も Azure だけでなく「どこでも動く」という流れが来ている気がします。そのためには、Azure Functions が依存しているストレージをどうするか?などの問題はあるにせよ、注目していい分野だと思っています。
スタンプ、ロケーション、クラスタ
Azure Functions のインフラは、スタンプという単位でデプロイされます。1つのスタンプに対して 1000台以上の Function App をクラウドを利用している人が作ることができます。ここで、重要なポイントが、ここでデプロイされているスタンプがみんな同じものとは限らないということです。新しいものもあれば、古いものもあります。また、AppService Plan, Premium Plan, Consumption Plan, Win/Linux でそれぞれ違っており、一般的にはロケーションごとに同じようなバージョンの Functions Host や、Extension Bundle 等がデプロイされますが、その基盤のスタンプのデプロイの周期はまた違っています。
なんでこちらの Function App だけ動かないの?
ですので、皆さんもたまにある機能が、あるリージョンで使えなかったのに、違うリージョンに変えたら動いたみたいなことはあるかもしれませんが、これはこの仕組み上当然のことです。また、古いスタンプと新しいスタンプが両立しているときに、新しいスタンプで作った Function App だけエラーが出るということも理屈上ありうると考えられるわけです。
お勧めしない実装
こういう細かい違いがあるので、「こういう実装はお勧めしない」というガイドがたまにあります。そういうガイドを守ることで、こういったインフラの違いに悩まされずにすみます。たとえば、DIを使って、 Azure Functions Host 側で定義されている、DIを書き換えるのは推奨されていません。Overriding host services 他の例としては、Resource Redirect のコールバックを自前で定義して、古いバージョンを無理くり動かすとかやっている場合に、ある特定の環境で起こる。よくわからないエラーに悩まされることになったりします。一般的にいって、Hackey な実装は怪しいと思ったほうがよいかもしれません。
Linux Consumption Plan のアーキテクチャ
Linux Consumption プランの面白いアーキテクチャについてご説明したいと思います。スタンプレベルの違いとしては、最初の図の通り「クラスタ」が存在し、そこに、コンテナが動作します。Linux Consumption 用のスタンプが、このクラスターのコンテナと連携することで、機能が実現されています。このアーキテクチャで面白く、かつ知っているとよいポイントとして、コールドスタート対策の機能であるプレースホルダーの機能を紹介しましょう。
プリキャッシュ
Linux Consumption プランでは、Cold Start 対策のために様々な工夫がなされています。コールドスタートは早ければ早いほどいいですが、普通に考えて、コンテナのイメージのサイズが大きいとそもそもダウンロードだけでも相当な時間がかかってしまいます。そこで、プリキャッシュと呼ばれる機構で、実際に使われるイメージのキャッシュは使われるより前に先に作成されています。
プレースホルダーモード
それだけではありません。コンテナの起動、そして、コンテナの中で使われている Azure Functions Host 、そして、Language Worker と呼ばれる各ランゲージ用のホストの起動はやはり一定の時間を必要としています。ですので、先にコンテナが起動されていて、中で起動されるホストも、ユーザが使う前に先に起動された状態で待機しています。ただし、ここでは、ユーザ固有の情報、例えば、読み込まれる Azure Functions のコードやバイナリ、AppSettings などは、まだここにはありません。
スペシャリゼーション
最初にリクエストが来た時点で、スペシャリゼーションというプロセスが走ります。この時点で、Run From Package により、Zip ファイルが、ブロブから読まれ展開、Azure Functions Host に登録されます。AppSettings も、ランゲージワーカー含めて、ユーザが設定されたものに設定されます。つまりここで、「ユーザの環境」がこのタイミングで出来上がります。ですので、この時点でホストを起動するよりずっと早くユーザが使える環境を作ることができます。
この挙動を覚えておくと、皆さんのプログラミングにもいろいろ役立つかもしれません。例えばある人は、Java で、JVMの起動時パラメータを AppSettings で渡そうとしていました。残念ながら動作しません。なぜなら、ユーザが Functions を使い始めた時点よりずっと早く、既に JVM が起動しているからです。
このタイミングでコンテナに魂が入って、特定のFunctionAppのバックエンドになるというイメージです。
AppSettingsの変更、終了処理
このコンテナも永久に生きるわけではありません。例えば AppSettings が変更されたとき、には、現在のコンテナが停止され、そのFunctionAppのバックエンドは別のコンテナになり、現在のものは停止・廃棄されます。一度スペシャリゼーションを受け付けたコンテナは2回スペシャリゼーションを受け付けません。このコンテナは他のものに再利用されません。
ここで先日であった面白い現象で、Azure の REST API にある、Delete Function が Linux Consumption でうまく動かないという問題がありました。Block Delete Function api in Linux Consumption
このようにFunctionAppのバックエンドのコンテナは永久に同じものとは限らないので、このAPIのように、コンテナの内部のディレクトリ構造に依存しているものはサポート外です。どうしてもしたい場合は方法はあります。このAPIのコードを読むと、コンパイルしたとにできる、ファンクションのメタデータが入ったディレクトリを単に削除していました。せっかく削除したところで、このアーキテクチャだと、コンテナは入れ替わることを前提に設計されているので、いつバックエンドでユーザが気づかないうちに新しいバージョンに変わっていくことはありうるわけで、そうなると、廃棄済みのコンテナのディスクの内容を書き換えても、新しいコンテナがそれを参照できるはずがないからです。
おわりに
自分的にとても興味深かった Linux Consumption のアーキテクチャも上記のようなことを抑えておくと、何かあったときのトラブルシューティングに役立つかもしれませんね!
是非皆さんもよかったら、Linux Consumption Plan を使ってみませんか?
オープンソースなので、こちらでコントリビューションも大歓迎です。