More than 1 year has passed since last update.

はじめに

この記事では、Docker環境でしかおこらない厄介な問題の原因を突き止めるためにGDBを利用する際のヒントを解説します。これにより、怪しい部分を絞り込んでいくための取っ掛かりを作ることができます。

前提となる知識・スキル

知識・スキルとして以下をおおよその前提としています。

  1. C言語およびGo言語によるプログラミングができる。
  2. Dockerの基本操作ができる。
  3. Linux(UNIX)上でのGDBによる実行の追跡ができる。

なぜDockerでGDBか

Docker環境であるプログラム(Aとする)を実行してトラブルが起こった場合、

  1. Aにもともと問題があった
  2. Dockerに問題がある
  3. DockerとAの組み合わせに問題がある

のどれなのかを見極めて解決する必要がありますが、この切り分けはかならずしも簡単ではありません。そこできわめて強力な道具として登場するのがGDB(The GNU Project Debugger)です。GDBを使えばプログラムAだけでなくdockerの実行も同時に追跡するなどして、問題の源をすみやかに突き止めることが可能です。

追跡のステップ

ここでは、Docker環境でのGDBによる追跡の方法を以下のようなステップにわけて解説します。

  1. Docker環境を用意する
  2. Docker環境で動作するプログラムを途中から追跡する
  3. デバッグ用のDockerを用意する
  4. Dockerの動作モードについて
  5. Docker clientを追跡する
  6. Docker daemonを追跡する
  7. Docker initを追跡する
  8. Docker環境で動作するプログラムを最初から追跡する

環境

使用した環境は以下のとおりです。この記事の内容は他の環境の場合でもわずかな修正で適用可能です。

  • Ubuntu 14.04.3 amd64(x64, x86-64)版
  • Docker 1.9.1

GDBはUbuntuに標準インストールされている7.7.1を使用しています。

DockerはGo言語で記述されていますが、GDBによる実行の追跡が可能です。詳細についてはGo言語公式ページの解説をごらんください。

Docker環境を用意する

まず、デスクトップ用のamd64(x64, x86-64)版Ubuntu 14.04.3の環境を用意します。

この環境にDockerをインストールします。手順はDockerの公式ドキュメントに従いますが、冗長な部分(aptパッケージ索引の更新を連続して実行、など)は省略しています。内容はたびたび変更されるようですので最新のものを参照してください。

準備

gpgキーの追加

root@ubuntu:~# apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
Executing: gpg --ignore-time-conflict --no-options --no-default-keyring --homedir /tmp/tmp.lS9KM2wwRu --no-auto-check-trustdb --trust-model always --keyring /etc/apt/trusted.gpg --primary-keyring /etc/apt/trusted.gpg --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
gpg: requesting key 2C52609D from hkp server p80.pool.sks-keyservers.net
gpg: key 2C52609D: public key "Docker Release Tool (releasedocker) <docker@docker.com>" imported
gpg: Total number processed: 1
gpg:               imported: 1  (RSA: 1)
root@ubuntu:~#

aptソースの更新

ログイン後、Terminalを開いてスーパーユーザーになります。

user@ubuntu:~$ sudo bash
[sudo] password for user: xxxxxxx
root@ubuntu:~# 

/etc/apt/sources.list.d/docker.listを作成します。

root@ubuntu:~# echo "deb https://apt.dockerproject.org/repo ubuntu-trusty main" > /etc/apt/sources.list.d/docker.list

aptパッケージの索引を更新します。

root@ubuntu:~# apt-get update
Ign http://us.archive.ubuntu.com trusty InRelease
Get:1 http://security.ubuntu.com trusty-security InRelease [64.4 kB]
(中略)
Ign http://us.archive.ubuntu.com trusty/universe Translation-en_US             
Fetched 3,800 kB in 17s (215 kB/s)                                             
Reading package lists... Done
root@ubuntu:~# 

旧repoを削除します(新しい環境なので入っていません)。

root@ubuntu:~# apt-get purge lxc-docker
Reading package lists... Done
Building dependency tree       
Reading state information... Done
Package 'lxc-docker' is not installed, so not removed
0 upgraded, 0 newly installed, 0 to remove and 240 not upgraded.
root@ubuntu:~# 

atpが意図したレポジトリを使用することを確認します。

