通过使用pstree -g命令可以看出,每个进程都不是独立存在的,而是以进程组的方式存在。

比如,rsyslogd的程序,他负责linux操作系统的日志处理。rsyslogd的主程序main,和它要用到的内核日志模块imklog等,相互协作,共同完成rsyslogd程序的职责。而k8s项目所做的,其实就是将"进程组"的概念映射到容器技术中。

还是以rsyslogd为例,已知rsyslogd有三个进程组成:imklog、imuxsock、main主程序。这三个进程要运行在同一台机器上,否则之间基于socket的通信和文件交换,都会出现问题。

现在,我们把rsyslogd这个应用容器化,由于受限于容器的“单进程模型”,这三个模块必须被分别制作成三个不同的容器。而在这三个容器运行的时候,他们设置的内存配额都是1GB。

假设k8s集群有两个节点:node-1上有3GB可用内存,node-2上有2.5GB可用内存。

这时,假设我要用docker swarm来运行这个rsyslogd程序。为了能够让这三个容器都运行在同一台机器上,我就必须在另外两个容器上设置一个affinity=main(与main容器有亲密性)的约束,即:它们俩必须和main容器运行在同一个机器上。然后执行docker run main , docker run imklog, docker run imuxsock创建这三个容器。这样,这三个容器都会进入swarm的待调度队列。然后,main容器和imklog容器都先后出队被调度到了node-2上,可是,当imuxsock容器出队开始被调度时,swarm就只能把它放在node-2上,但是node-2上只有0.5GB了,并不足以运行imuxsock容器;但是又必须在node-2上运行。由此看出,这就是一个典型的成组调度没有被妥善处理的例子。

上面这个问题可以由kubernetes解决了,pod是k8s里的原子调度单位。这就意味着,k8s项目的调度器,是统一按照pod而非容器的资源需求进行计算的。

所以,像上面这三个容器,正是一个典型的由三个容器组成的pod。k8s项目在调度时,自然就会去选择可用内存等于3GB的node-1节点进行绑定,而不会考虑node-2.

像这样容器间紧密协作,称之为“超亲密关系”。这些具有“超亲密关系”容器的典型特征有:相互之间会发生直接的文件交换、使用localhost或者socket文件进行本地通信、会发生非常频繁的远程调用、需要共享某些linux namespace(比如,一个容器要加入另一个容器的network namespace等)

这也就意味着,并不是所有有“关系”的容器都属于一个pod。比如,PHP应用的容器和MySQL虽然会发生访问关系。单并没有必要、也不应该部署在一台机器上,它们更适合做两个pod。

pod在k8s项目里有个重要的意义:容器设计模型,关于pod最重要的一个事实是:它只是一个逻辑概念。也就是说linux真正处理的,还是宿主机操作系统linux容器的namespace和cgroups,而并不存在一个所谓的pod的边界或者隔离环境。

pod,其实是一组共享了某些资源的容器。pod里的所有容器,共享的是同一个network namespace,并且可以声明共享同一个volumes。

例如:一个有A、B两个容器的pod,不就是等同于一个容器A共享另外一个容器B的网络和volume的玩法。

就好像通过docker run --net --volumes-from这样的命令就能实现。

但是,容器B就必须比容器A先启动,这样一个pod里的多个容器就不是对等关系,而是拓扑关系了。

所以,在k8s项目里,pod的实现需要使用一个中间容器Infra。在这个pod中,Infra容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过join network namespace的方式,与infra容器关联在一起。这个infra镜像是一个用汇编语言编写的、永远处于“暂停状态的容器,解压后大小也只有100-200KB左右。

而在infra容器做好network namespace后,用户容器就可以加入到infra容器的network namespace当中了。所以,如果你查看这些容器在宿主机上的namespace文件,他们指向的值一定是完全一样的。

这也就意味着,对于pod里的容器A和容器B来说:

  1. 他们可以直接使用localhost进行通信;
  2. 他们看到的网络设备跟infra容器看到的完全一样;
  3. 一个pod只有一个ip地址,也就是这个pod的network namespace对应ip地址;
  4. 当然,其他的所有网络资源,都是一个pod一份,并且被该pod中的所有容器共享;
  5. pod的生命周期只跟infra容器一致,而与容器AB无关。

而对于同一个pod里面的所有用户容器来说,它们的进出流量,也可以认为都是通过infra容器完成的。将来如果我们要为k8s开发一个网络插件时,应该重点考虑的是如何配置这个pod的network namespace,而不是每一个用户容器如何使用你的网络配置,这是没有意义的。

这就意味着,如果你的网络插件需要在容器里安装某些包或者配置才能完成的话,是不可取的:infra容器里rootfs里几乎什么都没有,没有你随意发挥的空间。当然,这同时也意味着你的网络插件完全不必关心用户容器的启动与否,而只需要关注如何配置pod,也就是infra容器的network namespace即可。

有了这个设计之后,共享volume就很容易了,只需要设计在pod层即可。这样,一个volume对应的宿主机目录对于pod来说就只有一个,pod里的容器只要声明挂载这个volume,就一定可以共享这个volume对应的主机目录。

