GKEではコンテナ上で動くプロセスを簡単に調査したりデバッグを行ったりすることが出来ます。
この記事ではGKE上のコンテナプロセスに対してデバッグツールを適用して調査する方法を紹介します。
取り上げるデバッグツールはstrace/tcpdump/htop/netstat/ngrepです。
コンテナプロセスのデバッグで使えるコマンド集
まずはコンテナプロセスのデバッグ時に使えるコマンドを紹介していきます。
調べたい対象のコンテナプロセスの情報を集める
ローカルからkubectlでデバッグに必要となる情報を集めます。
ノードの特定
対象のpodが動作するノードを特定します。
$ NODE_NAME=$(kubectl get pods --selector=app=$APP_NAME -o jsonpath='{.items[0].spec.nodeName}'); echo $NODE_NAME
uidの取得
ノード上でのコンテナプロセスのPIDを特定するのに必要となるuidを取得します。
$ UUID=$(kubectl get pods --selector=app=$APP_NAME -o jsonpath='{.items[0].metadata.uid}'); echo $UUID
IPの取得
podのクラスターIPを特定します。
$ IP=$(kubeclt get po --selector=app=nginx -o jsonpath='{.items[*].status.podIP}'); echo $IP
ノードへのsshとデバッグ用のtoolboxの起動
次にノード上にsshしてデバッグツールを使う下準備をします。
ノードへのssh
以下でノードへとsshします。
$ gcloud compute ssh hands-on-user@$NODE_NAME -q
PIDを特定
uidを使ってコンテナプロセスのPIDを特定します。
該当するプロセスが複数ある場合は、実行されているコマンド(下の例ではngixが動いている)から判別してください。
$ ps -ea -o pid,ppid,cmd:50,cgroup --cumulative -H | grep $UUID | cut -c -100
24209 24192 /pause 12:devices:/kubepods/burstable/po
24265 24248 nginx: master process nginx -g daemon off; 12:devices:/kubepods/burstable/po
24279 24265 nginx: worker process 12:devices:/kubepods/burstable/po
toolboxの起動
toolboxを起動します。これはGKEのノード/CoreOS上で提供されている、ホストマシンを汚すことなくデッバク用のツールをインストールして実行できるコンテナ環境です。
$ sudo toolbox bash
コンテナプロセスに対してデバッグツールを使用
実際にデバッグツールを使用していきます。
strace
toolbox起動後に、$PID
にコンテナプロセスのPIDを指定して以下を実行します。
$ apt-get install -y strace; strace -vf -s 1024 -p $PID
tcpdump
interfaceにcbr0
を指定してtcpdumpを実行します(cbr0についてはGEKのネットワーク構成を参照)。
$ apt-get install -y tcpdump; tcpdump -ni cbr0 host $IP
htop
htopを実行します。
$ apt-get install -y htop; htop
netstat
コンテナ($PID
で指定)のネットワークスペース内に入ります。
$ nsenter --target $PID --net
netstatを実行します。
$ netstat -anp; exit;
ngrep
ngrepをインストールします。
$ apt-get install -y ngrep
コンテナのネットワークネームスペース内でngrepを実行します。
$ nsenter -t $PID --net ngrep -d any port 80
参考文献
GKEのトラブルシュートの方法
toolboxの使い方
toolboxのGitHub
実際にGKE上でコマンドを試す手順
この記事で紹介したコマンドを実際に試してみたい人のために、検証用のGKEクラスタ/コンテナプロセスを立ち上げる手順を紹介します。
環境を汚さずに検証できるように、コンテナ内からのCloud SDKの実行、専用のプロジェクトの新規作成を行っているので安心して試せるような手順になっています。
Cloud SDKとプロジェクトのセットアップ
Cloud SDKを使用するためにコンテナを起動します。
$ docker run -it --rm --name=gcloud google/cloud-sdk:254.0.0-alpine bin/bash
以下を打って表示されるリンク先に飛んでクレデンシャルを取得して Cloud SDKを認証します。
$ gcloud auth login
Go to the following link in your browser:
https://accounts.google.com/o/oauth2/auth?redirect_uri=....
Enter verification code:
プロジェクトの作成
プロジェクトを作成します。
$ PROJECT_NAME=strace-hands-on-$(head -c 100 /dev/random | xxd -p -l 5); echo $PROJECT_NAME
$ gcloud projects create $PROJECT_NAME
Cloud SDKをハンズオンのプロジェクト用に設定していきます。
$ {
gcloud config set project $PROJECT_NAME
gcloud config set compute/region asia-northeast1
gcloud config set compute/zone asia-northeast1-b
}
GKEクラスタの作成
Kubernetes Engine APIを有効化します。
以下のコマンドの出力結果のURLから有効化出来ます。
$ echo https://console.cloud.google.com/apis/api/container.googleapis.com/overview\?project\=$PROJECT_NAME
https://console.cloud.google.com/apis/api/container.googleapis.com/overview?project=strace-hands-on-fdffdcb6c5
クラスタを作成します。
$ gcloud container clusters create --preemptible --num-nodes=2 strace-hands-on
恐らく以下のようなエラーが出ると思いますが、Kubernetes Engine APIは有効化まで時間がかかるので、少し時間を置いてからリトライしてみたください。
ERROR: (gcloud.container.clusters.create) ResponseError: code=403, message=Google Compute Engine: Access Not Configured. Compute Engine API has not been used in project 311149706395 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/compute.googleapis.com/overview?project=311149706395 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.
kubectl設定
kubectlをインストールします
$ gcloud components install --quiet kubectl
kubectlをクラスタに向けます。
$ gcloud container clusters get-credentials strace-hands-on
nginxのpodをデプロイする
以下を打ってnginxのdeploymentとserviceを作成します。
$ cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
replicas: 1
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
resources:
limits:
cpu: 150m
---
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
EOF
podが動いていることを確認します。
$ kubectl get po
NAME READY STATUS RESTARTS AGE
nginx-deployment-77d9857f95-rggjf 1/1 Running 0 94m
nginxにリクエストを送るためのpodを起動します。
$ kubectl run alpine -it --image=alpine /bin/sh
アタッチしたalpineのpodで以下を実行してnginxのpodからレスポンスが返ることを確認します。
$ apk update && apk add curl
$ curl nginx 2>/dev/null | head -n 5
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
nginxのコンテナプロセスが動くノードにssh
以下を打ってCloud SDKのコンテナにアタッチしたパネルを新しく用意します。
$ docker exec -it gcloud /bin/bash
コンテナプロセスのuidを特定してメモしておきます。
$ APP_NAME=nginx; UUID=$(kubectl get pods --selector=app=$APP_NAME -o jsonpath='{.items[0].metadata.uid}'); echo $UUID
98d46832-aed8-11e9-9238-42010a920046
nginxのコンテナプロセスが動くノードへとsshします。
$ NODE_NAME=$(kubectl get pods --selector=app=nginx -o jsonpath='{.items[0].spec.nodeName}'); \
gcloud compute ssh hands-on-user@$NODE_NAME -q
nginxのコンテナプロセスに対してデバッグツールを適用
コンテナプロセスのPIDを特定します。
$ UUID=98d46832-aed8-11e9-9238-42010a920046
$ ps -ea -o pid,cmd:50,cgroup --cumulative -H | grep $UUID | cut -c -100 | grep worker
3768 nginx: worker process 12:memory:/kubepods/burstable/pod98d46832
$ PID=3768
toolboxを起動します。
$ sudo toolbox bash
Spawning container root-gcr.io_google-containers_toolbox-20180309-00 on /var/lib/toolbox/root-gcr.io_google-containers_toolbox-20180309-00.
Press ^] three times within 1s to kill container.
デバッグツールをインストールします。
$ apt-get update && apt-get install -y strace ngrep
straceを実行します。
$ PID=3768; strace -vf -s 1024 -p $PID
strace: Process 3768 attached
epoll_wait(8,
alpine podからリクエストを送ると、
$ curl nginx
以下のようにnginxがリクエスト処理時に発行するシステムコールが観察できるはずです。
余談ですが、nginxは静的ファイルをレスポンスとして返す場合、通常のwrite(2)ではなくsendfile(2)を使うんですね。
strace: Process 3768 attached
epoll_wait(8, [{EPOLLIN, {u32=4087242768, u64=140282014097424}}], 512, -1) = 1
accept4(6, {sa_family=AF_INET, sin_port=htons(52840), sin_addr=inet_addr("10.0.1.8")}, [110->16], SOCK_NONBLOCK) = 3
epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=4087243201, u64=140282014097857}}) = 0
epoll_wait(8, [{EPOLLIN, {u32=4087243201, u64=140282014097857}}], 512, 60000) = 1
recvfrom(3, "GET / HTTP/1.1\r\nHost: nginx\r\nUser-Agent: curl/7.65.1\r\nAccept: */*\r\n\r\n", 1024, 0, NULL, NULL) = 69
stat("/usr/share/nginx/html/index.html", {st_dev=makedev(0, 204), st_ino=1050025, st_mode=S_IFREG|0644, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=8, st_size=612, st_atime=2014-12-23T16:25:09+0000, st_mtime=2014-12-23T16:25:09+0000, st_ctime=2019-07-25T12:34:57+0000.523040476}) = 0
open("/usr/share/nginx/html/index.html", O_RDONLY|O_NONBLOCK) = 10
fstat(10, {st_dev=makedev(0, 204), st_ino=1050025, st_mode=S_IFREG|0644, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=8, st_size=612, st_atime=2014-12-23T16:25:09+0000, st_mtime=2014-12-23T16:25:09+0000, st_ctime=2019-07-25T12:34:57+0000.523040476}) = 0
writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1.7.9\r\nDate: Thu, 25 Jul 2019 12:45:24 GMT\r\nContent-Type: text/html\r\nContent-Length: 612\r\nLast-Modified: Tue, 23 Dec 2014 16:25:09 GMT\r\nConnection: keep-alive\r\nETag: \"54999765-264\"\r\nAccept-Ranges: bytes\r\n\r\n", iov_len=237}], 1) = 237
sendfile(3, 10, [0] => [612], 612) = 612
write(5, "10.0.1.8 - - [25/Jul/2019:12:45:24 +0000] \"GET / HTTP/1.1\" 200 612 \"-\" \"curl/7.65.1\" \"-\"\n", 89) = 89
close(10) = 0
setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0
recvfrom(3, "", 1024, 0, NULL, NULL) = 0
close(3) = 0
epoll_wait(8,
続いて、ngrepを使用してみます。
Ctrl+Cでstraceをデタッチして以下を打ちます。
$ nsenter -t $PID --net ngrep -d any port 80
interface: any
filter: (ip or ip6) and ( p:ort 80 )
再びalpine podからリクエストを送ると、
$ curl nginx
以下のようにnginxの通信の中身を見ることが出来ます。
interface: any
filter: (ip or ip6) and ( port 80 )
####
T 10.0.1.8:53908 -> 10.0.0.6:80 [AP]
GET / HTTP/1.1..Host: nginx..User-Agent: curl/7.65.1..Accept: */*....
##
T 10.0.0.6:80 -> 10.0.1.8:53908 [AP]
HTTP/1.1 200 OK..Server: nginx/1.7.9..Date: Thu, 25 Jul 2019 12:56:40 GMT..Content-Type: text/html..Content-Length: 612..Las
t-Modified: Tue, 23 Dec 2014 16:25:09 GMT..Connection: keep-alive..ETag: "54999765-264"..Accept-Ranges: bytes....
#
T 10.0.0.6:80 -> 10.0.1.8:53908 [AP]
<!DOCTYPE html>.<html>.<head>.<title>Welcome to nginx!</title>.<style>. body {. width: 35em;. margin: 0 aut
o;. font-family: Tahoma, Verdana, Arial, sans-serif;. }.</style>.</head>.<body>.<h1>Welcome to nginx!</h1>.<p>If y
ou see this page, the nginx web server is successfully installed and.working. Further configuration is required.</p>..<p>For
online documentation and support please refer to.<a href="http://nginx.org/">nginx.org</a>.<br/>.Commercial support is avai
lable at.<a href="http://nginx.com/">nginx.com</a>.</p>..<p><em>Thank you for using nginx.</em></p>.</body>.</html>.
#####
後片付け
alpine podをexit
して、ローカルのコンテナに復帰します。
$ exit
クラスターを削除します。
$ gcloud container clusters delete strace-hands-on -q
プロジェクトを削除します。
$ gcloud projects delete $PROJECT_NAME -q
最後にCloud SDKのコンテナを全てexitして後始末は完了です。
toolboxコンテナのネームスペースについて補足
コンテナは、プロセスに固有のネームスペース(=プロセスからのOSの各種リソースの見え方)を割り当てることで、プロセスを隔離したものですが、ネームスペースの割り当て方によっては、通常のプロセスとコンテナのプロセスとの中間状態にあるようなプロセスを作成できます。
toolboxはその一種で、ほぼ通常のホストのプロセスにように振る舞いながら、ファイルシステムだけ固有の名前空間が割り当てられているので、ホストマシンを汚すことなくデバッグツールのインストールと実行を行うことができます。
以下では、toolboxなどのネームスペースを見ていきます。
まず、ホストのデフォルトのネームスペースは以下のようになっています。
$ ls -la /proc/$$/ns
total 0
dr-x--x--x 2 hands-on-user hands-on-user 0 Jul 25 08:20 .
dr-xr-xr-x 9 hands-on-user hands-on-user 0 Jul 25 08:15 ..
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 net -> 'net:[4026531993]'
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 user -> 'user:[4026531837]'
lrwxrwxrwx 1 hands-on-user hands-on-user 0 Jul 25 08:20 uts -> 'uts:[4026531838]'
一方で、toolboxのネームスペースは以下のようになっており、cgroup(CPUやメモリなどの使用を制限)をmnt(ファイルシステムのマウントポイントを隔離)以外はホストの通常のプロセスのネームスペースと同一であることが分かります。
$ ls -la /proc/$$/ns
total 0
dr-x--x--x 2 root root 0 Jul 25 08:22 .
dr-xr-xr-x 9 root root 0 Jul 25 08:21 ..
lrwxrwxrwx 1 root root 0 Jul 25 08:22 cgroup -> cgroup:[4026532621]
lrwxrwxrwx 1 root root 0 Jul 25 08:22 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Jul 25 08:22 mnt -> mnt:[4026532622]
lrwxrwxrwx 1 root root 0 Jul 25 08:22 net -> net:[4026531993]
lrwxrwxrwx 1 root root 0 Jul 25 08:22 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Jul 25 08:22 pid_for_children -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Jul 25 08:22 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jul 25 08:22 uts -> uts:[4026531838]
一方でk8sにデプロイされたコンテナプロセスの場合は、全てのネームスペースが固有なものを割り当てられており、ホストマシンのシステムからはほぼ完全に隔離されています。
$ sudo ls -la /proc/$PID/ns
total 0
dr-x--x--x 2 root root 0 Jul 25 08:28 .
dr-xr-xr-x 9 root root 0 Jul 25 08:12 ..
lrwxrwxrwx 1 root root 0 Jul 25 08:28 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Jul 25 08:28 ipc -> 'ipc:[4026532233]'
lrwxrwxrwx 1 root root 0 Jul 25 08:28 mnt -> 'mnt:[4026532618]'
lrwxrwxrwx 1 root root 0 Jul 25 08:28 net -> 'net:[4026532236]'
lrwxrwxrwx 1 root root 0 Jul 25 08:28 pid -> 'pid:[4026532620]'
lrwxrwxrwx 1 root root 0 Jul 25 08:28 pid_for_children -> 'pid:[4026532620]'
lrwxrwxrwx 1 root root 0 Jul 25 08:28 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Jul 25 08:28 uts -> 'uts:[4026532619]'
また、nsenterコマンドによって、ネームスペースの一部をコンテナプロセスなどと共有することができますが、以下のようにネットワークネームスペースがコンテナプロセスのものと確かに同一になっていることが分かります。
$ nsenter -t $PID --net sh -c "ls -la /proc/\$\$/ns" | grep net
lrwxrwxrwx 1 root root 0 Jul 25 08:31 net -> net:[4026532236]
$ sudo ls -la /proc/$PID/ns | grep net
lrwxrwxrwx 1 root root 0 Jul 25 08:28 net -> net:[4026532236]
参考文献
コンテナのネームスペースに関しては以下が参考になります。