雑記帳

整理しない情報集

更新情報

Dockerのホストネットワークを使わずにnetdevを取得する

公開日:

カテゴリ: ネットワーク

node-exporterでネットワークI/Oを収集するときにnetlinkもしくは/proc/net/devを参照するのですが、ネットワークモードをホストにしない場合、コンテナのネットワークI/Oが収集されます。

# ホストの / を /host にマウントしている場合
docker exec -it node-exporter cat /host/proc/net/dev
Inter-|   Receive                                                |  Transmit
 face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
    lo:       0       0    0    0    0     0          0         0        0       0    0    0    0     0       0          0
  eth0: 2557314   18097    0    0    0     0          0         0 55619187   18085    0    0    0     0       0          0

取得したいのはホストのネットワークI/Oなのですが、全く別のネットワークであるためか取得できません。そこで、どうにかしてホストのネットワークI/Oを取得してみます。

(netlinkはちょっと調べたレベルでは分からないので一旦置いておきます)

そもそもどうなっているのか

まずはホストをマウントしている/host/proc/net/devが何を見ているのかを調べてみます。

docker exec -it node-exporter ls -al /host/proc/net/dev
docker exec -it node-exporter ls -al /host/proc/net
docker exec -it node-exporter ls -al /host/proc
docker exec -it node-exporter readlink -f /host/proc/net/dev
# ls -al /host/proc/net/dev
-r--r--r--    1 root     root             0 Jul  6 01:45 /host/proc/net/dev
# ls -al /host/proc/net
lrwxrwxrwx    1 root     root             8 Jul  5 10:47 /host/proc/net -> self/net
# ls -al /host/proc (抜粋)
lrwxrwxrwx    1 root     root             8 Jul  5 10:47 net -> self/net
lrwxrwxrwx    1 root     root             0 Jan  1  1970 self -> 64785
# readlink -f /host/proc/net/dev
/host/proc/64566/net/dev

どうやら、/host/proc/net/host/proc/self/net/host/proc/<pid>/netにリンクされているようです。selfは自身のプロセスを指すので、ホスト側のファイルシステムではコンテナの名前空間にリンクされているように見えます(詳しくは分かっていません)。

ホスト側のデータを探す

ところで、node-exporterで出力される他のネットワーク関連の収集項目を見ていると、node_network_upなどホストのNICが見えているものもあります。というより、node_network_receive_*node_network_transmit_*以外の項目はホスト側の項目が見えています。

よくよく考えてみたら、ホストのファイルシステムをそのままマウントしているので、ホスト側のプロセスのnetdevを直接見に行けば良いのでは・・・?

docker exec -it node-exporter cat /host/proc/1/net/dev
Inter-|   Receive                                                |  Transmit
 face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
    lo:    8440      96    0    0    0     0          0         0     8440      96    0    0    0     0       0          0
  eth0:       0       0    0    0    0     0          0         0        0       0    0    0    0     0       0          0
 wlan0: 48405598  101479    0    0    0     0          0      8171 206104502  186753    0    0    0     0       0          0
br-a3d1745f80cc: 196255706   62689    0    0    0     0          0         0  7371572   46110    0    7    0     0       0          0
docker0:    1364      16    0    0    0     0          0         0     5145      13    0   23    0     0       0          0
veth3d9b8d1: 6963776    7296    0    0    0     0          0         0  1903540    7307    0    0    0     0       0          0

(長いので一部省略)

dockerネットワークのIF名が長いのでレイアウトが崩れていますが、ホスト側のnetdevが見えました。ということで、このファイルを監視すればホスト側のネットワークI/Oが取得できそうです。

実装方法

node-exporterのソースを見に行ってみます。netdev関連はcollector/netdev_linux.goにあります。

