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