Nacos
Dynamic Naming and Configuration Service
一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台
使用
官方文档: https://nacos.io/zh-cn/docs/what-is-nacos.html
核心功能
服务注册:Nacos Client 会通过发送 REST 请求的方式向 Nacos Server 注册自己的服务,提供自身的元数据,比如 ip 地址、端口等信息。Nacos Server 接收到注册请求后,就会把这些元数据信息存储在一个双层的内存 Map 中。
服务心跳:在服务注册后,Nacos Client 会维护一个定时心跳来持续通知 Nacos Server,说明服务一直处于可用状态,防止被剔除。默认 5s 发送一次心跳。
服务同步:Nacos Server 集群之间会互相同步服务实例,用来保证服务信息的一致性。
服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个 REST 请求给 Nacos Server,获取上面注册的服务清单,并且缓存在 Nacos Client 本地,同时会在 Nacos Client 本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存。
服务健康检查:Nacos Server 会开启一个定时任务用来检查注册服务实例的健康情况,对于超过 15s 没有收到客户端心跳的实例会将它的 healthy 属性置为 false(客户端服务发现时不会发现),如果某个实例超过 30 秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)。
快速开始
搭建 Nacos-client 服务
1)引入依赖
父 Pom 中支持 spring cloud&spring cloud alibaba, 引入依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <dependencyManagement> <dependencies> <!--引入springcloud的版本--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.SR3</version> <type>pom</type> <scope>import</scope> </dependency>
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.1.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies>
</dependencyManagement>
<repositories> <repository> <id>spring</id> <url>https: <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories>
|
当前项目 pom 中引入依赖
1 2 3 4
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
|
2)application.properties 中配置
1 2 3 4 5
| server.port=8002
spring.application.name=service-user
spring.cloud.nacos.discovery.server-addr=localhost:8848
|
更多配置:https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-discovery
3)启动 springboot 应用,nacos 管理端界面查看是否成功注册
![img](https://bolg2022.oss-cn-hangzhou.aliyuncs.com/202209202240348.png)
服务注册表结构
![img](https://bolg2022.oss-cn-hangzhou.aliyuncs.com/202209202241144.png)
数据模型
Nacos 数据模型 Key 由三元组唯一确定, Namespace 默认是空串,公共命名空间(public),分组默认是 DEFAULT_GROUP。
![img](https://bolg2022.oss-cn-hangzhou.aliyuncs.com/202209202241770.jpeg)
服务领域模型
![img](https://bolg2022.oss-cn-hangzhou.aliyuncs.com/202209202242379.jpeg)
官网地址:https://nacos.io/zh-cn/docs/architecture.html
服务实例数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| [{ "clusterName": "DEFAULT", "enabled": true, "ephemeral": true, "healthy": true, "instanceHeartBeatInterval": 5000, "instanceHeartBeatTimeOut": 15000, "instanceId": "127.0.0.1#8880#DEFAULT#DEFAULT_GROUP@@orderService", "instanceIdGenerator": "simple", "ip": "127.0.0.1", "ipDeleteTimeout": 30000, "metadata": {}, "port": 8880, "serviceName": "DEFAULT_GROUP@@orderService", "weight": 1.0 }, { "clusterName": "DEFAULT", "enabled": true, "ephemeral": true, "healthy": true, "instanceHeartBeatInterval": 5000, "instanceHeartBeatTimeOut": 15000, "instanceId": "127.0.0.1#8888#DEFAULT#DEFAULT_GROUP@@orderService", "instanceIdGenerator": "simple", "ip": "127.0.0.1", "ipDeleteTimeout": 30000, "metadata": {}, "port": 8888, "serviceName": "DEFAULT_GROUP@@orderService", "weight": 1.0 }]
|
原理
![img](https://bolg2022.oss-cn-hangzhou.aliyuncs.com/202209202246625.png)
Nacos 基本架构
Naming Service:用来做服务发现的模块
Config Service:用来提供配置项管理、动态更新配置和元数据的功能
![image-20220920233149787](https://bolg2022.oss-cn-hangzhou.aliyuncs.com/202209202331584.png)
Nacos Core 模块:提供一系列的平台基础功能,是支撑 Nacos 上层业务场景的基石
![image-20220920233324003](https://bolg2022.oss-cn-hangzhou.aliyuncs.com/202209202333942.png)
Nacos 还有一个“一致性协议”,用来确保 Nacos 集群中各个节点之间的数据一致性。Nacos 内部支持两种一致性协议,一种是侧重 一致性的 Raft 协议,基于集群中选举出来的 Leader 节点进行数据写入;另一种是针对临 时节点的 Distro 协议,它是一个侧重可用性(或最终一致性)的分布式一致性协议。
Nacos 自动装配原理
Nacos 是怎么在启动阶段自动加载配置项并开启相关功能的呢?
将 Nacos 依赖项添加到项目中,同时也引入了 Nacos 自带的自动装配器,比如下面 这几个被引入的自动装配器就掌管了 Nacos 核心功能的初始化任务。
NacosDiscoveryAutoConfiguration:服务发现功能的自动装配器,它主要做两件事 儿:加载 Nacos 配置项,声明 NacosServiceDiscovery 类用作服务发现;
NacosServiceAutoConfiguration:声明核心服务治理类 NacosServiceManager, 它可以通过 service id、group 等一系列参数获取已注册的服务列表;
NacosServiceRegistryAutoConfiguration:Nacos 服务注册的自动装配器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
@ConditionalOnNacosDiscoveryEnabled public class NacosDiscoveryAutoConfiguration {
@Bean @ConditionalOnMissingBean public NacosDiscoveryProperties nacosProperties() { return new NacosDiscoveryProperties(); }
@Bean @ConditionalOnMissingBean public NacosServiceDiscovery nacosServiceDiscovery( NacosDiscoveryProperties discoveryProperties, NacosServiceManager nacosServiceManager) { return new NacosServiceDiscovery(discoveryProperties, nacosServiceManager); }
}
|
NacosDiscoveryProperties 类通过 ConfigurationProperties 注解从 spring.cloud.nacos.discovery 路径下获取配置项,Spring 框架会自动将这些配置项解析 到 NacosDiscoveryProperties 类定义的类属性中。这样一来 Nacos 就完成了配置项的加 载,在其它业务流程中,只需要注入 NacosDiscoveryProperties 类就可以读取 Nacos 的 配置参数。
1 2 3 4 5
| @ConfigurationProperties("spring.cloud.nacos.discovery") public class NacosDiscoveryProperties {
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class NacosServiceDiscovery { private NacosDiscoveryProperties discoveryProperties; private NacosServiceManager nacosServiceManager; public List<ServiceInstance> getInstances(String serviceId) throws NacosException { String group = discoveryProperties.getGroup(); List<Instance> instances = namingService().selectInstances(serviceId, group, true); return hostToServiceInstanceList(instances, serviceId); }
public List<String> getServices() throws NacosException { String group = discoveryProperties.getGroup(); ListView<String> services = namingService().getServicesOfServer(1, Integer.MAX_VALUE, group); return services.getData(); }
... }
|
通过 NacosServiceDiscovery 暴露的方法,我们就能够根据 serviceId(注册到 nacos 的 服务名称)查询到可用的服务实例,获取到服务实例列表之后,调用方就可以发起远程服 务调用了。
Nacos 配置项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| cloud: nacos: discovery: server-addr: localhost:8848 service: coupon-customer-serv heart-beat-interval: 5000 heart-beat-timeout: 20000 metadata: mydata: abc naming-load-cache-at-start: false namespace: dev cluster-name: Cluster-A group: myGroup register-enabled: true
|
![image-20220920235023875](https://bolg2022.oss-cn-hangzhou.aliyuncs.com/202209202350665.png)
Nacos 服务发现底层实现
Nacos Client 通过一种主动轮询的机制从 Nacos Server 获取服务注册信息,包括地址列 表、group 分组、cluster 名称等一系列数据。简单来说,Nacos Client 会开启一个本地 的定时任务,每间隔一段时间,就尝试从 Nacos Server 查询服务注册表,并将最新的注 册信息更新到本地。这种方式也被称之为“Pull”模式,即客户端主动从服务端拉取的模 式。 负责拉取服务的任务是 UpdateTask 类,它实现了 Runnable 接口。Nacos 以开启线程的 方式调用 UpdateTask 类中的 run 方法,触发本地的服务发现查询请求。
UpdateTask 这个类隐藏得非常深,它是 HostReactor的一个内部类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| public class UpdateTask implements Runnable { @Override public void run() { long delayTime = DEFAULT_DELAY; try { ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters)); if (serviceObj == null) { updateService(serviceName, clusters); return; } if (serviceObj.getLastRefTime() <= lastRefTime) { updateService(serviceName, clusters); serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters)); } else { refreshOnly(serviceName, clusters); } lastRefTime = serviceObj.getLastRefTime(); if (!eventDispatcher.isSubscribed(serviceName, clusters) && !futureMap .containsKey(ServiceInfo.getKey(serviceName, clusters))) { NAMING_LOGGER.info("update task is stopped, service:" + serviceName + ", clusters:" + clusters); return; } if (CollectionUtils.isEmpty(serviceObj.getHosts())) { incFailCount(); return; } delayTime = serviceObj.getCacheMillis(); resetFailCount(); } catch (Throwable e) { incFailCount(); NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e); } finally { executor.schedule(this, Math.min(delayTime << failCount, DEFAULT_DELAY * 60), TimeUnit.MILLISECONDS); } } }
|
客户端负载均衡
Spring Cloud Loadbalancer 采用了客户端负载均衡技术,每个发起服务调用的客户端都 存有完整的目标服务地址列表,根据配置的负载均衡策略,由客户端自己决定向哪台服务 器发起调用。
![image-20220921000035451](https://bolg2022.oss-cn-hangzhou.aliyuncs.com/202209210000321.png)
客户端负载均衡的优势很明显。
网络开销小:由客户端直接发起点对点的服务调用,没有中间商赚差价;
配置灵活:各个客户端可以根据自己的需要灵活定制负载均衡策略。
客户端负载均衡技术往往需要依赖服务发现技术来获取服务列表
Loadbalancer 工作原理
…todo 2022/09/21
OpenFeign
OpenFeign 组件的前身是 Netflix Feign 项目,它最早是作为 Netflix OSS 项目的一部 分,由 Netflix 公司开发。后来 Feign 项目被贡献给了开源组织,于是才有了我们今天使 用的 Spring Cloud OpenFeign 组件。
OpenFeign 提供了一种声明式的远程调用接口,它可以大幅简化远程调用的编程体验
OpenFeign 使用了一种“动态代理”技术来封装远程服务调用的过程
1 2 3 4 5
| @FeignClient(value = "hello-world-serv") public interface HelloWorldService { @PostMapping("/sayHello") String hello(String guestName); }
|
服务的名称、接口类型、访问路径已经通过注解做了声明
OpenFeign 通过解析这些注解标签生成一个“动态代理类”,这个代理类会将接口调用转 化为一个远程服务调用的 Request,并发送给目标服务。
OpenFeign 的动态代理
在项目初始化阶段,OpenFeign 会生成一个代理类,对所有通过该接口发起的远程调用进 行动态代理。
![image-20220921001114427](https://bolg2022.oss-cn-hangzhou.aliyuncs.com/202209210011297.png)
上图中的步骤 1 到步骤 3 是在项目启动阶段加载完成的,只有第 4 步“调用远程服务”是 发生在项目的运行阶段。
首先,在项目启动阶段,OpenFeign 框架会发起一个主动的扫包流程,从指定的目录下扫 描并加载所有被 @FeignClient 注解修饰的接口。
然后,OpenFeign 会针对每一个 FeignClient 接口生成一个动态代理对象,即图中的 FeignProxyService,这个代理对象在继承关系上属于 FeignClient 注解所修饰的接口的实 例。
接下来,这个动态代理对象会被添加到 Spring 上下文中,并注入到对应的服务里,也就 是图中的 LocalService 服务。
最后,LocalService 会发起底层方法调用。实际上这个方法调用会被 OpenFeign 生成的代理对象接管,由代理对象发起一个远程服务调用,并将调用的结果返回给 LocalService。
![image-20220921001251664](https://bolg2022.oss-cn-hangzhou.aliyuncs.com/202209210012483.png)
**OpenFeign 组件加载过程**
OpenFeign 动态代理类的创建过程
- 项目加载:在项目的启动阶段,EnableFeignClients 注解扮演了“启动开关”的角 色,它使用 Spring 框架的 Import 注解导入了 FeignClientsRegistrar 类,开始了 OpenFeign 组件的加载过程。
- 扫包:FeignClientsRegistrar 负责 FeignClient 接口的加载,它会在指定的包路径下 扫描所有的 FeignClients 类,并构造 FeignClientFactoryBean 对象来解析 FeignClient 接口。
- 解析 FeignClient 注解:FeignClientFactoryBean 有两个重要的功能,一个是解析 FeignClient 接口中的请求路径和降级函数的配置信息;另一个是触发动态代理的构造过程。其中,动态代理构造是由更下一层的 ReflectiveFeign 完成的。
- 构建动态代理对象:ReflectiveFeign 包含了 OpenFeign 动态代理的核心逻辑,它主 要负责创建出 FeignClient 接口的动态代理对象。ReflectiveFeign 在这个过程中有两个 重要任务,一个是解析 FeignClient 接口上各个方法级别的注解,将其中的远程接口 URL、接口类型(GET、POST 等)、各个请求参数等封装成元数据,并为每一个方法 生成一个对应的 MethodHandler 类作为方法级别的代理;另一个重要任务是将这些 MethodHandler 方法代理做进一步封装,通过 Java 标准的动态代理协议,构建一个实 现了 InvocationHandler 接口的动态代理对象,并将这个动态代理对象绑定到FeignClient 接口上。这样一来,所有发生在 FeignClient 接口上的调用,最终都会由它 背后的动态代理对象来承接。
MethodHandler 的构建过程涉及到了复杂的元数据解析,OpenFeign 组件将 FeignClient 接口上的各种注解封装成元数据,并利用这些元数据把一个方法调用“翻译”成一个远程调用的 Request 请求。
“元数据的解析”是如何完成的呢?它依赖于 OpenFeign 组件中的 Contract 协议解析功能。Contract 是 OpenFeign 组件中定义的顶层抽象接口,它有一系 列的具体实现,其中和我们实战项目有关的是 SpringMvcContract 这个类,从这个类的名 字中我们就能看出来,它是专门用来解析 Spring MVC 标签的。
SpringMvcContract 的继承结构是 SpringMvcContract->BaseContract->Contract。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| @Override protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { if (!RequestMapping.class.isInstance(methodAnnotation) && !methodAnnotation .annotationType().isAnnotationPresent(RequestMapping.class)) { return; } RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class); RequestMethod[] methods = methodMapping.method(); if (methods.length == 0) { methods = new RequestMethod[] { RequestMethod.GET }; } checkOne(method, methods, "method"); data.template().method(Request.HttpMethod.valueOf(methods[0].name()));
checkAtMostOne(method, methodMapping.value(), "value"); if (methodMapping.value().length > 0) { String pathValue = emptyToNull(methodMapping.value()[0]); if (pathValue != null) { pathValue = resolve(pathValue); if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) { pathValue = "/" + pathValue; } data.template().uri(pathValue, true); } }
parseProduces(data, method, methodMapping);
parseConsumes(data, method, methodMapping);
parseHeaders(data, method, methodMapping);
data.indexToExpander(new LinkedHashMap<>()); }
|
通过上面的方法,我们可以看到,OpenFeign 对 RequestMappings 注解的各个属性都做 了解析。
Debug
在 OpenFeign 组件的 FeignClientsRegistrar 中打上一个断点,这是 OpenFeign 初始化的起点
日志信息打印
1 2 3 4
| logging: level: com.geekbang.coupon.customer.feign.TemplateService: debug com.geekbang.coupon.customer.feign.CalculationService: debug
|
还需要在应用的上下文中使用代码的方式声明 Feign 组件的日志级别
1 2 3 4 5
| @Bean Logger.Level feignLogger() { return Logger.Level.FULL; }
|
OpenFeign 超时判定
1 2 3 4 5 6 7 8 9 10 11 12 13
| feign: client: config: default: connectTimeout: 1000 readTimeout: 5000 coupon-template-serv: connectTimeout: 1000 readTimeout: 2000
|
OpenFeign 降级
降级逻辑是在远程服务调用发生超时或者异常(比如 400、500 Error Code)的时候,自 动执行的一段业务逻辑。可以根据具体的业务需要编写降级逻辑,比如执行一段兜底逻辑将服务请求从失败状态中恢复,或者发送一个失败通知到相关团队提醒它们来线上排查 问题。
OpenFeign 实现 Client 端的服务降级
OpenFeign 对服务降级的支持是借助 Hystrix 组件实现的
OpenFeign 支持两种不同的方式来指定降级逻辑,一种是定义 fallback 类,另一种是定义 fallback 工厂
通过 fallback 类实现降级是最为简单的一种途径
如果你想要在降级方法中获取到异常的具体原因,那么你就要借助 fallback 工厂的方式来 指定降级逻辑了
自定义的 fallback 工厂需要实现 FallbackFactory 接口
![image-20220921003602982](https://bolg2022.oss-cn-hangzhou.aliyuncs.com/202209210036491.png)