1. 最佳实践之 Producer


1. 一个应用尽可能用一个 Topic,消息子类型用 tags 来标识,tags 可以由应用自由设置。
  • 只有发送消息设置了 tags,消费方在订阅消息时,才可以利用 tags 在 broker 做消息过滤。
message.setTags("TagA");
2. 每个消息在业务层面的唯一标识码,要设置到 keys 字段,方便将来定位消息丢失问题。
  • 服务器会为每个消息创建索引呢(哈希索引),应用可以通过 topic、key 来查询这条消息内容,以及消息被谁消费。
  • 由于是哈希索引,请务必保证 key 尽可能唯一,这样可以避免潜在的哈希冲突。
// 订单 ID
String orderId = "12345435346";
message.setKeys(orderId);
3. 如有可靠性需要,消息发送成功或者失败,要打印消息日志(sendresult 和 key 消息)。
4. 如果相同性质的消息量大,使用批量消息,可以提升性能。
5. 建议消息大小不超过 512 KB。
6. send(msg) 会阻塞,如果有性能要求,可以使用异步的方式:send(msg, callback)。
7. 如果在一个 JVM 中,有多个生产者进行大数据处理,建议:
  • 少数生产者使用异步发送方式(3~5 个就够了)。
  • 通过 setInstanceName 方法,给每个生产者设置一个实例名。
8. send 消息方法,只要不抛异常,就代表发送成功。但是发送成功会有多个状态,在 sendResult 里定义。
  • SEND_OK:消息发送成功。
  • FLUSH_DISK_TIMEOUT:消息发送成功,但是服务器刷盘超时,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失。
  • FLUSH_SLAVE_TIMEOUT:消息发送成功,但是服务器同步到 Slave 时超时,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失。
  • SLAVE_NOT_AVAILABLE:消息发送成功,但是此时 slave 不可用,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失。
  • 如果状态时是FLUSH_DISK_TIMEOUT 或 FLUSH_SLAVE_TIMEOUT,并且 Broker 正好关闭,此时,可以丢失这条消息,或者重发。但建议最好重发,由消费端去重。
  • Producer 向 Broker 发送请求会等待响应,但如果达到最大等待时间,未得到响应,则客户端将抛出 RemotingTimeoutException。默认等待时间是 3 秒,如果使用 send(msg, timeout),则可以自己设定超时时间,但超时时间不能设置太小,因为 Broker 需要一些时间来刷新磁盘或与从属设备同步。如果该值超过 syncFlushTimeout,则该值可能影响不大,因为 Broker 可能会在超时之前返回 FLUSH_DISK_TIMEOUT 或 FLUSH_SLAVE_TIMEOUT 的响应。
9. 对于消息不可丢失的应用,务必要有消息重发机制。
  • Producer 的 send 方法本身支持内部重试:
  • 至多重试 3 次。
  • 如果发送失败,则轮转到下一个 Broker。
  • 这个方法的总耗时时间不超过 sendMsgTimeout 设置的值,默认 10s。所以,如果本身向 broker 发送消息产生的超时异常,就不会再做重试。

以上策略仍然不能保证消息一定发送成功,为保证消息一定成功,建议将消息存储到db,由后台线程定时重试,保证消息一定到达 Broker。

2. 最佳实践之 Consumer


消费者组和订阅
  • 不同的消费者群体可以独立地消费同样的主题,并且每个消费者都有自己的消费偏移量(offsets)。
  • 确保同一组中的每个消费者订阅相同的主题。
消息监听器(MessageListener)
  • 顺序(Orderly)
  • 消费者将锁定每个 MessageQueue,以确保每个消息被挨个按顺序使用。这将导致性能损失。
  • 如果关心消息的顺序时,它就很有用了。不建议抛出异常,可以返回 ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT 代替。
  • 消费状况(Consume Status)
  • 对于 MessageListenerConcurrently,可以返回 RECONSUME_LATER 告诉消费者,当前不能消费它并且希望以后重新消费,然后可以继续使用其他消息。
  • 对于 MessageListenerOrderly,如果关心顺序,就不能跳过消息,可以返回 SUSPEND_CURRENT_QUEUE_A_MOMENT 来告诉消费者等待片刻。
  • 阻塞(Blocking)
  • 不建议阻塞 Listener,因为它会阻塞线程池,最终可能会停止消费程序。
  • 线程数
  • 消费者使用一个 ThreadPoolExecutor 来处理内部的消费,因此可以通过设置 setConsumeThreadMin 或 setConsumeThreadMax 来更改它。
  • 从何处开始消费
  • 当建立一个新的 Consumer Group 时,需要决定是否需要消费 Broker 中已经存在的历史消息。
  • CONSUME_FROM_LAST_OFFSET 将忽略历史消息,并消费此后生成的任何内容。
  • CONSUME_FROM_FIRST_OFFSET 将消耗 Broker 中存在的所有消息。还可以使用 CONSUME_FROM_TIMESTAMP 来消费在指定的时间戳之后生成的消息。
  • 重复
  • RocketMQ 无法避免消息重复,如果业务对重复消费非常敏感,务必在业务层面做去重:
  • 使用记录消息唯一键进行去重。
  • 使用业务层面的状态机制去重。

