好久不见,最近工作一直被一堆事所捆绑,也没有大块的时间去看技术相关的东西,以至于部分伙伴的留言都没有及时的回复,先和大家分享一句话突然想起来的一句吧,不变的可能是这个世界,不断变化的可能是我们自己。
微服务架构在近几年变的越来越流行,这可能会成为我们的一个必备技能(本人的建议是如果现有的系统没有遇到什么运维、业务瓶颈的话不要为了单纯的使用微服务技术而去进行改造)。
在微服务的早期随着Spring Cloud的支撑,好多都使用了Spring默认的Netflix全家桶,但是现在Netflix也不再进行维护且社区活跃度也比较低,所以在注册中心部分很多团队都会在Nacos、Zookeeper、Kubernetes中进行选择,本文不对其中的优缺点进行比较和分析,直接来干货Spring Cloud Alibaba Nacos 服务如何注册。
一、服务注册初始化阶段
其实对于服务的注册基本上所有开源项目的实现思路基本一致,无非就是服务的注册、心跳维持,看下图直接找到服务注册的源码,一般这种代码我都是从AutoConfig开始看了,大家可以先大体看一下,这几个AutoConfig功能其实就是初始化红框内的其他的的非AutoConfig类。
项目启动以后首先会通过NacosDiscoveryAutoConfiguration根据配置文件初始化NacosDiscoveryProperties,NacosDiscoveryProperties为配置文件中配置的服务注册相关参数(如serverAddr、group等),然后紧接着初始化NacosServiceDiscovery,该类主要是通过的NacosDiscoveryProperties的namingServiceInstance属性实现获取注册中心的相关信息。最后初始化NacosDiscoveryClient对NacosServiceDiscovery进行封装。
在服务注册方面,会通过NacosServiceRegistryAutoConfiguration依次初始化NacosServiceRegistry、NacosRegistration、NacosAutoServiceRegistration,在初始化NacosServiceRegistry时会对之前的NacosDiscoveryProperties的namingServiceInstance属性进行实例化,其中服务的注册的核心代码(com.alibaba.cloud.nacos.registry.NacosServiceRegistry#register)也在该类中。在初始化AbstractAutoServiceRegistration时候,大家会发现它的父类实现了ApplicationListener,当发布事件时候会执行onApplicationEvent中的代码(可以参考我伙伴的文章),这也是服务注册的入口。
二、服务注册阶段
在AbstractAutoServiceRegistration的onApplicationEvent方法中执行了start方法,start方法中会执行register方法,最终调用的方法就是之前说到的com.alibaba.cloud.nacos.registry.NacosServiceRegistry#register,完成对服务进行注册。
public void register(Registration registration) {
if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
} else {
String serviceId = registration.getServiceId();
String group = this.nacosDiscoveryProperties.getGroup();
Instance instance = this.getNacosInstanceFromRegistration(registration);
try {
this.namingService.registerInstance(serviceId, group, instance);
log.info("nacos registry, {} {} {}:{} register finished", new Object[]{group, serviceId, instance.getIp(), instance.getPort()});
} catch (Exception var6) {
log.error("nacos registry, {} register failed...{},", new Object[]{serviceId, registration.toString(), var6});
ReflectionUtils.rethrowRuntimeException(var6);
}
}
}
在服务注册的代码中,最核心的是this.namingService.registerInstance(serviceId, group, instance);这一行,最终实现注册的方法是com.alibaba.nacos.client.naming.net.NamingProxy#registerService,在这个方法中通过调用nacas的api包中的reqAPI来与注册中心的服务端进行交互从而完成注册。
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
LogUtils.NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", new Object[]{this.namespaceId, serviceName, instance});
Map<String, String> params = new HashMap(9);
params.put("namespaceId", this.namespaceId);
params.put("serviceName", serviceName);
params.put("groupName", groupName);
params.put("clusterName", 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", JSON.toJSONString(instance.getMetadata()));
this.reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, "POST");
}
public String callServer(String api, Map<String, String> params, String body, String curServer, String method) throws NacosException {
long start = System.currentTimeMillis();
long end = 0L;
this.injectSecurityInfo(params);
List<String> headers = this.builderHeaders();
String url;
if (!curServer.startsWith("https://") && !curServer.startsWith("http://")) {
if (!curServer.contains(":")) {
curServer = curServer + ":" + this.serverPort;
}
url = HttpClient.getPrefix() + curServer + api;
} else {
url = curServer + api;
}
HttpResult result = HttpClient.request(url, headers, params, body, "UTF-8", method);
end = System.currentTimeMillis();
MetricsMonitor.getNamingRequestMonitor(method, url, String.valueOf(result.code)).observe((double)(end - start));
if (200 == result.code) {
return result.content;
} else if (304 == result.code) {
return "";
} else {
throw new NacosException(result.code, result.content);
}
}
在reqAPI的最后部分的代码com.alibaba.nacos.client.naming.net.NamingProxy#callServer(java.lang.String, java.util.Map<java.lang.String,java.lang.String>, java.lang.String, java.lang.String, java.lang.String)中,与服务端进行交互的过程中,会将自身的信息发送给服务端,服务端响应OK后完成注册,请求服务端的信息如下。
地址:http://192.168.***.***:8848/nacos/v1/ns/instance
内容:{app=nacos-provider, metadata={"preserved.register.source":"SPRING_CLOUD"}, ip=192.168.***.***, weight=1.0, ephemeral=true, serviceName=DEFAULT_GROUP@@nacos-provider, encoding=UTF-8, groupName=DEFAULT_GROUP, namespaceId=public, port=8081, enable=true, healthy=true, clusterName=DEFAULT}
三、心跳的维持
服务注册到注册中心服务端后,服务端与客户端会一直保持一个心跳,来对服务的可用性进行维护,在之前的部分我们有说过NacosServiceRegistry类中会对namingService进行实例化,同样也是利用nacos的api包通过反射来进行初始化。
public static NamingService createNamingService(Properties properties) throws NacosException {
try {
Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.naming.NacosNamingService");
Constructor constructor = driverImplClass.getConstructor(Properties.class);
NamingService vendorImpl = (NamingService)constructor.newInstance(properties);
return vendorImpl;
} catch (Throwable var4) {
throw new NacosException(-400, var4);
}
}
在对NacosNamingService进行初始化的过程中,我们会在构造函数中发现他还会初始化com.alibaba.nacos.client.naming.NacosNamingService#beatReactor,而BeatReactor中存在一个线程池,我们在服务注册之前(com.alibaba.nacos.client.naming.NacosNamingService#registerInstance(java.lang.String, java.lang.String, com.alibaba.nacos.api.naming.pojo.Instance)方法)就会向这个线程池中发布Task。
public BeatReactor(NamingProxy serverProxy, int threadCount) {
this.lightBeatEnabled = false;
this.dom2Beat = new ConcurrentHashMap();
this.serverProxy = serverProxy;
this.executorService = new ScheduledThreadPoolExecutor(threadCount, new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("com.alibaba.nacos.naming.beat.sender");
return thread;
}
});
}
public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
LogUtils.NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
String key = this.buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
BeatInfo existBeat = null;
if ((existBeat = (BeatInfo)this.dom2Beat.remove(key)) != null) {
existBeat.setStopped(true);
}
this.dom2Beat.put(key, beatInfo);
this.executorService.schedule(new BeatReactor.BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
MetricsMonitor.getDom2BeatSizeMonitor().set((double)this.dom2Beat.size());
}
下面我们在看一下这个定时任务中的内容,其实与服务的注册原理相似,都是与服务端进行一个交互,如果服务端响应该服务不可用,则重新进行注册,如果依旧可用(或已经重新注册)则继续将新的Task(BeatTask)放入到线程池中,从而不断的与服务端维持心跳。
地址:http://192.168.***.***:8848/nacos/v1/ns/instance/beat
内容:{app=nacos-provider, namespaceId=public, port=8081, clusterName=DEFAULT, ip=192.168.***.***, serviceName=DEFAULT_GROUP@@nacos-provider, encoding=UTF-8}
class BeatTask implements Runnable {
BeatInfo beatInfo;
public BeatTask(BeatInfo beatInfo) {
this.beatInfo = beatInfo;
}
public void run() {
if (!this.beatInfo.isStopped()) {
long nextTime = this.beatInfo.getPeriod();
try {
JSONObject result = BeatReactor.this.serverProxy.sendBeat(this.beatInfo, BeatReactor.this.lightBeatEnabled);
long interval = (long)result.getIntValue("clientBeatInterval");
boolean lightBeatEnabled = false;
if (result.containsKey("lightBeatEnabled")) {
lightBeatEnabled = result.getBooleanValue("lightBeatEnabled");
}
BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
if (interval > 0L) {
nextTime = interval;
}
int code = 10200;
if (result.containsKey("code")) {
code = result.getIntValue("code");
}
if (code == 20404) {
Instance instance = new Instance();
instance.setPort(this.beatInfo.getPort());
instance.setIp(this.beatInfo.getIp());
instance.setWeight(this.beatInfo.getWeight());
instance.setMetadata(this.beatInfo.getMetadata());
instance.setClusterName(this.beatInfo.getCluster());
instance.setServiceName(this.beatInfo.getServiceName());
instance.setInstanceId(instance.getInstanceId());
instance.setEphemeral(true);
try {
BeatReactor.this.serverProxy.registerService(this.beatInfo.getServiceName(), NamingUtils.getGroupName(this.beatInfo.getServiceName()), instance);
} catch (Exception var10) {
}
}
} catch (NacosException var11) {
LogUtils.NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}", new Object[]{JSON.toJSONString(this.beatInfo), var11.getErrCode(), var11.getErrMsg()});
}
BeatReactor.this.executorService.schedule(BeatReactor.this.new BeatTask(this.beatInfo), nextTime, TimeUnit.MILLISECONDS);
}
}
}
小结
上文介绍了Spring Cloud Alibaba Nacos 服务注册的实现原理与源码解析,相信大家应该大体了解了服务是如何注册到Nacos中,但是可能大家接下来还有疑惑,我们在服务调用的时候,是从哪里获取的所要调用的服务可用列表,以及这个列表是如何与服务端共同维护的,请听下回分解。