在没有工作负载之前,我们都是直接手动创建一个一个的Pod。但是如果当我们需要将一个复杂的服务部署到庞大的集群上并且实现高可用,我们总不能每个节点上都部署一遍吧。况且如果你可以将所有的Pod手动都创建好,但是如果镜像发生了更新,我们也需要将一个一个的Pod手动进行更新。太麻烦了,这个时候我们就可以使用工作负载,它可以帮助我们更高的管理Pod。

工作负载

工作负载 | Kubernetes

工作负载是在 Kubernetes 上运行的应用程序。

为了减轻用户的使用负担,通常不需要用户直接管理每个 Pod。 而是使用负载资源来替用户管理一组 Pod。 这些负载资源通过配置控制器来确保正确类型的、处于运行状态的 Pod 个数是正确的,与用户所指定的状态相一致。

Kubernetes 提供若干种内置的工作负载资源

  • Deployment和ReplicaSet
  • StatefulSet:有状态服务,为 Pod 提供持久存储和持久标识符。
  • DaemonSet:守护型应用部署,如日志、监控组价。
  • Job和CronJob:定时任务部署,指定时间运行。

Deployment

Deployment是Kubernetes中用于定义Pod副本集的对象,它负责管理应用的部署和更新。通过Deployment,可以实现应用的滚动更新、回滚以及自动修复

工作模式

Deployment并不直接管理Pod,而是管理ReplicaSet,ReplicaSet再管理Pod。在我们创建 Deployment 的时候,它会用自己的 Pod 规范创建一个 ReplicaSet。当更新一个 Deployment 并修改副本数量时,它会把更新内容传递给下游的 ReplicaSet。

一个 ReplicaSet 对象就是由副本数目的定义和一个 Pod 模板组成的, 它的定义就是 Deployment 的一个子集。它主要关注的是Pod实例的数量,并在Pod出现问题时,维持所需副本数量。

Deployment则是建立在ReplicaSet之上的更高级别的抽象,用于管理Pod副本集和应用程序的部署。然后它多了滚动更新、回滚以及自动修复这些功能。

img

主要组成部分

  1. 模板(Template):定义了要创建的Pod的规范,包括镜像、环境变量、卷等。当副本数不足时会根据模板新建Pod。
  2. 副本数(Replica Count):指定了希望运行的Pod副本数量。
  3. 升级策略(Update Strategy):定义了应用更新的策略,包括滚动更新、Recreate等。
  4. 标签选择器(Label Selector):用于选择要进行管理的Pod副本集。
  5. 滚动升级(Rolling Update):一种升级策略,通过逐步替换旧的Pod实例来实现平滑的升级。

特点

  1. 滚动更新:保证更新过程不中断服务。
  2. 副本管理:保证始终有指定数量的实例在运行。
  3. 自动修复:当Pod发生故障时会自动替换为新的Pod。
  4. 版本回滚:允许用户回滚到先前的版本,以应对更新带来的问题

示例:使用Deployment部署Nginx

apiVersion: apps/v1 # API版本
kind: Deployment
metadata: # 元数据部分
name: nginx-deployment # 资源名称
namespace:
labels: # 给deployment打的标签
app: nginx # 名为app值为nginx的标签
spec: # 定义部署对象的规范
replicas: 3 # 副本数量
selector: # 选择器部分,用于指定哪些Pod副本属于此部署
matchLabels: # 匹配标签部分。Service通过这里来匹配Pod
app: nginx # 指定要匹配的标签为app=nginx,用于选取具有这个标签的Pod副本
template: # 模板部分
metadata:
labels:
app: nginx # 需要与selector设置的标签一致
spec:
containers: # 用于定义Pod中的容器
- name: nginx # 容器名称
image: reg.redrock.team/library/nginx:latest
ports:
- containerPort: 80 # 暴露容器的80端口供外部访问
kubectl apply -f deploy.yaml

可以看到RS的NAME是在Deployment之后加了一段字符串,而Pod是在RS后又加了一段字符串,说明Pod其实是由RS直接管理的。

副本伸缩

方式一:命令行

kubectl scale deploy NAME --replicas=pod数量 -n NAMESPACE

方式二:编辑yaml文件

修改replicas值即可。

