Nacos源码分析系列 - 服务注册
Author:zxw
email:502513206@qq.com
@ Jishou University
@Nacos:https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html
1.前言
nacos官方有集成好的nacos-server包,copy下来只需要运行其中的startup.bat文件即可启动注册中心,但是这边我们从源码的角度入手注册中心,所以将源码包拷贝下来,通过手动启动的方式启动nacos注册中心。首先获取到源码后,在本地导入相关的包然后找到consol
e工程,启动Nacos类。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zNBdKRIQ-1598514282237)(nacos源码分析系列 - 注册中心.assets/image-20200824222148890.png)]
2.注册中心
可以看到注册中心其实就是一个Springboot的项目
@SpringBootApplication(scanBasePackages = "com.alibaba.nacos")
@ServletComponentScan
@EnableScheduling
public class Nacos {
public static void main(String[] args) {
SpringApplication.run(Nacos.class, args);
}
}
3.启动类
这边我使用的是spring-cloud版本的nacos客户端,导入了spring-cloud-starter-alibaba-nacos-discovery
在该工程中主要注入了下面这些类,根据之前分析cloud-eureka源码的经验,基本逻辑就在AutoConfiguration类中,让我们对每个类一个一个解析
// EnableAutoConfiguration
com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration,
com.alibaba.cloud.nacos.ribbon.RibbonNacosAutoConfiguration,
com.alibaba.cloud.nacos.endpoint.NacosDiscoveryEndpointAutoConfiguration,
com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration,
com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientConfiguration,
com.alibaba.cloud.nacos.discovery.reactive.NacosReactiveDiscoveryClientConfiguration,
com.alibaba.cloud.nacos.discovery.configclient.NacosConfigServerAutoConfiguration
// BootstrapConfiguration
com.alibaba.cloud.nacos.discovery.configclient.NacosDiscoveryClientConfigServiceBootstrapConfiguration
注入的对象很多,但是大部分都会汇聚到一个对象里进行生命周期的保管,所以我主要挑几个有用的对象来讲
NacosDiscoveryAutoConfiguration
@Bean
@ConditionalOnMissingBean
public NacosDiscoveryProperties nacosProperties() {
// 配置文件类,spring.cloud.nacos.discovery
return new NacosDiscoveryProperties();
}
}
NacosDiscoveryClientConfiguration
@Bean
public DiscoveryClient nacosDiscoveryClient(
NacosServiceDiscovery nacosServiceDiscovery) {
// 这个就相当于我们当前的实例的客户端了,会持有上面那个配置类的信息
return new NacosDiscoveryClient(nacosServiceDiscovery);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.nacos.discovery.watch.enabled",
matchIfMissing = true)
public NacosWatch nacosWatch(NacosDiscoveryProperties nacosDiscoveryProperties,
ObjectProvider<TaskScheduler> taskScheduler) {
// 该类会在启动后发布一次心跳
return new NacosWatch(nacosDiscoveryProperties, taskScheduler);
}
NacosServiceRegistryAutoConfiguration
@Bean
public NacosServiceRegistry nacosServiceRegistry(
NacosDiscoveryProperties nacosDiscoveryProperties) {
// 根据类名可以看到,跟服务注册相关了
return new NacosServiceRegistry(nacosDiscoveryProperties);
}
// 继续往下
public NacosServiceRegistry(NacosDiscoveryProperties nacosDiscoveryProperties) {
this.nacosDiscoveryProperties = nacosDiscoveryProperties;
this.namingService = nacosDiscoveryProperties.namingServiceInstance();
}
public NamingService namingServiceInstance() {
if (null != namingService) {
return namingService;
}
// 通过工厂创建namingService对象
try {
namingService = NacosFactory.createNamingService(getNacosProperties());
}
catch (Exception e) {
log.error("create naming service error!properties={},e=,", this, e);
return null;
}
return namingService;
}
NacosNamingService
跟服务注册、心跳等有关的所有代码都在该类中,在该类构造方法中会调用init方法
private void init(Properties properties) {
// 获取namespace
namespace = InitUtils.initNamespaceForNaming(properties);
// 初始化注册中心地址
initServerAddr(properties);
InitUtils.initWebRootContext();
// 初始化缓存目录地址,默认在/nacos/naming/public 目录下
initCacheDir();
// 初始化日志名称,默认为naming.log
initLogName(properties);
// 后台会启动一个线程用来创建namingEvent
eventDispatcher = new EventDispatcher();
// 以下几个比较重要,在后面具体讲
serverProxy = new NamingProxy(namespace, endpoint, serverList, properties);
beatReactor = new BeatReactor(serverProxy, initClientBeatThreadCount(properties));
hostReactor = new HostReactor(eventDispatcher, serverProxy, cacheDir, isLoadCacheAtStart(properties),
initPollingThreadCount(properties));
}
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.parseBoolean(isUseCloudNamespaceParsing)) {
tmpNamespace = TenantUtil.getUserTenantForAns();
tmpNamespace = TemplateUtils.stringEmptyAndThenExecute(tmpNamespace, new Callable<String>() {
@Override
public String call() {
String namespace = System.getProperty(SystemPropertyKeyConst.ANS_NAMESPACE);
LogUtils.NAMING_LOGGER.info("initializer namespace from System Property :" + namespace);
return namespace;
}
});
tmpNamespace = TemplateUtils.stringEmptyAndThenExecute(tmpNamespace, new Callable<String>() {
@Override
public String call() {
String namespace = System.getenv(PropertyKeyConst.SystemEnv.ALIBABA_ALIWARE_NAMESPACE);
LogUtils.NAMING_LOGGER.info("initializer namespace from System Environment :" + namespace);
return namespace;
}
});
}
tmpNamespace = TemplateUtils.stringEmptyAndThenExecute(tmpNamespace, new Callable<String>() {
@Override
public String call() {
String namespace = System.getProperty(PropertyKeyConst.NAMESPACE);
LogUtils.NAMING_LOGGER.info("initializer namespace from System Property :" + namespace);
return namespace;
}
});
if (StringUtils.isEmpty(tmpNamespace) && properties != null) {
tmpNamespace = properties.getProperty(PropertyKeyConst.NAMESPACE);
}
// 默认返回"public"
tmpNamespace = TemplateUtils.stringEmptyAndThenExecute(tmpNamespace, new Callable<String>() {
@Override
public String call() {
return UtilAndComs.DEFAULT_NAMESPACE_ID;
}
});
return tmpNamespace;
}
namingProxy
在该对象中,启动了两个定时线程,一个是用来更新权限验证时间,一个用来更新服务注册时间
private void initRefreshTask() {
this.executorService = new ScheduledThreadPoolExecutor(2, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.naming.updater");
t.setDaemon(true);
return t;
}
});
// 30秒
this.executorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
refreshSrvIfNeed();
}
}, 0, vipSrvRefInterMillis, TimeUnit.MILLISECONDS);
// 5秒
this.executorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
securityProxy.login(getServerList());
}
}, 0, securityInfoRefreshIntervalMills, TimeUnit.MILLISECONDS);
refreshSrvIfNeed();
this.securityProxy.login(getServerList());
}
NacosAutoServiceRegistration
在springcloud中,服务启动代码都是通过AutoServiceRegistration的实现类来操作
@Override
protected void register() {
if (!this.registration.getNacosDiscoveryProperties().isRegisterEnabled()) {
log.debug("Registration disabled.");
return;
}
if (this.registration.getPort() < 0) {
this.registration.setPort(getPort().get());
}
// 调用NacosServiceRegistry的register()方法
super.register();
}
NacosServiceRegistry
@Override
public void register(Registration registration) {
if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
return;
}
String serviceId = registration.getServiceId();
String group = nacosDiscoveryProperties.getGroup();
Instance instance = getNacosInstanceFromRegistration(registration);
try {
// 注册我们的client
namingService.registerInstance(serviceId, group, instance);
log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
instance.getIp(), instance.getPort());
}
catch (Exception e) {
log.error("nacos registry, {} register failed...{},", serviceId,
registration.toString(), e);
// rethrow a RuntimeException if the registration is failed.
// issue : https://github.com/alibaba/spring-cloud-alibaba/issues/1132
rethrowRuntimeException(e);
}
}
接下来看看该类的服务注册代码。第一次创建时会通过groupname + servicename创建一个心跳实例并启动心跳检测定时任务
registerInstance
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
// groupname 和 servicename 组合,DEFAULT_GROUP@@serviceName
// 默认groupname为DEFAULT_GROUP
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
// 默认为true
if (instance.isEphemeral()) {
// 构建心跳实例
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
// 启动心跳定时任务
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
// 服务注册
serverProxy.registerService(groupedServiceName, groupName, instance);
}
registerService
在该模块中组装实例参数,然后发送到/nacos/v1/ns/instance
接口
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
instance);
// 组装实例信息
final Map<String, String> params = new HashMap<String, String>(16);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, serviceName);
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);
}
reqApi()
首先会发送一次注册请求,如果请求失败则进入重试注册循环中,有3次重试次数,如果依然注册失败则会抛出异常。有关注册是使用的HttpClient对象
public String reqApi(String api, Map<String, String> params, Map<String, String> body, List<String> servers,
String method) throws NacosException {
params.put(CommonParams.NAMESPACE_ID, getNamespaceId());
// 判断服务地址
if (CollectionUtils.isEmpty(servers) && StringUtils.isEmpty(nacosDomain)) {
throw new NacosException(NacosException.INVALID_PARAM, "no server available");
}
NacosException exception = new NacosException();
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 {
// 发起HttpClient请求
return callServer(api, params, body, server, method);
} catch (NacosException e) {
exception = e;
if (NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug("request {} failed.", server, e);
}
}
index = (index + 1) % servers.size();
}
}
// 重试注册
if (StringUtils.isNotBlank(nacosDomain)) {
for (int i = 0; i < UtilAndComs.REQUEST_DOMAIN_RETRY_COUNT; 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);
}
}
}
}
// 注册失败,抛出异常
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());
}
接下来看看注册中心端的,在InstanceController.register()
中
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
// 获取servucename
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
// 获取namespace
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
// 构建instance实例
final Instance instance = parseInstance(request);
// 注册到nacos的生命周期中
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
ServiceManager
,通过该类管理我们的service生命周期活动
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
// 将当前服务加入service
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
Service service = getService(namespaceId, serviceName);
if (service == null) {
throw new NacosException(NacosException.INVALID_PARAM,
"service not found, namespace: " + namespaceId + ", service: " + serviceName);
}
// 添加一致性协议
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
createServiceIfAbsent
在执行该方法的时候首先从Map获取service实例,如果没有则在当前namespace下创建当前服务实例
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
throws NacosException {
// 从当前namespaceId的map中取出该servicename对应的Service,如果不存在就创建
Service service = getService(namespaceId, serviceName);
// 如果为空,则为当前服务创建service实例
if (service == null) {
Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
service = new Service();
service.setName(serviceName);
service.setNamespaceId(namespaceId);
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();
putServiceAndInit(service);
if (!local) {
addOrReplaceService(service);
}
}
}
自此nacos的服务注册流程就结束了,nacos中首先使用一个Map存储namespace - Map的关系,在Map中又以Key和实例信息对应。