AWS SDKを用いたオブジェクト削除
AWS S3はあらゆるサービスの基盤になっているので、S3上のオブジェクトを大量に消すコードを書いたことがある人は多いかと思います。
削除が遅いと感じたことはありませんか?
遅いのをJavaScriptのせいにしていませんか?
DeleteObjectCommandで一個ずつ削除
DeleteObjectで一個ずつ消すことも可能ですが、これは論外ですね。
for await (const {Contents} of paginateListObjectsV2({
client
}, {
Bucket
})) {
for (const {Key} of Contents ?? []) {
await client.send(new DeleteObjectCommand({
Bucket,
Key
}))
}
}
測定結果
あまりの遅さに大量オブジェクトで測定することを断念しました。
DeleteObjectsCommandで削除
AWS SDKを使っている場合に、大量のオブジェクト削除はこのように書くことが多いと思います。
for await (const {Contents} of paginateListObjectsV2({
client
}, {
Bucket
})) {
if (Contents === undefined) {
continue
}
await client.send(new DeleteObjectsCommand({
Bucket,
Delete: {
Quiet: true,
Objects: Contents.map(i => ({
Key: i.Key
}))
}
}))
}
Create 10000 object in target bucket
batch 12273 ms 1.2273ms/key
pagingしながらキーを最大1000個取得して、DeleteObjectsで一括削除。何も問題ないように思われます。
秒間815件、JavaScirptならこの程度と思っていませんか?
遅い理由はシンプルで、オブジェクト削除のリクエストを並行して出すことができていません。
かといって、全部並行してPromise.allにするのはあまりにも乱暴です。
ya-synのexecuteTasksを利用する
ya-synを使うと下記のようにコードを書き換えることができます。
最大1000個分のタスクを読み込み、SDKに合わせて最大50並列でリクエストを流します。
const sp = new SynchronizerProvider()
await sp.executeTasks({
maxTasksInFlight: 1000,
maxTasksInExecution: 50,
taskSource: async function* () {
for await (const {Contents} of paginateListObjectsV2({
client
}, {
Bucket
})) {
if (Contents === undefined) {
continue
}
yield {
task: Contents.map(i => i.Key!) as string[]
}
}
}(),
taskExecutor: async ({task}) => {
await client.send(new DeleteObjectsCommand({
Bucket,
Delete: {
Quiet: true,
Objects: task.map(Key => ({
Key
}))
}
}))
}
})
実行結果
Create 10000 object in target bucket
batch executor 3280 ms 0.328ms/key
秒間3048件ぐらいなので、nodejsベースとしては悪くない数字だと思います。
maxTasksInExecutionを指定せずに、Key PrefixごとにSemaphoreを分ければもう少し高速化できる可能性もあります。
キーごとにセマフォ分離
特定prefixへのアクセスを緩和するため、先頭4文字prefix単位での実行数を2に絞ってみました。
prefixで厳密に分けるとリクエストが増えてしまうので、最初の1件のprefixだけをみています。本気でやるなら並列度と、prefixの調整でもう少し速くなると思います。
const sp = new SynchronizerProvider()
const connectionSemaphore = sp.createSynchronizer({
maxConcurrentExecution: 50
})
await sp.executeTasks({
maxTasksInFlight: 1000,
taskSource: async function* () {
for await (const {Contents} of paginateListObjectsV2({
client
}, {
Bucket
})) {
if (Contents === undefined) {
continue
}
yield {
task: Contents.map(i => i.Key!) as string[]
}
}
}(),
taskExecutor: async ({task}) => {
await sp.forKey(task[0].substr(0, 4), 2).synchronized(async () => {
await connectionSemaphore.synchronized(async () => {
await client.send(new DeleteObjectsCommand({
Bucket,
Delete: {
Quiet: true,
Objects: task.map(Key => ({
Key
}))
}
}))
})
})
}
})
10万件でやってみました。4135件/秒ぐらい。
割と時間がかかる上に課金が怖いので、1回しか試してないです。
Create 100000 object in target bucket
batch executor 24179 ms 0.24179ms/key
自作ライブラリ ya-syn
今回紹介した実装はya-syn v1.2.0の実装の一部になっています。
今回の例だとページングサイズとバッチ削除のサイズがどちらも1000なので簡単ですが、このサイズが合わない場合でもya-synなら簡単に対応できます。