在 k8s 生产话落地时,有许多问题需要考虑,问题主要分为两大类:
计算节点相关:
控制平面相关:
改选哪种类型的操作系统来部署 k8s,市面上的操作系统主要分为两大类:
通用操作系统:
专为容器优化的最小化操作系统:
这么多种类的操作系统,改选择哪一种,主要从以下方面进行评估和选择:
生态系统与成熟度
通用操作系统相较于容器化操作系统的优势就是成熟,但是容器化也有其优势所在:
为了保证生产环境的稳定,生产环境的很多基础设施应该是不可变的,不可变基础设施包括:不可变的容器镜像,不可变的主机操作系统等,可变意味着风险。
可变基础设施的风险
其中 Atomic 操作系统就是一个不可变操作系统,它是由 Red Hat 支持的软件包安装系统,支持多种 Distro: Fedora, CentOS, RHEL,其优势如下:
在云原生环境下,追求最小化操作系统的原则就是只安装必要的工具,这里的必要指的是支持系统运行的最小工具集。任何调试工具,比如性能排查,网络排查工具,均可以后期以容器形式运行,这样就可以保证操作系统的性能和稳定性,以及减少漏洞,安全保障。
操作系统构建流程
社区有一个一个的rpm的repo,会在rpm的repo基础之上,创建自己的rpm的snapshot,我们在公司会去拉取社区rpm的镜像,然后构建我们自己的snapshot,这个snapshot除了镜像之外,还会去看社区有哪些patch,以及生产系统发现的问题,这些问题修复一并会通过rpm rebuilder变成一个rpm包,放在rpm的snapshot里面。
这样其实我们自己维护了一套rpm的repo,有了这个repo之后,有个工具叫做rpm-ostree,这个工具做的事情就是将rpm包编译成一个ostree的格式。
我们追求的是最小的操作系统镜像,所以这里面只安装最核心的服务,需要辅助的工具怎么办?这里面有buildah,它的输出就是docker image。
所以这里面会构建两种,一种是基本的操作系统镜像,通过ostree保持最小的集合,一些utility一些辅助的工具,我们通过docker build构建成docker的镜像,提供后续做整个操作系统的问题排查。
有了ostree之后,它可以通过http的形式暴露出来,那这里面会有两条线,一条就是裸金属的这条线,我们启的是bare mental,bare mental里面有kick start,kick start可以直接去调用ostree直接加载这个操作系统,完成操作系统的启动。
ostree这种模式对于虚拟机来说,是不能完全支持的,所以还有另外一条线,对于虚拟化的技术栈上面我们会有packer builder,通过这个builder将ostree构建成一个一个的存在glance的一个一个操作系统镜像,openstack在启动虚拟机的时候会去读取这些镜像,然后将虚拟机启动起来。
所以以用户最后在使用的时候,无论是裸金属,还是虚拟机,它的感受是一样的。
kubelet 周期性地向 API Server 进行汇报,并更新节点的相关健康和资源使用信息。
计算节点在保证容器有足够资源运行的同时,也得保证支撑节点正常运行的进程有足够的资源,比如 systemd、journald、sshd、dockerd、kubelet 等。
为了使这些服务能够正常运行,需要为其预留足够的资源,kubelet 通过众多启动参数为系统预留 CPU、内存、PID等资源,比如 SystemReserved、KubeReserved等。
kubelet 会获取当前节点的资源容量(Capacity)并计算资源可分配额(Allocatable):
Allocatable 是用户 Pod 可用的资源,是资源容量减去分配给系统的资源的剩余部分。
kubelet 会在系统资源不够时中止一些容器进程,以空出系统资源,保证节点的稳定性。
kubelet 的驱逐只停止 Pod 所有容器进程,并不会直接删除 Pod。
Pod 的 status.phase
会被标记为 Failed, status.reason
会被设置为 Evicted,status.message
则会记录被驱逐的原因。
kubelet 依赖内嵌的开源软件 cAdvisor 周期性检查节点资源使用情况,当不可用资源(内存,磁盘等)紧张时,将按照一定的驱逐策略进行驱逐。
驱逐策略
kubelet 获取节点的可用额信息后,会结合节点的容量信息来判断当前节点运行的 Pod 是否满足驱逐条件,驱逐条件可以是绝对值或百分比,当监控资源的可使用额少于设定的驱逐条件时,就会发起驱逐。
为了防止小资源被多次回收,可以通过参数 evictionMinimumReclaim
设置每次回收资源的最小值。
驱逐又分为软驱逐和硬驱逐:
基于内存压力的驱逐
memory.available
表示当前系统的可用内存情况,kubelet 默认设置了 memory.available<100Mi 的硬驱逐条件。
当发生驱逐时,kubelet 会将节点的 MemoryPressure
状态设置为 True, 调度器会阻止 BestEffort Pod 调度到内存承压的节点。
当内存不足时,kubelet 将按照以下策略按顺序驱逐 Pod:
基于磁盘压力的驱逐
以下任何一项满足驱逐条件时,它会将节点的 DiskPressure 状态设置为 True, 调度器不会再调度任何 Pod 到该节点上。
有容器运行时分区的驱逐行为:
无容器运行时分区驱逐行为:
回收已经退出的容器和未使用的镜像后,如果节点依然满足驱逐条件,kubelet 就会开始驱逐正在运行的 Pod, 进一步释放磁盘空间。
kubelet 按照以下驱逐顺序:
通过设置 Pod 的 request 和 limit 参数,分为三种不同的 QoS Class: BestEffort, Burstable, Guaranteed。
这三个参数会计算出 Pod 所需的资源限制,然后设置容器和Pod 对应的 Cgroup。
关于Cgroup 与 容器的关系可以看我之前写的这篇文章:
Docker与Linux之间的关系——Namespace,Cgroups, 网络通信总结
CPU Cgroup 配置
内存 Cgroup 配置
OOM Killer 行为
当k8s 节点发生故障时,仅依靠 k8s 本身的服务组件是无法发现的,比如:
为了解决这个问题,社区引入了守护进程 node-problem-detector,从各个守护进程收集节点问题,并且上报给上游服务组件。
k8s 中的故障主要分为三大类,系统故障,用户自定义监控故障,健康检查故障:
当节点发生故障时,node-problem-detector 会通过设置 NodeCondition 或者创建 Event 对象来汇报问题。
但是NPD 只修改 node condition 对象,并不会对节点状态和调度产生影响,也就是说当节点发生故障时,NPD 只负责上报,不负责处理。
要想驱逐异常节点上的Pod 以及阻止 Pod 调度到故障节点,需要自定义控制器,监听 NPD 汇报的 condition, taint node 信息,进行处理。
当节点或者Pod 中的容器异常时,可以通过下边的方法进行排查。
除了像 CPU,内存这种必备的节点资源被 k8s 内置监控外,用户也可以自定义扩展资源,并且在像使用内置监控资源一样在定义 Pod 的时候进行限制,需要注意的是自定义扩展资源无法使用 kubernetes.io 作为资源域名。
常见的扩展资源包括:
定义扩展资源:
k8s 集群在生产化落地有许多需要注意的地方,第一步就是如何搭建高可用的集群,搭建高可用集群需要考虑许多方面的内容,比如说基础架构层的容灾与安全,如何高效的与企业自身的公共服务集成,如何搭建高可用的数据层和控制层,应用开发者与集群管理员需要关注的方向点有哪些等等。
高可用的数据中心
Node 的生命周期管理
主机管理
生产化集群管理
目前集群的搭建主要由四种方式:
kubespray 就是利用 kubeadmin 和 ansible 将对应的搭建命令给自动化了,缺点就是由于网络等问题会导致中间经常出现部署超时等错误,需要不断的重试。
搭建步骤可以参考这篇文章:
kubespray部署高可用K8s集群
使用 Kubespray 安装 Kubernetes