どうもこんにちは。エンジニアの わんこ。 です。
前回のエントリーLiVE CMを支える技術でご紹介させていただきました 負荷シュミレートツール 千手観音 ですが、自身で使っているうちに致命的な欠点を見つけてしまい、バージョンアップしましたのでご紹介させていただきます。
千手観音ver1.0の致命的な欠点
千手観音では、アタッカーのLambdaFunctionのステート管理をDynamoDBを使って実装していたのですが、DynamoDBはキャパシティーの縮小は一日に4回までという制限がありました。
1,000個のLambdaFunctionを同時に並べて攻撃をする際に、各FunctionのステートをDynamoDBで更新するので、キャパシティーを一時的に上げ、攻撃完了後にまた下げる必要があります。 そうしないとせっかく安価に攻撃ができるツールなのに、DynamoDBのキャパシティーが休眠中も課金され続け高額な請求になりえるからです。
しかし、4回という制限があり、自動でキャパシティーの上げ下げをしてくれなくなり結局手動で変更する必要が出てきました。
StepFunctions の導入
- 脱DynamoDBでのステート管理
- 動的なStateMachineの生成/削除
- 結果的にコストダウン成功
StepFunctionsを使うと、各コンポーネントのワークフローを簡単に構築することが出来ます。 コンポーネントごとの結果による分岐や、並列実行も可能です。 今までDynamoDBを使って並列実行させていたLambdaFunctionのステート管理を全てStepFunctionsに任せることが可能になりました。
StepFunctionsはAPI経由でStateMachineを簡単に構築することが可能なので、千手観音では攻撃要求を受けた際に最適なStateMachineを動的に生成し、完了後には削除を行うようにしています。
また、StepFunctions自体は無料で利用できるので、DynamoDBを利用しなくなった分コストがお安くなったのも結果的にですが嬉しいことでした。
StateMachineの構築
aws-sdk-goを利用し、StateMachine作成用のDefinition構築はこのように書くことができます(一部抜粋
func createDefinition(reqID string, workerCount int) string { // attackerの横並びを作ります。 branches := make([]DefinitionParallelBranch, workerCount) for index := 0; index < workerCount; index++ { s := make(map[string]stater) stateName := fmt.Sprintf("Attack%06d", index) s[stateName] = DefinitionTask{ State: State{ Type: "Task", Comment: fmt.Sprintf("%s-attack-%06d", reqID, index), End: true, InputPath: "$", }, Resource: getAttackerArn(), } b := DefinitionParallelBranch{ StartAt: stateName, States: s, } branches[index] = b } // 横並びのまとめの作成 states := make(map[string]stater) states["Attackers"] = DefinitionParallel{ State: State{Type: "Parallel"}, Next: "Summary", Branches: branches, } states["Summary"] = DefinitionTask{ State: State{Type: "Task", End: false}, Next: "Notify", Resource: getSummaryArn(), } states["Notify"] = DefinitionTask{ State: State{Type: "Task", End: false}, Next: "Deleter", Resource: getNotifyArn(), } states["Deleter"] = DefinitionTask{ State: State{Type: "Task", End: true}, Resource: getStateDeleteEnqueArn(), } def := Definition{ Comment: fmt.Sprintf("thgom-statemaschine-%s", reqID), StartAt: "Attackers", States: states, } json, _ := json.Marshal(def) j := string(json) return j }
実際に上記のコードで生成されるStateMachineのビジュアルワークフローは このような形になります。 しかも実行中はコンポーネントごとに色が変わってとてもわかりやすいです。
ちなみに、HAROiDでは1000個以上Lambdaを並べることも頻繁にあるのですがその場合はビジュアルワークフローは固まってしまい、遷移を見ることが出来ませんでした(当たり前
StateMachine構築時の注意点
- Definitionで設定できるJSONは1MBまで
- StateMachineのネストも可能
- 自身のStateMachineの削除は当然できない
1MBまでのJSONしか設定が出来ないので、千手観音では3000個のLambdaFunctionを並列にするのが限界でした。
1つのLambdaFunctionでだいたい200~400ユーザー分の攻撃が可能なので600k~1200kユーザーのシュミレートが可能です。
また、StepFunctionsの強力なところはDefinitionTaskにはResourceとして指定できればなんでも(?)実行出来るので、StateMachineのネストが可能です。そうなってくると、1MBのJSONの制限はあってないようなものですね。 細かいワークフローを作り、それらをまとめるワークフローで一連の流れを作ると管理も楽になるのではないでしょうか。
また、千手観音では動的にStateMachineを構築しているので当然ゴミのようなStateMachineが大量に生成されていくことになります。
実行中のStateMachineの中のDefinitionTaskでは当然ながら削除が出来ないので、上図の Deleter
というTaskでStateMachine削除用のLambdaを非同期で呼び出しています。
API Gatewayとの組み合わせ
今までは千手観音を実行するのに、Apexを利用していました。 CUIでお手軽に実行することはできるのですが、IAMの作成や権限管理などで誰でもいつでも出来る状態ではありませんでした。 と、いうより管理側がめんどくさい。
API Gatewayはロジックを書かずしてAPIKeyでの認証を設定することが可能です。 またAPIKey毎に実行スロットルや実行回数の制限が可能なので、プロジェクト毎にAPIKeyを発行してあげることも容易です。
curl -X POST \ -H 'Content-Type:application/json' \ -H 'x-api-key: xxxxxxxxxxxxxxxxxxx' \ -d @attack.json \ https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod
上記のようなcurlコマンド一つで10万rpsの負荷が$2ほどで実行することが可能になりました。とってもお手軽でお財布にも優しいですね。
API Keyだけで実行できるので、CIなどと組み合わせれば各プロジェクトのAPI等のE2Eテストも容易に実行できます。
Wanted
HAROiDではこのように、過負荷なサーバー運用を支えるツールや、新しい技術をどんどん取り入れていきます。
マイクロサービスやサーバーレス、課題を解決するために必要なものは即検証し、実践していきます。
そんな環境でチャレンジしたい方、絶賛募集中です。 軽いお気持ちでランチでも、会食でもお誘いいただければわんこ。がすぐに駆けつけます。