WJ-Yuan / Notes

My Tech Notes
https://wj-yuan.github.io/Notes/
0 stars 0 forks source link

[Note]K8s #15

Open WJ-Yuan opened 9 months ago

WJ-Yuan commented 9 months ago

k8s

k8s 与 docker 区别

Docker 并非 k8s 唯一支持的容器运行时

k8s 概览

k8s 整体承担两个角色:

k8s 集群一般由一个主节点(master, 又称为 control plan 或者 head node)和若干子节点(node, 又称为 data plan)构成。 所谓 k8s 原生应用是指知道自己运行在 k8s 上的能够查询 k8s API 的应用。

# 查看 k8s 配置
kubectl config view

# 查看当前使用的 context
kubectl config current-context

# 设置当前 context
kubectl config use-context <context-name>

Pod

Pod 是 k8s 的管理基本单位。一般一个 Pod 包含一个容器,当然也可以又多个容器。

kubectl apply 将 yaml 文件 post 到 k8s 的 API server

# 基于清单文件部署 Pod
kubectl apply -f <Pod.yml> # e.g. kubectl apply -f Pod.yml

# 查看 Pod 状态
kubectl get Pods

# 查看具体 Pod 的状态
kubectl get Pods <Pod name> -o [wide|yaml] # e.g. kubectl get Pods hello-Pod -o yaml

# 在 Pod 的第一个容器中执行命令
kubectl exec <Pod name> -- <command> # e.g. kubectl exec hello-Pod -- ps aux

# 进入 Pod 的容器交互执行命令
kubectl exec -it <Pod name> -- [sh|/bin/bash] # e.g. kubectl exec -it hello-Pod -- /bin/bash
# 退出 Pod
exit

# 查看 Pod 日志
kubectl logs <Pod name>

# 删除 Pod
kubectl delete -f <Pod config yaml> # e.g. kubectl delete -f Pod.yml

Pod.yml

apiVersion: v1 # 定义了用来创建部署对象的 API 组和 API 版本,格式为 <api-group>/<api-version>,Pod 因为部署在 core 组上,因此可以省略
kind: Pod # 告诉 k8s 要部署的对象类型
metadata: # 对于部署对象的描述,可以用来定义名称和标签,一般还会知道 namespace,未指定会使用 default 命名空间
  name: hello-pod
  labels:
    zone: prod
    version: v1
spec: # 定义 Pod 中所运行的容器
  containers:
    - name: hello-ctr # 容器名称
      image: nigelpoulton/k8sbook:1.0 # 容器镜像
      ports:
        - containerPort: 8080

Deployment

# 应用 Deployment
kubectl apply -f <deploy.yml>

# 查看 Deployment
kubectl get deploy <deploy name> # e.g. kubectl get deploy hello-deploy

# 查看 Deployment 创建的 ReplicaSet
kubectl get rs

Deploy.yaml

apiVersion: apps/v1 # 指定使用的 API 版本,旧版 K8s 使用 apps/v1beta1
kind: Deployment # 告诉 k8s 定义的是一个 Deployment 对象
metadata: # 定义 Deployment 的名字和标签等
  name: hello-deploy
spec:
  replicas: 10 # 部署多少个 Pod 副本
  selector: # Deployment 管理的 Pod 所必须具备的标签
    matchLabels:
      app: hello-world
  revisionHistoryLimit: 5
  progressDeadlineSeconds: 300
  minReadySeconds: 10
  strategy: # k8s 如何执行更新操作
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  template: # Deployment 管理的 Pod 模板
    metadata:
      labels:
        app: hello-world
    spec:
      containers:
        - name: hello-pod
          image: nigelpoulton/k8sbook:latest
          ports:
            - containerPort: 8080

访问应用

为了经由一个固定的域名或 IP 地址来访问应用,甚至从集群外部来访问它,我们需要 k8s Service 对象(相关知识后续探讨)

滚动升级

