簡介
NSQ是Go語言編寫的,開源的分散式訊息佇列中介軟體,其設計的目的是用來大規模地處理每天數以十億計級別的訊息。NSQ 具有分散式和去中心化拓撲結構,該結構具有無單點故障、故障容錯、高可用性以及能夠保證訊息的可靠傳遞的特徵,是一個成熟的、已在大規模生成環境下應用的產品。
NSQ在國內公司用的很少,在使用當中愈發的覺得驚喜,比如他的簡單易用、部署快捷,再比如之前比較困擾的 延時定時訊息,發現nsq 也支援,官方文件比較全,諮詢問題時回覆也非常的耐心和即時,所以我覺得有必要釋出一篇文章來介紹下nsq,惠及大眾。
nsq 有三個必要的組建nsqd、nsqlookupd、nsqadmin 其中nsqd 和 nsqlookup是必須部署的 下面我們一一介紹。
nsqd :
負責接收訊息,儲存佇列和將訊息傳送給客戶端,nsqd 可以多機器部署,當你使用客戶端向一個topic傳送訊息時,可以配置多個nsqd地址,訊息會隨機的分配到各個nsqd上,nsqd優先把訊息儲存到記憶體channel中,當記憶體channel滿了之後,則把訊息寫到磁碟檔案中。他監聽了兩個tcp埠,一個用來服務客戶端,一個用來提供http的介面 ,nsqd 啟動時置頂下nsqlookupd地址即可:
nsqd –lookupd-tcp-address=127.0.0.1:4160
也可以指定埠 與資料目錄
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地址與狀態
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();
}
同時此篇文章 更新到了自己部落格