Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
Expand All @@ -53,13 +54,16 @@
import org.apache.hadoop.hbase.ClusterMetrics.Option;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.HRegionLocation;
import org.apache.hadoop.hbase.MetaTableAccessor;
import org.apache.hadoop.hbase.ServerName;
import org.apache.hadoop.hbase.UnknownRegionException;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.DoNotRetryRegionException;
import org.apache.hadoop.hbase.client.RegionInfo;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.master.RackManager;
import org.apache.hadoop.hbase.master.assignment.AssignmentManager;
import org.apache.hadoop.hbase.net.Address;
Expand Down Expand Up @@ -96,6 +100,7 @@ public class RegionMover extends AbstractHBaseTool implements Closeable {
private boolean ack = true;
private int maxthreads = 1;
private int timeout;
private List<String> isolateRegionIdArray;
private String loadUnload;
private String hostname;
private String filename;
Expand All @@ -112,6 +117,7 @@ private RegionMover(RegionMoverBuilder builder) throws IOException {
this.excludeFile = builder.excludeFile;
this.designatedFile = builder.designatedFile;
this.maxthreads = builder.maxthreads;
this.isolateRegionIdArray = builder.isolateRegionIdArray;
this.ack = builder.ack;
this.port = builder.port;
this.timeout = builder.timeout;
Expand Down Expand Up @@ -156,6 +162,7 @@ public static class RegionMoverBuilder {
private boolean ack = true;
private int maxthreads = 1;
private int timeout = Integer.MAX_VALUE;
private List<String> isolateRegionIdArray = new ArrayList<>();
private String hostname;
private String filename;
private String excludeFile = null;
Expand Down Expand Up @@ -216,6 +223,14 @@ public RegionMoverBuilder maxthreads(int threads) {
return this;
}

/**
* Set the region ID to isolate on the region server.
*/
public RegionMoverBuilder isolateRegionIdArray(List<String> isolateRegionIdArray) {
this.isolateRegionIdArray = isolateRegionIdArray;
return this;
}

/**
* Path of file containing hostnames to be excluded during region movement. Exclude file should
* have 'host:port' per line. Port is mandatory here as we can have many RS running on a single
Expand Down Expand Up @@ -499,7 +514,85 @@ Collection<ServerName> filterRSGroupServers(RSGroupInfo rsgroup,
private void unloadRegions(ServerName server, List<ServerName> regionServers,
List<RegionInfo> movedRegions) throws Exception {
while (true) {
List<RegionInfo> isolateRegionInfoList = Collections.synchronizedList(new ArrayList<>());
RegionInfo isolateRegionInfo = null;
if (isolateRegionIdArray != null && !isolateRegionIdArray.isEmpty()) {
// Region will be moved to target region server with Ack mode.
final ExecutorService isolateRegionPool = Executors.newFixedThreadPool(maxthreads);
List<Future<Boolean>> isolateRegionTaskList = new ArrayList<>();
List<RegionInfo> recentlyIsolatedRegion = Collections.synchronizedList(new ArrayList<>());
boolean allRegionOpsSuccessful = true;
for(String isolateRegionId : isolateRegionIdArray) {
Result result = MetaTableAccessor.scanByRegionEncodedName(conn,
isolateRegionId);
HRegionLocation hRegionLocation = MetaTableAccessor.getRegionLocation(conn,
result.getRow());
if (hRegionLocation != null) {
isolateRegionInfo = hRegionLocation.getRegion();
isolateRegionInfoList.add(isolateRegionInfo);
} else {
LOG.error("One of the Region " + isolateRegionId + " doesn't exists/can't fetch from"
+ " meta...Quitting now");
// We only move the regions if all the regions were found.
allRegionOpsSuccessful = false;
break;
}
if (hRegionLocation.getServerName() == server) {
LOG.info("Region " + isolateRegionId + " already exists on server : "
+ server.getHostname());
} else {
Future<Boolean> isolateRegionTask = isolateRegionPool.submit(
new MoveWithAck(conn, isolateRegionInfo, hRegionLocation.getServerName(), server,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we are moving regions to the RS we actually wanted to unload? Sounds contradictory, no? And since we are doing it before we actually do the unloads, it may even make situation worse for the case of overloaded RS.

@mihir6692 mihir6692 Oct 19, 2023

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we are moving regions to the RS we actually wanted to unload? Sounds contradictory, no?

Yes and Yes, it does sound contradictory But Region isolation means we are isolating one or more regions to a single RS, and if that RS have existing regions, then we would have to unload them.

And since we are doing it before we actually do the unloads, it may even make situation worse for the case of overloaded RS.

Region isolation can be done in two ways here.

  1. Unload the target RS, move the regions on the target RS to isolate them and put the target RS in draining/decommission mode.. While we do this, HMaster could move another region on the target RS before target RS is in draining/decommission mode. (We could disable/enable balancer switch in this option but it would be unnecessary in my opinion.)

  2. Move the regions on the target RS, put the target RS in draining/decommission mode and then unload the rest of regions from the RS. This would ensure that while we unload regions from the target RS, HMaster can't move any new Regions on the RS.

With option 2, It seems cleaner and more efficient to me. Granted that target RS could be overloaded for some amount of time but it would be easier and cleaner to ensure that target RS only have the regions intended for isolation.

Hope this make sense.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather do #1 with an extra round of unload to address the case master moved regions there during the isolating phase. It would reduce the risk of problems due to overloaded RS.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In approach 1, All the regions would be offloaded first including hotspotting region. This can lead to resource contention on the new region where hotspotting moves. Also hotspotting region could get stuck in region transition if region is holding some read-write lock.

recentlyIsolatedRegion));
isolateRegionTaskList.add(isolateRegionTask);
}
}

if (!allRegionOpsSuccessful) {
// Failed to fetch one of the region's RegionInfo, so we exit from here.
break;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if we've succeed to submit a first few before the failing to fetch meta location, we won't wait for the ack on the ones we submitted? This could lead the RS even more overloaded, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if we've succeed to submit a first few before the failing to fetch meta location, we won't wait for the ack on the ones we submitted?

Tool haven't submitted any region move task yet, it is only collection RegionInfo for possible move tasks.

At this point, if flag allRegionOpsSuccessful is set to false that means that tool fails to fetch RegionInfo for one of the region that needs to be isolated. By running break tool will exit from while (true) loop without submitting any region move (i.e. isolation) request And we won't be submitted any move request, at this point we only have list of region move task in isolateRegionTaskList

If allRegionOpsSuccessful is set to true then all RegionInfo were found and waitMoveTasksToFinish will submit region move (i.e. isolation) task in Ack mode.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't you submitting a task for each region on your thread pool on line #562? For example, if you pass 3 regions, the first two are located in meta, but the third isn't, you will break the while, but two tasks for the located regions would be running.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I totally missed that. Will take a look into it. Thanks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed it in 911df20

} else if (!isolateRegionTaskList.isEmpty()) {

// Now that we have fetched all the region's regionInfo, we can move them.
waitMoveTasksToFinish(isolateRegionPool, isolateRegionTaskList,
admin.getConfiguration().getLong(MOVE_WAIT_MAX_KEY, DEFAULT_MOVE_WAIT_MAX));

// Get New Location for all the regions in isolateRegionIdArray and
// check that all of them are on the target RS else exit.
for(String isolateRegionId : isolateRegionIdArray) {
Result result = MetaTableAccessor.scanByRegionEncodedName(conn,
isolateRegionId);
HRegionLocation hRegionLocation = MetaTableAccessor.getRegionLocation(conn,
result.getRow());
if (!(hRegionLocation != null && hRegionLocation.getServerName().equals(server))) {
LOG.error("One of the Region " + isolateRegionId + " move failed OR stuck in"
+ " transition...Quitting now");
allRegionOpsSuccessful = false;
break;
}
}
} else {
LOG.info("All regions already exists on server : " + server.getHostname());
}


if (!allRegionOpsSuccessful) {
// If all the regions are not online on the target server,
// we don't put RS in decommission mode and exit from here.
break;
} else {
// Once region has been moved to target RS, Let's put the target RS into decommission mode,
// so master doesn't assign new region to the target RS while we unload the target RS.
// Also pass 'offload' flag as false since we don't want master to offload the target RS.
List<ServerName> listOfServer = new ArrayList<>();
listOfServer.add(server);
LOG.info("Putting server : " + server.getHostname() + " in decommission/draining mode");
admin.decommissionRegionServers(listOfServer, false);
}
}
List<RegionInfo> regionsToMove = admin.getRegions(server);
// Remove all the regions from the online Region list, that we just isolated.
regionsToMove.removeAll(isolateRegionInfoList);
regionsToMove.removeAll(movedRegions);
if (regionsToMove.isEmpty()) {
LOG.info("No Regions to move....Quitting now");
Expand Down Expand Up @@ -774,6 +867,13 @@ protected void addOptions() {
this.addRequiredOptWithArg("o", "operation", "Expected: load/unload/unload_from_rack");
this.addOptWithArg("m", "maxthreads",
"Define the maximum number of threads to use to unload and reload the regions");
this.addOptWithArg("isolateRegionIds",
"Comma separated list of Region IDs to isolate on the RegionServer and put "
+ " region server host in draining mode. This option should only be used with '-o unload'"
+ " only. By putting region server in decommission/draining mode, master can't assign any"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove extra "only" at the and of the sentence: "This option should only be used with '-o isolate_regions' only"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

+ " new region on this server. If one or more regions are not found OR failed to isolate"
+ " successfully, utility will exist without putting RS in draining/decommission mode."
+ " Ex. --isolateRegionIds id1,id2,id3");
this.addOptWithArg("x", "excludefile",
"File with <hostname:port> per line to exclude as unload targets; default excludes only "
+ "target host; useful for rack decommisioning.");
Expand All @@ -795,9 +895,14 @@ protected void addOptions() {
protected void processOptions(CommandLine cmd) {
String hostname = cmd.getOptionValue("r");
rmbuilder = new RegionMoverBuilder(hostname);
this.loadUnload = cmd.getOptionValue("o").toLowerCase(Locale.ROOT);
if (cmd.hasOption('m')) {
rmbuilder.maxthreads(Integer.parseInt(cmd.getOptionValue('m')));
}
if (cmd.hasOption("isolateRegionIds") && this.loadUnload.equals("unload")) {
rmbuilder.isolateRegionIdArray(Arrays.asList(
cmd.getOptionValue("isolateRegionIds").split(",")));
}
if (cmd.hasOption('n')) {
rmbuilder.ack(false);
}
Expand All @@ -813,7 +918,6 @@ protected void processOptions(CommandLine cmd) {
if (cmd.hasOption('t')) {
rmbuilder.timeout(Integer.parseInt(cmd.getOptionValue('t')));
}
this.loadUnload = cmd.getOptionValue("o").toLowerCase(Locale.ROOT);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,126 @@ public void testFailedRegionMove() throws Exception {
}
}

public int findRSWith2OrMoreRegionsOfATable(TableName tableName) throws Exception {
SingleProcessHBaseCluster cluster = TEST_UTIL.getHBaseCluster();
int numOfRS = cluster.getNumLiveRegionServers();
int serverIndex = -1;
for (int i=0; i < numOfRS ; i++) {
HRegionServer destinationRegionServer = cluster.getRegionServer(i);
List<HRegion> hRegions = destinationRegionServer.getRegions().stream()
.filter(hRegion -> hRegion.getRegionInfo().getTable().equals(tableName))
.collect(Collectors.toList());
if (hRegions.size() >= 2) {
serverIndex = i;
break;
}
}
if (serverIndex == -1) {
throw new Exception("This shouln't happen, No RS found with more than 2 regions of table : "
+ tableName);
}
return serverIndex;
}

public int findDestinationRS(int sourceRSIndex) throws Exception {
SingleProcessHBaseCluster cluster = TEST_UTIL.getHBaseCluster();
int destinationRSIndex = -1;
int numOfRS = cluster.getNumLiveRegionServers();
for (int i = 0 ; i < numOfRS ; i++) {
if (i != sourceRSIndex) {
destinationRSIndex = i;
break;
}
}
if (destinationRSIndex == -1) {
throw new Exception("This shouldn't happen, No RS found which is different than source RS");
}
return destinationRSIndex;
}

public void regionIsolationOperation(int sourceRSIndex, int destinationRSIndex,
int numRegionsToIsolate) throws Exception {
final TableName tableName = TableName.valueOf(name.getMethodName());
SingleProcessHBaseCluster cluster = TEST_UTIL.getHBaseCluster();
Admin admin = TEST_UTIL.getAdmin();
HRegionServer sourceRS = cluster.getRegionServer(sourceRSIndex);
List<HRegion> hRegions = sourceRS.getRegions().stream()
.filter(hRegion -> hRegion.getRegionInfo().getTable().equals(tableName))
.collect(Collectors.toList());
List<String> listOfRegionIDsToIsolate = new ArrayList<>();
for (int i = 0 ; i < numRegionsToIsolate; i++) {
listOfRegionIDsToIsolate.add(hRegions.get(i).getRegionInfo().getEncodedName());
}

HRegionServer destinationRS = cluster.getRegionServer(destinationRSIndex);
String destinationRSName = destinationRS.getServerName().getAddress().toString();
RegionMover.RegionMoverBuilder rmBuilder =
new RegionMover.RegionMoverBuilder(destinationRSName, TEST_UTIL.getConfiguration()).ack(true)
.maxthreads(8).isolateRegionIdArray(listOfRegionIDsToIsolate);
try (RegionMover rm = rmBuilder.build()) {
LOG.debug("Unloading {} except regions : {}", destinationRS.getServerName(),
listOfRegionIDsToIsolate);
rm.unload();
Assert.assertEquals(listOfRegionIDsToIsolate.size(),
destinationRS.getNumberOfOnlineRegions());
for (int i = 0; i < listOfRegionIDsToIsolate.size(); i++) {
Assert.assertEquals(listOfRegionIDsToIsolate.get(i),
destinationRS.getRegions().get(i).getRegionInfo().getEncodedName());
}
LOG.debug("Successfully Isolated " + listOfRegionIDsToIsolate.size() + " regions : "
+ listOfRegionIDsToIsolate + " on " + destinationRS.getServerName());
} finally {
admin.recommissionRegionServer(destinationRS.getServerName(), null);
}
}

public void loadDummyDataInTable(TableName tableName) throws Exception {
Admin admin = TEST_UTIL.getAdmin();
Table table = TEST_UTIL.getConnection().getTable(tableName);
List<Put> puts = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
puts.add(new Put(Bytes.toBytes("rowkey_" + i)).addColumn(Bytes.toBytes("fam1"),
Bytes.toBytes("q1"), Bytes.toBytes("val_" + i)));
}
table.put(puts);
admin.flush(tableName);
}

@Test
public void testIsolateSingleRegionOnTheSameServer() throws Exception {
final TableName tableName = TableName.valueOf(name.getMethodName());
loadDummyDataInTable(tableName);
int sourceRSIndex = findRSWith2OrMoreRegionsOfATable(tableName);
// Isolating 1 region on the same region server.
regionIsolationOperation(sourceRSIndex, sourceRSIndex, 1);
}

@Test
public void testIsolateSingleRegionOnTheDifferentServer() throws Exception {
final TableName tableName = TableName.valueOf(name.getMethodName());
loadDummyDataInTable(tableName);
int sourceRSIndex = findRSWith2OrMoreRegionsOfATable(tableName);
int destinationRSIndex = findDestinationRS(sourceRSIndex);
// Isolating 1 region on the different region server.
regionIsolationOperation(sourceRSIndex, destinationRSIndex, 1);
}

@Test
public void testIsolateMultipleRegionsOnTheSameServer() throws Exception {
final TableName tableName = TableName.valueOf(name.getMethodName());
loadDummyDataInTable(tableName);
int sourceRSIndex = findRSWith2OrMoreRegionsOfATable(tableName);
// Isolating 2 regions on the same region server.
regionIsolationOperation(sourceRSIndex, sourceRSIndex, 2);
}

@Test
public void testIsolateMultipleRegionsOnTheDifferentServer() throws Exception {
final TableName tableName = TableName.valueOf(name.getMethodName());
loadDummyDataInTable(tableName);
int sourceRSIndex = findRSWith2OrMoreRegionsOfATable(tableName);
int destinationRSIndex = findDestinationRS(sourceRSIndex);
// Isolating 2 regions on the different region server.
regionIsolationOperation(sourceRSIndex, destinationRSIndex, 2);
}
}