ベンチマークの罠

ベンチマークを誇示する人々は、決して全体像を見せようとはしません。彼らが測定するのは、ほとんど何も行わないハンドラーです。JSONのHello Worldや、静的な文字列です。その真空状態においては、Fiberが圧勝します。Ginよりも秒間のリクエスト処理数が多く、メモリ消費量も少ない。それは紛れもないエンジニアリングの成果です。しかし、あなたのアプリケーションは真空状態ではありません。

私はケニアでリアルタイムのセキュリティアプリを構築しています。ナイロビやモンバサ全域のユーザーからGPS座標を取り込み、犯罪多発地帯や交通事故に関するアラートを送信します。つまり、PostgreSQLへの書き込み、Firebase Cloud Messagingの呼び出し、そして逆ジオコーディングのルックアップが発生することを意味します。スタックにネットワークホップやデータベースのラウンドトリップが含まれる場合、ルーティングでナノ秒を節約するフレームワークの恩恵は目に見えません。ボトルネックは決してルーターではなく、SMSゲートウェイのタイムアウトであったり、インシデントレポートのテーブルをスキャンするPostGISのクエリであったりするのです。生の処理能力(スループット)は、本番環境に直面するまでは魅力的に見えます。

標準ライブラリへの忠誠心

Ginはnet/httpを直接ベースに構築されています。これは、言葉で言う以上に重要な意味を持ちます。

すべてのGo開発者はnet/httpを知っています。デバッグの際は、慣れ親しんだスタックトレースに従うことができます。5年前に書かれたミドルウェアも、特別な手順なしにそのまま導入できます。リクエストおよびレスポンスオブジェクトは、Goのドキュメントに記載されている通りに正確に動作します。Ginが予測可能な状態を保てるのは、言語そのものに近い存在であり続けているからです。

一方、Fiberはその基盤を、ゼロアロケーションのパフォーマンスに最適化されたカスタムHTTPエンジンであるfasthttpに置き換えています。それを実現するために、Fiberはリクエスト/レスポンス・プーリングを使用しています。各リクエストの後にガベージコレクタにメモリを回収させる代わりに、Fiberはメモリブロックを再利用します。メモリをクリアして、次の着信接続に渡すのです。そのトリックこそがスピードの源です。そして同時に、危険の源でもあるのです。

再利用されるメモリに潜む危険

プーリングは理論上は安全に聞こえます。しかし実際には、微妙な「契約」が生じます。それは、ハンドラーが終了した後は、リクエストデータの参照を絶対に保持してはならない、というものです。もしgoroutineがリクエストよりも長く生存したり、後で処理するためにリクエストボディのスライスをキャプチャしたりすると、Fiberはそのメモリを回収し、次のユーザーに渡してしまいます。

その結果、データの汚染が発生します。このバグはユニットテストには現れません。ナイロビのドライバーのGPS座標が、突如としてキスムの地図上に表示される、といった形で現れます。あるユーザーのJWTトークンが、別のユーザーのコンテキストに漏洩する、といった形で現れます。これらは「時間的な不具合(temporal failures)」です。ロギングを追加すると、ロギング自体が新しいメモリを割り当ててタイミングを変化させてしまうため、バグは消えてしまいます。結局、幽霊を追いかけることになります。

Ginの場合、標準ライブラリが新しいリクエストオブジェクトを割り当てます。汚染されるプールは存在しません。アプリケーションが、人々の安全に直結する実際の場所を扱う場合、その安全性は極めて重要です。

エコシステムの重力

また、「互換性」という静かなコストも存在します。

ほとんどのGoミドルウェアはnet/httpを前提としています。JWTバリデーター、OpenTelemetryエクスポート、CORSハンドラー、レートリミッターなどはすべて、標準的なインターフェースに従っています。Ginはそのインターフェースをネイティブに理解しています。ミドルウェアを投入すれば、そのまま動作します。

Fiberは独自のコンテキスト型と独自のハンドラーシグネチャを持っています。そのため、アダプターが必要になります。アダプターが公式であることもあれば、アップストリームのライブラリから1年遅れていることもあります。負荷がかかったときに、わずかに異なる挙動を示すこともあります。少人数のチームを運営しているとき、認証ミドルウェアが自分のノートPCでは動作するのに、サーバーでは正当なトークンを拒否する理由をデバッグするために、余分な時間を費やす余裕はありません。あなたが望むのは、go getが「退屈」であることです。Ginはそれを退屈なままに保ってくれます。そして、午前3時にまさに求めているのは、それなのです。

アプリケーションが本当に必要としているもの

私のセキュリティサービスは、複数のユーザーから数秒おきに送られてくる位置情報を処理します。高リスクな道路にジオフェンスを設定し、通知をトリガーします。通知が遅れるのは苛立たしいことですが、通知が間違っているのは危険なことです。プーリングされたメモリの破損によって、誰かを進行中の事故現場へと送り込んでしまうようなことは、あってはならないことです。

ここでは、スループットよりも信頼性が勝ります。pprofをアタッチしたときに意味の通じるスタックトレースが必要です。カスタムメモリプールの内部ライフサイクルを理解することを強いないメモリプロファイルが必要です。このプロジェクトを引き継ぐ次の開発者が、fasthttpのオブジェクトモデルを学習することなく、午後にはオンボーディングを完了できることが必要です。Ginはそれを私に与えてくれます。運用の健全性はベンチマークには反映されませんが、それこそがサービスを支えるものなのです。