root@ubuntu:~# apt-cache policy docker-engine
docker-engine:
  Installed: (none)
  Candidate: 1.9.1-0~trusty
  Version table:
     1.9.1-0~trusty 0
        500 https://apt.dockerproject.org/repo/ ubuntu-trusty/main amd64 >Packages
     1.9.0-0~trusty 0
        500 https://apt.dockerproject.org/repo/ ubuntu-trusty/main amd64 >Packages
     1.8.3-0~trusty 0
        500 https://apt.dockerproject.org/repo/ ubuntu-trusty/main amd64 >Packages
     1.8.2-0~trusty 0
        500 https://apt.dockerproject.org/repo/ ubuntu-trusty/main amd64 >Packages
     1.8.1-0~trusty 0
        500 https://apt.dockerproject.org/repo/ ubuntu-trusty/main amd64 >Packages
     1.8.0-0~trusty 0
        500 https://apt.dockerproject.org/repo/ ubuntu-trusty/main amd64 >Packages
     1.7.1-0~trusty 0
        500 https://apt.dockerproject.org/repo/ ubuntu-trusty/main amd64 >Packages
     1.7.0-0~trusty 0
        500 https://apt.dockerproject.org/repo/ ubuntu-trusty/main amd64 >Packages
     1.6.2-0~trusty 0
        500 https://apt.dockerproject.org/repo/ ubuntu-trusty/main amd64 >Packages
     1.6.1-0~trusty 0
        500 https://apt.dockerproject.org/repo/ ubuntu-trusty/main amd64 >Packages
     1.6.0-0~trusty 0
        500 https://apt.dockerproject.org/repo/ ubuntu-trusty/main amd64 >Packages
     1.5.0-0~trusty 0
        500 https://apt.dockerproject.org/repo/ ubuntu-trusty/main amd64 >Packages
root@ubuntu:~# 

aufsストレージドライバーを利用できるよう、linux-image-extraカーネルパッケージをインストールします。

root@ubuntu:~# sudo apt-get install linux-image-extra-$(uname -r)
Reading package lists... Done
Building dependency tree       
Reading state information... Done
linux-image-extra-3.19.0-25-generic is already the newest version.
linux-image-extra-3.19.0-25-generic set to manually installed.
0 upgraded, 0 newly installed, 0 to remove and 240 not upgraded.
root@ubuntu:~# 

この時点では最新版がすでにインストールされていました。

Dockerパッケージのインストール

Dockerのパッケージはdocker-engineという名前になっていることに注意してインストールします。

root@ubuntu:~# sudo apt-get install docker-engine
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following extra packages will be installed:
  aufs-tools cgroup-lite git git-man liberror-perl
Suggested packages:
  git-daemon-run git-daemon-sysvinit git-doc git-el git-email git-gui gitk
  gitweb git-arch git-bzr git-cvs git-mediawiki git-svn
The following NEW packages will be installed:
  aufs-tools cgroup-lite docker-engine git git-man liberror-perl
0 upgraded, 6 newly installed, 0 to remove and 240 not upgraded.
Need to get 11.0 MB of archives.
After this operation, 60.3 MB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 http://us.archive.ubuntu.com/ubuntu/ trusty/universe aufs-tools amd64 1:3.2+20130722-1.1 [92.3 kB]
(中略)
Setting up cgroup-lite (1.9) ...
cgroup-lite start/running
Setting up docker-engine (1.9.1-0~trusty) ...
docker start/running, process 7922
Processing triggers for libc-bin (2.19-0ubuntu6.6) ...
Processing triggers for ureadahead (0.100.0-16) ...
root@ubuntu:~# 

Dockerの動作確認

Dockerデーモン起動

root@ubuntu:~# sudo service docker start
start: Job is already running: docker

すでに起動されていました。

Dockerが正しくインストールされているか確認します。

root@ubuntu:~# sudo docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b901d36b6f2f: Pull complete 
0a6ba66e537a: Pull complete 
Digest: sha256:8be990ef2aeb16dbcb9271ddfe2610fa6658d13f6dfb8bc72074cc1ca36966a7
Status: Downloaded newer image for hello-world:latest

Hello from Docker.
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker Hub account:
 https://hub.docker.com

For more examples and ideas, visit:
 https://docs.docker.com/userguide/

root@ubuntu:~# 

ここではhello-worldというコンテナを起動してメッセージが出力されることを確かめています。以上でDockerのインストールは終わりです。

Dockerの環境を確認

dockerの環境を確認するにはdocker infoコマンドを使用します。

