天天看點

高并發下如何實作無損擴縮容

作者:閃念基因

“在今天的數字時代,應用程式和線上服務的高并發已成為常态。當數百萬使用者同時通路您的網站或應用時,如何應對這一挑戰變得至關重要。”

為了確定高可用性和無損性能,您需要能夠在不中斷服務的情況下擴充或縮減資源。本文将深入探讨高并發下如何實作無損擴縮容,以確定您的業務在壓力下保持頂尖表現。

01

介紹

1.1 文章介紹

在本文中,我們将探讨高并發環境下的無損擴縮容政策。我們将詳細介紹在應用程式上線和下線時可能出現的問題,并提供實際解決辦法。這些建議将有助于確定您的系統在應對高并發負載時保持穩定和高性能。

1.2 高并發和無損擴縮容的介紹

高并發是指系統需要同時處理大量使用者請求的情況。這可能包括電子商務網站的促銷活動、社交媒體平台上的熱門話題或應用程式的爆發性流量。在這種情況下,傳統的擴容政策可能會導緻系統性能下降,使用者體驗下降,甚至系統崩潰。

無損擴縮容是一種政策,允許您在不中斷服務的情況下動态地增加或減少計算、存儲和網絡資源。這可以通過自動化工具和政策來實作,以滿足實際需求。

1.3 一般微服務無損擴縮容的問題

擴容情況:在應用上線釋出的過程中,一個常見但具有挑戰性的情況是在服務剛剛啟動後,系統可能還處于JVM JIT編譯階段或者某些中間件加載的過程中。此時,如果系統面臨大規模的請求流量沖擊,可能會導緻新啟動的服務執行個體不堪重負。

在實際場景中,我們曾遇到這樣的情況:當服務提供者(provider)啟動後,卻遭遇到資料庫連接配接異常,這是因為系統未在啟動前做好必要的資源準備工作。盡管服務提供者已在注冊中心中注冊,但由于資料庫異常尚未得到修複,服務提供者無法正常提供服務,這會導緻大量請求無法正常響應,最終傳回異常結果。

縮容情況:應用縮容的過程中,常見問題之一是服務消費者在感覺到服務提供者已下線時存在一定的延遲。這意味着在某段時間内,請求仍然被路由到已下線的服務提供者執行個體,導緻連接配接被拒絕異常。

在實際應用中,可能存在一種情況,即部署了服務提供者的其中一個執行個體,并且該服務執行個體在被消費者調用後,通過kill -9強制終止。盡管服務程序實際上已經被終止,但服務的注冊資訊可能仍然存留在注冊中心或者消費者本地緩存清單中,未能清除。是以,消費者服務仍能夠發現該執行個體,擷取其IP和端口資訊,進行調用它,出現異常。

另外一個問題是服務執行個體在接收到SIGKILL信号時會立即關閉,但此時可能仍有請求在隊列中等待處理。如果立即關閉服務執行個體,這些請求将會丢失。

假設百勝的業務中,有一個購物車服務。這個購物車服務負責管理每個使用者的購物車内容,并提供添加商品、删除商品、結算等操作。此服務通常以微服務的形式部署在容器中,并由負載均衡器分發請求。在某個瞬間,購物車服務的某個執行個體接收到了大量的請求,這些請求都需要修改購物車内容,例如添加商品到購物車。服務執行個體正忙于處理這些請求,将它們添加到購物車,但此時,作業系統或容器編排工具決定(KILL)終止該執行個體。就會損害使用者體驗,導緻使用者資料丢失,喪失信任。

1.4 實作無損擴容的必要

