このポストは、2017 年 9 月 19 日に投稿された Processing 100,000 Events Per Second on Azure Functions の翻訳です。

しばしば、 Azure Functions の従量課金プランのスケーラビリティ/スループットの限界についてお客様からご質問をいただきます。簡易な答えは常に「それは場合によって異なりますが、ワークロードはどの程度ですか ?」です。 本日は、 Event Hubs /  IoT Hubs の高スケールなワークロードを Functions で実行し、プラットフォームから得られるパフォーマンスを最大限に引き出すために注意すべきいくつかの重要なポイントについて説明したいと思います。

私たちは Azure CAT チームと連携して、Function と Event Hubs を使用したシンプルで代表的なイベント処理パイプラインを構築しました。テレメトリは Application Insights に送信されます。

負荷生成器(Function で実行されています)は、バッチ処理されたメッセージを、Ingestion Event Hub に書き込みます。これらのメッセージは、与えられたセンサーからの一連の測定値を表します。Function は、Ingestion Event Hub からメッセージを収集し、そこから個々の測定値を抽出し、それらを追加のテレメトリとともに、それぞれ天気/地震データ用の Event Hub に書き込みます。加えて同じ  Function App 内の 2 つの Function(従量課金プラン)は、それぞれ個別の測定値を処理し、集約されたテレメトリを Application Insights に送信します。

Performance

我々は、合計 9 日間、毎秒 100,000 イベントの負荷目標の下でシステムを実行しました。その間、システムは合計 760 億イベントを処理しました。パイプラインの e2e 待ち時間、すなわち、メッセージを Ingestion Event Hub に書き込んでから天気/地震の Function でメッセージを処理するまでに要した時間を測定しました。結果は次のとおりです。

E2E Latency Percentiles

P50 P90 P95 P99 P99.9 P99.99 Max
1,102.42ms 2,755.56ms 3,788.30ms 11,894.12ms 50,367.23ms 111,240.50ms 239,890.10ms

簡単にお伝えすると:

・メッセージの半分が、処理ハブに書き込まれてから1.2秒以内に処理されました

・10 件中 9 件のメッセージが3秒以内に処理されました

・1 分未満で 1,000 件のメッセージのうち 999 件が処理されました

・すべてのメッセージが 5 分以内に処理されました

Monitoring

Azure Functions には、WebJobs ダッシュボードと Application Insights(Azure FunctionsとApplication Insightsの統合は現在プレビュー中)の 2 つの監視用ソリューションが組み込まれています。 ダッシュボードは長時間実行されるジョブを考慮して設計されており、1  秒あたりに 10,000 回以上の Function が実行されるシナリオには最適化されていません。 幸いなことに、Application Insights は驚異的に頑健なテレメトリ システムであり、大規模なシナリオでは Azure Functions との連携が優れていることを確認しました。

App Insightを有効にするのは簡単です。Function App にインストルメント キーを追加するだけで、Azure Functions は自動的にApp Insightsにデータを送信し始めます。 詳細は こちら をご覧ください。

Azureダッシュボードは高度にカスタマイズ可能で、App Insightsはその視覚的コンポーネントを固定するのに非常に効果的です。 このシナリオでは、非常に便利な監視ダッシュボードをまとめるのに 1〜2 時間程度しかかかりませんでした。

Configuration

今回の結果を達成するためにいくつか注目すべき設定を行いました。

それぞれの詳細については、以下を参照してください。

Batching

Event Hubs でトリガーされる Function を記述して、単一のメッセージまたは複数のメッセージのバッチ処理を行うことができます。 後者に関しては、とても優れた性能特性を持っています。 スプリットする関数を例に取ってみましょう:

 public static async Task Run(
  EventData[] sensorEvent,
  PartitionContext partitionContext,
  IAsyncCollector outputWeatherData,
  IAsyncCollector outputSeismicData,
  TraceWriter log)
  {
    foreach (var sensorData in sensorEvent)
    {
      SensorType sensorType = SensorType.Unknown;

      try
      {                   
        if (sensorData.Properties.ContainsKey("SensorType"))
        {
          System.Enum.TryParse(sensorData.Properties["SensorType"].ToString(), out sensorType);
        }

        await ProcessEvent(sensorData, sensorType, partitionContext, outputWeatherData, outputSeismicData);
      }
      catch(Exception ex)
      {
        telemetryHelper.PostException(ex, sensorData, partitionContext.Lease.PartitionId, sensorType.ToString());
      }
    }                                   
  }

このコードに関する主な注意事項:

配列ベースのアプローチは、各関数の実行よりもオーバーヘッドの面で優れています。このシステムは、関数を呼び出すときに多数のアクションを実行します。これらのアクションは、イベントごとに1回ではなく、イベントの配列に対して 1 回だけ発生します。注:JavaScript の Function の場合、バッチ処理を有効にするために、function.jsonの "cardinality" プロパティを明示的に "many" に設定する必要があります。( こちら の例を参照してください)

例外処理に対するこのアプローチは、メッセージの消失/スキップを防ぐ場合に重要です。 一般的には、後の処理/分析に失敗したイベントを格納するように例外ハンドラを作成します。 Azure Functions には Event Hub のデッドレターが組み込まれていないため、これは重要です。

WebJobs Dashboard

上記のとおり、私たちは監視のためにApp Insightを使用していたので、ダッシュボードを無効にしました。アプリケーション設定で、 AzureWebJobsDashboard の設定を削除します。

Partition Configuration

Azure Functions は Event Hubs SDK で提供される EventProcessorHost(詳細は こちら を参照)を使用して、Event Hub メッセージを処理します。 EventProcessorHost は、アプリケーションを実行する各VMが複数のパーティションのリースを取得し、それらのパーティションでメッセージを処理できるようにする仕組みです。つまり、Event Hub に 2 つのパーティションしかない場合、任意の時間に 2 つの VM しかメッセージを処理できません。つまり、パーティション数が Function のスケーラビリティに上限を設定します。

Event Hub の Basic と Standard には、Event Hub ごとに 32 個までのパーティションの既定の制限がありますが、 課金サポート に問い合わせることでこの制限を増やすことができます。Event Hub を100 個のパーティションに設定することにより、各 Function を 100 台の VM で同時に実行することができるようになりました。天気データ用の Function を 1分間に実行した一意の VM の数を数えると、下記のような結果になります。

別の簡単なクエリを使用して、94 台の VM 上で作業が均等に分散されているか確認することができます。

Partition Keys

Event Hub のプログラミングガイドには、パーティションキーの 概要 と、それをいつ使用したらよいかが記載されています。 このシナリオでは順序付けやステートフルな要件がないため、パーティションキーなしでイベントを生成しました。 これにより、実行のスループットと可用性が向上しました。

Protocol Buffers

1秒間に10万回以上のイベントを読み書きする場合は、シリアライズ・ステップを実行するのに要する時間とデータサイズの観点から、これらのイベントのシリアライゼーションとデシリアライゼーションを可能な限り効率的にしたいと考えています。 プロトコルバッファー は、扱いやすい高性能シリアル化フォーマットです。 イベントから気象測定値のバッチをデシリアライズして処理するコードの例を次に示します。

 if (sensorType == SensorType.Weather)
{
  var batch = WeatherReadingBatch.Parser.ParseFrom(sensorData.GetBytes());
  var messages = batch.SensorReadings
    .Select(reading => EnrichData(enqueuedTime, reading));
  await WriteOutput(messages, sensorData.PartitionKey, outputWeatherData);
}

このシナリオで使用する.protoファイルを知りたい場合は、 ここ を参照してください。

Cost

Function App とその依存リソースを 9 日間実行するための費用総額は、 約 1,200 ドル でした。各サービスの 1 時間あたりのコストは次のようになります。

Service Cost per Hour (USD)
Functions $2.71
Storage $1.80
Application Insights $1.03

注目すべき重要な点は次のとおりです。

Azure Monitor REST API を使用して実行可能な実行カウントと実行ユニットのデータを使用することで、Function Appのコストをより詳細に把握できます(詳細は こちら )。 1 時間分のデータを照会すると、次のようになります。

ここでのFunction Execution Units は、mb-milliseconds 単位で測定されることに注意してください。これらをgb-secondsに変換するには、1,024,000で割ります。(Function の価格設定の詳細は ここ にあります。簡単な補助プログラムは ここ にあります):

時間あたりのコスト=(6,500,012回の実行*($ 0.20 / 1,000,000))+((90,305,037,184単位/(1024 * 1000))* $ 0.000016)= 2.71ドル

Summary

Azure Functions の従量課金プランは、何百もの VM 上で実行するようにアプリケーションをスケーリングすることができ、膨大な計算容量の確保のために料金を支払うことなく、高パフォーマンスが要求されるシナリオを実現可能にします。 Azure Functions の詳細とサーバレス技術でのクラウドアプリケーションの構築については、 ここ から始めてください。