func procNetDevStats(filter *deviceFilter, logger *slog.Logger) (netDevStats, error) {
	metrics := netDevStats{}

	fs, err := procfs.NewFS(*procPath)
	if err != nil {
		return metrics, fmt.Errorf("failed to open procfs: %w", err)
	}

	netDev, err := fs.NetDev()

node-exporterではnetdevのPIDを指定する方法は無さそうです。ただprometheusのprocfsという別リポジトリのライブラリを呼び出して実行しているみたいなので、procfs次第では簡単に自力で実装できそうです。

公式ドキュメントを読むと、任意のPIDを指定する方法がありました。ということで、こんな感じのコードでPID1のnetdevを取得できそうです。

fs, _ := procfs.NewFS("/host/proc")
proc, _ := fs.Proc(1)
netdev, _ := proc.NetDev()

あとはpromhttp経由でメトリクスを出力するHTTPサーバを構築すれば良さそうです。

コードを書く

package main

import (
    "log"
    "net/http"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "github.com/prometheus/procfs"
)

var (
    rxBytesGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_receive_bytes",
        Help: "Cumulative count of bytes received",
    }, []string{"interface"})
    rxPacketsGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_receive_packets",
        Help: "Cumulative count of packets received",
    }, []string{"interface"})
    rxErrorsGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_receive_errors",
        Help: "Cumulative count of receive errors encountered",
    }, []string{"interface"})
    rxDroppedGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_receive_dropped",
        Help: "Cumulative count of packets dropped while receiving",
    }, []string{"interface"})
    rxFIFOGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_receive_fifo",
        Help: "Cumulative count of FIFO buffer errors",
    }, []string{"interface"})
    rxFrameGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_receive_frame",
        Help: "Cumulative count of packet framing errors",
    }, []string{"interface"})
    rxCompressedGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_receive_compressed",
        Help: "Cumulative count of compressed packets received by the device driver",
    }, []string{"interface"})
    rxMulticastGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_receive_multicast",
        Help: "Cumulative count of multicast frames received by the device driver",
    }, []string{"interface"})
    txBytesGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_transmit_bytes",
        Help: "Cumulative count of bytes transmitted",
    }, []string{"interface"})
    txPacketsGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_transmit_packets",
        Help: "Cumulative count of packets transmitted",
    }, []string{"interface"})
    txErrorsGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_transmit_errors",
        Help: "Cumulative count of transmit errors encountered",
    }, []string{"interface"})
    txDroppedGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_transmit_dropped",
        Help: "Cumulative count of packets dropped while transmitting",
    }, []string{"interface"})
    txFIFOGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_transmit_fifo",
        Help: "Cumulative count of FIFO buffer errors",
    }, []string{"interface"})
    txCollisionsGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_transmit_collisions",
        Help: "Cumulative count of collisions detected on the interface",
    }, []string{"interface"})
    txCarrierGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_transmit_carrier",
        Help: "Cumulative count of carrier losses detected by the device driver",
    }, []string{"interface"})
    txCompressedGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Name: "netdev_transmit_compressed",
        Help: "Cumulative count of compressed packets transmitted by the device driver",
    }, []string{"interface"})
)

