見出し画像

ステージング環境では気づけなかった! 負荷対策が招いたTcpFallbackの落とし穴


はじめに

この記事はColorful Paletteアドベントカレンダー 12/23の記事です。
株式会社Colorful Paletteでサーバサイドエンジニアをしているつーです。
普段はゲーム内機能のサーバ実装、インフラの開発運用などに携わっています。
今回はゲームサーバの負荷対策とTCP Fallbackの落とし穴について説明します。
一見関係なさそうな2つのワードですが地味に関係があり、ちゃんと伏線回収をするので興味がある方は読んでいただけると嬉しいです。

負荷対策

はじめに負荷対策について説明します。
ゲームでは1画面の構成に多くのユーザデータを使用し、それがサーバ負荷に繋がるため、様々な負荷対策が行われています。
今回は画面構成に必要なユーザデータが多く、フレンド一覧画面やランキング画面等様々な導線があるユーザープロフィール画面を例に負荷対策について考えてみます。

プロフィール画面例

この画面では合計7種類のデータが表示されており、データによっては複数のレコードで構成されています。
1種類のデータに対し1テーブル用意し、データを取得するのに1クエリ発行すると仮定した場合、合計7クエリ実行する必要があります。

プロフィール情報取得APIのサーバ負荷として以下の問題が考えられます。

  • フレンド数が多い場合、レコード数が膨大な量になるため、データベースの通信帯域を大きく消費する。

  • データベースをシャーディングしている場合、複数のデータベースに対するクエリ発行が必要になる。 フレンドが各シャードに分散している場合、単体のデータベースに対するクエリの発行回数は変わらないが1リクエストにおける総クエリ数の増加によりレスポンス時間が長くなる。

  • 負荷の集中によるデータベースのレイテンシ増、コネクション枯渇

こうした問題を回避するため、頻繁に参照される変化の少ないリードオンリーなユーザデータをインメモリデータベースであるRedisなどにキャッシュし、データベースへのアクセスを抑制しています。

ユーザデータキャッシュ取得、登録のシーケンス図

Redisにはプロフィールの表示に用いるユーザデータをオブジェクト化してキャッシュしており、このオブジェクトの構造に破壊的変更を加える際はRedisをFlushAll(キーを全削除し初期化)するオペレーションが必要でした。
上記のシーケンス図からわかるように、RedisをFlushAllした状態でメンテナンスを開放するとキャッシュが蓄積されるまで頻繁にデータベースに直接アクセスが行われ、サーバダウンの危険性が高まります。
これを防ぐため、メンテナンス開放前にデータベースからRedisにユーザデータを予めキャッシュするウォームアップバッチを実行しています。

メンテナンスで発生したアクシデント

とあるプロジェクトで初めてそのウォームアップバッチをメンテナンス中に実行したときの話です。
結果問題なくリリースできたので今となっては思い出話ですが、当時メンテナンスを担当していた自分はかなりヒヤッとしました。
バッチのステージング環境での検証も完了し、オペレーション計画通りにCodeBuildからバッチを起動しようとしたところ、バッチ実行ログにこのようなエラーが流れてきました。

20XX-XX-XX 00:00:00.000 WARN  [lettuce-nioEventLoop-8-1] i.l.c.c.t.DefaultClusterTopologyRefresh [ipAddress=] [userId=null] - Unable to connect to [redis-cache.production.local:6379]: production-cache.clustercfg.apne1.cache.amazonaws.com: Name does not resolve

このエラーはRedisホスト名の名前解決に失敗したことを示すエラーです。何度実行してもエラーは変わらず、当初はRedisに起因するものだと考えていました。調査を進めていくと、ゲームAPIをホストしているEC2からはRedisに正常に接続できていたため、次第にCodeBuildのネットワーク設定に問題があると考え、急遽ウォームアップバッチをEC2から実行しました。
その結果、正常に名前解決が行なえ、予定通りメンテナンスを開放することができました。

原因

メンテナンス後、色々調査を重ねた結果「TCP Fallbackに失敗していた」ということがわかりました。
TCP Fallbackの概要と、リリース前の段階で検知できなかった原因を考えてみます。

TCP Fallbackとは

