NSQ 介紹

組成golang

  • nsqlookupd
  • nsqd
  • nsqadmin

NSQ 是一個實時分佈式消息傳遞平臺。sql

功能

  • 支持沒有 SPOF 的分佈式拓撲
  • 可水平擴展(無代理,無縫地向集羣添加更多節點)
  • 基於低延遲推送的消息傳遞(性能)
  • 組合負載均衡和多播樣式消息路由
  • 擅長流媒體(高吞吐量)和麪向工做(低吞吐量)工做負載
  • 主要在內存中(超出高水位標記的消息透明地保存在磁盤上)
  • 消費者查找生產者的運行時發現服務(nsqlookupd)
  • 傳輸層安全性(TLS)
  • 數據格式不可知
  • 幾個依賴項(易於部署)和一個理智,有界,默認配置
  • 簡單的 TCP 協議,支持任何語言的客戶端庫
  • 統計信息,管理操做和生成器的 HTTP 接口(無需發佈客戶端庫)
  • 與 statsd 集成用於實時儀器
  • 強大的集羣管理界面(nsqadmin)

保障

保證與任何分佈式系統同樣,實現目標是進行智能權衡。 經過對這些權衡的實際狀況保持透明,咱們但願對 NSQ 在生產中部署時的行爲有所期待。編程

  • 消息不耐用(默認) 雖然系統支持「釋放閥」(--mem-queue-size),而後消息將透明地保存在磁盤上,但它主要是內存中的消息傳遞平臺。 --mem-queue-size 能夠設置爲 0,以確保全部傳入的消息都持久保存到磁盤。 在這種狀況下,若是節點發生故障,您可能會減小故障表面(即 OS 或底層 IO 子系統發生故障)。 沒有內置複製。 可是,管理這種權衡的方式有多種,例如部署拓撲和技術,它們以容錯的方式主動地將主題從主機並持久化到磁盤。
  • 消息至少傳遞一次 因爲各類緣由,能夠屢次傳遞消息(客戶端超時,斷開鏈接,從新排隊等)。 客戶有責任執行冪等操做或重複數據刪除。
  • 消息未排序 不能依賴傳遞給消費者的消息順序。 與消息傳遞語義相似,這是從新隊列的結果,內存和磁盤存儲的組合,以及每一個 nsqd 節點不共享的事實。 經過在消費者中引入一個延遲窗口來接受消息並在處理以前對它們進行排序(可是,爲了給定義的消費者它的消息是有序的而不是在整個集羣中排序)是相對簡單的。 保留這個不變量必須丟棄落在該窗口以外的消息)。
  • 消費者最終找到全部主題生產者 發現服務(nsqlookupd)旨在最終保持一致。 nsqlookupd 節點不協調以維護狀態或回答查詢。 在分區的兩端仍然能夠回答查詢的意義上,網絡分區不會影響可用性。 部署拓撲具備減輕這些類型問題的最重要影響。

設計

NSQ 是 simplequeue(simplehttp 的一部分)的繼承者,所以設計爲(沒有特定的順序):後端

  • 支持拓撲,實現高可用性並消除 SPOF
  • 解決了對更強大的消息傳遞保證的需求
  • 限制單個進程的內存佔用(經過將某些消息保存到磁盤)
  • 大大簡化了生產者和消費者的配置要求
  • 提供簡單的升級途徑
  • 提升效率

簡化配置和管理

單個 nsqd 實例旨在一次處理多個數據流。 流稱爲「主題」,主題具備 1 個或多個「通道」。 每一個頻道都會收到主題全部消息的副本。 實際上,通道映射到消耗主題的下游服務。數組

主題和渠道沒有先驗配置。 經過發佈到命名主題或經過訂閱命名主題上的通道來首次使用時建立主題。 首次使用時,經過訂閱指定的頻道建立頻道。緩存

主題和通道都相互獨立地緩衝數據,防止緩慢的消費者致使其餘渠道的積壓(一樣適用於主題級別)。安全

