摘要 :注册中心一直是微服务架构中重要组件,是众多服务相互通信构建的基础。本文以
Nacos
为例,着重讲解其服务注册中心的基础概念及原理。通过本文,你可以了解微服务中注册的完整生命周期,并知晓服务注册中心在微服务架构中的作用,同时对
Nacos
中的服务发现原理有一个全面的认识。
前言
在介绍注册中心 之前,我们不妨先来考虑这样一个场景。假设现在你所负责系统包含两个微服务即服务A
和服务B
。由于服务并未采用原先的单体架构,这也就导致如果需要进行服务间的通信时,不能像之前那样通过Bean
注入的方式来完成业务间的调用。这主要是因为在微服务的架构风格下,由于不同的服务间相互部署,所以服务间的通信需要通过Http
来完成。
因此服务A
和服务B
的通信,需要借助网络
来完成服务间的通信。进一步,根据网络通信的知识,我们知道如果要借助Http
来进行网络通信的话,需要要知道目标服务的IP
和端口
信息。
换言之,如果要实现服务A
和服务B
的通信,我们必须在代码服务A
或服务B
中写死对方所在的IP
和端口
,然后通过RestTemplate
来完成服务间的调用。 这样的方式在系统服务数较少时是完全可行的。但当系统服务增多时,通过硬编码方式维护服务IP
方式的弊端也便显现。
此时,一旦当前服务所依赖的服务发生迁移或扩容时,当前服务就必须得修改源码并重启。其次,如果每个子服务都依赖另外的多个外部服务。 与此同时,由于服务间错综复杂的依赖关系,这会使得各个服务间维护的配置信息将会变得异常复杂,并且子系统之间的依赖维护也会变得异常困难。这明显不是微服务模式下最初的本意,而为了避免维护服务间这种错综的关系,注册中心的概念应运而生。
注册中心
正如前文所述,在注册中心未问世之前。我们如果要维护服务间信息时,只能通过写死代码
的方式来维护所依赖服务的IP
信息,进而来保证服务间通信的正常进行,而这种方式的最大弊端便在于当服务数增多时,依赖关系便会变得异常复杂,难于管理。
此时,如果能有这样一款组件
,其可以帮助我们自动发现服务,并记录服务的信息。当服务间需要通信时,完全借助该组件记录服务信息来完成对应服务的调用。从而也就避免在代码中手动维护服务信息尴尬处境。而注册中心
便是解决这一困境的组件。
事实上,注册中心
解决的首要任务就是服务注册与服务发现。 通过服务注册与服务发现
我们就可以将系统内的服务的进行统一化管理。如下的这张图便对此过程进行了详细的描述。
从上图中的步骤中我们可以看出,首先,服务提供者
向注册中心发起了注册,进而将自己的地 址信息上报到注册中心,这个过程就是服务注册 。接下来,每隔一段时间服务消费者便会从 注册中心
获取服务提供者
的的服务列表,或者由服务中心将服务列表的变动推送给服务消费者,这个过程便是服务发现。最后,服务提供者
便可以根据从注册中心
获取的服务信息来实现对服务提供者
的调用。
现如今,微服务技术已经十分成熟,也随之诞生了许多优秀的开源注册中心,如Zookeeper、Eureka、Consul、Nacos
等。接下来,笔者将主要用Nacos
来搭建一个示例,从而对Nacos
中服务注册的原理进行剖析,如果大家对其他注册中心
感兴趣话可以自行研究~
接下来,笔者
将会手把手带你完成服务中心搭建,同时构建两个微服务,用以实现服务的注册、发现和调用。
搭建注册中心,实现服务调用
Nacos
服务注册中心的安装包可在Nacos
官网进行下载,现在后进入对应bin
目录通过 .\startup.cmd -m standalone
即可实现Nacos
注册中心单机模式启动。
启动
Nacos
访问控制台对应的http://192.168.40.1:8848
即可进入到Nacos
的管理后台。具体如下所示:
访问
Nacos
后台管理
通过如上操作,我们便构建出了一个单机版的Nacos
注册中心。完成注册中心的搭建后,接下来我们便开始相关服务的构建。此次,我们会通过SpringBoot
项目构建一个cloud-provider
来作为服务的提供者的,同时构建cloud-consumer
来作为服务的消费者。
构建服务者
我们的服务者相对简单,我们仅提供一个UserController
的服务层,其内部有一个getUserInfo
方法可以通过传入的Id
信息构建一个UserInfo
对象。具体代码如下:
java
@RestController
@RequestMapping(("/user"))
public class UserController {
@GetMapping("info/{id}")
public String getUserInfo(@PathVariable("id") String id) {
return JSONUtil.toJsonStr(new UserInfo(id,"cloud-provider"));
}
}
构建消费者
我们的消费者通用类似,其内部也仅有一个UserConsumer
的控制层,其内部会通过调用消费者
提供的user/info
路径,进而获取到服务者
返回的用户信息,其代码如下:
java
@RestController
@RequestMapping("userConsumers")
public class UserConsumer {
@Autowired
private RestTemplate restTemplate;
@GetMapping("info/{id}")
public ResponseEntity<String> getUserInfo(@PathVariable("id") String id){
return restTemplate.getForEntity("http://cloud-provider/user/info/" + id, String.class);
}
}
整个项目我们通过Maven
父子结构来进行搭建,服务者
和消费者
搭建完毕后的效果如下图所示。
整个项目中所用到的依赖如下:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>${spring-cloud-bootstrap}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.0</version>
</dependency>
</dependencies>
服务注册及调用
此时,我们分别启动SpringCloudProvider
和SpringCloudConsumer
来完成服务提供者和消费者的启动,启动后访问我们之前的Nacos
管理页面,可以看到相关的服务已成功注册到Nacos
上。
进一步,当我们调用服务提供者cloud-consumer
中的userconsumers/info
时,可以看到其成功请求到cloud-provider
中所提供的接口路径,并成功返回相关内容信息。
如上,我们便从零开始搭建出了Nacos
注册中心,同时利用SpringBoot
构建出了cloud-provider
和cluod-consumer
两个微服务,并完成了两个服务间的通信。
作为一名开发者,单单会构建一个服务,并实现服务注册其实是远远不够的。接下来,我们便对Nacos
服务注册原理进行深入剖析。
Ps: 如下内容可能需要对
SpringBoot
装配原理有一定基础,对此部分不了解的读者,可建议先补齐相关知识后~
Nacos
服务注册原理
众所周知,当我们为项目引入Nacos-discovery
后,其后自动注入一个名为NacosServiceRegistryAutoConfiguration
的配置类。该类源码如下:
java
@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(
NacosServiceManager nacosServiceManager,
NacosDiscoveryProperties nacosDiscoveryProperties) {
return new NacosServiceRegistry(nacosServiceManager, nacosDiscoveryProperties);
}
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosRegistration nacosRegistration(
ObjectProvider<List<NacosRegistrationCustomizer>> registrationCustomizers,
NacosDiscoveryProperties nacosDiscoveryProperties,
ApplicationContext context) {
return new NacosRegistration(registrationCustomizers.getIfAvailable(),
nacosDiscoveryProperties, context);
}
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosAutoServiceRegistration nacosAutoServiceRegistration(
NacosServiceRegistry registry,
AutoServiceRegistrationProperties autoServiceRegistrationProperties,
NacosRegistration registration) {
return new NacosAutoServiceRegistration(registry,
autoServiceRegistrationProperties, registration);
}
}
通过上述代码,我们可以看到NacosServiceRegistryAutoConfiguration
作为Nacos
服务注册的自动装配器。其内部会向Spring
容器中分别注入如下三个Bean
对象:
-
NacosAutoServiceRegistration
:其是一个自动化服务注册类,主要用于简化服务的注册过程。它会在Spring Boot
应用启动时自动注册服务到Nacos
。 -
NacosRegistration
:其封装了服务的元数据(如服务名、实例 ID、主机地址等)。它代表了在Nacos
中注册的具体服务实例。 -
NacosServiceRegistry
:作为Nacos
的核心服务注册类,主要负责与Nacos Server
进行通信,处理实际的服务注册、更新和注销操作。
总结来看,NacosAutoServiceRegistration
是启动时自动进行服务注册的入口,负责创建 NacosRegistration
实例。而 NacosRegistration
封装了服务实例的详细信息,并被 NacosServiceRegistry
用于实际的注册过程。NacosServiceRegistry
则是与 Nacos
服务器交互的主要组件,处理所有与服务注册相关的操作。通过这三个组件的协作Nacos
才能够实现高效的服务注册和发现机制。如下这张图便准确的反映了NacosAutoServiceRegistration
、NacosRegistration
、NacosServiceRegistry
三者间的关系。
经过上述分析,我们知道对于Nacos
客户端服务的注册,主要通过NacosServiceRegistry
这一对象来实现。更进一步,其实是通过内部的registry
方法来实现。
NacosServiceRegistry#registry()
java
public void register(Registration registration) {
// 获取服务属性信息
NamingService namingService = namingService();
String serviceId = registration.getServiceId();
String group = nacosDiscoveryProperties.getGroup();
// 构建服务实例
Instance instance = getNacosInstanceFromRegistration(registration);
// 服务注册
namingService.registerInstance(serviceId, group, instance);
// ....省略其他无关代码
}
不难发现,NacosServiceRegistry
的register
方法其实主要完成两件事。一件是获取服务的配置,并构建一个服务实例对象。另一件则是将构建的服务实例对象通过registerInstance
进行注册。而registerInstance
的内部逻辑如下:
NamingHttpClientProxy#registerInstance
java
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
<1> 推送心跳,检测服务状态
if (instance.isEphemeral()) {
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
// 创建个Map 准备组装参数
final Map<String, String> params = new HashMap<String, String>(32);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, groupedServiceName);
params.put(CommonParams.GROUP_NAME, groupName);
params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
params.put(IP_PARAM, instance.getIp());
params.put(PORT_PARAM, String.valueOf(instance.getPort()));
params.put(WEIGHT_PARAM, String.valueOf(instance.getWeight()));
params.put("enable", String.valueOf(instance.isEnabled()));
params.put(HEALTHY_PARAM, String.valueOf(instance.isHealthy()));
params.put(EPHEMERAL_PARAM, String.valueOf(instance.isEphemeral()));
params.put(META_PARAM, JacksonUtils.toJson(instance.getMetadata()));
// <2> 请求Nacos服务端,完成服务注册
reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
}
对于registerService
方法,其主要做了如下:
- 发送心跳包
此处的beatReactor.addBeatInfo
其实是向定时任务线程池中加入一个定时任务,该任务每隔5s
向服务端发送一次心跳请求。具体来看, Nacos
在发送心跳的时候,会调用ScheduledThreadPoolExecutor
的schedule
方法,在schedule
要执行的任务中,如果正常发送完心跳,会再次调用schedule
方法。与此同时,线程任务BeatTask
的run
方法,每次执行会先判断isStopped
,如果是false
说明心跳停止,就不会触发下次执行任务。
- 注册实例
不难看到,在reqApi
方法之前,其会通过Map
结构来封装当次请求的参数信息。当信息构建完成后,会将这些信息通过http
请求发送给服务端。其中UtilAndComs.nacosUrlInstance
相关变量信息如下:
java
public static String webContext = "/nacos";
public static String nacosUrlBase = webContext + "/v1/ns";
public static String nacosUrlInstance = nacosUrlBase + "/instance";
可以看到其代表路径为nacos/v1/ns/instance
。而该路径在Nacos
官网上对应的行为即为服务实例注册!
(注:前缀/v2
代表为版本2x之后,笔者当前项目引用的nacos-client
版本为2.0.4
,所以其源码仍显示请求/v1
)
看到此,相信你其实已经猜出Nacos
注册的逻辑了。其实现服务注册原理无非就是通过HTTP
,Nacos
服务端会提供注册的 API接口
给客户端进行 Http
调用。为了一探究竟,我们继续来看其中的reqApi
方法
NamingHttpClientProxy#reqApi
java
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());
String nacosDomain = serverListManager.getNacosDomain();
// 通过重试机制,不断请求远程服务
for (int i = 0; i < maxRetry; i++) {
try {
return callServer(api, params, body, nacosDomain, method);
} catch (NacosException e) {
if (NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug("request {} failed.", nacosDomain, e);
}
}
}
}
}
这里的callServer
我们便不具体看了,其内部其实就是Http
的调用。方法的大致指脉络如下:
- 构建
URL
:根据当前Nacos
的服务器地址和API
路径构建完整的请求URL
。 - 发送请求:使用
nacosRestTemplate
发送HTTP
请求,并获取响应结果。 - 处理响应:如果响应成功,返回响应数据。如果响应状态码为 304(未修改),返回空字符串。其他情况下,抛出
NacosException
异常。
进一步,如果callServer
抛出NacosException
异常,则会进入此处的重试机制,进行不断重试,而默认的重试次数为3
次。当然也可以通过配置namingRequestDomainMaxRetryCount
属性来指定重试次数。
如上便是Nacos
服务注册全部代码,其实Nacos
实现服务注册发现的逻辑并不复杂,其本质无非就是通过Http
来完成对Nacos
服务端会提供注册的 API接口
来完成服务信息的推送,从而将相关服务信息维护在Nacos
这一注册中心中。
总结
本文主要对微服务
中的注册中心的原理进行了介绍,并搭建了单机版Nacos
注册中心,在此基础上利用SpringBoot
构建出了cloud-provider
和cloud-consumer
两个微服务,完成了服务在服务间的注册与调用。进一步,深入对Nacos
的服务注册原理进行了深入剖析,并对Nacos
中部分源码进行了解读,希望本文对你理解微服务中服务的注册和调用有深刻认识。