最近の砂場活動その6: パフォーマンスを改善する

最近は登壇とかでエネルギーを取られて、砂場活動シリーズ(?)全然できていませんでした。せっかく前期に砂場を作ったので、勉強の場にしていきます。現状のボトルネックがどこにあって、どこを改善していきましょう、というループを回します。ISUCONとかやってる人には当たり前のことばかりだと思うけど、その辺あまり慣れていないのでぼちぼちとやっていきます。派手なことななく地味にやっていく。

現状を知る

改善をするためには現状のパフォーマンスを知ることが重要です。趣味サイトはユーザーが自分しかいないので、ベンチマークスクリプトをかけてパフォーマンスを見ていきます。スプレッドシートにまとめていくのは面倒だったので、Mackerelのプラグインを書いて可視化しました。

改善していく中でRDS/ElastiCacheのCPU/メモリ/コネクション数/トラフィック量などが一目で分かるようなダッシュボードも作りました。結果ではなく原因になった場所がどこかを調べるのに便利。

数値だけ見ても分からないことも多いので、CloudWatch LogsにアプリやNginxのログを流すように設定。

あれこれ改善

自分しか使っていないとアホなことをやっていても問題にならないけど、ベンチマークを回すとそういったところが問題点になってくるので改善していく。

  • fullscanが走っているところにDBにインデックスを張る
  • コネクションプールを使い回す
  • N+1を潰す
    • 潰しましょう
  • Redisのパイプラインを使う
    • 1つずつコマンドを送るのではなくまとめてやれる方法
  • 必要なフィールドのみ取得するようにする
    • 特徴量やhtmlもredisに雑に放り込んで学習時には使っていたけど、apiのレスポンスを返すときには使っていない(SELECT *的なことになっていた)
    • 結構巨大なフィールドなので、不必要なものは削って必要なものだけ返すようにする
    • jsonとしてredisのvalueに放り込んでたけど、取ってくるフィールドを選択できるようにハッシュ型に変更した
    • https://github.com/syou6162/go-active-learning/pull/31
  • アプリケーションサーバーやDBのリソースは余ってるけど、レイテンシーが悪化するようになってきた
    • ちょっと悩んでしまったけど、負荷試験をされる側ではなくて負荷試験をする側のネットワーク帯域がボトルネックになっていた
    • 負荷試験を複数のインスタンス(Fargate)から実行するようにして負荷分散
  • goroutineでtoo many open files
  • nginxでToo many open files
    • 何やねんと思ったけど、ファイルディスクリプタの上限に引っかかってしまっていた
    • Fargateで動かしている場合も変更できるので増やした

次回

この辺までやるとあとはFargateの数増やしたり、ElastiCache/RDSのスペックを上げたり、リードレプリカの台数を増やせば捌けるリクエスト数も増やしていけそうなかという目処が立ってきた。次回作にご期待ください。

負荷試験をしていたら順調にAWSへの課金額も伸びてきた😇。

理論から学ぶデータベース実践入門 ~リレーショナルモデルによる効率的なSQL (WEB+DB PRESS plus)

理論から学ぶデータベース実践入門 ~リレーショナルモデルによる効率的なSQL (WEB+DB PRESS plus)

特定のエンドポイントの負荷試験の結果を投稿するMackerelプラグインを書いた

これ何

特定のエンドポイントに毎秒XリクエストをY秒流した結果をMackerelのメトリックとして投稿するプラグインを書きました。こんな感じでレスポンスタイムの99パーセンタルなどが可視化できます。

mkrを使っている人は以下でインストールできます。

% mkr plugin install syou6162/mackerel-plugin-request-pressure

当たり前ですが、他人のサイトにリクエストを飛ばしまくって迷惑をかけないようにしてください。

何で作った

Nginxのアクセスログなどがすでにあれば、レイテンシーの99パーセンタイルなどを可視化できるプラグインはすでに存在します。大体のユースケースはこれで十分。

しかし、当たり前ですがある程度流量のあるサイトでないと平均以外の値はあまり信用できません。自分が運用している趣味サイトは訪問者が自分とGoogle botくらいなので、流量が少ないです。ベンチマークスクリプトをかけてログを取ればよいですが、ログを取って可視化までやるのは面倒だなと思ってMackerelのプラグインにしました。プラグインとしてしこんでおけば定常的にレイテンシが分かって便利。-metric-key-prefixを付けておけばエンドポイント毎にグラフを出せます。このプラグイン自体も結構負荷がかかるものなので、負荷試験を行なう目的のサーバーで実行せずmonitoring専用のホストを作ってそこで実行するのがオススメです。

