从今天开始,我们将以Java后端技术为切入点,深入探讨微服务架构。本章的重点将聚焦于微服务中最关键的环节之一:服务发现与注册。文章将循序渐进,由浅入深,逐步引领你进入微服务的广阔世界。不论你是技术新手还是经验丰富的专家,我都希望通过这篇文章,能够为你提供独特而有价值的见解与收获。
好的,我们开始!
单体架构vs微服务架构
单体架构
首先,我们来看看以前的单体架构。一个归档包(例如WAR格式)通常包含了应用程序的所有功能和逻辑,这种结构使得我们将其称为单体应用。单体应用的设计理念强调将所有功能模块打包成一个整体,便于部署和管理。这种架构模式被称为单体应用架构,意指通过一个单一的WAR包来承载整个应用的所有责任和功能。
图片
正如我们所展示的这张简单示例图所示,我们可以更深入地分析单体架构的优缺点,以便全面理解其在软件开发和系统设计中的影响。
微服务架构
微服务的核心理念是将传统的单体应用程序根据业务需求进行拆分,将其分解为多个独立的服务,从而实现彻底的解耦。每个微服务专注于特定的功能或业务逻辑,遵循“一个服务只做一件事”的原则,类似于操作系统中的进程。这样的设计使得每个服务都可以独立部署,甚至可以拥有自己的数据库,从而提高了系统的灵活性和可维护性。
图片
通过这种方式,各个小服务相互独立,能够更有效地应对业务变化,快速迭代开发和发布,同时降低了系统整体的复杂性,这就是微服务架构的本质。当然,微服务架构同样存在其优缺点,因为没有任何一种“银弹”能够完美解决所有问题。接下来,让我们深入分析一下这些优缺点:
优点
- 服务小而内聚:微服务将应用拆分为多个独立服务,每个服务专注于特定功能,使得系统更具灵活性和可维护性。与传统单体应用相比,修改几行代码往往需要了解整个系统的架构和逻辑,而微服务架构则允许开发人员仅专注于相关的功能,提升了开发效率。
- 简化开发过程:不同团队可以并行开发和部署各自负责的服务,这提高了开发效率和发布频率。
- 按需伸缩:微服务的松耦合特性允许根据业务需求对各个服务进行独立扩展和部署,便于根据流量变化动态调整资源,优化性能。
- 前后端分离:作为Java开发人员,我们可以专注于后端接口的安全性和性能,而不必关注前端的用户交互体验。
- 容错性:某个服务的失败不会影响整个系统的可用性,提高了系统的可靠性。
缺点
- 运维复杂性增加:管理多个服务增加了运维的复杂性,而不仅仅是一个WAR包,这大大增加了运维人员的工作量,涉及的技术栈(如Kubernetes、Docker、Jenkins等)也更为复杂。
- 通信成本:服务之间的相互调用需要网络通信,可能导致延迟和性能问题。
- 数据一致性挑战:分布式系统中,维护数据一致性和处理分布式事务变得更加困难。
- 性能监控与问题定位:需要更多的监控工具和策略来跟踪各个服务的性能,问题排查变得复杂。
应用场景
所以微服务也并不是适合所有项目。他只适合部分场景这里列举一些典型案例:
- 大型复杂项目:微服务架构通过将系统拆分为多个小型服务,降低了每个服务的复杂性,使得团队能够更加专注于各自负责的功能模块,从而显著提升开发和维护的效率。
- 快速迭代项目:微服务架构能够使得不同团队独立开发和发布各自的服务,从而实现更高频率的迭代和更快的市场反应。
- 并发高的项目:微服务架构则提供了灵活的弹性伸缩能力,各个服务可以根据需求独立扩展,确保系统在高并发情况下依然能保持良好的性能和稳定性。
好的,关于微服务的基本概念我们已经介绍完毕。接下来,我们将深入探讨微服务架构中至关重要的一环:服务注册与发现。这一部分是微服务生态系统的核心,直接影响到系统的灵活性和可扩展性。
注册中心
从上面的讨论中,我们可以看到,微服务架构的核心在于将各个模块独立分开,以实现更好的灵活性和可维护性。然而,这种模块化设计也带来了网络传输上的消耗,因此,理解微服务之间是如何进行网络调用的变得尤为重要。
接下来,我们将逐步探讨微服务之间的通信方式,以及这些方式如何影响系统的整体性能。
调用方式
让我们先思考一个关键问题:在微服务架构中,如何有效地维护复杂的调用关系,以确保各个服务之间的协调与通信顺畅?
如果你对微服务还不太熟悉,不妨换个角度考虑:我们的电脑是如何实现对其他网站的调用和访问的?
固定调用
我们最简单的做法是将 IP 地址或域名硬编码在我们的代码中,以便直接进行调用。例如,考虑以下这段代码示例:
//1:服务之间通过RestTemplate调用,url写死
String url = "http://localhost:8020/order/findOrderByUserId/"+id;
User result = restTemplate.getForObject(url,User.class);
//2:类似还有其他http工具调用
String url = "http://localhost:8020/order/findOrderByUserId/" + id;
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
try (Response response = client.newCall(request).execute()) {
String jsonResponse = response.body().string();
// 处理 jsonResponse 对象。省略代码
从表面上看,虽然将 IP 地址或域名硬编码在代码中似乎是一个简单的解决方案,但实际上这并不是一个明智的做法。就像我们在访问百度搜索时,不会在浏览器中输入其 IP 地址,而是使用更为便捷和易记的域名。微服务之间的通信同样如此,每个微服务都有自己独特的服务名称。
在这里,域名服务器的作用非常关键,它负责存储域名与 IP 地址的对应关系,从而使我们能够准确地调用相应的服务器进行请求和响应。微服务架构中也存在类似的机制,这就是我们所说的“服务发现与注册中心”。可以想象,这个注册中心就像是微服务的“域名服务器”,它存储了各个微服务的名称和它们的网络位置。
图片
在配置域名时,我们需要在 DNS 记录中填写各种信息;而在微服务的注册中心中,类似的配置工作也同样重要,只是通常是在配置文件中完成。当你的服务启动时,它会自动向注册中心注册自己的信息,确保其他服务能够找到并调用它。
"域名"调用
因此,当我们进行服务调用时,整个过程将变得更加熟悉和直观。例如,考虑下面这段代码示例:
//使用微服务名发起调用
String url = "http://mall‐order/order/findOrderByUserId/"+id;
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模块的依赖。他俩其实是一样的,目的就是让自己的服务注册到中心去。
<dependencies>
<dependency>
<groupId>com.xiaoyu.mall</groupId>
<artifactId>mall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<!-- nacos服务注册与发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
请添加一些必要的配置文件信息,下面的内容相对简单。不过,每个服务都需要独立指定一个微服务名称,这里仅提供一个示例供参考。
server:
port: 8040
spring:
application:
name: mall-user #微服务名称
#配置nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: 9f545878-ca6b-478d-8a5a-5321d58b3ca3
命名空间
如果不特别配置命名空间(namespace),则系统会默认将资源部署在公共空间(public)中。在这种情况下,如果需要使用其他命名空间,用户必须自行创建一个新的命名空间。例如:
图片
好的,现在我们来启动这两个服务,看看运行效果。这样一来,两个服务都成功注册了。不过需要特别注意的是,如果希望这两个服务能够相互通信,务必将它们部署在同一个命名空间下。
图片
我们也可以查看每个服务的详细信息,这些信息包含了丰富的内容。
图片
示例代码
此时,我们并没有集成任何其他工具,而只是单独将 Nacos 的 Maven 依赖集成到我们的项目中。在这个阶段,我们已经可以通过注解的方式来使服务名称生效,这样就无需在代码中硬编码 IP 地址。接下来,我们来看看配置类的具体代码如下:
@Bean
@LoadBalanced //mall-order => ip:port
public RestTemplate restTemplate() {
return new RestTemplate();
}
然后,我们可以将用户端的业务代码编写得更加简洁明了,如下所示:
@RequestMapping(value = "/findOrderByUserId/{id}")
public R findOrderByUserId(@PathVariable("id") Integer id) {
log.info("根据userId:"+id+"查询订单信息");
// ribbon实现,restTemplate需要添加@LoadBalanced注解
// mall-order ip:port
String url = "http://mall-order/order/findOrderByUserId/"+id;
R result = restTemplate.getForObject(url,R.class);
return result;
}
我们的订单端业务代码相对简单,呈现方式如下:
@RequestMapping("/findOrderByUserId/{userId}")
public R findOrderByUserId(@PathVariable("userId") Integer userId) {
log.info("根据userId:"+userId+"查询订单信息");
List<OrderEntity> orderEntities = orderService.listByUserId(userId);
return R.ok().put("orders", orderEntities);
}
我们来看下调用情况,以确认是否确实能够实现预期的效果。
图片
第三方组件OpenFeign
在单体架构中,你会直接使用 RestTemplate 类来调用自身的其他服务?显然是不可能的,因此,在这种情况下,借助流行的第三方组件 OpenFeign 可以显著简化服务之间的交互。OpenFeign 提供了一种声明式的方式来定义 HTTP 客户端,使得我们可以更方便地进行服务调用,同时保持代码的可读性和可维护性。
首先,我们需要在项目的 pom.xml 文件中添加相应的 Maven 依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
初次之外,还需要加一个注解在启动类上:
@SpringBootApplication
@EnableFeignClients //扫描和注册feign客户端bean定义
public class MallUserFeignDemoApplication {、
public static void main(String[] args) {
SpringApplication.run(MallUserFeignDemoApplication.class, args);
}
}
以前写ip地址那里换成类的时候,我们需要单独定义一下服务类:
@FeignClient(value = "mall-order",path = "/order")
public interface OrderFeignService {
@RequestMapping("/findOrderByUserId/{userId}")
R findOrderByUserId(@PathVariable("userId") Integer userId);
}
这样一来,我们在调用服务时就可以采用更加简洁和直观的写法。是不是觉得这种方式使用起来更加舒服?
@Autowired
OrderFeignService orderFeignService;
@RequestMapping(value = "/findOrderByUserId/{id}")
public R findOrderByUserId(@PathVariable("id") Integer id) {
//feign调用
R result = orderFeignService.findOrderByUserId(id);
return result;
}
同样可以正常调用成功。
图片
不过,在实施过程中还有一些需要注意的细节。许多开发者倾向于将这些调用封装到一个单独的微服务模块——即 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 发起一次请求,以完成注册。如下图示:
image
为了提高效率,我们不再逐步进行源码追踪,尽管之前已经详细讲解过如何查看 Spring 的自动配置。今天,我们将直接关注关键源码的位置,以快速理解 Nacos 的实现细节。
@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());
}
//此处省略非关键代码
在服务注册的过程中,我们可以观察到构建了一些自身的 IP 和端口信息。这些信息对于服务的正确识别和调用至关重要。此外,这里值得一提的是命名空间(Namespace)的概念。命名空间在 Nacos 中用于实现租户(用户)粒度的隔离,这对于微服务架构中的资源管理尤为重要。
命名空间的常见应用场景之一是不同环境之间的隔离,比如开发、测试环境与生产环境的资源隔离。
图片
接下来,我们将进行一个服务调用,这里使用的是 gRPC 协议。实际上,这个过程可以简化为一个方法调用。
private <T extends Response> T requestToServer(AbstractNamingRequest request, Class<T> responseClass)
throws NacosException {
try {
request.putAllHeader(
getSecurityHeaders(request.getNamespace(), request.getGroupName(), request.getServiceName()));
Response response =
requestTimeout < 0 ? rpcClient.request(request) : rpcClient.request(request, requestTimeout);
//此处省略非关键代码
服务端处理
当 Nacos 服务端接收到来自客户端的 gRPC 调用请求后,会立即启动一系列处理流程,以确保请求能够得到有效响应。关键代码的实现细节可以参考下面这部分。
@Override
@TpsControl(pointName = "RemoteNamingServiceSubscribeUnSubscribe", name = "RemoteNamingServiceSubscribeUnsubscribe")
@Secured(action = ActionTypes.READ)
@ExtractorManager.Extractor(rpcExtractor = SubscribeServiceRequestParamExtractor.class)
public SubscribeServiceResponse handle(SubscribeServiceRequest request, RequestMeta meta) throws NacosException {
String namespaceId = request.getNamespace();
String serviceName = request.getServiceName();
String groupName = request.getGroupName();
String app = RequestContextHolder.getContext().getBasicContext().getApp();
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
Service service = Service.newService(namespaceId, groupName, serviceName, true);
Subscriber subscriber = new Subscriber(meta.getClientIp(), meta.getClientVersion(), app, meta.getClientIp(),
namespaceId, groupedServiceName, 0, request.getClusters());
ServiceInfo serviceInfo = ServiceUtil.selectInstancesWithHealthyProtection(serviceStorage.getData(service),
metadataManager.getServiceMetadata(service).orElse(null), subscriber.getCluster(), false, true,
subscriber.getIp());
if (request.isSubscribe()) {
clientOperationService.subscribeService(service, subscriber, meta.getConnectionId());
NotifyCenter.publishEvent(new SubscribeServiceTraceEvent(System.currentTimeMillis(),
NamingRequestUtil.getSourceIpForGrpcRequest(meta), service.getNamespace(), service.getGroup(),
service.getName()));
} else {
clientOperationService.unsubscribeService(service, subscriber, meta.getConnectionId());
NotifyCenter.publishEvent(new UnsubscribeServiceTraceEvent(System.currentTimeMillis(),
NamingRequestUtil.getSourceIpForGrpcRequest(meta), service.getNamespace(), service.getGroup(),
service.getName()));
}
return new SubscribeServiceResponse(ResponseCode.SUCCESS.getCode(), "success", serviceInfo);
}
这段代码包括提取请求信息、创建相关对象、处理订阅或取消订阅的操作,并返回相应的结果。通过这种方式,Nacos 可以高效管理微服务的服务发现和注册功能。
心跳监测
在 Nacos 2.1 版本之前,每个服务在运行时都会向注册中心发送一次请求,以通知其当前的存活状态和正常性。这种机制虽然有效,但在高并发环境下可能会引入额外的网络负担和延迟。
然而,升级到 2.1 版本后,这一过程发生了显著的变化。首先,我们需要思考一下心跳监测的本质。显然,心跳监测是一种定期检查机制,这意味着服务会在设定的时间间隔内自动发送心跳信号以确认其存活状态。因此,可以合理地推测,这一功能在客户端实现为一个定时任务,它会按照预定的时间频率定期向注册中心报告服务的健康状态。
为了更好地理解这一机制的实现,我们接下来将重点关注相关的关键代码。
public final void start() throws NacosException {
// 省略一些代码
clientEventExecutor = new ScheduledThreadPoolExecutor(2, r -> {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.remote.worker");
t.setDaemon(true);
return t;
});
// 省略一些代码
clientEventExecutor.submit(() -> {
while (true) {
try {
if (isShutdown()) {
break;
}
ReconnectContext reconnectContext = reconnectionSignal
.poll(keepAliveTime, TimeUnit.MILLISECONDS);
if (reconnectContext == null) {
// check alive time.
if (System.currentTimeMillis() - lastActiveTimeStamp >= keepAliveTime) {
boolean isHealthy = healthCheck();
if (!isHealthy) {
// 省略一些代码
我将与健康监测无关的代码基本去除了,这样你可以更加直观地观察 Nacos 是如何进行实例健康监测的。由于健康监测的核心目的在于确认服务的可用性,因此这一过程的实现相对简单。
在这段代码中,我们可以清晰地看到,健康监测并不涉及任何复杂的数据传输。其主要功能仅仅是向服务器发送请求,以检测服务器是否能够成功响应。这种设计极大地降低了网络开销,使得监测过程更加高效。
图片
服务端的代码同样清晰且简单。如下所示:
@Override
@TpsControl(pointName = "HealthCheck")
public HealthCheckResponse handle(HealthCheckRequest request, RequestMeta meta) {
return new HealthCheckResponse();
}
总体而言,这种优化显著减少了网络 I/O 的消耗,提升了系统的整体性能。乍一看,似乎并没有做什么复杂的操作,但这并不意味着我们就无法判断客户端是否能够正常连接。实际上,关键的判断逻辑被设计在外层代码中。
Connection connection = connectionManager.getConnection(GrpcServerConstants.CONTEXT_KEY_CONN_ID.get());
RequestMeta requestMeta = new RequestMeta();
requestMeta.setClientIp(connection.getMetaInfo().getClientIp());
requestMeta.setConnectionId(GrpcServerConstants.CONTEXT_KEY_CONN_ID.get());
requestMeta.setClientVersion(connection.getMetaInfo().getVersion());
requestMeta.setLabels(connection.getMetaInfo().getLabels());
requestMeta.setAbilityTable(connection.getAbilityTable());
//这里刷新下时间。用来代表它确实存活
connectionManager.refreshActiveTime(requestMeta.getConnectionId());
prepareRequestContext(request, requestMeta, connection);
//这次处理的返回
Response response = requestHandler.handleRequest(request, requestMeta);
别着急,服务端同样运行着一个定时任务,负责定期扫描和检查各个客户端的状态。我们看下:
public void start() {
initConnectionEjector();
// Start UnHealthy Connection Expel Task.
RpcScheduledExecutor.COMMON_SERVER_EXECUTOR.scheduleWithFixedDelay(() -> {
runtimeConnectionEjector.doEject();
MetricsMonitor.getLongConnectionMonitor().set(connections.size());
}, 1000L, 3000L, TimeUnit.MILLISECONDS);
//省略部分代码,doEject方法再往后走,你就会发现这样一段代码
//outdated connections collect.
for (Map.Entry<String, Connection> entry : connections.entrySet()) {
Connection client = entry.getValue();
if (now - client.getMetaInfo().getLastActiveTime() >= KEEP_ALIVE_TIME) {
outDatedConnections.add(client.getMetaInfo().getConnectionId());
} else if (client.getMetaInfo().pushQueueBlockTimesLastOver(300 * 1000)) {
outDatedConnections.add(client.getMetaInfo().getConnectionId());
}
}
//省略部分代码,
通过这些分析,你基本上已经掌握了核心概念和实现细节。我们不需要再多做赘述。我们继续往下看。
负载均衡
谈到负载均衡,首先我们需要确保本地拥有一份服务器列表,以便于合理地分配负载。因此,关键在于我们如何从注册中心获取这些可用服务的信息。那么,具体来说,我们应该如何在本地有效地发现和获取这些服务呢?
服务发现
服务发现的机制会随着实例的增加或减少而动态变化,因此我们需要定期更新可用服务列表。这就引出了一个重要的设计考量:为什么不将服务发现的检索任务直接整合到心跳任务中呢?
首先,心跳任务的主要目的是监测服务实例的健康状态,确保它们能够正常响应请求。而服务发现则侧重于及时更新和获取当前可用的服务实例信息。这两者的目的明显不同,因此将它们混合在一起可能会导致逻辑上的混淆和功能上的复杂性。
此外,两者的时间间隔也各有不同。心跳监测可能需要更频繁地进行,以及时发现和处理服务故障,而服务发现的频率可以根据具体需求适当调整。基于这些原因,将心跳监测和服务发现分开成两个独立的定时任务,显然是更合理的选择。
接下来,让我们深入研究服务发现的关键代码,看看具体是如何实现这一机制的:
public void run() {
//省略部分代码
if (serviceObj == null) {
serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
serviceInfoHolder.processServiceInfo(serviceObj);
lastRefTime = serviceObj.getLastRefTime();
return;
}
if (serviceObj.getLastRefTime() <= lastRefTime) {
serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
serviceInfoHolder.processServiceInfo(serviceObj);
}
//省略部分代码
当然,接下来我们将探讨服务器端的处理逻辑,以下是服务端处理的关键代码部分:
public QueryServiceResponse handle(ServiceQueryRequest request, RequestMeta meta) throws NacosException {
String namespaceId = request.getNamespace();
String groupName = request.getGroupName();
String serviceName = request.getServiceName();
Service service = Service.newService(namespaceId, groupName, serviceName);
String cluster = null == request.getCluster() ? "" : request.getCluster();
boolean healthyOnly = request.isHealthyOnly();
ServiceInfo result = serviceStorage.getData(service);
ServiceMetadata serviceMetadata = metadataManager.getServiceMetadata(service).orElse(null);
result = ServiceUtil.selectInstancesWithHealthyProtection(result, serviceMetadata, cluster, healthyOnly, true,
NamingRequestUtil.getSourceIpForGrpcRequest(meta));
return QueryServiceResponse.buildSuccessResponse(result);
}
这样一来,我们便能够获得一些关键的服务信息。
负载均衡算法
如果同一个微服务存在多个 IP 地址,那么在进行服务调用时,我们该如何选择具体的服务器呢?通常,我们会想到使用 Nginx 作为服务端的负载均衡工具。然而,除了在服务器端进行负载均衡之外,我们同样可以在微服务客户端配置负载算法,以优化请求的分发。
此时,我们要明确的是,这部分逻辑实际上并不属于 Nacos 的职责范围,而是由另一个组件——Ribbon 来负责。Ribbon 专注于实现客户端负载均衡,确保在微服务架构中,客户端能够智能地选择合适的服务器进行调用,从而提高系统的性能和稳定性。
接下来,我们可以深入查看 Ribbon 的关键代码,了解它是如何选择服务器的。,具体来说,Ribbon 通过一个名为 LoadBalance 的类来拦截请求,并根据预设的负载均衡策略来挑选合适的服务器。
图片
让我们来深入分析一下关键代码,其实所有的负载均衡算法逻辑都集中在 getServer 方法的实现中。
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer, hint);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server,
isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
我们可以对负载均衡策略进行局部配置,以便根据特定的业务需求和场景灵活调整服务调用的行为。
#被调用的微服务名
mall‐order:
ribbon:
#指定使用Nacos提供的负载均衡策略(优先调用同一集群的实例,基于随机&权重)
NFLoadBalancerRuleClassName:com.alibaba.cloud.nacos.ribbon.NacosRule
当然,我们也可以进行全局配置,以便在整个系统范围内统一管理负载均衡策略和参数。
@Bean
public IRule ribbonRule() {
// 指定使用Nacos提供的负载均衡策略(优先调用同一集群的实例,基于随机权重)
return new NacosRule();
}
总结
随着本文的深入探讨,我们对微服务架构中的服务发现与注册机制有了更全面的认识。从单体架构的局限性到微服务的灵活性,我们见证了架构演进的历程。服务发现与注册作为微服务通信的基石,其重要性不言而喻。通过Nacos这一强大的注册中心,我们不仅实现了服务的动态注册与发现,还通过心跳监测、负载均衡等机制,确保了服务的高可用性和稳定性。
在技术选型上,Nacos的gRPC实现展示了其在性能优化方面的潜力,同时也带来了系统复杂性的挑战。然而,通过精心设计的客户端和服务端代码,我们能够有效地管理服务实例,实现服务的快速响应和负载均衡。这些机制的实现,不仅提升了系统的伸缩性和容错性,也为微服务的快速发展提供了坚实的基础。