實作無損擴縮容的原因是多方面的:

  • 高可用性:在高并發環境下,使用者期望服務始終可用。無損擴縮容確定即使在負載增加時也能保持服務的可用性。
  • 性能優化:無損擴縮容允許配置設定更多資源以提高系統性能,以應對高并發壓力。
  • 成本控制:通過動态配置設定資源,您可以減少不必要的成本,避免過度配置。
  • 自動化:實施無損擴容通常涉及自動化工具和決策系統,可以自動執行資源配置設定的操作,減少了手動幹預的需要,提高了效率。
  • 靈活性:系統的架構可以更加靈活,适應變化的負載需求。無損擴容通常與容器化、微服務架構等現代技術相結合,提供更大的靈活性。
  • 快速響應:自動化擴容政策可以快速響應負載增加的情況,進而降低了使用者等待時間,提高了系統的可用性。

02

早期擴容方案

擴容方案:

高并發下如何實作無損擴縮容

縮容方案:

當容器收到下線信号時,利用Kubernetes提供的PreStop鈎子,執行以下操作用于優雅終止應用。

1. 調用shutdown接口,通知服務注冊中心立即down掉本應用執行個體。

2. 等待95秒,確定下線服務在調用方本地緩存的服務執行個體清單中失效。

3. 執行pkill終止應用程序。

apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  containers:
  - name: lifecycle-demo-container
    image: nginx
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh","-c","curl -X GET http://127.0.0.1:8080/xx/instance/shutdown -H "Content-type:application/json";sleep
            95;pkill java"]           

為什麼等待95秒?

當執行PreStop時,會立即使應用執行個體在Eureka下線。是以需要考量調用方本地緩存重新整理所需最大時間(依據配置預設時間),包括:

Eureka Server端讀寫緩存同步間隔,預設30S eureka.responseCacheUpdateIntervalMs=30000 和eureka.shouldUseReadOnlyResponseCache=true

Eureka Client端的服務清單緩存同步間隔,預設30S eureka.client.refresh.interval=30

Ribbon服務清單緩存同步間隔,預設30S ribbon.ServerListRefreshInterval=30000

綜合在預設配置情況下,各調用方緩存重新整理機制,95秒可以覆寫從Eureka下線到服務調用方緩存完全重新整理的最大時間。這樣可以確定在關閉應用程序前,調用方不會再通過本地緩存通路到已下線的服務執行個體。

03

優化方案

盡管在部署中實施了上述水準擴縮容方案,但在一些項目中仍然出現了各種問題,這可能涉及到多個方面,需要進一步分析和解決,下面描述優化方案和問題原因。

3.1 延時注冊

描述:預設情況下應用容器啟動後預設直接注冊到注冊中心,意味着準備好提供服務,然而,雖然應用容器已經注冊到服務注冊中心,但這并不意味着它已經完全準備好應對來自外部的請求。比如:但某些業務在提供服務前,需要進行預啟動檢查,通過後才可注冊至注冊中心。

解決方案:

Kubernetes提供了就緒探針,合理使用可進行延時注冊。

apiVersion: v1
kind: Pod
metadata:
  name: goproxy
  labels:
    app: goproxy
spec:
  containers:
  - name: goproxy
    image: registry.k8s.io/goproxy:0.1
    ports:
    - containerPort: 8080
    readinessProbe:
      httpGet:
        path: /application/readiness
        port: 8080
        scheme: HTTP
      initialDelaySeconds: 15
      periodSeconds: 10
    livenessProbe:
      tcpSocket:
        port: 8080
      initialDelaySeconds: 15
      periodSeconds: 10           

改造readiness探針接口,以Java代碼片段為例:

@GetMapping(value = "/application/readiness")
public void readiness(){
    // 1. 進行系統啟動後 資料預熱


    // 2. 進行系統(業務)啟動預檢


    // 3. 自檢成功後注冊到注冊中心,不通過傳回異常
}           

容器啟動後進行readiness探針,通過則注冊到注冊中心,不通過則不注冊。這樣可以避免不ready的執行個體注冊上線。通過擴充就緒檢查機制,可以更準确地控制服務執行個體的注冊時機,保證注冊到注冊中心的執行個體一定是ready狀态,進而提高服務可靠性。

3.2 啟動預加載