通道能夠而且一般也會鏈接多個客戶端。 假設全部鏈接的客戶端都處於準備接收消息的狀態,則每條消息將被傳遞到隨機客戶端。 例如:服務器

img

總而言之,消息是從主題 - >通道(每一個通道接收該主題的全部消息的副本)多播的,可是從通道 - >消費者均勻分佈(每一個消費者接收該通道的一部分消息)。markdown

NSQ 還包括一個輔助應用程序 nsqlookupd,它提供了一個目錄服務,消費者能夠在其中查找提供他們有興趣訂閱的主題的 nsqd 實例的地址。 在配置方面,這將消費者與生產者分離(他們都只須要知道在哪裏聯繫 nsqlookupd 的常見實例,從不相互聯繫),從而下降了複雜性和維護。網絡

在較低級別,每一個 nsqd 具備與 nsqlookupd 的長期 TCP 鏈接,在該鏈接上它按期推送其狀態。 此數據用於通知 nsqlookupd 將爲消費者提供哪些 nsqd 地址。 對於消費者,將公開 HTTP /lookup 端點以進行輪詢。

要引入主題的新的不一樣使用者,只需啓動配置了 nsqlookupd 實例的地址的 NSQ 客戶端。 添加新的使用者或新發布者不須要進行任何配置更改,從而大大下降了開銷和複雜性。

注意:在未來的版本中,啓發式 nsqlookupd 用於返回地址能夠基於深度,鏈接的客戶端數量或其餘「智能」策略。 目前的實施就是所有。 最終,目標是確保全部生產者都被閱讀,使深度保持接近零。

值得注意的是,nsqd 和 nsqlookupd 守護進程旨在獨立運行,不須要兄弟之間的通訊或協調。

咱們還認爲,有一種方法能夠集中查看,內省和管理集羣。 咱們創建了 nsqadmin 來作到這一點。 它提供了一個 Web UI 來瀏覽主題/渠道/消費者的層次結構,並檢查每一個層的深度和其餘關鍵統計數據。 此外,它還支持一些管理命令,例如刪除和清空通道(當通道中的消息能夠安全地丟棄以便將深度恢復爲 0 時,這是一個有用的工具)。

簡單的升級路徑

這是咱們的最高優先事項之一。 咱們的生產系統處理大量流量,全部流量都創建在咱們現有的消息傳遞工具之上,所以咱們須要一種方法來緩慢而有條不紊地升級基礎架構的特定部分,幾乎沒有影響。

首先,在消息生產者方面,咱們構建了 nsqd 來匹配 simplequeue。 具體來講,nsqd 將 HTTP /put 端點(就像 simplequeue 同樣)暴露給 POST 二進制數據(有一點須要注意,端點須要另一個指定「主題」的查詢參數)。 想要切換到開始發佈到 nsqd 的服務只須要進行少許的代碼更改。

其次,咱們在 Python 和 Go 中構建了庫,這些庫與咱們現有庫中習慣使用的功能和習慣相匹配。 經過將代碼更改限制爲引導,這簡化了消息使用者方面的轉換。 全部業務邏輯都保持不變。

最後,咱們構建了實用程序來將新舊組件粘合在一塊兒。 這些均可以在存儲庫的 examples 目錄中找到:

  • nsq_pubsub - 將相似 HTTP 接口的 pubsub 暴露給 NSQ 集羣中的主題
  • nsq_to_file - 將給定主題的全部消息永久寫入文件
  • nsq_to_http - 對主題中的全部消息執行 HTTP 請求到(多個)端點

消除 SPOF

NSQ 旨在以分佈式方式使用。 nsqd 客戶端(經過 TCP)鏈接到提供指定主題的全部實例。 沒有中間人,沒有消息經紀人,也沒有 SPOF:

tumblr_mat85kr5td1qj3yp2.png

此拓撲消除了連接單個聚合訂閱源的須要。 而是直接從全部生產者消費。 從技術上講,哪一個客戶端鏈接到哪一個 NSQ 並不重要,只要有足夠的客戶端鏈接到全部生產者以知足消息量,就能夠保證全部客戶端最終都會被處理。

