java php apache linux Ubuntu google 云计算 编程 mysql Firefox 开源 shell 微软 Python Android Windows nginx 程序员 centos wordpress

Java微服務開發指南 — 集群管理、失敗轉移和負載均衡的實踐

集群管理、失敗轉移和負載均衡的實踐

    在前一章節中,我們快速的介紹了集群管理、Linux容器,接下來讓我們使用這些技術來解決微服務的伸縮性問題。作為參考,我們使用的微服務工程來自於第二、第三和第四章節(Spring Boot、Dropwizard和WildFly Swarm)中的內容,接下來的步驟都適合上述三款框架。

開始

    我們需要將微服務打包成為Docker鏡像,最終將其部署到Kubernetes,首先進入到項目工程hola-springboot,然後啟動jboss-forge,然後安裝fabric8插件,這個插件使我們安裝maven插件變得非常容易。

$ cd ~/Documents/workspace/microservices-camp/
weipengktekiMBP:microservices-camp weipeng2k$ ls
README.md         SUMMARY.md        book              book.json         hola-backend      hola-dropwizard   hola-springboot   hola-wildflyswarm hola-x            resource
$ forge
Using Forge at /usr/local/Cellar/jboss-forge/3.7.1.Final/libexec

    _____                    
   |  ___|__  _ __ __ _  ___
   | |_ / _ \| `__/ _` |/ _ \  \\
   |  _| (_) | | | (_| |  __/  //
   |_|  \___/|_|  \__, |\___|
                   |__/      

JBoss Forge, version [ 3.7.1.Final ] - JBoss, by Red Hat, Inc. [ http://forge.jboss.org ]
Hit '<TAB>' for a list of available commands and 'man [cmd]' for help on a specific command.

To quit the Shell, type 'exit'.

[microservices-camp]$ addon-install --coordinate io.fabric8.forge:devops,2.2.148
***SUCCESS*** Addon io.fabric8.forge:devops,2.2.148 was installed successfully.
[microservices-camp]$ cd hola-springboot/
[hola-springboot]$ ls
hola-springboot.iml  mvnw  mvnw.cmd  pom.xml  src  target

[hola-springboot]$ fabric8-setup
***SUCCESS*** Added Fabric8 Maven support with base Docker image: fabric8/JAVA-jboss-openjdk8-jdk:1.0.10. Added the following Maven profiles [f8-build, f8-deploy, f8-local-deploy] to make building the project easier, e.g. mvn -Pf8-local-deploy
    運行完fabric8-setup之後,然後在IDE中打開pom.xml,可以發現增加了一些屬性。

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <docker.assemblyDescriptorRef>artifact</docker.assemblyDescriptorRef>
    <docker.from>docker.io/fabric8/java-jboss-openjdk8-jdk:1.0.10</docker.from>
    <docker.image>weipeng2k/${project.artifactId}:${project.version}</docker.image>
    <docker.port.container.http>8080</docker.port.container.http>
    <docker.port.container.jolokia>8778</docker.port.container.jolokia>
    <fabric8.iconRef>icons/spring-boot</fabric8.iconRef>
    <fabric8.readinessProbe.httpGet.path>/health</fabric8.readinessProbe.httpGet.path>
    <fabric8.readinessProbe.httpGet.port>8080</fabric8.readinessProbe.httpGet.port>
    <fabric8.readinessProbe.initialDelaySeconds>5</fabric8.readinessProbe.initialDelaySeconds>
    <fabric8.readinessProbe.timeoutSeconds>30</fabric8.readinessProbe.timeoutSeconds>
    <fabric8.service.containerPort>8080</fabric8.service.containerPort>
    <fabric8.service.name>hola-springboot</fabric8.service.name>
    <fabric8.service.port>80</fabric8.service.port>
    <fabric8.service.type>LoadBalancer</fabric8.service.type>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>
註意這裏docker.image改為了$username,使用者需要結合自己的情況進行修改
    並且添加了兩個maven插件:docker-maven-plugin和fabric8-maven-plugin。

<plugin>
  <groupId>io.fabric8</groupId>
  <artifactId>docker-maven-plugin</artifactId>
  <version>0.14.2</version>
  <configuration>
      <images>
          <image>
              <name>${docker.image}</name>
              <build>
                  <from>${docker.from}</from>
                  <assembly>
                      <basedir>/app</basedir>
                      <descriptorRef>${docker.assemblyDescriptorRef}</descriptorRef>
                  </assembly>
                  <env>
                      <JAR>${project.artifactId}-${project.version}.war</JAR>
                      <JAVA_OPTIONS>-Djava.security.egd=/dev/./urandom</JAVA_OPTIONS>
                  </env>
              </build>
          </image>
      </images>
  </configuration>
</plugin>
<plugin>
  <groupId>io.fabric8</groupId>
  <artifactId>fabric8-maven-plugin</artifactId>
  <version>2.2.100</version>
  <executions>
      <execution>
          <id>json</id>
          <phase>generate-resources</phase>
          <goals>
              <goal>json</goal>
          </goals>
      </execution>
      <execution>
          <id>attach</id>
          <phase>package</phase>
          <goals>
              <goal>attach</goal>
          </goals>
      </execution>
  </executions>
</plugin>
    除了maven插件,還增加了一些maven profiles:

f8-build
構建docker鏡像和Kubernetes的manifest.yml
f8-deploy
構建docker鏡像、部署到遠端的docker註冊中心,並部署應用到Kubernetes
f8-local-deploy
構建docker鏡像,生成Kubernetes的manifest.yml並部署到本地運行的Kubernetes
    jboss-forge插件是Fabric8開源項目的一部分,Fabric8構建工具可以同Docker、Kubernetes、OpenShift進行交互,同時基於maven插件可以提供給應用諸如:依賴註入Spring/CDI、訪問Kubernetes和OpenShift的API。Fabric8同時也集成了API管理,chaos monkey以及NetflixOSS等功能。

將微服務構建為Docker鏡像

    當hola-springboot項目添加了maven插件後,就可以使用maven命令來構建docker鏡像了,在構建鏡像之前需要確定系統以及安裝了CDK。 由於我們並沒有安裝CDK,所以需要進行一些配置和操作 ,首先需要將Docker服務暴露出管理API的端口,以及添加環境變量。

筆者並沒有在mac進行構建,而是將代碼push到git後,在Ubuntu上執行構建的。
    更改Docker服務,暴露管理的API端口的操作首先需要更改docker的配置,以ubuntu為例:sudo vi /lib/systemd/system/docker.service,將其中的 ExecStart 的值做修改:

ExecStart=/usr/bin/dockerd -H fd:// -H unix:///var/run/docker.sock -H tcp://0.0.0.0:4243
    然後重啟docker daemon,通過以下命令完成重啟:

sudo systemctl daemon-reload
sudo systemctl restart docker
可以通過telnet localhost 4243 來檢測一下,docker的API是否開始暴露了。
    接下來需要設置環境變量,通過修改~/.profile,修改內容如下:

export DOCKER_HOST="tcp://127.0.0.1:4243"
PATH="$HOME/bin:$HOME/.local/bin:$PATH"
    這個DOCKER_HOST環境變量就是fabric8構建時獲取的Docker API,然後完成鏡像的構建。在做完這些操作之後,在hola-springboot工程下,運行fabric8構建命令。

$:~/Documents/workspace/microservices-camp/hola-springboot$ mvn -Pf8-build
[info] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building hola-springboot 1.0
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] Added environment annotations:
[INFO]     Service hola-springboot selector: {project=hola-springboot, provider=fabric8, group=com.murdock.examples} ports: 80
[INFO]     ReplicationController hola-springboot replicas: 1, image: fabric8/hola-springboot:1.0
[INFO] Template is now:
[INFO]     Service hola-springboot selector: {project=hola-springboot, provider=fabric8, group=com.murdock.examples} ports: 80
[INFO]     ReplicationController hola-springboot replicas: 1, image: fabric8/hola-springboot:1.0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 36.954 s
[INFO] Finished at: 2017-08-12T21:24:56+08:00
[INFO] Final Memory: 52M/762M
[INFO] ------------------------------------------------------------------------
部署到Kubernetes

    如果我們安裝了Docker的客戶端,我們可以在構建完成後查看是否生成了對應的鏡像,可以看到通過mvn -Pf8-build之後,在本地安裝了鏡像。

$ sudo docker images
REPOSITORY                        TAG                 IMAGE ID            CREATED             SIZE
weipeng2k/hola-springboot           1.0                 7405f14d812e        21 seconds ago      436MB
fabric8/java-jboss-openjdk8-jdk   1.0.10              1a13e31efd4b        13 months ago       421MB
    如果環境配置正確,並且啟動了CDK,就可以使用mvn -Pf8-local-deploy來部署到本地的Kubernetes上。Kubernetes暴露了一個REST接口,並允許外界操作集群,Kubernetes遵循最終狀態一致的原則,使用者只需要描述部署的要求和模式,Kubernetes就會盡力將其完成。舉個例子:如果我們需要啟動一個運行了hola-springboot的Pod,我們可以通過HTTP請求,將一個JSON/YAML傳遞給Kubernetes,Kubernetes會理解請求,然後創建對應的Pod,在Pod中創建Docker容器來運行應用,並在集群中對Pod進行調度。一個Kubernetes的Pod是原子化的,能夠在Kubernetes集群中進行調度,同時它是有至少一個Docker容器所組成。

    通過使用fabric8-maven-plugin可以將項目完成構建並通過Kubernetes的REST接口創建Pod,只需要運行mvn -Pf8-local-deploy,然後通過CDK的oc get pod檢查是否部署成功。由於譯者並沒有使用CDK ,所以采用的方式是通過mvn -Df8-build來構建鏡像,然後用minikube將鏡像在本地完成部署的方式來進行,下面介紹一下具體的操作方式。我們觀察可以發現,通過執行mvn -Pf8-build後在本地的docker鏡像倉庫中已經生成了鏡像,如果使用原生的docker是否可以啟動它呢?我們嘗試一下。

以下內容在功能上同原文一致,但是工具使用上不會使用CDK
docker部署屬於新增內容
$ sudo docker run -d -p 8080:8080 --name hola-spring -it weipeng2k/hola-springboot:1.0
264c5d2ca402910d3c567dd4f0f19b6e04a769e503ac8d198441f35b2e8c7d58
$ curl http://localhost:8080/api/holaV1
Hola Spring Boot @ 172.17.0.2
    可以看到我們通過docker run啟動了一個容器,並將容器的8080端口綁定到了本機的8080上,當然啟動一個容器是不夠的,我們需要分布式部署應用,因此需要多個容器實例,可以通過在本機不同端口上啟動多個容器實例,具體做法如下:

$ sudo docker run -d -p 8081:8080 -it --name hola-springboot2 weipeng2k/hola-springboot:1.0
492451e8b52993084246dc2b4ff78e7fff41f95d89e3f01670c2d6cec9774a47
$ curl http://localhost:8081/api/holaV1
Hola Spring Boot @ 172.17.0.3
    通過啟動一個映射在本機8081端口上的容器,這樣客戶端就可以通過負載均衡策略來訪問其中一個容器中的應用了。由於鏡像具備不變遞交的特性,那麼就可以在不同的主機上部署多個相同的容器實現分布式部署,這麼一看,實現分布式部署在docker的幫助下其實很簡單呢,但是還是會面對以下問題:

如果其中一個容器實例崩潰了呢?
整個系統的負載是否可以支持?
如果擴容,已經有了最終的容器實例數,但是要新增多少個呢?
客戶端的負載均衡如何保證健壯性?
    直接使用docker容器來進行部署,會面對這些問題,而接下來將使用Kubernetes進行部署,在它的幫助下來解決這些問題。接下來,采用yml的方式部署到Kubernetes,首先運行完成mvn -Pf8-build之後,在classes目錄下會生成Kubernetes的部署描述符。

microservices-camp/hola-springboot/target/classes$ ls
application.properties  com  kubernetes.json  kubernetes.yml  META-INF
    其中kubernetes.json和kubernetes.yml是f8插件替我們生成的,但是該文件的格式只有CDK認識,標準的Kubernetes無法識別,我們需要從中截取一部分才能夠使用,生成部署描述文件主要由兩部分,一個是Service,另一個是ReplicationController,我們在正式部署時,很少的面對Pod,因為它太細節,更多的是面向Service和ReplicationController。我們使用yml來創建,先將kubernetes.yml中的ReplicationController部分拷貝到另一個文件中,文件名可以自定義,文件內容如下:

譯者創建了一個rc.yml的部署描述文件,放置在hola-springboot工程目錄下
apiVersion: "v1" #指定api版本,此值必須在kubectl api-version中
kind: "ReplicationController" #指定創建資源的角色/類型
metadata: #資源的元數據/屬性
  annotations: #自定義註解列表
    fabric8.io/git-branch: "master"
    fabric8.io/git-commit: "116ea497b4f32f48d9395827c5879955b1eca71d"
  labels:
    project: "hola-springboot"
    provider: "fabric8"
    version: "1.0"
    group: "com.murdock.examples"
  name: "hola-springboot"
spec:
  replicas: 1 #副本數量
  selector: #RC通過spec.selector來篩選要控制的Pod
    project: "hola-springboot"
    provider: "fabric8"
    version: "1.0"
    group: "com.murdock.examples"
  template: #這裏寫Pod的定義
    metadata:
      annotations: {}
      labels:
        project: "hola-springboot"
        provider: "fabric8"
        version: "1.0"
        group: "com.murdock.examples"
    spec: #這裏是資源內容
      containers:
      - args: []
        command: []
        env: #指定容器中的環境變量
        - name: "KUBERNETES_NAMESPACE"
          valueFrom:
            fieldRef:
              fieldPath: "metadata.namespace"
        image: "weipeng2k/hola-springboot:1.0"
        name: "hola-springboot"
        ports:
        - containerPort: 8080
          name: "http"
        - containerPort: 8778
          name: "jolokia"
        readinessProbe: #pod內容器健康檢查的設置
          httpGet: #通過httpget檢查健康,返回200-399之間,則認為容器正常
            path: "/health"
            port: 8080
          initialDelaySeconds: 5
          timeoutSeconds: 30
        securityContext: {}
        volumeMounts: []
      imagePullSecrets: []
      nodeSelector: {}
      volumes: []
    運行kubectl create命令可以將鏡像進行部署,創建對應的Pod。

$ kubectl create -f rc.yml
replicationcontroller "hola-springboot" created
    運行kubectl get pods可以查看創建的Pod,可以看到hola-springboot-fmfc7就是我們進行部署的Pod

$ kubectl get pod
NAME                              READY     STATUS    RESTARTS   AGE
hello-minikube-2210257545-ndc6s   1/1       Running   4          20d
hola-springboot-fmfc7             1/1       Running   0          1m
一個Java的微服務啟動時間到自檢完成,可能在30s以上
    接下來,可以通過kubectl delete pod $pod_name來停止一個Pod。

$ kubectl delete pod hola-springboot-fmfc7
pod "hola-springboot-fmfc7" deleted
$ kubectl get pods
NAME                              READY     STATUS    RESTARTS   AGE
hello-minikube-2210257545-ndc6s   1/1       Running   4          20d
hola-springboot-k843c             0/1       Running   0          4s
    可以看到,雖然通過kubectl delete刪除了一個Pod,但是Kubernetes迅速的又啟動了一個,簡直就是一個打不死的小強,更準確的說另一個Pod在我們刪除上一個Pod後被創建了。Kubernetes能夠為你的微服務做到啟動、停止和自動重啟,你能想象如果你要檢查一個服務的容量有多困難嗎?讓我們接下來探索Kubernetes帶來的更有價值的集群管理技術,並用這些技術來管理我們的微服務。

伸縮性

    微服務架構的一個巨大優點就是可伸縮性,我們能夠在集群中復制很多服務,但不用擔心端口沖突、JVM或者依賴缺失。在Kubernetes中,可以通過 ReplicationController 完成伸縮工作,我們先查看一下本機環境中部署的 ReplicationController。

$ kubectl get rc
NAME              DESIRED   CURRENT   READY     AGE
hola-springboot   1         1         0         1m
    可以看到我們創建了一個 ReplicationController,在rc.yml中,我們定義replicas為1,這代表在任何時候都要有一個Pod實例運行我們的微服務應用,如果一個Pod掛掉,Kubernetes會進行判斷該Pod的最終狀態是需要幾個,然後為我們創建等額的Pod,如果我們需要改變Pod(復制體)的數量,做到服務的伸縮該怎麼辦呢?

$ kubectl scale rc hola-springboot --replicas=3
replicationcontroller "hola-springboot" scaled
    運行kubectl scale rc $rc_name --replicas=$count可以調整復制控制器的Pod目標數目,上述命令將hola-springboot調整為3個。我們使用kubectl get pods觀察一下是否生效。

$ kubectl get pods
NAME                              READY     STATUS    RESTARTS   AGE
hola-springboot-6fj5k             1/1       Running   0          1h
hola-springboot-k843c             1/1       Running   0          1h
hola-springboot-ltzs5             1/1       Running   0          1h
    如果這個過程中這些Pod有掛掉,Kubernetes將會盡可能的確定復制體的數量是 3。註意,我們不需要改變這些服務監聽的端口或者任何不自然的端口映射,每一個實例都是在監聽8080端口,而且相互之間不會沖突。接下來將復制體的數量設置為0,只需要運行以下命令:

$ kubectl scale rc hola-springboot --replicas=0
replicationcontroller "hola-springboot" scaled
$ kubectl get pods
No resources found.
    Kubernetes也具備根據各種指標,諸如:CPU、內存使用率或者用戶自定義觸發器來進行 自行伸縮 的能力,能控制復制體(Pod)的增加或者減少。自行伸縮已經超過了本書的範疇,但是它仍是我們需要關註的集群管理技術。

一般一個Pod會占一個CPU,如果是4核,那麼就可以運行4個,超過4個後,你會發現各個Pod在交替啟動運行,本機使用的A8是4核處理器,在默認情況下無法保證4個以上的Pod都處於Running狀態
服務發現(Service discovery)

    我們需要理解Kubernetes中最後一個概念是 Sevice , 一個Service是一組Pods之間間接關系的簡單抽象,它是應用用來描述一組Pods的表現形式。我們在之前的例子中看到Kubernetes是如何管理Pods的創建和消亡,我們也了解到Kubernetes能夠方便的進行一個服務的伸縮,在接下來的例子中,我們將嘗試啟動hola-backend服務,然後使用hola-springboot與hola-backend之間進行通信。

hola-backend服務之前作為服務提供方,提供了Book的創建和查詢服務,這裏的backend比原書中的例子更貼近現實
    接下來將hola-backend鏡像化,然後分別在Docker和Kubernetes中嘗試部署, 這部分內容在原書中沒有涉及。對於hola-backend的鏡像構建,我們需要將WildFly構建在其中,所以先進入hola-backend工程目錄,執行mvn構建,然後在target目錄下建立Dockerfile並進行鏡像構建。

microservices-camp/hola-backend$ mvn clean package
microservices-camp/hola-backend$ cd target/
weipeng2k@weipeng2k-workstation:~/Documents/workspace/microservices-camp/hola-backend/target$ ls
classes  generated-sources  hola-backend  hola-backend-swarm.jar  hola-backend.war  hola-backend.war.original  maven-archiver  maven-status
microservices-camp/hola-backend/target$ vi Dockerfile
    Dockerfile的內容如下:

FROM jboss/wildfly
MAINTAINER weipeng2k "weipeng2k@126.com"
ADD hola-backend.war /opt/jboss/wildfly/standalone/deployments/
    可以看到父鏡像來自於jboss/wildfly,構建時將war包拷貝到jboss WildFly默認的部署目錄,執行sudo docker build -t="weipeng2k/hola-backend:1.0" .進行鏡像構建,隨後在啟動這個鏡像時,將會完成hola-backend的部署。

如果不想本地構建進項,可以執行sudo docker pull weipeng2k/hola-backend:1.0從docker hub上獲取
    啟動鏡像,通過執行sudo docker run --name hola-backend -itd -p 8080:8080 weipeng2k/hola-backend:1.0(如果自己構建的鏡像,註意鏡像的名稱),啟動容器後將本機的8080端口與容器中的8080進行映射,可以使用Postman進行測試。

    接下來需要創建Kubernetes對應的ReplicationController和Service,需要編寫Kubernetes描述文件,先看一下ReplicationController描述文件。

rc.yml和svc.yml可以在hola-backend工程目錄下找到
apiVersion: "v1" #指定api版本,此值必須在kubectl api-version中
kind: "ReplicationController" #指定創建資源的角色/類型
metadata: #資源的元數據/屬性
  labels:
    project: "hola-backend"
    provider: "fabric8"
    version: "1.0"
    group: "com.murdock.examples"
  name: "hola-backend"
spec:
  replicas: 1 #副本數量
  selector: #RC通過spec.selector來篩選要控制的Pod
    project: "hola-backend"
    provider: "fabric8"
    version: "1.0"
    group: "com.murdock.examples"
  template: #這裏寫Pod的定義
    metadata:
      annotations: {}
      labels:
        project: "hola-backend"
        provider: "fabric8"
        version: "1.0"
        group: "com.murdock.examples"
    spec: #這裏是資源內容
      containers:
      - args: []
        command: []
        env: #指定容器中的環境變量
        - name: "KUBERNETES_NAMESPACE"
          valueFrom:
            fieldRef:
              fieldPath: "metadata.namespace"
        image: "weipeng2k/hola-backend:1.0"
        name: "hola-backend"
        ports:
        - containerPort: 8080
          name: "http"
        readinessProbe: #pod內容器健康檢查的設置
          httpGet: #通過httpget檢查健康,返回200-399之間,則認為容器正常
            path: "/"
            port: 8080
          initialDelaySeconds: 5
          timeoutSeconds: 30
        securityContext: {}
        volumeMounts: []
      imagePullSecrets: []
      nodeSelector: {}
      volumes: []
    在rc.yml中定義了Pods,以及服務對應的鏡像,通過kubectl create -f rc.yml可以讓Kubernetes創建對應的ReplicationController,然後按照replicas中對復制體的數量約定,開始創建一個Pod。

啟動的過程比較慢,原因在於下載weipeng2k/hola-backend:1.0這個鏡像,後續讀者可以使用aliyun等鏡像,如果要確定當前Kubernetes的工作狀態,可以使用kubectl describe pods來觀察當前的部署情況

可以通過minikube ssh登錄,然後執行docker pull weipeng2k/hola-backend:1.0後,可以觀察到下載進度,該過程比較緩慢
    當hola-backend的ReplicationController啟動後,可以通過kubectl get pods檢查一下當前Pod的狀態。

$ kubectl get pods
NAME                 READY     STATUS    RESTARTS   AGE
hola-backend-t06sg   1/1       Running   1          2d
$ kubectl get rc
NAME           DESIRED   CURRENT   READY     AGE
hola-backend   1         1         1         2d
    可以看到我們檢查了剛創建的ReplicationController和由ReplicationController創建出來的Pod,接著我們開始創建Service,通過kubectl create -f svc.yml可以讓Kubernetes創建對應的服務。

apiVersion: "v1"
kind: "Service"
metadata:
  annotations: {}
  labels:
    project: "hola-backend"
    provider: "fabric8"
    version: "1.0"
    group: "com.murdock.examples"
  name: "hola-backend"
spec:
  deprecatedPublicIPs: []
  externalIPs: []
  ports:
  - port: 80
    protocol: "TCP"
    targetPort: 8080
  selector: # Service使用該selector來對應到Pods
    project: "hola-backend"
    provider: "fabric8"
    group: "com.murdock.examples"
  type: "LoadBalancer"
    通過kubectl get service可以查看在Kubernetes中部署的服務。

$ kubectl get svc
NAME           CLUSTER-ip   EXTERNAL-IP   PORT(S)        AGE
hola-backend   10.0.0.131   <pending>     80:30699/TCP   1h
kubernetes     10.0.0.1     <none>        443/TCP        33d
    註意 Service 創建出來後,有兩個有趣的屬性需要我們關註一下。一個是 CLUSTER-IP,這個虛擬的IP被分配給了一個Service實例,分配之後不會變化,這是一個固定的IP,在Kubernetes集群中運行的實例都能夠通過這個IP和後面的 Pods 進行通信。而 Service後面的Pods是被 SELECTOR 選擇出來的。可以看到當前svc.yml中定義的selector對於屬性的指定。Pods在Kubernetes中可以被標定上任意的屬性,你可以使用(類似:version、component或者team等)各種KV表示。在這個例子中,一個名為hola-backend服務,它會選擇標有project=hola-backend and provider=fabric8的Pods,這代表著只要能夠通過該選擇器選擇到的Pods,都能夠用這個服務的CLUSTER-IP所訪問到,而這個過程不需要復雜的分布式註冊中心(例如:Zookeeper、Consule或者Eureka)支持,這個機制是Kubernetes內置的特性,集群級別(Cluster-level)的 DNS 也被內置在Kubernetes中。使用DNS作為微服務的服務發現機制是非常有挑戰和困難的,在Kubernetes中集群的DNS指向CLUSTER-IP,而這個CLUSTER-IP對於各個服務是固定不變的,這樣就不會出現使用傳統的DNS解決方案中面對的DNS緩存或者其他詭異的問題。

原書中是component=backend,應該是錯誤,不應該是component而是project
    我們可以給hola-springboot添加一組環境變量,讓其books.backendHost和books.backendPort能夠指定到hola-backend服務上,這樣我們就可以在Kubernetes中運行hola-springboot並使其調用後端的hola-backend服務,在hola-springboot中添加配置。

<fabric8.env.GREETING_BACKENDSERVICEHOST >hola-backend</fabric8.env.GREETING_BACKENDSERVICEHOST>
<fabric8.env.GREETING_BACKENDSERVICEPORT>80</fabric8.env.GREETING_BACKENDSERVICEPORT>
    Spring Boot會解析我們在應用中配置的application.properties,但是它會被系統環境變量所覆蓋,通過執行mvn fabric8:json可以生成新的Kubernetes描述文件。這裏原書中的Spring Boot示例存在問題 ,因為它沒有定義和環境變量之間的映射關系,這樣做是無法工作的,因此譯者采取的方式是修改application.properties中的內容,重新構建鏡像(1.1版本),然後在Kubernetes中部署,把這個例子跑起來。

    在沒有部署到容器之前,我們對於hola-springboot中application.properties的定義是基於ip的,因此我們需要將其修改為基於域名,如下所示:

helloapp.saying=Guten Tag aus
management.security.enabled=false
books.backendHost=hola-backend
books.backendPort=80
其中hola-backend服務支持在Kubernetes中對hola-backend這個域名解析,同時將請求派發到後端的Pods上
    修改pom.xml,將版本號改為1.1,保證每次鏡像的構建都是獨立的。

    在hola-springboot工程目錄下執行mvn -Pf8-build,構建鏡像到本地,然後通過docker push $image將其推到docker hub,這樣Kubernetes才能夠下載對應的鏡像。新的鏡像是weipeng2k/hola-springboot:1.1,這樣也需要新的rc.yml,我們只需要修改原來spring-boot中的鏡像版本即可(將其從1.0改為1.1)。

在hola-springboot工程根目錄下,rc2.yml和svc2.yml對應的是修改後的Kubernetes配置
    運行kubectl create -f rc2.yml和kubectl create -f svc2.yml,創建了hola-springboot對應的ReplicationController和Service,通過kubectl get pods可以觀察已經創建的Pods。

$ kubectl get pods
NAME                    READY     STATUS    RESTARTS   AGE
hola-backend-t06sg      1/1       Running   1          2d
hola-springboot-mdx1v   1/1       Running   0          1m
    現在我們想象有一種Debug技術,能夠有端口轉發的能力,它能夠建立起hola-springboot-mdx1v和本機之間的管理,這樣我們就可以驗證服務是否正常工作,這個功能在Kubernetes中通過kubectl port-forward來完成。由於kubectl port-forward會啟動一個進程來完成本機和Pods之間的通信,所以我們將其設定成為後臺進程,防止它退出。

$ kubectl port-forward hola-springboot-mdx1v 9000:8080 &
[1] 7478
$ kubectl port-forward hola-backend-t06sg 9001:8080 &
[2] 7490
    我們建立兩個port-forward,分別指向hola-springboot和hola-backend,端口分別是本機的9000和9001,現在我們嘗試訪問一下hola-springboot。

$ curl http://localhost:9000/api/holaV1
Handling connection for 9000
Hola Spring Boot @ 172.17.0.5
    返回的172.17.0.5就是Kubernetes為當前Pod分配的IP,接著我們需要訪問一下Book接口,在此之前我們先添加一本書。

$ curl -H "Content-Type: application/json" -X POST -d '{"name":"Java編程的藝術","authorName":"魏鵬","publishDate":"2015-07-01"}' http://localhost:9001/hola-backend/rest/books/
Handling connection for 9001
    在訪問hola-backend添加了一個數據之後,我們訪問hola-springboot。

$ curl http://localhost:9000/api/books/1
Handling connection for 9000
{id=1, version=0, name=Java編程的藝術, authorName=魏鵬, publishDate=2015-07-01}
    我們可以看到hola-backend返回給了hola-springboot正確的查詢數據,因此我們的服務被正確的發現了,而這僅僅是使用到了Kubernetes服務發現的一部分特性。需要特別指出的是,這個過程我們沒有使用任何額外的客戶端組件來完成服務的註冊和發現,我們雖然使用了Java來完成這個場景的構建,但是Kubernetes集群的DNS能夠提供技術無關的支持,一個基礎通用的服務發現機制。

容忍失敗(Fault Tolerance)

    構建類似微服務架構這樣的復雜分布式系統,需要在心中有一個重要的假設:沒有什麼不會壞的(things will fail)。我們能花大量的精力來防止失敗,但是就算這樣,我們也不能預防所有的案例,因此面對這個必然的假設唯一的解法就是:我們面向失敗進行設計,換一句話說就是,如何做到在一個充滿變數的不穩定環境中幸存。

figure out how to survive in an environment where there are failures
集群自愈

    如果一個服務開始變得有問題,我們怎樣發現它呢?理想情況下我們的集群管理系統能夠探測到它,並且通過一切可能的方式通知到我們,這種方式是傳統環境慣用的手段。當把環境切換到一定規模的微服務環境下,我們有一大堆類似的微服務應用,我們真的需要停下來逐個分析到底是哪裏導致了一個服務的不正常嗎?長時間運行的服務可能處於不健康的狀態。一個簡單的途徑就是把我們的微服務設計成為一種能夠在任意時刻被殺死,特別是能夠當它們出現問題時被殺死的架構體系。

    Kubernetes提供一組開箱即用的健康探測儀(Probe),我們能用它們來實現集群對實例的管理,使之能夠做到自愈。第一個需要介紹的是readiness探測儀,Kubernetes通過它來判斷當前的Pod是否能夠被服務發現或者掛載到負載均衡上,需要readiness探測儀的原因是應用在容器中啟動之後,仍需要一段時間啟動應用的進程,這時需要等待應用已經完全啟動完成之後才能夠讓流量進入。

    如果我們在剛啟動的階段放入流量,該Pod並不一定在那一刻處於正常狀態,用戶就會獲得失敗的結果或者不一致的狀態。使用readiness探測儀,我們能夠讓Kubernetes查詢一個HTTP端點,當且僅當該端點返回200或者其他結果時才會放入流量。如果Kubernetes探測到一個Pod在一段時間內readiness探測儀一直返回失敗,這個Pod將會被殺死並重啟。

    另一個健康探測儀被稱為liveness探測儀,它和readiness探測儀很像,它被檢測的時間段是在Pod到達ready狀態後,當時在流量放入前(可以理解為早於readiness探測儀)。如果liveness探測儀(可能也是一個簡單的HTTP端點)返回了一個不健康的狀態(比如:HTTP 500),Kubernetes就會自動的殺死該Pod並重啟。

    當我們使用jboss-forge工具以及fabric8-setup命令進行工程創建時,一個readiness探測儀就被默認創建了,可以觀察一下hola-springboot的pom.xml,如果沒有創建,可以使用fabric8-readiness-probe或者fabric8-liveness-probe兩個maven命令進行創建。

<fabric8.readinessProbe.httpGet.path>/health</fabric8.readinessProbe.httpGet.path>
<fabric8.readinessProbe.httpGet.port>8080</fabric8.readinessProbe.httpGet.port>
<fabric8.readinessProbe.initialDelaySeconds>5</fabric8.readinessProbe.initialDelaySeconds>
<fabric8.readinessProbe.timeoutSeconds>30</fabric8.readinessProbe.timeoutSeconds>
    可以看到在pom.xml中對於readiness探測儀的配置,而生成的Kubernetes描述文件中的內容如下:

readinessProbe: #pod內容器健康檢查的設置
  httpGet: #通過httpget檢查健康,返回200-399之間,則認為容器正常
    path: "/health"
    port: 8080
  initialDelaySeconds: 5
  timeoutSeconds: 30
    這意味著readiness探測儀在hola-springboot中已經進行了配置,而Kubernetes會周期性檢查Pod的/heath端點。當我們給hola-springboot添加了actuator時,就默認添加了一個/heath端點,這點在Dropwizard和WildFly Swarm也可以按照一樣模式處理!

斷路器(Circuit Breaker)

    作為服務提供者,你的職責是提供給消費者穩定的服務,根據《Java環境下的微服務》章節提到的承諾設計,服務提供者也會依賴其他的服務或者下遊系統,但是這種依賴不能是強依賴。一個服務提供者對其消費者服務承諾有完全的責任,因為分布式系統中總會存在失敗,而這些失敗將會導致承諾無法被兌現。在我們之前的例子中,hola-spring會調用到hola-backend,如果hola-backend不可用,將會發生什麼?在這個場景下我們如何能夠信守服務承諾?

    我們需要處理這些分布式系統中常見的錯誤,一個服務可能會不可用,底層網絡可能會間歇性的不穩定,後端服務可能因為負載升高而導致它的響應變慢,一個後端服務的bug可能會造成服務調用時收到異常。如果我們不顯式的處理這些場景,我們就會面臨自己提供的服務降級的風險,可能會使線程處於阻塞狀態,不僅如此可能會造成獲取的資源(如:數據庫鎖或者其他資源)沒有被釋放,進而導致整個分布式系統出現雪崩。為了解決這個問題,我們將使用NetflixOSS工具棧下的Hystrix。

    Hystrix是一個支持容錯的Java類庫,它能夠支持微服務兌現服務的承諾,主要在以下幾個方面:

當依賴不可用時提供保護
支持監控並提供調用依賴時的超時機制,用於調用依賴服務時的延遲問題
負載隔離和自愈
優雅降級
實時的監控失敗狀態
支持處理失敗時的業務邏輯
    使用Hystrix的方式,就是使用命令模式,將調用外部依賴的邏輯放置在HystrixCommand的實現中,也就是將調用外部可能失敗的代碼放置在run()方法中。為了幫助我們開始了解這個過程,我們從hola-wildflyswarm項目入手,我們使用Netflix的最佳實踐,顯式的進行調用,為什麼這樣做?因為調試分布式系統是一件非常困難的工作,調用棧分布在不同的機器上,而越少的幹預使用者,使調試過程變得簡單就顯得很重要了。

雖然Hystrix提供了註解的使用方式,但是我們仍然使用最基本的方式去用
    在hola-wildflyswarm項目中,增加依賴:

<dependency>
    <groupId>com.netflix.hystrix</groupId>
    <artifactId>hystrix-core</artifactId>
    <version>${hystrix.version}</version>
</dependency>
    但實際上只需要增加<hystrix.version>1.5.12</hystrix.version>到pom屬性即可。 原書中的示例在新的docker ce下理論已經無法跑起來,支離破碎的實例代碼讓讀者無法串聯起整個過程,因此譯者認為首先需要將hola-wildflyswarm跑起來,然後添加斷路器的相關功能。

這個過程中,譯者會介紹一下如何將鏡像推送到aliyun,避免中美交互的網速問題。
    首先在hola-wildflyswarm工程下,啟動jboss-forge,然後運行fabric8-setup。然後修改POM,以下針對關鍵點做說明,具體的內容可以GitHub上對應的pom。對docker-maven-plugin需要增加一些配置,用來禁用fabric8提供的jolokia,這個組件目前和WildFly有兼容性問題。

<plugin>
    <groupId>io.fabric8</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>0.14.2</version>
    <configuration>
        <images>
            <image>
                <name>${docker.image}</name>
                <build>
                    <from>${docker.from}</from>
                    <assembly>
                        <basedir>/app</basedir>
                        <inline>
                            <id>${project.artifactId}</id>
                            <files>
                                <file>
                                    <source>
                                        ${project.build.directory}/${project.build.finalName}-swarm.jar
                                    </source>
                                    <outputDirectory>/</outputDirectory>
                                </file>
                            </files>
                        </inline>
                    </assembly>
                    <env>
                        <JAVA_APP_JAR>${project.build.finalName}-swarm.jar</JAVA_APP_JAR>
                        <AB_JOLOKIA_OFF>true</AB_JOLOKIA_OFF>
                        <AB_OFF>true</AB_OFF>
                        <JOLOKIA_OFF>true</JOLOKIA_OFF>
                    </env>
                </build>
            </image>
        </images>
    </configuration>
</plugin>
    maven配置屬性需要調整,鏡像from的位置需要調整,不能使用fabric8的tomcat,否則跑不起來,因為hola-wildflyswarm到底還是一個基於JavaEE的項目。Kubernetes的readiness地址需要調整一下,這裏放置了一個rest接口。

<properties>
    <failOnMissingWebXml>false</failOnMissingWebXml>
    <docker.from>docker.io/fabric8/java-jboss-openjdk8-jdk:1.0.10</docker.from>
    <docker.image>weipeng2k/${project.artifactId}:${project.version}</docker.image>
    <fabric8.readinessProbe.httpGet.path>/api/holaV1</fabric8.readinessProbe.httpGet.path>
    <fabric8.readinessProbe.httpGet.port>8080</fabric8.readinessProbe.httpGet.port>
    <fabric8.readinessProbe.initialDelaySeconds>5</fabric8.readinessProbe.initialDelaySeconds>
    <fabric8.readinessProbe.timeoutSeconds>30</fabric8.readinessProbe.timeoutSeconds>
    <fabric8.env.GREETING_BACKEND_SERVICE_HOST>hola-backend</fabric8.env.GREETING_BACKEND_SERVICE_HOST>
    <fabric8.env.GREETING_BACKEND_SERVICE_PORT>80</fabric8.env.GREETING_BACKEND_SERVICE_PORT>
    <fabric8.env.AB_JOLOKIA_OFF>true</fabric8.env.AB_JOLOKIA_OFF>
    <version.wildfly-swarm>2017.6.1</version.wildfly-swarm>
    <hystrix.version>1.5.12</hystrix.version>
</properties>
    可以看到hola-wildflyswarm通過fabric8.env的配置形式,將系統變量進行了設置,使得容器能夠在運行時感知到底層hola-backend提供的服務所在域名和端口,而這個hola-backend域名對應的真實地址,只有在Kubernetes環境中,由Kubernetes提供給所有的Pod。

    在hola-wildflyswarm下,可以看到存在rc.yml和svc.yml,同時我們在項目中進行REST調用後,不再使用Map來作為返回值,而是定義了Book這個類型用來承接返回,接下來我們將它們運行起來。

$ kubectl create -f rc.yml
replicationcontroller "hola-wildflyswarm" created
$ kubectl create -f svc.yml
service "hola-wildflyswarm" created
    運行kubectl port-forward hola-backend-t06sg 9001:8080 &,我們做一下數據的添加,將hola-backend的一個Pod端口映射到本機的9001上。

$ curl -H "Content-Type: application/json" -X POST -d '{"name":"Java編程的藝術","authorName":"魏鵬","publishDate":"2015-07-01"}' http://localhost:9001/hola-backend/rest/books/
Handling connection for 9001
    添加數據之後,我們通過kubectl get pods查詢一下創建的Pod。

$ kubectl get pods
NAME                      READY     STATUS    RESTARTS   AGE
hola-backend-t06sg        1/1       Running   4          24d
hola-springboot-mdx1v     1/1       Running   3          21d
hola-wildflyswarm-9gv39   1/1       Running   0          1h
    接著創建hola-wildflyswarm-9gv39的代理,kubectl port-forward hola-wildflyswarm-9gv39 9002:8080 &,然後再請求一下接口,看是否能夠獲得數據。

$ curl http://localhost:9002/api/books/1
Handling connection for 9002
Book{id=2, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
    從調用接口的返回可以看到,hola-wildflyswarm通過REST調用到了hola-backend,但是如果hola-backend應用出現問題,我們再請求hola-wildflyswarm會發生什麼呢?

$ kubectl scale rc hola-backend --replicas=0
replicationcontroller "hola-backend" scaled
$ kubectl get pods
NAME                      READY     STATUS    RESTARTS   AGE
hola-springboot-mdx1v     1/1       Running   3          21d
hola-wildflyswarm-9gv39   1/1       Running   0          1h
$ curl http://localhost:9002/api/books/1
Handling connection for 9002
^C
    我們先將hola-backend縮容到0個,然後再請求原有的hola-wildflyswarm接口,結果卡在那裏了,也就是hola-wildflyswarm提供的服務無法兌現承諾,我們需要使用Hystrix改造後,就可以在hola-backend後端服務出現問題時,依舊在一定程度上兌現服務承諾。

public class BookCommand extends HystrixCommand<Book> {

    private final String host;
    private final int port;
    private final Long bookId;

    public BookCommand(String host, int port, Long bookId) {
        super(Setter.withGroupKey(
                HystrixCommandGroupKey.Factory
                        .asKey("wildflyswarm.backend"))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withCircuitBreakerEnabled(true)
                        .withCircuitBreakerRequestVolumeThreshold(5)
                        .withMetricsRollingStatisticalWindowInMilliseconds(5000)
                ));

        this.host = host;
        this.port = port;
        this.bookId = bookId;
    }

    @Override
    protected Book run() throws Exception {
        String backendServiceUrl = String.format("http://%s:%d",
                host, port);
        System.out.println("Sending to: " + backendServiceUrl);
        Client client = ClientBuilder.newClient();
        Book book = client.target(backendServiceUrl).path("hola-backend").path("rest").path("books").path(
                bookId.toString()).request().accept("application/json").get(Book.class);

        return book;
    }

    @Override
    protected Book getFallback() {
        Book book = new Book();
        book.setAuthorName("老中醫");
        book.setId(999L);
        book.setPublishDate(new date());
        book.setVersion(1);
        book.setName("頸椎病康復指南");

        return book;
    }
}
    我們可以看到,通過繼承HystrixCommand然後返回Book,首先看構造函數,它進行了Hystrix配置。最關鍵的點是將有可能失敗的代碼(遠程調用邏輯)放置在run()方法中,在run()方法中對遠程hola-backend服務進行調用。

    如果後端服務在一段時間內不可用,斷路器就會激活,請求將會被攔截,以快速失敗的形式體現出來,斷路器行為將會在後端服務恢復後關閉,這樣就相當於在後端服務出現問題時,進行了服務降級。在BookCommand示例中,可以看到對hola-backend後端請求出現5次問題後將會激活斷路器,同時配置了5秒的窗口,用於檢查後端服務是否恢復。

可以通過搜索Hystrix文檔來了解更多的配置項和相關功能,這些配置功能可以通過外部配置或者運行時配置加以運用
    如果後端服務變得不可用或者延遲較高,Hystrix的斷路器將會介入,隨著斷路器的介入,我們hola-wildflyswarm服務的承諾如何兌現?答案是和場景相關。舉個例子:如果我們的服務是給用戶一個專屬的書籍推薦列表,我們會調用後臺的圖書推薦服務,如果圖書推薦服務變得很慢或者不可用時該怎麼辦?我們將會降級圖書推薦服務,可能返回一個通用的推薦圖書列表。為了達到這個效果,我們需要使用Hystrix的fallback方法。在BookCommand示例中,通過實現getFallback()方法,在這個方法中,我們返回了一本默認的書。

    接下來看一下使用了BookCommand的新接口:

@Path("/api")
public class BookResource2 {

    @Inject
    @ConfigProperty(name = "GREETING_BACKEND_SERVICE_HOST",
            defaultValue = "localhost")
    private String backendServiceHost;
    @Inject
    @ConfigProperty(name = "GREETING_BACKEND_SERVICE_PORT",
            defaultValue = "8080")
    private int backendServicePort;

    @Path("/books2/{bookId}")
    @GET
    public String greeting(@PathParam("bookId") Long bookId) {
        return new BookCommand(backendServiceHost, backendServicePort, bookId).execute().toString();
    }
}
    我們先創建一個腳本,interval.sh,它用來每隔1秒鐘調用一下hola-wildflyswarm的服務。

#!/bin/sh
for i in $(seq 1000); do
  curl http://localhost:9002/api/books2/1
  echo ""
  sleep 1
done;
    原有的hola-wildflyswarm服務不支持斷路器,我們將其停止,可以通過kubectl delete all -l project=hola-wildflyswarm,然後使用svc2.yml和rc2.yml創建新的hola-wildflyswarm。我們先將interval腳本運行起來,這時hola-backend服務並沒有啟動,然後在一段時間後,將hola-backend服務通過kubectl scale rc hola-wildflyswarm --replicas=1擴容1個實例,然後可以看到,對hola-wildflyswarm服務的請求返回出現了變化。

需要往新生成的hola-backend中添加Book數據,因為重啟後數據丟失
Book{id=999, name='頸椎病康復指南', version=1, authorName='老中醫', publishDate=Sun Sep 24 11:18:45 UTC 2017}
Book{id=999, name='頸椎病康復指南', version=1, authorName='老中醫', publishDate=Sun Sep 24 11:18:46 UTC 2017}
Book{id=999, name='頸椎病康復指南', version=1, authorName='老中醫', publishDate=Sun Sep 24 11:18:47 UTC 2017}
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
    當後端服務恢復時,調用鏈路轉而正常。這個例子比原書中顯得更加通用,但是應用對於fallback或者優雅降級並不是一直優於無法兌現服務承諾,原因是這些選擇是基於場景的。例如,如果設計一個資金轉換服務,如果後端服務掛掉了,你可能更希望拒絕轉賬,而不是fallback,你可能更希望的解決方式是當後端服務恢復後,轉賬才能繼續。因此,這裏不存在 銀彈 ,但是理解fallback和降級卻可以這樣來:是否使用fallback取決於用戶體驗,是否選擇優雅降級取決於業務場景。

隔水艙(Bulkhead)

    我們看到Hystrix提供了許多開箱即可用的功能,服務的一兩次失敗並不會由於服務的延遲導致斷路器的開啟。而這種情況就是分布式環境下最糟糕的,它很容易導致所有的工作線程瞬間都被卡主,從而導致級聯錯誤,由此導致服務不可用。我們希望能夠減輕由依賴服務的延遲導致所有資源不可用的困境,為了達到目的,我們引入了一項技術,叫做隔水艙 -- Bulkhead。隔水艙是將資源切分成不同的部分,一個部分耗盡,不會影響到其他,你可以從飛機或者船只的設計中看到這個模式,當出現撞擊或意外時,只有會部分受損,而不是全部。

    Hystrix通過線程池技術來實現Bulkhead模式,每一個下遊依賴都會被分配一個線程池,在這個線程池中會和外部系統通信。Netflix針對這些線程池進行了調優以確保它們的上下文切換對用戶的影響降到最低,但是如果你非常關註這個點,也可以做一些測試或者調優。如果一個下遊依賴延遲變高,為這個下遊依賴分配的線程池將會耗盡,進而導致對這個依賴發起的新請求被拒絕,這時就可以采用服務降級策略而不用等著錯誤的級聯。

    如果不想使用線程池來完成Bulkhead模式,Hystrix也支持調用線程上的semaphores來實現這個模式,可以訪問Hystrix Documentation來獲得更多信息。

    Hystrix的默認實現是為下遊依賴分配了一個核心線程數為10的線程池,該線程池不是采用阻塞隊列作為任務的傳遞裝置,而是使用了一個SynchronousQueue。如果你需要調整它,可以通過線程池配置加以調整,下面我們看一個例子:

public class BookCommandBuckhead extends HystrixCommand<Book> {

    private final String host;
    private final int port;
    private final Long bookId;

    public BookCommandBuckhead(String host, int port, Long bookId) {
        super(Setter.withGroupKey(
                HystrixCommandGroupKey.Factory
                        .asKey("wildflyswarm.backend.buckhead"))
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                        .withMaxQueueSize(-1)
                        .withCoreSize(5)
                )
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withCircuitBreakerEnabled(true)
                        .withCircuitBreakerRequestVolumeThreshold(5)
                        .withMetricsRollingStatisticalWindowInMilliseconds(5000)
                ));

        this.host = host;
        this.port = port;
        this.bookId = bookId;
    }

    @Override
    protected Book run() throws Exception {
        String backendServiceUrl = String.format("http://%s:%d",
                host, port);
        System.out.println("Sending to: " + backendServiceUrl);
        Client client = ClientBuilder.newClient();
        Book book = client.target(backendServiceUrl).path("hola-backend").path("rest").path("books").path(
                bookId.toString()).request().accept("application/json").get(Book.class);
        Random random = new Random();
        int i = random.nextInt(1000) + 1;
        Thread.sleep(i);

        return book;
    }

    @Override
    protected Book getFallback() {
        Book book = new Book();
        book.setAuthorName("老中醫");
        book.setId(999L);
        book.setPublishDate(new Date());
        book.setVersion(1);
        book.setName("頸椎病康復指南");

        return book;
    }
}
    上面例子中,將線程池的核心線程數設置為5個,也就是同一時刻,只能接受5個並發請求,在run()方法中,可以看到我們選擇隨機的睡眠一段時間,同時創建第三個rest接口:

@Path("/api")
public class BookResource3 {

    @Inject
    @ConfigProperty(name = "GREETING_BACKEND_SERVICE_HOST",
            defaultValue = "localhost")
    private String backendServiceHost;
    @Inject
    @ConfigProperty(name = "GREETING_BACKEND_SERVICE_PORT",
            defaultValue = "8080")
    private int backendServicePort;

    @Path("/books3/{bookId}")
    @GET
    public String greeting(@PathParam("bookId") Long bookId) {
        return new BookCommandBuckhead(backendServiceHost, backendServicePort, bookId).execute().toString();
    }
}
    原有的hola-wildflyswarm服務不支持Bulkhead,我們將其停止,可以通過kubectl delete all -l project=hola-wildflyswarm,然後使用svc3.yml和rc3.yml創建新的hola-wildflyswarm。我們使用quick腳本,快速的發起調用,如果超過5個線程同時訪問hola-backend,將會觸發fallback。

#!/bin/sh
sleep 5
for i in $(seq 20); do
  curl http://localhost:9002/api/books3/1
  echo ""
done;
    運行上面的腳本,觀察輸出,可以看到在正常的返回中出現了fallback數據,也就是下遊依賴資源耗盡時,不會拖住當前服務的線程,不會造成級聯錯誤。

