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   public void updatePeerConfig(String id, ReplicationPeerConfig peerConfig)
221     throws ReplicationException {
222     this.replicationPeers.updatePeerConfig(id, peerConfig);
223   }
224   /**
225    * Removes a peer cluster and stops the replication to it.
226    * @param id a short name that identifies the cluster
227    */
228   public void removePeer(String id) throws ReplicationException {
229     this.replicationPeers.removePeer(id);
230   }
231 
232   /**
233    * Restart the replication stream to the specified peer.
234    * @param id a short name that identifies the cluster
235    */
236   public void enablePeer(String id) throws ReplicationException {
237     this.replicationPeers.enablePeer(id);
238   }
239 
240   /**
241    * Stop the replication stream to the specified peer.
242    * @param id a short name that identifies the cluster
243    */
244   public void disablePeer(String id) throws ReplicationException {
245     this.replicationPeers.disablePeer(id);
246   }
247 
248   /**
249    * Get the number of slave clusters the local cluster has.
250    * @return number of slave clusters
251    */
252   public int getPeersCount() {
253     return this.replicationPeers.getAllPeerIds().size();
254   }
255 
256   /**
257    * Map of this cluster's peers for display.
258    * @return A map of peer ids to peer cluster keys
259    * @deprecated use {@link #listPeerConfigs()}
260    */
261   @Deprecated
262   public Map<String, String> listPeers() {
263     Map<String, ReplicationPeerConfig> peers = this.listPeerConfigs();
264     Map<String, String> ret = new HashMap<String, String>(peers.size());
265 
266     for (Map.Entry<String, ReplicationPeerConfig> entry : peers.entrySet()) {
267       ret.put(entry.getKey(), entry.getValue().getClusterKey());
268     }
269     return ret;
270   }
271 
272   public Map<String, ReplicationPeerConfig> listPeerConfigs() {
273     return this.replicationPeers.getAllPeerConfigs();
274   }
275 
276   public ReplicationPeerConfig getPeerConfig(String id) throws ReplicationException {
277     return this.replicationPeers.getReplicationPeerConfig(id);
278   }
279 
280   /**
281    * Get the replicable table-cf config of the specified peer.
282    * @param id a short name that identifies the cluster
283    */
284   public String getPeerTableCFs(String id) throws ReplicationException {
285     return this.replicationPeers.getPeerTableCFsConfig(id);
286   }
287 
288   /**
289    * Set the replicable table-cf config of the specified peer
290    * @param id a short name that identifies the cluster
291    * @deprecated use {@link #setPeerTableCFs(String, Map)}
292    */
293   @Deprecated
294   public void setPeerTableCFs(String id, String tableCFs) throws ReplicationException {
295     this.replicationPeers.setPeerTableCFsConfig(id, tableCFs);
296   }
297 
298   /**
299    * Set the replicable table-cf config of the specified peer
300    * @param id a short name that identifies the cluster
301    * @param tableCfs the table and column-family list which will be replicated for this peer.
302    * A map from tableName to column family names. An empty collection can be passed
303    * to indicate replicating all column families. Pass null for replicating all table and column
304    * families
305    */
306   public void setPeerTableCFs(String id, Map<TableName, ? extends Collection<String>> tableCfs)
307       throws ReplicationException {
308     this.replicationPeers.setPeerTableCFsConfig(id, getTableCfsStr(tableCfs));
309   }
310 
311   /**
312    * Get the state of the specified peer cluster
313    * @param id String format of the Short name that identifies the peer,
314    * an IllegalArgumentException is thrown if it doesn't exist
315    * @return true if replication is enabled to that peer, false if it isn't
316    */
317   public boolean getPeerState(String id) throws ReplicationException {
318     return this.replicationPeers.getStatusOfPeerFromBackingStore(id);
319   }
320 
321   @Override
322   public void close() throws IOException {
323     if (this.zkw != null) {
324       this.zkw.close();
325     }
326     if (this.connection != null) {
327       this.connection.close();
328     }
329   }
330 
331 
332   /**
333    * Find all column families that are replicated from this cluster
334    * @return the full list of the replicated column families of this cluster as:
335    *        tableName, family name, replicationType
336    *
337    * Currently replicationType is Global. In the future, more replication
338    * types may be extended here. For example
339    *  1) the replication may only apply to selected peers instead of all peers
340    *  2) the replicationType may indicate the host Cluster servers as Slave
341    *     for the table:columnFam.
342    */
343   public List<HashMap<String, String>> listReplicated() throws IOException {
344     List<HashMap<String, String>> replicationColFams = new ArrayList<HashMap<String, String>>();
345     HTableDescriptor[] tables = this.connection.listTables();
346 
347     for (HTableDescriptor table : tables) {
348       HColumnDescriptor[] columns = table.getColumnFamilies();
349       String tableName = table.getNameAsString();
350       for (HColumnDescriptor column : columns) {
351         if (column.getScope() != HConstants.REPLICATION_SCOPE_LOCAL) {
352           // At this moment, the columfam is replicated to all peers
353           HashMap<String, String> replicationEntry = new HashMap<String, String>();
354           replicationEntry.put(TNAME, tableName);
355           replicationEntry.put(CFNAME, column.getNameAsString());
356           replicationEntry.put(REPLICATIONTYPE, REPLICATIONGLOBAL);
357           replicationColFams.add(replicationEntry);
358         }
359       }
360     }
361 
362     return replicationColFams;
363   }
364 
365   /**
366    * Enable a table's replication switch.
367    * @param tableName name of the table
368    * @throws IOException if a remote or network exception occurs
369    */
370   public void enableTableRep(final TableName tableName) throws IOException {
371     if (tableName == null) {
372       throw new IllegalArgumentException("Table name cannot be null");
373     }
374     HBaseAdmin admin = null;
375     try {
376       admin = new HBaseAdmin(this.connection.getConfiguration());
377       if (!admin.tableExists(tableName)) {
378         throw new TableNotFoundException("Table '" + tableName.getNamespaceAsString()
379             + "' does not exists.");
380       }
381     } finally {
382       try {
383         admin.close();
384       } catch (IOException e) {
385         LOG.warn("Failed to close admin connection.");
386         LOG.debug("Details on failure to close admin connection.", e);
387       }
388     }
389     byte[][] splits = getTableSplitRowKeys(tableName);
390     checkAndSyncTableDescToPeers(tableName, splits);
391     setTableRep(tableName, true);
392   }
393 
394   /**
395    * Disable a table's replication switch.
396    * @param tableName name of the table
397    * @throws IOException if a remote or network exception occurs
398    */
399   public void disableTableRep(final TableName tableName) throws IOException {
400     if (tableName == null) {
401       throw new IllegalArgumentException("Table name is null");
402     }
403 
404     HBaseAdmin admin = null;
405     try {
406       admin = new HBaseAdmin(this.connection.getConfiguration());
407       if (!admin.tableExists(tableName)) {
408         throw new TableNotFoundException("Table '" + tableName.getNamespaceAsString()
409             + "' does not exists.");
410       }
411     } finally {
412       try {
413         admin.close();
414       } catch (IOException e) {
415         LOG.warn("Failed to close admin connection.");
416         LOG.debug("Details on failure to close admin connection.", e);
417       }
418     }
419     setTableRep(tableName, false);
420   }
421 
422   /**
423    * Get the split row keys of table
424    * @param tableName table name
425    * @return array of split row keys
426    * @throws IOException
427    */
428   private byte[][] getTableSplitRowKeys(TableName tableName) throws IOException {
429     HTable table = null;
430     try {
431       table = new HTable(this.connection.getConfiguration(), tableName);
432       byte[][] startKeys = table.getStartKeys();
433       if (startKeys.length == 1) {
434         return null;
435       }
436       byte[][] splits = new byte[startKeys.length - 1][];
437       for (int i = 1; i < startKeys.length; i++) {
438         splits[i - 1] = startKeys[i];
439       }
440       return splits;
441     } finally {
442       if (table != null) {
443         try {
444           table.close();
445         } catch (IOException e) {
446           LOG.warn("Unable to close table");
447         }
448       }
449     }
450   }
451 
452   /**
453    * Connect to peer and check the table descriptor on peer:
454    * <ol>
455    * <li>Create the same table on peer when not exist.</li>
456    * <li>Throw exception if the table exists on peer cluster but descriptors are not same.</li>
457    * </ol>
458    * @param tableName name of the table to sync to the peer
459    * @param splits table split keys
460    * @throws IOException
461    */
462   private void checkAndSyncTableDescToPeers(final TableName tableName, final byte[][] splits)
463       throws IOException {
464     List<ReplicationPeer> repPeers = listReplicationPeers();
465     if (repPeers == null || repPeers.size() <= 0) {
466       throw new IllegalArgumentException("Found no peer cluster for replication.");
467     }
468     for (ReplicationPeer repPeer : repPeers) {
469       Configuration peerConf = repPeer.getConfiguration();
470       HTableDescriptor htd = null;
471       HBaseAdmin repHBaseAdmin = null;
472       try {
473         repHBaseAdmin = new HBaseAdmin(peerConf);
474         htd = this.connection.getHTableDescriptor(tableName);
475         HTableDescriptor peerHtd = null;
476         if (!repHBaseAdmin.tableExists(tableName)) {
477           repHBaseAdmin.createTable(htd, splits);
478         } else {
479           peerHtd = repHBaseAdmin.getTableDescriptor(tableName);
480           if (peerHtd == null) {
481             throw new IllegalArgumentException("Failed to get table descriptor for table "
482                 + tableName.getNameAsString() + " from peer cluster " + repPeer.getId());
483           } else if (!peerHtd.equals(htd)) {
484             throw new IllegalArgumentException("Table " + tableName.getNameAsString()
485                 + " exists in peer cluster " + repPeer.getId()
486                 + ", but the table descriptors are not same when comapred with source cluster."
487                 + " Thus can not enable the table's replication switch.");
488           }
489         }
490       } finally {
491         if (repHBaseAdmin != null) {
492           try {
493             repHBaseAdmin.close();
494           } catch (IOException e) {
495             LOG.warn("Failed to close admin connection.");
496             LOG.debug("Details on failure to close admin connection.", e);
497           }
498         }
499       }
500     }
501   }
502 
503   @VisibleForTesting
504   public void peerAdded(String id) throws ReplicationException {
505     this.replicationPeers.peerAdded(id);
506   }
507 
508   @VisibleForTesting
509   List<ReplicationPeer> listReplicationPeers() {
510     Map<String, ReplicationPeerConfig> peers = listPeerConfigs();
511     if (peers == null || peers.size() <= 0) {
512       return null;
513     }
514     List<ReplicationPeer> listOfPeers = new ArrayList<ReplicationPeer>(peers.size());
515     for (Entry<String, ReplicationPeerConfig> peerEntry : peers.entrySet()) {
516       String peerId = peerEntry.getKey();
517       try {
518         Pair<ReplicationPeerConfig, Configuration> pair = this.replicationPeers.getPeerConf(peerId);
519         Configuration peerConf = pair.getSecond();
520         ReplicationPeer peer = new ReplicationPeerZKImpl(peerConf, peerId, pair.getFirst());
521         listOfPeers.add(peer);
522       } catch (ReplicationException e) {
523         LOG.warn("Failed to get valid replication peers. "
524             + "Error connecting to peer cluster with peerId=" + peerId + ". Error message="
525             + e.getMessage());
526         LOG.debug("Failure details to get valid replication peers.", e);
527         continue;
528       }
529     }
530     return listOfPeers;
531   }
532 
533   /**
534    * Set the table's replication switch if the table's replication switch is already not set.
535    * @param tableName name of the table
536    * @param isRepEnabled is replication switch enable or disable
537    * @throws IOException if a remote or network exception occurs
538    */
539   private void setTableRep(final TableName tableName, boolean isRepEnabled) throws IOException {
540     HBaseAdmin admin = null;
541     try {
542       admin = new HBaseAdmin(this.connection.getConfiguration());
543       HTableDescriptor htd = admin.getTableDescriptor(tableName);
544       if (isTableRepEnabled(htd) ^ isRepEnabled) {
545         boolean isOnlineSchemaUpdateEnabled =
546             this.connection.getConfiguration()
547                 .getBoolean("hbase.online.schema.update.enable", true);
548         if (!isOnlineSchemaUpdateEnabled) {
549           admin.disableTable(tableName);
550         }
551         for (HColumnDescriptor hcd : htd.getFamilies()) {
552           hcd.setScope(isRepEnabled ? HConstants.REPLICATION_SCOPE_GLOBAL
553               : HConstants.REPLICATION_SCOPE_LOCAL);
554         }
555         admin.modifyTable(tableName, htd);
556         if (!isOnlineSchemaUpdateEnabled) {
557           admin.enableTable(tableName);
558         }
559       }
560     } finally {
561       if (admin != null) {
562         try {
563           admin.close();
564         } catch (IOException e) {
565           LOG.warn("Failed to close admin connection.");
566           LOG.debug("Details on failure to close admin connection.", e);
567         }
568       }
569     }
570   }
571 
572   /**
573    * @param htd table descriptor details for the table to check
574    * @return true if table's replication switch is enabled
575    */
576   private boolean isTableRepEnabled(HTableDescriptor htd) {
577     for (HColumnDescriptor hcd : htd.getFamilies()) {
578       if (hcd.getScope() != HConstants.REPLICATION_SCOPE_GLOBAL) {
579         return false;
580       }
581     }
582     return true;
583   }
584 }