nacos整体架构
nacos整体架构可以分为三个部分,位于核心C位的nacos服务,方便用户直观查看和操作的nacos控制台,以及使用nacos功能的客户端服务,如图:
nacos服务架构
nacos服务主要是由功能访问入口OpenAPI,注册中心Naming Service,配置中心Config Service,nacos core核心包,以及一致性协议等部分组成,如图:
外部服务,包括nacos客户端服务,nacos控制台服务,都是通过OpenAPI操作使用注册中心和配置中心的,而一致性协议,保证了nacos集群各节点之间的数据同步。
注册中心
注册中心的使用可以分两部分来看,一部分是服务提供者在注册中心注册服务,一部分是服务消费者在注册中心拉取服务列表发现服务,下面我们分别从服务注册、服务发现的客户端代码和服务端代码,对服务注册和发现的整个过程进行详细了解。
服务注册
客户端
nacos提供了供客户端引入的SDK工具包,之前的篇章中已经进行了简单的使用,不同的客户端架构可以通过符合自身架构的方式对工具包进行调用,这不是本篇重点,不做详细展开,只以SpringCloud为例进行一下简单说明:
SpringCloud使用了自动装配的功能,其中AutoServiceRegistrationAutoConfiguration就是服务注册相关的配置类,该类中注入了AutoServiceRegistration的实例,AutoServiceRegistration是一个接口类,抽象类AbstractAutoServiceRegistration实现了该接口,而NacosAutoServiceRegistration就继承了AbstractAutoServiceRegistration;
项目启动时,因为AbstractAutoServiceRegistration同时实现了ApplicationListener,项目最终会调用AbstractAutoServiceRegistration中的onApplicationEvent方法,方法中会调用到NacosAutoServiceRegistration的register()注册方法,继续向下运行其最终会调用到nacos工具包中NamingService的registerInstance方法,该方法之前的篇章中有进行过调用,是nacos SDK中提供的用于服务注册的方法。
我们的源码解析就从NamingService的registerInstance方法开始,registerInstance方法的实现代码:
@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();
//设置心跳周期,默认值5秒
beatInfo.setPeriod(instanceInterval == 0 ? DEFAULT_HEART_BEAT_INTERVAL : instanceInterval);
//建立心跳连接
beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
}
//服务注册
serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
}
可以看到,客户端在进行注册时,同时进行了两步操作,注册和建立心跳,我们先从服务注册的代码看起:
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
final Map<String, String> params = new HashMap<String, String>(9);
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", JSON.toJSONString(instance.getMetadata()));
//封装了一个http请求,请求地址是UtilAndComs.NACOS_URL_INSTANCE
reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, HttpMethod.POST);
}
访问地址就是OpenAPI的服务注册接口:/nacos/v1/ns/instance(POST),之前篇章中有进行过直接调用。
//请求地址拼接"/nacos/v1/ns/instance(POST)"
public static String WEB_CONTEXT = "/nacos";
public static String NACOS_URL_BASE = WEB_CONTEXT + "/v1/ns";
public static String NACOS_URL_INSTANCE = NACOS_URL_BASE + "/instance";
再看一下建立心跳部分:
public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
BeatInfo existBeat = null;
if ((existBeat = dom2Beat.remove(key)) != null) {
existBeat.setStopped(true);
}
dom2Beat.put(key, beatInfo);
//启动一个BeatTask线程任务,延时getPeriod()(默认5秒)后开始执行
executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
}
BeatTask线程任务:
@Override
public void run() {
if (beatInfo.isStopped()) {
return;
}
//sendBeat发送心跳请求
//请求地址为"/nacos/v1/ns/instance/beat(PUT)"
long result = serverProxy.sendBeat(beatInfo);
long nextTime = result > 0 ? result : beatInfo.getPeriod();
//启动下一次任务,默认5秒后开始
executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}
通过客户端的代码可以看出,客户端再进行服务注册时,先进行了一次注册服务的请求,然后每隔5秒(默认),就会发出一次心跳请求,下边我们来看服务端时怎么处理这两个请求的;
服务端
同样先从注册开始
@CanDistro
@PostMapping
public String register(HttpServletRequest request) throws Exception {
String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
//具体注册逻辑的实现
serviceManager.registerInstance(namespaceId, serviceName, parseInstance(request));
return "ok";
}
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
//创建一个空服务
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);
}
重点在于createEmptyService创建空服务的过程,后续两步主要就是为了将注册的服务实例放入创建的服务中,注意一个服务可以注册多个服务实例。创建空服务方法中会同样先调用getService方法,查看服务是否已存在,不存在则会创建服务并调用putServiceAndInit方法:
private void putServiceAndInit(Service service) throws NacosException {
//将创建的服务放到内存中
putService(service);
//建立服务端心跳状态检查机制,方法启动了一个定时任务ClientBeatCheckTask,延时5秒开始,每5秒执行一次
service.init();
//数据一致性监听
consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
}
首先调用putService,这个方法将新的service放入服务内存中,存放方式是放入了一个双层的ConcurrentHashMap容器中,第一层key值是NamespaceId命名空间ID,第二层key值是serviceName,最后的value为service对象:
public void putService(Service service) {
//serviceMap是一个两层的ConcurrentHashMap<String, Map<String, Service>>
//第一层key值是NamespaceId命名空间ID,不存在加锁创建
if (!serviceMap.containsKey(service.getNamespaceId())) {
synchronized (putServiceLock) {
if (!serviceMap.containsKey(service.getNamespaceId())) {
serviceMap.put(service.getNamespaceId(), new ConcurrentHashMap<>(16));
}
}
}
//第一层key值是serviceName
serviceMap.get(service.getNamespaceId()).put(service.getName(), service);
}
存放完service空服务之后调用了service.init()方法,这个方法主要创建了一个线程任务ClientBeatCheckTask,用于检查心跳更新健康状态:
@Override
public void run() {
try {
if (!getDistroMapper().responsible(service.getName())) {
return;
}
//获取service所有注册服务实例,所以该线程一旦启动,后续无论服务再注册多少实例,都在该线程的检测范围内
List<Instance> instances = service.allIPs(true);
for (Instance instance : instances) {
//如果当前时间-最后一次客户端心跳发送时间>心跳超时时间,则判断当前实例不健康
if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {
if (!instance.isMarked()) {
if (instance.isHealthy()) {
//修改健康状态
instance.setHealthy(false);
//启动服务变更事件,发送基于UDP协议的消息
getPushService().serviceChanged(service);
SpringContext.getAppContext().publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance));
}
}
}
}
if (!getGlobalConfig().isExpireInstance()) {
return;
}
for (Instance instance : instances) {
if (instance.isMarked()) {
continue;
}
//超出删除时间的实例进行删除
if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) {
deleteIP(instance);
}
}
} catch (Exception e) {
}
}
从代码可以看出,服务端在这里只是检查了心跳的更新时间,或者说是在检查客户端最后一次心跳请求的时间,并不会主动连接客户端去确认服务实例的状态。
再看接收客户端心跳部分:
@CanDistro
@PutMapping("/beat")
public JSONObject beat(HttpServletRequest request) throws Exception {
JSONObject result = new JSONObject();
result.put("clientBeatInterval", switchDomain.getClientBeatInterval());
String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID,
Constants.DEFAULT_NAMESPACE_ID);
String beat = WebUtils.required(request, "beat");
RsInfo clientBeat = JSON.parseObject(beat, RsInfo.class);
if (!switchDomain.isDefaultInstanceEphemeral() && !clientBeat.isEphemeral()) {
return result;
}
if (StringUtils.isBlank(clientBeat.getCluster())) {
clientBeat.setCluster(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
}
String clusterName = clientBeat.getCluster();
Instance instance = serviceManager.getInstance(namespaceId, serviceName, clientBeat.getCluster(),
clientBeat.getIp(),
clientBeat.getPort());
if (instance == null) {
instance = new Instance();
instance.setPort(clientBeat.getPort());
instance.setIp(clientBeat.getIp());
instance.setWeight(clientBeat.getWeight());
instance.setMetadata(clientBeat.getMetadata());
instance.setClusterName(clusterName);
instance.setServiceName(serviceName);
instance.setInstanceId(instance.getInstanceId());
instance.setEphemeral(clientBeat.isEphemeral());
serviceManager.registerInstance(namespaceId, serviceName, instance);
}
//获取service
Service service = serviceManager.getService(namespaceId, serviceName);
if (service == null) {
throw new NacosException(NacosException.SERVER_ERROR,
"service not found: " + serviceName + "@" + namespaceId);
}
//启动一个线程任务ClientBeatProcessor更新了心跳时间和健康状态
service.processClientBeat(clientBeat);
result.put("clientBeatInterval", instance.getInstanceHeartBeatInterval());
return result;
}
重点在service.processClientBeat方法内的线程,更新了最后心跳时间,如果该实例已经是不健康状态,还会将其修改回健康状态:
@Override
public void run() {
Service service = this.service;
String ip = rsInfo.getIp();
String clusterName = rsInfo.getCluster();
int port = rsInfo.getPort();
Cluster cluster = service.getClusterMap().get(clusterName);
List<Instance> instances = cluster.allIPs(true);
for (Instance instance : instances) {
if (instance.getIp().equals(ip) && instance.getPort() == port) {
//设定最后心跳时间
instance.setLastBeat(System.currentTimeMillis());
if (!instance.isMarked()) {
//如果已经是不健康状态,更新为健康
if (!instance.isHealthy()) {
instance.setHealthy(true);
//发送服务变更事件,发送基于UDP协议的消息
getPushService().serviceChanged(service);
}
}
}
}
}
注意在注册部分检查最后心跳时间和接收客户端心跳请求部分,在健康状态发生变更的部分,都有调用getPushService().serviceChanged(service)方法,这是一个服务变更的事件,会向指定服务推送服务的信息,和下边说的服务发现部分有关。
服务发现
客户端
服务发现需要调用SDK中namingService.selectInstances方法,实现代码如下:
@Override
public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy, boolean subscribe) throws NacosException {
ServiceInfo serviceInfo;
//subscribe控制是否订阅,默认设置为true
if (subscribe) {
//方法内启用了一个UpdateTask线程
serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","));
} else {
serviceInfo = hostReactor.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","));
}
//对健康状态进行过滤
return selectInstances(serviceInfo, healthy);
}
可以看出,客户端在请求服务信息的同时,默认对服务信息进行了订阅,向内追溯可以发现方法内启动了一个UpdateTask线程任务:
@Override
public void run() {
try {
ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
if (serviceObj == null) {
//发送请求,更新本地服务列表
updateServiceNow(serviceName, clusters);
executor.schedule(this, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
return;
}
if (serviceObj.getLastRefTime() <= lastRefTime) {
//同上
updateServiceNow(serviceName, clusters);
serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
} else {
refreshOnly(serviceName, clusters);
}
//开启下一次任务,缓存间隔CacheMillis在服务端设置,默认为10秒
executor.schedule(this, serviceObj.getCacheMillis(), TimeUnit.MILLISECONDS);
//获取最后关联时间,在服务端设置,为服务端当前系统时间
lastRefTime = serviceObj.getLastRefTime();
} catch (Throwable e) {
NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e);
}
}
public void updateServiceNow(String serviceName, String clusters) {
ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
try {
//发送请求,地址"/nacos/v1/ns/instance/list(GET)"
String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUDPPort(), false);
if (StringUtils.isNotEmpty(result)) {
//更新本地服务信息
processServiceJSON(result);
}
} catch (Exception e) {
NAMING_LOGGER.error("[NA] failed to update serviceName: " + serviceName, e);
} finally {
if (oldService != null) {
synchronized (oldService) {
oldService.notifyAll();
}
}
}
}
从代码可以看出,线程任务每10秒发送一次请求用来获取服务信息,那么10秒内服务信息发生的变化,客户端在周期内无法感知吗?不是的,发送请求的方法中,有一个参数pushReceiver.getUDPPort(),说明客户端有接收UDP请求的端口,并在请求获取服务信息时告知了服务端。我们反过来看UpdateTask,发现他是HostReactor的一个内部类,HostReactor的构造方法如下:
public HostReactor(EventDispatcher eventDispatcher, NamingProxy serverProxy, String cacheDir,
boolean loadCacheAtStart, int pollingThreadCount) {
executor = new ScheduledThreadPoolExecutor(pollingThreadCount, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("com.alibaba.nacos.client.naming.updater");
return thread;
}
});
this.eventDispatcher = eventDispatcher;
this.serverProxy = serverProxy;
this.cacheDir = cacheDir;
if (loadCacheAtStart) {
this.serviceInfoMap = new ConcurrentHashMap<String, ServiceInfo>(DiskCache.read(this.cacheDir));
} else {
this.serviceInfoMap = new ConcurrentHashMap<String, ServiceInfo>(16);
}
this.updatingMap = new ConcurrentHashMap<String, Object>();
this.failoverReactor = new FailoverReactor(this, cacheDir);
//接受UDP通讯信息
this.pushReceiver = new PushReceiver(this);
}
HostReactor在创建的同时,创建了一个用于UDP通讯的类,内部代码不贴了,大概意思就是创建一个线程池内有一个线程执行UDP通讯,这时回想刚刚服务注册的代码中,每当健康状态发生变更时,都会调用getPushService().serviceChanged方法发出UDP通讯,可知除了每10秒获取一次服务信息,服务端和客户端还在通过UDP协议进行通讯。
服务端
@GetMapping("/list")
public JSONObject list(HttpServletRequest request) throws Exception {
String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID,
Constants.DEFAULT_NAMESPACE_ID);
String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
String agent = WebUtils.getUserAgent(request);
String clusters = WebUtils.optional(request, "clusters", StringUtils.EMPTY);
String clientIP = WebUtils.optional(request, "clientIP", StringUtils.EMPTY);
Integer udpPort = Integer.parseInt(WebUtils.optional(request, "udpPort", "0"));
String env = WebUtils.optional(request, "env", StringUtils.EMPTY);
boolean isCheck = Boolean.parseBoolean(WebUtils.optional(request, "isCheck", "false"));
String app = WebUtils.optional(request, "app", StringUtils.EMPTY);
String tenant = WebUtils.optional(request, "tid", StringUtils.EMPTY);
boolean healthyOnly = Boolean.parseBoolean(WebUtils.optional(request, "healthyOnly", "false"));
//返回服务列表
return doSrvIPXT(namespaceId, serviceName, agent, clusters, clientIP, udpPort, env, isCheck, app, tenant,
healthyOnly);
}
doSrvIPXT代码比较长,但重点逻辑比较容易理解,看注释:
public JSONObject doSrvIPXT(String namespaceId, String serviceName, String agent, String clusters, String clientIP,
int udpPort,
String env, boolean isCheck, String app, String tid, boolean healthyOnly)
throws Exception {
ClientInfo clientInfo = new ClientInfo(agent);
JSONObject result = new JSONObject();
//获取指定服务service对象
Service service = serviceManager.getService(namespaceId, serviceName);
//获取缓存时间间隔cacheMillis,默认10秒
long cacheMillis = switchDomain.getDefaultCacheMillis();
try {
if (udpPort > 0 && pushService.canEnablePush(agent)) {
//添加UDP通讯客户端,就是在一个map里维护了所有订阅该服务的客户端通讯信息
pushService.addClient(namespaceId, serviceName,
clusters,
agent,
new InetSocketAddress(clientIP, udpPort),
pushDataSource,
tid,
app);
cacheMillis = switchDomain.getPushCacheMillis(serviceName);
}
} catch (Exception e) {
cacheMillis = switchDomain.getDefaultCacheMillis();
}
//获取服务下所有实例
List<Instance> srvedIPs;
srvedIPs = service.srvIPs(Arrays.asList(StringUtils.split(clusters, ",")));
if (service.getSelector() != null && StringUtils.isNotBlank(clientIP)) {
srvedIPs = service.getSelector().select(clientIP, srvedIPs);
}
//根据健康与否,将服务的实例放入ipMap
Map<Boolean, List<Instance>> ipMap = new HashMap<>(2);
ipMap.put(Boolean.TRUE, new ArrayList<>());
ipMap.put(Boolean.FALSE, new ArrayList<>());
for (Instance ip : srvedIPs) {
ipMap.get(ip.isHealthy()).add(ip);
}
JSONArray hosts = new JSONArray();
//将实例信息放入JSONObject ipObj
for (Map.Entry<Boolean, List<Instance>> entry : ipMap.entrySet()) {
List<Instance> ips = entry.getValue();
if (healthyOnly && !entry.getKey()) {
continue;
}
for (Instance instance : ips) {
if (!instance.isEnabled()) {
continue;
}
JSONObject ipObj = new JSONObject();
ipObj.put("ip", instance.getIp());
ipObj.put("port", instance.getPort());
ipObj.put("valid", entry.getKey());
ipObj.put("healthy", entry.getKey());
ipObj.put("marked", instance.isMarked());
ipObj.put("instanceId", instance.getInstanceId());
ipObj.put("metadata", instance.getMetadata());
ipObj.put("enabled", instance.isEnabled());
ipObj.put("weight", instance.getWeight());
ipObj.put("clusterName", instance.getClusterName());
if (clientInfo.type == ClientInfo.ClientType.JAVA &&
clientInfo.version.compareTo(VersionUtil.parseVersion("1.0.0")) >= 0) {
ipObj.put("serviceName", instance.getServiceName());
} else {
ipObj.put("serviceName", NamingUtils.getServiceName(instance.getServiceName()));
}
ipObj.put("ephemeral", instance.isEphemeral());
hosts.add(ipObj);
}
}
//实例信息放入返回结果
result.put("hosts", hosts);
result.put("name", serviceName);
result.put("cacheMillis", cacheMillis);
//设置最后关联时间为当前系统时间
result.put("lastRefTime", System.currentTimeMillis());
result.put("checksum", service.getChecksum());
result.put("useSpecifiedURL", false);
result.put("clusters", clusters);
result.put("env", env);
result.put("metadata", service.getMetadata());
return result;
}
综上,服务发现的大概流程就是:客户端启动定时任务,默认每10秒请求一次服务端更新订阅的服务信息到本地,同时维护一个UDP协议的通讯任务;服务端在接到请求后,返回客户端请求获取的服务信息,并将客户端的通讯地址信息维护到本地内存,对应其订阅的服务,当该服务中实例的健康状态发生变化时,将变更情况基于UDP推送给所有订阅该服务的客户端。
整体运行图
整体运行过程如图所示:
结束
注册中心的运行过程实现原理大致就是这些,下次再解析一下配置中心。