システムデザイン2026年6月14日3 分で読了

六つの駅、六つの死に方:あるHTTPリクエストの生存の旅

六つの駅、六つの死に方:あるHTTPリクエストの生存の旅

序章:なぜこの道を理解する必要があるのか

深夜二時、画面の前に座り、冷たい timeout の一行を見つめながら、こう自問する夜がある —— この小さなリクエストは一体どこで死んだのか。ログの一行すら残さぬほど、静かに。

アラートが真夜中に鳴る。APIが timeout を返す。サービスのログを開く —— 真っさらだ。エラーも一つなく、スタックトレースもない。まるでリクエストが飛び立った瞬間に空気へ溶け、コードが実際に走る場所に一度も触れなかったかのように。

その瞬間が私に教えたのは、のちに生存原則となった一つの真実だった ——分散システムをデバッグするには、リクエストが辿る物理的な道を理解しなければならない。頭の中で想像する道ではなく。

私たちは頭の中で、まっすぐな矢印を描きがちだ:client → server。だが実際には、その矢印の両端のあいだに、ずらりと並んだ関所がある。それぞれの関所には、独自の規則・記憶・気質を持つ門番がいる。リクエストは「まっすぐ飛ぶ」のではない —— 一区間ずつ、転送され検査されアドレス変換され負荷分散される。そして各区間で、まったく異なる死に方をしうる。

この記事は、その道について私が理解したすべてを書き留める場所だ。解剖する具体的なアーキテクチャはこうだ:

[Client] → [DNS] → [nginx (Allow List)] → [APISIX] → [NodePort] → [Backend Pod]

最初に押さえておくべき重要な一点:このアーキテクチャでは、nginxAPISIX も Kubernetes クラスタの「外」にいる。 クラスタが外界に開くのは、たった一つの扉だけ —— NodePort である。なぜこう設計されるのか、そしてそれがどんな帰結をもたらすのかは、第五駅で明らかになる。


第一駅 — Client:旅は最初のバイトがマシンを出る前に始まっている

実際に起きていること

最初の「なるほど」:APIを呼ぶとき、リクエストはすぐには飛び立たない。一連の準備を経なければならず、その大半はあなた自身のマシン上で起きている。

ステップ1 — 名前解決。 まずクライアントは api.example.com がどのIPに対応するかを知る必要がある。DNS層(第二駅)に尋ねる。だが外へ問い合わせる前に、ローカルキャッシュを確認する。

ステップ2 — TCP接続の確立。 IPを得たら、クライアントは三ウェイハンドシェイク(SYN → SYN-ACK → ACK)でTCP接続を開く。これが、新しい接続のたびに、実データを送る前に挨拶だけで一往復のネットワーク往復を要する理由だ。

ステップ3 — TLSハンドシェイク(HTTPSの場合)。 HTTPSなら、TCPのあとにもう一つ儀式が続く:証明書の交換、暗号方式の交渉、セッション鍵の導出。さらに一〜二往復を要する。

なぜ keep-alive とコネクションプールが存在するのか

上の三ステップが高くつくからだ。もしリクエストごとにTCPハンドシェイクとTLSハンドシェイクを一から繰り返したら、遅延は凄まじいものになる。だからクライアント(そして下流のすべてのプロキシ)は keep-alive を使う:リクエストが終わっても接続を開いたまま保持し、次のリクエストが再利用できるようにする。「コネクションプール」とは、そうした開きっぱなしの接続を入れた籠のことだ。

これはまた、巧妙なバグの一種の種でもある —— 古くなった接続(stale connection)だ。向こう側のサーバーが静かに接続を閉じる(タイムアウト、再起動)が、クライアントはまだその終了シグナルを受け取っていない。接続が生きていると思い込み、死んだパイプにリクエストを送り、タイムアウトまで待ち続ける。

死にやすい場所: プール内の古い接続。特にバックエンドが再起動した直後、クライアント/プロキシが接続を長く保持しすぎたときに頻発する。