Pod为什么要有副本数?

  • 使用多个副本来运行同一个服务,可以提高应用的可用性。如果一个Pod因为故障而被杀死,其他Pod会立即来替代它,从而保证了服务的稳定性。
  • 多个副本可以将流量分散到不同的Pod,从而实现负载均衡,避免所有流量集中在一个Pod上导致负载过高。流量高时,我们可以增加副本数,少时可以减少副本数,从而节省资源。
  • 多个Pod可以实现平滑的滚动更新。
镜像更新

Deployment支持两种镜像更新策略:重建更新(Recreate)滚动更新(默认)(RollingUpdate),可以通过strategy选项进行配置。

重建更新(Recreate)

spec下增加如下字段

spec:
strategy: # 策略
type: Recreate # 重建更新策略

滚动更新(RollingUpdate)

strategy:
type: RollingUpdate # 滚动更新策略,可以不加,因为默认就是滚动更新
rollingUpdate:
maxUnavailable: 25%
maxSurge: 25%

MaxSurgeMaxUnavailable,这两个参数决定了更新过程的速度。这两个参数可以是 Pod 数量,也可以是 Deployment 的实例数量百分比。

为什么要有这两个参数?为什么要设置更新过程的速度?

  • 我们的服务很重要,对服务的变更需要非常谨慎。因此决定在关闭旧版本Pod之前,需要首先启动新Pod。只有新 Pod 启动、运行并就绪之后,才能杀死旧 Pod。
  • 假如我们的集群已经满载,无法负担多余的Pod消耗,我们自然需要先关掉旧Pod,然后才启动新Pod。

MaxUnavailable设置为 0 意味着:“在新 Pod 启动并就绪之前,不要关闭任何旧 Pod”。

MaxSurge设置为 100% 的意思是:“立即启动所有新 Pod”,也就是说我们有足够的资源,我们希望尽快完成更新。

这两个默认值是25%,假如我们有3个副本,意味着更新时允许 25%(0.75个取整为1) 的Pod处于不可用状态,允许最多增加1个新Pod。

版本回退

Deployment支持版本升级过程中的暂停,继续功能以及版本回退等诸多功能。

kubectl rollout option NAME -n NAMESPACE
kubectl rollout:版本升级相关功能,支持下面的选项:

status:显示当前升级状态
history:显示升级历史记录
pause:暂停版本升级过程
resume:继续已经暂停的版本升级过程
restart:重启版本升级过程
undo:回滚到上一级版本(可以使用--to-revision回滚到指定版本)

金丝雀发布

Deployment支持更新过程中的控制,如"暂停(pause)"或"继续(resume)"更新操作。

有时我们不想让测试版本影响所有用户,即使是短时间也不行。所以我们可以部分推出新版本。例如我们部署新旧两组实例,1% 的流量发送给新版本,先筛选一小部分的用户请求路由到新的pod应用,继续观察能否稳定地按期望的方式运行。确定没问题之后再继续完成余下的pod资源滚动更新,否则立即回滚更新操作。

现在的镜像版本是latest,切换版本到1.19.2,变更途中使用pause

kubectl set image deploy nginx-deployment nginx=reg.redrock.team/library/nginx:1.19.2 -n lesson-demo&& kubectl rollout pause deploy nginx-deployment -n lesson-demo

发现1.19.2版本只增加了一个RS,而latest版本没有减少

image-20240413215726365

查看更新状态,发现deployment正在等待更新,并且一个已经更新完成。

image-20240413215935892

然后继续更新

kubectl rollout resume deploy nginx-deployment -n lesson-demo

全部更新完成

服务发现与负载均衡

服务(Service) | Kubernetes

为什么需要服务发现?

传统的部署应用服务方式都是直接部署在给定的机器上,访问服务时,我们只需要访问该机器的IP即可。但K8s集群中的应用都是通过Pod去部署的,而 pod 生命周期是短暂的。在 Pod 的生命周期过程中,比如它创建或销毁,它的 IP 地址都会发生变化,这样就不能使用传统的部署方式,不能指定 IP 去访问指定的应用。

Service

前面我们通过Deployment创建了一组Pod,这些Pod组需要提供一个统一的访问入口,以及怎么去控制流量负载均衡到这个组里面。其实我们可以在部署时就提供一个模板以及访问方式,使应用服务暴露在外部。这时就需要服务发现,也就是K8s中的Service。

示例:通过Service暴露Nginx服务

apiVersion: v1
kind: Service
metadata:
name: nginx-service
namespace: lesson-demo
spec:
selector:
app: nginx # 匹配具有该标签的Pod
ports:
- protocol: TCP
# 服务监听的端口号,当其他服务或外部客户端与该服务通信时,它们将使用这个端口号进行通信,该端口号是服务对外部暴露的入口。即通过clusterIP: port可以访问到某个service
port: 9376
# targetPort可以直接指定Pod的端口,也可以指定Pod端口所对应的名称。
# targetPort一定要与containerPort暴露出的端口对应
targetPort: 80

Service的类型

ClusterIP

  • 只在集群内部生效的IP,集群内所有节点都能访问到它。
  • ClusterIP类型也是我们不指定类型时的默认Service类型。

apply之后,我们就可以通过访问ClusterIP:PORT的形式来访问到我们的服务。

访问方式:ClusterIP:PORT

image-20240414172201653

这个 IP 地址就是 Service 的 IP 地址,这个 IP 地址在集群里面可以被其它 pod 所访问,相当于通过这个 IP 地址提供了统一的一个 pod 的访问入口,以及服务发现。而Endpoints是每个Pod的后端IP。

NodePort

当需要从集群外部访问Service时,就可以使用NodePort。

NodePort会在每个NodeIP上启用一个相同的端口来暴露服务。NodeIP就是我们在创建集群时指定的节点IP,端口限定范围为30000-32767。

当我们访问NodeIP:PORT时,流量会由 kube-proxy 网络组件进一步到给对应的 Pod。

访问方式:NodeIP:PORT

apiVersion: v1
kind: Service
metadata:
name: nginx-service
namespace: lesson-demo
spec:
type: NodePort
selector:
app: nginx
ports:
- port: 80
targetPort: 80
nodePort: 30007

LoadBalancer

NodePort会在每台Node上监听端口接收用户流量,但在实际情况下,对用户暴露的只会有一个IP和端口,那这么多台Node该使用哪台让用户来访问呢?而且如果只访问一个NodeIP,那么这个节点压力会很大。

这时就需要前面加一个公网负载均衡器为项目提供统一访问入口了。

LoadBalancer 通常是云服务厂商提供的负载均衡器,我们通过外部的负载均衡器将流量路由到创建的服务商。

将Service的类型设置为LoadBalancer后,我们有两个选择,一个是设置负载均衡器的IP,另一种负载均衡器会自动为我们分配一个ExternalIP作为出口。

image

访问方式:EXTERNALIP:PORT

apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: LoadBalancer
selector:
app: nginx
ports:
- port: 80
targetPort: 80

externalIPs

一般我们的服务只暴露了ClusterIP,而NodePort的端口又有30000-32767的范围限制。这时我们就可以配置上externalIP,表示来自externalIP的流量能进入我们的服务,

externalIP一般填负载均衡器的IP。

注意:externalIPs一定不能填NodeIP,不然会导致该节点直接崩溃(calico、kubelet、kube-proxy等组件无法与apiserver进行通信),出现The connection to the server apiserver.cluster.local:6443 was refused - did you specify the right host or port?

原因分析可以看k8s IPVS模式下externalIP导致节点故障浅析_external-ip-CSDN博客

apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
externalIPs:
- 1.2.3.4
selector:
app: nginx
ports:
- port: 80
targetPort: 80

Service四种类型的对比

类型 用途介绍
ClusterIP 默认类型,自动分配一个仅clusterIp内部可以访问的虚拟ip地址
NodePort ClusterIP基础上为Service机器上绑定一个端口,这样就可以通过NodePort来访问服务
LoadBalancer NodePort基础上借助云服务商创建一个外部负载均衡器,并将请求转发到NodePort来访问服务
externalIPs 把集群外部的服务引入到集群内部来,在集群内部直接使用

Ingress和IngressRoute

没有Ingress之前,我们使用NodePort对外暴露服务,但当服务多了以后,会存在一些弊端,比如暴露太多的的端口,无法实现域名转发等等。

Ingress其实就是从Kubernetes集群外部访问的一个入口,它可以帮助我们通过不同域名来将流量匹配到对应的服务,类似于Nginx。

Ingress在Service上面一层,可以定义集群外部到集群内Service的HTTP和HTTPS的路由。

img

使用方法:先自行部署IngressController → 再部署Ingress资源。

IngressController的功能:

  • 接受外部流量,并将请求负载均衡到内部Pod
  • 部署Ingress路由转发规则
  • 监控Pod,并在添加或删除Pod后自动更新负载均衡规则。

