Skip to content

Commit 1b74fe0

Browse files
authored
[improve][broker] Part-1 of PIP-434: Expose Netty channel configuration WRITE_BUFFER_WATER_MARK to pulsar conf and pause receive requests when channel is unwritable (#24423)
1 parent 7c8d3c9 commit 1b74fe0

12 files changed

Lines changed: 657 additions & 11 deletions

File tree

pulsar-broker-common/src/main/java/org/apache/pulsar/broker/ServiceConfiguration.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,58 @@ The max allowed delay for delayed delivery (in milliseconds). If the broker rece
927927
)
928928
private int brokerMaxConnections = 0;
929929

930+
@FieldContext(
931+
category = CATEGORY_POLICIES,
932+
doc = "It relates to configuration \"WriteBufferHighWaterMark\" of Netty Channel Config. If the number of bytes"
933+
+ " queued in the write buffer exceeds this value, channel writable state will start to return \"false\"."
934+
)
935+
private int pulsarChannelWriteBufferHighWaterMark = 64 * 1024;
936+
937+
@FieldContext(
938+
category = CATEGORY_POLICIES,
939+
doc = "It relates to configuration \"WriteBufferLowWaterMark\" of Netty Channel Config. If the number of bytes"
940+
+ " queued in the write buffer is smaller than this value, channel writable state will start to return"
941+
+ " \"true\"."
942+
)
943+
private int pulsarChannelWriteBufferLowWaterMark = 32 * 1024;
944+
945+
@FieldContext(
946+
category = CATEGORY_POLICIES,
947+
doc = "If enabled, the broker will pause reading from the channel to deal with new request once the writer"
948+
+ " buffer is full, until it is changed to writable."
949+
)
950+
private boolean pulsarChannelPauseReceivingRequestsIfUnwritable = false;
951+
952+
@FieldContext(
953+
category = CATEGORY_POLICIES,
954+
doc = "After the connection is recovered from an pause receiving state, the channel will be rate-limited"
955+
+ " for a of time window to avoid overwhelming due to the backlog of requests. This parameter defines"
956+
+ " how long the rate limiting should last, in millis. Once the bytes that are waiting to be sent out"
957+
+ " reach the \"pulsarChannelWriteBufferHighWaterMark\", the timer will be reset. Setting a negative"
958+
+ " value will disable the rate limiting."
959+
)
960+
private int pulsarChannelPauseReceivingCooldownMs = 5000;
961+
962+
@FieldContext(
963+
category = CATEGORY_POLICIES,
964+
doc = "After the connection is recovered from a pause receiving state, the channel will be rate-limited for a"
965+
+ " period of time to avoid overwhelming due to the backlog of requests. This parameter defines how"
966+
+ " many requests should be allowed in the rate limiting period."
967+
968+
)
969+
private int pulsarChannelPauseReceivingCooldownRateLimitPermits = 5;
970+
971+
@FieldContext(
972+
category = CATEGORY_POLICIES,
973+
doc = "After the connection is recovered from a pause receiving state, the channel will be rate-limited for a"
974+
+ " period of time defined by pulsarChannelPauseReceivingCooldownMs to avoid overwhelming due to the"
975+
+ " backlog of requests. This parameter defines the period of the rate limiter in milliseconds. If the rate"
976+
+ " limit period is set to 1000, then the unit is requests per 1000 milli seconds. When it's 10, the unit"
977+
+ " is requests per every 10ms."
978+
979+
)
980+
private int pulsarChannelPauseReceivingCooldownRateLimitPeriodMs = 10;
981+
930982
@FieldContext(
931983
category = CATEGORY_POLICIES,
932984
doc = "The maximum number of connections per IP. If it exceeds, new connections are rejected."

pulsar-broker/src/main/java/org/apache/pulsar/broker/service/PulsarChannelInitializer.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ protected void initChannel(SocketChannel ch) throws Exception {
8181
// disable auto read explicitly so that requests aren't served until auto read is enabled
8282
// ServerCnx must enable auto read in channelActive after PulsarService is ready to accept incoming requests
8383
ch.config().setAutoRead(false);
84+
ch.config().setWriteBufferHighWaterMark(pulsar.getConfig().getPulsarChannelWriteBufferHighWaterMark());
85+
ch.config().setWriteBufferLowWaterMark(pulsar.getConfig().getPulsarChannelWriteBufferLowWaterMark());
8486
ch.pipeline().addLast("consolidation", new FlushConsolidationHandler(1024, true));
8587
if (this.enableTls) {
8688
ch.pipeline().addLast(TLS_HANDLER, new SslHandler(this.sslFactory.createServerSslEngine(ch.alloc())));

pulsar-broker/src/main/java/org/apache/pulsar/broker/service/ServerCnx.java

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import io.netty.channel.ChannelHandler;
3838
import io.netty.channel.ChannelHandlerContext;
3939
import io.netty.channel.ChannelOption;
40+
import io.netty.channel.ChannelOutboundBuffer;
4041
import io.netty.handler.codec.haproxy.HAProxyMessage;
4142
import io.netty.handler.ssl.SslHandler;
4243
import io.netty.util.concurrent.FastThreadLocal;
@@ -182,6 +183,7 @@
182183
import org.apache.pulsar.transaction.coordinator.TransactionCoordinatorID;
183184
import org.apache.pulsar.transaction.coordinator.exceptions.CoordinatorException;
184185
import org.apache.pulsar.transaction.coordinator.impl.MLTransactionMetadataStore;
186+
import org.apache.pulsar.utils.TimedSingleThreadRateLimiter;
185187
import org.slf4j.Logger;
186188
import org.slf4j.LoggerFactory;
187189

@@ -192,6 +194,8 @@
192194
* parameter instance lifecycle.
193195
*/
194196
public class ServerCnx extends PulsarHandler implements TransportCnx {
197+
private static final Logger PAUSE_RECEIVING_LOG = LoggerFactory.getLogger(ServerCnx.class.getName()
198+
+ ".pauseReceiving");
195199
private final BrokerService service;
196200
private final SchemaRegistryService schemaService;
197201
private final String listenerName;
@@ -251,6 +255,10 @@ public class ServerCnx extends PulsarHandler implements TransportCnx {
251255

252256
private final long connectionLivenessCheckTimeoutMillis;
253257
private final TopicsPattern.RegexImplementation topicsPatternImplementation;
258+
private final boolean pauseReceivingRequestsIfUnwritable;
259+
private final TimedSingleThreadRateLimiter requestRateLimiter;
260+
private final int pauseReceivingCooldownMilliSeconds;
261+
private boolean pausedDueToRateLimitation = false;
254262

255263
// Tracks and limits number of bytes pending to be published from a single specific IO thread.
256264
static final class PendingBytesPerThreadTracker {
@@ -314,6 +322,14 @@ public ServerCnx(PulsarService pulsar, String listenerName) {
314322
// the null check is a workaround for #13620
315323
super(pulsar.getBrokerService() != null ? pulsar.getBrokerService().getKeepAliveIntervalSeconds() : 0,
316324
TimeUnit.SECONDS);
325+
this.pauseReceivingRequestsIfUnwritable =
326+
pulsar.getConfig().isPulsarChannelPauseReceivingRequestsIfUnwritable();
327+
this.requestRateLimiter = new TimedSingleThreadRateLimiter(
328+
pulsar.getConfig().getPulsarChannelPauseReceivingCooldownRateLimitPermits(),
329+
pulsar.getConfig().getPulsarChannelPauseReceivingCooldownRateLimitPeriodMs(),
330+
TimeUnit.MILLISECONDS);
331+
this.pauseReceivingCooldownMilliSeconds =
332+
pulsar.getConfig().getPulsarChannelPauseReceivingCooldownMs();
317333
this.service = pulsar.getBrokerService();
318334
this.schemaService = pulsar.getSchemaRegistryService();
319335
this.listenerName = listenerName;
@@ -442,11 +458,62 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
442458
}
443459
}
444460

461+
private void checkPauseReceivingRequestsAfterResumeRateLimit(BaseCommand cmd) {
462+
if (!pauseReceivingRequestsIfUnwritable
463+
|| pauseReceivingCooldownMilliSeconds <= 0 || cmd.getType() == BaseCommand.Type.PONG
464+
|| cmd.getType() == BaseCommand.Type.PING) {
465+
return;
466+
}
467+
if (PAUSE_RECEIVING_LOG.isDebugEnabled()) {
468+
final ChannelOutboundBuffer outboundBuffer = ctx.channel().unsafe().outboundBuffer();
469+
if (outboundBuffer != null) {
470+
PAUSE_RECEIVING_LOG.debug("Start to handle request [{}], totalPendingWriteBytes: {}, channel"
471+
+ " isWritable: {}", cmd.getType(), outboundBuffer.totalPendingWriteBytes(),
472+
ctx.channel().isWritable());
473+
} else {
474+
PAUSE_RECEIVING_LOG.debug("Start to handle request [{}], channel isWritable: {}",
475+
cmd.getType(), ctx.channel().isWritable());
476+
}
477+
}
478+
// "requestRateLimiter" will return the permits that you acquired if it is not opening(has been called
479+
// "timingOpen(duration)").
480+
if (requestRateLimiter.acquire(1) == 0 && !pausedDueToRateLimitation) {
481+
log.warn("[{}] Reached rate limitation", this);
482+
// Stop receiving requests.
483+
pausedDueToRateLimitation = true;
484+
ctx.channel().config().setAutoRead(false);
485+
// Resume after 1 second.
486+
ctx.channel().eventLoop().schedule(() -> {
487+
if (pausedDueToRateLimitation) {
488+
log.info("[{}] Resuming connection after rate limitation", this);
489+
ctx.channel().config().setAutoRead(true);
490+
pausedDueToRateLimitation = false;
491+
}
492+
}, 1, TimeUnit.SECONDS);
493+
}
494+
}
495+
445496
@Override
446497
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
447-
if (log.isDebugEnabled()) {
448-
log.debug("Channel writability has changed to: {}", ctx.channel().isWritable());
498+
if (pauseReceivingRequestsIfUnwritable && ctx.channel().isWritable()) {
499+
log.info("[{}] is writable, turn on channel auto-read", this);
500+
ctx.channel().config().setAutoRead(true);
501+
requestRateLimiter.timingOpen(pauseReceivingCooldownMilliSeconds, TimeUnit.MILLISECONDS);
502+
} else if (pauseReceivingRequestsIfUnwritable && !ctx.channel().isWritable()) {
503+
final ChannelOutboundBuffer outboundBuffer = ctx.channel().unsafe().outboundBuffer();
504+
if (outboundBuffer != null) {
505+
if (PAUSE_RECEIVING_LOG.isDebugEnabled()) {
506+
PAUSE_RECEIVING_LOG.debug("[{}] is not writable, turn off channel auto-read,"
507+
+ " totalPendingWriteBytes: {}", this, outboundBuffer.totalPendingWriteBytes());
508+
}
509+
} else {
510+
if (PAUSE_RECEIVING_LOG.isDebugEnabled()) {
511+
PAUSE_RECEIVING_LOG.debug("[{}] is not writable, turn off channel auto-read", this);
512+
}
513+
}
514+
ctx.channel().config().setAutoRead(false);
449515
}
516+
ctx.fireChannelWritabilityChanged();
450517
}
451518

452519
@Override
@@ -3652,8 +3719,9 @@ public CompletableFuture<Optional<Boolean>> checkConnectionLiveness() {
36523719
}
36533720

36543721
@Override
3655-
protected void messageReceived() {
3656-
super.messageReceived();
3722+
protected void messageReceived(BaseCommand cmd) {
3723+
checkPauseReceivingRequestsAfterResumeRateLimit(cmd);
3724+
super.messageReceived(cmd);
36573725
if (connectionCheckInProgress != null && !connectionCheckInProgress.isDone()) {
36583726
connectionCheckInProgress.complete(Optional.of(true));
36593727
connectionCheckInProgress = null;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.pulsar.utils;
20+
21+
import java.util.concurrent.TimeUnit;
22+
import lombok.Getter;
23+
import lombok.extern.slf4j.Slf4j;
24+
25+
@Slf4j
26+
public class TimedSingleThreadRateLimiter {
27+
28+
@Getter
29+
private final int rate;
30+
@Getter
31+
private final long periodAtMs;
32+
private long lastTimeReset;
33+
@Getter
34+
private int remaining;
35+
private long closeAfterAtMs;
36+
37+
public TimedSingleThreadRateLimiter(final int rate, final long period, final TimeUnit unit) {
38+
this.rate = rate;
39+
this.periodAtMs = unit.toMillis(period);
40+
this.lastTimeReset = System.currentTimeMillis();
41+
this.remaining = rate;
42+
}
43+
44+
public int acquire(int permits) {
45+
final long now = System.currentTimeMillis();
46+
if (permits < 0) {
47+
return 0;
48+
}
49+
if (now > closeAfterAtMs) {
50+
return permits;
51+
}
52+
mayRenew(now);
53+
if (remaining > permits) {
54+
remaining -= permits;
55+
if (log.isDebugEnabled()) {
56+
log.debug("acquired: {}, remaining:{}", permits, remaining);
57+
}
58+
return permits;
59+
} else {
60+
int acquired = remaining;
61+
remaining = 0;
62+
if (log.isDebugEnabled()) {
63+
log.debug("acquired: {}, remaining:{}", acquired, remaining);
64+
}
65+
return acquired;
66+
}
67+
}
68+
69+
public void timingOpen(long closeAfter, final TimeUnit unit) {
70+
if (closeAfter <= 0) {
71+
this.closeAfterAtMs = 0;
72+
} else {
73+
this.closeAfterAtMs = System.currentTimeMillis() + unit.toMillis(closeAfter);
74+
}
75+
}
76+
77+
private void mayRenew(long now) {
78+
if (now > lastTimeReset + periodAtMs) {
79+
remaining = rate;
80+
lastTimeReset = now;
81+
}
82+
}
83+
}

pulsar-broker/src/test/java/org/apache/pulsar/broker/service/utils/ClientChannelHelper.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.netty.channel.embedded.EmbeddedChannel;
2424
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
2525
import java.util.Queue;
26+
import org.apache.pulsar.common.api.proto.BaseCommand;
2627
import org.apache.pulsar.common.api.proto.CommandAck;
2728
import org.apache.pulsar.common.api.proto.CommandAddPartitionToTxnResponse;
2829
import org.apache.pulsar.common.api.proto.CommandAddSubscriptionToTxnResponse;
@@ -71,7 +72,7 @@ public Object getCommand(Object obj) {
7172
private final PulsarDecoder decoder = new PulsarDecoder() {
7273

7374
@Override
74-
protected void messageReceived() {
75+
protected void messageReceived(BaseCommand cmd) {
7576
}
7677

7778
@Override

pulsar-broker/src/test/java/org/apache/pulsar/client/api/MockBrokerService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.apache.pulsar.client.api.MockBrokerServiceHooks.CommandSubscribeHook;
4747
import org.apache.pulsar.client.api.MockBrokerServiceHooks.CommandTopicLookupHook;
4848
import org.apache.pulsar.client.api.MockBrokerServiceHooks.CommandUnsubscribeHook;
49+
import org.apache.pulsar.common.api.proto.BaseCommand;
4950
import org.apache.pulsar.common.api.proto.CommandAck;
5051
import org.apache.pulsar.common.api.proto.CommandCloseConsumer;
5152
import org.apache.pulsar.common.api.proto.CommandCloseProducer;
@@ -132,7 +133,7 @@ public void channelActive(ChannelHandlerContext ctx) throws Exception {
132133
}
133134

134135
@Override
135-
protected void messageReceived() {
136+
protected void messageReceived(BaseCommand cmd) {
136137
}
137138

138139
@Override

0 commit comments

Comments
 (0)