[Azure + javascript]Functionsのhttp triggerな関数でSearchの検索結果を返す

暖簾に腕押し

私がAzure Functionsに提供してほしいと切に願っている機能の一つに、Search Bindings(仮称)があります。なお、私がこの機能実装のリクエストを出しました。

しかし、現時点ではこのような機能が提供される気配は全くなく、付帯するロジックを記述するしかありません。暖簾に腕押し、というやつです。

実際、このようなロジックを書くことすら億劫なのですが、嘆いていても仕方がないですので、javascriptからAzure Searchへ問い合わせを行うnpmライブラリazure-searchを利用し、レスポンスを返すところまで実装することにしました。

役割

  • クライアント : webAPIを利用し、HTTP Responseに含まれた検索結果を受け取ります。

  • Azure Functions : クライアントからHTTP Requestを受け取り、Azure Searchに問い合わせをし、結果をクライアントに返します。

  • Azure Search : クエリをFunctionsから受け取り、検索結果を返します。また、Indexerという機能を利用し、定期的にTable Storageからデータを同期します。

  • Table Storage : 検索対象となるデータがストックされています。Indexerによって、定期的にデータ参照されます。

データ構造

検索対象となるデータ構造は以下のようなものです。

[
  {
    "PartitionKey": "2018-08-29:myroom",
    "RowKey": "ecd53616-e756-41fb-98d2-fe2b387e0c8a",
    "id": "ecd53616-e756-41fb-98d2-fe2b387e0c8a",
    "channel": "myroom",
    "body": "5000兆円 欲しい!!!",
    "author": "ytnobody",
    "visible": true,
    "timestamp": 1535508299
  },
  ...
  ...
]

PartitionKeyおよびRowKeyはいずれもTable Storageで必須の項目です。(参照:Azure ストレージ テーブルの設計ガイド: スケーラブルな設計とハイパフォーマンスなテーブル

Search側のスキーマ構造

インデックスmessageには、Table Storageに格納されているデータ構造を、ほぼそのまま持ってきています。Table Storageで利用していたPartitionKeyおよびRowKeyはここでは使いません。

  • id Edm.String (key, retrievable, searchable)
  • channel Edm.String (retrievable, filterable)
  • body Edm.String (retrievable, searchable)
  • author Edm,String (retrievable, filterable)
  • visible Edm.Boolean (retrievable, filterable)
  • timestamp Edm.Int64 (retrievable, filterable, sortable)

普通に実装

最初、以下のように実装しました。

// function.json
{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ],
  "disabled": false
}
// index.js

module.exports = function (context, req) {
    // Searchクライアント初期化
    const AzureSearch = require("azure-search");
    const client = AzureSearch({
        url: process.env.SEARCH_URL, 
        key: process.env.SEARCH_KEY
    });

    // 検索ワードをスペースで切って配列にする
    const words = req.query.word ? req.query.word.split(' ') : [];

    // ページング指定
    const page = req.query.page ? parseInt(req.query.page) : 1;
    const top  = req.query.size ? parseInt(req.query.size) : 10;
    const skip = top * (page - 1);

    // 検索オプション
    const search = words.map(w => `body:${w}`).join(' AND ');
    const filter = 'visible eq 1';
    const searchOptions = {
        queryType:  "full",
        searchMode: "all",
        top:        top,
        skip:       skip,
        search:     search,
        filter:     filter
    };

    // 問い合わせ
    client.search('message', searchOptions, (err, results) => {
        context.res = err ? {
            status:  500,
            headers: {"Content-type": "application/json"},
            body:    {"message": `Internal Server Error: ${err}`}
        } :
        {
            status:  200,
            headers: {"Content-type": "application/json"},
            body:    results
        };
        context.done();
    });
};

察しの良い方なら気づいたかもしれませんが、このロジックは期待通りには動かず、502エラーを返してしまいます。

何がダメなのか

期待通りに動かない原因は、client.search(...)の結果を受け取る前にmodule.exports自体が処理を終えてしまうからです。

レスポンスらしいレスポンスを設定しないまま処理が終わってしまうので、502エラーを返す、ということです。

対応方法

