如何使用訊息佇列、Spring Boot和Kubernetes擴充套件微服務

banq發表於2019-01-31

當您進行大規模設計和構建應用程式時,您將面臨兩個重大挑戰:可伸縮性和健壯性。
您應該精心設計您的服務,即使它受到間歇性過載,它仍然可靠地執行。以蘋果商店為例,每年都有數百萬的Apple客戶預先註冊購買新的iPhone,這是數百萬人同時購買物品,蘋果商店流量如果描述為每秒的請求數量,那麼在下午6點到8點之間是有峰值最高峰。

現在想象一下,你的任務是構建這樣的應用程式。
  • 您正在建立一個商店,使用者可以在那裡購買自己喜歡的商品。
  • 您構建一個微服務來呈現網頁並提供靜態資產。您還構建了一個後端REST API來處理傳入的請求。
  • 您希望將兩個元件分開,因為使用相同的REST API,您可以為網站和移動應用程式提供服務。

你的商店上線了!
您決定將應用程式擴充套件為前端的四個例項和後端的四個例項,因為您預測網站比平常更繁忙。網站開始接收越來越多的流量。
前端服務正在處理繁忙流量。但是您注意到連線到資料庫的後端正在努力跟上事務的數量,不用擔心,您可以將後端的伺服器數量擴充套件到8個例項。
收到的流量更多,後端無法應對。
一些服務開始丟棄連線。憤怒的客戶與您的客戶服務取得聯絡。而現在你被淹沒在大流量中,你的後端無法應付這種大流量,失去了很多連線。電子商務網站丟失連線就是丟失交易,就是收入損失。

您的應用程式並非設計為健壯且高度可用:
  • 前端和後端緊密耦合 -  實際上它不能在沒有後端的情況下處理應用程式
  • 前端和後端必須一致擴充套件 -  如果沒有足夠的後端,你可能會淹沒在流量中
  • 如果後端不可用,則無法處理傳入的事務。


您可以重新設計體系結構,以便將前端和後端與佇列分離:
前端將訊息釋出到佇列,而後端則一次處理一個待處理訊息。
架構有一些明顯的好處:
  • 如果後端不可用,則佇列充當緩衝區
  • 如果前端產生的訊息多於後端可以處理的訊息,則這些訊息將緩衝在佇列中
  • 您可以獨立於前端擴充套件後端 - 即您可以擁有數百個前端服務和後端的單個例項

太好了,但是你如何構建這樣的應用程式?您如何設計可處理數十萬個請求的服務?您如何部署動態擴充套件的應用程式?在深入瞭解部署和擴充套件的細節之前,讓我們關注應用程式。

編寫Spring應用程式
現在服務有三個元件:前端,後端和訊息代理。
前端是一個簡單的Spring Boot Web應用程式,帶有Thymeleaf模板引擎;後端是一個消耗佇列訊息的工作者。由於Spring Boot與JSM具有出色的整合,因此您可以使用它來傳送和接收非同步訊息。您可以在learnk8s / spring-boot-k8s-hpa中找到一個連線到JSM的前端和後端應用程式的示例專案。
請注意,該應用程式是用Java 10編寫的,才能利用改進的Docker容器整合。只有一個程式碼庫,您可以將專案配置為作為前端或後端執行。
您應該知道該應用程式具有:

  • 你可以購買物品的主頁
  • 管理皮膚,您可以在其中檢查佇列中的訊息數
  • /health當應用程式準備好接收流量時發出訊號的端點
  • /submit從表單接收提交併在佇列中建立訊息的端點
  • 一個/metrics端點,以露出待處理的訊息的數量在佇列中(以後會更多)

該應用程式可以在兩種模式下執行:
  1. 作為前端,應用程式呈現人們可以購買物品的網頁。
  2. 作為工作者,應用程式等待佇列中的訊息並處理它們。

請注意,在示例專案中,透過等待五秒來模擬處理Thread.sleep(5000)。您可以透過更改application.yaml為您的值來配置任一模式的應用程式。

