文章目录
1 PulsarClient
Pulsar 中的 Producer 和 Consumer 都是由数据流(Pulsar中还有管理流-Admin)客户端创建的,它管理了所有 Producer 和 Consumer 的实例,当数据流客户端关闭时,所有由它创建的 Producer 和 Consumer 都会被关闭。
1.1 创建客户端
创建 PulsarClient 对象,如下:
PulsarClient build = PulsarClient.builder()
.listenerThreads(1)
.ioThreads(1)
.serviceUrl("pulsar://127.0.0.1:6650")
.build();
这是建造者模式
,参数较多时使用建造者设计模式可以更直观清晰。这个类的主要方法:
主要作用:
- 创建producer/consumer/reader/tableview
- 元数据信息相关:得到topic的分区列表
- transaction相关
1.2 重要属性&参数
PulsarClient 主要初始化线程池、连接池、权限模型、查询broker分区 topic所属等用的LookupService、流量控制、存放生产消费者的集合。
重要属性
private LookupService lookup;
private final ConnectionPool cnxPool;
@Getter
private final Timer timer;
private final ExecutorProvider externalExecutorProvider;
private final ExecutorProvider internalExecutorProvider;
- LookupService:
ConnectionPool
:- 时间轮 WheelTimer:
- 线程池 Executor:
可设置的参数
PulsarClient 上可以设置的参数详见《深入解析 Apache Pulsar》
P42
这些参数可以在 org.apache.pulsar.client.impl.conf.ClientConfigurationData
中查看
// broker地址
private String serviceUrl;
private transient ServiceUrlProvider serviceUrlProvider;
// auth开头都是认证相关的
private Authentication authentication;
private String authPluginClassName;
private String authParams;
private Map<String, String> authParamMap;
// 客户端和服务端接口交互的超时时间
private long operationTimeoutMs = 30000;
// 多久打印一下数据,比如消费者的消费速度、字节数、消费者总共接收了多少消息等
private long statsIntervalSeconds = 60;
// 客户端和服务端接口交互用的线程数
private int numIoThreads = 1;
// 消费者的线程数
private int numListenerThreads = 1;
// 连接池用的,相当于最大连接数创建
private int connectionsPerBroker = 1;
// tcp nagle算法
private boolean useTcpNoDelay = true;
// tls协议相关的
private boolean useTls = false;
private String tlsTrustCertsFilePath = "";
private boolean tlsAllowInsecureConnection = false;
private boolean tlsHostnameVerificationEnable = false;
// 用于创建Semaphore,控制客户端向服务端请求速率 例如查询topic属于哪个broker,查询分区数
private int concurrentLookupRequest = 5000;
private int maxLookupRequest = 50000;
// 重定向次数 查询topic属于哪个broker时随机选个broker看看是否存在,不存在定向到其他broker
private int maxLookupRedirects = 20;
// 当前请求过多,对服务端造成限流,会拒绝客户端请求 场景:还是查询topic属于哪个broker和查询分区数
private int maxNumberOfRejectedRequestPerConnection = 50;
// channel建立后,创建一个定时心跳任务保活channel,该参数是多久发一次心跳请求
private int keepAliveIntervalSeconds = 30;
// 客户端与服务端的连接超时时间
private int connectionTimeoutMs = 10000;
// pulsar有admin web接口,该参数是http请求的超时时间
private int requestTimeoutMs = 60000;
// 退避初始时间 例如:连接服务端失败了,是不是要重试,那过多久重试呢,重试后又失败了,再过多久重试呢,比如初始1秒,再失败通过一套计算方式,得出下次重试3秒后,再失败10秒后 就是干这个用的
private long initialBackoffIntervalNanos = TimeUnit.MILLISECONDS.toNanos(100);
// 接上面:不能一直重试,因为越来越久,总得有个封顶然后再从初识或某个点开始 这个就是封顶值
private long maxBackoffIntervalNanos = TimeUnit.SECONDS.toNanos(60);
// EpollEventLoop调用select时的等待策略
private boolean enableBusyWait = false;
private String listenerName;
private boolean useKeyStoreTls = false;
private String sslProvider = null;
private String tlsTrustStoreType = "JKS";
private String tlsTrustStorePath = null;
private String tlsTrustStorePassword = null;
private Set<String> tlsCiphers = Sets.newTreeSet();
private Set<String> tlsProtocols = Sets.newTreeSet();
// 发送消息后,再没有收到服务端回调数据还在内存中。用于控制同一时刻内存中存在的消息大小限制
private long memoryLimitBytes = 0;
private String proxyServiceUrl;
private ProxyProtocol proxyProtocol;
private boolean enableTransaction = false;
private Clock clock = Clock.systemDefaultZone();
1.3 ConnectionPool
连接池的主要功能包括:
- 创建并cache连接:getConnection
- 归还连接:releaseConnection
- 关闭连接:closeAllConnections
ConnectionPool 构造函数主要是按照 netty 网络客户端方式初始化相关成员变量,如下:
bootstrap = new Bootstrap();
// 绑定io线程池
bootstrap.group(eventLoopGroup);
// 配置channel类型,如果支持Epoll的话会变成Epoll的channel
bootstrap.channel(EventLoopUtil.getClientSocketChannelClass(eventLoopGroup));
// 设置tcp的连接超时时间
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, conf.getConnectionTimeoutMs());
// 设置tcp no delay
bootstrap.option(ChannelOption.TCP_NODELAY, conf.isUseTcpNoDelay());
// 配置allocator
bootstrap.option(ChannelOption.ALLOCATOR, PulsarByteBufAllocator.DEFAULT);
try {
// *** 绑定channelInitializer ***
channelInitializerHandler = new PulsarChannelInitializer(conf, clientCnxSupplier);
bootstrap.handler(channelInitializerHandler);
} catch (Exception e) {
log.error("Failed to create channel initializer");
throw new PulsarClientException(e);
}
PulsarChannelInitializer 用来初始化和 broker 端的连接,如下:
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("consolidation", new FlushConsolidationHandler(1024, true));
// Setup channel except for the SsHandler for TLS enabled connections
ch.pipeline().addLast("ByteBufPairEncoder", tlsEnabled ? ByteBufPair.COPYING_ENCODER : ByteBufPair.ENCODER);
// 定长解码器
ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(
Commands.DEFAULT_MAX_MESSAGE_SIZE + Commands.MESSAGE_SIZE_FRAME_PADDING, 0, 4, 0, 4));
// 到这里可以拿到 RPC 协议反序列化后的对象,进行客户端逻辑处理
// 实际在这个类 ClientCnx 里面处理所有逻辑
ch.pipeline().addLast("handler", clientCnxSupplier.get());
}
1.4 ClientCnx
主要负责和服务端交互的逻辑,这个类的层次结构如下:
public class ClientCnx extends PulsarHandler;
public abstract class PulsarHandler extends PulsarDecoder;
public abstract class PulsarDecoder extends ChannelInboundHandlerAdapter;
ClientCnx的主要方法/功能:
- 连接生命周期管理(netty Handler 里面的方法)
- channelActive:发送一个 ConnectCommand 请求给服务端
- channelInActive
- exceptionCaught
- …
- 发送request:主动发送 RPC 的方法(通过 sendRequestAndHandleTimeout 发送),并按照业务逻辑处理
- GetLastMessageId
- GetTopics
- GetSchema
- GetOrCreateSchema
- AckResponse
- Lookup;
- 处理response:继承自 PulsarDecoder 的 handleXXXXX RPC 处理逻辑
- handleConnected
- handleAckResponse
- handleLookupResponse
- …
- 注册/ 删除业务逻辑对象:
- consumer
- producer
- transactionMetaStoreHandler
- transactionBufferHandler
2 Producer
2.1 创建入口 newProducer
Producer 创建是调用了 PulsarClient.newProducer()
,code如下:
@Override
public ProducerBuilder<byte[]> newProducer() {
return new ProducerBuilderImpl<>(this, Schema.BYTES);
}
@Override
public <T> ProducerBuilder<T> newProducer(Schema<T> schema) {
ProducerBuilderImpl<T> producerBuilder = new ProducerBuilderImpl<>(this, schema);
if (!memoryLimitController.isMemoryLimited()) {
// set default limits for producers when memory limit controller is disabled
producerBuilder.maxPendingMessages(NO_MEMORY_LIMIT_DEFAULT_MAX_PENDING_MESSAGES);
producerBuilder.maxPendingMessagesAcrossPartitions(
NO_MEMORY_LIMIT_DEFAULT_MAX_PENDING_MESSAGES_ACROSS_PARTITIONS);
}
return producerBuilder;
}
依然使用建造者模式,使 API 看起来更清晰直观,和 PulsarClient 一样,Producer 通过 ProducerConfigurationData 收集参数。
最终通过 create() -> createAsync
-> client.createProducerAsyn()
创建 Producer,code如下:
@Override
public CompletableFuture<Producer<T>> createAsync() {
// config validation
checkArgument(!(conf.isBatchingEnabled() && conf.isChunkingEnabled()),
"Batching and chunking of messages can't be enabled together");
if (conf.getTopicName() == null) {
return FutureUtil
.failedFuture(new IllegalArgumentException("Topic name must be set on the producer builder"));
}
try {
setMessageRoutingMode();
} catch (PulsarClientException pce) {
return FutureUtil.failedFuture(pce);
}
return interceptorList == null || interceptorList.size() == 0
? client.createProducerAsync(conf, schema, null)
: client.createProducerAsync(conf, schema, new ProducerInterceptors(interceptorList));
}
2.2 创建分析 createProducerAsync
private <T> CompletableFuture<Producer<T>> createProducerAsync(String topic,
ProducerConfigurationData conf,
Schema<T> schema,
ProducerInterceptors interceptors) {
CompletableFuture<Producer<T>> producerCreatedFuture = new CompletableFuture<>();
getPartitionedTopicMetadata(topic).thenAccept(metadata -> {
if (log.isDebugEnabled()) {
log.debug("[{}] Received topic metadata. partitions: {}", topic, metadata.partitions);
}
ProducerBase<T> producer;
if (metadata.partitions > 0) {
producer = newPartitionedProducerImpl(topic, conf, schema, interceptors, producerCreatedFuture,
metadata);
} else {
producer = newProducerImpl(topic, -1, conf, schema, interceptors, producerCreatedFuture,
Optional.empty());
}
producers.add(producer);
}).exceptionally(ex -> {
log.warn("[{}] Failed to get partitioned topic metadata: {}", topic, ex.getMessage());
producerCreatedFuture.completeExceptionally(ex);
return null;
});
return producerCreatedFuture;
}
先创建 CompletableFuture,等到真正调用时 get 获取,异步编程将性能压缩到极致,Pulsar中使用了大量的异步编程。
代码中 getPartitionedTopicMetadata(topic)
是获取 topic 的分区数,获取分区数后再决定创建分区 TopicProducer 还是非分区 TopicProducer,创建完成放入producers,PulsarClient时初始化了两个集合将来收集生产者和消费者。
这里仅介绍非分区 TopicProducer 实现,分区 TopicProducer 可以看作是由多个非分区 TopicProducer 组成。(非分区 topic 的生产者只创建一个,分区 Topic 的生产者有多少分区数就创建多少生产者)
ProducerImpl:主要是发送消息的api
ProducerBase:定义发送消息api抽象方法以及消息体的构建入口
HandlerState:维护生产者的状态,例如 初始化、准备就绪、连接断开等
2.3 构造函数 ProducerImpl
public ProducerImpl(PulsarClientImpl client, String topic, ProducerConfigurationData conf,
CompletableFuture<Producer<T>> producerCreatedFuture, int partitionIndex, Schema<T> schema,
ProducerInterceptors interceptors) {
super(client, topic, conf, producerCreatedFuture, schema, interceptors);
// 一个链接下的生产者唯一标识,一个链接可以创建多个生产者
this.producerId = client.newProducerId();
this.producerName = conf.getProducerName();
if (StringUtils.isNotBlank(producerName)) {
this.userProvidedProducerName = true;
}
// 分区索引号,当前介绍的是非分区topic,所以是-1
this.partitionIndex = partitionIndex;
// 发送中的消息队列,意思:调用服务端发送前放入队列,服务端响应成功从队列移除
this.pendingMessages = createPendingMessagesQueue();
// 如果服务端没有响应,数据全是在客户端,太多对内存有隐患。要限制最大数量
if (conf.getMaxPendingMessages() > 0) {
this.semaphore = Optional.of(new Semaphore(conf.getMaxPendingMessages(), true));
} else {
this.semaphore = Optional.empty();
}
// 消息压缩实现
this.compressor = CompressionCodecProvider.getCompressionCodec(conf.getCompressionType());
// 去重用的
if (conf.getInitialSequenceId() != null) {
long initialSequenceId = conf.getInitialSequenceId();
this.lastSequenceIdPublished = initialSequenceId;
this.lastSequenceIdPushed = initialSequenceId;
this.msgIdGenerator = initialSequenceId + 1L;
} else {
this.lastSequenceIdPublished = -1L;
this.lastSequenceIdPushed = -1L;
this.msgIdGenerator = 0L;
}
// 为了数据传输安全,加密用的
if (conf.isEncryptionEnabled()) {
String logCtx = "[" + topic + "] [" + producerName + "] [" + producerId + "]";
if (conf.getMessageCrypto() != null) {
this.msgCrypto = conf.getMessageCrypto();
} else {
// default to use MessageCryptoBc;
MessageCrypto msgCryptoBc;
try {
msgCryptoBc = new MessageCryptoBc(logCtx, true);
} catch (Exception e) {
}
this.msgCrypto = msgCryptoBc;
}
} else {
this.msgCrypto = null;
}
// 固定间隔重新生成数据密钥密码
if (this.msgCrypto != null) {
// Regenerate data key cipher at fixed interval
keyGeneratorTask = client.eventLoopGroup().scheduleWithFixedDelay(() -> {
try {
msgCrypto.addPublicKeyCipher(conf.getEncryptionKeys(), conf.getCryptoKeyReader());
} catch (CryptoException e) {
}
}, 0L, 4L, TimeUnit.HOURS);
}
// 发送超时控制
if (conf.getSendTimeoutMs() > 0) {
sendTimeout = client.timer().newTimeout(this, conf.getSendTimeoutMs(), TimeUnit.MILLISECONDS);
}
this.createProducerTimeout = System.currentTimeMillis() + client.getConfiguration().getOperationTimeoutMs();
// 批量发送
if (conf.isBatchingEnabled()) {
BatcherBuilder containerBuilder = conf.getBatcherBuilder();
if (containerBuilder == null) {
containerBuilder = BatcherBuilder.DEFAULT;
}
this.batchMessageContainer = (BatchMessageContainerBase)containerBuilder.build();
this.batchMessageContainer.setProducer(this);
} else {
this.batchMessageContainer = null;
}
// 数据打印频率
if (client.getConfiguration().getStatsIntervalSeconds() > 0) {
stats = new ProducerStatsRecorderImpl(client, conf, this);
} else {
stats = ProducerStatsDisabled.INSTANCE;
}
if (conf.getProperties().isEmpty()) {
metadata = Collections.emptyMap();
} else {
metadata = Collections.unmodifiableMap(new HashMap<>(conf.getProperties()));
}
// 当前类引用传入ConnectionHandler
this.connectionHandler = new ConnectionHandler(this,
new BackoffBuilder()
.setInitialTime(client.getConfiguration().getInitialBackoffIntervalNanos(), TimeUnit.NANOSECONDS)
.setMax(client.getConfiguration().getMaxBackoffIntervalNanos(), TimeUnit.NANOSECONDS)
.setMandatoryStop(Math.max(100, conf.getSendTimeoutMs() - 100), TimeUnit.MILLISECONDS)
.create(),
this);
// 这个是关键
grabCnx();
}
上面介绍了 ProducerImpl 中的关键属性含义,最后构造调用了 grabCnx()
,这是创建生产者的核心入口。
void grabCnx() {
this.connectionHandler.grabCnx();
}
2.4 ConnectionHandler实现
ConnectionHandler.grabCnx()
核心代码:
protected void grabCnx() {
// 在 ProducerImpl 构造函数里,初始化 ConnectionHandler 时已赋值,默认为 null
if (CLIENT_CNX_UPDATER.get(this) != null) {
log.warn("[{}] [{}] Client cnx already set, ignoring reconnection request",
state.topic, state.getHandlerName());
return;
}
......
try {
// state 和 connection 都是 ProducerImpl 中的对象
state.client.getConnection(state.topic) // 1. 获取连接
.thenAccept(cnx -> connection.connectionOpened(cnx)) // 2.打开连接(包括向broker发送创建producer的请求)
.exceptionally(this::handleConnectionError);
} catch (Throwable t) {
log.warn("[{}] [{}] Exception thrown while getting connection: ", state.topic, state.getHandlerName(), t);
reconnectLater(t);
}
}
主要包括两部分:1. 获取与 broker 的连接;2. 向 broker 发送创建生产者的请求
1.PulsarClientImpl.getConnection()
核心代码:
public CompletableFuture<ClientCnx> getConnection(final String topic) {
TopicName topicName = TopicName.get(topic);
// lookup对象(即 LookupService) 是 PulsarClient 构造时创建
return lookup.getBroker(topicName)
.thenCompose(pair -> getConnection(pair.getLeft(), pair.getRight()));
}
重点看下 LookupService.getBroker(topicName)
,即寻找 topic 所属的 broker,大体的逻辑是:
- 在配置的 serviceUrl 列表中随机获取一个 Broker 地址,开始寻找 Topic 是否在这个 Broker 中
- clientCnx 向 Broker 发送 lookup 请求,如果 Topic 的 owner 不是该 Broker,则需要根据该 Broker 返回的新地址进行重定向请求
2.向 broker 发送创建生产者的请求
当找到 Topic 所属的 Broker 地址后,再回到上面的 grabCnx()
方法,接着会调用 ConnectionHandler.Connection.connectionOpened(cnx)
,核心代码:
@Override
public void connectionOpened(final ClientCnx cnx) {
......
final long epoch;
cnx.registerProducer(producerId, this);
long requestId = client.newRequestId();
SchemaInfo schemaInfo = null;
if (schema != null) {
......
}
cnx.sendRequestWithId(
// 构造创建生产者的命令,携带生产者的配置信息,请求创建
Commands.newProducer(topic, producerId, requestId, producerName, conf.isEncryptionEnabled(), metadata,
schemaInfo, epoch, userProvidedProducerName,
conf.getAccessMode(), topicEpoch, client.conf.isEnableTransaction(),
conf.getInitialSubscriptionName()),
requestId).thenAccept(response -> { // 收到服务端返回的创建结果
String producerName = response.getProducerName();
long lastSequenceId = response.getLastSequenceId();
schemaVersion = Optional.ofNullable(response.getSchemaVersion());
schemaVersion.ifPresent(v -> schemaCache.put(SchemaHash.of(schema), v));
// We are now reconnected to broker and clear to send messages. Re-send all pending messages and
// set the cnx pointer so that new messages will be sent immediately
synchronized (ProducerImpl.this) {
// 当前生产者关闭
if (getState() == State.Closing || getState() == State.Closed) {
// Producer was closed while reconnecting, close the connection to make sure the broker
// drops the producer on its side
cnx.removeProducer(producerId);
cnx.channel().close();
return;
}
// 重置
resetBackoff();
log.info("[{}] [{}] Created producer on cnx {}", topic, producerName, cnx.ctx().channel());
connectionId = cnx.ctx().channel().toString();
connectedSince = DateFormatter.now();
if (conf.getAccessMode() != ProducerAccessMode.Shared && !topicEpoch.isPresent()) {
log.info("[{}] [{}] Producer epoch is {}", topic, producerName, response.getTopicEpoch());
}
topicEpoch = response.getTopicEpoch();
if (this.producerName == null) {
// 生产者的名字是服务端创建好响应回来的
this.producerName = producerName;
}
if (this.msgIdGenerator == 0 && conf.getInitialSequenceId() == null) {
// Only update sequence id generator if it wasn't already modified. That means we only want
// to update the id generator the first time the producer gets established, and ignore the
// sequence id sent by broker in subsequent producer reconnects
// 初始最后一次的SequenceId,可能连接断过
this.lastSequenceIdPublished = lastSequenceId;
// 初始化应该从Producer上一个SequenceId+1开始
this.msgIdGenerator = lastSequenceId + 1;
}
// 可能之前断过连接,重连后消息需要重发
resendMessages(cnx, epoch);
}
}).exceptionally((e) -> {
......
});
}
至此,创建生产者的过程就完成了,但是有三个问题没有进一步分析:
- 生产写入的 topic 如果没有提前创建,如何处理?
在服务端:
(1)如果 zk /managed-ledgers 下没有该 topic,则说明 topic 不存在;
(2)如果 zk /admin/partitioned-topics下没有该 topic,则返回空metadata,分区数默认0;
(3)如果 topic 不是带分区的,即 partitionIndex == -1,如: public/default/topic;
(4)如果开启了自动创建 topic 且类型是分区 topic.
满足以上4点,则自动创建分区 topic,这里只是把数据写入 zk 的 /admin/partitioned-topics 下。- 寻找 topic 所属 broker,服务端是如何处理的?
即服务端 lookup 的实现原理,涉及 namespace bundle 的分配。可参考lookup原理- 创建 Producer,服务端是如何处理的?
创建 Producer 时会先创建 topic,创建 topic 会创建一个 Ledger 用于将来写数据。创建的 Topic 实例维护 Ledger,也会维护Producer列表。
3 sendMessage
3.1 客户端发送
《深入解析 Apache Pulsar》
P49
Producer 发送数据的入口方法是 ProducerImpl.internalSendWithTxnAsync()
,通过 ProducerImpl.sendMessage()
方法对消息、生产者等构建 Send Command,如下:
protected ByteBufPair sendMessage(long producerId, long sequenceId, int numMessages,
MessageId messageId, MessageMetadata msgMetadata,
ByteBuf compressedPayload) {
if (messageId instanceof MessageIdImpl) {
return Commands.newSend(producerId, sequenceId, numMessages, getChecksumType(),
((MessageIdImpl) messageId).getLedgerId(), ((MessageIdImpl) messageId).getEntryId(),
msgMetadata, compressedPayload);
} else {
return Commands.newSend(producerId, sequenceId, numMessages, getChecksumType(), -1, -1, msgMetadata,
compressedPayload);
}
}
最终调用 ProducerImpl.processOpSendMsg()
将 Send Command 发送到 broker 服务端。
3.2 服务端写入
《深入解析 Apache Pulsar》
P96
Broker 端由 ServerCnx 处理客户端发来的请求,包括读写请求,这里写请求通过 ServerCnx.handleSend()
方法处理。
@Override
protected void handleSend(CommandSend send, ByteBuf headersAndPayload) {
checkArgument(state == State.Connected);
CompletableFuture<Producer> producerFuture = producers.get(send.getProducerId());
if (producerFuture == null || !producerFuture.isDone() || producerFuture.isCompletedExceptionally()) {
log.warn("[{}] Received message, but the producer is not ready : {}. Closing the connection.",
remoteAddress, send.getProducerId());
close();
return;
}
Producer producer = producerFuture.getNow(null);
if (log.isDebugEnabled()) {
printSendCommandDebug(send, headersAndPayload);
}
if (producer.isNonPersistentTopic()) {
// avoid processing non-persist message if reached max concurrent-message limit
if (nonPersistentPendingMessages > maxNonPersistentPendingMessages) {
final long producerId = send.getProducerId();
final long sequenceId = send.getSequenceId();
final long highestSequenceId = send.getHighestSequenceId();
service.getTopicOrderedExecutor().executeOrdered(producer.getTopic().getName(), SafeRun.safeRun(() -> {
commandSender.sendSendReceiptResponse(producerId, sequenceId, highestSequenceId, -1, -1);
}));
producer.recordMessageDrop(send.getNumMessages());
return;
} else {
nonPersistentPendingMessages++;
}
}
startSendOperation(producer, headersAndPayload.readableBytes(), send.getNumMessages());
if (send.hasTxnidMostBits() && send.hasTxnidLeastBits()) {
TxnID txnID = new TxnID(send.getTxnidMostBits(), send.getTxnidLeastBits());
producer.publishTxnMessage(txnID, producer.getProducerId(), send.getSequenceId(),
send.getHighestSequenceId(), headersAndPayload, send.getNumMessages(), send.isIsChunk(),
send.isMarker());
return;
}
// This position is only used for shadow replicator
Position position = send.hasMessageId()
? PositionImpl.get(send.getMessageId().getLedgerId(), send.getMessageId().getEntryId()) : null;
// Persist the message
if (send.hasHighestSequenceId() && send.getSequenceId() <= send.getHighestSequenceId()) {
producer.publishMessage(send.getProducerId(), send.getSequenceId(), send.getHighestSequenceId(),
headersAndPayload, send.getNumMessages(), send.isIsChunk(), send.isMarker(), position);
} else {
producer.publishMessage(send.getProducerId(), send.getSequenceId(), headersAndPayload,
send.getNumMessages(), send.isIsChunk(), send.isMarker(), position);
}
}