Push or Pull?


采用Pull模型還是Push模型是很多中間件都會面臨的一個問題。消息中間件、配置管理中心等都會需要考慮Client和Server之間的交互采用哪種模型:

  • 服務端主動推送數據給客戶端?

  • 客戶端主動從服務端拉取數據?

本篇文章對比Pull和Push,結合消息中間件的場景進一步探討有沒有其他更合適的模型。

Push VS Pull

1. Push

Push即服務端主動發送數據給客戶端。在服務端收到消息之后立即推送給客戶端。

Push模型最大的好處就是實時性。因為服務端可以做到只要有消息就立即推送,所以消息的消費沒有“額外”的延遲。

但是Push模式在消息中間件的場景中會面臨以下一些問題:

  • 在Broker端需要維護Consumer的狀態,不利於Broker去支持大量的Consumer的場景

  • Consumer的消費速度是不一致的,由Broker進行推送難以處理不同的Consumer的狀況

  • Broker難以處理Consumer無法消費消息的情況(Broker無法確定Consumer的故障是短暫的還是永久的)

  • 大量的推送消息會加重Consumer的負載或者沖垮Consumer

Pull模式可以很好的應對以上的這些場景。

2.Pull

Pull模式由Consumer主動從Broker獲取消息。

這樣帶來了一些好處:

  • Broker不再需要維護Consumer的狀態(每一次pull都包含了其實偏移量等必要的信息)

  • 狀態維護在Consumer,所以Consumer可以很容易的根據自身的負載等狀態來決定從Broker獲取消息的頻率

Pull模式還有一個好處是可以聚合消息。

因為Broker無法預測寫一條消息產生的時間,所以在收到消息之后只能立即推送給Consumer,所以無法對消息聚合后再推送給Consumer。 而Pull模式由Consumer主動來獲取消息,每一次Pull時都盡可能多的獲取已近在Broker上的消息。

但是,和Push模式正好相反,Pull就面臨了實時性的問題。

因為由Consumer主動來Pull消息,所以實時性和Pull的周期相關,這里就產生了“額外”延遲。如果為了降低延遲來提升Pull的執行頻率,可能在沒有消息的時候產生大量的Pull請求(消息中間件是完全解耦的,Broker和Consumer無法預測下一條消息在什么時候產生);如果頻率低了,那延遲自然就大了。

另外,Pull模式狀態維護在Consumer,所以多個Consumer之間需要相互協調,這里就需要引入ZK或者自己實現NameServer之類的服務來完成Consumer之間的協調。

有沒有一種方式,能結合Push和Pull的優勢,同時變各自的缺陷呢?答案是肯定的。

Long-Polling

使用long-polling模式,Consumer主動發起請求到Broker,正常情況下Broker響應消息給Consumer;在沒有消息或者其他一些特殊場景下,可以將請求阻塞在服務端延遲返回。

long-polling不是一種Push模式,而是Pull的一個變種。

那么:

  • 在Broker一直有可讀消息的情況下,long-polling就等價於執行間隔為0的pull模式(每次收到Pull結果就發起下一次Pull請求)。

  • 在Broker沒有可讀消息的情況下,請求阻塞在了Broker,在產生下一條消息或者請求“超時之前”響應請求給Consumer。

以上兩點避免了多余的Pull請求,同時也解決Pull請求的執行頻率導致的“額外”的延遲。

注意上面有一個概念:“超時之前”。每一個請求都有超時時間,Pull請求也是。“超時之前”的含義是在Consumer的“Pull”請求超時之前。

基於long-polling的模型,Broker需要保證在請求超時之前返回一個結果給Consumer,無論這個結果是讀取到了消息或者沒有可讀消息。

因為Consumer和Broker之間的時間是有偏差的,且請求從Consumer發送到Broker也是需要時間的,所以如果一個請求的超時時間是5秒,而這個請求在Broker端阻塞了5秒才返回,那么Consumer在收到Broker響應之前就會判定請求超時。所以Broker需要保證在Consumer判定請求超時之前返回一個結果。

通常的做法時在Broker端可以阻塞請求的時間總是小於long-polling請求的超時時間。比如long-polling請求的超時時間為30秒,那么Broker在收到請求后最遲在25s之后一定會返回一個結果。中間5s的差值來應對Broker和Consumer的始終存在偏差和網絡存在延遲的情況。 (可見Long-Polling模式的前提是Broker和Consumer之間的時間偏差沒有“很大”)

Long-Polling還存在什么問題嗎,還能改進嗎?

Dynamic Push/Pull

“在Broker一直有可讀消息的情況下,long-polling就等價於執行間隔為0的pull模式(每次收到Pull結果就發起下一次Pull請求)。”

