微服务架构——不可或缺的注册中心

打印 上一主题 下一主题

主题 887|帖子 887|积分 2663

从本日开始,我们将以Java后端技能为切入点,深入探讨微服务架构。本章的重点将聚焦于微服务中最关键的环节之一:服务发现与注册。文章将循规蹈矩,由浅入深,逐步引领你进入微服务的广阔世界。不论你是技能新手还是经验丰富的专家,我都希望通过这篇文章,可以或许为你提供独特而有代价的见解与收获。
好的,我们开始!
单体架构vs微服务架构

单体架构

首先,我们来看看从前的单体架构。一个归档包(例如WAR格式)通常包罗了应用程序的所有功能和逻辑,这种布局使得我们将其称为单体应用。单体应用的设计理念强调将所有功能模块打包成一个整体,便于摆设和管理。这种架构模式被称为单体应用架构,意指通过一个单一的WAR包来承载整个应用的所有责任和功能。

正如我们所展示的这张简朴示例图所示,我们可以更深入地分析单体架构的优缺点,以便全面理解其在软件开辟和系统设计中的影响。
微服务架构

微服务的核心理念是将传统的单体应用程序根据业务需求举行拆分,将其分解为多个独立的服务,从而实现彻底的解耦。每个微服务专注于特定的功能或业务逻辑,遵循“一个服务只做一件事”的原则,类似于操纵系统中的进程。这样的设计使得每个服务都可以独立摆设,以致可以拥有自己的数据库,从而提高了系统的灵活性和可维护性。

通过这种方式,各个小服务相互独立,可以或许更有效地应对业务变化,快速迭代开辟和发布,同时降低了系统整体的复杂性,这就是微服务架构的本质。当然,微服务架构同样存在其优缺点,因为没有任何一种“银弹”可以或许美满办理所有问题。接下来,让我们深入分析一下这些优缺点:
长处


  • 服务小而内聚:微服务将应用拆分为多个独立服务,每个服务专注于特定功能,使得系统更具灵活性和可维护性。与传统单体应用相比,修改几行代码往往需要了解整个系统的架构和逻辑,而微服务架构则答应开辟职员仅专注于相关的功能,提升了开辟效率。
  • 简化开辟过程:不同团队可以并行开辟和摆设各自负责的服务,这提高了开辟效率和发布频率。
  • 按需伸缩:微服务的松耦合特性答应根据业务需求对各个服务举行独立扩展和摆设,便于根据流量变化动态调整资源,优化性能。
  • 前后端分离:作为Java开辟职员,我们可以专注于后端接口的安全性和性能,而不必关注前端的用户交互体验。
  • 容错性:某个服务的失败不会影响整个系统的可用性,提高了系统的可靠性。
缺点


  • 运维复杂性增加:管理多个服务增加了运维的复杂性,而不仅仅是一个WAR包,这大大增加了运维职员的工作量,涉及的技能栈(如Kubernetes、Docker、Jenkins等)也更为复杂。
  • 通信成本:服务之间的相互调用需要网络通信,大概导致延迟和性能问题。
  • 数据一致性挑衅:分布式系统中,维护数据一致性和处理分布式事务变得更加困难。
  • 性能监控与问题定位:需要更多的监控工具和策略来跟踪各个服务的性能,问题排查变得复杂。
应用场景

以是微服务也并不是适合所有项目。他只适合部分场景这里枚举一些典型案例:

  • 大型复杂项目:微服务架构通过将系统拆分为多个小型服务,降低了每个服务的复杂性,使得团队可以或许更加专注于各自负责的功能模块,从而显著提升开辟和维护的效率。
  • 快速迭代项目:微服务架构可以或许使得不同团队独立开辟和发布各自的服务,从而实现更高频率的迭代和更快的市场反应。
  • 并发高的项目:微服务架构则提供了灵活的弹性伸缩本事,各个服务可以根据需求独立扩展,确保系统在高并发环境下依然能保持精良的性能和稳固性。
好的,关于微服务的基本概念我们已经介绍完毕。接下来,我们将深入探讨微服务架构中至关紧张的一环:服务注册与发现。这一部分是微服务生态系统的核心,直接影响到系统的灵活性和可扩展性。
注册中心

