面向 .NET 开发者的 RocketMQ 入门指南(一):整体架构

目录

  • 前言
  • [从 Remoting 说起](#从 Remoting 说起)
    • [Remoting 链路如何工作](#Remoting 链路如何工作)
  • [Remoting 架构的使用成本](#Remoting 架构的使用成本)
  • [RocketMQ 5:Proxy 与 gRPC](#RocketMQ 5:Proxy 与 gRPC)
  • [gRPC 也有自己的边界](#gRPC 也有自己的边界)
  • 两套协议共享的消息核心
    • [1. 获取路由](#1. 获取路由)
    • [2. 发送消息](#2. 发送消息)
    • [3. Broker 存储](#3. Broker 存储)
    • [4. Consumer 获取并处理消息](#4. Consumer 获取并处理消息)
    • [5. 提交处理结果](#5. 提交处理结果)
  • [本系列使用的 .NET SDK](#本系列使用的 .NET SDK)
  • [用 Producer 和 PushConsumer 做一个简单验证](#用 Producer 和 PushConsumer 做一个简单验证)
  • 小结
  • 参考资料

前言

这个系列是写给对 RocketMQ 感兴趣的 .NET 开发者的。我希望从 .NET 应用开发的视角出发,介绍 RocketMQ 的基本概念、整体架构和常见使用方式,帮助读者建立对 RocketMQ 的初步认识。

受个人能力和理解所限,文中难免存在疏漏。如果有描述不严谨或错误之处,欢迎指正。

本文将使用我自己编写的 eventhorizon-cli/EventHorizon.RocketMQ 作为演示客户端。这是一个非官方的 RocketMQ .NET 客户端,同时支持 RocketMQ 5 gRPC 和 classic Remoting。

今天接触 RocketMQ,会同时看到 Remoting 和 gRPC 两套客户端。要理解它们为什么并存,最好从 RocketMQ 早期的 Remoting 架构说起,再看 RocketMQ 5 为什么增加 Proxy 和 gRPC。顺着这条演进路径,NameServer、Broker、Proxy 的职责以及消息实际经过的路径也会清楚很多。

从 Remoting 说起

Remoting 是 RocketMQ 自研的一套基于 TCP 的 RPC 协议,可以简单理解为 RocketMQ 各组件之间约定的通信方式。在经典架构中,Producer、Consumer、NameServer 和 Broker 都通过 Remoting 交换请求与响应:客户端先向 NameServer 查询路由,再直接连接 Broker 收发消息。客户端还承担路由发现、负载均衡和故障处理等工作。

经典 Remoting 架构主要包含四类角色:

  • Producer:创建消息,并将消息发送到 Topic。
  • Consumer:订阅 Topic,从 Broker 获取消息并交给业务代码处理。
  • NameServer:保存 Broker 注册的 Topic 路由,供 Producer 和 Consumer 查询。
  • Broker:接收、存储和投递消息,并保存消费进度等运行数据。

它们之间的关系如下:

图中的虚线表示路由注册和查询,实线表示消息相关的请求。NameServer 不存储消息,也不转发消息。客户端从 NameServer 取得路由后,会直接连接对应的 Broker。

Remoting 链路如何工作

Broker 启动后,会将自身地址、Topic 和队列信息注册到 NameServer,并通过心跳维持路由的有效性。

Producer 发送消息前,先从 NameServer 查询 Topic 路由。路由中包含可写的 MessageQueue 及其所在的 Broker。Producer 选择一个队列,与目标 Broker 建立连接,然后直接发送消息。

Consumer 的过程类似。它先查询订阅 Topic 的路由,再连接对应的 Broker。Consumer Group 中有多个实例时,客户端还要完成队列分配,让各实例共同分担消息。所谓 PushConsumer,也不是 Broker 主动连接应用,而是客户端在后台持续拉取或长轮询,再通过回调把消息交给业务代码。

这套架构有一个鲜明特点:客户端不只是协议调用方,还是 RocketMQ 运行机制的一部分。路由缓存、Broker 连接、心跳、负载均衡、消息缓存、重试、消费位点和异常节点隔离等逻辑,都有相当一部分运行在客户端中。

Remoting 架构的使用成本

这种由客户端承担较多运行逻辑的设计,通常称为"富客户端"。它并不是缺点本身:客户端可以直接访问 Broker,不需要额外的代理层;经过多年演进,Remoting SDK 也积累了较完整的 Producer、Consumer 和 Admin 能力。已有 RocketMQ 4.x 集群或依赖经典消费模型的系统,继续使用 Remoting 很常见。

问题在于,当接入环境从 Java 为主的内网应用扩展到多语言和云原生场景后,这种设计会带来一些成本。

客户端实现复杂

客户端需要理解 NameServer 路由、Broker 拓扑、队列分配、重试和消费进度等内部机制。实现一个能够收发消息的客户端并不难,但要在各种异常和并发场景下保持正确,工作量会明显增加。

RocketMQ 4.x Java SDK 中的顺序消费、广播消费、负载均衡、消息缓存、重试、位点管理、流控和故障转移等能力,都属于这类富客户端逻辑。客户端功能越多,升级和维护的成本也越高。

多语言客户端难以保持一致

Remoting 是 RocketMQ 自己的通信协议,不像 gRPC 那样可以从统一的接口定义生成不同语言的基础代码。每种语言都需要重新实现协议编解码和客户端行为,再分别处理并发模型、异常类型与生命周期。

结果不只是开发成本高,不同语言 SDK 的功能范围和行为也更难长期保持一致。官方文档将 Remoting SDK 描述为与服务端版本同步演进的方案,早期和主流实现主要围绕 Java 体系展开。

应用需要直接访问 Broker

Remoting 客户端从 NameServer 获取 Broker 地址后直接连接 Broker。这意味着应用所在网络必须能够访问 Broker 注册的地址。

在传统内网部署中,这通常不是问题;但在容器、Kubernetes、跨网络或云产品接入场景中,Broker 对外注册地址、端口映射和网络边界都会影响客户端连接。客户端也会感知 Broker 拓扑的变化。

客户端与服务端演进联系较紧

Remoting 同时用于 RocketMQ 内部组件通信和客户端访问,客户端 SDK 也长期跟随主仓库演进。服务端协议增加能力时,各语言 SDK 需要分别跟进,客户端和服务端之间的版本边界不容易完全独立。

这些问题并不意味着 Remoting 不能继续使用,而是说明它更适合最初的设计环境。RocketMQ 5 引入 gRPC,并不是简单地把传输协议换掉,而是重新划分客户端、接入层和 Broker 的职责。

RocketMQ 5:Proxy 与 gRPC

2022 年 9 月,Apache RocketMQ 5.0.0 发布。这个版本增加了基于 gRPC 的多语言客户端,并在服务端引入 Proxy。新的客户端不再直接连接 NameServer 和 Broker,而是以 Proxy 作为统一入口:

Proxy 不存储消息,也不替代 NameServer。它负责面向客户端的协议适配、权限和消费管理等计算逻辑,再访问 NameServer 和 Broker 完成实际请求。Broker 则继续承担消息存储与投递。

Proxy 可以和 Broker 运行在同一进程,也就是 Local 模式;也可以作为独立的无状态集群部署,也就是 Cluster 模式。前者适合从原有架构平滑增加 gRPC 接入,后者可以让接入层和存储层分别扩展。

标准化协议与多语言 API

RocketMQ 5 使用 Protocol Buffers 定义消息模型和接口,并通过 HTTP/2 上的 gRPC 提供服务。各语言客户端都遵循同一份 rocketmq-apis 协议定义,不需要各自重新设计一套底层通信格式。

统一协议不能自动消除不同语言之间的差异,但它给出了共同的消息模型、状态码和服务接口,降低了各语言 SDK 保持一致的成本。官方的 gRPC 客户端也从 RocketMQ 主仓库中独立出来,按照协议接口与服务端协作。

客户端可以更轻

Proxy 隔离了 NameServer 和 Broker 拓扑,应用只需要配置 Proxy Endpoint。协议适配、权限管理和部分消费管理逻辑可以放到 Proxy 或服务端,客户端不必复刻完整的经典富客户端实现。

RocketMQ 5 还引入了 POP 消费机制,并提供 SimpleConsumer 这类面向消息的 API。客户端可以按消息进行接收、确认、修改不可见时间,而不必直接管理每个队列的消费位点。

这里的"轻量"不等于客户端没有状态或逻辑。连接管理、心跳、长轮询和消息处理仍然需要客户端参与,只是客户端与 Broker 内部实现之间的耦合有所降低。

网络入口更集中

gRPC 客户端只需要访问 Proxy,不需要直接访问 NameServer 返回的每个 Broker 地址。对于跨网络、容器和云环境,这能减少客户端对 Broker 拓扑和注册地址的感知。

gRPC 也是云原生环境中常见的 RPC 框架,基于 HTTP/2,较容易接入拦截器和 Service Mesh 等基础设施。这里解决的是接入方式和网络边界问题,并不代表引入 gRPC 后就不再需要处理超时、重试和故障转移。

gRPC 也有自己的边界

Remoting 与 gRPC 不是"旧协议完全被新协议替代"的关系。采用 gRPC 之前,还需要考虑几个前提:

  • gRPC SDK 需要 RocketMQ 5.0 及以上服务端,并启用 Proxy。
  • gRPC API 经过重新设计,与 Remoting API 不兼容,切换客户端需要修改应用代码。
  • 独立部署 Proxy 会增加一个服务组件和一次网络转发;使用 Local 模式时,Proxy 与 Broker 运行在同一进程。
  • 两套 SDK 的 Producer、Consumer 和管理能力并不完全相同,选择时仍要核对实际需求。

因此,Remoting 依然适合已有经典集群、需要直连 Broker,或者依赖 PullConsumer、LitePullConsumer、Admin 等经典能力的场景。gRPC 更适合已经具备 RocketMQ 5 Proxy、希望使用标准协议和多语言轻量客户端的新接入场景。

两条链路的主要区别可以概括为:

对比项 classic Remoting RocketMQ 5 gRPC
客户端连接目标 NameServer 和 Broker Proxy
是否直接感知 Broker 路由
服务端版本 可用于经典 Remoting 部署 需要 RocketMQ 5.0+ 与 Proxy
主要特点 能力完整、直连 Broker 标准协议、统一入口、多语言友好

两套协议共享的消息核心

接入链路虽然不同,消息最终仍然由 Broker 存储和投递。先认识三个贯穿两套协议的概念:

  • Topic 是消息的逻辑分类。Producer 向 Topic 发送消息,Consumer 订阅 Topic。
  • MessageQueue 是 Topic 的分区单元。一个 Topic 可以包含多个 MessageQueue,并分布在不同 Broker 上。
  • Consumer Group 表示一组具有相同消费目标的 Consumer。在常见的集群消费模式下,同一 Group 中的实例共同分担消息,不同 Group 可以分别消费同一个 Topic。

一条普通消息的主流程可以简化为:

1. 获取路由

Broker 向 NameServer 注册路由。Remoting 客户端直接查询 NameServer;gRPC 客户端向 Proxy 发起请求,由 Proxy 查询或刷新路由。这个阶段传递的是 Topic、MessageQueue 和 Broker 地址等元数据,不包含消息正文。

2. 发送消息

Producer 创建消息并指定 Topic。Remoting 客户端选择 MessageQueue 后直接发送到 Broker;gRPC 客户端先把消息发送给 Proxy,再由 Proxy 转发到 Broker。

3. Broker 存储

Broker 收到消息后,将消息顺序写入 CommitLog,随后根据 Topic 和队列构建 ConsumeQueue 等索引数据。可以暂时把 CommitLog 理解为消息正文的存储位置,把 ConsumeQueue 理解为按 Topic 和队列查找消息的索引。

4. Consumer 获取并处理消息

Remoting Consumer 直接从 Broker 拉取消息;gRPC Consumer 通过 Proxy 获取消息。PushConsumer 会在后台维护拉取或长轮询,然后调用 Handler,因此它对业务代码表现为"推送"。

5. 提交处理结果

Handler 处理成功后,客户端确认消息或提交消费进度。处理失败、超时或确认结果丢失时,消息可能被重新投递;超过最大次数后,还可能进入死信队列。

这意味着消息进入 Handler 后仍有可能再次出现。无论使用 Remoting 还是 gRPC,消费业务都需要考虑幂等。

本系列使用的 .NET SDK

本系列使用的 EventHorizon.RocketMQ 同时实现了 Remoting 和 gRPC 两条链路,并分别发布为两个 NuGet 包:

两个 NuGet 包可以出现在同一个 .NET 应用中,但它们的消息模型、发送结果、异常和 Consumer 类型是相互独立的,不能混用。

用 Producer 和 PushConsumer 做一个简单验证

最后用 gRPC Producer 和 PushConsumer 验证其中一条链路。应用通过 HTTP API 发送消息,Producer 将消息发往 RocketMQ,PushConsumer 收到消息后打印正文。这里选择 gRPC 只是为了让示例保持简短,Remoting 客户端会在后续文章中单独介绍。

项目仓库提供了包含 NameServer、Broker、Proxy 和 Dashboard 的本地环境:

shell 复制代码
git clone https://github.com/eventhorizon-cli/EventHorizon.RocketMQ.git
cd EventHorizon.RocketMQ
docker compose -f test-environments/rocketmq/compose.yaml up -d --wait

环境启动后,gRPC Proxy 地址是 127.0.0.1:8081,并且已经创建 Topic eventhorizon-test-topic。访问 RocketMQ Dashboard 可以查看集群、Topic、Consumer Group 和消息。

新建 ASP.NET Core 应用,并安装 RocketMQ 客户端和 Swagger:

shell 复制代码
dotnet new web -n RocketMQQuickStart --framework net8.0
cd RocketMQQuickStart
dotnet add package EventHorizon.RocketMQ.Grpc
dotnet add package Swashbuckle.AspNetCore

Program.cs 替换为下面的代码:

csharp 复制代码
using System.Text;
using EventHorizon.RocketMQ.Grpc;
using EventHorizon.RocketMQ.Grpc.Consumer;
using EventHorizon.RocketMQ.Grpc.Consumer.Push;
using EventHorizon.RocketMQ.Grpc.Producer;
using Microsoft.Extensions.DependencyInjection;
using RocketMQMessage = EventHorizon.RocketMQ.Grpc.Producer.Message;

const string topic = "eventhorizon-test-topic";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var rocketMQ = builder.Services.AddRocketMQGrpc(options =>
{
    options.Endpoint = "127.0.0.1:8081";
    options.UseTLS = false;
});

rocketMQ.AddGrpcProducer();
rocketMQ.AddGrpcPushConsumer<PrintMessageHandler>(
    ServiceLifetime.Scoped,
    options =>
    {
        options.GroupName = "rocketmq-quick-start";
        options.Subscribe(topic, new FilterExpression("*"));
    });

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

app.MapPost("/messages", async (
    SendMessageRequest request,
    IGrpcProducer producer,
    CancellationToken cancellationToken) =>
{
    var message = new RocketMQMessage(
        topic,
        Encoding.UTF8.GetBytes(request.Message));
    var receipt = await producer.SendAsync(message, cancellationToken);

    return Results.Ok(new { receipt.MessageId });
})
.WithName("SendMessage")
.WithSummary("发送一条 RocketMQ 消息");

await app.RunAsync();

public sealed record SendMessageRequest(string Message);

public sealed class PrintMessageHandler : IGrpcPushMessageHandler
{
    public ValueTask<ConsumeResult> HandleAsync(
        GrpcMessageView message,
        CancellationToken cancellationToken)
    {
        Console.WriteLine(
            "Received: {0}",
            Encoding.UTF8.GetString(message.Body));

        return ValueTask.FromResult(ConsumeResult.Success);
    }
}

这里的 UseTLS = false 只适用于本文的本地测试环境。生产环境应根据实际部署配置 TLS 和访问凭证。

启动应用,并将监听地址固定为 http://localhost:5000

shell 复制代码
dotnet run --urls http://localhost:5000

打开 Swagger UI,找到 POST /messages,使用下面的请求体发送消息:

json 复制代码
{
  "message": "Hello RocketMQ"
}

接口会返回 RocketMQ 生成的 messageId。同一应用中的 PushConsumer 随后会收到这条消息,并在控制台输出:

text 复制代码
Received: Hello RocketMQ

这条消息经过了完整的 gRPC 链路:HTTP API 调用 Producer,Producer 通过 Proxy 将消息发送到 Broker;PushConsumer 再通过 Proxy 获取消息并交给 PrintMessageHandler。Handler 返回 Success 后,客户端向服务端确认处理结果。

小结

RocketMQ 最初使用 Remoting 作为组件与客户端的通信协议。经典客户端直接查询 NameServer、连接 Broker,并在本地承担路由、负载均衡、消费位点和故障处理等逻辑。这种富客户端架构成熟且能力完整,但也提高了多语言实现、客户端升级和复杂网络接入的成本。

RocketMQ 5 没有移除 Remoting,而是在原有 Broker 和 NameServer 之外增加了 Proxy,并使用 Protocol Buffers 和 gRPC 定义新的客户端协议。它通过统一入口、标准协议和更轻的 API 降低多语言接入成本,同时也带来了 Proxy 部署、服务端版本和 API 迁移等新前提。

两条链路最终都回到 Broker:消息写入 CommitLog,通过 ConsumeQueue 等索引被 Consumer 找到,处理成功后确认或提交进度,处理失败时进入重新投递流程。理解这一点,后面再看不同 Producer、Consumer 和消息类型时,就不会把协议入口与消息存储混在一起。

参考资料