K8s集群运维,这些工具和技巧帮你省力又省心

409 阅读17分钟

无服务器架构在开发者中逐渐流行,让开发人员不需要再付出额外的精力管理服务器就可以运行自己的应用。但在这种架构下,集群本身依然需要一定的管理和运维工作。本文将简单介绍一些实用的工具和技巧,帮助大家更省心地运维自己的 Kubernetes 集群。

image.png


延伸阅读, 点击链接了解 Akamai Cloud Computing


一、借助 Knative 简化应用管理

要对无服务器化的 Kubernetes 应用程序进行管理,离不开高效的工具。Knative 是一款建立在 Kubernetes 之上,并提供强大功能的工具集,可以用于管理无服务器应用程序。

Knative 支持可自定义的事件和触发器,能够控制应用程序的响应方式。它是一种可移植的,并且提供商中立的工具,因此可以将它与首选的托管 Kubernetes 服务(如 Linode Kubernetes Engine)一起使用,也可以将其安装在本地集群上。

Knative 可提供:

  • 自动缩放: 提供基于流量和需求的 pod 自动缩放,包括缩放至零。
  • 事件驱动计算: 允许无服务器工作负载响应事件和触发器。
  • 可移植性: 能够跨不同的云提供商和环境工作。允许开发人员在不修改代码的情况下将无服务器应用程序部署至不同的环境中。
  • 可扩展性: 提供了一组可定制的构建块,以满足特定的应用程序要求。
  • 企业可扩展性: 得到了 Puppet 与 Outfit7 等公司的信任。

工作原理

Knative 的功能分为 Knative Eventing 和 Knative Serving。

  • Eventing: API 的集合,通过 HTTP POST 请求启用接收器,或将事件从生产者路由到消费者。
  • Serving: 将一组对象定义为 Kubernetes 自定义资源定义 (CRD),或创建 Kubernetes API 的扩展。这将决定无服务器工作负载如何与具有以下资源的 Kubernetes 集群交互。

Knative 使用 Kubernetes 作为编排器,Istio 处理查询路由和负载均衡。我们可以使用 YAML 或 Knative Operator for Kubernetes 将 Knative 安装到集群上。

二、Kubernetes 集群的主动扩展

当集群资源不足时,Cluster Autoscaler 会提供新节点并将其加入集群。使用 Kubernetes 时你可能会注意到,创建节点并将其加入集群的过程可能需要花费数分钟。在这段时间里,应用程序很容易被连接淹没,因为已经无法进一步扩展了。

如何消除如此长的等待时间?

主动扩展(Proactive scaling),或者:

  • 理解集群 Autoscaler 的工作原理并最大限度提升其效用;
  • 使用 Kubernetes scheduler 为节点分配另一个 Pod;以及
  • 主动配置工作节点,以改善扩展效果。

1.Cluster Autoscaler 如何在 Kubernetes 中生效

Cluster Autoscaler 在触发自动扩展时并不检查内存或 CPU 的可用数,而是会对事件作出反应,检查所有不可调度的 Pod。当调度器找不到能容纳某个 Pod 的节点时,我们就说这个 Pod 是不可调度的。

我们可以这样创建一个集群来测试看看。

$ linode-cli lke cluster-create \
 --label learnk8s \
 --region eu-west \
 --k8s_version 1.23 \
 --node_pools.count 1 \
 --node_pools.type g6-standard-2 \
 --node_pools.autoscaler.enabled enabled \
 --node_pools.autoscaler.max 10 \
 --node_pools.autoscaler.min 1 \
$ linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig

用下列命令验证安装已成功完成:

$ kubectl get pods -A --kubeconfig=kubeconfig

用环境变量导出 kubeconfig 文件通常是一种很方便的做法,为此可以运行:

$ export KUBECONFIG=${PWD}/kubeconfig
$ kubectl get pods

2. 部署应用程序

让我们部署一个需要 1GB 内存和 250m* CPU 的应用程序。

注意:m = 内核的千分之一容量,因此 250m = CPU 的 25% 容量。

apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 1
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
   spec:
     containers:
       - name: podinfo
         image: stefanprodan/podinfo
         ports:
           - containerPort: 9898
         resources:
           requests:
             memory: 1G
             cpu: 250m

用下列命令将资源提交至集群:

$ kubectl apply -f podinfo.yaml

随后很快会发现一些情况。首先,三个 Pod 几乎会立即开始运行,另有一个 Pod 处于 “未决” 状态。

随后很快:

  • 几分钟后,Autoscaler 创建了一个额外的 Pod,并且
  • 第四个 Pod 会被部署到一个新节点中。

最终,第四个 Pod 被部署到一个新节点中

第四个 Pod 为何没有部署到第一个节点中?让我们一起看看已分配的资源。

3.Kubernetes 节点中资源的分配

Kubernetes 集群中部署的 Pod 会消耗内存、CPU 以及存储资源。而且在同一个节点上,操作系统和 Kubelet 也需要消耗内存和 CPU。

在 Kubernetes 工作节点上,内存和 CPU 会被拆分为:

  1. 运行操作系统和系统守护进程(如 SSH、Systemd 等)所需的资源。
  2. 运行 Kubernetes 代理程序(如 Kubelet、容器运行时以及节点故障检测程序等)所需的资源。
  3. 可用于 Pod 的资源。
  4. 为排空阈值(Eviction threshold)保留的资源。

如果集群运行了 DaemonSet(如 kube-proxy),那么可用内存和 CPU 数量还将进一步减少。

那么不妨降低需求,以确保能将所有 Pod 都放入同一个节点中:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 4
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
   spec:
     containers:
       - name: podinfo
         image: stefanprodan/podinfo
         ports:
           - containerPort: 9898
         resources:
           requests:
             memory: 0.8G # <- lower memory
             cpu: 200m    # <- lower CPU

使用下列命令修改这个部署:

$ kubectl apply -f podinfo.yaml

选择恰当数量的 CPU 和内存以优化实例的运行,这是个充满挑战的工作。Learnk8s 计算器工具可以帮助我们更快速地完成这项工作。

一个问题解决了,但是创建新节点花费的时间呢?

迟早会需要四个以上的副本,我们是否真的需要等待好几分钟,随后才能创建新的 Pod?

简单来说:是的!Linode 必须从头开始创建和配置新虚拟机,随后将其连接到集群。这个过程经常会超过两分钟。

但其实还有替代方案:在需要时主动创建已经配置好的节点。例如可以配置让 Autoscaler 始终准备好一个备用节点。当 Pod 被部署到备用节点后,Autoscaler 可以主动创建另一个备用节点。然而 Autoscaler 并没有内置这样的功能,但我们可以很容易地重新创建。

创建一个请求数与节点资源相等的 Pod:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.8G

这个 Pod 完全不执行任何操作。

该节点的作用只是确保节点能够被充分使用起来。

随后还需要确保当工作负载需要扩展时,这个占位 Pod 能够被快速清除。为此可以使用 Priority Class。

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
 name: overprovisioning
value: -1
globalDefault: false
description: "Priority class used by overprovisioning."
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     priorityClassName: overprovisioning # <--
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.8G

提交后,配置工作已全部完成。可能需要等待一会让 Autoscaler 创建节点,随后我们将有两个节点:

  1. 一个包含四个 Pod 的节点
  2. 一个包含一个占位 Pod 的节点

如果将部署扩展为 5 个副本会怎样?是否要等待 Autoscaler 创建另一个新节点?

用下列命令测试看看吧:

kubectl scale deployment/podinfo --replicas=5

我们将会看到:

  1. 第五个 Pod 会立即创建出来,并在 10 秒内变为 “正在运行” 的状态。
  2. 占位 Pod 会被清除,以便为第五个 Pod 腾出空间。

随后:

  1. Cluster autoscaler 会注意到未决的占位 Pod 并配置一个新的节点。
  2. 占位 Pod 会被部署到新创建的节点中。

在可以有更多节点时,为何又要主动创建出一个节点?

我们可以将占位 Pod 扩展到多个副本,每个副本都会预配置一个 Kubernetes 节点,准备接受标准工作负载。然而这些节点虽然是闲置的,但它们产生的费用依然会计入云服务账单。因此一定要慎重,不要创建太多节点。

4. 将 Cluster Autoscaler 与 Horizontal Pod Autoscaler 配合使用

为理解这项技术的含义,我们可以将 Cluster autoscaler 和 Horizontal Pod Autoscaler(HPA)结合在一起来看。HPA 可用于提高部署中的副本数量。

随着应用程序收到越来越多流量,我们可以让 Autoscaler 调整处理请求的副本数量。当 Pod 耗尽所有可用资源后,会触发 Cluster autoscaler 新建一个节点,这样 HPA 就可以继续创建更多副本。

可以这样新建一个集群来测试上述效果:

$ linode-cli lke cluster-create \
 --label learnk8s-hpa \
 --region eu-west \
 --k8s_version 1.23 \
 --node_pools.count 1 \
 --node_pools.type g6-standard-2 \
 --node_pools.autoscaler.enabled enabled \
 --node_pools.autoscaler.max 10 \
 --node_pools.autoscaler.min 3 \
$ linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig-hpa

用下列命令验证安装过程已成功完成:

$ kubectl get pods -A --kubeconfig=kubeconfig-hpa

使用环境变量导出 kubeconfig 文件是一种方便的做法:

$ export KUBECONFIG=${PWD}/kubeconfig-hpa
$ kubectl get pods

使用 Helm 安装 Prometheus 并查看该部署的相关指标。

$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
$ helm install prometheus prometheus-community/prometheus

Kubernetes 为 HPA 提供了一个控制器,借此可以动态增减副本数量。然而 HPA 也有一些局限性:

  1. 无法拆箱即用。需要安装 Metrics Server 来汇总并暴露出指标。
  2. PromQL 查询无法做到拆箱即用。

