1. 程式人生 > 其它 >一些關於佇列的理論:吞吐量、延遲和頻寬

一些關於佇列的理論:吞吐量、延遲和頻寬

本文大體上是對https://blog.rabbitmq.com/posts/2012/05/some-queuing-theory-throughput-latency-and-bandwidth的翻譯。

假設在 RabbitMQ 中有一個佇列,你有一些客戶端會消費這個佇列。 如果沒有配置QoS(Quality of Service),那麼 RabbitMQ服務會以網路和客戶端允許的速度將佇列的所有訊息推送到你的客戶端, 客戶端將所有訊息儲存在記憶體中,記憶體壓力會急劇增加。此時RabbitMQ端可能會顯示佇列的訊息是空的,因為此時有數百萬條訊息存在於客戶端記憶體中等待客戶端處理,此時,如果你增加一個新的消費者,佇列並沒有新的訊息傳送給這個消費者,因為所有訊息目前都存在於原來的消費者端,並且可能會存在很久,即便新增加的消費者可以更快的處理這一類訊息。這種情況是相當不理想的。

因此,預設的 QoS設定為客戶端提供了無限的緩衝區,這可能會導致不妥的行為和不佳的效能。 但是應該將 QoS 設定為多少? 這個值應該讓消費者保持飽和,但同時要最小化客戶端的緩衝區大小,以便更多訊息保留在 RabbitMQ 的佇列中,從而可供新消費者使用或在其他消費者空閒時傳送給消費者。

假設 RabbitMQ 傳送一條訊息到達消費者端一共需要50ms, 客戶端收到訊息之後處理需要花費4ms。 一旦消費者處理了訊息,它就會向 RabbitMQ 傳送一個 ack,這又需要 50 毫秒。 所以我們的總往返時間為 104 毫秒。 如果我們將 QoS 設定為 1 ,那就意味著RabbitMQ不會發送下一條訊息直到這次流程結束。因此,客戶端每 104 毫秒中只有 4 毫秒處於忙碌狀態,即 3.8% 的時間。 我們希望它 100% 的時間都處於忙碌狀態。

如果我們做一個除法:總往返時間 / 處理時間,即104 / 4 = 26。假設,我們將QoS設定為26:假設客戶端有 26 條訊息緩衝,準備好並且等待處理。 (這是一個明智的假設:一旦您設定了 basic.qos 然後從佇列中消費,RabbitMQ將從你訂閱的佇列中向客戶端傳送儘可能多的訊息,直至達到 QoS 限制。如果假設訊息不是很大而且頻寬很高,很可能 Rabbit 將能夠以比您的客戶端處理它們更快的速度將訊息傳送到您的消費客戶端。因此,從完整的假設中進行所有數學計算是合理的(並且更簡單))。如果每條訊息需要 4 毫秒的處理時間,那麼處理整個緩衝區總共需要 26 * 4 = 104 毫秒。前4ms是客戶端對第一條訊息的處理,然後客戶端發出一個 ack 並繼續處理來自緩衝區的下一條訊息,該確認需要 50 毫秒才能到達RabbitMQ Broker。Broker然後向客戶端發出一條新訊息,這需要 50 毫秒,因此當 104 毫秒過去並且客戶端完成處理其緩衝區時,來自代理的下一條訊息已經到達並準備好等待客戶端來處理。因此客戶端一直處在飽和狀態:有更大的 QoS 不會讓它更快;但是我們最小化了緩衝區大小,從而最小化了客戶端中訊息的延遲:客戶端緩衝訊息的時間不會超過它們所需的時間,以保持客戶端的工作飽和。事實上,客戶端能夠在下一條訊息到達之前完全排空緩衝區,因此緩衝區實際上是空的。

如果處理時間和網路行為保持不變,這個解決方案絕對沒問題。 但是考慮一下如果網路速度突然減半會發生什麼:您的預取緩衝區(就是QoS)不再足夠大,現在客戶端將處於空閒狀態,等待新訊息到達,因為客戶端能夠以比 RabbitMQ 提供新訊息的速度更快地處理訊息。

為了解決這個問題,我們可能需要將 QoS 增加一倍(或接近一倍)。如果我們將它從 26 增加到 51,客戶端處理每條訊息的時間依舊是 4 毫秒,那麼我們現在緩衝區中有 51 * 4 = 204 毫秒的訊息,其中 4 毫秒將用於處理一條訊息,剩下的 200 毫秒用於傳送一條ACK到 RabbitMQ 並接收下一條訊息。因此,我們現在可以應對網路速度減半的情況。

