UUIDは、世界に一つだけのIDだ。
絶対にかぶらない。
だから安心して、システムAかBのどちらかに登録する。
一意なのだから、これで十分な気がする。
だが、この構成には、どこか筋の悪さがつきまとう。
IDを見ても、それがAにあるのかBにあるのか、分からない。
一意ではあるのに、居場所を教えてくれない。
この「なんとなく気持ち悪い」を、ちゃんと言葉にできるだろうか。
宛先のないIDの何が悪いのか
悪さを4点に分解する
まず、その筋の悪さを分解してみる。
題材は、UUIDで生成した一意なIDを、システムAかBのどちらかに登録する構成だ。
IDそのものは重複しない。
だが、どちらのシステムにあるかは、IDからは分からない。
この気持ち悪さは、四点に割れる。
- ルーティング情報の欠如。IDから所在を導けないため、全数問い合わせが強制される。読み取りコストは常に最大2倍になり、「どちらにも無い」の確認には両システムの正常応答が必須になる
- 責任境界の曖昧化。「どちらにあるか」を知る責任が未定義のため、各呼び出し元に判別ロジックが重複実装されるか、設計されていないルーティング層が後から生える
- 整合性保証の構造的不在。「重複が起こらない」はIDの生成の性質であって、登録の性質ではない。同じIDがA・B両方に登録される事故を構造的に発生不可能にする仕組みが無い(生成時一意 ≠ 格納時排他)
- 進化耐性の低さ。システムCの追加や移行の際、トポロジ変更のコストが呼び出し元全員に波及する
四つに見えるが、根は一点に集まる。
一意性と所在を混同し、後者を捨てていることだ。
定石とされるprefix埋め込み、単一の解決レイヤ、facadeは、すべてこの一点を埋める形をしている。
所在が「導出できる」とはどういうことか
この悪さを「決定論的に処理できない」と言いたくなる。
だが、それは少しずれている。
「Aに聞いて、無ければBに聞く」というアルゴリズム自体は、同じ入力に同じ結果を返す、立派に決定論的なものだ。
正確なのは、「IDから所在を導出できない」という言い方だ。
計算では決まらず、問い合わせという観測をしないと分からない。
対比の基準は、コンシステントハッシュやシャーディングキーだ。hash(ID) mod N のように、所在が純粋関数で決まる構成。
「決定論的」と言いたくなった直感は、この「導出可能な状態」との差を指していた。
識別子であってロケータではない
この悪さを一言でいうなら、「識別子であってロケータではない」が、いちばん通じる。
URNとURLという枯れた語彙に乗るので、聞いた側が自力で意味を展開できる。
| 一意性 | 所在 | それだけで取得可能 | |
|---|---|---|---|
URN(例:urn:isbn:...) | ○ | × | × |
URL(例:https://...) | △(移動で変わる) | ○ | ○ |
トレードオフは、きれいに対称だ。
URNは永続だが解決の仕組みが要り、URLは即アクセスできるがリンク切れのリスクを抱える。
DOIはその中間で、URN的な永続IDをdoi.orgというURLで解決するハイブリッドだ。
冒頭のUUID構成は、いわば「doi.orgが存在しない世界のDOI」だ。
名前だけあって、解決機構が無い。
それにしても、なぜこの悪さは一言にしにくいのか。
悪さが、性質の欠如だからだ。
何かが悪いのではなく、何かが無い。
無いものは、名指ししにくい。
比喩に置き換え、既存概念に接続し、同型の問題と比較する。
そうして初めて、輪郭が見えてくる。
同型問題:Bearerヘッダの中身が分からない
悪さの本質は同じで、価格が違う
その「同型の問題」が、身近なところにある。Authorization: Bearer に、APIキーとJWTのどちらかが入ってくる構成だ。
これも、構造は同じ問題を抱えている。
値が「自分が何者か」を宣言せず、受け取り側が判別の責任を負う。
ただ、こちらには救いがある。
JWTは xxx.yyy.zzz という形状から型を推定できる。
だから全数問い合わせではなく、安価な形状判定で分岐できる。
それでも、悪さは残る。
- 判別がヒューリスティックである。「JWTに見えるAPIキーを発行しない」という暗黙の前提に依存しており、保証ではなく偶然の非衝突にすぎない
- 認証失敗の意味が曖昧になる。JWTとして無効なのか、APIキーとして不一致なのかが区別できない
- 第三の形式を追加すると、判別ロジックが育っていく
UUID構成とBearer構成は、悪さの本質が同じで、悪さの価格が違う。
判別のコストが、「問い合わせ(高い・可用性に依存)」か「形状検査(安い・ローカルで完結)」かの差に還元される。
判別可能性とは、所在解決の手前にある、安価な近道なのだ。
定石は自己記述化
だから対処も、UUID構成と同じ方向を向く。
値に、自分が何者かを語らせる。
Stripeの sk_live_... のようにprefixで型を宣言させるか、schemeを分離する。Bearer はOAuth2トークン用に限定し、APIキーは X-API-Key のような別ヘッダに移す。
後者は、RFC 6750(OAuth 2.0 Bearer Token Usage)の意味論にも沿っている。
不在の証明ができない
最後に、この問題の本当の怖さを、非エンジニアにも届く形にしておく。
鍵の比喩が効く。
鍵だけ渡されて、どこで使う鍵かは教えてもらえない。
だから、手当たり次第に試して回るしかない。
総当たりのコストは、まだいい。
我慢すれば済む話だ。
本当に深刻なのは、その先にある。
全部試して開かなかったことが、何を意味するのか分からない、という点だ。
「存在する」は、一回開けば確定する。
だが「存在しない」と言い切るには、すべての錠前が正常に動いていたことが前提になる。
存在確認と不在確認のコストは、非対称だ。
不在側だけが、全システムの可用性に寄りかかっている。
障害のとき、「データがロストした」のか「一時的に見えないだけ」なのかを区別できない。
これは運用上、致命的だ。
だから、冒頭の「なんとなく気持ち悪い」の正体は、コストの話ではなかった。
この構成では、不在の証明ができない。
開かなかったとき、「鍵が違う」のか「開かずの扉が混ざっていた」のか、分からない。
一意なIDは、たしかに鍵だった。
ただ、鍵は住所を兼ねてくれない。
そのことを設計の最初に忘れると、しわ寄せは消えず、受け取る側の全員に静かに散らばっていく。
