簡介php
NSQ是Go語言編寫的,開源的分佈式消息隊列中間件,其設計的目的是用來大規模地處理天天數以十億計級別的消息。NSQ 具備分佈式和去中心化拓撲結構,該結構具備無單點故障、故障容錯、高可用性以及可以保證消息的可靠傳遞的特徵,是一個成熟的、已在大規模生成環境下應用的產品。html
NSQ在國內公司用的不多,在使用當中愈發的以爲驚喜,好比他的簡單易用、部署快捷,再好比以前比較困擾的 延時定時消息,發現nsq 也支持,官方文檔比較全,諮詢問題時回覆也很是的耐心和即時,因此我以爲有必要發佈一篇文章來介紹下nsq,惠及大衆。git
nsq 有三個必要的組建nsqd、nsqlookupd、nsqadmin 其中nsqd 和 nsqlookup是必須部署的 下面咱們一一介紹。github
nsqd :web
負責接收消息,存儲隊列和將消息發送給客戶端,nsqd 能夠多機器部署,當你使用客戶端向一個topic發送消息時,能夠配置多個nsqd地址,消息會隨機的分配到各個nsqd上,nsqd優先把消息存儲到內存channel中,當內存channel滿了以後,則把消息寫到磁盤文件中。他監聽了兩個tcp端口,一個用來服務客戶端,一個用來提供http的接口 ,nsqd 啓動時置頂下nsqlookupd地址便可:算法
nsqd –lookupd-tcp-address=127.0.0.1:4160
也能夠指定端口 與數據目錄sql
nsqd –lookupd-tcp-address=127.0.0.1:4160 --broadcast-address=127.0.0.1 -tcp-address=127.0.0.1:4154 -http-address=」0.0.0.0:4155″ –data-path=/data/nsqdata
其餘配置項可詳見官網服務器
nsqlookupd:
主要負責服務發現 負責nsqd的心跳、狀態監測,給客戶端、nsqadmin提供nsqd地址與狀態php7
nsqadmin:
nsqadmin是一個web管理界面 啓動方式以下:異步
nsqadmin –lookupd-http-address=127.0.0.1:4161
channel詳情頁示例圖以下 ,empty能夠清空當前channel的信息,delete刪除當前channel, pause是暫停消息消費。
圖中也有幾個比較重要的參數 depth當前的積壓量,in-flight表明已經投遞還未消費掉的消息,deferred是未消費的定時(延時)消息數,ready count比較重要,go的客戶端是經過設置max-in-flight 除以客戶端鏈接數獲得的,表明一次推給客戶端多少條消息,或者客戶端準備一次性接受多少條消息,謹慎設置其值,由於可能形成服務器壓力,若是消費能力比較弱,rdy建議設置的低一點好比3
Topic 和 Channel
其實nsqd至關於kafka當中的分區,channel和consumers客戶端的多個鏈接 至關於kafka的消費組,但nsq比kafka使用方式便捷概念上更容易理解
拋開與kafka的對比,nsq的topic 能夠設置多個channel,由於有可能有多個業務方須要定值topic的消息,這樣互不影響,
固然一個消息會發送topic下的全部channel,而後會分配到不一樣客戶端的鏈接上,以下圖。
這篇文章主要介紹nsq的使用,源碼就不展開講,若是有興趣的同窗多的話 過幾天我會再開一篇專門敘述nsq的源碼與分析。
這裏提下延時消息:
nsq支持延時消息的投遞,好比我想這條消息5分鐘以後才被投遞出去被客戶端消費,較於普通的消息投遞,多了個毫秒數,默認支持最大的毫秒數爲3600000毫秒也就是60分鐘,不過這個值能夠在nsqd 啓動的時候 用 -max-req-timeout參數修改最大值。
延時消息可用於如下場景,好比一個訂單超過30分鐘未付款,修改其狀態 或者給客戶發短信提醒,好比以前看到的滴滴打車訂單完成後 必定時間內未評價的能夠未其設置默認值,再好比用戶的積分過時,等等場景避免了全表掃描,異步處理,kafka不支持延時消息的投遞,目前知道支持的有rabbitmq rocketmq,可是rabbitmq 有坑,有可能會超時投遞,而rocketmq只有阿里雲付費版支持的比較好。
nsq延時消息的實現是用最小堆算法完成,做者繼承實現heap的一系類接口,專門寫了一個pqueque最小堆的優先隊列,在internal/pequeque 目錄能夠看到相關實現,pub的時候若是chanMsg.deferred != 0則會調用channel.PutMessageDeferred方法,最終會調用繼承了go heap接口的pqueque.push方法
延時消息的處理 和普通消息同樣都是 nsqd/protocol_v2.go下messagePump 中把消息發送給客戶端 而後在queueScanWorker中分別處理,pop是peekAndShift方法中,拿當前時間 和 deferred[0]對好比果大於 就彈出發送給客戶端 以下代碼:
func (n *NSQD) queueScanWorker(workCh chan *Channel, responseCh chan bool, closeCh chan int) { for { select { case c := <-workCh: now := time.Now().UnixNano() dirty := false if c.processInFlightQueue(now) { dirty = true } if c.processDeferredQueue(now) { dirty = true } responseCh <- dirty case <-closeCh: return } } } func (c *Channel) processDeferredQueue(t int64) bool { c.exitMutex.RLock() defer c.exitMutex.RUnlock() if c.Exiting() { return false } dirty := false for { c.deferredMutex.Lock() item, _ := c.deferredPQ.PeekAndShift(t) c.deferredMutex.Unlock() if item == nil { goto exit } dirty = true msg := item.Value.(*Message) _, err := c.popDeferredMessage(msg.ID) if err != nil { goto exit } c.put(msg) } exit: return dirty } func (pq *PriorityQueue) PeekAndShift(max int64) (*Item, int64) { if pq.Len() == 0 { return nil, 0 } item := (*pq)[0] if item.Priority > max { return nil, item.Priority - max } heap.Remove(pq, 0) return item, 0 }
php和go的客戶端的使用
官網客戶端連接:Client Libraries php客戶端以前官網有一個5年前比較老的客戶端,已經沒人維護 甚至沒法運行,因而我貢獻了一個php72擴展版本 php-nsq,速度塊了近三倍,正在逐步完善,支持各類配置與特性,目前已被官網收納,簡單介紹下使用 順便求下star
php-nsq pub :
$nsqd_addr = array( "127.0.0.1:4150", "127.0.0.1:4154" ); $nsq = new Nsq(); $is_true = $nsq->connect_nsqd($nsqd_addr); for($i = 0; $i < 20; $i++){ $nsq->publish("test", "nihao"); }
php-nsq 延時pub :
參數 僅僅多一個毫秒參數,so easy!
$deferred = new Nsq(); $isTrue = $deferred->connectNsqd($nsqdAddr); for($i = 0; $i < 20; $i++){ $deferred->deferredPublish("test", "message daly", 3000); // 第三值默認範圍 millisecond default : [0 < millisecond < 3600000] ,能夠更改 上面已提到 }
php-nsq sub :
拋異常消息能夠自動重試,重試時間能夠有retry_delay_time設定,多少時間後再次接收被重試的消息
$nsq_lookupd = new NsqLookupd("127.0.0.1:4161"); //the nsqlookupd tcp addr $nsq = new Nsq(); $config = array( "topic" => "test", "channel" => "struggle", "rdy" => 2, //optional , default 1 "connect_num" => 1, //optional , default 1 "retry_delay_time" => 5000, //optional, default 0 , after 5000 msec, message will be retried ); $nsq->subscribe($nsq_lookupd, $config, function($msg){ echo $msg->payload; echo $msg->attempts; echo $msg->message_id; echo $msg->timestamp; });
go client pub
package main import ( "github.com/nsqio/go-nsq" ) var producer *nsq.Producer func main() { nsqd := "127.0.0.1:4150" producer, err := nsq.NewProducer(nsqd, nsq.NewConfig()) producer.Publish("test", []byte("nihao")) if err != nil { panic(err) } }
go client sub
package main import ( "fmt" "sync" "github.com/nsqio/go-nsq" ) type NSQHandler struct { } func (this *NSQHandler) HandleMessage(msg *nsq.Message) error { fmt.Println("receive", msg.NSQDAddress, "message:", string(msg.Body)) return nil } func testNSQ() { waiter := sync.WaitGroup{} waiter.Add(1) go func() { defer waiter.Done() config:=nsq.NewConfig() config.MaxInFlight=9 //創建多個鏈接 for i := 0; i<10; i++ { consumer, err := nsq.NewConsumer("test", "struggle", config) if nil != err { fmt.Println("err", err) return } consumer.AddHandler(&NSQHandler{}) err = consumer.ConnectToNSQD("127.0.0.1:4150") if nil != err { fmt.Println("err", err) return } } select{} }() waiter.Wait() } func main() { testNSQ(); }
同時此篇文章 更新到了本身博客