結論から書くと、以下の2点を直すと良いです。

  1. module.exportsを、Promisereturnするように変更する。
  2. function.jsonにて、http output bindingsのname$returnにする。(portalの場合、応答パラメータ名のところにある「関数の戻り値を使用する」をチェックする)

なおしてみる

なおした後の実装がこちら。

// function.json
{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "$return"
    }
  ],
  "disabled": false
}
// index.js

module.exports = function (context, req) {
    // Searchクライアント
    const AzureSearch = require("azure-search");

    // 検索ワードをスペースで切って配列にする
    const words = req.query.word ? req.query.word.split(' ') : [];

    // ページング指定
    const page = req.query.page ? parseInt(req.query.page) : 1;
    const top  = req.query.size ? parseInt(req.query.size) : 10;
    const skip = top * (page - 1);

    // 検索オプション
    const search = words.map(w => `body:${w}`).join(' AND ');
    const filter = 'visible eq 1';
    const searchOptions = {
        queryType:  "full",
        searchMode: "all",
        top:        top,
        skip:       skip,
        search:     search,
        filter:     filter
    };

    // 問い合わせ
    const promise = Promise.resolve(AzureSearch({
        url: process.env.SEARCH_URL, 
        key: process.env.SEARCH_KEY
    }))
    .then(client => client.search('message', searchOptions))
    .then(results => {
        return {
            status:  200,
            headers: {"Content-type": "application/json"},
            body:    results
        };
    })
    .catch(err => {
        return {
            status:  500,
            headers: {"Content-type": "application/json"},
            body:    {"message": `Internal Server Error: ${err}`}
        };
    });

    return promise;
};

function.jsonでは、http output bindingsのname$returnとなっており、functionの戻り値をレスポンスに使う設定となっています。

そしてindex.jsではPromise.resolve(...).then(result => ...).catch(err => ...)の形式でSearchに問い合わせを行った後の処理をハンドリングするよう定義し、promiseそのものをreturnするロジックへと書き換えられました。

本当はドキュメントに書いておいて欲しかった、もしくは・・・

実はPromiseをreturnすることで解決できるという事について、公式ドキュメントには書かれていないようです(ソース。openになっているし、書こうとはしてる模様)。

今回の解決法は、上記issueを辿ってようやく見つけることができたものでした。

この手の手間をなくすためにも、Search Bindingsが欲しいな、と思うのでした。

まとめ

  • Search Bindings欲しいですね。

2018-08-30 追記

node-azure-searchでPromise/thenを使った書き方では、検索結果のヒット件数を取得することができないという問題がありました。

const AzureSearch = require('azure-search');
const word = '5000兆円';
AzureSearch(...)
    .then(client => search({
        queryType:  "full",
        searchMode: "all",
        top:        20,
        skip:       0,
        search:     `message:${word}`,
        filter:     'visible eq 1',
        orderby:    'timestamp desc',
        count:      true // <--- @odata.countをレスポンスに含めるための指定
    }))
    .then(rows => {
        // rowsは検索結果(オブジェクト)が入った配列。
        // ここで検索結果のヒット件数である@odata.countを利用したいができない!!
    })
    .catch(err => { ... });

これはnode-azure-searchのindex.jsを修正することで、取得できるようになります。(ただしインターフェイスを破壊する変更です)

