Sionの技術ブログ

SREとして日々の学習を書いて行きます。twitterは@sion_cojp

AWS S3の特定パスにある大量のオブジェクトをGoで並列に別バケットに移動する

f:id:sion_cojp:20210628192147p:plain:w0

はじめに

チカクのまごちゃんねるというサービスでタイトルのような作業があったので、

そのとき使ったコードのサンプルの共有と背景を若干伏せて紹介してます。

記事を見て、何か良い知見があれば教えてください!

TL;DR

  • 数千万オブジェクト/数百万TBをGoで並列移動(コピー)した
  • DeepArchiveにして70%削減できたよ
  • 初期コストはかかったけど、すぐペイするよ

バケット構成

bucket/ に1 ~ Nの連番(id)があり、その配下には共通したディレクティブ(a ~ d)が入ってます。

aディレクトリにaa/bbというディレクトリがあり(もしくは片方ない)、その中にオブジェクトが入ってる状態です。

今回は「bucket/{id}/a/aa」「bucket/{id}/a/bb」の中身を全部他のバケットに移す作業です。

# bucket
bucket/
├── 1
│   ├── a
│   │   ├── aa
│   │   │   └── hoge.txt
│   │   └── bb
│   │       └── fuga.txt
│   ├── b
│   ├── c
│   └── d
├── 2
│   ├── a
│   │   └── bb
│   │       └── piyo.txt
│   ├── b
│   ├── c
│   └── d
.
.
.
└── 10000
│   ├── a
│   │   │   └── foo.txt
│   │   └── bb
│   │       └── bar.txt
│   ├── b
│   ├── c
│   └── d

Why

移動?

  • awsのコストが高いので、調査して色々と削減する動きをしていたら、s3がコストランキング2位だった
  • とあるバケットは、頻繁にアクセスしないけどSTANDARDで保存してた
  • さらにその中の特定パスのオブジェクト(数千万/数百TB)は1年間で使う事がほぼないけど保存する必要があった
  • 無限にオブジェクトが増えていくバケットだった

    • 一旦STANDARD_IAにしたが、それでもめちゃくちゃコストかかってたので、特定パスをDeepArchiveにすることでコストカットを提案
  • lifeycleで適用しようと考えたが、特定パスが相当な数があったためlifecycleの上限(1000)を超える。またlifecycleは正規表現が使えない

How

進め方

f:id:sion_cojp:20210625151840p:plain:w300

issueに切って頭出し。会議の内容や作業手順が全てissueに残していきました。

PdM/サーバサイド/その他関係するエンジニアに進捗、実行するタイミング、スポットでかかったコストや予想されるコストなど変化する事象を密にコミュニケーションしながら進めてました。

コストの洗い出しとGlacier or DeepArchive

まずトップ10あたりのidに対し、対象の容量の比率を出しました。

そして全体に照らし合わせ、どれくらいの容量とコストがあるのかを出しました。

次にGlaicer/DeepArchiveにしたときの、もし全オブジェクトを取り出した場合のコストを試算したところ、5.3ヶ月に1回取り出すのであれば、DeepArchiveの方が安いことがわかりました。

取り出しコスト =
標準データ取り出しリクエスト(オブジェクト数 / 1000件 * お値段)
+ 取り出し(オブジェクト数 * お値段)
+ GET(オブジェクト数  / 1000件 * お値段) 
+ COPY(STANDARD_IA等にコピー。オブジェクト数 / 1000件 * お値段)

※1000件ごとに値段がかかる。お値段はGlacier/DeepArchiveで違う
ref: https://aws.amazon.com/jp/s3/pricing/

取り出す頻度を加味してDeepArchiveにしたほうが圧倒的に安いという判断になりました。

あとで紹介するのですが、DeepArchiveにする初期コストを試算しておくと完璧だったと思います。

コード

github.com

社内ロジックを伏せてサンプルコードを載せてます。

最初はawscliでやろうとしたのですが、オブジェクトをGETするだけでも全然終わる気配がなかったので、Goで並列化するしかなかったです。

