Nacos-服务注册
一.介绍
1.1 Nacos的实现原理
图片来自: https://www.cnblogs.com/wuzhenzhao/p/13625491.html
1.2 本地启动
- 下载源码包: https://github.com/alibaba/nacos.git
下载好后,我个人选择切换到master分支,代码比较完整。
- 找到console,选择Nacos这个类,然后启动, 如果有报错,可能是本地config文件夹下需要一个application.properties文件,从console里面复制一份到本地即可;
- 本地启动时候设置一下单机模式: -Dnacos.standalone=true
1.3 注册中心
注册中心其实就是一个Springboot的项目
@SpringBootApplication(scanBasePackages = "com.alibaba.nacos")
@ServletComponentScan
@EnableScheduling
public class Nacos {
public static void main(String[] args) {
SpringApplication.run(Nacos.class, args);
}
}
启动后会初始化naming,console,config包下面的controllers接口,保存到methodsCache中。
@Component
@EnableScheduling
@PropertySource("/application.properties")
public class ConsoleConfig {
@Autowired
private ControllerMethodsCache methodsCache;
@PostConstruct
public void init() {
methodsCache.initClassMethod("com.alibaba.nacos.naming.controllers");
methodsCache.initClassMethod("com.alibaba.nacos.console.controller");
methodsCache.initClassMethod("com.alibaba.nacos.config.server.controller");
}
}
1.4 Client启动类
spring-cloud-starter-alibaba-nacos-discovery 工程中 META-INF\spring.factories文件注入的类;
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.nacos.NacosDiscoveryAutoConfiguration,\
com.alibaba.cloud.nacos.ribbon.RibbonNacosAutoConfiguration,\
com.alibaba.cloud.nacos.endpoint.NacosDiscoveryEndpointAutoConfiguration,\
com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientAutoConfiguration,\
com.alibaba.cloud.nacos.discovery.configclient.NacosConfigServerAutoConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.discovery.configclient.NacosDiscoveryClientConfigServiceBootstrapConfiguration
二. 服务注册客户端的处理
先来一张服务注册流程图:
图片来自: https://blog.csdn.net/wangwei19871103/article/details/105787403
2.1 NacosDiscoveryAutoConfiguration配置实例化
public class NacosDiscoveryAutoConfiguration {
@Bean
public NacosServiceRegistry nacosServiceRegistry(
NacosDiscoveryProperties nacosDiscoveryProperties) {
return new NacosServiceRegistry(nacosDiscoveryProperties);
}
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosRegistration nacosRegistration(
NacosDiscoveryProperties nacosDiscoveryProperties,
ApplicationContext context) {
return new NacosRegistration(nacosDiscoveryProperties, context);
}
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosAutoServiceRegistration nacosAutoServiceRegistration(
NacosServiceRegistry registry, //上面方法实例化的
AutoServiceRegistrationProperties autoServiceRegistrationProperties,
NacosRegistration registration) { //上面方法实例化的
return new NacosAutoServiceRegistration(registry, autoServiceRegistrationProperties, registration);
}
}
2.1.1 实例化 NacosDiscoveryProperties
读取的是配置文件里面的内容:
@ConfigurationProperties("spring.cloud.nacos.discovery")
public class NacosDiscoveryProperties { ... )
2.1.2 实例化 NacosServiceRegistry
public NacosServiceRegistry(NacosDiscoveryProperties nacosDiscoveryProperties) {
this.nacosDiscoveryProperties = nacosDiscoveryProperties;
this.namingService = nacosDiscoveryProperties.namingServiceInstance();
}
后面工厂调用到:
namingService= NacosFactory.createNamingService(getNacosProperties());
getNacosProperties方法里面加一些秘钥,clusterName啥的操作;
创建方法里面用到反射;
2.1.3 实例化 NacosRegistration
public NacosRegistration(NacosDiscoveryProperties nacosDiscoveryProperties,
ApplicationContext context) {
this.nacosDiscoveryProperties = nacosDiscoveryProperties;
this.context = context;
}
这里就填充一下; 不过在init初始化方法里面填充了一部分management和心跳的信息;
2.1.4 实例化 NacosAutoServiceRegistration
参数 AutoServiceRegistrationProperties里面是几个配置;
public NacosAutoServiceRegistration(ServiceRegistry<Registration> serviceRegistry,
AutoServiceRegistrationProperties autoServiceRegistrationProperties,
NacosRegistration registration) {
super(serviceRegistry, autoServiceRegistrationProperties);
this.registration = registration;
}
2.2 NacosDiscoveryClientAutoConfiguration
@Configuration
@ConditionalOnNacosDiscoveryEnabled
@AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class,
CommonsClientAutoConfiguration.class })
public class NacosDiscoveryClientAutoConfiguration {
@Bean //读取配置文件实例化
@ConditionalOnMissingBean
public NacosDiscoveryProperties nacosProperties() {
return new NacosDiscoveryProperties();
}
@Bean
public DiscoveryClient nacosDiscoveryClient(
NacosDiscoveryProperties discoveryProperties) {
return new NacosDiscoveryClient(discoveryProperties); // 当前的实例的客户端
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.nacos.discovery.watch.enabled", matchIfMissing = true)
public NacosWatch nacosWatch(NacosDiscoveryProperties nacosDiscoveryProperties) {
return new NacosWatch(nacosDiscoveryProperties); // 该类会在启动后发布一次心跳
}
}
2.2.1 实例化NacosWatch
public NacosWatch(NacosDiscoveryProperties properties) {
this(properties, getTaskScheduler());
}
@Override
public void start() {
if (this.running.compareAndSet(false, true)) {
this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(
this::nacosServicesWatch, this.properties.getWatchDelay()); //延迟30000ms
}
}
public void nacosServicesWatch() {
// nacos doesn't support watch now , publish an event every 30 seconds.
this.publisher.publishEvent(
new HeartbeatEvent(this, nacosWatchIndex.getAndIncrement()));
}
2.3 NacosNamingService
1.4.第2节中创建的NamingService跟服务注册、心跳等有关的所有代码都在该类中,在该类构造方法中会调用init方法;
2.3.1 init()方法
private void init(Properties properties) {
namespace = InitUtils.initNamespaceForNaming(properties); //获取namespace
initServerAddr(properties); //初始化注册中心地址
InitUtils.initWebRootContext();
initCacheDir(); // 初始化缓存目录地址,默认在/nacos/naming/public 目录下
initLogName(properties); // 初始化日志名称,默认为naming.log
eventDispatcher = new EventDispatcher(); // 后台会启动一个线程用来创建namingEvent
//下面几个重要, BeatReactor创建心跳反应堆,HostReactor创建主机反应堆
serverProxy = new NamingProxy(namespace, endpoint, serverList);
serverProxy.setProperties(properties);
beatReactor = new BeatReactor(serverProxy, initClientBeatThreadCount(properties));
hostReactor = new HostReactor(eventDispatcher, serverProxy, cacheDir, isLoadCacheAtStart(properties), initPollingThreadCount(properties));
}
2.3.2 EventDispatcher方法创建namingEvent
public EventDispatcher() {
executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
executor.execute(new Notifier());
}
private class Notifier implements Runnable {
@Override
public void run() {
while (true) {
ServiceInfo serviceInfo = null; //5秒
serviceInfo = changedServices.poll(5, TimeUnit.MINUTES);
List<EventListener> listeners = observerMap.get(serviceInfo.getKey());
for (EventListener listener : listeners) {
List<Instance> hosts = Collections.unmodifiableList(serviceInfo.getHosts());
listener.onEvent(new NamingEvent(serviceInfo.getName(), serviceInfo.getGroupName(), serviceInfo.getClusters(), hosts));
}
}
}
}
2.3.3 InitUtils.initNamespaceForNaming()方法
nacos的数据模型是由三元组确定,分别为Namespace、Group、dataid。下面方法用于获取Namespace,默认返回public
/* 初始化命名空间以进行命名。 配置初始化不一样,因此不能直接重用 */
public static String initNamespaceForNaming(Properties properties) {
String tmpNamespace = null;
String isUseCloudNamespaceParsing =
properties.getProperty(PropertyKeyConst.IS_USE_CLOUD_NAMESPACE_PARSING,
System.getProperty(SystemPropertyKeyConst.IS_USE_CLOUD_NAMESPACE_PARSING,
String.valueOf(Constants.DEFAULT_USE_CLOUD_NAMESPACE_PARSING)));
if (Boolean.valueOf(isUseCloudNamespaceParsing)) {
tmpNamespace = TenantUtil.getUserTenantForAns();
tmpNamespace = TemplateUtils.stringEmptyAndThenExecute(tmpNamespace, new Callable<String>() {
@Override
public String call() {
String namespace = System.getProperty(SystemPropertyKeyConst.ANS_NAMESPACE);
return namespace;
}
});
tmpNamespace = TemplateUtils.stringEmptyAndThenExecute(tmpNamespace, new Callable<String>() {
@Override
public String call() {
String namespace = System.getenv(PropertyKeyConst.SystemEnv.ALIBABA_ALIWARE_NAMESPACE);
return namespace;
}
});
}
tmpNamespace = TemplateUtils.stringEmptyAndThenExecute(tmpNamespace, new Callable<String>() {
@Override
public String call() {
String namespace = System.getProperty(PropertyKeyConst.NAMESPACE);
return namespace;
}
});
if (StringUtils.isEmpty(tmpNamespace) && properties != null) {
tmpNamespace = properties.getProperty(PropertyKeyConst.NAMESPACE);
}
tmpNamespace = TemplateUtils.stringEmptyAndThenExecute(tmpNamespace, new Callable<String>() {
@Override
public String call() {
return UtilAndComs.DEFAULT_NAMESPACE_ID; //默认返回"public"
}
});
return tmpNamespace;
}
2.3.4 NamingProxy方法
public NamingProxy(String namespaceId, String endpoint, String serverList) {
this.namespaceId = namespaceId;
this.endpoint = endpoint;
if (StringUtils.isNotEmpty(serverList)) {
this.serverList = Arrays.asList(serverList.split(","));
if (this.serverList.size() == 1) {
this.nacosDomain = serverList;
}
}
initRefreshSrvIfNeed();
}
内部函数 initRefreshSrvIfNeed
private void initRefreshSrvIfNeed() {
// endpoint为空则返回
endpointScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.naming.serverlist.updater");
t.setDaemon(true);
return t;
}
});
//启动了定时线程更新服务注册时间; 30秒
executorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
refreshSrvIfNeed();
}
}, 0, vipSrvRefInterMillis, TimeUnit.MILLISECONDS);
refreshSrvIfNeed();
}
2.4 NacosAutoServiceRegistration
2.4.1 NacosWatch添加发布者
2.4.2 Springboot启动时调用 finishRefresh
@Override
protected void finishRefresh() {
super.finishRefresh();
WebServer webServer = startWebServer();
if (webServer != null) {
publishEvent(new ServletWebServerInitializedEvent(webServer, this));
}
}
2.4.3 NacosAutoServiceRegistration绑定上面的event
public class NacosAutoServiceRegistration
extends AbstractAutoServiceRegistration<Registration> { ...}
父类中方法
@Override
@SuppressWarnings("deprecation")
public void onApplicationEvent(WebServerInitializedEvent event) {
bind(event);
}
@Deprecated
public void bind(WebServerInitializedEvent event) {
his.start();
}
继续调用start:
public void start() {
if (!isEnabled()) {
return;
}
if (!this.running.get()) {
this.context.publishEvent(
new InstancePreRegisteredEvent(this, getRegistration())); //发布实例预注册事件
register(); //这里调用注册方法
if (shouldRegisterManagement()) {
registerManagement();
}
this.context.publishEvent(
new InstanceRegisteredEvent<>(this, getConfiguration())); //发布实例注册事件
this.running.compareAndSet(false, true);
}
}
2.5 NacosServiceRegistry
2.5.1 NacosServiceRegistry注册
@Override
public void register(Registration registration) {
//对应当前应用的application.name
String serviceId = registration.getServiceId();
//表示服务实例信息
Instance instance = getNacosInstanceFromRegistration(registration);
//通过命名服务进行注册
namingService.registerInstance(serviceId, instance);
}
2.5.2 NacosNamingService开始注册实例
主要做两个动作
- 如果当前注册的是临时节点,则构建心跳信息,通过beat反应堆来构建心跳任务
- 调用registerService发起服务注册
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
//是临时节点,则构建心跳信息
if (instance.isEphemeral()) {
BeatInfo beatInfo = new BeatInfo();
beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
beatInfo.setIp(instance.getIp());
beatInfo.setPort(instance.getPort());
beatInfo.setCluster(instance.getClusterName());
beatInfo.setWeight(instance.getWeight());
beatInfo.setMetadata(instance.getMetadata());
beatInfo.setScheduled(false);
long instanceInterval = instance.getInstanceHeartBeatInterval();
beatInfo.setPeriod(instanceInterval == 0 ? DEFAULT_HEART_BEAT_INTERVAL : instanceInterval);
//启动心跳定时任务, 添加心跳信息进行处理
beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
}
//调用服务代理类进行注册
serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
}
addBeatInfo方法里面有一个调度任务
class BeatTask implements Runnable {
BeatInfo beatInfo;
public BeatTask(BeatInfo beatInfo) {
this.beatInfo = beatInfo;
}
@Override
public void run() {
long result = serverProxy.sendBeat(beatInfo);
long nextTime = result > 0 ? result : beatInfo.getPeriod();
//在给定延时之后调度任务
executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}
调用registerService方法(逻辑比较简单):
public void registerService(String serviceName, String groupName, Instance instance) {
final Map<String, String> params = new HashMap<String, String>(9);
//params 填充参数,namespaceId,serviceName,groupName,ip.port,ephemeral等参数(...省略)
//请求API
reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, HttpMethod.POST);
}
调用reqAPI方法:服务在进行注册的时候会轮询配置好的注册中心的地址:
reqApi()首先会发送一次注册请求,如果请求失败则进入重试注册循环中,有3次重试次数,如果依然注册失败则会抛出异常。
public String reqAPI(String api, Map<String, String> params, List<String> servers, String method) {
params.put(CommonParams.NAMESPACE_ID, getNamespaceId());
Exception exception = new Exception();
if (servers != null && !servers.isEmpty()) {
//随机获取一台服务器节点
Random random = new Random(System.currentTimeMillis());
int index = random.nextInt(servers.size());
// 遍历服务列表
for (int i = 0; i < servers.size(); i++) {
String server = servers.get(index); //获得索引位置的服务节点
try {
return callServer(api, params, server, method); //调用指定服务
} catch (NacosException e) { ... }
index = (index + 1) % servers.size(); //轮询下一个
}
}
for (int i = 0; i < UtilAndComs.REQUEST_DOMAIN_RETRY_COUNT; i++) {
return callServer(api, params, nacosDomain);
}
}
callServer(api, params, server, method) 调用,通过HttpURLConnection 进行发起调用。
public String callServer(String api, Map<String, String> params, String curServer, String method) {
HttpClient.HttpResult result = HttpClient.request(url, headers, params, UtilAndComs.ENCODING, method);
}
debug会发现有一个请求: http://xxxx:8848/nacos/v1/ns/instance ;
另外还有: /nacos/v1/ns/instance/beat的定时任务请求;
三. Nacos注册中心端的处理
3.1 InstanceController.register()接口
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
final Instance instance = parseInstance(request); //从请求中解析出instance实例
//调用 ServiceManager 进行服务的注册
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
3.1.1 registerInstance方法
serviceManager,通过该类管理service生命周期活动;
public void registerInstance(String namespaceId, String serviceName, Instance instance) {
//创建空服务,实际上是初始化一个ConcurrentHashMap集合
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
//从serviceMap中,根据namespaceId和serviceName得到一个服务对象
Service service = getService(namespaceId, serviceName);
//调用addInstance创建一个服务实例, 添加一致性协议
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
3.1.2 createServiceIfAbsent方法
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster){
Service service = getService(namespaceId, serviceName); //从serviceMap中获取服务对象
if (service == null) { //如果为空。则初始化
service = new Service();
(...省略部分代码)
service.setLastModifiedMillis(System.currentTimeMillis());
service.recalculateChecksum();
if (cluster != null) {
cluster.setService(service);
service.getClusterMap().put(cluster.getName(), cluster);
}
service.validate();
putServiceAndInit(service);
if (!local) {
addOrReplaceService(service);
}
}
}
Nacos是通过不同的 namespace 来维护服务的,而每个namespace下有不同的group,不同的group下才有对应的Service ,再通过这个 serviceName 来确定服务实例。
第一次进来则会进入初始化,初始化完会调用 putServiceAndInit;
/** Map(namespace, Map(group::serviceName, Service)). */
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
putServiceAndInit方法
private void putServiceAndInit(Service service) throws NacosException {
putService(service); //把服务信息保存到serviceMap集合
service.init(); //建立心跳检测机制
//实现数据一致性监听,ephemeral=true表示采用Distro协议,false表示采用raft
consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
}
3.1.3 addInstance方法
获取到服务后把服务实例添加到集合中,然后基于一致性协议进行数据的同步。然后调用addInstance ;
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips){
//组装key,例如: com.alibaba.nacos.naming.iplist.ephemeral.public##DEFAULT_GROUP@@nacos-config-client
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
Service service = getService(namespaceId, serviceName); //获取刚刚组装的服务
synchronized (service) {
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
Instances instances = new Instances();
instances.setInstanceList(instanceList);
consistencyService.put(key, instances); // 上一步实现监听的类里添加注册服务
}
}
然后给服务注册方发送注册成功的响应(return "ok")。结束服务注册流程;
3.2 InstanceController.beat()接口
@CanDistro
@PutMapping("/beat")
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public ObjectNode beat(HttpServletRequest request) throws Exception {
ObjectNode result = JacksonUtils.createEmptyJsonNode();
result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, switchDomain.getClientBeatInterval());
String beat = WebUtils.optional(request, "beat", StringUtils.EMPTY); //解析beat参数
RsInfo clientBeat = null;
if (StringUtils.isNotBlank(beat)) {
clientBeat = JacksonUtils.toObj(beat, RsInfo.class);
}
//获取namespaceId,serviceName ,serviceName ,ip ,port等参数(...省略)
//获取实例
Instance instance = serviceManager.getInstance(namespaceId, serviceName, serviceName, ip, port);
//如果没有获取到实例则新建一个
if (instance == null) {
instance = new Instance();
//填充实例的属性值ip ,port等(...省略)
//注册实例
serviceManager.registerInstance(namespaceId, serviceName, instance);
}
Service service = serviceManager.getService(namespaceId, serviceName);
if (clientBeat == null) {
clientBeat = new RsInfo();
clientBeat.setIp(ip);
clientBeat.setPort(port);
clientBeat.setCluster(clusterName);
}
//处理客户端心跳方法下章单独分析,主要是客户端续约
service.processClientBeat(clientBeat);
result.put(CommonParams.CODE, NamingResponseCode.OK);
if (instance.containsMetadata(PreservedMetadataKeys.HEART_BEAT_INTERVAL)) {
result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, instance.getInstanceHeartBeatInterval());
}
result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled());
return result; //返回客户端心跳
}
参考:
https://blog.csdn.net/V_zxw/article/details/108244306,
https://www.cnblogs.com/wuzhenzhao/p/13625491.html