週末にインフラ方面の知識を付ける練習として負荷試験を行なっていたけど、パフォーマンスを継続的に見たりアラート飛ばせるようになったりするので便利。

MACHINE LEARNING Meetup KANSAI #3で機械学習を使った趣味サービスにおける工夫を紹介しました

最近、仕事のちゃんとした登壇が多かったので、趣味でやっているWebサービスにおける機械学習関連の工夫について発表してきました。基本的に昔ブログで紹介したエントリのまとめバージョンです。

趣味の余暇時間で開発しているサービスなので、いかに手間をかけずに済むかというのが大事です。テーマは怠惰!!

効率的なアノテーション方法

機械学習、データ数が多くないとなかなか精度が出ないですが、教師データをたくさん作るのも手間がかかります。そこで能動学習を使って効率的にアノテーションしていくツールを作ったので、それの紹介をしました。

精度の継続的なモニタリング

機械学習のコードでは、テストは通ってたけど実は本番では精度が下っていた...ということが起きがちです。きちんと計測して人間が目視で確認ということが毎回できれば理想的かもしれませんが、趣味でやっているサービスでは手間は最小限にしたいものです。

そこで、テストデータに対する分類性能をMackerelに継続的に送っています(Golangのライブラリを使っています)。グラフで可視化をするとどこで性能が劣化したか分かりますし、性能に対して監視ルールを入れることもできます。グラフにアノテーションをすることもできるので、deploy時のcommit idなどを書いておくと、どのcommitやPull Requestから挙動がおかしくなったかを突き止めやすいので便利です。

多様性を持たせた簡単な推薦方法

二値分類器でよい精度が出たとして、実際にユーザーに結果を提示することを考えると、スコア上位の結果は似たようなもので偏って困ってしまう場合があります。多様性のある推薦方法をお手軽にやるために、劣モジュラ最適化を使った方法を使っています。

組合せ最適化の問題を解くことになるわけですが、Golangで100行もない形で書けますし、貪欲法なので結果を出すのも速いです。貪欲法ではありますが、賢い人が性能の下限はこれより悪くならないよ!というのを証明してくれているので、ある程度安心して使うことができますね。

パイプラインジャングルと戦う

機械学習を使ったサービスは「データのクロールジョブ」「学習ジョブ」「予測ジョブ」など様々なバッチジョブが登場します。昔は「クロールが10分で終わるから15分後に学習ジョブをスタートさせる」「学習は20分で終わるから予測ジョブは30分後にスタートさせる」といった具合にcronやJenkinsで頑張ってジョブを整理していました。しかし、これだと予想より時間がかかった場合に思った挙動をしないとか、前段の処理が失敗した場合の考慮がうまくできず破滅しがちでした。

こういった機械学習に関連するパイプラインをいい感じに扱うツールがいくつかあります。

しかし、趣味でパイプライン管理ツールのお世話までやっているのはちょっとダルいです。そこで、AWSのマネージトサービスであるStep Functionsを使ってパイプラインの管理をすることにしました。

ステートマシーンを書けばよくて、図が出てくるのでどこで失敗したかが一目で分かりますし、失敗したときのretryなども簡単に指定できます。お手軽です。

Pythonではじめる機械学習 ―scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎

Pythonではじめる機械学習 ―scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎

「今日から始める機械学習〜はてなの事例〜」というタイトルでデブサミ2018関西で登壇しました

株式会社ハカルスの染田さん(Machine Learning Meetup KANSAI繋がり)からご紹介頂きまして、タイトルの内容で登壇してきました

登壇に当たって考えたこと

「機械学習を始めたいけど、まだちょっとハードルが高くて踏み出せていない」という方が「機械学習やってみよう!」と思える内容の依頼(大分意訳してるけど)をもらっていました。このテーマ、結構話し方が難しいなと思っていました。というのも、最近の登壇では「機械学習のサービス運用や組織運用、結構大変だけど、はてなではこんな感じでやっています」という話をすることが多かったからです。例えばこれ。

