k8s拾遗

Author Avatar
Sean Yu 12月 28, 2020
  • 在其它设备中阅读本文章

接触k8s这么久,相对来说对于k8s的各种实用算是比较熟悉了,这里重读了一次《k8s权威指南》,查漏补缺,记录一下。

image.png

Kubernetes的服务发现机制

首先,每个Kubernetes中的Service都有唯一的Cluster IP及唯一的名称,而名称是由开发者自己定义的,部署时也没必要改变,所以完全可 以被固定在配置中。接下来的问题就是如何通过Service的名称找到对应的Cluster IP。

Kubernetes里的3种IP

  • Node IP: Node的IP地址。
    Node IP是Kubernetes集群中每个节点的物理网卡的IP地址, 是一个真实存在的物理网络,所有属于这个网络的服务器都能通过这个 网络直接通信,不管其中是否有部分节点不属于这个Kubernetes集群。 这也表明在Kubernetes集群之外的节点访问Kubernetes集群之内的某个节 点或者TCP/IP服务时,都必须通过Node IP通信。
  • Pod IP: Pod的IP地址。
    Pod IP是每个Pod的IP地址,它是Docker Engine根据docker0 网桥的IP地址段进行分配的,通常是一个虚拟的二层网络,前面说过, Kubernetes要求位于不同Node上的Pod都能够彼此直接通信,所以 Kubernetes里一个Pod里的容器访问另外一个Pod里的容器时,就是通过 Pod IP所在的虚拟二层网络进行通信的,而真实的TCP/IP流量是通过 Node IP所在的物理网卡流出的。(StatefulSet的pod ip可以保持重启不改变)
  • Cluster IP: Service的IP地址。
    Cluster IP,它也是一种虚拟的IP,但更像一 个“伪造”的IP网络,原因有以下几点。
    • Cluster IP仅仅作用于Kubernetes Service这个对象,并由 Kubernetes管理和分配IP地址(来源于Cluster IP地址池)。
    • Cluster IP无法被Ping,因为没有一个“实体网络对象”来响应。
    • Cluster IP只能结合Service Port组成一个具体的通信端口,单独 的Cluster IP不具备TCP/IP通信的基础,并且它们属于Kubernetes集群这 样一个封闭的空间,集群外的节点如果要访问这个通信端口,则需要做 一些额外的工作。
    • 在Kubernetes集群内,Node IP网、Pod IP网与Cluster IP网之间 的通信,采用的是Kubernetes自己设计的一种编程方式的特殊路由规 则,与我们熟知的IP路由有很大的不同。

volume 与 pv

Volume是被定义在Pod上的

PV可以被理解成Kubernetes集群中的某个网络存储对应的一块存储,它与Volume类似,但有以下区别。

  • PV只能是网络存储,不属于任何Node,但可以在每个Node上 访问。
  • PV并不是被定义在Pod上的,而是独立于Pod之外定义的。

如果某个Pod想申请某种类型的PV,则首先需要定义一个 PersistentVolumeClaim对象:

然后,在Pod的Volume定义中引用上述PVC即可

configmap

我们不可能在启动Docker容器后再修改容器里的配置文件,然后用新的配置文件重启容器里的用户主进程。为了解决这个问题,Docker提供了两种方式:

  • 在运行时通过容器的环境变量来传递参数;
  • 通过Docker Volume将容器外的配置文件映射到容器内。

首先,把所有的配置项都当作key-value字符串,当然value可以来自 某个文本文件,比如配置项password=123456、user=root、 host=192.168.8.4用于表示连接FTP服务器的配置参数。这些配置项可以 作为Map表中的一个项,整个Map的数据可以被持久化存储在 Kubernetes的Etcd数据库中,然后提供API以方便Kubernetes相关组件或 客户应用CRUD操作这些数据,上述专门用来保存配置参数的Map就是 Kubernetes ConfigMap资源对象。

接下来,Kubernetes提供了一种内建机制,将存储在etcd中的 ConfigMap通过Volume映射的方式变成目标Pod内的配置文件,不管目标Pod被调度到哪台服务器上,都会完成自动映射。进一步地,如果 ConfigMap中的key-value数据被修改,则映射到Pod中的“配置文件”也会随之自动更新。于是,Kubernetes ConfigMap就成了分布式系统中最为简单(使用方法简单,但背后实现比较复杂)且对应用无侵入的配置中心。

pod

Pod的状态

image.png

重启策略包括Always、OnFailure和Never,默认值为Always。

  • Always: 当容器失效时,由kubelet自动重启该容器。
  • OnFailure: 当容器终止运行且退出码不为0时,由kubelet自动 重启该容器。
  • Never: 不论容器运行状态如何,kubelet都不会重启该容器。

image.png

healthy check

  • LivenessProbe探针:用于判断容器是否存活(Running状态)
  • ReadinessProbe探针:用于判断容器服务是否可用(Ready状态)

为什么pod会被驱逐?

k8s根据qos(服务质量)决定是否要驱逐一个pod,根据实际经验来看,通常一个node上资源不足的时候,就会选择占用资源多的pod驱逐

qos有三种级别:

  • Best Effort
    Pod中所有容器都未定义资源配置(Requests和Limits都未定义)
  • Guaranteed
    Pod中的所有容器对所有资源类型都定义了Limits和Requests,并且所有容器的Limits值都和Requests值全部相等(且都不为0)
  • Burstable
    当一个Pod既不为Guaranteed级别,也不为BestEffort级别时,该Pod 的QoS级别就是Burstable

Best Effort 会最先被驱逐, Burstable 其次, Guaranteed 最后。

什么时候pod被oomkilled?

在可共享的资源耗尽时,pod被驱逐,不可被共享的资源耗尽时,pod被oomkilled(pod的资源大于limit)

升级和回滚–rolling update

image.png

hpa

k8s可以基于cpu和内存进行hpa,但是套用到jvm上可能不是很合适,可以通过普罗米修斯来检测指标,控制hpa

image.png

image.png

Kubernetes API Server

架构

  • API层: 主要以REST方式提供各种API接口
  • 访问控制层: 当客户端访问API接口时,访问控制层负责对用 户身份鉴权,验明用户身份
  • 注册表层: Kubernetes把所有资源对象都保存在注册表 (Registry)中。
  • etcd数据库:用于持久化存储Kubernetes资源对象的KV数据库。

image.png

List-Watch机制

以一个完整的Pod调度过程为 例,对API Server的List-Watch机制进行说明。

image.png

crd

Kubernetes中的CRD在API Server中的设计和实现机制。根据Kubernetes的设计,每种官方内建的资源对象如Node、 Pod、Service等的实现都包含以下主要功能。

  • 资源对象的元数据(Schema)的定义: 可以将其理解为数据库Table的定义,定义了对应资源对象的数据结构,官方内建资源对象 的元数据定义是固化在源码中的。
  • 资源对象的校验逻辑: 确保用户提交的资源对象的属性的合法性。
  • 资源对象的CRUD操作代码: 可以将其理解为数据库表的CRUD代码,但比后者更难,因为API Server对资源对象的CRUD操作都会保存到etcd数据库中,对处理性能的要求也更高,还要考虑版本兼容性和版本转换等复杂问题。
  • 资源对象相关的“自动控制器”(如RC、Deployment等资源对 象背后的控制器): 这是很重要的一个功能。因为Kubernetes是一个以自动化为核心目标的平台,用户给出期望的资源对象声明,运行过程中则由资源背后的“自动控制器”负责,确保对应资源对象的数量、状态、 行为都始终符合用户的预期

类似地,每个自定义CRD的开发人员都需要实现上面这些功能。为了减小编程的难度与工作量,API Server的设计者们做出了大量的努 力,使得上面前3个功能无须编程实现,直接编写YAML定义文件即可 实现。对于唯一需要编程的第4个功能来说,由于API Server提供了大量 的基础API库,特别是易用的List-Watch的编程框架,也使得CRD自动控 制器的编程难度大大减小。