好在我们可以使用 KEDA,它通过一些实用功能(包括从 Prometheus 读取指标)扩展了 HPA 控制器的用法。安装好 Prometheus 和 KEDA 之后,创建一个部署。

在这个实验中,将使用一个每秒可以处理固定数量请求的应用。每个 Pod 每秒最多可以处理十个请求,如果 Pod 收到第 11 个请求,会将请求挂起,稍后再处理。

apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 4
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
     annotations:
       prometheus.io/scrape: "true"
   spec:
     containers:
       - name: podinfo
         image: learnk8s/rate-limiter:1.0.0
         imagePullPolicy: Always
         args: ["/app/index.js", "10"]
         ports:
           - containerPort: 8080
         resources:
           requests:
             memory: 0.9G
---
apiVersion: v1
kind: Service
metadata:
 name: podinfo
spec:
 ports:
   - port: 80
     targetPort: 8080
 selector:
   app: podinfo

将资源提交至集群后,为了生成一些流量,可以使用 Locust。下列 YAML 定义将创建一个分布式负载测试集群:

apiVersion: v1
kind: ConfigMap
metadata:
 name: locust-script
data:
 locustfile.py: |-
   from locust import HttpUser, task, between
   class QuickstartUser(HttpUser):
       @task
       def hello_world(self):
           self.client.get("/", headers={"Host": "example.com"})
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: locust
spec:
 selector:
   matchLabels:
     app: locust-primary
 template:
   metadata:
     labels:
       app: locust-primary
   spec:
     containers:
       - name: locust
         image: locustio/locust
         args: ["--master"]
         ports:
           - containerPort: 5557
             name: comm
           - containerPort: 5558
             name: comm-plus-1
           - containerPort: 8089
             name: web-ui
         volumeMounts:
           - mountPath: /home/locust
             name: locust-script
     volumes:
       - name: locust-script
         configMap:
           name: locust-script
---
apiVersion: v1
kind: Service
metadata:
 name: locust
spec:
 ports:
   - port: 5557
     name: communication
   - port: 5558
     name: communication-plus-1
   - port: 80
     targetPort: 8089
     name: web-ui
 selector:
   app: locust-primary
 type: LoadBalancer
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
 name: locust
spec:
 selector:
   matchLabels:
     app: locust-worker
 template:
   metadata:
     labels:
       app: locust-worker
   spec:
     containers:
       - name: locust
         image: locustio/locust
         args: ["--worker", "--master-host=locust"]
         volumeMounts:
           - mountPath: /home/locust
             name: locust-script
     volumes:
       - name: locust-script
         configMap:
           name: locust-script

将其提交至集群后,Locust 会读取下列 locustfile.py 文件,该文件存储在一个 ConfigMap 中:

from locust import HttpUser, task, between
class QuickstartUser(HttpUser):
   @task
   def hello_world(self):
       self.client.get("/")

该文件并没有什么特别的作用,只是向一个 URL 发出请求。若要连接至 Locust 仪表板,我们需要提供其负载均衡器的 IP 地址。为此可使用下列命令获取地址:

$ kubectl get service locust -o jsonpath='{.status.loadBalancer.ingress[0].ip}'

随后打开浏览器并访问该 IP 地址即可。

KEDA 可以连接由 Prometheus 收集的指标,并将其发送给 Kubernetes。最后,它还将使用这些指标创建一个 Horizontal Pod Autoscaler (HPA)。

我们可以用下列命令手工检查 HPA:

$ kubectl get hpa
$ kubectl describe hpa keda-hpa-podinfo

并使用下列命令提交该对象:

$ kubectl apply -f scaled-object.yaml

接下来可以测试扩展效果了。在 Locust 仪表板中用下列设置启动一项实验:

可以看到,副本的数量增加了!

效果不错,但有个问题不知道你是否注意到。

当该部署扩展到 8 个 Pod 后,需要等待几分钟,随后才能在新节点中创建新的 Pod。在这段时间里,每秒处理的请求数量也不再增加了,因为当前的 8 个副本每个都只能处理 10 个请求。

让我们试试看收缩容量并重复该实验:

kubectl scale deployment/podinfo --replicas=4 # or wait for the autoscaler to remove pods

这次,我们将用一个占位 Pod 实现超量配置(Overprovision):

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
 name: overprovisioning
value: -1
globalDefault: false
description: "Priority class used by overprovisioning."
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     priorityClassName: overprovisioning
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.9G

提交至集群,然后打开 Locust 仪表板并用下列设置重复实验:

这一次,新节点将在后台创建,每秒请求数量将持续增减,不会原地踏步。

三、Kubernetes 的多区域扩展

对 Kubernetes 来说,跨越多个地域(Region)部署工作负载是个有趣的挑战。一种比较流行的方法是在每个地域部署一个集群,然后设法对多个集群进行必要的编排。

我们可以让一个集群中的节点分布在不同地域,或者在每个地域部署一个集群。接下来就一起看看如何:

  1. 分别在北美、欧洲和东南亚各自创建一个集群。
  2. 创建第四个集群,将其作为上述三个集群的编排器。
  3. 设置一个将三个集群连接在一起的网络,从而实现跨集群的无缝通信。