現場の武器:

bash
curl -v https://api.example.com/
# よく読む:"Re-using existing connection"(再利用)
# vs "Trying <ip>... Connected"(新規に開いている)

第二駅 — DNS:なぜ「IPを変えたのに古いサーバーに当たる」のか

DNSが何をするのか、そしてなぜキャッシュするのか

DNSはドメイン名をIPに翻訳する。単純に聞こえるが、世界のDNSシステムは一日に何兆ものクエリをさばく。もしすべてのクエリがルートサーバーまで辿る必要があれば、システムは崩壊する。だからDNSあらゆる層でキャッシュするよう設計されている —— これがその悲劇のすべてを理解する鍵だ。

TTLTime To Live)は、DNSレコードが自らについて宣言する数値だ:「N秒のあいだ私を覚えておけ、期限切れまで再び尋ねるな」。各キャッシュ層がこの数値を独自のやり方で尊重する。

なぜIP変更は即座に有効にならないのか

14:00に TTL = 300(5分)でバックエンドのIPを変えるとしよう。何が起きるか?

  • クライアントOSのキャッシュ:TTLが切れるまで古いIPを保持。

  • ISPのリゾルバ:自分のコピーを保持し、別の瞬間に期限切れになる(キャッシュした時刻が違うため)。

  • CDN、中間プロキシ:さらに別のコピー。

結果:TTLの窓のあいだ、一部のトラフィックは新しいIPへ、一部は古いIP —— たった今停止したサーバー —— へ流れ続ける。これがDNSのバグがしばしば**「半分」**のバグである理由だ:半分のリクエストは成功し、半分は失敗する。そしてまさにその「半分性」が診断を狂おしく難しくする —— 再試行すると、しばしば成功する半分に当たるからだ。

運用上の教訓

重要なサービスのIPを変える前に、数時間前にTTLを小さく(数十秒に)下げておき、古いTTLがネットワーク全体で切れるのを待ち、それから変更する。安定したら、負荷を減らすためにTTLを再び上げる。基本的な手法だが、忘れられやすい。

死にやすい場所: サーバーを変えたのに、TTLが切れていない窓のあいだ、一部のトラフィックがまだ古いIPに当たる。

現場の武器:

bash
dig +short api.example.com          # 現在解決されているIP
dig api.example.com | grep -A1 ANSWER  # 残りのTTLを見る —— ゼロへのカウントダウン

第三駅 — nginx (Allow List):痕跡を残さぬ殺人者

ここが、私が最も長く立ち止まりたい駅だ。なぜなら私の深夜二時のリクエストが死んだのはここであり、しかも、ほとんどの資料がきちんと説明しないやり方で死んだからだ。

ここでの nginx は最外層だ:誰が通るかを制御する allow list を備えたリバースプロキシ。allow list の設定には三つのスタイルがあり、なぜそれぞれ振る舞いが異なるのかを理解することこそが肝心だ。

スタイル1 — 送信元IPでフィルタ、そして「人違い」の罠

location /api/ {
    allow 10.0.0.0/8;
    deny  all;
    proxy_pass http://apisix_upstream;
}

ngx_http_access_module入ってくるTCP接続のIPをチェックする。問題:nginxが別のロードバランサーの後ろにいる場合、nginxが受け取るTCP接続はLBからであり、クライアントからではない。ネットワークレベルでは、本当のクライアントはLBの背後に「消えて」いる。

なぜか? TCP層では、各ホップは直前のホップしか見えないからだ。LBはnginxへ新しい接続を開くので、nginxにはLBのIPしか見えない。本当のクライアントIPはHTTP(L7)層の X-Forwarded-For ヘッダに収められている —— だが allow/deny は接続層で働くため、デフォルトではそのヘッダを無視する。

修正 —— 本当のIPを復元するためにnginxにヘッダを信頼させる:

set_real_ip_from 10.0.0.0/8;        # 内部LBからの接続のときだけXFFを信頼する
real_ip_header   X-Forwarded-For;