CRD本身只是一段声明,用于定义用户自定义的资源对象。但仅有 CRD的定义并没有实际作用,用户还需要提供管理CRD对象的CRD控制 器(CRD Controller),才能实现对CRD对象的管理。CRD控制器通常 可以通过Go语言进行开发,并需要遵循Kubernetes的控制器开发规范, 基于客户端库client-go进行开发,需要实现Informer、 ResourceEventHandler、Workqueue等组件具体的功能处理逻辑。

创建CRD的定义

与其他资源对象一样,对CRD的定义也使用YAML配置进行声明。

CRD定义中的关键字段如下。

  • group: 设置API所属的组,将其映射为API URL中“/apis/”的 下一级目录,设置networking.
  • scope: 该API的生效范围,可选项为Namespaced(由 Namespace限定)和Cluster(在集群范围全局生效,不局限于任何 Namespace),默认值为Namespaced。
  • versions: 设置此CRD支持的版本,可以设置多个版本,用列表形式表示。
  • names: CRD的名称,包括单数、复数、kind、所属组等名称 的定义,可以设置如下参数。

基于CRD的定义创建自定义资源对象

基于CRD的定义,用户可以像创建Kubernetes系统内置的资源对象 (如Pod)一样创建CRD资源对象。

除了需要设置该CRD资源对象的名称,还需要在spec段设置相应的 参数。在spec中可以设置的字段是由CRD开发者自定义的,需要根据 CRD开发者提供的手册进行配置。这些参数通常包含特定的业务含义, 由CRD控制器进行处理。

然后,用户就可以像操作Kubernetes内置的资源对象(如Pod、 RC、Service)一样去操作CRD资源对象了,包括查看、更新、删除和 watch等操作。

CRD的高级特性

随着Kubernetes的演进,CRD也在逐步添加一些高级特性和功能, 包括subresources子资源、校验(Validation)机制、自定义查看CRD时需要显示的列,以及finalizer预删除钩子。

Kubernetes Proxy API

Kubernetes Proxy API接口,这类接口的作用是代理REST请求,即 Kubernetes API Server把收到的REST请求转发到某个Node上的kubelet守 护进程的REST端口,由该kubelet进程负责响应。

集群功能模块之间的通信

Kubernetes API Server作为集群的核心,负责 集群各功能模块之间的通信。集群内的各个功能模块通过API Server将 信息存入etcd,当需要获取和操作这些数据时,则通过API Server提供的 REST接口(用GET、LIST或W A TCH方法)来实现,从而实现各模块之 间的信息交互。

常见的一个交互场景是kubelet进程与API Server的交互。每个Node 上的kubelet每隔一个时间周期,就会调用一次API Server的REST接口报 告自身状态,API Server在接收到这些信息后,会将节点状态信息更新 到etcd中。此外,kubelet也通过API Server的Watch接口监听Pod信息, 如果监听到新的Pod副本被调度绑定到本节点,则执行Pod对应的容器创 建和启动逻辑;如果监听到Pod对象被删除,则删除本节点上相应的Pod 容器;如果监听到修改Pod的信息,kubelet就会相应地修改本节点的Pod 容器。

image.png

另一个交互场景是kube-controller-manager进程与API Server的交互。kube-controller-manager中的Node Controller模块通过API Server提供 的Watch接口实时监控Node的信息,并做相应处理。

还有一个比较重要的交互场景是kube-scheduler与API Server的交互。Scheduler通过API Server的Watch接口监听到新建Pod副本的信息后,会检索所有符合该Pod要求的Node列表,开始执行Pod调度逻辑, 在调度成功后将Pod绑定到目标节点上。

为了缓解集群各模块对API Server的访问压力,各功能模块都采用 缓存机制来缓存数据。各功能模块定时从API Server获取指定的资源对 象信息(通过List-Watch方法),然后将这些信息保存到本地缓存中, 功能模块在某些情况下不直接访问API Server,而是通过访问缓存数据 来间接访问API Server。

Controller Manager

每个Controller通过API Server提供的(List-Watch)接口实时监控集群中特定资源的状态变化,当发生各种故障导致某资源对象的状态发生变化时,Controller会尝试将其状态调整为期望的状态。比如当某个 Node意外宕机时,Node Controller会及时发现此故障并执行自动化修复 流程,确保集群始终处于预期的工作状态。

