S3を使った「選択式投票システム」の実現性について考えてみる

FusicでAWS番長をしている杉本と申します。
一番好きなAWSサービスはS3です。
普段はエンジニアチームのマネージャをやりながら、エンジニアとしては[TeamFake](https://fusic.co.jp/doings/125)に所属してRailsやAndroidをメインで触りつつ、大学での専攻や前職の知識を活かしてチームメンバーに電子回路・半田付けをレクチャーしてます。

さて今回は大好きなS3でやってみたかったことの調査をしましたので、そちらをまとめたいと思います。

## 仮想案件「選択式の投票システム」
もし「選択式の投票システム」を構築するとしたら、みなさんはどんな構成にしますか？
EC2上にAPIサーバシステムを何かしらの言語で構築とか、KinesisやAPI Gatewayに投げるとかでしょうか？
要件やそのときのリソースによっても選択肢は変わってくると思います。
こういう仮想案件があったらどういうアーキテクチャーにしたら面白いかな？とか普段から考えるのが好きで、たまに一人で妄想しています。
そんな中、ふとS3を使って簡易的な構成でできそうな気がしたので、その実現性について検討してみました。

## S3のバージョニング機能
まさにこの機能を使って、「選択肢毎にPUTするオブジェクトを分けて回答時にどんどん上書きして、集計はその履歴数でカウントすればいいんじゃね？」とビール片手に思いつきました。
負荷はS3がなんとかうまいこと捌いてくれるだろうし、サーバ運用自体も不要になりそうという期待感でホクホクしました。

## 検討項目：アジェンダ
- 仮想の条件を設定
- どの程度の同時アクセスまで耐えられるのか？
- 同一オブジェクトのバージョニングの上限値はあるのか？
- 投票数の集計について
- 想定コストについて

## 仮想の条件を設定
できるだけ厳しい方が面白いかなと、なかなかの人気番組で視聴者も前のめりの投票コーナーというものにしてみました。
※実際の人気番組がどの程度なのかはよくわからないので、妄想の限りです。数値には責任を持てません。

|項目|条件|
---|---
人口|120,000,000人
視聴率|25%
投票率|50%
選択肢の分散|50%
回答時間内の1分間に集中する率|50%

オブジェクト自体を1秒毎のタイムスタンプでキーを分ければ、`62,500履歴/秒`できればいいということになりますので、これで検討を進めたいと思います。

## どの程度の同時アクセスまで耐えられるのか？
`62,500履歴/秒`でPUTができるか検証してみました。
ブラウザからの書き込みで高負荷は難しいので、AWS CLIを使って簡易的なスクリプトでやってみました。

```bash:put_object.sh
#/bin/sh

bucketname=${1}
answer=${2}

for i in {1..62500}
  do
  echo "${i}"
  /usr/bin/aws s3 cp d s3://${bucketname}/question01/${answer} >/dev/null 2>&1 &
done
```

### 速度を出すために同一リージョンのEC2上で実行
ちょっと費用をケチってt2.nanoでトライ！

```
$ sh put_object.sh bucketname a
put_object.sh: fork: Cannot allocate memory
```

(´・ω・｀)

### よし！奮発するよ！r3.largeだ！
``
$ sh put_object.sh                                                                                                                  
ImportError: /usr/lib64/python2.7/lib-dynload/_csv.so: failed to map segment from shared object: Cannot allocate memory
ImportError: /usr/lib64/python2.7/lib-dynload/unicodedata.so: failed to map segment from shared object: Cannot allocate memory
```

(´・ω・｀)<15GBを食いつぶすとは…

高負荷をかけるって難しいですね。。
人力侮らざるべからず！

### ふと社内向けAWS資格取得研修で話したことを思い出した
Fusicでは社員のAWS力を上げる施策の一環として、月数回程度のAWS資格取得研修をしています。
その中でS3がテーマのときにこんな話をしたことがあったな〜と思い出しました。

リクエスト率およびリクエストパフォーマンスに関する留意事項
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/request-rate-perf-considerations.html

>Amazon S3 は非常に高いリクエストレートをサポートするように拡張されます。
リクエストレートが一貫して増加している場合、Amazon S3 は、高いリクエストレートをサポートするため、必要に応じて自動的にバケットを分割します。
ただし、バケットに対するリクエストレートが急速に増加して、毎秒 300 回の PUT/LIST/DELETE リクエストまたは毎秒 800 回の GET リクエストを超えることが予想される場合は、リクエストレートの一時的な制限を防ぐため、サポートケースを開いてワークロードの増加に備えることをお勧めします。

つまりオブジェクトの書き込みにはAWS側でその性能が定義されていてそれを超える場合はお問い合わせをするか、あるいはプラクティスに習ってキーが偏らないようにアプリ側を工夫する必要があるんでした。
そもそも `62,500履歴/秒` は無理な話でした。
なので、要件で秒間800以上の回答があり得る場合は、お問い合わせをするか、キーがランダムになるようにアプリを設計しないといけませんね。

ちなみに「例 2: キー名の文字列を左右反転する」が簡単に実装できそうな感じでいいなと思いました。今回のケースで言えば、タイムスタンプを反転させたキーをプレフィックスにするとかでしょうか。
ただし、集計がめんどくさくなるというデメリットはありそうです。

## 同一オブジェクトのバージョニングの上限値はあるのか？
ドキュメントを漁ってみましたが、履歴数の上限についての記述をみつけることができませんでした。
そこで、先程のスクリプトでバックグラウンド実行させていた部分を順次実行に変えて根気強く履歴を重ねてみました。

```
$ sh chk_obj.sh bucket_name a
---------------------
Total Count:22000
---------------------
```

まあ、このくらい可能であればいいかなーというのと、0バイトの空ファイルといえどもPUT自体にも課金されるので、この程度で許してあげることにしました。
個人のAWSアカウントなので。


## 投票数の集計について
さて先にスクリプトの実行結果を見せてしまいましたが、そちらは以下の簡易的なスクリプトを作成してオブジェクトの履歴数をカウントしました。

```bash:chk_obj.sh
#/bin/sh

bucketname=${1}
answer=${2}

cnt=`/usr/bin/aws s3api list-object-versions --bucket "${bucketname}" --prefix "question01/${answer}" | grep 'LastModified' | wc -l`
echo "---------------------"
echo "Total Count:${cnt}"
echo "---------------------"
```

ちなみにAmazon Linux上のAWS CLIでは問題なかったのですが、ローカルのMacにbrewでインストールしたAWS CLIでは以下のように取得する履歴数の上限値を指定しないとデフォルトでは1000件しか取れませんでした。
`aws s3api list-object-versions --bucket "${bucketname}" --prefix "question01/${answer}" --max-items 100000`

それぞれのバージョンは以下でした。

```:AmazonLinux
$ aws --version
aws-cli/1.10.56 Python/2.7.12 Linux/4.4.23-31.54.amzn1.x86_64 botocore/1.4.46
```

```:Mac
$ aws --version
aws-cli/1.11.13 Python/2.7.10 Darwin/15.6.0 botocore/1.4.70
```

ここの違いについてはよくわかっていないので、ご存じの方がいたら教えてほしいです。

## 想定コストについて
https://aws.amazon.com/jp/s3/pricing/

保存しているオブジェクトは空ファイルで0バイトなので、料金はかからないだろうと思ってたらこんな注意書きがあることに気づきました。

>標準 – 低頻度アクセスストレージの請求対象となる最小オブジェクトサイズは 128 KB です。128 KB より小さいサイズのオブジェクトを保存することもできますが、128 KB のストレージとして課金されます。

ということで、同時アクセスを無視して最初に掲げた条件で回答されたとすると、 `15,000,000履歴（オブジェクト）` が保存されるので、そのオブジェクト保存期間を14日とすると

```
15000000*128/1024/1024/30*14*0.025 ≒ 21.4ドル
```

あとは回答毎にPUTリクエストが発生するので、以下費用もかかりますね。

>東京リージョン
PUT、COPY、POST、または LIST リクエスト	$0.0047 : 1,000 リクエストあたり

```
15000000/1000*0.0047 = 70.5ドル
```

合計92ドルくらいとなりました。
※ただし、集計時のGETや転送料は別途かかります。

## まとめ
- 秒間リクエスト800以下が保証されるのあれば、S3のバージョニング機能を使って簡易的な投票システムはサーバレスで構築できそうです
- コストとしても100ドル以下で1500万回答くらいの規模が実現できそうです
- 高負荷な同時アクセスの負荷試験をするのは難しく、人力の偉大さを改めて実感しました

何か機会があれば実案件に導入してみたいなと今日も夢見て、これからもいろんな妄想をしていきたいと思います。