在Kubernetes中,为什么我们需要Pod,而不是直接的一个容器? Pod是什么?如何更好的使用Pod?

为什么需要Pod?

先抛出几个问题。

  1. 为什么在kubernetes我们不直接使用一个单独的容器(container),而是用pod来封装一个或多个容器呢?
  2. 为什么我们要运行多个容器呢?
  3. 我们能将我们所有的应用程序都放到一个容器里面运行么?

Pod是什么?

先让我们来看下官方文档对Pod的定义:

Pod是一个或一个以上的 容器(例如Docker容器)组成的,且具有共享存储/网络/UTS/PID的能力,以及运行容器的规范。并且在kubernetes中,Pod是最小的可被调度的原子单位。

通俗来讲,Pod就是一组容器的集合,在Pod里面的容器共享网络/存储(kubernetes实现共享一组的namespace去替代每个container各自的NS,来实现这种能力),所以它们可以通过localhost进行内部的通信。虽然网络存储都是共享的,但是cpu和memory就不是。多容器之间可以有属于自己的cgroup,也就是说我们可以单独的对Pod中的容器做资源(MEM/CPU)使用的限制。

Pod就像是我们的一个”专有主机”,上面除了运行我们的主应用程序之外,还可以运行一个与该应用紧密相关的进程。如日志收集工具、git文件拉取器、配置文件更新重启器等。因为在kubernetes中,一个Pod里的所有container都只会被分配到同一台主机上运行。

如何正确使用Pod?

在”Kubernetes Up Running“ 这本书中讲的一个很好的例子,在这里分享一下。

既然一个Pod可以包含多个容器,就像一个主机包含有多个进程一样,那我是不是可以将Wordpress和mysql数据库都以容器的方式放在一个Pod里面运行?大家仔细想想,这会有什么问题。 可以从资源管理、服务可扩展等方面上进行思考下。

也许大家已经想到了,有两个主要原因:

第一,WordPress和它的db不是真正的共生关系。想象一下,如果WordPress 容器和 database 容器都运行在不同的机器(aka:Node节点)上,它们之间依然可以通过网络交互的方式实现正常的工作。

第二,从服务扩容上来看,你通常不会将Wordpress和Mysql作为一个单元来一起扩容。因为我们常规只想扩容我们的前端服务(Wordpress),创建更多的wordpress容器。来接受更多的流量。

另外这本书给我们一个很好的方法。就是我们决定在设计应用程序时,怎样来组织pod中的container?

首先可以在脑海中仔细思考下:“这些进程容器在不同机器上是否能正常的工作运行?”。如果答案是否定的。那么将这些进程以每个进程一个容器的方式放到一个Pod中组合在一起是合适的。反之,以多个Pod运行这些容器是正确的方式。

Pod的管理操作

在传统运维工作中,我们是和各种服务进程打交道。到了容器集群环境中,我们就是操作Pod来对服务进行检查维护。 这里只简单介绍下几个操作, 其中kubectl有很多命令都和docker run类似

kubectl run pod-name --image=docker.io/aliasmee/ikev2-alpine-vpn:latest --restart=Never

Tips: restart[Never: "创建一个普通pod", Always: "创建一个deployment", OnFailure: "创建一个job"]

Tips: Service Type [ClusterIP, NodePort, LoadBalancer]

Tips: -f 替换成 -n number 将显示特定number数量行的日志, 另外--previous可以显示之前容器的日志,如果该容器在出现问题不停重启的情况下。

kubectl exec -it pod-name

Tips: 如果该pod有多个容器的话,可以使用-c container 指定进入的目标容器。

kubectl cp pod-name:/path/to/file /tmp/localpath/file

Pod的资源分配

在pod里面,我们可以针对常规的两种资源对容器做使用限制,它们分别是Memory、Cpu。因为我们知道,主机上的资源是有限的,我们要尽可能要确保主机资源能够有效的发挥最大作用。并保证每个运行在该主机上的容器都能合理的得到相应资源,得以正常运行,不能因为某些容器内部异常(如内存泄漏)的问题,导致整个主机资源耗尽,其它容器无法正常被调度,甚至主机上的服务都无响应。

因此,为了确保计算和内存资源被合理的使用,我们需要对我们的服务做深入的分析,规划它们的资源使用。

