View Javadoc

1   /*
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  package org.apache.hadoop.hbase.replication.regionserver;
20  
21  import java.io.IOException;
22  import java.io.InterruptedIOException;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Map.Entry;
29  import java.util.TreeMap;
30  import java.util.UUID;
31  import java.util.concurrent.atomic.AtomicLong;
32  
33  import org.apache.commons.lang.StringUtils;
34  import org.apache.commons.logging.Log;
35  import org.apache.commons.logging.LogFactory;
36  import org.apache.hadoop.conf.Configuration;
37  import org.apache.hadoop.fs.Path;
38  import org.apache.hadoop.hbase.Cell;
39  import org.apache.hadoop.hbase.CellScanner;
40  import org.apache.hadoop.hbase.CellUtil;
41  import org.apache.hadoop.hbase.HBaseConfiguration;
42  import org.apache.hadoop.hbase.HConstants;
43  import org.apache.hadoop.hbase.Stoppable;
44  import org.apache.hadoop.hbase.TableName;
45  import org.apache.hadoop.hbase.TableNotFoundException;
46  import org.apache.hadoop.hbase.classification.InterfaceAudience;
47  import org.apache.hadoop.hbase.client.Connection;
48  import org.apache.hadoop.hbase.client.ConnectionFactory;
49  import org.apache.hadoop.hbase.client.Delete;
50  import org.apache.hadoop.hbase.client.Mutation;
51  import org.apache.hadoop.hbase.client.Put;
52  import org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException;
53  import org.apache.hadoop.hbase.client.Row;
54  import org.apache.hadoop.hbase.client.Table;
55  import org.apache.hadoop.hbase.protobuf.generated.AdminProtos.WALEntry;
56  import org.apache.hadoop.hbase.protobuf.generated.HBaseProtos;
57  import org.apache.hadoop.hbase.protobuf.generated.WALProtos.BulkLoadDescriptor;
58  import org.apache.hadoop.hbase.protobuf.generated.WALProtos.StoreDescriptor;
59  import org.apache.hadoop.hbase.regionserver.wal.WALEdit;
60  import org.apache.hadoop.hbase.util.Bytes;
61  import org.apache.hadoop.hbase.util.Pair;
62  
63  /**
64   * <p>
65   * This class is responsible for replicating the edits coming
66   * from another cluster.
67   * </p><p>
68   * This replication process is currently waiting for the edits to be applied
69   * before the method can return. This means that the replication of edits
70   * is synchronized (after reading from WALs in ReplicationSource) and that a
71   * single region server cannot receive edits from two sources at the same time
72   * </p><p>
73   * This class uses the native HBase client in order to replicate entries.
74   * </p>
75   *
76   * TODO make this class more like ReplicationSource wrt log handling
77   */
78  @InterfaceAudience.Private
79  public class ReplicationSink {
80  
81    private static final Log LOG = LogFactory.getLog(ReplicationSink.class);
82    private final Configuration conf;
83    // Volatile because of note in here -- look for double-checked locking:
84    // http://www.oracle.com/technetwork/articles/javase/bloch-effective-08-qa-140880.html
85    private volatile Connection sharedHtableCon;
86    private final MetricsSink metrics;
87    private final AtomicLong totalReplicatedEdits = new AtomicLong();
88    private final Object sharedHtableConLock = new Object();
89    // Number of hfiles that we successfully replicated
90    private long hfilesReplicated = 0;
91    private SourceFSConfigurationProvider provider;
92  
93    /**
94     * Create a sink for replication
95     *
96     * @param conf                conf object
97     * @param stopper             boolean to tell this thread to stop
98     * @throws IOException thrown when HDFS goes bad or bad file name
99     */
100   public ReplicationSink(Configuration conf, Stoppable stopper)
101       throws IOException {
102     this.conf = HBaseConfiguration.create(conf);
103     decorateConf();
104     this.metrics = new MetricsSink();
105 
106     String className =
107         conf.get("hbase.replication.source.fs.conf.provider",
108           DefaultSourceFSConfigurationProvider.class.getCanonicalName());
109     try {
110       @SuppressWarnings("rawtypes")
111       Class c = Class.forName(className);
112       this.provider = (SourceFSConfigurationProvider) c.newInstance();
113     } catch (Exception e) {
114       throw new IllegalArgumentException("Configured source fs configuration provider class "
115           + className + " throws error.", e);
116     }
117   }
118 
119   /**
120    * decorate the Configuration object to make replication more receptive to delays:
121    * lessen the timeout and numTries.
122    */
123   private void decorateConf() {
124     this.conf.setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER,
125         this.conf.getInt("replication.sink.client.retries.number", 4));
126     this.conf.setInt(HConstants.HBASE_CLIENT_OPERATION_TIMEOUT,
127         this.conf.getInt("replication.sink.client.ops.timeout", 10000));
128     String replicationCodec = this.conf.get(HConstants.REPLICATION_CODEC_CONF_KEY);
129     if (StringUtils.isNotEmpty(replicationCodec)) {
130       this.conf.set(HConstants.RPC_CODEC_CONF_KEY, replicationCodec);
131     }
132    }
133 
134   /**
135    * Replicate this array of entries directly into the local cluster using the native client. Only
136    * operates against raw protobuf type saving on a conversion from pb to pojo.
137    * @param entries
138    * @param cells
139    * @param replicationClusterId Id which will uniquely identify source cluster FS client
140    *          configurations in the replication configuration directory
141    * @param sourceBaseNamespaceDirPath Path that point to the source cluster base namespace
142    *          directory
143    * @param sourceHFileArchiveDirPath Path that point to the source cluster hfile archive directory
144    * @throws IOException If failed to replicate the data
145    */
146   public void replicateEntries(List<WALEntry> entries, final CellScanner cells,
147       String replicationClusterId, String sourceBaseNamespaceDirPath,
148       String sourceHFileArchiveDirPath) throws IOException {
149     if (entries.isEmpty()) return;
150     if (cells == null) throw new NullPointerException("TODO: Add handling of null CellScanner");
151     // Very simple optimization where we batch sequences of rows going
152     // to the same table.
153     try {
154       long totalReplicated = 0;
155       // Map of table => list of Rows, grouped by cluster id, we only want to flushCommits once per
156       // invocation of this method per table and cluster id.
157       Map<TableName, Map<List<UUID>, List<Row>>> rowMap =
158           new TreeMap<TableName, Map<List<UUID>, List<Row>>>();
159 
160       // Map of table name Vs list of pair of family and list of hfile paths from its namespace
161       Map<String, List<Pair<byte[], List<String>>>> bulkLoadHFileMap = null;
162 
163       for (WALEntry entry : entries) {
164         TableName table =
165             TableName.valueOf(entry.getKey().getTableName().toByteArray());
166         Cell previousCell = null;
167         Mutation m = null;
168         int count = entry.getAssociatedCellCount();
169         for (int i = 0; i < count; i++) {
170           // Throw index out of bounds if our cell count is off
171           if (!cells.advance()) {
172             throw new ArrayIndexOutOfBoundsException("Expected=" + count + ", index=" + i);
173           }
174           Cell cell = cells.current();
175           // Handle bulk load hfiles replication
176           if (CellUtil.matchingQualifier(cell, WALEdit.BULK_LOAD)) {
177             if (bulkLoadHFileMap == null) {
178               bulkLoadHFileMap = new HashMap<String, List<Pair<byte[], List<String>>>>();
179             }
180             buildBulkLoadHFileMap(bulkLoadHFileMap, table, cell);
181           } else {
182             // Handle wal replication
183             if (isNewRowOrType(previousCell, cell)) {
184               // Create new mutation
185               m =
186                   CellUtil.isDelete(cell) ? new Delete(cell.getRowArray(), cell.getRowOffset(),
187                       cell.getRowLength()) : new Put(cell.getRowArray(), cell.getRowOffset(),
188                       cell.getRowLength());
189               List<UUID> clusterIds = new ArrayList<UUID>();
190               for (HBaseProtos.UUID clusterId : entry.getKey().getClusterIdsList()) {
191                 clusterIds.add(toUUID(clusterId));
192               }
193               m.setClusterIds(clusterIds);
194               addToHashMultiMap(rowMap, table, clusterIds, m);
195             }
196             if (CellUtil.isDelete(cell)) {
197               ((Delete) m).addDeleteMarker(cell);
198             } else {
199               ((Put) m).add(cell);
200             }
201             previousCell = cell;
202           }
203         }
204         totalReplicated++;
205       }
206 
207       // TODO Replicating mutations and bulk loaded data can be made parallel
208       if (!rowMap.isEmpty()) {
209         LOG.debug("Started replicating mutations.");
210         for (Entry<TableName, Map<List<UUID>, List<Row>>> entry : rowMap.entrySet()) {
211           batch(entry.getKey(), entry.getValue().values());
212         }
213         LOG.debug("Finished replicating mutations.");
214       }
215 
216       if (bulkLoadHFileMap != null && !bulkLoadHFileMap.isEmpty()) {
217         LOG.debug("Started replicating bulk loaded data.");
218         HFileReplicator hFileReplicator =
219             new HFileReplicator(this.provider.getConf(this.conf, replicationClusterId),
220                 sourceBaseNamespaceDirPath, sourceHFileArchiveDirPath, bulkLoadHFileMap, conf,
221                 getConnection());
222         hFileReplicator.replicate();
223         LOG.debug("Finished replicating bulk loaded data.");
224       }
225 
226       int size = entries.size();
227       this.metrics.setAgeOfLastAppliedOp(entries.get(size - 1).getKey().getWriteTime());
228       this.metrics.applyBatch(size + hfilesReplicated, hfilesReplicated);
229       this.totalReplicatedEdits.addAndGet(totalReplicated);
230     } catch (IOException ex) {
231       LOG.error("Unable to accept edit because:", ex);
232       throw ex;
233     }
234   }
235 
236   private void buildBulkLoadHFileMap(
237       final Map<String, List<Pair<byte[], List<String>>>> bulkLoadHFileMap, TableName table,
238       Cell cell) throws IOException {
239     BulkLoadDescriptor bld = WALEdit.getBulkLoadDescriptor(cell);
240     List<StoreDescriptor> storesList = bld.getStoresList();
241     int storesSize = storesList.size();
242     for (int j = 0; j < storesSize; j++) {
243       StoreDescriptor storeDescriptor = storesList.get(j);
244       List<String> storeFileList = storeDescriptor.getStoreFileList();
245       int storeFilesSize = storeFileList.size();
246       hfilesReplicated += storeFilesSize;
247       for (int k = 0; k < storeFilesSize; k++) {
248         byte[] family = storeDescriptor.getFamilyName().toByteArray();
249 
250         // Build hfile relative path from its namespace
251         String pathToHfileFromNS = getHFilePath(table, bld, storeFileList.get(k), family);
252 
253         String tableName = table.getNameWithNamespaceInclAsString();
254         if (bulkLoadHFileMap.containsKey(tableName)) {
255           List<Pair<byte[], List<String>>> familyHFilePathsList = bulkLoadHFileMap.get(tableName);
256           boolean foundFamily = false;
257           for (int i = 0; i < familyHFilePathsList.size(); i++) {
258             Pair<byte[], List<String>> familyHFilePathsPair = familyHFilePathsList.get(i);
259             if (Bytes.equals(familyHFilePathsPair.getFirst(), family)) {
260               // Found family already present, just add the path to the existing list
261               familyHFilePathsPair.getSecond().add(pathToHfileFromNS);
262               foundFamily = true;
263               break;
264             }
265           }
266           if (!foundFamily) {
267             // Family not found, add this family and its hfile paths pair to the list
268             addFamilyAndItsHFilePathToTableInMap(family, pathToHfileFromNS, familyHFilePathsList);
269           }
270         } else {
271           // Add this table entry into the map
272           addNewTableEntryInMap(bulkLoadHFileMap, family, pathToHfileFromNS, tableName);
273         }
274       }
275     }
276   }
277 
278   private void addFamilyAndItsHFilePathToTableInMap(byte[] family, String pathToHfileFromNS,
279       List<Pair<byte[], List<String>>> familyHFilePathsList) {
280     List<String> hfilePaths = new ArrayList<String>();
281     hfilePaths.add(pathToHfileFromNS);
282     familyHFilePathsList.add(new Pair<byte[], List<String>>(family, hfilePaths));
283   }
284 
285   private void addNewTableEntryInMap(
286       final Map<String, List<Pair<byte[], List<String>>>> bulkLoadHFileMap, byte[] family,
287       String pathToHfileFromNS, String tableName) {
288     List<String> hfilePaths = new ArrayList<String>();
289     hfilePaths.add(pathToHfileFromNS);
290     Pair<byte[], List<String>> newFamilyHFilePathsPair =
291         new Pair<byte[], List<String>>(family, hfilePaths);
292     List<Pair<byte[], List<String>>> newFamilyHFilePathsList =
293         new ArrayList<Pair<byte[], List<String>>>();
294     newFamilyHFilePathsList.add(newFamilyHFilePathsPair);
295     bulkLoadHFileMap.put(tableName, newFamilyHFilePathsList);
296   }
297 
298   private String getHFilePath(TableName table, BulkLoadDescriptor bld, String storeFile,
299       byte[] family) {
300     return new StringBuilder(100).append(table.getNamespaceAsString()).append(Path.SEPARATOR)
301         .append(table.getQualifierAsString()).append(Path.SEPARATOR)
302         .append(Bytes.toString(bld.getEncodedRegionName().toByteArray())).append(Path.SEPARATOR)
303         .append(Bytes.toString(family)).append(Path.SEPARATOR).append(storeFile).toString();
304   }
305 
306   /**
307    * @param previousCell
308    * @param cell
309    * @return True if we have crossed over onto a new row or type
310    */
311   private boolean isNewRowOrType(final Cell previousCell, final Cell cell) {
312     return previousCell == null || previousCell.getTypeByte() != cell.getTypeByte() ||
313         !CellUtil.matchingRow(previousCell, cell);
314   }
315 
316   private java.util.UUID toUUID(final HBaseProtos.UUID uuid) {
317     return new java.util.UUID(uuid.getMostSigBits(), uuid.getLeastSigBits());
318   }
319 
320   /**
321    * Simple helper to a map from key to (a list of) values
322    * TODO: Make a general utility method
323    * @param map
324    * @param key1
325    * @param key2
326    * @param value
327    * @return the list of values corresponding to key1 and key2
328    */
329   private <K1, K2, V> List<V> addToHashMultiMap(Map<K1, Map<K2,List<V>>> map, K1 key1, K2 key2, V value) {
330     Map<K2,List<V>> innerMap = map.get(key1);
331     if (innerMap == null) {
332       innerMap = new HashMap<K2, List<V>>();
333       map.put(key1, innerMap);
334     }
335     List<V> values = innerMap.get(key2);
336     if (values == null) {
337       values = new ArrayList<V>();
338       innerMap.put(key2, values);
339     }
340     values.add(value);
341     return values;
342   }
343 
344   /**
345    * stop the thread pool executor. It is called when the regionserver is stopped.
346    */
347   public void stopReplicationSinkServices() {
348     try {
349       if (this.sharedHtableCon != null) {
350         synchronized (sharedHtableConLock) {
351           if (this.sharedHtableCon != null) {
352             this.sharedHtableCon.close();
353             this.sharedHtableCon = null;
354           }
355         }
356       }
357     } catch (IOException e) {
358       LOG.warn("IOException while closing the connection", e); // ignoring as we are closing.
359     }
360   }
361 
362 
363   /**
364    * Do the changes and handle the pool
365    * @param tableName table to insert into
366    * @param allRows list of actions
367    * @throws IOException
368    */
369   protected void batch(TableName tableName, Collection<List<Row>> allRows) throws IOException {
370     if (allRows.isEmpty()) {
371       return;
372     }
373     Table table = null;
374     try {
375       Connection connection = getConnection();
376       table = connection.getTable(tableName);
377       for (List<Row> rows : allRows) {
378         table.batch(rows);
379       }
380     } catch (RetriesExhaustedWithDetailsException rewde) {
381       for (Throwable ex : rewde.getCauses()) {
382         if (ex instanceof TableNotFoundException) {
383           throw new TableNotFoundException("'" + tableName + "'");
384         }
385       }
386       throw rewde;
387     } catch (InterruptedException ix) {
388       throw (InterruptedIOException) new InterruptedIOException().initCause(ix);
389     } finally {
390       if (table != null) {
391         table.close();
392       }
393     }
394   }
395 
396   private Connection getConnection() throws IOException {
397     // See https://en.wikipedia.org/wiki/Double-checked_locking
398     Connection connection = sharedHtableCon;
399     if (connection == null) {
400       synchronized (sharedHtableConLock) {
401         connection = sharedHtableCon;
402         if (connection == null) {
403           connection = sharedHtableCon = ConnectionFactory.createConnection(conf);
404         }
405       }
406     }
407     return connection;
408   }
409 
410   /**
411    * Get a string representation of this sink's metrics
412    * @return string with the total replicated edits count and the date
413    * of the last edit that was applied
414    */
415   public String getStats() {
416     return this.totalReplicatedEdits.get() == 0 ? "" : "Sink: " +
417       "age in ms of last applied edit: " + this.metrics.refreshAgeOfLastAppliedOp() +
418       ", total replicated edits: " + this.totalReplicatedEdits;
419   }
420 
421   /**
422    * Get replication Sink Metrics
423    * @return MetricsSink
424    */
425   public MetricsSink getSinkMetrics() {
426     return this.metrics;
427   }
428 }