描述:某業務系統中,擴容初期可能會出現HTTP 500 、504錯誤,表示網關逾時,當應用程式負載較大時異常情況更多。經過多次排查和優化,總結出以下優化方案:

ribbon預加載優化

在目前的微服務架構中,使用Ribbon負載均衡器,負責将請求分發到後端的服務執行個體。然而,Ribbon在初始化時需要從服務注冊中心擷取服務清單,以決定如何分發請求。這個初始化過程可能會花費一定的時間,特别是高并發情況下,加上服務執行個體數量龐大或者注冊中心偶爾響應較慢的情況下,這個時長更會延長。可以做以下配置将Ribbon初始化工作前置在提供服務之前

ribbon:
  eager-load:
    enabled: true
    clients: xxxService, xxxxService # 消費端服務名           

緩存預加載優化

業務系統通常會實施緩存預加載政策,以優化系統的性能和響應時間。然而,緩存加載的一個常見挑戰是確定加載的資料完整性和一緻性。在實際場景中,雖然緩存預加載可以顯著提升初期系統的響應性,但仍然可能出現部分資料未加載到緩存的情況。為了解決這一問題,通常采用以下政策:

1. 資料完備性檢測

緩存初次加載時機可以放在Spring架構的生命周期鈎子中實作,比如:CommandLineRunner、ApplicationRunner、ApplicationListener ... 按照需求選擇即可。

2. 重新加載機制

重新加載的時機可以選擇在 第一步 中重試,也可選擇在readiness接口中實作,主要實作是對于未加載的資料,實施重新加載機制,確定資料在後續的通路中可以被緩存。

3. 錯誤處理

這可以包括自動重試、錯誤日志記錄和通知等。當然對于嚴重阻塞業務程序的情況,可以選擇不注冊至注冊中心

靜态資源預加載優化

在實際場景中,我們遇到了具體的挑戰。在百勝某應用中,采用了ShardingSphere中間件來實作資料庫分片和SQL改寫。然而,在性能排查過程中,發現ShardingSphere的SQL改寫需要頻繁使用SQL解析器,而在初次解析SQL時,程式執行的解析過程耗費了大量時間,其占比達到了整體執行時間的一半左右。降低了使用者體驗品質。

高并發下如何實作無損擴縮容

為了解決這一性能瓶頸,可以采取預熱SQL解析器的優化措施,在系統啟動過程中,預熱SQL解析器,提前完成一些常見SQL語句的解析工作,以減少初次解析的成本。

類似地,需排查其他有影響的靜态資源是否預加載,也是性能優化措施。如配置檔案、模闆、SPI擴充等。

健康檢測政策優化

在實際應用場景中,我們曾面臨一種情況,即服務在啟動後出現中間件連接配接異常以及資源準備不當的問題,導緻服務執行個體在被通路後,大量的請求遭遇異常響應。這種問題的根本原因在于在服務啟動初期,系統的資源和依賴項未能正确初始化和準備,進而導緻了應用執行個體的不穩定性。為了優化這一問題,關鍵在于改進健康檢測政策:

  1. 主動健康檢測:中間件架構擴充或者使用健康檢測工具主動監測服務的狀态,以及時發現問題。停止提供服務等。
  2. 故障轉移:提高中間件的高可用性,在發現故障時,将請求重定向到備用中間件伺服器,以確定服務的穩定。

3.3 異步消費問題

問題1:

在一個實際的應用場景中,我們遇到了以下情況:百勝某應用采用了Pulsar消息中間件,特别是在進行擴容操作時,出現了一個問題。問題的核心在于Pulsar消費服務在應用執行個體注冊到服務注冊中心之前,就已經進行了消息消費連接配接池的初始化,并開始消費消息。這導緻在應用程式啟動過程中,工作線程被大量的消息消費任務占用,結果應用注冊至注冊中心後,無法有效地處理正常請求,最終導緻了大量的請求錯誤。

