1. 程式人生 > >《譯文:RabbitMQ關於吞吐量,延遲和頻寬的一些理論》

《譯文:RabbitMQ關於吞吐量,延遲和頻寬的一些理論》

原文連結 譯者:flystarfly

該文閱讀自RabbitMQ官方網站,分享好文給大家

你在Rabbit有一個佇列,然後一些消費者從這個佇列中消費。如果你根本沒有設定QoS(basic.qos),那麼Rabbit會把所有的佇列訊息都按照網路和客戶端允許的速度推送給客戶端。消費者將會飛速增加它們的記憶體佔用,因為它們將所有訊息都快取在自己的RAM中。如果您詢問Rabbit,佇列可能會顯示為空,但會有大量在客戶端中,正準備由客戶端應用程式處理的訊息未被確認。如果您新增新的消費者,則佇列中不會有訊息傳送給新的消費者。即使有其他消費者可用於更快地處理這樣的訊息,它們也只是在現有的客戶端快取,並且可能在那裡很長一段時間。這是相當次優的。

因此,預設的QoS預取設定為客戶提供了無限的緩衝區,這可能導致不良的行為和效能。但是,怎樣的QoS預取緩衝區大小才是您應該設定的?設定的目的是讓消費者保持工作飽和狀態,同時儘量減少客戶端的緩衝區大小,以便更多的訊息留在Rabbit的佇列中,來可供新消費者使用,或在消費者空閒時傳送給消費者。

比方說Rabbit從這個佇列中拿出一條訊息需要50ms,把它放到網路上,然後到達消費者。客戶端處理訊息需要4ms。一旦消費者處理了訊息,它就會發送一個ACK給Rabbit,這個Rabbit需要進一步傳送50ms的資訊給Rabbit進行處理。所以我們總共有104ms的往返時間。如果我們有1個訊息的QoS預取設定,那麼在這個往返行程完成之後,Rabbit不會發送下一個訊息。因此,客戶端每104ms只有4ms,或3.8%的時間忙碌,而我們希望百分之百的時間都在忙碌中。

Consuming from a Queue

如果我們在每個訊息的客戶端上執行總的往返時間/處理時間,則得到104/4 = 26。如果我們具有26個訊息的QoS預取,就解決了我們的問題:假設客戶端具有26個訊息緩衝,等待處理。 (這是一個明智的假設:一旦你設定了basic.qos,然後從一個佇列中消耗,Rabbit將傳送儘可能多的訊息到你訂閱到客戶端的佇列,直到QoS限制。訊息不是很大,頻寬也很高,所以Rabbit很可能比你的客戶端更快地傳送訊息到你的客戶端,所以從假設的完整性來做所有的數學是合理的(也是更簡單的)客戶端緩衝區)。如果每條訊息需要4ms的處理來處理,那麼總共需要26×4 = 104ms來處理整個緩衝區。第一個4ms是第一個訊息的客戶端處理。客戶端然後發出一個確認,然後繼續處理緩衝區中的下一條訊息。這一點需要50ms才能到達代理。代理向客戶端發出一條新訊息,這需要50ms的時間,所以到了104ms時間,客戶端已經完成緩衝區的處理,代理的下一條訊息已經到達,並準備好等待客戶端來處理它。因此,客戶端始終處於忙碌狀態:具有較大的QoS預取不會使其更快;但是我們最大限度地減少了緩衝區的大小,從而減少了客戶端訊息的延遲:訊息被客戶端緩衝了,不再需要為了保持客戶端的工作。事實上,客戶端能夠在下一條訊息到達之前完全排空緩衝區,因此緩衝區實際上保持為空。

這個解決方案絕對沒問題,只要處理時間和網路行為保持不變。但考慮一下如果網路突然間速度減半會發生什麼情況:預取緩衝區不夠大,現在客戶端會閒置,等待新訊息到達,因為客戶端能夠處理訊息的速度比Rabbit能夠提供新訊息。

