こんにちは、井上です。
先週末、東急ハンズのネットショッピングサイト、ハンズネットのWEBアプリケーション・サーバーを、ElasticBeanstalk MultiContainer Docker でリプレイスを行いましたのでその紹介をしたいと思います。

ハンズネットはシステム構成として、ざっくりとフロントのWEBアプリケーション・サーバーとバックエンドのAPIサーバーにより構成されています。
バックエンドのAPIサーバーについては、2014年の4月頃からBeanstalk/PHPに順次置き換える作業を進めており、DynamoDBとBeanstalkによるオートスケールの効果もありそこそこ負荷に耐えられるようになってきたのですが、それに伴いフロントエンドの部分がスケールしないことがボトルネックになってきていました。
特にTVの放送時などの突発的なアクセス増の際には、手動によるスケールには限界があり、
ヒルナンデスや、マツコの知らない世界などの特集でハンズの商品が紹介されると、サーバーに接続しづらい状況になることが度々ありました。恐るべしマツコ効果。

オートスケール化の一番のポイントは、サーバー自体に状態を持たないようにするという部分だと思うのですが、以前よりWEBアプリケーション・サーバーとAPIサーバーが分離されていたため、その部分では大きな障壁なく進める事が出来ました。
一部、特集コンテンツのようなものや、画像、CSSなど、サーバー内で保持しているデータがあったため、まずはそれらをCloudFront経由での配信に切り替え、第2段階としてBeanstalkに乗せる作業を行いました。
今回この作業によりフロントエンドの部分もオートスケールに対応したため、スパイクアクセスで接続しにくくなる状況は改善されるのではと思っています。
Dockerについては、本番環境に投入して間もないことから、運用上どういう問題が発生するかまだ未知数な部分がありますが、何かありましたら、こちらで報告させていただきます。
今のところ、レスポンスタイムも切り替え前と変わらず、安定して稼働しています。
1点困ったのが、ルートドメインとBeanstalkの相性の悪さです。
ハンズネットは、 https://hands.net/ というサブドメインなしのURLで運用しており、今まではRoute53のAliasレコードとしてELBを登録していたのですが、AliasレコードにはBeanstalkで払いだされる xxxxx.elasticbeanstalk.com といったドメインを指定することができません。
なので、せっかくBeanstalkで SWAP URLなどの機能があってもそれを使う事はできず、AliasレコードとしてBeanstalkの生成するELBを指定するという形になってしまっています。
新バージョンへの環境を切り替えを行う際には、Route53でAliasレコードをいじらないといけないですし、Rebuild EnvironmetするとELBが変わってしまうのでうかつに行えません。
何か良い方法などありましたら教えて頂けると嬉しいです。
また、ハンズがなぜサーバー負荷対策を必死こいてやってるかというと、毎年8月末に、ハンズメッセという大バーゲンがあるのも要因のひとつです。
お店も行列が出来るほどの大盛況なのですが、ネットストアも毎年大わらわで、現在、負荷対策真っ盛りです。