對於 nsqlookupd,經過運行多個實例來實現高可用性。 它們不直接相互通訊,數據被認爲最終是一致的。 消費者輪詢全部已配置的 nsqlookupd 實例並將響應聯合起來。 陳舊,不可訪問或其餘故障節點不會使系統中止運行。

消息傳遞保證

NSQ 保證消息將至少傳送一次,儘管可能存在重複消息。 消費者應該期待這一點並重複數據刪除或執行冪等操做。

此保證做爲協議的一部分強制執行,其工做方式以下(假設客戶端已成功鏈接並訂閱了某個主題):

  1. 客戶端表示他們已準備好接收消息
  2. NSQ 發送消息並在本地臨時存儲數據(在從新排隊或超時的狀況下)
  3. 客戶端分別回覆指示成功或失敗的 FIN(完成)或 REQ(從新排隊)。 若是客戶端沒有回覆 NSQ 將在可配置的持續時間後超時並自動從新排隊消息)

這可確保致使消息丟失的惟一邊緣狀況是 nsqd 進程的不正常關閉。 在這種狀況下,內存中的任何消息(或任何未刷新到磁盤的緩衝寫入)都將丟失。

若是防止消息丟失是最重要的,那麼即便是這種邊緣狀況也能夠減輕。 一種解決方案是站起來接收相同部分消息副本的冗餘 nsqd 對(在不一樣的主機上)。 由於您已經將您的消費者編寫爲冪等的,因此對這些消息進行雙倍時間沒有下游影響,而且容許系統在不丟失消息的狀況下忍受任何單個節點故障。

須要注意的是,NSQ 提供了構建模塊,以支持各類生產用例和可配置的耐用性。

有限的內存佔用

nsqd 提供了一個配置選項--mem-queue-size,它將肯定給定隊列在內存中保留的消息數。 若是隊列的深度超過此閾值,則消息將透明地寫入磁盤。 這將給定 nsqd 進程的內存佔用限制爲 mem-queue-size * #_of_channels_and_topics

tumblr_mavte17V3t1qj3yp2.png

此外,精明的觀察者可能已經肯定這是經過將此值設置爲低(例如 1 或甚至 0)來得到更高的交付保證的便捷方式。 磁盤支持的隊列旨在經受不乾淨的重啓(儘管消息可能會被傳遞兩次)。

此外,與消息傳遞保證相關,乾淨關閉(經過向 nsqd 進程發送 TERM 信號)能夠安全地保留當前在內存中,傳輸中,延遲和各類內部緩衝區中的消息。

注意,名稱以字符串``#ephemeral`結尾的主題/頻道不會緩存到磁盤,而是在傳遞 mem-queue-size 後丟棄消息。 這使得不須要消息保證的消費者可以訂閱頻道。 在最後一個客戶端斷開鏈接後,這些短暫的通道也將消失。 對於短暫的主題,這意味着已經建立,使用和刪除了至少一個頻道(一般是短暫的頻道)。

效率

NSQ 旨在經過「memcached-like」命令協議與簡單的大小前綴響應進行通訊。全部消息數據都保存在覈心中,包括嘗試次數,時間戳等元數據。這消除了從服務器到客戶端來回複製數據,這是從新排隊消息時先前工具鏈的固有屬性。這也簡化了客戶端,由於他們再也不須要負責維護消息狀態。

此外,經過下降配置複雜性,設置和開發時間大大減小(特別是在主題有> 1 個消費者的狀況下)。

對於數據協議,咱們作出了一項關鍵設計決策,經過將數據推送到客戶端而不是等待它來提升性能和吞吐量。這個概念,咱們稱之爲 RDY 狀態,實質上是客戶端流控制的一種形式。

當客戶端鏈接到 nsqd 並訂閱某個通道時,它將處於 RDY 狀態 0.這意味着不會向客戶端發送任何消息。當客戶端準備好接收消息時,它會發送一個命令,將其 RDY 狀態更新爲準備處理的某些#,例如 100.沒有任何其餘命令,100 條消息將被推送到客戶端,由於它們可用(每次遞減)該客戶端的服務器端 RDY 計數)。

