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.client.replication;
20  
21  import com.google.common.annotations.VisibleForTesting;
22  
23  import java.io.Closeable;
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Map.Entry;
31  
32  import org.apache.commons.lang.StringUtils;
33  import org.apache.commons.logging.Log;
34  import org.apache.commons.logging.LogFactory;
35  import org.apache.hadoop.conf.Configuration;
36  import org.apache.hadoop.hbase.Abortable;
37  import org.apache.hadoop.hbase.HColumnDescriptor;
38  import org.apache.hadoop.hbase.HConstants;
39  import org.apache.hadoop.hbase.HTableDescriptor;
40  import org.apache.hadoop.hbase.TableName;
41  import org.apache.hadoop.hbase.TableNotFoundException;
42  import org.apache.hadoop.hbase.classification.InterfaceAudience;
43  import org.apache.hadoop.hbase.classification.InterfaceStability;
44  import org.apache.hadoop.hbase.client.HBaseAdmin;
45  import org.apache.hadoop.hbase.client.HConnection;
46  import org.apache.hadoop.hbase.client.HConnectionManager;
47  import org.apache.hadoop.hbase.client.HTable;
48  import org.apache.hadoop.hbase.replication.ReplicationException;
49  import org.apache.hadoop.hbase.replication.ReplicationFactory;
50  import org.apache.hadoop.hbase.replication.ReplicationPeer;
51  import org.apache.hadoop.hbase.replication.ReplicationPeerConfig;
52  import org.apache.hadoop.hbase.replication.ReplicationPeerZKImpl;
53  import org.apache.hadoop.hbase.replication.ReplicationPeers;
54  import org.apache.hadoop.hbase.replication.ReplicationQueuesClient;
55  import org.apache.hadoop.hbase.util.Pair;
56  import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher;
57  
58  /**
59   * <p>
60   * This class provides the administrative interface to HBase cluster
61   * replication. In order to use it, the cluster and the client using
62   * ReplicationAdmin must be configured with <code>hbase.replication</code>
63   * set to true.
64   * </p>
65   * <p>
66   * Adding a new peer results in creating new outbound connections from every
67   * region server to a subset of region servers on the slave cluster. Each
68   * new stream of replication will start replicating from the beginning of the
69   * current HLog, meaning that edits from that past will be replicated.
70   * </p>
71   * <p>
72   * Removing a peer is a destructive and irreversible operation that stops
73   * all the replication streams for the given cluster and deletes the metadata
74   * used to keep track of the replication state.
75   * </p>
76   * <p>
77   * To see which commands are available in the shell, type
78   * <code>replication</code>.
79   * </p>
80   */
81  @InterfaceAudience.Public
82  @InterfaceStability.Evolving
83  public class ReplicationAdmin implements Closeable {
84    private static final Log LOG = LogFactory.getLog(ReplicationAdmin.class);
85  
86    public static final String TNAME = "tableName";
87    public static final String CFNAME = "columnFamlyName";
88  
89    // only Global for now, can add other type
90    // such as, 1) no global replication, or 2) the table is replicated to this cluster, etc.
91    public static final String REPLICATIONTYPE = "replicationType";
92    public static final String REPLICATIONGLOBAL = Integer
93        .toString(HConstants.REPLICATION_SCOPE_GLOBAL);
94  
95    private final HConnection connection;
96    // TODO: replication should be managed by master. All the classes except ReplicationAdmin should
97    // be moved to hbase-server. Resolve it in HBASE-11392.
98    private final ReplicationQueuesClient replicationQueuesClient;
99    private final ReplicationPeers replicationPeers;
100   /**
101    * A watcher used by replicationPeers and replicationQueuesClient. Keep reference so can dispose
102    * on {@link #close()}.
103    */
104   private final ZooKeeperWatcher zkw;
105 
106   /**
107    * Constructor that creates a connection to the local ZooKeeper ensemble.
108    * @param conf Configuration to use
109    * @throws IOException if an internal replication error occurs
110    * @throws RuntimeException if replication isn't enabled.
111    */
112   public ReplicationAdmin(Configuration conf) throws IOException {
113     if (!conf.getBoolean(HConstants.REPLICATION_ENABLE_KEY,
114         HConstants.REPLICATION_ENABLE_DEFAULT)) {
115       throw new RuntimeException("hbase.replication isn't true, please " +
116           "enable it in order to use replication");
117     }
118     this.connection = HConnectionManager.getConnection(conf);
119     try {
120       zkw = createZooKeeperWatcher();
121       try {
122         this.replicationPeers = ReplicationFactory.getReplicationPeers(zkw, conf, this.connection);
123         this.replicationPeers.init();
124         this.replicationQueuesClient =
125             ReplicationFactory.getReplicationQueuesClient(zkw, conf, this.connection);
126         this.replicationQueuesClient.init();
127       } catch (Exception exception) {
128         if (zkw != null) {
129           zkw.close();
130         }
131         throw exception;
132       }
133     } catch (Exception exception) {
134       if (connection != null) {
135         connection.close();
136       }
137       if (exception instanceof IOException) {
138         throw (IOException) exception;
139       } else if (exception instanceof RuntimeException) {
140         throw (RuntimeException) exception;
141       } else {
142         throw new IOException("Error initializing the replication admin client.", exception);
143       }
144     }
145   }
146 
147   private ZooKeeperWatcher createZooKeeperWatcher() throws IOException {
148     // This Abortable doesn't 'abort'... it just logs.
149     return new ZooKeeperWatcher(connection.getConfiguration(), "ReplicationAdmin", new Abortable() {
150       @Override
151       public void abort(String why, Throwable e) {
152         LOG.error(why, e);
153         // We used to call system.exit here but this script can be embedded by other programs that
154         // want to do replication stuff... so inappropriate calling System.exit. Just log for now.
155       }
156 
157       @Override
158       public boolean isAborted() {
159         return false;
160       }
161     });
162   }
163 
164   /**
165    * Add a new peer cluster to replicate to.
166    * @param id a short name that identifies the cluster
167    * @param clusterKey the concatenation of the slave cluster's
168    * <code>hbase.zookeeper.quorum:hbase.zookeeper.property.clientPort:zookeeper.znode.parent</code>
169    * @throws IllegalStateException if there's already one slave since
170    * multi-slave isn't supported yet.
171    * @deprecated Use addPeer(String, ReplicationPeerConfig, Map) instead.
172    */
173   @Deprecated
174   public void addPeer(String id, String clusterKey) throws ReplicationException {
175     this.addPeer(id, new ReplicationPeerConfig().setClusterKey(clusterKey), null);
176   }
177 
178   @Deprecated
179   public void addPeer(String id, String clusterKey, String tableCFs)
180     throws ReplicationException {
181     this.replicationPeers.addPeer(id,
182       new ReplicationPeerConfig().setClusterKey(clusterKey), tableCFs);
183   }
184 
185   /**
186    * Add a new remote slave cluster for replication.
187    * @param id a short name that identifies the cluster
188    * @param peerConfig configuration for the replication slave cluster
189    * @param tableCfs the table and column-family list which will be replicated for this peer.
190    * A map from tableName to column family names. An empty collection can be passed
191    * to indicate replicating all column families. Pass null for replicating all table and column
192    * families
193    */
194   public void addPeer(String id, ReplicationPeerConfig peerConfig,
195       Map<TableName, ? extends Collection<String>> tableCfs) throws ReplicationException {
196     this.replicationPeers.addPeer(id, peerConfig, getTableCfsStr(tableCfs));
197   }
198 
199   @VisibleForTesting
200   static String getTableCfsStr(Map<TableName, ? extends Collection<String>> tableCfs) {
201     String tableCfsStr = null;
202     if (tableCfs != null) {
203       // Format: table1:cf1,cf2;table2:cfA,cfB;table3
204       StringBuilder builder = new StringBuilder();
205       for (Entry<TableName, ? extends Collection<String>> entry : tableCfs.entrySet()) {
206         if (builder.length() > 0) {
207           builder.append(";");
208         }
209         builder.append(entry.getKey());
210         if (entry.getValue() != null && !entry.getValue().isEmpty()) {
211           builder.append(":");
212           builder.append(StringUtils.join(entry.getValue(), ","));
213         }
214       }
215       tableCfsStr = builder.toString();
216     }
217     return tableCfsStr;
218   }
219 
220   /**
221    * Removes a peer cluster and stops the replication to it.
222    * @param id a short name that identifies the cluster
223    */
224   public void removePeer(String id) throws ReplicationException {
225     this.replicationPeers.removePeer(id);
226   }
227 
228   /**
229    * Restart the replication stream to the specified peer.
230    * @param id a short name that identifies the cluster
231    */
232   public void enablePeer(String id) throws ReplicationException {
233     this.replicationPeers.enablePeer(id);
234   }
235 
236   /**
237    * Stop the replication stream to the specified peer.
238    * @param id a short name that identifies the cluster
239    */
240   public void disablePeer(String id) throws ReplicationException {
241     this.replicationPeers.disablePeer(id);
242   }
243 
244   /**
245    * Get the number of slave clusters the local cluster has.
246    * @return number of slave clusters
247    */
248   public int getPeersCount() {
249     return this.replicationPeers.getAllPeerIds().size();
250   }
251 
252   /**
253    * Map of this cluster's peers for display.
254    * @return A map of peer ids to peer cluster keys
255    * @deprecated use {@link #listPeerConfigs()}
256    */
257   @Deprecated
258   public Map<String, String> listPeers() {
259     Map<String, ReplicationPeerConfig> peers = this.listPeerConfigs();
260     Map<String, String> ret = new HashMap<String, String>(peers.size());
261 
262     for (Map.Entry<String, ReplicationPeerConfig> entry : peers.entrySet()) {
263       ret.put(entry.getKey(), entry.getValue().getClusterKey());
264     }
265     return ret;
266   }
267 
268   public Map<String, ReplicationPeerConfig> listPeerConfigs() {
269     return this.replicationPeers.getAllPeerConfigs();
270   }
271 
272   public ReplicationPeerConfig getPeerConfig(String id) throws ReplicationException {
273     return this.replicationPeers.getReplicationPeerConfig(id);
274   }
275 
276   /**
277    * Get the replicable table-cf config of the specified peer.
278    * @param id a short name that identifies the cluster
279    */
280   public String getPeerTableCFs(String id) throws ReplicationException {
281     return this.replicationPeers.getPeerTableCFsConfig(id);
282   }
283 
284   /**
285    * Set the replicable table-cf config of the specified peer
286    * @param id a short name that identifies the cluster
287    * @deprecated use {@link #setPeerTableCFs(String, Map)}
288    */
289   @Deprecated
290   public void setPeerTableCFs(String id, String tableCFs) throws ReplicationException {
291     this.replicationPeers.setPeerTableCFsConfig(id, tableCFs);
292   }
293 
294   /**
295    * Set the replicable table-cf config of the specified peer
296    * @param id a short name that identifies the cluster
297    * @param tableCfs the table and column-family list which will be replicated for this peer.
298    * A map from tableName to column family names. An empty collection can be passed
299    * to indicate replicating all column families. Pass null for replicating all table and column
300    * families
301    */
302   public void setPeerTableCFs(String id, Map<TableName, ? extends Collection<String>> tableCfs)
303       throws ReplicationException {
304     this.replicationPeers.setPeerTableCFsConfig(id, getTableCfsStr(tableCfs));
305   }
306 
307   /**
308    * Get the state of the specified peer cluster
309    * @param id String format of the Short name that identifies the peer,
310    * an IllegalArgumentException is thrown if it doesn't exist
311    * @return true if replication is enabled to that peer, false if it isn't
312    */
313   public boolean getPeerState(String id) throws ReplicationException {
314     return this.replicationPeers.getStatusOfPeerFromBackingStore(id);
315   }
316 
317   @Override
318   public void close() throws IOException {
319     if (this.zkw != null) {
320       this.zkw.close();
321     }
322     if (this.connection != null) {
323       this.connection.close();
324     }
325   }
326 
327 
328   /**
329    * Find all column families that are replicated from this cluster
330    * @return the full list of the replicated column families of this cluster as:
331    *        tableName, family name, replicationType
332    *
333    * Currently replicationType is Global. In the future, more replication
334    * types may be extended here. For example
335    *  1) the replication may only apply to selected peers instead of all peers
336    *  2) the replicationType may indicate the host Cluster servers as Slave
337    *     for the table:columnFam.
338    */
339   public List<HashMap<String, String>> listReplicated() throws IOException {
340     List<HashMap<String, String>> replicationColFams = new ArrayList<HashMap<String, String>>();
341     HTableDescriptor[] tables = this.connection.listTables();
342 
343     for (HTableDescriptor table : tables) {
344       HColumnDescriptor[] columns = table.getColumnFamilies();
345       String tableName = table.getNameAsString();
346       for (HColumnDescriptor column : columns) {
347         if (column.getScope() != HConstants.REPLICATION_SCOPE_LOCAL) {
348           // At this moment, the columfam is replicated to all peers
349           HashMap<String, String> replicationEntry = new HashMap<String, String>();
350           replicationEntry.put(TNAME, tableName);
351           replicationEntry.put(CFNAME, column.getNameAsString());
352           replicationEntry.put(REPLICATIONTYPE, REPLICATIONGLOBAL);
353           replicationColFams.add(replicationEntry);
354         }
355       }
356     }
357 
358     return replicationColFams;
359   }
360 
361   /**
362    * Enable a table's replication switch.
363    * @param tableName name of the table
364    * @throws IOException if a remote or network exception occurs
365    */
366   public void enableTableRep(final TableName tableName) throws IOException {
367     if (tableName == null) {
368       throw new IllegalArgumentException("Table name cannot be null");
369     }
370     HBaseAdmin admin = null;
371     try {
372       admin = new HBaseAdmin(this.connection.getConfiguration());
373       if (!admin.tableExists(tableName)) {
374         throw new TableNotFoundException("Table '" + tableName.getNamespaceAsString()
375             + "' does not exists.");
376       }
377     } finally {
378       try {
379         admin.close();
380       } catch (IOException e) {
381         LOG.warn("Failed to close admin connection.");
382         LOG.debug("Details on failure to close admin connection.", e);
383       }
384     }
385     byte[][] splits = getTableSplitRowKeys(tableName);
386     checkAndSyncTableDescToPeers(tableName, splits);
387     setTableRep(tableName, true);
388   }
389 
390   /**
391    * Disable a table's replication switch.
392    * @param tableName name of the table
393    * @throws IOException if a remote or network exception occurs
394    */
395   public void disableTableRep(final TableName tableName) throws IOException {
396     if (tableName == null) {
397       throw new IllegalArgumentException("Table name is null");
398     }
399 
400     HBaseAdmin admin = null;
401     try {
402       admin = new HBaseAdmin(this.connection.getConfiguration());
403       if (!admin.tableExists(tableName)) {
404         throw new TableNotFoundException("Table '" + tableName.getNamespaceAsString()
405             + "' does not exists.");
406       }
407     } finally {
408       try {
409         admin.close();
410       } catch (IOException e) {
411         LOG.warn("Failed to close admin connection.");
412         LOG.debug("Details on failure to close admin connection.", e);
413       }
414     }
415     setTableRep(tableName, false);
416   }
417 
418   /**
419    * Get the split row keys of table
420    * @param tableName table name
421    * @return array of split row keys
422    * @throws IOException
423    */
424   private byte[][] getTableSplitRowKeys(TableName tableName) throws IOException {
425     HTable table = null;
426     try {
427       table = new HTable(this.connection.getConfiguration(), tableName);
428       byte[][] startKeys = table.getStartKeys();
429       if (startKeys.length == 1) {
430         return null;
431       }
432       byte[][] splits = new byte[startKeys.length - 1][];
433       for (int i = 1; i < startKeys.length; i++) {
434         splits[i - 1] = startKeys[i];
435       }
436       return splits;
437     } finally {
438       if (table != null) {
439         try {
440           table.close();
441         } catch (IOException e) {
442           LOG.warn("Unable to close table");
443         }
444       }
445     }
446   }
447 
448   /**
449    * Connect to peer and check the table descriptor on peer:
450    * <ol>
451    * <li>Create the same table on peer when not exist.</li>
452    * <li>Throw exception if the table exists on peer cluster but descriptors are not same.</li>
453    * </ol>
454    * @param tableName name of the table to sync to the peer
455    * @param splits table split keys
456    * @throws IOException
457    */
458   private void checkAndSyncTableDescToPeers(final TableName tableName, final byte[][] splits)
459       throws IOException {
460     List<ReplicationPeer> repPeers = listReplicationPeers();
461     if (repPeers == null || repPeers.size() <= 0) {
462       throw new IllegalArgumentException("Found no peer cluster for replication.");
463     }
464     for (ReplicationPeer repPeer : repPeers) {
465       Configuration peerConf = repPeer.getConfiguration();
466       HTableDescriptor htd = null;
467       HBaseAdmin repHBaseAdmin = null;
468       try {
469         repHBaseAdmin = new HBaseAdmin(peerConf);
470         htd = this.connection.getHTableDescriptor(tableName);
471         HTableDescriptor peerHtd = null;
472         if (!repHBaseAdmin.tableExists(tableName)) {
473           repHBaseAdmin.createTable(htd, splits);
474         } else {
475           peerHtd = repHBaseAdmin.getTableDescriptor(tableName);
476           if (peerHtd == null) {
477             throw new IllegalArgumentException("Failed to get table descriptor for table "
478                 + tableName.getNameAsString() + " from peer cluster " + repPeer.getId());
479           } else if (!peerHtd.equals(htd)) {
480             throw new IllegalArgumentException("Table " + tableName.getNameAsString()
481                 + " exists in peer cluster " + repPeer.getId()
482                 + ", but the table descriptors are not same when comapred with source cluster."
483                 + " Thus can not enable the table's replication switch.");
484           }
485         }
486       } finally {
487         if (repHBaseAdmin != null) {
488           try {
489             repHBaseAdmin.close();
490           } catch (IOException e) {
491             LOG.warn("Failed to close admin connection.");
492             LOG.debug("Details on failure to close admin connection.", e);
493           }
494         }
495       }
496     }
497   }
498 
499   @VisibleForTesting
500   List<ReplicationPeer> listReplicationPeers() {
501     Map<String, ReplicationPeerConfig> peers = listPeerConfigs();
502     if (peers == null || peers.size() <= 0) {
503       return null;
504     }
505     List<ReplicationPeer> listOfPeers = new ArrayList<ReplicationPeer>(peers.size());
506     for (Entry<String, ReplicationPeerConfig> peerEntry : peers.entrySet()) {
507       String peerId = peerEntry.getKey();
508       try {
509         Pair<ReplicationPeerConfig, Configuration> pair = this.replicationPeers.getPeerConf(peerId);
510         Configuration peerConf = pair.getSecond();
511         ReplicationPeer peer = new ReplicationPeerZKImpl(peerConf, peerId, pair.getFirst());
512         listOfPeers.add(peer);
513       } catch (ReplicationException e) {
514         LOG.warn("Failed to get valid replication peers. "
515             + "Error connecting to peer cluster with peerId=" + peerId + ". Error message="
516             + e.getMessage());
517         LOG.debug("Failure details to get valid replication peers.", e);
518         continue;
519       }
520     }
521     return listOfPeers;
522   }
523 
524   /**
525    * Set the table's replication switch if the table's replication switch is already not set.
526    * @param tableName name of the table
527    * @param isRepEnabled is replication switch enable or disable
528    * @throws IOException if a remote or network exception occurs
529    */
530   private void setTableRep(final TableName tableName, boolean isRepEnabled) throws IOException {
531     HBaseAdmin admin = null;
532     try {
533       admin = new HBaseAdmin(this.connection.getConfiguration());
534       HTableDescriptor htd = admin.getTableDescriptor(tableName);
535       if (isTableRepEnabled(htd) ^ isRepEnabled) {
536         boolean isOnlineSchemaUpdateEnabled =
537             this.connection.getConfiguration()
538                 .getBoolean("hbase.online.schema.update.enable", true);
539         if (!isOnlineSchemaUpdateEnabled) {
540           admin.disableTable(tableName);
541         }
542         for (HColumnDescriptor hcd : htd.getFamilies()) {
543           hcd.setScope(isRepEnabled ? HConstants.REPLICATION_SCOPE_GLOBAL
544               : HConstants.REPLICATION_SCOPE_LOCAL);
545         }
546         admin.modifyTable(tableName, htd);
547         if (!isOnlineSchemaUpdateEnabled) {
548           admin.enableTable(tableName);
549         }
550       }
551     } finally {
552       if (admin != null) {
553         try {
554           admin.close();
555         } catch (IOException e) {
556           LOG.warn("Failed to close admin connection.");
557           LOG.debug("Details on failure to close admin connection.", e);
558         }
559       }
560     }
561   }
562 
563   /**
564    * @param htd table descriptor details for the table to check
565    * @return true if table's replication switch is enabled
566    */
567   private boolean isTableRepEnabled(HTableDescriptor htd) {
568     for (HColumnDescriptor hcd : htd.getFamilies()) {
569       if (hcd.getScope() != HConstants.REPLICATION_SCOPE_GLOBAL) {
570         return false;
571       }
572     }
573     return true;
574   }
575 }