執行應用程式
預設情況下,應用程式作為前端和工作程式啟動。您可以執行該應用程式,只要您在本地執行ActiveMQ例項,您就應該能夠購買物品並讓系統處理這些物品。
如果檢查日誌,則應該看到工作程式處理專案。
有效!編寫Spring Boot應用程式很容易。
一個更有趣的主題是學習如何將Spring Boot連線到訊息代理。

使用JMS傳送和接收訊息
Spring JMS(Java訊息服務)是一種使用標準協議傳送和接收訊息的強大機制。如果您以前使用過JDBC API,那麼您應該熟悉JMS API,因為它的工作方式類似。您可以使用JMS使用的最流行的訊息代理是ActiveMQ  - 一個開源訊息伺服器。使用這兩個元件,您可以使用熟悉的介面(JMS)將訊息釋出到佇列(ActiveMQ),並使用相同的介面來接收訊息。更妙的是,Spring Boot與JMS的整合非常好,因此您可以立即加快速度。
實際上,以下短類封裝了用於與佇列互動的邏輯:

@Component
public class QueueService implements MessageListener {
private static final Logger LOGGER = LoggerFactory.getLogger(QueueService.class);
@Autowired
  private JmsTemplate jmsTemplate;
  public void send(String destination, String message) {
    LOGGER.info("sending message='{}' to destination='{}'", message, destination);
    jmsTemplate.convertAndSend(destination, message);
  }
@Override
  public void onMessage(Message message) {
    if (message instanceof ActiveMQTextMessage) {
      ActiveMQTextMessage textMessage = (ActiveMQTextMessage) message;
      try {
        LOGGER.info("Processing task " + textMessage.getText());
        Thread.sleep(5000);
        LOGGER.info("Completed task " + textMessage.getText());
      } catch (InterruptedException e) {
        e.printStackTrace();
      } catch (JMSException e) {
        e.printStackTrace();
      }
    } else {
      LOGGER.error("Message is not a text message " + message.toString());
    }
  }
}

您可以使用該send方法將訊息釋出到命名佇列。此外,Spring Boot將為onMessage每個傳入訊息執行該方法。
最後一個難題是如何讓Spring Boot使用該類。您可以透過在Spring Boot應用程式中註冊偵聽器來在後臺處理訊息,如下所示:

@SpringBootApplication
@EnableJms
public class SpringBootApplication implements JmsListenerConfigurer {
  @Autowired
  private QueueService queueService;
public static void main(String[] args) {
    SpringApplication.run(SpringBootApplication.class, args);
  }
@Override
  public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
    SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint();
    endpoint.setId("myId");
    endpoint.setDestination("queueName");
    endpoint.setMessageListener(queueService);
    registrar.registerEndpoint(endpoint);
  }
}

其中id是使用者的唯一識別符號,destination是佇列的名稱。
您可以從GitHub上的專案中完整地讀取Spring佇列服務的原始碼

部署
驗證了應用程式的工作原理,現在是時候部署它了。您可以啟動VPS,安裝Tomcat,並花些時間製作自定義指令碼來測試,構建,打包和部署應用程式。或者您可以編寫您希望擁有的描述:一個訊息代理和兩個使用負載均衡器部署的應用程式。
Kubernetes之類的協調器可以閱讀您的願望清單並提供正確的基礎設施,花在基礎架構上的時間減少意味著更多的時間編碼,這次你將把應用程式部署到Kubernetes。但在開始之前,您需要一個Kubernetes叢集。
您可以註冊Google雲平臺或Azure,並使用雲提供商Kubernetes提供的服務。或者,您可以在將應用程式移動到雲之前在本地嘗試Kubernetes。
minikube是一個打包為虛擬機器的本地Kubernetes叢集。如果您使用的是Windows,Linux和Mac,那就太好了,因為建立群集需要五分鐘。
您還應該安裝kubectl客戶端以連線到您的群集。
你可以找到關於如何安裝的說明minikube,並kubectl從官方文件。如果您在Windows上執行,則應檢視有關如何安裝Kubernetes和Docker的詳細指南
啟動一個具有8GB RAM和一些額外配置的叢集:

