go实现的消息中间件调研:nsq、nats和nats-jetstream

NSQNATS
持久化支持,需要配置nats-core 不支持,nats-stream(弃用)/jetstream支持
实时性支持支持
高性能支持支持
低资源消耗支持支持
功能:广播支持支持
可追踪支持不支持
分布式支持支持
功能:负载均衡支持支持
高可用支持支持
可伸缩支持支持
可靠性非高可靠高可靠
幂等性不支持不支持
顺序性不支持支持
集群架构对称集群架构:简单,水平扩展非对称集群架构:需要额外配置
请求响应模型不支持支持
kv时效存储功能不支持jetstream支持
文档丰富网上资料挺多nats资料不少,但是jetstream资料太少
接入简单非常简单不是很简单
官网https://nsq.io/https://nats.io/
源码https://github.com/nsqiohttps://github.com/nats-io

下面详细对两种消息进行调研

NSQ

对称集群架构,有中心节点做调解

  1. 无单点故障问题
  2. 可以直接水平扩展
  3. 低延迟
  4. 提供:负载均衡和广播的消息路由
  5. 支持TLS
  6. 有集群管理界面
  7. 提供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

测试网络

curl http://127.0.0.1:32773/ping

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的外部映射

http://localhost:32812/

可以看到对应消息的消费情况
在这里插入图片描述

持久化查看

可以去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.42MB8.6MB12.23MB
cpu:最高37.34%63%65%
cpu:平均 - 38.8%38.55%
异步调用内存7.3MB7.76MBtimeout问题
cpu:最高117%126%?
cpu:平均 - 113%?
广播同步调用内存6.82MB7.51MB9.28MB
cpu:最高48.28%53.55%102%
cpu:平均43.4643.64%94%
异步调用内存7.219MB7.26MBtimeout问题
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

v1 namespace (as of nsqd v0.2.29+):

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.49MB70.32MB75.79MB
cpu:最高57.19%94.82%88.39%
cpu:平均45.76%76.58%76.01%
异步调用内存58.36MB75.78MB80.09MB
cpu:最高84.63%95.90%99.59%
cpu:平均76.17%76.73%83.26%
广播同步调用内存63.96MB82.27MB102.2MB
cpu:最高106%121%133%
cpu:平均95%109%116%
异步调用内存96.18MB120.9MB99.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

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值