在分布式系统中的中心管理服务模式下,往往采用的模式是1个manager服务节点,多个worker节点,然后由manager来管控这些worker节点。但是本篇文章不是来讲manager如何管理的问题,而是woker识别发现manager服务的问题。目前一种比较简单的做法,通过worker节点本地配置的方式,来指定manager服务地址。这种方式实现较为容易,但是可维护性并不高。比如一个简单的场景,如果manager节点地址发生改变,其下worker节点内所标明的manager地址就得被动地一个个更新了。我们可以用一个专业的术语表示这个现象:Service Discovery(服务发现)。
服务发现说到底就是让客户端如何快速,高效地“找到”服务端。前面提到的通过本地配置直接指明地址的方式是一种,但是确切地来说,它并不高效。
一种更为高效的方式应该是下面这种:
客户端始终联系(通信)的是一个固定(共享)的地址,而不是实际的地址,通过这个共享的地址,我们能够找到实际的地址。
可能有人会说了,这不就是代理地址的意思嘛。但其实这并不完全等同于代理地址的意思。在后面的篇幅内,后具体介绍这里面的差异。
针对上节提到的大原则的前提下,我们有哪些可行的方案呢?从最近Hadoop社区讨论中,笔者归纳出了以下几种:
下面笔者给出社区上被提出过的第二种方案的具体代码实现,大家可以仔细理解其中的解决过程(这里依赖的外部存储是ZK,分布式系统服务为HDFS)。
/** * 服务发现抽象类. */ public abstract class NameserviceDiscovery implements Configurable, Closeable { private static final Logger LOG = LoggerFactory.getLogger(NameserviceDiscovery.class); /** 针对每个服务发现实例,进行缓存构造. */ protected static final LoadingCache<Id, NameserviceDiscovery> CACHE = CacheBuilder.newBuilder() .expireAfterAccess(1, TimeUnit.MINUTES) .removalListener(getRemover()) .build(getLoader()); /** Local configuration. */ private Configuration conf; static class Id { // 服务发现类 Class<? extends NameserviceDiscovery> clazz; // 节点当前配置信息 Configuration conf; Id( Class<? extends NameserviceDiscovery> className, Configuration config) { this.clazz = className; this.conf = config; } } /** * 从缓存中获得服务发现实例 */ public static NameserviceDiscovery get(Configuration conf) { Class<? extends NameserviceDiscovery> clazz = conf.getClass( DFS_DISCOVERY_CLASS_KEY, DFS_DISCOVERY_CLASS_DEFAULT, NameserviceDiscovery.class); try { Id key = new Id(clazz, conf); return CACHE.get(key); } catch (ExecutionException e) { LOG.error("Cannot get a nameservice discovery from the cache", e); } return ReflectionUtils.newInstance(clazz, conf); } private static CacheLoader<Id, NameserviceDiscovery> getLoader() { return new CacheLoader<Id, NameserviceDiscovery>() { @Override public NameserviceDiscovery load(Id id) throws Exception { return ReflectionUtils.newInstance(id.clazz, id.conf); } }; } private static RemovalListener<Id, NameserviceDiscovery> getRemover() { return new RemovalListener<Id, NameserviceDiscovery>() { @Override public void onRemoval( RemovalNotification<Id, NameserviceDiscovery> notification) { NameserviceDiscovery discovery = notification.getValue(); try { discovery.close(); } catch (IOException e) { LOG.error("Cannot close nameservice discovery"); } } }; } @Override public void setConf(Configuration config) { this.conf = config; } @Override public Configuration getConf() { return this.conf; } /** * 获取服务地址方法 */ public abstract Collection<String> getNameServiceIds(); public abstract Map<String, Map<String, InetSocketAddress>> public abstract Map<String, Map<String, InetSocketAddress>> getHttpAddresses(); public abstract Map<String, Map<String, InetSocketAddress>> getHttpsAddresses(); }
下面是基于ZK的服务发现实现类,
/** * 基于ZK的服务发现实现类. */ public class ZookeeperBasedNameserviceDiscovery extends NameserviceDiscovery implements DynamicNameserviceDiscovery { private static final Logger LOG = LoggerFactory.getLogger(ZookeeperBasedNameserviceDiscovery.class); /** ZK管理器接口. */ private ZKCuratorManager zkManager; /** 实际地址信息的ZK存储目录. */ private String baseZNode; /** * 初始化ZK连接操作 */ public void init() { if (zkManager == null) { Configuration conf = getConf(); baseZNode = conf.get( DFS_DISCOVERY_ZK_PARENT_PATH_KEY, DFS_DISCOVERY_ZK_PARENT_PATH_DEFAULT); try { zkManager = new ZKCuratorManager(conf); zkManager.start(); } catch (IOException e) { LOG.error("Cannot initialize the ZK connection", e); } } } /** * 关闭ZK连接 */ public void close() throws IOException { if (zkManager != null) { zkManager.close(); zkManager = null; } } /** * 从ZK中获取地址的操作方法 */ Map<String, Map<String, InetSocketAddress>> getAddresses( final String attr) throws IOException { Map<String, Map<String, InetSocketAddress>> ret = Maps.newLinkedHashMap(); try { List<String> nsIds = zkManager.getChildren(baseZNode); for (String nsId : nsIds) { Map<String, InetSocketAddress> nsMap = Maps.newLinkedHashMap(); String pathNs = baseZNode + "/" + nsId; List<String> nnIds = zkManager.getChildren(pathNs); for (String nnId : nnIds) { String pathNn = pathNs + "/" + nnId; String pathAddress = pathNn + "/" + attr; String addr = zkManager.getStringData(pathAddress); InetSocketAddress sockAddr = NetUtils.createSocketAddr(addr); nsMap.put(nnId, sockAddr); } ret.put(nsId, nsMap); } } catch (Exception e) { LOG.error("Cannot get the addresses", e); throw new IOException(e.getMessage()); } return ret; } /** * 其它类型方法 */ @Override public Collection<String> getNameServiceIds() { init(); try { return getAddresses("rpcAddress").keySet(); } catch (IOException e) { // Fallback to the configuration based return getConf().getTrimmedStringCollection(DFS_NAMESERVICES); } } @Override public Map<String, Map<String, InetSocketAddress>> getRpcAddresses() { init(); try { return getAddresses("rpcAddress"); } catch (IOException e) { // Fallback to the configuration based Configuration conf = getConf(); return DFSUtilClient.getAddresses(conf, null, HdfsClientConfigKeys.DFS_NAMENODE_RPC_ADDRESS_KEY); } } @Override public Map<String, Map<String, InetSocketAddress>> getHttpAddresses() { init(); try { return getAddresses("httpAddress"); } catch (IOException e) { // Fallback to the configuration based Configuration conf = getConf(); return DFSUtilClient.getAddresses(conf, null, HdfsClientConfigKeys.DFS_NAMENODE_HTTP_ADDRESS_KEY); } } @Override public Map<String, Map<String, InetSocketAddress>> getHttpsAddresses() { init(); try { return getAddresses("httpsAddress"); } catch (IOException e) { // Fallback to the configuration based Configuration conf = getConf(); return DFSUtilClient.getAddresses(conf, null, HdfsClientConfigKeys.DFS_NAMENODE_HTTPS_ADDRESS_KEY); } } }
使用的方式很简单,调用底层NameserviceDiscovery的接口即可。在系统中将配置解析操作方法替换为上述接口方式的话,服务发现的方式就优化成了第二种方案了,可维护性也增强了许多。以上就是一个简单的依赖外部Store的服务发现的实现方案。
[1].https://issues.apache.org/jira/browse/HADOOP-15774. Discovery of HA servers
[2].https://issues.apache.org/jira/browse/HDFS-13312. NameNode High Availability ZooKeeper based discovery rather than explicit nn1,nn2 configs