root@ubuntu:~# uname -r
3.19.0-25-generic
root@ubuntu:~# docker info
Containers: 1
Images: 2
Server Version: 1.9.1
Storage Driver: aufs
 Root Dir: /var/lib/docker/aufs
 Backing Filesystem: extfs
 Dirs: 4
 Dirperm1 Supported: true
Execution Driver: native-0.2
Logging Driver: json-file
Kernel Version: 3.19.0-25-generic
Operating System: Ubuntu 14.04.3 LTS
CPUs: 2
Total Memory: 1.938 GiB
Name: ubuntu
ID: Y3NE:3SYW:H2CE:Y36R:DJSK:3CB7:UXBZ:ZI4B:BE4V:FWJZ:4W4T:Y4M5
WARNING: No swap limit support
root@ubuntu:~# 

Dockerのバージョンは1.9.1であることがわかります。

Docker環境で動作するプログラムを途中から追跡する

glibcのソースコードパッケージをインストール

準備として、glibcのソースコードパッケージを入手してインストールしておきます。

root@ubuntu:~/tmp# apt-get install eglibc-source
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following NEW packages will be installed:
  eglibc-source
0 upgraded, 1 newly installed, 0 to remove and 198 not upgraded.
Need to get 14.0 MB of archives.
After this operation, 25.3 MB of additional disk space will be used.
Get:1 http://us.archive.ubuntu.com/ubuntu/ trusty-updates/main eglibc->source all 2.19-0ubuntu6.6 [14.0 MB]
Fetched 14.0 MB in 7s (1,869 kB/s)                                             
Selecting previously unselected package eglibc-source.
(Reading database ... 147228 files and directories currently installed.)
Preparing to unpack .../eglibc-source_2.19-0ubuntu6.6_all.deb ...
Unpacking eglibc-source (2.19-0ubuntu6.6) ...
Setting up eglibc-source (2.19-0ubuntu6.6) ...
root@ubuntu:~/tmp# mkdir -p /build/buildd
root@ubuntu:~/tmp# tar xJCf /build/buildd /usr/src/glibc/eglibc-2.19.tar.xz 
root@ubuntu:~/tmp# 

追跡するソースコードの用意

ここでは対象となるソースコードの例として以下のファイル~/tmp/helloloop.cを用意します。

helloloop.c
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv) {
  int i;
  for (i = 0;;) {
    printf("Hello, world! (%d)\n", i++);
    sleep(1);
  }
}

これをデバッグ用にコンパイルします。

root@ubuntu:~/tmp# gcc -g -o helloloop helloloop.c
root@ubuntu:~/tmp# 

通常の追跡手順

まず、Dockerを使わずにGDBにより実行を追跡してみましょう。

root@ubuntu:~/tmp# gdb ./helloloop 
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
(中略)
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./helloloop...done.
(gdb) list
1   #include <stdio.h>
2   #include <unistd.h>
3   
4   int main(int argc, char **argv) {
5     int i;
6     for (i = 0;;) {
7       printf("Hello, world! (%d)\n", i++);
8       sleep(1);
9     }
10  }
(gdb) break 8
Breakpoint 1 at 0x4005ad: file helloloop.c, line 8.
(gdb) run
Starting program: /home/nao/tmp/helloloop 
Hello, world! (0)

Breakpoint 1, main (argc=1, argv=0x7fffffffe6a8) at helloloop.c:8
8       sleep(1);
(gdb) c
Continuing.
Hello, world! (1)

Breakpoint 1, main (argc=1, argv=0x7fffffffe6a8) at helloloop.c:8
8       sleep(1);
(gdb) quit
A debugging session is active.

    Inferior 1 [process 5514] will be killed.

Quit anyway? (y or n) y
root@ubuntu:~/tmp# 

Docker上での追跡手順

次に、Docker上で実行中のhelloloopの実行を追跡してみます。まず、適当なコンテナ(ここではubuntuを使っています)でhelloloopを実行します。volumeの指定によりホストとコンテナでディレクトリを同じ名前で共有しています。

root@ubuntu:~/tmp# docker run -it --rm -v ${HOME}/tmp:${HOME}/tmp ubuntu ${HOME}/tmp/helloloop
Hello, world! (0)
Hello, world! (1)

helloloopが実行を開始したところで(ホスト側で)別のターミナルを開き、プロセスを調べます。GDBでこのプロセスを指定して追跡します。

root@ubuntu:~# cd tmp
root@ubuntu:~/tmp# ps a|grep helloloop
  6021 pts/7    Sl+    0:00 docker run -it --rm -v /home/nao/tmp:/home/nao/tmp ubuntu /home/user/tmp/helloloop
  6041 pts/13   Ss+    0:00 /home/user/tmp/helloloop
  6059 pts/4    S+     0:00 grep --color=auto helloloop
