现在主流的注册中心还是挺多的,并且实现得都很不错,像zookeeper、Nacos、Etcd、Consul、Eureka等。笔者呢,比较喜欢zookeeper多一点,因为zk的节点特性以及监听机制提供的便利确实很大。当然Nacos我也喜欢,所以本文基于spring cloud alibaba着重分析一下Nacos这个注册中心是如何实现服务的注册与发现的。
这里顺带一下,携程的Apollo配置中心底层也有用到Eureka这么个东东哦,所以它们都是很优秀的
Nacos的客户端访问服务端有两种方式:
1、Open API ; 2、SDK
由于Nacos服务端只提供了REST接口(就是controller),所以SDK底层本质也是调用的REST接口,换句话说SDK就是对Http请求的封装。
服务注册
现在来看一下服务是什么时机注册的,又是怎么注册的。
我们都知道spring cloud 是一个规范,里面的组件,只要按照这个规范去实现就可以集成进去。在spring-cloud-commons包下的META-INF/spring.factories有这么一个很重要的自动装配类—AutoServiceRegistrationAutoConfiguration。熟悉springboot 自动装配原理,对META-INF/spring.factories肯定不陌生。我们看看这个类长什么样
@Configuration(proxyBeanMethods = false)
@Import(AutoServiceRegistrationConfiguration.class)
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled",
matchIfMissing = true)
public class AutoServiceRegistrationAutoConfiguration {
@Autowired(required = false)
private AutoServiceRegistration autoServiceRegistration;
@Autowired
private AutoServiceRegistrationProperties properties;
@PostConstruct
protected void init() {
if (this.autoServiceRegistration == null && this.properties.isFailFast()) {
throw new IllegalStateException("Auto Service Registration has "
+ "been requested, but there is no AutoServiceRegistration bean");
}
}
}
AutoServiceRegistrationAutoConfiguration里有个属性成员AutoServiceRegistration,它是一个空接口。
从类关系图能知道它有一个抽象实现类AbstractAutoServiceRegistration,这是spring cloud 提供的一个实现类,然后我们的NacosAutoServiceRegistration继承了它。
public class NacosAutoServiceRegistration
extends AbstractAutoServiceRegistration<Registration> {
// 本质就是委派给父类的register
@Override
protected void register() {
if (!this.registration.getNacosDiscoveryProperties().isRegisterEnabled()) {
log.debug("Registration disabled.");
return;
}
if (this.registration.getPort() < 0) {
this.registration.setPort(getPort().get());
}
// 调用父类的方法 this.serviceRegistry.register(getRegistration());
super.register();
}
}
我们看看父类register的具体实现,父类调用了ServiceRegistry接口的register。ServiceRegistry是spring cloud提供的服务注册规范
public interface ServiceRegistry<R extends Registration> {
void register(R registration);
void deregister(R registration);
void close();
void setStatus(R registration, String status);
<T> T getStatus(R registration);
}
所以我们肯定能够知道Nacos肯定有实现改接口的实现类来进行服务注册,不然怎么能叫spring cloud alibaba nacos呢。这个类就是NacosServiceRegistry。我们看看它是如何进行服务注册的
public class NacosServiceRegistry implements ServiceRegistry<Registration> {
@Override
public void register(Registration registration) {
// 省略部分代码
NamingService namingService = namingService();
String serviceId = registration.getServiceId();
String group = nacosDiscoveryProperties.getGroup();
Instance instance = getNacosInstanceFromRegistration(registration);
try {
// 注册当前服务实例
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);
}
}
// 省略部分代码
}
关键的一句代码就是namingService.registerInstance(serviceId, group, instance)。这个方法里面有这么一个关键的知识点,那就是心跳包监测机制
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
if (instance.isEphemeral()) {
// 创建心跳信息实现健康检测
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
// 正式注册
serverProxy.registerService(groupedServiceName, groupName, instance);
}
方法里主要两步,第一:创建心跳信息实现健康检测 ;第二:调用Nacos的REST 接口实现服务注册。
客户端在向Nacos服务端注册服务实例时,会添加一个心跳包信息,在addBeatInfo方法里,会有一个定时任务,每隔三秒向服务端一个数据包,然后启动一个线程不断检测服务端的回应,如果在设定时间内没有收到服务端的回应,则认为服务器出现了故障。当然,Nacos服务端会根据客户端的心跳包不断更新服务的状态。
接下来,我们来看看Nacos服务端是怎么注册服务的。核心就是委派给serviceManager进行注册
@RestController
@RequestMapping(UtilsAndCommons.NACOS_NAMING_CONTEXT + "/instance")
public class InstanceController {
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
final Instance instance = parseInstance(request);
// 委派给serviceManager进行注册
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
}
我们着重分析一下serviceManager.registerInstance(namespaceId, serviceName, instance)
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);
}
// 将当前注册的服务保存到service中
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
我们跟一下createEmptyService源码,里面就只有一行代码,就是调用createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)。看createServiceIfAbsent方法名就大概猜测知道干啥了—如果不存在则创建新的。我们看看createServiceIfAbsent源码
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
throws NacosException {
// 根据namespaceId、serviceName在缓存查找对应的服务
Service service = getService(namespaceId, serviceName);
// 如果缓存中查不到,就创建新的
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));
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);
}
}
}
关键的方法就是putServiceAndInit,我们看看它是初始化并放到缓存中去的
private void putServiceAndInit(Service service) throws NacosException {
// 放缓存
putService(service);
// 初始化
service.init();
// 实现数据一致性的监听
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
}
我们来具体分析这三个步骤。
第一个步骤:放缓存。就是通过成员变量private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>()来实现的,通过map的containsKey来判断是否缓存存在。其实在spring的IOC实现中,也是有很多类似这样的场景。
第二个步骤:初始化。就是通过本地定时任务不断检测当前服务下的所有实例最后发送心跳包的时间,如果超时,则有示例故障了,会设置healthy为false表示服务不健康,并且会发送服务变更事件。等我们将故障的服务实例下线后,服务健康状态将会恢复健康。
服务实例的最后心跳包时间谁来操作?不要忘了分析客服端注册服务时,客户端有这么一步:定时发送心跳包,建立心跳包机制。至此,服务端跟客户端已经“双向”监测,在心跳机制下,不断检测彼此是否故障了。如果服务端故障,客户端的请求完全打不通,像fast-fail。如果服务的实例有故障的,服务端知道了可以踢除它,保障客户端的请求打到的实例都是健康可用的
第三个步骤:consistencyService.listen实现数据一致性的监听。服务下的示例数据当然要保证数据一致性啦,不然如果不一样的,那还得了?Nacos的数据一致性算法用的Raft。同样采用这种算法的还有Redis Sentinel(哨兵模式)的leader选举、Etcd等。
Redis 是我们经常用的,也是很重要的。 后续笔者研究到Redis的哨兵模式,我们再继续一探究竟。
看到这,Nacos的服务注册就差不多有个宏观理解了。这里总结一下
- Nacos 客户端通过SDK,SDK本质又是Open Api 发起服务注册。
- Nacos服务端(controller)接收到请求后执行上面三个步骤:放缓存、初始化以及实现数据一致性的监听
服务发现
服务发现就简单很多。就是Nacos客户端调用Open api或者SDK查服务列表,服务端接受到请求后根据将查询到服务包装成json格式返回。
那客户端啥时候发起服务列表查询?如果客户端查的时差内,刚好服务端的服务实例有挂的,那客户端的请求岂不是有打到挂的服务实例去?不是吧?
我们接着分析,揭开一层层疑惑。
客户端本身会维护一个本地服务地址列表,不会在每次请求时都去请求一次服务端的来拉取最新的服务地址。那么这个本地服务地址列表就有一个时效性问题。Nacos提供subscribe(String serviceName,EventListener listener)来订阅监听。
客户端有一个HostReactor类,在com.alibaba.nacos.client.naming.core包下。它里面有一个UpdateTask线程,每10s发送一次pull拉取请求,获取服务最新的地址列表。对于服务端,由于服务端和服务提供者示例建立心跳机制,一旦服务示例出现故障,服务端察觉出后,会发送一个push消息给Nacos客户端,也就是我们的消费者。这个push消息是使用DatagramSocket来实现的,是java net包下类。
public class HostReactor implements Closeable {
// 省略部分代码
private static final long DEFAULT_DELAY = 1000L;
public synchronized ScheduledFuture<?> addTask(UpdateTask task) {
return executor.schedule(task, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
}
}
为什么要用DatagramSocket呢?因为服务端发现故障得必须马上告知客户端消费者,让他们及时止损,越快越好。而DatagramSocket基于UDP协议实现的,了解UDP协议都知道UDP无需建立链接,就可以发送数据。不像TCP会经过三次握手,四次挥手来建立全双工的连接。感兴趣的,我再开一篇总结网络协议的博文。
服务消费者收到服务端发来的push消息之后,使用HostReactor中提供的ServiceInfo processServiceJson(String json)方法解析消息,并更新本地服务地址列表。
最后总结一下服务动态感知原理
Dubbo给Nacos的“关爱”
不是一家人不进一家门,Dubbo跟Nacos这两兄弟都是阿里的中间件。如果RPC框架、注册中心是它们,那么服务的注册是依托Dubbo的自动装配实现的。Dubbo这个哥哥还是挺关爱Nacos这个弟弟的。我们看看具体怎么回事。
在spring-cloud-alibaba-dubbo下的META-INF/spring.factories文件中自动装配了一个和服务注册相关的配置类DubboServiceRegistrationAutoConfiguration。
@Configuration(proxyBeanMethods = false)
@ConditionalOnNotWebApplication
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled",
matchIfMissing = true)
@AutoConfigureAfter(DubboServiceRegistrationAutoConfiguration.class)
@Aspect
public class DubboServiceRegistrationNonWebApplicationAutoConfiguration {
private static final String REST_PROTOCOL = "rest";
// 实际注入的是NacosserviceRegistry
@Autowired
private ServiceRegistry serviceRegistry;
@Autowired
private Registration registration;
private volatile Integer serverPort = null;
private volatile boolean registered = false;
@Autowired
private DubboServiceMetadataRepository repository;
@Around("execution(* org.springframework.cloud.client.serviceregistry.Registration.getPort())")
public Object getPort(ProceedingJoinPoint pjp) throws Throwable {
setServerPort();
return serverPort != null ? serverPort : pjp.proceed();
}
@EventListener(ApplicationStartedEvent.class)
public void onApplicationStarted() {
register();
}
private void register() {
if (registered) {
return;
}
serviceRegistry.register(registration);
registered = true;
}
}
里面有一个切面和监听器。切面是做port处理,而监听器会监听ApplicationStartedEvent事件,该事件是在刷新上下文后之后、调用application命令之前触发的。收到事件后,调用serviceRegistry.register,最终就是我们前文分析的调用NacosserviceRegistry中的register方法实现服务的注册。
本文就到此结束了,我们下文见,谢谢
github: honey开源系列组件作者