0x00 前言

随着微服务架构和云计算的普及,越来越多企业的应用都上了云,不仅是云基础设施 IaaS ,如 kubernetes 等 PaaS 项目也是越来越热门。但新的技术会带来新的架构复杂,同时也会使排查问题更加困难,因此很多运维和开发同学都觉得用平时用的顺手的工具和手段在容器里排查问题不好使了。工欲善其事必先利其器,正是由于这样的情况,所以我们排查容器问题的时候,需要引入新的工具和手段。

0x10 容器基础

在学习工具的使用前,我们首先需要简单的了解下容器的原理。假如一台机器是一间房子,那么进程就是住在里面的一个个的人,在单体应用的时代,所有人都住在一间房子里,而容器技术就是通过一些手段把这些人都隔开,让每个人都以为自己住上有独卫(网络,IPC,namespace 等资源)的单间。而
这些隔离和限制的主要使用的如下技术:

  • cgroups 资源限制
  • namespace 资源隔离
  • rootfs 文件系统隔离

在单体应用的时代,所有的进程都在同一个命名空间里,且启动的进程都没有隔离命名空间,那么自然调试工具进程也在同一个命名空间,也就可以 debug 其他进程。而容器技术由于分割成一个个的小房间,如果想要查看单个房间的情况,虽然在大管家(宿主机)的上帝视角一样可以看到, 但为了减少干扰并更加符合我们平时的使用习惯,我们就需要进入到房间(命名空间)里面查看。

例如我们可以通过 docker inspect CONTAINER_ID 获取到某个容器资源隔离的文件的地址,如下"SandboxKey": "/var/run/docker/netns/50def85bf6e2"就是。

@ubuntu ➜ k8s-debug  docker inspect 0dde03166e02
...
            "SandboxKey": "/var/run/docker/netns/50def85bf6e2",
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "EndpointID": "7faa1e764317cdfadf7f31b7ed2fecff62b458211f79fadb3362c8e22755f326",
            "Gateway": "172.17.0.1",                    
...

而容器的诊断工具就是自带了部分调试工具的镜像,并能根据容器 ID 帮我们自动地进入到房间(网络,IPC 命名空间等)。
在初步了解了容器的原理后,我们便可进入工具的介绍了。

0x20 netshoot

首先介绍的第一个工具是netshoot,netshoot 的自我定位就是容器网络诊断的瑞士军刀,简单来说,netshoot 其实就是一个装满了各种工具的镜像,他用起来也很简单。

  • 执行docker run -it --net container:<container_name|container_id> nicolaka/netshoot 就行,这里的--net是 docker 命令指定该容器要联结到哪个容器的网络命名空间
  • 如果要进入宿主机的命名空间则指定--net host就行了
  • 如果要诊断 docker NIC设备的网络 情况,则可以用工具nsenter进入NIC 设备的命名空间排查,后面我会介绍这个工具

另外,如果是在 kubernetes 里面,我们可以通过执行kubectl run test-lab --generator=run-pod/v1 --rm -i --tty --overrides='{"spec": {"hostNetwork": true}}' --image nicolaka/netshoot -- /bin/bash 这个命令进入宿主机的网络。别怕这个命令长,我来一一解释下这条命令的各个选项的作用

  • kubectl run test_lab --generator=pod/v1 --rm -i --tty意思是创建一个一次性的名叫test_lab的 Pod 资源并且使用标准输入输出交互
  • --overrides='{"spec": {"hostNetwork": true}}'意思是使用宿主机网络,具体哪台宿主机要看这个 Pod 调度到哪个节点。
  • --image nicolaka/netshoot指定 Pod 的镜像
  • -- 这是 bash 的内置命令选项,是标志命令的结束的意思,举个例子:如果我想要在文件里用grep搜索-v字符串,grep -v filename-v会被视为选项,但我如果使用grep -- -v filename那么就可以正常搜索了

0x21 演示