root@ubuntu:~/tmp# gdb
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
(中略)
Type "apropos word" to search for commands related to "word".
(gdb) attach 6041
Attaching to process 6041
Reading symbols from /home/nao/tmp/helloloop...done.
Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/libc-2.19.so...done.
done.
Loaded symbols for /lib/x86_64-linux-gnu/libc.so.6
Reading symbols from /lib64/ld-linux-x86-64.so.2...Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/ld-2.19.so...done.
done.
Loaded symbols for /lib64/ld-linux-x86-64.so.2
0x00007f652d5a8f20 in __nanosleep_nocancel ()
    at ../sysdeps/unix/syscall-template.S:81
81  T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
(gdb) where
#0  0x00007f652d5a8f20 in __nanosleep_nocancel ()
    at ../sysdeps/unix/syscall-template.S:81
#1  0x00007f652d5a8dd4 in __sleep (seconds=0)
    at ../sysdeps/unix/sysv/linux/sleep.c:137
#2  0x00000000004005b7 in main (argc=1, argv=0x7ffd9674b508) at helloloop.c:8
(gdb) frame 2
#2  0x00000000004005b7 in main (argc=1, argv=0x7ffed5cbbff8) at helloloop.c:8
8       sleep(1);
(gdb) list
3   
4   int main(int argc, char **argv) {
5     int i;
6     for (i = 0;;) {
7       printf("Hello, world! (%d)\n", i++);
8       sleep(1);
9     }
10  }
(gdb) break 8
Breakpoint 1 at 0x4005ad: file helloloop.c, line 8.
(gdb) cont
Continuing.

Breakpoint 1, main (argc=1, argv=0x7ffed5cbbff8) at helloloop.c:8
8       sleep(1);
(gdb) cont
Continuing.

Breakpoint 1, main (argc=1, argv=0x7ffed5cbbff8) at helloloop.c:8
8       sleep(1);
(gdb) quit
A debugging session is active.

    Inferior 1 [process 6041] will be detached.

Quit anyway? (y or n) y
Detaching from program: /home/nao/tmp/helloloop, process 6041
root@ubuntu:~/tmp# 

このようにDocker上で動作しているプログラムは、ホストからもプロセスとして見えていて(プロセス番号はDocker内でのものと異なることに注意)ホスト上のGDBからデバッグすることが可能です。ただし、ここで見たようにプログラムの起動後にプロセス番号を調べてGDBに知らせる手順が必要です。プログラムが起動直後に終了する場合はソースコードを書き換えて待ち時間を入れるか、後述するようにDocker serverと同時にデバッグをおこなうといった方法をとる必要があります。

デバッグ用のDockerを用意する

Dockerのレポジトリからソースコードを取得して1.9.1に切り替えます。コンパイル等はDocker構築用コンテナdocker-devの内部で行われますが、その環境に合わせてソースコードのディレクトリを用意しておきます。

root@ubuntu:~# git clone https://github.com/docker/docker.git
root@ubuntu:~# cd docker
root@ubuntu:~/docker# git checkout v1.9.1
root@ubuntu:~/docker# mkdir -p /go/src/github.com/docker/
root@ubuntu:~/docker# ln -s `pwd` /go/src/github.com/docker/

次に、docker-dev内部のソースコードを取り出せるよう、Makefile中で指定しているDocker実行時のオプション--rm--name docker-dev-tmpで置き換えます。すなわち、

Makefile
DOCKER_RUN_DOCKER := docker run --rm -it --privileged $(DOCKER_ENVS) $(DOCKER_MOUNT) "$(DOCKER_IMAGE)"

この行を以下のように書き換えます。

Makefile
DOCKER_RUN_DOCKER := docker run --name docker-dev-tmp -it --privileged $(DOCKER_ENVS) $(DOCKER_MOUNT) "$(DOCKER_IMAGE)"

さらに、環境変数DOCKER_DEBUGでデバッグ用Dockerのビルドを指定します。

root@ubuntu:~/docker# export DOCKER_DEBUG=1

Dockerをビルドします。

root@ubuntu:~/docker# make
docker build -t "docker-dev:HEAD" .
Sending build context to Docker daemon 262.1 MB
Step 1 : FROM ubuntu:14.04
 ---> 89d5d8e8bafb