客戶端庫旨在發送命令以在達到可配置的max-in-flight設置的~25%時更新 RDY 計數(並正確考慮到多個 nsqd 實例的鏈接,進行適當劃分)

tumblr_mataigNDn61qj3yp2.png

這是一個重要的性能旋鈕,由於一些下游系統可以更容易地批量處理消息並從更高的max-in-flight中獲益。

值得注意的是,由於它具備緩衝和推送功能,可以知足流(通道)的獨立副本的須要,因此咱們已經生成了一個相似於 simplequeue 和 pubsub 組合的守護進程。 這在簡化咱們系統的拓撲方面很是強大,咱們傳統上維護上面討論的舊工具鏈。

Go

咱們早期作出了戰略決策,在 Go 中構建 NSQ 核心。 咱們最近在博客上寫了關於咱們對 Go 的使用,並提到了這個項目 - 瀏覽該帖子以瞭解咱們對語言的見解可能會有所幫助。

關於 NSQ,Go 通道(不要與 NSQ 通道混淆)和語言內置的併發功能很是適合 nsqd 的內部工做。 咱們利用緩衝通道來管理內存消息隊列,並沒有縫地將溢出寫入磁盤。

標準庫能夠輕鬆編寫網絡層和客戶端代碼。 內置的內存和 cpu 分析鉤子突出了優化的機會,而且須要不多的集成工做。 咱們還發現,單獨測試組件,使用接口模擬類型以及迭代構建功能很是容易。

內部構件

NSQ 由 3 個守護進程組成:

  • nsqd 是接收,排隊和向客戶端傳遞消息的守護程序。
  • nsqlookupd 是管理拓撲信息並提供最終一致的發現服務的守護程序。
  • nsqadmin 是一個 Web UI,能夠實時內省集羣(並執行各類管理任務)。

NSQ 中的數據流被建模爲流和消費者的樹。 主題是不一樣的數據流。 頻道是訂閱特定主題的消費者的邏輯分組。

單個 nsqd 能夠有不少主題,每一個主題能夠有不少通道。 通道接收主題的全部消息的副本,在通道上的每一個消息在其訂戶之間分發時啓用多播樣式傳送,從而實現負載平衡。

這些原語構成了表達各類簡單和複雜拓撲的強大框架。

主題與通道

主題和通道,NSQ 的核心原語,最能說明系統設計如何無縫轉換爲 Go 的功能。

Go 的通道(如下稱爲「go-chan」用於消除歧義)是表達隊列的天然方式,所以 NSQ 主題/通道的核心只是消息指針的緩衝轉發。 緩衝區的大小等於--mem-queue-size 配置參數。

從線上讀取數據後,向主題發佈消息的行爲包括:

  1. Message 結構的實例化(以及消息體[]字節的分配)
  2. 讀取鎖定以獲取主題
  3. 讀鎖以檢查發佈的能力
  4. 發送緩衝的 go-chan

爲了從主題到其通道獲取消息,主題不能依賴於典型的 go-chan 接收語義,由於在 go-chan 上接收的多個 goroutine 將分發消息,而指望的最終結果是將每一個消息複製到每一個通道(goroutine)。

相反,每一個主題都維護着 3 個主要的 goroutine。 第一個稱爲路由器,負責從傳入的 go-chan 讀取新發布的消息並將它們存儲在隊列(內存或磁盤)中。

第二個叫作 messagePump,負責將消息複製並推送到通道,如上所述。

第三個負責 DiskQueue IO,稍後將討論。

通道稍微複雜一點,可是共享暴露單個輸入和單個輸出 go-chan 的基本目標(以抽象出內部消息可能在內存或磁盤上的事實):

682fc358-5f76-11e3-9b05-3d5baba67f13.png

此外,每一個通道維護 2 個按時間排序的優先級隊列,負責延遲和正在進行的消息超時(以及 2 個隨附的 goroutine 用於監控它們)。

經過管理每通道數據結構來改進並行化,而不是依賴於 Go 運行時的全局計時器調度程序。