set_real_ip_from はセキュリティ上きわめて重要だ:自分が管理する送信元からのヘッダだけを信頼すること。さもないと攻撃者が X-Forwarded-For を偽装して allow list を回避しうる。

スタイル2 — DNSキャッシュの罠:なぜnginxは死んだIPを抱え込むのか

ここが最も価値のある部分だ。デフォルトでは、proxy_pass が静的なドメイン名を含むとき、nginxは設定ロード時にちょうど一度だけDNSを解決し、そのIPを次のリロードまでキャッシュする。

proxy_pass http://api.internal.example.com;   # ← 一度解決され、永遠に記憶される

なぜnginxはこうするのか? その設計思想が絶対的な速度だからだ。リクエストごとのDNS解決は遅延を加え、外部サービスへの依存を生む。だからnginxは起動時に前もって解決することを選び、以降の各リクエストがDNSに1ミリ秒も費やさないようにする。合理的なトレードオフだ —— バックエンドがIPを変えるまでは。

Kubernetes / クラウドの世界では、バックエンドのIPは絶えず変わる:Podの再起動、オートスケール、フェイルオーバー。一方nginxは、起動時のIPを頑なに抱え込み続ける。症状はまさに私の深夜二時の物語だ:断続的な 502/504、もう誰もいない「幽霊」アドレスへの呼び出し —— なのに設定は完璧に正しく見える

修正、そしてなぜ効くのか:

resolver 10.0.0.10 valid=10s;       # DNSリゾルバを宣言、結果は10秒だけ信頼
set $backend "api.internal.example.com";
proxy_pass http://$backend;          # 変数だから、nginxは実行時に解決せざるを得ない

核心はnginxの内部規則にある:proxy_pass が変数を含むとき、nginxはもはや起動時に前もって解決できない(変数の値は実行時にしか分からないため)。resolver を使って実行時にDNSを尋ねざるを得なくなり、valid=10s を尊重して定期的に再問い合わせする。小さな構文の変更が、キャッシュの振る舞いを完全に反転させる。

スタイル3 — map でドメインフィルタ、そしてなぜ if は危険なのか

map $host $allowed {
    default            0;
    "api.example.com"  1;
    "~^.*\.internal$"  1;   # 正規表現 —— 一文字間違えば漏らすか誤って遮断する
}
server {
    if ($allowed = 0) { return 403; }
}

map は効率的に動く。nginxが起動時にルックアップテーブルを構築するからだ。だが location ブロック内の if は危険なことで悪名高い("if is evil")—— 理由は、nginxの if が通常の条件文ではなく、内部の設定選択メカニズムに介入するため、他のディレクティブと組み合わさると直感に反する振る舞いを引き起こすことにある。server レベルで return のような単純なことに if を使うのは安全だが、location 内の if に複雑なロジックを詰め込むのは脆い。

死にやすい場所: realip の設定ミスによる無言の 403(スタイル1)、または死んだDNSキャッシュによる 502/504(スタイル2 —— 最も静かな殺人者)。

現場の武器:

bash
nginx -T              # マージされた設定の全体をダンプ —— include 後の本当の allow list を見る

黄金のコツ —— nginxが実際にどのIPへ接続するかを見るために、ログフォーマットに $upstream_addr を加える:

log_format up '$remote_addr -> $host -> $upstream_addr status=$status';

$upstream_addr が死んだIPを指していたら —— DNSキャッシュのバグを現行犯で捕まえたことになる。


第四駅 — APISIX:なぜここのエラーは「親切」なのか

nginx vs APISIX —— 何が違うのか

自然な疑問:すでにnginxがあるのに、なぜAPISIXも必要なのか?(面白いことに、APISIX自体が nginx + OpenResty の上に構築されている。)答えは知性のレベルにある。

