golang实战-nsq集群入门与坑

这段时间,群里的gofun大增,讨论了nsq在集群使用的使用问题。这里简单整理了一下,希望有所帮助。

作为实时的分布式消息处理平台,nsq设计的目的是用来大规模地处理每天数以十亿计级别的消息。
由于具有分布式和去中心化拓扑结构,该结构具有无单点故障、故障容错、高可用性以及能够保证消息的可靠传递的特征。nsq基本是go开发服务端的消息中间件首选。
如果您在使用过程中,发现nsq的consumer无法接受到集群消息,或者想知道consumer与nsqd之间的拓扑关系,可以看看。

##组件构成
nsq有三个组件以及辅助的几个工具构成。
nsqd
nsqd 是一个守护进程,负责接收,排队,投递消息给客户端。
它可以独立运行,不过通常它是由 nsqlookupd 实例所在集群配置的(它在这能声明 topics 和 channels,以便大家能找到)。

  • 服务启动后有两个端口:一个给客户端,另一个是 HTTP API。还能够开启HTTPS。
  • 同一台服务器启动多个nsqd,要注意端口和数据路径必须不同,包括:–lookupd-tcp-address、 -tcp-address、–data-path。
  • 删除topic、channel需要http api调用。

nsqlookupd
nsqlookupd 是守护进程,负责管理拓扑信息并提供最终一致性的发现服务。客户端通过查询 nsqlookupd 来发现指定话题(topic)的生产者,并且 nsqd 节点广播话题(topic)和通道(channel)信息。

  • 该服务运行后有两个端口:TCP 接口,nsqd 用它来广播;HTTP 接口,客户端用它来发现和管理。
  • 在生产环境中,为了高可用,最好部署三个nsqlookupd服务。

nsqadmin
nsqadmin 是一套 WEB UI,用来汇集集群的实时统计,并执行不同的管理任务。
运行后,能够通过4171端口查看并管理topic和channel。

  • 通常只需要运行一个。

utilities
常见基础功能、数据流处理工具,如nsq_stat、nsq_tail、nsq_to_file、nsq_to_http、nsq_to_nsq、to_nsq 。目前在apps目录下。

基本要点

Topic和Channel
官方有个非常漂亮的动态图,展示了一个topic对应多个channel的效果
nsq design
表达了三个含义:

  • 没有router
    对于消息中间件,话题(topic)和通道(channel)是非常基本的,他们是1:N 的关系。
    相对于RabitMQ,NSQ没有router这一层,功能也简化了不少,因此运维非常容易上手。

  • 消费者对应Channel
    如果channel没有消费,消息将会保留。如果同一个channel有多个消费者,则会轮训,按序分配给就绪(当前无处理任务)的消费者。因此,多消费者情况下,无法保证有序执行。

  • 存储
    Topic和Channel缓冲的数据相互独立,防止缓慢消费者造成对其他通道造成积压(同样适用于话题级别)。

消息创建与接收

  • 发布者
    消息发布,只能面向具体的nsqd服务进行。在API中对应的是nsq.Producer,直接初始化,就可以用了,非常简单:
    config := nsq.NewConfig()
	p, err := nsq.NewProducer("127.0.0.1:4150", config) 
	if err != nil {
		panic(err)
	}
	//发布一条消息
	p.Publish("test", []byte(time.Now().String()))

代码中有两个含义非常重要:

  1. 一个topic的发布者只对应一个具体的NSQD,但可以多个发布者同时向一个NSQD发送消息,他们是N:1的关系。
  2. NSQD与topic是1:N的关系。

代码中的config是连接配置,作为发布者,不用刻意修改,在集群中足够使用。

  • 消费者
    消费者的理解要复杂一些,集群中最容易碰到无法接受到多节点消息的问题。结合官方多个文档及踩过的坑,需要注意:
  1. consumer要接收消息,是要连接到具体的nsqd服务的。通常我们能通过封装好的方法,基于lookupd服务来获取所有的nsqd服务地址并连接。
  2. 一个消费者订阅的topic分布在哪些nsqd服务中,则会直接连接。**nsqd之间是绝对不会互传topic的具体数据的。**下图描绘了consumer与nsqd的关系:
    consumer.png-76.7kB
  3. 当多个nsqd服务都有相同的topic的时候,consumer要修改默认设置config.MaxInFlight才能连接。
  4. consumer与topic没有直接联系,而是通过具体的channel接受数据。如果consumer退出,channel不会自动删除。 如果不再需要,需要通过http端口删除channel,否则很可能会导致磁盘空间不足。

只要注意这几点,就很容易写出基本符合业务的代码:

    config:=nsq.NewConfig()
    //最大允许向两台NSQD服务器接受消息,默认是1,要特别注意
    config.MaxInFlight=2
   	c1, err1 := nsq.NewConsumer("test", "test-channel1", nsq.NewConfig()) // 新建一个消费者
	if err1 != nil {
		panic(err1)
	}
	//对消息进行处理的具体方法
	receive:=func(msg *nsq.Message)error{
	    fmt.Println(string(msg.Body)
	    return nil
	}
	// 添加消息处理的具体实现
	c1.AddHandler(nsq.HandlerFunc(receive)) 
	//将消费者连接到具体的NSQD
	//if err := c1.ConnectToNSQD("127.0.0.1:4150"); err != nil { 
	//	panic(err)
	//}
	//或者,如果启动了Lookupd服务,可通过nsqlookupd再分发给具体的nsqd
	if err := c1.ConnectToNSQLookupd("127.0.0.1:4161"); err != nil {
		panic(err)
	}

当消费者解析数据抛出错误后,channel会requene,但间隔时间将会越来越长。

##两节点集群示例
以下为同一台设备部署一个nsqlookupd、两个nsqd、一个admin的部署脚本和go代码,可以做一个简单的集群调试:
###nsq下载
进入官网nsq.io的下载链接,获取最新版的执行文件。
对于macos,可以直接brew install
###服务启动与停止
以下脚本,为服务启动、停止脚本,可直接运行。需要注意chmod +x

nsq_start.sh

#服务启动
#注意更改一下 --data-path 所指定的数据存放路径,否则会无法运行。
echo '删除日志文件'
rm -f nsqlookupd.log
rm -f nsqd1.log
rm -f nsqd2.log
rm -f nsqadmin.log

echo '启动nsq服务'
nohup nsqlookupd >nsqlookupd.log 2>&1&

echo '启动nsqd服务'
nohup nsqd --lookupd-tcp-address=0.0.0.0:4160 -tcp-address="0.0.0.0:4150"  --data-path=~/nsqd1  >nsqd1.log 2>&1&
nohup nsqd --lookupd-tcp-address=0.0.0.0:4160 -tcp-address="0.0.0.0:4152" -http-address="0.0.0.0:4153" --data-path=~/nsqd2 >nsqd2.log 2>&1&

echo '启动nsqdadmin服务'
nohup nsqadmin --lookupd-http-address=0.0.0.0:4161 >nsqadmin.log 2>&1&

nsq_shutdown.sh

#nsq_shutdown.sh
#服务停止
ps -ef | grep nsq| grep -v grep | awk '{print $2}' | xargs kill -2

运行后,访问本机:4171端口,就能够通过web页面进行查看:
nsq admin效果
###运行代码

package main

import (
	"github.com/nsqio/go-nsq"
	"time"
	"fmt"
	"utils/waitwraper"
)

func main() {
	var wg waitwraper.WaitWraper
	//接受消息
	consume()
	//分别向不同的服务节点发送消息
	wg.Wrap(func(){	produce("node1","localhost:4150")})
	wg.Wrap(func (){produce("node2","localhost:4152")})
	
	wg.Wait()
}
func produce(tag string,addr string) {
	config := nsq.NewConfig()
	p, err := nsq.NewProducer(addr, config)
	if err != nil {
		panic(err)
	}
	for {
		time.Sleep(time.Second*5)
		p.Publish("test", []byte(tag+":"+time.Now().String()))
	}
}
func consume() {
	config := nsq.NewConfig()
	//注意MaxInFlight的设置,默认只能接受一个节点
	config.MaxInFlight=2
	c, err := nsq.NewConsumer("test", "consum", config)
	if err != nil {
		panic(err)
	}
	hand := func(msg *nsq.Message) error{
		fmt.Println(string(msg.Body))
		return nil
	}
	c.AddHandler(nsq.HandlerFunc(hand))
	if err:= c.ConnectToNSQLookupd("localhost:4161");err!=nil{
		fmt.Println(err)
	}
}

代码运行后,看到的UI会有点变化:
消息处理中的UI

##小结
最后再次强调一下非常有用的几个细节:

  • nsqd启动时,端口和数据存放要不同。

  • 消息发送必须指定具体的某个nsqd;而消费则可以通过lookupd获取再重定向

  • 消费者接受数据时,要设置 config.MaxInFlight。

  • channel在消费者退出后并不会删除,需要特别注意。如果仅仅是想利用消息中间件解耦服务,不考虑离线数据保存,不妨考虑nats。

  • channel的名字,有很多限制,基本ASSCI字符+数字,以及点号".",下划线"_"。中文、以及空格、冒号":"、横线"-"等都不得出现。

  • channel有多个消费者时,无法保证消息处理的有序性。

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
在Go语言中,没有内置的SCP(Secure Copy)功能,但你可以使用第方库来实现SCP功能。一个常用的第三方库是`github.com/pkg/sftp`,它提供了SSH文件传输协议的实现。 要使用该库进行SCP操作,你需要先建立一个SSH连接,然后使用SFTP客户端进行文件传输。下面是一个简单的示例代码: ```go package main import ( "fmt" "golang.org/x/crypto/ssh" "github.com/pkg/sftp" "io" "os" ) func main() { // SSH连接配置 config := &ssh.ClientConfig{ User: "username", Auth: []ssh.AuthMethod{ ssh.Password("password"), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), } // 建立SSH连接 conn, err := ssh.Dial("tcp", "example.com:22", config) if err != nil { fmt.Println("Failed to connect to the server:", err) return } defer conn.Close() // 创建SFTP客户端 client, err := sftp.NewClient(conn) if err != nil { fmt.Println("Failed to create SFTP client:", err) return } defer client.Close() // 打开本地文件 localFile, err := os.Open("/path/to/local/file") if err != nil { fmt.Println("Failed to open local file:", err) return } defer localFile.Close() // 创建远程文件 remoteFile, err := client.Create("/path/to/remote/file") if err != nil { fmt.Println("Failed to create remote file:", err) return } defer remoteFile.Close() // 将本地文件内容复制到远程文件 _, err = io.Copy(remoteFile, localFile) if err != nil { fmt.Println("Failed to copy file:", err) return } fmt.Println("File copied successfully!") } ``` 请注意,上述示例代码中的`username`、`password`、`example.com:22`、`/path/to/local/file`和`/path/to/remote/file`需要根据实际情况进行替换。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值