更新 deploy.yml 清单文件

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-deploy
spec:
  replicas: 10
  selector:
    matchLabels:
      app: hello-world
  revisionHistoryLimit: 5
  progressDeadlineSeconds: 300
  minReadySeconds: 10 # k8s 每个 Pod 更新之间的间隔最小为 10s
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1 # 不允许出现比期望状态少超过一个的情况
      maxSurge: 1 # 不允许出现比期望状态多超过一个的情况
  template:
    metadata:
      labels:
        app: hello-world
    spec:
      containers:
        - name: hello-pod
          image: nigelpoulton/k8sbook:edge # 只有这一行修改了
          ports:
            - containerPort: 8080
# 更新
kubectl apply -f <deploy.yml> --record

# 查看更新过程
kubectl rollout status deployment <deployment name>

回滚

# 查看 deployment 的历史版本
kubectl rollout history deployment <deploy name>

# 回滚
kubectl rollout undo deployment <hello-deploy> --to-revision=1 # 回滚到版本1

# 删除
kubectl delete -f <deploy.yml>

Service

Pod 的 IP 地址是动态可增减的,不可靠,因此需要 Service

当通过 Service 转发流量到 Pod 时,通常会现在集群内部的 DNS 中查询 Service 的 IP 地址。流量被发送到该 IP 地址后,会被 Service 转发到其中一个 Pod。 k8s 元素应用是可以直接查询 Endpoint API,而无需查找 DNS 和使用 Service IP 的。

类型有 ClusterIP(默认,从集群内部访问可用), NodePort, LoadBalancer, ExternalName(可以将流量路由到集群之外的系统中去)

命令式创建 Service (不推荐)

# 创建 Service
kubectl expose deployment <deployment name> --name=<service name> --target-port=<target port> --type=<service type>

声明式创建

svc.yml

apiVersion: v1
kind: Service
metadata:
  name: svc-test
  labels:
    chapter: services
spec: # 这里是 Service 的配置
  # ipFamilyPolicy: PreferDualStack
  # ipFamilies:
  # - IPv4
  # - IPv6
  type: NodePort # 类型
  ports:
    - port: 8080 # Service 对集群内部暴露的端口
      nodePort: 30001 # Service 对集群外外部暴露的端口
      targetPort: 8080 # 应用 Pod 对外暴露的端口
      protocol: TCP # 协议
  selector:
    chapter: services # Service 的标签选择器
# 部署 Service
kubectl apply -f svc.yml

# 查看 Service
kubectl get svc [<service name>]
kubectl describe svc [<service name>]

# 删除 Service
kubectl delete svc <service name>

# 查看 Endpoint 对象
kubectl get ep <endpoint name>
kubectl describe ep <endpoint name>

服务发现 (Service Discovery)

服务发现包括两个主要组件

服务注册

服务注册就是把服务的 Service 的连接信息注册到服务仓库,以便其他微服务能够发现它并进行连接。

注册的具体过程如下:

注册后端使用的是 IPVS (Linux IP Virtual Server, 即 IP 虚拟服务器), 1.11 以前是 iptables,后端配置被存储在一个 Endpoint 对象中,同时承载流量的网络也就绪了。

整个过程如下:

POST Service 配置到 API Server --> 分配 Cluster IP --> 配置被持久化到集群存储 --> 维护有 Pod IP 的 Endpoint 创建 --> 集群 DNS 发现新的 Service --> 创建 DNS 记录 --> kube-proxy 拉取 Service 配置 --> 创建 IPVS 规则

服务发现

服务发现使用集群 DNS 将名称解析为 IP 地址:k8s 会为每个容器注入 /etc/resolv.conf 文件,其中有集群 DNS 服务的 IP 地址和搜索域,容器会先向集群 DNS 发送一个将名称为 <service name> 解析为 IP 地址的请求,集群 DNS 会返回 <service name> 的 ClusterIP,然后容器就知道往哪个 IP 发送请求了。