上の役割でのnginxは、主に低レベルの仕事をする:転送、IPフィルタ、ドメインフィルタ。APISIXは完全な API Gateway であり、レイヤー7の深くで動作する —— HTTPのセマンティクスを理解し、APIのライフサイクルを管理する:パス/メソッド/ヘッダによるルーティング、認証(JWT、key-auth、OAuth)、レート制限、リクエスト/レスポンスの変換、可観測性。これらを、リロードなしで実行時に設定可能な動的プラグインシステム(通常はetcd経由)を通じて行う。

言い換えれば:nginxは門番、APISIXはビル全体の管理人だ。

なぜAPISIXのエラーは診断しやすいのか

それが意図的で、意味を持つエラーだからだ。DNSキャッシュの無言の死とは異なり、APISIXは通常、ステータスコードでその理由を述べる:

  • 401 Unauthorized —— 認証情報の欠落または誤り。

  • 403 Forbidden —— 認証は有効だが権限不足。

  • 429 Too Many Requests —— レート制限超過。

  • 502 Bad Gateway —— 「upstreamを呼んだが、まともに応答しなかった」

  • 504 Gateway Timeout —— 「upstreamを呼んだが、いつまでも応答がなかった」

責任の切り分け —— ここで最も重要なデバッグスキル

ステータス

意味

責任の所在

500

コードがリクエストを処理し、例外を投げた

Backend Pod —— ロジック/コードのエラー

502

ゲートウェイはupstreamに届いたが、壊れた応答/接続拒否を受けた

APISIX → backend の経路、またはバックエンドのクラッシュ

504

ゲートウェイはupstreamを呼んだが、待ち時間を使い切った

バックエンドが遅すぎる、または経路の輻輳

この違いを理解すれば何時間も節約できる:502/504 を見たらコードを読み込んで座っていてはいけない(コードはそもそも走ってすらいない)、ネットワーク経路とバックエンドの健全性を調べよ。500 を見たら、そのとき初めてコードを開く。

死にやすい場所: 502(ネットワーク/upstreamのエラー)を 500(コードのエラー)と取り違える —— 間違った墓を掘ること。


第五駅 — NodePort:唯一の扉、そしてなぜそれが便利かつ危険なのか

この駅は最大の「なるほど」を運んでくる。そしてアーキテクチャの設計が、その深いトレードオフを露わにする場所だ。

背景:境界で隔てられた二つの世界

nginxとAPISIXはクラスタのにいる。バックエンドPodにいる。そのあいだにはネットワークの境界がある:K8sの内部ネットワーク(Podネットワーク)は通常、外部から直接ルーティングできない。では、APISIX(外)はどうやってPod(内)に届くのか?

Kubernetesはサービスを外部に公開するいくつかの方法を提供する。ここでは NodePort を使う —— クラスタのすべてのノード上に固定ポート(範囲 30000–32767)を開くService種別だ。<任意のノードIP>:<nodeport> を呼べばサービスに届く。

APISIXはupstreamを node-ip:31080 を指すよう宣言する。それで終わり —— これがこのアーキテクチャにおける「サービスディスカバリ」のすべてだ。Ingress Controllerも、いかなる動的ディスカバリ機構も不要なほど単純である。

kubectl get svc の一行を読む —— そして各数字を理解する

NAME          TYPE       CLUSTER-IP     PORT(S)        AGE
backend-svc   NodePort   10.96.12.34    80:31080/TCP   30d

80:31080/TCP という塊は、まるごと一つの物語を語る:

  • 80 —— ClusterIPのポート、クラスタ内から呼ぶ者のため(10.96.12.34:80)。

  • 31080 —— すべてのノード上に開かれたポート、クラスタ外から呼ぶ者のため(node-ip:31080)。

初心者が立ち止まる点: NodePortClusterIP置き換えない、その上に積み重なる。あらゆるNodePort Serviceは暗黙にClusterIPを持つ。なぜか? K8sの内部機構がルーティングのために依然としてClusterIPを必要とするからだ —— NodePortは、その上に取り付けられた「外部への公開」層にすぎない。帰結:あなたのバックエンドは複数の入口経路を併存させており、択一ではない。

核心の「なるほど」:NodePortはアドレスではなく漏斗である

これが最も微妙な点であり、下層の機構を理解することが重要になる場所だ。

APISIXが node-A:31080 を呼ぶとき、実際に処理するPodはノードAにないかもしれない。 下層の機構:各ノードでは kube-proxy プロセスが走り、iptables(または IPVS)のルールをあらかじめ構築している。パケットがノードAのポート31080に到着すると、そのルールが宛先Podを選ぶ —— ノードA上のPodかもしれないし、ノードB上のPodかもしれない —— そして DNAT(宛先アドレス変換)を行い、パケットをそこへ転送する。これはノード間の**「余分なホップ」**であり、ネットワーク層の下で静かに起きている。

その機構を理解すると、二つの大きな罠が浮かび上がる:

罠1 —— 単一のノードIPをハードコードするな。 NodePortすべてのノードに開くが、もしAPISIXが一つの固定ノードIPだけを指していたら、バックエンド全体の運命はその一つのノードの健全性に縛られる。そのノードが死ぬ → 他のノード上のPodが元気に生きていても、バックエンド全体がAPISIXにとって「消える」。これはnginxのDNSキャッシュの罠の近縁だ:単一のアドレスを不死だと思うな。 解決策:ノードの前にL4ロードバランサーを置くか、APISIXに複数のノードIPとヘルスチェックを与える。

罠2 —— 送信元IPが隠される(なぜ?)。 パケットがノードAからノードB(Podがいる場所)へ跳ぶとき、ノードAは SNAT を行わねばならない —— 送信元アドレスをノードA自身のIPに変える。なぜ必須か? Podからの応答パケットは、(DNATが起きた)ノードAに戻って逆変換され、それからクライアントに返される必要があるからだ。SNATがなければ、Podはクライアントに直接返答してしまい、アドレス変換の連鎖が壊れる。帰結:バックエンドはノードのIPを見るのであって、APISIX/クライアントの本当のIPではない。監査のために本当のIPのログが必要な者は、ここで苦しむ。

逃げ道は externalTrafficPolicy: Local だ —— パケットを受け取ったまさにそのノード上のPodにのみルーティングするので、余分なホップもSNATもなく、送信元IPが保たれる。だがトレードオフ:Podのいないノードはトラフィックを受け取らず、負荷分散はより不均等になる。

アーキテクチャ全体のトレードオフ

「nginx/APISIX がクラスタ外 → NodePortPod」というモデルは、きわめて単純で独立している:Ingress Controller不要、動的サービスディスカバリ不要、ゲートウェイはクラスタのライフサイクルから切り離されている。引き換えに、APISIX(賢いL7)はバックエンドの負荷分散を kube-proxy(内容を見ないL4)に委ねねばならない。APISIXはどのPodが生きている/死んでいるかを知らず、個々のPodまでヘルスチェックできない —— ただNodePortの扉を叩き、kube-proxyが道案内してくれると信じるだけだ。これは単純さきめ細かな制御のあいだのトレードオフである。


第六駅 — Pod:なぜ「Running」は「準備完了」を意味しないのか

旅のすべてを経て、旅人は目的地に辿り着く:バックエンドPod —— コードが実際に走る場所だ。

初心者が混同しがちな二つの状態

最後の罠は、「動いている」と「提供する準備ができている」の違いにある:

Running は、コンテナが起動し、まだ死んでいないことを意味するだけだ。Podがトラフィックを受け取る準備ができているかについては何も語らない。Running になりたてのPodは、キャッシュをロード中、DBへの接続を開いている最中、初期化中かもしれない —— まだ提供できない。

Readinessプローブ「私はリクエストを受け取る準備ができているか?」 という問いに答える。機構:K8sは定期的にPodのエンドポイント(例:/healthz)を呼ぶ。このプローブが通って初めて、PodのIPがServiceの Endpoints リストに加えられる —— つまり、そこで初めてkube-proxyの負荷分散の輪に入る。readinessが誤設定(または欠落)していると、kube-proxyはまだ準備のできていないPodにリクエストを押し込みうる → 断続的なエラー、ある瞬間は動き次の瞬間は動かない。

