• 71760

    文章

  • 657

    评论

  • 17

    友链

  • 最近新加了换肤功能,大家多来逛逛吧~~~~
  • 喜欢这个网站的朋友可以加一下QQ群,我们一起交流技术。

使用Statefulset在Kubernetes集群中部署etcd集群

撸了今年阿里、腾讯和美团的面试,我有一个重要发现.......>>

随着微服务架构的火爆,Etcd作为服务发现或者分部式存储的基础平台也越来越频繁的出现在我们的视野里。因此对于快速部署一套高可用的Etcd集群的需求也越来越强烈,本次就带领大家一起使用Kubernetes的Statefulset特性快速部署一套Etcd集群。

什么是Kubernetes?

Kubernetes 是一个用于容器集群的自动化部署、扩容以及运维的开源平台。

使用Kubernetes,你可以快速高效地响应客户需求:

  • 快速并且无意外的部署你的应用。
  • 动态地对应用进行扩容。
  • 无缝地发布新特性。
  • 仅使用需要的资源以优化硬件使用。

什么是Etcd?

Etcd的目的是提供一个分布式键值动态数据库,维护一个"Configuration Registry"。 这个Registry的基础之一是Kubernetes集群发现和集中的配置管理。 它在某些方面类似于Redis,经典的LDAP配置后端以及Windows注册表。

Etcd的目标是:

  • 简单:定义良好,面向用户的API(JSON and gRPC)
  • 安全:自动TLS和可选的客户端证书身份验证
  • 快速:benchmarked 写 10,000 次/秒
  • 可靠:使用Raft协议作为分布式基础

官方已经有了Etcd-Operator,我为什么还要使用这种方式部署?

首先来看优点:

  • Etcd-Operator的部署方式对Etcd版本和Kubernetes版本都有要求,详见官方文档
  • 如果使用Etcd v2 Api则无法对数据进行备份,官方的Etcd集群数据备份仅仅支持Etcd v3版本
  • Statefulset部署Etcd数据更加可靠。如果使用Etcd-Operator部署Etcd,不幸的是某天我的Kubernetes集群出现了问题,导致所有的Etcd Pod出现故障,那么很遗憾,如果没有对数据进行备份,我只能Pray God Bless,祈求自己不要背锅。即便使用Etcd V3 Api并且使用了数据备份,那么也可能丢失一部分数据,因为Etcd-Operator的备份是定时备份的。
  • Statefulset的配置更加灵活,比如需要亲和性的配置,可以直接在Statefulset中增加即可。

当然,任何美好的事务都不是完美的。 使用Statefulset部署Etcd也需要一定的条件:

  • 必须有可靠的网络存储作为支持。
  • 创建集群比Etcd-Operator略微繁琐。

好,接下来,让我们进入正题:

如何在Kubernetes上快速部署一套Etcd集群。

首先,在Kubernetes上创建一个Headless的Service

apiVersion: v1
kind: Service
metadata:
  labels:
    k8s-app: infra-etcd-cluster
    app: infra-etcd
  name: infra-etcd-cluster
  namespace: default
spec:
  clusterIP: None
  ports:
  - name: infra-etcd-cluster-2379
    port: 2379
    protocol: TCP
    targetPort: 2379
  - name: infra-etcd-cluster-2380
    port: 2380
    protocol: TCP
    targetPort: 2380
  selector:
    k8s-app: infra-etcd-cluster
    app: infra-etcd
  type: ClusterIP

创建Headless类型的Service是为了方便使用域名访问到Etcd的节点。2379和2380分别对应Etcd的Client Port和Peer Port。

接下来,让我们来创建Statefulset资源:

前提条件:你的集群必须提前创建好PV,以便Statefulset生成的Pod可以使用到。如果你使用StorageClass来管理PV,则无需手动创建。这里已Ceph-RBD为例。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  labels:
    k8s-app: infra-etcd-cluster
    app: etcd
  name: infra-etcd-cluster
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      k8s-app: infra-etcd-cluster
      app: etcd
  serviceName: infra-etcd-cluster
  template:
    metadata:
      labels:
        k8s-app: infra-etcd-cluster
        app: etcd
      name: infra-etcd-cluster
    spec:
      containers:
      - command:
        - /bin/sh
        - -ec
        - |
          HOSTNAME=$(hostname)
          echo "etcd api version is ${ETCDAPI_VERSION}"

          eps() {
              EPS=""
              for i in $(seq 0 $((${INITIAL_CLUSTER_SIZE} - 1))); do
                  EPS="${EPS}${EPS:+,}http://${SET_NAME}-${i}.${SET_NAME}.${CLUSTER_NAMESPACE}:2379"
              done
              echo ${EPS}
          }

          member_hash() {
              etcdctl member list | grep http://${HOSTNAME}.${SET_NAME}.${CLUSTER_NAMESPACE}:2380 | cut -d':' -f1 | cut -d'[' -f1
          }

          initial_peers() {
                PEERS=""
                for i in $(seq 0 $((${INITIAL_CLUSTER_SIZE} - 1))); do
                PEERS="${PEERS}${PEERS:+,}${SET_NAME}-${i}=http://${SET_NAME}-${i}.${SET_NAME}.${CLUSTER_NAMESPACE}:2380"
                done
                echo ${PEERS}
          }

          # etcd-SET_ID
          SET_ID=${HOSTNAME##*-}
          # adding a new member to existing cluster (assuming all initial pods are available)
          if [ "${SET_ID}" -ge ${INITIAL_CLUSTER_SIZE} ]; then
              export ETCDCTL_ENDPOINTS=$(eps)

              # member already added?
              MEMBER_HASH=$(member_hash)
              if [ -n "${MEMBER_HASH}" ]; then
                  # the member hash exists but for some reason etcd failed
                  # as the datadir has not be created, we can remove the member
                  # and retrieve new hash
                  if [ "${ETCDAPI_VERSION}" -eq 3 ]; then
                      ETCDCTL_API=3 etcdctl --user=root:${ROOT_PASSWORD} member remove ${MEMBER_HASH}
                  else
                      etcdctl --username=root:${ROOT_PASSWORD} member remove ${MEMBER_HASH}
                  fi
              fi
              echo "Adding new member"
              rm -rf /var/run/etcd/*
              # ensure etcd dir exist
              mkdir -p /var/run/etcd/
              # sleep 60s wait endpoint become ready
              echo "sleep 60s wait endpoint become ready,sleeping..."
              sleep 60

              if [ "${ETCDAPI_VERSION}" -eq 3 ]; then
                  ETCDCTL_API=3 etcdctl --user=root:${ROOT_PASSWORD} member add ${HOSTNAME} --peer-urls=http://${HOSTNAME}.${SET_NAME}.${CLUSTER_NAMESPACE}:2380 | grep "^ETCD_" > /var/run/etcd/new_member_envs
              else
                  etcdctl --username=root:${ROOT_PASSWORD} member add ${HOSTNAME} http://${HOSTNAME}.${SET_NAME}.${CLUSTER_NAMESPACE}:2380 | grep "^ETCD_" > /var/run/etcd/new_member_envs
              fi
              
              

              if [ $? -ne 0 ]; then
                  echo "member add ${HOSTNAME} error."
                  rm -f /var/run/etcd/new_member_envs
                  exit 1
              fi

              cat /var/run/etcd/new_member_envs
              source /var/run/etcd/new_member_envs

              exec etcd --name ${HOSTNAME} \
                  --initial-advertise-peer-urls http://${HOSTNAME}.${SET_NAME}.${CLUSTER_NAMESPACE}:2380 \
                  --listen-peer-urls http://0.0.0.0:2380 \
                  --listen-client-urls http://0.0.0.0:2379 \
                  --advertise-client-urls http://${HOSTNAME}.${SET_NAME}.${CLUSTER_NAMESPACE}:2379 \
                  --data-dir /var/run/etcd/default.etcd \
                  --initial-cluster ${ETCD_INITIAL_CLUSTER} \
                  --initial-cluster-state ${ETCD_INITIAL_CLUSTER_STATE}
          fi

          for i in $(seq 0 $((${INITIAL_CLUSTER_SIZE} - 1))); do
              while true; do
                  echo "Waiting for ${SET_NAME}-${i}.${SET_NAME}.${CLUSTER_NAMESPACE} to come up"
                  ping -W 1 -c 1 ${SET_NAME}-${i}.${SET_NAME}.${CLUSTER_NAMESPACE} > /dev/null && break
                  sleep 1s
              done
          done

          echo "join member ${HOSTNAME}"
          # join member
          exec etcd --name ${HOSTNAME} \
              --initial-advertise-peer-urls http://${HOSTNAME}.${SET_NAME}.${CLUSTER_NAMESPACE}:2380 \
              --listen-peer-urls http://0.0.0.0:2380 \
              --listen-client-urls http://0.0.0.0:2379 \
              --advertise-client-urls http://${HOSTNAME}.${SET_NAME}.${CLUSTER_NAMESPACE}:2379 \
              --initial-cluster-token etcd-cluster-1 \
              --data-dir /var/run/etcd/default.etcd \
              --initial-cluster $(initial_peers) \
              --initial-cluster-state new

        env:
        - name: INITIAL_CLUSTER_SIZE
          value: "3"
        - name: CLUSTER_NAMESPACE
          valueFrom: 
            fieldRef:
              fieldPath: metadata.namespace
        - name: ETCDAPI_VERSION
          value: "3"
        - name: ROOT_PASSWORD
          value: '@123#'
        - name: SET_NAME
          value: "infra-etcd-cluster"
        - name: GOMAXPROCS
          value: "4"
        image: gcr.io/etcd-development/etcd:v3.3.8
        imagePullPolicy: Always
        lifecycle:
          preStop:
            exec:
              command:
              - /bin/sh
              - -ec
              - |
                HOSTNAME=$(hostname)

                member_hash() {
                    etcdctl member list | grep http://${HOSTNAME}.${SET_NAME}.${CLUSTER_NAMESPACE}:2380 | cut -d':' -f1 | cut -d'[' -f1
                }

                eps() {
                    EPS=""
                    for i in $(seq 0 $((${INITIAL_CLUSTER_SIZE} - 1))); do
                        EPS="${EPS}${EPS:+,}http://${SET_NAME}-${i}.${SET_NAME}.${CLUSTER_NAMESPACE}:2379"
                    done
                    echo ${EPS}
                }
                
                export ETCDCTL_ENDPOINTS=$(eps)

                SET_ID=${HOSTNAME##*-}
                # Removing member from cluster
                if [ "${SET_ID}" -ge ${INITIAL_CLUSTER_SIZE} ]; then
                    echo "Removing ${HOSTNAME} from etcd cluster"
                    if [ "${ETCDAPI_VERSION}" -eq 3 ]; then
                        ETCDCTL_API=3 etcdctl --user=root:${ROOT_PASSWORD} member remove $(member_hash)
                    else
                        etcdctl --username=root:${ROOT_PASSWORD} member remove $(member_hash)
                    fi
                    if [ $? -eq 0 ]; then
                        # Remove everything otherwise the cluster will no longer scale-up
                        rm -rf /var/run/etcd/*
                    fi
                fi
        name: infra-etcd-cluster
        ports:
        - containerPort: 2380
          name: peer
          protocol: TCP
        - containerPort: 2379
          name: client
          protocol: TCP
        resources:
          limits:
            cpu: "4"
            memory: 4Gi
          requests:
            cpu: "4"
            memory: 4Gi
        volumeMounts:
        - mountPath: /var/run/etcd
          name: datadir
  updateStrategy:
    type: OnDelete
  volumeClaimTemplates:
  - metadata:
      name: datadir
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi
      selector:
        matchLabels:
          k8s.cloud/storage-type: ceph-rbd

注意:SET_NAME必须与Statefulset的Name一致

这个时候你的Etcd已经可以在内部通过

http://${SET_NAME}-${i}.${SET_NAME}.${CLUSTER_NAMESPACE}:2379

访问了。

最后一步:创建Client Service

如果你的集群网络方案使Pod可以从Kubernetes集群外部访问,或者你的Etcd 集群只需要在Kubernetes集群内部访问,可以省略该步骤

apiVersion: v1
kind: Service
metadata:
  labels:
    k8s-app: infra-etcd-cluster-client
    app: infra-etcd
  name: infra-etcd-cluster-client
  namespace: default
spec:
  ports:
  - name: infra-etcd-cluster-2379
    port: 2379
    protocol: TCP
    targetPort: 2379
  selector:
    k8s-app: infra-etcd-cluster
    app: infra-etcd
  sessionAffinity: None
  type: NodePort

大功告成!你可以使用NodePort顺利访问到Etcd集群。

扩容与缩容

###扩容

只需要将Statefulset中的replicas改变即可。例如,我想把集群数量扩容为5个。

kubectl scale --replicas=5 statefulset infra-etcd-cluster

缩容

然后某一天我发现五个节点对我来说有些浪费,想使用三个节点。OK,只需要执行以下命令即可,

kubectl scale --replicas=3 statefulset infra-etcd-cluster

所有的源码均可以在我的Github中找到,如果感觉本文对你有用,请在Github上点下Star。如果发现任何问题可以提交PR,一起为开源做贡献。


695856371Web网页设计师②群 | 喜欢本站的朋友可以收藏本站,或者加入我们大家一起来交流技术!

欢迎来到梁钟霖个人博客网站。本个人博客网站提供最新的站长新闻,各种互联网资讯。 还提供个人博客模板,最新最全的java教程,java面试题。在此我将尽我最大所能将此个人博客网站做的最好! 谢谢大家,愿大家一起进步!

转载原创文章请注明出处,转载至: 梁钟霖个人博客www.liangzl.com

1条评论

Loading...
  • softblade810L

    博主你好,请教一个问题: 我是使用etcd-operator的方式在k8s上创建的etcd集群,官方在etcd可靠性上的做法,是使用backup-operator和restore-operator的方式,在故障节点数≥(N+1)/2时,基于定时快照备份的数据,进行集群的重建。但由于快照方式并不是连续数据备份,就是你说的也可能导致数据丢失。 那么使用StatefulSet方式创建的etcd集群是如何解决这个问题的呢? 我看etcd的描述,当故障节点数大于等于(N+1)/2时,就认为集群故障了,只能删了重建,否则会有问题。这也是operator在这种情况下不再去拉故障节点的缘故。StatefulSet方式难道可以在majority节点故障的时候还能恢复集群吗? 谢谢!



发表评论

电子邮件地址不会被公开。 必填项已用*标注

自定义皮肤
注册梁钟霖个人博客