Skip to content

Commit 0e68185

Browse files
committed
HBASE-30238 Add bandwidth throttling for bulkload HFile copy during replication
Introduce hbase.replication.bulkload.copy.bandwidth.mb to rate-limit HFile copy from source HDFS in HFileReplicator.Copier. The limit is enforced through a shared RateLimiter across copy threads, and ReplicationSink updates the rate through configuration reload without requiring RegionServer restart.
1 parent 8076457 commit 0e68185

4 files changed

Lines changed: 207 additions & 9 deletions

File tree

hbase-server/src/main/java/org/apache/hadoop/hbase/replication/ReplicationSinkServiceImpl.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.apache.hadoop.hbase.ScheduledChore;
2929
import org.apache.hadoop.hbase.Server;
3030
import org.apache.hadoop.hbase.Stoppable;
31+
import org.apache.hadoop.hbase.conf.ConfigurationObserver;
3132
import org.apache.hadoop.hbase.regionserver.HRegionServer;
3233
import org.apache.hadoop.hbase.regionserver.RegionServerCoprocessorHost;
3334
import org.apache.hadoop.hbase.regionserver.ReplicationSinkService;
@@ -41,7 +42,7 @@
4142
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos;
4243

4344
@InterfaceAudience.Private
44-
public class ReplicationSinkServiceImpl implements ReplicationSinkService {
45+
public class ReplicationSinkServiceImpl implements ReplicationSinkService, ConfigurationObserver {
4546
private static final Logger LOG = LoggerFactory.getLogger(ReplicationSinkServiceImpl.class);
4647

4748
private Configuration conf;
@@ -90,6 +91,14 @@ public void stopReplicationService() {
9091
}
9192
}
9293

94+
@Override
95+
public void onConfigurationChange(Configuration conf) {
96+
this.conf = conf;
97+
if (this.replicationSink != null) {
98+
this.replicationSink.onConfigurationChange(conf);
99+
}
100+
}
101+
93102
@Override
94103
public ReplicationLoad refreshAndGetReplicationLoad() {
95104
if (replicationLoad == null) {

hbase-server/src/main/java/org/apache/hadoop/hbase/replication/regionserver/HFileReplicator.java

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import java.io.Closeable;
2121
import java.io.FileNotFoundException;
2222
import java.io.IOException;
23+
import java.io.InputStream;
2324
import java.io.InterruptedIOException;
25+
import java.io.OutputStream;
2426
import java.math.BigInteger;
2527
import java.util.ArrayList;
2628
import java.util.Deque;
@@ -37,7 +39,6 @@
3739
import java.util.concurrent.TimeUnit;
3840
import org.apache.hadoop.conf.Configuration;
3941
import org.apache.hadoop.fs.FileSystem;
40-
import org.apache.hadoop.fs.FileUtil;
4142
import org.apache.hadoop.fs.Path;
4243
import org.apache.hadoop.fs.permission.FsPermission;
4344
import org.apache.hadoop.hbase.HConstants;
@@ -57,6 +58,7 @@
5758
import org.slf4j.Logger;
5859
import org.slf4j.LoggerFactory;
5960

61+
import org.apache.hbase.thirdparty.com.google.common.util.concurrent.RateLimiter;
6062
import org.apache.hbase.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder;
6163

6264
/**
@@ -75,10 +77,20 @@ public class HFileReplicator implements Closeable {
7577
public static final String REPLICATION_BULKLOAD_COPY_HFILES_PERTHREAD_KEY =
7678
"hbase.replication.bulkload.copy.hfiles.perthread";
7779
public static final int REPLICATION_BULKLOAD_COPY_HFILES_PERTHREAD_DEFAULT = 10;
80+
/**
81+
* Bandwidth limit in MB/s for copying HFiles from source cluster during bulkload replication. 0
82+
* means no limit. Can be changed dynamically via configuration reload.
83+
*/
84+
public static final String REPLICATION_BULKLOAD_COPY_BANDWIDTH_MB_KEY =
85+
"hbase.replication.bulkload.copy.bandwidth.mb";
86+
public static final double REPLICATION_BULKLOAD_COPY_BANDWIDTH_MB_DEFAULT = 0;
7887

7988
private static final Logger LOG = LoggerFactory.getLogger(HFileReplicator.class);
8089
private static final String UNDERSCORE = "_";
8190
private final static FsPermission PERM_ALL_ACCESS = FsPermission.valueOf("-rwxrwxrwx");
91+
private static final int COPY_BUFFER_SIZE = 65536;
92+
// null means no throttling
93+
private volatile RateLimiter rateLimiter;
8294

8395
private Configuration sourceClusterConf;
8496
private String sourceBaseNamespaceDirPath;
@@ -99,6 +111,14 @@ public HFileReplicator(Configuration sourceClusterConf, String sourceBaseNamespa
99111
String sourceHFileArchiveDirPath, Map<String, List<Pair<byte[], List<String>>>> tableQueueMap,
100112
Configuration conf, AsyncClusterConnection connection, List<String> sourceClusterIds)
101113
throws IOException {
114+
this(sourceClusterConf, sourceBaseNamespaceDirPath, sourceHFileArchiveDirPath, tableQueueMap,
115+
conf, connection, sourceClusterIds, RateLimiter.create(Double.MAX_VALUE));
116+
}
117+
118+
public HFileReplicator(Configuration sourceClusterConf, String sourceBaseNamespaceDirPath,
119+
String sourceHFileArchiveDirPath, Map<String, List<Pair<byte[], List<String>>>> tableQueueMap,
120+
Configuration conf, AsyncClusterConnection connection, List<String> sourceClusterIds,
121+
RateLimiter rateLimiter) throws IOException {
102122
this.sourceClusterConf = sourceClusterConf;
103123
this.sourceBaseNamespaceDirPath = sourceBaseNamespaceDirPath;
104124
this.sourceHFileArchiveDirPath = sourceHFileArchiveDirPath;
@@ -120,6 +140,7 @@ public HFileReplicator(Configuration sourceClusterConf, String sourceBaseNamespa
120140
REPLICATION_BULKLOAD_COPY_HFILES_PERTHREAD_DEFAULT);
121141

122142
sinkFs = FileSystem.get(conf);
143+
this.rateLimiter = rateLimiter;
123144
}
124145

125146
@Override
@@ -336,16 +357,13 @@ public Void call() throws IOException {
336357
sourceHFilePath = new Path(sourceBaseNamespaceDirPath, hfiles.get(i));
337358
localHFilePath = new Path(stagingDir, sourceHFilePath.getName());
338359
try {
339-
FileUtil.copy(sourceFs, sourceHFilePath, sinkFs, localHFilePath, false, conf);
340-
// If any other exception other than FNFE then we will fail the replication requests and
341-
// source will retry to replicate these data.
360+
copyWithThrottle(sourceHFilePath, localHFilePath);
342361
} catch (FileNotFoundException e) {
343362
LOG.info("Failed to copy hfile from " + sourceHFilePath + " to " + localHFilePath
344363
+ ". Trying to copy from hfile archive directory.", e);
345364
sourceHFilePath = new Path(sourceHFileArchiveDirPath, hfiles.get(i));
346-
347365
try {
348-
FileUtil.copy(sourceFs, sourceHFilePath, sinkFs, localHFilePath, false, conf);
366+
copyWithThrottle(sourceHFilePath, localHFilePath);
349367
} catch (FileNotFoundException e1) {
350368
// This will mean that the hfile does not exists any where in source cluster FS. So we
351369
// cannot do anything here just log and continue.
@@ -358,5 +376,16 @@ public Void call() throws IOException {
358376
}
359377
return null;
360378
}
379+
380+
private void copyWithThrottle(Path src, Path dst) throws IOException {
381+
try (InputStream in = sourceFs.open(src); OutputStream out = sinkFs.create(dst)) {
382+
byte[] buf = new byte[COPY_BUFFER_SIZE];
383+
int bytesRead;
384+
while ((bytesRead = in.read(buf)) >= 0) {
385+
rateLimiter.acquire(bytesRead);
386+
out.write(buf, 0, bytesRead);
387+
}
388+
}
389+
}
361390
}
362391
}

hbase-server/src/main/java/org/apache/hadoop/hbase/replication/regionserver/ReplicationSink.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import java.util.stream.Collectors;
4343
import org.apache.commons.lang3.StringUtils;
4444
import org.apache.hadoop.conf.Configuration;
45+
import org.apache.hadoop.hbase.conf.ConfigurationObserver;
4546
import org.apache.hadoop.fs.Path;
4647
import org.apache.hadoop.hbase.Cell;
4748
import org.apache.hadoop.hbase.CellUtil;
@@ -71,6 +72,7 @@
7172
import org.slf4j.LoggerFactory;
7273

7374
import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
75+
import org.apache.hbase.thirdparty.com.google.common.util.concurrent.RateLimiter;
7476

7577
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.WALEntry;
7678
import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos;
@@ -94,7 +96,7 @@
9496
* TODO make this class more like ReplicationSource wrt log handling
9597
*/
9698
@InterfaceAudience.Private
97-
public class ReplicationSink {
99+
public class ReplicationSink implements ConfigurationObserver {
98100

99101
private static final Logger LOG = LoggerFactory.getLogger(ReplicationSink.class);
100102
private final Configuration conf;
@@ -108,6 +110,7 @@ public class ReplicationSink {
108110
private long hfilesReplicated = 0;
109111
private SourceFSConfigurationProvider provider;
110112
private WALEntrySinkFilter walEntrySinkFilter;
113+
private final RateLimiter bulkLoadCopyRateLimiter = RateLimiter.create(Double.MAX_VALUE);
111114

112115
/**
113116
* Row size threshold for multi requests above which a warning is logged
@@ -143,6 +146,25 @@ public ReplicationSink(Configuration conf, RegionServerCoprocessorHost rsServerH
143146
throw new IllegalArgumentException(
144147
"Configured source fs configuration provider class " + className + " throws error.", e);
145148
}
149+
updateBulkLoadCopyBandwidth(conf);
150+
}
151+
152+
@Override
153+
public void onConfigurationChange(Configuration newConf) {
154+
updateBulkLoadCopyBandwidth(newConf);
155+
}
156+
157+
double getBulkLoadCopyRateLimiterRate() {
158+
return bulkLoadCopyRateLimiter.getRate();
159+
}
160+
161+
private void updateBulkLoadCopyBandwidth(Configuration conf) {
162+
double bandwidthMb = conf.getDouble(HFileReplicator.REPLICATION_BULKLOAD_COPY_BANDWIDTH_MB_KEY,
163+
HFileReplicator.REPLICATION_BULKLOAD_COPY_BANDWIDTH_MB_DEFAULT);
164+
double newRate = bandwidthMb <= 0 ? Double.MAX_VALUE : bandwidthMb * 1024 * 1024;
165+
bulkLoadCopyRateLimiter.setRate(newRate);
166+
LOG.info("Bulkload copy bandwidth updated: {}",
167+
bandwidthMb <= 0 ? "unlimited" : bandwidthMb + " MB/s");
146168
}
147169

148170
private WALEntrySinkFilter setupWALEntrySinkFilter() throws IOException {
@@ -319,7 +341,7 @@ public void replicateEntries(List<WALEntry> entries, final ExtendedCellScanner c
319341
Configuration providerConf = this.provider.getConf(this.conf, replicationClusterId);
320342
try (HFileReplicator hFileReplicator = new HFileReplicator(providerConf,
321343
sourceBaseNamespaceDirPath, sourceHFileArchiveDirPath, bulkLoadHFileMap, conf,
322-
getConnection(), entry.getKey())) {
344+
getConnection(), entry.getKey(), bulkLoadCopyRateLimiter)) {
323345
hFileReplicator.replicate();
324346
LOG.debug("Finished replicating {} bulk loaded data", entry.getKey().toString());
325347
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.replication.regionserver;
19+
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
23+
import java.io.IOException;
24+
import java.io.OutputStream;
25+
import org.apache.hadoop.conf.Configuration;
26+
import org.apache.hadoop.fs.FSDataOutputStream;
27+
import org.apache.hadoop.fs.FileSystem;
28+
import org.apache.hadoop.fs.Path;
29+
import org.apache.hadoop.hbase.HBaseTestingUtil;
30+
import org.apache.hadoop.hbase.testclassification.ReplicationTests;
31+
import org.apache.hadoop.hbase.testclassification.SmallTests;
32+
import org.junit.jupiter.api.AfterAll;
33+
import org.junit.jupiter.api.BeforeAll;
34+
import org.junit.jupiter.api.Tag;
35+
import org.junit.jupiter.api.Test;
36+
import org.junit.jupiter.api.TestInstance;
37+
38+
/**
39+
* Unit tests for bulkload copy bandwidth throttling in {@link HFileReplicator} and
40+
* {@link ReplicationSink}. Does not require a running HBase cluster.
41+
*/
42+
@Tag(ReplicationTests.TAG)
43+
@Tag(SmallTests.TAG)
44+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
45+
public class TestHFileReplicatorBandwidth {
46+
47+
private final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
48+
49+
@BeforeAll
50+
public void setUpBeforeClass() throws Exception {
51+
TEST_UTIL.startMiniDFSCluster(1);
52+
}
53+
54+
@AfterAll
55+
public void tearDownAfterClass() throws Exception {
56+
TEST_UTIL.shutdownMiniDFSCluster();
57+
}
58+
59+
/**
60+
* Verify that onConfigurationChange updates the RateLimiter rate dynamically.
61+
*/
62+
@Test
63+
public void testBandwidthDynamicUpdate() throws Exception {
64+
Configuration conf = TEST_UTIL.getConfiguration();
65+
conf.set("hbase.replication.source.fs.conf.provider",
66+
TestSourceFSConfigurationProvider.class.getCanonicalName());
67+
ReplicationSink sink = new ReplicationSink(conf, null);
68+
69+
// Default: unlimited
70+
assertEquals(Double.MAX_VALUE, sink.getBulkLoadCopyRateLimiterRate(), 0.0);
71+
72+
// Apply 50 MB/s
73+
Configuration newConf = new Configuration(conf);
74+
newConf.setDouble(HFileReplicator.REPLICATION_BULKLOAD_COPY_BANDWIDTH_MB_KEY, 50.0);
75+
sink.onConfigurationChange(newConf);
76+
assertEquals(50.0 * 1024 * 1024, sink.getBulkLoadCopyRateLimiterRate(), 1.0);
77+
78+
// Reset to unlimited (0 = no limit)
79+
Configuration resetConf = new Configuration(conf);
80+
resetConf.setDouble(HFileReplicator.REPLICATION_BULKLOAD_COPY_BANDWIDTH_MB_KEY, 0);
81+
sink.onConfigurationChange(resetConf);
82+
assertEquals(Double.MAX_VALUE, sink.getBulkLoadCopyRateLimiterRate(), 0.0);
83+
}
84+
85+
/**
86+
* Verify that throttled copy actually slows down I/O at the specified rate. Creates a file of
87+
* known size, copies it through a throttled stream at 1 MB/s, and asserts the elapsed time is at
88+
* least (fileSize / 1MB * 0.8) seconds.
89+
*/
90+
@Test
91+
public void testCopyIsThrottled() throws Exception {
92+
Configuration conf = TEST_UTIL.getConfiguration();
93+
FileSystem fs = TEST_UTIL.getTestFileSystem();
94+
Path testDir = TEST_UTIL.getDataTestDirOnTestFS("testCopyIsThrottled");
95+
fs.mkdirs(testDir);
96+
97+
// Write a ~2MB test file
98+
Path srcFile = new Path(testDir, "src");
99+
int fileSize = 2 * 1024 * 1024;
100+
try (FSDataOutputStream out = fs.create(srcFile)) {
101+
writeBytes(out, fileSize);
102+
}
103+
104+
Path dstFile = new Path(testDir, "dst");
105+
106+
// Throttle at 1 MB/s
107+
double limitMbPerSec = 1.0;
108+
org.apache.hbase.thirdparty.com.google.common.util.concurrent.RateLimiter limiter =
109+
org.apache.hbase.thirdparty.com.google.common.util.concurrent.RateLimiter
110+
.create(limitMbPerSec * 1024 * 1024);
111+
112+
long start = System.currentTimeMillis();
113+
try (java.io.InputStream in = fs.open(srcFile); OutputStream out = fs.create(dstFile)) {
114+
byte[] buf = new byte[65536];
115+
int bytesRead;
116+
while ((bytesRead = in.read(buf)) >= 0) {
117+
limiter.acquire(bytesRead);
118+
out.write(buf, 0, bytesRead);
119+
}
120+
}
121+
long elapsed = System.currentTimeMillis() - start;
122+
123+
// At 1MB/s, 2MB should take >= 1600ms (allow 20% margin for JVM overhead)
124+
long minExpectedMs = (long) (fileSize * 1000L / (limitMbPerSec * 1024 * 1024) * 0.8);
125+
assertTrue(elapsed >= minExpectedMs,
126+
"Expected throttled copy >= " + minExpectedMs + "ms, got " + elapsed + "ms");
127+
}
128+
129+
private static void writeBytes(OutputStream out, int size) throws IOException {
130+
byte[] buf = new byte[65536];
131+
int written = 0;
132+
while (written < size) {
133+
int chunk = Math.min(buf.length, size - written);
134+
out.write(buf, 0, chunk);
135+
written += chunk;
136+
}
137+
}
138+
}

0 commit comments

Comments
 (0)