Livenessプローブ「私はまだ生きているか、それともハングしたか?」 という問いに答える。このプローブが失敗すると、K8sはPod殺して再起動する。罠:過度に敏感に設定する(タイムアウトが短すぎる)と、重いリクエストを処理している真っ最中にK8sがPodを殺す —— 自らの手で不安定さを製造する。

重要なつながり:Endpointsこそが橋である

readinessが Endpoints を制御していると理解すれば、強力なデバッグツールが手に入る。Endpoints は、Serviceが「準備完了」とみなすPod IPのリストだ。このリストが空なら、Serviceへ入るすべてのリクエストは虚空に落ちる —— Podがまだ Running であってもだ。

死にやすい場所: 誤ったreadinessプローブが、まだ準備のできていないPodにトラフィックを殺到させる —— 特にデプロイ直後に頻発する。症状:デプロイが終わり、エラーが数分ちらつき、やがて自然に治る(最後のPodがついに準備完了になったとき)。

現場の武器:

bash
kubectl get endpoints backend-svc    # 現在ReadyとみなされているPod IPのリスト
kubectl describe pod <pod-name>       # readiness/liveness と最近のイベントを見る
kubectl get pod <pod-name> -o wide    # Podがどのノードにいるか(NodePortの罠と照合)

検死:次回のための照合表

症状

容疑の駅

なぜ

サーバーを変えたのにまだ古いIPに当たる

DNS

TTLが切れておらず、多層キャッシュが古いIPを保持

断続的な 502/504$upstream_addr が見慣れぬIPを指す

nginx

死んだDNSキャッシュ —— resolver + 変数が必要

403 + ログ "access forbidden by rule"

nginx

allow/deny が誤ったIPを見ている —— realip が必要

明確な 401/403/429

APISIX

意図的なエラー —— メッセージをよく読む

502 だがAPISIXのログは綺麗

NodePort

指している先のノードが死んでいる

負荷が一つのノードに大きく偏る

NodePort

単一ノードIPをハードコード、均等に分散していない

バックエンドのログがノードのIPばかり

NodePort

SNATが送信元IPを隠す —— externalTrafficPolicy: Local を検討

デプロイ直後の断続的エラー

Pod

誤ったreadinessプローブ、準備前にトラフィックを受信

endpoints が空、リクエストが虚空へ

Pod/Service

readinessを通るPodが一つもない

そして私の深夜二時のリクエストは? 犯人は 第三駅 だった —— 静的な proxy_pass のせいで、nginxが死んだIPを抱え込んでいた。resolver という一語を欠いた設定の一行が、眠れぬ一夜と骨に刻まれた教訓に化けた。


結び:速く行くために、ゆっくり進め

あの夜のあとの最大の教訓は、技術的なコツではなく、一つの態度だった。

システムが壊れたとき、私たちの本能は、最も馴染みのあるもの —— たいていはコード —— に飛びつく。だが旅人はコードに触れる前に、六つの関所を通り抜けていた。犯人はそのどこに潜んでいてもおかしくない。

優れたデバッグとは、速く推測することではなく、順を追って歩き、決して飛ばさないことだ。リクエストが辿ったまさにその道をなぞる —— client、DNS、nginx、APISIX、NodePortPod —— そして各駅で、ちょうど二つの問いを発する:「旅人はここを通ったか? そして、去るとき、まだ生きていたか?」

システムを深く理解するとは、すべてのディレクティブを暗記することではなく、各層がなぜ存在するのか、それが何をトレードオフしているのか、誤解されたときどんなふうに自分を裏切るのかを理解することだ。この記事のすべての層 —— DNSTTLから、nginxのキャッシュ、NodePortの漏斗まで —— は、速度・単純さ・制御のあいだのトレードオフの決断である。そのトレードオフを理解すれば、バグを直せるだけでなく、より良いシステムを設計できる。

