NATS Request-Reply 模式

FreeGuideOnline 最新 2026-07-12

NATS Request-Reply 模式全面指南

什么是 Request-Reply 模式

Request-Reply(请求-回复)是分布式系统中实现同步或异步远程过程调用(RPC)的经典消息模式。在 NATS 中,该模式通过点对点的通信语义,让一个客户端可以向某个「主题」(Subject)发送请求,并等待一个或多个响应。

与发布-订阅(Pub-Sub)不同,请求-回复模式天然带有背压超时机制,且响应直接返回给请求方,无需提前建立点对点通道。这使得 NATS 在微服务间命令查询、服务解耦、负载均衡等场景中极为高效。

工作原理

NATS 的请求-回复建立在核心的发布-订阅机制之上,通过一个巧妙的「收件箱」(Inbox)模式实现:

  1. 请求方发送一条消息到目标 Subject(例如 orders.process),并在消息头中附加一个唯一的回复 Subject(通常是 _INBOX.xxx 形式的随机字符串)。
  2. 服务方监听该 Subject,收到请求后处理业务逻辑,并将结果发布到请求中携带的回复 Subject 上。
  3. 请求方在发起请求后,内部创建一个临时订阅来监听这个回复 Subject,一旦收到响应(或达到超时时间),流程结束。
  4. 如果多个服务方监听同一个 Subject,任意一个会收到请求并处理(默认情况下,NATS 会在订阅者之间自动分发消息,实现负载均衡)。

整个过程对开发者屏蔽了回复 Subject 的创建与销毁,使用起来像一次函数调用。

请求方                            NATS Server                       服务方
  |                                 |                                 |
  |--- PUB orders.process           |                                 |
  |    Reply: _INBOX.abc123  ------>|                                 |
  |                                 |--- 分发消息到 orders.process -->|
  |                                 |     订阅者                      |
  |                                 |                                 |
  |                                 |<---- PUB _INBOX.abc123 ---------|
  |<-- 收到响应消息 ----------------|                                 |
  |                                 |                                 |

必备工具:NATS CLI

在深入代码前,我们先用官方命令行工具 nats 体验请求-回复的全过程。你可以从 NATS 官网 安装或使用 Docker 快速启动一个本地 Server:

docker run -p 4222:4222 -ti nats:latest

之后安装 natscli

# macOS
brew tap nats-io/nats-tools
brew install nats-io/nats-tools/nats

# 或直接下载二进制文件

验证连接:

nats server check connection

基础命令行交互

启动一个服务端来响应请求:

nats reply 'greet' 'Hello, {{ . }}!'

这条命令会监听 greet 主题,并将收到的请求体作为模板变量 {{ . }} 填入回复字符串。

在另一个终端窗口发送请求:

nats request 'greet' 'World' --timeout 2s

输出:

16:35:22 Sending request on "greet"
16:35:22 Received on "_INBOX.xxx...": Hello, World!

这就是一个完整的请求-回复交互。命令行工具自动为我们创建了 _INBOX 并管理回复订阅。

编程实战

使用 Go 客户端

首先安装 NATS Go 客户端:

go get github.com/nats-io/nats.go/@latest

服务端代码(request-reply-server.go)

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/nats-io/nats.go"
)

func main() {
	// 连接 NATS
	nc, err := nats.Connect(nats.DefaultURL)
	if err != nil {
		log.Fatal(err)
	}
	defer nc.Close()

	fmt.Println("服务端已启动,监听 subject: orders.process")

	// 订阅请求,使用 Subscribe 或 QueueSubscribe 实现负载均衡
	// QueueSubscribe 可以将多个实例加入同一个队列组,一次只有一个收到请求
	_, err = nc.QueueSubscribe("orders.process", "order-workers", func(msg *nats.Msg) {
		request := string(msg.Data)
		fmt.Printf("收到请求: %s\n", request)

		// 模拟业务处理
		time.Sleep(500 * time.Millisecond)
		response := fmt.Sprintf("订单 %s 处理成功", request)

		// 回复到 msg.Reply 指定的 Subject
		err := msg.Respond([]byte(response))
		if err != nil {
			log.Printf("回复失败: %v", err)
		}
	})
	if err != nil {
		log.Fatal(err)
	}

	// 阻塞等待
	select {}
}

客户端代码(request-reply-client.go)

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/nats-io/nats.go"
)

func main() {
	nc, err := nats.Connect(nats.DefaultURL)
	if err != nil {
		log.Fatal(err)
	}
	defer nc.Close()

	// 发送请求并等待响应,超时设为 2 秒
	msg, err := nc.Request("orders.process", []byte("order-123"), 2*time.Second)
	if err != nil {
		if err == nats.ErrTimeout {
			log.Fatal("请求超时,可能没有服务方在线")
		}
		log.Fatal(err)
	}

	fmt.Printf("收到响应: %s\n", string(msg.Data))
}

运行:

# 终端1 - 启动服务端
go run request-reply-server.go

# 终端2 - 发送请求
go run request-reply-client.go

使用 Python 客户端

安装客户端:

pip install nats-py

服务端代码

import asyncio
import nats
from nats.js import api

