引言
承接 手写分布式存储系统v0.2版本 ,今天开始新的迭代开发。主要实现 服务发现
功能
一、什么是服务发现
由于咱们的服务是分布式的,那从服务管理的角度来看肯定是要有一个机制来知道具体都有哪些实例可以提供服务。举个例子就是,张三家里在全国各地有不少火锅加盟店,那张三肯定要有一个方式知道这些火锅店加盟店的情况。例如上海又新开了一家加盟店,那么这家加盟店肯定要先通过某种方式联系张三,这样张三才能将配方以及食材供应给这家新的加盟店等等。
疑问
-
为什么不能通过域名映射的方式来做映射,客户端通过域名调用服务就好了为啥要专门做服务发现
答:域名映射是对外提供服务时使用的,而我们的系统还有很多场景要做内部的服务管理,例如某个节点故障了,为了服务能够继续保证高可用,咱们的分布式存储系统就要将这个节点上所管理的数据分给其余的节点进行管理等,这个时候系统内部就需要明确知道各个分布式节点的信息。
二、服务发现设计
目前服务发现设计主要有以下几种
- 配置化:将所有节点的信息写在服务配置里,像ES等
- 使用能保证一致性的外部服务:如kafka、bookkeeper等,外部服务有zookeeper、etcd、consul等
- 主从架构里,所有从节点启动时自动向主服务注册自己的节点信息:如hdfs、yarn等
为了方便扩展,同时咱们的存储服务能够设计成无主架构,因此采用第二种采用外部服务zookeeper来进行实现。实现的大致流程如下图
所有节点实例在启动时,都去zookeeper上创建属于自己的目录,在节点下线时就将自己对应的目录进行删除。这样只需要监听“服务发现目录”就能知道是否有节点上下线。同时为了避免服务故障时没能正确删除自己的目录,因此咱们采用zookeeper临时目录的功能,例如节点1启动并在zookeeper创建对应临时目录后,会每隔一小段时间向zookeeper发送请求也就是心跳,证明自己的服务还正常;如果zookeeper在等待一段时间后,没收到某个节点的心跳,就会默认这个服务已经挂了并将其对应的临时目录进行删除。
三、代码实现
由于把全部代码贴上来不太现实且不易于阅读,就将开发时核心测试样例贴上来供大家伙参考
package com.sherlock;import org.apache.zookeeper.*;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Stat;import java.io.File;
import java.util.List;
import java.util.concurrent.CountDownLatch;/*** author: shalock.lin* date: 2024/2/4* describe:*/
public class BaseZookeeper implements Watcher {private static ZooKeeper zookeeper;public static void main(String[] args) throws Exception {BaseZookeeper baseZookeeper = new BaseZookeeper();baseZookeeper.connectZookeeper("127.0.0.1:2181");List<String> children = baseZookeeper.getChildren("/");System.out.println(children);AsyncCallback.StringCallback scb = new AsyncCallback.StringCallback() {@Overridepublic void processResult(int rc, String path, Object ctx, String name) {System.out.println(rc);}};asyncCreateFullPathOptimistic(zookeeper,"/distributed-storage-system/available/shalocklindeMacBook-Pro.local", "testData".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT,scb, null);Thread.sleep(5000);List<String> afterChildren = baseZookeeper.getChildren("/");System.out.println(afterChildren);}/*** 超时时间*/private static final int SESSION_TIME_OUT = 2000;private CountDownLatch countDownLatch = new CountDownLatch(1);@Overridepublic void process(WatchedEvent event) {if (event.getState() == Event.KeeperState.SyncConnected) {System.out.println("Watch received event");countDownLatch.countDown();}}/**连接zookeeper* @param host* @throws Exception*/public void connectZookeeper(String host) throws Exception{zookeeper = new ZooKeeper(host, SESSION_TIME_OUT, this);countDownLatch.await();System.out.println("zookeeper connection success");}/*** 获取路径下所有子节点* @param path* @return* @throws KeeperException* @throws InterruptedException*/public List<String> getChildren(String path) throws KeeperException, InterruptedException{List<String> children = zookeeper.getChildren(path, false);return children;}/*** 获取节点上面的数据* @param path 路径* @return* @throws KeeperException* @throws InterruptedException*/public String getData(String path) throws KeeperException, InterruptedException{byte[] data = zookeeper.getData(path, false, null);if (data == null) {return "";}return new String(data);}/*** 设置节点信息* @param path 路径* @param data 数据* @return* @throws KeeperException* @throws InterruptedException*/public Stat setData(String path, String data) throws KeeperException, InterruptedException{Stat stat = zookeeper.setData(path, data.getBytes(), -1);return stat;}/*** 删除节点* @param path* @throws InterruptedException* @throws KeeperException*/public void deleteNode(String path) throws InterruptedException, KeeperException{zookeeper.delete(path, -1);}/*** 获取某个路径下孩子的数量* @param path* @return* @throws KeeperException* @throws InterruptedException*/public Integer getChildrenNum(String path) throws KeeperException, InterruptedException{int childenNum = zookeeper.getChildren(path, false).size();return childenNum;}/*** 关闭连接* @throws InterruptedException*/public void closeConnection() throws InterruptedException{if (zookeeper != null) {zookeeper.close();}}public static void asyncCreateFullPathOptimistic(final ZooKeeper zk, final String originalPath, final byte[] data,final List<ACL> acl, final CreateMode createMode,final AsyncCallback.StringCallback callback, final Object ctx) {zk.create(originalPath, data, acl, createMode, new AsyncCallback.StringCallback() {@Overridepublic void processResult(int rc, String path, Object ctx, String name) {if (rc != KeeperException.Code.NONODE.intValue()) {callback.processResult(rc, path, ctx, name);return;}// Since I got a nonode, it means that my parents don't exist// create mode is persistent since ephemeral nodes can't be// parentsString parent = new File(originalPath).getParent().replace("\\", "/");asyncCreateFullPathOptimistic(zk, parent, new byte[0], acl,CreateMode.PERSISTENT, new StringCallback() {@Overridepublic void processResult(int rc, String path, Object ctx, String name) {if (rc == KeeperException.Code.OK.intValue() || rc == KeeperException.Code.NODEEXISTS.intValue()) {// succeeded in creating the parent, now// create the original pathasyncCreateFullPathOptimistic(zk, originalPath, data,acl, createMode, callback, ctx);} else {callback.processResult(rc, path, ctx, name);}}}, ctx);}}, ctx);}
}
四、功能演示
整个功能验证逻辑如下
-
服务启动前观测zookeeper对应目录下不存在数据
-
启动服务,从控制台能看到服务正常启动
-
再观测zookeeper对应目录下注册了服务的主机名
-
通过打印输出,能看到存在该目录下的服务信息(当前存的是测试样例数据)
-
停止服务,并持续观测一段时间,可以看到目录已被zookeeper删除
五、总结
终于开发一点跟“分布式”相关的内容了,在使用zookeeper时踩了一点坑, 启动服务时报下述异常org.apache.zookeeper.KeeperException$SessionExpiredException: KeeperErrorCode = Session expired for
,通过调试发现zookeeper服务端返回的信息非常有限无法得出有用的信息。结果网上的答案,排除了是防火墙、超时配置等问题后,最终发现是自己在调用zookeeper创建路径是直接传了完整的路径也就是多级目录/distributed-storage-system/available/shalocklindeMacBook-Pro.local
导致的报错,原因是zookeeper不支持递归创建多级目录,只能参考bookkeeper开发工具类从代码层面递归去zookeeper创建路径。惭愧的是,已经接触zookeeper多年,并且也翻过它的代码,却连这个基本的点都不知晓。因此进一步验证上一篇的想法,就是很多东西真的要自己去实现一遍,否则只沉浸理论容易陷入一种“什么都懂”、“什么都是理所当然”的幻觉并自我感觉良好,但这很有可能会令我们的技术止步不前