あらためて考えてみると、機械学習ができるとどういういいことがあるんだっけ...というのを考えなおすいい機会になったなぁと思います。機械学習、昔と比べると当たり前のものになってきつつありますが、全ての人が身に付けるべきもの/全てのサービスで導入されるべきかというと、そうではありません。とはいえ、機械学習今よりはもう少し普及して欲しい(特に関西で!)よねと思い、以下の内容で話しました。

  • 始めるハードルは全然高くない
    • 世の中、深層学習使ってないと人ではないみたいな雰囲気が一部であります(?)が、ハイパラチューニング不要の平均化パーセプトロンで十分な応用例は結構あります
    • 学習データも数百件程度で始められますし、AWS Lambdaの無料枠で収まるくらいの計算リソースで始められます
  • 楽しい
    • ペットがどんどん賢くなるようなイメージで段々愛着が湧いてきます
  • これまでのアプリケーションエンジニアの知識やドメイン知識がめちゃくちゃ役に立つ
    • 自分がやることもできそうだけど、データサイエンティストのような専門職の人がやったものには敵わない...? => そんなことはないです
    • 効く特徴量のありかを知っているのはサービスを作っているアプリケーションエンジニア
    • 機械学習の精度に一番効いてくるのはデータ数だが、アプリケーションエンジニアならば定常的にデータを集める仕組みを作ることができる
    • 「アプリケーションエンジニアのスキル + 機械学習のスキル」というよりは「アプリケーションエンジニアのスキル × 機械学習のスキル」で、自分のスキルをさらに生かすことができる手段の一つとして考えてもらえれば

大分エモいですね。

もちろん、Computer visionのようにベースラインが深層学習になってしまったものや生成タスクのように深層学習のほうが比較的向いているタスクもあると思いますが、最初のステップはもっともっと気楽に捉えてよいと思います。手を動かしてみたらうまくいかないのはよくあることなので、Kaggler-ja SlackMachine Learning Meetup KANSAIのような相談できるコミュニティを最後に案内させてもらいました。

この発表を聞いて/スライドを見て、機械学習を始めてくれる人が出てくると私としても嬉しいです。感想お待ちしております。

関連リンク

Mackerel Meetup #12で異常検知機能について発表しました

タイトルの通りですが、Mackerel Meetup #12で登壇してきました。

f:id:syou6162:20180802195908j:plain

ユーザーの皆さんからご要望を直接聞けるので、Meetupは開発者としてもとてもありがたい場になっています。参加してくださった皆さま、ありがとうございました。私が発表したスライドはこちらです。

発表時間が20分だったこともあり詳細は大分割愛していますが、異常検知の手法の詳細や異常検知のような機械学習を作る際の社内の体制をどう作っていったかといった話は過去の発表スライドにありますので、ご興味ある方はこちらも是非ご参照ください。

はてな社内でKaggleハッカソンを行ないました(TakingDataリベンジマッチ編)

先週末、はてな社内でKaggleハッカソンを行ないました。丸一日、各自好きなKaggleのコンペに取り組んで、得られた知見を共有するという会です。

自分は以前TalkingDataというコンペに参加していたのですが、データサイズが結構大きく、一月くらいやってみたももの試行錯誤に四苦八苦してしまい、途中で離脱していました...。このハッカソンでは、そういったデータセットでも何とかできるようになろう!ということを目標にして参加しました。もちろん1日だけではさすがに時間が足りないので、ハッカソン前の10日くらいは定時後にちまちま作業をやっていました。

以下はハッカソン終了後に使った発表資料です。Kaggle上位の人にとっては当たり前のことしか書いてないかもしれませんが、社内でこういった知見をじわじわと貯めていくことが大事だと思っています。なお、ハッカソン終了後にAWSのでかいインスタンスを借りてやりなおしたところ、無事目標のTop 10%入り相当のスコアに到達しました!!

モチベーション

TalkingDataのデータセットについて

  • データサイズ
    • 学習用データサイズがかなり大きい(約1.8億件)
    • ちなみにテストセットも1800万件程度ある
  • 特徴量
    • カラム数自体は少ない、文字列ではなくハッシュ化された数値が与えられる
      • ip, app, device, os, channel, click_time
    • かなりスパース(特にip特徴量)。pd.get_dummiesとかやると即死
  • ラベル
    • is_attributed: アプリをダウンロードしたか(二値)
    • 正例が50万件以下、割合では0.02%程度でextremely unbalanced data
  • 評価指標はAUC