1. 创建集群管理器

首先创建用于管理其余集群的集群。我们可以通过下列命令创建该集群并保存 Kubeconfig 文件。

$ linode-cli lke cluster-create \
 --label cluster-manager \
 --region eu-west \
 --k8s_version 1.23
$ linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig-cluster-manager

随后通过下列命令验证安装过程已成功完成:

$ kubectl get pods -A --kubeconfig=kubeconfig-cluster-manager

还需要在集群管理器中安装 Karmada,这个管理系统可以帮助我们跨越多个 Kubernetes 集群或多个云平台运行自己的云原生应用程序。Karmada 是一种安装在集群管理器中的控制平面,其他集群中需要安装代理程序。

理论部分说的差不多了,接下来看看具体要用的代码。可以用 Helm 安装 Karmada API 服务器:

$ helm repo add karmada-charts https://raw.githubusercontent.com/karmada-io/karmada/master/charts
$ helm repo list
NAME            URL
karmada-charts   https://raw.githubusercontent.com/karmada-io/karmada/master/charts

由于 Karmada API 服务器必须能被所有其他集群访问,因此我们必须:

  • 从节点上将其暴露出来;并且
  • 确保连接是可信任的。

首先通过下列命令获取承载了控制平面的节点的 IP 地址:

kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="ExternalIP")].address}' \
 --kubeconfig=kubeconfig-cluster-manager

随后用下列命令安装 Karmada 控制平面:

$ helm install karmada karmada-charts/karmada \
 --kubeconfig=kubeconfig-cluster-manager \
 --create-namespace --namespace karmada-system \
 --version=1.2.0 \
 --set apiServer.hostNetwork=false \
 --set apiServer.serviceType=NodePort \
 --set apiServer.nodePort=32443 \
 --set certs.auto.hosts[0]="kubernetes.default.svc" \
 --set certs.auto.hosts[1]="*.etcd.karmada-system.svc.cluster.local" \
 --set certs.auto.hosts[2]="*.karmada-system.svc.cluster.local" \
 --set certs.auto.hosts[3]="*.karmada-system.svc" \
 --set certs.auto.hosts[4]="localhost" \
 --set certs.auto.hosts[5]="127.0.0.1" \
 --set certs.auto.hosts[6]="<insert the IP address of the node>"

安装完成后,通过下列命令获得 Kubeconfig 并连接到 Karmada API:

kubectl get secret karmada-kubeconfig \
 --kubeconfig=kubeconfig-cluster-manager \
 -n karmada-system \
 -o jsonpath={.data.kubeconfig} | base64 -d > karmada-config

不过为什么这里要用另一个 Kubeconfig 文件?

按照设计,Karmada API 是为了取代标准的 Kubernetes API,同时依然提供了用户需要的全部功能。换句话说,我们可以借助 kubectl 创建横跨多个集群的部署。

在测试 Karmada API 和 kubectl 之前,还需要调整 Kubeconfig 文件。默认情况下生成的 Kubeconfig 只能在集群网络的内部使用,不过只需调整这几行内容就可以消除这一限制:

apiVersion: v1
kind: Config
clusters:
 - cluster:
     certificate-authority-data: LS0tLS1CRUdJTi…
     insecure-skip-tls-verify: false
     server: https://karmada-apiserver.karmada-system.svc.cluster.local:5443 # <- this works only in the cluster
   name: karmada-apiserver
# truncated

将之前获取的节点 IP 地址替换进去:

apiVersion: v1
kind: Config
clusters:
 - cluster:
     certificate-authority-data: LS0tLS1CRUdJTi…
     insecure-skip-tls-verify: false
     server: https://<node's IP address>:32443 # <- this works from the public internet
   name: karmada-apiserver
# truncated

接下来就可以开始测试 Karmada 了。

2. 安装 Karmada 代理程序

运行下列命令检索所有部署和所有集群:

$ kubectl get clusters,deployments --kubeconfig=karmada-config
No resources found

可想而知,目前没有任何部署,也没有任何额外的集群。

我们可以添加几个集群并将其连接到 Karmada 控制平面,为此需要重复执行下列命令三次:

linode-cli lke cluster-create \
 --label <insert-cluster-name> \
 --region <insert-region> \
 --k8s_version 1.23
linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig-<insert-cluster-name>

执行时分别使用如下的值:

Cluster name eu, region eu-west 以及 kubeconfig file kubeconfig-eu
Cluster name ap, region ap-south 以及 kubeconfig file kubeconfig-ap
Cluster name us, region us-west 以及 kubeconfig file kubeconfig-us

随后通过下列命令确认集群已经成功创建:

$ kubectl get pods -A --kubeconfig=kubeconfig-eu
$ kubectl get pods -A --kubeconfig=kubeconfig-ap
$ kubectl get pods -A --kubeconfig=kubeconfig-us

接下来要将这些集群加入 Karmada 集群。Karmada 需要在其他每个集群中使用代理程序来协调控制平面的部署。

