AWS
lambda
stepfunctions

1年を振り返るサービスをStep Functionsで実装してみる

More than 1 year has passed since last update.

この記事はServerless Advent Calendar 2016の12日目の記事になります。
遅くなり失礼いたしました。

ここ数年、年末になると1年を振り返るサービスを開発しておりまして、簡単なシステムながら毎年多くの方にご利用頂いております。(毎年、年末年始がほぼない状況でお仕事しており、年明け早々家庭内で険悪なムードが流れることも)

基本構成

現在提供しているサービスの概要ですが、
1. 対象ユーザのIDを入力してもらう
2. 暫く待つ
3. 1年間の写真の中から最も人気のあった写真9枚がまとめった画像が表示される
といった極めてシンプルなサービスです。

フルAWSで構築しておりまして、構成はこの通り、一般的な構成となっております。

  • DNSをRoute53で管理
  • ロードバランサとしてELB(ACMから証明書利用)
  • webサーバ群はt2インスタンスでCloudWatchを活用したAuto Scaling
  • webサーバからSQSへエンキュー
  • 裏で動いているワーカがSQSからデキュー
  • 人気のある写真をページング検索
  • 画像を生成したらS3へアップロード
  • ユーザはCloudFront経由で画像を取得
  • ワーカはc4のスポットで1/8程度のコストに抑える
  • ワーカもCloudWatchを活用したAuto Scaling

スクリーンショット 2016-12-13 18.21.26.png

サーバレスで運用したい!

さて、システムを開発・運用するにあたり色々と問題がありまして…。
バイラルの性質を持つため、リアルタイムアクセスが1000〜10万〜それ以上と大きくスケールが変化し予測も難しいという環境の中で、肝としては、

  • 如何にワーカのコストを下げるか
  • Auto Scalingのパラメータを適切に調整してちゃんとスケールさせる

ですが、

  • 🔥スケーリングのパラメータは結構地味に調整していかないとインスタンスを増やしすぎることがあったり、
  • 🔥🔥🔥🔥スポットインスタンスで運用しているインスタンス群の価格が大きく上昇することがあり他の群に引っ越す必要があったり(例えばc4.8xlargeが通常$0.4程度が$2ぐらいまで上昇しちゃったときに、c4.4xlargeは$0.2程度だったり。よく有ります)

という大きな問題があり、なかなか手放し運用が難しく年末の忙しい時間をいとも簡単に奪い取られてしまいます…。今年はサーバレスで運用して楽したい!

サーバレスシステムの検討

さて、上記のシステムをサーバレスにする場合、単純に考えるとこうなると思います。
スクリーンショット 2016-12-13 19.45.35.png

  • html、js、cssをS3に配置し、S3のweb hostingインタフェース
  • jsのSDKからCognitoのゲスト認証でLambdaをInvoke
  • Lambdaで、ページング検索して人気のある画像を取得し、画像を生成しS3にアップロード

ただし、この構成の場合、ページング検索の都合上、投稿が大量にあるユーザと少量のユーザとで時間が違いすぎ、Lambdaの実行にかかる時間が読めずタイムアウトを適切に設定できないばかりか、時間内に終わることが出来ない問題も発生してしまいます。

この問題は、Lambdaをsearch用とgenerate用に分割し、search用ではページング検索をせず、searchからsearchを呼ぶようにして、全て検索完了するとgenerate用Lambdaに渡すことで解決出来ました

スクリーンショット 2016-12-13 19.56.56.png

しかしこの場合、処理の全体進捗が分かりづらい問題が発生しています。
なんか、先日のre:Invent 2016で発表されたStep Functionsが、複数のLambdaを繋ぐ今回の構成に適していそうですね!

ステップファンクションで少し組んでみた

ということでやってみました。(4日目 とテーマ被っていてごめんなさい)

早速ですが、こちらが生成したGraphです。

スクリーンショット 2016-12-13 20.22.30.png

Codeはこちら