image.png

Replication Controller

不是我们日常创建pod所指的rc,Replication Controller是指“副本控制器”。

Replication Controller的核心作用是确保在任何时候集群中某个RC 关联的Pod副本数量都保持预设值。如果发现Pod的副本数量超过预期 值,则Replication Controller会销毁一些Pod副本;反之,Replication Controller会自动创建新的Pod副本,直到符合条件的Pod副本数量达到 预设值。需要注意:只有当Pod的重启策略是Always时 (RestartPolicy=Always),Replication Controller才会管理该Pod的操作(例如创建、销毁、重启等)。

总结一下Replication Controller的职责,如下所述。

  • 确保在当前集群中有且仅有N个Pod实例,N是在RC中定义的Pod副本数量。
  • 通过调整RC的spec.replicas属性值来实现系统扩容或者缩容。
  • 通过改变RC中的Pod模板(主要是镜像版本)来实现系统的滚动升级。

Node Controller

kubelet进程在启动时通过API Server注册自身的节点信息,并定时 向API Server汇报状态信息,API Server在接收到这些信息后,会将这些 信息更新到etcd中。在etcd中存储的节点信息包括节点健康状况、节点 资源、节点名称、节点地址信息、操作系统版本、Docker版本、kubelet 版本等。节点健康状况包含“就绪”(True)“未就绪”(False)和“未 知”(Unknown)三种。

Node Controller通过API Server实时获取Node的相关信息,实现管理 和监控集群中的各个Node的相关控制功能,Node Controller的核心工作 流程如图5.8所示。

image.png

ResourceQuota Controller

ResourceQuota Controller(资源配额管理),确保了指定的资源对象在任何时候都不会超量占用系统物理资源,避 免了由于某些业务进程的设计或实现的缺陷导致整个系统运行紊乱甚至 意外宕机,对整个集群的平稳运行和稳定性有非常重要的作用。

目前Kubernetes支持如下三个层次的资源配额管理。

  • 容器级别,可以对CPU和Memory进行限制。
  • Pod级别,可以对一个Pod内所有容器的可用资源进行限制。
  • Namespace级别,为Namespace(多租户)级别的资源限制, 包括:
    • Pod数量;
    • RC数量;
    • Service数量;
    • ResourceQuota数量;
    • Secret数量;
    • 可持有的PV数量。

Kubernetes的配额管理是通过Admission Control(准入控制)来控制 的,Admission Control当前提供了两种方式的配额约束,分别是 LimitRanger与ResourceQuota。其中LimitRanger作用于Pod和Container, ResourceQuota则作用于Namespace,限定一个Namespace里的各类资源的使用总额。

Namespace Controller

用户通过API Server可以创建新的Namespace并将其保存在etcd中, Namespace Controller定时通过API Server读取这些Namespace的信息。

如果Namespace被API标识为优雅删除(通过设置删除期限实现,即设置 DeletionTimestamp属性),则将该NameSpace的状态设置成Terminating 并保存到etcd中。同时Namespace Controller删除该Namespace下的 ServiceAccount、RC、Pod、Secret、PersistentVolume、ListRange、 ResourceQuota和Event等资源对象。

Service Controller与Endpoints Controller

本节讲解Endpoints Controller,在这之前,让我们先看看Service、 Endpoints与Pod的关系。如图5.10所示,Endpoints表示一个Service对应 的所有Pod副本的访问地址,Endpoints Controller就是负责生成和维护所 有Endpoints对象的控制器。

image.png

它负责监听Service和对应的Pod副本的变化,

  • 如果监测到Service被 删除,则删除和该Service同名的Endpoints对象。
  • 如果监测到新的Service 被创建或者修改,则根据该Service信息获得相关的Pod列表,然后创建 或者更新Service对应的Endpoints对象。
  • 如果监测到Pod的事件,则更新 它所对应的Service的Endpoints对象(增加、删除或者修改对应的 Endpoint条目)。

那么,Endpoints对象是在哪里被使用的呢? 答案是每个Node上的 kube-proxy进程,kube-proxy进程获取每个Service的Endpoints,实现了 Service的负载均衡功能。