我们可以使用 Helm 安装 Karmada 代理程序并将其链接至集群管理器:

$ helm install karmada karmada-charts/karmada \
 --kubeconfig=kubeconfig-<insert-cluster-name> \
 --create-namespace --namespace karmada-system \
 --version=1.2.0 \
 --set installMode=agent \
 --set agent.clusterName=<insert-cluster-name> \
 --set agent.kubeconfig.caCrt=<karmada kubeconfig certificate authority> \
 --set agent.kubeconfig.crt=<karmada kubeconfig client certificate data> \
 --set agent.kubeconfig.key=<karmada kubeconfig client key data> \
 --set agent.kubeconfig.server=https://<insert node's IP address>:32443 \

上述命令同样需要重复三次,每次分别插入下列变量:

  • 集群名称:分别为 eu、ap 和 us。
  • 集群管理器的证书授权机构。我们可以在 karmada-config 文件的 clusters [0].cluster ['certificate-authority-data'] 中找到该值,这些值可以通过 base64 进行解码。
  • 用户的客户端证书数据。我们可以在 karmada-config 文件的 users [0].user ['client-certificate-data'] 中找到该值,这些值可以通过 base64 进行解码。
  • 用户的客户端密钥数据。我们可以在 karmada-config 文件的 users [0].user ['client-key-data'] 中找到该值,这些值可以通过 base64 进行解码。
  • 承载 Karmada 控制平面的节点的 IP 地址。

3. 借助 Karmada Policies 编排多集群部署

只要配置正确无误,即可将工作负载提交给 Karmada,由它将任务分发给其他集群。

为了进行测试,首先需要创建一个部署:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: hello
spec:
 replicas: 3
 selector:
   matchLabels:
     app: hello
 template:
   metadata:
     labels:
       app: hello
   spec:
     containers:
       - image: stefanprodan/podinfo
         name: hello
---
apiVersion: v1
kind: Service
metadata:
 name: hello
spec:
 ports:
   - port: 5000
     targetPort: 9898
 selector:
   app: hello

随后通过下列命令将该部署提交至 Karmada API 服务器:

$ kubectl apply -f deployment.yaml --kubeconfig=karmada-config

该部署包含三个副本,那么是否可以平均分发给这三个集群?一起来验证一下:

$ kubectl get deployments --kubeconfig=karmada-config
NAME    READY   UP-TO-DATE   AVAILABLE
hello   0/3     0            0

Karmada 为何没有创建 Pod?先来看看这个部署:

$ kubectl describe deployment hello --kubeconfig=karmada-config
Name:                   hello
Namespace:              default
Selector:               app=hello
Replicas:               3 desired | 0 updated | 0 total | 0 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Events:
 Type     Reason             From               Message
 ----     ------             ----               -------
 Warning  ApplyPolicyFailed  resource-detector  No policy match for resource

Karmada 并不知道该如何处理这个部署,因为我们尚未指定策略。

Karmada 调度器会使用策略将工作负载分配给集群。那么我们定义一个简单的策略,为每个集群分配一个副本:

apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
 name: hello-propagation
spec:
 resourceSelectors:
   - apiVersion: apps/v1
     kind: Deployment
     name: hello
   - apiVersion: v1
     kind: Service
     name: hello
 placement:
   clusterAffinity:
     clusterNames:
       - eu
       - ap
       - us
   replicaScheduling:
     replicaDivisionPreference: Weighted
     replicaSchedulingType: Divided
     weightPreference:
       staticWeightList:
         - targetCluster:
             clusterNames:
               - us
           weight: 1
         - targetCluster:
             clusterNames:
               - ap
           weight: 1
         - targetCluster:
             clusterNames:
               - eu
           weight: 1

将该策略提交给集群,然后再来看看部署和 Pod:

$ kubectl get deployments --kubeconfig=karmada-config
NAME    READY   UP-TO-DATE   AVAILABLE
hello   3/3     3            3
$ kubectl get pods --kubeconfig=kubeconfig-eu
NAME                    READY   STATUS    RESTARTS
hello-5d857996f-hjfqq   1/1     Running   0
$ kubectl get pods --kubeconfig=kubeconfig-ap
NAME                    READY   STATUS    RESTARTS
hello-5d857996f-xr6hr   1/1     Running   0
$ kubectl get pods --kubeconfig=kubeconfig-us
NAME                    READY   STATUS    RESTARTS
hello-5d857996f-nbz48   1/1     Running   0

Karmada 会为每个集群分配一个 Pod,因为策略中为每个集群定义了相等的权重。

我们用下列命令将该部署扩展为 10 个副本:

$ kubectl scale deployment/hello --replicas=10 --kubeconfig=karmada-config

随后查看 Pod 会看到如下的结果:

$ kubectl get deployments --kubeconfig=karmada-config
NAME    READY   UP-TO-DATE   AVAILABLE
hello   10/10   10           10
$ kubectl get pods --kubeconfig=kubeconfig-eu
NAME                    READY   STATUS    RESTARTS
hello-5d857996f-dzfzm   1/1     Running   0
hello-5d857996f-hjfqq   1/1     Running   0
hello-5d857996f-kw2rt   1/1     Running   0
hello-5d857996f-nz7qz   1/1     Running   0
$ kubectl get pods --kubeconfig=kubeconfig-ap
NAME                    READY   STATUS    RESTARTS
hello-5d857996f-pd9t6   1/1     Running   0
hello-5d857996f-r7bmp   1/1     Running   0
hello-5d857996f-xr6hr   1/1     Running   0
$ kubectl get pods --kubeconfig=kubeconfig-us
NAME                    READY   STATUS    RESTARTS
hello-5d857996f-nbz48   1/1     Running   0
hello-5d857996f-nzgpn   1/1     Running   0
hello-5d857996f-rsp7k   1/1     Running   0

随后修改策略,让 EU 和 US 集群各承载 40% 的 Pod,让 AP 集群只承载 20%。

apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
 name: hello-propagation
spec:
 resourceSelectors:
   - apiVersion: apps/v1
     kind: Deployment
     name: hello
   - apiVersion: v1
     kind: Service
     name: hello
 placement:
   clusterAffinity:
     clusterNames:
       - eu
       - ap
       - us
   replicaScheduling:
     replicaDivisionPreference: Weighted
     replicaSchedulingType: Divided
     weightPreference:
       staticWeightList:
         - targetCluster:
             clusterNames:
               - us
           weight: 2
         - targetCluster:
             clusterNames:
               - ap
           weight: 1
         - targetCluster:
             clusterNames:
               - eu
           weight: 2

提交策略后可以看到,Pod 的分配情况也酌情产生了变化:

$ kubectl get pods --kubeconfig=kubeconfig-eu
NAME                    READY   STATUS    RESTARTS   AGE
hello-5d857996f-hjfqq   1/1     Running   0          6m5s
hello-5d857996f-kw2rt   1/1     Running   0          2m27s
$ kubectl get pods --kubeconfig=kubeconfig-ap
hello-5d857996f-k9hsm   1/1     Running   0          51s
hello-5d857996f-pd9t6   1/1     Running   0          2m41s
hello-5d857996f-r7bmp   1/1     Running   0          2m41s
hello-5d857996f-xr6hr   1/1     Running   0          6m19s
$ kubectl get pods --kubeconfig=kubeconfig-us
hello-5d857996f-nbz48   1/1     Running   0          6m29s
hello-5d857996f-nzgpn   1/1     Running   0          2m51s
hello-5d857996f-rgj9t   1/1     Running   0          61s
hello-5d857996f-rsp7k   1/1     Running   0          2m51s

Pod 在三个集群中运行,但我们该如何访问?

先来看看 Karmada 中的服务:

$ kubectl describe service hello --kubeconfig=karmada-config
Name:              hello
Namespace:         default
Labels:            propagationpolicy.karmada.io/name=hello-propagation
                  propagationpolicy.karmada.io/namespace=default
Selector:          app=hello
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.105.24.193
IPs:               10.105.24.193
Port:              <unset>  5000/TCP
TargetPort:        9898/TCP
Events:
 Type     Reason                  Message
 ----     ------                  -------
 Normal   SyncSucceed             Successfully applied resource(default/hello) to cluster ap
 Normal   SyncSucceed             Successfully applied resource(default/hello) to cluster us
 Normal   SyncSucceed             Successfully applied resource(default/hello) to cluster eu
 Normal   AggregateStatusSucceed  Update resourceBinding(default/hello-service) with AggregatedStatus successfully.
 Normal   ScheduleBindingSucceed  Binding has been scheduled
 Normal   SyncWorkSucceed         Sync work of resourceBinding(default/hello-service) successful.

这些服务被部署在全部的三个集群中,但彼此之间并未连接。

尽管 Karmada 可以管理多个集群,但它并未提供任何网络机制将这三个集群连接在一起。换句话说,Karmada 是一种跨越多个集群编排部署的好工具,但我们需要通过其他机制让这些集群相互通信。

4. 使用 Istio 连接多个集群

Istio 通常被用于控制同一个集群中应用程序之间的网络流量,它可以检查所有传入和传出的请求,并通过 Envoy 以代理的方式发送这些请求。

Istio 控制平面负责更新并收集来自这些代理的指标,还可以发出指令借此转移流量。

借助 Istio,我们可以定义策略来管理集群中的流量

 

因此我们可以用 Istio 拦截到特定服务的所有流量,并将其重定向至三个集群之一。这就是所谓的 Istio 多集群配置。为此首先要在三个集群中安装 Istio:

$ helm repo add istio https://istio-release.storage.googleapis.com/charts
$ helm repo list
NAME            URL
istio                 https://istio-release.storage.googleapis.com/charts

用下列命令将 Istio 安装给三个集群:

$ helm install istio-base istio/base \
 --kubeconfig=kubeconfig-<insert-cluster-name> \
 --create-namespace --namespace istio-system \
 --version=1.14.1

将 cluster-name 分别替换为 ap、eu 和 us,并将该命令同样执行三遍。