為了解決這個問題,我們僅僅需要翻倍(或幾乎是雙倍)QoS預取大小。如果我們把這個大小從26升到51,而客戶仍然在每4ms處理訊息,那麼我們現在有51 * 4 = 204ms訊息緩衝區中,其中4ms將用於處理訊息,剩餘200ms為傳送一個ACK回Rabbit和接收下一個訊息。因此,我們現在可以應對網路速度的減半。

但是,如果網路正常執行,現在將QoS預取提高一倍,意味著每個訊息都會駐留在客戶端緩衝區中一段時間​​,而不是在到達客戶端時立即處理。再次,從現在51條訊息的完整緩衝區開始,我們知道新訊息將在客戶端完成處理第一條訊息100ms後開始出現在客戶端。但是在這100ms內,客戶端將會處理50個可用的100/4 = 25個訊息。這意味著當新的訊息到達客戶端時,它將被新增到緩衝區的末尾,當客戶端從緩衝區的頭部移除時。因此,緩衝區總是保持50 – 25 = 25個訊息長度,因此每個訊息將在緩衝區中保持25 * 4 = 100ms,Rabbit傳送給客戶端以及客戶端開始處理它的時間從50ms增加到150ms 。

因此,我們看到,增加預取緩衝區,使客戶端可以應對惡化的網路效能,但是同時也會使得客戶端繁忙,大大增加了網路正常執行時的延遲。

同樣,排除掉網路的效能惡化,如果客戶端開始處理每個訊息40毫秒,而不是4ms,會發生什麼?如果Rabbit的佇列以前是穩定的(即入口和出口速率相同),它現在將開始快速增長,因為出口率降到了原來的十分之一。您可能會決定嘗試通過新增更多的消費者來處理這種增長的積壓,但現在有訊息正在被現有客戶端緩衝。假設26條訊息的原始緩衝區大小,客戶端將花費40ms處理第一條訊息,然後將確認訊息傳送回Rabbit並移至下一條訊息。 ack仍然需要50ms才能到達Rabbit,而Rabbit發出一個新的訊息還需要50ms,但是在100ms內,客戶端只處理了100/40 = 2.5個訊息,而不是其餘的25個訊息。因此緩衝區在這個點上是25 – 3 = 22個訊息長。現在來自Rabbit的新訊息,不是立即處理,而是會位於第23位,落後於其他22個等待處理的訊息,直到22 * 40 = 880ms後才會被客戶端觸及。考慮到從Rabbit到客戶端的網路延遲僅為50ms,現在這個額外增加的880ms延遲相當於多增加了延遲的95%(880 /(880 + 50)= 0.946)。

更糟糕的是,如果我們將緩衝區大小加倍到51條訊息以應對網路效能下降,會發生什麼?第一條訊息處理完畢後,會在客戶端快取50條訊息。 100ms後(假設網路執行正常),一條新的訊息將從Rabbit到達,客戶端將處理這50條訊息中的第三條訊息(緩衝區現在為47條訊息長)的一半,因此新訊息將在緩衝區中是第48位,並且不會再觸及直到47 * 40 = 1880ms之後。再一次,考慮到向客戶端傳送訊息的網路延遲僅為50ms,現在這個1880ms的延遲意味著客戶端緩衝佔據了超過97%的延遲(1880 /(1880 + 50)= 0.974)。這可能是不可接受的:如果資料處理得很快,而不是在客戶端收到資料後2秒,資料才可能是有效的和有用的!如果其他消費客戶端空閒,他們無能為力:一旦Rabbit向客戶端傳送訊息,訊息就是客戶端的責任,直到他們拒絕或拒絕訊息為止。一旦訊息被髮送到客戶端,客戶端不能竊取彼此的訊息。你想要的是讓客戶端保持忙碌,但是客戶端儘可能少地快取訊息,這樣訊息就不會被客戶端緩衝區延遲,因此新消費的客戶端可以快速地接收到來自Rabbit佇列的訊息。