从上面的讨论中,我们可以看到,微服务架构的核心在于将各个模块独立分开,以实现更好的灵活性和可维护性。然而,这种模块化设计也带来了网络传输上的斲丧,因此,理解微服务之间是怎样举行网络调用的变得尤为紧张。
接下来,我们将逐步探讨微服务之间的通信方式,以及这些方式怎样影响系统的整体性能。
调用方式

让我们先思考一个关键问题:在微服务架构中,怎样有效地维护复杂的调用关系,以确保各个服务之间的和谐与通信顺畅?
假如你对微服务还不太熟悉,不妨换个角度考虑:我们的电脑是怎样实现对其他网站的调用和访问的?
固定调用

我们最简朴的做法是将 IP 地址或域名硬编码在我们的代码中,以便直接举行调用。例如,考虑以下这段代码示例:
  1. //1:服务之间通过RestTemplate调用,url写死
  2. String url = "http://localhost:8020/order/findOrderByUserId/"+id;
  3. User result = restTemplate.getForObject(url,User.class);
  4. //2:类似还有其他http工具调用
  5. String url = "http://localhost:8020/order/findOrderByUserId/" + id;
  6. OkHttpClient client = new OkHttpClient();
  7. Request request = new Request.Builder()
  8.         .url(url)
  9.         .build();
  10. try (Response response = client.newCall(request).execute()) {
  11.     String jsonResponse = response.body().string();
  12.     // 处理 jsonResponse 对象。省略代码
复制代码
从外貌上看,虽然将 IP 地址或域名硬编码在代码中好像是一个简朴的办理方案,但实际上这并不是一个明智的做法。就像我们在访问百度搜索时,不会在浏览器中输入其 IP 地址,而是使用更为便捷和易记的域名。微服务之间的通信同样如此,每个微服务都有自己独特的服务名称。
在这里,域名服务器的作用非常关键,它负责存储域名与 IP 地址的对应关系,从而使我们可以或许准确地调用相应的服务器举行哀求和相应。微服务架构中也存在类似的机制,这就是我们所说的“服务发现与注册中心”。可以想象,这个注册中心就像是微服务的“域名服务器”,它存储了各个微服务的名称和它们的网络位置。

在设置域名时,我们需要在 DNS 记录中填写各种信息;而在微服务的注册中心中,类似的设置工作也同样紧张,只是通常是在设置文件中完成。当你的服务启动时,它会主动向注册中心注册自己的信息,确保其他服务可以或许找到并调用它。
"域名"调用

因此,当我们举行服务调用时,整个过程将变得更加熟悉和直观。例如,考虑下面这段代码示例:
  1. //使用微服务名发起调用
  2. String url = "http://mall‐order/order/findOrderByUserId/"+id;
  3. List<Order> orderList = restTemplate.getForObject(url, List.class);
复制代码
当然,这此中涉及很多需要细致实现的技能细节,但我们在初步理解时,可以先关注服务发现与注册中心的核心功能。简而言之,它们的主要目的是为了方便微服务之间的调用,淘汰开辟者在服务通信时所需处理的复杂性。
通过引入服务发现与注册中心,我们不再需要手动维护大量的 IP 地址与服务名称之间的关系。
设计思路

作为注册中心,它的主要功能是有效维护各个微服务的信息,例如它们的IP地址(当然,这些地址可以是内网的)。鉴于注册中心本身也是一个服务,因此在微服务架构中,它可以被视为一个紧张的组件。每个微服务在举行注册和发现之前,都必须举行适当的设置,才能确保它们可以或许相互识别和通信。
这就类似于在当地设置一个DNS服务器,假如没有这样的设置,我们就无法通过域名找到相应的IP地址,进而无法举行有效的网络通信。

在这个系统中,健康监测扮演着至关紧张的角色,其主要目的在于确保客户端可以或许及时获知服务器的状态,尤其是在服务器发生故障时,尽管这种监测无法做到完全实时。健康监测的紧张性在于,我们的微服务架构中,每个模块通常会启动多个实例。尽管这些实例的功能相同,目的在于分担哀求负载,但它们的可用性却大概有所不同。

例如,同一个服务名称大概会对应多个IP地址。然而,假如此中某个IP对应的服务出现故障,客户端就不应该再尝试调用这个服务的IP。相反,应该优先选择其他可用的IP,这样就可以或许有效实现高可用性。
接下来谈谈负载均衡。在这里需要留意的是,每个服务节点仅将其IP地址注册到注册中心,而注册中心本身并不负责详细调用哪个IP。这一切都完全取决于客户端的设计和实现。因此,在之前讨论域名调用的部分中提到,这内里的细节实际上还有很多。
注册中心的角色相对简朴,它的主要职责是网络和维护可用的IP地址,并将这些信息提供给客户端。详细的实现细节和操纵流程,可以参考下面的图片

实战

这样一来,关于系统架构的各个方面,我们基本上都已经有了全面的了解。接下来,我们可以直接进入实践环节,举行详细的使用演示。在这里,我们将以Spring Cloud Alibaba为例,选择Nacos作为我们的服务发现与注册中心。
准备工作

JDK:这是开辟必备的基础环境。
Maven:仍然会用maven举行项目的依靠管理。并启动Springboot项目。
Nacos Server:你需要自己搭建好一个nacos服务端。
Nacos Docker 快速开始

假如你本身没有nacos,我发起你可以在当地通过Docker快速搭建一个Nacos实例。详细步骤可以参考官方文档中的快速入门指南:Nacos Quick Start with Docker
通过这种方式,你可以在最短的时间内搭建起一个稳固的Nacos服务。
windows 当地

当然,你也可以选择在当地直接搭建Nacos服务。按照以下步骤举行操纵,这里就以此为例举行阐明。首先下载:https://github.com/alibaba/nacos/releases
然后当地直接解压后运行命令即可乐成,如下:
startup.cmd -m standalone

打开当地地址:http://127.0.0.1:8848/nacos/index.html

Spring Boot 启动

那么如今,我们可以直接开始启动当地的两个服务:一个是用户模块,另一个是订单模块。此外,我们还将创建一个公共模块,以便于共享通用的功能和资源。为了简化演示,我们将编写最基本的代码,主要目的是为学习和演示提供一个清楚的框架。我们的项目布局如图所示:

首先,公共模块的主要职责是导入所有服务共享的依靠,这样可以确保各个模块之间的一致性和复用性。这里就不演示了。我们只看下order和user模块的依靠。他俩其实是一样的,目的就是让自己的服务注册到中心去。
  1. <dependencies>
  2.     <dependency>
  3.         <groupId>com.xiaoyu.mall</groupId>
  4.         <artifactId>mall-common</artifactId>
  5.         <version>0.0.1-SNAPSHOT</version>
  6.         <scope>compile</scope>
  7.     </dependency>
  8.    
  9.     <dependency>
  10.         <groupId>com.alibaba.cloud</groupId>
  11.         <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  12.     </dependency>
  13. </dependencies>
复制代码
请添加一些必要的设置文件信息,下面的内容相对简朴。不过,每个服务都需要独立指定一个微服务名称,这里仅提供一个示例供参考。
  1. server:
  2.   port: 8040
  3. spring:
  4.   application:
  5.     name: mall-user  #微服务名称
  6.   #配置nacos注册中心地址
  7.   cloud:
  8.     nacos:
  9.       discovery:
  10.         server-addr: 127.0.0.1:8848
  11.         namespace: 9f545878-ca6b-478d-8a5a-5321d58b3ca3
复制代码
命名空间

假如不特殊设置命名空间(namespace),则系统会默认将资源摆设在公共空间(public)中。在这种环境下,假如需要使用其他命名空间,用户必须自行创建一个新的命名空间。例如:

好的,如今我们来启动这两个服务,看看运行效果。这样一来,两个服务都乐成注册了。不过需要特殊留意的是,假如希望这两个服务可以或许相互通信,务必将它们摆设在同一个命名空间下。

我们也可以检察每个服务的详细信息,这些信息包罗了丰富的内容。

示例代码

此时,我们并没有集成任何其他工具,而只是单独将 Nacos 的 Maven 依靠集成到我们的项目中。在这个阶段,我们已经可以通过注解的方式来使服务名称生效,这样就无需在代码中硬编码 IP 地址。接下来,我们来看看设置类的详细代码如下:
  1. @Bean
  2. @LoadBalanced  //mall-order => ip:port
  3. public RestTemplate restTemplate() {
  4.     return new RestTemplate();
  5. }
复制代码
然后,我们可以将用户端的业务代码编写得更加简洁明了,如下所示:
  1. @RequestMapping(value = "/findOrderByUserId/{id}")
  2. public R  findOrderByUserId(@PathVariable("id") Integer id) {
  3.     log.info("根据userId:"+id+"查询订单信息");
  4.     // ribbon实现,restTemplate需要添加@LoadBalanced注解
  5.     // mall-order  ip:port
  6.     String url = "http://mall-order/order/findOrderByUserId/"+id;
  7.     R result = restTemplate.getForObject(url,R.class);
  8.     return result;
  9. }
复制代码
我们的订单端业务代码相对简朴,呈现方式如下:
  1. @RequestMapping("/findOrderByUserId/{userId}")
  2. public R findOrderByUserId(@PathVariable("userId") Integer userId) {
  3.     log.info("根据userId:"+userId+"查询订单信息");
  4.     List<OrderEntity> orderEntities = orderService.listByUserId(userId);
  5.     return R.ok().put("orders", orderEntities);
  6. }
复制代码
我们来看下调用环境,以确认是否确实可以或许实现预期的效果。

第三方组件OpenFeign

在单体架构中,你会直接使用 RestTemplate 类来调用自身的其他服务?显然是不大概的,因此,在这种环境下,借助流行的第三方组件 OpenFeign 可以显著简化服务之间的交互。OpenFeign 提供了一种声明式的方式来界说 HTTP 客户端,使得我们可以更方便地举行服务调用,同时保持代码的可读性和可维护性。
首先,我们需要在项目的 pom.xml 文件中添加相应的 Maven 依靠。
  1. <dependency>
  2.     <groupId>org.springframework.cloud</groupId>
  3.     <artifactId>spring-cloud-starter-openfeign</artifactId>
  4. </dependency>
复制代码
初次之外,还需要加一个注解在启动类上:
  1. @SpringBootApplication
  2. @EnableFeignClients //扫描和注册feign客户端bean定义
  3. public class MallUserFeignDemoApplication {、
  4.   public static void main(String[] args) {
  5.       SpringApplication.run(MallUserFeignDemoApplication.class, args);
  6.   }
  7. }
复制代码
从前写ip地址那里换成类的时候,我们需要单独界说一下服务类:
  1. @FeignClient(value = "mall-order",path = "/order")
  2. public interface OrderFeignService {
  3.     @RequestMapping("/findOrderByUserId/{userId}")
  4.     R findOrderByUserId(@PathVariable("userId") Integer userId);
  5. }
复制代码
这样一来,我们在调用服务时就可以接纳更加简洁和直观的写法。是不是以为这种方式使用起来更加舒服?
  1. @Autowired
  2. OrderFeignService orderFeignService;
  3. @RequestMapping(value = "/findOrderByUserId/{id}")
  4. public R  findOrderByUserId(@PathVariable("id") Integer id) {
  5.     //feign调用
  6.     R result = orderFeignService.findOrderByUserId(id);
  7.     return result;
  8. }
复制代码
同样可以正常调用乐成。

不过,在实施过程中还有一些需要留意的细节。很多开辟者倾向于将这些调用封装到一个单独的微服务模块——即 api-service,并将其作为子项目依靠于当前的微服务。这种做法可以或许有效地将外部 API 调用与内部服务逻辑举行区分,避免将不同类型的功能稠浊在同一个包中。看下:

好的,到此为止,我们已经完成了一个完整的调用流程。这一切的设置和设置为我们后续的开辟奠定了坚固的基础。接下来,我们就可以专注于实现实际的业务逻辑,比如数据库的调用与存储操纵。
学习进阶

接下来我们将深入探讨相关内容。由于很多细节尚未细致讲解,之前的实战环节主要旨在让大家对服务注册与发现中心的作用有一个初步的理解。为了更好地把握这一主题,我们需要关注一些关键问题,例如客户端的负载均衡、心跳监测以及服务注册与发现等。
接下来,我们将通过分析源码,带领大家全面了解 Nacos 是怎样高效办理注册中心的三大核心任务的。
gRPC

在这里,我想先介绍一下 Nacos 的实现方式。自 Nacos 2.1 版本起,官方不再推荐使用 HTTP 等传统的 RPC 调用方式,虽然这些方式仍然是被支持的。假如你计划顺遂升级到 Nacos,需特殊关注一个设置参数:在 application.properties 文件中设置 nacos.core.support.upgrade.from.1x=true。
在之前的分析中,我们已经探讨过 Nacos 1.x 版本的实现,那个版本确实是通过常规的 HTTP 调用举行交互的,Nacos 服务端会实现一些 Controller,就像我们自己构建的微服务一样,源码的可读性非常高,容易理解。调用方式如下面的图示所示:

但是,自 Nacos 2.1 版本以来,系统举行了紧张的升级,转而接纳了 gRPC。gRPC 是一个开源的远程过程调用(RPC)框架,最初由 Google 开辟。它使用 HTTP/2 作为传输协议,提供更高效的网络通信,并使用 Protocol Buffers 作为消息格式,从而实现了快速且高效的数据序列化和反序列化。

性能优化:gRPC 基于 HTTP/2 协议,支持多路复用,答应在一个连接上同时发送多个哀求,淘汰延迟和带宽使用。
二进制负载: 与基于文本的 JSON/XML 相比,协议缓冲区序列化为紧凑的二进制格式。
流控与双向流:gRPC 支持流式数据传输,可以或许实现客户端和服务器之间的双向流通信,适用于实时应用。
办理 GC 问题:通过真实的长连接,淘汰了频繁连接和断开的对象创建,进而降低了 GC(垃圾回收)压力,提升了系统性能。
Nacos 升级使用 gRPC 是基于其众多长处,但我也必须强调,没有任何技能是所谓的“银弹”,这也是我一贯的观点。最明显的缺点是系统复杂性的增加。因此,在选择技能方案时,必须根据自身的业务需求做出明智的决策。
在新版 Nacos 的源码中,你会发现很多以 .proto 后缀命名的文件。这些文件界说了消息的布局,此中每条消息代表一个小的信息逻辑记录,包罗一系列称为字段(fields)的名称-值对。这种界说方式使得数据的传输息争析变得更加高效和灵活。
例如,我们可以随便找一个 Nacos 中的缓冲区文件。

虽然这不是我们讨论的重点,但值得指出的是,gRPC 的引入将为 Nacos 带来显著的性能优化。尽管我们在这里不深入探讨其详细实现,但了解这一点是很紧张的,因为在后续的所有调用中,gRPC 都将发挥关键作用。
服务注册

当我们的服务启动时,会发生一个紧张的过程:服务实例会向 Nacos 发起一次哀求,以完成注册。如下图示:

为了提高效率,我们不再逐步举行源码追踪,尽管之前已经详细讲解过怎样检察 Spring 的主动设置。本日,我们将直接关注关键源码的位置,以快速理解 Nacos 的实现细节。
  1. @Override
  2. public void register(Registration registration) {
  3.   //此处省略非关键代码
  4.     NamingService namingService = namingService();
  5.     String serviceId = registration.getServiceId();
  6.     String group = nacosDiscoveryProperties.getGroup();
  7.     Instance instance = getNacosInstanceFromRegistration(registration);
  8.     try {
  9.         namingService.registerInstance(serviceId, group, instance);
  10.         log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
  11.                 instance.getIp(), instance.getPort());
  12.     }
  13.     //此处省略非关键代码
复制代码
在服务注册的过程中,我们可以观察到构建了一些自身的 IP 和端口信息。这些信息对于服务的正确识别和调用至关紧张。此外,这里值得一提的是命名空间(Namespace)的概念。命名空间在 Nacos 中用于实现租户(用户)粒度的隔离,这对于微服务架构中的资源管理尤为紧张。
命名空间的常见应用场景之一是不同环境之间的隔离,比如开辟、测试环境与生产环境的资源隔离。

接下来,我们将举行一个服务调用,这里使用的是 gRPC 协议。实际上,这个过程可以简化为一个方法调用。
  1. private <T extends Response> T requestToServer(AbstractNamingRequest request, Class<T> responseClass)
  2.         throws NacosException {
  3.     try {
  4.         request.putAllHeader(
  5.                 getSecurityHeaders(request.getNamespace(), request.getGroupName(), request.getServiceName()));
  6.         Response response =
  7.                 requestTimeout < 0 ? rpcClient.request(request) : rpcClient.request(request, requestTimeout);
  8.         //此处省略非关键代码
复制代码
服务端处理

当 Nacos 服务端接收到来自客户端的 gRPC 调用哀求后,会立即启动一系列处理流程,以确保哀求可以或许得到有效相应。关键代码的实现细节可以参考下面这部分。
  1. @Override
  2. @TpsControl(pointName = "RemoteNamingServiceSubscribeUnSubscribe", name = "RemoteNamingServiceSubscribeUnsubscribe")
  3. @Secured(action = ActionTypes.READ)
  4. @ExtractorManager.Extractor(rpcExtractor = SubscribeServiceRequestParamExtractor.class)
  5. public SubscribeServiceResponse handle(SubscribeServiceRequest request, RequestMeta meta) throws NacosException {
  6.     String namespaceId = request.getNamespace();
  7.     String serviceName = request.getServiceName();
  8.     String groupName = request.getGroupName();
  9.     String app = RequestContextHolder.getContext().getBasicContext().getApp();
  10.     String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
  11.     Service service = Service.newService(namespaceId, groupName, serviceName, true);
  12.     Subscriber subscriber = new Subscriber(meta.getClientIp(), meta.getClientVersion(), app, meta.getClientIp(),
  13.             namespaceId, groupedServiceName, 0, request.getClusters());
  14.     ServiceInfo serviceInfo = ServiceUtil.selectInstancesWithHealthyProtection(serviceStorage.getData(service),
  15.             metadataManager.getServiceMetadata(service).orElse(null), subscriber.getCluster(), false, true,
  16.             subscriber.getIp());
  17.     if (request.isSubscribe()) {
  18.         clientOperationService.subscribeService(service, subscriber, meta.getConnectionId());
  19.         NotifyCenter.publishEvent(new SubscribeServiceTraceEvent(System.currentTimeMillis(),
  20.                 NamingRequestUtil.getSourceIpForGrpcRequest(meta), service.getNamespace(), service.getGroup(),
  21.                 service.getName()));
  22.     } else {
  23.         clientOperationService.unsubscribeService(service, subscriber, meta.getConnectionId());
  24.         NotifyCenter.publishEvent(new UnsubscribeServiceTraceEvent(System.currentTimeMillis(),
  25.                 NamingRequestUtil.getSourceIpForGrpcRequest(meta), service.getNamespace(), service.getGroup(),
  26.                 service.getName()));
  27.     }
  28.     return new SubscribeServiceResponse(ResponseCode.SUCCESS.getCode(), "success", serviceInfo);
  29. }
复制代码
这段代码包括提取哀求信息、创建相关对象、处理订阅或取消订阅的操纵,并返回相应的结果。通过这种方式,Nacos 可以高效管理微服务的服务发现和注册功能。
心跳监测

在 Nacos 2.1 版本之前,每个服务在运行时都会向注册中心发送一次哀求,以通知其当前的存活状态和正常性。这种机制虽然有效,但在高并发环境下大概会引入额外的网络负担和延迟。
然而,升级到 2.1 版本后,这一过程发生了显著的变化。首先,我们需要思考一下心跳监测的本质。显然,心跳监测是一种定期检查机制,这意味着服务会在设定的时间间隔内主动发送心跳信号以确认其存活状态。因此,可以合理地推测,这一功能在客户端实现为一个定时任务,它会按照预定的时间频率定期向注册中心报告服务的健康状态。
为了更好地理解这一机制的实现,我们接下来将重点关注相关的关键代码。
  1. public final void start() throws NacosException {
  2.        // 省略一些代码
  3.         
  4.         clientEventExecutor = new ScheduledThreadPoolExecutor(2, r -> {
  5.             Thread t = new Thread(r);
  6.             t.setName("com.alibaba.nacos.client.remote.worker");
  7.             t.setDaemon(true);
  8.             return t;
  9.         });
  10.         
  11.         // 省略一些代码
  12.         
  13.         clientEventExecutor.submit(() -> {
  14.             while (true) {
  15.                 try {
  16.                     if (isShutdown()) {
  17.                         break;
  18.                     }
  19.                     ReconnectContext reconnectContext = reconnectionSignal
  20.                             .poll(keepAliveTime, TimeUnit.MILLISECONDS);
  21.                     if (reconnectContext == null) {
  22.                         // check alive time.
  23.                         if (System.currentTimeMillis() - lastActiveTimeStamp >= keepAliveTime) {
  24.                             boolean isHealthy = healthCheck();
  25.                             if (!isHealthy) {
  26.                                  // 省略一些代码
复制代码
我将与健康监测无关的代码基本去除了,这样你可以更加直观地观察 Nacos 是怎样举行实例健康监测的。由于健康监测的核心目的在于确认服务的可用性,因此这一过程的实现相对简朴。
在这段代码中,我们可以清楚地看到,健康监测并不涉及任何复杂的数据传输。其主要功能仅仅是向服务器发送哀求,以检测服务器是否可以或许乐成相应。这种设计极大地降低了网络开销,使得监测过程更加高效。

服务端的代码同样清楚且简朴。如下所示:
  1. @Override
  2. @TpsControl(pointName = "HealthCheck")
  3. public HealthCheckResponse handle(HealthCheckRequest request, RequestMeta meta) {
  4.     return new HealthCheckResponse();
  5. }
复制代码
总体而言,这种优化显著淘汰了网络 I/O 的斲丧,提升了系统的整体性能。乍一看,好像并没有做什么复杂的操纵,但这并不意味着我们就无法判断客户端是否可以或许正常连接。实际上,关键的判断逻辑被设计在外层代码中。
  1. Connection connection = connectionManager.getConnection(GrpcServerConstants.CONTEXT_KEY_CONN_ID.get());
  2. RequestMeta requestMeta = new RequestMeta();
  3. requestMeta.setClientIp(connection.getMetaInfo().getClientIp());
  4. requestMeta.setConnectionId(GrpcServerConstants.CONTEXT_KEY_CONN_ID.get());
  5. requestMeta.setClientVersion(connection.getMetaInfo().getVersion());
  6. requestMeta.setLabels(connection.getMetaInfo().getLabels());
  7. requestMeta.setAbilityTable(connection.getAbilityTable());
  8. //这里刷新下时间。用来代表它确实存活
  9. connectionManager.refreshActiveTime(requestMeta.getConnectionId());
  10. prepareRequestContext(request, requestMeta, connection);
  11. //这次处理的返回
  12. Response response = requestHandler.handleRequest(request, requestMeta);
复制代码
别着急,服务端同样运行着一个定时任务,负责定期扫描和检查各个客户端的状态。我们看下:
  1. public void start() {
  2.     initConnectionEjector();
  3.     // Start UnHealthy Connection Expel Task.
  4.     RpcScheduledExecutor.COMMON_SERVER_EXECUTOR.scheduleWithFixedDelay(() -> {
  5.         runtimeConnectionEjector.doEject();
  6.         MetricsMonitor.getLongConnectionMonitor().set(connections.size());
  7.     }, 1000L, 3000L, TimeUnit.MILLISECONDS);
  8. //省略部分代码,doEject方法再往后走,你就会发现这样一段代码
  9. //outdated connections collect.
  10. for (Map.Entry<String, Connection> entry : connections.entrySet()) {
  11.     Connection client = entry.getValue();
  12.     if (now - client.getMetaInfo().getLastActiveTime() >= KEEP_ALIVE_TIME) {
  13.         outDatedConnections.add(client.getMetaInfo().getConnectionId());
  14.     } else if (client.getMetaInfo().pushQueueBlockTimesLastOver(300 * 1000)) {
  15.         outDatedConnections.add(client.getMetaInfo().getConnectionId());
  16.     }
  17. }
  18. //省略部分代码,
复制代码
通过这些分析,你基本上已经把握了核心概念和实现细节。我们不需要再多做赘述。我们继续往下看。
负载均衡

谈到负载均衡,首先我们需要确保当地拥有一份服务器列表,以便于合理地分配负载。因此,关键在于我们怎样从注册中心获取这些可用服务的信息。那么,详细来说,我们应该怎样在当地有效地发现和获取这些服务呢?
服务发现

服务发现的机制会随着实例的增加或淘汰而动态变化,因此我们需要定期更新可用服务列表。这就引出了一个紧张的设计考量:为什么不将服务发现的检索任务直接整合到心跳任务中呢?
首先,心跳任务的主要目的是监测服务实例的健康状态,确保它们可以或许正常相应哀求。而服务发现则侧重于及时更新和获取当前可用的服务实例信息。这两者的目的明显不同,因此将它们混淆在一起大概会导致逻辑上的混淆和功能上的复杂性。
此外,两者的时间间隔也各有不同。心跳监测大概需要更频繁地举行,以及时发现和处理服务故障,而服务发现的频率可以根据详细需求适当调整。基于这些原因,将心跳监测和服务发现分开成两个独立的定时任务,显然是更合理的选择。
接下来,让我们深入研究服务发现的关键代码,看看详细是怎样实现这一机制的:
[code]public void run() {    //省略部分代码    if (serviceObj == null) {        serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);        serviceInfoHolder.processServiceInfo(serviceObj);        lastRefTime = serviceObj.getLastRefTime();        return;    }        if (serviceObj.getLastRefTime()

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

南七星之家

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表