可以把Ingress controller理解为一个监听器,它可以不断地与 kube-apiserver 打交道,实时的感知后端 service、pod 的变化,当得到这些变化信息后,Ingress controller 再结合 Ingress 的配置,更新反向代理负载均衡器,达到服务发现的作用。

Ingress Controller的实现有多种,比如Ingress-Nginx、Traefik等,由于Ingress-Nginx的配置比较麻烦,所以我们一般使用它的替代品Traefik。

Traefik Ingress Controller

优势:

  1. 部署过程简单
  2. 使用Go语言开发,完美贴合K8s
  3. Traefik衍生的IngressRoute配置更加简洁,我们一般使用IngressRoute
  4. 拥有多种中间件,可以用于将请求路由到目的地之前或之后来调整请求。
安装

使用Helm安装(Helm是K8s中的一个包管理器)

# helm安装
$ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
$ chmod 700 get_helm.sh
$ ./get_helm.sh
helm repo add traefik https://traefik.github.io/charts
helm repo list
helm repo update
helm pull traefik/traefik
kubectl create namespace traefik
helm install traefik traefik/traefik -n traefik

部署完成后,我们就可以把某个域名解析到我们的集群(下面把k8s.yiiong.top解析到172.20.14.20),然后我们可以配置IngressRoute规则,Traefik就可以根据IngressRoute规则来将流量路由到对应的服务,并为每个服务提供独立的域名。

路由实现

Traefik有两个服务入口(entryPoint)

  • 80,对应entryPoint:web
  • 443,对应entryPoint:websecure

Traefik默认以NodePort形式暴露,对于如下的Traefik来说,它的外部入口就是3035031971,从30350进入80入口,从31971进入443入口,具体规则可以在values.yaml中修改。

如果我们不想通过域名:端口(k8s.yiiong.top:30350),我们可以通过配置Nginx的反向代理,实现直接访问域名到我们对应的服务。

NAME              TYPE       CLUSTER-IP   EXTERNAL-IP   PORT(S)                      AGE
service/traefik NodePort 10.96.1.38 <none> 80:30350/TCP,443:31971/TCP 174d

路由实现

image-20240418223033153

IngressRoute

Ingress是官方的东西,而IngressRoute则是Traefik Ingress Controller封装好的一种特殊Ingress资源。

配置一个HTTP的IngressRoute

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: nginx-http
namespace: lesson-demo
spec:
entryPoints: # 入口点,入口点是在我们Ingress控制器中定义的入口端口,表示进入该端口的服务才遵循该IngressRoute的路由
- web
routes:
- match: Host(`k8s.yiiong.top`) # 匹配这个域名
kind: Rule # 路由类型的规则
services:
- name: nginx-service # 匹配名为nginx-service的svc
port: 9376 # 这个svc暴露在9376,二者要相同

接下来访问http://k8s.yiiong.top:30350即可访问到我们对应的服务。

image-20240420212251143

配置HTTPS的IngressRoute

我们这里先使用openssl来生成一个自签证书。

openssl genrsa -out ca.key 4096

openssl req -x509 -new -nodes -sha512 -days 36500 \
-subj "/C=CN/ST=Chongqing/L=Chongqing/O=Redrock/OU=Personal/CN=k8s.yiiong.top" \
-key ca.key \
-out ca.crt


openssl genrsa -out tls.key 4096

openssl req -sha512 -new \
-subj "/C=CN/ST=Chongqing/L=Chongqing/O=Redrock/OU=Personal/CN=k8s.yiiong.top" \
-key tls.key \
-out tls.csr

openssl x509 -req -sha512 -days 3650 \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-in tls.csr \
-out tls.crt

创建证书的Secret(K8s中一种存储和管理敏感数据的资源类型)

kubectl create secret -n lesson-demo generic traefik-tls --from-file=tls.crt --from-file=tls.key

配置Yaml

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: nginx-https
namespace: lesson-demo
spec:
# 这里指定https只能走443端口
entryPoints:
# 使用websecure作入口
- websecure
routes:
- kind: Rule
match: Host(`k8s.yiiong.top`)
services:
- name: nginx-service
port: 9376
# tls相关配置
tls:
# 指定证书,是我们前面创建的证书
secretName: traefik-tls

接下来访问https://k8s.yiiong.top:31971

image-20240420220702430

image-20240420220839470

