Kubernetes 核心概念

云原生与微服务

云原生的定义中包含微服务、容器、服务网格、不可变基础设施和声明式 API 等代表技术,构建云原生应用主要依靠这些关键技术。

需要区分的是,云原生更侧重应用程序的运行环境,它是以 Kubernetes 和容器为基础的云环境;而微服务架构对应的是应用程序的软件架构。两者处在不同层面,只是在实践中往往同时出现。

微服务的优势

  1. 敏捷开发:微服务可以让服务更敏捷地开发上线,符合互联网业务快速迭代的特征。
  2. 技术自由:不需要局限在同一种语言。
  3. 弹性扩缩容:微服务可以灵活地应对服务的扩缩容问题。
  4. 组织匹配:微服务架构可以非常好地与组织架构相匹配。

Service Mesh 核心组件

服务拆分之后,服务间通信、配置、治理都需要基础设施来兜底。Service Mesh 把这些能力从业务代码中下沉出来,通常由以下组件构成。

  1. 服务注册中心:服务间通信的基础组件。服务注册自身节点,让调用方发现被调方的服务节点,以达到服务间点对点通信的目的。
  2. 配置中心:用于服务的基础配置更新,达到代码和配置分离的目的,减少服务的发布次数,让配置变更更快更及时。
  3. API 网关:通过统一的网关层收敛鉴权、链路 ID 生成等基础服务,并聚合后端服务为客户端提供 RESTful 接口;同时负责南北向流量(外网入口流量)的流量治理。
  4. 服务治理:通过限流、熔断等基础组件,杜绝微服务架构出现雪崩的隐患。
  5. 链路追踪:通过 trace 把整个微服务链路清晰地绘制出来,实现精准的故障排查,极大降低故障排查难度。
  6. 监控告警:通过 Prometheus 和 Grafana 这类基础组件绘制服务状态监控大盘,针对资源、服务、业务各项指标做精准的监控报警。

可观测性

可观测性是通过系统向外部输出信息,来检测系统内部的运行状态,主要包括日志、监控指标和链路追踪三部分。在 Kubernetes 中,日志一般用 EFK(Elasticsearch、Fluentd、Kibana)收集并分析,监控指标一般用 Prometheus 采集、配合 Grafana 展示,这两项都可以做到对业务代码无侵入。

链路追踪则是三者中唯一无法完全做到无侵入的一环,Istio 也不例外。网格代理 Envoy 能够自动识别 trace 中的 header,在请求 upstream 时自动生成 span 并携带发送;但要把整个链路的追踪信息串联起来,仍需要在代码中额外透传一部分链路追踪信息。

Istio 规定需要携带以下 header。

1
2
3
4
5
6
7
x-request-id
x-b3-traceid
x-b3-spanid
x-b3-parentspanid
x-b3-sampled
x-b3-flags
x-ot-span-context

拿到这些信息后,再通过 Jaeger 的类库等工具进行 trace 分析。

Kubernetes 架构与组件

Kubernetes 集群由 Master 与 Node 两类角色组成,Master 负责决策与状态管理,Node 负责真正运行容器。

Kubernetes 架构与网络

Master 组件

  • API Server:提供 HTTP/HTTPS RESTful API,即 Kubernetes API,是集群的前端接口。各种客户端以及 Kubernetes 其他组件都通过它管理集群的各种资源。
  • Scheduler:负责决定将 Pod 放在哪个 Node 上运行。调度时会充分考虑集群的拓扑结构、当前各节点的负载,以及应用对高可用、性能、数据亲和性的需求。
  • Controller Manager:负责管理集群各种资源,保证资源处于预期的状态。为满足不同业务场景,其中实现了多种 Controller。
  • etcd:负责保存集群的配置信息和各种资源的状态信息。数据变化时 etcd 通过 watch 通知 API Server,其他组件再从 API Server 上 watch 这些变化——etcd 只与 API Server 交互,它并不认识其余组件。

Node 组件

  • kubelet:运行在集群所有节点上,负责启动 Pod 和容器,是 Node 上的 agent。当 Scheduler 确定在某个 Node 上运行 Pod 后,会把 Pod 的具体配置信息(image、volume 等)发送给该节点的 kubelet,kubelet 依此创建并运行容器,并向 Master 报告运行状态。kubelet 会在 API Server 上注册当前工作节点,定期汇报节点资源使用情况,并通过 cAdvisor 监控容器和节点的资源占用状况;它还会定时向 API Server 汇报心跳,一段时间内心跳没有更新,controller manager 就会把该节点的 Ready condition 置为 Unknownkubectl get nodes 显示 NotReady),并把它上面的 Pod 标记为 NodeLost,随后按驱逐策略处理。
  • kube-proxy:每个节点上运行的网络代理,实现 Service 概念的一部分。它监听 API Server 中 Service 和 Endpoint 的变化情况,并通过 iptables 或 ipvs 为服务配置负载均衡(仅支持 TCP 和 UDP)。
  • Container Runtime:负责运行容器的软件。Kubernetes 支持多种容器运行时,如 containerd、CRI-O 以及任何实现了 CRI 的运行时;Docker 自 1.20 起被标记弃用,内置的 dockershim 在 1.24 中已正式移除。
  • Pod 网络:以 CNI 插件形式提供,实现 Pod 间的相互通信,常见实现有 flannel、calico。
  • CoreDNS(早期为 kube-dns):为集群提供 DNS 服务,是 Pod 内使用的 DNS 服务器,执行 kubeadm init 时作为附加组件安装。

查看 kube-proxy 当前的工作模式。

1
curl 127.0.0.1:10249/proxyMode

集群运行模式

Kubernetes 的各组件可以有三种部署形态。

  1. 独立组件模式:系统各组件直接以守护进程的方式运行于节点之上,各组件相互协作构成集群。

    独立组件模式

  2. 静态 Pod 模式:除 kubelet 和容器运行时之外的其他组件(etcd、kube-apiserver、kube-controller-manager、kube-scheduler 等)都以静态 Pod 对象的形式运行于 Master 主机之上。

    静态 Pod 模式

  3. 自托管模式(self-hosted):类似第二种方式,同样把除 kubelet 和容器运行时之外的组件运行为 Pod,区别在于这些 Pod 托管运行在集群自身之上、受控于 DaemonSet 类型的控制器,而不是静态 Pod 对象。

kubeadm 部署的集群运行为第 2 种或第 3 种模式,默认是静态 Pod 模式;需要自托管模式时,kubeadm init 加上 --features-gates=selfHosting 选项即可。第 1 种模式需要把各组件跑在系统之上的独立守护进程中,其间用到的证书及 Token 等认证信息也都需要手动生成,过程烦琐且极易出错,一般不推荐使用。

kubeadm 与 kubectl

  • kubeadm:用于初始化集群。
  • kubectl:客户端命令行工具,用来部署和管理应用,查看、创建、删除和更新各种资源。
1
2
3
4
kubectl get nodes                                # 查看节点状态
kubectl get pod --all-namespaces # 查看所有命名空间的 pod,可简写为 kubectl get pod -A
kubectl get pod -A -o wide # 额外显示 IP、所在节点等详细信息
kubectl describe pod -n kube-system etcd-k8s01 # 查看指定命名空间下某个 pod 的详情

下面这段会话演示了给工作节点补打角色标签,以及静态 Pod 模式下控制面组件全部落在 Master 节点的样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@k8s01 ~]# kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s01 Ready master 18m v1.18.6
k8s02 Ready <none> 15m v1.18.6
k8s03 Ready <none> 15m v1.18.6

[root@k8s01 ~]# kubectl label nodes k8s02 node-role.kubernetes.io/node=
node/k8s02 labeled
[root@k8s01 ~]# kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s01 Ready master 18m v1.18.6
k8s02 Ready node 15m v1.18.6
k8s03 Ready <none> 15m v1.18.6

[root@k8s01 ~]# kubectl get pod -A -o wide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE
kube-system calico-kube-controllers-76d4774d89-xqw6g 1/1 Running 0 44m 172.18.235.129 k8s03
kube-system calico-node-cwv6n 1/1 Running 0 44m 192.168.43.101 k8s01
kube-system coredns-7ff77c879f-7frh7 1/1 Running 0 48m 172.18.73.66 k8s01
kube-system etcd-k8s01 1/1 Running 0 48m 192.168.43.101 k8s01
kube-system kube-apiserver-k8s01 1/1 Running 0 48m 192.168.43.101 k8s01
kube-system kube-controller-manager-k8s01 1/1 Running 0 48m 192.168.43.101 k8s01
kube-system kube-proxy-x57zq 1/1 Running 0 48m 192.168.43.101 k8s01
kube-system kube-scheduler-k8s01 1/1 Running 0 48m 192.168.43.101 k8s01

kubectl create deployment httpd-app --image=httpd --replicas=2 为例,一次部署请求会这样流转。

  1. kubectl 发送部署请求到 API Server,API Server 校验后把 Deployment 对象写进 etcd。
  2. Deployment controller 通过自己的 Informer 从 API Server watch 到这个新对象,创建出对应的 ReplicaSet;ReplicaSet controller 同理 watch 到 ReplicaSet,创建出两个 Pod。
  3. Scheduler watch 到 spec.nodeName 还是空的 Pod,执行调度并把两个副本分别绑定到 k8s02 和 k8s03。
  4. 两个节点上的 kubelet 各自 watch 到绑到自己身上的 Pod,创建并运行容器。

这里没有”API Server 主动通知某个组件”这回事:控制器、调度器、kubelet 都是自己持有 Informer 去 list-watch API Server。正因为是拉模型,控制器重启后重新 list 一遍就能续上,也才谈得上水平扩展和自愈。

之后执行 kubectl get pod 时,API Server 会从 etcd 中读取这些数据返回。

常见 Deployment 对象示例

核心资源对象

Kubernetes 的 API 对象大体可分为五个类别:工作负载(Workload)、发现和负载均衡(Discovery & LB)、配置和存储(Config & Storage)、集群(Cluster)、元数据(Metadata)。它们基本上都围绕同一个核心目的设计,即如何更好地运行和丰富 Pod 资源,从而为容器化应用提供更灵活、更完善的操作与管理组件。

Kubernetes 的调度器是统一按照 Pod 而非容器的资源需求进行计算的,Pod 是 Kubernetes 里的原子调度单位。可以这样类比二者的关系:Pod 扮演的是传统基础设施里虚拟机的角色,而容器则是这个虚拟机里运行的用户程序。容器本质是被 Namespace 和 Cgroups 约束起来的一组进程,PID 1 底下可以有任意子进程(这也正是需要 --init、信号转发、僵尸进程回收这些东西的原因)。”一个容器只跑一个主进程”是最佳实践而不是机制约束——这样做生命周期才和容器一致、信号语义才清晰。

工作负载

Pod 是工作负载型资源中的基础资源,它负责运行容器,并为其解决环境性的依赖,例如向容器注入共享的或持久化的存储卷、配置信息或密钥数据等。其上的控制器按应用形态来选择。

  • 无状态应用ReplicaSet 用于确保每个 Pod 副本在任一时刻均能满足目标数量;Deployment 用于管理无状态的持久化应用(例如 HTTP 服务器),它为 Pod 和 ReplicaSet 提供声明式更新,是构建在 ReplicaSet 之上的更高级控制器。
  • 有状态应用StatefulSet 用于管理有状态的持久化应用(如数据库服务程序),与 Deployment 的不同之处在于它会为每个 Pod 创建一个独有的持久性标识符,并确保各 Pod 之间的顺序性。
  • 每节点一份DaemonSet 用于确保每个节点都运行某个 Pod 的一个副本,新增的节点会被自动添加此类 Pod,节点移除时此类 Pod 会被回收。常用于运行集群存储守护进程(如 glusterd、ceph)、日志收集进程(如 fluentd、logstash),以及监控进程(如 Prometheus 的 NodeExporter、collectd、Datadog agent、Ganglia 的 gmond 等)。
  • 一次性任务Job 用于管理运行完成后即可终止的应用,例如批处理作业。Job 创建一个或多个 Pod 并确保其符合目标数量,直到 Pod 正常结束而终止。

发现和负载均衡

这一类包含为工作负载提供发现机制与负载均衡的 ServiceEndpoint 资源,以及通过七层代理实现请求流量负载均衡的 Ingress 资源。

Service 是建立在一组 Pod 对象之上的资源抽象,它通过标签选择器选定一组 Pod,为这组 Pod 定义统一固定的访问入口。Service 有自己的 IP 和端口,并为后端 Pod 提供负载均衡。Service 对象创建完成后即可作为服务被各客户端访问,但要真正响应这些请求,还是要依赖各后端的资源对象。

需要注意的是,Service 的地址并不配置于任何主机或容器的网络接口之上,而是通过 Node 之上的 kube-proxy 配置成 iptables 或 ipvs 规则,从而将发往此地址的所有流量调度至其后端的各 Pod 对象。Service 网络在集群创建时予以指定,而各 Service 的地址则在用户创建 Service 时动态配置。

Service 架构

Service 主要有三种常用类型,每一种都以其前一种为基础才能实现。

  1. ClusterIP:仅用于集群内部通信。
  2. NodePort:接入集群外部请求,工作于每个节点的主机 IP 之上。
  3. LoadBalancer:把外部请求负载均衡至多个 Node 的主机 IP 的 NodePort 之上。它需要协同集群外部的组件才能实现,而该外部组件并不接受 Kubernetes 的管理。

端口概念辨析

端口关系

  • port<cluster ip>:port,提供给集群内部访问 Service 的入口。
  • nodePort<node ip>:nodePort,提供给集群外部访问 Service 的入口。
  • targetPort:容器真正监听的那个端口。可以直接写端口号,也可以写 containerPortname 来引用。
  • containerPort:声明性的元数据,只是把容器监听的端口写出来(便于起名字和查看),本身不参与任何转发。

这两者不是”映射”关系——写数字时 targetPort 必须等于容器实际监听的端口,否则流量送不到。

可以类比 docker run -p 16379:6379 --name k8s-redis redis redis-server:16379 是宿主机侧的端口,对应的是 nodePort;6379 才同时是 targetPort 和 containerPort,容器侧这两个值始终是同一个端口。

请求是如何到达 Pod 的

当通过 Service 的域名访问时,会先由 CoreDNS 解析出 Service 对应的 Cluster IP(虚拟 IP)。请求到达宿主机网络后,就会被 kube-proxy 所配置的 iptables 或 ipvs 规则拦截,之后被转发到实际的某个后端 Pod 上面去。

以 ipvs 模式、后端 3 个 Pod 为例。

  1. 生成一个 Service 时,kube-proxy 先在宿主机创建一个虚拟网卡(如 kube-ipvs0),并以 Service IP 作为它的 IP 地址。
  2. kube-proxy 通过 Linux 的 IPVS 模块为这个 IP 地址设置 3 个虚拟主机(负载均衡策略为轮询),虚拟主机的 IP 和端口正是对应被代理的那三个 Pod。

这样就不需要为每个 Pod 单独维护 iptables 规则,而是把规则放到内核态处理,降低了维护规则的代价;但包过滤、SNAT 等能力仍需要 iptables 来实现。

自己跟一遍这条链

上面是结论,想真正搞懂就得把规则翻出来看。iptables 模式下是一条三级跳转(下面是链的结构示意,实际输出里 XXX 是根据 Service 名字算出的哈希后缀):

1
2
3
4
5
6
7
8
9
10
11
iptables -t nat -L KUBE-SERVICES -n     # 第一级:按 目的IP:端口 匹配到 Service
# -d <ClusterIP>/32 --dport 80 -j KUBE-SVC-XXXXXX

iptables -t nat -L KUBE-SVC-XXXXXX -n # 第二级:在这里做负载均衡
# -m statistic --mode random --probability 0.33333 -j KUBE-SEP-AAAAAA
# -m statistic --mode random --probability 0.50000 -j KUBE-SEP-BBBBBB
# -j KUBE-SEP-CCCCCC

iptables -t nat -L KUBE-SEP-AAAAAA -n # 第三级:真正的 DNAT
# -j KUBE-MARK-MASQ (打标记,回程要 SNAT)
# -j DNAT --to-destination <PodIP>:80

第二级那三行概率值最值得看:为什么是 0.33333、0.50000、然后没有概率?

因为这些规则是顺序求值的,每一条只在前面都没命中时才轮到它。三个后端要各占 1/3:

  • 第一条 1/3 命中 → 第一个 Pod;
  • 没命中的 2/3 里,第二条给 1/2 → 整体 2/3 × 1/2 = 1/3
  • 都没命中的剩下 1/3 无条件走第三条 → 1/3

所以第 k 条的概率是 1/(n-k+1),而不是每条都写 1/n。理解了这一点,两个推论也就自然了:

一、iptables 模式没有会话保持,也配不了调度算法。 它用的是 statistic random,每个新连接独立掷一次骰子——同一个客户端的两次请求可能落到不同 Pod。想要会话保持只能靠 Service 的 sessionAffinity: ClientIP(那会额外用 -m recent 记住来源 IP),而 lc/wlc/sh 这类算法在 iptables 模式下根本无从表达。IPVS 模式才有 -s rr|lc|wlc|sh 可选:

1
2
3
4
ipvsadm -Ln          # 直接看到 ClusterIP 下挂着哪几个 RealServer、用什么算法
# TCP 10.96.0.10:80 rr
# -> 10.244.1.12:80 Masq 1 0 0
# -> 10.244.2.13:80 Masq 1 0 0

二、删 Pod 的瞬间为什么会有连接被打到已经死掉的后端。 规则更新不是原子的:Pod 被删 → endpoint controller 摘 endpoint → kube-proxy watch 到变化 → 重写这一串规则。这几步之间有毫秒到秒级的窗口,窗口内旧规则还在,流量照旧被 DNAT 到那个正在退出的 Pod 上。这也正是 Pod 优雅退出要在 preStop 里先 sleep 几秒的原因——等规则传播完再真正关闭监听。

规则条数上两种模式差别很大:iptables 模式下规则数随 Service 数 × 后端数 线性增长,而且是线性遍历匹配,几千个 Service 时 kube-proxy 每次全量刷新要花几百毫秒甚至更久;IPVS 用的是内核里的哈希表,查找接近 O(1),这才是大规模集群要换 IPVS 的真正原因。

配置与存储

Kubernetes 支持通过标准的 CSI(Container Storage Interface)统一存储接口扩展支持更多类型的存储系统。这一类中最常用的两个对象如下。

  • ConfigMap:以环境变量或存储卷的方式接入到 Pod 资源的容器中,可被多个同类的 Pod 共享引用,从而实现一次修改、多处生效。
  • Secret:存储敏感数据,如私钥、密码等。

集群级资源

Pod、Deployment、Service 和 ConfigMap 等资源属于名称空间级别,可由相应的项目管理员管理。除此之外还有一些集群级别的资源,用于定义集群自身的配置信息,它们仅应该由集群管理员进行操作。

  • Namespace:资源对象名称的作用范围,绝大多数对象都隶属于某个名称空间,默认时隶属于 default
  • Node:Kubernetes 集群的工作节点,其标识符在当前集群中必须是唯一的。
  • Role:名称空间级别的、由规则组成的权限集合,可被 RoleBinding 引用。
  • ClusterRole:集群级别的、由规则组成的权限集合,可被 RoleBinding 和 ClusterRoleBinding 引用。
  • RoleBinding:将 Role 中的许可权限绑定在一个或一组用户之上,它隶属于且仅能作用于一个名称空间;绑定时既可以引用同一名称空间中的 Role,也可以引用全局名称空间中的 ClusterRole。
  • ClusterRoleBinding:将 ClusterRole 中定义的许可权限绑定在一个或一组用户之上,它能够引用全局名称空间中的 ClusterRole,并能通过 Subject 添加相关信息。