{
  "Comment": "An example of the Amazon States Language using a choice state.",
  "StartAt": "SearchingState",
  "States": {
    "SearchingState": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:${region}:${account}:function:searchingState",
      "Next": "ChoiceState",
      "InputPath": "$",
      "ResultPath": "$",
      "Retry": [
        {
          "ErrorEquals": [ "States.TaskFailed" ],
          "IntervalSeconds": 5,
          "MaxAttempts": 10,
          "BackoffRate": 2.0
        },
        {
          "ErrorEquals": ["States.Timeout"],
          "IntervalSeconds": 5,
          "MaxAttempts": 5,
          "BackoffRate": 2.0
        }
      ],
      "Catch": [
        {
          "ErrorEquals": [ "States.ALL" ],
          "Next": "DeadState"
        }
      ]
    },
    "ChoiceState": {
      "Type" : "Choice",
      "Choices": [
        {
          "Variable": "$.contentsCount",
          "NumericEquals": 0,
          "Next": "NoResultState"
        },
        {
          "Variable": "$.maxId",
          "NumericGreaterThan": 0,
          "Next": "SearchingState"
        },
        {
          "Variable": "$.maxId",
          "NumericEquals": 0,
          "Next": "ResultState"
        }
      ],
      "Default": "SearchingState"
    },

    "ResultState": {
      "Type" : "Task",
      "InputPath": "$",
      "Resource": "arn:aws:lambda:${region}:${account}:function:resultState",
      "End": true
    },


    "NoResultState": {
      "Type" : "Task",
      "InputPath": "$.username",
      "Resource": "arn:aws:lambda:${region}:${account}:function:noResultState",
      "End": true
    },

    "DeadState": {
      "Type" : "Task",
      "InputPath": "$.username",
      "Resource": "arn:aws:lambda:${region}:${account}:function:deadState",
      "End": true
    }
  }
}

設計ポイント

InputPathとOutputPathがちょっと分かりづらかったです。API GWを触ったことをある人なら理解しやすいと思いますが、JSONPath形式なので、API GWとLambdaを繋ぐように書けば良い感じです。
サンプルでは例がありませんでしたが、Lambdaへパラメータを全て渡したり、Lambdaから出てくる結果を全て受け取る場合は、「$」を指定すると良いです。

STATE InputPath OutputPath 概要
SearchingState $ (username / results) $ (username / contents / maxId / contentsCount) 検索実行
ChoiceState $ (contentsCount / maxId) (through) 選択して遷移する
ResultState $ (username / contents / contentsCount / likeCount) - 結果を生成して終了
NoResultState $.username - 結果なしで終了
DeadState $.username - リトライ回数を超えたものを別のキューに積んで終了

あとは、普通にLambdaを書いていけば良いので困ることはなさそうです。
とりあえず最初は、「$」にしておくと 、開発段階では捗ると思います。

実行してみた

本番コードと同様の処理を実際に組んで実行してみました。
結構ステップ数多くなるなというのが正直なところです。

147投稿あるユーザの場合 (98ステップ)

スクリーンショット 2016-12-13 20.59.08.png