敗因分析

  • 主に2点
    • データの巨大さに負けて、特徴量エンジニアリングの工夫がなかなかできなかった
    • クロスバリデーションなしで学習が6時間くらいかかり、パラメーターチューニングも満足にできない
  • 以下の2つについて取り組んだ
    • Negative down sampling
    • Bayesian optimization

Negative down sampling

  • 正例と同じ分量になるだけ正例をサンプリングして学習データにする方法
  • 元々の学習データ全体と比べて1%以下のデータ量になり、試行錯誤がとてもやりやすい
    • 計算時間もメモリ量も
  • 負側のサンプリングは何回も別のセットを作ることができるので、その回数だけ学習器を作ってbagging(例: rank averaging)することで最終的な精度向上も目指すことも
  • TalkingData AdTracking Fraud Detection Challenge (1st place solution)

Bayesian optimization(モチベーション)

  • KaggleをやりだすようになってからLightGBMという決定木ベースのものを使うように
    • 使いやすさ(スケーリングなどが多少不要になる。欠損値もよしなに)や精度面でもよい
  • 一方で、これまで使うことが多かったSVMやロジステック回帰と比べて、ハイパーパラメータが多く、チューニングが大変になった
    • 学習率、葉の数、binの数、samplingのratioなどなど10くらいある
  • ハイパーパラメータで時間を食ってしまうため、試行錯誤の回数を増やせない
  • ベイズ最適化でチューニングの回数を減らせないか

ハイパーパラメータの例

それぞれのパラメータを4-5個の選択肢を用意して真面目にCVすると結構大変。

params = {
    'learning_rate': 0.01,
    'num_leaves': 255,  # 2^max_depth - 1
    'max_depth': 8,  # -1 means no limits
    'min_child_samples': 200,  # Minimum number of data need in a child(min_data_in_leaf)
    'max_bin': 255,  # Number of bucketed bin for feature values
    'subsample': 0.9,  # Subsample ratio of the training instance.
    'subsample_freq': 1,  # frequence of subsample, <=0 means no enable
    'colsample_bytree': 0.5,  # Subsample ratio of columns when constructing each tree.
    'min_child_weight': 0,  # Minimum sum of instance weight(hessian) needed in a child(leaf)
    'scale_pos_weight': 1,
    'reg_lambda': 0.0,  # L2 regularization term on weights
}

脱線: ハイパラチューニングでそこまで精度が劇的に上がることは少ないのでは?

  • それはその通り
  • しかし、チューニング結果を人が全て見れるわけではないケースも社内で増えつつある
    • 例: 異常検知などの再学習
  • 以下を同時に叶えたい
    • チューニングをミスって極端に悪くならないようにしたい
    • 多数のハイパラのグリッドサーチはメモリも計算時間もかかるからはしょりたい
  • Kaggleで多少精度が欲しいときに役に立つ…かも?

Baysian optimization(方法論)

  • ベイズ的最適化(Bayesian Optimization)の入門とその応用
  • 入力をハイパーパラメター、出力をスコア(例: AUCやloglossなど)とする関数回帰の問題と見なす
  • ガウス過程は関数回帰をやってくれる代表選手であり、関数空間の事後分布を計算できる
  • 次にどこハイパーパラメータを試行錯誤するか、事後分布の広がりが一番大きい点を優先的に探す
  • グリッドサーチやランダムサーチと比べて、データにどのような特徴があるを考えてハイパーパラメータの探索を行なうため、学習回数を減らせる
  • 補足: やってること、どこかで見たことありますよね
    • 多椀バンディットのUCBとかと考え方が似ている(し、実際に関係ある)

ハッカソンでの初期状態

  • LB(leaderboard)のprivateスコアが0.9691844
  • 特徴量は基本的なもの
    • ip, app, device, os, channel ,click_time
    • ['ip', 'device', 'os']でグルーピングしたときのappの異なり数など
      • この辺がめっちゃ効くので、工夫したい
  • 分類器はLightGBM。ハイパラチューニングはそんなに頑張ってない
    • が、メモリに乗る学習データがせいぜい8000万件、学習もCVなしで(確か)6時間かかる… 😇

初手

Negative Down Samplingをやってみる

  • 1.8億件の中から負例を正例と同数サンプリング
    • 学習データは約100万件程度まで落ちる
    • 特徴量は変えない
  • LightGBMを使って学習
    • めっちゃ早くなる(パラメータによるけど、1分くらいで終わる)
  • AUCが落ちる 😇 (0.95程度まで落下)

