云原生探索系列(六):深入解析Pod

174 阅读9分钟

前言

刚接触k8s时,公司运维同事分享k8s相关技术,记忆最清晰的一句话就是Pod是k8s调度的最小单位,然后巴拉巴拉说了一大堆专业术语,可能他觉得大家能听懂吧,我嘴上说着“嗯嗯嗯”,心里却想着这是啥玩意啊, 完全不知道运维同事在讲啥,这也促使我翻阅k8s相关书籍,最起码搞清专业术语,不至于和技术人员出现"交流障碍"。好了,不废话了,进入正题,深入解析Pod。

为啥先理解Pod

为啥笔者先从Pod开始了解呢?一个是在运维同事分享时提到“Pod是k8s调度的最小单位”,另一个是,在k8s中所有的资源对象 几乎都是为Pod服务的。Service用来控制Pod网络,Deployment来维护Pod的运行状态,服务账户(ServiceAccount)和基于 角色的权限控制(RBAC)管理Pod的运行权限,可以看到都跟Pod有关,Pod是唯一正在容纳并运行容器的载体,所以先从Pod开始理解。

Pod架构

网络协作

在接触容器时,我们都知道,在容器里奉行“一个容器只运行一个进程”,而实际情况中许多服务很复杂,需要多个进程互相配合完成,这也就意味着 需要多个容器相互配合。Kubernetes巧妙的解决了这个问题,他为这些容器提供了场所,就是Pod对象。
Pod是Kubernetes调度的最小单位,而不是容器,Pod由多个容器共同组成,这些容器之间共享一定限度的资源配置以便方便写作。
Kubernetes在每个Pod中默认启动一个sandbox容器,这个容器的主要作用是提供基础环境。我们来实际验证一下:
nginx-deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.8
          ports:
            - containerPort: 80
          volumeMounts:
            - mountPath: "/usr/share/nginx/html"
              name: nginx-vol
        - name: tomcat
          image: tomcat
          ports:
            - containerPort: 90
      volumes:
        - name: nginx-vol
          hostPath:
            path: "/opt"

接下来,运行如下命令:

kubectl apply -f nginx-deploy.yaml

成功之后,可以看到pod如下:

kubectl get po -n nginx -l app=nginx -owide
NAME                                READY   STATUS    RESTARTS   AGE   IP            NODE         NOMINATED NODE   READINESS GATES
nginx-deployment-77788b9c8c-5fllr   2/2     Running   0          45m   10.244.2.55   k8s-node02   <none>           <none>
nginx-deployment-77788b9c8c-bk7zw   2/2     Running   0          44m   10.244.2.56   k8s-node02   <none>           <none>

看到pod运行在节点k8s-node02 ,登录该节点机器,执行如下命令:

docker ps | grep pause

执行结果如下:

415529623c1a   registry.aliyuncs.com/google_containers/pause:3.4.1   "/pause"                 51 minutes ago   Up 51 minutes             k8s_POD_nginx-deployment-77788b9c8c-bk7zw_nginx_8cea04b5-97bc-4044-b73c-b3c182b64eb7_0
4f2919dfe22b   registry.aliyuncs.com/google_containers/pause:3.4.1   "/pause"                 52 minutes ago   Up 52 minutes             k8s_POD_nginx-deployment-77788b9c8c-5fllr_nginx_d632d6e7-812d-4746-8656-6d0c98997842_0

从输出结果就可以看到,每一个pod都运行了一个pause容器,这就是sandbox容器。
对于同一个Pod中的容器来说:

  • 可以通过localhost互相访问

  • 看到的网络设备都是sandbox容器的网络设备

  • 一个pod只有一个IP地址,这也就是sandbox容器的IP地址,也就是这个pod的Network Namespace对应的IP地址

  • 一个pod中的所有网络资源都是被容器共享的

  • pod的声明周期只跟sandbox容器一致,与其他容器无关

    我们可以用下图来简单表示pod的内部结构:

image.png

pod中所有容器的网络流量都是经过Sandbox容器的。这一点很重要,我们要针对网络命名空间来思考问题,而不是针对 某个容器的网络进行思考。如果你要为 Kubernetes 开发一个网络插件时,应该重点考虑的是 如何配置这个 Pod 的 Network Namespace,而不是每一个用户容器如何使用你的网络配置,这是 没有意义的。

文件协作

一个 Volume 对应的宿主机目录对于 Pod 来说就只有一个,Pod 里的容器只要声明挂载这 个 Volume,就一定可以共享这个 Volume 对应的宿主机目录。
经典案例:WAR包与Web服务器,我们现在有一个 Java Web 应用的 WAR 包,它需要被放在 Tomcat 的 webapps 目录下运行起来。
现在我有一个hello.war,就是输出Hello World! ,现在将其构建为一个镜像,如下: Dockerfile