Service Controller其实是属于Kubernetes集群与外部的云平台之间的一个接口控制器。Service Controller监听Service 的变化,如果该Service是一个LoadBalancer类型的 Service(externalLoadBalancers=true),则Service Controller确保在外部 的云平台上该Service对应的LoadBalancer实例被相应地创建、删除及更 新路由转发表。

Scheduler

Scheduler在整个系统中承担了“承上启下”的重要功 能,“承上”是指它负责接收Controller Manager创建的新Pod,为其安排 一个落脚的“家”—目标Node;“启下”是指安置工作完成后,目标Node上 的kubelet服务进程接管后继工作,负责Pod生命周期中的“下半生”。

Scheduler的作用是将待调度的Pod(API新创 建的Pod、Controller Manager为补足副本而创建的Pod等)按照特定的调 度算法和调度策略绑定(Binding)到集群中某个合适的Node上,并将 绑定信息写入etcd中。在整个调度过程中涉及三个对象,分别是待调度 Pod列表、可用Node列表,以及调度算法和策略。简单地说,就是通过 调度算法调度为待调度Pod列表中的每个Pod从Node列表中选择一个最 适合的Node。

随后,目标节点上的kubelet通过API Server监听到Kubernetes Scheduler产生的Pod绑定事件,然后获取对应的Pod清单,下载Image镜像并启动容器。

image.png

kubelet

kubelet服务进程。该进程用于处理Master下发到本节点的任务,管理 Pod及Pod中的容器。每个kubelet进程都会在API Server上注册节点自身 的信息,定期向Master汇报节点资源的使用情况,并通过cAdvisor监控 容器和节点资源。

节点管理

kubelet在启动时通过API Server注册节点信息,并定时向API Server 发送节点的新消息,API Server在接收到这些信息后,将这些信息写入 etcd。

Pod管理

kubelet通过以下几种方式获取自身Node上要运行的Pod清单。

  • 文件: kubelet启动参数“–config”指定的配置文件目录下的文 件(默认目录为“/etc/ kubernetes/manifests/”)。通过–file-check- frequency设置检查该文件目录的时间间隔,默认为20s。
  • HTTP端点(URL): 通过“–manifest-url”参数设置。通过– http-check-frequency设置检查该HTTP端点数据的时间间隔,默认为 20s。
  • API Server: kubelet通过API Server监听etcd目录,同步Pod列表。

所有以非API Server方式创建的Pod都叫作Static Pod。kubelet将 Static Pod的状态汇报给API Server,API Server为该Static Pod创建一个 Mirror Pod和其相匹配。Mirror Pod的状态将真实反映Static Pod的状态。 当Static Pod被删除时,与之相对应的Mirror Pod也会被删除。在本章中 只讨论通过API Server获得Pod清单的方式。

kubelet通过API Server Client 使用Watch加List的方式监听“/registry/nodes/$”当前节点的名称和“/registry/pods”目录,将获取的信息同步到本地缓存中。

kubelet监听etcd,所有针对Pod的操作都会被kubelet监听。如果发现
有新的绑定到本节点的Pod,则按照Pod清单的要求创建该Pod。

如果发现本地的Pod被修改,则kubelet会做出相应的修改,比如在
删除Pod中的某个容器时,会通过Docker Client删除该容器。

如果发现删除本节点的Pod,则删除相应的Pod,并通过Docker
Client删除Pod中的容器。