今年は 8月27日〜 (ネットストアは 8月26日 18:00〜) です。お得な商品を多数販売しますので、ぜひチェックしてみてください!
ではでは。
下記のblogで紹介されているように、AWS PHP SDKの 2.7.7 から PHPの型とjsonとDynamoDBの型の相互変換を行うクラス、Marshaler が追加されています。
DynamoDB JSON and Array Marshaling for PHP
この Marshaler クラスを使うと、型変換が行われるため、数値型に大きな値を入れている場合に注意が必要です。
以下に例を示します。
id = 1 のレコードに number = 9223372036854775808 という値が入っているレコードを取り出してみます。
|
$result = $client->getItem([ 'TableName' => 'test_int', 'Key' => ['id' => ['N' => '1']] ]); var_dump($result['Item']); |
結果
|
array(2) { ["number"]=> array(1) { ["N"]=> string(19) "9223372036854775808" } ["id"]=> array(1) { ["N"]=> string(1) "1" } } |
9223372036854775808 という値が取り出せました。
これを Marshaler でPHPの型に戻してみます。
使用するメソッドは、unmarshalItem です。
|
use AwsDynamoDbMarshaler; : : $marshaler = new Marshaler(); $data = $marshaler->unmarshalItem($result['Item']); var_dump($data); |
結果
|
array(2) { ["number"]=> float(9.2233720368548E+18) ["id"]=> int(1) } |
9.2233720368548E+18 になってしまいました。/(^o^)\ ナンテコッタイ
原因
PHPでの整数の最大値はPHP_INT_MAXで定められており、9223372036854775807 になっています。(64bitの場合)
それを超えた数字を整数に変換しようとすると、指数表記になってしまいます。
参考:PHP>マニュアル>言語リファレンス>型>整数
一方、DynamoDBの方は、下記ドキュメントにて、最大 38 桁の精度とあるように、 99999999999999999999999999999999999999 まで投入できます。
参考:DynamoDB データモデル
この辺りの相互変換は、言語側の精度に依存する形になると思うので、使っている言語側の仕様を確認しておいた方がよさそうです。
たしか、rubyの場合には BigDecimal に変換されたと思います。
参考
開発環境で作ったDynamoDBのテーブルと全く同じテーブルを本番環境で作ったり、
既存のテーブルのインデックス定義をちょっと変えて作り直したい、そんなケースがあると思います。
マネージメントコンソールで作ってもよいのですが、インデックス名を間違ったり、インデックスが不足したりすると思わぬトラブルになります。(実際ありました)
きちんと、テーブル定義をjsonファイルで管理しておけばよいのですが、そうでない場合は、既存のテーブルからテーブル定義となるjsonを吐き出して、それを読みこませて作成したいと考えると思います。
ただ、aws dynamodb describe-table で吐き出すjsonには、create-table に不要な項目が多数含まれていて、そのままでは実行できません。
「ああjson手で直すのめんどくさいなー」 と弊社今井の周りでつぶやいていたところ、
つ 「出来た」
と下記のようなスクリプトが送られてきました。仕事早っ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
#!/bin/sh # テーブル名指定 $ table_name=TABLE_NAME # テーブル定義抽出 $ aws dynamodb describe-table --table-name ${table_name} | jq '.Table' | jq 'del(.GlobalSecondaryIndexes[].ItemCount)' | jq 'del(.GlobalSecondaryIndexes[].IndexStatus)' | jq 'del(.GlobalSecondaryIndexes[].IndexSizeBytes)' | jq 'del(.GlobalSecondaryIndexes[].ProvisionedThroughput.NumberOfDecreasesToday)' | jq 'del(.GlobalSecondaryIndexes[].ProvisionedThroughput.LastIncreaseDateTime)' | jq 'del(.GlobalSecondaryIndexes[].ProvisionedThroughput.LastDecreaseDateTime)' | jq 'del(.LocalSecondaryIndexes[].IndexStatus)' | jq 'del(.LocalSecondaryIndexes[].ItemCount)' | jq 'del(.LocalSecondaryIndexes[].IndexSizeBytes)' | jq 'del(.LocalSecondaryIndexes[].ProvisionedThroughput.NumberOfDecreasesToday)' | jq 'del(.LocalSecondaryIndexes[].ProvisionedThroughput.LastIncreaseDateTime)' | jq 'del(.LocalSecondaryIndexes[].ProvisionedThroughput.LastDecreaseDateTime)' | jq 'del(.ProvisionedThroughput.NumberOfDecreasesToday)' | jq 'del(.ProvisionedThroughput.LastIncreaseDateTime)' | jq 'del(.ProvisionedThroughput.LastDecreaseDateTime)' | jq 'del(.TableSizeBytes)' | jq 'del(.TableStatus)' | jq 'del(.ItemCount)' | jq 'del(.CreationDateTime)' > ${table_name}.json |
jq でjsonの要素簡単に消せるんですね。
これで出力された json を使って、下記のように作成すればまったく同じ定義のテーブルが作れました。
|
$ aws dynamodb create-table --cli-input-json file://TABLE_NAME.json |
まとめるとこんな感じです。

最初からテーブル定義をjsonで管理する
上記のような事態にならないように、最初からjsonで管理しておけばよかったな、と思ってます。
具体的には、下記のような手順でしょうか。
1)テーブル定義のスケルトンを作成
|
$ aws dynamodb create-table --generate-cli-skeleton > TABLE_NAME.json |
2)スケルトンをベースに編集する
不要な定義消したり、インデックス名をきめたりとか。
3)テーブル作る
|
$ aws dynamodb create-table --cli-input-json file://TABLE_NAME.json |
describe-table とかできちんと出来ているか確認する。
4)git に登録
問題なければ、git などに入れておく。
|
$ git add TABLE_NAME.json |
おわりに
もっとこうしたほうがいいよとか、CloudFormation使うとこんな感じでできるとか、追加情報ありましたら教えていただけると嬉しいです。
あと、プロビジョニングツールとかないのでしょうか。(後から追加できるのがGSIくらいだから需要ない?)