近期業務大量突增微服務效能優化總結-4.增加對於同步微服務的 HTTP 請求等待佇列的監控

乾貨滿滿張雜湊發表於2021-11-04

最近,業務增長的很迅猛,對於我們後臺這塊也是一個不小的挑戰,這次遇到的核心業務介面的效能瓶頸,並不是單獨的一個問題導致的,而是幾個問題揉在一起:我們解決一個之後,發上線,之後發現還有另一個的效能瓶頸問題。這也是我經驗不足,導致沒能一下子定位解決;而我又對我們後臺整個團隊有著固執的自尊,不想通過大量水平擴容這種方式挺過壓力高峰,導致線上連續幾晚都出現了不同程度的問題,肯定對於我們的業務增長是有影響的。這也是我不成熟和要反思的地方。這系列文章主要記錄下我們針對這次業務增長,對於我們後臺微服務系統做的通用技術優化,針對業務流程和快取的優化由於只適用於我們的業務,這裡就不再贅述了。本系列會分為如下幾篇:

  1. 改進客戶端負載均衡演算法
  2. 開發日誌輸出異常堆疊的過濾外掛
  3. 針對 x86 雲環境改進非同步日誌等待策略
  4. 增加對於同步微服務的 HTTP 請求等待佇列的監控以及雲上部署,需要小心達到例項網路流量上限導致的請求響應緩慢
  5. 針對系統關鍵業務增加必要的侵入式監控

增加對於同步微服務的 HTTP 請求等待佇列的監控

同步微服務對於請求超時存在的問題

相對於基於 spring-webflux 的非同步微服務,基於 spring-webmvc 的同步微服務沒有很好的處理客戶端有請求超時配置的情況。當客戶端請求超時時,客戶端會直接返回超時異常,但是呼叫的服務端任務,在基於 spring-webmvc 的同步微服務並沒有被取消,基於 spring-webflux 的非同步微服務是會被取消的目前,還沒有很好的辦法在同步環境中可以取消這些已經超時的任務

我們的基於 spring-webmvc 的同步微服務,HTTP 容器使用的是 Undertow。在 spring-boot 環境下,我們可以配置處理 HTTP 請求的執行緒池大小:

server:
  undertow:
    # 以下的配置會影響buffer,這些buffer會用於伺服器連線的IO操作
    # 如果每次需要 ByteBuffer 的時候都去申請,對於堆記憶體的 ByteBuffer 需要走 JVM 記憶體分配流程(TLAB -> 堆),對於直接記憶體則需要走系統呼叫,這樣效率是很低下的。
    # 所以,一般都會引入記憶體池。在這裡就是 `BufferPool`。
    # 目前,UnderTow 中只有一種 `DefaultByteBufferPool`,其他的實現目前沒有用。
    # 這個 DefaultByteBufferPool 相對於 netty 的 ByteBufArena 來說,非常簡單,類似於 JVM TLAB 的機制
    # 對於 bufferSize,最好和你係統的 TCP Socket Buffer 配置一樣
    # `/proc/sys/net/ipv4/tcp_rmem` (對於讀取)
    # `/proc/sys/net/ipv4/tcp_wmem` (對於寫入)
    # 在記憶體大於 128 MB 時,bufferSize 為 16 KB 減去 20 位元組,這 20 位元組用於協議頭
    buffer-size: 16364
    # 是否分配的直接記憶體(NIO直接分配的堆外記憶體),這裡開啟,所以java啟動引數需要配置下直接記憶體大小,減少不必要的GC
    # 在記憶體大於 128 MB 時,預設就是使用直接記憶體的
    directBuffers: true
    threads:
      # 設定IO執行緒數, 它主要執行非阻塞的任務,它們會負責多個連線, 預設設定每個CPU核心一個讀執行緒和一個寫執行緒
      io: 4
      # 阻塞任務執行緒池, 當執行類似servlet請求阻塞IO操作, undertow會從這個執行緒池中取得執行緒
      # 它的值設定取決於系統執行緒執行任務的阻塞係數,預設值是IO執行緒數*8
      worker: 128

其背後的執行緒池,是 jboss 的執行緒池:org.jboss.threads.EnhancedQueueExecutor,spring-boot 目前不能通過配置修改這個執行緒池的佇列大小,預設佇列大小是 Integer.MAX

我們需要監控這個執行緒池的佇列大小,並針對這個指標做一些操作:

  • 當這個任務持續增多的時候,就代表這時候請求處理跟不上請求到來的速率了,需要報警。
  • 當累積到一定數量時,需要將這個例項暫時從註冊中心取下,並擴容。
  • 待這個佇列消費完之後,重新上線。
  • 當超過一定時間還是沒有消費完的話,將這個例項重啟。

