エージェントSDKなしで、/procからサービス間依存関係をマッピングする
どのサービスがどのサービスと通信しているかを知るために、何かを計装する必要はありません。カーネルにはすでに記録されています。
2026年7月10日 · 8分で読了
「各サービスにエージェントを追加する」というのが、サービス検出に対してほとんどのオブザーバビリティツールが出す答えであり、 すべてのデプロイマニフェストに触れ、すべてのプロセスを再起動し、自分に直接レポートしない十数個のサービスの間でSDKバージョンを 同期させ続ける覚悟があるなら、それは合理的な答えです。しかし、誰もまだ設定を1行たりとも変えることに同意していない状態で、 プロダクトを使い始めて最初の5分でシステムをマッピングしようとしているなら、その答えはかなり合理性を欠きます。
カーネルはすでにどのソケットが開いていて、その所有者が誰かを知っています。あとはそれを読みに行くだけです。
/procに実際にあるもの
Linux上のすべてのプロセスは/proc/<pid>配下にディレクトリを持ち、その中のnet/tcp(IPv6用のnet/tcp6)には、そのプロセスのネットワーク名前空間から見えるすべてのTCPソケットが一覧されています: ローカルアドレス、リモートアドレス、接続の状態、そして — ここで重要になるフィールドである — inode番号です。 このinodeは、実際にそのソケットであるファイルディスクリプタに対して、/proc/<pid>/fd/内の シンボリックリンクの参照先として見つかるのと同じ番号です。各プロセスのファイルディスクリプタを走査し、inodeをソケットテーブルと 突き合わせれば、プロセスひとつにも許可を求めることなく、プロセスと接続のマップが得られます。
そこから先は発明ではなく足し算です: 各PIDをそのコンテナにマッピングし(cgroupのパスやDocker APIが直接教えてくれます)、 「プロセス4821が10.0.4.12:5432への確立された接続を持っている」という生のリストが、 「ordersコンテナがpostgresコンテナと通信している」に変わります。ordersの中にエージェントは不要、postgresの中にもエージェントは不要、 どちらのコード内にも自分が観測されていると知っているパスは一切ありません。
実際にイテレーションを要した部分
テーブルを一度読むのは簡単な80%です。問題は/proc/net/tcpがスナップショットであることです: 2回の読み取りの間に開いて閉じた接続は、どちらのサンプルにも一切現れません。60秒のポーリング間隔に対して45秒ごとに発火する ヘルスチェック、200msで完了する送信呼び出し、5分間のウィンドウ内で一度だけ接続してそれきりのバッチジョブ — これらはすべて 実在する依存関係であり、単純な2枚のスナップショットの差分では常にこれらを取り逃します。トラフィックの多いサービスでは、 興味深いトラフィックの大部分がまさにこの種の短命な接続であり、トポロジービューがすでに「完全だ」と自分を納得させた後で それに気づくのは厄介なことです。
私たちの解決策は、単純なスナップショットではなくスライディングな保持ウィンドウです: 直近5回のポーリングサイクルのいずれかで 見えたエッジはグラフに残り続け、そのウィンドウ全体で本当に不在だった後にのみ削除されます。賢いアルゴリズムというわけではありません — どちらかといえば「必要だと思うより少し長く、見たものを覚えておく」に近いものですが、それらしく見えるトポロジーと 実際に正しいトポロジーの違いを生む部分です。
このアプローチが行き着く限界
限界について率直であることには価値があります。コンテナごとにネームスペースが分かれる構成(DockerやKubernetesで一般的なケース)は、 ホスト共有のテーブルひとつではなく、各コンテナ自身のネットワークネームスペースを読むことを意味します — 会計処理は増えますが、 根本的に難しくなるわけではありません。暗号化されたトラフィックは接続を隠しません — ここで必要なのはペイロードではなくTCPの メタデータだけです — ただし、2つのコンテナが通信しているという事実しかわからず、どのプロトコルでどのエンドポイントに 向かっているのかはわかりません。そのためにBeylaベースのRED指標のレイヤーがあり、この2つは互いの代替ではなく、 一緒に読むことを前提に設計されています。そしてこれは基本的にTCPレイヤーのビューです: UDPベースのプロトコルには まったく別の読み取り方が必要で、それはリストには入っていますが、まだエージェントには実装されていません。
