Skip to content

Commit 1b72829

Browse files
committed
HBASE-30238 Add distributed bulkload replication event tracking
Coordinate replicated bulkload WAL events across target region servers using ZooKeeper in-progress and completed markers. Add master-side cleanup for completed markers and cover cross-RS retry replay with MiniCluster.
1 parent 9e7665d commit 1b72829

9 files changed

Lines changed: 930 additions & 35 deletions

File tree

hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
import org.apache.hadoop.hbase.master.cleaner.HFileCleaner;
142142
import org.apache.hadoop.hbase.master.cleaner.LogCleaner;
143143
import org.apache.hadoop.hbase.master.cleaner.ReplicationBarrierCleaner;
144+
import org.apache.hadoop.hbase.master.cleaner.ReplicationBulkLoadEventCleaner;
144145
import org.apache.hadoop.hbase.master.cleaner.SnapshotCleanerChore;
145146
import org.apache.hadoop.hbase.master.hbck.HbckChore;
146147
import org.apache.hadoop.hbase.master.http.MasterDumpServlet;
@@ -435,6 +436,7 @@ public class HMaster extends HBaseServerBase<MasterRpcServices> implements Maste
435436
// The exclusive hfile cleaner pool for scanning the archive directory
436437
private DirScanPool exclusiveHFileCleanerPool;
437438
private ReplicationBarrierCleaner replicationBarrierCleaner;
439+
private ReplicationBulkLoadEventCleaner replicationBulkLoadEventCleaner;
438440
private MobFileCleanerChore mobFileCleanerChore;
439441
private MobFileCompactionChore mobFileCompactionChore;
440442
private RollingUpgradeChore rollingUpgradeChore;
@@ -1795,6 +1797,9 @@ conf, getMasterFileSystem().getFileSystem(), new Path(archiveDir, path),
17951797
replicationBarrierCleaner =
17961798
new ReplicationBarrierCleaner(conf, this, getConnection(), replicationPeerManager);
17971799
getChoreService().scheduleChore(replicationBarrierCleaner);
1800+
replicationBulkLoadEventCleaner =
1801+
new ReplicationBulkLoadEventCleaner(conf, this, getZooKeeper());
1802+
getChoreService().scheduleChore(replicationBulkLoadEventCleaner);
17981803

17991804
final boolean isSnapshotChoreEnabled = this.snapshotCleanupStateStore.get();
18001805
this.snapshotCleanerChore = new SnapshotCleanerChore(this, conf, getSnapshotManager());
@@ -1982,6 +1987,7 @@ protected void stopChores() {
19821987
hfileCleaners = null;
19831988
}
19841989
shutdownChore(replicationBarrierCleaner);
1990+
shutdownChore(replicationBulkLoadEventCleaner);
19851991
shutdownChore(snapshotCleanerChore);
19861992
shutdownChore(hbckChore);
19871993
shutdownChore(regionsRecoveryChore);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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.master.cleaner;
19+
20+
import java.util.concurrent.TimeUnit;
21+
import org.apache.hadoop.conf.Configuration;
22+
import org.apache.hadoop.hbase.ScheduledChore;
23+
import org.apache.hadoop.hbase.Stoppable;
24+
import org.apache.hadoop.hbase.replication.regionserver.ReplicationBulkLoadEventTracker;
25+
import org.apache.hadoop.hbase.zookeeper.ZKWatcher;
26+
import org.apache.yetus.audience.InterfaceAudience;
27+
import org.apache.zookeeper.KeeperException;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
/**
32+
* Cleans completed replicated bulk load event markers after they are old enough.
33+
*/
34+
@InterfaceAudience.Private
35+
public class ReplicationBulkLoadEventCleaner extends ScheduledChore {
36+
37+
private static final Logger LOG = LoggerFactory.getLogger(ReplicationBulkLoadEventCleaner.class);
38+
39+
public static final String PERIOD_MS_KEY =
40+
"hbase.master.cleaner.replication.bulkload.event.period.ms";
41+
public static final int PERIOD_MS_DEFAULT = (int) TimeUnit.MINUTES.toMillis(10);
42+
public static final String DONE_TTL_MS_KEY = "hbase.replication.bulkload.event.done.ttl.ms";
43+
public static final long DONE_TTL_MS_DEFAULT = TimeUnit.DAYS.toMillis(1);
44+
45+
private final ReplicationBulkLoadEventTracker tracker;
46+
private final long doneTtlMs;
47+
48+
public ReplicationBulkLoadEventCleaner(Configuration conf, Stoppable stopper, ZKWatcher zkw) {
49+
super("ReplicationBulkLoadEventCleaner", stopper,
50+
conf.getInt(PERIOD_MS_KEY, PERIOD_MS_DEFAULT));
51+
this.tracker = new ReplicationBulkLoadEventTracker(conf, zkw);
52+
this.doneTtlMs = conf.getLong(DONE_TTL_MS_KEY, DONE_TTL_MS_DEFAULT);
53+
}
54+
55+
@Override
56+
protected void chore() {
57+
try {
58+
int deleted = tracker.cleanDoneMarkers(doneTtlMs);
59+
if (deleted > 0) {
60+
LOG.info("Cleaned {} replicated bulkload event marker(s)", deleted);
61+
}
62+
} catch (KeeperException e) {
63+
LOG.warn("Failed to clean replicated bulkload event markers", e);
64+
}
65+
}
66+
67+
public void choreForTesting() {
68+
chore();
69+
}
70+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public void startReplicationService() throws IOException {
7979
if (server instanceof HRegionServer) {
8080
rsServerHost = ((HRegionServer) server).getRegionServerCoprocessorHost();
8181
}
82-
this.replicationSink = new ReplicationSink(this.conf, rsServerHost);
82+
this.replicationSink = new ReplicationSink(this.conf, rsServerHost, server.getZooKeeper());
8383
this.server.getChoreService().scheduleChore(new ReplicationStatisticsChore(
8484
"ReplicationSinkStatistics", server, (int) TimeUnit.SECONDS.toMillis(statsPeriodInSecond)));
8585
}
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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 java.io.IOException;
21+
import java.io.InterruptedIOException;
22+
import java.nio.charset.StandardCharsets;
23+
import java.util.List;
24+
import java.util.Objects;
25+
import java.util.concurrent.TimeUnit;
26+
import org.apache.hadoop.conf.Configuration;
27+
import org.apache.hadoop.hbase.TableName;
28+
import org.apache.hadoop.hbase.util.Bytes;
29+
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
30+
import org.apache.hadoop.hbase.util.MD5Hash;
31+
import org.apache.hadoop.hbase.zookeeper.ZKUtil;
32+
import org.apache.hadoop.hbase.zookeeper.ZKWatcher;
33+
import org.apache.hadoop.hbase.zookeeper.ZNodePaths;
34+
import org.apache.yetus.audience.InterfaceAudience;
35+
import org.apache.zookeeper.KeeperException;
36+
import org.apache.zookeeper.data.Stat;
37+
38+
/**
39+
* Coordinates replicated bulk load events across all region servers in the sink cluster.
40+
*/
41+
@InterfaceAudience.Private
42+
public class ReplicationBulkLoadEventTracker {
43+
44+
public static final String ZNODE_KEY = "hbase.replication.bulkload.event.tracker.znode";
45+
public static final String ZNODE_DEFAULT = "bulkload-events";
46+
public static final String BUCKET_WIDTH_MS_KEY =
47+
"hbase.replication.bulkload.event.bucket.width.ms";
48+
public static final long BUCKET_WIDTH_MS_DEFAULT = TimeUnit.HOURS.toMillis(1);
49+
public static final String WAIT_TIMEOUT_MS_KEY =
50+
"hbase.replication.bulkload.event.wait.timeout.ms";
51+
public static final long WAIT_TIMEOUT_MS_DEFAULT = TimeUnit.MINUTES.toMillis(1);
52+
public static final String WAIT_INTERVAL_MS_KEY =
53+
"hbase.replication.bulkload.event.wait.interval.ms";
54+
public static final long WAIT_INTERVAL_MS_DEFAULT = TimeUnit.SECONDS.toMillis(1);
55+
56+
private static final byte[] EMPTY_BYTES = new byte[0];
57+
private static final String IN_PROGRESS = "in-progress";
58+
private static final String DONE = "done";
59+
60+
private final ZKWatcher zkw;
61+
private final long bucketWidthMs;
62+
private final long waitTimeoutMs;
63+
private final long waitIntervalMs;
64+
private final String inProgressZNode;
65+
private final String doneZNode;
66+
67+
public ReplicationBulkLoadEventTracker(Configuration conf, ZKWatcher zkw) {
68+
this.zkw = Objects.requireNonNull(zkw, "zkw");
69+
this.bucketWidthMs = Math.max(1, conf.getLong(BUCKET_WIDTH_MS_KEY, BUCKET_WIDTH_MS_DEFAULT));
70+
this.waitTimeoutMs = Math.max(0, conf.getLong(WAIT_TIMEOUT_MS_KEY, WAIT_TIMEOUT_MS_DEFAULT));
71+
this.waitIntervalMs = Math.max(1, conf.getLong(WAIT_INTERVAL_MS_KEY, WAIT_INTERVAL_MS_DEFAULT));
72+
73+
String baseZNode = ZNodePaths.joinZNode(this.zkw.getZNodePaths().replicationZNode,
74+
conf.get(ZNODE_KEY, ZNODE_DEFAULT));
75+
this.inProgressZNode = ZNodePaths.joinZNode(baseZNode, IN_PROGRESS);
76+
this.doneZNode = ZNodePaths.joinZNode(baseZNode, DONE);
77+
}
78+
79+
public Event newEvent(String replicationClusterId, TableName table, byte[] encodedRegionName,
80+
long bulkLoadSeqNum, long writeTime) {
81+
String bucket = Long.toString(Math.max(0, writeTime) / bucketWidthMs);
82+
String eventKey = replicationClusterId + '\n' + table.getNameWithNamespaceInclAsString() + '\n'
83+
+ Bytes.toStringBinary(encodedRegionName) + '\n' + bulkLoadSeqNum;
84+
return new Event(bucket, MD5Hash.getMD5AsHex(Bytes.toBytes(eventKey)), eventKey);
85+
}
86+
87+
public ClaimResult claim(Event event) throws IOException {
88+
long deadline = EnvironmentEdgeManager.currentTime() + waitTimeoutMs;
89+
String inProgressPath = getInProgressPath(event);
90+
try {
91+
while (true) {
92+
if (isDone(event)) {
93+
return ClaimResult.COMPLETED;
94+
}
95+
ZKUtil.createWithParents(zkw, ZKUtil.getParent(inProgressPath));
96+
if (ZKUtil.createEphemeralNodeAndWatch(zkw, inProgressPath, event.getData())) {
97+
return ClaimResult.CLAIMED;
98+
}
99+
if (isDone(event)) {
100+
return ClaimResult.COMPLETED;
101+
}
102+
long now = EnvironmentEdgeManager.currentTime();
103+
if (now >= deadline) {
104+
throw new IOException("Timed out waiting for replicated bulkload event " + event);
105+
}
106+
sleep(Math.min(waitIntervalMs, deadline - now));
107+
}
108+
} catch (KeeperException e) {
109+
throw new IOException("Failed to claim replicated bulkload event " + event, e);
110+
}
111+
}
112+
113+
public void markDone(Event event) throws IOException {
114+
try {
115+
ZKUtil.createSetData(zkw, getDonePath(event), event.getData());
116+
} catch (KeeperException e) {
117+
throw new IOException("Failed to mark replicated bulkload event done " + event, e);
118+
}
119+
}
120+
121+
public void release(Event event) throws IOException {
122+
try {
123+
ZKUtil.deleteNodeFailSilent(zkw, getInProgressPath(event));
124+
} catch (KeeperException e) {
125+
throw new IOException("Failed to release replicated bulkload event " + event, e);
126+
}
127+
}
128+
129+
public boolean isInProgress(Event event) throws KeeperException {
130+
return ZKUtil.checkExists(zkw, getInProgressPath(event)) != -1;
131+
}
132+
133+
public boolean isDone(Event event) throws KeeperException {
134+
return ZKUtil.checkExists(zkw, getDonePath(event)) != -1;
135+
}
136+
137+
public int cleanDoneMarkers(long ttlMs) throws KeeperException {
138+
long minAgeMs = Math.max(0, ttlMs);
139+
long now = EnvironmentEdgeManager.currentTime();
140+
List<String> buckets = ZKUtil.listChildrenNoWatch(zkw, doneZNode);
141+
if (buckets == null) {
142+
return 0;
143+
}
144+
int deleted = 0;
145+
for (String bucket : buckets) {
146+
String doneBucketPath = ZNodePaths.joinZNode(doneZNode, bucket);
147+
List<String> eventIds = ZKUtil.listChildrenNoWatch(zkw, doneBucketPath);
148+
if (eventIds == null) {
149+
continue;
150+
}
151+
for (String eventId : eventIds) {
152+
String donePath = ZNodePaths.joinZNode(doneBucketPath, eventId);
153+
Stat stat = new Stat();
154+
if (ZKUtil.getDataNoWatch(zkw, donePath, stat) == null) {
155+
continue;
156+
}
157+
if (now - stat.getMtime() < minAgeMs) {
158+
continue;
159+
}
160+
String inProgressPath =
161+
ZNodePaths.joinZNode(ZNodePaths.joinZNode(inProgressZNode, bucket), eventId);
162+
if (ZKUtil.checkExists(zkw, inProgressPath) != -1) {
163+
continue;
164+
}
165+
ZKUtil.deleteNodeFailSilent(zkw, donePath);
166+
deleted++;
167+
}
168+
deleteIfEmpty(doneBucketPath);
169+
deleteIfEmpty(ZNodePaths.joinZNode(inProgressZNode, bucket));
170+
}
171+
return deleted;
172+
}
173+
174+
private void deleteIfEmpty(String znode) throws KeeperException {
175+
List<String> children = ZKUtil.listChildrenNoWatch(zkw, znode);
176+
if (children == null || !children.isEmpty()) {
177+
return;
178+
}
179+
try {
180+
ZKUtil.deleteNodeFailSilent(zkw, znode);
181+
} catch (KeeperException.NotEmptyException e) {
182+
// Another region server added a child after our list; keep the bucket.
183+
}
184+
}
185+
186+
private String getInProgressPath(Event event) {
187+
return ZNodePaths.joinZNode(ZNodePaths.joinZNode(inProgressZNode, event.getBucket()),
188+
event.getEventId());
189+
}
190+
191+
private String getDonePath(Event event) {
192+
return ZNodePaths.joinZNode(ZNodePaths.joinZNode(doneZNode, event.getBucket()),
193+
event.getEventId());
194+
}
195+
196+
private void sleep(long sleepMs) throws InterruptedIOException {
197+
try {
198+
Thread.sleep(Math.max(1, sleepMs));
199+
} catch (InterruptedException e) {
200+
Thread.currentThread().interrupt();
201+
InterruptedIOException ioe =
202+
new InterruptedIOException("Interrupted while waiting for replicated bulkload event");
203+
ioe.initCause(e);
204+
throw ioe;
205+
}
206+
}
207+
208+
public enum ClaimResult {
209+
CLAIMED(true),
210+
COMPLETED(false);
211+
212+
private final boolean claimed;
213+
214+
ClaimResult(boolean claimed) {
215+
this.claimed = claimed;
216+
}
217+
218+
public boolean isClaimed() {
219+
return claimed;
220+
}
221+
}
222+
223+
public static final class Event {
224+
private final String bucket;
225+
private final String eventId;
226+
private final String eventKey;
227+
228+
private Event(String bucket, String eventId, String eventKey) {
229+
this.bucket = bucket;
230+
this.eventId = eventId;
231+
this.eventKey = eventKey;
232+
}
233+
234+
public String getBucket() {
235+
return bucket;
236+
}
237+
238+
public String getEventId() {
239+
return eventId;
240+
}
241+
242+
private byte[] getData() {
243+
return eventKey == null ? EMPTY_BYTES : eventKey.getBytes(StandardCharsets.UTF_8);
244+
}
245+
246+
@Override
247+
public boolean equals(Object o) {
248+
if (this == o) {
249+
return true;
250+
}
251+
if (!(o instanceof Event)) {
252+
return false;
253+
}
254+
Event event = (Event) o;
255+
return Objects.equals(bucket, event.bucket) && Objects.equals(eventId, event.eventId);
256+
}
257+
258+
@Override
259+
public int hashCode() {
260+
return Objects.hash(bucket, eventId);
261+
}
262+
263+
@Override
264+
public String toString() {
265+
return "Event{bucket='" + bucket + "', eventId='" + eventId + "'}";
266+
}
267+
}
268+
}

0 commit comments

Comments
 (0)