下面我来演示几个例子:

  1. 使用 tcpdump 抓容器的包并拷贝 pcap 文件出来,便于用 wireshark 分析
    @ubuntu ➜ k8s-debug  mkdir -p /tmp/netshoot
    @ubuntu ➜ k8s-debug  docker ps
    CONTAINER ID        IMAGE                      COMMAND             CREATED             STATUS              PORTS                                          NAMES
    0dde03166e02        jumpserver/jms_all:1.5.4   "entrypoint.sh"     3 weeks ago         Up 3weeks          ...   mystifying_williamson
    @ubuntu ➜ k8s-debug  docker run -it -v /tmp/netshoot:/tmp --net container:0dde03166e02  nicolaka/netshoot
    Welcome to Netshoot! (github.com/nicolaka/netshoot)               
    root @ / 
    [1]   → tcpdump -nn -i any -w /tmp/pkg.pcap
    [2]   → exit
    
    具体命令使用与之前的差不多,只不过把宿主机上的/tmp/netshoot目录 bind-mount 到了容器的/tmp目录
  2. 有时候我们还需要调试 bridge 或者 overlay 网络,可以使用 nsenter,nsenter 可以进入任何命名空间
    @ubuntu ➜ ~  docker network ls
    NETWORK ID          NAME                DRIVER              SCOPE
    ...
    0ipu2p43c6jh        ingress             overlay             swarm
    697402c52a87        none                null                local
    @ubuntu ➜ ~   docker run -it --rm -v /var/run/docker/netns:/var/run/docker/netns --privileged=true nicolaka/netshoot
    Welcome to Netshoot! (github.com/nicolaka/netshoot)
    root @ /
    [1] 🐳  → ls /var/run/docker/netns/
    1-0ipu2p43c6  50def85bf6e2  83f9ffa847d7  default       ingress_sbox
    root @ /var/run/docker/netns
    [6] 🐳  → nsenter --net=/var/run/docker/netns/1-0ipu2p43c6 sh
    root @ /run/docker/netns
    [#] 🐳  → ifconfig
    br0     Link encap:Ethernet  HWaddr 02:61:F2:E4:26:3B
            inet addr:10.255.0.1  Bcast:10.255.255.255  Mask:255.255.0.0
            UP BROADCAST RUNNING MULTICAST  MTU:1450  Metric:1
    ...
    vxlan0  Link encap:Ethernet  HWaddr 02:61:F2:E4:26:3B
            UP BROADCAST RUNNING MULTICAST  MTU:1450  Metric:1
            RX packets:0 errors:0 dropped:0 overruns:0 frame:0
            TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
            collisions:0 txqueuelen:0
            RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
    root @ /run/docker/netns
    [#] 🐳  → bridge fdb show br0
    33:33:00:00:00:01 dev br0 self permanent
    01:00:5e:00:00:01 dev br0 self permanent
    02:61:f2:e4:26:3b dev vxlan0 master br0 permanent
    ...
    
    上面的命令首先我们是进入了1-0ipu2p43c6的命名空间,即那个叫 ingress 的 overlay 网络,然后可以通过查看这个 NIC 设备上的fdb 表
  3. 我们也可以通过挂载 docker 的 unix sock 文件查看容器的 metrics
    docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock nicolaka/netshoot ctop
    
    如下图所示
    1.png
    netshoot 工具非常强大,还有很多功能可以自行去探索,netshoot上有详细的说明。

0x30 docker-debug

上面介绍的 netshoot 主要定位于 docker 网络的诊断,从名字就可以看出来。而我们现在介绍的工具 docker-debug可以说是 netshoot 的升级版,他不仅可以进入目标容器的网络命名空间,还可以进入 pid,user,filesystem,ipc 的命名空间,所以我们可以操作的空间就更大了。话不多说,我们开始演示。

0x31 安装

首先我们要下载 docker-debug 的二进制文件

@ubuntu ➜ docker-debug  wget docker-debug https://github.com/zeromake/docker-debug/releases/download/0.6.2/docker-debug-linux-amd64 -O docker-debug
@ubuntu ➜ docker-debug  chmod +x docker-debug
@ubuntu ➜ docker-debug  mv docker-debug /usr/bin
@ubuntu ➜ docker-debug  docker-debug info
Version:	0.6.2
Platform:	TravisLinux
Commit:		cf4cc41
Time:		2019-06-20 05:40:52 +0000

然后我赋予了文件执行权限并移动到/usr/bin目录下,如果执行docker-debug info看到有正确输出,则说明安装成功了

0x32 使用

使用就很简单了,首先我们获取到容器的名字或者容器 ID

@ubuntu ➜ docker-debug  docker ps
CONTAINER ID        IMAGE                      COMMAND             CREATED             STATUS              PORTS                                          NAMES
0dde03166e02        jumpserver/jms_all:1.5.4   "entrypoint.sh"     3 weeks ago         Up 3 weeks          ...   mystifying_williamson

然后执行docker-debug <CONTAINER_ID|CONTAINER_NAME> COMMAND就可以了

@ubuntu ➜ docker-debug  docker-debug 0dde03166e02 bash
bash-5.0# netstat -lntp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
...
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      52/python3.6
tcp        0      0 0.0.0.0:8081            0.0.0.0:*               LISTEN      110/java
...
bash-5.0# ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02
          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:32776325 errors:0 dropped:0 overruns:0 frame:0
...
bash-5.0# ls /mnt/container/
anaconda-post.log  dev                lib                mnt                root               srv                usr
bin                etc                lib64              opt                run                sys                var
config             home               media              proc               sbin               tmp

我们可以看到已经进入了目标容器的 ipc,network,filesystem,pid 的命名空间了,而目标容器的root则挂载在了/mnt/container目录下。
此外,我们还可以通过设置 docker-debug 在~/.docker-debug/config.toml的配置文件使用自定义的诊断镜像

version = "0.6.1"
image = "nicolaka/netshoot:latest"
mount_dir = "/mnt/container"
timeout = 10000000000
config_default = "default"

[config]
  [config.default]
    host = "unix:///var/run/docker.sock"
    tls = false
    cert_dir = ""
    cert_password = ""

其他就不做过多介绍了

0x40 kubectl-debug

在介绍了 docker 的 debug 的工具后,我们了解了容器诊断工具的原理和使用,接下来我们要学习 kubernetes 的容器诊断工具。虽然 kubernetes 上也可以用我上面介绍的那些工具,但 kubernetes 上的容器毕竟运行在不同的 node 上,用起来就不太方便,所以就要用到 kubectl-debug 这个工具了。

kubectl-debug 其实就是一个 kubectl 的插件,他的原理和 docker 容器诊断工具大同小异。kubectl-debug 可以帮我们在 某个 Pod 的节点上起一个容器,并将这个容器加入到目标容器的pid,network,user,icp 的命名空间。kubectl-debug 架构主要可以分为两部分:

  • 客户端:kubectl-debug 二进制文件
  • 服务端:agent 容器
    客户端通过控制 node 上的 agent 服务端与容器运行时通信,从而启动一个容器并进入到指定 Pod 的命名空间,可以说 agent 就是一个 debug 容器与客户端之间的中继。而从 kubectl-debug 的工作模式来看,可以分为两种模式:
  • 非常驻服务端:agentless
  • 常驻服务端: DaemonSet
    简单来说就是 agentless 模式只有在每次 kubectl-debug 进行调试 Pod 的时候才会启动一个 agent 服务端,调试完成后自动清理 agent,此模式的优点是不那么占用 kubernetes 集群资源,而 DaemonSet 模式就是在每个节点上都会常驻一个 DaemonSet 的 agent, 好处就是启动快。
    此外针对 node 节点无法直接访问的情况,kubectl-debug 还有一个 port-forward 模式,这里就不多介绍了。

由于 kubectl-debug 可能还不太完善,agentless 模式我这里用不了,所以我用的是 DaemonSet 模式,下面开始演示。

0x41 安装客户端

安装过程和 docker-debug 差不多

  1. 下载二进制文件: wget https://github.com/aylei/kubectl-debug/releases/download/v0.1.0/kubectl-debug_0.1.0_linux_amd64.tar.gz -O kubectl-debug.tar.gz
  2. 解压文件: tar -zxvf kubectl-debug.tar.gz kubectl-debug

0x42 安装 agent 服务端

  1. 下载 DaemonSet 的 yaml 文件:wget -f https://raw.githubusercontent.com/aylei/kubectl-debug/master/scripts/agent_daemonset.yml
  2. 修改agent_daemonset.yml 文件
      ...
      18       hostNetwork: true  # 需要加上 hostNetwork: true,hostPort:10027 才会生效
      19       hostPID: true
      20       tolerations:
      21         - key: node-role.kubernetes.io/master
      22           effect: NoSchedule
      23       containers:
      24         - name: debug-agent
      25           image: aylei/debug-agent:v0.1.1 # 老版本镜像有问题,使用 v0.1.1新版本
      ...
      39           ports:
      40             - containerPort: 10027
      41               hostPort: 10027
      ...
    
  3. 创建 DaemonSet:kubectl apply -f agent_daemonset.yaml, 接下来我们可以看到每个节点上都创建了 debug-agent 的 DaemonSet,并且宿主机上都监听了10027端口。
     root @ master ➜  k8s-debug   kubectl get pods
     NAME                                   READY     STATUS    RESTARTS   AGE
     debug-agent-5gfk6                      1/1       Running   0          22h
     ...
     root @ master ➜  k8s-debug  netstat -lntp | grep 10027
     tcp6       0      0 :::10027                :::*                LISTEN      15510/debug-agent
    
  4. 执行命令kubectl-debug <POD_NAME>就可以进行调试了
root @ master ➜  k8s-debug  kubectl get pods
NAME                             READY     STATUS             RESTARTS   AGE
licai-gwapi-77465b4c66-hdjlb     1/1       Running            0          3d
...
root @ master ➜  k8s-debug  kubectl-debug licai-gwapi-77465b4c66-hdjlb --agentless=false --port-forward=false
pulling image nicolaka/netshoot:latest...
...
bash-5.0# ps -ef | grep java
1 root     23:24 /usr/local/openjdk-8/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties...
192 root      0:00 grep java
bash-5.0# exit
exit
root @ master ➜  k8s-debug

我们可以看到已经进入了目标容器的命名空间了,而kubectl-debug 客户端正是与每个 node 上的 10027 端口通信来控制 agent 对 Pod 的调试。
除了这些之外,kubectl-debug 还有很多配置可以自定义,kubectl-debug页面也有详细的介绍,至此从 docker 到 kubernetes 的调试工具介绍完成了。