Base chart 将只安装通用资源,例如 Roles 和 RoleBindings。实际的安装会被打包到 istiod chart 中。但在执行该操作前,我们首先需要配置 Istio Certificate Authority (CA),以确保这些集群可以相互连接和信任。

在一个新目录中使用下列命令克隆 Istio 代码库:

$ git clone https://github.com/istio/istio

创建一个 certs 文件夹并进入该目录:

$ mkdir certs
$ cd certs

使用下列命令创建根证书:

$ make -f ../istio/tools/certs/Makefile.selfsigned.mk root-ca

该命令将生成下列文件:

  • root-cert.pem:生成的根证书
  • root-key.pem:生成的根密钥
  • root-ca.conf:供 OpenSSL 生成根证书的配置
  • root-cert.csr:为根证书生成的 CSR

对于每个集群,还需要为 Istio Certificate Authority 生成一个中间证书和密钥:

$ make -f ../istio/tools/certs/Makefile.selfsigned.mk cluster1-cacerts
$ make -f ../istio/tools/certs/Makefile.selfsigned.mk cluster2-cacerts
$ make -f ../istio/tools/certs/Makefile.selfsigned.mk cluster3-cacerts

上述命令会在名为 cluster1、cluster2 和 cluster3 的目录下生成下列文件:

$ kubectl create secret generic cacerts -n istio-system \
 --kubeconfig=kubeconfig-<cluster-name>
 --from-file=<cluster-folder>/ca-cert.pem \
 --from-file=<cluster-folder>/ca-key.pem \
 --from-file=<cluster-folder>/root-cert.pem \
 --from-file=<cluster-folder>/cert-chain.pem

我们需要使用下列变量执行这些命令:

| cluster name | folder name |
| :----------: | :---------: |
|      ap      |  cluster1   |
|      us      |  cluster2   |
|      eu      |  cluster3   |

上述操作完成后,可以安装 istiod 了:

$ helm install istiod istio/istiod \
 --kubeconfig=kubeconfig-<insert-cluster-name> \
 --namespace istio-system \
 --version=1.14.1 \
 --set global.meshID=mesh1 \
 --set global.multiCluster.clusterName=<insert-cluster-name> \
 --set global.network=<insert-network-name>

使用下列变量将上述命令重复执行三遍:

| cluster name | network name |
| :----------: | :----------: |
|      ap      |   network1   |
|      us      |   network2   |
|      eu      |   network3   |

我们还可以使用拓扑注释来标记 Istio 的命名空间:

$ kubectl label namespace istio-system topology.istio.io/network=network1 --kubeconfig=kubeconfig-ap
$ kubectl label namespace istio-system topology.istio.io/network=network2 --kubeconfig=kubeconfig-us
$ kubectl label namespace istio-system topology.istio.io/network=network3 --kubeconfig=kubeconfig-eu

5. 通过东西网关为流量创建隧道

接下来我们还需要:

  • 一个网关,借此通过隧道将流量从一个集群发送到另一个
  • 一种机制,借此发现其他集群中的 IP 地址

可以使用 Helm 安装网关:

$ helm install eastwest-gateway istio/gateway \
 --kubeconfig=kubeconfig-<insert-cluster-name> \
 --namespace istio-system \
 --version=1.14.1 \
 --set labels.istio=eastwestgateway \
 --set labels.app=istio-eastwestgateway \
 --set labels.topology.istio.io/network=istio-eastwestgateway \
 --set labels.topology.istio.io/network=istio-eastwestgateway \
 --set networkGateway=<insert-network-name> \
 --set service.ports[0].name=status-port \
 --set service.ports[0].port=15021 \
 --set service.ports[0].targetPort=15021 \
 --set service.ports[1].name=tls \
 --set service.ports[1].port=15443 \
 --set service.ports[1].targetPort=15443 \
 --set service.ports[2].name=tls-istiod \
 --set service.ports[2].port=15012 \
 --set service.ports[2].targetPort=15012 \
 --set service.ports[3].name=tls-webhook \
 --set service.ports[3].port=15017 \
 --set service.ports[3].targetPort=15017 \

使用下列变量将上述命令执行三遍:

| cluster name | network name |
| :----------: | :----------: |
|      ap      |   network1   |
|      us      |   network2   |
|      eu      |   network3   |

随后对于每个集群,使用下列资源暴露一个网关:

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
 name: cross-network-gateway
spec:
 selector:
   istio: eastwestgateway
 servers:
   - port:
       number: 15443
       name: tls
       protocol: TLS
     tls:
       mode: AUTO_PASSTHROUGH
     hosts:
       - "*.local"

并使用下列命令将文件提交至集群:

$ kubectl apply -f expose.yaml --kubeconfig=kubeconfig-eu
$ kubectl apply -f expose.yaml --kubeconfig=kubeconfig-ap
$ kubectl apply -f expose.yaml --kubeconfig=kubeconfig-us

对于发现机制,我们需要共享每个集群的凭据。这是因为集群并不知道彼此的存在。

