containerd をデバッグする手順のメモ

containerd はコンテナランタイムのひとつ。CRI に準拠しており、Kubernetes のコンテナランタイムとして利用できる。 仕事で containerd の調査ができそうな気配があるので(あるかな?)、containerd を動作確認しつつ調査できるようにデバッグ環境を整える。

Go のインストール

containerd は Go 言語で記述されている。

github.com

ということで、まずは Go 言語をインストールする。

# yum install golang
...
# go version
go version go1.14.12 linux/amd64

Delve のインストール

Go で記述されたプログラムのデバッグには Delve がおすすめらしい。

Note that Delve is a better alternative to GDB when debugging Go programs built with the standard toolchain. It understands the Go runtime, data structures, and expressions better than GDB. Delve currently supports Linux, OSX, and Windows on amd64. For the most up-to-date list of supported platforms, please see the Delve documentation.

引用元: https://golang.org/doc/gdb

ということで、README を参考に Delve をインストールする。

# git clone https://github.com/go-delve/delve
# cd delve
# go install github.com/go-delve/delve/cmd/dlv

# dlv version
Delve Debugger
Version: 1.4.0
Build: $Id: 67422e6f7148fa1efa0eac1423ab5594b223d93b $

containerd のインストール

getting-started を参考に、GitHub のアーカイブからファイルを展開した。ちなみに selinux は黙殺した。

# wget https://github.com/containerd/containerd/releases/download/v1.5.0/cri-containerd-cni-1.5.0-linux-amd64.tar.gz
# tar -xf cri-containerd-cni-1.5.0-linux-amd64.tar.gz
# cp etc/systemd/system/containerd.service /usr/lib/systemd/system/
# cp usr/local/bin/* /usr/local/bin/
# cp usr/local/sbin/* /usr/local/sbin/
# setenforce 0
# systemctl daemon-reload
# systemctl restart containerd

しかしこのバイナリは debuginfo が削られてるため、Delve からは扱えないっぽい。

# ps -ef | grep containerd
root       11606       1  0 20:56 ?        00:00:00 /usr/local/bin/containerd
# dlv attach 11606
could not attach to pid 11606: could not open debug info

たしかにバイナリに debug セクションっぽいものはない(そもそも Go のデバッグ情報どういうセクション名になるのか知らんけど、Debugging Go Code with GDB によると、.debug_* sections. みたいな名前になるらしい)。

# readelf -S /usr/local/bin/containerd | grep debug

ということで、この方法はダメそう。

containerd のビルド

Delve のために go build -gcflags=all="-N -l" を設定して containerd を自前でビルドする。

以下の PR によると、GODEBUG=1 環境変数を設定すると上記ビルドオプションが設定されるらしい。
Add build option "GODEBUG=1"

BUILDING.md を参考にビルドする。

# wget -c https://github.com/google/protobuf/releases/download/v3.11.4/protoc-3.11.4-linux-x86_64.zip
# unzip protoc-3.11.4-linux-x86_64.zip -d /usr/local

# go get github.com/containerd/containerd
# cd $GOPATH/src/github.com/containerd/containerd/
# git checkout -b work refs/tags/v1.5.0

# GODEBUG=1 BUILDTAGS=no_btrfs make
# make install

readelf で確認しても debug 情報はちゃんと付いてる様子。

# readelf -S bin/containerd | grep debug
  [33] .zdebug_aranges   PROGBITS         0000000000000000  038affa0
  [34] .zdebug_pubnames  PROGBITS         0000000000000000  038b017a
  [35] .zdebug_info      PROGBITS         0000000000000000  038bf450
  [36] .zdebug_abbrev    PROGBITS         0000000000000000  03b168a6
  [37] .zdebug_line      PROGBITS         0000000000000000  03b173b2
  [38] .zdebug_frame     PROGBITS         0000000000000000  03c4aaa3
  [39] .zdebug_str       PROGBITS         0000000000000000  03cc3ebd
  [40] .zdebug_loc       PROGBITS         0000000000000000  03cc5971
  [41] .zdebug_pubtypes  PROGBITS         0000000000000000  03cda3b3
  [42] .zdebug_ranges    PROGBITS         0000000000000000  03d12fbc
  [43] .debug_gdb_script PROGBITS         0000000000000000  03d50530

runc のビルド

runc は go.mod で記載されたバージョンでビルドする。

# go get github.com/opencontainers/runc
# cd $GOPATH/src/github.com/opencontainers/runc
# cat ../../containerd/containerd/go.mod | grep opencontainers/runc
        github.com/opencontainers/runc v1.0.0-rc93
# git checkout -b work refs/tags/v1.0.0-rc93
# make BUILDTAGS='seccomp selinux'
go build -trimpath "-mod=vendor" "-buildmode=pie"  -tags "seccomp selinux" -ldflags "-X main.gitCommit="12644e614e25b05da6fd08a38ffa0cfe1903fdec" -X main.version=1.0.0-rc93 " -o runc .
# mv runc /usr/local/sbin/runc

Delve でデバッグする

dlv attach で起動済みのプロセスにアタッチする。

# systemctl restart containerd
# ps -ef | grep containerd
root       10937       1  0 22:30 ?        00:00:00 /usr/local/bin/containerd

# dlv attach 10937

うまく動いてるっぽい。

(dlv) funcs Pull
github.com/containerd/containerd.(*Client).Pull
github.com/containerd/containerd.(*Client).Pull.func1
github.com/containerd/containerd.(*Client).Pull.func2
github.com/containerd/containerd.WithPullLabel
github.com/containerd/containerd.WithPullLabel.func1
github.com/containerd/containerd.WithPullSnapshotter
github.com/containerd/containerd.WithPullSnapshotter.func1
github.com/containerd/containerd.WithPullUnpack
github.com/containerd/containerd/pkg/cri/server.(*criService).PullImage
github.com/containerd/containerd/pkg/cri/server.(*criService).PullImage.func1
github.com/containerd/containerd/pkg/cri/server.(*criService).encryptedImagesPullOpts
github.com/containerd/containerd/pkg/cri/server.(*instrumentedService).PullImage
github.com/containerd/containerd/pkg/cri/server.(*instrumentedService).PullImage.func1
github.com/containerd/containerd/remotes/docker.ContextWithAppendPullRepositoryScope
...
(dlv) break github.com/containerd/containerd/pkg/cri/server.(*criService).PullImage
(dlv) continue // ← 別ターミナルで crictl pull ubuntu を実行する
> github.com/containerd/containerd/pkg/cri/server.(*criService).PullImage() /root/go/src/github.com/containerd/containerd/pkg/cri/server/image_pull.go:92 (hits goroutine(83):1 total:1) (PC: 0x557fc2f618fd)
    87: // 4) Is the content important if we cached necessary information in-memory
    88: // after we pull the image? How to manage the disk usage of contents? If some
    89: // contents are missing but snapshots are ready, is the image still "READY"?
    90:
    91: // PullImage pulls an image with authentication config.
=>  92: func (c *criService) PullImage(ctx context.Context, r *runtime.PullImageRequest) (*runtime.PullImageResponse, error) {
    93:         imageRef := r.GetImage().GetImage()
    94:         namedRef, err := distribution.ParseDockerRef(imageRef)
    95:         if err != nil {
    96:                 return nil, errors.Wrapf(err, "failed to parse image reference %q", imageRef)
    97:         }

さいごに

これでソースコードあんまり読まずに実行パスやら呼び出し関係を確認できるねやったー。