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_golang
とprometheus/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.go
・go.mod
・Dockerfile
の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"}
おわり
多分ここまでしなくても簡単にできる方法があると思いますが(というかホストネットワークを使えば不要)、メトリクス作成の勉強にはなったので良かったとします。
カテゴリ: ネットワーク