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