不在集群里使用HTTPS

  • 在集群中使用HTTPS太麻烦了,要配这么多东西,有没有什么更好的办法可以把它绕开,然后我们也能使用HTTPS请求呢?
  • 答案是在集群中配置一个网关机,让这个网关机来控制流量的进入,我们只需要关注流量进入网关后,把tls给卸载掉,最后用http的流量进入我们的集群,这种方案即安全,还方便,不过缺点是只适合在多节点的集群中使用
中间件middleware

主要用来对HTTP请求做一些修改

img

HTTP强制跳转HTTPS

实现效果:访问http://k8s.yiiong.top:30350自动跳转至https://k8s.yiiong.top:31971

apply刚才的HTTPS的IngressRoute。

然后配置一个中间件

这里会用到RedirectScheme的内置中间件,配置如下:

apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: redirect-https-middleware
namespace: lesson-demo
spec:
redirectScheme:
scheme: https
port: "31971" # 由于我的HTTPS在31971端口,指定一下端口,让其重定向到31971

然后在HTTP的Yaml文件里最后添加刚刚创建的中间件。

middlewares:
- name: redirect-https-middleware

可见HTTP被成功重定向到HTTPS

image-20240420223215246

去除请求前缀

有时候我们只有一个域名,但我们想通过这一个域名访问不同的应用。

在Nginx中,我们可以配置Location来匹配流量,Traefik也可以这么做。

但是定制不同的前缀后,由于应用本身并没有这些前缀,导致请求返回404,这时候我们就需要对请求的path进行处理。

先创建一个带前缀的IngressRoute

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: nginx-http
namespace: lesson-demo
spec:
entryPoints:
- web
routes:
- match: Host(`k8s.yiiong.top`) && PathPrefix(`/nginx`)
kind: Rule
services:
- name: nginx-service
port: 9376

直接访问会返回404

image-20240420225042831

配置中间件stripPrefix

apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: prefix-url-middleware
namespace: lesson-demo
spec:
stripPrefix:
prefixes:
- /nginx

修改IngressRoute,使用中间件

middlewares:
- name: prefix-url-middleware

image-20240420235338285

灰度发布

灰度发布我们有时候也会称为金丝雀发布(Canary),主要就是让一部分测试的服务也参与到线上去,经过测试观察看是否符合上线要求。

假设一个应用现在运行着V1版本,新的V2版本需要上线,这时候我们需要在集群中部署好V2版本,然后通过Traefik提供的带权重的轮询(WRR)来实现该功能。

这和我们之前讲Nginx带权重的负载均衡很类似。

需要两个及以上的Service,然后再加一个TraefikService

apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: app
spec:
weighted:
services:
- name: appv1 # 服务1
weight: 3
port: 80
kind: Service
- name: appv2 # 服务2
weight: 1
port: 80
kind: Service

我们给v1版本配置权重3,给要上线的v2版本配置权重1.也就是说我们3次访问domain.com都会是v1版本,第4次就是v2版本。和我们当时Nginx举的8881和8882的那个例子一模一样。

创建TraefikService类型后,我们在IngressRoute类型中将service写为traefikService即可

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: app-ingressroute-canary
spec:
entryPoints:
- web
routes:
- match: Host(`domain.com`)
kind: Rule
services:
- name: app
kind: TraefikService

待v2版本测试没问题后,就可以将流量全切到v2了。

Traefik的中间件还有很多,比如Basic Auth(用户身份认证)、ipWhiteList(定义IP白名单)、mirroring(流量复制)等等

大家可以通过Traefik Proxy Middleware Overview - Traefik进行学习更多的中间件,甚至可以自行开发自定义中间件。

总结:服务暴露模式

暴露模式 访问方式
Service (NodePort) + Deployment NodeIP:NodePort
IngressRoute + Service(CluserIP) + Deployment 域名
LoadBalancer + Service + Deployment 负载均衡器IP:PORT

第二种方式是最常用的,之所以Service被设置成ClusterIP,是因为流量进入集群后直接就由Ingress Controller管理,不再通过kube-proxy管理。我们常常将域名解析到集群主机,然后IngressRoute设置相同的主机名,就能实现流量匹配

作业:

1.将第三节课课后作业使用FastAPI开发的Web项目部署在K8s上,并通过**IngressRoute + Service(CluserIP) + Deployment + Middleware(去前缀)**的方式暴露服务。

2.部署2个以上的服务在K8s上,利用IngressRoute用域名区分访问。

最终将服务地址(要求能访问到)和整个部署流程记录文档提交到yiiong@redrock.team

截止时间5月5日