Apache Pulsar中DeadLetterPolicy的具体含义与使用
DeadLetterPolicy的核心定义与功能分析
在分布式消息传递系统中,可靠性和容错性是确保数据一致性和系统稳定性的关键因素。Apache Pulsar作为一种高性能的分布式消息队列系统,提供了多种机制以应对消息消费失败的情况,其中DeadLetterPolicy(死信策略)是一项重要的功能。DeadLetterPolicy通过定义消息在多次重试失败后的行为,有效地隔离了无法处理的消息,从而避免对正常消息流的干扰。本文将深入探讨DeadLetterPolicy在Pulsar中的定义、核心属性、配置方法及其在可靠消息传递中的作用。
DeadLetterPolicy的核心属性主要包括maxRedeliverCount、deadLetterTopic和retryLetterTopic。maxRedeliverCount参数用于指定消息在被发送到死信主题之前允许的最大重试次数。这一机制能够防止因无限重试而导致的资源浪费,并为开发者提供了一种平衡消息处理可靠性和系统性能的方式。例如,在高并发场景下,合理设置maxRedeliverCount可以减少不必要的重试开销,同时确保关键消息不会因过早丢弃而丢失。
retryLetterTopic和deadLetterTopic是DeadLetterPolicy的另外两个重要属性。retryLetterTopic用于存储需要重新尝试消费的消息,而deadLetterTopic则作为最终接收失败消息的容器。这种两阶段处理方式使得开发者可以根据业务需求灵活调整消息处理策略。例如,在金融交易系统中,可以通过将失败的消息转移到死信主题并手动处理,从而确保交易数据的一致性。此外,DeadLetterPolicy还支持通过initialSubscriptionName参数为死信主题创建初始订阅。如果未设置该参数,则不会自动创建订阅;而在Broker的allowAutoSubscriptionCreation功能被禁用的情况下,可能导致DLQ生产者创建失败。这一特性强调了配置依赖性的重要性,尤其是在多租户或严格权限管理的环境中。
DeadLetterPolicy deadLetterPolicy = DeadLetterPolicy.builder()
.maxRedeliverCount(5)
.deadLetterTopic("persistent://my-tenant/my-namespace/my-topic-dlq")
.retryLetterTopic("persistent://my-tenant/my-namespace/my-topic-retry")
.build();
Consumer<byte[]> consumer = pulsarClient.newConsumer()
.topic("persistent://my-tenant/my-namespace/my-topic")
.subscriptionName("my-subscription")
.deadLetterPolicy(deadLetterPolicy)
.subscribe();
上述代码中,maxRedeliverCount被设置为5,表示消息最多可以重试5次。如果超过此限制,则失败的消息会被发送到指定的死信主题(my-topic-dlq)。需要注意的是,Pulsar客户端会自动订阅重试主题,但不会订阅死信主题,因此用户需要手动创建额外的消费者来处理死信消息。
DeadLetterPolicy的触发条件与工作流程
触发条件
DeadLetterPolicy 的触发条件主要包括以下几种:
- 确认超时(Acknowledgment Timeout) :当消费者未能在规定时间内确认消息时,系统会将其重新投递到重试主题(Retry Letter Topic)。如果重试次数达到
maxRedeliverCount,消息将被转移到死信主题。 - 负确认(Negative Acknowledgment) :消费者可以显式地对一条消息发送负确认(
negativeAcknowledge),表示该消息未能被成功处理。这种操作会导致消息重新进入队列进行重试,直至达到最大重试限制。 - 重试主题失败处理 :在 Pulsar 中,
Retry Letter Topic是 DeadLetterPolicy 的前置步骤。消息首先会被发送到重试主题进行多次尝试;若仍然失败,则最终被转移到死信主题。 这些触发条件的设计使得 DeadLetterPolicy 能够灵活应对各种消费失败场景,同时为开发者提供了清晰的失败处理路径。
不同订阅模式下的行为差异
源码分析过程
ConsumerImpl#negativeAcknowledge(MessageId messageId)
public void negativeAcknowledge(MessageId messageId) {
negativeAcksTracker.add(messageId);
// Ensure the message is not redelivered for ack-timeout, since we did receive an "ack"
unAckedMessageTracker.remove(MessageIdAdvUtils.discardBatch(messageId));
}
当调用consumer.negativeAcknowledge方法进行异常ACK,会先将消息放入到NegativeAcksTracker队列中,然后从UnAckedMessageTracker没有确认ACK队列中移除。
private synchronized void add(MessageId messageId, int redeliveryCount) {
if (nackedMessages == null) {
nackedMessages = new HashMap<>();
}
long backoffNs;
if (negativeAckRedeliveryBackoff != null) {
backoffNs = TimeUnit.MILLISECONDS.toNanos(negativeAckRedeliveryBackoff.next(redeliveryCount));
} else {
backoffNs = nackDelayNanos;
}
nackedMessages.put(MessageIdAdvUtils.discardBatch(messageId), System.nanoTime() + backoffNs);
if (this.timeout == null) {
// Schedule a task and group all the redeliveries for same period. Leave a small buffer to allow for
// nack immediately following the current one will be batched into the same redeliver request.
this.timeout = timer.newTimeout(this::triggerRedelivery, timerIntervalNanos, TimeUnit.NANOSECONDS);
}
}
在add方法中,Netty 中的newTimeout方法用于创建一个新的定时任务。定时调用triggerRedelivery。
private synchronized void triggerRedelivery(Timeout t) {
if (nackedMessages.isEmpty()) {
this.timeout = null;
return;
}
// Group all the nacked messages into one single re-delivery request
Set<MessageId> messagesToRedeliver = new HashSet<>();
long now = System.nanoTime();
nackedMessages.forEach((msgId, timestamp) -> {
if (timestamp < now) {
addChunkedMessageIdsAndRemoveFromSequenceMap(msgId, messagesToRedeliver, this.consumer);
messagesToRedeliver.add(msgId);
}
});
if (!messagesToRedeliver.isEmpty()) {
messagesToRedeliver.forEach(nackedMessages::remove);
consumer.onNegativeAcksSend(messagesToRedeliver);
log.info("[{}] {} messages will be re-delivered", consumer, messagesToRedeliver.size());
consumer.redeliverUnacknowledgedMessages(messagesToRedeliver);
}
this.timeout = timer.newTimeout(this::triggerRedelivery, timerIntervalNanos, TimeUnit.NANOSECONDS);
}
首先将timeout设置为null,后面执行完成后在设置定时,首先将到时任务转移到messagesToRedeliver,从原集合移除(里面还设计到分块消息,后面系列分析),consumer.redeliverUnacknowledgedMessages。
@Override
public void redeliverUnacknowledgedMessages(Set<MessageId> messageIds) {
if (messageIds.isEmpty()) {
return;
}
if (conf.getSubscriptionType() != SubscriptionType.Shared
&& conf.getSubscriptionType() != SubscriptionType.Key_Shared) {
// We cannot redeliver single messages if subscription type is not Shared
redeliverUnacknowledgedMessages();
return;
}
ClientCnx cnx = cnx();
if (isConnected() && cnx.getRemoteEndpointProtocolVersion() >= ProtocolVersion.v2.getValue()) {
int messagesFromQueue = removeExpiredMessagesFromQueue(messageIds);
Iterables.partition(messageIds, MAX_REDELIVER_UNACKNOWLEDGED).forEach(ids -> {
getRedeliveryMessageIdData(ids).thenAccept(messageIdData -> {
if (!messageIdData.isEmpty()) {
ByteBuf cmd = Commands.newRedeliverUnacknowledgedMessages(consumerId, messageIdData);
cnx.ctx().writeAndFlush(cmd, cnx.ctx().voidPromise());
}
});
});
if (messagesFromQueue > 0) {
increaseAvailablePermits(cnx, messagesFromQueue);
}
if (log.isDebugEnabled()) {
log.debug("[{}] [{}] [{}] Redeliver unacked messages and increase {} permits", subscription, topic,
consumerName, messagesFromQueue);
}
return;
}
if (cnx == null || (getState() == State.Connecting)) {
log.warn("[{}] Client Connection needs to be established for redelivery of unacknowledged messages", this);
} else {
log.warn("[{}] Reconnecting the client to redeliver the messages.", this);
cnx.ctx().close();
}
}
其中分为两点进行分析: 第一点:判断当前消费类型不属于SubscriptionType.Shared和SubscriptionType.Key_Shared类型。 第二点:属于SubscriptionType.Shared和SubscriptionType.Key_Shared类型。
首先分析第一点,调用redeliverUnacknowledgedMessages方法:
```
/**
* 重新投递未确认的消息给消费者。
* <p>
* 此方法确保在消费者重新连接时,处理可能发生的竞争条件,并清除本地消息队列以避免重复处理。
* 它还会向代理发送重新投递未确认消息的命令。
*/
@Override
public void redeliverUnacknowledgedMessages() {
// First : synchronized in order to handle consumer reconnect produce race condition, when broker receive
// redeliverUnacknowledgedMessages and consumer have not be created and
// then receive reconnect epoch change the broker is smaller than the client epoch, this will cause client epoch
// smaller than broker epoch forever. client will not receive message anymore.
// Second : we should synchronized `ClientCnx cnx = cnx()` to
// prevent use old cnx to send redeliverUnacknowledgedMessages to a old broker
synchronized (ConsumerImpl.this) {
ClientCnx cnx = cnx();
// 检查客户端连接是否支持重新投递功能
if (cnx != null && cnx.getRemoteEndpointProtocolVersion() < ProtocolVersion.v2.getValue()) {
if ((getState() == State.Connecting)) {
log.warn("[{}] Client Connection needs to be established "
+ "for redelivery of unacknowledged messages", this);
} else {
log.warn("[{}] Reconnecting the client to redeliver the messages.", this);
cnx.ctx().close();
}
return;
}
// 清除本地消息队列并更新消费纪元(consumerEpoch)
int currentSize;
incomingQueueLock.lock();
try {
// 我们应该每次增加consumerEpoch,因为MultiTopicsConsumerImpl也会增加它,
// 我们需要保持两个consumerEpoch相同
if (conf.getSubscriptionType() == SubscriptionType.Failover
|| conf.getSubscriptionType() == SubscriptionType.Exclusive) {
CONSUMER_EPOCH.incrementAndGet(this);
}
// 清除本地消息
currentSize = incomingMessages.size();
clearIncomingMessages();
unAckedMessageTracker.clear();
} finally {
incomingQueueLock.unlock();
}
// 如果通道已连接,我们应该发送重发命令给代理
if (cnx != null && isConnected(cnx)) {
cnx.ctx().writeAndFlush(Commands.newRedeliverUnacknowledgedMessages(
consumerId, CONSUMER_EPOCH.get(this)), cnx.ctx().voidPromise());
if (currentSize > 0) {
increaseAvailablePermits(cnx, currentSize);
}
if (log.isDebugEnabled()) {
log.debug("[{}] [{}] [{}] Redeliver unacked messages and send {} permits", subscription, topic,
consumerName, currentSize);
}
} else {
log.warn("[{}] Send redeliver messages command but the client is reconnect or close, "
+ "so don't need to send redeliver command to broker", this);
}
}
}
```
synchronized 来保护代码块,具体包含两个关键原因:
1、处理消费者重连时的竞态条件(Race Condition): 当 broker 接收到重新投递未确认消息的请求 (redeliverUnacknowledgedMessages) 时,如果此时消费者尚未完成创建,并且随后发生了客户端与 broker 的 epoch同步问题,就可能导致客户端的 epoch 始终小于 broker 的 epoch。这将导致客户端再也无法接收到任何消息。
2、防止使用过期的连接对象发送请求: 在获取当前连接对象 (ClientCnx cnx = cnx()) 时也需要同步,以确保不会使用旧的连接对象向已经失效的 broker 发送 redeliverUnacknowledgedMessages 请求。
currentSize = incomingMessages.size(); clearIncomingMessages(); unAckedMessageTracker.clear();
这三行代码清除掉incomingMessages接受到的消息队列,unAckedMessageTracker为进行应答的消息队列。
```
public static ByteBuf newRedeliverUnacknowledgedMessages(long consumerId, long consumerEpoch) {
BaseCommand cmd = localCmd(Type.REDELIVER_UNACKNOWLEDGED_MESSAGES);
cmd.setRedeliverUnacknowledgedMessages()
.setConsumerId(consumerId)
.setConsumerEpoch(consumerEpoch);
return serializeWithSize(cmd);
}
protected void increaseAvailablePermits(ClientCnx currentCnx, int delta) {
int available = AVAILABLE_PERMITS_UPDATER.addAndGet(this, delta);
while (available >= getCurrentReceiverQueueSize() / 2 && !paused) {
if (AVAILABLE_PERMITS_UPDATER.compareAndSet(this, available, 0)) {
sendFlowPermitsToBroker(currentCnx, available);
break;
} else {
available = AVAILABLE_PERMITS_UPDATER.get(this);
}
}
}
如果通道已经连接,会向broker发送newRedeliverUnacknowledgedMessages,如果接受消息队列清楚之前的大小大于0,首先向AVAILABLE_PERMITS_UPDATER(原子更新整形字段的更新器)增加currentSize大小数量,并且判断available当前可用许可数超过接收队列大小的一半且未暂停消费,则发送Flow许可给 Broker,让Broker重新推送消息。
我们接着分析Broker端接受到REDELIVER_UNACKNOWLEDGED_MESSAGES命令后的处理。
Broker端接受到REDELIVER_UNACKNOWLEDGED_MESSAGES命令后的处理过程
在PulsarDecoder#channelRead接受到请求后根据cmd类型最终调用,ServerCnx#handleRedeliverUnacknowledged。
@Override
protected void handleRedeliverUnacknowledged(CommandRedeliverUnacknowledgedMessages redeliver) {
checkArgument(state == State.Connected);
if (log.isDebugEnabled()) {
log.debug("[{}] redeliverUnacknowledged from consumer {}, consumerEpoch {}",
remoteAddress, redeliver.getConsumerId(),
redeliver.hasConsumerEpoch() ? redeliver.getConsumerEpoch() : null);
}
CompletableFuture<Consumer> consumerFuture = consumers.get(redeliver.getConsumerId());
if (consumerFuture != null && consumerFuture.isDone() && !consumerFuture.isCompletedExceptionally()) {
Consumer consumer = consumerFuture.getNow(null);
if (redeliver.getMessageIdsCount() > 0 && Subscription.isIndividualAckMode(consumer.subType())) {
consumer.redeliverUnacknowledgedMessages(redeliver.getMessageIdsList());
} else {
if (redeliver.hasConsumerEpoch()) {
consumer.redeliverUnacknowledgedMessages(redeliver.getConsumerEpoch());
} else {
consumer.redeliverUnacknowledgedMessages(DEFAULT_CONSUMER_EPOCH);
}
}
}
}
通过判断redeliver.getMessageIdsCount() > 0 && Subscription.isIndividualAckMode(consumer.subType())消息Id数量大于0,并且消息类型属于SubType.Shared.equals(subType) || SubType.Key_Shared.equals(subType);调用consumer.redeliverUnacknowledgedMessages(redeliver.getConsumerEpoch());
public void redeliverUnacknowledgedMessages(long consumerEpoch) {
// cleanup unackedMessage bucket and redeliver those unack-msgs again
clearUnAckedMsgs();
blockedConsumerOnUnackedMsgs = false;
if (log.isDebugEnabled()) {
log.debug("[{}-{}] consumer {} received redelivery", topicName, subscription, consumerId);
}
if (pendingAcks != null) {
List<PositionImpl> pendingPositions = new ArrayList<>((int) pendingAcks.size());
MutableInt totalRedeliveryMessages = new MutableInt(0);
//计算每个批次中未确认消息数量并统计
pendingAcks.forEach((ledgerId, entryId, batchSize, stickyKeyHash) -> {
int unAckedCount = (int) getUnAckedCountForBatchIndexLevelEnabled(PositionImpl.get(ledgerId, entryId),
batchSize);
totalRedeliveryMessages.add(unAckedCount);
pendingPositions.add(new PositionImpl(ledgerId, entryId));
});
//移除已记录的待确认条目
for (PositionImpl p : pendingPositions) {
pendingAcks.remove(p.getLedgerId(), p.getEntryId());
}
//更新重投递消息的统计信息
msgRedeliver.recordMultipleEvents(totalRedeliveryMessages.intValue(), totalRedeliveryMessages.intValue());
//调用订阅对象进行实际的消息重投递
subscription.redeliverUnacknowledgedMessages(this, pendingPositions);
} else {
subscription.redeliverUnacknowledgedMessages(this, consumerEpoch);
}
//最后触发流量控制,允许继续发送消息
flowConsumerBlockedPermits(this);
}
我们具体分析一下subscription.redeliverUnacknowledgedMessages(this, pendingPositions),其中会获取Dispatcher调度器,这里分析的是Exclusive、Failover,在PersistentSubscription的构造函数中可以看到这两个的Dispatcher为PersistentDispatcherSingleActiveConsumer,现在我们看一下PersistentDispatcherSingleActiveConsumer#redeliverUnacknowledgedMessages
@Override
public void redeliverUnacknowledgedMessages(Consumer consumer, List<PositionImpl> positions) {
// We cannot redeliver single messages to single consumers to preserve ordering.
redeliverUnacknowledgedMessages(consumer, DEFAULT_CONSUMER_EPOCH);
}
可以看到因为无法将单个信息重新传递给单个消费者,以保持排序,所以给默认值-1,redeliverUnacknowledgedMessages主要取消游标的挂起读请求并重置位置:调用 cursor.rewind() 准备重新读取,触发读取更多条目:调用 readMoreEntries() 开始向客户端重新投递消息。
当前文章只分析Exclusive、Failover模式的消息重试,后面文章会分析共享模式,存在问题欢迎拍砖