minikube start \
  --memory 8096 \
  --extra-config=controller-manager.horizontal-pod-autoscaler-upscale-delay=1m \
  --extra-config=controller-manager.horizontal-pod-autoscaler-downscale-delay=2m \
  --extra-config=controller-manager.horizontal-pod-autoscaler-sync-period=10s


請注意,如果您使用的是預先存在的minikube例項,則可以透過銷燬VM來重新調整VM的大小。只是新增--memory 8096將不會有任何影響。
驗證安裝是否成功。您應該看到列為表的一些資源。叢集已經準備就緒,也許您應該立即開始部​​署?
還沒。
你必須先裝好你的東西。
部署到Kubernetes的應用程式必須打包為容器。畢竟,Kubernetes是一個容器協調器,所以它本身無法執行你的jar。容器類似於fat jar:它們包含執行應用程式所需的所有依賴項。甚至JVM也是容器的一部分。所以他們在技術上是一個更胖的fat-jar。

Docker
將應用程式打包為容器的流行技術是Docker。如果您沒有安裝Docker,可以按照Docker官方網站上的說明進行操作
直接在minikube建立容器影像。
先,按照此命令列印的說明連線Docker客戶端到minikube:

minikube docker-env

請注意,如果切換終端,則需要在minikube重新連線到內部的Docker守護程式。每次使用不同的終端時都應遵循相同的說明。
從專案的根目錄構建容器影像:

docker build -t spring-k8s-hpa .

可以驗證映象是否已構建並準備好執行:
docker images | grep spring

群集已準備好,您打包應用程式,可以要求Kubernetes部署應用程式。

部署到Kubernetes
您的應用程式有三個元件:

  • 呈現前端的Spring Boot應用程式
  • ActiveMQ作為訊息代理
  • 處理事務的Spring Boot後端

您應該分別部署這三個元件。
對於他們每個人你應該建立:
  • 一個部署描述什麼容器部署和配置物件
  • 一個Service物件,充當Deployment部署建立的應用程式的所有例項的負載均衡器

部署中的每個應用程式例項都稱為Pod。

部署ActiveMQ
讓我們從ActiveMQ開始吧。
您應該建立一個activemq-deployment.yaml包含以下內容的檔案:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: queue
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: queue
    spec:
      containers:
      - name: web
        image: webcenter/activemq:5.14.3
        imagePullPolicy: IfNotPresent
        ports:
          - containerPort: 61616
        resources:
          limits:
            memory: 512Mi



該模板冗長但直接易讀:
  • 您從名為webcenter / activemq的官方登錄檔中請求了一個activemq容器
  • 容器在埠61616上公開訊息代理
  • 為容器分配了512MB的記憶體
  • 您要求提供單個副本 - 您的應用程式的單個例項 

建立一個activemq-service.yaml :

apiVersion: v1
kind: Service
metadata:
  name: queue
spec:
  ports:
  - port: 61616 
    targetPort: 61616
  selector:
    app: queue


幸運的是,這個模板更短!
yaml解讀:
  • 您建立了一個公開埠61616的負載均衡器
  • 傳入流量將分發到具有型別標籤的所有Pod(請參閱上面的部署) app: queue
  • 這targetPort是Pods暴露的埠

您可以使用以下命令建立資源:

kubectl create -f activemq-deployment.yaml
kubectl create -f activemq-service.yaml


您可以使用以下命令驗證資料庫的一個例項是否正在執行:

kubectl get pods -l=app=queue


部署前端
建立fe-deployment.yaml:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: spring-boot-hpa
        imagePullPolicy: IfNotPresent
        env:
        - name: ACTIVEMQ_BROKER_URL
          value: "tcp://queue:61616"
        - name: STORE_ENABLED
          value: "true"
        - name: WORKER_ENABLED
          value: "false"
        ports:
        - containerPort: 8080
        livenessProbe:
          initialDelaySeconds: 5
          periodSeconds: 5
          httpGet:
            path: /health
            port: 8080
        resources:
          limits:
            memory: 512Mi


署看起來很像以前的一個。
但是有一些新的欄位:
  • 有一個section 可以注入環境變數
  • 還有liveness probe探針,可以告訴您應用程式何時可以接受流量 


建立fe-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  ports:
  - nodePort: 32000
    port: 80
    targetPort: 8080
  selector:
    app: frontend
  type: NodePort


使用下面命令建立k8s資源:

kubectl create -f fe-deployment.yaml
kubectl create -f fe-service.yaml


可以使用以下命令驗證前端應用程式的一個例項是否正在執行:

kubectl get pods -l=app=frontend


部署後端
建立backend-deployment.yaml:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: backend
      annotations:
        prometheus.io/scrape: 'true'
    spec:
      containers:
      - name: backend
        image: spring-boot-hpa
        imagePullPolicy: IfNotPresent
        env:
        - name: ACTIVEMQ_BROKER_URL
          value: "tcp://queue:61616"
        - name: STORE_ENABLED
          value: "false"
        - name: WORKER_ENABLED
          value: "true"
        ports:
        - containerPort: 8080
        livenessProbe:
          initialDelaySeconds: 5
          periodSeconds: 5
          httpGet:
            path: /health
            port: 8080
        resources:
          limits:
            memory: 512Mi


建立backend-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: backend
  spec:
    ports:
    - nodePort: 31000
      port: 80
      targetPort: 8080
    selector:
      app: backend
    type: NodePort


建立者兩個資源:

kubectl create -f backend-deployment.yaml
kubectl create -f backend-service.yaml


可以驗證後端的一個例項是否正在執行:

kubectl get pods -l=app=backend


部署完成。
它真的有效嗎?
您可以使用以下命令啟動,然後在瀏覽器中訪問該應用程式:
minikube service backend
minikube service frontend

手動擴充套件以滿足不斷增長的需求
單個工作程式可能無法處理大量訊息。實際上,它當時只能處理一條訊息。
如果您決定購買數千件物品,則需要數小時才能清除佇列。
此時您有兩個選擇:

  • 你可以手動放大和縮小
  • 您可以建立自動縮放規則以自動向上或向下擴充套件

讓我們先從基礎知識開始。
您可以使用以下方法將後端擴充套件為三個例項:
kubectl scale --replicas=5 deployment/backend
可以驗證Kubernetes是否建立了另外五個例項:
kubectl get pods

應用程式可以處理五倍以上的訊息。
一旦訊息佇列排空,您可以縮小:
kubectl scale --replicas=1 deployment/backend

如果您知道最多的流量何時達到您的服務,手動擴大和縮小都很棒。
如果不這樣做,設定自動縮放器允許應用程式自動縮放而無需手動干預。
您只需要定義一些規則。

公開應用程式指標
Kubernetes如何知道何時擴充套件您的應用?
很簡單,你必須告訴它。
自動調節器透過監控指標來工作。只有這樣,它才能增加或減少應用程式的例項。
因此,您可以將佇列長度公開為度量標準,並要求autoscaler觀察該值。佇列中的待處理訊息越多,Kubernetes將建立的應用程式例項就越多。
那麼你如何公開這些指標呢?
應用程式有一個/metrics端點,用於公開佇列中的訊息數。如果您嘗試訪問該頁面,您會注意到以下內容:

# HELP messages Number of messages in the queue
# TYPE messages gauge
messages 0


應用程式不會將指標公開為JSON格式。格式為純文字,是公開Prometheus指標的標準。不要擔心記憶格式。大多數情況下,您將使用其中一個Prometheus客戶端庫

在Kubernetes中使用應用程式指標
已準備好進行自動縮放 - 但您應首先安裝度量伺服器。實際上,預設情況下,Kubernetes不會從您的應用程式中提取指標。如果您願意,可以啟用Custom Metrics API
要安裝Custom Metrics API,您還需要Prometheus  - 時間序列資料庫。安裝Custom Metrics API所需的所有檔案都可以方便地打包在learnk8s / spring-boot-k8s-hpa中
應下載該儲存庫的內容,並將當前目錄更改monitoring為該專案的資料夾。
cd spring-boot-k8s-hpa/monitoring