与命名空间相关的 Service 名称:<object-name>.<namespace-name>.svc.cluster.local。比如 namespace 为 dev 的名称为 ent 的 Service 名称为 et.dev.svc.cluster.local

让这个发送到的请求到达需要一些“黑科技”,因为这个 IP 地址在一个特殊的名为服务网络 (Service network) 的网络上,没有路由可达。

请求 DNS 解析 Service 名称 --> 收到 ClusterIP --> 发送流量至 ClusterIP --> 无路由。发送至容器的默认网关 --> 转发至节点 --> 无路由。发送至节点的默认网关 --> 被节点内核处理 --> 捕获 (IPSV) 规则 --> 将目标 IP 的值重写为 Pod 的 IP

实践

namespace-server.yml

apiVersion: v1
kind: Namespace
metadata:
  name: dev
--- # 同一 yaml 文件中,不同对象可以用 `---` 三个中划线隔开
apiVersion: v1
kind: Namespace
metadata:
  name: prod
---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: enterprise
  namespace: dev
  labels:
    app: enterprise
spec:
  selector:
    matchLabels:
      app: enterprise
  replicas: 2
  template:
    metadata:
      labels:
        app: enterprise
    spec:
      terminationGracePeriodSeconds: 1
      containers:
        - image: nigelpoulton/k8sbook:text-dev
          name: enterprise-ctr
          ports:
            - containerPort: 8080
---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: enterprise
  namespace: prod
  labels:
    app: enterprise
spec:
  selector:
    matchLabels:
      app: enterprise
  replicas: 2
  template:
    metadata:
      labels:
        app: enterprise
    spec:
      terminationGracePeriodSeconds: 1
      containers:
        - image: nigelpoulton/k8sbook:text-prod
          name: enterprise-ctr
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: ent
  namespace: dev
spec:
  ports:
    - port: 8080
  selector:
    app: enterprise
---
apiVersion: v1
kind: Service
metadata:
  name: ent
  namespace: prod
spec:
  ports:
    - port: 8080
  selector:
    app: enterprise
---
apiVersion: v1
kind: Pod
metadata:
  name: jump
  namespace: dev
spec:
  terminationGracePeriodSeconds: 5
  containers:
    - image: ubuntu
      name: jump
      tty: true
      stdin: true
# 部署
kubectl apply -f namespace-server.yml

# 获取 dev 命名空间下的所有内容
kubectl get all -n dev

# 进入 jump Pod
Kubectl exec -it jump -n dev -- /bin/bash

# 查看 /etc/resolv.conf 内容
cat /etc/resolv.conf

# 安装 curl
apt-get update && apt-get install curl -y

# 测试 dev 环境 ent
curl ent:8080

# 测试 prod 环境 ent
curl ent.prod.svc.cluster.svc:8080

服务端问题排查

# 确认 coredns Deployment 及其管理的 Pod 是运行状态
kubectl get deploy -n kube-system -l k8s-app=kube-dns
kubectl get Pods -n kube-system -l k8s-app=kube-dns

# 检查每个 Pod 的日志
kubectl logs <pod-name> -n kube-system

# 查看相关 Service 及其 Endpoint 对象
kubectl get svc kube-dns -n kube-system
kubectl get ep -n kube-system -l k8s-app=kube-dns

# 其他技巧
# 其一个 dnsutils 的节点来测试
kubectl run -it dnsutils --image gcr.io/kubernetes-e2e-test-images/dnsutils:latest
# 进入 dnsutils 输入命令测试
nslookup kubernetes
# 可以删掉系统 dns 来测试出错
kubectl delete Pod -n kube-system -l k8s-app=kube-dns

k8s 存储

k8s 的存储子系统被称为持久化卷子系统 (Persistent Volume Subsystem)。 k8s 支持多种途径的多种类型的存储,如 NFS, SMB, 对象存储等,他们在 k8s 统称为 (Volume)。k8s 通过 CSI(容器存储接口,拥有清晰接口的开放标准) 实现胶水层连接存储和持久化卷子系统,使各类型的存储以插件的形式映射到集群,使得 Pod 可以使用 PV。

