核心概念
在討論NSQ如何在實踐中使用前,先理解NSQ隊列的架構原理是非常值得的。它的設計很簡單,可以通過幾個核心概念來理解。
Topic ——一個topic就是程序發布消息的一個邏輯鍵,當程序第一次發布消息時就會創建topic。
Channels ——channel組與消費者相關,是消費者之間的負載均衡,channel在某種意義上來說是一個“隊列”。每當一個發布者發送一條消息到一個topic,消息會被復制到所有消費者連接的channel上,消費者通過這個特殊的channel讀取消息,實際上,在消費者第一次訂閱時就會創建channel。
Channel會將消息進行排列,如果沒有消費者讀取消息,消息首先會在內存中排隊,當量太大時就會被保存到磁盤中。
Message s——消息構成了我們數據流的中堅力量,消費者可以選擇結束消息,表明它們正在被正常處理,或者重新將他們排隊待到后面再進行處理。每個消息包含傳遞嘗試的次數,當消息傳遞超過一定的閥值次數時,我們應該放棄這些消息,或者作為額外消息進行處理。
NSQ在操作期間同樣運行着兩個程序:
Nsqd ——nsqd守護進程是NSQ的核心部分,它是一個單獨的監聽某個端口進來的消息的二進制程序。每個nsqd節點都獨立運行,不共享任何狀態。當一個節點啟動時,它向一組nsqlookupd節點進行注冊操作,並將保存在此節點上的topic和channel進行廣播。
客戶端可以發布消息到nsqd守護進程上,或者從nsqd守護進程上讀取消息。通常,消息發布者會向一個單一的local nsqd發布消息,消費者從連接了的一組nsqd節點的topic上遠程讀取消息。如果你不關心動態添加節點功能,你可以直接運行standalone模式。
Nsqlookupd ——nsqlookupd服務器像consul或etcd那樣工作,只是它被設計得沒有協調和強一致性能力。每個nsqlookupd都作為nsqd節點注冊信息的短暫數據存儲區。消費者連接這些節點去檢測需要從哪個nsqd節點上讀取消息。
消息的生命周期
讓我們觀察一個關於nsq如何在實際中工作的更為詳細的例子。
NSQ推薦通過他們相應的nsqd實例使用協同定位發布者,這意味着即使面對網絡分區,消息也會被保存在本地,直到它們被一個消費者讀取。更重要的是,發布者不必去發現其他的nsqd節點,他們總是可以向本地實例發布消息。
首先,一個發布者向它的本地nsqd發送消息,要做到這點,首先要先打開一個連接,然后發送一個包含topic和消息主體的發布命令,在這種情況下,我們將消息發布到事件topic上以分散到我們不同的worker中。
事件topic會復制這些消息並且在每一個連接topic的channel上進行排隊,在我們的案例中,有三個channel,它們其中之一作為檔案channel。消費者會獲取這些消息並且上傳到S3。
每個channel的消息都會進行排隊,直到一個worker把他們消費,如果此隊列超出了內存限制,消息將會被寫入到磁盤中。
Nsqd節點首先會向nsqlookup廣播他們的位置信息,一旦它們注冊成功,worker將會從nsqlookup服務器節點上發現所有包含事件topic的nsqd節點。
然后每個worker向每個nsqd主機進行訂閱操作,用於表明worker已經准備好接受消息了。這里我們不需要一個完整的連通圖,但我們必須要保證每個單獨的nsqd實例擁有足夠的消費者去消費它們的消息,否則channel會被隊列堆着。
從客戶端庫代碼中抽取一部分,這里是一個關於如何處理我們的消息的一段代碼:
如果因為某些原因第三方發生故障了,我們可以處理這些故障,在這個代碼片中,我們有三種處理邏輯:
1、如果超過了某個嘗試次數閥值,我們就將消息丟棄。
2、如果消息已經被處理成功了,我們就結束消息。
3、如果發生了錯誤,我們將需要傳遞的消息重新進行排隊。
正如你所看到的,NSQ隊列的行為既簡單又明確。
在我們的案例中,我們在丟棄消息之前將容忍MAX_DELIVERY_ATTEMPTS * BACKOFF_TIME分鍾的故障。
在Segment系統中,我們統計消息嘗試的次數、消息丟棄數、消息重新排隊數等等,然后結束某些消息以保證我們有一個好的服務質量。如果消息丟棄數超過了我們設置的閥值,我們將在任何時候對服務發出警報。
在實踐中
在生產環境中,我們幾乎在我們所有的實例中運行nsqd守護程序,發布者之間協同定位。NSQ在實際生產中運行良好有幾個原因:
簡單的協議 ——如果你的隊列已經有了一個很好的客戶端庫,這個不是一個很大的問題,但如果你現在的客戶端庫存在bug或者過時了,一個簡單的協議就能體現出優勢了。
NSQ有一個快速的二進制協議,通過短短的幾天工作量就可以很簡單地實現這些協議,我們還自己創建了我們的純JS驅動(當時只存在coffeescript驅動),這個純JS驅動運行的很穩定可靠。
運行簡單 ——NSQ沒有復雜的水印設置或JVM級別的配置,相反,你可以配置保存到內存中的消息的數量和消息最大值,如果隊列被消息填滿了,消息會被保存到磁盤上。
分布式 ——因為NSQ沒有在守護程序之間共享信息,所以它從一開始就是為了分布式操作而生。個別的機器可以隨便宕機隨便啟動而不會影響到系統的其余部分,消息發布者可以在本地發布,即使面對網絡分區。
這種“分布式優先”的設計理念意味着NSQ基本上可以永遠不斷地擴展,需要更高的吞吐量?那就添加更多的nsqd吧。
唯一的共享狀態就是保存在lookup節點上,甚至它們不需要全局視圖,配置某些nsqd注冊到某些lookup節點上這是很簡單的配置,唯一關鍵的地方就是消費者可以通過lookup節點獲取所有完整的節點集。
清晰的故障事件——NSQ在組件內建立了一套明確關於可能導致故障的的故障權衡機制,這對消息傳遞和恢復都有意義。
我是最少意外原則的堅定信仰者,尤其是當它涉及到分布式系統時。系統發生故障,我們接收它,但我們不可能會指望系統以意外的形式發生故障,你最終會忽略這些故障案例,因為你甚至都不打算考慮它們為什么會發生。
雖然它們可能不像Kafka系統那樣提供嚴格的保證級別,但NSQ簡單的操作使故障情況非常明顯。
UNIX-y工具 ——NSQ是一個很好的通用型工具,所以NSQ附帶了很多實用的程序,這些程序是多用途和可組合的。
除了TCP協議,NSQ提供一個簡單的CURL的HTTP接口用於維護操作,它從CLI附帶了二進制文件管道,用tail跟蹤隊列的尾部,從一個隊列使用管道到另外一個隊列,還有HTTP發布訂閱。
甚至還有一個用於監控和暫停隊列的管理面板,包括一個動態的計數器在上面。
丟失了什么?
正如我所提到的,簡單並不是沒有折衷:
沒有復制 ——不像其他的隊列組件,NSQ並沒有提供任何形式的復制和集群,也正是這點讓它能夠如此簡單地運行,但它確實對於一些高保證性高可靠性的消息發布沒有足夠的保證。
我們可以通過降低文件同步的時間來部分避免,只需通過一個標志配置,通過EBS支持我們的隊列。但是這樣仍然存在一個消息被發布后馬上死亡,丟失了有效的寫入的情況。
基本消息路由 ——在NSQ中,topic和channel幾乎是你所有能獲得到的東西,沒有關於路由和基於key的親和力的觀念。我們很樂意為各種用例提供支持,無論是根據條件去篩選消息,還是根據條件路由到某些節點上。取而代之的是,我們最終建立了路由worker,它們處於隊列之間,扮演一個聰明的直通濾波器。
沒有嚴格的順序 ——雖然Kafka由一個有序的日志構成,但NSQ不是。消息可以在任何時間以任何順序進入隊列。在我們使用的案例中,這通常沒有關系,因為所有的數據都被加上了時間戳,但它並不適合需要嚴格順序的情況。
無數據重復刪除功能 ——Aphyr已經在他的文章中廣泛探討了基於超時系統的危險性。NSQ同樣也調入了這個陷阱,它使用了心跳檢測機制去測試消費者是否存活還是死亡。我們之前已經寫過關於很多原因會導致我們的worker無法完成心跳檢測,所以在worker中必須有一個單獨的步驟確保冪等性。
簡單的工作原理
正如你所看到的,后面看到的所有好處的基本核心就是簡單性,NSQ是一個簡單的隊列,這意味着它很容易進行故障推理和很容易發現bug。消費者可以自行處理故障事件而不會影響系統剩下的其余部分。
事實上,簡單性是我們決定使用NSQ的首要因素,這方便與我們的許多其他軟件一起維護,通過引入隊列使我們得到了堪稱完美的表現,通過隊列甚至讓我們增加了幾個數量級的吞吐量。
今天,我們面臨一個更加復雜的未來,我們越來越多的worker需要一套嚴格可靠性和順序性保障,這已經超過了NSQ提供的簡單功能。
我們計划在其他基礎設施中用Kafka替換NSQ,在生產上從JVM中運行可以獲取更多的好處。關於Kafka我們有一個明確的權衡,我們自己必須肩負起更多負責的運營。另一方面,它擁有一個可復制的、有序的日志可以提供給我們更好的服務。
但對於其他適合NSQ的worker,它為我們服務的相當好,我們期待着繼續鞏固它的堅實的基礎。