新增同步微服務 HTTP 請求等待佇列監控

幸運的是,org.jboss.threads.EnhancedQueueExecutor 本身通過 JMX 暴露了 HTTP servlet 請求的執行緒池的各項指標:

image

我們的專案中,使用兩種監控:

  • prometheus + grafana 微服務指標監控,這個主要用於報警以及快速定位問題根源
  • JFR 監控,這個主要用於詳細定位單例項問題

對於 HTTP 請求等待佇列監控,我們應該通過 prometheus 介面向 grafana 暴露,採集指標並完善響應操作。

暴露 prometheus 介面指標的程式碼是:

@Log4j2
@Configuration(proxyBeanMethods = false)
//需要在引入了 prometheus 並且 actuator 暴露了 prometheus 埠的情況下才載入
@ConditionalOnEnabledMetricsExport("prometheus")
public class UndertowXNIOConfiguration {
    @Autowired
    private ObjectProvider<PrometheusMeterRegistry> meterRegistry;
    //只初始化一次
    private volatile boolean isInitialized = false;

    //需要在 ApplicationContext 重新整理之後進行註冊
    //在載入 ApplicationContext 之前,日誌配置就已經初始化好了
    //但是 prometheus 的相關 Bean 載入比較複雜,並且隨著版本更迭改動比較多,所以就直接偷懶,在整個 ApplicationContext 重新整理之後再註冊
    // ApplicationContext 可能 refresh 多次,例如呼叫 /actuator/refresh,還有就是多 ApplicationContext 的場景
    // 這裡為了簡單,通過一個簡單的 isInitialized 判斷是否是第一次初始化,保證只初始化一次
    @EventListener(ContextRefreshedEvent.class)
    public synchronized void init() {
        if (!isInitialized) {
            Gauge.builder("http_servlet_queue_size", () ->
            {
                try {
                    return (Integer) ManagementFactory.getPlatformMBeanServer()
                            .getAttribute(new ObjectName("org.xnio:type=Xnio,provider=\"nio\",worker=\"XNIO-2\""), "WorkerQueueSize");
                } catch (Exception e) {
                    log.error("get http_servlet_queue_size error", e);
                }
                return -1;
            }).register(meterRegistry.getIfAvailable());
            isInitialized = true;
        }
    }
}

之後,呼叫 /actuator/prometheus 我們就能看到對應的指標:

# HELP http_servlet_queue_size  
# TYPE http_servlet_queue_size gauge
http_servlet_queue_size 0.0

當發生佇列堆積時,我們能快速的報警,並且直觀地從 grafana 監控上發現:

image

對於公有云部署,關注網路限制的監控

現在的公有云,都會針對物理機資源進行虛擬化,對於網路網路卡資源,也是會虛擬化的。以 AWS 為例,其網路資源的虛擬化實現即 ENA(Elastic Network Adapter)。它會對以下幾個指標進行監控並限制:

  • 頻寬:每個虛擬機器例項(AWS 中為每個 EC2 例項),都具有流量出的最大頻寬以及流量入的最大頻寬。這個統計使用一種網路 I/O 積分機制,根據平均頻寬使用率分配網路頻寬,最後的效果是允許短時間內超過額定頻寬,但是不能持續超過。
  • 每秒資料包 (PPS,Packet Per Second) 個數:每個虛擬機器例項(AWS 中為每個 EC2 例項)都限制 PPS 大小
  • 連線數:建立連線的個數是有限的
  • 連結本地服務訪問流量:一般在公有云,每個虛擬機器例項 (AWS 中為每個 EC2 例項)訪問 DNS,後設資料伺服器等,都會限制流量

同時,成熟的公有云,這些指標一般都會對使用者提供展示分析介面,例如 AWS 的 CloudWatch 中,就提供了以下幾個指標的監控:

image

在業務流量突增時,我們通過 JFR 發現訪問 Redis 有效能瓶頸,但是 Redis 本身的監控顯示他並沒有遇到效能瓶頸。這時候就需要檢視是否因為網路流量限制導致其除了問題,在我們出問題的時間段,我們發現 NetworkBandwidthOutAllowanceExceeded 事件顯著提高了很多:

image

對於這種問題,就得需要考慮垂直擴容(提升例項配置)與水平擴容(多例項負載均衡)了,或者減少網路流量(增加壓縮等)

微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer

相關文章