Hiroaki Mizuguchi: The Offical Site

Linux Network NamespaceをGoで操作する

2019-12-24 18:23

TL;DR

  1. Go言語のgoroutineはdefaultではpreemptiveに動作するOS Threadが切り替わるのでOS Threadに強く紐づくlinuxのnamespace関連の操作を行うときはruntime.LockOSThread()しておく必要がある。1
  2. Go言語でLinuxのnetwork namespaceを操作したい場合はCNIのライブラリを使うのが便利

なんでこんな事してるの?

テナント(200~)毎にVMを用意してると管理やコストが大きいため、アドレス空間が衝突してるテナントに対してHTTP(S)リバースプロキシを提供する仕組みを作ってみようと思った。

Proof of Concept

試しに下記のコードを実行してみる。

package main import ( "log" "net" "net/http" "os" "runtime" "github.com/containernetworking/plugins/pkg/ns" ) func main() { nspath := os.Args[1] addr := os.Args[2] var err error var l net.Listener ns.WithNetNSPath(nspath, func(_ ns.NetNS) error { l, err = net.Listen("tcp", addr) return nil }) runtime.UnlockOSThread() if err != nil { log.Fatal(err) } if err := http.Serve(l, nil); err != nil { log.Fatal(err) } } 

このコード動かすには下記の様にネットワーク的に隔離されたコンテナを用意しておくとよい。

# build binary go build -o nsproxy nsproxy.go # setup environment docker run -d --net none --name pause k8s.gcr.io/pause:3.1 ns=$(docker inspect --format '{{ .NetworkSettings.SandboxKey }}' pause) # run program sudo ./nsproxy "$ns" 127.0.0.1:8080 & 

このバイナリを動かした場合、HTTPサーバーとして動作しているタイミングではコンテナのnetwork namaspace(以後netnsと表記)には存在していない。

# ls -l /proc/1/ns/net # hostの初期netnsの情報 lrwxrwxrwx 1 root root 0 Dec 24 21:42 /proc/1/ns/net -> 'net:[4026531984]' # ls -l /proc/$(pgrep nsproxy)/task/*/ns/net # nsproxyプロセスはホストのnetnsに居る lrwxrwxrwx 1 root root 0 Dec 24 21:42 /proc/4377/task/4377/ns/net -> 'net:[4026531984]' lrwxrwxrwx 1 root root 0 Dec 24 21:47 /proc/4377/task/4378/ns/net -> 'net:[4026531984]' lrwxrwxrwx 1 root root 0 Dec 24 21:47 /proc/4377/task/4379/ns/net -> 'net:[4026531984]' lrwxrwxrwx 1 root root 0 Dec 24 21:47 /proc/4377/task/4380/ns/net -> 'net:[4026531984]' lrwxrwxrwx 1 root root 0 Dec 24 21:47 /proc/4377/task/4381/ns/net -> 'net:[4026531984]' lrwxrwxrwx 1 root root 0 Dec 24 21:47 /proc/4377/task/4382/ns/net -> 'net:[4026531984]' lrwxrwxrwx 1 root root 0 Dec 24 21:47 /proc/4377/task/4393/ns/net -> 'net:[4026531984]' # ls -l /proc/$(docker inspect --format '{{.State.Pid}}' pause)/task/*/ns/net # containerのnetnsの情報 lrwxrwxrwx 1 root root 0 Dec 24 21:50 /proc/3867/task/3867/ns/net -> 'net:[4026532117]' 

しかしながらnsenterを用いてコンテナのnetnsの中に入ると127.0.0.1:8080でhttpサーバーが動作していることが分かる。