FROM scratch

# 将 WAR 包添加到镜像中
COPY hello.war /hello.war

使用 scratch 作为基础镜像意味着镜像将不包含任何操作系统或环境,它将只包含您添加的文件。 然后执行如下命令:

docker build -t helloworld .

这样就构建成功了。这个镜像很简单,只有一个 WAR 包(hello.war)放在根目录下.
好接下来,我们来写配置文件:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld-deployment
  namespace: hello
spec:
  selector:
    matchLabels:
      app: hello
  replicas: 1
  template:
    metadata:
      labels:
        app: hello
    spec:
      initContainers:
        - image: registry.cn-beijing.aliyuncs.com/haohb/common:helloworld1.0
          name: war
          command: ["cp", "/FingerAppPublish.war", "/app"]
          volumeMounts:
            - mountPath: /app
              name: app-volume
      containers:
        - name: tomcat
          image: tomcat:9.0.22
          command: ["/usr/local/tomcat/bin/catalina.sh", "run"]
          ports:
            - containerPort: 8080
              hostPort: 8001
          volumeMounts:
            - mountPath: "/usr/local/tomcat/webapps"
              name: app-volume
      volumes:
        - name: app-volume
          emptyDir: {}

我们定义了两个容器,第一个容器使用的镜像是 registry.cn-beijing.aliyuncs.com/haohb/common:helloworld1.0,这个镜像 里只有一个 WAR 包(FingerAppPublish.war)放在根目录下。


而第二个容器则使用的是一个标准的 Tomcat 镜像。 不过,你可能已经注意到,WAR 包容器的类型不再是一个普通容器,而是一个 Init Container 类型 的容器。


在 Pod 中,所有 Init Container 定义的容器,都会比 spec.containers 定义的用户容器先启动。并且,Init Container 容器会按顺序逐一启动,而直到它们都启动并且退出了,用户容器才会启动。
所以,这个 Init Container 类型的 WAR 包容器启动后,我执行了一句 "cp /FingerAppPublish.war /app", 把应用的 WAR 包拷贝到 /app 目录下,然后退出。
而后这个 /app 目录,就挂载了一个名叫 app-volume 的 Volume。
Tomcat 容器,同样声明了挂载 app-volume 到自己的 webapps 目录下。 所以,等 Tomcat 容器启动时,它的 webapps 目录下就一定会存在 FingerAppPublish.war 文件:这个文件 正是 WAR 包容器启动时拷贝到这个 Volume 里面的,而这个 Volume 是被这两个容器共享的。
好了,我们执行如下命令进行创建:

kubectl apply -f hello-deploy.yaml

执行后,pod创建成功,我们可以通过pod的IP进行访问测试:

curl 10.244.2.71:8080/FingerAppPublish/

输出结果如下:

<html>
<body>
<h2>Hello World!</h2>
</body>
</html>

这就证明成功了,我们也可以进入这个容器查看,

kubectl exec -it -n hello helloworld-deployment-6b597758fd-nb9rt -- /bin/bash

然后,查看/usr/local/tomcat/webapps目录,

drwxr-x--- 6 root root        84 Jul 16 02:32 FingerAppPublish
-rw-r--r-- 1 root root  22158840 Jul 16 02:31 FingerAppPublish.war

可以看到确实有这个war包,在容器内,我们也可以通过localhost来访问:

curl localhost:8080/FingerAppPublish/

输出结果和上面是一样的。

sidecar

上面这个案例中,用一种“组合”方式,解决了 WAR 包与 Tomcat 容器之间耦合关系的问题。
实际上,这个所谓的“组合”操作,正是容器设计模式里最常用的一种模式,它的名字叫: sidecar。
sidecar 指的就是我们可以在一个 Pod 中,启动一个辅助容器,来完成一些独立于主进程(主容器)之外的工作。
比如,在我们的这个应用 Pod 中,Tomcat 容器是我们要使用的主容器,而 WAR 包容器的存在, 只是为了给它提供一个 WAR 包而已。所以,我们用 Init Container 的方式优先运行 WAR 包容 器,扮演了一个 sidecar 的角色。

最后

在这篇文章中,深入浅出地介绍了Pod的架构,不得不说,初次接触时可能会觉得有些迷糊!但请放心!只要用心去研究和实践,坚持几天后,肯定能对Pod有更深层次的理解。期待未来学习更多关于Pod的调度和资源管理知识!