可以建立自定義指標API:

kubectl create -f ./metrics-server
kubectl create -f ./namespaces.yaml
kubectl create -f ./prometheus
kubectl create -f ./custom-metrics-api


應該等到以下命令返回自定義指標列表:

kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq .

任務完成!
您已準備好使用指標。
實際上,您應該已經找到了佇列中訊息數量的自定義指標:

kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/messages" | jq .


恭喜,您有一個公開指標的應用程式和使用它們的指標伺服器。
您最終可以啟用自動縮放!

在Kubernetes中進行自動擴充套件部署
Kubernetes有一個名為Horizo​​ntal Pod Autoscaler的物件,用於監視部署並上下調整Pod的數量。
您將需要其中一個來自動擴充套件例項。
您應該建立一個hpa.yaml包含以下內容的檔案:

apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: spring-boot-hpa
spec:
  scaleTargetRef:
    apiVersion: extensions/v1beta1
    kind: Deployment
    name: backend 
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Pods
    pods:
      metricName: messages
      targetAverageValue: 10


這個檔案很神秘,所以讓我為你解釋一下:
  • 在scaleTargetRef指定Kubernetes監視的部署。在這種情況下,它是前面的worker部署。
  • 使用messages指標metrics來擴充套件您的Pod。當佇列中有超過十條訊息時,Kubernetes將觸發自動擴充套件。
  • 至少,部署應該有兩個Pod。10 個Pods是上限。

建立資源:

kubectl create -f hpa.yaml

提交這個自動縮放器autoscaler後,您應該注意到後端的副本數量是兩個。這是有道理的,因為您要求自動縮放器始終至少執行兩個副本。
您可以檢查觸發自動縮放器的條件以及由此產生的事件:

kubectl describe hpa

自動縮放器autoscaler能夠將Pod數量擴充套件到2,並且它已準備好監視部署。
令人興奮的東西,但它有效嗎?

負載測試
只有一種方法可以知道它是否有效:在佇列中建立大量訊息。
轉到前端應用程式並開始新增大量訊息。在新增訊息時,使用以下方法監視Horizo​​ntal Pod Autoscaler的狀態:
kubectl describe hpa

Pod的數量從2上升到4,然後是8,最後是10。
該應用程式隨訊息數量而變化!歡呼!
您剛剛部署了一個完全可伸縮的應用程式,可根據佇列中的待處理訊息數進行擴充套件。
另外,縮放演算法如下:

MAX(CURRENT_REPLICAS_LENGTH * 2, 4)

在解釋演算法時,文件沒有多大幫助。您可以在程式碼中找到詳細資訊
此外,每分鐘都會重新評估每個放大,而每兩分鐘縮小一次。
以上所有都是可以調整的設定。
但是你還沒有完成。

什麼比自動縮放例項更好?自動縮放叢集。​​​​​​​
跨節點縮放Pod非常有效。但是,如果群集中沒有足夠的容量來擴充套件Pod,該怎麼辦?如果達到峰值容量,Kubernetes將使Pods處於暫掛狀態並等待更多資源可用。如果您可以使用類似於橫向Pod Autoscaler的自動縮放器,對於節點則會很棒。

您可以擁有一個叢集自動縮放器,可以在您需要更多資源時為Kubernetes叢集新增更多節點。叢集自動縮放器具有不同的形式和大小。它也是由雲提供商指定的。
請注意,使用minikube您將無法使用自動縮放器進行測試,因為它根據定義是單節點。
您可以在Github上找到有關叢集自動調節器雲提供程式實現的[url=https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscalercluster-autoscaler]更多資訊[/url]。


概括
大規模設計應用程式需要仔細規劃和測試。
基於佇列的體系結構是一種出色的設計模式,可以解耦您的微服務並確保它們可以獨立擴充套件和部署。
雖然您可以部署部署指令碼,但可以更輕鬆地利用容器協調器(如Kubernetes)自動部署和擴充套件應用程式。​​​​​​​

相關文章