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.regionserver.snapshot;
019
020import java.io.IOException;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Iterator;
024import java.util.List;
025import java.util.concurrent.Callable;
026import java.util.concurrent.ExecutionException;
027import java.util.concurrent.ExecutorCompletionService;
028import java.util.concurrent.Future;
029import java.util.concurrent.ThreadPoolExecutor;
030import java.util.concurrent.TimeUnit;
031import org.apache.hadoop.conf.Configuration;
032import org.apache.hadoop.hbase.Abortable;
033import org.apache.hadoop.hbase.DroppedSnapshotException;
034import org.apache.hadoop.hbase.HBaseInterfaceAudience;
035import org.apache.hadoop.hbase.TableName;
036import org.apache.hadoop.hbase.client.RegionReplicaUtil;
037import org.apache.hadoop.hbase.errorhandling.ForeignException;
038import org.apache.hadoop.hbase.errorhandling.ForeignExceptionDispatcher;
039import org.apache.hadoop.hbase.master.snapshot.MasterSnapshotVerifier;
040import org.apache.hadoop.hbase.master.snapshot.SnapshotManager;
041import org.apache.hadoop.hbase.procedure.ProcedureMember;
042import org.apache.hadoop.hbase.procedure.ProcedureMemberRpcs;
043import org.apache.hadoop.hbase.procedure.RegionServerProcedureManager;
044import org.apache.hadoop.hbase.procedure.Subprocedure;
045import org.apache.hadoop.hbase.procedure.SubprocedureFactory;
046import org.apache.hadoop.hbase.procedure.ZKProcedureMemberRpcs;
047import org.apache.hadoop.hbase.regionserver.HRegion;
048import org.apache.hadoop.hbase.regionserver.HRegionServer;
049import org.apache.hadoop.hbase.regionserver.RegionServerServices;
050import org.apache.hadoop.hbase.util.Threads;
051import org.apache.hadoop.hbase.zookeeper.ZKWatcher;
052import org.apache.yetus.audience.InterfaceAudience;
053import org.apache.yetus.audience.InterfaceStability;
054import org.apache.zookeeper.KeeperException;
055import org.slf4j.Logger;
056import org.slf4j.LoggerFactory;
057
058import org.apache.hbase.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder;
059
060import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotDescription;
061
062/**
063 * This manager class handles the work dealing with snapshots for a {@link HRegionServer}.
064 * <p>
065 * This provides the mechanism necessary to kick off a online snapshot specific {@link Subprocedure}
066 * that is responsible for the regions being served by this region server. If any failures occur
067 * with the subprocedure, the RegionSeverSnapshotManager's subprocedure handler,
068 * {@link ProcedureMember}, notifies the master's ProcedureCoordinator to abort all others.
069 * <p>
070 * On startup, requires {@link #start()} to be called.
071 * <p>
072 * On shutdown, requires {@link #stop(boolean)} to be called
073 */
074@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG)
075@InterfaceStability.Unstable
076public class RegionServerSnapshotManager extends RegionServerProcedureManager {
077  private static final Logger LOG = LoggerFactory.getLogger(RegionServerSnapshotManager.class);
078
079  /** Maximum number of snapshot region tasks that can run concurrently */
080  private static final String CONCURENT_SNAPSHOT_TASKS_KEY =
081    "hbase.snapshot.region.concurrentTasks";
082  private static final int DEFAULT_CONCURRENT_SNAPSHOT_TASKS = 3;
083
084  /** Conf key for number of request threads to start snapshots on regionservers */
085  public static final String SNAPSHOT_REQUEST_THREADS_KEY = "hbase.snapshot.region.pool.threads";
086  /** # of threads for snapshotting regions on the rs. */
087  public static final int SNAPSHOT_REQUEST_THREADS_DEFAULT = 10;
088
089  /**
090   * Conf key for max time to keep threads in snapshot request pool waiting. The configured value
091   * must be greater than 0.
092   */
093  public static final String SNAPSHOT_TIMEOUT_MILLIS_KEY = "hbase.snapshot.region.timeout";
094  /** Keep threads alive in request pool for max of 300 seconds */
095  public static final long SNAPSHOT_TIMEOUT_MILLIS_DEFAULT = 5 * 60000;
096
097  /** Conf key for millis between checks to see if snapshot completed or if there are errors */
098  public static final String SNAPSHOT_REQUEST_WAKE_MILLIS_KEY =
099    "hbase.snapshot.region.wakefrequency";
100  /** Default amount of time to check for errors while regions finish snapshotting */
101  private static final long SNAPSHOT_REQUEST_WAKE_MILLIS_DEFAULT = 500;
102
103  private RegionServerServices rss;
104  private ProcedureMemberRpcs memberRpcs;
105  private ProcedureMember member;
106
107  static long getSnapshotTimeoutMillis(Configuration conf) {
108    long timeoutMillis = conf.getLong(SNAPSHOT_TIMEOUT_MILLIS_KEY, SNAPSHOT_TIMEOUT_MILLIS_DEFAULT);
109    if (timeoutMillis > 0) {
110      return timeoutMillis;
111    }
112    LOG.warn("{} must be greater than 0, but is {}. Using default value {}",
113      SNAPSHOT_TIMEOUT_MILLIS_KEY, timeoutMillis, SNAPSHOT_TIMEOUT_MILLIS_DEFAULT);
114    return SNAPSHOT_TIMEOUT_MILLIS_DEFAULT;
115  }
116
117  /**
118   * Exposed for testing.
119   * @param conf       HBase configuration.
120   * @param parent     parent running the snapshot handler
121   * @param memberRpc  use specified memberRpc instance
122   * @param procMember use specified ProcedureMember
123   */
124  RegionServerSnapshotManager(Configuration conf, HRegionServer parent,
125    ProcedureMemberRpcs memberRpc, ProcedureMember procMember) {
126    this.rss = parent;
127    this.memberRpcs = memberRpc;
128    this.member = procMember;
129  }
130
131  public RegionServerSnapshotManager() {
132  }
133
134  /**
135   * Start accepting snapshot requests.
136   */
137  @Override
138  public void start() {
139    LOG.debug("Start Snapshot Manager " + rss.getServerName().toString());
140    this.memberRpcs.start(rss.getServerName().toString(), member);
141  }
142
143  /**
144   * Close <tt>this</tt> and all running snapshot tasks
145   * @param force forcefully stop all running tasks
146   */
147  @Override
148  public void stop(boolean force) throws IOException {
149    String mode = force ? "abruptly" : "gracefully";
150    LOG.info("Stopping RegionServerSnapshotManager " + mode + ".");
151
152    try {
153      this.member.close();
154    } finally {
155      this.memberRpcs.close();
156    }
157  }
158
159  /**
160   * If in a running state, creates the specified subprocedure for handling an online snapshot.
161   * Because this gets the local list of regions to snapshot and not the set the master had, there
162   * is a possibility of a race where regions may be missed. This detected by the master in the
163   * snapshot verification step.
164   * @return Subprocedure to submit to the ProcedureMember.
165   */
166  public Subprocedure buildSubprocedure(SnapshotDescription snapshot) {
167
168    // don't run a snapshot if the parent is stop(ping)
169    if (rss.isStopping() || rss.isStopped()) {
170      throw new IllegalStateException(
171        "Can't start snapshot on RS: " + rss.getServerName() + ", because stopping/stopped!");
172    }
173
174    // check to see if this server is hosting any regions for the snapshots
175    // check to see if we have regions for the snapshot
176    List<HRegion> involvedRegions;
177    try {
178      involvedRegions = getRegionsToSnapshot(snapshot);
179    } catch (IOException e1) {
180      throw new IllegalStateException("Failed to figure out if we should handle a snapshot - "
181        + "something has gone awry with the online regions.", e1);
182    }
183
184    // We need to run the subprocedure even if we have no relevant regions. The coordinator
185    // expects participation in the procedure and without sending message the snapshot attempt
186    // will hang and fail.
187
188    LOG.debug("Launching subprocedure for snapshot " + snapshot.getName() + " from table "
189      + snapshot.getTable() + " type " + snapshot.getType());
190    ForeignExceptionDispatcher exnDispatcher = new ForeignExceptionDispatcher(snapshot.getName());
191    Configuration conf = rss.getConfiguration();
192    long timeoutMillis = getSnapshotTimeoutMillis(conf);
193    long wakeMillis =
194      conf.getLong(SNAPSHOT_REQUEST_WAKE_MILLIS_KEY, SNAPSHOT_REQUEST_WAKE_MILLIS_DEFAULT);
195
196    switch (snapshot.getType()) {
197      case FLUSH:
198        SnapshotSubprocedurePool taskManager =
199          new SnapshotSubprocedurePool(rss.getServerName().toString(), conf, rss);
200        return new FlushSnapshotSubprocedure(member, exnDispatcher, wakeMillis, timeoutMillis,
201          involvedRegions, snapshot, taskManager);
202      case SKIPFLUSH:
203        /*
204         * This is to take an online-snapshot without force a coordinated flush to prevent pause The
205         * snapshot type is defined inside the snapshot description. FlushSnapshotSubprocedure
206         * should be renamed to distributedSnapshotSubprocedure, and the flush() behavior can be
207         * turned on/off based on the flush type. To minimized the code change, class name is not
208         * changed.
209         */
210        SnapshotSubprocedurePool taskManager2 =
211          new SnapshotSubprocedurePool(rss.getServerName().toString(), conf, rss);
212        return new FlushSnapshotSubprocedure(member, exnDispatcher, wakeMillis, timeoutMillis,
213          involvedRegions, snapshot, taskManager2);
214
215      default:
216        throw new UnsupportedOperationException("Unrecognized snapshot type:" + snapshot.getType());
217    }
218  }
219
220  /**
221   * Determine if the snapshot should be handled on this server NOTE: This is racy -- the master
222   * expects a list of regionservers. This means if a region moves somewhere between the calls we'll
223   * miss some regions. For example, a region move during a snapshot could result in a region to be
224   * skipped or done twice. This is manageable because the {@link MasterSnapshotVerifier} will
225   * double check the region lists after the online portion of the snapshot completes and will
226   * explicitly fail the snapshot.
227   * @return the list of online regions. Empty list is returned if no regions are responsible for
228   *         the given snapshot.
229   */
230  private List<HRegion> getRegionsToSnapshot(SnapshotDescription snapshot) throws IOException {
231    List<HRegion> onlineRegions =
232      (List<HRegion>) rss.getRegions(TableName.valueOf(snapshot.getTable()));
233    Iterator<HRegion> iterator = onlineRegions.iterator();
234    // remove the non-default regions
235    while (iterator.hasNext()) {
236      HRegion r = iterator.next();
237      if (!RegionReplicaUtil.isDefaultReplica(r.getRegionInfo())) {
238        iterator.remove();
239      }
240    }
241    return onlineRegions;
242  }
243
244  /**
245   * Build the actual snapshot runner that will do all the 'hard' work
246   */
247  public class SnapshotSubprocedureBuilder implements SubprocedureFactory {
248
249    @Override
250    public Subprocedure buildSubprocedure(String name, byte[] data) {
251      try {
252        // unwrap the snapshot information
253        SnapshotDescription snapshot = SnapshotDescription.parseFrom(data);
254        return RegionServerSnapshotManager.this.buildSubprocedure(snapshot);
255      } catch (IOException e) {
256        throw new IllegalArgumentException("Could not read snapshot information from request.");
257      }
258    }
259
260  }
261
262  /**
263   * We use the SnapshotSubprocedurePool, a class specific thread pool instead of
264   * {@link org.apache.hadoop.hbase.executor.ExecutorService}. It uses a
265   * {@link java.util.concurrent.ExecutorCompletionService} which provides queuing of completed
266   * tasks which lets us efficiently cancel pending tasks upon the earliest operation failures.
267   * HBase's ExecutorService (different from {@link java.util.concurrent.ExecutorService}) isn't
268   * really built for coordinated tasks where multiple threads as part of one larger task. In RS's
269   * the HBase Executor services are only used for open and close and not other threadpooled
270   * operations such as compactions and replication sinks.
271   */
272  static class SnapshotSubprocedurePool {
273    private final Abortable abortable;
274    private final ExecutorCompletionService<Void> taskPool;
275    private final ThreadPoolExecutor executor;
276    private volatile boolean stopped;
277    private final List<Future<Void>> futures = new ArrayList<>();
278    private final String name;
279
280    SnapshotSubprocedurePool(String name, Configuration conf, Abortable abortable) {
281      this.abortable = abortable;
282      // configure the executor service
283      long keepAlive = getSnapshotTimeoutMillis(conf);
284      int threads = conf.getInt(CONCURENT_SNAPSHOT_TASKS_KEY, DEFAULT_CONCURRENT_SNAPSHOT_TASKS);
285      this.name = name;
286      executor = Threads.getBoundedCachedThreadPool(threads, keepAlive, TimeUnit.MILLISECONDS,
287        new ThreadFactoryBuilder().setNameFormat("rs(" + name + ")-snapshot-pool-%d")
288          .setDaemon(true).setUncaughtExceptionHandler(Threads.LOGGING_EXCEPTION_HANDLER).build());
289      taskPool = new ExecutorCompletionService<>(executor);
290    }
291
292    boolean hasTasks() {
293      return futures.size() != 0;
294    }
295
296    /**
297     * Submit a task to the pool. NOTE: all must be submitted before you can safely
298     * {@link #waitForOutstandingTasks()}. This version does not support issuing tasks from multiple
299     * concurrent table snapshots requests.
300     */
301    void submitTask(final Callable<Void> task) {
302      Future<Void> f = this.taskPool.submit(task);
303      futures.add(f);
304    }
305
306    /**
307     * Wait for all of the currently outstanding tasks submitted via {@link #submitTask(Callable)}.
308     * This *must* be called after all tasks are submitted via submitTask.
309     * @return <tt>true</tt> on success, <tt>false</tt> otherwise
310     * @throws SnapshotCreationException if the snapshot failed while we were waiting
311     */
312    boolean waitForOutstandingTasks() throws ForeignException, InterruptedException {
313      LOG.debug("Waiting for local region snapshots to finish.");
314
315      int sz = futures.size();
316      try {
317        // Using the completion service to process the futures that finish first first.
318        for (int i = 0; i < sz; i++) {
319          Future<Void> f = taskPool.take();
320          f.get();
321          if (!futures.remove(f)) {
322            LOG.warn("unexpected future" + f);
323          }
324          LOG.debug("Completed " + (i + 1) + "/" + sz + " local region snapshots.");
325        }
326        LOG.debug("Completed " + sz + " local region snapshots.");
327        return true;
328      } catch (InterruptedException e) {
329        LOG.warn("Got InterruptedException in SnapshotSubprocedurePool", e);
330        if (!stopped) {
331          Thread.currentThread().interrupt();
332          throw new ForeignException("SnapshotSubprocedurePool", e);
333        }
334        // we are stopped so we can just exit.
335      } catch (ExecutionException e) {
336        Throwable cause = e.getCause();
337        if (cause instanceof ForeignException) {
338          LOG.warn("Rethrowing ForeignException from SnapshotSubprocedurePool", e);
339          throw (ForeignException) e.getCause();
340        } else if (cause instanceof DroppedSnapshotException) {
341          // we have to abort the region server according to contract of flush
342          abortable.abort("Received DroppedSnapshotException, aborting", cause);
343        }
344        LOG.warn("Got Exception in SnapshotSubprocedurePool", e);
345        throw new ForeignException(name, e.getCause());
346      } finally {
347        cancelTasks();
348      }
349      return false;
350    }
351
352    /**
353     * This attempts to cancel out all pending and in progress tasks (interruptions issues)
354     */
355    void cancelTasks() throws InterruptedException {
356      Collection<Future<Void>> tasks = futures;
357      LOG.debug("cancelling " + tasks.size() + " tasks for snapshot " + name);
358      for (Future<Void> f : tasks) {
359        // TODO Ideally we'd interrupt hbase threads when we cancel. However it seems that there
360        // are places in the HBase code where row/region locks are taken and not released in a
361        // finally block. Thus we cancel without interrupting. Cancellations will be slower to
362        // complete but we won't suffer from unreleased locks due to poor code discipline.
363        f.cancel(false);
364      }
365
366      // evict remaining tasks and futures from taskPool.
367      futures.clear();
368      while (taskPool.poll() != null) {
369      }
370      stop();
371    }
372
373    /**
374     * Abruptly shutdown the thread pool. Call when exiting a region server.
375     */
376    void stop() {
377      if (this.stopped) return;
378
379      this.stopped = true;
380      this.executor.shutdown();
381    }
382  }
383
384  /**
385   * Create a default snapshot handler - uses a zookeeper based member controller.
386   * @param rss region server running the handler
387   * @throws KeeperException if the zookeeper cluster cannot be reached
388   */
389  @Override
390  public void initialize(RegionServerServices rss) throws KeeperException {
391    this.rss = rss;
392    ZKWatcher zkw = rss.getZooKeeper();
393    this.memberRpcs =
394      new ZKProcedureMemberRpcs(zkw, SnapshotManager.ONLINE_SNAPSHOT_CONTROLLER_DESCRIPTION);
395
396    // read in the snapshot request configuration properties
397    Configuration conf = rss.getConfiguration();
398    long keepAlive = getSnapshotTimeoutMillis(conf);
399    int opThreads = conf.getInt(SNAPSHOT_REQUEST_THREADS_KEY, SNAPSHOT_REQUEST_THREADS_DEFAULT);
400
401    // create the actual snapshot procedure member
402    ThreadPoolExecutor pool =
403      ProcedureMember.defaultPool(rss.getServerName().toString(), opThreads, keepAlive);
404    this.member = new ProcedureMember(memberRpcs, pool, new SnapshotSubprocedureBuilder());
405  }
406
407  @Override
408  public String getProcedureSignature() {
409    return SnapshotManager.ONLINE_SNAPSHOT_CONTROLLER_DESCRIPTION;
410  }
411
412}