微服务:kubernetes StatefulSet应用部署

郎家岭伯爵 2022年05月20日 440次浏览

背景

前面我们介绍了kubernetes集群部署service应用部署

此前的部署案例是无需存储数据的,不需要记录状态,可以随意扩充副本,每个副本都是一样且可替代的;针对数据库、Redis等需要存储数据、记录状态的应用,则不能随意扩充副本。

针对这种应用场景,K8S为我们提供了StatefulSet

实现

StatefulSet特性

  • Service的CLUSTER-IP是空的,Pod名字也是固定的。
  • Pod的创建和销毁是有序的,创建是顺序的,销毁是逆序的。
  • Pod创建不会改变名字,但IP是会改变的,所以不能用IP直连。访问时,如果直接使用Service名字连接,会随机转发请求。

部署StatefulSet应用

删除已有部署

删除已有部署命令:

kubectl delete all --all

创建yaml文件

创建文件命令:

vim mongo.yaml

文件内容:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb
spec:
  serviceName: mongodb
  replicas: 3
  selector:
    matchLabels:
      app: mongodb
  template:
    metadata:
      labels:
        app: mongodb
    spec:
      containers:
        - name: mongo
          image: mongo:4.4
          # IfNotPresent 仅本地没有镜像时才远程拉,Always 永远都是从远程拉,Never 永远只用本地镜像,本地没有则报错
          imagePullPolicy: IfNotPresent
---
apiVersion: v1
kind: Service
metadata:
  name: mongodb
spec:
  selector:
    app: mongodb
  type: ClusterIP
  # HeadLess
  clusterIP: None
  ports:
    - port: 27017
      targetPort: 27017

部署yaml文件

部署yaml文件命令:

# 部署yaml文件
kubectl apply -f mongo.yaml

# 查看pod
kubectl get pod

# 查看service
kubectl get service

查看statefulset副本:

kubectl get statefulset

访问指定Pod

访问StatefulSet应用时,如果直接用Service名字访问,则会随机转发到不同的Pod

minikube中可通过pod-name.service-name的方式来访问指定的Pod。但在集群中经过博主多次测试,无法实现这种方式访问指定的Pod。或许博主水平有限,大家如有高明的见解可在评论区留言。

那么我们怎样来访问指定的Pod呢?

例如我们要访问mongodb-0这个Pod,则可以通过如下命令进入Pod内部:


# 注意双横杠与bash之间有一个空格
kubectl exec -it mongodb-0 -- bash

# 接下来输入mongo,访问mongodb数据库
mongo

记录一个坑

问题

当我们创建一个临时的mongodb的Pod,创建命令如下:

kubectl run mongodb-client --rm --tty -i --restart='Never' --image docker.io/bitnami/mongodb:4.4.10-debian-10-r20 --command -- bash

# mongodb-client:Pod的名称
# --rm:临时Pod,退出即会删除

连接mongodb数据库:

mongo

此时会出现如下报错:

MongoDB shell version v4.4.10
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Error: couldn't connect to server 127.0.0.1:27017, connection attempt failed: SocketException: Error connecting to 127.0.0.1:27017 :: caused by :: Connection refused :
connect@src/mongo/shell/mongo.js:374:17
@(connect):2:6
exception: connect failed
exiting with code 1

原因

这是因为未启动数据库服务就去连接数据库。

解决

启动数据库服务即可。

# 直接启动
mongod

或者

# 更改指定运行路径启动。推荐使用!
mongod --dbpath '/usr/data/db'

创建目录:

# 进入usr目录
cd usr/

# 创建目录
# -p:递归创建目录
mkdir -p data/db/

连接mongodb数据库:

注:

  • 当前shell窗口需要保持开启mongodb服务,因此连接mongodb需要在一个新的shell窗口进行连接。

数据持久化

kubernetes集群不会处理数据的存储,因此当Pod重建时,数据就会丢失

我们可以为数据库挂载一个磁盘来确保数据的安全,例如可以通过云存储、本地磁盘、NFS等方式。

  • 云存储:不限定节点,不受集群影响,安全稳定。需要云服务商提供,裸机集群是没有的。
  • 本地磁盘:可以挂载在某个节点上的目录,但是需要限定Pod运行在这个节点上。
  • NFS:不限定节点,不受集群影响。

方式一:hostPath挂载

hostPath挂载,把节点上的一个目录挂载在Pod。仅供minikube单节点测试使用,不适用于多节点集群。

此方式不推荐使用,因此这里不再赘述。

方式二:挂载磁盘(推荐)

理论部分

通过多层抽象来实现磁盘的挂载,继而实现数据的持久化保存。

多层抽象的作用:

  • 更好地分工。运维人员负责提供好存储,开发人员不需要关注磁盘的细节,只需要写一个申请单即可。
  • 方便云服务商提供不同类型的配置,配置细节不需要开发者关注,主要一个申请单。
  • 动态创建,开发人员写好申请单后,供应商根据需求自动创建所需存储卷。

各抽象层的作用:

Storage Class(SC)
将存储卷划分为不同的种类,例如:SSD,普通磁盘,本地磁盘,按需使用。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: slow
provisioner: kubernetes.io/aws-ebs
parameters:
  type: io1
  iopsPerGB: "10"
  fsType: ext4

Persistent Volume(PV)
描述卷的具体信息,例如磁盘大小、访问模式

