微服务架构设计:探究服务注册中心,深入分析Nacos服务注册原理
点击
关注
公众号,“
技术干货
” 及时达!
从上图中的步骤中我们可以看出,首先,
访问控制台对应的
通过如上操作,我们便构建出了一个单机版的
进一步,当我们调用服务提供者
如上,我们便从零开始搭建出了
众所周知,当我们为项目引入
经过上述分析,我们知道对于
(注:前缀
❝ 「摘要」 :注册中心一直是微服务架构中重要组件,是众多服务相互通信构建的基础。本文以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
对象。具体代码如下:
@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
路径,进而获取到
服务者
返回的用户信息,其代码如下:
@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
父子结构来进行搭建,
服务者
和
消费者
搭建完毕后的效果如下图所示。
整个项目中所用到的依赖如下:
<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
的配置类。该类源码如下:
@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()
❞
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
❞
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
相关变量信息如下:
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
❞
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
中部分源码进行了解读,希望本文对你理解微服务中服务的注册和调用有深刻认识。
点击
关注
公众号,“
技术干货
” 及时达!