前言
我们下载下来的nacos的源码中,在他提供的测试类中有一个叫做NamingExample,其中有服务注册的demo,我们就从这个例子来剖析服务注册的流程。
1、NamingExample#main
这里通过这个测试类我们发现服务注册分为三个步骤:
- 设置属性,如
nacos的服务端地址,连接的命名空间等- 通过工厂方法创建出来一个
NamingService- 调用
NamingService#registerInstance进行服务注册

1.1、NamingFactory#createNamingService
这里就是通过反射创建出来
NamingService,我们看下NamingService的构造方法,看在初始化的时候做了哪些事情
public static NamingService createNamingService(String serverList) throws NacosException {
try {
Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.naming.NacosNamingService");
Constructor constructor = driverImplClass.getConstructor(String.class);
NamingService vendorImpl = (NamingService) constructor.newInstance(serverList);
return vendorImpl;
} catch (Throwable e) {
throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
}
}
1.2 、NacosNamingService构造方法
这里执行两个步骤:
- 将
nacos服务端的链接地址设置到属性中- 调用
init方法进行初始化
public NacosNamingService(String serverList) throws NacosException {
Properties properties = new Properties();
properties.setProperty(PropertyKeyConst.SERVER_ADDR, serverList);
//在这里会调用init方法
init(properties);
}
1.2.1 、NacosNamingService#init
这里重点初始化了两个组件:
changeNotifier(事件通知器):这个后续会讲到,当服务订阅的时候,服务变更,nacosServer通知到对应的client,客户端收到这个通知后会发布对应的事件,然后你可以注册对应的监听者消费对应的事件NamingClientProxyDelegate(客户端代理):内部包含了服务端的相关信息,和与服务端通信的相关组件,来实现对应的服务订阅,上线,下线等操作。
private void init(Properties properties) throws NacosException {
ValidatorUtils.checkInitParam(properties);
//获取namespace
this.namespace = InitUtils.initNamespaceForNaming(properties);
InitUtils.initSerialization();
InitUtils.initWebRootContext(properties);
initLogName(properties);
//事件通知器
this.changeNotifier = new InstancesChangeNotifier();
NotifyCenter.registerToPublisher(InstancesChangeEvent.class, 16384);
NotifyCenter.registerSubscriber(changeNotifier);
this.serviceInfoHolder = new ServiceInfoHolder(namespace, properties);
//这里会创建一个NamingClientProxyDelegate
this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, properties, changeNotifier);
}
1.3 NacosNamingService#registerInstance
- 这里首先会构建
Instance实例,然后把注册的相关信息填充进去- 调用重载方法进行注册,这里重载就是调用
clientProxy来和nacos服务端进行交互,注册对应的实例信息。
public void registerInstance(String serviceName, String groupName, String ip, int port, String clusterName)
throws NacosException {
//创建 instance 实例
Instance instance = new Instance();
instance.setIp(ip);
instance.setPort(port);
instance.setWeight(1.0); //设置weight
instance.setClusterName(clusterName); //设置集群名字
//注册
registerInstance(serviceName, groupName, instance);
}
//服务注册,重载方法
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
NamingUtils.checkInstanceIsLegal(instance);
//进行注册
clientProxy.registerService(serviceName, groupName, instance);
}
1.3.1、NamingClientProxyDelegate#registerService
这里使用了委托模式,会根据实例是不是临时节点而判断选择
gRpc组件还是http组件,由于对gRpc客户端的请求方式不了解,所以我们直接看http请求流程吧。
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
//服务注册
getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
}
private NamingClientProxy getExecuteClientProxy(Instance instance) {
//根据实例是不是临时节点选择使用gRpc还是http
return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
}
1.3.2、NamingHttpClientProxy#registerService
1,判断如果是临时节点,就会封装一个BeatInfo添加到beatReactor组件中,这个是用来发送心跳续约的。
2. 构造请求参数,调用reqApi发送请求
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
instance);
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
//如果是临时节点,然后就会封装beatInfo,添加到beatReactor组件中
if (instance.isEphemeral()) { //如果是临时性质的,这个是为了发送心跳
//创建心跳信息,也就是续约信息
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
//添加到beatReactor,看下addBeatInfo方法
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
final Map<String, String> params = new HashMap<String, String>(16);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, groupedServiceName);
params.put(CommonParams.GROUP_NAME, groupName);
params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
params.put("ip", instance.getIp());
params.put("port", String.valueOf(instance.getPort()));
params.put("weight", String.valueOf(instance.getWeight()));
params.put("enable", String.valueOf(instance.isEnabled()));
params.put("healthy", String.valueOf(instance.isHealthy()));
params.put("ephemeral", String.valueOf(instance.isEphemeral()));
params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
//封装请求参数并发送请求
reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
}
1.3.3、NamingHttpClientProxy#registerService
- 这里包了一层,做了一层失败重试的逻辑处理,首先会随机选一个
server注册,如果失败那么尝试下一个,有几个server重试几次- 请求路径是
/nacos/v1/ns/instance,服务端对应的处理接口是InstanceController#register
public String reqApi(String api, Map<String, String> params, Map<String, String> body, List<String> servers,
String method) throws NacosException {
//添加namespace
params.put(CommonParams.NAMESPACE_ID, getNamespaceId());
//这个远端地址不能为空
if (CollectionUtils.isEmpty(servers) && !serverListManager.isDomain()) {
throw new NacosException(NacosException.INVALID_PARAM, "no server available");
}
NacosException exception = new NacosException();
//失败重试,多个service的时候会随机选一个,然后如果失败,轮询下一个,有多少个service就会重拾多少次
if (serverListManager.isDomain()) {
String nacosDomain = serverListManager.getNacosDomain();
for (int i = 0; i < maxRetry; i++) {
try {
//这里发送请求
return callServer(api, params, body, nacosDomain, method);
} catch (NacosException e) {
exception = e;
if (NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug("request {} failed.", nacosDomain, e);
}
}
}
} else {
//有几个service,重拾几次
Random random = new Random(System.currentTimeMillis());
//随机选择一个server
int index = random.nextInt(servers.size());
//这里如果index失败的话那么,index+1重试下一个
for (int i = 0; i < servers.size(); i++) {
//具体发送方法
String server = servers.get(index);
try {
return callServer(api, params, body, server, method);
} catch (NacosException e) {
exception = e;
if (NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug("request {} failed.", server, e);
}
}
//失败的话server之间轮询
index = (index + 1) % servers.size();
}
}
NAMING_LOGGER.error("request: {} failed, servers: {}, code: {}, msg: {}", api, servers, exception.getErrCode(),
exception.getErrMsg());
throw new NacosException(exception.getErrCode(),
"failed to req API:" + api + " after all servers(" + servers + ") tried: " + exception.getMessage());
}
1.3.4、InstanceController#register
- 从请求中解析出来实例信息
getInstanceOperator方法会判断是服务端注册还是客户端注册,registerInstance执行真正的注册逻辑,由于我们这里是服务端注册,所以看下InstanceOperatorServiceImpl#registerInstance
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
// 拿出来namespaceId ,默认的 namespace 是 public
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
// 获取 serviceName
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
// 解析出来 instance
final Instance instance = parseInstance(request);
//注册实例
getInstanceOperator().registerInstance(namespaceId, serviceName, instance);
return "ok";
}
1.3.5、InstanceOperatorServiceImpl#registerInstance
这里调用
serviceManager的registerInstance方法来进行实例注册
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
com.alibaba.nacos.naming.core.Instance coreInstance = (com.alibaba.nacos.naming.core.Instance) instance;
//注册服务端实例
serviceManager.registerInstance(namespaceId, serviceName, coreInstance);
}
1.3.6、serviceManager#registerInstance
- 这里首先创建了一个空的service
- 然后根据namespaceId和serviceName获取刚刚创建的service。
- 调用addInstance添加实例。
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
//创建空的service
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
//先是根据namespace与serviceName 获取service ,如果没有的话,就创建,最开始的时候,肯定是没有的,然后就会创建一个service,看下这个getService 方法
//获取service
Service service = getService(namespaceId, serviceName);
//这里指定不能是null了
if (service == null) {
throw new NacosException(NacosException.INVALID_PARAM,
"service not found, namespace: " + namespaceId + ", service: " + serviceName);
}
//添加实例
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
1.3.6.1、serviceManager#createEmptyService
- 这里直接调用
createServiceIfAbsent方法,在createServiceIfAbsent方法中会根据namespace和serviceName去获取service,如果不存在的话,则进行创建,然后保存这个service到ServiceMap中并初始化。
public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException {
//创建如果不存在的话
createServiceIfAbsent(namespaceId, serviceName, local, null);
}
//这里就是创建一个空的service
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
throws NacosException {
//根据namespace和serviceName获取service
Service service = getService(namespaceId, serviceName);
if (service == null) {//如果不存在的话
Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
service = new Service();
service.setName(serviceName);
service.setNamespaceId(namespaceId);
//取出来group
service.setGroupName(NamingUtils.getGroupName(serviceName));
// now validate the service. if failed, exception will be thrown
//最后修改事件时间
service.setLastModifiedMillis(System.currentTimeMillis());
service.recalculateChecksum();
if (cluster != null) { //集群
cluster.setService(service);
service.getClusterMap().put(cluster.getName(), cluster);
}
service.validate();
//put service并且初始化
putServiceAndInit(service);
if (!local) {
addOrReplaceService(service);
}
}
}
1.3.6.2、serviceManager#getService
这里发现获取
service就是从map中获取的首先根据namespace选取对应的map,然后再根据serviceName获取对应的service。
// namespace ---》 serivceName ,service
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
public Service getService(String namespaceId, String serviceName) {
if (serviceMap.get(namespaceId) == null) { //如果namespace没有的话
return null;
}
//这个其实就是去serviceMap这个成员中获取
return chooseServiceMap(namespaceId).get(serviceName);
}
1.3.6.4、serviceManager#putServiceAndInit
- 这里会把创建出来的
service再put到对应的map中。这个map就相当于一个注册表- 调用
init方法对service进行初始化- 最后就是添加两个监听器,用来监听
service数据的变更
//将service缓存起来并且初始化这个service
//就是将这个service 放到 serviceMap中,然后service进行初始化, 再就是添加两个监听器。
private void putServiceAndInit(Service service) throws NacosException {
//将这个service放到serviceMap中
putService(service);
service = getService(service.getNamespaceId(), service.getName());
//服务初始化,重点看下这个方法
service.init();
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
}
1.3.6.5、Service#init
这里往健康检查组件中添加一个定时任务,用来实现服务故障剔除,这个后续讲到故障下线会说到。
//这里这个初始化有个非常重要的地方就是往健康检查器中添加一个任务,健康检查的任务,这个任务其实就是扫描这个service里面长时间没有心跳的instance(服务实例),然后进行健康状态改变,服务下线
public void init() {
//往健康check中添加,这个是重点,服务的故障下线就是通过这个实现的。
HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
//cluster初始化
entry.getValue().setService(this);
entry.getValue().init();
}
}
1.3.6.6、serviceManager#addInstance
再上序步骤中
Service已经创建完成,这里会调用addInstance添加服务对应的实例信息。这里大体分为四个步骤:
- 根据
nameSpace,ServiceName和是否是临时节点生成一个key- 根据
nameSpace和ServiceName获取Service- 加锁,然后更新服务的实例列表添加当前实例
- 调用一致性服务同步到集群中的其他节点
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException {
//创建一个key
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
//获取service
Service service = getService(namespaceId, serviceName);
synchronized (service) {
//更新,然后获得到这个服务里面所有实例
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
//塞到instances中
Instances instances = new Instances();
instances.setInstanceList(instanceList);
//调用保存
//调用一致性服务的put方法,这里会调用到DelegateConsistencyServiceImpl#put方法
consistencyService.put(key, instances);
}
}
1.3.6.7、DelegateConsistencyServiceImpl#put
把实例信息同步到集群中的其他节点上,这里是个委托,会根据key是不是临时节点而选择不同的
consisitencyService,我们这里是临时节点,所以对应的是DistroConsistencyServiceImpl
public void put(String key, Record value) throws NacosException {
//这个方法会根据你这个key是临时的还是永久的选择一个consisitencyService
mapConsistencyService(key).put(key, value);
}
1.3.6.8、DelegateConsistencyServiceImpl#put
- 这里
onPut会把数据put到本地内存中,并添加通知任务- 同步到集群中的其他节点
//这里的Record是instance的父类
public void put(String key, Record value) throws NacosException {
//往本地里面存储+内存队列
onPut(key, value);
// If upgrade to 2.0.X, do not sync for v1.
if (ApplicationUtils.getBean(UpgradeJudgement.class).isUseGrpcFeatures()) {
return;
}
//集群间同步
distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
// 延迟1s ,默认是2000 ms ,然后/2
globalConfig.getTaskDispatchPeriod() / 2);
}
1.3.6.8.1、DelegateConsistencyServiceImpl#onPut
- 这个就是把实例信息保存到内存中,这里的
dataStore就是个map。- 添加通一个通知任务,事件是
dataChange。
//这里需要判断一下是否是临时节点,如果是的话,就封装一个Datum,这个东西就是个kv,不用太多care它,接着就是调用datastore 的put方法进行存储。
//最后如果有这个key的监听器的话,就会接着往下走,没有的话就返回,我们在初始化service 的时候是注册了2个监听器的,往上翻翻就可以看到。接着就是调用notifier添加任务。
//先看下这个datastore
public void onPut(String key, Record value) {
//判断是否是临时
if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
//封装datum
Datum<Instances> datum = new Datum<>();
datum.value = (Instances) value;
datum.key = key;
//自增
datum.timestamp.incrementAndGet();
//放到datastore中进行存储
dataStore.put(key, datum);
}
//如果listener里面没有这个key的话直接返回,这个玩意是发布订阅的东西,进行通知,这个玩意service在初始化的会添加进去
if (!listeners.containsKey(key)) {
return;
}
//添加通知任务
notifier.addTask(key, DataOperation.CHANGE);
}
1.3.6.8.2、DelegateConsistencyServiceImpl#addTask
- 这里是做了个幂等,如果是
change事件就往缓存中存一份,然后后续进来就会发现已经存在CHANGE事件,不会重新发送。- 把任务添加到队列中,我们看下什么时候会从队列中获取任务出来,发现在
Notifier#run方法中会不断从队列中取出任务,我们看下。
public void addTask(String datumKey, DataOperation action) {
//如果已经存在,并且是change事件
if (services.containsKey(datumKey) && action == DataOperation.CHANGE) {
return;
}
//如果是change事件
if (action == DataOperation.CHANGE) {
//往这个缓存中放一份
services.put(datumKey, StringUtils.EMPTY);
}
//往任务队列中放
tasks.offer(Pair.with(datumKey, action));
}
1.3.6.8.3、DelegateConsistencyServiceImpl.Notifier#run
这里就是不断取出队列中的任务,然后调用
handle方法进行处理
public void run() {
Loggers.DISTRO.info("distro notifier started");
//就是取出任务队列中的任务,调用handle方法进行处理
for (; ; ) {
try {
//取出任务
Pair<String, DataOperation> pair = tasks.take();
//处理任务
handle(pair);
} catch (Throwable e) {
Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
}
}
}
1.3.6.8.4、DelegateConsistencyServiceImpl.Notifier#handle
- 这里先清理缓存,之前为了防止重复事件的时候设置的
- 通知所有的监听者数据已经变更,
Service也是对应的监听者,然后会回调到Service#onChange方法。
//这里直接通知,调用listener 的onChange活着onDelete执行相关的工作。我们那个时候是将service作为listener注册进来了,所以我们看下service的onChange方法
private void handle(Pair<String, DataOperation> pair) {
try {
String datumKey = pair.getValue0();
DataOperation action = pair.getValue1();
//就是从这个缓存中移除
services.remove(datumKey);
int count = 0;
if (!listeners.containsKey(datumKey)) {
return;
}
for (RecordListener listener : listeners.get(datumKey)) {
count++;
try {
//通知数据已经变更
if (action == DataOperation.CHANGE) {//已经改变
listener.onChange(datumKey, dataStore.get(datumKey).value);
continue;
}
//通知数据删除
if (action == DataOperation.DELETE) {//删除
listener.onDelete(datumKey);
continue;
}
} catch (Throwable e) {
Loggers.DISTRO.error("[NACOS-DISTRO] error while notifying listener of key: {}", datumKey, e);
}
}
if (Loggers.DISTRO.isDebugEnabled()) {
Loggers.DISTRO
.debug("[NACOS-DISTRO] datum change notified, key: {}, listener count: {}, action: {}",
datumKey, count, action.name());
}
} catch (Throwable e) {
Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
}
}
}
1.3.6.8.5、Service#onChange
这里终于到了更新
Service的实例信息了,调用updateIPs更新实例列表。
public void onChange(String key, Instances value) throws Exception {
Loggers.SRV_LOG.info("[NACOS-RAFT] datum is changed, key: {}, value: {}", key, value);
for (Instance instance : value.getInstanceList()) {
if (instance == null) {
// Reject this abnormal instance list:
throw new RuntimeException("got null instance " + key);
}
//处理权重问题
if (instance.getWeight() > 10000.0D) {
instance.setWeight(10000.0D);
}
if (instance.getWeight() < 0.01D && instance.getWeight() > 0.0D) {
instance.setWeight(0.01D);
}
}
//更新instance,核心的在updateIPs这个方法方法中,参数1是新的instance集合,参数2是是否是临时节点
updateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key));
recalculateChecksum();
}
1.3.6.8.6、Service#updateIPs
这里其实就是更新实例信息,这里其实还会设计到
push实例变更到消息到客户端,是通过udp实现的,这个后续会说。
public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
for (String clusterName : clusterMap.keySet()) {
ipMap.put(clusterName, new ArrayList<>());
}
for (Instance instance : instances) {
try {
if (instance == null) {
Loggers.SRV_LOG.error("[NACOS-DOM] received malformed ip: null");
continue;
}
if (StringUtils.isEmpty(instance.getClusterName())) {
//cluster是null的话就设置为DEFAULT
instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
}
//cluster不存在的话就创建对应的cluster
if (!clusterMap.containsKey(instance.getClusterName())) {
Loggers.SRV_LOG
.warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",
instance.getClusterName(), instance.toJson());
Cluster cluster = new Cluster(instance.getClusterName(), this);
cluster.init();
//塞到map中
getClusterMap().put(instance.getClusterName(), cluster);
}
List<Instance> clusterIPs = ipMap.get(instance.getClusterName());
if (clusterIPs == null) {
clusterIPs = new LinkedList<>();
ipMap.put(instance.getClusterName(), clusterIPs);
}
clusterIPs.add(instance);
} catch (Exception e) {
Loggers.SRV_LOG.error("[NACOS-DOM] failed to process ip: " + instance, e);
}
}
for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
//make every ip mine
List<Instance> entryIPs = entry.getValue();
//某个集群,更新集群下面的instance,这个更新instance列表代码很多,我就不贴出来了,主要思想还是比对新老的,然后找出新的instance,与挂了的instance,
// 注意这一步是更新 cluster对象里面的集合,其实就是2个set,一个存临时节点的,一个是存永久节点的。
clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
}
//设置最后修改时间
setLastModifiedMillis(System.currentTimeMillis());
//获取pushservice,然后服务改变,通知改变,这里的push是UdpPush,然后通过spring的监听者来实现推送
getPushService().serviceChanged(this);
ApplicationUtils.getBean(DoubleWriteEventListener.class).doubleWriteToV2(this, ephemeral);
//这里下面就是在做打印日志
StringBuilder stringBuilder = new StringBuilder();
for (Instance instance : allIPs()) {
stringBuilder.append(instance.toIpAddr()).append("_").append(instance.isHealthy()).append(",");
}
Loggers.EVT_LOG.info("[IP-UPDATED] namespace: {}, service: {}, ips: {}", getNamespaceId(), getName(),
stringBuilder.toString());
}
总结
这里整个流程就跑通了,大体逻辑不复杂,但是这个异步更新实例信息设计的点很值得学习,通过事件传递变更实例信息,解藕也更利于扩展。
本文深入剖析了Nacos服务注册的流程,从NamingExample的main方法开始,详细介绍了 NamingService的创建、初始化,特别是NacosNamingService的构造方法和服务注册过程。在注册过程中,涉及了NamingClientProxyDelegate的使用,心跳续约机制,HttpClient的请求,以及服务端InstanceController的处理和Service的管理。服务注册最终通过一致性服务将实例信息同步到集群,实现了服务的注册。
4009

被折叠的 条评论
为什么被折叠?



