001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.util;
019
020import java.io.BufferedInputStream;
021import java.io.BufferedOutputStream;
022import java.io.Closeable;
023import java.io.DataInputStream;
024import java.io.DataOutputStream;
025import java.io.File;
026import java.io.FileInputStream;
027import java.io.FileOutputStream;
028import java.io.IOException;
029import java.net.InetAddress;
030import java.nio.file.Files;
031import java.nio.file.Paths;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Collection;
035import java.util.Collections;
036import java.util.EnumSet;
037import java.util.HashSet;
038import java.util.Iterator;
039import java.util.List;
040import java.util.Locale;
041import java.util.Optional;
042import java.util.Set;
043import java.util.concurrent.Callable;
044import java.util.concurrent.CancellationException;
045import java.util.concurrent.ExecutionException;
046import java.util.concurrent.ExecutorService;
047import java.util.concurrent.Executors;
048import java.util.concurrent.Future;
049import java.util.concurrent.TimeUnit;
050import java.util.concurrent.TimeoutException;
051import java.util.function.Predicate;
052import org.apache.commons.io.IOUtils;
053import org.apache.hadoop.conf.Configuration;
054import org.apache.hadoop.hbase.ClusterMetrics.Option;
055import org.apache.hadoop.hbase.HBaseConfiguration;
056import org.apache.hadoop.hbase.HConstants;
057import org.apache.hadoop.hbase.HRegionLocation;
058import org.apache.hadoop.hbase.MetaTableAccessor;
059import org.apache.hadoop.hbase.ServerName;
060import org.apache.hadoop.hbase.UnknownRegionException;
061import org.apache.hadoop.hbase.client.Admin;
062import org.apache.hadoop.hbase.client.Connection;
063import org.apache.hadoop.hbase.client.ConnectionFactory;
064import org.apache.hadoop.hbase.client.DoNotRetryRegionException;
065import org.apache.hadoop.hbase.client.RegionInfo;
066import org.apache.hadoop.hbase.client.RegionInfoBuilder;
067import org.apache.hadoop.hbase.client.Result;
068import org.apache.hadoop.hbase.master.RackManager;
069import org.apache.hadoop.hbase.master.RegionState;
070import org.apache.hadoop.hbase.master.assignment.AssignmentManager;
071import org.apache.hadoop.hbase.net.Address;
072import org.apache.hadoop.hbase.rsgroup.RSGroupInfo;
073import org.apache.hadoop.hbase.zookeeper.MetaTableLocator;
074import org.apache.hadoop.hbase.zookeeper.ZKWatcher;
075import org.apache.hadoop.hbase.zookeeper.ZNodePaths;
076import org.apache.yetus.audience.InterfaceAudience;
077import org.slf4j.Logger;
078import org.slf4j.LoggerFactory;
079
080import org.apache.hbase.thirdparty.com.google.common.net.InetAddresses;
081import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine;
082import org.apache.hbase.thirdparty.org.apache.commons.collections4.CollectionUtils;
083
084/**
085 * Tool for loading/unloading regions to/from given regionserver This tool can be run from Command
086 * line directly as a utility. Supports Ack/No Ack mode for loading/unloading operations.Ack mode
087 * acknowledges if regions are online after movement while noAck mode is best effort mode that
088 * improves performance but will still move on if region is stuck/not moved. Motivation behind noAck
089 * mode being RS shutdown where even if a Region is stuck, upon shutdown master will move it
090 * anyways. This can also be used by constructiong an Object using the builder and then calling
091 * {@link #load()} or {@link #unload()} methods for the desired operations.
092 */
093@InterfaceAudience.Public
094public class RegionMover extends AbstractHBaseTool implements Closeable {
095  public static final String MOVE_RETRIES_MAX_KEY = "hbase.move.retries.max";
096  public static final String MOVE_WAIT_MAX_KEY = "hbase.move.wait.max";
097  public static final String SERVERSTART_WAIT_MAX_KEY = "hbase.serverstart.wait.max";
098  public static final int DEFAULT_MOVE_RETRIES_MAX = 5;
099  public static final int DEFAULT_MOVE_WAIT_MAX = 60;
100  public static final int DEFAULT_SERVERSTART_WAIT_MAX = 180;
101
102  private static final Logger LOG = LoggerFactory.getLogger(RegionMover.class);
103
104  private RegionMoverBuilder rmbuilder;
105  private boolean ack = true;
106  private int maxthreads = 1;
107  private int timeout;
108  private List<String> isolateRegionIdArray;
109  private String loadUnload;
110  private String hostname;
111  private String filename;
112  private String excludeFile;
113  private String designatedFile;
114  private int port;
115  private Connection conn;
116  private Admin admin;
117  private RackManager rackManager;
118
119  private RegionMover(RegionMoverBuilder builder) throws IOException {
120    this.hostname = builder.hostname;
121    this.filename = builder.filename;
122    this.excludeFile = builder.excludeFile;
123    this.designatedFile = builder.designatedFile;
124    this.maxthreads = builder.maxthreads;
125    this.isolateRegionIdArray = builder.isolateRegionIdArray;
126    this.ack = builder.ack;
127    this.port = builder.port;
128    this.timeout = builder.timeout;
129    setConf(builder.conf);
130    this.conn = ConnectionFactory.createConnection(conf);
131    this.admin = conn.getAdmin();
132
133    // if the hostname of master is ip, it indicates that the master/RS has enabled use-ip, we need
134    // to resolve the current hostname to ip to ensure that the RegionMover logic can be executed
135    // normally, see HBASE-27304 for details.
136    ServerName master = admin.getClusterMetrics(EnumSet.of(Option.MASTER)).getMasterName();
137    if (InetAddresses.isInetAddress(master.getHostname())) {
138      if (!InetAddresses.isInetAddress(this.hostname)) {
139        this.hostname = InetAddress.getByName(this.hostname).getHostAddress();
140      }
141    }
142
143    // Only while running unit tests, builder.rackManager will not be null for the convenience of
144    // providing custom rackManager. Otherwise for regular workflow/user triggered action,
145    // builder.rackManager is supposed to be null. Hence, setter of builder.rackManager is
146    // provided as @InterfaceAudience.Private and it is commented that this is just
147    // to be used by unit test.
148    rackManager = builder.rackManager == null ? new RackManager(conf) : builder.rackManager;
149  }
150
151  private RegionMover() {
152  }
153
154  @Override
155  public void close() {
156    IOUtils.closeQuietly(this.admin, e -> LOG.warn("failed to close admin", e));
157    IOUtils.closeQuietly(this.conn, e -> LOG.warn("failed to close conn", e));
158  }
159
160  /**
161   * Builder for Region mover. Use the {@link #build()} method to create RegionMover object. Has
162   * {@link #filename(String)}, {@link #excludeFile(String)}, {@link #maxthreads(int)},
163   * {@link #ack(boolean)}, {@link #timeout(int)}, {@link #designatedFile(String)} methods to set
164   * the corresponding options.
165   */
166  public static class RegionMoverBuilder {
167    private boolean ack = true;
168    private int maxthreads = 1;
169    private int timeout = Integer.MAX_VALUE;
170    private List<String> isolateRegionIdArray = new ArrayList<>();
171    private String hostname;
172    private String filename;
173    private String excludeFile = null;
174    private String designatedFile = null;
175    private String defaultDir = System.getProperty("java.io.tmpdir");
176    @InterfaceAudience.Private
177    final int port;
178    private final Configuration conf;
179    private RackManager rackManager;
180
181    public RegionMoverBuilder(String hostname) {
182      this(hostname, createConf());
183    }
184
185    /**
186     * Creates a new configuration and sets region mover specific overrides
187     */
188    private static Configuration createConf() {
189      final Configuration conf = HBaseConfiguration.create();
190      return overrideConf(conf);
191    }
192
193    private static Configuration overrideConf(Configuration conf) {
194      conf.setInt("hbase.client.prefetch.limit", 1);
195      conf.setInt("hbase.client.pause", 500);
196      conf.setInt("hbase.client.retries.number", 100);
197      return conf;
198    }
199
200    /**
201     * @param hostname Hostname to unload regions from or load regions to. Can be either hostname or
202     *                 hostname:port.
203     * @param conf     Configuration object
204     */
205    public RegionMoverBuilder(String hostname, Configuration conf) {
206      String[] splitHostname = hostname.toLowerCase().split(":");
207      this.hostname = splitHostname[0];
208      if (splitHostname.length == 2) {
209        this.port = Integer.parseInt(splitHostname[1]);
210      } else {
211        this.port = conf.getInt(HConstants.REGIONSERVER_PORT, HConstants.DEFAULT_REGIONSERVER_PORT);
212      }
213      this.filename = defaultDir + File.separator + System.getProperty("user.name") + this.hostname
214        + ":" + Integer.toString(this.port);
215      this.conf = overrideConf(new Configuration(conf));
216    }
217
218    /**
219     * Path of file where regions will be written to during unloading/read from during loading
220     * @return RegionMoverBuilder object
221     */
222    public RegionMoverBuilder filename(String filename) {
223      this.filename = filename;
224      return this;
225    }
226
227    /**
228     * Set the max number of threads that will be used to move regions
229     */
230    public RegionMoverBuilder maxthreads(int threads) {
231      this.maxthreads = threads;
232      return this;
233    }
234
235    /**
236     * Set the region ID to isolate on the region server.
237     */
238    public RegionMoverBuilder isolateRegionIdArray(List<String> isolateRegionIdArray) {
239      this.isolateRegionIdArray = isolateRegionIdArray;
240      return this;
241    }
242
243    /**
244     * Path of file containing hostnames to be excluded during region movement. Exclude file should
245     * have 'host:port' per line. Port is mandatory here as we can have many RS running on a single
246     * host.
247     */
248    public RegionMoverBuilder excludeFile(String excludefile) {
249      this.excludeFile = excludefile;
250      return this;
251    }
252
253    /**
254     * Set the designated file. Designated file contains hostnames where region moves. Designated
255     * file should have 'host:port' per line. Port is mandatory here as we can have many RS running
256     * on a single host.
257     * @param designatedFile The designated file
258     * @return RegionMoverBuilder object
259     */
260    public RegionMoverBuilder designatedFile(String designatedFile) {
261      this.designatedFile = designatedFile;
262      return this;
263    }
264
265    /**
266     * Set ack/noAck mode.
267     * <p>
268     * In ack mode regions are acknowledged before and after moving and the move is retried
269     * hbase.move.retries.max times, if unsuccessful we quit with exit code 1.No Ack mode is a best
270     * effort mode,each region movement is tried once.This can be used during graceful shutdown as
271     * even if we have a stuck region,upon shutdown it'll be reassigned anyway.
272     * <p>
273     * @return RegionMoverBuilder object
274     */
275    public RegionMoverBuilder ack(boolean ack) {
276      this.ack = ack;
277      return this;
278    }
279
280    /**
281     * Set the timeout for Load/Unload operation in seconds.This is a global timeout,threadpool for
282     * movers also have a separate time which is hbase.move.wait.max * number of regions to
283     * load/unload
284     * @param timeout in seconds
285     * @return RegionMoverBuilder object
286     */
287    public RegionMoverBuilder timeout(int timeout) {
288      this.timeout = timeout;
289      return this;
290    }
291
292    /**
293     * Set specific rackManager implementation. This setter method is for testing purpose only.
294     * @param rackManager rackManager impl
295     * @return RegionMoverBuilder object
296     */
297    @InterfaceAudience.Private
298    public RegionMoverBuilder rackManager(RackManager rackManager) {
299      this.rackManager = rackManager;
300      return this;
301    }
302
303    /**
304     * This method builds the appropriate RegionMover object which can then be used to load/unload
305     * using load and unload methods
306     * @return RegionMover object
307     */
308    public RegionMover build() throws IOException {
309      return new RegionMover(this);
310    }
311  }
312
313  /**
314   * Loads the specified {@link #hostname} with regions listed in the {@link #filename} RegionMover
315   * Object has to be created using {@link #RegionMover(RegionMoverBuilder)}
316   * @return true if loading succeeded, false otherwise
317   */
318  public boolean load() throws ExecutionException, InterruptedException, TimeoutException {
319    ExecutorService loadPool = Executors.newFixedThreadPool(1);
320    Future<Boolean> loadTask = loadPool.submit(getMetaRegionMovePlan());
321    boolean isMetaMoved = waitTaskToFinish(loadPool, loadTask, "loading");
322    if (!isMetaMoved) {
323      return false;
324    }
325    loadPool = Executors.newFixedThreadPool(1);
326    loadTask = loadPool.submit(getNonMetaRegionsMovePlan());
327    return waitTaskToFinish(loadPool, loadTask, "loading");
328  }
329
330  private Callable<Boolean> getMetaRegionMovePlan() {
331    return getRegionsMovePlan(true);
332  }
333
334  private Callable<Boolean> getNonMetaRegionsMovePlan() {
335    return getRegionsMovePlan(false);
336  }
337
338  private Callable<Boolean> getRegionsMovePlan(boolean moveMetaRegion) {
339    return () -> {
340      try {
341        List<RegionInfo> regionsToMove = readRegionsFromFile(filename);
342        if (regionsToMove.isEmpty()) {
343          LOG.info("No regions to load.Exiting");
344          return true;
345        }
346        Optional<RegionInfo> metaRegion = getMetaRegionInfoIfToBeMoved(regionsToMove);
347        if (moveMetaRegion) {
348          if (metaRegion.isPresent()) {
349            loadRegions(Collections.singletonList(metaRegion.get()));
350          }
351        } else {
352          metaRegion.ifPresent(regionsToMove::remove);
353          loadRegions(regionsToMove);
354        }
355      } catch (Exception e) {
356        LOG.error("Error while loading regions to " + hostname, e);
357        return false;
358      }
359      return true;
360    };
361  }
362
363  private Optional<RegionInfo> getMetaRegionInfoIfToBeMoved(List<RegionInfo> regionsToMove) {
364    return regionsToMove.stream().filter(RegionInfo::isMetaRegion).findFirst();
365  }
366
367  private void loadRegions(List<RegionInfo> regionsToMove) throws Exception {
368    ServerName server = getTargetServer();
369    List<RegionInfo> movedRegions = Collections.synchronizedList(new ArrayList<>());
370    LOG.info("Moving " + regionsToMove.size() + " regions to " + server + " using "
371      + this.maxthreads + " threads.Ack mode:" + this.ack);
372
373    final ExecutorService moveRegionsPool = Executors.newFixedThreadPool(this.maxthreads);
374    List<Future<Boolean>> taskList = new ArrayList<>();
375    int counter = 0;
376    while (counter < regionsToMove.size()) {
377      RegionInfo region = regionsToMove.get(counter);
378      ServerName currentServer = MoveWithAck.getServerNameForRegion(region, admin, conn);
379      if (currentServer == null) {
380        LOG
381          .warn("Could not get server for Region:" + region.getRegionNameAsString() + " moving on");
382        counter++;
383        continue;
384      } else if (server.equals(currentServer)) {
385        LOG.info(
386          "Region " + region.getRegionNameAsString() + " is already on target server=" + server);
387        counter++;
388        continue;
389      }
390      if (ack) {
391        Future<Boolean> task = moveRegionsPool
392          .submit(new MoveWithAck(conn, region, currentServer, server, movedRegions));
393        taskList.add(task);
394      } else {
395        Future<Boolean> task = moveRegionsPool
396          .submit(new MoveWithoutAck(admin, region, currentServer, server, movedRegions));
397        taskList.add(task);
398      }
399      counter++;
400    }
401
402    moveRegionsPool.shutdown();
403    long timeoutInSeconds = regionsToMove.size()
404      * admin.getConfiguration().getLong(MOVE_WAIT_MAX_KEY, DEFAULT_MOVE_WAIT_MAX);
405    waitMoveTasksToFinish(moveRegionsPool, taskList, timeoutInSeconds);
406  }
407
408  /**
409   * Unload regions from given {@link #hostname} using ack/noAck mode and {@link #maxthreads}.In
410   * noAck mode we do not make sure that region is successfully online on the target region
411   * server,hence it is best effort.We do not unload regions to hostnames given in
412   * {@link #excludeFile}. If designatedFile is present with some contents, we will unload regions
413   * to hostnames provided in {@link #designatedFile}
414   * @return true if unloading succeeded, false otherwise
415   */
416  public boolean unload() throws InterruptedException, ExecutionException, TimeoutException {
417    return unloadRegions(false);
418  }
419
420  /**
421   * Unload regions from given {@link #hostname} using ack/noAck mode and {@link #maxthreads}.In
422   * noAck mode we do not make sure that region is successfully online on the target region
423   * server,hence it is best effort.We do not unload regions to hostnames given in
424   * {@link #excludeFile}. If designatedFile is present with some contents, we will unload regions
425   * to hostnames provided in {@link #designatedFile}. While unloading regions, destination
426   * RegionServers are selected from different rack i.e regions should not move to any RegionServers
427   * that belong to same rack as source RegionServer.
428   * @return true if unloading succeeded, false otherwise
429   */
430  public boolean unloadFromRack()
431    throws InterruptedException, ExecutionException, TimeoutException {
432    return unloadRegions(true);
433  }
434
435  private boolean unloadRegions(boolean unloadFromRack)
436    throws ExecutionException, InterruptedException, TimeoutException {
437    return unloadRegions(unloadFromRack, null);
438  }
439
440  /**
441   * Isolated regions specified in {@link #isolateRegionIdArray} on {@link #hostname} in ack Mode
442   * and Unload regions from given {@link #hostname} using ack/noAck mode and {@link #maxthreads}.
443   * In noAck mode we do not make sure that region is successfully online on the target region
444   * server,hence it is the best effort. We do not unload regions to hostnames given in
445   * {@link #excludeFile}. If designatedFile is present with some contents, we will unload regions
446   * to hostnames provided in {@link #designatedFile}
447   * @return true if region isolation succeeded, false otherwise
448   */
449  public boolean isolateRegions()
450    throws ExecutionException, InterruptedException, TimeoutException {
451    return unloadRegions(false, isolateRegionIdArray);
452  }
453
454  private boolean unloadRegions(boolean unloadFromRack, List<String> isolateRegionIdArray)
455    throws InterruptedException, ExecutionException, TimeoutException {
456    deleteFile(this.filename);
457    ExecutorService unloadPool = Executors.newFixedThreadPool(1);
458    Future<Boolean> unloadTask = unloadPool.submit(() -> {
459      List<RegionInfo> movedRegions = Collections.synchronizedList(new ArrayList<>());
460      try {
461        // Get Online RegionServers
462        List<ServerName> regionServers = new ArrayList<>();
463        RSGroupInfo rsgroup = admin.getRSGroup(Address.fromParts(hostname, port));
464        LOG.info("{} belongs to {}", hostname, rsgroup.getName());
465        regionServers.addAll(filterRSGroupServers(rsgroup, admin.getRegionServers()));
466        // Remove the host Region server from target Region Servers list
467        ServerName server = stripServer(regionServers, hostname, port);
468        if (server == null) {
469          LOG.info("Could not find server '{}:{}' in the set of region servers. giving up.",
470            hostname, port);
471          LOG.debug("List of region servers: {}", regionServers);
472          return false;
473        }
474        // Remove RS not present in the designated file
475        includeExcludeRegionServers(designatedFile, regionServers, true);
476
477        // Remove RS present in the exclude file
478        includeExcludeRegionServers(excludeFile, regionServers, false);
479
480        if (unloadFromRack) {
481          // remove regionServers that belong to same rack (as source host) since the goal is to
482          // unload regions from source regionServer to destination regionServers
483          // that belong to different rack only.
484          String sourceRack = rackManager.getRack(server);
485          List<String> racks = rackManager.getRack(regionServers);
486          Iterator<ServerName> iterator = regionServers.iterator();
487          int i = 0;
488          while (iterator.hasNext()) {
489            iterator.next();
490            if (racks.size() > i && racks.get(i) != null && racks.get(i).equals(sourceRack)) {
491              iterator.remove();
492            }
493            i++;
494          }
495        }
496
497        // Remove decommissioned RS
498        Set<ServerName> decommissionedRS = new HashSet<>(admin.listDecommissionedRegionServers());
499        if (CollectionUtils.isNotEmpty(decommissionedRS)) {
500          regionServers.removeIf(decommissionedRS::contains);
501          LOG.debug("Excluded RegionServers from unloading regions to because they "
502            + "are marked as decommissioned. Servers: {}", decommissionedRS);
503        }
504
505        stripMaster(regionServers);
506        if (regionServers.isEmpty()) {
507          LOG.warn("No Regions were moved - no servers available");
508          return false;
509        } else {
510          LOG.info("Available servers {}", regionServers);
511        }
512        unloadRegions(server, regionServers, movedRegions, isolateRegionIdArray);
513      } catch (Exception e) {
514        LOG.error("Error while unloading regions ", e);
515        return false;
516      } finally {
517        if (movedRegions != null) {
518          writeFile(filename, movedRegions);
519        }
520      }
521      return true;
522    });
523    return waitTaskToFinish(unloadPool, unloadTask, "unloading");
524  }
525
526  @InterfaceAudience.Private
527  Collection<ServerName> filterRSGroupServers(RSGroupInfo rsgroup,
528    Collection<ServerName> onlineServers) {
529    if (rsgroup.getName().equals(RSGroupInfo.DEFAULT_GROUP)) {
530      return onlineServers;
531    }
532    List<ServerName> serverLists = new ArrayList<>(rsgroup.getServers().size());
533    for (ServerName server : onlineServers) {
534      Address address = Address.fromParts(server.getHostname(), server.getPort());
535      if (rsgroup.containsServer(address)) {
536        serverLists.add(server);
537      }
538    }
539    return serverLists;
540  }
541
542  private void unloadRegions(ServerName server, List<ServerName> regionServers,
543    List<RegionInfo> movedRegions, List<String> isolateRegionIdArray) throws Exception {
544    while (true) {
545      List<RegionInfo> isolateRegionInfoList = Collections.synchronizedList(new ArrayList<>());
546      RegionInfo isolateRegionInfo = null;
547      if (isolateRegionIdArray != null && !isolateRegionIdArray.isEmpty()) {
548        // Region will be moved to target region server with Ack mode.
549        final ExecutorService isolateRegionPool = Executors.newFixedThreadPool(maxthreads);
550        List<Future<Boolean>> isolateRegionTaskList = new ArrayList<>();
551        List<RegionInfo> recentlyIsolatedRegion = Collections.synchronizedList(new ArrayList<>());
552        boolean allRegionOpsSuccessful = true;
553        boolean isMetaIsolated = false;
554        RegionInfo metaRegionInfo = RegionInfoBuilder.FIRST_META_REGIONINFO;
555        List<HRegionLocation> hRegionLocationRegionIsolation =
556          Collections.synchronizedList(new ArrayList<>());
557        for (String isolateRegionId : isolateRegionIdArray) {
558          if (isolateRegionId.equalsIgnoreCase(metaRegionInfo.getEncodedName())) {
559            isMetaIsolated = true;
560            continue;
561          }
562          Result result = MetaTableAccessor.scanByRegionEncodedName(conn, isolateRegionId);
563          HRegionLocation hRegionLocation =
564            MetaTableAccessor.getRegionLocation(conn, result.getRow());
565          if (hRegionLocation != null) {
566            hRegionLocationRegionIsolation.add(hRegionLocation);
567          } else {
568            LOG.error("Region " + isolateRegionId + " doesn't exists/can't fetch from"
569              + " meta...Quitting now");
570            // We only move the regions if all the regions were found.
571            allRegionOpsSuccessful = false;
572            break;
573          }
574        }
575
576        if (!allRegionOpsSuccessful) {
577          break;
578        }
579        // If hbase:meta region was isolated, then it needs to be part of isolateRegionInfoList.
580        if (isMetaIsolated) {
581          ZKWatcher zkWatcher = new ZKWatcher(conf, null, null);
582          List<HRegionLocation> result = new ArrayList<>();
583          for (String znode : zkWatcher.getMetaReplicaNodes()) {
584            String path = ZNodePaths.joinZNode(zkWatcher.getZNodePaths().baseZNode, znode);
585            int replicaId = zkWatcher.getZNodePaths().getMetaReplicaIdFromPath(path);
586            RegionState state = MetaTableLocator.getMetaRegionState(zkWatcher, replicaId);
587            result.add(new HRegionLocation(state.getRegion(), state.getServerName()));
588          }
589          ServerName metaSeverName = result.get(0).getServerName();
590          // For isolating hbase:meta, it should move explicitly in Ack mode,
591          // hence the forceMoveRegionByAck = true.
592          if (!metaSeverName.equals(server)) {
593            LOG.info("Region of hbase:meta " + metaRegionInfo.getEncodedName() + " is on server "
594              + metaSeverName + " moving to " + server);
595            submitRegionMovesWhileUnloading(metaSeverName, Collections.singletonList(server),
596              movedRegions, Collections.singletonList(metaRegionInfo), true);
597          } else {
598            LOG.info("Region of hbase:meta " + metaRegionInfo.getEncodedName() + " already exists"
599              + " on server : " + server);
600          }
601          isolateRegionInfoList.add(RegionInfoBuilder.FIRST_META_REGIONINFO);
602        }
603
604        if (!hRegionLocationRegionIsolation.isEmpty()) {
605          for (HRegionLocation hRegionLocation : hRegionLocationRegionIsolation) {
606            isolateRegionInfo = hRegionLocation.getRegion();
607            isolateRegionInfoList.add(isolateRegionInfo);
608            if (hRegionLocation.getServerName() == server) {
609              LOG.info("Region " + hRegionLocation.getRegion().getEncodedName() + " already exists"
610                + " on server : " + server.getHostname());
611            } else {
612              Future<Boolean> isolateRegionTask =
613                isolateRegionPool.submit(new MoveWithAck(conn, isolateRegionInfo,
614                  hRegionLocation.getServerName(), server, recentlyIsolatedRegion));
615              isolateRegionTaskList.add(isolateRegionTask);
616            }
617          }
618        }
619
620        if (!isolateRegionTaskList.isEmpty()) {
621          isolateRegionPool.shutdown();
622          // Now that we have fetched all the region's regionInfo, we can move them.
623          waitMoveTasksToFinish(isolateRegionPool, isolateRegionTaskList,
624            admin.getConfiguration().getLong(MOVE_WAIT_MAX_KEY, DEFAULT_MOVE_WAIT_MAX));
625
626          Set<RegionInfo> currentRegionsOnTheServer = new HashSet<>(admin.getRegions(server));
627          if (!currentRegionsOnTheServer.containsAll(isolateRegionInfoList)) {
628            // If all the regions are not online on the target server,
629            // we don't put RS in decommission mode and exit from here.
630            LOG.error("One of the Region move failed OR stuck in transition...Quitting now");
631            break;
632          }
633        } else {
634          LOG.info("All regions already exists on server : " + server.getHostname());
635        }
636        // Once region has been moved to target RS, put the target RS into decommission mode,
637        // so master doesn't assign new region to the target RS while we unload the target RS.
638        // Also pass 'offload' flag as false since we don't want master to offload the target RS.
639        List<ServerName> listOfServer = new ArrayList<>();
640        listOfServer.add(server);
641        LOG.info("Putting server : " + server.getHostname() + " in decommission/draining mode");
642        admin.decommissionRegionServers(listOfServer, false);
643      }
644      List<RegionInfo> regionsToMove = admin.getRegions(server);
645      // Remove all the regions from the online Region list, that we just isolated.
646      // This will also include hbase:meta if it was isolated.
647      regionsToMove.removeAll(isolateRegionInfoList);
648      regionsToMove.removeAll(movedRegions);
649      if (regionsToMove.isEmpty()) {
650        LOG.info("No Regions to move....Quitting now");
651        break;
652      }
653      LOG.info("Moving {} regions from {} to {} servers using {} threads .Ack Mode: {}",
654        regionsToMove.size(), this.hostname, regionServers.size(), this.maxthreads, ack);
655
656      Optional<RegionInfo> metaRegion = getMetaRegionInfoIfToBeMoved(regionsToMove);
657      if (metaRegion.isPresent()) {
658        RegionInfo meta = metaRegion.get();
659        // hbase:meta should move explicitly in Ack mode.
660        submitRegionMovesWhileUnloading(server, regionServers, movedRegions,
661          Collections.singletonList(meta), true);
662        regionsToMove.remove(meta);
663      }
664      submitRegionMovesWhileUnloading(server, regionServers, movedRegions, regionsToMove, false);
665    }
666  }
667
668  private void submitRegionMovesWhileUnloading(ServerName server, List<ServerName> regionServers,
669    List<RegionInfo> movedRegions, List<RegionInfo> regionsToMove, boolean forceMoveRegionByAck)
670    throws Exception {
671    final ExecutorService moveRegionsPool = Executors.newFixedThreadPool(this.maxthreads);
672    List<Future<Boolean>> taskList = new ArrayList<>();
673    int serverIndex = 0;
674    for (RegionInfo regionToMove : regionsToMove) {
675      // To move/isolate hbase:meta on a server, it should happen explicitly by Ack mode, hence the
676      // forceMoveRegionByAck = true.
677      if (ack || forceMoveRegionByAck) {
678        Future<Boolean> task = moveRegionsPool.submit(new MoveWithAck(conn, regionToMove, server,
679          regionServers.get(serverIndex), movedRegions));
680        taskList.add(task);
681      } else {
682        Future<Boolean> task = moveRegionsPool.submit(new MoveWithoutAck(admin, regionToMove,
683          server, regionServers.get(serverIndex), movedRegions));
684        taskList.add(task);
685      }
686      serverIndex = (serverIndex + 1) % regionServers.size();
687    }
688    moveRegionsPool.shutdown();
689    long timeoutInSeconds = regionsToMove.size()
690      * admin.getConfiguration().getLong(MOVE_WAIT_MAX_KEY, DEFAULT_MOVE_WAIT_MAX);
691    waitMoveTasksToFinish(moveRegionsPool, taskList, timeoutInSeconds);
692  }
693
694  private boolean waitTaskToFinish(ExecutorService pool, Future<Boolean> task, String operation)
695    throws TimeoutException, InterruptedException, ExecutionException {
696    pool.shutdown();
697    try {
698      if (!pool.awaitTermination((long) this.timeout, TimeUnit.SECONDS)) {
699        LOG.warn("Timed out before finishing the " + operation + " operation. Timeout: "
700          + this.timeout + "sec");
701        pool.shutdownNow();
702      }
703    } catch (InterruptedException e) {
704      pool.shutdownNow();
705      Thread.currentThread().interrupt();
706    }
707    try {
708      return task.get(5, TimeUnit.SECONDS);
709    } catch (InterruptedException e) {
710      LOG.warn("Interrupted while " + operation + " Regions on " + this.hostname, e);
711      throw e;
712    } catch (ExecutionException e) {
713      LOG.error("Error while " + operation + " regions on RegionServer " + this.hostname, e);
714      throw e;
715    }
716  }
717
718  private void waitMoveTasksToFinish(ExecutorService moveRegionsPool,
719    List<Future<Boolean>> taskList, long timeoutInSeconds) throws Exception {
720    try {
721      if (!moveRegionsPool.awaitTermination(timeoutInSeconds, TimeUnit.SECONDS)) {
722        moveRegionsPool.shutdownNow();
723      }
724    } catch (InterruptedException e) {
725      moveRegionsPool.shutdownNow();
726      Thread.currentThread().interrupt();
727    }
728    for (Future<Boolean> future : taskList) {
729      try {
730        // if even after shutdownNow threads are stuck we wait for 5 secs max
731        if (!future.get(5, TimeUnit.SECONDS)) {
732          LOG.error("Was Not able to move region....Exiting Now");
733          throw new Exception("Could not move region Exception");
734        }
735      } catch (InterruptedException e) {
736        LOG.error("Interrupted while waiting for Thread to Complete " + e.getMessage(), e);
737        throw e;
738      } catch (ExecutionException e) {
739        boolean ignoreFailure = ignoreRegionMoveFailure(e);
740        if (ignoreFailure) {
741          LOG.debug("Ignore region move failure, it might have been split/merged.", e);
742        } else {
743          LOG.error("Got Exception From Thread While moving region {}", e.getMessage(), e);
744          throw e;
745        }
746      } catch (CancellationException e) {
747        LOG.error("Thread for moving region cancelled. Timeout for cancellation:" + timeoutInSeconds
748          + "secs", e);
749        throw e;
750      }
751    }
752  }
753
754  private boolean ignoreRegionMoveFailure(ExecutionException e) {
755    boolean ignoreFailure = false;
756    if (e.getCause() instanceof UnknownRegionException) {
757      // region does not exist anymore
758      ignoreFailure = true;
759    } else if (
760      e.getCause() instanceof DoNotRetryRegionException && e.getCause().getMessage() != null
761        && e.getCause().getMessage()
762          .contains(AssignmentManager.UNEXPECTED_STATE_REGION + "state=SPLIT,")
763    ) {
764      // region is recently split
765      ignoreFailure = true;
766    }
767    return ignoreFailure;
768  }
769
770  private ServerName getTargetServer() throws Exception {
771    ServerName server = null;
772    int maxWaitInSeconds =
773      admin.getConfiguration().getInt(SERVERSTART_WAIT_MAX_KEY, DEFAULT_SERVERSTART_WAIT_MAX);
774    long maxWait = EnvironmentEdgeManager.currentTime() + maxWaitInSeconds * 1000;
775    while (EnvironmentEdgeManager.currentTime() < maxWait) {
776      try {
777        List<ServerName> regionServers = new ArrayList<>();
778        regionServers.addAll(admin.getRegionServers());
779        // Remove the host Region server from target Region Servers list
780        server = stripServer(regionServers, hostname, port);
781        if (server != null) {
782          break;
783        } else {
784          LOG.warn("Server " + hostname + ":" + port + " is not up yet, waiting");
785        }
786      } catch (IOException e) {
787        LOG.warn("Could not get list of region servers", e);
788      }
789      Thread.sleep(500);
790    }
791    if (server == null) {
792      LOG.error("Server " + hostname + ":" + port + " is not up. Giving up.");
793      throw new Exception("Server " + hostname + ":" + port + " to load regions not online");
794    }
795    return server;
796  }
797
798  private List<RegionInfo> readRegionsFromFile(String filename) throws IOException {
799    List<RegionInfo> regions = new ArrayList<>();
800    File f = new File(filename);
801    if (!f.exists()) {
802      return regions;
803    }
804    try (
805      DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(f)))) {
806      int numRegions = dis.readInt();
807      int index = 0;
808      while (index < numRegions) {
809        regions.add(RegionInfo.parseFromOrNull(Bytes.readByteArray(dis)));
810        index++;
811      }
812    } catch (IOException e) {
813      LOG.error("Error while reading regions from file:" + filename, e);
814      throw e;
815    }
816    return regions;
817  }
818
819  /**
820   * Write the number of regions moved in the first line followed by regions moved in subsequent
821   * lines
822   */
823  private void writeFile(String filename, List<RegionInfo> movedRegions) throws IOException {
824    try (DataOutputStream dos =
825      new DataOutputStream(new BufferedOutputStream(new FileOutputStream(filename)))) {
826      dos.writeInt(movedRegions.size());
827      for (RegionInfo region : movedRegions) {
828        Bytes.writeByteArray(dos, RegionInfo.toByteArray(region));
829      }
830    } catch (IOException e) {
831      LOG.error("ERROR: Was Not able to write regions moved to output file but moved "
832        + movedRegions.size() + " regions", e);
833      throw e;
834    }
835  }
836
837  private void deleteFile(String filename) {
838    File f = new File(filename);
839    if (f.exists()) {
840      f.delete();
841    }
842  }
843
844  /**
845   * @param filename The file should have 'host:port' per line
846   * @return List of servers from the file in format 'hostname:port'.
847   */
848  private List<String> readServersFromFile(String filename) throws IOException {
849    List<String> servers = new ArrayList<>();
850    if (filename != null) {
851      try {
852        Files.readAllLines(Paths.get(filename)).stream().map(String::trim)
853          .filter(((Predicate<String>) String::isEmpty).negate()).map(String::toLowerCase)
854          .forEach(servers::add);
855      } catch (IOException e) {
856        LOG.error("Exception while reading servers from file,", e);
857        throw e;
858      }
859    }
860    return servers;
861  }
862
863  /**
864   * Designates or excludes the servername whose hostname and port portion matches the list given in
865   * the file. Example:<br>
866   * If you want to designated RSs, suppose designatedFile has RS1, regionServers has RS1, RS2 and
867   * RS3. When we call includeExcludeRegionServers(designatedFile, regionServers, true), RS2 and RS3
868   * are removed from regionServers list so that regions can move to only RS1. If you want to
869   * exclude RSs, suppose excludeFile has RS1, regionServers has RS1, RS2 and RS3. When we call
870   * includeExcludeRegionServers(excludeFile, servers, false), RS1 is removed from regionServers
871   * list so that regions can move to only RS2 and RS3.
872   */
873  private void includeExcludeRegionServers(String fileName, List<ServerName> regionServers,
874    boolean isInclude) throws IOException {
875    if (fileName != null) {
876      List<String> servers = readServersFromFile(fileName);
877      if (servers.isEmpty()) {
878        LOG.warn("No servers provided in the file: {}." + fileName);
879        return;
880      }
881      Iterator<ServerName> i = regionServers.iterator();
882      while (i.hasNext()) {
883        String rs = i.next().getServerName();
884        String rsPort = rs.split(ServerName.SERVERNAME_SEPARATOR)[0].toLowerCase() + ":"
885          + rs.split(ServerName.SERVERNAME_SEPARATOR)[1];
886        if (isInclude != servers.contains(rsPort)) {
887          i.remove();
888        }
889      }
890    }
891  }
892
893  /**
894   * Exclude master from list of RSs to move regions to
895   */
896  private void stripMaster(List<ServerName> regionServers) throws IOException {
897    ServerName master = admin.getClusterMetrics(EnumSet.of(Option.MASTER)).getMasterName();
898    stripServer(regionServers, master.getHostname(), master.getPort());
899  }
900
901  /**
902   * Remove the servername whose hostname and port portion matches from the passed array of servers.
903   * Returns as side-effect the servername removed.
904   * @return server removed from list of Region Servers
905   */
906  private ServerName stripServer(List<ServerName> regionServers, String hostname, int port) {
907    for (Iterator<ServerName> iter = regionServers.iterator(); iter.hasNext();) {
908      ServerName server = iter.next();
909      if (
910        server.getAddress().getHostName().equalsIgnoreCase(hostname)
911          && server.getAddress().getPort() == port
912      ) {
913        iter.remove();
914        return server;
915      }
916    }
917    return null;
918  }
919
920  @Override
921  protected void addOptions() {
922    this.addRequiredOptWithArg("r", "regionserverhost", "region server <hostname>|<hostname:port>");
923    this.addRequiredOptWithArg("o", "operation",
924      "Expected: load/unload/unload_from_rack/isolate_regions");
925    this.addOptWithArg("m", "maxthreads",
926      "Define the maximum number of threads to use to unload and reload the regions");
927    this.addOptWithArg("i", "isolateRegionIds",
928      "Comma separated list of Region IDs hash to isolate on a RegionServer and put region server"
929        + " in draining mode. This option should only be used with '-o isolate_regions'."
930        + " By putting region server in decommission/draining mode, master can't assign any"
931        + " new region on this server. If one or more regions are not found OR failed to isolate"
932        + " successfully, utility will exist without putting RS in draining/decommission mode."
933        + " Ex. --isolateRegionIds id1,id2,id3 OR -i id1,id2,id3");
934    this.addOptWithArg("x", "excludefile",
935      "File with <hostname:port> per line to exclude as unload targets; default excludes only "
936        + "target host; useful for rack decommisioning.");
937    this.addOptWithArg("d", "designatedfile",
938      "File with <hostname:port> per line as unload targets;" + "default is all online hosts");
939    this.addOptWithArg("f", "filename",
940      "File to save regions list into unloading, or read from loading; "
941        + "default /tmp/<usernamehostname:port>");
942    this.addOptNoArg("n", "noack",
943      "Turn on No-Ack mode(default: false) which won't check if region is online on target "
944        + "RegionServer, hence best effort. This is more performant in unloading and loading "
945        + "but might lead to region being unavailable for some time till master reassigns it "
946        + "in case the move failed");
947    this.addOptWithArg("t", "timeout", "timeout in seconds after which the tool will exit "
948      + "irrespective of whether it finished or not;default Integer.MAX_VALUE");
949  }
950
951  @Override
952  protected void processOptions(CommandLine cmd) {
953    String hostname = cmd.getOptionValue("r");
954    rmbuilder = new RegionMoverBuilder(hostname, getConf());
955    this.loadUnload = cmd.getOptionValue("o").toLowerCase(Locale.ROOT);
956    if (cmd.hasOption('m')) {
957      rmbuilder.maxthreads(Integer.parseInt(cmd.getOptionValue('m')));
958    }
959    if (this.loadUnload.equals("isolate_regions") && cmd.hasOption("isolateRegionIds")) {
960      rmbuilder
961        .isolateRegionIdArray(Arrays.asList(cmd.getOptionValue("isolateRegionIds").split(",")));
962    }
963    if (cmd.hasOption('n')) {
964      rmbuilder.ack(false);
965    }
966    if (cmd.hasOption('f')) {
967      rmbuilder.filename(cmd.getOptionValue('f'));
968    }
969    if (cmd.hasOption('x')) {
970      rmbuilder.excludeFile(cmd.getOptionValue('x'));
971    }
972    if (cmd.hasOption('d')) {
973      rmbuilder.designatedFile(cmd.getOptionValue('d'));
974    }
975    if (cmd.hasOption('t')) {
976      rmbuilder.timeout(Integer.parseInt(cmd.getOptionValue('t')));
977    }
978  }
979
980  @Override
981  protected int doWork() throws Exception {
982    boolean success;
983    try (RegionMover rm = rmbuilder.build()) {
984      if (loadUnload.equalsIgnoreCase("load")) {
985        success = rm.load();
986      } else if (loadUnload.equalsIgnoreCase("unload")) {
987        success = rm.unload();
988      } else if (loadUnload.equalsIgnoreCase("unload_from_rack")) {
989        success = rm.unloadFromRack();
990      } else if (loadUnload.equalsIgnoreCase("isolate_regions")) {
991        if (rm.isolateRegionIdArray != null && !rm.isolateRegionIdArray.isEmpty()) {
992          success = rm.isolateRegions();
993        } else {
994          LOG.error("Missing -i/--isolate_regions option with '-o isolate_regions' option");
995          LOG.error("Use -h or --help for usage instructions");
996          printUsage();
997          success = false;
998        }
999      } else {
1000        printUsage();
1001        success = false;
1002      }
1003    }
1004    return (success ? 0 : 1);
1005  }
1006
1007  public static void main(String[] args) {
1008    try (RegionMover mover = new RegionMover()) {
1009      mover.doStaticMain(args);
1010    }
1011  }
1012}