async def main():
    nc = await nats.connect("nats://localhost:4222")

    async def order_handler(msg):
        request = msg.data.decode()
        print(f"收到请求: {request}")
        # 模拟处理
        await asyncio.sleep(0.5)
        response = f"订单 {request} 处理成功"
        await msg.respond(response.encode())

    # 使用 queue group 均衡负载
    await nc.subscribe("orders.process", "order-workers", cb=order_handler)
    print("服务端已启动,监听 orders.process...")

    # 保持运行
    while True:
        await asyncio.sleep(1)

if __name__ == '__main__':
    asyncio.run(main())

客户端代码

import asyncio
import nats

async def main():
    nc = await nats.connect("nats://localhost:4222")

    try:
        resp = await nc.request("orders.process", b"order-456", timeout=2)
        print(f"收到响应: {resp.data.decode()}")
    except asyncio.TimeoutError:
        print("请求超时,没有可用服务")

    await nc.close()

if __name__ == '__main__':
    asyncio.run(main())

高级用法与配置

设置请求超时

Request 调用中明确设置超时时间至关重要,可以防止客户端无限阻塞。上例 Go 中传入 2*time.Second,Python 中传入 timeout=2。如果超时,客户端会收到错误,可据此实现重试或降级。

手动管理回复订阅

虽然 nc.Request() 封装了临时订阅的创建与销毁,但在某些场景(例如需要接收多个响应)下,你可以手动控制:

// 创建一个唯一的回复收件箱
inbox := nats.NewInbox()

// 订阅该收件箱
sub, _ := nc.SubscribeSync(inbox)

// 发布请求,并指定回复 Subject
nc.PublishRequest("orders.process", inbox, []byte("order-data"))

// 等待最多 2 秒,获取一条消息
msg, _ := sub.NextMsg(2 * time.Second)

// 处理响应...

这样可以实现更灵活的交互,例如等待多条结果后聚合。

请求的负载均衡

使用 Queue Subscriber(队列订阅者)可以将服务横向扩展。具有相同队列名称的所有订阅者形成一个组,NATS Server 会将每条请求以轮询或公平分配的方式发给组内一个成员。这确保了单条请求不会广播给所有实例。

处理无响应者的情况

当没有任何订阅者监听目标 Subject 时,NATS 客户端会在超时后返回错误。你可以在发送请求前使用 nc.Flush() 确认连接状态,或通过 JetStream 提供的持久化特性缓解,但基本模式本身依赖在线订阅者。

与 Pub-Sub 模式的对比

特性 Request-Reply Publish-Subscribe
消息流向 点对点,请求收到单个响应 一对多,发布后所有订阅者收到
响应方式 自动通过 _INBOX 回复 无内置响应机制,需自行设计 Reply 主题
超时控制 客户端自带超时 无超时概念,完全异步
负载均衡 通过 Queue Group 实现 所有订阅者都会收到消息
典型场景 RPC、查询、命令 事件广播、数据分发、日志收集

最佳实践

  1. 始终设置合理的超时:防止因服务不可用导致客户端线程挂起。
  2. 使用 Queue Subscriber 负载均衡:轻松水平扩展服务处理能力。
  3. 幂等性设计:请求可能因网络抖动超时后重试,确保服务端重复处理同一请求无副作用。
  4. 避免大负载回复:NATS 单条消息默认最大 1 MB。若需返回大量数据,考虑返回资源 URL 或通过对象存储传递。
  5. 在 Kubernetes 等动态环境中动态发现服务:通过统一 Subject 命名约定即可,无需额外注册中心。
  6. 优先使用 NATS 的内置 Request 方法,它已经处理了订阅泄漏和并发安全。
  7. 监控 NATS 连接和错误,尤其是在生产环境,使用 NATS 的监控端点或埋点库。

常见问题

问:如果多个服务实例返回响应怎么办?
默认的 nc.Request() 只读取第一条响应并自动取消后续订阅。如果手动管理订阅,可以读取多条,但通常不符合请求-回复的单次语义。

问:能否设置请求优先级?
NATS Core 本身不提供优先级队列。如需优先级,可以考虑使用不同的 Subject 命名(例如 orders.highorders.normal),或借助 JetStream 的消费者优先级。

问:如何保证请求不丢失?
若服务方在收请求时崩溃,消息会丢失(Core NATS 是不持久化的)。使用 NATS JetStream 可以获得持久化和重试能力,但此时请求-回复模式演变为基于流的请求-回复,需要额外配置。

问:_INBOX 主题会占用大量资源吗?
每个请求会临时创建一个订阅,请求完成后便会取消。客户端会复用连接,开销极低,可处理每秒数十万请求。

总结

NATS 的 Request-Reply 模式通过极简的 API 提供了高性能、可扩展的远程调用能力。它完美诠释了 NATS 的“少即是多”哲学——隐藏 Inbox 细节、自动订阅清理、内置超时,让开发者用几行代码就能构建健壮的微服务通信。无论是简单的命令查询,还是复杂的服务编排,掌握这一模式都能大幅提升你基于 NATS 构建分布式系统的效率。

现在,你可以打开终端,用 nats replynats request 立刻体验,或直接集成到你喜欢的语言中开始构建服务。