目录

  • 整体架构
  • 控制面
    • kube-apiserver
      • 访问控制
      • 通知
    • kube-scheduler
      • 概述
      • 默认调度策略
    • kube-controller-manager
    • etcd
      • 架构
      • Raft协议
        • 日志复制
  • 数据面
    • kubelet
    • kube-proxy

整体架构

  • 集群架构图

控制面

  • 控制面是kubernetes的核心组件,负责管理和控制集群的整体行为,它包含了一系列的控制器和管理组件,用于处理集群的配置、调度、伸缩、监控等任务
  • 控制面的控制器都运行在主节点上

kube-apiserver

Kubernetes API 服务器验证并配置 API 对象的数据, 这些对象包括 pods、services、replicationcontrollers 等。 API 服务器为 REST 操作提供服务,并为集群的共享状态提供前端, 所有其他组件都通过该前端进行交互。

  • kubernetes api服务器作为核心组件,其他组件或者客户端(如kubectl等)都会去调用它。它以restful API的形式提供了可以查询、修改集群状态的CRUD接口。它将状态存储到etcd中。它提供了一种一致的方式将对象存储到etcd中,并会对这些对象进行校验,这样客户端就无法存入非法的对象了,校验逻辑位于源码的pkg/apis/apps/validation/validation.go文件中
  • kube-apiserver是唯一能够直接和etcd进行通信的api服务器,所有其他组件都是通过api服务器间接的读取、写入数据到etcd,这样的好处第一是可以使用乐观锁验证系统的健壮性,第二是将实际的存储机制从其他组件抽离,未来替换起来也更加容易

访问控制

  • 通过认证插件认证客户端。认证插件是在API服务器上配置的,API服务器会轮流调用这些插件,直到有一个能够确认是谁发送了该请求。这是通过检查HTTP请求来实现的,例如Authorization标头
  • 通过授权插件授权客户端。这也是在API服务器上配置的,授权插件的主要作用是决定认证的用户是否可以对请求资源执行请求操作。例如,在创建pod的时候,API服务器会轮询所有的授权插件,来确认该用户是否可以在请求命名空间创建pod,当插件确认了用户可以执行该操作的时候,API服务器才会执行下一步操作
  • 通过准入控制插件验证或修改资源请求。这只会发生在涉及到编辑和删除等危险操作时候,如果仅仅是读取数据,则不会做准入控制的验证。例如AlwaysPullImages、ServiceAccount等

通知

  • API服务器做的事情就是启动一些控制器,以及其它一些组件来监控已部署资源的变更,它没有告诉它们应该去做什么。客户端通过创建到API服务器的HTTP连接来监听变更,通过此连接,客户端会接收到监听对象的一系列变更通知,每当对象更新,服务器把新版本对象发送至所有监听该对象的客户端,流程如下图所示

kube-scheduler

Kubernetes 调度器是一个控制面进程,负责将 Pods 指派到节点上。 调度器基于约束和可用资源为调度队列中每个 Pod 确定其可合法放置的节点。 调度器之后对所有合法的节点进行排序,将 Pod 绑定到一个合适的节点。 在同一个集群中可以使用多个不同的调度器;kube-scheduler 是其参考实现。

概述

  • 我们通常不会去指定pod应该运行在哪个集群节点上,这项工作是由调度器完成的。宏观上看,调度器的操作比较简单,就是利用API服务器的监听机制等待新创建的pod,然后给每个新的、没有节点集的pod分配节点
  • 调度器需要做的是通过API服务器更新pod的定义,然后API服务器去通知kubelet该pod已经被调度过,当目标节点上的kubelet发现该pod被调度到本节点,它就会创建并且运行pod的容器
  • 尽管宏观上调度的过程比较简单,但是实际上为pod选择最佳节点的任务并不简单,有很多选择节点的策略,最简单的选择节点的方式是不关心节点上已经运行的pod,随机选择一个节点;一些复杂的调度算法如机器学习,会预测接下来一段时间哪种类型的pod将会被调度,然后以最大的硬件利用率、无需重新调度已经运行的pod的方式来调度。kubernetes的默认调度器实现方式处于最简单和最复杂的程度之间

默认调度策略

  1. 过滤所有节点,找出能分配给pod的可用节点列表
  2. 对所有可用节点按照优先级排序,找出最优节点。如果多个节点都有最高的优先级分数,那么则循环分配,确保平均分配给pod
  • 为了查找哪些节点可用,控制器会下发一组配置好的预测函数,这些函数会检查下面的内容
  1. 节点是否能满足pod对硬件资源的请求
  2. 节点是否耗尽资源
  3. pod是否要求被调度到指定节点
  4. 节点是否有和pod规格定义里的节点选择器一致的标签
  5. 如果pod要求绑定指定的主机端口,那么这个节点上的这个端口是否已经被占用
  6. 如果pod要求有特定类型的卷,该节点是否能为此pod加载此卷,或者说该节点上是否已经有pod在使用该卷了
  7. pod是否能容忍节点的污点
  8. pod是否定义了节点、pod的亲和性以及反亲和性规则?如果是,那么调度节点给该pod是否会违反规则
  • 如果上述检查均通过,得到的节点是可行节点,对所有节点进行检查之后,会得到节点集的一个子集
  • 尽管这些节点都能够满足要求,但是其中的一些还是可能优于另外一些。假设某个集群有两个节点A,BA,B A,B都满足要求,但是AA A节点运行了10个pod,BB B节点一个pod都没有运行,显然调度器选择BB B节点是更优的;假设这两个节点是云平台提供的服务,应该将pod调度给节点AA A,将节点BB B释放回云服务商以节省资金。所以不同的场景下可能采用不同的策略,这就需要使用pod的高级调度
  • 默认情况下,归属同一服务和ReplicaSet的pod会分散在不同的节点上。但是并不保证每次都是这样,不过可以通过定义pod的亲和性或反亲和性规则强制pod分散在集群内或者集中在一起

kube-controller-manager

Kubernetes 控制器管理器是一个守护进程,内嵌随 Kubernetes 一起发布的核心控制回路。 在机器人和自动化的应用中,控制回路是一个永不休止的循环,用于调节系统状态。 在 Kubernetes 中,每个控制器是一个控制回路,通过 API 服务器监视集群的共享状态, 并尝试进行更改以将当前状态转为期望状态。 目前,Kubernetes 自带的控制器例子包括副本控制器、节点控制器、命名空间控制器和服务账号控制器等。

  • 控制器做了许多不同的事情,但是它们都是通过API服务器监听资源变更,总的来说,控制器执行一个“调和”循环,将实际状态调整为期望状态,这个期望状态是在资源的spec部分定义的,然后将新的实际状态写入资源的status部分。控制器利用监听机制来订阅变更
  • 每个控制器都是独立存在的,但是它们之间不会互相通信,它们都会连接到API服务器,通过监听机制,请求订阅该控制器负责的一系列资源的变更

etcd

  • etcd是使用go语言编写的分布式、高可用的一致性键值存储系统系统,用于提供可靠的分布式键值存储、配置共享和服务发现等功能
  • etcd基于raft协议,通过复制日志文件的方式来保证数据的强一致性。当客户端写一个key的时候,首先会存储到etcd的leader上,然后再通过raft协议复制到etcd集群的所有成员中,以此维护各成员状态的一致性以实现可靠性。虽然etcd是一个强一致的系统,但是也支持从非leader节点读数据以提高性能,但是写操作仍然需要leader的支持,所以当发生网络分区的时候,写操作可能失败
  • etcd默认数据一更新就落盘持久化,数据持久化存储使用WAL(write ahead log, 预写式日志)格式,WAL记录了数据变化的全过程,在etcd中所有数据在提交之前都要先写入WAL中;etcd的snapshot文件则存储了某一时刻etcd的所有数据,默认设置为每10000条记录做一次快照,经过快照后WAL文件即可删除

架构

  • 网络层:提供网络数据读写功能,监听服务端口,完成集群节点之间数据通信,收发客户端数据
  • Raft模块:Raft强一致性算法的具体实现
  • 存储模块:涉及KV存储、WAL文件,Snapshot管理等,用于处理etcd支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等,是etcd对用户提供的大多数API功能的具体实现
  • 复制状态机:这是一个抽象的模块,状态机的数据维护在内存中,定期持久化到磁盘,每次写请求都会持久化到WAL文件,并根据写请求的内容修改状态机数据。除了在内存中存有所有数据的状态以及节点的索引之外,etcd还通过WAL进行持久化存储。基于WAL的存储系统其特点就是所有的数据在提交之前都会事先记录日志。Snapshot是为了防止数据过多而进行的状态快照。