Namespace 的作用边界

Namespace 可以把一个物理的 cluster 在逻辑上划分为多个虚拟 cluster,每个虚拟 cluster 就是一个 Namespace,不同 Namespace 里的资源对象是完全隔离的。但要注意这种隔离仅用于限制资源对象名称的作用域,并不能实现 Pod 间的网络通信隔离,网络层面的隔离要靠 NetworkPolicy。

两个内置的常用 Namespace:default 是创建资源时默认放入的名称空间,kube-system 存放 Kubernetes 自己创建的系统资源。

Namespace

元数据型资源

此类资源对象用于为集群内部的其他资源配置其行为或特性。例如 HorizontalPodAutoscaler 可用于自动伸缩工作负载类型资源对象的规模,Pod 模板资源可为 Pod 资源的创建预制模板,而 LimitRange 则可为名称空间的资源设置 CPU 和内存等系统级资源的数量限制。

容器运行时与 CRI

CRI(Container Runtime Interface,容器运行时接口)是 kubelet 与底层容器运行时之间的标准接口。kubelet 调用下层容器运行时(如 containerd、Docker)的执行过程,都是通过 CRI 的 gRPC 接口间接完成的。

引入 CRI 带来两个好处。

  1. 将 kubelet 与容器运行时解耦,运行时可以自由替换而不必改动 kubelet。
  2. 解放 kubelet 的负担,运行时相关的实现细节不再堆积在 kubelet 内部。

CRI 接口本身可以分为两部分。

  1. 容器运行时服务 RuntimeService:主要负责管理 Pod 和容器的生命周期,例如创建、删除、查询容器。
  2. 镜像服务 ImageService:主要负责容器镜像的生命周期管理,例如拉取、删除、查询镜像。

顺带记住 Kubernetes 里这三个容易混淆的接口缩写。

  • CNI:容器网络接口,负责网络。
  • CSI:容器存储接口,负责存储。
  • CRI:容器运行时接口,负责容器运行。

图解汇总

📌 素材来源:以下图解为 尚硅谷 Kubernetes 课程 的配套截图,图片版权归尚硅谷所有,仅作个人学习笔记与复习整理之用。

概述与集群搭建

  • 课程内容介绍
  • 概述、特性和架构组件
  • 核心概念
  • k8s 集群搭建(平台规划和搭建方式介绍)
  • 搭建 k8s 集群(kubeadm 方式)
  • 搭建 k8s 集群(二进制方式)
  • 搭建 k8s 集群总结
  • 搭建高可用的集群(一)
  • 搭建高可用的集群(二)

命令行与资源清单

  • 命令行工具 kubectl
  • yaml 文件说明
  • 快速编写 yaml 文件
  • yaml 文件字段说明
  • yaml 文件示例
  • 共享存储示例 yaml

Pod

  • Pod
  • Pod 实现机制之共享网络
  • Pod 实现机制之共享存储
  • Pod 镜像拉取策略
  • Pod 资源限制
  • Pod 重启策略
  • Pod 健康检查
  • 创建 Pod 流程

Pod 调度

  • Pod 调度之节点选择器
  • Pod 调度之节点亲和性
  • Pod 调度之污点和污点容忍
  • 污点容忍

控制器与服务暴露

  • Controller 控制器(deployment)
  • controller
  • Service
  • Ingress

配置管理、安全与存储

  • 配置管理之 secret
  • 配置管理之 configMap
  • k8s 集群安全机制
  • k8s 集群安全机制之 rbac 实现鉴权
  • 持久存储之 nfs
  • 持久存储之 pv 和 pvc

监控、Helm 与项目部署

  • 集群监控平台
  • helm(概述)
  • helm(快速部署应用)
  • helm(自己创建 chart)
  • helm(chart 模板)
  • 部署项目流程介绍
  • 部署 java 项目