RankAveragingをやってみる

  • もしかしてbagging必須?!と思ってひとまずやってみる
  • rank averagingをやってみる
  • 若干上がるが、0.96に全く届かない…

教訓: aggregation系の特徴量が入っている場合は特徴量生成でサンプリングするとダメ

  • 「この事例と同じip/deviceを持っている事例の数」といったaggregation(sum/cumsum/ratio/{prev,next}_click_time_diffなど)系の操作が入る特徴量を使う場合、サンプリング後のデータフレームに対して特徴量を作っていたのがいけなかった
    • ipのようなsparseな特徴量の場合、サンプリングすると出る/出ないで結果が大きく変わってしまうため
  • ダメな例: 元データ => negative down sampling => 特徴量生成(10分) => 学習(1分)
  • よい例: 元データ => 特徴量生成(1時間) => negative down sampling => 学習(1分)

Negative down samplingのやり方を改善した結果

  • 学習データ100万件のみで元の精度と同じ結果までいけた
  • rank averagingをすることで多少上がった(0.97に乗るくらい)
    • とはいえ、割とすぐにサチる(5〜10くらいで十分)
  • 試行錯誤がとてもやりやすくなった 🎉
    • パラメータ変えたいときにも特徴量生成待たなくてよくなった
    • 特徴量足したいときにも新しく列を追加するだけでよくなった

特徴量生成

  • 特徴量を変えて実験をする度に毎回1時間待っているのはダルい
  • 列毎に結果を吐いて学習時に列と足し込んでいく形式でやると時間をかけずにやれて便利
  • BigQueryでこういった特徴量を作れるとよさそうですね
    • 30分程度でできるらしい。11th solution

Bayesian optimizationやってみる

  • 色々パッケージがあるけど、skoptを使ってみる
  • ベイズ最適化自体の計算は軽い
  • LightGBMのようにハイパーパラメータが多く、人手で結果を見てられない場合はまあまあ有用?
    • 例: dailyで再学習が必要な場合
    • 人手で結果を毎度見れるようなものでは普通のグリッドサーチで十分か
space = [
    Integer(3, 31, name='max_depth'),
    Integer(7, 1023, name='num_leaves'),
    Integer(50, 1000, name='min_child_samples'),
    Real(1, 999, "log-uniform", name='scale_pos_weight'),
    Real(0.6, 0.9, name='subsample'),
    Real(0.4, 0.9, name='colsample_bytree'),
    Real(0.000001, 0.1, "log-uniform", name='learning_rate'),
    Real(0.000001, 0.1, "log-uniform", name='reg_lambda')
]

res = gp_minimize(baysian_optimization_objective, space, n_calls=100)
print(res)
print(res.fun)
print(res.x)

ベイズ最適化所感

  • ベイズ最適化を使っただけで精度が上がった、というのはtalkingdataでは特になかった
  • cross-validationやってられん!という場合には使ってもよいかも
  • 方法論としては深層学習のチューニングにも使える

最終結果

  • LBのprivateスコアが0.9815761(525位/Top 13.2%)
    • あと一歩足りなかった…
  • negative down samplingで高速にiterationを回せるようになった後であれこれ追加した特徴量が一番効いた

その他面白いやつを共有

pickleの代わりにfeatherを使う

  • 特徴量を追加したDataFrameをpickleでファイルに保存したい
  • pickleのread/write時に結構なメモリ量を消費する
    • せっかく特徴量を作ったのにOOMで死んで水の泡 😇
  • featherだともっと省メモリ、高速に読み書きできる
    • 5分くらいかかっていたのが1分くらいで終わる

pandas芸

  • 特定の条件でグルーピングした上で、そのグループ内で次のクリック時間の計算を効率的に
  • SQLのwindow関数とかでも同じようなことができる