# nsenter --net=$(docker inspect --format '{{ .NetworkSettings.SandboxKey }}' pause) bash # ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever # ss -ltn State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 127.0.0.1:8080 0.0.0.0:* # curl http://127.0.0.1:8080 -v * Expire in 0 ms for 6 (transfer 0x5627619e7f50) * Trying 127.0.0.1... * TCP_NODELAY set * Expire in 200 ms for 4 (transfer 0x5627619e7f50) * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0) > GET / HTTP/1.1 > Host: 127.0.0.1:8080 > User-Agent: curl/7.64.0 > Accept: */* > < HTTP/1.1 404 Not Found < Content-Type: text/plain; charset=utf-8 < X-Content-Type-Options: nosniff < Date: Tue, 24 Dec 2019 12:58:10 GMT < Content-Length: 19 < 404 page not found * Connection #0 to host 127.0.0.1 left intact 

たくさんのコンテナからアクセスできるようにしてみる

この方法がどれだけスケールすのか試してみる。 Listenするポートを複数になるように拡張する。

package main import ( "log" "net" "net/http" "os" "runtime" "sync" "github.com/containernetworking/plugins/pkg/ns" ) func main() { addr := os.Args[1] var ls []net.Listener for _, nspath := range os.Args[2:] { ns.WithNetNSPath(nspath, func(_ ns.NetNS) error { l, err := net.Listen("tcp", addr) if err != nil { log.Fatal(err) } ls = append(ls, l) return nil }) } runtime.UnlockOSThread() var wg sync.WaitGroup for _, l := range ls { wg.Add(1) go func(l net.Listener){ err := http.Serve(l, nil) if err != nil { log.Print(err) } wg.Done() }(l) } wg.Wait() } 

下記の様に100個ほどコンテナを用意する

# 100個のコンテナを作成する seq 1000 1999 | xargs -I '{}' -exec docker run -d --net none --name 'pause{}' k8s.gcr.io/pause:3.1 # 100個のコンテナに対してListenする sudo ./nsproxy 127.0.0.1:8080 $(docker inspect --format '{{.NetworkSettings.SandboxKey}}' pause{100..199} ) & 

プロセスの稼働開始直後の状態

$ sudo cat /proc/$(pgrep nsproxy)/status Name: nsproxy Umask: 0022 State: S (sleeping) Tgid: 17082 Ngid: 0 Pid: 17082 PPid: 17068 TracerPid: 0 Uid: 0 0 0 0 Gid: 0 0 0 0 FDSize: 128 Groups: 0 NStgid: 17082 NSpid: 17082 NSpgid: 17068 NSsid: 3567 VmPeak: 618548 kB VmSize: 561720 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 10980 kB VmRSS: 10980 kB RssAnon: 6608 kB RssFile: 4372 kB RssShmem: 0 kB VmData: 161968 kB VmStk: 140 kB VmExe: 2444 kB VmLib: 1500 kB VmPTE: 140 kB VmSwap: 0 kB HugetlbPages: 0 kB CoreDumping: 0 Threads: 7 SigQ: 0/15453 SigPnd: 0000000000000000 ShdPnd: 0000000000000000 SigBlk: 0000000000000000 SigIgn: 0000000000000000 SigCgt: ffffffffffc1feff CapInh: 0000000000000000 CapPrm: 0000003fffffffff CapEff: 0000003fffffffff CapBnd: 0000003fffffffff CapAmb: 0000000000000000 NoNewPrivs: 0 Seccomp: 0 Speculation_Store_Bypass: thread vulnerable Cpus_allowed: ffff,ffffffff,ffffffff,ffffffff,ffffffff,ffffffff,ffffffff,ffffffff Cpus_allowed_list: 0-239 Mems_allowed: 00000000,00000001 Mems_allowed_list: 0 voluntary_ctxt_switches: 6 nonvoluntary_ctxt_switches: 0 

開始直後ではRSSが10980 kB程度とかなり軽量であることが分かる。

まとめ

network namespaceを触るのは怖くないので皆さんも触ってみてください。CNIのライブラリ自体は軽量なのでぜひとも実装自体を覗いてみてください。


  1. https://github.com/golang/go/wiki/LockOSThread ↩︎