kubernetes提供了两种模式来让我们设置,一种是request,另一种是limits。前面也说了,我们pod中的每个容器都有单独的cgroup。所以我们根据需要针对每个容器都要做限制。

如下所示:

resource:
  requests:
    cpu: "200m"
    memory: "500Mi"
  limmits:
    cpu: "2000m"
    memory: "2Gi"

简单对上面的一段代码解释:

Pod 存储(Volume)

有无状态的应用,那么自然就有有状态的应用。如我们的web前端,nginx都是无状态的。这些应用本省就很好维护,而且方便横向扩展。但对于一些应用,比如database、redis等需要保存服务状态的应用,因为容器本身的生命周期就很短,我们需要保证在该容器被删除、重启,即使被调度到另外一台主机上的情况下,该应用的数据可以接着之前的状态继续对外提供服务。

kubernetes 提供了很多种类型的volumes,常用的有emptyDir(可以用内存作为存储tmpfs)、hostPath、Cloud provider EBS、NFS、GlusterFS、PVC(动态申请卷)。

Pod 网络

Pod与Pod之间的通信,可以理解成不同VM之间的通信。因为每个Pod有自己唯一的IP地址、端口空间。实现Pod网络互通的方式有L2/L3以及Overlay。 实际上,Kubernetes没有实现这些网络平面的工作,但它定义了一种规范CNI(Container Network Interface)插件。目前有多种解决方案。如Flannel、Weavenet、Calico。

由于本篇主要说的是Pod。所以这里简单的说下flannel.

首先,Node节点上除了有自己的物理地址(称为host network),还有flannel运行的cni桥接网络。另外还有docker0. 每个Node节点在加入集群之后会通过etcd分配获得1个小子网。这些子网中的IP是预留给每个Pod提供的。也就是说每个Pod在创建时,会从中分配获得一个有效的IP地址。 下面通过文字大致说下一个包的流向。

  1. 当运行在NodeA节点(192.168.1.2/32)上的PodA(10.0.0.2/32)想要访问 PodB(10.0.1.3/32)80端口时,PodB运行在NodeB节点上(192.168.1.3/32)。PodA 对NodeA说:“主人,我这有个数据包,请帮我送到10.0.1.3/32 这个位置上。谢谢。”

  2. 这时NodeA上的flannel程序会从etcd中查询:“请问,你把10.0.1.0/24这个子网分配到哪个Node节点上了?”,etcd会回复:“10.0.1.0/24子网在NodeB节点上”。这时NodeA将PodA发出的包扔给NodeB:“hi,Man,你有一封数据包。请注意查收”。

  3. 当NodeB接收了这个从NodeA发来的数据包时,解开包发现,这个包的真实目的地是送往10.0.1.3/32的。这时NodeB会查询自己的路由表,看到通往10.0.1.0/24路由都发送给flannel0 bridge上,这时包继续流向该桥接网卡上:“hi,PodB,有你的数据包,是10.0.0.2/32 发来的。你赶紧收一下。”

  4. 这时PodA收到这个包后,根据请求的参数,然后返回相应的数据给10.0.0.2/32….

Pod 生命周期

Pod在集群中可能有以下几种状态:

Pod的生命是短暂的。

Pod 调度

这里再说下pod的调度。

通常,我们在创建pod时,schedule将会根据要求的资源,选择一台满足的Node节点,而这个选择是随机的。而这种调度将完全交给kubernetes集群。

考虑下面的情况。假设我们在集群上部署了ML应用,该应用在运行中,需要大量的GPU资源,或者另外一种类型的应用,如database应用,为了更好的提高读写,加大处理效率,需要选择SSD来做数据存储。

以上举出的两种情况,在现实程序世界中,我们肯定会经常遇到的。

为了解决上述的要求,kubernetes提供了几种pod的调度方式。基本上是依靠Label来实现的。如,我们在带有GPU资源的Node节点上,打上label hardwardType=GPU;在带有SSD资源的Node节点上,打上label diskType=SSD。这样我们在调度pod时,就可以根据label来select这些专有Node节点。 kubernetes提供了几种方式:

第一种NodeSelector 是比较简单的一种方式,只是匹配了目标labels的key/value是否符合(未来将会被遗弃)

