AWS S3の特定パスにある大量のオブジェクトをGoで並列に別バケットに移動する
チカクのまごちゃんねるというサービスでタイトルのような作業があったので、 そのとき使ったコードのサンプルの共有と背景を若干伏せて紹介してます。 記事を見て、何か良い知見があれば教えてください! bucket/ に1 ~ Nの連番(id)があり、その配下には共通したディレクティブ(a ~ d)が入ってます。 aディレクトリにaa/bbというディレクトリがあり(もしくは片方ない)、その中にオブジェクトが入ってる状態です。 今回は「bucket/{id}/a/aa」「bucket/{id}/a/bb」の中身を全部他のバケットに移す作業です。 無限にオブジェクトが増えていくバケットだった lifeycleで適用しようと考えたが、特定パスが相当な数があったためlifecycleの上限(1000)を超える。またlifecycleは正規表現が使えない
issueに切って頭出し。会議の内容や作業手順が全てissueに残していきました。 PdM/サーバサイド/その他関係するエンジニアに進捗、実行するタイミング、スポットでかかったコストや予想されるコストなど変化する事象を密にコミュニケーションしながら進めてました。 まずトップ10あたりのidに対し、対象の容量の比率を出しました。 そして全体に照らし合わせ、どれくらいの容量とコストがあるのかを出しました。 次に 取り出す頻度を加味して あとで紹介するのですが、 社内ロジックを伏せてサンプルコードを載せてます。 最初は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時間かかりました。 実行時にはアラートチャンネルと顧客からの問い合わせチャンネルを見て、問題がないか確認してました。 まず上記のbashを作り、 終わった時のslack通知には https://github.com/catatsuy/notify_slack を使いました。 2つファイルを作って こんな感じの通知が来てくれました。
Amazon S3 の AWS 請求および使用状況レポートを理解する - Amazon Simple Storage Service 移動時は$8200ほどかかりましたが、移動後は70%のコスト削減ができました。(具体的な数字は伏せてますが、すぐペイ出来る計算になります) 特にかかったのが、いつでもrollbackできるように 通信費に関しては、EC2上で動かしたので無料です。 初期コストはある程度かかるのは分かってたものの、プロジェクト進める前に具体的な数字を出した方が良かったですね。反省。 t2だとネットワーク。c5.largeだとメモリ数(OOM Killer)。 それぞれ並列(30)に耐えれなくてc4.4xlargeにしました。 src/destのオブジェクト自体の比較をしてより精度を高めたかったのですが、 https://github.com/google/go-cmp 使ってみたが、行毎で差分が出てほぼ全部差分になり思ったdiffにならず。 色々あって時間が足りなかったので、トータルオブジェクト数の一致性と、軽く目diffだけでOKと判断しました。 コードは若干diff実装の名残りが残ってるかもしれないです。 https://aws.amazon.com/jp/premiumsupport/knowledge-center/s3-resolve-503-slowdown-throttling/ 実行者側でリクエスト制御するまで、aws側でslow downエラー(制限をかける)を返す事があるらしい。 雑に並列数(30)に下げて対応しました。 上記エラーが発生し、どうやらmapを並列で取り扱う場合はmutex.Lock / Unlockが必要だったということ。 まぁそれはそうだよねという感じで実装しました。 ref: mapの競合状態のはなし - 今川館はじめに
TL;DR
DeepArchive
にして70%削減できたよバケット構成
# 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
移動?
STANDARD
で保存してた
STANDARD_IA
にしたが、それでもめちゃくちゃコストかかってたので、特定パスをDeepArchive
にすることでコストカットを提案How
進め方
コストの洗い出しとGlacier or DeepArchive
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
にする初期コストを試算しておくと完璧だったと思います。コード
どれくらいかかった?
障害が起こってないかの確認
終わったときの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
nohup /bin/bash run.bash &
でバックグラウンドで動かしました。$ 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
nohup /bin/bash watch.bash &
でバックグラウンドで動かしました。移動時と移動後の値段
STANDARD_IA
のままコピーして、lifecycle適用で一気にDeepArchive
にしたので、そのときのAPN-EarlyDelete-SIA
が$4000かかりました。起こった問題について
1. インスタンス耐えれなかった問題
2. オブジェクト自体のdiffは諦めた
3. Aws::S3::Errors::SlowDownエラー
4. Go側の並列時にmapの取り扱い
fatal error: concurrent map read and map write