$ ./quick.sh
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=999, name='頸椎病康復指南', version=1, authorName='老中醫', publishDate=Tue Sep 26 14:34:44 UTC 2017}
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
負載均衡

    在一個具備高度伸縮能力的分布式系統中,我們需要一個方式能否發現服務並在服務集群上做到負載均衡。就像我們之前看到的例子,微服務應用必須能夠處理失敗,我們依賴的服務實例時刻都在加入或者離開集群,因此我們需要通過負載均衡來應對可能的調用失敗。不太成熟的途徑做到負載均衡的方式是使用round-robin域名解析,但是這個往往不足以應對負載均衡的場景,因此我們也需要會話粘連,自動伸縮等負載的負載均衡策略。讓我們看一下在微服務環境下做負載均衡與傳統的方式有何不同吧。

Kubernetes的負載均衡

    Kubernetes最好的一點就是提供了許多開箱即用的分布式特性,而不需要添加額外的服務端組件或者客戶端依賴。Kubernetes服務提供了微服務的發現機制的同時也提供了服務端的負載均衡功能,事實上,一個Kubernetes服務就是一組Pod的抽象層,它們(Pods)是依靠label selector選擇出來的,而針對選出來的這些Pods,Kubernetes將會把請求按照一定策略發送給它們,默認的策略是round-robin,但是可以改變這個策略,比如會話粘連。需要註意的是,客戶端不需要將Pod主動添加到Service,而是讓Service通過label selector來選擇Pods。客戶端使用CLUSTER-IP或者Kubernetes提供的DNS來訪問服務,而這個DNS並非傳統的DNS,在傳統的DNS實現中,TTL問題是DNS作為負載均衡的難題,但是在Kubernetes中卻不存在這個問題,同時Kubernetes也不依賴硬件做到負載均衡,這些功能完全是內置的。

    為了演示Kubernetes的負載均衡,我們嘗試將hola-backend擴容到2個,然後循環請求前端的hola-wildflyswarm接口,觀察返回的結果。