這是上面long-polling在服務端一直有可消費消息的處理情況。在這個情況下,一條消息如果在long-polling請求返回時到達服務端,那么它被Consumer消費到的延遲是:

假設Broker和Consumer之間的一次網絡開銷時間為R毫秒,
那么這條消息需要經歷3R才能到達Consumer

第一個R:消息已經到達Broker,但是long-polling請求已經讀完數據准備返回Consumer,從Broker到Consumer消耗了R
第二個R:Consumer收到了Broker的響應,發起下一次long-polling,這個請求到達Broker需要一個R
的時間
第三個R:Broker收到請求讀取了這條數據,那么返回到Consumer需要一個R的時間

所以總共需要3R(不考慮讀取的開銷,只考慮網絡開銷)

另外,在這種情況下Broker和Consumer之間一直在進行請求和響應(long-polling變成了間隔為0的pull)。

考慮這樣一種方式,它有long-polling的優勢,同時能減少在有消息可讀的情況下由Broker主動push消息給Consumer,減少不必要的請求。

消息中間件的Consumer實現

在消息中間件的Consumer中會有一個Buffer來緩存從Broker獲取的消息,而用戶的消費線程從這個Buffer中獲取消費來消息,獲取消息的線程和消費線程通過這個Buffer進行數據傳遞。

  • pull線程從服務端獲取數據,然后寫入到Buffer

  • consume線程從Buffer獲取消息進行消費

有這個Buffer的存在,是否可以在long-polling請求時將Buffer剩余空間告知給Broker,由Broker負責推送數據。此時Broker知道最多可以推送多少條數據,那么就可以控制推送行為,不至於沖垮Consumer。

上面這幅圖是akka的Dynamic Push/Pull示意圖,思路就是每次請求會帶上本地當前可以接收的數據的容量,這樣在一段時間內可以由Server端主動推送消息給請求方,避免過多的請求。

akka的Dynamic Push/Pull模型非常適合應用到Consumer獲取消息的場景。

Broker端對Dynamic Push/Pull的處理流程大致如下:

收到long-polling請求
while(有數據可以消費&請求沒超時&Buffer還有容量) {
    讀取一批消息
    Push到Consumer
    Buffer-PushedAmount 即減少Buffer容量
}

response long-polling請求
結束(等待下一個long-polling再次開始這個流程)

Consumer端對Dynamic Push/Pull的處理流程大致如下:

收到Broker的響應:

if (long-polling的response) {
    將獲取的消息寫入Buffer
    獲取Buffer的剩余容量和其他狀態
    發起新的long-polling請求
} else {
    // Dynamic Push/Pull的推送結果
    將獲取的消息寫入到Buffer(不發起新的請求)
}

舉個例子:

Consumer發起請求時Buffer剩余容量為100,Broker每次最多返回32條消息,那么Consumer的這次long-polling請求Broker將在執行3次push(共push96條消息)之后返回response給Consumer(response包含4條消息)。

如果采用long-polling模型,Consumer每發送一次請求Broker執行一次響應,這個例子需要進行4次long-polling交互(共4個request和4個response,8次網絡操作;Dynamic Push/Pull中是1個request,三次push和一個response,共5次網絡操作)。

總結:

Dynamic Push/Pull的模型利用了Consumer本地Buffer的容量作為一次long-polling最多可以返回的數據量,相對於long-polling模型減少了Consumer發起請求的次數,同時減少了不必要的延遲(連續的Push之間沒有延遲,一批消息到Consumer的延遲就是一個網絡開銷;long-polling最大會是3個網絡開銷)。

Dynamic Push/Pull還有一些需要考慮的問題,比如連續推送的順序性保證,如果丟包了怎么處理之類的問題,有興趣可以自己考慮一下(也可以私下交流)。

結語

本篇內容比較了Push、Poll、Long-Polling、Dynamic Push/Pull模型。

  • Push模型實時性好,但是因為狀態維護等問題,難以應用到消息中間件的實踐中。

  • Pull模式實現起來會相對簡單一些,但是實時性取決於輪訓的頻率,在對實時性要求高的場景不適合使用。

  • Long-Polling結合了Push和Pull各自的優勢,在Pull的基礎上保證了實時性,實現也不會非常復雜,是比較常用的一種實現方案。

  • Dynamic Push/Pull在Long-Polling的基礎上,進一步優化,減少更多不必要的請求。但是先對實現起來會復雜一些,需要處理更多的異常情況。

 

參考內容:Google->Reactive Stream Processing with Akka Streams

 

往期文章:

消息中間件核心實體(1)

消息中間件核心實體(0)

消息的寫入和讀取流程

NameServer模塊划分

Client模塊划分

Broker模塊划分

消息中間件架構討論

業務方對消息中間件的需求

消息中間件中的一些概念

什么是分布式消息中間件?

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM