nsq 優秀的訊息佇列

吳振宇發表於2018-01-17

簡介

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();

}

同時此篇文章 更新到了自己部落格

相關文章