但是,如果網路執行突然恢復正常,QoS 加倍意味著每條訊息將在客戶端緩衝區中停留一段時間,而不是在到達客戶端後立即處理。同樣的,假設此時客戶端已經有了51條訊息,我們知道新訊息將在客戶端處理完第一條訊息後 100 毫秒(50 * 2)出現在客戶端。但是在這 100 毫秒內,客戶端將處理 50 條可用訊息中的 100 / 4 = 25 條。這意味著當一條新訊息到達客戶端時,當客戶端從緩衝區的頭部刪除時,它將被新增到緩衝區的末尾。因此,緩衝區將始終保持 50 - 25 = 25 條訊息的長度,因此每條訊息將在緩衝區中停留 25 * 4 = 100 毫秒,從而增加了 RabbitMQ 將其傳送到客戶端和客戶端開始處理它之間的延遲,從 50 毫秒到 150 毫秒。

因此,我們看到通過增加QoS可以讓客戶端可以在保持飽和的同時應對突然惡化的網路,但是當網路恢復正常執行時,訊息消費的延遲會大大增加。

同樣地,如果網路一直保持穩定,客戶端現在處理一條訊息突然需要花費40毫秒而不是4毫秒了,會發生什麼?如果 RabbitMQ 中的佇列之前是穩定長度(即入口和出口速率相同),現在它將開始快速增長,因為出口速率已降至原來的十分之一。你可能決定通過新增更多消費者來嘗試解決這個不斷增長的積壓,但現在有一些訊息被現有客戶端緩衝。假設原始緩衝區大小為 26 條訊息,客戶端將花費 40 毫秒處理第一條訊息,然後將 ack 傳送回 RabbitMQ 並移動到下一條訊息,ACK 仍然需要 50 毫秒才能到達 RabbitMQ,再需要 50 毫秒讓 RabbitMQ傳送一條新訊息,但是在這 100 毫秒內,客戶端只處理了 100 / 40 = 2.5 條後續的訊息,而不是剩餘的 所有25 條訊息。因此,此時緩衝區的長度為 25 - 3 = 22 條訊息。此時來自於 RabbitMQ 的新訊息並不會被立即處理,因為在他之前還有22條訊息待處理,也就是在 22 * 40 = 880 毫秒內新訊息不會被客戶端處理。鑑於從 RabbitMQ到客戶端的網路延遲只有 50 毫秒,這個額外的 880 毫秒延遲佔了目前總延遲的 95% :(880 / (880 + 50) = 0.946)。

更糟糕的是,如果此時QoS是51(為了應對網路惡化),處理完第一條訊息後,將在客戶端緩衝 50 條其他訊息。 100 毫秒後(假設此時網路執行恢復正常了),一條新訊息從 RabbitMQ 發到客戶端,客戶端此時正在處理 50 條訊息中的第 3 條(緩衝區現在有 47 條訊息),因此新訊息將位於緩衝區中的第 48 位,並且在接下來的 47 * 40 = 1880 毫秒內不會被處理。同樣,考慮到將訊息傳送到客戶端的網路延遲僅為 50 毫秒,這進一步的 1880 毫秒佔了目前總延遲的97% (1880 / (1880 + 50) = 0.974)。這是不可接受的:資料可能只有在及時處理時才有效和有用,而不是在客戶端收到後大約 2 秒再處理,如果其他消費客戶端空閒,則他們無能為力:一旦 RabbitMQ 向客戶端傳送了訊息,這個訊息就是這個客戶端的責任,直到它確認或拒絕該訊息。一旦訊息傳送到客戶端,客戶端就不能相互竊取訊息。您想要的是讓客戶端保持忙碌,但讓客戶端緩衝儘可能少的訊息,以便訊息不會被客戶端緩衝區延遲,因此新的消費客戶端可以從 RabbitMQ的佇列中快速獲取訊息。

後文不寫了,作者介紹了一種演算法:https://queue.acm.org/detail.cfm?id=2209336 應對這種情況,並且實現了一個DEMO:https://gist.github.com/msackman/2658712,使用demo:https://gist.github.com/msackman/2658727。

SpringAMQP顯然是沒有支援這些,並且預設設定了250的Qos,但是同樣的,這需要結合自己的業務場景,網路延遲給出一個最佳值,按照本文的計算方式則是RT / 處理時間,但是對於SpringAMQP是這樣嗎? SpringAMQP如果是主動ACK,那麼這個ack操作顯然是同步執行的,也就是後50ms並不能納入計算,某種程度上他也是屬於訊息處理階段的耗時。

但是我認為這篇文章所要表達的東西是比較有價值的,會讓你更加全面的思考消費者和訊息代理之間的關係。