1. 程式人生 > >kubernetes實戰之部署一個接近生產環境的consul叢集

kubernetes實戰之部署一個接近生產環境的consul叢集

系列目錄

前面我們介紹瞭如何在windows單機以及如何基於docker部署consul叢集,看起來也不是很複雜,然而如果想要把consul部署到kubernetes叢集中並充分利用kubernetes叢集的伸縮和排程功能並非易事.前面我們首先部署一個節點,部署完成以後獲取它的ip,然後其它的ip都join到這個ip裡組成叢集.

前面的部署方式存在以下問題:

  • 叢集易主

我們知道,在kubernetes裡,當節點發生故障或者資源不足時,會根據策略殺掉節點的一些pod轉而將pod移到其它節點上.這時候我們就需要重新獲取主節點ip,然後將新的節點加入進去,以上做法不利於充分發揮kubernetes自身的伸縮功能.

  • 新節點加入

不管是新節點加入或者失敗後重新生成的節點重新加入叢集,都需要知道主節點ip,這會產生和上面相同的問題,就是需要人工介入.

理想的狀態是,當叢集主節點切換時,新節點仍然能夠在無需人工介入的情況下自動加入叢集.我們解決這個問題的思路如下:使用kubernetes叢集的dns功能,而不直接硬編碼節點的ip.如果叢集中有三個server,則這三個sever中必然有一個是主節點,我們可以依次嘗試通過dns來解析到具體的節點,依次嘗試加入每一個sever節點,嘗試到真正的主節點時便能夠加入叢集.

我們首先建立服務,定義服務的檔名為consul-service.yml

apiVersion: v1
kind: Service
metadata:  
   name: consul  
   labels:    
     name: consul
spec:  
   type: ClusterIP  
   ports:    
    - name: http      
      port: 8500      
      targetPort: 8500    
    - name: https      
      port: 8443
      targetPort: 8443
    - name: rpc
      port: 8400
      targetPort: 8400    
    - name: serflan-tcp      
      protocol: "TCP"      
      port: 8301      
      targetPort: 8301    
    - name: serflan-udp      
      protocol: "UDP"      
      port: 8301      
      targetPort: 8301    
    - name: serfwan-tcp      
      protocol: "TCP"      
      port: 8302      
      targetPort: 8302    
    - name: serfwan-udp      
      protocol: "UDP"      
      port: 8302      
      targetPort: 8302
    - name: server      
      port: 8300      
      targetPort: 8300    
    - name: consuldns      
      port: 8600      
      targetPort: 8600  
   selector:    
    app: consul

通過以上服務我們把pod的埠對映到叢集中,通過名稱我們可以看到每一個商品做什麼型別通訊用的.這個服務會選擇標籤為app: consul的pod.通過這個示例我們也可以看到,對於一些複雜的服務,採用nodeport型別的服務是很不可取的.這裡使用的是clusterip型別的服務.後面我們會通過nginx ingress controller把http埠暴露到叢集外,供外部呼叫.

我們通過kubectl create -f consul-service.yml來建立這個服務

這裡我們採用的是statefulset的方式建立的server,這裡之所以使用statefulset是因為statefulset包含的pod名稱是固定的(普通pod以一定hash規則隨機生成),如果命名規則固定,pod數量固定,則我們可以預先知道它們的dns規則,以便在自動加入叢集中時使用. statefulset建立檔案如下(名為consul-statefulset.yml)

apiVersion: apps/v1beta1 
kind: StatefulSet
metadata:
  name: consul
spec:
  serviceName: consul
  replicas: 3
  template: 
    metadata:
      labels:
        app: consul
    spec:
      
      terminationGracePeriodSeconds: 10
      containers:
      - name: consul
        image: consul:latest
        args:
             - "agent"
             - "-server"
             - "-bootstrap-expect=3"
             - "-ui"
             - "-data-dir=/consul/data"
             - "-bind=0.0.0.0"
             - "-client=0.0.0.0"
             - "-advertise=$(PODIP)"
             - "-retry-join=consul-0.consul.$(NAMESPACE).svc.cluster.local"
             - "-retry-join=consul-1.consul.$(NAMESPACE).svc.cluster.local"
             - "-retry-join=consul-2.consul.$(NAMESPACE).svc.cluster.local"
             - "-domain=cluster.local"
             - "-disable-host-node-id"
        env:
            - name: PODIP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
        ports:
            - containerPort: 8500
              name: ui-port
            - containerPort: 8400
              name: alt-port
            - containerPort: 53
              name: udp-port
            - containerPort: 8443
              name: https-port
            - containerPort: 8080
              name: http-port
            - containerPort: 8301
              name: serflan
            - containerPort: 8302
              name: serfwan
            - containerPort: 8600
              name: consuldns
            - containerPort: 8300
              name: server

通過以上定義檔案我們可以看到,裡面最核心的部分是--retry-join後面的dns規則,由於我們指定了pod名稱為consul,並且有三個例項,因此它們的名稱為consul-0,consul-1,consul-2,即便有節點失敗,起來以後名稱仍然是固定的,這樣不管新起的podIp是多少,通過dns都能夠正確解析到它.

這裡的data-dir以前沒有提到過,很容易理解,就是consul持久化資料儲存的位置.我們前面說過server節點的資料都是要持久化儲存的,這個data-dir便是持久化資料儲存的位置.

我們可以通過進入到pod內部來檢視consul叢集的成員資訊

[centos@k8s-master ~]$ clear
[centos@k8s-master ~]$ kubectl exec -it consul-0 /bin/sh
/ # consul members
Node      Address           Status  Type    Build  Protocol  DC   Segment
consul-0  10.244.1.53:8301  alive   server  1.4.4  2         dc1  <all>
consul-1  10.244.1.54:8301  alive   server  1.4.4  2         dc1  <all>
consul-2  10.244.1.58:8301  alive   server  1.4.4  2         dc1  <all>
/ #

通過以上大家可以看到,Ip 53和54是相鄰的,但是58是跳躍的,這是因為我故意刪除了其中的一個pod,由於我們在建立statefulset的時候指定的副本集個數為3,因此kubernetes會重新建立一個pod,經過測試這個新建立的pod仍然能夠正確加入叢集,符合我們的需求.

我們在容器內執行curl localhost:8500的時候,會出現以下結果

<a href="/ui/">Moved Permanently</a>.

不用擔心,以上結果是正確的,因為通過瀏覽器訪問的話,localhost:8500會自動跳轉到http://localhost:8500/ui/dc1/services預設ui展示介面,由於curl無法模擬瀏覽器互動跳轉行為,因此顯示以上內容永久重定向.

以上節本完成了demo演示,但是仍然有兩個問題

  • 第一,我們直接把data儲存到了容器內,如果容器被銷燬然後重新建立,則資料會丟失.這樣顯然是存在風險的,正確的做法是把容器內持久化資料的目錄掛載到宿主機上,但是由於我的測試叢集中有一主一從,因此把容器目錄掛載到宿主機會造成目錄衝突,所以這裡沒有把儲存內容掛載到宿主機.

其它有狀態服務也要把持久化資料目錄掛載到宿主機上

  • 我們在部署server的時候為了保證高可用,兩個或以上server不能部署到同一臺機器或者由於故障出現被轉移到了相同機器,這樣一方面增加了server的壓力,同時另一方面也降低了可用性,因為宿主機宕機時上面的節點都會宕掉,這樣與多server部署高可用的初衷相違背的.實際上,部署其它有狀態的服務也要考慮到這個問題.在kubernetes裡,解決這個問題使用的是pod的反親和屬性,親和pod和主動靠攏,反親和屬性的pod則恰恰相反,它們之間相互排斥.利用這個特性可以滿足我們的需求.這裡之所以沒有使用反親和的原因也是叢集的節點不夠,如果節點相互排斥就無法部署成功了.

下面貼出完整的配置

apiVersion: apps/v1beta1 
kind: StatefulSet
metadata:
  name: consul
spec:
  serviceName: consul
  replicas: 3
  template: 
    metadata:
      labels:
        app: consul
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - consul
              topologyKey: kubernetes.io/hostname
      terminationGracePeriodSeconds: 10
      containers:
      - name: consul
        image: consul:latest
        args:
             - "agent"
             - "-server"
             - "-bootstrap-expect=3"
             - "-ui"
             - "-data-dir=/consul/data"
             - "-bind=0.0.0.0"
             - "-client=0.0.0.0"
             - "-advertise=$(PODIP)"
             - "-retry-join=consul-0.consul.$(NAMESPACE).svc.cluster.local"
             - "-retry-join=consul-1.consul.$(NAMESPACE).svc.cluster.local"
             - "-retry-join=consul-2.consul.$(NAMESPACE).svc.cluster.local"
             - "-domain=cluster.local"
             - "-disable-host-node-id"
        volumeMounts:
            - name: data
              mountPath: /consul/data
        env:
            - name: PODIP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
        ports:
            - containerPort: 8500
              name: ui-port
            - containerPort: 8400
              name: alt-port
            - containerPort: 53
              name: udp-port
            - containerPort: 8443
              name: https-port
            - containerPort: 8080
              name: http-port
            - containerPort: 8301
              name: serflan
            - containerPort: 8302
              name: serfwan
            - containerPort: 8600
              name: consuldns
            - containerPort: 8300
              name: server
      volumes:
        - name: data
          hostPath:
            path: /home/data