ID Type Timestamp
1 ExecutionStarted Dec 13, 2016 8:20:23 PM
2 TaskStateEntered Dec 13, 2016 8:20:23 PM
3 LambdaFunctionScheduled Dec 13, 2016 8:20:23 PM
4 LambdaFunctionStarted Dec 13, 2016 8:20:23 PM
5 LambdaFunctionSucceeded Dec 13, 2016 8:20:25 PM
6 TaskStateExited Dec 13, 2016 8:20:25 PM
7 ChoiceStateEntered Dec 13, 2016 8:20:25 PM
8 ChoiceStateExited Dec 13, 2016 8:20:25 PM
9 TaskStateEntered Dec 13, 2016 8:20:25 PM
10 LambdaFunctionScheduled Dec 13, 2016 8:20:25 PM
11 LambdaFunctionStarted Dec 13, 2016 8:20:25 PM
12 LambdaFunctionSucceeded Dec 13, 2016 8:20:26 PM
13 TaskStateExited Dec 13, 2016 8:20:26 PM
14 ChoiceStateEntered Dec 13, 2016 8:20:26 PM
15 ChoiceStateExited Dec 13, 2016 8:20:26 PM
16 TaskStateEntered Dec 13, 2016 8:20:26 PM
17 LambdaFunctionScheduled Dec 13, 2016 8:20:26 PM
18 LambdaFunctionStarted Dec 13, 2016 8:20:26 PM
19 LambdaFunctionSucceeded Dec 13, 2016 8:20:27 PM
20 TaskStateExited Dec 13, 2016 8:20:27 PM
21 ChoiceStateEntered Dec 13, 2016 8:20:27 PM
22 ChoiceStateExited Dec 13, 2016 8:20:27 PM
23 TaskStateEntered Dec 13, 2016 8:20:27 PM
24 LambdaFunctionScheduled Dec 13, 2016 8:20:27 PM
25 LambdaFunctionStarted Dec 13, 2016 8:20:27 PM
26 LambdaFunctionSucceeded Dec 13, 2016 8:20:28 PM
27 TaskStateExited Dec 13, 2016 8:20:28 PM
28 ChoiceStateEntered Dec 13, 2016 8:20:28 PM
29 ChoiceStateExited Dec 13, 2016 8:20:28 PM
30 TaskStateEntered Dec 13, 2016 8:20:28 PM
31 LambdaFunctionScheduled Dec 13, 2016 8:20:28 PM
32 LambdaFunctionStarted Dec 13, 2016 8:20:28 PM
33 LambdaFunctionSucceeded Dec 13, 2016 8:20:28 PM
34 TaskStateExited Dec 13, 2016 8:20:28 PM
35 ChoiceStateEntered Dec 13, 2016 8:20:28 PM
36 ChoiceStateExited Dec 13, 2016 8:20:28 PM
37 TaskStateEntered Dec 13, 2016 8:20:29 PM
38 LambdaFunctionScheduled Dec 13, 2016 8:20:29 PM
39 LambdaFunctionStarted Dec 13, 2016 8:20:29 PM
40 LambdaFunctionSucceeded Dec 13, 2016 8:20:29 PM
41 TaskStateExited Dec 13, 2016 8:20:29 PM
42 ChoiceStateEntered Dec 13, 2016 8:20:29 PM
43 ChoiceStateExited Dec 13, 2016 8:20:29 PM
44 TaskStateEntered Dec 13, 2016 8:20:29 PM
45 LambdaFunctionScheduled Dec 13, 2016 8:20:29 PM
46 LambdaFunctionStarted Dec 13, 2016 8:20:29 PM
47 LambdaFunctionSucceeded Dec 13, 2016 8:20:30 PM
48 TaskStateExited Dec 13, 2016 8:20:30 PM
49 ChoiceStateEntered Dec 13, 2016 8:20:30 PM
50 ChoiceStateExited Dec 13, 2016 8:20:30 PM
51 TaskStateEntered Dec 13, 2016 8:20:30 PM
52 LambdaFunctionScheduled Dec 13, 2016 8:20:30 PM
53 LambdaFunctionStarted Dec 13, 2016 8:20:30 PM
54 LambdaFunctionSucceeded Dec 13, 2016 8:20:31 PM
55 TaskStateExited Dec 13, 2016 8:20:31 PM
56 ChoiceStateEntered Dec 13, 2016 8:20:31 PM
57 ChoiceStateExited Dec 13, 2016 8:20:31 PM
58 TaskStateEntered Dec 13, 2016 8:20:31 PM
59 LambdaFunctionScheduled Dec 13, 2016 8:20:31 PM
60 LambdaFunctionStarted Dec 13, 2016 8:20:31 PM
61 LambdaFunctionSucceeded Dec 13, 2016 8:20:31 PM
62 TaskStateExited Dec 13, 2016 8:20:31 PM
63 ChoiceStateEntered Dec 13, 2016 8:20:31 PM
64 ChoiceStateExited Dec 13, 2016 8:20:31 PM
65 TaskStateEntered Dec 13, 2016 8:20:31 PM
66 LambdaFunctionScheduled Dec 13, 2016 8:20:31 PM
67 LambdaFunctionStarted Dec 13, 2016 8:20:32 PM
68 LambdaFunctionSucceeded Dec 13, 2016 8:20:32 PM
69 TaskStateExited Dec 13, 2016 8:20:32 PM
70 ChoiceStateEntered Dec 13, 2016 8:20:32 PM
71 ChoiceStateExited Dec 13, 2016 8:20:32 PM
72 TaskStateEntered Dec 13, 2016 8:20:32 PM
73 LambdaFunctionScheduled Dec 13, 2016 8:20:32 PM
74 LambdaFunctionStarted Dec 13, 2016 8:20:32 PM
75 LambdaFunctionSucceeded Dec 13, 2016 8:20:33 PM
76 TaskStateExited Dec 13, 2016 8:20:33 PM
77 ChoiceStateEntered Dec 13, 2016 8:20:33 PM
78 ChoiceStateExited Dec 13, 2016 8:20:33 PM
79 TaskStateEntered Dec 13, 2016 8:20:33 PM
80 LambdaFunctionScheduled Dec 13, 2016 8:20:33 PM
81 LambdaFunctionStarted Dec 13, 2016 8:20:33 PM
82 LambdaFunctionSucceeded Dec 13, 2016 8:20:34 PM
83 TaskStateExited Dec 13, 2016 8:20:34 PM
84 ChoiceStateEntered Dec 13, 2016 8:20:34 PM
85 ChoiceStateExited Dec 13, 2016 8:20:34 PM
86 TaskStateEntered Dec 13, 2016 8:20:34 PM
87 LambdaFunctionScheduled Dec 13, 2016 8:20:34 PM
88 LambdaFunctionStarted Dec 13, 2016 8:20:34 PM
89 LambdaFunctionSucceeded Dec 13, 2016 8:20:35 PM
90 TaskStateExited Dec 13, 2016 8:20:35 PM
91 ChoiceStateEntered Dec 13, 2016 8:20:35 PM
92 ChoiceStateExited Dec 13, 2016 8:20:35 PM
93 TaskStateEntered Dec 13, 2016 8:20:35 PM
94 LambdaFunctionScheduled Dec 13, 2016 8:20:35 PM
95 LambdaFunctionStarted Dec 13, 2016 8:20:35 PM
96 LambdaFunctionSucceeded Dec 13, 2016 8:20:36 PM
97 TaskStateExited Dec 13, 2016 8:20:36 PM
98 ExecutionSucceeded Dec 13, 2016 8:20:36 PM

