设计理念
- Pod 是一个逻辑单位
- Kubernetes 真正处理的其实是一组共享了某些资源的的容器
- 共享network,volume, IPC, UTS等namespace
- 如果说容器是进程,那么pod就是进程组
- 在一个真正的操作系统里,进程并不是独自运行的,而是以进程组的方式组织在一起
- 例如负责日志处理的rsyslogd程序,它的主程序和它要用到的内核日志模块 imklog 等,同属于一个进程组
- 这些进程相互协作,共同完成 rsyslogd 程序的职责
- 进程组中的进程有着密切的关系,必须部署在同一个机器上
- 例如需要基于 Socket 的通信或者涉及文件交换
- 在一个真正的操作系统里,进程并不是独自运行的,而是以进程组的方式组织在一起
- 为了解决 进程组的调度, Mesos 中使用了资源囤积的机制
- 在所有资源都囤积完毕后都满足时,才开始进行调度
- 缺点在于调度效率损失和死锁以及饥饿的可能性
- 由于资源抢占还不支持
- k8s则是把一个pod当做一个整体来调度
- 因此如果两个容器不必要在同一个机器上,则应该分为两个pod
- 例如一个后端应用和mysql数据库,最好分成两个pod,还方面水平扩容
pause容器
- 通过业务无关的
pause
容器来判断pod的生存状态- 最先启动,除了等待终止信号退出外什么都不干
k8s.gcr.io/pause
镜像是一个用汇编语言编写,大小只有 100~200 KB 左右
- 管理所有namespace并允许业务容器共享它们
- 凡是调度、网络、存储,以及安全相关的属性,基本上是 Pod 级别的
CNI
接口给pause
容器配置相关的网络,然后 Pod 中其他的容器都使用 pause 容器的网络- 所有的网络流量都经过
pause
容器转发 - pod内容器共享一个IP,通过 localhost 相互通信
- 所有的网络流量都经过
initcontainer
- 主要负责初始化工作
- 准备一些变化的配置文件
- 做一些前置条件的校验,如网络是否联通
- 当所有的 initContainers运行完成后,Kubernetes才会开始初始化应用Pod
- 在init容器完成后,主容器启动的同时,可以通过
pod.spec.lifecycle.postStart
进行初始化操作- 注意这一操作和容器是并发运行的
- 多个initContainer会按顺序一次运行一个,每个initContainers必须运行成功,下一个才能够运行
- 普通业务容器时并发启动的
- 通过启动前的检查可以规定普通容器的启动顺序
- InitContainer 执行成功后就退出并删除,而普通容器可能会一直在执行
- initContainers不支持Readiness Probe(就绪探针),因为它们必须在Pod就绪之前运行完成
pod启动过程
kubectl
首先会执行一些客户端验证操作,以确保不合法的请求将会快速失败,不会发送给apiserver
- 例如创建不支持的资源或使用格式错误的镜像名称
- 通过减少不必要的负载来提高系统性能
kubectl
查询用户信息以发送给apiserver
以供身份认证- 用户凭证保存在
kubeconfig
文件中,kubectl
通过以下顺序来找到该文件的位置- 如果提供了
--kubeconfig
参数, 就使用该参数提供的 kubeconfig 文件 - 使用
$KUBECONFIG
环境变量提供的 kubeconfig 文件 - 就使用默认的 kubeconfig 文件
$HOME/.kube/config
- 如果提供了
kubectl
会把这些信息附加到请求头中
- 用户凭证保存在
apiserver
不仅会验证用户身份,还会进行鉴权,即用户是否有该操作的权限kubectl
向apiserver
发送HTTP请求- Kubernetes 支持多个 API 版本,每个版本都在不同的 API 路径下
- 例如
/api/v1
或者/apis/extensions/v1beta1
- 不同的 API 版本表明不同的稳定性和支持级别
- 例如
- 一旦请求发送之后获得成功的响应,
kubectl
将会根据所需的输出格式打印 success message
- Kubernetes 支持多个 API 版本,每个版本都在不同的 API 路径下
apiserver
把请求根据YAML文件里的apiVersion
,kind
等字段通过routes匹配指定Handler- 然后Handler构造对应的API对象,并储存到
etcd
中
- 然后Handler构造对应的API对象,并储存到
controller manager
会一直watchapiserver
,如果有更新就会自动执行相应控制器scheduler
监听pod和node信息,选择适合的node,并将该信息写入etcd
- 过滤+打分的策略
kubelet
每隔一定时间向apiserver
通过NodeName
获取自身 Node 上所要运行的 Pod 清单- 会通过与自己的内部缓存进行比较来检测新增加的 Pod,如果有差异,就开始同步 Pod 列表
CRI
调用dockershim
,dockershim
把K8S指令转换为具体容器API发送给Docker Daemon- Docker Daemon首先创建
pause
容器, 然后启动initcontainer,最后再启动业务容器- 首先拉取容器的镜像
- 再把容器信息发送给 Dockerd 守护进程启动容器实例
- 如果 Pod 中配置了钩子(Hook),容器启动之后就会运行这些 Hook
pod停止过程
- 停止阶段可以利用
pod.spec.lifecycle.preStop
钩子进行一些收尾操作,以实现优雅停止- 在preStop之前,容器主进程不会受到
sigterm
指令,仍然可以正常服务
- 在preStop之前,容器主进程不会受到
- 同时会有强制关闭时限,时间到后K8S会发送
sigkill
指令强行关闭pod- 倒计时和
preStop
并行发生的,不会等待preStop
结束 - 可以通过
pod.spec.terminationGracePeriod
修改默认的30S
- 倒计时和
- Pod停止时iptables会被更新,但并不影响已经建立的连接
- 但是对于无连接的UDP,则后续流量无法到达
- 在pod停止过程中,由于K8S是以微服务方式部署,kubelet和endpoint是同时并行时操作的
- 这可能导致pod已经被杀死了,但是endpoint还来不及修改iptables
- 此时外部用户访问会报错,无法实现无感知热更新
- 解决方案是在preStop阶段等待5-10秒,保证APIserver发出的删除指令传播到了每个K8S组件
- 这可能导致pod已经被杀死了,但是endpoint还来不及修改iptables
pod设计模式:sidecar
-
在 Pod 里面,可以定义一些专门的容器,来执行主业务容器所需要的一些辅助工作
- 例如日志收集,debug,监控等
-
这种做法一个明显的优势就是在于其实将辅助功能从我的业务容器解耦了,能够独立发布 Sidecar 容器
- 很多与 Pod 网络相关的配置和管理,也都可以交给 sidecar 完成,而完全无须干涉用户容器
- 熔断、路由、服务发现、计量、流控、监视、重试、幂等、鉴权等控制面上的功能,以及其相关的配置更新,本质来上来说,和服务的关系并不大
- 但是传统的工程做法是在开发层面完成这些功能,这就会导致各种维护上的问题,而且还会受到特定语言和编程框架的约束和限制
-
更重要的是这个能力是可以重用的,即同样的一个监控 Sidecar 或者日志 Sidecar,可以被全公司的人共用的
- 最典型的例子莫过于 Istio 这个微服务治理项目了
-
进程间通讯机制是这个设计模式的重点,千万不要使用任何对应用服务有侵入的方式
- 可以通过信号的方式,或是通过共享内存的方式实现
- 最好的方式就是网络远程调用的方式,因为都在127.0.0.1上通讯,所以开销并不明显
-
服务协议
- 这里有两层协议,一个是 Sidecar 到 service 的内部协议,另一个是 Sidecar 到远端 Sidecar 或 service 的外部协议
- 对于内部协议,需要尽量靠近和兼容本地 service 的协议;对于外部协议,需要尽量使用更为开放更为标准的协议。但无论是哪种,都不应该使用与语言相关的协议
-
sidecar本质是把控制代码与逻辑(业务)代码分离
- Sidecar 中所实现的功能应该是控制面上的东西,而不是业务逻辑上的东西,所以不要把业务逻辑设计到 Sidecar 中
-
三种典型的sidecar用法:日志收集、代理容器、适配器
service mesh
-
微服务框架的问题
- 对于传统的微服务框架,如果框架本身出现了致命的bug或者框架新功能的实现需要整个依赖该框架调用链的服务全部升级,由于业务方众多,这种升级的推动是十分困难的
- 各个微服务使用的语言众多(go,java,c++,python),需要针对每个语言实现对应的微服务框架
- RPC通信协议多样化(thrift, grpc, http),微服务框架需要针对每种协议实现流量治理等功能
- 框架服务发现和治理等内容都位于业务进程内,与业务耦合严重,难以进行性能调优和问题排查
-
service mesh的优势:
- 容易兼容语言差异
- 基础设施可以独立于业务进行升级
- 无感知老服务改造
- 统一控制和治理,业务程序可以专注业务
-
Service Mesh 就像是网络七层模型中的第四层 TCP 协议。其把底层的那些非常难控制的网络通讯方面的控制面的东西都管了(比如:丢包重传、拥塞控制、流量控制),而更为上面的应用层的协议,只需要关心自己业务应用层上的事了。如 HTTP 的 HTML 协议
pod状态
- 对于包含多个容器的 Pod,只有它里面所有的容器都进入异常状态后,Pod 才会进入
Failed
状态。在此之前,Pod 都是Running
状态- Pod 的
READY
字段会显示正常容器的个数
- Pod 的
- pod状态(
pod.status.phase
)pending
: API对象已经被创建并保存在ETCD中,但容器还没全部被创建成功- condition字段表述具体状态,比如
unschedulabed
- condition字段表述具体状态,比如
running
: pod已经和节点绑定,容器都创建成功且至少有一个在运行- 未必能提供服务,可能在不断重启
- condition处于ready状态才能提供服务
succeeded
: pod内所有容器东运行完毕- 多见于job中
failed
: 至少有一个容器以非0返回码退出unknown
: pod状态未被kubelet汇报给kube-apiserver,可能是主从节点间的通信问题
健康检查
-
Readiness Probe
- 用来判断一个 pod 是否处在就绪状态(启动完成),当这个 pod 不处在就绪状态的时候,接入层会把相应的流量从这个 pod 上面移除
- Readiness 主要应对的是启动之后无法立即对外提供服务的这些应用
- 比如加载缓存数据,连接数据库等
- 多用于扩容和升级时
-
Liveness Probe
-
用来判断一个pod是否存活,如果不存活则直接杀掉pod,再根据重启策略判断是否重启
- 如果关心这个容器退出后的上下文环境,比如容器退出后的日志、文件和目录,请将 restartPolicy 设置为
Never
- 如果关心这个容器退出后的上下文环境,比如容器退出后的日志、文件和目录,请将 restartPolicy 设置为
-
因为一旦容器被自动重新创建,这些内容就有可能丢失掉了(被垃圾回收了)
-
Liveness 适用场景是支持那些可以重新拉起的应用(重启以实现自愈)
-
- 如果使用 tcpSocket 方式进行判断的时候,如果遇到了 TLS 的服务,那可能会造成后边 TLS 里面有很多这种未健全的 tcp connection,那这个时候需要自己对业务场景上来判断,这种的链接是否会对业务造成影响
资源保障和限制
-
可以设定的资源有:CPU,Mem, 储存, GPU等
-
通过
spec.container.reousrces
中的requests
和limits
字段设定- 最小单位为整数
- 在调度的时候,kube-scheduler 只会按照 requests 的值进行计算
- 设置 Cgroups 限制的时候,kubelet 则会按照 limits 的值来进行设置
-
QoS
- 最高优先级:
guaranteed
, 中等:burstable
, 最低:besteffort
- 用户本身无法指定优先级,只能通过request和limit的组合确定
- 当 Pod 里的每一个 Container 都同时设置了 requests 和 limits,并且 requests 和 limits 值相等的时候,这个 Pod 就属于
Guaranteed
类别- 当 Pod 仅设置了 limits 没有设置 requests 的时候,Kubernetes 会自动为它设置与 limits 相同的 requests 值
- 而当 Pod 不满足 Guaranteed 的条件,但至少有一个 Container 设置了 requests,那么这个 Pod 就会被划分到
Burstable
类别 - 如果一个 Pod 既没有设置 requests,也没有设置 limits,那么它的 QoS 类别就是
BestEffort
- 当 Pod 里的每一个 Container 都同时设置了 requests 和 limits,并且 requests 和 limits 值相等的时候,这个 Pod 就属于
- 最高优先级:
-
独占CPU的核
- 类似于在使用容器的时候通过设置
cpuset
把容器绑定到某个 CPU 的核上,而不是像 cpushare 那样共享 CPU 的计算能力 - 此时由于操作系统在 CPU 之间进行上下文切换的次数大大减少,容器里应用的性能会得到大幅提升
- 设置方式
- Pod 必须是
Guaranteed
的 QoS 类型 - 将 Pod 的 CPU 资源的 requests 和 limits 设置为同一个相等的整数值即可
- Pod 必须是
- 类似于在使用容器的时候通过设置