因此,如果網路變慢,緩衝區太小會導致客戶端空閒,但如果網路正常執行,緩衝區太大會導致大量額外的延遲;如果客戶端突然開始花費更長時間來處理每個緩衝區,訊息比正常。很明顯,你真正想要的是一個不同的緩衝區大小。這些問題在網路裝置中是常見的,並且一直是很多研究的主題。主動佇列管理演算法試圖嘗試丟棄或拒絕訊息,以避免訊息長時間坐在緩衝區中。當緩衝器保持空閒(每個訊息只遭受網路延遲,並且根本不在緩衝器中)並且緩衝器在那裡吸收尖峰時,達到最低延遲。 Jim Gettys一直從網路路由器的角度來研究這個問題:區域網和廣域網效能之間的差異正在遭受同樣的問題。實際上,無論何時,在生產者(在本例中為Rabbit)和消費者(客戶端應用程式邏輯)之間都有一個緩衝區,雙方的效能可以動態變化,您將會遇到這樣的問題。最近出現了一種名為Controlled Delay的新演算法,它在解決這些問題上表現得很好。

作者聲稱他們的CoDel(“coddle”)演算法是一個“無旋鈕”演算法。這實際上是一個謊言:這裡有兩個旋鈕,他們都需要適當的設定。但是每次效能改變時都不需要改變它們,這是一個巨大的好處。我已經為我們的AMQP Java客戶端實現了這個演算法,作為QueueingConsumer的一個變種。雖然原來的演算法是針對TCP層的,那麼丟棄資料包是有效的(TCP本身會處理丟失資料包的重傳),但在AMQP中這不太有禮拜!因此,我的實現使用Rabbit的basic.nack擴充套件來顯式地將訊息返回給佇列,以便其他人可以處理它們。

使用它幾乎和普通的QueueingConsumer一樣,除了你應該提供三個額外的引數給建構函式來獲得最好的效能。

  1. 首先是requeue,它設定當訊息被阻塞,是否應該重新排序或丟棄。如果設定為FALSE,那麼它們將被丟棄,這樣可能會觸發死信交換機制。
  2. 第二個是targetDelay,這是訊息在客戶端QoS預取緩衝區中等待的可接受時間(以毫秒為單位)。
  3. 第三個是interval,是以毫秒為單位的一個訊息的預期最壞情況處理時間。這不一定是精確的,但在一個數量級內肯定有幫助。

您仍然應該適當地設定QoS預取大小。如果不這樣做,可能是客戶端會收到很多訊息,然後如果他們在緩衝區中的時間太長,演算法將不得不將它們返回給Rabbit。訊息返回給Rabbit時,很容易產生大量額外的網路流量。一旦效能偏離規範,CoDel演算法就意味著只會開始丟棄(或拒絕)訊息,因此一個可行的例子可能會有所幫助。

同樣,假設每個方向的網路遍歷時間為50ms,並且我們期望客戶端平均花費4ms的時間處理每條訊息,但是這可以達到20ms。因此我們把CoDel的interval引數設定為20。有時網路速度減半,所以每個方向的遍歷時間可以是100ms。為此,我們將basic.qos預取設定為204/4 = 51.是的,這意味著在網路正常執行的大部分時間內,緩衝區將保持25個訊息(見前面的工作),但是我們認為這可以接受。我們預期每個訊息將在緩衝區中駐留25 * 4 = 100ms,因此將CoDel的targetDelay設定為100。

正常執行時,CoDel不會礙事,很少有訊息會被nacked。但是,如果客戶端開始處理訊息的速度比正常情況慢,CoDel會發現訊息已經被客戶端快取了太久,那就將這些訊息返回給佇列。如果這些訊息被重新發送,則它們將可用於傳送給其他客戶端。

這在目前是非常具有實驗性的,也有可能看到CoDel不適合處理純IP的AMQP訊息的原因。另外值得記住的是,通過nacks重新發送訊息是一個相當昂貴的操作,所以最好設定CoDel的引數來確保極少數的訊息會在正常操作中被nacked。後臺管理外掛會是一個來檢查有多少訊息被nacked的簡單方法。一如以往,評論,反饋和改進是最受歡迎的!