NSQ | NATS | |
---|---|---|
持久化 | 支持,需要配置 | nats-core 不支持,nats-stream(弃用)/jetstream支持 |
实时性 | 支持 | 支持 |
高性能 | 支持 | 支持 |
低资源消耗 | 支持 | 支持 |
功能:广播 | 支持 | 支持 |
可追踪 | 支持 | 不支持 |
分布式 | 支持 | 支持 |
功能:负载均衡 | 支持 | 支持 |
高可用 | 支持 | 支持 |
可伸缩 | 支持 | 支持 |
可靠性 | 非高可靠 | 高可靠 |
幂等性 | 不支持 | 不支持 |
顺序性 | 不支持 | 支持 |
集群架构 | 对称集群架构:简单,水平扩展 | 非对称集群架构:需要额外配置 |
请求响应模型 | 不支持 | 支持 |
kv时效存储功能 | 不支持 | jetstream支持 |
文档丰富 | 网上资料挺多 | nats资料不少,但是jetstream资料太少 |
接入简单 | 非常简单 | 不是很简单 |
官网 | https://nsq.io/ | https://nats.io/ |
源码 | https://github.com/nsqio | https://github.com/nats-io |
下面详细对两种消息进行调研
NSQ
对称集群架构,有中心节点做调解
- 无单点故障问题
- 可以直接水平扩展
- 低延迟
- 提供:负载均衡和广播的消息路由
- 支持TLS
- 有集群管理界面
- 提供http接口用于统计和管理
提示:
- nsq-d:消息的服务端
- nsq-lookup:消息集群的节点发现和topic管理模块
- nsq-admin:集群的web管理模块
对于我们这边只需使用nsq的单实例即可,下面这句话摘自官网
- I just want to use nsqd as a work queue on a single node, is that a suitable use case?
Yep, nsqd can run standalone just fine.nsqlookupd is beneficial in larger distributed environments.
翻译:nsqd 可以独立运行就好了。nsqlookupd 在较大的分布式环境中很有用。
安装
docker pull nsqio/nsq:latest
运行
这里分为三个部分
- nsqd:消息处理模块
- nsq-admin:控制管理模块
- nsq-lookup:服务注册与管理模块
采用compose进行启动,文件docker-compose.yml如下
version: '3'
services:
nsqlookupd:
image: nsqio/nsq
command: /nsqlookupd
ports:
- "24160:4160"
- "24161:4161"
nsqd:
image: nsqio/nsq
command: sh -c "/nsqd --mem-queue-size=0 --lookupd-tcp-address=nsqlookupd:4160 && /nsq_to_file --topic=test --output-dir=/tmp --lookupd-http-address=nsqlookupd:4161"
depends_on:
- nsqlookupd
ports:
- "24150:4150"
- "24151:4151"
nsqadmin:
image: nsqio/nsq
command: /nsqadmin --lookupd-http-address=nsqlookupd:4161
depends_on:
- nsqlookupd
ports:
- "24171:4171"
持久化配置
–mem-queue-size:这个设置为0才会实时持久化,否则会在消息达到这个值之后才持久化
启动
docker-compose up -d
测试运行是否OK,查看lookup的4061的被映射接口,这里看到是32773
zhouzhenyong@shizi-2 ~/f/l/nsq> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ca6866c3fe16 nsqio/nsq "/nsqadmin --lookupd…" About a minute ago Up About a minute 4150-4151/tcp, 4160-4161/tcp, 4170/tcp, 0.0.0.0:32775->4171/tcp nsq_nsqadmin_1
5dfdcaaa9078 nsqio/nsq "/nsqd --lookupd-tcp…" About a minute ago Up About a minute 4160-4161/tcp, 4170-4171/tcp, 0.0.0.0:32777->4150/tcp, 0.0.0.0:32776->4151/tcp nsq_nsqd_1
1cb50ae6bd3c nsqio/nsq "/nsqlookupd" About a minute ago Up About a minute 4150-4151/tcp, 4170-4171/tcp, 0.0.0.0:32774->4160/tcp, 0.0.0.0:32773->4161/tcp nsq_nsqlookupd_1
测试网络
zhouzhenyong@shizi-2 ~/f/l/nsq> curl http://127.0.0.1:32773/ping
OK
发送和订阅
在消息消费方面nsq提供了两种方式
- 负载均衡:topic和channel都相同,则是负载均衡
- 广播(多播):topic相同,但是channel不同,则是广播
接入方面支持客户端和http
发送消息:http
向nsqd发送消息,其中端口是nsqd的http端口4151外部映射
curl -d ‘’ 'http://127.0.0.1:32810/pub?topic=test’
发送消息:client
除了http发送外,也可以使用客户端的tcp端口进行发送
func TestPub(t *testing.T) {
cfg := nsq.NewConfig()
// 连接 nsqd 的 tcp 连接
//producer, err := nsq.NewProducer("127.0.0.1:4150", cfg)
producer, err := nsq.NewProducer("127.0.0.1:32811", cfg)
if err != nil {
log.Fatal(err)
}
// 发布消息
var count int
for {
count++
body := fmt.Sprintf("test %d", count)
fmt.Println("发布消息:" + body)
if err := producer.Publish("test", []byte(body)); err != nil {
log.Fatal("publish error: " + err.Error())
}
time.Sleep(1 * time.Second)
}
}
订阅消息:负载均衡
订阅消息这里使用客户端go-nsq
// 消费者1
func TestSub1(t *testing.T) {
cfg := nsq.NewConfig()
consumer, err := nsq.NewConsumer("test-topic", "channel0", cfg)
if err != nil {
log.Fatal(err)
}
// 处理信息
consumer.AddHandler(nsq.HandlerFunc(func(message *nsq.Message) error {
log.Println(string(message.Body))
return nil
}))
// 连接 nsqd 的 tcp 连接
//if err := consumer.ConnectToNSQD("127.0.0.1:4150"); err != nil {
if err := consumer.ConnectToNSQD("127.0.0.1:32811"); err != nil {
log.Fatal(err)
}
<-consumer.StopChan
}
// 消费者2
func TestSub2(t *testing.T) {
cfg := nsq.NewConfig()
consumer, err := nsq.NewConsumer("test-topic", "channel0", cfg)
if err != nil {
log.Fatal(err)
}
// 处理信息
consumer.AddHandler(nsq.HandlerFunc(func(message *nsq.Message) error {
log.Println(string(message.Body))
return nil
}))
// 连接 nsqd 的 tcp 连接
//if err := consumer.ConnectToNSQD("127.0.0.1:4150"); err != nil {
if err := consumer.ConnectToNSQD("127.0.0.1:32811"); err != nil {
log.Fatal(err)
}
<-consumer.StopChan
}
订阅消息:广播
// 消费者1
func TestSub1(t *testing.T) {
cfg := nsq.NewConfig()
// 这里的channel
consumer, err := nsq.NewConsumer("test-topic", "channel1", cfg)
if err != nil {
log.Fatal(err)
}
// 处理信息
consumer.AddHandler(nsq.HandlerFunc(func(message *nsq.Message) error {
log.Println(string(message.Body))
return nil
}))
// 连接 nsqd 的 tcp 连接
//if err := consumer.ConnectToNSQD("127.0.0.1:4150"); err != nil {
if err := consumer.ConnectToNSQD("127.0.0.1:32811"); err != nil {
log.Fatal(err)
}
<-consumer.StopChan
}
// 消费者2
func TestSub2(t *testing.T) {
cfg := nsq.NewConfig()
consumer, err := nsq.NewConsumer("test-topic", "channel2", cfg)
if err != nil {
log.Fatal(err)
}
// 处理信息
consumer.AddHandler(nsq.HandlerFunc(func(message *nsq.Message) error {
log.Println(string(message.Body))
return nil
}))
// 连接 nsqd 的 tcp 连接
//if err := consumer.ConnectToNSQD("127.0.0.1:4150"); err != nil {
if err := consumer.ConnectToNSQD("127.0.0.1:32811"); err != nil {
log.Fatal(err)
}
<-consumer.StopChan
}
可观测:消息查看
admin查看:
端口是admin端口4171的外部映射
可以看到对应消息的消费情况
持久化查看
可以去nsqd中执行持久化文件命令,拉取配置到指定目录,即可查看
/nsq_to_file --topic=test --output-dir=/tmp --lookupd-http-address=nsqlookupd:4161
然后去tmp目录查看,可以看到对应的持久化文件
资源占用
内存启动都是在5M以下
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
443bb321d408 nsq_nsqadmin_1 0.00% 3.246MiB / 2.681GiB 0.12% 51.7kB / 281kB 0B / 0B 9
f6253d1ef3b9 nsq_nsqd_1 0.18% 3.441MiB / 2.681GiB 0.13% 40.6kB / 51.5kB 0B / 0B 10
0a26ed171177 nsq_nsqlookupd_1 0.00% 2.02MiB / 2.681GiB 0.07% 48.2kB / 38.2kB 0B / 0B 7
功能压测
本机硬件指标
- 处理器:2.8GHz四核 i7处理器
- 内存:16G
- 观察工具(可能有小伙伴想知道):lazydocker(懒人docker,帮你把docker stats信息快捷展示的工具)
场景:
一个生产者,两个消费者
1万 | 10万 | 100万 | |||
---|---|---|---|---|---|
负载均衡 | 同步调用 | 内存 | 7.42MB | 8.6MB | 12.23MB |
cpu:最高 | 37.34% | 63% | 65% | ||
cpu:平均 | - | 38.8% | 38.55% | ||
异步调用 | 内存 | 7.3MB | 7.76MB | timeout问题 | |
cpu:最高 | 117% | 126% | ? | ||
cpu:平均 | - | 113% | ? | ||
广播 | 同步调用 | 内存 | 6.82MB | 7.51MB | 9.28MB |
cpu:最高 | 48.28% | 53.55% | 102% | ||
cpu:平均 | 43.46 | 43.64% | 94% | ||
异步调用 | 内存 | 7.219MB | 7.26MB | timeout问题 | |
cpu:最高 | 98.61% | 113% | ? | ||
cpu:平均 | - | 68% | ? |
负载均衡:同步调用(压测代码)
var totalNsqNum = 1
var totalNsqSize = 10000
func TestPub(t *testing.T) {
cfg := nsq.NewConfig()
// 连接 nsqd 的 tcp 连接
//producer, err := nsq.NewProducer("127.0.0.1:4150", cfg)
producer, err := nsq.NewProducer("127.0.0.1:32776", cfg)
if err != nil {
log.Fatal(err)
}
// 发布消息
for i := 0; i < totalNsqNum * totalNsqSize; i++ {
if err := producer.Publish("test-topic", []byte(fmt.Sprintf("test %d", i))); err != nil {
log.Fatal("publish error: " + err.Error())
}
}
log.Printf("send finish")
}
var pressNsqCount1 = 0
var pressNsqCount2 = 0
// 消费者1
func TestSub1(t *testing.T) {
cfg := nsq.NewConfig()
consumer, err := nsq.NewConsumer("test-topic", "channel0", cfg)
if err != nil {
log.Fatal(err)
}
// 处理信息
consumer.AddHandler(nsq.HandlerFunc(func(message *nsq.Message) error {
pressNsqCount1++
if pressNsqCount1%((totalNsqNum*totalNsqSize)/100) == 0 {
log.Printf("[consumer] received msg (%v) ratio: %s", string(message.Body), util.ToString((pressNsqCount1*100)/(totalNsqNum*totalNsqSize)))
}
return nil
}))
// 连接 nsqd 的 tcp 连接
//if err := consumer.ConnectToNSQD("127.0.0.1:4150"); err != nil {
if err := consumer.ConnectToNSQD("127.0.0.1:32776"); err != nil {
log.Fatal(err)
}
<-consumer.StopChan
}
// 消费者2:与TestSub1的代码完全相同
func TestSub2(t *testing.T) {
// ... 省略 ...
}
负载均衡:异步调用(压测代码)
var totalNsqNum = 1
var totalNsqSize = 10000
func TestPub(t *testing.T) {
cfg := nsq.NewConfig()
// 连接 nsqd 的 tcp 连接
//producer, err := nsq.NewProducer("127.0.0.1:4150", cfg)
producer, err := nsq.NewProducer("127.0.0.1:32776", cfg)
if err != nil {
log.Fatal(err)
}
doneChan := make(chan *nsq.ProducerTransaction, totalNsqNum * totalNsqSize)
// 发布消息
for i := 0; i < totalNsqNum * totalNsqSize; i++ {
// 异步:
if err := producer.PublishAsync("test-topic", []byte(fmt.Sprintf("test %d", i)), doneChan, "test"); err != nil {
log.Fatal("publish error: " + err.Error())
}
//time.Sleep(1 * time.Second)
}
for i := 0; i < totalNsqNum * totalNsqSize; i++ {
trans := <- doneChan
if trans.Error != nil {
t.Fatalf(trans.Error.Error())
}
if trans.Args[0].(string) != "test" {
t.Fatalf(`proxied arg "%s" != "test"`, trans.Args[0].(string))
}
}
log.Printf("send finish")
}
广播:同步调用(压测代码)
var totalNsqNum = 1
var totalNsqSize = 10000
func TestPub(t *testing.T) {
cfg := nsq.NewConfig()
// 连接 nsqd 的 tcp 连接
//producer, err := nsq.NewProducer("127.0.0.1:4150", cfg)
producer, err := nsq.NewProducer("127.0.0.1:32776", cfg)
if err != nil {
log.Fatal(err)
}
// 发布消息
for i := 0; i < totalNsqNum * totalNsqSize; i++ {
if err := producer.Publish("test-topic", []byte(fmt.Sprintf("test %d", i))); err != nil {
log.Fatal("publish error: " + err.Error())
}
//time.Sleep(1 * time.Second)
}
log.Printf("send finish")
}
var pressNsqCount1 = 0
var pressNsqCount2 = 0
// 消费者1
func TestSub1(t *testing.T) {
cfg := nsq.NewConfig()
consumer, err := nsq.NewConsumer("test-topic", "channel0", cfg)
if err != nil {
log.Fatal(err)
}
// 处理信息
consumer.AddHandler(nsq.HandlerFunc(func(message *nsq.Message) error {
pressNsqCount1++
if pressNsqCount1%((totalNsqNum*totalNsqSize)/100) == 0 {
log.Printf("[consumer] received msg (%v) ratio: %s", string(message.Body), util.ToString((pressNsqCount1*100)/(totalNsqNum*totalNsqSize)))
}
return nil
}))
// 连接 nsqd 的 tcp 连接
//if err := consumer.ConnectToNSQD("127.0.0.1:4150"); err != nil {
if err := consumer.ConnectToNSQD("127.0.0.1:32776"); err != nil {
log.Fatal(err)
}
<-consumer.StopChan
}
// 消费者2:与TestSub1的代码差不多都相同,只有channel不同
func TestSub2(t *testing.T) {
// ... 省略 ...
consumer, err := nsq.NewConsumer("test-topic", "channel1", cfg)
// ... 省略 ...
}
广播:异步调用(压测代码)
同负载均衡:异步调用(压测代码)
所有Http的api
- /ping - liveness
- /info - version
- /stats - comprehensive runtime telemetry
- /pub - publish a message to a topic
- /mpub - publish multiple messages to a topic
- /config - configure nsqd
- /debug/pprof - pprof debugging portal
- /debug/pprof/profile - generate pprof CPU profile
- /debug/pprof/goroutine - generate pprof goroutine profile
- /debug/pprof/heap - generate pprof heap profile
- /debug/pprof/block - generate pprof blocking profile
- /debug/pprof/threadcreate - generate pprof OS thread profile
v1 namespace (as of nsqd v0.2.29+):
- /topic/create - create a new topic
- /topic/delete - delete a topic
- /topic/empty - empty a topic
- /topic/pause - pause message flow for a topic
- /topic/unpause - unpause message flow for a topic
- /channel/create - create a new channel
- /channel/delete - delete a channel
- /channel/empty - empty a channel
- /channel/pause - pause message flow for a channel
- /channel/unpause - unpause message flow for a channel
NATS
架构简单:非对称集群架构,无中心节点。在吞吐量方面与业内的消息中间件比较
如果有人想要真正接受拥有一个简单但性能超强的系统而不需要额外的维护开销的想法呢?如果有人想要进行传统的发布/订阅,但同时也希望进行请求/回复,甚至可能是分散收集,同时又要保持简单明了,该怎么办?
产品区别介绍
nats消息中心有三款产品,都是消息的,nats,nats-stream,nats jetstream
- nats:第一代基于最多交付一次模型设计的系统
- nats-stream:nats不支持持久化等功能,为了应对这里提供了nats-stream系统
- jetstrem:nats升级到2.0后,nats-stream的适应不再合适,因此将nats-stream升级改造到了2.0版,并将nats-strema后续弃用,官方建议使用jetstream
我们这里对nats和jetstream都进行调研和实践下,nats-stream因为2023年6月弃用,就不再调研和实践
nats core
安装
docker pull nats:latest
运行
docker run -p 4222:4222 -ti nats:latest
NATS的单机可以达到一千万的mps(每秒处理的消息数)
NATS功能
go客户端使用
go get github.com/nats-io/nats.go
1. 发布订阅模型
// 发布订阅模型:订阅1
func TestNatsSub11(t *testing.T) {
// Connect to a server
nc, _ := nats.Connect(nats.DefaultURL)
// Simple Async Subscriber
_, err := nc.Subscribe("foo", func(m *nats.Msg) {
fmt.Printf("Received a message: %s\n", string(m.Data))
})
if err != nil {
return
}
time.Sleep(100000 * time.Second)
nc.Close()
}
// 发布订阅模型:订阅2
func TestNatsSub12(t *testing.T) {
// Connect to a server
nc, _ := nats.Connect(nats.DefaultURL)
// Simple Async Subscriber
_, err := nc.Subscribe("foo", func(m *nats.Msg) {
fmt.Printf("Received a message: %s\n", string(m.Data))
})
if err != nil {
return
}
time.Sleep(100000 * time.Second)
nc.Close()
}
// 发布订阅模型:发布
func TestNatsPub1(t *testing.T) {
// Connect to a server
nc, _ := nats.Connect(nats.DefaultURL)
// Simple Publisher
err := nc.Publish("foo", []byte("Hello World"))
if err != nil {
return
}
nc.Close()
}
其中订阅api也可以使用通道
func TestNatsSub31(t *testing.T) {
nc, _ := nats.Connect(nats.DefaultURL)
// 通道订阅
ch := make(chan *nats.Msg, 64)
nc.ChanSubscribe("send3", ch)
msg := <- ch
fmt.Println(string(msg.Data))
// Close connection
nc.Close()
}
2. 请求响应模型:RPC
其中走哪个这里用到了随机的调用,也就是随机的负载均衡策略
// 请求响应模型:请求
func TestNatsPub2(t *testing.T) {
nc, _ := nats.Connect(nats.DefaultURL)
// 发出请求,并获取响应
msg, err := nc.Request("request", []byte("help me"), 10*time.Millisecond)
if err != nil {
// 如果超时,则这里返回
return
}
fmt.Println(string(msg.Data))
nc.Close()
}
// 请求响应模型:响应1
func TestNatsSub21(t *testing.T) {
nc, _ := nats.Connect(nats.DefaultURL)
// 接收到请求,并返回响应
_, err := nc.Subscribe("request", func(m *nats.Msg) {
m.Respond([]byte("answer is 111"))
})
if err != nil {
return
}
time.Sleep(100000 * time.Second)
nc.Close()
}
// 请求响应模型:响应2
func TestNatsSub22(t *testing.T) {
nc, _ := nats.Connect(nats.DefaultURL)
// 接收到请求,并返回响应
_, err := nc.Subscribe("request", func(m *nats.Msg) {
m.Respond([]byte("answer is 000"))
})
if err != nil {
return
}
time.Sleep(100000 * time.Second)
nc.Close()
}
有了请求响应模型后,其实整个服务就可以如下这种部署
3. 主题层次结构
// 消息的层级结构,通过subj进行处理,支持*和>
// * 匹配单个
// type.*.tag 匹配 type.key1.tag、type.key2.tag等
// type.* 匹配 type.key1、type.key2等,但是不匹配 type.key1.tag
// > 匹配多个
// type.> 匹配 type.key1.tag、type.key1.tag、type.key2.tag也匹配type.key1、type.key2等
func TestNatsPub4(t *testing.T) {
nc, _ := nats.Connect(nats.DefaultURL)
// 发出请求,并获取响应
err := nc.Publish("type.key.tag", []byte("help me"))
if err != nil {
return
}
nc.Close()
}
func TestNatsSub41(t *testing.T) {
nc, _ := nats.Connect(nats.DefaultURL)
// 通道订阅
ch := make(chan *nats.Msg, 64)
nc.ChanSubscribe("type.key.tag", ch)
msg := <- ch
fmt.Println(string(msg.Data))
// Close connection
nc.Close()
}
func TestNatsSub42(t *testing.T) {
nc, _ := nats.Connect(nats.DefaultURL)
// 通道订阅
ch := make(chan *nats.Msg, 64)
nc.ChanSubscribe("type.*.tag", ch)
msg := <- ch
fmt.Println(string(msg.Data))
// Close connection
nc.Close()
}
4. 持久化
消息发送后持久化存储,nats不支持
5. 可靠性
????
nats-stream
该项目在2023年6月后官网就不再维护,因此我们不再对该项目做实践,网上有不少相关文章
jetsteam
jetstream 分布式安全、多租户和水平扩展能力。以下是一个jetstream的应用架构示意图
说明:
- 所有的主题都位于同一个流中
- 消费者只能消费流中消息的子集,且拥有自己的消费模式
- 支持多种消息确认模式
安装
docker pull nats:latest
运行:单机模式
这里运行单机模式,-js 就是jetstream模式
docker run -d --name jetstream -p 4222:4222 -ti nats:latest -js
可以看到默认的jetstream配置
- 最大内存:2.01G
- 最大存储:31.89G
- 持久化文件位置:/tmp/nats/jetstream
[1] 2022/01/04 07:27:14.441992 [INF] Starting nats-server
[1] 2022/01/04 07:27:14.442241 [INF] Version: 2.6.6
[1] 2022/01/04 07:27:14.442274 [INF] Git: [878afad]
[1] 2022/01/04 07:27:14.442375 [INF] Name: NB4NCPMQMOOO4W55VKPMBIONAPVE2OHGOIHEBKOX3R56GTAZFOKDBC65
[1] 2022/01/04 07:27:14.442408 [INF] Node: 3q53sKDc
[1] 2022/01/04 07:27:14.442504 [INF] ID: NB4NCPMQMOOO4W55VKPMBIONAPVE2OHGOIHEBKOX3R56GTAZFOKDBC65
[1] 2022/01/04 07:27:14.443942 [INF] Starting JetStream
[1] 2022/01/04 07:27:14.445360 [INF] _ ___ _____ ___ _____ ___ ___ _ __ __
[1] 2022/01/04 07:27:14.445386 [INF] _ | | __|_ _/ __|_ _| _ \ __| /_\ | \/ |
[1] 2022/01/04 07:27:14.445389 [INF] | || | _| | | \__ \ | | | / _| / _ \| |\/| |
[1] 2022/01/04 07:27:14.445391 [INF] \__/|___| |_| |___/ |_| |_|_\___/_/ \_\_| |_|
[1] 2022/01/04 07:27:14.445393 [INF]
[1] 2022/01/04 07:27:14.445394 [INF] https://docs.nats.io/jetstream
[1] 2022/01/04 07:27:14.445396 [INF]
[1] 2022/01/04 07:27:14.445398 [INF] ---------------- JETSTREAM ----------------
[1] 2022/01/04 07:27:14.445403 [INF] Max Memory: 2.01 GB
[1] 2022/01/04 07:27:14.445530 [INF] Max Storage: 31.89 GB
[1] 2022/01/04 07:27:14.445534 [INF] Store Directory: "/tmp/nats/jetstream"
[1] 2022/01/04 07:27:14.445536 [INF] -------------------------------------------
[1] 2022/01/04 07:27:14.446129 [INF] Listening for client connections on 0.0.0.0:4222
[1] 2022/01/04 07:27:14.446522 [INF] Server is ready
如果想修改内存和持久化文件位置的话,这样即可
./jetstream.conf
jetstream = {
// jetstream数据存放位置:/data/nats-server/jetstream
// 然后挂载主机外部位置为:./persistent-data/server-nx
store_dir: "/data/nats-server/"
// 1GB
max_memory_store: 1073741824
// 10GB
max_file_store: 10737418240
}
docker run -d
-v /Users/zhouzhenyong/file/learn/nats-single:/config
-v /Users/zhouzhenyong/file/learn/nats-single/data:/data/nats-server/jetstream
–name jetstream
-p 4222:4222
-ti nats:latest
-js
–config /config/jetstream.conf
–server_name js1
提示:
–config: 是容器内路径的配置文件,因此需要先将文件路径映射
–server_name:jetstream如果配置了配置文件,则该值必填
启动后就是按照自己的想法
[1] 2022/01/04 08:05:39.692553 [INF] Starting nats-server
[1] 2022/01/04 08:05:39.692661 [INF] Version: 2.6.6
[1] 2022/01/04 08:05:39.692666 [INF] Git: [878afad]
[1] 2022/01/04 08:05:39.692669 [INF] Name: js1
[1] 2022/01/04 08:05:39.692728 [INF] Node: XJAGookQ
[1] 2022/01/04 08:05:39.692770 [INF] ID: NDADV475NJR3TBWNURNIZ5EKJYPHPG5UPKHLV5K4NV6ZN3JJSFVANA6F
[1] 2022/01/04 08:05:39.692801 [INF] Using configuration file: /config/jetstream.conf
[1] 2022/01/04 08:05:39.695574 [INF] Starting JetStream
[1] 2022/01/04 08:05:39.695958 [INF] _ ___ _____ ___ _____ ___ ___ _ __ __
[1] 2022/01/04 08:05:39.695990 [INF] _ | | __|_ _/ __|_ _| _ \ __| /_\ | \/ |
[1] 2022/01/04 08:05:39.695994 [INF] | || | _| | | \__ \ | | | / _| / _ \| |\/| |
[1] 2022/01/04 08:05:39.695997 [INF] \__/|___| |_| |___/ |_| |_|_\___/_/ \_\_| |_|
[1] 2022/01/04 08:05:39.695999 [INF]
[1] 2022/01/04 08:05:39.696002 [INF] https://docs.nats.io/jetstream
[1] 2022/01/04 08:05:39.696005 [INF]
[1] 2022/01/04 08:05:39.696007 [INF] ---------------- JETSTREAM ----------------
[1] 2022/01/04 08:05:39.696016 [INF] Max Memory: 1.00 GB
[1] 2022/01/04 08:05:39.696071 [INF] Max Storage: 10.00 GB
[1] 2022/01/04 08:05:39.696083 [INF] Store Directory: "/data/nats-server/jetstream"
[1] 2022/01/04 08:05:39.696087 [INF] -------------------------------------------
[1] 2022/01/04 08:05:39.696739 [INF] Listening for client connections on 0.0.0.0:4222
[1] 2022/01/04 08:05:39.697089 [INF] Server is ready
运行:集群模式
我们这里运行jetstream集群,节点3个
配置文件:jetstream.config
debug: true
trace: false
# Each server can connect to clients on the internal port 4222
# (mapped to external ports in our docker-compose)
port: 4222
# Persistent JetStream data store
jetstream = {
// jetstream数据存放位置:/data/nats-server/jetstream
// 然后挂载主机外部位置为:./persistent-data/server-nx
store_dir: "/data/nats-server/"
// 1GB
max_memory_store: 1073741824
// 10GB
max_file_store: 10737418240
}
# Cluster formation
cluster = {
name: "JSC"
listen: "0.0.0.0:4245"
# Servers can connect to one another at
# the following routes
routes = [
"nats://n1:4245"
"nats://n2:4245"
"nats://n3:4245"
]
}
docker compose 文件
version: '3'
services:
n1:
container_name: n1
image: synadia/jsm:nightly
entrypoint: /nats-server
command: "--config /config/jetstream.conf --server_name S1"
networks:
- nats
ports:
- 4222:4222
volumes:
- ./config:/config
- ./persistent-data/server-n1/:/data/nats-server/jetstream
n2:
container_name: n2
image: synadia/jsm:nightly
entrypoint: /nats-server
command: "--config /config/jetstream.conf --server_name S2"
networks:
- nats
ports:
- 4223:4222
volumes:
- ./config:/config
- ./persistent-data/server-n2/:/data/nats-server/jetstream
n3:
container_name: n3
image: synadia/jsm:nightly
entrypoint: /nats-server
command: "--config /config/jetstream.conf --server_name S3"
networks:
- nats
ports:
- 4224:4222
volumes:
- ./config:/config
- ./persistent-data/server-n3/:/data/nats-server/jetstream
networks:
nats: {}
go客户端使用
go get github.com/nats-io/nats.go
1. 任一消费者消费:负载均衡
一个生产者,两个消费者。生产者发出的消息只要任一消费者消费一次即可。如果有多个同时,则采用轮询的负载均衡
package test
import (
"context"
"github.com/simonalong/gole/util"
"log"
"math"
"testing"
"time"
"github.com/nats-io/nats.go"
uuid "github.com/satori/go.uuid"
)
var fetchStreamName = "fetchStream"
var fetchSubjectAll = "fetchSubject.*"
var fetchSubject = "fetchSubject.key1"
func TestProducer1(t *testing.T) {
nc, _ := nats.Connect("localhost:4222")
js, _ := nc.JetStream()
ctx, cancel := context.WithTimeout(context.Background(), 1000*time.Second)
defer cancel()
info, err := js.StreamInfo(fetchStreamName)
if nil == info {
_, err = js.AddStream(&nats.StreamConfig{
Name: fetchStreamName,
Subjects: []string{fetchSubjectAll},
Retention: nats.WorkQueuePolicy,
Replicas: 1,
Discard: nats.DiscardOld,
Duplicates: 30 * time.Second,
}, nats.Context(ctx))
if err != nil {
log.Fatalf("can't add: %v", err)
}
}
results := make(chan int64)
var totalTime int64
var totalMessages int64
go func() {
i := 0
for {
js.Publish(fetchSubject, []byte("message=="+util.ToString(i)), nats.Context(ctx))
log.Printf("[publisher] sent %d", i)
time.Sleep(1 * time.Second)
i++
}
}()
for {
select {
case <-ctx.Done():
cancel()
log.Printf("sent %d messages with average time of %f", totalMessages, math.Round(float64(totalTime/totalMessages)))
js.DeleteStream(fetchStreamName)
return
case usec := <-results:
totalTime += usec
totalMessages++
}
}
}
func TestConsumer1(t *testing.T) {
ctx, _ := context.WithTimeout(context.Background(), 1000*time.Second)
id := uuid.NewV4().String()
nc, _ := nats.Connect("localhost:4222", nats.Name(id))
js, _ := nc.JetStream()
sub, _ := js.PullSubscribe(fetchSubject, "group")
for {
msgs, _ := sub.Fetch(1, nats.Context(ctx))
msg := msgs[0]
log.Printf("[consumer: %s] received msg (%v)", id, string(msg.Data))
msg.Ack(nats.Context(ctx))
}
}
func TestConsumer2(t *testing.T) {
ctx, _ := context.WithTimeout(context.Background(), 1000*time.Second)
id := uuid.NewV4().String()
nc, _ := nats.Connect("localhost:4222", nats.Name(id))
js, _ := nc.JetStream()
sub, _ := js.PullSubscribe(fetchSubject, "group")
for {
msgs, _ := sub.Fetch(1, nats.Context(ctx))
msg := msgs[0]
log.Printf("[consumer: %s] received msg (%v)", id, string(msg.Data))
msg.Ack(nats.Context(ctx))
}
}
发送方
=== RUN TestProducer1
2022/01/07 18:31:01 [publisher] sent 0
2022/01/07 18:31:02 [publisher] sent 1
2022/01/07 18:31:03 [publisher] sent 2
2022/01/07 18:31:04 [publisher] sent 3
2022/01/07 18:31:05 [publisher] sent 4
2022/01/07 18:31:06 [publisher] sent 5
2022/01/07 18:31:07 [publisher] sent 6
消费者1
=== RUN TestConsumer1
[consumer: bafbba98-2501-4ef6-b234-75511bfef305] received msg (message==0)
[consumer: bafbba98-2501-4ef6-b234-75511bfef305] received msg (message==2)
[consumer: bafbba98-2501-4ef6-b234-75511bfef305] received msg (message==4)
[consumer: bafbba98-2501-4ef6-b234-75511bfef305] received msg (message==6)
消费者2
=== RUN TestConsumer2
[consumer: 9680b494-fc05-414a-bc91-06e3ec0e85ba] received msg (message==1)
[consumer: 9680b494-fc05-414a-bc91-06e3ec0e85ba] received msg (message==3)
[consumer: 9680b494-fc05-414a-bc91-06e3ec0e85ba] received msg (message==5)
2. 持久化
而且消息支持持久化,即使消息中心宕机,在消费者没有消费情况下,后续重启起来,消费者启动会将之前没有消费的继续消费
3. 广播
var index = "-index1"
var broadcastStreamName = "broadcastStreamName" + index
var broadcastSubjectAll = "broadcast.subject" + index + ".*"
var broadcastSubject = "broadcast.subject" + index + ".key"
func TestDemoSend2(t *testing.T) {
nc, _ := nats.Connect("localhost:4222")
js, _ := nc.JetStream()
nctx := nats.Context(context.Background())
info, _ := js.StreamInfo(broadcastStreamName)
if nil == info {
_, _ = js.AddStream(&nats.StreamConfig{
Name: broadcastStreamName,
Subjects: []string{broadcastSubjectAll},
}, nctx)
}
tctx, cancel := context.WithTimeout(nctx, 10000*time.Second)
deadlineCtx := nats.Context(tctx)
results := make(chan int64)
var totalTime int64
var totalMessages int64
go func() {
i := 0
for {
js.Publish(broadcastSubject, []byte("data "+util.ToString(i)), deadlineCtx)
time.Sleep(1 * time.Second)
i++
}
}()
for {
select {
case <-deadlineCtx.Done():
cancel()
log.Infof("sent %d messages with average time of %f", totalMessages, math.Round(float64(totalTime/totalMessages)))
js.DeleteStream(broadcastStreamName)
return
case usec := <-results:
totalTime += usec
totalMessages++
}
}
}
func TestDemoConsumer12(t *testing.T) {
ctx2, _ := context.WithTimeout(context.Background(), 1000*time.Second)
ctx := nats.Context(ctx2)
id := uuid.NewV4().String()
nc, _ := nats.Connect("localhost:4222", nats.Name(id))
js, _ := nc.JetStream()
sub, _ := js.QueueSubscribeSync(broadcastSubject, "myqueuegroup", nats.Durable(id), nats.DeliverNew())
for {
msg, err := sub.NextMsgWithContext(ctx)
if nil != err {
log.Printf("err sub4 %v", err.Error())
time.Sleep(1 * time.Second)
continue
}
log.Printf("[consumer sub4: %s] received msg (%v)", id, string(msg.Data))
msg.Ack(ctx)
}
}
func TestDemoConsumer13(t *testing.T) {
ctx2, _ := context.WithTimeout(context.Background(), 1000*time.Second)
ctx := nats.Context(ctx2)
id := uuid.NewV4().String()
nc, _ := nats.Connect("localhost:4222", nats.Name(id))
js, _ := nc.JetStream()
sub, _ := js.QueueSubscribeSync(broadcastSubject, "myqueuegroup", nats.Durable(id), nats.DeliverNew())
for {
msg, err := sub.NextMsgWithContext(ctx)
if nil != err {
log.Printf("err sub4 %v", err.Error())
time.Sleep(1 * time.Second)
continue
}
log.Printf("[consumer sub4: %s] received msg (%v)", id, string(msg.Data))
msg.Ack(ctx)
}
}
数据发送
2022/01/10 14:30:49 [Info] nats_jestream_test.go:122 [publisher] sent 0
2022/01/10 14:30:50 [Info] nats_jestream_test.go:122 [publisher] sent 1
2022/01/10 14:30:51 [Info] nats_jestream_test.go:122 [publisher] sent 2
2022/01/10 14:30:52 [Info] nats_jestream_test.go:122 [publisher] sent 3
2022/01/10 14:30:53 [Info] nats_jestream_test.go:122 [publisher] sent 4
2022/01/10 14:30:54 [Info] nats_jestream_test.go:122 [publisher] sent 5
2022/01/10 14:30:55 [Info] nats_jestream_test.go:122 [publisher] sent 6
消费者1接收
=== RUN TestSubDemo1
[consumer: ad36771b-4bb7-44bc-a6dc-f9d6659c0c8f] received msg (message==0)
[consumer: ad36771b-4bb7-44bc-a6dc-f9d6659c0c8f] received msg (message==1)
[consumer: ad36771b-4bb7-44bc-a6dc-f9d6659c0c8f] received msg (message==2)
[consumer: ad36771b-4bb7-44bc-a6dc-f9d6659c0c8f] received msg (message==3)
[consumer: ad36771b-4bb7-44bc-a6dc-f9d6659c0c8f] received msg (message==4)
[consumer: ad36771b-4bb7-44bc-a6dc-f9d6659c0c8f] received msg (message==5)
[consumer: ad36771b-4bb7-44bc-a6dc-f9d6659c0c8f] received msg (message==6)
消费者2接收
=== RUN TestSubDemo2
[consumer: 6d9a2e1d-f412-4e29-a0d5-b0a65ab4623f] received msg (message==0)
[consumer: 6d9a2e1d-f412-4e29-a0d5-b0a65ab4623f] received msg (message==1)
[consumer: 6d9a2e1d-f412-4e29-a0d5-b0a65ab4623f] received msg (message==2)
[consumer: 6d9a2e1d-f412-4e29-a0d5-b0a65ab4623f] received msg (message==3)
[consumer: 6d9a2e1d-f412-4e29-a0d5-b0a65ab4623f] received msg (message==4)
[consumer: 6d9a2e1d-f412-4e29-a0d5-b0a65ab4623f] received msg (message==5)
[consumer: 6d9a2e1d-f412-4e29-a0d5-b0a65ab4623f] received msg (message==6)
5. kv操作(仿redis功能)
put
get
delete
purge
create
update
keys
watch
func GetBucket(js nats.JetStreamContext, name string, ttl time.Duration) nats.KeyValue {
if kv, _ := js.KeyValue(name);nil != kv {
return kv
}
kv, _ := js.CreateKeyValue(&nats.KeyValueConfig{
// 桶名字
Bucket: name,
// 保存key的实效性
TTL: ttl,
})
return kv
}
// 这里仅列出一部分
func TestKv1(t *testing.T) {
nc, _ := nats.Connect("localhost:4222")
js, _ := nc.JetStream()
kv := GetBucket(js, "bucket", 3 * time.Second)
// put
kv.Put("key.k1", []byte("value12"))
// delete
kv.Delete("key.k1")
// purge,跟delete一样,但是会清理的更彻底
kv.Purge("key.k1")
// 更新
kv.Update("key.k1", []byte("value_chg"), 0)
// get
data, _ := kv.Get("key.k1")
if nil != data {
fmt.Println(string(data.Value()))
} else {
fmt.Println("null")
}
// keys
keys1, _ := kv.Keys()
// watchs
keyWatcher, _ := kv.WatchAll()
for {
select {
case kvs := <-keyWatcher.Updates():
if nil != kvs {
fmt.Println(string(kvs.Key()) + " = " + string(kvs.Value()))
}
}
}
}
单机jetstream压测
本机硬件指标
- 处理器:2.8GHz四核 i7处理器
- 内存:16G
- 观察工具(可能有小伙伴想知道):lazydocker(懒人docker,帮你把docker stats信息快捷展示的工具)
压测
分别进行负载均衡和广播的压测,查看对应的内存和cpu占用率,采用同步和异步的两种方式分别进行查看
- 1万
- 10万
- 100万
- 1000万:时间太长了,本机就不跑了,大家可以根据上面的例子改造进行跑下试下
压测场景:一个生产者和两个消费者
1万 | 10万 | 100万 | |||
---|---|---|---|---|---|
负载均衡 | 同步调用 | 内存 | 54.49MB | 70.32MB | 75.79MB |
cpu:最高 | 57.19% | 94.82% | 88.39% | ||
cpu:平均 | 45.76% | 76.58% | 76.01% | ||
异步调用 | 内存 | 58.36MB | 75.78MB | 80.09MB | |
cpu:最高 | 84.63% | 95.90% | 99.59% | ||
cpu:平均 | 76.17% | 76.73% | 83.26% | ||
广播 | 同步调用 | 内存 | 63.96MB | 82.27MB | 102.2MB |
cpu:最高 | 106% | 121% | 133% | ||
cpu:平均 | 95% | 109% | 116% | ||
异步调用 | 内存 | 96.18MB | 120.9MB | 99.28MB | |
cpu:最高 | 125% | 132% | 183% | ||
cpu:平均 | 112% | 119% | 135% |
负载均衡:同步压测代码
package test
import (
"context"
"github.com/simonalong/gole/util"
"log"
"testing"
"time"
"github.com/nats-io/nats.go"
uuid "github.com/satori/go.uuid"
)
var fetchPressStreamName = "fetchPressStreamName"
var fetchPressSubjectAll = "fetch.press.subject.*"
var fetchPressSubject = "fetch.press.subject.key1"
var natsConnect *nats.Conn
var totalNum = 1
var totalSize = 10000
func TestPressProducer1(t *testing.T) {
for i := 0; i < totalNum; i++ {
sendMsg()
}
time.Sleep(1000000 * time.Hour)
}
func sendMsg() {
if nil == natsConnect {
nc, _ := nats.Connect("localhost:4222")
natsConnect = nc
}
js, _ := natsConnect.JetStream()
ctx := context.Background()
info, err := js.StreamInfo(fetchPressStreamName)
if nil == info {
_, err = js.AddStream(&nats.StreamConfig{
Name: fetchPressStreamName,
Subjects: []string{fetchPressSubjectAll},
Retention: nats.WorkQueuePolicy,
Replicas: 1,
Discard: nats.DiscardOld,
Duplicates: 30 * time.Second,
}, nats.Context(ctx))
if err != nil {
log.Fatalf("can't add: %v", err)
}
}
go func() {
for i := 0; i < totalSize; i++ {
js.Publish(fetchPressSubject, []byte("message=="+util.ToString(i)), nats.Context(ctx))
//log.Printf("[publisher] sent %d", i)
//time.Sleep(1 * time.Second)
}
log.Printf("send finish")
}()
}
var pressCount1 = 0
var pressCount2 = 0
func TestPressConsumer1(t *testing.T) {
id := uuid.NewV4().String()
nc, _ := nats.Connect("localhost:4222", nats.Name(id))
js, _ := nc.JetStream()
sub, _ := js.PullSubscribe(fetchPressSubject, "group")
for {
msgs, err := sub.Fetch(1)
if nil != err {
log.Printf("err %v", err.Error())
time.Sleep(1 * time.Second)
continue
}
msg := msgs[0]
pressCount1++
if pressCount1%((totalNum * totalSize)/100) == 0 {
log.Printf("[consumer: %s] received msg (%v) ratio: %s", id, string(msg.Data), util.ToString((pressCount1*100)/(totalNum * totalSize)))
}
msg.Ack()
}
}
func TestPressConsumer2(t *testing.T) {
id := uuid.NewV4().String()
nc, _ := nats.Connect("localhost:4222", nats.Name(id))
js, _ := nc.JetStream()
sub, _ := js.PullSubscribe(fetchPressSubject, "group")
for {
msgs, err := sub.Fetch(1)
if nil != err {
log.Printf("err %v", err.Error())
time.Sleep(1 * time.Second)
continue
}
msg := msgs[0]
pressCount2++
if pressCount2%((totalNum * totalSize)/100) == 0 {
log.Printf("[consumer: %s] received msg (%v) ratio: %s", id, string(msg.Data), util.ToString((pressCount2*100)/(totalNum * totalSize)))
}
msg.Ack()
}
}
负载均衡:异步压测代码
func sendMsg() {
if nil == natsConnect {
nc, _ := nats.Connect("localhost:4222")
natsConnect = nc
}
js, _ := natsConnect.JetStream()
ctx := context.Background()
info, err := js.StreamInfo(fetchPressStreamName)
if nil == info {
_, err = js.AddStream(&nats.StreamConfig{
Name: fetchPressStreamName,
Subjects: []string{fetchPressSubjectAll},
Retention: nats.WorkQueuePolicy,
Replicas: 1,
Discard: nats.DiscardOld,
Duplicates: 30 * time.Second,
}, nats.Context(ctx))
if err != nil {
log.Fatalf("can't add: %v", err)
}
}
go func() {
for i := 0; i < totalSize; i++ {
// 只是这里变化了
js.PublishAsync(fetchPressSubject, []byte("message=="+util.ToString(i)))
//log.Printf("[publisher] sent %d", i)
//time.Sleep(1 * time.Second)
}
log.Printf("send finish")
}()
}
广播:同步压测代码
package test
import (
"context"
"github.com/lunny/log"
"github.com/nats-io/nats.go"
uuid "github.com/satori/go.uuid"
"github.com/simonalong/gole/util"
"testing"
"time"
)
var indexPress = "-index1"
var broadcastPressStreamName = "broadcastStreamName" + indexPress
var broadcastPressSubjectAll = "broadcast.subject" + indexPress + ".*"
var broadcastPressSubject = "broadcast.subject" + indexPress + ".key"
var totalBNum = 100
var totalBSize = 10000
func TestBroadcastPressProducer1(t *testing.T) {
for i := 0; i < totalBNum; i++ {
sendBMsg()
}
time.Sleep(1000000 * time.Hour)
}
func sendBMsg() {
if nil == natsConnect {
nc, _ := nats.Connect("localhost:4222")
natsConnect = nc
}
js, _ := natsConnect.JetStream()
ctx := context.Background()
info, err := js.StreamInfo(broadcastPressStreamName)
if nil == info {
_, err = js.AddStream(&nats.StreamConfig{
Name: broadcastPressStreamName,
Subjects: []string{broadcastPressSubjectAll},
}, nats.Context(ctx))
if err != nil {
log.Fatalf("can't add: %v", err)
}
}
go func() {
for i := 0; i < totalBSize; i++ {
// 同步:
js.Publish(broadcastPressSubject, []byte("message=="+util.ToString(i)), nats.Context(ctx))
}
log.Printf("send finish")
}()
}
var pressBCount1 = 0
var pressBCount2 = 0
func TestDemoPressConsumer12(t *testing.T) {
id := uuid.NewV4().String()
nc, _ := nats.Connect("localhost:4222", nats.Name(id))
js, _ := nc.JetStream()
sub, _ := js.QueueSubscribeSync(broadcastPressSubject, "myqueuegroup", nats.Durable(id), nats.DeliverNew())
for {
msg, err := sub.NextMsgWithContext(nats.Context(context.Background()))
if nil != err {
log.Printf("err sub4 %v", err.Error())
time.Sleep(1 * time.Second)
continue
}
pressBCount1++
if pressBCount1%((totalBNum*totalBSize)/100) == 0 {
log.Printf("[consumer: %s] received msg (%v) ratio: %s", id, string(msg.Data), util.ToString((pressBCount1*100)/(totalBNum*totalBSize)))
}
msg.Ack()
}
}
func TestDemoPressConsumer13(t *testing.T) {
id := uuid.NewV4().String()
nc, _ := nats.Connect("localhost:4222", nats.Name(id))
js, _ := nc.JetStream()
sub, _ := js.QueueSubscribeSync(broadcastPressSubject, "myqueuegroup", nats.Durable(id), nats.DeliverNew())
for {
msg, err := sub.NextMsgWithContext(nats.Context(context.Background()))
if nil != err {
log.Printf("err sub4 %v", err.Error())
time.Sleep(1 * time.Second)
continue
}
pressBCount2++
if pressBCount2%((totalBNum*totalBSize)/100) == 0 {
log.Printf("[consumer: %s] received msg (%v) ratio: %s", id, string(msg.Data), util.ToString((pressBCount2*100)/(totalBNum*totalBSize)))
}
msg.Ack()
}
}
广播:异步压测代码
func sendBMsg() {
if nil == natsConnect {
nc, _ := nats.Connect("localhost:4222")
natsConnect = nc
}
js, _ := natsConnect.JetStream()
ctx := context.Background()
info, err := js.StreamInfo(broadcastPressStreamName)
if nil == info {
_, err = js.AddStream(&nats.StreamConfig{
Name: broadcastPressStreamName,
Subjects: []string{broadcastPressSubjectAll},
}, nats.Context(ctx))
if err != nil {
log.Fatalf("can't add: %v", err)
}
}
go func() {
for i := 0; i < totalBSize; i++ {
// 同步:
//js.Publish(broadcastPressSubject, []byte("message=="+util.ToString(i)), nats.Context(ctx))
// 只是这里修改了下,变成异步,就可以了
js.PublishAsync(broadcastPressSubject, []byte("message=="+util.ToString(i)))
}
log.Printf("send finish")
}()
}
相关的压测图太多了,这里只贴一个
内存部分是使用docker stats查看具体的值
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
06b757d5b06d jetstream 0.12% 99.25MiB / 2.681GiB 3.62% 3.05GB / 3.08GB 0B / 0B 14
问题
1. 在100万的负载均衡压测中,超时问题
生产端
panic: runtime error: integer divide by zero [recovered]
panic: runtime error: integer divide by zero
goroutine 35 [running]:
testing.tRunner.func1.2({0x12c6680, 0x1558920})
/usr/local/go/src/testing/testing.go:1209 +0x24e
testing.tRunner.func1()
/usr/local/go/src/testing/testing.go:1212 +0x218
panic({0x12c6680, 0x1558920})
/usr/local/go/src/runtime/panic.go:1038 +0x215
go-learn/src/message.TestProducer1(0x0)
/Users/zhouzhenyong/project/go-private/go-learn/src/message/nats_jetstream_loadbalance_test.go:60 +0x578
目前怀疑是这里的nats配置的context超时了,进而中断了,去掉后看了下源码,发现默认是5秒
func TestProducer1(t *testing.T) {
// 省略部分代码。。。这些代码上面有
ctx, cancel := context.WithTimeout(context.Background(), 1000*time.Second)
defer cancel()
// 。。。
for {
select {
case <-ctx.Done():
cancel()
// 这里打印
log.Printf("sent %d messages with average time of %f", totalMessages, math.Round(float64(totalTime/totalMessages)))
js.DeleteStream(fetchStreamName)
return
case usec := <-results:
totalTime += usec
totalMessages++
}
}
}
消费端
func TestConsumer2(t *testing.T) {
ctx, _ := context.WithTimeout(context.Background(), 1000*time.Second)
id := uuid.NewV4().String()
nc, _ := nats.Connect("localhost:4222", nats.Name(id))
js, _ := nc.JetStream()
sub, _ := js.PullSubscribe(fetchSubject, "group")
for {
msgs, err := sub.Fetch(1, nats.Context(ctx))
if nil != err {
// 出现异常,这里打印:nats: timeout
log.Printf("err %v", err.Error())
time.Sleep(1 * time.Second)
continue
}
msg := msgs[0]
count2++
if count2 %1000 == 0 {
log.Printf("[consumer: %s] received msg (%v) ratio: %s", id, string(msg.Data), util.ToString((count2 * 100)/(100 * 10000)))
}
msg.Ack(nats.Context(ctx))
}
}
还有就是nats的客户端这里,我这里也是配置了1000s后超时,在一些情况下也是会超时的,其中如果我们不配置,则默认是5秒超时与发送端一样,nats的源码部分
const (
defaultRequestWait = 5 * time.Second
defaultAccountCheck = 20 * time.Second
)
func (nc *Conn) JetStream(opts ...JSOpt) (JetStreamContext, error) {
js := &js{
nc: nc,
opts: &jsOpts{
pre: defaultAPIPrefix,
// 这里配置了默认5秒
wait: defaultRequestWait,
},
}
// 省略。。。。。
}
func (sub *Subscription) Fetch(batch int, opts ...PullOpt) ([]*Msg, error) {
// 省略。。。。。
ttl := o.ttl
if ttl == 0 {
ttl = js.opts.wait
}
// 省略。。。。。
// 如果我们不配置上下文,那么这里就会使用默认的,默认的这里的ttl就是上面的5秒
if ctx == nil {
// 这里的ttl
ctx, cancel = context.WithTimeout(context.Background(), ttl)
defer cancel()
} else if _, hasDeadline := ctx.Deadline(); !hasDeadline {
// 省略。。。。。。
}
}
解决超时问题
在压测的案例中,将ctx进行去掉,不再使用
func TestProducer1(t *testing.T) {
nc, _ := nats.Connect("localhost:4222")
js, _ := nc.JetStream()
ctx, cancel := context.WithTimeout(context.Background(), 1000*time.Second)
defer cancel()
info, err := js.StreamInfo(fetchStreamName)
if nil == info {
_, err = js.AddStream(&nats.StreamConfig{
Name: fetchStreamName,
Subjects: []string{fetchSubjectAll},
Retention: nats.WorkQueuePolicy,
Replicas: 1,
Discard: nats.DiscardOld,
Duplicates: 30 * time.Second,
}, nats.Context(ctx))
if err != nil {
log.Fatalf("can't add: %v", err)
}
}
results := make(chan int64)
var totalTime int64
var totalMessages int64
go func() {
num := 100
data := 10000
for i := 0; i < num * data; i++ {
// 原先为: js.Publish(fetchSubject, []byte("message=="+util.ToString(i)), nats.Context(ctx))
js.PublishAsync(fetchSubject, []byte("message=="+util.ToString(i)))
i++
}
log.Printf("send finish")
}()
}
参考:
https://www.jianshu.com/p/d62c1e2c01ba?ivk_sa=1024320u
https://www.yuque.com/simonalong/jishu/gtgy06#nZROL
https://www.jianshu.com/p/341082dadd3e
https://blog.huoding.com/2021/05/21/907
https://cn.wbsnail.com/p/using-cloudevents-with-nats
http://ljchen.net/2019/03/17/NATS-Streaming%E5%AE%9E%E8%B7%B5/
nats-stream的信息
https://hub.docker.com/_/nats-streaming
nats-stream用法
https://www.cnblogs.com/zeppelin/p/7261033.html
https://www.jianshu.com/p/27a49b9d4306
nats、nats-stream、jetsteam
https://gcoolinfo.medium.com/comparing-nats-nats-streaming-and-nats-jetstream-ec2d9f426dc8
jetstream运行
http://thinkmicroservices.com/blog/2021/jetstream/nats-jetstream.html
https://stackabuse.com/asynchronous-pubsub-messaging-in-java-with-nats-jetstream/
https://juejin.cn/post/6952792674655010853
https://github.com/nats-io/nats.go/issues/712