第二种Affinity 相对Nodeselector来说,表达式语法更强大,可匹配的操作条件模式增多。

第三种PodAffinity 又叫pod亲和性,这个是让一些pod和另外的服务的pod总是调度在同一台Node节点上。比如web服务和redis实例。

前几种都是保证会调度到该相应匹配的节点上。而第四种Taint(污点),它可以让一台Node节点成为某服务的专属节点。前提是这个服务必须能Toleration(容忍)这个污点。

即没有特殊表明容忍该污点的Pod将从不会被调度该Node节点上。这个的用途经常给一些专属的服务使用,例如们为了保证运行生产服务的Pod尽可能和其它环境的pod隔离,想让生产pod部署到专有节点上。这时我们就可以采用Taint来实现这个需求。

Pod的健康就绪检查

在kubernetes中,我们的服务一旦部署到集群中,k8s相当于一个大管家,它将会照顾好我们的应用服务。并会根据我们服务需要的资源,确保被调度到满足要求的Node节点上。且会定时的检测,一旦发现该服务pod所在的节点出现故障,k8s将会把pod重新调度到其它可用的节点上。努力保证服务的稳定、可靠性。极大的减少了运维人员的工作时间。

Tips: Pod 本身不会自我修复,比如自动迁移到其他Node节点运行,而是由上层的Deployment/ReplicaSets控制器来实现。

这是容器外部的调度,k8s会替我们照看好我们的进程。但我们服务本身也要做好可用性的探测检查,也要保证容器在启动时,我们的应用服务也能正常快速的启动,并且在长期运行过程中,也要保持健康稳定的运行。

下面就说一下k8s为我们提供的服务健康检测的方式.

容器的健康检查目前分为两种类型: 一是Liveness存活检查、二是Readiness准备就绪检查

两种类型各有千秋,着重点不同。Liveness检查确保我们的服务在长期不停机运行中,可以稳定的提供服务,一旦检查失败次数达到我们预设的值后,该容器将会被重启。如果该容器加了Readiness检查,那么在重启后,将会根据检查条件,判断是否已准备就绪。这里需要注意的是,如果该容器在启动时,readiness没有通过,那么它将不会被加入到前面的负载均衡的池子中(endpoints)。这可以很好的避免该服务启动不正常而却被负载均衡识别成正常服务,返回给客户端错误的相应。

Tips: 如果我们容器内的服务启动相对慢一些,比如运行时需要加载生成一些资源。我们的Readniess可能要考虑设置下延迟检测时间(initialDelaySecond)

Pod VS Container

试比较Kubernetes中的Pod和Docker中的container:

Pod:

  1. 多container共享网络、存储
  2. 像在“专用主机”上运行多应用,而不必把多应用全塞到一个container中
  3. 方便监控,我们可以对pod中的多个容器单独设置不同的健康检查,记录日志及分析
  4. 不用担心单个容器内多进程,其某个进程崩溃导致整个容器挂掉的情况

Container:

  1. 容器设计理念是一个容器只运行一个主进程(其产生的子进程不算)
  2. 单个容器,与其它container是”完全隔离”的
  3. 常规情况下,无法与其它容器共享网络、存储。只能通过expose的端口进行相互访问

Tips:Pod中的多container就不是完全隔离了.

在虚拟化的领域里,最基本的可调度的单元对象是VM.

在容器化的领域里,最基本的可调度的单元对象是container

在kubernetes的领域里,最基本的可调度单位是Pod。

总结

按照容器的设计理念,每个容器只运行单个进程。而要想实现多个container被绑定在一起进行管理的需求。我们需要一种高级别的概念来实现这个。在kubernetes中,这就是Pod。 在Pod里面,container之间可以共享网络(IP/Port)、共享存储(Volume)、共享Hostname。

另外,Pods可以理解成一个”逻辑主机”,它与非容器领域的物理主机或者VM有着类似的行为。在同一个Pod运行的进程就像在同一物理主机或VM上运行的进程一样。只是这些进程被单独的放到单个container内。

参考:

What are kubernetes pods anyway

Kubernetes networking

An illustrated guide to kubernetes networking

Kubernetes flannel networking

Kubernetes: Up and Running

Kubernetes in Action