(中略)
---> Making bundle: binary (in bundles/1.9.1/binary)
Building: bundles/1.9.1/binary/docker-1.9.1
Created binary: bundles/1.9.1/binary/docker-1.9.1

docker-dev-tmpからgolangのソースコードを取り出します。

root@ubuntu:~/docker# mkdir -p /usr/local/go
root@ubuntu:~/docker# docker cp docker-dev-tmp:/usr/local/go/src /usr/local/go/
root@ubuntu:~/docker# docker cp docker-dev-tmp:/go/src/github.com/golang /go/src/github.com/

これでdocker-dev-tmpは不要になったので消去しておきます。

root@ubuntu:~/docker# docker rm docker-dev-tmp

Dockerを停止して、バイナリーを新しく構築したものと入れ替えて再開します。

root@ubuntu:~/docker# service docker stop
docker stop/waiting
root@ubuntu:~/docker# mv /usr/bin/docker /usr/bin/docker-distributed
root@ubuntu:~/docker# cp bundles/1.9.1/binary/docker /usr/bin/
root@ubuntu:~/docker# service docker start 
docker start/running, process 10774

新しく構築したDockerが動作していることを確認できます。

root@ubuntu:~/docker# docker info
Containers: 0
(中略)
WARNING: No swap limit support
root@ubuntu:~/docker# 

以上でデバッグ用のDocker(とデバッグに必要なファイル類)が用意できました。

Dockerの動作モードについて

Dockerにはclient, daemon, initの3つのモードがあります。この3つは実質的には別のプログラムなのですがひとつのDockerバイナリー/usr/bin/dockerにまとめられており、起動時のオプションでモードを切り替えます。Dockerによりプログラムをコンテナ内で実行する場合はこの3つのモードが協調して動作します。

Docker clientとはdocker runなど、ユーザーがコマンドラインからdockerを利用する際のインターフェースとなるモードです。Docker daemonと通信してユーザーによる入力情報を送信し、処理結果を受信してユーザーに通知します。

Docker daemonはコンテナの生成・実行・削除などのサービスを提供するモードです。Linux上のサーバープロセスとして動作します。

root@ubuntu:~/docker# service docker status
docker start/running, process 10774

Docker initはコンテナ内部の実行環境を整備するためのモードです。Docker daemonによるコンテナの実行環境作成後、コンテナ内で最初のプログラムとして起動します。そして環境を整備したのちにユーザーの指定したコマンドを実行します。

Docker内部の実行を追跡するには、この3つのモードを意識して使い分ける必要があります。

Docker clientを追跡する

Docker clientは通常の手順で追跡できます。main.mainにブレークポイントを設定して実行し、nextコマンドによるステップ実行を繰り返してみましょう。

root@ubuntu:~/docker# gdb /usr/bin/docker
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
(中略)
Reading symbols from /usr/bin/docker...done.
(gdb) break main.main
Breakpoint 1 at 0x495570: file /go/src/github.com/docker/docker/docker/docker.go, line 17.
(gdb) run run -it --rm ubuntu ls
Starting program: /usr/bin/docker run -it --rm ubuntu ls
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff7e4a700 (LWP 11061)]
[New Thread 0x7ffff7649700 (LWP 11062)]
[New Thread 0x7ffff6e48700 (LWP 11063)]
[New Thread 0x7ffff6647700 (LWP 11064)]

Breakpoint 1, main.main ()
    at /go/src/github.com/docker/docker/docker/docker.go:17