TCP Fallbackは、OSレベルでの機能であり、通常UDPプロトコルを使用するDNSクエリをTCPプロトコルで再実行することを指します。
まず、名前解決を行う際のDNSリクエストにはUDPを使用し、DNSサーバはそのままDNSメッセージの返却を行います。
しかし、DNSメッセージが512Byteを超過する場合は異なる挙動を取ります。具体的には、DNSサーバがレスポンスが512Byteに収まるようにDNSメッセージの切り捨てを行い、切り捨てを行ったことを示す「TCビット」という情報を付加してレスポンスを返却します。クライアント側はレスポンスにTCビットが付与されていた場合、TCPを使用して再度DNSサーバへリクエストを行うことで、全DNSメッセージを取得することができます。

TCP Fallbackのシーケンス図

参考リンク


なぜステージング環境では名前解決が成功したのか

まず、ステージング環境で名前解決が成功した理由を考えます。
ステージング環境は準本番環境で、本番環境とできるだけサーバ構成等を統一することで「本番環境にリリースしても問題ないか」を検証する環境です。とはいえ、ステージング環境はリリースの直前しか使用されず、基本的なQA検証は普通の開発環境で行われることが多いため、「定期実行しているバッチや、ロードバランサーの配置は統一しているが、インスタンスのスペックや台数は削減している」といった構成を取るプロジェクトが殆かと思います。
ここに落とし穴がありました。
そのプロジェクトはアクセスがかなり多いため、本番環境のRedisはクラスタリングを行っており、12シャード構成で1シャードに2ノードのインスタンスを使用していました。
一方、ステージング環境のRedisはクラスタリングは行っていましたが、2シャード構成で1シャードに1ノードのインスタンスしか配置していませんでした。

本番とステージングのnslookup結果を比較してみると、

本番環境

$ nslookup production-cache.clustercfg.apne1.cache.amazonaws.com
Server:		10.0.1.1
Address:	10.0.1.1#53

Non-authoritative answer:
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.2
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.3
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.4
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.5
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.6
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.7
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.8
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.9
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.10
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.11
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.12
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.13
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.14
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.15
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.16
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.17
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.18
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.19
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.20
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.21
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.22
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.23
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.24
Name:	production-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.25

ステージング環境

$ nslookup staging-cache.clustercfg.apne1.cache.amazonaws.com
Server:		10.0.1.1
Address:	10.0.1.1#53

Non-authoritative answer:
Name:	staging-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.2
Name:	staging-cache.clustercfg.apne1.cache.amazonaws.com
Address: 10.0.1.3

となっており、名前解決で返されるipアドレスの数が大きく違います。
そのため、ステージング環境ではIPアドレス数が少なく、DNSメッセージが512Byteを超過しなかったためTCP Fallbackが起きず、本番環境ではIPアドレスが多数あるためTCP Fallbackが発生し、名前解決に失敗していたようです。

なぜゲームAPIをホストしているEC2では名前解決が行え、バッチを実行したCodeBuildでは名前解決が失敗したのか

結論、実行環境OSの違いが原因でした。
ゲームAPIの実行に使用していたOSはAmazonLinux2、CodeBuildに使用していたOSはAlpineベースのものでした。先程も説明したように、TCP FallbackはOSレイヤの機能なので、OS側が対応していなければ実行するアプリケーションが同じでも正常に動作しません。
OpenSearchのドキュメントですが、Alpine 3.18以前のバージョンでは20ノード以上のクラスタの名前解決に失敗する可能性があるとされています。


参考リンク


終わりに

今回は実際にあった運用の経験を振り返りながら負荷対策とTCP Fallbackの落とし穴について説明しました。
極論、本番環境とステージング環境を同じ構成にすれば今回のようなケースは防ぐことができますが、インフラ予算は有限です。
余分なインフラリソースにコストをかけるより、コンテンツの品質を向上させてユーザ体験を改善するほうが建設的だと思います。
また、APIとバッチの行環境を統一すれば発生しなかった問題でもありますが、こちらはバッチの起動速度や管理のしやすさといった運用オペレーションコストに影響が出ます。
これらの要件を天秤にかけるのがステージング環境設計の難しさであり、大抵の場合最適解はなく折衷案を採択することになるんでしょうね…

この記事を書いて、改めてチームで作り上げたゲームを完璧な状態で提供できるかどうかはリリース作業を担うサーバエンジニアにかかっているなと感じました。
ゲームを最高の状態でユーザの皆さんに届けられるよう、ノートラブルでリリースを行いたいですね。

最後まで読んでくださりありがとうございました