train_df['click_time'] = (train_df['click_time'].astype(np.int64, copy=False) // 10 ** 9).astype(np.int32, copy=False)
train_df['next_click'] = (train_df.groupby(['ip', 'app', 'device', 'os']).click_time.shift(-1) - train_df.click_time).astype(np.float32, copy=False)

validationデータの切り方

  • 学習データをシャッフルしてvalidationを作ると、validationでのAUCとtestでのAUCが結構違う
    • 時系列要素が強いデータなので
  • 最後の1日をvalidationにするとtestとのAUCの乖離が小さくなるので安心
  • 手元のvalidationとLBのスコアが乖離しないようにまずやりましょう

参考

前処理大全[データ分析のためのSQL/R/Python実践テクニック]

前処理大全[データ分析のためのSQL/R/Python実践テクニック]

最近の砂場活動その5: AWS Step Functionsで機械学習のワークフローの管理をする

はてなブログのHTTPS配信をやっていた同僚からAWS Step Functionsはいいぞ!というのを教えてもらいました(発表資料)。機械学習のワークフロー管理にもこれは便利そうだなーと思って、自分でも試してみました。やってる内容はN番煎じです...。

機械学習とワークフローの管理

状態を持つワークフローの管理、機械学習でも難しいので悩むところですね。例えば

  • データの取得
  • 前処理
  • 特徴量の生成
  • モデルの学習
  • 検証データに対する精度をトラッキングできるように記録
  • S3等に学習済みのモデルファイルを配置
  • 新しいデータに対して予測を行なう
  • 全てが終わったらslackに通知

などがぱっと上げられますが、ときどきどこかがエラーでこけます。エラーでこけていてもretryして解決するならretryして欲しいし、何度も失敗するようだったらexponential backoffしながらretryして欲しいです。あまりに何度も失敗するようなら系として失敗にして欲しいし、系として失敗したならslackで通知をしたりフローの中のどこでこけたのかが一目瞭然に分かって欲しいものです。この問題の解決方法としてフローのどこの処理を現在行なっていて、どのジョブは前回どういう状態だったか(成功したのか、何回失敗したのかなど)を記録しておくという方法が考えられます。しかし、これを自前でやるのは明らかに面倒です。こういったことを引き受けてくれるマネージドサービスの一つにAWS Step Functionsがあります。

機械学習のワークフローをStep Functionsで管理する

ML Newsで定期的に機械学習関係のワークフローを回しているので、これをStep Functionsで管理します。小さなアプリケーションなのであまり大したことはしていないですが、以下の2つのことをしています。

  • 1: Twitterで話題になっているURLをクローリング(特徴量で必要になる本文等を取得)
  • 2: 学習データから分類器を構築、新規のURLに対して分類*1、推薦リストを更新する

1が終わる前に2が始まっても仕方ないですし、1が何らかの理由で失敗していたら2は開始しないで欲しいです*2。こういった制御をStep Functionsにやらせます。Step Functionsでワークフローの管理をするには、ステートマシーンを書くだけです。見てもらったら分かると思うけど、ステートマシンはこういうやつです。

フローが図として分かるの最高だし、水色の進行中のところを見れば今どこの処理をやっているか分かります。赤の失敗のところを見ればどこでこけたかがログを見なくても一目で分かりますし、失敗したステートをクリックするとそのコンポーネントのエラーログを見ることができます。長大になっているワークフローのどこでこけているかCloudWatch Logsから根性で探すというのは地獄なので、かなり便利であることが分かりますね。

詳細は他の方が書いているエントリを見てくれ!!

参考までに今回ステートマシンを作ったCloudFormationのコードの断片を置いておきます。

ステートマシンを構築するCloudFormation

  MyStateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      StateMachineName: HelloWorld-StateMachine
      RoleArn: 
        Fn::ImportValue:
          !Sub "${IAMStackName}:StepFunctionsRole"
      DefinitionString: !Sub |
        {
          "StartAt": "AddRecentUrls",
          "States": {
            "AddRecentUrls": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:BatchJobTriggerResource",
              "Next": "WaitXSecondsForAddingRecentUrls"
            },
            "WaitXSecondsForAddingRecentUrls": {
              "Type": "Wait",
              "Seconds": 60,
              "Next": "GetJobStatusOfAddingRecentUrls"
            },
            "GetJobStatusOfAddingRecentUrls": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:PollCheckJobFunction",
              "ResultPath": "$.status",
              "Next": "HasJobCompleteAddingRecentUrls"
            },
            "HasJobCompleteAddingRecentUrls": {
              "Type": "Choice",
              "Choices": [
                {
                  "Variable": "$.status",
                  "StringEquals": "FAILED",
                  "Next": "JobFailedAddingRecentUrls"
                },
                {
                  "Variable": "$.status",
                  "StringEquals": "SUCCEEDED",
                  "Next": "UpdateRecommendation"
                }
              ],
              "Default": "WaitXSecondsForAddingRecentUrls"
            },
            "JobFailedAddingRecentUrls": {
              "Type": "Fail",
              "Cause": "AWS Batch Job Failed",
              "Error": "DescribeJob returned FAILED"
            },
            "UpdateRecommendation": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:UpdateRecommendationBatchJobTriggerResource",
              "Next": "WaitXSecondsForUpdatingRecommendation"
            },
            "WaitXSecondsForUpdatingRecommendation": {
              "Type": "Wait",
              "Seconds": 60,
              "Next": "GetJobStatusOfUpdatingRecommendation"
            },
            "GetJobStatusOfUpdatingRecommendation": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:PollCheckJobFunction",
              "ResultPath": "$.status",
              "Next": "HasJobCompleteUpdatingRecommendation"
            },
            "HasJobCompleteUpdatingRecommendation": {
              "Type": "Choice",
              "Choices": [
                {
                  "Variable": "$.status",
                  "StringEquals": "FAILED",
                  "Next": "JobFailedUpdatingRecommendation"
                },
                {
                  "Variable": "$.status",
                  "StringEquals": "SUCCEEDED",
                  "Next": "JobSucceedUpdatingRecommendation"
                }
              ],
              "Default": "WaitXSecondsForUpdatingRecommendation"
            },
            "JobFailedUpdatingRecommendation": {
              "Type": "Fail",
              "Cause": "AWS Batch Job Failed",
              "Error": "DescribeJob returned FAILED"
            },
            "JobSucceedUpdatingRecommendation": {
              "Type": "Succeed"
            }
          }
        }