存储提供者 -CSI-> PVS

PVS = PV(持久化卷,Persistent Volume) + PVC(持久化卷申请,Persistent Volume Claim) + SC(存储类,Storage Class)

创建 PV

gke-pv.yml

apiVersion: v1
kind: PersistentVolume # 类型
metadata:
  name: pv1
  labels:
    env: book
spec:
  accessModes: # 定义了 PV 是如何挂载的, 有 ReadWriteOnce(RWO),ReadWriteMany(RWM), ReadOnlyMany(ROM) 几种可选值
    # - ROW: 限制一个 PV 只能以读写方式被挂载或绑定到一个 PVC,尝试将其绑定到多个 PVC 会失败
    # - RWM:允许一个 PV 以读写方式被挂载或绑定到多个 PVC,这种模式通常只支持注入 NFS 这样的文件或对象存储,块存储只支持 RWO
    # - ROM:允许 PV 以制度的方式绑定到多个 PVC
    # 一个 PV 只能设置一种模式
    # Pod 不能直接与 PV 对接,而是必须通过 PVC 与某个 PV 绑定
    - ReadWriteOnce # PV 的访问模式是只读写
  storageClassName: test # 存储类设置为 test,让 K8S 将该 PV 归入名为 test 的存储类
  capacity: # 容量,告诉 k8s 这个 PV 的容量是多少,可以比实际的物理存储资源更少,不能更多
    storage: 10Gi
  # 用于定义在 PVC 被释放之后,如何处理对应的 PV
  # - Delete 默认策略,危险,会删除对应 PV 对象以及外部存储系统中关联的存储资源
  # - Retain 保留对应的 PV 对象以及外部存储系统中关联的存储资源,但是也会导致其他 PVC 无法使用该 PV
  # 如果要继续使用保留的 PV,则需要执行
  # 1. 手动删除该 PV
  # 2. 格式化外部存储系统中相对应的存储资源
  # 3. 重新创建 PV
  persistentVolumeReclaimPolicy: Retain # Reclaim 策略为 Retain
  gcePersistentDisk: # 将 PV 与后端存储上已创建的设备名称关联,如下 PV 被映射到已创建的 GCE 磁盘的 uber-disk 上
    pdName: uber-disk
# 创建 pv
kubectl apply -f gke-pv.yml

# 查看 pv 是否已创建
kubectl get pv pv1
kubectl describe pv pv1

创建 PVC

gke-pvc.yml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc1
spec: # PVC 的 spec 中的值必须与要绑定的 PV 一致, PV 可以拥有比 PVC 更大的容量
  accessModes:
    - ReadWriteOnce
  storageClassName: test
  resources:
    requests:
      storage: 10Gi
# 部署 PVC
kubectl apply -f gke-pvc.yml

# 查看 PVC 是否已经创建并绑定到 PV
kubectl get pvc pvc1

将 Pod 与 PVC 相关联

volPod.yml

apiVersion: v1
kind: Pod
metadata:
  name: volpod
spec:
  volumes:
  - name: data # 定义一个名为 data 的卷,使用名为 pvc1 的 PVC
    persistentVolumeClaim:
      claimName: pvc1
  containers:
  - name: ubuntu-ctr
    image: ubuntu:latest
    command:
    - /bin/bash
    - "-c"
    - "sleep 60m"
    volumeMounts:
    - mountPath: /data
      name: data

存储类

手动创建和维护大量的 PV 和 PVC 是难以完成的任务,因此需要一些动态制备的手段。因此产生了存储类。

存储类允许用户为存储定义不同的类或层(tier)。用户可以根据存储的类型来自行决定如何定义类。 k8s 中存储类被作为资源定义在 storage.k8s.io/v1 的 API 组中。