由於hola-backend實例使用的是內存數據庫,我們將2個實例中的數據初始化成不一樣的,這樣就可以通過返回的數據來推測出請求到了哪個實例
    首先我們進行擴容hola-backend。

$ kubectl scale rc hola-backend --replicas=2
replicationcontroller "hola-backend" scaled
$ kubectl get pods
NAME                      READY     STATUS    RESTARTS   AGE
hola-backend-crrt8        0/1       Running   0          7s
hola-backend-n3sg5        1/1       Running   2          13d
hola-wildflyswarm-bk18g   1/1       Running   1          11d
    擴容完成後,我們在將Pod端口進行映射,因為需要數據初始化操作。

$ kubectl port-forward hola-backend-n3sg5 9001:8080 > backend1.log &
[1] 3471
$ kubectl port-forward hola-backend-crrt8 9002:8080 > backend2.log &
[2] 3487
$ kubectl port-forward hola-wildflyswarm-bk18g 9000:8080 > wildfly.log &
[3] 3514
    對hola-backend實例分別進行數據初始化。

$ curl -H "Content-Type: application/json" -X POST -d '{"name":"Java編程的藝術","authorName":"魏鵬","publishDate":"2015-07-01"}' http://localhost:9001/hola-backend/rest/books/
$ curl -H "Content-Type: application/json" -X POST -d '{"name":"頸椎病康復指南","authorName":"老中醫","publishDate":"2015-07-01"}' http://localhost:9002/hola-backend/rest/books/
    由於hola-wildflyswarm實例由DNS負載均衡來請求到後端,我們運行腳本loadbalance.sh。