3. 最佳实践之 NameServer


  • 在 Apache RocketMQ 中,NameServer 用于协调分布式系统的各个组件,主要通过管理主题路由信息来实现协调。
  • 管理由两部分组成:
  • Brokers 定期更新保存在每个名称服务器中的元数据。
  • 名称服务器是为客户端提供最新的路由信息服务的,包括生产者、消费者和命令行客户端。
  • 因此,在启动 brokers 和 clients 之前,我们需要告诉它们如何通过给它们提供的一个名称服务器地址列表来访问名称服务器。在 Apache RocketMQ 中,可以用四种方式完成。
编程的方式
  • 对于 brokers,我们可以在 broker 的配置文件中指定。
namesrvAddr=name-server-ip1:port;name-server-ip2:port
  • 对于生产者和消费者,我们可以给他们提供姓名服务器地址列表如下:
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.setNamesrvAddr("name-server1-ip:port;name-server2-ip:port");

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
consumer.setNamesrvAddr("name-server1-ip:port;name-server2-ip:port");
  • 如果从 shell 中使用管理命令行,也可以这样指定:
sh mqadmin command-name -n name-server1-ip:port;name-server2-ip:port -X OTHER-OPTION
  • 一个简单的例子,在 NameServer 节点上查询集群信息:
sh mqadmin -n localhost:9876 clusterList
  • 如果将管理工具集成到自己的项目中,可以这样:
DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt("please_rename_unique_group_name");
defaultMQAdminExt.setNamesrvAddr("name-server1-ip:port;name-server2-ip:port");
Java 参数
  • NameServer 的地址列表也可以通过 java 参数 rocketmq.namesrv.addr,在启动之前指定。
环境变量
  • 可以设置 NAMESRV_ADDR 环境变量。如果设置了,Broker 和 clients 将检查并使用其值。
HTTP 断点(HTTP Endpoint)
  • 如果没有使用前面提到的方法指定 NameServer 地址列表,Apache RocketMQ 将每 2 分钟发送一次 HTTP 请求,以获取和更新 NameServer 地址列表,初始延迟 10 秒。
  • 默认情况下,访问的 HTTP 地址是:
http://jmenv.tbsite.net:8080/rocketmq/nsaddr
  • 通过 Java 参数 rocketmq.namesrv.domain,可以修改 jmenv.tbsite.net
  • 通过 Java 参数 rocketmq.namesrv.subgroup,可以修改 nsaddr
优先级
  • 编程方式 > Java 参数 > 环境变量 > HTTP 方式

4. JVM 与 Linux 内核配置


JVM 配置
  • 推荐使用最新发布的 JDK 版本,使用服务器编译器和 8g 堆。
  • 设置相同的 Xms 和 Xmx 值,以防止 JVM 调整堆大小以获得更好的性能。简单的 JVM 配置如下所示:
-server -Xms8g -Xmx8g -Xmn4g
  • 如果不关心 Broker 的启动时间,可以预先触摸 Java 堆,以确保在 JVM 初始化期间分配页是更好的选择。
-XX:+AlwaysPreTouch
  • 禁用偏置锁定可能会减少 JVM 暂停:
-XX:-UseBiasedLocking
  • 对于垃圾回收,建议使用 G1 收集器:
-XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePrecent=25 -XX:InitiatingHeapOccupancyPercent=30
  • 这些 GC 选项看起来有点激进,但事实证明它在生产环境中具有良好的性能。
  • -XX:MaxGCPauseMillis 不要设置太小的值,否则 JVM 将使用一个小的新生代,这将导致非常频繁的新生代 GC。
  • 推荐使用滚动 GC 日志文件:
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFile=5 -XX:GCLogFileSize=30m
  • 如果写入 GC 文件会增加代理的延迟,请将重定向 GC 日志文件考虑在内存文件系统中:
-Xloggc:/dev/shm/mq_gc_%p.log
Linux 内核配置
  • 在 bin 目录中,有一个 os.sh 脚本列出了很多内核函数,只需要稍微的修改,就可以用于生产环境。