elido.me/abc123 を開くと、ブラウザが何かを読み込む前に、その短い文字列を完全なウェブアドレスに変換しなければなりません。この仕組みはほとんどの人が想像するよりシンプルです。URLショートナーは短いコードから長い転送先URLへのマッピングを保存します。短いリンクをクリックすると、サービスはそのコードを検索キーとして扱い、転送先を見つけ、ブラウザにどこへ行くべきかを伝えるHTTPリダイレクトを返します。リクエスト1つが入り、リダイレクト1つが出る。
それがすべてのアイデアです。それ以外はすべて3つの課題に対するエンジニアリングです:検索を高速にすること、コードを短くユニークに保つこと、そして誰も遅らせずにクリックを記録すること。この記事では、Elidoのエッジアーキテクチャを具体例として使いながら、URLショートナーが端から端までどのように動くかを解説します。ショートナー全般にも当てはまる説明を心がけています。スラッグからURLへのマッピング、短いコードの生成方法、データの保存場所、多くの人が躓く301対302のリダイレクトの選択、HTTPリダイレクトがネットワーク上で実際にどのように見えるか、エッジでキャッシュすることがなぜ重要か、そしてクリックが非同期でどのようにカウントされるかを説明します。
URLショートナーの仕組み:中心にあるマッピング#
インフラを取り除いてしまえば、URLショートナーはリダイレクトハンドラーが付いたキーバリューストアです。キーはスラッグ(ドメインの後ろにある短いコード)で、バリューは転送先(最初に貼り付けた長いURL)です。
短いリンクを作成すると、ショートナーは1行を書き込みます:このスラッグはあの転送先を指す。誰かが短いリンクを訪れると、ショートナーはその行を読み返して処理します。リンクの作成は稀ですが、読み取りは常時行われます。1つのマーケティングリンクは一度書き込まれ、その後ライフタイムで数十万回読み取られることがあります。この読み取りが圧倒的に多いという比率が、ワークロードについての最も重要な事実であり、後続のすべての設計判断(特にキャッシュ)を形作ります。
マッピング自体は転送先以上のものを保持できます。Elidoでは、スラッグはターゲティングルールを持つことができるため、1つの短いリンクが国・デバイス・言語・時間によって異なる転送先にルーティングされます。それがスマートリンクと呼ばれるもので、基本的なルックアップと読み取り後の小さなルール評価は変わりません。中核の関係は変わりません:スラッグが入り、転送先が出る。
短いコードの生成#
スラッグがキーであれば、どこから来るのでしょうか?よく使われるアプローチが2つあり、ほとんどのショートナーはそのどちらか、または両方の組み合わせを使います。
1つ目は、データベースIDのbase62エンコードです。新しいリンクごとに、データベースから自動インクリメントの整数IDが割り当てられます。そのIDをbase62(URLセーフな62文字 a-z、A-Z、0-9)でエンコードします。ID 1は b になり、ID 125は2文字のコードになります。base62は密度が高く、3文字で約23万8000個のリンクをカバーし、5文字で約9億1600万個、6文字で約560億個をカバーします。コードは短く保たれ、ユニークなIDに1対1でマッピングされるため衝突しません。トレードオフは、連続したIDが推測可能なことであるため、多くのシステムはエンコード前にIDスペースをシャッフルまたはオフセットします。
2つ目のアプローチはランダム生成です。同じ文字セットから固定長のランダムな文字列を選び、データベースで既に使われていないことを確認します。衝突が起きれば別のものを生成します。適切な長さでは衝突は極めて稀なため、リトライループはほとんど実行されません。ランダムスラッグは列挙不可能なため、セキュリティ上の理由から支持されています。
カスタムスラッグ(elido.me/spring-sale のようなブランドスラッグ)は、どちらのスキームにも追加できます。ユーザーが文字列を指定し、ショートナーは許可された文字かどうかを検証し、保存前に同じインデックスに対してユニーク性を確認します。スラッグが生成されたものでも手動で選ばれたものでも、同じ場所に落ち着きます:データストアのユニークカラムに。
データの保存場所#
スラッグから転送先へのマッピングには、「このスラッグは何を指すか」に素早くかつ一貫して答えられる場所が必要です。真実の記録(ソース・オブ・トゥルース)としては、ほぼ常にリレーショナルデータベースが使われます。Elidoはスラッグをユニークインデックスカラムとして保存したPostgresを使用しており、ルックアップはテーブルスキャンではなく単一のキー読み取りになります。Postgresはすべてのリンク・ユーザー・ワークスペースの標準レコードを保持しています。
しかし、読み取りが圧倒的に多い比率を考えると、すべてのクリックでPostgresにアクセスするのは無駄です。Postgresでのキー付きスラッグ検索は通常1〜3ミリ秒かかりますが、バイラルリンクのクリック量でその数字を掛け合わせると、データベース接続が有限なリソースであることを考えれば素早く感じられなくなります。そのため本番環境のショートナーはデータベースの前にキャッシュを置きます。キャッシュは実際にほとんどの読み取りが行われる場所で、データベースはキャッシュにないものに対するフォールバックです。
ElidoはPostgresの前に2層キャッシュを実行しています。1層目はリダイレクトバイナリ自体の内部に存在するインプロセスのLRUキャッシュで、ネットワークホップなしに数百ナノ秒で転送先を返します。2層目は同じリージョンのRedisクラスターで、1ミリ秒未満で応答します。コールドミス(どちらの層も最近見ていないスラッグ)だけが api-core へのオリジンgRPC呼び出しにフォールスルーし、Postgresを読み取ります。両方のキャッシュ層を合わせたヒット率は約99.4%で、データベースへのアクセスは約167リクエストに1回です。そのキャッシュの動作の詳細(エビクションポリシーやこれまでに発生した障害モードを含む)はキャッシュ戦略の解説記事に記載されています。
HTTPリダイレクトとは実際何か#
ショートナーが転送先を取得したら、ブラウザに渡す必要があります。これはHTTPリダイレクトで行われます。HTTPリダイレクトは特定の種類のレスポンスです。200 OK でページコンテンツを返す代わりに、サーバーは 3xx ステータスコードと実際のURLを示す Location ヘッダーを返します。ネットワーク上でのレスポンスは小さいです:
HTTP/1.1 302 Found
Location: https://shop.example.com/spring-collection
Content-Length: 0
ブラウザはステータスコードを読み取り、Location ヘッダーを確認して、そのアドレスへの新しいリクエストを即座に発行します。クリックした人には1回のナビゲーションに見えますが、その裏では2つのリクエストが発生しており、短いリンクが途中でクイックディレクトリ検索として機能しています。各 3xx コードのセマンティクスはRFC 7231で定義されており、MozillaのHTTPリダイレクトガイドが各コードの実用的なリファレンスとして最もわかりやすいです。
ボディは空です。レンダリングするものがないからです。ペイロード全体はステータス行とヘッダーだけです。リダイレクトの配信コストが低い理由の一つはここにあります:テンプレートも、コンテンツのデータベース結合も、マークアップもありません。スラッグを解決し、1つのヘッダーを設定し、送信する。
301対302:アナリティクスを決める選択#
ここでショートナーは、ほとんどのユーザーが見ることはないが、リンクが編集可能でトラッカブルかどうかを決める静かな選択をします。リダイレクトのステータスコードは形式的なものではありません。2つの一般的なオプションは非常に異なる動作をします。
301 Moved Permanently はブラウザとサーバーとの間にあるすべてのプロキシやCDNに、この短いリンクは常に同じ場所を指すと伝えます。そのためキャッシュされます。301 は積極的に保存されます。次回その訪問者が短いリンクをクリックすると、ブラウザはキャッシュから解決し、ショートナーに一切アクセスしないことがあります。往復を省く点では優れていますが、リンクツールにとっては2つの点で問題があります。まず、アナリティクスが盲目になります:ブラウザキャッシュから配信されたクリックはサーバーに届かないので、カウントされません。次に、転送先が事実上固定されます。リンクの向き先を変更しても、301 をすでにキャッシュしている人は自分でキャッシュが期限切れになるまで(これはコントロールできません)古いアドレスに辿り着き続けます。
302 Found(および厳格な従兄弟の 307 Temporary Redirect)はブラウザに「これは一時的なので、次回また確認してください」と伝えます。ブラウザはマッピングをキャッシュしないため、すべてのクリックが短いリンクをサーバーに再リクエストします。その追加の往復こそがリンクツールが求めるものです。すべてのクリックがインフラに届くのでカウント可能であり、サーバーが毎回転送先を新たに解決するため、リンクの向き先を変えると次のクリックから新しいターゲットが有効になります。コストはクリックあたり1回のネットワーク往復ですが、うまく構築されたエッジはこれを一桁ミリ秒に抑えます。
これがElidoが 302 をデフォルトとする理由です。編集可能な転送先と正確なクリックデータこそがマネージドリンクの存在意義であり、301 は両方をキャッシュ最適化のために犠牲にします(通常はそれを望まない最適化です)。RFC 7231では 301 がデフォルトでキャッシュ可能であり 302 はヘッダーで特別に指定しない限り保存されないと明記されており、それがまさに2つのユースケースが求める動作です。恒久的なリダイレクトが正しい限られたケース(たとえば真のドメイン移行)もありますが、追跡可能で編集可能な短いリンクには一時的リダイレクトが正しいデフォルトです。
低レイテンシーのためのエッジキャッシング#
リダイレクトは同期的でブロッキングです。訪問者のブラウザはリダイレクトが届くまで短いリンクで停止し、その後でようやく実際に重要なページのロードを開始できます。スラッグの解決に費やすすべてのミリ秒が、訪問者の待ち時間に追加されます。だから本格的なショートナーはルックアップを訪問者のできるだけ近くに配置します。
Elidoはフランクフルト、アッシュバーン、シンガポールのエッジPOP(ポイント・オブ・プレゼンス)でリダイレクトハンドラーを実行し、トラフィックを最も近いPOPにルーティングします。ハンドラーはGoでfasthttpの上に書かれています。その選択の理由は、ゼロアロケーションのリクエストパスが持続的な負荷下でGCの停止を予測可能に保つためです。インメモリキャッシュと組み合わせることで、キャッシュヒット時のp95をPOPで測定してフランクフルトのメジアン約4.8msからシンガポールのp95約14ms(地理的に広いため)まで15ms未満に抑えています。この予算のほとんどは物理的なネットワーク転送(訪問者と最寄りのPOPとの間の避けられない距離)で、ソフトウェアで最適化できない部分です。各リージョンのレイテンシー予算の詳細はp95-15ms以下の達成記事に記録されています。
ルックアップを単一の中央サーバーではなくエッジに置くことが、即座に感じられるリダイレクトと目に見えるもたつきのあるリダイレクトの違いです。このワークロードではエニーキャストエッジルーティングがDNSのみのセットアップより優れている理由でもあります(エッジPOP対DNSのみのルーティングで比較しています)。要約すると、ネットワークが地理を処理し、キャッシュが速度を担います。
リダイレクトを遅らせずにクリックをカウントする#
リダイレクトするだけのショートナーはリダイレクトサービスです。リンク管理ツールにするのは、すべてのクリックをカウントし、誰がどこからどのデバイスでクリックしたかを伝えることです。難しいのは、訪問者をそれで待たせないことです。
答えは2つを完全に分離することです。リダイレクトハンドラーがスラッグを解決すると、即座に 302 レスポンスを送信します。クリックの記録はその後、ファイア・アンド・フォーゲットの作業として行われます。ハンドラーはクリックイベント(スラッグ、タイムスタンプ、短縮されたIP、user-agentハッシュ)をメッセージキューに追加して先に進みます。書き込みの確認を待ちません。キューが一時的に利用できない場合、リダイレクトを遅延させるよりもクリックをドロップする方を選択します(インフラ障害でクリックを失うことは許容でき、リダイレクトを失敗させることは許容できないという意図的な判断です)。
ElidoはそのキューにRedpandaを使用しています。別のコンシューマー(クリックインジェスター)がキューからイベントを読み取り、ClickHouseに書き込みます。ClickHouseはクリックアナリティクスのような大量のアペンドと集計ワークロードのために構築されたカラム型データベースです。訪問者のリダイレクトはすでに数ミリ秒前に完了しており、アナリティクスの行はホットパスを完全に外れて数秒後に保存されます。キューの設計についてはファイア・アンド・フォーゲットのクリックインジェスションで、カラム型ストアがアナリティクスにPostgresより優れている理由についてはクリックアナリティクスにClickHouseを使う理由で説明しています。
この分離こそがアナリティクスが詳細でありながら遅くない理由です。訪問者が待っている間はカウント・スコアリング・集計のいずれも行われないため、リダイレクトパスはスリムに保たれます。
まとめ#
URLショートナーは端から端まで短いシーケンスです。リンクを作成すると、ショートナーはスラッグからユニークキーとして生成・検証しながら、スラッグから転送先へのマッピングをPostgresに保存します。訪問者がクリックすると、リクエストは最寄りのエッジPOPに届き、ハンドラーはインメモリキャッシュからスラッグを解決します(ミスの場合のみRedis、そしてデータベースへとフォールスルーします)。Location ヘッダーに転送先を含む 302 を返すため、クリックはカウント可能で転送先は編集可能です。そしてクリックイベントを別のコンシューマーが保存するためキューに送信しますが、誰も待たせません。
各パーツは個別にはシンプルです。エンジニアリングは比率と予算にあります:キャッシュを求める読み取り重視のワークロード、エッジを求めるレイテンシーの上限、ホットパスを外れていることを求めるアナリティクスの要件。これを直接活用したい場合は、スマートリンク機能がルール層を公開し、APIとSDKでコードからリンクを作成でき、開発者向けソリューションページとエッジリダイレクトアーキテクチャドキュメントで詳細を確認できます。ツールを評価している場合は、URLショートナーとは何かとURLショートナーは安全かのユーザー側の解説、そして料金ページで無料ティアの終わりを確認できます。