優化措施:

為了解決這一問題,我們采用了MQ消費延遲初始化。優化Pulsar消費服務,以確定在應用程式完全啟動後才初始化消息消費連接配接池。這樣可以避免在應用程式啟動初期由于消息消費任務的過早啟動而占用了工作線程,確定應用在正常負載下能夠提供穩定的性能。

問題2:

在百勝實際場景中,我們面臨了一個在縮容過程中出現的複雜問題。具體而言,問題的症結在于應用在進行縮容時的終止過程出現了不同步的情況,導緻了一系列異常情況。這些異常表現為應用在銷毀階段産生異常消息,其中包括"Do not request a bean from BeanFactory in a destroy method Implementation."的異常資訊。

問題的根本原因如下:在Pod銷毀時,服務執行個體會先從Eureka等服務注冊中心下線,然後等待大約95秒的時間,之後才執行應用程序的終止操作。然而,問題出現在等待時間結束後,Pulsar消息處理線程池并未被及時終止,仍在繼續消費消息。與此同時,目前的JVM已經接收到銷毀指令,導緻消息處理過程無法繼續擷取所需的bean資訊,最終引發了異常。

優化措施:

我們重新定義了應用接收到JVM銷毀指令時的中間件銷毀邏輯。具體而言,我們根據業務場景的需求,按照特定的順序逐一銷毀不同的中間件連接配接。特别是,我們增強了Pulsar中間件的銷毀邏輯,将其優先銷毀消費者線程池。

這一優化措施確定了在縮容過程中,不僅服務執行個體能夠安全下線,而且中間件連接配接也得到了精心管理。通過根據特定順序銷毀中間件連接配接,特别是Pulsar消費者線程池的優先銷毀,我們消除了不同步的情況,確定了銷毀過程的可靠性和可維護性,提高了系統的整體性能和穩定性。這一政策對于保障高并發負載下系統的順暢運作具有重要意義。

3.4 其他優化

啟動優化對于各種類型的應用程式和系統都至關重要,特别是對于需要快速響應使用者請求的Web應用程式和雲原生應用。通過有效的啟動優化,可以提供更好的使用者體驗,減少資源消耗,并支援高并發環境下的穩定性。

優化應用啟動時間

  • 減少依賴項:SpringBoot程式的依賴項錯綜複雜,很容易引入到不必要的依賴。通過檢查項目的依賴項,删除不必要的依賴項可以提升啟動速度。
  • 優化自動配置:Spring Boot提供了自動配置機制,根據應用程式的依賴項和配置,自動配置各種元件。過多的自動配置導緻Spring掃描加載的類過多,影響啟動速度,可以使用@EnableAutoConfiguration的exclude屬性,排除不必要的元件。
  • 合理的延遲初始化:對于旁路業務上的Bean,可以選擇懶加載的模式,在需要時才進行初始化。

04

優化後擴縮容方案

4.1 擴容方案

高并發下如何實作無損擴縮容

4.2 縮容方案

05

寫在最後

在高并發環境下,實作無損擴縮容至關重要。這篇文章深入研究了高并發下的挑戰,介紹了無損擴縮容的必要性和優勢。我們探讨了擴容問題,包括延時注冊、健康檢測、餓加載和啟動優化的解決方案,以確定應用在高負載情況下的穩定性和性能。此外,我們還研究了縮容問題,詳細介紹了注冊中心緩存時間的調整和處理異步消息消費延遲的方法。通過這些政策,可以有效解決高并發下的擴容和縮容挑戰,確定業務保持頂尖性能和高可用性。無損擴縮容政策不僅提高了系統的性能和可用性,還有助于控制成本、提高靈活性和快速響應變化的需求。這些實踐将有助于確定您的業務在高并發環境下表現卓越。

作者:嚴林

來源-微信公衆号:百勝技術團隊

出處:https://mp.weixin.qq.com/s/_U22VFgl166n3Y0eCNUUvA