单个存储类

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: fast
provisioner: kubernetes.io/aws-ebs # 告诉 k8s 要使用哪个插件
parameters: # 和插件相关的参数
  type: io1
  zones: eu-west-1a
  iopsPerGB: '10'

以上定义了一个名为 fast 的存储类,存储后端是 AWS “爱尔兰” 域 eu-west-1a 上的固态硬盘 (io1),性能级别为 10 IOPs/GB

多个存储类

用户可以配置任意梳理的 StorageClass 对象,不过每个 SC 只能关联一个存储后端。但每个存储后端可以提供多个存储的类/层,进一步对应多个 SC 对象。

部署和使用 SC 对象的基本流程

  1. 创建 k8s 集群及其存储后端
  2. 确保与存储后端对应的插件是就绪的
  3. 创建一个 StorageClass 对象
  4. 创建一个 PVC 对象并通过名称与 SC 对象关联
  5. 部署一个 Pod,使用基于该 PVC 的卷

实践

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: slow # 由 pv-ticket 引用
  annotations:
    storageclass.kubernetes.io/is-default-class: 'true'
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-standard
reclaimPolicy: Retain
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pv-ticket # 由 class-pod 引用
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: slow # 匹配 SC 名称
  resources:
    requests:
      storage: 25Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: class-pod
spec:
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: pv-ticket # 匹配 PVC 名称
  containers:
    - name: ubuntu-ctr
      image: ubuntu:latest
      command:
        - /bin/bash
        - '-c'
        - 'sleep 60m'
      volumeMounts:
        - mountPath: /data
          name: data
# 创建
kubectl apply -f <yaml-config-name>

# 查看 SC
kubectl get sc <sc-name>
kubectl describe sc <sc-name>

# 查看 pvc
kubectl get pvc <pvc-name>
kubectl describe <pvc-name>

# 查看 pv
Kubectl get pv

# 删除 Pod
kubectl delete Pod <Pod-name>
# 删除 pvc
kubectl delete pvc <pvc-name>
# 删除 sc
kubectl delete sc <sc-name>

ConfigMap

多数业务系统由应用和配置两部分组成。k8s 中将应用与配置解耦,它们被分别构建和存储。比如我们打包时镜像都是同一个,但是部署时,读取了不同环境的配置。

k8s 通过提供一个名为 ConfigMap (CM) 的对象,将配置数据从 Pod 中剥离出来,使用它可以动态地在 Pod 运行时注入数据。其实我们从名字就可以看出来,是配置映射。

ConfigMap 可以命令式,也可以用声明文件。通常用于存储以下非敏感配置数据

不应该使用 ConfigMap 来存储凭证或密码等敏感信息,而是应该采用 Secret 来存储,k8s 会对 Secret 的值进行混淆。

命令式

# 创建 ConfigMap
# --from-file 指根据命令行中的字面值创建 ConfigMap
# --from-file 指根据文件创建
kubectl create configmap(可以简写为 cm) <config-map-name> [--from-literal <key>=<value>] [--from-file <filename>]
# kubectl create configmap testmap1 --from-literal shrotname=msb.com --from-literal longname=maficsandbox.com

# 查看
kubectl describe cm <config-map-name>
kubectl get cm [<config-map-name>] [-o yaml]
# 注意,ConfigMap 对象没有状态,因此它没有 spec 和 status 部分,取而代之的是 data

声明式

multimap.yml

kind: ConfigMap
apiVersion: v1
metadata:
  name: multimap
data:
  given: Nigel
  family: Poulton
kubectl apply -f multimap.yml

ConfigMap 注入 Pod 和 容器

作为环境变量

kubectl apply -f envPod.yml
kubectl exec envPod -- env | grep Name

multimap.yml

kind: ConfigMap
apiVersion: v1
metadata:
  name: multimap
data:
  given: Nigel
  family: Poulton

envPod.yml

apiVersion: v1
kind: Pod
metadata:
  labels:
    chapter: configmaps
  name: envpod
