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