apiVersion: v1
kind: PersistentVolume
metadata:
  name: mongodata
spec:
  capacity:
    storage: 2Gi
  volumeMode: Filesystem  # Filesystem(文件系统) Block(块)
  accessModes:
    - ReadWriteOnce       # 卷可以被一个节点以读写方式挂载
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /root/data
  nodeAffinity:
    required:
      # 通过 hostname 限定在某个节点创建存储卷
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - node2

Persistent Volume Claim(PVC):

对存储需求的一个申明,可以理解为一个申请单,系统根据这个申请单去找一个合适的PV。还可以根据PVC自动创建PV。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongodata
spec:
  accessModes: ["ReadWriteOnce"]
  storageClassName: "local-storage"
  resources:
    requests:
      storage: 2Gi

创建示例

云盘

使用云盘存储,需要同步使用云服务商的kubernetes集群。本文中的案例是本地裸机搭建,因此该部分暂时略过。

本地磁盘

不支持动态创建,需要提前创建好。

注:

  • 数据卷挂载的节点以及节点中的路径要确保存在,否则挂载会失败的。
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb
spec:
  serviceName: mongodb
  replicas: 1
  selector:
    matchLabels:
      app: mongodb
  template:
    metadata:
      labels:
        app: mongodb
    spec:
      containers:
        - image: mongo:5.0
          imagePullPolicy: IfNotPresent
          name: mongo
          volumeMounts:
            - mountPath: /data/db
              name: mongo-data
      volumes:
        - name: mongo-data
          persistentVolumeClaim:
             claimName: mongodata
---
apiVersion: v1
kind: Service
metadata:
  name: mongodb
spec:
  clusterIP: None
  ports:
  - port: 27017
    protocol: TCP
    targetPort: 27017
  selector:
    app: mongodb
  type: ClusterIP
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mongodata
spec:
  capacity:
    storage: 2Gi
  volumeMode: Filesystem  # Filesystem(文件系统) Block(块)
  accessModes:
    - ReadWriteOnce       # 卷可以被一个节点以读写方式挂载
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /root/data
  nodeAffinity:
    required:
      # 通过 hostname 限定在某个节点创建存储卷
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - node2
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongodata
spec:
  accessModes: ["ReadWriteOnce"]
  storageClassName: "local-storage"
  resources:
    requests:
      storage: 2Gi

注:

  • vim编辑器中,可通过依次输入:Esc -- 冒号 -- set paste,进行粘贴。
  • vim编辑器中,可通过依次输入:Esc -- 冒号 -- %d,清空文件内容。

ConfigMap和Secret

之前数据库的地址以及账号密码都是固定在代码中的,接下来我们将要通过ConfigMapSecret提取出配置文件,更好地贴合实际开发。

ConfigMap

官方文档

configmap.yaml文件内容:

apiVersion: v1
kind: ConfigMap
metadata:
  name: mongo-config
data:
  mongoHost: mongodb-0.mongodb

部署以及查看:

# 应用
kubectl apply -f configmap.yaml

# 查看
kubectl get configmap mongo-config -o yaml

Secret

官方文档

一些重要数据,例如密码、TOKEN,我们可以放到secret中。

注:

secret.yaml文件内容:

apiVersion: v1
kind: Secret
metadata:
  name: mongo-secret
# Opaque 用户定义的任意数据,更多类型介绍 https://kubernetes.io/zh/docs/concepts/configuration/secret/#secret-types
type: Opaque
data:
  # 数据要 base64。https://tools.fun/base64.html
  mongo-username: bW9uZ291c2Vy
  mongo-password: bW9uZ29wYXNz

部署以及查看:

# 应用
kubectl apply -f secret.yaml

# 查看
kubectl get secret mongo-secret -o yaml

使用方法

作为环境变量使用

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb
spec:
  serviceName: mongodb
  replicas: 3
  selector:
    matchLabels:
      app: mongodb
  template:
    metadata:
      labels:
        app: mongodb
    spec:
      containers:
        - name: mongo
          image: mongo:4.4
          # IfNotPresent 仅本地没有镜像时才远程拉,Always 永远都是从远程拉,Never 永远只用本地镜像,本地没有则报错
          imagePullPolicy: IfNotPresent
          env:
          - name: MONGO_INITDB_ROOT_USERNAME
            valueFrom:
              secretKeyRef:
                name: mongo-secret
                key: mongo-username
          - name: MONGO_INITDB_ROOT_PASSWORD
            valueFrom:
              secretKeyRef:
                name: mongo-secret
                key: mongo-password
          # Secret 的所有数据定义为容器的环境变量,Secret 中的键名称为 Pod 中的环境变量名称
          # envFrom:
          # - secretRef:
          #     name: mongo-secret

挂载为文件(更适合证书文件)

官方文档

挂载后,会在容器中对应路径生成文件,一个key一个文件,内容就是value。

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  containers:
  - name: mypod
    image: redis
    volumeMounts:
    - name: foo
      mountPath: "/etc/foo"
      readOnly: true
  volumes:
  - name: foo
    secret:
      secretName: mysecret

总结

本文介绍了如何“优雅”地部署数据库等需记录状态的服务。

后续我们将继续介绍如何更快速地部署这些常规应用,如何通过类似于DockerHub拉取镜像的操作直接部署服务。

kubernetes为我们提供了Helm的操作方式。