View Javadoc

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  
19  package org.apache.hadoop.hbase.replication.regionserver;
20  
21  import java.io.IOException;
22  import java.net.ConnectException;
23  import java.net.SocketTimeoutException;
24  import java.net.UnknownHostException;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.concurrent.Callable;
31  import java.util.concurrent.CompletionService;
32  import java.util.concurrent.ExecutionException;
33  import java.util.concurrent.ExecutorCompletionService;
34  import java.util.concurrent.Future;
35  import java.util.concurrent.LinkedBlockingQueue;
36  import java.util.concurrent.ThreadPoolExecutor;
37  import java.util.concurrent.TimeUnit;
38  
39  import org.apache.commons.lang.StringUtils;
40  import org.apache.commons.logging.Log;
41  import org.apache.commons.logging.LogFactory;
42  import org.apache.hadoop.conf.Configuration;
43  import org.apache.hadoop.fs.Path;
44  import org.apache.hadoop.hbase.Abortable;
45  import org.apache.hadoop.hbase.HBaseConfiguration;
46  import org.apache.hadoop.hbase.HConstants;
47  import org.apache.hadoop.hbase.TableNotFoundException;
48  import org.apache.hadoop.hbase.classification.InterfaceAudience;
49  import org.apache.hadoop.hbase.client.HConnection;
50  import org.apache.hadoop.hbase.client.HConnectionManager;
51  import org.apache.hadoop.hbase.ipc.RpcServer;
52  import org.apache.hadoop.hbase.protobuf.ReplicationProtbufUtil;
53  import org.apache.hadoop.hbase.protobuf.generated.AdminProtos.AdminService.BlockingInterface;
54  import org.apache.hadoop.hbase.replication.HBaseReplicationEndpoint;
55  import org.apache.hadoop.hbase.replication.ReplicationPeer.PeerState;
56  import org.apache.hadoop.hbase.replication.regionserver.ReplicationSinkManager.SinkPeer;
57  import org.apache.hadoop.hbase.util.Bytes;
58  import org.apache.hadoop.hbase.util.FSUtils;
59  import org.apache.hadoop.hbase.wal.WAL.Entry;
60  import org.apache.hadoop.ipc.RemoteException;
61  
62  import com.google.common.annotations.VisibleForTesting;
63  
64  /**
65   * A {@link org.apache.hadoop.hbase.replication.ReplicationEndpoint} 
66   * implementation for replicating to another HBase cluster.
67   * For the slave cluster it selects a random number of peers
68   * using a replication ratio. For example, if replication ration = 0.1
69   * and slave cluster has 100 region servers, 10 will be selected.
70   * <p>
71   * A stream is considered down when we cannot contact a region server on the
72   * peer cluster for more than 55 seconds by default.
73   * </p>
74   */
75  @InterfaceAudience.Private
76  public class HBaseInterClusterReplicationEndpoint extends HBaseReplicationEndpoint {
77  
78    private static final Log LOG = LogFactory.getLog(HBaseInterClusterReplicationEndpoint.class);
79  
80    private static final long DEFAULT_MAX_TERMINATION_WAIT_MULTIPLIER = 2;
81  
82    private HConnection conn;
83    private Configuration conf;
84    // How long should we sleep for each retry
85    private long sleepForRetries;
86    // Maximum number of retries before taking bold actions
87    private int maxRetriesMultiplier;
88    // Socket timeouts require even bolder actions since we don't want to DDOS
89    private int socketTimeoutMultiplier;
90    // Amount of time for shutdown to wait for all tasks to complete
91    private long maxTerminationWait;
92    // Size limit for replication RPCs, in bytes
93    private int replicationRpcLimit;
94    //Metrics for this source
95    private MetricsSource metrics;
96    // Handles connecting to peer region servers
97    private ReplicationSinkManager replicationSinkMgr;
98    private boolean peersSelected = false;
99    private String replicationClusterId = "";
100   private ThreadPoolExecutor exec;
101   private int maxThreads;
102   private Path baseNamespaceDir;
103   private Path hfileArchiveDir;
104   private boolean replicationBulkLoadDataEnabled;
105   private Abortable abortable;
106 
107   @Override
108   public void init(Context context) throws IOException {
109     super.init(context);
110     this.conf = HBaseConfiguration.create(ctx.getConfiguration());
111     decorateConf();
112     this.maxRetriesMultiplier = this.conf.getInt("replication.source.maxretriesmultiplier", 300);
113     this.socketTimeoutMultiplier = this.conf.getInt("replication.source.socketTimeoutMultiplier",
114         maxRetriesMultiplier);
115     // A Replicator job is bound by the RPC timeout. We will wait this long for all Replicator
116     // tasks to terminate when doStop() is called.
117     long maxTerminationWaitMultiplier = this.conf.getLong(
118         "replication.source.maxterminationmultiplier",
119         DEFAULT_MAX_TERMINATION_WAIT_MULTIPLIER);
120     this.maxTerminationWait = maxTerminationWaitMultiplier *
121         this.conf.getLong(HConstants.HBASE_RPC_TIMEOUT_KEY, HConstants.DEFAULT_HBASE_RPC_TIMEOUT);
122     // TODO: This connection is replication specific or we should make it particular to
123     // replication and make replication specific settings such as compression or codec to use
124     // passing Cells.
125     this.conn = HConnectionManager.createConnection(this.conf);
126     this.sleepForRetries =
127         this.conf.getLong("replication.source.sleepforretries", 1000);
128     this.metrics = context.getMetrics();
129     // ReplicationQueueInfo parses the peerId out of the znode for us
130     this.replicationSinkMgr = new ReplicationSinkManager(conn, ctx.getPeerId(), this, this.conf);
131     // per sink thread pool
132     this.maxThreads = this.conf.getInt(HConstants.REPLICATION_SOURCE_MAXTHREADS_KEY,
133       HConstants.REPLICATION_SOURCE_MAXTHREADS_DEFAULT);
134     this.exec = new ThreadPoolExecutor(maxThreads, maxThreads, 60, TimeUnit.SECONDS,
135         new LinkedBlockingQueue<Runnable>());
136     this.exec.allowCoreThreadTimeOut(true);
137     this.abortable = ctx.getAbortable();
138     // Set the size limit for replication RPCs to 95% of the max request size.
139     // We could do with less slop if we have an accurate estimate of encoded size. Being
140     // conservative for now.
141     this.replicationRpcLimit = (int)(0.95 * (double)conf.getLong(RpcServer.MAX_REQUEST_SIZE,
142       RpcServer.DEFAULT_MAX_REQUEST_SIZE));
143 
144     this.replicationBulkLoadDataEnabled =
145         conf.getBoolean(HConstants.REPLICATION_BULKLOAD_ENABLE_KEY,
146           HConstants.REPLICATION_BULKLOAD_ENABLE_DEFAULT);
147     if (this.replicationBulkLoadDataEnabled) {
148       replicationClusterId = this.conf.get(HConstants.REPLICATION_CLUSTER_ID);
149     }
150     // Construct base namespace directory and hfile archive directory path
151     Path rootDir = FSUtils.getRootDir(conf);
152     Path baseNSDir = new Path(HConstants.BASE_NAMESPACE_DIR);
153     baseNamespaceDir = new Path(rootDir, baseNSDir);
154     hfileArchiveDir = new Path(rootDir, new Path(HConstants.HFILE_ARCHIVE_DIRECTORY, baseNSDir));
155   }
156 
157   private void decorateConf() {
158     String replicationCodec = this.conf.get(HConstants.REPLICATION_CODEC_CONF_KEY);
159     if (StringUtils.isNotEmpty(replicationCodec)) {
160       this.conf.set(HConstants.RPC_CODEC_CONF_KEY, replicationCodec);
161     }
162   }
163 
164   private void connectToPeers() {
165     getRegionServers();
166 
167     int sleepMultiplier = 1;
168 
169     // Connect to peer cluster first, unless we have to stop
170     while (this.isRunning() && replicationSinkMgr.getNumSinks() == 0) {
171       replicationSinkMgr.chooseSinks();
172       if (this.isRunning() && replicationSinkMgr.getNumSinks() == 0) {
173         if (sleepForRetries("Waiting for peers", sleepMultiplier)) {
174           sleepMultiplier++;
175         }
176       }
177     }
178   }
179 
180   /**
181    * Do the sleeping logic
182    * @param msg Why we sleep
183    * @param sleepMultiplier by how many times the default sleeping time is augmented
184    * @return True if <code>sleepMultiplier</code> is &lt; <code>maxRetriesMultiplier</code>
185    */
186   protected boolean sleepForRetries(String msg, int sleepMultiplier) {
187     try {
188       if (LOG.isTraceEnabled()) {
189         LOG.trace(msg + ", sleeping " + sleepForRetries + " times " + sleepMultiplier);
190       }
191       Thread.sleep(this.sleepForRetries * sleepMultiplier);
192     } catch (InterruptedException e) {
193       LOG.debug("Interrupted while sleeping between retries");
194     }
195     return sleepMultiplier < maxRetriesMultiplier;
196   }
197 
198   private List<List<Entry>> createBatches(final List<Entry> entries) {
199     int numSinks = Math.max(replicationSinkMgr.getNumSinks(), 1);
200     int n = Math.min(Math.min(this.maxThreads, entries.size()/100+1), numSinks);
201     // Maintains the current batch for a given partition index
202     Map<Integer, List<Entry>> entryMap = new HashMap<>(n);
203     List<List<Entry>> entryLists = new ArrayList<>();
204     int[] sizes = new int[n];
205 
206     for (int i = 0; i < n; i++) {
207       entryMap.put(i, new ArrayList<Entry>(entries.size()/n+1));
208     }
209 
210     for (Entry e: entries) {
211       int index = Math.abs(Bytes.hashCode(e.getKey().getEncodedRegionName())%n);
212       int entrySize = (int)e.getKey().estimatedSerializedSizeOf() +
213           (int)e.getEdit().estimatedSerializedSizeOf();
214       // If this batch is oversized, add it to final list and initialize a new empty batch
215       if (sizes[index] > 0 /* must include at least one entry */ &&
216           sizes[index] + entrySize > replicationRpcLimit) {
217         entryLists.add(entryMap.get(index));
218         entryMap.put(index, new ArrayList<Entry>());
219         sizes[index] = 0;
220       }
221       entryMap.get(index).add(e);
222       sizes[index] += entrySize;
223     }
224 
225     entryLists.addAll(entryMap.values());
226     return entryLists;
227   }
228 
229   /**
230    * Do the shipping logic
231    */
232   @Override
233   public boolean replicate(ReplicateContext replicateContext) {
234     CompletionService<Integer> pool = new ExecutorCompletionService<Integer>(this.exec);
235     List<List<Entry>> batches;
236     String walGroupId = replicateContext.getWalGroupId();
237     int sleepMultiplier = 1;
238 
239     if (!peersSelected && this.isRunning()) {
240       connectToPeers();
241       peersSelected = true;
242     }
243 
244     int numSinks = replicationSinkMgr.getNumSinks();
245     if (numSinks == 0) {
246       LOG.warn("No replication sinks found, returning without replicating. The source should retry"
247           + " with the same set of edits.");
248       return false;
249     }
250 
251     batches = createBatches(replicateContext.getEntries());
252 
253     while (this.isRunning() && !exec.isShutdown()) {
254       if (!isPeerEnabled()) {
255         if (sleepForRetries("Replication is disabled", sleepMultiplier)) {
256           sleepMultiplier++;
257         }
258         continue;
259       }
260       try {
261         int futures = 0;
262         for (int i=0; i<batches.size(); i++) {
263           List<Entry> entries = batches.get(i);
264           if (!entries.isEmpty()) {
265             if (LOG.isTraceEnabled()) {
266               LOG.trace("Submitting " + entries.size() +
267                   " entries of total size " + replicateContext.getSize());
268             }
269             // RuntimeExceptions encountered here bubble up and are handled in ReplicationSource
270             pool.submit(createReplicator(entries, i));
271             futures++;
272           }
273         }
274         IOException iox = null;
275 
276         long lastWriteTime = 0;
277         for (int i=0; i<futures; i++) {
278           try {
279             // wait for all futures, remove successful parts
280             // (only the remaining parts will be retried)
281             Future<Integer> f = pool.take();
282             int index = f.get().intValue();
283             List<Entry> batch = batches.get(index);
284             batches.set(index, Collections.<Entry>emptyList()); // remove successful batch
285             // Find the most recent write time in the batch
286             long writeTime = batch.get(batch.size() - 1).getKey().getWriteTime();
287             if (writeTime > lastWriteTime) {
288               lastWriteTime = writeTime;
289             }
290           } catch (InterruptedException ie) {
291             iox =  new IOException(ie);
292           } catch (ExecutionException ee) {
293             // cause must be an IOException
294             iox = (IOException)ee.getCause();
295           }
296         }
297         if (iox != null) {
298           // if we had any exceptions, try again
299           throw iox;
300         }
301         // update metrics
302         if (lastWriteTime > 0) {
303           this.metrics.setAgeOfLastShippedOp(lastWriteTime, walGroupId);
304         }
305         return true;
306 
307       } catch (IOException ioe) {
308         // Didn't ship anything, but must still age the last time we did
309         this.metrics.refreshAgeOfLastShippedOp(walGroupId);
310         if (ioe instanceof RemoteException) {
311           ioe = ((RemoteException) ioe).unwrapRemoteException();
312           LOG.warn("Can't replicate because of an error on the remote cluster: ", ioe);
313           if (ioe instanceof TableNotFoundException) {
314             if (sleepForRetries("A table is missing in the peer cluster. "
315                 + "Replication cannot proceed without losing data.", sleepMultiplier)) {
316               sleepMultiplier++;
317             }
318           } else {
319             LOG.warn("Peer encountered RemoteException, rechecking all sinks: ", ioe);
320             replicationSinkMgr.chooseSinks();
321           }
322         } else {
323           if (ioe instanceof SocketTimeoutException) {
324             // This exception means we waited for more than 60s and nothing
325             // happened, the cluster is alive and calling it right away
326             // even for a test just makes things worse.
327             sleepForRetries("Encountered a SocketTimeoutException. Since the " +
328               "call to the remote cluster timed out, which is usually " +
329               "caused by a machine failure or a massive slowdown",
330               this.socketTimeoutMultiplier);
331           } else if (ioe instanceof ConnectException || ioe instanceof UnknownHostException) {
332             LOG.warn("Peer is unavailable, rechecking all sinks: ", ioe);
333             replicationSinkMgr.chooseSinks();
334           } else {
335             LOG.warn("Can't replicate because of a local or network error: ", ioe);
336           }
337         }
338         if (sleepForRetries("Since we are unable to replicate", sleepMultiplier)) {
339           sleepMultiplier++;
340         }
341       }
342     }
343     return false; // in case we exited before replicating
344   }
345 
346   protected boolean isPeerEnabled() {
347     return ctx.getReplicationPeer().getPeerState() == PeerState.ENABLED;
348   }
349 
350   @Override
351   protected void doStop() {
352     disconnect(); //don't call super.doStop()
353     if (this.conn != null) {
354       try {
355         this.conn.close();
356         this.conn = null;
357       } catch (IOException e) {
358         LOG.warn("Failed to close the connection");
359       }
360     }
361     // Allow currently running replication tasks to finish
362     exec.shutdown();
363     try {
364       exec.awaitTermination(maxTerminationWait, TimeUnit.MILLISECONDS);
365     } catch (InterruptedException e) {
366     }
367     // Abort if the tasks did not terminate in time
368     if (!exec.isTerminated()) {
369       String errMsg = "HBaseInterClusterReplicationEndpoint termination failed. The " +
370           "ThreadPoolExecutor failed to finish all tasks within " + maxTerminationWait + "ms. " +
371           "Aborting to prevent Replication from deadlocking. See HBASE-16081.";
372       abortable.abort(errMsg, new IOException(errMsg));
373     }
374     notifyStopped();
375   }
376 
377   // is this needed? Nobody else will call doStop() otherwise
378   @Override
379   public State stopAndWait() {
380     doStop();
381     return super.stopAndWait();
382   }
383 
384   @VisibleForTesting
385   protected Replicator createReplicator(List<Entry> entries, int ordinal) {
386     return new Replicator(entries, ordinal);
387   }
388 
389   @VisibleForTesting
390   protected class Replicator implements Callable<Integer> {
391     private List<Entry> entries;
392     private int ordinal;
393     public Replicator(List<Entry> entries, int ordinal) {
394       this.entries = entries;
395       this.ordinal = ordinal;
396     }
397 
398     protected void replicateEntries(BlockingInterface rrs, final List<Entry> batch,
399         String replicationClusterId, Path baseNamespaceDir, Path hfileArchiveDir)
400         throws IOException {
401       if (LOG.isTraceEnabled()) {
402         long size = 0;
403         for (Entry e: entries) {
404           size += e.getKey().estimatedSerializedSizeOf();
405           size += e.getEdit().estimatedSerializedSizeOf();
406         }
407         LOG.trace("Replicating batch " + System.identityHashCode(entries) + " of " +
408             entries.size() + " entries with total size " + size + " bytes to " +
409             replicationClusterId);
410       }
411       try {
412         ReplicationProtbufUtil.replicateWALEntry(rrs, batch.toArray(new Entry[batch.size()]),
413           replicationClusterId, baseNamespaceDir, hfileArchiveDir);
414         if (LOG.isTraceEnabled()) {
415           LOG.trace("Completed replicating batch " + System.identityHashCode(entries));
416         }
417       } catch (IOException e) {
418         if (LOG.isTraceEnabled()) {
419           LOG.trace("Failed replicating batch " + System.identityHashCode(entries), e);
420         }
421         throw e;
422       }
423     }
424 
425     @Override
426     public Integer call() throws IOException {
427       SinkPeer sinkPeer = null;
428       try {
429         sinkPeer = replicationSinkMgr.getReplicationSink();
430         BlockingInterface rrs = sinkPeer.getRegionServer();
431         replicateEntries(rrs, entries, replicationClusterId, baseNamespaceDir, hfileArchiveDir);
432         replicationSinkMgr.reportSinkSuccess(sinkPeer);
433         return ordinal;
434       } catch (IOException ioe) {
435         if (sinkPeer != null) {
436           replicationSinkMgr.reportBadSink(sinkPeer);
437         }
438         throw ioe;
439       }
440     }
441   }
442 }