上記のStep Functionsを定期的に起動するLambdaとCloud Watch Events Rule

  StepFunctionsTrigger:
    Type: AWS::Lambda::Function
    Description: "データ取得→学習および推薦リストの作成をやるStepFunctionsをkickするLambda Function"
    Properties:
      FunctionName: StepFunctionsTrigger
      Role: 
        Fn::ImportValue: !Sub "${IAMStackName}:StepFunctionsRole"
      Handler: index.lambda_handler
      Runtime: python3.6
      MemorySize: 128
      Timeout: 10
      Environment:
        Variables:
          STATE_MACHINE_ARN: !Ref MyStateMachine
      Code:
        ZipFile: |
          import os
          import boto3
          from datetime import datetime as dt
          
          client = boto3.client('stepfunctions')
          def lambda_handler(event, context):
              try:
                  response = client.start_execution(
                      stateMachineArn=os.environ['STATE_MACHINE_ARN'],
                      name=dt.now().strftime('%Y_%m_%d_%H_%M_%S'),
                  )
              except Exception as e:
                  raise e
  StepFunctionsTriggerRule:
    Type: AWS::Events::Rule
    Properties:
      Name: StepFunctionsTriggerRule
      ScheduleExpression: rate(3 hours)
      Targets:
        - Id: StepFunctionsTrigger
          Arn: !GetAtt StepFunctionsTrigger.Arn
      State: "ENABLED"
  StepFunctionsPermissionForEventsToInvokeLambda:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref StepFunctionsTrigger 
      SourceArn: !GetAtt StepFunctionsTriggerRule.Arn
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com

AWS Step Functionsのモニタリング

ステートマシンの状態遷移を見ているのは楽しいですが、監視はモニタリングツールに任せたいですね。Mackerelをお使いの方はプラグインを入れてもらうとすぐにモニタリングできます。

実行時間や失敗した数などがメトリックとして取れるので、適宜監視を仕込むと安心です。

f:id:mackerelio:20180420191310p:plain

AWS Step Functionsの類似ツール

ワークフロー管理はAWS Step Functionsが初めて出したわけではなく、同じようなものがすでにいくつかあります。

これらのツールを使ったことがあるわけではないので的を外しているかもしれませんが、AWS Step Functionsを使うと

  • AWS Batch/Lambda/ECSのタスク/EMRなど既存のAWSスタックとの連携がスムーズにできる
  • マネージトサービスなので、ワークフロー監視システム自体の管理をする必要がない

といったところが利点かなと思います。

Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂版

Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂版

*1:バッチでまとめてやっています

*2:これまではCloudWatch Eventで適当に回していて、これくらい立ったら1が終わってるから2をやればよかろうとやっていたり、1が失敗しても2は問答無用で走る形になっていました...