🐱

[AWS IoT Core]Kinesis Data Streamsでホットシャードを発生させた話

2023/03/15に公開

はじめに

AWS IoT CoreからKinesis Data Streamsにデータを送る際、パーティションキーの設定を誤って同一の文字列にしていた為にホットシャードを発生させてしまったというお話をします。

前提

mockmockから10万台分の位置情報を1分間隔で30分間、IoT Coreへ送信する負荷テストを行っていた(計300万件のデータ送信)
・IoT Coreで受信した情報をKinesis Data Streams→Lambda→DynamoDBの流れでデータを保存する

【IoT Coreの受信データ例】

{
    "list" : [
       {
           "vin" : "TEST-100001",
            "timestamp":"2022-11-01T10:10:00+09:00",
            "lat":35.658278,
            "lon":139.701611
        }
    ]  
}

Jsonの項目名: vinの値は「TEST-100001~TEST-200000」の間の値を時系列で1ずつ増やした値をIoT Coreへ送信します。
lat(緯度)とlon(経度)の値はmockmockの位置ジェネレータで生成して送信します。

参考:mockmockの位置ジェネレーターを使ってみる

パーティションキーとシャードの関係

パーティションキーは、ストリームにデータを送信するアプリケーション側で指定されます。

【IoT Coreの設定画面】

ストリーム先のKinesis Data Streams(以下、Kinesis)では、パーティションキーをハッシュ化してどのシャードに割り当てるかを決めます。そのため、パーティションキーの値が近似値であっても、異なるシャードに均等に分散してくれます。


AWSでのストリーム処理入門

同じパーティションキーのデータを受信した場合には、同じシャードにデータが送信されることで

  1. データ処理の順序を保証される
  2. 同じグループの処理が効率化できる

などのメリットがあります。

一方で、あるパーティションキーに対して大量のデータが集中してしまった場合、そのパーティションが割り当てられたシャードに過剰な負荷がかかり、ホットシャードが発生します。

今回設定していたパーティションキー vin の値は 「TEST-100001,TEST-100002,....TEST-199999,TEST-200000」 のように異なる値にしてIoT Coreへ送信していたため、Kinesis側でハッシュ化されうまくシャードにデータが分散されるものと思っていました。

発生した事象

AWS Management ConsoleのLambda関数の[モニタリング]タブからCloudWatchメトリクスを確認したところ、
「あれれ~?おかしいぞ~?」

対応前

  1. Kinesisのシャード数を8、Lambdaのシャードあたりの同時バッチは1にしていたが、メトリクス Total Concurrent executions(Lambda同時起動数)を確認したところ、Lambdaが1つしか起動していなかった。
  2. Kinesisがデータを受信してから、そのデータをLambdaに送信するまでの時間を表すメトリクス IteratorAge の値が増加し続けており、Lambdaの処理がまともに遂行できていないようだった。

CloudWatchの[ロググループ]からLambdaで生成しているログの中身も確認してみると、Lambda→DynamoDBへ書き込み(BatchWriteItem)が失敗した場合の戻り値UnprocessedItems を含むエラーログが大量に出力されていました。

原因

IoT Coreルールアクションのパーティションキーの設定が、JSON要素を参照する ${vin} ではなく vin になっていた。
 →全レコードのパーティションキーが同一の文字列 vin になりホットシャードが発生。

対応

パーティションキーをランダムなユニークID ${newuuid()} に変更

newuuid()は、IoT Coreで利用可能な関数で、16バイトのランダムなUUIDを返すものです。
例: newuuid() = 123a4567-b89c-12d3-e456-789012345000

当初想定していた通りの設定にするならば、${vin} に変更するべきでしたが、ホットシャードの発生を体験し、今回構築しているサービスでは同じシャードにデータを送信するメリットよりも、シャードへの振り分けをランダムに行った方がメリットがありそうだ、ということになり ${newuuid()} を採用することにしました。

エビデンス

気になっていた全ての事象が無事解決したことを確認しました。

  1. メトリクスTotal Concurrent executionsの値が想定通りの値:8になり、シャードごとにLambdaが起動していることを確認。
  2. メトリクス IteratorAge の値が一定になり、順次Lambdaの処理を遂行できていることを確認。
  3. Lambdaが出力したエラーログが無いことを確認。

各設定値

最終的に採用した設定値を一部抜粋しています。

IoT Core

ルールアクション

項目 設定値
アクション 1 Kinesis Stream
ストリーム名 location-stream
パーティションキー ${newuuid()}

Kinesis Data Streams

データストリーム名

項目 設定値
データストリーム名 location-stream

データストリームの容量

項目 設定値
容量モード プロビジョンド
プロビジョニングされたシャード 8
書き込み容量 最大 8 MiB/秒、8,000 レコード/秒
書き込み容量 最大 8 MiB/秒、8,000 レコード/秒

Lambda

基本設定

項目 設定値
メモリ 256MB
エフェメラルストレージ 512MB
タイムアウト 1分

トリガー

項目 設定値
ソース Kinesis
ストリーム location-stream
バッチサイズ 100000
バッチウィンドウ 15
シャードあたりの同時バッチ 1

※当初バッチウィンドウの値を30にしていましたが、Lambda→DynamoDBへ書き込みが一割ほど失敗していた為に15に変更しました。

参考:変更前の荒ぶるメトリクス

DynamoDB

テーブル

項目 設定値
テーブルクラス DynamoDB標準
読み込み/書き込みキャパシティーの設定 オンデマンド

最後に

ホットシャードが発生した事例をなかなかのデータ量で体験できました。
どなたかの参考になれば幸いです。

以上、えみり〜でした|ωΦ)ฅ

参考サイト

デベキャン

Discussion