spec:
  containers:
    - name: ctr1
      image: busybox
      command: ['sleep']
      args: ['infinity']
      env: # 定义了两个会出现在容器中的环境变量
        - name: FIRSTNAME
          valueFrom:
            configMapKeyRef:
              name: multimap
              key: given # 与容器中的 given entry 应色号
        - name: LASTNAME
          valueFrom:
            configMapKeyRef:
              name: multimap
              key: family

与容器启动命令

startuppod.yml

apiVersion: v1
kind: Pod
metadata:
  name: startup-pod
  labels:
    chapter: configmaps
spec:
  restartPolicy: OnFailure
  containers:
    - name: args1
      image: busybox
      command: [
          '/bin/sh',
          '-c',
          'echo First name $(FIRSTNAME) last name $(LASTNAME)', # 有两个环境变量
          'wait',
        ]
      env:
        - name: FIRSTNAME
          valueFrom:
            configMapKeyRef:
              name: multimap
              key: given
        - name: LASTNAME
          valueFrom:
            configMapKeyRef:
              name: multimap
              key: family
# 创建容器
kubectl apply -f startuppod.yml
# 查看容器日志
kubectl logs <pod-name> -c args1

与卷

对卷的更新会同步到容器中。

cmPod.yml

apiVersion: v1
kind: Pod
metadata:
  name: cmvol
spec:
  volumes: # 定义了一个特殊类型的卷,称为 ConfigMap 卷
    - name: volmap # 卷的名称
      configMap:
        name: multimap # 基于 ConfigMap multimap
  containers:
    - name: ctr
      image: nginx
      volumeMounts:
        - name: volmap # 将 volmap 卷挂载到 /etc/name,我们可以在这里找到文件
          mountPath: /etc/name
kubectl apply -f cmPod.yml
kubectl exec volPod -- ls /etc/name

StatefulSet

用于部署和管理由状态的应用

StatefulSet 和 Deployment 都支持自愈、自动扩缩容、滚动更新等特性,但是 StatefulSet 可以确保

以上三者称为 Pod 的状态 ID(stick ID)。状态 ID 在及时发生故障、扩缩容以及其他调度操作后,依然保持不变。

StatefulSet 中的 Pod 名字遵循 <StatefulSet-name>-<Integer> 的规则,其中 integer 是一个从零开始的索引号,第一个创建的是 0。StatefulSet 的名字必须是有效的 DNS 名字。StatefulSet 对 Pod 的启动和停止是有序的,扩缩容也是。

删除 StatefulSet 并不是按序种植所有 Pod 的,因此删除前要将其缩容至 0.

因为 DNS 不变,所以其他应用可以通过向 DNS 查询所有 Pod 信息来实现对这些 Pod 的直接连接:

在二者如此关联后,Service 会为所匹配的每个 Pod 副本创建 DNS SRV 记录。其他 Pod 可以通过对 headless Service 发起 DNS 查询来获取 StatefulSet 的信息。

apiVersion: v1
kind: Service
metadata:
  name: mongo-prod
spec:
  ClusterIP: None
  selector:
    app: mongo
    env: prod
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: sts-mongo
spec:
  ServiceName: mongo-prod
# 部署
kubectl apply -f sts.yml

# 查看
kubectl get sts --watch

# 删除
# 先缩容到0
kubectl scale sts <sts-name> --replicas=0
# 删除
kubectl delete sts <sts-name>

域名格式 <object-name>.<service-name>.<namespace-name>.svc.cluster.local

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: tkb-sts
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  serviceName: 'dullahan'
  template:
    metadata:
      labels:
        app: web
    spec:
      terminationGracePeriodSeconds: 10
      containers:
        - name: ctr-web
          image: nginx:latest
          ports:
            - containerPort: 80
              name: web
          volumeMounts:
            - name: webroot
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: webroot
      spec:
        accessModes: ['ReadWriteOnce']
        storageClassName: 'flash'
        resources:
          requests:
            storage: 1Gi