#!/bin/sh
for i in $(seq 10); do
  curl http://localhost:9000/api/books/1
  echo ""
  sleep 1
done;
    以下是輸出,讀者運行時可能與此有所不同,但是理論上出現不同的輸出。

Book{id=1, name='頸椎病康復指南', version=0, authorName='老中醫', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='頸椎病康復指南', version=0, authorName='老中醫', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='頸椎病康復指南', version=0, authorName='老中醫', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java編程的藝術', version=0, authorName='魏鵬', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='頸椎病康復指南', version=0, authorName='老中醫', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='頸椎病康復指南', version=0, authorName='老中醫', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='頸椎病康復指南', version=0, authorName='老中醫', publishDate=Wed Jul 01 00:00:00 UTC 2015}
我們需要客戶端負載均衡嗎?

    如果你需要更加精細化的負載均衡控制,那麼也可以在Kubernetes中使用客戶端負載均衡,使用特定的算法來決定調用到服務背後的哪個Pod。你升職可以實現類似權重的負載均衡,跳過哪些看起來失敗率較高的Pod,而這種客戶端負載均衡技術往往都是語言相關的。在大多數情況下,還是推薦使用語言或者技術無關的方案,因為Kubernetes內置的服務端負載均衡技術已經足夠了,但是如果你想使用客戶端負載均衡,那麼你可以嘗試類似:SmartStack、bakerstreet.io或者NetflixOSS Ribbon。

    在接下來的例子中,我們使用NetflixOSS Ribbon來做客戶端負載均衡,有許多不同的方式使用Ribbon,並且可以選擇一些不同的註冊和發現客戶端進行適配,比如:Eureka和Consul,但是當運行在Kubernetes中時,我們可以選擇使用Kubernetes內置的API來完成服務發現。要啟動這個特性,需要使用Kubeflix中的ribbon-discovery,我們首先需要新增依賴:

<dependency>
    <groupId>org.wildfly.swarm</groupId>
    <artifactId>ribbon</artifactId>
</dependency>
<dependency>
    <groupId>io.fabric8.kubeflix</groupId>
    <artifactId>ribbon-discovery</artifactId>
    <version>${kubeflix.version}</version>
</dependency>
其中${kubeflix.version}為1.0.15
    在Spring Boot應用中,我們可以使用Spring Cloud,它也提供了對Ribbon的整合,我們可以這樣依賴:

<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon-core</artifactId>
    <version>${ribbon.version}</version>
</dependency>
<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon-loadbalancer</artifactId>
    <version>${ribbon.version}</version>
</dependency>
    在pom中,需要增加環境變量配置。

<fabric8.env.USE_KUBERNETES_DISCOVERY>true</fabric8.env.USE_KUBERNETES_DISCOVERY>
    接著看如何將Ribbon整合入我們的應用,我們新建了一個rest接口。

@Path("/api")
public class BookResource4 {

    @Inject
    @ConfigProperty(name = "GREETING_BACKEND_SERVICE_HOST",
            defaultValue = "localhost")
    private String backendServiceHost;
    @Inject
    @ConfigProperty(name = "GREETING_BACKEND_SERVICE_PORT",
            defaultValue = "8080")
    private int backendServicePort;

    private String useKubernetesDiscovery;

延伸阅读

    评论