前言
- 在API架构解析一(项目的启动和加载)中已经介绍了如何构造API的静态存储单元ApiModel和API的执行轨迹的接口Api以及其实现类ApiHandler,同时介绍了如何构造拦截器调用链并把ApiHandler封装在InterceptChain的最后一层,通过反射完成最后的业务逻辑调用。
注册项目到zk
- 为了使调用方发现自己,API项目在启动的时候需要将项目的基本信息,包括API名称,版本号,IP地址等信息注册到Zookeeper上,利用zk的注册与发现机制调用方可以很容易找到对应版本的API并访问。
- 首先需要搭建zk环境,可以参考介绍zk的技术文章,这不是本文的重点,搭建好集群后,可以拿到zk的ZkSessionManager,具体如下。
private static ZkSessionManager getZkMgr() {
ZkSessionManager zkSessionManager = ZkUtils.getInstance(zkConf.getServer(), zkConf.getTimeout() + "");
zkSessionManager.addConnectionListener(new ApiZkListener());
return zkSessionManager;
}
- 其中zkConf是API项目中的配置文件,主要配置了zk集群的ip地址,连接超时时间以及通用的注册路径等。
- ApiZkListener是ConnectionListener接口的实现类,主要用来监听与zk集群的连接情况,最主要的是重写ConnectionListener接口的expired()方法用来保活,具体如下。
public class ApiZkListener implements ConnectionListener {
private static final Logger logger = LoggerFactory.getLogger(ApiZkListener.class);
@Override
public void expired() {
logger.error("***ApiZkListener expired");
reconnect();
}
@Override
public void disconnected() {
logger.error("***ApiZkListener disconnected");
reconnect();
}
private void reconnect() {
try {
logger.error("让ZK服务端API机器列表刷新");
Thread.sleep(10000l);
if (!RegisterMgr.existsApi()) {
RegisterMgr.regApiNode();
logger.error("API机器重连完成");
} else {
logger.error("ZK服务端并未注销本机器,或者watcher已生效,结束本次尝试");
}
} catch (Throwable throwable) {
logger.error("ZK reconnect失败", throwable);
}
}
@Override
public void syncConnected() {
logger.error("***ApiZkListener syncConnected");
}
}
- 把项目信息如API名称,版本号等(放在项目的配置文件中),以EPHEMERAL临时节点模式注册到zk上,核心代码如下。
private static final ZkSessionManager zkMgr = getZkMgr();
private static volatile ZooKeeper zooKeeper;
private static volatile boolean over = false;
private static final Lock lock = new ReentrantLock();
public static boolean regApiNode() {
boolean flag = false;
lock.lock();
try {
if (over) {
logger.error("项目已手动结束 无需注册zk");
return true;
}
ApiConfig config = ApiService.getApiConfig();
if (config != null && config.isRegisterToZk()) {
String path = getCurApiPath();
ZooKeeper zk = getZooKeeper();
try {
zk.create(path, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
zk.getData(path, watcher, null);
flag = true;
logger.error("注册zk success path=" + path);
} catch (KeeperException e) {
if (e instanceof KeeperException.NoNodeException) {
logger.error("注册zk error path:" + path, e);
} else if (e instanceof KeeperException.NodeExistsException) {
logger.error("注册zk 重复");
flag = true;
} else {
logger.error("注册zk error 重试 path:" + path, e);
reloadZooKeeper();
regApiNode();
}
} catch (InterruptedException e) {
logger.error("注册zk error path:" + path, e);
}
} else {
flag = true;
logger.error("zk无需注册");
}
} finally {
lock.unlock();
}
return flag;
}
private static final Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
regApiNode();
}
};
- 加锁是为了确保一台服务器只注册一次。
- zk.getData(path, watcher, null)添加watcher事件,监听节点的变化。
- 到此为止,每个API项目在启动的时候就可以把自己的项目信息注册到zk上了,假设注册路径为mist/apilist,注册结构如下mapi_100_192.168.100.100
利用代理发现API,保证服务对用户的透明
- 真实系统中,可能会存在多套API并存同时对外提供服务的情况,比如给移动端提供的API,给公司内部其他业务线提供的API,开放出去给第三方合作公司提供的API等等,为了统一管理可以在API接口层的前面提供一个proxy代理。
- 利用proxy代理可以给用户提供透明的API接口服务,用户按照约定请求即可,proxy负责将请求路由纷发到具体的API,为此需要给每个API关联一个标识用来区分,并且将这个标识映射成用户的key值,提供给用户,用户发起请求时会带着自己的userKey这样就可以区分了。
- 有些API比如提供给移动端的API可能要兼容多个版本,因此proxy路由的粒度要到版本这个级别,因为同一个API不同版本的逻辑可能是不同的,对于有多版本需求的客户端请求的时候需要加上对应的版本号,同时配置一个默认的版本号处理固定版本的请求。
Proxy的路由和分发
- 项目启动的时候从zk上加载各API信息,核心代码如下
private static final ZkSessionManager zkMgr = ZkUtils.getInstance(config.getServer(), config.getTimeout() + "");
private static ZooKeeper zk = zkMgr.getZooKeeper();
private static void loadApiServer() {
try {
List<String> list = zk.getChildren(config.getZkPath(), watcher);
doLoadApiServer(list);
} catch (KeeperException e) {
if (e instanceof KeeperException.NoNodeException) {
logger.error("请检查目录 path=" + config.getZkPath(), e);
} else {
zk = ZkUtils.ZK_SESSION_MANAGER.getZooKeeper();
logger.error("loadServer异常 休眠50ms", e);
ThreadUtil.sleep(50);
loadApiServer();
}
} catch (Exception e) {
logger.error("加载zk失败", e);
}
}
private static final Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.None) {
zk = zkMgr.getZooKeeper();
}
loadApiServer();
}
};
- 其中config.getZkPath()从配置文件读取,即为API注册的路径mist/apilist,watcher用于注册监听事件。
- doLoadApiServer(list)核心代码如下
private static volatile Map<String, ApiServerMap> apiServerListMap = new HashMap<String, ApiServerMap>();
private static void doLoadApiServer(List<String> list) {
Map<String, ApiServerModel> apiMap = CACHE.getByTime().getApiServerModelMap(); //从DB加载配置数据
Map<String, ApiServerMap> map = new HashMap<String, ApiServerMap>(apiMap.size());
for (String apiNameDesc : list) {
ApiVersionModel model = buildApiServerModel(apiNameDesc);
if (model != null) {
ApiServerMap tempMap = map.get(model.getName());
if (tempMap == null) {
tempMap = new ApiServerMap();
map.put(model.getName(), tempMap);
}
tempMap.putApiModel(model); //按版本聚集服务器,并维护最新版本
}
}
apiServerListMap = map;
logger.error("zk列表=" + list + ",apiServerListMap=" + JSONUtil.json(map));
}
- CACHE中主要维护API服务的信息和用户注册的信息这两个纬度,从数据库中读取。
public class ApiServerModel {
private String name; //API名称
private String invokeServiceId; //执行服务,支持RPC调用
private String infoServiceId; //查询API提供接口集合的信息服务
private Integer accessType;
}
public class ChannelModel {
private String name; //用户注册的名称
private Integer valid; //
private String code; //用户的userKey
private String desc;
private String apiName; //对应的具体api
private String secretKey; //加密的密钥
private Integer encryptType; //加密粒度,是否加密
private Integer skipSign; //跳过签名验证
}
- ApiVersionModel中存储的就是API注册搭配zk上的基本信息mapi_100_192.168.100.100,结构如下,buildApiServerModel()将字符串分割转换成ApiVersionModel类
class ApiVersionModel {
private String name; //api名称
private Integer version; //api版本号
private String ip; //服务器ip地址
}
- ApiServerMap这个数据结构中维护着按版本聚集的对应api服务机器的关系,因为同一API的同一版本一般也是由多台机器部署来对外提供服务。其结核心代码如下
public class ApiServerMap {
private Integer newestVersion;
private final Map<Integer, List<ApiVersionModel>> versionToApiModels = new HashMap<Integer, List<ApiVersionModel>>(32);
public Integer getNewestVersion() {
return newestVersion;
}
public List<ApiVersionModel> getApiModels(Integer version) {
return versionToApiModels.get(version);
}
public List<ApiVersionModel> getNewestApiModels() {
return versionToApiModels.get(newestVersion);
}
public void putApiModel(ApiVersionModel model) {
List<ApiVersionModel> list = versionToApiModels.get(model.getVersion());
if (list == null) {
list = new ArrayList<ApiVersionModel>(32);
versionToApiModels.put(model.getVersion(), list);
}
list.add(model);
if (newestVersion == null || newestVersion < model.getVersion()) {
newestVersion = model.getVersion();
}
}
}
小结
- 至此项目的注册与发现的大体流程就介绍完了,API在启动的时候会将自身的基本信息注册到ZK集群上,proxy代理启动的时候会从zk上搂取各API的信息,完成转发和路由。
- 下面将会介绍客户端发起一次请求的详细流转步骤。