0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DynamoDB接続がある日突然できなくなる…UnknownEndpointと無制限並列処理の落とし穴

Posted at

はじめに

S3にCSVが置かれたらLambdaが起動して、DynamoDBの在庫情報を更新する仕組みを作っていました。

嬉しいことに在庫の更新対象が増えてきたのでLambdaのメモリを増強したところ、今まで動いていたLambdaハンドラーのDynamoDBへのget処理が一斉にエラーを出力するようになりました。

エラー内容

UnknownEndpoint: Inaccessible host: `dynamodb.ap-northeast-1.amazonaws.com' at port `undefined'. This service may not be available in the `ap-northeast-1' region."

エラーの解説

エラーの内容は書いてある通り「DynamoDBエンドポイントに到達できていないこと」です。一般的には以下のような場合に発生します。

  • DNS解決できない
  • TCP接続できない
  • ルーティングできない
  • “接続先URLの形” が壊れている

まず疑ったこと

※ 今まで動いており、Regionも変更しているはずがないので今回の調査では割愛します。

エンドポイントの指定をしているか

書いてある通り、エンドポイントが undefined となっているのでソースコード上でエンドポイントの指定をしていないか確認しました。が、エンドポイントの指定をしていませんでした。

エンドポイントは指定をしない場合は自動的にエンドポイントの指定を行うので undefined になることはありえません。

const getClient = (): DocumentClient => {
  if (client === null) {
    const service = captureAWSClient(
      new DynamoDB({
        region:
          'ap-northeast-1',
        httpOptions: {
          timeout: 3000,
          agent: awsHttpsAgent,
        },
        maxRetries: 2,
      }),
    );
    client = new DocumentClient({ service });
  }
  return client;
};

VPC接続をしているか

VPCに入れていないLambdaは、AWSの管理するネットワーク上で動きます

DynamoDBなどのAWSサービスの**パブリックエンドポイント(例:dynamodb.ap-northeast-1.amazonaws.com)**へ、AWS側のネットワークで普通に到達できますが、VPC内に設置している場合はSGの設定などを適切にしない場合はDynamoDBに到達できない可能性があるため調査しました。

結果、 VPC接続はしておりませんでした。

そして迷走したこと

特定のS3イベントでのみこのエラーが発生してしまう ということもありこの事象が迷宮入りしてしまうところでした。
(LambdaをやめてこのハンドラーだけEC2などの他のサービスで動かすことも検討していました、、、)

エンドポイントやregionの指定に誤りがないVPC接続をしていない ... にも関わらず一向に解消されず、到達できないとエラーを吐き続けており、直近でリリースした修正をリバートしたりChatGPTとひたすら相談をし続けていました。
(しかし、ChatGPTも同じことを延々と言い始めて収拾がつかなくなってしまった。)

結論

よく見ると、Lambdaが無制限並列処理を行なっている

在庫更新は「レコードごとに」処理していて、流れはざっくりこうです。

  1. SKUでDynamoDBから現状を取る(get)
  2. 在庫数を更新して保存(put / update)

これを全部のレコードに対して回していたわけですが、実装がこんな感じでした。

❌ 悪い例:Bluebird Promise.map で concurrency を指定していない

import { DynamoDB } from "aws-sdk";
import { Promise as bbPromise } from "bluebird";

const doc = new DynamoDB.DocumentClient({ region: "ap-northeast-1" });

type Item = { pk: string; sk: string; value: number };

export async function handler(items: Item[]) {
  // ❌ concurrency指定なし
  await bbPromise.map(items, async (item) => {
    const current = await doc.get({
      TableName: "Example",
      Key: { pk: item.pk, sk: item.sk },
    }).promise();

    await doc.put({
      TableName: "Example",
      Item: { ...current.Item, ...item },
    }).promise();
  });

  return { ok: true };
}

Promise.map は、並列数を指定しないと“無制限”に処理を行います
件数が増えると、Lambdaの中でDynamoDBに対して大量のリクエストが一気に投げられてしまいます。

実際に、「特定のS3イベント」が実施されるCSVは件数もかなり多くそれに伴い並列処理数が増える仕様でした。

なぜいきなり動かなくなったのか?

ここが一番モヤモヤしたところで、コード自体は変えてないのに突然壊れ始めました。
これが最大の落とし穴で、いきなり動かなくなった理由は「LambdaのCPUの仕様」にありました。

AWSのConfigure Lambda function memoryには以下のように書いてあります。

A higher memory allocation can improve performance for functions that use imported libraries, Lambda layers, Amazon Simple Storage Service (Amazon S3) or Amazon Elastic File System (Amazon EFS). Adding more memory proportionally increases the amount of CPU, increasing the overall computational power available. If a function is CPU, network or memory-bound, then increasing the memory setting can dramatically improve its performance.

日本語訳:インポートされたライブラリ、Lambda レイヤー、Amazon Simple Storage Service (Amazon S3)、または Amazon Elastic File System (Amazon EFS) を使用する関数では、メモリ割り当てを増やすことでパフォーマンスが向上します。メモリを追加すると、CPU 使用量が比例して増加し、利用可能な全体的な計算能力が向上します。関数が CPU、ネットワーク、またはメモリにバインドされている場合、メモリ設定を増やすことでパフォーマンスが大幅に向上する可能性があります。

つまり、 Lambdaのメモリ割り当てを増やすことでCPUも増強され処理能力が向上する ということでした。

そのため、 処理するレコード件数が元々多い * 無制限並列処理 * Lambdaの処理能力向上 が重なって短時間のDynamoDBへの接続が急増したことで接続が不安定となりエラーに至っていました。

無制限並列によって、「いつでも必ず壊れる」というより、
しばらく運よく耐えていたのが、環境や入力の変化で耐えられなくなった、という感じに近いです。


解決:concurrency を付けるだけ

✅ 良い例:並列数を制限する

import { DynamoDB } from "aws-sdk";
import { Promise as bbPromise } from "bluebird";

const doc = new DynamoDB.DocumentClient({ region: "ap-northeast-1" });

type Item = { pk: string; sk: string; value: number };

export async function handler(items: Item[]) {
  // ✅ 並列数を制限
  await bbPromise.map(
    items,
    async (item) => {
      const current = await doc.get({
        TableName: "Example",
        Key: { pk: item.pk, sk: item.sk },
      }).promise();

      await doc.put({
        TableName: "Example",
        Item: { ...current.Item, ...item },
      }).promise();
    },
    { concurrency: 10 }
  );

  return { ok: true };
}

これだけで、あの「UnknownEndpoint: Inaccessible host...」が止まりました。

まとめ

  • 無制限並列処理をしない
  • Lambdaはメモリ割り当てによってCPUの使用量が変わる
  • DynamoDBは短時間の並列読み取りが行われるとエラーになる

今回の「UnknownEndpoint: Inaccessible host...」はWeb検索をしても、ChatGPTに聞いてみても解消策が見つからず、AIと共に生きている私としては苦い経験になりました。

今回の経験でますますDynamoDBをRDBに移行したいなと思っていますがその話はまたいつかしたいですね。

この記事が悩める開発者の一助となっていただければ幸いです。
またどこかでお会いしましょう。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?