色々あって3日後にstg。5日後にprod実行だったので、1日くらいでベースは出来て、そこからいろんな要望(flag見るとわかるかと)が出て少しずつ修正していった感じです。

社内にGoに精通した人がそんなにいないので、2日目にmeetsでリアルタイムにロジックを説明してレビューしてもらいました。

今までsyncパッケージのWaitGroupで並列化してたのですが、今回はgolang.org/x/sync/ を使ってみてとてもわかりやすくて良かったです。

golang.org/x/sync/ を使ったGoの並列処理 - Sionの技術ブログ

どれくらいかかった?

30並列で、copyが2,3日。deleteが6時間かかりました。

障害が起こってないかの確認

実行時にはアラートチャンネルと顧客からの問い合わせチャンネルを見て、問題がないか確認してました。

終わったときのslack通知

$ vim run.bash

#!/bin/bash -
date > /tmp/s3.log
./bin/s3-move-other-bucket --src src_bucket --dest dest_bucket --parallel 30
echo $? >> /tmp/s3.log
date >> /tmp/s3.log

まず上記のbashを作り、nohup /bin/bash run.bash & でバックグラウンドで動かしました。

終わった時のslack通知には https://github.com/catatsuy/notify_slack を使いました。

$ vim hoge.toml
[slack]
url = "https://hooks.slack.com/services/xxxxxxxxxx"
channel = "#channel名"
interval = "1s"
$ vim watch.bash
#!/bin/bash -
while true
do
        ALIVE=`ps auxww | grep "/bin/bash hoge.bash"  | grep -v grep | wc -l`
        if [ ${ALIVE} = 0 ]; then
                echo "<@Uxxxxx> スクリプト終わったよ!" | /home/ec2-user/notify_slack -c /home/ec2-user/hoge.toml
                break
        fi
        sleep 10
done

2つファイルを作って nohup /bin/bash watch.bash & でバックグラウンドで動かしました。

こんな感じの通知が来てくれました。

f:id:sion_cojp:20210628185218p:plain

移動時と移動後の値段

Amazon S3 の AWS 請求および使用状況レポートを理解する - Amazon Simple Storage Service

移動時は$8200ほどかかりましたが、移動後は70%のコスト削減ができました。(具体的な数字は伏せてますが、すぐペイ出来る計算になります)

特にかかったのが、いつでもrollbackできるようにSTANDARD_IAのままコピーして、lifecycle適用で一気にDeepArchiveにしたので、そのときのAPN-EarlyDelete-SIAが$4000かかりました。

通信費に関しては、EC2上で動かしたので無料です。

初期コストはある程度かかるのは分かってたものの、プロジェクト進める前に具体的な数字を出した方が良かったですね。反省。

起こった問題について

1. インスタンス耐えれなかった問題

t2だとネットワーク。c5.largeだとメモリ数(OOM Killer)。

それぞれ並列(30)に耐えれなくてc4.4xlargeにしました。

2. オブジェクト自体のdiffは諦めた

src/destのオブジェクト自体の比較をしてより精度を高めたかったのですが、

https://github.com/google/go-cmp 使ってみたが、行毎で差分が出てほぼ全部差分になり思ったdiffにならず。

色々あって時間が足りなかったので、トータルオブジェクト数の一致性と、軽く目diffだけでOKと判断しました。

コードは若干diff実装の名残りが残ってるかもしれないです。

3. Aws::S3::Errors::SlowDownエラー

https://aws.amazon.com/jp/premiumsupport/knowledge-center/s3-resolve-503-slowdown-throttling/

実行者側でリクエスト制御するまで、aws側でslow downエラー(制限をかける)を返す事があるらしい。

雑に並列数(30)に下げて対応しました。

4. Go側の並列時にmapの取り扱い

fatal error: concurrent map read and map write

上記エラーが発生し、どうやらmapを並列で取り扱う場合はmutex.Lock / Unlockが必要だったということ。

まぁそれはそうだよねという感じで実装しました。

ref: mapの競合状態のはなし - 今川館