apiVersion: v1
kind: Pod
metadata:
  name: two-containers
spec:
  restartPolicy: Never
  volumes:
  - name: shared-data
    hostPath:
      path: /data
  containers:
  - name: nginx-container
    image: nginx
    volumeMounts:
    - name: shared-data
      mountPath: /usr/share/nginx/html
  - name: debian-container
    image: debian
    volumeMounts:
    - name: shared-data
      mountPath: /pod-data
    command: ["/bin/sh"]
    args: ["-c", "echo Hello from the debian container > /pod-data/index.html"]

这个例子中,两个容器都声明挂载了shared-data这个volume。而shared-data是hostPath类型。所以,它对应在宿主机上的目录就是:/data。而这个目录,其实就被同时绑定挂载进了上述两个容器中。

这就是为什么nginx容器可以读取到debian生成的index.html的原因了。

pod这种“超亲密关系”容器的设计思想,实际上就是希望,当用户想在一个容器里跑多个功能并不相关的应用时,应该优先考虑它们是不是应该被描述成一个pod里的多个容器。

为了能够掌握这种思考方式,你就应该尽量尝试使用它来描述一些用单个容器难以解决的问题。

例1:WAR包与WEB服务器

我们现在有一个Java Web应用的WAR包,它需要被放在Tomcat的webapps目录下运行起来。

例如,你现在只能用docker来做这件事情,那该如何处理这个组合关系呢?

  • 一种方法是,把WAR包直接放在Tomcat镜像的webapps目录下,做成一个新的镜像运行起来。可是,这时候,如果你要更新WAR包的内容,或者要升级Tomcat镜像,就要重新制作一个新的发布镜像,非常麻烦。
  • 另一种方法是,你压根不管WAR包,永远只发布一个Tomcat容器。不过,这个容器的webapps目录,就必须声明一个hostPath类型的Volume,从而把宿主机上的WAR包挂载到Tomcat容器当中运行起来。不过,这样你就必须要解决一个问题,即:如何让每一台宿主机,都预先准备好这个存储有WAR包的目录呢?这样来看,你只能独立维护一套分布式存储系统了

实际上,有了pod之后,这样的问题就很容易解决了。我们可以把WAR包和Tomcat分别做成镜像,然后把他们作为一个pod里的两个容器“组合”在一起。这个pod的配置文件如下:

apiVersion: v1
kind: Pod
metadata:
  name: javaweb-2
spec:
  initContainers:
  - image: geektime/sample:v2
    name: war
    command: ["cp", "/sample.war", "/app"]
    volumeMounts:
    - mountPath: /app
      name: app-volume
  containers:
  - image: geektime/tomcat:7.0
    name: tomcat
    command: ["sh","-c","/root/apache-tomcat-7.0.42-v2/bin/start.sh"]
    volumeMounts:
    - mountPath: /root/apache-tomcat-7.0.42-v2/webapps
      name: app-volume
    ports:
    - containerPort: 8080
      hostPort: 8001
  volumes:
  - name: app-volume
    emptyDir: {}

在这个pod中,我们定义了两个容器,第一个容器使用的镜像是sample:v2,这个镜像里只有一个WAR包放在根目录下。而第二个容器则使用的是一个标准tomcat镜像。

war包类型的容器是init container类型,这个类型的容器启动后,执行cp命令,把WAR包拷贝到/app目录下。会按顺序逐一启动,知道都启动并且退出了,用户容器才会启动。

接下来,tomcat容器同样声明挂载app-volume到自己的webapps目录下。

所以,等tomcat容器启动时,它的webapps目录下就一定会存在sample.war文件,这个文件正是WAR包容器启动时拷贝到这个volume里面的,而这个volume是被两个容器共享的。

像这样,解决了WAR包与Tomcat容器之间的耦合关系问题。这种模式也被称为sidecar。sidecar指的是我们可以在一个pod中,启动一个辅助容器,来完成一些独立主进程之外的工作。

总结:

实际上,无论是从具体的实现原理,还是使用方法、特性、功能等方面,容器与虚拟机都是不同的,容器的缺点:它不能像虚拟机那样,完全模拟本地物理机环境中的部署方法。

对于容器来说,一个容器永远只能管理一个进程。所以,将一个原本运行在虚拟机里的应用,迁移到容器中的想法是不对的。

现在我们可以这么理解pod的本质:pod,实际上是在扮演传统基础设施“虚拟机”;而容器,则是这个虚拟机里运行的用户程序。

你可以把这个虚拟机想象成一个pod,把这些进程分别做成容器镜像,把有序的容器,定义为init container。这才是更加合理的、松耦合的容器编排诀窍,也是从传统应用架构,到微服务架构最自然的过渡方式。

最后,注意:不要强行把整个应用塞到一个容器里,甚至不惜使用docker in docker 这种在生产环境中后患无穷的解决方案,是大忌。

 

07-14 20:42