# SpringCloud-Netflix **Repository Path**: XYnan/SpringCloud-Netflix ## Basic Information - **Project Name**: SpringCloud-Netflix - **Description**: eureka,ribbon,fegin,hystrix,zuul笔记 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-05-07 - **Last Updated**: 2021-09-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## Spring Cloud - Netflix ### Eureka #### 1. eureka client 启动流程? 1. 创建 EurekaInstanceConfig 和 EurekaClientConfig 2. 通过 EurekaInstanceConfig 创建应用信息管理器(ApplicationInfoManager) 3. 处理是否需要注册和抓取注册表,如果不需要,释放一些资源 4. 初始化调度线程池、初始化心跳线程池、初始化缓存刷新(注册表)线程池 6. 初始化 EurekaTransport ,是 EurekaClient 与 EurekaServer 之间的通信组件 7. 拉取注册表,初始化调度任务(就是上边三个线程池),初始化 InstanceInfoReplicator(服务注册使用) #### 2. eureka client 如何进行服务注册? 1. 服务注册是在 InstanceInfoReplicator 中来做的 2. InstanceInfoReplicator 中有一个定时调度线程,将自己作为线程放入并延迟40秒进行注册调用 3. 运行 run 方法,如果实例信息脏了,去进行服务注册 4. 定时进行任务调度 #### 3. eureka server 如何处理服务注册的? 1. Spring Cloud 代理了 jersey 的一些方法,当进行注册时,会传递 InstanceInfo 信息到 eureka server 中去 2. jersey 调用 InstanceRegistry 方法进行注册,最后调用到抽象类 AbstractInstanceRegistry 中去 3. 用一个 ConcurrentHashMap 来做内存注册表,里面保存着所有的注册实例({ServiceA:{id-001:Lease}}) #### 4. eureka server 注册表多级缓存机制 1. eureka server 的注册表采用多级缓存机制,分别是只读缓存,读写缓存,注册表 2. 访问顺序依次是:只读缓存 -> 读写缓存 -> 注册表 3. 缓存类初始化时,会将注册表数据都初始化为JSON串到读写缓存中 #### 5. eureka server 注册表多级缓存过期机制 1. 注册表多级缓存过期机制分别为:主动过期 + 被动过期 + 定时过期 2. 主动过期:服务注册、下线、故障时都会刷新读写缓存,都会调用 invalidateCache 方法使读写缓存过期 3. 定时过期:读写缓存中的键值有效期是180s,过了180s自动过期,在读写缓存构建时设置的 4. 被动过期:多级缓存会开启定时任务,每30秒同步读写缓存数据到只读缓存,这里有一个问题,就是会存在30秒延迟,也就是说你有服务注册、故障、下线了,那么最多会延迟30秒后才会被其他服务感知 #### 6. eureka server 注册表的三层队列机制 1. 三层队列:最近注册队列、最近下线队列、最近更新队列 2. 当服务发生注册、故障、下线时,会把对应服务实例放入到最近更新队列中 3. 有一个定时任务,每30秒处理一下最近更新队列,清除更新队列中停留3分钟的实例信息 #### 7. eureka client 注册表的抓取 1. 全量抓取,服务注册时进行全量抓取,走服务端的三级缓存获取注册表全部信息 2. 增量抓取,定时任务每30秒进行增量抓取,走服务端的三级缓存,三级缓存走最近更新队列获取增量注册表 3. eureka server 返回增量注册表和全量注册表的hash值 4. eureka client 拿到增量注册表,合并到本地注册表中,然后计算hash,并和服务端返回的hash进行比较 5. hash一致直接成功,hash不一致就拉取全量注册表(一致性hash对比机制) #### 8. eureka client 发送心跳? 1. 定时线程每30秒发送一次心跳,发送成功后更新成功心跳的时间戳 #### 9. eureka server 接收心跳? 1. server 端通过 jersey 接收了心跳请求,并更新对应 Lease 的 lastUpdateTimestamp 为当前时间 + 90s #### 10. eureka server 服务下线及实例摘除 1. eureka client 关闭相关线程池(调度、心跳、注册表、监控等)并发送下线请求 2. eureka server 接收请求,在注册表中移除指定实例,将移除实例加入到最近更新队列和最近下线队列 3. 更新服务实例下线时间、清理读写缓存 4. 其他实例增量拉取的时候就会拉取到实例下线信息 #### 11. eureka server 自动故障感知及实例摘除 1. 每60秒执行定时任务,扫描故障实例 2. 如果实例驱逐时间大于0(主动下线)、或者最后心跳时间距当前时间超过180秒(故障下线),加入到过期实例列表中 3. 从实例列表中定期随机驱逐实例,每次驱逐实例个数最多为本地注册实例个数的15% #### 12. eureka server 自我保护机制 1. AbstractInstanceRegistry 中有两个属性 numberOfRenewsPerMinThreshold(期望最少心跳实例个数) 和 expectedNumberOfRenewsPerMin(期望心跳实例个数) 2. MeasuredRate 统计每分钟发送心跳实例个数 3. 当进行实例摘除的时候,先判断是否开启自我保护机制,如果开启了自我保护,且最后一分钟心跳个数<=期望最少心跳实例个数,则认为自己网络故障,触发自我保护,停止移除过期实例 #### 13. eureka server 集群注册表同步以及高可用 1. PeerEurekaNodes 中有定时任务,每10分钟解析配置中 eureka server 节点的 url 并更新到本地,做节点信息复制使用 2. eureka server 在初始化时,从其他 eureka server 节点拉取注册表放到本地 3. eureka server 在接收实例的注册、下线、心跳、故障时,会同步到其他 eureka server 节点上 #### 14. eureka server 集群注册表同步使用3层队列的批处理机制 ### Ribbon #### 1. 为什么使用 RestTemplate 进行调用就会进行自动进行负载均衡? 1. RestTemplate 被 @LoadBalanced 标注之后,会在容器进行过初始化的时候进行处理 2. 代码在 LoadBalanceAutoConfiguration 中,收集项目中所有被 @LoadBalanced 标注的 RestTemplate 对象 3. 对这些 RestTemplate 对象进行定制,本质上就是给他们增加拦截器 Interceptor (LoadBalancerInterceptor) 4. 之后通过 RestTemplate 对象进行的所有请求调用都会被 LoadBalancerInterceptor 并进行处理负载均衡处理 #### 2. LoadBalancerInterceptor 如何改变 RestTemplate 的行为? 1. RestTemplate 进行服务调用会被 LoadBalancerInterceptor 的 intercept 方法所拦截 ```java // request 原始请求,body 请求体,execution 真正执行请求的http通信组件 public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { final URI originalUri = request.getURI(); String serviceName = originalUri.getHost(); Assert.state(serviceName != null, Request URI does not contain a valid hostname: + originalUri); return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution)); } ``` 2. 先获取到 request 的原始 url,然后通过 url 拿到服务名称 3. 然后通过 requestFactory 搞出来一个新的请求,通过 loadBalancerClient 进行服务调用 #### 3. LoadBalancerInterceptor 的 LoadBalancerClient 是如何来的? LoadBalancerClient 的定义在 RibbonAutoConfiguration 中 #### 4. SpringBoot 与 Ribbon 整合时默认的 ILoadBalancer 是谁? 1. ILoadBalancer 在 拦截器的拦截方法的下一步被调用,是被 SpringClietnFactory 所获取的 2. SpringClietnFactory 通过 ServiceID 来获取对应的配置上下文(ApplicationContext),然后拿到对应的 LoadBalancer 3. 获取的 LoadBalacer 肯定是在 Configuration 中配置的一个 Bean,在 RibbonClientConfiguration 中 ```java @Bean @ConditionalOnMissingBean public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList serverList, ServerListFilter serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) { if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) { return this.propertiesFactory.get(ILoadBalancer.class, config, name); } return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList, serverListFilter, serverListUpdater); } ``` #### 5. 核心组件:ZoneAwareLoadBalancer 如何通过注册表拿到服务实例信息并后续不断更新服务实例信息呢? ZoneAwareLoadBalancer 的作用就是获取到 ServiceA 对应的所有服务实例信息并不断更新它们 1. ZoneAwareLoadBalancer 初始化的时候调用父类 DynamicServerListLoadBalancer,DynamicServerListLoadBalancer 初始化父类 BaseLoadBalancer 并调用自己的 restOfInit 方法 2. restOfInit 方法调用 updateListOfServers 的作用获取 ServiceA ServerList(ServiceA对应的所有实例信息) 3. 通过 EurekaClient 和服务名获取到所有实例的服务器信息 4. 将这些实例的服务器信息更新到 BaseLoadBalancer 的 allServerList 中去 #### 6. Ribbon 在首次获取到注册表后如何持续更新注册的? 常规方法就是有一个定时任务定时去拉取注册表信息,eureka 是每 30 秒更新一次注册表,那么 LoadBalance 肯定也有一个组件定时去拉取注册表更新到本地的 1. DynamicServerListLoadBalancer 的 enableAndInitLearnNewServersFeature 就是专门做这个事的 2. serverListUpdater.start(updateAction)方法 3. PollingServerListUpdater 调用自己内部类的 start 方法,其实就是定时调用 DynamicServerListLoadBalancer 的 updateListOfServers 方法 4. 定时任务默认1秒后开始调用,之后每30秒进行一次注册表更新的调用 #### 7. 默认负载均衡算法如何选择一个 server? 1. 在拦截器中进入 LoadBalancerClient 的 execute 方法中,拿到 ServiceId 对应的 LoadBalance 2. 通过 LoadBalance 进行负载均衡获取对应的 server,这里底层肯定是调用 Rule 的负载均衡算法 3. ZoneAwareLoadBalancer 调用 BaseLoadBalancer 的 chooseServer 方法,然后调用到 rule.choose(key) 方法 4. 这里默认是轮询算法,调用 PredicateBasedRule 的 chooseServer 方法,然后调用到 AbstractServerPredicate 的 choose 方法 5. AbstractServerPredicate 的 choose 方法,用取模算法来获取本次需要调用的服务下标,然后获取服务列表对应下标的服务信息返回 6. 拿到需要调用的 server 信息,进行服务名替换并进行调用 ### Feign #### 1. Feign 入口点在哪里? @EnableFeignClients 开启配置扫描,将需要扫描的包和自定义配置交给 FeignClientsRegistrar。FeignClientsRegistrar 其实就做了两件事 - 注册加载 Feign 的所有配置信息 - 根据配置信息,通过 FeignClientFactory 创建 Feign 对象的 Bean ##### 1.1 registerDefaultConfiguration 注册 Feign 的全局配置 这里就是把 Feign 的全局配置信息构建成 Bean 并注册到 Spring 容器中。配置信息中包含 Feign 的需要扫描的包,需要扫描的类和默认配置类等 ##### 1.2 registerFeignClients 进行 FeignClient 的注册 拿到一个类路径扫描器,扫描指定包或指定类中包含 @Feign 注解的类。 - 扫描器中有一个 @FeignClient 的注解过滤器,也就是说被扫描的类只有包含 @FeignClient 注解才会被收集 - 在指定包或指定类中收集到可以进行注册的类后,进行 FeignClient 的注册工作 - 拿到被注册类的 @FeignClient 配置信息,对 FeignClientFactoryBean 进行属性设置(如: url、name、type、fallback等),然后进行注册工作 #### 2. FeignClient 对象是如何通过动态代理来生成的 FeignClient 对象都是从 FeignClientFactoryBean 的来构造生成的 ```java class FeignClientFactoryBean{ // getObject 的部分代码 public Object getObject() throws Exception { FeignContext context = applicationContext.getBean(FeignContext.class); Feign.Builder builder = feign(context); if (!StringUtils.hasText(this.url)) { String url; if (!this.name.startsWith(http)) { url = http:// + this.name; } else { url = this.name; } url += cleanPath(); return loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, url)); } } } ``` ##### 2.1 Feign.Builder 对象到底是如何生成的? Feign.Builder 先从 Spring 容器中拿到 FeignContext 上下文对象,然后调用 feign 方法来生成 Builder 对象。这其中又做了几件事,当然最重要的就是设置 Builder 对象的配置信息了 - 从 FeignContext 上下文中拿到日志组件、编码器、解码器、Spring 注解解析器、以及自定义配置并设置到 Builder 对象中 - 读取配置文件,这里有一个读取的优先级,优先级从低到高 - 先读取我们自定义的 Configuration 配置 - 读取 application.yml 中 feign.client 打头的配置 - 最后读取 application.yml 中 feign.client.ClientA 打头的配置 ##### 2.2 loadBalance 方法是如何生成动态代理对象的 拿到 FeignContext 上下文中的 Client 和 Targeter 对象,调用 Targeter 的 target 方法来生成动态代理对象,这里调用 target 方法时,会有两种逻辑 - 当 Hystrix 没有开启的时候,使用的是 Feign 默认的动态代理逻辑 - 设置 Builder 的 SynchronousMethodHandlerFactory 和 ParseHandlersByName - 进入 ReflectiveFeign 类中生成 **InvocationHandler** 和动态代理对象并返回 - 当 Hystrix 开启的时候,使用的是 Hystrix 的动态代理逻辑 #### 3. 动态代理对象如何进行请求调用 动态代理对象进行方法调用时,会被 Feign 的 FeignInvocationHandler 所拦截并执行 invoke 方法 ```java class FeignInvocationHandler{ // 省略部分逻辑 public Object invoke(Object proxy, Method method, Object[] args){ return dispatch.get(method).invoke(args); } } ``` 通过 dispatch 获取到方法处理器,然后调用方法处理器的 invoke 方法进行请求 ##### 3.1 SynchronousMethodHandler 的 invoke 方法 拿到 method 对应的方法处理器,调用 invoke 方法,SynchronousMethodHandler 的内部会调用 LoadBalanceFeignClient 的请求方法 - 创建 Ribbon 请求,拿到请求 Client 的配置,最主要的是拿到 ZoneAwareLoadBalancer 去创建 FeignLoadBalancer - 拿到 FeignLoadBalancer 并缓存到 cache 中,然后进行真正的请求调用 - 真正的请求调用是有 LoadBalancerCommand 来执行的,LoadBalancerCommand 进行负载均衡选择到服务器,然后进行请求调用并返回 #### 4. Feign 的重试机制 feign 的重试分两种,一种是 feign 集成的 ribbon 进行重试,另一种是 feign 自身的重试 ##### 4.1 Ribbon 重试 消费者通过 feign 集成的 ribbon 进行服务调用时,会通过配置文件中的 ribbon 重试参数进行失败重试。 如通过下面的参数进行重试 MaxAutoRetries: 1,服务调用发生异常,再对当前的服务提供者进行一次请求,一共是进行两次服务调用 MaxAutoRetriesNextServer: 3,最大重试的服务提供者次数,一共是进行三次服务提供者的切换 OkToRetryOnAllOperations: true,会触发重试的异常类型 上面三个参数组合,如果一直发生异常,一共会进行 8 次服务调用 ##### 4.2 Feign 重试 feign 的重试是基于 SynchronousMethodHandler 的 invoke 方法进行的,此功能默认在 SpringCloud 中是开启的,不过 SpringCloud 自带的重试器是空的,所以并不会进行重试。如果想使用这个功能的话,需要用户配置重试器,一般会用 feign 自带的 Retryer.Default 类就可以,Retryer.Default 默认情况下发生异常调用时会进行五次重试 ### Zuul #### 1. 过滤器类型和执行顺序 ##### 1.1 pre 过滤器 ServletDetectionFilter(-3), 判断是不是 dispatcherServletRequest, 如果是,打上标志 Servlet30WrapperFilter(-2), 包装请求做兼容 FromBodyWrapperFilter(-1) DebugFilter(1) PreDecorationFilter(5)