func init() {
    if err := prometheus.Register(rxBytesGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(rxPacketsGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(rxErrorsGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(rxDroppedGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(rxFIFOGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(rxFrameGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(rxCompressedGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(rxMulticastGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(txBytesGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(txPacketsGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(txErrorsGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(txDroppedGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(txFIFOGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(txCollisionsGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(txCarrierGauge); err != nil {
        panic(err)
    }
    if err := prometheus.Register(txCompressedGauge); err != nil {
        panic(err)
    }
}

func main() {
    promHandler := promhttp.Handler()
    handleFunc := func(w http.ResponseWriter, r *http.Request) {
        fs, err := procfs.NewFS("/host/proc")
        if err != nil {
            panic(err)
        }

        proc, err := fs.Proc(1)
        if err != nil {
            panic(err)
        }

        netdev, err := proc.NetDev()
        if err != nil {
            panic(err)
        }

        for _, stats := range netdev {
            name := stats.Name
            rxBytesGauge.WithLabelValues(name).Set(float64(stats.RxBytes))
            rxPacketsGauge.WithLabelValues(name).Set(float64(stats.RxPackets))
            rxErrorsGauge.WithLabelValues(name).Set(float64(stats.RxErrors))
            rxDroppedGauge.WithLabelValues(name).Set(float64(stats.RxDropped))
            rxFIFOGauge.WithLabelValues(name).Set(float64(stats.RxFIFO))
            rxFrameGauge.WithLabelValues(name).Set(float64(stats.RxFrame))
            rxCompressedGauge.WithLabelValues(name).Set(float64(stats.RxCompressed))
            rxMulticastGauge.WithLabelValues(name).Set(float64(stats.RxMulticast))
            txBytesGauge.WithLabelValues(name).Set(float64(stats.TxBytes))
            txPacketsGauge.WithLabelValues(name).Set(float64(stats.TxPackets))
            txErrorsGauge.WithLabelValues(name).Set(float64(stats.TxErrors))
            txDroppedGauge.WithLabelValues(name).Set(float64(stats.TxDropped))
            txFIFOGauge.WithLabelValues(name).Set(float64(stats.TxFIFO))
            txCollisionsGauge.WithLabelValues(name).Set(float64(stats.TxCollisions))
            txCarrierGauge.WithLabelValues(name).Set(float64(stats.TxCarrier))
            txCompressedGauge.WithLabelValues(name).Set(float64(stats.TxCompressed))
        }

        promHandler.ServeHTTP(w, r)
    }

    mux := http.NewServeMux()
    mux.HandleFunc("/metrics", handleFunc)
    server := http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    log.Fatal(server.ListenAndServe())
}

動けばいいで作ったのでひどいコードを見た感じがしますが、とりあえず動くのでこれで良しとします。

procfsのマウントポイントが/host/procでハードコードされているので、異なる方は環境に合わせて変更するかflagパッケージを使用して起動引数を使用するオプションにした方が良いでしょう。

module netdev
go 1.23.0

require github.com/prometheus/client_golang v1.22.0
require github.com/prometheus/procfs v0.17.0

require (
        github.com/beorn7/perks v1.0.1 // indirect
        github.com/cespare/xxhash/v2 v2.3.0 // indirect
        github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
        github.com/prometheus/client_model v0.6.1 // indirect
        github.com/prometheus/common v0.62.0 // indirect
        golang.org/x/sys v0.33.0 // indirect
        google.golang.org/protobuf v1.36.5 // indirect
)

go.modはこんな感じ。prometheus/client_golangprometheus/procfsがあればOKです。

FROM golang:alpine3.22 as builder
WORKDIR /app
COPY . ./
RUN go mod tidy && go build -ldflags="-w -s" -tags netgo -a -o netdev .

FROM scratch
COPY --from=builder /app/netdev /app/
ENTRYPOINT ["/app/netdev"]

あとはDockerfileを書いてイメージの作成準備は完了です。libc依存が無いので、まっさらなscratchイメージが使えます。

動かす

# services:
  netdev-exporter:
    build:
      context: ./netdev
    container_name: netdev-exporter
    volumes:
      - /:/host:ro,rslave
    expose:
      - 8080
    networks:
      - prometheus
    restart: always

Prometheusのcompose.ymlのサービスに作成したexporterを追加します。先程作成したnetdev.gogo.modDockerfileの3ファイルがcompose.ymlと同じディレクトリ内のnetdevディレクトリにある想定です。

# scrape_configs:
  - job_name: 'netdev-exporter'
    scrape_interval: 15s
    static_configs:
      - targets: ['netdev-exporter.prometheus:8080']

prometheus.ymlに取得するexporterを追加します。

あとはGrafanaなどで追加したexporterからデータを受け取れば良いでしょう。値は累積値であるためirate()などで加工すれば求められます。GaugeではなくCounterを使えと言われそうですが

# 受信バイト数
netdev_receive_bytes{instance="netdev-exporter.prometheus:8080", job="netdev-exporter"}
# 送信バイト数
netdev_receive_bytes{instance="netdev-exporter.prometheus:8080", job="netdev-exporter"}

おわり

多分ここまでしなくても簡単にできる方法があると思いますが(というかホストネットワークを使えば不要)、メトリクス作成の勉強にはなったので良かったとします。

カテゴリ: ネットワーク