@@ -492,7 +492,7 @@ module.exports = function (options) {
            return new Promise(function (resolve, reject) {
              args.push(function (err, value, data) {
                if (err) reject(err)
-               else resolve(value)
+               else resolve(data) // resolve(value) not contains '@odata.count'
              })
              fn.apply(self, args)
            })

利用する側は以下のようになります。

const AzureSearch = require('azure-search');
const word = '5000兆円';
AzureSearch(...)
    .then(client => search({
        queryType:  "full",
        searchMode: "all",
        top:        20,
        skip:       0,
        search:     `message:${word}`,
        filter:     'visible eq 1',
        orderby:    'timestamp desc',
        count:      true // <--- @odata.countをレスポンスに含めるための指定
    }))
    .then(result => {
        const count = result['@odata.count'];  // 検索結果のヒット件数。
        const rows  = result['value'];         // rowsは検索結果(オブジェクト)が入った配列。 
        ...
    })
    .catch(err => { ... });

破壊的な変更であるため、forkして利用しています。

書評 Azureテクノロジ入門2018

遅ればせながら、新年おめでとうございます。

さて、昨年末にAzureの風雲児であるところの@myfinder氏より献本いただきました。改めてお礼申し上げます。ありがとうございます。

本書について

2016年11月に出版されたAzureテクノロジ入門2016の改訂版となります。

もともとAzure関連の技術書は数少ないのですが、本書はその中でも入門者・初学者向けの位置付けとして発売されています。索引まで含めて233ページとなっており、定価は2500円と、技術書としてはかなり安価です。

著者陣はほぼ日本マイクロソフトの現任エバンジェリストやアーキテクトなどで占められており、マイクロソフトがいかにAzureというプロダクトを重要視しているかがわかります。

本書の目次

  1. Azureの基本と全体像
  2. AzureのインフラとIaaS ~ 仮想マシン、ストレージ、ネットワーク
  3. データベースと分析サービス
  4. 開発者のためのPaaS ~ Azure App ServicesとAzure Functions
  5. アイデンティティ管理と認証・認可
  6. 地上に広がるハイブリッドクラウド ~ Azure Stack

書評

表紙に「Azureを知るための最初の1冊!」「さらに進化したAzure全体像がよくわかる!」と書かれているのですが、まさにその通りの内容です。

そこそこAzureを使っている立場からすると、2章と5章は順序が逆でもいいかもしれないな、と感じました。AzureにおいてはそのくらいにID管理(Azure Active Directory 通称Azure AD, AAD)が肝となりますし、IaaSについてはAzureのPaaS/SaaSについて理解してからフォーカスするくらいでも遅くないと思います。

とはいえ、各章は初学者にもわかりやすいよう図解が多く用いられており、概念を理解する上で非常に心強い一助となることでしょう。すべて通読する必要はなく、手始めに利用する予定のあるサービスに応じて、必要な個所だけピックアップして読むのがおすすめです。ある程度ユースケースをカバーできたら、今度は通読してAzureの可能性をもっと広く学習することができます。

また、ある程度熟練した向きにとっても、次に利用する予定のAzureサービスについて手早く把握しておきたい場合に、辞書のように利用できることでしょう。というのも、本書では各章の要所要所にオンラインドキュメントのURLが記載されており、細かい部分についてはそちらで補完するような構成となっているためです。

あらゆることを正確に記憶しておける人間は稀です。本書は、Azureに関する知の高速道路を読者に提供してくれることでしょう。そして忘れかけたAzureの知見を改めて引き出す際にも十分に価値を発揮してくれると思います。

2017年のPaaS/SaaS周りを振り返って – フルマネージドDBサービス発展の年

2017年も終わりを迎えようとしておりますが、2017年はPaaS/SaaSにとって非常にインパクトの大きい一年でした。

このエントリでは、PaaS/SaaS利用者の観点から、2017年を振り返ってみようと思います。

なお、私は主にMicrosoft Azureを利用しているため、内容の濃さなどの面でどうしてもMicrosoft Azure寄りとなりますことをあらかじめご了承ください。

フルマネージドDBサービス発展の年

2017年は、フルマネージドDBサービスの利便性が大変向上した一年だったと思います。

特に「DBインスタンス」という考え方から脱却したサービスが登場し、本格的に利用された年だったと言えるのではないでしょうか。

アプリケーション開発者からすると、インスタンスのことを気にするのは本質的ではありません。そのような本質的ではないことについて一切触れる必要なく、DBをサービスとして利用することは、2017年で一気に身近になってきたことだと思います。

また、料金体系的にも「トランザクションごとに課金」というパターンが浸透してきたのも2017年の特徴ではないでしょうか。これによって、真に「利用した分だけ支払う」ことが現実のものとなったと思えます。

Azure Cosmos DB

スキーマレスなNoSQL DBとして利用でき、各リージョン間でデータレプリケーションが可能なDBaaSです。

もともとAzure Cosmos DBの源流として、Azure DocumentDBというサービスが2010年から提供されていたのですが、Build 2017にて名称をAzure Cosmos DBとし、以下のようなAPIへと対応しました。

料金体系についてはRU(Request Unit)と呼ばれる単位で計上されることになっており、インスタンスベースの課金体系ではないことが非常に重要です。

Azure Database for MySQL/PostgreSQL (プレビュー)

Azure SQL Databaseで培われてきたフルマネージドDBサービスの堅牢性とスケーラビリティが、MySQLおよびPostgreSQLのインターフェースで利用できるようになったものです。これもBuild 2017で発表されました。

従来であればMySQL/PostgreSQLともにVMを立ち上げ(あるいはオンプレミスのリソースを用意して)そこにインストールし、パフォーマンスモニタリングに気を使いながら、自力で運用するものでした。

このサービスは現時点ではまだプレビューですので、手放しで本番に投入して良いものかどうかは微妙だと思いますが、上記のような運用から解放される道筋が示されていることについて、将来十分に考慮に値することだと思います。

こちらの料金体系は、時間課金のコンピューティングユニットとデータ容量課金のストレージの合算値となり、やはりインスタンスベースの料金体系ではありません。

Amazon DynamoDB

Amazon DynamoDBは、フルマネージドDBサービスの中でも元祖と言えるでしょう。2012年にサービス提供が開始され、2013年に東京リージョンで利用可能となりました。

リージョン間でのレプリケーション(クロスリージョンレプリケーション)にも対応しており、課金体系もデータ量とスループットに基づいたものとなっています。

これらのことは、近代的なフルマネージドDBサービスの基礎となっており、Amazon DynamoDBがサービスを通して示したビジョンは大変重要なものと言えるでしょう。

フルマネージドDBサービスは開発者をより開発に集中させる

上記で紹介したサービスはいずれも、開発者からDBの管理という手間をなくそうという方向性のサービスです。自力でDBの管理を行う場合にかかるコストと、上記サービスを利用した場合のコストは、性質的に全く異なるものです。

また、自力でDBの管理を行う場合は、どうしても人間そのものがボトルネックになってしまいます(腰痛や風邪などの体調不良、冠婚葬祭に伴う休暇など)。速く移動するという本質で見た時に馬車から自動車へと変遷していった歴史があるように、DBの管理やスケーリングについてもより利便性の高い手段を検討していくことが肝要なのではないでしょうか。

そして、開発者がより開発に集中できるようにし、生産性の向上に繋げていくことが、最終的な競争力に寄与するのだろうと私は思います。

FunctionsのApp Serviceプランで快適な気になった話

※このエントリはMicrosoft Azure Advent Calendar 2017の3日目のエントリとなります。

個人的に運営しているサービスの運営にAzure Functions(従量課金プラン)を使っていたが・・・

@ytnobodyです。

私が個人で運営している「うまミる」(旧:南関テイオー)という競馬予想サービスでは、予想データを作成するロジックをAzure Functionsで実行しています。

そのうまミるですが、先月くらいに不可解な問題に直面しながらも、従量課金プランでだましだまし運用していたのですが、とうとう現象が毎日発生するようになってしまいました。

予想ロジックの複雑化に伴い、従量課金プランでは捌ききれなくなったのだろうと思い、思い切ってApp Serviceプラン(Basic S1)にデプロイしてたところ、予想以上に快適でした。

当たり前だけど速い

当然のことなのですが、従量課金プランと比較して段違いに速いです。料金的には、それまではだいたい月額1500円程度だったところが5400円ほどにアップしていますので、個人で支払うにはなかなかの額面になってしまったと感じています。

しかし、毎朝Functionsの状況を確認する必要性がなくなったことを考えると、コスパは予想以上に良いと感じました。

実際どのくらいのコンピューティングリソースを使っているのかを把握できる

PaaSを使うという観点では正直なところ、どれだけコンピューティングリソースを使っているのかという点について、個人的には全く気にしたくないところではあります。

しかし、安定稼働とコストの両立を考えると、どれくらいのコンピューティングリソースを使っているのかを把握しないと、ちょうどよさのあるプラン選定ができない、というのが現状です。

App Serviceプランにすると、Application Insightsを有効化するだけで簡単にリソース使用状況や関数の実行に関する統計(成功・失敗や関数の実行にかかった時間など)が可視化できます。これはちょうどよさのあるプランを選定するうえでありがたい話です。

そういえばこの原稿を書きながらApplication Insightsを見ていたら、妙に失敗率のたかい関数がなぜ失敗しているのかをようやく把握することができました(これについてはそのうち別途エントリを起こすと思います)。

まとめ

以前サポートを受けた際、「従量課金プランでは厳しいかもしれませんので、App Serviceプランもご検討ください」という助言をいただいていましたが、明らかに快適になりました。当然元々のペインポイントであった「毎日関数が起動できなくなってしまう」という問題も解決しています。

最後に、状況にもよるのでしょうが、Azure Functionsについては「従量課金プランで感覚をつかんだら、App Serviceプラン + Application Insightsも試してみてほしい。MSの本気はどうやらこっち側だ。」ということでまとめとさせていただきたいと思います。

Azure Functionsの関数実行時にエラーとなった場合のとりあえずの処置方法

今朝、南関テイオーの予想配信関数が実行失敗しており、その復旧作業を行いました。本エントリではその一部始終をまとめてみました。

原因を確認する

南関テイオーの予想配信関数は7:10(JST)に起動するようにTimer Triggerが設定されています。

Azure Functionsでは、関数ごとに「モニター」という項目があり、そこで実行ログを閲覧することができます。

まず実行ログになんらかの異常が吐き出されているだろうということで確認してみたところ、案の定、予想配信関数の実行時に、以下のようなエラーが出ていました。

Exception while executing function: Functions.getRace
Microsoft.Azure.WebJobs.Host.FunctionInvocationException : Exception while executing function: Functions.getRace ---> System.ApplicationException :       0 [unknown (0xFB4)] bash 4924 D:Program Files (x86)Gitbin..usrbinbash.exe: *** fatal error - Couldn't set directory to \?PIPE temporarily.
Cannot continue.

   at async Microsoft.Azure.WebJobs.Script.Description.ScriptFunctionInvoker.ExecuteScriptAsync(String path,String arguments,Object[] invocationParameters,FunctionInvocationContext context) at C:projectsazure-webjobs-sdk-scriptsrcWebJobs.ScriptDescriptionScriptScriptFunctionInvoker.cs : 114
   at async Microsoft.Azure.WebJobs.Script.Description.ScriptFunctionInvoker.InvokeCore(Object[] parameters,FunctionInvocationContext context) at C:projectsazure-webjobs-sdk-scriptsrcWebJobs.ScriptDescriptionScriptScriptFunctionInvoker.cs : 64
   at async Microsoft.Azure.WebJobs.Script.Description.FunctionInvokerBase.Invoke(Object[] parameters) at C:projectsazure-webjobs-sdk-scriptsrcWebJobs.ScriptDescriptionFunctionInvokerBase.cs : 95
   at async Microsoft.Azure.WebJobs.Host.Executors.VoidTaskMethodInvoker`2.InvokeAsync[TReflected,TReturnType](TReflected instance,Object[] arguments)
   at async Microsoft.Azure.WebJobs.Host.Executors.FunctionInvoker`2.InvokeAsync[TReflected,TReturnValue](Object instance,Object[] arguments)
   at async Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.InvokeAsync(IFunctionInvoker invoker,ParameterHelper parameterHelper,CancellationTokenSource timeoutTokenSource,CancellationTokenSource functionCancellationTokenSource,Boolean throwOnTimeout,TimeSpan timerInterval,IFunctionInstance instance)
   at async Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithWatchersAsync(IFunctionInstance instance,ParameterHelper parameterHelper,TraceWriter traceWriter,CancellationTokenSource functionCancellationTokenSource)
   at async Microsoft.Azure…

予測

ひとまずリトライを何度か試してみたところ、全て同じ現象が発生。これは以前サポートに相談した時と同様にリソース枯渇が原因なのではないか?と予想。

復旧手順

復旧までの手順は以下の通りとなり、今回は無事に復旧することができました。

  1. 新しいFunctions Appリソースを作成する(今回はリージョンの問題ではないので同一リージョンでOKだった)
  2. アプリケーション設定(環境変数)を新しいFunctions Appリソースにコピペする
  3. 従来のFunctions Appリソースを停止する(重複起動を防ぐ)
  4. 新しいFunctions App側でデプロイオプションを設定する(ここで関数のソースコードがデプロイされる)
  5. 関数を実行する

特に4の手順が実行できるよう、bitbucketなどを利用してgitレポジトリ連携をしておくことが、手早い復旧をする上で肝要と言えます。

まとめ

Functions App(特に従量課金プラン)においては、リソース枯渇による関数実行の失敗がありえます。これはクラウドサービスを活用する以上、付いて回る類の問題と言えるのではないでしょうか。

そのような問題を乗り越えるためには、最初から「壊れてもすぐ直せる」ようにシステムを作っておくことが大事だと考えます。

AzureのSupportが大変素晴らしいという話

山の日(8/11)、私がひっそりと進めている南関競馬予想「南関テイオー」が朝の予想バッチをサボタージュしてしまいました。南関テイオーの開発秘話についてはこちらに書いてありますので、興味のある方は是非読んでみてください。なお、南関テイオーは事業化できるように鋭意調整中です。

南関テイオーの大まかな構成

南関テイオーは、Azure(従量課金プラン)上に予想エンジンを構築してあり、予想結果と当選情報をBloggerに垂れ流すようにしてあります。最近まで一部、機械学習を用いた学習モデルを適用している箇所がありましたが、試行錯誤の末、一般的な数式に置き換えました(その方が回収率が高い)。

南関テイオーの大まかなシステム構成

forkに失敗する?

山の日に起こった現象は、「forkに失敗するせいで関数が起動できない」というものでした。以下、実際にAzure Portalで確認した内容です。

Azure Portalで確認

念のためAzureSupportに聞いてみる

あまりにもよくわからない現象でしたので、@AzureSupportに質問することにしました。

その後Direct Messageで詳細を聞かれましたが、Support Requestを送るように指示されました。

We recommend filing a support case via “Resource health” from the Azure Portal. After selecting the resource you are having issues with, navigate to “Resource health” by clicking the link under the “Support + Troubleshooting” section of the left blade. Select the option for “Contact Support” and follow the “New support request” workflow to submit your case. If you do not have a Support Plan enabled, be sure to select “Resource health” as the Support Plan.

オペレーション方法も教えてくれて、めっちゃ親切です。

で、この指示に従ってSupport Requestを送りました(念のため英語で・・・本当に拙い英語で・・・)。

すると10分後・・・

電話がかかってきた!

電話の相手は日本マイクロソフトのサポート担当の女性の方でした。英語ではなく日本語で対応してくれたことをありがたく思っていたところ、サポート曰く、

「事業影響度をAに設定したのはなぜですか?」

とのこと。事業影響度というのは、Support Requestのパラメータの一つであり、A,B,Cの3段階で1つ選択して設定することができるものなのですが、それぞれ以下のような定義となっています。

A. Critical impact – Significant loss or degradation of services (重大な影響 – サービスの大幅な損失または劣化)

B. Moderate impact – Moderate loss or degradation of services (中程度の影響 – 中程度の損失またはサービスの低下)

C. Minimal impact – Minimal loss or degradation of services (最小限の影響 – サービスの最小限の損失または劣化)

南関テイオーは当時も現在もサービスとして有償提供しているものではありません。しかし冒頭にも書いた通り、事業化を視野に入れていることも事実です。そこで私は先ほどの質問に対し、

「現時点では個人で稼働させているものであり、サービスインしているわけではないのですが、将来的にサービスインさせた時に、現在のこの状況はお客さんからクレームが来ることになりますので、Aとしました。」

と答えたところ、

「わかりました、それでは事業影響度をAとして対応を進めさせていただきます。後ほどエンジニアから連絡します。」

とのお返事をいただきました。すごい。ちゃんと話すことは重要ですね。

サポート部門のエンジニアさんからの連絡

数分後、サポートエンジニアのXさん(こちらは男性)からお電話がありました(祝日にもかかわらず対応ありがとうございました)。どこかで聞いたようなお名前の方でしたが、それはさておき、まずは調査してくださるということでした。

さらに数分後、Xさんから改めてお電話があり、関連しているかもしれない異変があることを説明してくれました。

Xさん「当該時間帯に、デプロイ先のインスタンスでメモリリソースの枯渇があったようです。現時点では解決しているので、関数を再実行できるようでしたらお試しいただけますでしょうか?」

私「ありがとうございます。試してみます。(インスタンスのメモリリソース枯渇って、PaaS利用者としては気にしたくないことだよなぁ・・・)」

Xさん「それから、現在Consumption Planで稼働しているようですが、関数のメモリ使用量が多いようですので、App Service Planでの稼働もご検討してみてください。」

私「わかりました、ありがとうございます。」

若干の違和感を感じながらも、ひとまず関数の再実行を試みたところ、正常に稼働しました。Azure Functionsでforkできない時には、メモリ不足を疑うと良いようですね。

まとめ

どうしてもわからない障害にぶち当たった時でも、24/365で対応してくれるAzureのサポート体制に大変感動しましたし、そして本当に助かりました。

Azure Web Appsのデプロイオプションと.sshの関係性

今日色々とAzure Web Appsで設定をしていて、デプロイオプション周りでハマりました。とはいえ、わかってしまえばどうと言うことのないことなので、簡単に説明していきます。

Azure Web Appsでデプロイオプションを変更すると、~/.sshが初期化される

見た通りのことなのですが、例えばn0bisuke氏が書いたこのエントリを参考にデプロイ設定をしたとします(なお、リンク先は本当に参考になるエントリです)。

で、一通り設定が終わった後に、Azure Portalから「デプロイオプション」> 「切断」を実行すると、 ~/.ssh の中身が空っぽになります。

この挙動に気がつかないと、改めて「デプロイオプション」を再設定しても「なぜかgit cloneに失敗する」という状態に陥ることになります。

まとめ

デプロイオプションを変更したら、 ~/.ssh の中身を確認しましょう。

Azure FunctionsでqueueTriggerのスロットリングをする

Azure Functionsを使った以下のようなサイト巡回の仕組みについて、是非とも気をつけたい箇所がありましたので、共有です。

このような特定のサイトを巡回する仕組みを作るとき、巡回先のサイトに対して秒間10リクエストくらいの比較的高密度なリクエストを送ることは、普通に考えると相手先のシステムにとって迷惑であろうと想像がつくことでしょう。

このような場合、リクエスト密度を軽減するために、並列リクエスト数の制限を行ったり、リクエストとリクエストの間にクールダウンを設けたりすることがあります。

クールダウンについては適宜sleepを入れるなどの対応をすることで、一応相手先に対するリクエスト密度の軽減が見込めることでしょう。しかし、並列リクエスト数の制限を行わないことには、ひっきりなしにリクエストを投げることになりかねません。

Queue Triggerにおけるジョブの並列処理数調整

今回例に挙げた構成の場合、巡回対象URLが多くなってくるとQueue Storageに一気にジョブが登録され、それらが片っ端から一気にQueue Triggerによって処理されてしまうことが問題となります。

上記の図にもある通り、Azure FunctionsにおけるQueue Triggerの並列処理数は、デフォルトでは16となっています。

この並列処理数を下げるためには、プロジェクト直下に以下のような内容でhost.jsonを作成し、デプロイする必要があります。

{
    "queues": {
        "batchSize": 3
    }
}

このqueues.batchSizeこそが、Queue Triggerの並列処理数設定となっています。

このように、相手先のシステムに過剰な負担を与えないようにすることができます。

まとめ

web巡回をAzure Functionsでシステム化しようとしている方は、是非ともQueue Triggerの並列処理数にも気をつけてください。

なお、host.jsonに関わる情報はgithubのazure-webjobs-sdk-scriptプロジェクトしか情報源を見つけることができませんでしたが、host.jsonをしっかり確認することで、Azure Functionsのチューニングが可能とも言えますので、要チェックです。

Azure Functions CosmosDB input bindingsを使う上でのコツ – バインド変数編

さて、今度はバインド変数に関する躓きを共有します。

クエリエクスプローラとFunctions経由との間でクエリの結果が異なる???

例えば以下のようなクエリをCosmosDBに投げるとします。

SELECT c.id FROM c WHERE CEILING(c.age / 10) * 10 = 30

クエリエクスプローラでは以下のような結果が得られました。

[
    {
        "id": "user-123"
    },
    {
        "id": "user-124"
    },
    {
        "id": "user-133"
    }
]

今度はクエリの30に当たる箇所を{generation}として、sqlQueryに設定します。

SELECT c.id FROM c WHERE CEILING(c.age / 10) * 10 = {generation}

こんな感じですね。そしてFunctions側でQueue Storageトリガーを使って以下のようなジョブを受け取ります。

{"generation": 30}

この時、Functionsで受け取れるCosmosDB inout bindingsの結果は以下のようなものとなります。

[]

すっからかんですね。。。一体どういうことでしょう。

理由

初めて知った時には非常に驚きましたが、どうやら各bindingsにわたってくる値は文字列としてキャストされてしまうっぽいということです。

本当は以下のように展開してほしいのですが。。。

SELECT c.id FROM c WHERE CEILING(c.age / 10) * 10 = 30

こんな感じになってしまうということです。

SELECT c.id FROM c WHERE CEILING(c.age / 10) * 10 = "30"

これは厄介ですね。。。。

対処

これを回避するには、クエリの方で強制的に数値に置き換えてやればよいのですが、CosmosDBのビルトイン関数には数値へのキャストをしてくれるものがありません。

そこで登場するのがユーザ定義関数(User Defined Function/UDF)です。

CosmosDBでは、javascriptを使ってユーザが関数を定義することができるのです。

CosmosDBのブレードにあるスクリプトエクスプローラというメニューから、UDFの定義ができます。

UDF開発画面

上記画像のような関数を定義してやり、sqlQuery側を以下のように変更すると、無事に期待した結果が得られます。

SELECT c.id FROM c WHERE CEILING(c.age / 10) * 10 = udf.ConvertToNumber({generation})

このほか、時刻にまつわる関数などもCosmosDBにはビルトインでは存在しませんが、このようなUDFを作って対応することが可能です。

CosmosDBのUDFは非常に強力な機能ですので、是非とも有効に活用していきたいものですね。

元ネタ:https://stackoverflow.com/questions/44916811/difference-among-the-azure-cosmosdb-query-explorers-results-and-the-azure-funct/44918767#44918767

Azure Functions CosmosDB input bindingsを使う上でのコツ – 余剰演算子編

先日あたりからAzure FunctionsのCosmosDB input bindingsを使っていて、いくつかはまりどころのような挙動に出くわしましたので、その説明と対処法を紹介していきます。

sqlQueryで剰余演算子(Modulation Symbol)がそのまま使えない

例えば以下のようなQueryをCosmosDBに投げるとします。

SELECT c.id FROM c WHERE c.level % 20

このクエリを直接クエリエクスプローラで実行する場合、期待する結果が得られます。

ところがこのクエリをAzure FunctionsのCosmosDB input bindingsにsqlQueryとして指定した場合、以下のようなエラーがでてしまい、期待した動作をしません。

2017-07-05T16:45:43.113 Exception while executing function: Functions.MyFunction. Microsoft.Azure.WebJobs.Host: The '%' at position 34 does not have a closing '%'.

理由

実はsqlQueryの仕様として、%というのはアプリケーション設定の値を参照するために使う記号でして、%myappSetting%のような感じで使うものだったんですね。

そのため、単独で%が登場すること自体があってはならない、と、そういうことが理由となっています。

対処

対処法の一つとして、アプリケーション設定値に%を設定して、それを使えばよい、というものがあります。

アプリケーション設定から値を設定する

上記画像のように、例えばmodulationsymbolというアプリケーション設定値を定義しておき、sqlQuery側では以下のように%%modulationsymbol%に置き換えることで、期待通りの結果を得ることができます。

SELECT c.id FROM c WHERE c.level %modulationsymbol% 20

あまりカッコよくはないですが、致し方がない感じはしますね。

元ネタ:https://stackoverflow.com/questions/44903632/sqlquery-of-documentdb-input-bindings-with-modulation-symbol-makes-functions-fa