投稿がない(取得できない)ユーザの場合 (14ステップ)

スクリーンショット 2016-12-13 21.17.03.png

ID Type Timestamp
1 ExecutionStarted Dec 13, 2016 8:41:06 PM
2 TaskStateEntered Dec 13, 2016 8:41:06 PM
3 LambdaFunctionScheduled Dec 13, 2016 8:41:06 PM
4 LambdaFunctionStarted Dec 13, 2016 8:41:06 PM
5 LambdaFunctionSucceeded Dec 13, 2016 8:41:07 PM
6 TaskStateExited Dec 13, 2016 8:41:07 PM
7 ChoiceStateEntered Dec 13, 2016 8:41:07 PM
8 ChoiceStateExited Dec 13, 2016 8:41:07 PM
9 TaskStateEntered Dec 13, 2016 8:41:07 PM
10 LambdaFunctionScheduled Dec 13, 2016 8:41:07 PM
11 LambdaFunctionStarted Dec 13, 2016 8:41:08 PM
12 LambdaFunctionSucceeded Dec 13, 2016 8:41:08 PM
13 TaskStateExited Dec 13, 2016 8:41:08 PM
14 ExecutionSucceeded Dec 13, 2016 8:41:08 PM

試算

  • Step Functionsは、状態遷移ごとに0.0000025ドル
  • 投稿数平均150
  • 利用ユーザ1000万

とすると、3750ドルですね!
1000万ユーザに対して3750ドルは安いのか高いのか…これ以外にもLambdaの利用料金だったり、外部との転送料金だったりが掛かってくるので、この規模ではコスト的に見合わない可能性がありますが、もう少し規模の小さいシステムであれば問題なさそうな気もします。

ハマったところ

エラー表示

とりあえず訳の分からないエラーが出がちです。一番ハマったのはこちら
スクリーンショット 2016-12-13 1.59.19.png

なんでしょうねこれ…。一個ずつ潰したところ、ここが問題でした。
スクリーンショット 2016-12-13 1.59.26.png

この空白が問題だったのですが、あんなエラーを出さなくても…。

文字数制限

ドキュメントに書いてあるのですが、Step FunctionではInput/Outputの最大文字数は32,000字です。
この時、LambdaFunctionStartedで止まり、Lambdaが実行されないため、Functionsが実行中のまま動かなくなるので注意が必要です。(手動でAbortする必要があります)

なんか色々と大変だけど使ってみよう

まとめとしては、【やっていく気持ち】ですね。
とりあえず、トラフィック少し流して実験してみようと思います。

Step Functionsを触る際には、ドキュメントをしっかりと読み込むことをオススメします。開発スピードに雲泥の差が出てきます。
AWS Step Functions Document
Amazon States Languages
JSON Path