Naocs 是Alibaba开源的微服务生态中的一个组件,可以实现微服务治理中的服务注册发现,配置中心功能。
项目地址: https://github.com/alibaba/Nacos
Service is a first-class citizen in Nacos. Nacos supports discovering, configuring, and managing almost all types of services:
Kubernetes Service
gRPC & Dubbo RPC Service
Spring Cloud RESTful Service
引用官方的话来说,在Naocs中服务的地位非常重要。所以Nacos支持服务的发现、配置和管理所有类型的服务,包括Kubernetes Service、gRPC 和 Dubbo RPC Service 以及 Spring Cloud RESTful Service。
我们已经搭建起来了Nacos的集群。
只要涉及到集群,那么就会涉及到主从的关系。在Nacos中采用了的是Leader-Follower模式。显然这种模式是存在者选举机制的,谁当老大谁当老二,还是要走走民主的过程滴。
Raft算法演示
Nacos 集群采用Raft算法,来实现的Leader选举。
选举算法在RaftCore中,包括数据处理和同步。
Raft中,节点有三种角色:
选举过程在两个时间节点发送:
选举的过程:
所有节点在最开始的时候,都是Follower状态。如果在一段时间内没有收到Leader的心跳(可能是Leader挂了或者根本没有Leader),
就会将自己的状态改为Candidate,开始竞选,并且会增加一个叫term的票据。
在上面的演示地址的动画中,我们可以清晰的看到。Raft算法实现数据同步的方式是采用了 2PC提交的方式保证了数据的一致性。
过程:
服务注册 :
在程序启动过的时候,我们可以看到这个类 NacosDiscoveryProperties,有一个注解@ConfigurationProperties 去加载我们在application.properties 文件中的 spring.cloud.nacos.discovery 的相关配置
//加载配置类
@ConfigurationProperties("spring.cloud.nacos.discovery")
public class {
private String serverAddr;
...
@PostConstruct
public void init() throws SocketException {
...
// 获取服务的Ip地址
if (StringUtils.isEmpty(ip)) {
// traversing network interfaces if didn't specify a interface
if (StringUtils.isEmpty(networkInterface)) {
ip = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
}
else {
...
}
}
//设置参数的默认值
this.overrideFromEnv(environment);
}
}
真正的服务注册类 NacosServiceRegistry
NacosServiceRegistry,在Spring 初始化加载 NacosServiceRegistryAutoConfigugration配置类的时候,会被创建
NacosServiceRegistryAutoConfiguretion
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@ConditionalOnNacosDiscoveryEnabled
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled",
matchIfMissing = true)
@AutoConfigureAfter({
AutoServiceRegistrationConfiguration.class,
AutoServiceRegistrationAutoConfiguration.class,
NacosDiscoveryAutoConfiguration.class })
public class NacosServiceRegistryAutoConfiguration {
@Bean
public NacosServiceRegistry nacosServiceRegistry(
NacosDiscoveryProperties nacosDiscoveryProperties) {
//开始创建NacosServiceRegistry
//nacosDiscoveryProperties在上面已经被初始化完成
return new NacosServiceRegistry(nacosDiscoveryProperties);
}
这是服务注册时的最主要类 算是源码入口了!!!!
NacosServiceRegistry
//实现了ServiceRegistry这个接口。这个接口是SpringCloud的关于服务注册提供的同一的接口,
public class NacosServiceRegistry implements ServiceRegistry<Registration> {
private final NacosDiscoveryProperties nacosDiscoveryProperties;
private final NamingService namingService;
public NacosServiceRegistry(NacosDiscoveryProperties nacosDiscoveryProperties) {
this.nacosDiscoveryProperties = nacosDiscoveryProperties;
this.namingService = nacosDiscoveryProperties.namingServiceInstance();
}
//服务注册
//很多同学可能会犹豫,这个方法到底是在哪里调用的呢?因为我们在这里并没有看到有那个方法传入了Registration这个参数
//其实它是在一个名为 AbstractAutoServiceRegistration 这个类里面的 start()方法中调用的。
//AbstractAutoServiceRegistration 实现了ApplicationListener接口 传入的泛型是WebServerInitializedEvent
//监听web容器启动的时候执行 里面的bind()方法,在bind()中执行start()方法
@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();
//使用配置文件,去创建一个Inctanse实例,包括服务提供者的Ip地址和端口号
Instance instance = getNacosInstanceFromRegistration(registration);
try {
//注册一个实例到nacos server,这个resgisterInstance方法在 NacosNamingService类中
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);
}
}
NacosNamingSevice
public class NacosNamingService implements NamingService {
...
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);
beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());
//这里面会启动一个线程认为BeatTask,默认每5秒获取一次心跳数据
this.beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
}
//同过NamingProxy代理类去真正的拼装请求参数,调用NacosAPI。
this.serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
}
}
NamingProxy
public class NamingProxy {
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});
//可以看到Alibaba的这个Nacos开源组件就是很接地气 哈哈哈O(∩_∩)O 这个代码太熟悉了把
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()));
//拼装完参数以后,调用Nacos Rest Api
this.reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, "POST");
}
...
//其他的reqApi重载方法我们可以不去看,但是这个方法需要去看看
public String reqAPI(String api, Map<String, String> params, String body, List<String> servers, String method) throws NacosException {
params.put("namespaceId", this.getNamespaceId());
if (CollectionUtils.isEmpty(servers) && StringUtils.isEmpty(this.nacosDomain)) {
throw new NacosException(400, "no server available");
} else {
NacosException exception = new NacosException();
if (servers != null && !servers.isEmpty()) {
Random random = new Random(System.currentTimeMillis());
//生成一个0到ServerAddrList.size()长度的随机整数
//这里其实就实在模拟客户端的负载均很算法,
int index = random.nextInt(servers.size());
int i = 0;
while(i < servers.size()) {
//随机选择出一个nacos server
String server = (String)servers.get(index);
try {
//调用API 注册服务
return this.callServer(api, params, body, server, method);
} catch (NacosException var13) {
exception = var13;
if (LogUtils.NAMING_LOGGER.isDebugEnabled()) {
LogUtils.NAMING_LOGGER.debug("request {} failed.", server, var13);
}
index = (index + 1) % servers.size();
++i;
}
}
}
...
}
}
...
//reqAPI的方法,经过一系列的重载后,最终执行了这个callServer方法
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;
}
//没错把使用的就是 HttpClient客户端请求
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);
}
}
}
至此Provider向Nacos中注册服务的流程分析的就差不多了。简单的总结一下这个服务注册的过程:
首先在 NacosDiscoveryAutoConfiguration 自动配置的类,在初始化NacosServiceDiscovery
NacosDiscoveryAutoConfiguretion:
public class NacosDiscoveryAutoConfiguration {
//这里是老规矩,从application.properties 初始化配置数据
@Bean
@ConditionalOnMissingBean
public NacosDiscoveryProperties nacosProperties() {
return new NacosDiscoveryProperties();
}
@Bean
@ConditionalOnMissingBean
public NacosServiceDiscovery nacosServiceDiscovery(
NacosDiscoveryProperties discoveryProperties) {
//初始化Nacos服务发现类 这个类中封装了获取服务List的方法
return new NacosServiceDiscovery(discoveryProperties);
}
}
NacosDiscoveryClientConfiguration:
@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
@ConditionalOnBlockingDiscoveryEnabled
@ConditionalOnNacosDiscoveryEnabled
@AutoConfigureBefore({
SimpleDiscoveryClientAutoConfiguration.class,
CommonsClientAutoConfiguration.class })
//NacosDiscoveryAutoConfiguration 初始化装载完成以后,在会初始化这个Bean,确保了依赖关系正确
@AutoConfigureAfter(NacosDiscoveryAutoConfiguration.class)
public class NacosDiscoveryClientConfiguration {
@Bean
public DiscoveryClient nacosDiscoveryClient(
NacosServiceDiscovery nacosServiceDiscovery) {
//初始化NacosDiscoveryClient客户端
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);
}
}
NcosDiscoveryClient:这个类算是我们的代码入口了,请看
//实现了Spring cloud 提供的DiscoveryClient提供的接口
public class NacosDiscoveryClient implements DiscoveryClient {
private NacosServiceDiscovery serviceDiscovery;
public NacosDiscoveryClient(NacosServiceDiscovery nacosServiceDiscovery) {
this.serviceDiscovery = nacosServiceDiscovery;
}
...
//获取服务实例 传入的serviceId
@Override
public List<ServiceInstance> getInstances(String serviceId) {
try {
return serviceDiscovery.getInstances(serviceId);
}
catch (Exception e) {
throw new RuntimeException(
"Can not get hosts from nacos server. serviceId: " + serviceId, e);
}
}
...
}
NacosServiceDiscovery
public class NacosServiceDiscovery {
...
/**
* Return all instances for the given service.
* @param serviceId id of service
* @return list of instances
* @throws NacosException nacosException
*/
public List<ServiceInstance> getInstances(String serviceId) throws NacosException {
String group = discoveryProperties.getGroup();
//可以看到还是在使用namingService调用方法,和注册的时候一样。
List<Instance> instances = discoveryProperties.namingServiceInstance()
.selectInstances(serviceId, group, true);
//数据转化并返回
return hostToServiceInstanceList(instances, serviceId);
}
...
}
NacosNamingService 这个类我们需要关注两个地方,下面也就之列出这两个地方的代码以作参考
public class NacosNamingService implements NamingService {
...
//初始化代码
private void init(Properties properties) {
namespace = InitUtils.initNamespaceForNaming(properties);
initServerAddr(properties);
InitUtils.initWebRootContext();
initCacheDir();
initLogName(properties);
eventDispatcher = new EventDispatcher();
serverProxy = new NamingProxy(namespace, endpoint, serverList, properties);
//创建一个服务消费者和Nacos Server的心跳监听
beatReactor = new BeatReactor(serverProxy, initClientBeatThreadCount(properties));
//创建服务动态更线程类
hostReactor = new HostReactor(eventDispatcher, serverProxy, cacheDir, isLoadCacheAtStart(properties),
initPollingThreadCount(properties));
}
...
// 获取Server的服务实例
@Override
public List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters, boolean subscribe) throws NacosException {
ServiceInfo serviceInfo;
//判断是否订阅了
//如果订阅了通过udp的方式同送服务变更信息
if (subscribe) {
serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","));
} else {
serviceInfo = hostReactor.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","));
}
List<Instance> list;
if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
return new ArrayList<Instance>();
}
return list;
}
...
}
HostReactor 服务动态更新类
我们知道服务动态更新有两种
pull请求主动拉去更新的服务数据
public class HostReactor {
...
public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
String key = ServiceInfo.getKey(serviceName, clusters);
if (failoverReactor.isFailoverSwitch()) {
return failoverReactor.getService(key);
}
ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
if (null == serviceObj) {
serviceObj = new ServiceInfo(serviceName, clusters);
serviceInfoMap.put(serviceObj.getKey(), serviceObj);
updatingMap.put(serviceName, new Object());
updateServiceNow(serviceName, clusters);
updatingMap.remove(serviceName);
} else if (updatingMap.containsKey(serviceName)) {
if (UPDATE_HOLD_INTERVAL > 0) {
// hold a moment waiting for update finish
synchronized (serviceObj) {
try {
serviceObj.wait(UPDATE_HOLD_INTERVAL);
} catch (InterruptedException e) {
NAMING_LOGGER.error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
}
}
}
}
//开启一个定时任务,这个任务会在默认1s后开始执行。具体执行UpdateTask任务
scheduleUpdateIfAbsent(serviceName, clusters);
return serviceInfoMap.get(serviceObj.getKey());
}
public void scheduleUpdateIfAbsent(String serviceName, String clusters) {
if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
return;
}
synchronized (futureMap) {
if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
return;
}
//执行UpdateTask任务
ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, clusters));
futureMap.put(ServiceInfo.getKey(serviceName, clusters), future);
}
}
}
UpdateTask
public class UpdateTask implements Runnable {
@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 {
// if serviceName already updated by push, we should not override it
// since the push data may be different from pull through force push
refreshOnly(serviceName, clusters);
}
//更新最后一次刷新时间
lastRefTime = serviceObj.getLastRefTime();
if (!eventDispatcher.isSubscribed(serviceName, clusters) &&
!futureMap.containsKey(ServiceInfo.getKey(serviceName, clusters))) {
// abort the update task:
NAMING_LOGGER.info("update task is stopped, service:" + serviceName + ", clusters:" + clusters);
return;
}
//延迟10s后执行
executor.schedule(this, serviceObj.getCacheMillis(), TimeUnit.MILLISECONDS);
} catch (Throwable e) {
NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e);
}
}
}
push请求同送数据
HostReactor
在HostReactor的构造方法中初始化了一个 pushReceiver 用作udp 的接收个发送
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);
this.pushReceiver = new PushReceiver(this);
}
PushReceiver
public void run() {
//不断循环监听server端的push请求,然后调用processServiceJSON方法对数据信息解析,并返回
while (true) {
try {
// byte[] is initialized with 0 full filled by default
byte[] buffer = new byte[UDP_MSS];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
//接收服务端的push过来的数据
udpSocket.receive(packet);
String json = new String(IoUtils.tryDecompress(packet.getData()), "UTF-8").trim();
NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());
PushPacket pushPacket = JSON.parseObject(json, PushPacket.class);
String ack;
if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {
hostReactor.processServiceJSON(pushPacket.data);
// send ack to server
ack = "{\"type\": \"push-ack\""
+ ", \"lastRefTime\":\"" + pushPacket.lastRefTime
+ "\", \"data\":" + "\"\"}";
} else if ("dump".equals(pushPacket.type)) {
// dump data to server
ack = "{\"type\": \"dump-ack\""
+ ", \"lastRefTime\": \"" + pushPacket.lastRefTime
+ "\", \"data\":" + "\""
+ StringUtils.escapeJavaScript(JSON.toJSONString(hostReactor.getServiceInfoMap()))
+ "\"}";
} else {
// do nothing send ack only
ack = "{\"type\": \"unknown-ack\""
+ ", \"lastRefTime\":\"" + pushPacket.lastRefTime
+ "\", \"data\":" + "\"\"}";
}
udpSocket.send(new DatagramPacket(ack.getBytes(Charset.forName("UTF-8")),
ack.getBytes(Charset.forName("UTF-8")).length, packet.getSocketAddress()));
} catch (Exception e) {
NAMING_LOGGER.error("[NA] error while receiving push data", e);
}
}
}
上面就是Nacos服务发现的基本过程,现在总结一下: