手动实现RPC框架系列文章
本系列文章,功能实现来自于 Github 作者 Java Guide的开源作品,我个人是选择边实现边学习的方式,本系列的文章是对Guide哥的作品地实现进行讲解和学习。( 作为我实现作品的笔记)
下面是Guide作品的连接,推荐大家可以直接进去下载并且学习
(一款基于 Netty+Kyro+Zookeeper 实现的自定义 RPC 框架-附详细实现过程和相关教程。) (github.com)
个人推荐,关于代码和实现部分直接在GitHub上拉取Guide哥的作品,也可以结合我本系列的文章一起学习,因为我写的文章相当于结合Guide哥的代码和文章再进行一个解读和分析,搭配使用味道更佳,我也是一个初学者,因为记录的是本人的学习过程,所以可能有不严谨和出错的地方,望海涵。如果有能帮助到您的地方,我将万分荣幸。
手动实现RPC框架_种一棵橙子树的博客-CSDN博客
前言
前面的部分,我们已经完成了网络传输模块的代码,并且对网络传输模块的实现流程进行了讲解。而现在我们则需要开始整合注册中心进行使用。我们前面提到过,注册中心负责服务地址的注册与发现,相当于一个目录,我们把服务的地址注册进去后,客户端就可以发现这些服务地址,然后就能去到对应的服务提供方进行连接,发起服务请求。
一、注册中心模块结构
这里的两个接口 ServiceDiscovery 和 ServiceRegistry 分别用于定义服务发现和服务注册。
服务注册接口
@SPI
public interface ServiceRegistry {
/**
* register service
*
* @param rpcServiceName rpc service name
* @param inetSocketAddress service address
*/
void registerService(String rpcServiceName, InetSocketAddress inetSocketAddress);
}
服务发现接口
@SPI
public interface ServiceDiscovery {
/**
* lookup service by rpcServiceName
*
* @param rpcRequest rpc service pojo
* @return service address
*/
InetSocketAddress lookupService(RpcRequest rpcRequest);
}
我们使用ZooKeeper来作为注册中心,并且实现这两个接口
服务注册实现类
@Slf4j
public class ZkServiceRegistryImpl implements ServiceRegistry {
@Override
public void registerService(String rpcServiceName, InetSocketAddress inetSocketAddress) {
String servicePath = CuratorUtils.ZK_REGISTER_ROOT_PATH + "/" + rpcServiceName + inetSocketAddress.toString();
CuratorFramework zkClient = CuratorUtils.getZkClient();
CuratorUtils.createPersistentNode(zkClient, servicePath);
}
}
当我们的服务被注册进ZooKeeper的时候,我们需要将完整的服务名称 rpcServiceName (由服务接口名字 + group+version)作为根节点,子节点就是对应的服务地址(ip+端口号)
version:服务版本,为后续不兼容升级提供可能
group:主要用于处理一个接口有多个实现类的情况。
就像这张图
然后我们要通过服务发现获取某个服务对应的地址的话,就直接根据完整的服务名称来获取到其下所有的子节点,然后根据负载均衡策略取出其中一个进行连接就可以了。服务发现的实现类如下
@Slf4j
public class ZkServiceDiscoveryImpl implements ServiceDiscovery {
private final LoadBalance loadBalance;
public ZkServiceDiscoveryImpl() {
this.loadBalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension("loadBalance");
}
@Override
public InetSocketAddress lookupService(RpcRequest rpcRequest) {
String rpcServiceName = rpcRequest.getRpcServiceName();
CuratorFramework zkClient = CuratorUtils.getZkClient();
List<String> serviceUrlList = CuratorUtils.getChildrenNodes(zkClient, rpcServiceName);
if (CollectionUtil.isEmpty(serviceUrlList)) {
throw new RpcException(RpcErrorMessageEnum.SERVICE_CAN_NOT_BE_FOUND, rpcServiceName);
}
// load balancing
String targetServiceUrl = loadBalance.selectServiceAddress(serviceUrlList, rpcRequest);
log.info("Successfully found the service address:[{}]", targetServiceUrl);
String[] socketAddressArray = targetServiceUrl.split(":");
String host = socketAddressArray[0];
int port = Integer.parseInt(socketAddressArray[1]);
return new InetSocketAddress(host, port);
}
}
我们根据rpcRequest中携带的信息可以获取到服务接口名字。然后通过Curator操作ZooKeeper来获取该服务接口路径下的所有子节点,用List集合来保存子节点服务的URL(ip+端口号)。
然后通过负载均衡策略选出一个子节点服务地址进行连接,这里要把ip地址和端口号进行一个拆分
这里的负载均衡策略,Guide哥实现了两种方式,我们可以根据Dubbo的规范来自定义负载均衡策略,实现的方法可以阅读下面的官方文档。
简单的意思就是,我们只需要去实现一个loadbalance接口就可以了。
这里Guide哥自己实现了loadbalance接口
@SPI
public interface LoadBalance {
/**
* Choose one from the list of existing service addresses list
*
* @param serviceUrlList Service address list
* @param rpcRequest
* @return target service address
*/
String selectServiceAddress(List<String> serviceUrlList, RpcRequest rpcRequest);
}
然后又编写了实现这个接口的抽象类
public abstract class AbstractLoadBalance implements LoadBalance {
@Override
public String selectServiceAddress(List<String> serviceAddresses, RpcRequest rpcRequest) {
if (CollectionUtil.isEmpty(serviceAddresses)) {
return null;
}
if (serviceAddresses.size() == 1) {
return serviceAddresses.get(0);
}
return doSelect(serviceAddresses, rpcRequest);
}
protected abstract String doSelect(List<String> serviceAddresses, RpcRequest rpcRequest);
}
然后通过继承抽象类重写方法的形式来自定义负载均衡策略。
第一个随机负载均衡策略很简单,就是利用随机数挑选lisi集合中的地址。
public class RandomLoadBalance extends AbstractLoadBalance {
@Override
protected String doSelect(List<String> serviceAddresses, RpcRequest rpcRequest) {
Random random = new Random();
return serviceAddresses.get(random.nextInt(serviceAddresses.size()));
}
}
第二个属于一致性哈希算法。
关于一致性哈希算法的基本思想,在我的Redis文章中有提到过,大家可以去阅读一下
学习记录篇:Redis缓存 (二)_种一棵橙子树的博客-CSDN博客
Dubbo本身也有给我们提供一致性哈希算法,大家可以去下面这篇文章阅读
Dubbo 一致性Hash负载均衡实现剖析 | Apache Dubbo
至于Guide哥手写的这个一致性哈希算法,我就没有过多的去研究了,我想先尝试使用上面那个简单的哈希算法让整个框架跑起来先,后面有精力的话,再去优化和理解这个一致性哈希算法。如果有朋友有关于这个哈希算法的理解,欢迎在评论区留言,我会细心阅读。谢谢!!
@Slf4j
public class ConsistentHashLoadBalance extends AbstractLoadBalance {
private final ConcurrentHashMap<String, ConsistentHashSelector> selectors = new ConcurrentHashMap<>();
@Override
protected String doSelect(List<String> serviceAddresses, RpcRequest rpcRequest) {
int identityHashCode = System.identityHashCode(serviceAddresses);
// build rpc service name by rpcRequest
String rpcServiceName = rpcRequest.getRpcServiceName();
ConsistentHashSelector selector = selectors.get(rpcServiceName);
// check for updates
if (selector == null || selector.identityHashCode != identityHashCode) {
selectors.put(rpcServiceName, new ConsistentHashSelector(serviceAddresses, 160, identityHashCode));
selector = selectors.get(rpcServiceName);
}
return selector.select(rpcServiceName + Arrays.stream(rpcRequest.getParameters()));
}
static class ConsistentHashSelector {
private final TreeMap<Long, String> virtualInvokers;
private final int identityHashCode;
ConsistentHashSelector(List<String> invokers, int replicaNumber, int identityHashCode) {
this.virtualInvokers = new TreeMap<>();
this.identityHashCode = identityHashCode;
for (String invoker : invokers) {
for (int i = 0; i < replicaNumber / 4; i++) {
byte[] digest = md5(invoker + i);
for (int h = 0; h < 4; h++) {
long m = hash(digest, h);
virtualInvokers.put(m, invoker);
}
}
}
}
static byte[] md5(String key) {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
byte[] bytes = key.getBytes(StandardCharsets.UTF_8);
md.update(bytes);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e.getMessage(), e);
}
return md.digest();
}
static long hash(byte[] digest, int idx) {
return ((long) (digest[3 + idx * 4] & 255) << 24 | (long) (digest[2 + idx * 4] & 255) << 16 | (long) (digest[1 + idx * 4] & 255) << 8 | (long) (digest[idx * 4] & 255)) & 4294967295L;
}
public String select(String rpcServiceKey) {
byte[] digest = md5(rpcServiceKey);
return selectForKey(hash(digest, 0));
}
public String selectForKey(long hashCode) {
Map.Entry<Long, String> entry = virtualInvokers.tailMap(hashCode, true).firstEntry();
if (entry == null) {
entry = virtualInvokers.firstEntry();
}
return entry.getValue();
}
}
}
最后返回一个InetSocketAddress对象,就是服务的地址了。
总结
本节文章对集成ZooKeeper注册中心的使用进行了学习和分析,对于注册中心的用处比较简单,就是提供服务注册,以及服务发现的功能,发现相比注册麻烦一些的地方在于服务发现会通过负载均衡策略来挑选一个服务进行使用,关于负载均衡的策略有很多,常见的轮询,权重,一致性哈希算法等等,各位朋友可以自行了解。不过平时我们使用Dubbo作为RPC框架直接使用Dubbo给我们提供的负载均衡策略就差不多够用了。