为了发现其他 IP 地址,集群必须能彼此访问,并将这些集群注册为流量的可能目的地。为此必须使用其他集群的 kubeconfig 文件创建一个 Kubernetes secret。

我们需要三个 Secret:

apiVersion: v1
kind: Secret
metadata:
 labels:
   istio/multiCluster: true
 annotations:
   networking.istio.io/cluster: <insert cluster name>
 name: "istio-remote-secret-<insert cluster name>"
type: Opaque
data:
 <insert cluster name>: <insert cluster kubeconfig as base64>

使用下列变量创建这三个 Secret:

| cluster name | secret filename |  kubeconfig   |
| :----------: | :-------------: | :-----------: |
|      ap      |  secret1.yaml   | kubeconfig-ap |
|      us      |  secret2.yaml   | kubeconfig-us |
|      eu      |  secret3.yaml   | kubeconfig-eu |

接下来需要向集群提交 Secret,但是请注意,不要将 AP 的 Secret 提交给 AP 集群。

为此需要执行下列命令:

$ kubectl apply -f secret2.yaml -n istio-system --kubeconfig=kubeconfig-ap
$ kubectl apply -f secret3.yaml -n istio-system --kubeconfig=kubeconfig-ap
$ kubectl apply -f secret1.yaml -n istio-system --kubeconfig=kubeconfig-us
$ kubectl apply -f secret3.yaml -n istio-system --kubeconfig=kubeconfig-us
$ kubectl apply -f secret1.yaml -n istio-system --kubeconfig=kubeconfig-eu
$ kubectl apply -f secret2.yaml -n istio-system --kubeconfig=kubeconfig-eu

至此,大部分操作已经完成,可以开始测试整个配置了。

6. 测试多集群网络连接

首先为一个睡眠中的 Pod 创建一个部署。可以使用该 Pod 向刚才创建的 Hello 部署发出请求:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: sleep
spec:
 selector:
   matchLabels:
     app: sleep
 template:
   metadata:
     labels:
       app: sleep
   spec:
     terminationGracePeriodSeconds: 0
     containers:
       - name: sleep
         image: curlimages/curl
         command: ["/bin/sleep", "3650d"]
         imagePullPolicy: IfNotPresent
         volumeMounts:
           - mountPath: /etc/sleep/tls
             name: secret-volume
     volumes:
       - name: secret-volume
         secret:
           secretName: sleep-secret
           optional: true

请用下列命令创建部署:

$ kubectl apply -f sleep.yaml --kubeconfig=karmada-config

因为该部署尚未指定策略,Karmada 将不处理该部署,使其处于 “未决” 状态。我们可以修改策略以包含该部署:

apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
 name: hello-propagation
spec:
 resourceSelectors:
   - apiVersion: apps/v1
     kind: Deployment
     name: hello
   - apiVersion: v1
     kind: Service
     name: hello
   - apiVersion: apps/v1
     kind: Deployment
     name: sleep
 placement:
   clusterAffinity:
     clusterNames:
       - eu
       - ap
       - us
   replicaScheduling:
     replicaDivisionPreference: Weighted
     replicaSchedulingType: Divided
     weightPreference:
       staticWeightList:
         - targetCluster:
             clusterNames:
               - us
           weight: 2
         - targetCluster:
             clusterNames:
               - ap
           weight: 2
         - targetCluster:
             clusterNames:
               - eu
           weight: 1

使用下列命令应用该策略:

$ kubectl apply -f policy.yaml --kubeconfig=karmada-config

要了解该 Pod 被部署到哪里,可以使用下列命令:

$ kubectl get pods --kubeconfig=kubeconfig-eu
$ kubectl get pods --kubeconfig=kubeconfig-ap
$ kubectl get pods --kubeconfig=kubeconfig-us

接下来,假设该 Pod 被部署到 US 集群,请执行下列命令:

for i in {1..10}
do
 kubectl exec --kubeconfig=kubeconfig-us -c sleep \
   "$(kubectl get pod --kubeconfig=kubeconfig-us -l \
   app=sleep -o jsonpath='{.items[0].metadata.name}')" \
   -- curl -sS hello:5000 | grep REGION
done

我们将会发现,响应会来自不同地域的不同 Pod!搞定!

总结

作为可移植、可扩展的开源平台,Kubernetes 帮助容器化应用程序的开发人员开发更可靠的基础架构,来快速响应高峰流量或重启失败等关键事件,并通过创建可扩展的容器组或 Pod 来优化云基础设施的容器编排部署和管理,为基础设施提供自动化修复能力。

Kubernetes 已成为云计算领域高频应用的抢手工具,选择经济易用、可持续性高的托管式 Kubernetes 服务尤为重要。Akamai Linode Kubernetes Engine (LKE) 正是专为开发者量身打造的云托管利器。在 Akamai Linode 平台上,无需支付高昂费用即可访问我们卓越、高效、广泛分布的基础架构。赶快关注 Akamai 机构号获取更多运维干货和云计算新资讯吧!


如果你喜欢我们的文章,欢迎关注我们↓↓↓

了解更多