以上需要注意的是,要掛載到的宿主機的目錄必須是預先存在的,kubernetes並不會在宿主機上建立需要的目錄.

由於主機節不能容納普通pod,因此要完成以上高可用部署,需要叢集中至少有四個節點.(如果不使用完整配置,則只需要一主一從即可)

通過以上配置,我們便可以在kubernetes叢集內部訪問consul叢集了,但是如果想要在叢集外部訪問.還需要將服務暴露到叢集外部,前面章節我們也講到過如何將服務暴露到叢集外部.這裡我們使用 nginx-ingress 方式將服務暴露到外部.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress-consul
  namespace: default
  annotations: 
    kubernets.io/ingress.class: "nginx"
spec:
  rules:
  - host: consul.my.com.local 
    http:
      paths:
      - path: 
        backend:
          serviceName: consul
          servicePort: 8500

由於我沒有申請域名,因此我使用的hosts裡新增對映的方式來訪問的.我已經在hosts檔案裡添加了對映,這裡直接開啟瀏覽器訪問

我們點選Nodes可以看到一共有三個節點

節點後面的綠色對號標識表示服務的狀態是可用的

節點掛掉後新啟動節點自動加入叢集

如果叢集中有節點掛掉後,存活的節點仍然符合法定數量(構建一個consul叢集至少需要兩個存活節點進行仲裁),由於statefulset規定的副本集的數量是3,因此k8s會保證有3個數量的副本集在執行,當k8s叢集發現執行的副本數量少於規定數量時,便會根據排程策略重新啟動一定數量pod以保證執行副本集數量和規定數量相符.由於在編排consul部署時使用了retry-join引數,因此有新增節點會自動嘗試重新加入叢集(傳統方式是根據ip來加入叢集),同樣如果掛掉的是master節點也不用擔心,如果存活節點數量仍然符合法定數量(這裡的法定數量並不是指statefulset副本集數量,而是consul組成叢集所需要最小節點數量),consul會依據一定策略重新選擇master節點.

我們先來看一下叢集中pod狀態

[root@k8s-master consul]# kubectl get pod
NAME                                       READY   STATUS    RESTARTS   AGE
consul-0                                   1/1     Running   0          9m20s
consul-1                                   1/1     Running   0          9m19s
consul-2                                   1/1     Running   0          9m17s
easymock-dep-84767b6f75-57qm2              1/1     Running   1708       35d
easymock-dep-84767b6f75-mnfzp              1/1     Running   1492       40d
mvcpoc-dep-5856db545b-qndrr                1/1     Running   1          46d
sagent-b4dd8b5b9-5m2jc                     1/1     Running   3          54d
sagent-b4dd8b5b9-brdn5                     1/1     Running   1          40d
sagent-b4dd8b5b9-hfmjx                     1/1     Running   2          50d
stock-dep-5766dfd785-gtlzr                 1/1     Running   0          5d18h
stodagent-6f47976ccb-8fzmv                 1/1     Running   3          56d
stodagent-6f47976ccb-cv8rg                 1/1     Running   2          50d
stodagent-6f47976ccb-vf7kx                 1/1     Running   3          56d
trackingapi-gateway-dep-79bb86bb57-x9xzp   1/1     Running   3          56d
www                                        1/1     Running   1          48d

我們看到有三個consul pod在執行

下面我們手動殺掉一個pod,來模擬故障.

[root@k8s-master consul]# kubectl delete pod consul-0
pod "consul-0" deleted

由於consul-0被幹掉,因此consul叢集中consul-0變得不可用

過一段時間後,k8s檢測到執行的pod數量少於stateful規定的數量,便會重新再啟動一個pod(需要注意的是,這裡的重新啟動並不是把原來pod重新啟動,而是重新再排程一個全新的pod到叢集中,這個pod與掛掉的pod無任何關係,當然ip也是不一樣的,如果仍然依賴ip,則會帶來無限麻煩)

我們可以看到,這時候consul-0服務的ip已經變成了86(前面是85)

部署nginx ingress可能並不是一件非常容易的事,由其是對使用單節點docker on windows的朋友來說,如果有的朋友不想部署nginx ingress,僅做為測試使用,也可以把服務的型別由ClusterIP更改為NodePort型別,這樣就可以像docker一樣把服務的埠對映到宿主機埠,方便測試.

本節涉及到的內容比較多,也相對比較複雜(前面也多次說過,部署有狀態服務是難點),因此後面會專門再開一節來詳細講解本文中的一些細節,有問題的朋友也可以留言或者通過其它方式聯絡我,大家共同交流