Raft协议

  • etcd实现一致性的核心是Raft协议,它采用的是非对称节点关系模型,也就是基于选主模型,只有主节点具有决策权。任意时刻有且仅有一个主节点,客户端只与主节点进行交互。在一个Raft协议组织的集群中,一共包含3类角色,leader(领导人),Candidate(候选人),Follower(群众)
  • 算法大致流程是这样的。首先开始一轮大选,所有群众都成为了候选人,当其中一个候选人得到了半数以上群众的选票,他就成为了领导人,开始一个新的任期(Term),其他候选人成为群众,接受领导人的领导;那么如果没有任何人得到半数以上的选票,本次任期就会以没有选出领导人而结束,从而开启下一个任期,开始一次新的选举。Raft算法保证在给定的一个任期内最多只有一个领导人
  • 上面只是简单的流程,下面进行详细的讨论
  • 在Raft算法中,任期号可以识别领导人是否合法,因为合法的任期号一定是大于等于其他候选者的任期号的,Leader在任期内需要定时向集群内其他节点广播心跳包,昭告自己的存在,当Follower接收到心跳包之后就会把自己的选举定时器清零重置,如果在规定的一个选举超时时间内Follower没收到来自Leader的心跳包,这时候Follower就会假定Leader已经不存在或者发生了故障,就会发起一次新的选举,因此Leader广播时间必须短于选举定时器的超时时间,否则就会频繁发生选举,切换Leader
  • 那么这就产生了一个问题,如果频繁出现平票的情况怎么办?没有Leader的话是无法提供服务的,Raft算法提出了randomized election timeouts的概念,也就是当发生平票的时候,对每个群众在某个时间区间内(150ms~300ms)随机设置一个超时时间,这样就可以在大多数情况下只有一个节点率先超时,赢得选举,并向其他节点发送心跳信息
日志复制
  • 当Leader接收客户端的请求的时候,每个请求都会解析成一个需要复制状态机执行的命令,然后Leader把这个作为一条日志加入到他的日志文件中,然后并行的向其他Raft节点发起AppendEntries RPC,,要求其他节点复制这个日志条目,当其他节点都“安全”的复制之后,Leader才会执行这条指令到状态机中,并向客户端返回结果,如果Follower没响应这个RPC,Leader会无限重试(甚至在响应给客户端之后),直到所有的Follower存储了和Leader一样的日志条目
  • 每条日志有三个字段,整数索引(log index),任期号(term)和指令(command)。节点上的日志有一个性质,如果不同的日志中某两个条目有相同的索引和任期号,说明这两个条目内容相同,且它们之前的所有条目都完全一样
  • 领导人选举的时候,选出的领导人一定是拥有之前任期提交的全部日志条目的节点,否则就会出现日志丢失,所以Raft算法使用投票的方式来阻止那些没有包含所有已提交日志条目的节点赢得选举,如果候选人的日志比集群内的大多数节点上的日志更加新,那么它一定包含所有已经提交的日志条目。因此在RequestVote RPC的接收方有一个检查,如果他自己的日志比候选人的日志更加新,那么它就会拒绝候选人的投票请求

数据面

  • 数据面是kubernetes集群中的工作节点,负责运行和执行应用程序的容器
  • 数据面的组件都运行在工作节点上,工作节点也就是实际pod容器运行的地方

kubelet

  • 简单来说,kubelet就是负责所有运行在工作组件上内容的组件,它的第一个任务就是在API服务器中创建一个Node资源来注册该节点。然后需要持续监控API服务器是否把该节点分配给pod,然后启动pod容器,具体实现方式是告知配置好的容器运行时来从特定容器镜像运行容器。kubelet随后持续监控运行的容器,向API服务器报告它们的状态、事件和资源消耗
  • kubelet也是运行容器存活探针的组件,当探针报错时它会重启容器。最后一点,当pod从API服务器删除时,kubelet终止容器,并告知API服务器pod已经被终止了
  • 但是kubelet也提供了一种不经过api服务器,由kubelet根据本地指定目录下的pod清单来运行和管理pod的方法,具体参考kubelet --pod-manifest-path参数用法,这样生成的pod叫做静态pod,因为它不会获取任何来自API服务器的更新或调度信息,绑定到kubelet上,并由kubelet控制

kube-proxy

Kubernetes 网络代理在每个节点上运行。网络代理反映了每个节点上 Kubernetes API 中定义的服务,并且可以执行简单的 TCP、UDP 和 SCTP 流转发,或者在一组后端进行 循环 TCP、UDP 和 SCTP 转发。 当前可通过 Docker-links-compatible 环境变量找到服务集群 IP 和端口, 这些环境变量指定了服务代理打开的端口。 有一个可选的插件,可以为这些集群 IP 提供集群 DNS。 用户必须使用 apiserver API 创建服务才能配置代理。

  • 除了kubelet,每个工作节点上还会运行kube-proxy,用于确保客户端可以通过kubernetes API连接到你定义的服务。kube-proxy确保对服务IP和端口的连接最终能到达支持服务的某个pod处。如果有多个pod支撑同一个服务,那么代理会发挥对pod的负载均衡作用
  • 当在API服务器中创建一个服务的时候,虚拟IP地址会立刻分配给它,之后的很短时间内,API服务器会通知所有运行在工作节点上的kube-proxy客户端有一个新服务已经被创建了。然后每个kube-proxy都会让该服务在自己的运行节点上可寻址,具体原理是通过建立一些iptables规则,确保每个目的地为服务的IP/端口对的数据包被解析,目的地址被修改,这样数据包就会被重定向到支持服务的一个pod