リダイレクトは訪問者を待たせます。短縮リンクをクリックすると、ブラウザーは待機し、302が返ってくるまで他に何も読み込まれません。だからservices/edge-redirectには「十分速い」という感覚ではなく、遅延予算があります。キャッシュヒット時のp50は5ms、p95は15msです。
これは結果ではなく目標として読んでください。予算はサービスのREADMEと当番のランブックに書かれており、負荷テストの合格基準もこれに基づいています。リージョンごとのパーセンタイルを裏付けるコミット済みのベンチマークや負荷テストの結果ファイルはないので、この記事では数字を載せません。代わりに、コードから読み取れるホットパスの動作と、同じくらい役に立つ、やらないことを説明します。
要点
- エッジは、低アロケーションのGo製HTTPライブラリで動くGoサービスで、現在は単一の本番ホスト上でリバースプロキシの背後に置かれており、マルチリージョンのエッジは計画中です。
- 検索は、プロセス内のLRU(10万エントリ、TTL 60秒)、次に共有のインメモリキャッシュ(TTL 5分)、最後にタイムアウト1.5秒でgRPC経由のapi-coreの順にたどります。
- クリックイベントは別のgoroutineから発行します。訪問者がそれを待つことはありません。
- 予算: キャッシュヒット時のp50は5ms、p95は15ms。これらは目標であり、計測済みのパーセンタイルはここでは公開していません。
予算がある理由
リダイレクトにかかる時間は、その後のすべてに上乗せされます。キャンペーンのリンクがトラフィックをランディングページに送るなら、リダイレクトはそのページのTime to First Byteの一部です。予算があると、この経路に処理を足したくなるたびに、決断を迫られます。収まるか、ワーカーに移すかです。
services/edge-redirect/README.mdの数字は次のとおりです。
| パーセンタイル | キャッシュヒット | キャッシュミス |
|---|---|---|
| p50 | 5ms | 40ms |
| p95 | 15ms | 100ms |
| p99 | 30ms | 200ms |
これらは設計上の目標です。ベンダー審査などで計測値が必要なら、この記事の最後で説明するk6のシナリオを自分のリンクに対して実行し、出力を保管してください。
リクエスト経路を順に追う
internal/handler/redirect.goのファイル冒頭に、流れが書かれています。順に見ていきます。
- ホストを小文字にし、パスからスラッグを取り出す。
resolver.ResolveがL1、L2、オリジンの順にたどる。- 状態で早期終了する。停止中のワークスペース、期限切れのリンク、パスワード保護されたリンク(訪問者はロック解除ページに進み、クリックは送出されません)、url-scannerが
blocked(451)としたリンク、管理者のブロックリストにあるリンク先ホスト。 - 待たずにクリックイベントを発火する。
- リンクが
301や307を指定していなければ、302で応答する。
手順3のどれも、キャッシュされたリンクレコードのフィールドか、メモリ上の集合の検索です。たとえばブロックリストのマッチャーは、バックグラウンドでapi-coreから同期されるため、確認の費用はデータベース呼び出しではなく、マップの検索数回です。
リンクプレビュー(+で終わるスラッグ)、JSONを求めるAIエージェントのリクエスト、エンドツーエンド暗号化されたリンクは、リダイレクトの前に別の分岐を通ります。どれもPostgresには触れません。
3層の検索
各層は意図的に小さく、最後の1つを除いてどれもベストエフォートです。
L1、プロセス内のLRU。 internal/cache/lru.goがhashicorp/golang-lru/v2/expirableを包んでいます。サイズはL1Size: 100_000エントリ、TTLはL1TTL: 60sで、どちらもinternal/config/config.goで設定されています。TTLがあるのは、無効化または削除されたリンクが、1つのノードから永遠に配信され続けないようにするためです。キーはhost + "/" + slugなので、2つのドメインで同じスラッグがあっても衝突しません。
L2、共有キャッシュ。 internal/cacheは、リンクをホストとスラッグをキーにしたJSONとしてL2TTL: 5mで保存します。エッジインスタンスが共有します。nilのクライアントも許容されます。キャッシュのメソッドはミスを返し、エッジは動き続けます。
オリジン、api-coreへのgRPC。 internal/origin/origin.goは2つのトランスポートに対応しています。ほかに設定がなければ、REST(GET /v1/lookup)が既定です。GRPC_ORIGIN_ADDRを設定するとgRPC(elido.v1.LookupService.Resolve)が使われ、本番ではこちらです。呼び出しはOriginTimeoutの1.5秒で制限されます。
リゾルバーは、重要な部分をそのまま引用できる短さです。
if v, ok := r.l1.Get(key); ok {
return v, nil
}
if v, ok := r.l2.Get(ctx, key); ok {
r.l1.Set(key, v)
return v, nil
}
link, err := r.origin.Resolve(ctx, host, slug)
if err != nil {
return nil, err
}
r.l1.Set(key, link)
r.l2.Set(ctx, key, link)
return link, nil
L2でのヒットはL1を補充します。両方でミスするとapi-coreに行き、両方を補充します。「見つからない」と言えるのはオリジンだけです。そう返された場合、リゾルバーはワイルドカードのカスタムドメイン向けに、親のワイルドカードホスト(abc.example.comを*.example.comに)で一度だけ再試行します。
この設計にないもの
この記事の以前の版には、コードが実際には行わないことが書かれていました。明確にしておきます。
singleflightのグループはありません。同じコールドなスラッグへの同時ミスが2つあれば、どちらもオリジンに届きます。- エッジにpub/subの購読はありません。リンクが変更されると、api-coreがL2のキーを削除します(
services/api-core/internal/linkmod/adapters.goのpurger)。L1にはプッシュされないので、編集したリンクがノードのメモリから最大60秒配信されることがあります。 - L1はTTLで制限されたLRUで、アドミッション制御されたキャッシュではなく、サイズはバイト数ではなくエントリ数で決まります。
cache-statusエンドポイント、層ごとのレスポンスヘッダー、エッジが出力するレイテンシのヒストグラムはありません。
60秒の窓があるため、編集可能なリンクには一時的なリダイレクトを使うべきで、同じ問題のブラウザー側では301と302が重要になります。キャッシュ設計は、URLリダイレクトのキャッシュ戦略でさらに詳しく扱っています。
エッジのコードで示せるPrometheusメトリクスは、fleetとoutcomeのラベルを持つelido_edge_redirect_totalだけです。ランブックには他のメトリクス名(edge_cache_*、edge_origin_grpc_duration)が出てきますが、現在のコードに登録されているものは見つからなかったので、文書化していません。
ホットパスから外れているもの
クリックの保存。 click.Producer.Emitはイベントをシリアライズしてfranz-goのプロデューサーに渡し、ハンドラーはそれをgo h.clicks.Emit(...)として呼びます。produceのコールバックはエラーをログに記録するだけです。イベントバスが遅くても、訪問者は気づきません。代償として、クリックが失われることがあり、これは撃ちっぱなしの一般的なトレードオフです。クリックを分析ストアに書き込むのはclick-ingesterの仕事で、それがクリック分析APIにデータを供給します。
ボットのチャレンジ。 リダイレクトにJavaScriptのチャレンジはありません。ボット検知はユーザーエージェントの部分文字列のリストで、クリックを数えるかどうかを決めるため、クローラーやリンクプレビューの取得もリダイレクトは受け取りますが、分析を水増ししません。意図的に粗いフィルターです。
スキャン。 url-scannerは、リンクの作成時と定期的に、リンクごとの判定を発行します。エッジは保存された判定(clean、suspect、blocked)を読むだけで、リダイレクトを止めるのはblockedだけです。
ルールの評価。 スマートリンクはリンクレコードとともに運ばれるため、国やデバイスなどのルールはキャッシュされたコピーに対して実行され、GeoIPの検索はローカルのデータベースを使ってプロセス内で行われます。ルールのあるキャッシュヒットで、api-coreへのネットワーク呼び出しは不要です。
サーバーの設定は控えめです。cmd/server/main.goは、サーバーに読み取りと書き込みで2秒のタイムアウト、アイドルで30秒のタイムアウト、リクエストボディの上限64 KBを設定しています。サービスのREADMEにあるとおり、リクエストごとのアロケーションが少ないため、net/httpではなくこちらを選びました。当社のハンドラーでの直接比較のベンチマークは公開していないので、これは計測結果ではなく設計上の理由です。
自分で計測する方法
リポジトリには、k6を使った負荷テストのランブック(docs/runbooks/19-load-testing.md)があります。ホットパスの公開前の合格基準は、5分間1.5k req/sでp95が15ms、5xxがゼロであることと、1分間5k req/sでp95が30ms、5xxが0.1%未満であることです。これらはしきい値であり、結果ではありません。
近くの負荷生成器からのテストは、サービスを計測します。遠く離れたホストからのテストは、あなたのネットワークを計測します。訪問者側の遅延は、SentryとDatadogによるリダイレクトの監視のように、外部から合成モニターで測ってください。
正直に言って、15msを計測値として顧客に伝えるのは、まだしません。リージョンごとの表を載せる前に埋めたい隙間は、エッジのレイテンシのヒストグラム、コールドミスのsingleflight、L1のプッシュ無効化、そして15msの数字を裏付けるコミット済みのベンチマークです。
Marius Voßは、ElidoのDevRelおよびエッジインフラ担当です。出典: services/edge-redirect、services/api-core/internal/linkmod/adapters.go、docs/runbooks/19-load-testing.md。外部ベンダーの主張は行っていません。
ブログの関連記事
- URLリダイレクトのキャッシュ戦略 - L1とL2の設計をより詳しく。
- 301と302のリダイレクト - 編集可能なリンクが一時的なリダイレクトを使う理由。
- スマートリンクの解説 - キャッシュされたレコード上で動くルール。
- SentryとDatadogによるリンクリダイレクトの監視 - 外部からの計測。
- リンククリック分析API - クリックイベントの行き着く先。
よくある質問
p95 15msは計測値ですか?
いいえ。キャッシュヒット時のp95 15ms(p50は5ms)は、edge-redirectサービスの遅延予算で、アラートと負荷テストの合格基準はこれに沿って書かれています。この記事では、リージョンごとの計測済みパーセンタイルは公開していません。引用できる、コミット済みのベンチマークや結果ファイルがないからです。
Elidoのリダイレクトはどのようにスラッグを解決しますか?
エッジはまずプロセス内のLRUを見て、次に共有のインメモリキャッシュ、最後にgRPCでapi-coreに問い合わせます。各キャッシュ層はベストエフォートなので、確定的な「見つからない」を返せるのはオリジンだけです。下の層でヒットしたものは、上の層にコピーされます。
リダイレクトの経路から外れているものは何ですか?
クリックの保存、分析、url-scannerのチェックです。エッジは別のgoroutineでクリックイベントをイベントバスに送出し、それを待たずに訪問者へ応答します。ボットのフィルタリングはユーザーエージェントの確認で、クリックを数えるかどうかを決めるものであり、訪問者が通過すべきチャレンジではありません。
Elidoが既定で302を使うのはなぜですか?
302は、ブラウザーにリンク先を恒久的なものとして扱わないよう伝えるため、編集したリンクは次の訪問で反映されます。リンクごとに301や307に設定することもできます。トレードオフは、301と302の記事で扱っています。
エッジPOPはどこにありますか?
現在の本番は、リバースプロキシの背後でエッジサービスを動かす単一のホストです。ヨーロッパ、北米、東南アジアにまたがるマルチリージョンのエッジ(それぞれが同じエッジのバイナリを動かす構成)は計画中で、まだ稼働していません。主要なデータベースとイベントバスはEUに置かれ、この設計では、ADR-0014が述べるとおり、リダイレクトのキャッシュデータだけがエッジノードの隣に置かれることになります。
編集したリンクが古いまま配信されることはありますか?
短時間なら、あります。リンクが変更されるとapi-coreは共有キャッシュのエントリを削除しますが、エッジノードのプロセス内のコピーは、60秒のTTLが切れるまで残ります。そのため、宛先の編集がすべてのノードに届くまで最大1分かかることがあり、編集可能なリンクに一時的なリダイレクトが向いている理由の1つです。
Elidoを試す
URLを貼り付けて短縮リンクを取得
登録不要。リンクは30日間有効。永久に保存するには登録してください。
登録不要。無料登録すると月 50 件のリンクを作成できます。