kubelet读取监听到的信息,如果是创建和修改Pod任务,则做如下
处理。

  • 为该Pod创建一个数据目录。
  • 从API Server读取该Pod清单。
  • 为该Pod挂载外部卷(External Volume)。
  • 下载Pod用到的Secret。
  • 检查已经运行在节点上的Pod,如果该Pod没有容器或Pause容 器(“kubernetes/pause”镜像创建的容器)没有启动,则先停止Pod里所 有容器的进程。如果在Pod中有需要删除的容器,则删除这些容器。
  • 用“kubernetes/pause”镜像为每个Pod都创建一个容器。该Pause 容器用于接管Pod中所有其他容器的网络。每创建一个新的Pod,kubelet 都会先创建一个Pause容器,然后创建其他容器。“kubernetes/pause”镜像 大概有200KB,是个非常小的容器镜像。
  • 为Pod中的每个容器做如下处理。
    • 为容器计算一个Hash值,然后用容器的名称去查询对应 Docker 容器的Hash值。若查找到容器,且二者的Hash值不同,则停止Docker中 容器的进程,并停止与之关联的Pause容器的进程;若二者相同,则不做任何处理。
    • 如果容器被终止了,且容器没有指定的restartPolicy(重启策略),则不做任何处理。
    • 调用Docker Client下载容器镜像,调用Docker Client运行容器。

创建pod的全过程:

image.png

image.png

cAdvisor

kubelet作为连接Kubernetes Master和各Node之间的桥梁,管理运行在Node上的Pod和容器。kubelet将每个Pod都转换成它的成员容器,同时从cAdvisor获取单独的容器使用统计信息,然后通过该REST API暴露 这些聚合后的Pod资源使用的统计信息。

在新的Kubernetes监控体系中,Metrics Server用于提供Core Metrics(核心指标),包括Node和Pod的CPU和内存使用数据。其他 Custom Metrics(自定义指标)则由第三方组件(如Prometheus)采集和 存储。

kube-proxy

在很多情况下,Service只是一个概念,而真正将Service的作用落实的是它背后的kube-proxy服务进程。

在Kubernetes集群的每个Node上都会运行一个kube-proxy服务进程,我们可以把这个进程看作Service的透明代理兼负载均衡器,其核心功能是将到某个Service的访问请求转发到后端的多个Pod实例上。

Service的Cluster IP与NodePort等概念是kube-proxy服务通过iptables的NA T转换实现的,kube-proxy在运行过程中动态创建与Service相关的iptables规则,这些规则实现了将访问服务(Cluster IP或NodePort)的请求负载分发到后端Pod的功能。由于iptables机制针对的是本地的kube-proxy端口,所以在每个Node上都要运行kube-proxy组件,这样一来,在Kubernetes集群内部,我们可以在任意Node上发起对Service的访问请求。综上所述,由于kube-proxy的作用,在Service的调用过程中客户端无须关心后端有几个Pod,中间过程的通信、负载均衡及故障恢复都是透明的。

image.png

iptables模式虽然实现起来简单,但存在无法避免的缺陷: 在集群中 的Service和Pod大量增加以后,iptables中的规则会急速膨胀,导致性能 显著下降,在某些极端情况下甚至会出现规则丢失的情况,并且这种故 障难以重现与排查,于是Kubernetes从1.8版本开始引入第3代的 IPVS(IP Virtual Server)模式。IPVS专门用于高性能负载均衡,并使用更高效的数据结构(Hash表),允许几乎无限的 规模扩张。

在IPVS模式下,kube-proxy又做了重要的升级,即使用 iptables的扩展ipset,而不是直接调用iptables来生成规则链。

关于 iptables: https://luffyao.com/2019/12/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3iptables%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/

iptables规则链是一个线性的数据结构,ipset则引入了带索引的数据 结构,因此当规则很多时,也可以很高效地查找和匹配。我们可以将 ipset简单理解为一个IP(段)的集合,这个集合的内容可以是IP地址、 IP网段、端口等,iptables可以直接添加规则对这个“可变的集合”进行操 作,这样做的好处在于可以大大减少iptables规则的数量,从而减少性能损耗。

假设要禁止上万个IP访问我们的服务器,则用iptables的话,就需要 一条一条地添加规则,会在iptables中生成大量的规则;但是用ipset的 话,只需将相关的IP地址(网段)加入ipset集合中即可,这样只需设置 少量的iptables规则即可实现目标。

关于service的工作原理详解,看这里:https://xigang.github.io/2019/07/21/kubernetes-service/

简单来说就是etcd中维护了service的selector关系,port与target port映射,以及对应的pod ip,kube proxy会根据这些去改写iptables,外部流量会通过service的node port,进入iptables,开始流量的规则路由。