リクエストは、私たちが聴き方を知っていれば、自らの死の物語をまるごと語ってくれる。必要なのは、その全行程を傍らで歩き通す忍耐だけだ —— 古びた道に残された一つひとつの足跡を辿る、懐旧の旅人のように。

—— もはやアラートのために起きていなくてよい夜に記す。

関連する読み物

Nginxは気まぐれなんかじゃない — ただ設定の読み方を間違えているだけソフトウェア開発
2026年6月11日3 min

Nginxは気まぐれなんかじゃない — ただ設定の読み方を間違えているだけ

開発者の間には、こんな笑える矛盾があります。バックエンドをRustで書くべきか、Goか、それともNode.jsか — そんな議論なら一週間でも喜んで続けるのに、いざ本番にデプロイするとなると、90%の人は黙って apt install nginx と打ち込み、前段に置いてしまう。Nginxはみんなの定番「門番」であり、同時に、たった一つのシンプルなリクエストがなぜ404を返し続けるのか分からず、人を深夜2時までデバッグさせる張本人でもあります。 面白いのはここです。バグはほぼ確実にNginxにはありません。原因は、設定ファイルを上から下へ順番に実行されるスクリプトのように読んでしまうこと — Nginxはまったくそんな動き方をしないのに、です。 この記事は、サーバーにコピペするための「Nginxよくあるエラー集」ではありません。狙いは思考のモデルを手渡すこと。Nginxがどう「考える」のかを理解すれば、一見魔法のように見える罠が、画面を前に固まる代わりに、予測できるものに変わります。

読む
10年間、盲目的に信じてきたものを理解するために、DNSサーバーを自作したシステムデザイン
2026年6月13日1 min

10年間、盲目的に信じてきたものを理解するために、DNSサーバーを自作した

毎日使っているのに、一度も正面から向き合ったことのない技術がある。私にとってDNSがまさにそれだった。 10年間、ドメイン名を打ち続けてきた。ドメインを買い、レコードを設定し、「伝播(propagation)」を待ち、上がらないと悪態をつき、上がると喜ぶ——なのに、なぜ上がるのかを本当には理解していなかった。私にとってDNSは壁のスイッチのようなものだった。押せば部屋が明るくなる。壊れたら電気屋を呼ぶ。 あるデプロイの夜まではそうだった。サイトが落ちた。IPへのpingは通る。IPに直接curlするとページが返ってくる。ただドメイン名だけが沈黙していた。私はそれを見つめながら、少し情けない事実に気づいた。どこから直せばいいのかすら分からない。なぜなら、壊れているその当のものを、一度もきちんと理解したことがなかったからだ。 そこで、おそらくエンジニアだけが合理的だと思えることをやった。ドキュメントをざっと読む代わりに、座ってRustでDNSサーバーを丸ごと書いたのだ。プロジェクト名は mini-dns。この記事は、ようやく蓋を開けたとき、そのブラックボックスが私に語ってくれたことの記録である。

読む
欲張りコード」を捨てれば開発は楽になる:実戦から学ぶ単一責任の原則(SRP)の力ソフトウェア開発
2026年5月24日2 min

欲張りコード」を捨てれば開発は楽になる:実戦から学ぶ単一責任の原則(SRP)の力

本番環境で発生する重大なバグの多くは、アルゴリズムの不備ではなく、一箇所に責務を詰め込みすぎることから生まれます。本記事は、単一責任の原則(SRP)を実務的な視点から掘り下げます。「便利だから」という罠がどのようにGod Objectを生み出すのか、典型的な注文処理関数がなぜ保守の地雷と化すのか、そしてシニアエンジニアがそれをいかにしてクリーンでテスト可能な層へとリファクタリングするのか。クリーンな設計はタダではありません。しかし機能が100に膨れ上がったとき、それこそがシステムの崩壊を防ぐ唯一の支えとなるのです。

読む