微服务架构设计:探究服务注册中心,深入分析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对象。具体代码如下:

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>

服务注册及调用

此时,我们分别启动SpringCloudProviderSpringCloudConsumer来完成服务提供者和消费者的启动,启动后访问我们之前的Nacos管理页面,可以看到相关的服务已成功注册到Nacos上。

进一步,当我们调用服务提供者cloud-consumer中的userconsumers/info时,可以看到其成功请求到cloud-provider中所提供的接口路径,并成功返回相关内容信息。

如上,我们便从零开始搭建出了Nacos注册中心,同时利用SpringBoot构建出了cloud-providercluod-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对象:

  1. NacosAutoServiceRegistration :其是一个自动化服务注册类,主要用于简化服务的注册过程。它会在Spring Boot应用启动时自动注册服务到 Nacos

  2. NacosRegistration :其封装了服务的元数据(如服务名、实例 ID、主机地址等)。它代表了在Nacos中注册的具体服务实例。

  3. NacosServiceRegistry :作为Nacos的核心服务注册类,主要负责与Nacos Server 进行通信,处理实际的服务注册、更新和注销操作。

总结来看,NacosAutoServiceRegistration 是启动时自动进行服务注册的入口,负责创建 NacosRegistration实例。而 NacosRegistration 封装了服务实例的详细信息,并被 NacosServiceRegistry 用于实际的注册过程。NacosServiceRegistry 则是与 Nacos服务器交互的主要组件,处理所有与服务注册相关的操作。通过这三个组件的协作Nacos才能够实现高效的服务注册和发现机制。如下这张图便准确的反映了NacosAutoServiceRegistrationNacosRegistrationNacosServiceRegistry三者间的关系。

经过上述分析,我们知道对于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);
   // ....省略其他无关代码
}

不难发现,NacosServiceRegistryregister方法其实主要完成两件事。一件是获取服务的配置,并构建一个服务实例对象。另一件则是将构建的服务实例对象通过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在发送心跳的时候,会调用ScheduledThreadPoolExecutorschedule方法,在schedule要执行的任务中,如果正常发送完心跳,会再次调用schedule方法。与此同时,线程任务BeatTaskrun方法,每次执行会先判断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注册的逻辑了。其实现服务注册原理无非就是通过HTTPNacos 服务端会提供注册的 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的调用。方法的大致指脉络如下:

  1. 构建 URL:根据当前Nacos的服务器地址和 API路径构建完整的请求 URL
  2. 发送请求:使用 nacosRestTemplate 发送 HTTP 请求,并获取响应结果。
  3. 处理响应:如果响应成功,返回响应数据。如果响应状态码为 304(未修改),返回空字符串。其他情况下,抛出 NacosException 异常。

进一步,如果callServer抛出NacosException异常,则会进入此处的重试机制,进行不断重试,而默认的重试次数为3次。当然也可以通过配置namingRequestDomainMaxRetryCount属性来指定重试次数。

如上便是Nacos服务注册全部代码,其实Nacos实现服务注册发现的逻辑并不复杂,其本质无非就是通过Http来完成对Nacos服务端会提供注册的 API接口来完成服务信息的推送,从而将相关服务信息维护在Nacos这一注册中心中。

总结

本文主要对微服务中的注册中心的原理进行了介绍,并搭建了单机版Nacos注册中心,在此基础上利用SpringBoot构建出了cloud-providercloud-consumer两个微服务,完成了服务在服务间的注册与调用。进一步,深入对Nacos的服务注册原理进行了深入剖析,并对Nacos中部分源码进行了解读,希望本文对你理解微服务中服务的注册和调用有深刻认识。

相关推荐
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck2 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei2 小时前
java的类加载机制的学习
java·学习
码农小旋风3 小时前
详解K8S--声明式API
后端
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml44 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~4 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616884 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7894 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java5 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet