几个星期前曾经分享过Nacos在配置中心部分的内容,今天来分享它的另一部分内容,即:服务发现。废话不多说,来看一下今天这篇小文章的目录结构吧:
一、概述
1.1> 从单体服务到微服务
1.1.1> 单体服务(all in one)
- 一个项目中包含了所有的服务。通常来说,如果一个war包/jar包里面包含一个应用的所有功能,则我们称这种架构为单体架构。即:所有服务都在service模块中。
- 如下图所示:
- 优点:
- 1> 架构简单,清晰。服务之间调用方便,快捷。
- 2> 服务部署简单。部署量少。运维量很低。
- 缺点:
- 1> 随着项目进展时间变长,整个项目的代码复杂度越来越高,很容易一个小改动,导致很大的系统问题。
- 2> 由于代码量很大,编译和部署会越来越慢,甚至20~30分钟都很正常。
- 3> 由于有的功能属于大量运算的CPU密集型模块,有的是大量读写磁盘的IO密集型模块。但是由于融合在了一个项目中,无法针对单个功能模块进行扩展。那么只能CPU和内存和磁盘都要提升,资源投入很大。
- 4> 如果我们想要针对已有项目改变技术选型,那么就需要针对整个项目进行修改,工作量将会巨大。
1.1.2> 集群级垂直化
- 随着用户量越来越大,服务器负载也随之增高,用户需求导致对产品的需求也会剧增。那么这一个war包中的代码量也会越来越大,面临着每一次代码修改(无论小修改还是大迭代),都要把整个商城的代码重新测试和部署,而且部署时间也会非常的漫长。针对这种情况,我们进行如下优化:
- 1> 通过横向增加服务器,把单台机器变成多台机器的集群。
- 2> 按照业务的垂直领域进行拆分,减少业务的耦合度,以及降低单个war包带来的伸缩性困难的问题。
- 如下图所示:
1.1.3> SOA
- 核心目标是把一些通用的、会被多个上层服务调用的共享业务提取成独立的基础服务,这些被提取出来的共享服务想对来说比较独立,并且可以重用。所以在SOA中,服务是最核心的抽象手段,业务被划分为一些粗粒度的业务服务和业务流程。
- SOA主要解决的问题是:
- 1> 信息孤岛。
- 2> 共享业务的重用。
- 如下图所示:
1.1.4> 微服务
- 那么被SOA拆分出来的服务是否也需要以业务功能为维度来进行拆分和独立部署,以降低业务的耦合及提升容错性?微服务就是这样一种解决方案。
- 我们可以把SOA看成微服务的超集,也就是多个微服务可以组成一个SOA服务。
- 伴随着服务颗粒化的细化,会导致原本10个服务可能拆分成了100个微服务,一旦服务规模扩大,就意味着服务的构建、发布、运维的复杂度也会成倍增加。
- 如下图所示:
- 优点:
- 1> 每个服务足够小,足够内聚,专注于一个业务功能点提供服务。代码更容易理解。
- 2> 有代码修改或部署上线,只会影响对应的微服务,而不会是整个服务。
- 3> 可针对服务是计算型还是IO型进行针对性的硬件升级。
- 4> 可以针对某些高吞吐服务进行硬件升级或者服务横向扩容,而不是对所有服务都升级,节约投入成本。
- 缺点:
- 1> 极大的增加了运维工作量,以前几个war包,现在可能需要部署几百个。
- 2> 微服务之间的互相调用,会增加通讯成本。
- 3> 分布式事务问题会引出数据一致性的问题。
- 4> 服务增多,如果管控成百上千的服务。如何准确并快速定位问题。
1.1.5> SOA与微服务的区别
- SOA关注的是服务的重用性及解决信息孤岛问题。
- 微服务关注的是解耦,虽然解耦和可重用性从特定的角度来看是一样的,但本质上是有区别的;
- 解耦:降低业务之间的耦合度。
- 重用性:关注的是服务的复用。
- 微服务会更多地关注在DevOps的持续交付上,因为服务粒度细化之后使得开发运维变得更加重要,因此微服务与容器化技术的结合更加紧密。
1.2> 服务发现介绍
- 在传统的系统部署中,服务运行在一个固定的已知的IP和端口上,如果一个服务需要调用另一个服务,那么可以通过地址直接调用。但是,在虚拟化或者容器化的环境中,服务实例的启动和销毁是很频繁的,那么服务地址也是在动态变化的。这种情况下,就需要服务发现机制了。服务发现的方式有如下两种:
- 客户端——服务发现 (client-side service discovery)
- 客户端通过查询服务注册中心,获取可用服务的实际网络地址(IP&PORT)。然后通过负载均衡算法来选择一个可用的服务实例,并将请求发送至该服务。
- 在服务启动的时候,向服务注册中心注册服务;在服务停止的时候,向服务注册中心注销服务。服务注册的一个典型实现方式就是通过heartbeat机制(心跳机制)定时刷新。
- 服务端——服务发现 (server-side service discovery)
- 客户端向load balancer上发送请求。load balancer查询服务注册中心,找到可用的服务,然后转发请求到该服务上。和客户端发现一样,服务都要到注册中心进行服务的注册和销毁。
- 服务发现调用流程
1.3> 服务发现技术对比
1.4> Nacos简介
- https://nacos.io/zh-cn/
- Nacos是阿里的一个开源产品,它是针对微服务架构中的服务发现、配置管理、服务治理的综合型解决方案。
- 官方介绍如下: 致力于帮助您发现、配置和管理微服务。 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。 帮助您更敏捷和容易地构建、交付和管理微服务平台。 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
1.5> Nacos特性
Nacos主要提供以下四大功能:
- 服务发现与服务健康检查 Nacos使服务更容易注册,并通过DNS或HTTP接口发现其他服务; Nacos还提供服务的实时健康检查,以防止向不健康的主机或服务实例发送请求。
- 动态配置服务 动态配置服务运行在所有环境中以集中和动态的方式管理所有服务的配置。 Nacos消除了在更新配置时重新部署应用程序,这使配置的更改更加高效和灵活。
- 动态DNS服务 Nacos提供基于DNS协议的服务发现能力,旨在支持异构语言的服务发现; 支持将注册在Nacos上的服务以域名的方式暴露端点,让三方应用方便查阅及发现。
- 服务和元数据管理 Nacos能让您从微服务平台建设的视角管理数据中心的所有服务及元数据(即:服务相关的一些配置和状态信息) 包括:管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略。
二、快速入门
2.1> 服务协作流程
- Spring Cloud常见集成方案
【注】
- Ribbon:基于客户端的负载均衡。
- Feign:可以帮我们更快捷、优雅地调用HTTP API。将HTTP报文请求方式伪装为简单的java接口调用方式。
2.2> 搭建Nacos服务端
请参见【配置中心Nacos】中Nacos的安装步骤
三、服务发现基础应用
3.1> Nacos架构图
- 官方给出的架构图如下所示:
【注】
- Provider APP :服务提供者
- Consumer APP :服务消费者
- Name Server:通过VIP(Vritual IP)或者DNS的方式实现Nacos高可用集群的服务路由。
- Nacos Server :Nacos服务提供者,里面包含:
- Open API:功能访问入口。
- Config Service:配置服务模块。在服务或者应用运行过程中,提供动态配置或者元数据以及配置管理的服务提供者。
- Naming Service:名字服务模块。提供分布式系统中所有对象(Object)、实体(Entity)的“名字”到关联的元数据之间的映射管理服务,服务发现和DNS就是名字服务的2大场景。
- Consistency Protocol:一致性协议,用来实现Nacos集群节点的数据同步。使用Raft算法(使用类似算法的中间件还有Etcd、Redis哨兵选举等)。
- Nacos Console:Nacos的控制台。
3.2> 前期准备
3.2.1> 添加父类pom依赖
<properties> <java.version>1.8</java.version> <spring.boot>2.1.3.RELEASE</spring.boot> <spring.cloud>Greenwich.RELEASE</spring.cloud> <spring.cloud.alibaba>2.1.0.RELEASE</spring.cloud.alibaba> </properties>
<dependencyManagement>
<dependencies>
<!-- 引入Spring Cloud Alibaba依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba}</version>
<type>pom</type>
<scope>import</scope>
</dependency><!-- 引入Spring Cloud依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring.cloud}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- 引入Spring Boot依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies>
</dependencyManagement>
3.2.2> 项目结构
3.3> 基于Feign+Nacos的服务调用
- Feign和OpenFeign两者的区别:
3.3.0> 整体项目
- nacos-provider
- 项目主pom
- 添加Lombok、Spring Cloud OpenFeign、Spring Cloud Alibaba Nacos Discovery、Spring Boot Web依赖
- nacos-provider-web
- pom.xml添加Spring Boot Web、Nacos Discovery、OpenFeign的依赖
- application.yml添加Nacos配置
- NacosProviderApplication类上添加@EnableDiscoveryClient和@EnableFeignClients注解
- ProviderController添加hello()方法
- nacos-provider-api
- 无
- nacos-provider-application
- 无
- nacos-consumer
- 项目主pom
- 添加Lombok、Spring Cloud OpenFeign、Spring Cloud Alibaba Nacos Discovery、Spring Boot Web依赖
- nacos-consumer-web
- application.yml添加Nacos配置
- ProviderServicepom.xml添加Spring Boot Web、Nacos Discovery、OpenFeign的依赖
- 在feign.service包中添加Feign的接口
- NacosConsumerApplication类上添加@EnableDiscoveryClient和@EnableFeignClients注解
- ConsumerController添加hello()方法,引入ProviderService并调用hello()方法
- nacos-consumer-api
- 无
- nacos-consumer-application
- 无
3.3.1> 架构图
3.3.2> nacos-provider相关代码
- 项目的主pom
<dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
<!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Cloud Alibaba Nacos Discovery --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- Spring Cloud --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
</dependencies>
【注】添加Lombok、Spring Boot Web、Spring Cloud OpenFeign、Spring Cloud Alibaba Nacos Discovery的依赖。
- nacos-provider-web
pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
</dependencies>
application.yml
server:
port: 7000
spring:
application:
name: nacos-provider
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
【注】添加nacos.discovery的配置
NacosProviderApplication
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class NacosProviderApplication {
public static void main(String[] args) {
SpringApplication.run(NacosProviderApplication.class, args);
}
}
【注】添加@EnableDiscoveryClient和@EnableFeignClients注解
ProviderController
@Slf4j
@RestController
@RequestMapping("/provider")
public class ProviderController {
/**
* http://localhost:7000/provider/hello
*/
@GetMapping("/hello")
public String hello() {
log.info("provider hello");
return "provider hello";
}
}
3.3.3> nacos-consumer相关代码
- 项目的主pom
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency><!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Cloud Alibaba Nacos Discovery --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- Spring Cloud --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
</dependencies>
【注】添加Lombok、Spring Boot Web、Spring Cloud OpenFeign、Spring Cloud Alibaba Nacos Discovery的依赖。
- nacos-consumer-web
pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
</dependencies>
application.yml
server:
port: 7002
spring:
application:
name: nacos-consumer
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
【注】添加nacos.discovery的配置
NacosConsumerApplication
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class NacosConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(NacosConsumerApplication.class, args);
}
}
ProviderService
@FeignClient(name = "nacos-provider")
public interface ProviderService {
@GetMapping("/provider/hello")
String hello();
}
ConsumerController
@Slf4j
@RestController
@RequestMapping("/consumer")
public class ConsumerController {
@Resource
private ProviderService providerService;/** * 基于Feign调用 * http://localhost:7002/consumer/hello */ @GetMapping("/hello") public String hello() { log.info("Feign invoke!"); return providerService.hello(); }
}
请求http://localhost:7002/consumer/hello
3.4>基于Dubbo+Nacos的服务调用
3.4.0> 整体项目
- nacos-provider
- 项目主pom
- 添加dubbo依赖
- nacos-provider-web
- application.yml添加dubbo配置
- pom.xml添加nacos-provider-application依赖(用于dubbo扫描该module)
- nacos-provider-api
- 接口OrderService
- nacos-provider-application
- pom.xml添加dubbo依赖和nacos-provider-api依赖
- 实现类OrderServiceImpl
- nacos-consumer
- 项目主pom
- 添加dubbo依赖
- nacos-consumer-web
- application.yml添加dubbo配置
- pom(添加nacos-provider-api依赖)
- ConsumerController添加getOrder()方法
- nacos-consumer-api
- 无
- nacos-consumer-application
- 无
3.4.1> 架构图
3.4.2> nacos-provider相关代码
- 项目的主pom
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
【注】添加dubbo依赖
- nacos-provider-web
application.yml
dubbo:
scan:
base-packages: com.muse.nacos.provider # Dubbo服务扫描基准包
protocol:
name: dubbo # dubbo 协议
port: 21880 # dubbo 协议端口 (与server.port一样,需要修改)
registry:
address: nacos://127.0.0.1 # Nacos注册地址
cloud:
subscribed-services: nacos-provider
【注】添加dubbo配置。
pom依赖。添加这个module的依赖,才能扫描到该module下使用了dubbo的@Service注解的类
<dependency>
<groupId>com.muse.nacos.provider</groupId>
<artifactId>nacos-provider-application</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- nacos-provider-api添加
OrderService接口
public interface OrderService {
/**
* 获得订单信息
*
* @return
*/
String getOrder();
}
- nacos-provider-application
pom依赖
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
<dependency>
<groupId>com.muse.nacos.provider</groupId>
<artifactId>nacos-provider-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
添加实现类OrderServiceImpl
@Slf4j
@Service // org.apache.dubbo.config.annotation.Service
public class OrderServiceImpl implements OrderService {
public String getOrder() {
log.info("Order Info");
return "Order Info";
}
}
3.4.3> nacos-consumer相关代码
- 项目的主pom
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
【注】添加dubbo依赖
- nacos-consumer-web
application.yml
dubbo:
scan:
base-packages: com.muse.nacos.consumer
cloud:
subscribed-services: nacos-provider
pom.xml
<dependency>
<groupId>com.muse.nacos.provider</groupId>
<artifactId>nacos-provider-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
ConsumerController
@Slf4j
@RestController
@RequestMapping("/consumer")
public class ConsumerController {@Reference // org.apache.dubbo.config.annotation.Reference private OrderService orderService; /** * 基于Dubbo调用 * http://localhost:7002/consumer/getOrder */ @GetMapping("/getOrder") public String getOrder() { log.info("getOrder invoke!"); return orderService.getOrder(); }
}
请求http://localhost:7002/consumer/getOrder
四、Nacos源码解读
4.1> Spring Cloud完成服务注册的时机
4.1.1> 通用服务注册标准
- 在spring-cloud-commons包中有一个类org.springframework.cloud.client.serviceregistry.ServiceRegistry,它是Spring Cloud提供的服务注册的标准。集成到Spring Cloud中实现服务注册的组件,都会实现该接口。如下所示:
- Nacos中针对这个接口有一个实现类是com.alibaba.cloud.nacos.registry.NacosServiceRegistry。如下所示:
4.1.2> Spring Cloud集成Nacos的实现过程
- 在spring-cloud-common包的META-INF/spring.factories中包含自动装配的配置信息,其中AutoServiceRegistrationAutoConfiguration就是服务注册相关的配置类。如下所示:
- 在AutoServiceRegistrationAutoConfiguration配置类中,可以看到注入了一个AutoServiceRegistration实例。如下图所示:
- 可以看出,AbstractAutoServiceRegistration抽象类实现了AutoServiceRegistration接口,并且最重要的是NacosAutoServiceRegistration继承了AbstractAutoServiceRegistration。如下图所示:
- 其中,从上面类图可见,AbstractAutoServiceRegistration实现了接口ApplicationListener。ApplicationListener类的作用就是提供一种事件监听机制,它的方法onApplicationEvent(E event)的作用就是监听某一个指定事件。而AbstractAutoServiceRegistration实现该方法,并且监听WebServerInitializedEvent事件(当Webserver初始化完成之后),调用this.bind(event)方法。如下所示:
4.1.3> Dubbo集成Nacos的实现过程
- 因为要简化标题,所以准确的说,介绍的应该是Spring Cloud Alibaba Dubbo集成Nacos的实现过程。
- spring-cloud-alibaba-dubbo
- DubboServiceRegistrationNonWebApplicationAutoConfiguration
4.2> NacosServiceRegistry的实现
- register(...)方法
- registerInstance(...) 方法
【注】serverProxy.registerService(...)方法在4.3节介绍。
- addBeatInfo(...)
- BeatTask.run(...)
4.3> 服务注册原理
- serverProxy.registerService(...)
- Nacos提供了SDK及Open API的形式来实现服务注册。这两种形式的本质是一样的,SDK方式只是提供了一种访问的封装,在底层仍然是基于HTTP协议完成请求的。
- InstanceController.register(...) 对应源码项目为:nacos-1.3.0
- registerInstance(...)
- createEmptyService(...)
- putServiceAndInit(...)
- service.init() 心跳检测机制
- pushService(...)
- 简单总结一下服务注册的完整过程:
- Nacos客户端通过Open API的形式发送服务注册请求。
- Nacos服务端收到请求后,做以下三件事:
- 构建一个Service对象保存到ConcurrentHashMap集合中。
- 使用定时任务对当前服务下的所有实例建立心跳检测机制。
- 基于数据一致性协议将服务数据进行同步。
4.4> 服务提供者地址查询原理
- InstanceController.list(...)
- doSrvIPXT(...)
- 总结一下上面关于doSrvIPXT方法的主要逻辑
- 根据namespaceId、serviceName获得Service实例。
- 从Service实例中基于srvIPs得到所有服务提供者的实例信息。
- 遍历组装JSON字符串并返回。
4.5> 服务地址动态感知原理
- 原理图
【解释】
Nacos客户端有一个HostReactor类,它的功能是实现服务的动态更新,基本原理如下:
- 客户端发起事件订阅后,在HostReactor中有一个UpdateTask线程,每10s发送一次Pull请求,获得服务端最新的地址列表。
- 对于服务端,它和服务提供者的实例之间维持了心跳检测,一旦服务提供者出现异常,则会发送一个Push消息给Nacos客户端,也就是服务消费者。
- 服务消费者收到请求之后,使用HostReactor中提供的processServiceJSON解析消息,并更新本地服务地址列表。
- HostReactor的建立
在init里会有三个任务:
- 1> FailoverReactor.SwitchRefresher,默认每5秒检测是否开启故障转移,若是开启,则把文件数据读入serviceMap。
- 2> FailoverReactor.DiskFileWriter,默认一天把服务信息写入本地。
- 3> 建立10秒后调用DiskFileWriter#run,检测本地缓存文件,若是没有则建立缓存文件。
- UpdateTask
- 服务获取和刷新的流程图