注意:在內部,Go 運行時使用單個優先級隊列和 goroutine 來管理計時器。 這支持(但不限於)整個時間包。 它一般不須要用戶時間排序的優先級隊列,但重要的是要記住它是一個單一鎖定的數據結構,可能會影響 GOMAXPROCS> 1 的性能。 請參閱 runtime / time.go。

後端/ DiskQueue

NSQ 的設計目標之一是限制內存中保存的消息數量。 它經過 DiskQueue(它擁有主題或通道的第三個主要 goroutine)透明地將消息溢出寫入磁盤來實現此目的。

因爲內存隊列只是一個 go-chan,若是可能的話,首先將消息路由到內存是很簡單的,而後回退到磁盤:

for msg := range c.incomingMsgChan {
 select {  case c.memoryMsgChan <- msg:  default:  err := WriteMessageToBackend(&msgBuf, msg, c.backend)  if err != nil {  // ... handle errors ...  }  } } 複製代碼

利用 Go 的 select 語句,只需幾行代碼便可表示此功能:上述默認狀況僅在 memoryMsgChan 已滿時執行。

NSQ 還具備短暫主題/渠道的概念。 它們丟棄消息溢出(而不是寫入磁盤)並在它們再也不有客戶訂閱時消失。 這是 Go 接口的完美用例。 主題和通道具備聲明爲後端接口而不是具體類型的結構成員。 正常主題和通道使用 DiskQueue,而 DummyBackendQueue 中使用短暫的存根,實現無操做後端。

下降 GC 壓力

在任何垃圾收集環境中,您都會受到吞吐量(執行有用工做),延遲(響應性)和駐留集大小(佔用空間)之間的緊張關係。

從 Go 1.2 開始,GC 就是標記 - 掃描(並行),非代數,非壓縮,中止世界,並且大多數都是精確的。 它大部分都是精確的,由於其他的工做沒有及時完成(它是針對 Go 1.3)。

Go GC 確定會繼續改進,但廣泛的事實是:你創造的垃圾越少,你收集的時間就越少。

首先,瞭解 GC 在實際工做負載下的表現很是重要。 爲此,nsqd 以 statsd 格式(以及其餘內部指標)發佈 GC 統計數據。 nsqadmin 顯示這些指標的圖表,讓您深刻了解 GC 在頻率和持續時間方面的影響.

爲了真正減小垃圾,您須要知道它的生成位置。 Go 工具鏈再一次提供了答案:

  • 使用測試包並使用 go test -benchmem來測試熱代碼路徑。 它描述了每次迭代的分配數量(基準運行能夠與 benchcmp 進行比較)。
  • 使用go build -gcflags -m構建,輸出轉義分析的結果。

考慮到這一點,如下優化證實對 nsqd 頗有用

  • 避免[]字節到字符串轉換。
  • 重用緩衝區或對象(有一天多是 sync.Pool 又是問題 4720)。
  • 預分配切片(指定 make 中的容量)並始終知道線上項目的數量和大小。
  • 對各類可配置撥號應用合理的限制(例如消息大小)。
  • 避免裝箱(使用 interface {})或沒必要要的包裝器類型(好比「多值」go-chan 的結構)。
  • 避免在熱代碼路徑中使用延遲(它分配)。

TCP 協議

NSQ TCP 協議是一個很好的例子,其中使用這些 GC 優化概念產生了很大的效果。

該協議採用長度前綴幀結構,使編碼和解碼變得簡單和高效:

[x][x][x][x][x][x][x][x][x][x][x][x]...
| (int32) || (int32) || (binary) | 4-byte || 4-byte || N-byte ------------------------------------...  size frame ID data 複製代碼

因爲幀的組件的確切類型和大小是提早知道的,咱們能夠避免encoding/binary包的方便性 Read()Write()包裝器(及其無關的接口查找和轉換),而是調用適當的二進制文件binary.BigEndian 方法直接。

爲了減小套接字 IO 系統調用,客戶端 net.Conn 包含 bufio.Reader 和 bufio.Writer。 Reader 公開了 ReadSlice(),它重用了它的內部緩衝區。 這在讀取插座時幾乎消除了分配,大大下降了 GC 的壓力。 這是可能的,由於與大多數命令相關聯的數據不會轉義(在不是這樣的邊緣狀況下,數據被顯式複製)。

在更低級別,MessageID 被聲明爲[16]字節,以便可以將其用做映射鍵(切片不能用做映射鍵)。 可是,因爲從套接字讀取的數據存儲爲[]字節,而不是經過分配字符串鍵產生垃圾,而且爲了不從切片到 MessageID 的後備數組的副本,使用不安全的包直接轉換切片到一個 MessageID:

id := *(*nsq.MessageID)(unsafe.Pointer(&msgID))
複製代碼

注意:這是一個黑客。 若是編譯器對此進行了優化而且問題 3512 能夠解決此問題,則沒有必要。 它還值得閱讀問題 5376,該問題討論了「const like」字節類型的可能性,它能夠在接受字符串的狀況下互換使用,而無需分配和複製。

相似地,Go 標準庫僅在字符串上提供數字轉換方法。 爲了不字符串分配,nsqd 使用直接在[]字節上操做的自定義基本 10 轉換方法。

這些彷佛是微優化,但 TCP 協議包含一些最熱門的代碼路徑。 總的來講,它們以每秒數萬條消息的速度,對分配數量和開銷產生了重大影響:

benchmark                    old ns/op    new ns/op    delta
BenchmarkProtocolV2Data 3575 1963 -45.09%  benchmark old ns/op new ns/op delta BenchmarkProtocolV2Sub256 57964 14568 -74.87% BenchmarkProtocolV2Sub512 58212 16193 -72.18% BenchmarkProtocolV2Sub1k 58549 19490 -66.71% BenchmarkProtocolV2Sub2k 63430 27840 -56.11%  benchmark old allocs new allocs delta BenchmarkProtocolV2Sub256 56 39 -30.36% BenchmarkProtocolV2Sub512 56 39 -30.36% BenchmarkProtocolV2Sub1k 56 39 -30.36% BenchmarkProtocolV2Sub2k 58 42 -27.59% 複製代碼

HTTP

NSQ 的 HTTP API 創建在 Go 的 net / http 包之上。 由於它只是 HTTP,因此幾乎任何現代編程環境均可以使用它,而無需特殊的客戶端庫。

它的簡潔性掩蓋了它的強大功能,由於 Go 的 HTTP 工具箱最有趣的一個方面是它支持的各類調試功能。 net / http / pprof 包直接與本機 HTTP 服務器集成,公開端點以檢索 CPU,堆,goroutine 和 OS 線程配置文件。 這些能夠直接從 go 工具中定位:

$ go tool pprof http://127.0.0.1:4151/debug/pprof/profile
複製代碼

這對於調試和分析正在運行的進程很是有價值!

此外,/stats 端點以 JSON 或漂亮打印的文本返回大量度量標準,使管理員能夠輕鬆實時地從命令行進行內省:

$ watch -n 0.5 'curl -s http://127.0.0.1:4151/stats | grep -v connected'
複製代碼

這產生連續輸出,如:

[page_views     ] depth: 0     be-depth: 0     msgs: 105525994 e2e%: 6.6s, 6.2s, 6.2s
 [page_view_counter ] depth: 0 be-depth: 0 inflt: 432 def: 0 re-q: 34684 timeout: 34038 msgs: 105525994 e2e%: 5.1s, 5.1s, 4.6s  [realtime_score ] depth: 1828 be-depth: 0 inflt: 1368 def: 0 re-q: 25188 timeout: 11336 msgs: 105525994 e2e%: 9.0s, 9.0s, 7.8s  [variants_writer ] depth: 0 be-depth: 0 inflt: 592 def: 0 re-q: 37068 timeout: 37068 msgs: 105525994 e2e%: 8.2s, 8.2s, 8.2s  [poll_requests ] depth: 0 be-depth: 0 msgs: 11485060 e2e%: 167.5ms, 167.5ms, 138.1ms  [social_data_collector ] depth: 0 be-depth: 0 inflt: 2 def: 3 re-q: 7568 timeout: 402 msgs: 11485060 e2e%: 186.6ms, 186.6ms, 138.1ms  [social_data ] depth: 0 be-depth: 0 msgs: 60145188 e2e%: 199.0s, 199.0s, 199.0s  [events_writer ] depth: 0 be-depth: 0 inflt: 226 def: 0 re-q: 32584 timeout: 30542 msgs: 60145188 e2e%: 6.7s, 6.7s, 6.7s  [social_delta_counter ] depth: 17328 be-depth: 7327 inflt: 179 def: 1 re-q: 155843 timeout: 11514 msgs: 60145188 e2e%: 234.1s, 234.1s, 231.8s  [time_on_site_ticks] depth: 0 be-depth: 0 msgs: 35717814 e2e%: 0.0ns, 0.0ns, 0.0ns  [tail821042#ephemeral ] depth: 0 be-depth: 0 inflt: 0 def: 0 re-q: 0 timeout: 0 msgs: 33909699 e2e%: 0.0ns, 0.0ns, 0.0ns 複製代碼

最後,每一個新的 Go 版本一般都會帶來可衡量的性能提高。 對最新版本的 Go 進行從新編譯提供免費提高時老是很好!

依賴

來自其餘生態系統,Go 的管理依賴關係的哲學(或缺少)須要一點時間來習慣。

NSQ 從一個單一的巨型倉庫發展而來,具備相對進口,內部包之間幾乎沒有分離,徹底接受關於結構和依賴管理的推薦最佳實踐。

有兩種主要的思想流派:

  1. 供應:將正確修訂的依賴項複製到應用程序的存儲庫中,並修改導入路徑以引用本地副本。
  2. Virtual Env:列出您須要的依賴項的修訂版,並在構建時生成包含這些固定依賴項的原始 GOPATH 環境。

注意:這實際上僅適用於二進制包,由於對於可導入包來講,對於使用哪一個版本的依賴項作出中間決策是沒有意義的。

NSQ 使用 gpm 爲上面的(2)提供支持。

它的工做原理是將您的依賴項記錄在 Godeps 文件中,咱們稍後將其用於構建 GOPATH 環境。

測試

Go 爲編寫測試和基準測試提供了堅實的內置支持,由於 Go 使得併發操做的建模變得如此簡單,因此在測試環境中創建一個完整的 nsqd 實例是微不足道的。

可是,初始實現的一個方面成爲測試的問題:全局狀態。最明顯的罪犯是使用一個全局變量,該變量在運行時保存對 nsqd 實例的引用,即 var nsqd * NSQd。

某些測試會經過使用短格式變量賦值(即 nsqd:= NewNSQd(...))無心中在本地範圍內屏蔽此全局變量。這意味着全局引用並未指向當前正在運行的實例,從而破壞了測試。

要解決此問題,將傳遞一個 Context 結構,其中包含配置元數據和對父 nsqd 的引用。全部對全局狀態的引用都被本地上下文替換,容許子(主題,通道,協議處理程序等)安全地訪問此數據並使其更可靠地進行測試。

穩健性

面對不斷變化的網絡條件或意外事件而不健壯的系統是在分佈式生產環境中不能很好地運行的系統。

NSQ 的設計和實現方式容許系統容忍故障並以一致,可預測和不使人驚訝的方式運行。

最重要的理念是快速失敗,將錯誤視爲致命錯誤,並提供調試確實發生的任何問題的方法。

可是,爲了作出反應,你須要可以發現異常狀況......

心跳和超時

NSQ TCP 協議是面向推送的。在鏈接,握手和訂閱以後,消費者處於 RDY 狀態 0.當消費者準備好接收消息時,它將該 RDY 狀態更新爲它願意接受的消息的數量。 NSQ 客戶端庫在後臺不斷地管理它,從而產生流控制的消息流。

nsqd 會按期經過鏈接發送心跳。客戶端能夠配置心跳之間的間隔,但 nsqd 在發送下一個以前須要響應。

應用程序級心跳和 RDY 狀態的組合避免了行頭阻塞,不然會致使心跳無效(即,若是消費者在處理消息流時落後於 OS 的接收緩衝區將填滿,阻止心跳)。

爲了保證進度,全部網絡 IO 都與相對於配置的心跳間隔的截止時間綁定。這意味着你能夠從字面上拔掉 nsqd 和消費者之間的網絡鏈接,它將檢測並正確處理錯誤。

檢測到致命錯誤時,強制關閉客戶端鏈接。正在進行的消息超時並從新排隊以便傳遞給另外一個消費者。最後,記錄錯誤並增長各類內部指標。

管理 Goroutines

啓動 goroutines 很是容易。 不幸的是,編排清理工做並不容易。 避免死鎖也具備挑戰性。 大多數狀況下,這歸結爲一個排序問題,在上游 goroutines 發送它以前,在 go-chan 上接收的 goroutine 退出。

爲何要關心呢? 這很簡單,孤立的 goroutine 是一個內存泄漏。 長時間運行的守護進程中的內存泄漏是很差的,特別是當指望您的進程在其餘全部失敗時都將保持穩定時。

更復雜的是,典型的 nsqd 進程在消息傳遞中涉及許多 goroutine。 在內部,消息「全部權」常常發生變化。 爲了可以乾淨地關閉,考慮全部進程內消息很是重要。

雖然沒有任何魔法子彈,但如下技術使其更容易管理......

WaitGroups

同步包提供了 sync.WaitGroup,可用於執行有多少 goroutine 的實時計算(並提供等待退出的方法)。

爲了減小典型的樣板,nsqd 使用這個包裝器:

type WaitGroupWrapper struct {
 sync.WaitGroup }  func (w *WaitGroupWrapper) Wrap(cb func()) {  w.Add(1)  go func() {  cb()  w.Done()  }() }  // can be used as follows: wg := WaitGroupWrapper{} wg.Wrap(func() { n.idPump() }) ... wg.Wait() 複製代碼

退出信號

在多個子 goroutine 中觸發事件的最簡單方法是提供一個在準備就緒時關閉的 go-chan。 在該 go-chan 上的全部未決接收將被激活,而不是必須向每一個 goroutine 發送單獨的信號。

func work() {
 exitChan := make(chan int)  go task1(exitChan)  go task2(exitChan)  time.Sleep(5 * time.Second)  close(exitChan) } func task1(exitChan chan int) {  <-exitChan  log.Printf("task1 exiting") }  func task2(exitChan chan int) {  <-exitChan  log.Printf("task2 exiting") } 複製代碼

同步退出

實現一個可靠的,無死鎖的退出路徑很是難以解決全部正在進行的消息。 一些提示:

  1. 理想狀況下,負責發送 go-chan 的 goroutine 也應該負責關閉它。

  2. 若是消息不能丟失,請確保清空相關的 go-chans(特別是無緩衝的!)以保證發送者能夠取得進展。

  3. 或者,若是消息再也不相關,則應將單個 go-chan 上的發送轉換爲選擇,並添加退出信號(如上所述)以保證進度。

  4. 通常順序應該是:

    1. 中止接受新鏈接(關閉偵聽器)
    2. 信號退出子 goroutines(見上文)
    3. 在 WaitGroup 上等待 goroutine 退出(見上文)
    4. 恢復緩衝的數據
    5. 刷新留在磁盤上的任何東西

日誌

最後,可使用的最重要的工具是記錄的 goroutines 的入口和出口! 它使得在死鎖或泄漏的狀況下識別罪魁禍首變得更加容易。

nsqd 日誌行包括將 goroutine 與其兄弟(和父級)相關聯的信息,例如客戶端的遠程地址或主題/通道名稱。

日誌是冗長的,但並不詳細到日誌壓倒性的程度。 有一條細線,但 nsqd 傾向於在發生故障時在日誌中提供更多信息,而不是試圖以犧牲實用性爲代價來減小干擾。

公衆號:吾輩的箱庭
公衆號:吾輩的箱庭
相關文章
相關標籤/搜索