SECCON Beginners CTF 2022 スコアサーバー(バックエンド)writeup
LifeMemoryTeamの@atponsです。SECCON Beginners CTF 2022へ参加いただき、ありがとうございました。
今回も前回のSECCON CTF 2021に引き続きスコアサーバー(バックエンド)を担当しましたので、インフラ構成のアップデートについて反省させていただければと思います。
SECCON Beginners CTF 2022
SECCON 公式サイトによれば、SECCON Beginnersの立ち位置は以下の通りになります。
日本国内の CTF のプレイヤーを増やし、人材育成とセキュリティ技術の底上げを目的としたCTF未経験者向け勉強会です。海外のCTF でも上位に入る若手のCTFプレイヤーにより運営されており、CTF未経験の方でも CTF に参加できるよう、わかりやすくセキュリティ技術を教えるワークショップとなっております。
@atponsはSECCON実行委員として、LifeMemoryTeamという団体のメンバーとしてNOCおよびインフラ周りの整備を起こっています。今回のSECCON Beginners CTFの技術スタックはほぼ昨年から変更はありません。
今回は、891チームのアクティブなチーム、1899名のアクティブユーザー(スコアサーバー上の速報値による、詳細は公式の発表をご覧ください)の方々にご参加いただき、参加者登録者数は2000名を超えました。過去最高ぐらいの参加者数の方にお楽しみいただけて嬉しい限りになります。
スコアサーバーの技術スタック
主なインフラ構成は以下の通りになります。変更があったものを太字にしておきます。
- Azure Kubernetes Service
- Azure Database for MariaDB
- Firebase Authentication
- Cloud Firestore
- Flux CD
- Azure Key Vault
- memcached
- Go
- GORM
- oapi-codgen
- uber-go/zap
- ozzo-validation
- tbls
- Bazel
- Grafana Loki
- Promtail
- Fluent Bit
- Prometheus
- さくらのクラウド
- Next.js
- Redis
スコアサーバーとして自作のLMTdというものを作っていますが、こちらの基盤は特に前回のSECCON CTF 2021より変更しておりません。後述する負荷試験の結果、昨年まで利用していたMemcachedを今回は採用せず、Redisを採用することにしました。スコアサーバーおよびRedis、Loki、Fluent Bitなど周辺基盤は全てKubernetes上で動作しており、Flux CDによるCDも今回も行っております。
負荷対策
前回のSECCON CTF 2021では、初速でスコアサーバーがダウンしてしまうという事態が発生し、競技開始後にご迷惑をおかけすることになりました。それへの反省も踏まえて3月〜4月より負荷試験の検討を始めました。
CTFスコアサーバー自作界隈で尊敬している方の中の一人であるWani Hackaseのhi120ki氏がちゃんとk6で負荷試験していたので、やはりk6で負荷試験しないといけないなということで今回はk6のシナリオを書いて負荷試験を行いました。
実際の構成に近づける必要があり、Firebase Authenticationの部分をどう書くか迷いましたが、REST APIのドキュメントをLMTのnomuken氏に教えてもらいこれでなんとか凌ぎました。実際はセッションつくってそれ使い回しましたが、もっとシナリオを真面目に書くなら使ってたと思います。
ボトルネックその1: memcached(本当かは怪しい)
1000VUを想定したシナリオを構築し、最初に見つけたボトルネックはmemcachedでした。(実際にはこれはあまり関係ないことが後で分かりましたが)
memcachedにはランキングや、ユーザーのSurveyの回答状況をキャッシュしているためAPIを叩くと必ず呼ばれるものになります。
エラーを見てみるとmemcachedのソケットのi/o timeoutが大量発生していました。とりあえずgomemcacheを使っているのでコネクション先に貼っておくかということでコネクション数をGORMと合わせました。
running (5m00.9s), 0000/1000 VUs, 6232 complete and 122 interrupted iterations default ✓ [======================================] 0000/1000 VUs 5m0s ✗ status is 200 ↳ 96% — ✓ 6019 / ✗ 213 checks.....................: 96.58% ✓ 6019 ✗ 213
そうすると大分改善されましたが、やはりi/o timeoutは出てしまっていました。
そのため、みんな使ってるし...ということでRedisへの移行を進めました。
抽象化されているので特に難しいこともなくRedisへの移行が完了しました。実際Redisは最近のバージョンでは改善されていますがmemcachedの方がリソースを上手く使うイメージがあるのでずっとmemcachedにしていましたが、一旦比較検討ということで入れ替えてみたところ、若干の改善が見られました。
running (5m00.8s), 0000/1000 VUs, 10049 complete and 202 interrupted iterations default ✓ [======================================] 0000/1000 VUs 5m0s ✗ status is 200 ↳ 97% — ✓ 9765 / ✗ 284 checks.....................: 97.17% ✓ 9765 ✗ 284
このあたりからpprofとかを取って真面目にボトルネックを潰す作業が始まりました。
ボトルネックその2: キャッシュのシリアライズをJSONからMessagePackにする
ランキングの所のフレイムグラフを見てみるとencoding/jsonがバチバチに重いことが分かります。
ランキングを生成した後Redisに載せたり見に行くわけですがその往復が重いということが分かりました。
特に頑張る必要もないのでgobでもなんでも良かったのですが、とりあえず一番楽なmsgpackにしたところキャッシュのサイズは約半分(つまりトラフィックも半分)になりました。
ボトルネックその3: GORMのコネクションプールやクエリ改善
何回か回してみると、Redis周りのエラーは出なくなりましたがDBがかなり暇していました。メモリもかなり暇なのでGORMのオプションのIdleConnを増やしたり、クエリの改善を行っていきました。
running (5m00.9s), 0000/1000 VUs, 21374 complete and 73 interrupted iterations default ✓ [======================================] 0000/1000 VUs 5m0s ✗ status is 200 ↳ 99% — ✓ 21267 / ✗ 107
ここまでやたらもうほぼ完走しています。最高。でもまだ失敗する...DBやPodのリソース見ても普通に暇そうなので何が行けないのでしょうか。他のものをデプロイするかと思ってKubernetesのマニフェストを読んでいたところ、あっ...ってなりました。
オイオイオイオイ、そら0.1コアじゃCPU暇だし、i/oも死ぬわ...
で、これ変えた結果
running (5m00.1s), 0000/1000 VUs, 46807 complete and 0 interrupted iterations default ✓ [======================================] 0000/1000 VUs 5m0s ✓ status is 200 checks.....................: 100.00% ✓ 46807 ✗ 0
:pleading_face: になってしまいました。お前がすべて悪かったんやね...
もうこれISUCONじゃん、と思いましたが結局設定が怪しかったですね。なんでこんな設定だったのかというと、手癖だと思います。
実際開催期間のスパイクも応答時間は伸びてしまいましたが、完全に応答ができなくなることはほぼなかったので良かったです。
新規機能
5月忙しくてほとんど手を付けられませんでしたが、せっかくRedisを入れたのでPub/Subをサポートして、各種イベントをDiscordに流せるようにしました。非同期で処理してスコアサーバーの負荷が上がることはないようにしたかったので、このタイミングでの実装になりました。
運営用のDiscordでしか使ってなかったんですが、チーム単位でWebhookとか設定できたら需要あったりするんだろうか。First bloodは全体に流すべきだったなとは思いました。
感想
今回はほぼ勝ちました。競技時間中に大きな障害は発生することもなく(一部Firebase周りでご迷惑をおかけしましたが)終了後の今も動いております。
問題サーバーのデプロイや、その他の裏話はまたどこかでお話できればと思いますので、ひとまずSECCON Beginners CTF 2022をお楽しみいただいたすべての皆様、そして運営の皆様ありがとうございました。
ご意見やご質問があればできる限り、解答しますのでぜひTwitterでお願いします。