17  func main() {
(gdb) list
12      "github.com/docker/docker/pkg/reexec"
13      "github.com/docker/docker/pkg/term"
14      "github.com/docker/docker/utils"
15  )
16  
17  func main() {
18      if reexec.Init() {
19          return
20      }
21  
(gdb) n

Breakpoint 1, main.main ()
    at /go/src/github.com/docker/docker/docker/docker.go:17
17  func main() {
(gdb) 
(中略)
(gdb) 
65      if err := c.Run(flag.Args()...); err != nil {
(gdb) 
bin   dev  home  lib64  mnt  proc  run   srv  tmp  var
boot  etc  lib   media  opt  root  sbin  sys  usr
[New Thread 0x7ffff5dc6700 (LWP 11117)]
[Thread 0x7ffff6647700 (LWP 11064) exited]
[Thread 0x7ffff6e48700 (LWP 11063) exited]
[Thread 0x7ffff7649700 (LWP 11062) exited]
[Thread 0x7ffff7e4a700 (LWP 11061) exited]
[Thread 0x1dd3880 (LWP 11057) exited]
[Inferior 1 (process 11057) exited normally]
(gdb) 
The program is not being run.
(gdb) quit
root@ubuntu:~/docker# 

Docker daemonを追跡する

Docker daemonを追跡するには、まずDockerのサービスを停止します。

root@ubuntu:~/docker# service docker stop
docker stop/waiting

そして、Docker daemonをGDB内から起動します。先ほどと同様、main.mainにブレークポイントを設定しておきます。そのまま実行を進めていくと、Docker clientからの通信待ちになります。

root@ubuntu:~/docker# gdb docker
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
(中略)
Reading symbols from docker...done.
(gdb) break main.main
Breakpoint 1 at 0x495570: file /go/src/github.com/docker/docker/docker/docker.go, line 17.
(gdb) run daemon
Starting program: /usr/bin/docker daemon
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff7e4a700 (LWP 11138)]
[New Thread 0x7ffff7649700 (LWP 11139)]
[New Thread 0x7ffff6e48700 (LWP 11140)]
[New Thread 0x7ffff6647700 (LWP 11141)]

Breakpoint 1, main.main ()
    at /go/src/github.com/docker/docker/docker/docker.go:17
17  func main() {
(gdb) n
18      if reexec.Init() {
(gdb) 
(中略)
(gdb) 
65      if err := c.Run(flag.Args()...); err != nil {

別のターミナルを開いて、Docker clientから適当なDocker操作をしてみましょう。ここでは先ほどと同様、docker run --rm ubuntu lsを実行します。すると、Docker clientからの通信を受け取ってさまざまな情報を出力しながら処理を行います。

(gdb) 
INFO[0011] [graphdriver] using prior storage driver "aufs" 
INFO[0011] API listen on /var/run/docker.sock           
INFO[0011] Firewalld running: false                     
INFO[0011] Default bridge (docker0) is assigned with an IP address 172.17.0.1/16. Daemon option --bip can be used to set a preferred IP address 
WARN[0012] Your kernel does not support swap memory limit. 
INFO[0012] Loading containers: start.                   

INFO[0012] Loading containers: done.                    
INFO[0012] Daemon has completed initialization          
INFO[0012] Docker daemon                                 commit=a34a1d5-dirty execdriver=native-0.2 graphdriver=aufs version=1.9.1
INFO[0044] POST /v1.21/containers/create                
[New Thread 0x7ffff5dc6700 (LWP 11187)]
INFO[0044] POST /v1.21/containers/2747dd5228b12047397adf5793733c7740f1e5f7b924923e7467e784dc176655/attach?stderr=1&stdout=1&stream=1 
INFO[0044] POST /v1.21/containers/2747dd5228b12047397adf5793733c7740f1e5f7b924923e7467e784dc176655/start 
INFO[0044] No non-localhost DNS nameservers are left in resolv.conf. Using default external servers : [nameserver 8.8.8.8 nameserver 8.8.4.4] 
INFO[0044] IPv6 enabled; Adding default IPv6 external servers : [nameserver 2001:4860:4860::8888 nameserver 2001:4860:4860::8844] 
[New Thread 0x7ffff55c5700 (LWP 11210)]
[New Thread 0x7ffff4dc4700 (LWP 11212)]

Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7ffff5dc6700 (LWP 11187)]
net.networkNumberAndMask (n=0x0, ip=..., m=...)
    at /usr/local/go/src/net/ip.go:436
436     if ip = n.IP.To4(); ip == nil {

途中、SIGSEGVで実行が停止してエラーが起きたように見えます。これはGo言語の実行環境のトリックなので気にせず継続してかまいません。

(gdb) c
Continuing.
INFO[0049] POST /v1.21/containers/2747dd5228b12047397adf5793733c7740f1e5f7b924923e7467e784dc176655/wait 
INFO[0049] GET /v1.21/containers/2747dd5228b12047397adf5793733c7740f1e5f7b924923e7467e784dc176655/json 
INFO[0049] DELETE /v1.21/containers/2747dd5228b12047397adf5793733c7740f1e5f7b924923e7467e784dc176655?v=1 

Dockerコンテナ上でのコマンドを実行後、再び通信待ちになりますのでCtrl-Cを入力するなどして実行を中断します。

^C
Program received signal SIGINT, Interrupt.
[Switching to Thread 0x1dd3880 (LWP 11134)]
runtime.futex () at /usr/local/go/src/runtime/sys_linux_amd64.s:278
278     MOVL    AX, ret+40(FP)
(gdb) quit
A debugging session is active.

    Inferior 1 [process 11134] will be killed.

Quit anyway? (y or n) y
root@ubuntu:~/docker# 

Docker initを追跡する

Docker initを追跡するにはDocker serverを経由します。まず、Docker initを起動する関数github.com/opencontainers/runc/libcontainer.(*initProcess).startにブレークポイントを設定してDocker Serverを起動します。

root@ubuntu:~/docker# gdb /usr/bin/docker
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
(中略)
Make breakpoint pending on future shared library load? (y or  [n]) n
(gdb) break github.com/opencontainers/runc/libcontainer.(*initProcess).start

Breakpoint 1 at 0x98b7b0: file /go/src/github.com/docker/docker/vendor/src/github.com/opencontainers/runc/libcontainer/process_linux.go, line 178.
(gdb) run daemon
Starting program: /usr/bin/docker daemon
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff7e4a700 (LWP 11488)]
(中略)
INFO[0000] Docker daemon                                 commit=a34a1d5-dirty execdriver=native-0.2 graphdriver=aufs version=1.9.1

通信待ちに入るので、別のターミナルを開いて、Docker clientから適当なDocker操作をしてみましょう。ここでは先ほどと同様、docker run --rm ubuntu lsを実行します。

INFO[0095] POST /v1.21/containers/create                
[New Thread 0x7ffff5dc6700 (LWP 11744)]
(中略)
[Switching to Thread 0x7ffff5e46700 (LWP 11492)]

Breakpoint 1, github.com/opencontainers/runc/libcontainer.>(*initProcess).start
    (p=0xc208685220, err=...)
    at /go/src/github.com/docker/docker/vendor/src/github.com/opencontainers/runc>/libcontainer/process_linux.go:178
178 func (p *initProcess) start() (err error) {

ブレークポイントで停止したらこれを無効化し、GDBが子プロセスであるDaemon initを追跡するよう設定します。Daemon initの入り口の関数github.com/docker/docker/pkg/reexec.Initにブレークポイントを設定して実行を再開します。

(gdb) disable 1
(gdb) set follow-fork-mode child
(gdb) break github.com/docker/docker/pkg/reexec.Init
Breakpoint 2 at 0x684a00: file /go/src/github.com/docker/docker/pkg/reexec/reexec.go, line 23.
(gdb) c
Continuing.
[New process 11659]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
process 11659 is executing new program: /usr/bin/docker
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
warning: Can't attach LWP 2: No child processes
warning: Can't attach LWP 3: No child processes
warning: Can't attach LWP 5: No child processes
[Switching to LWP 11659]

Breakpoint 2, github.com/docker/docker/pkg/reexec.Init (~r0=152)
    at /go/src/github.com/docker/docker/pkg/reexec/reexec.go:23
23  func Init() bool {

Docker initの入り口にたどり着きました。nextコマンドでステップ実行していくとコンテナ内でlsが実行されるのがわかります。

(gdb) list
18      registeredInitializers[name] = initializer
19  }
20  
21  // Init is called as the first part of the exec process and returns true if an
22  // initialization function was called.
23  func Init() bool {
24      initializer, exists := registeredInitializers[os.Args[0]]
25      if exists {
26          initializer()
27  
(gdb) n
24      initializer, exists := registeredInitializers[os.Args[0]]
(gdb) 
25      if exists {
(gdb) 
26          initializer()
(gdb) 
process 11659 is executing new program: /bin/ls
[Inferior 2 (process 11659) exited normally]
(gdb) INFO[0061] POST /v1.21/containers/fe209b10dc7cbc31084a76c0920862274c7372d456a59c7baf758fd1b1fecf4f/wait 

The program is not being run.
(gdb) INFO[0062] GET /v1.21/containers/fe209b10dc7cbc31084a76c0920862274c7372d456a59c7baf758fd1b1fecf4f/json 
INFO[0062] DELETE /v1.21/containers/fe209b10dc7cbc31084a76c0920862274c7372d456a59c7baf758fd1b1fecf4f?v=1 
Quit
(gdb) quit
root@ubuntu:~/docker# 

Docker環境で動作するプログラムを最初から追跡する

先ほど作成したプログラムhelloloopのDocker環境での実行を追跡することにしましょう。今度はプログラムの先頭から追跡することが可能です。

今回はDocker initのときの手順と同様です。まず、Docker initを起動する関数github.com/opencontainers/runc/libcontainer.(*initProcess).startにブレークポイントを設定してDocker daemonを起動します。

root@ubuntu:~/docker# gdb /usr/bin/docker
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
(中略)
Reading symbols from /usr/bin/docker...done.
(gdb) break github.com/opencontainers/runc/libcontainer.(*initProcess).start
Breakpoint 1 at 0x98b7b0: file /go/src/github.com/docker/docker/vendor/src/github.com/opencontainers/runc/libcontainer/process_linux.go, line 178.
(gdb) run daemon
Starting program: /usr/bin/docker daemon
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff7e4a700 (LWP 12954)]
[New Thread 0x7ffff7649700 (LWP 12955)]
[New Thread 0x7ffff6e48700 (LWP 12956)]
[New Thread 0x7ffff6647700 (LWP 12957)]
INFO[0000] [graphdriver] using prior storage driver "aufs" 
INFO[0000] API listen on /var/run/docker.sock           
INFO[0000] Firewalld running: false                     
INFO[0000] Default bridge (docker0) is assigned with an IP address 172.17.0.1/16. Daemon option --bip can be used to set a preferred IP >address 
WARN[0000] Your kernel does not support swap memory limit. 
INFO[0000] Loading containers: start.                   
....
INFO[0000] Loading containers: done.                    
INFO[0000] Daemon has completed initialization          
INFO[0000] Docker daemon                                 commit=a34a1d5-dirty execdriver=native-0.2 graphdriver=aufs version=1.9.1

別のターミナルを開いて、Docker clientからhelloloopを実行します。

root@ubuntu:~docker run -v \${HOME}/tmp:\${HOME}/tmp -it ubuntu \${HOME}/tmp/helloloop

INFO[0003] POST /v1.21/containers/create                
[New Thread 0x7ffff5dc6700 (LWP 12990)]
(中略)
178 func (p *initProcess) start() (err error) {

ブレークポイントで停止したらこれを無効化し、GDBが子プロセスであるDaemon initを追跡するよう設定します。Daemon initの入り口の関数github.com/docker/docker/pkg/reexec.Initにブレークポイントを設定して実行を再開します。

(gdb) disable 1
(gdb) set follow-fork-mode child
(gdb) break github.com/docker/docker/pkg/reexec.Init
Breakpoint 2 at 0x684a00: file /go/src/github.com/docker/docker/pkg/reexec/reexec.go, line 23.
(gdb) c
Continuing.
[New process 13012]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
process 13012 is executing new program: /usr/bin/docker
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
warning: Can't attach LWP 2: No child processes
warning: Can't attach LWP 3: No child processes
warning: Can't attach LWP 5: No child processes
[Switching to LWP 13012]

Breakpoint 2, github.com/docker/docker/pkg/reexec.Init (~r0=96)
    at /go/src/github.com/docker/docker/pkg/reexec/reexec.go:23
23  func Init() bool {

ここで対象を helloloop に切り替えます。ブレークポイントをmainに設定し、実行を再開します。

(gdb) file ~/tmp/helloloop
A program is being debugged already.
Are you sure you want to change the file? (y or n) y

Load new symbol table from "~/tmp/helloloop"? (y or n) y
Reading symbols from ~/tmp/helloloop...done.
(gdb) break main
Breakpoint 3 at 0x40057d: file helloloop.c, line 4.
(gdb) c
Continuing.
process 13012 is executing new program: /home/nao/tmp/helloloop

Breakpoint 3, main (argc=1, argv=0x7ffcb92cbae8) at helloloop.c:6
6     for (i = 0;;) {

mainの先頭で停止したことがわかります。

(gdb) list
1   #include <stdio.h>
2   #include <unistd.h>
3   
4   int main(int argc, char **argv) {
5     int i;
6     for (i = 0;;) {
7       printf("Hello, world! (%d)\n", i++);
8       sleep(1);
9     }
10  }
(gdb) c
Continuing.

このようにDocker serverとDocker initを経由することにより、Dockerコンテナ内で動作するプログラムの実行をホストのGDBで追跡することができます。

まとめ

DockerもDocker内のプログラムも実行をホスト側のGDBで追跡することができます。この方法を使うとDocker特有のトラブルの原因をすみやかに特定できます。

連絡先

著者に直接コンタクトを取りたい場合はnaoアットマークatbsd.comあてに電子メールを送ってください。

Dockerやその他の仮想環境に関するご相談等は株式会社あっとBSDまで。

以上。