Spring Cloud Netflix 常见问题及解决方案
Eureka 服务发现慢的原因
场景:
- 上线一个新的服务实例,但是服务消费者无感知,过了一段时间才知道
- 某一个服务实例下线了,服务消费之无感知,仍然向这个服务实例在发起请求
这其实就是服务发现的一个问题,当我们需要调用服务实例时,信息是从注册中心 Eureka 获取的,然后通过 Ribbon 选择一个服务实例发起调用,如果出现调用不到或者下线后还可以调用的问题,原因肯定是服务实例的信息更新不及时导致的。
Eureka 服务发现慢的原因有两个,一部分是因为注册中心服务缓存导致的 ,另一部分是因为客户端缓存导致的。
Eureka 服务端缓存
服务注册到注册中心后,服务实例信息是存储在注册表中的,也就是内存中。是一个 ConcurrentHashMap
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
但 Eureka 为了提高响应速度,在内部做了优化,将 Client 需要的实例信息,直接缓存起来,获取的时候直接从缓存中拿数据然后响应给 Client。
三级缓存
缓存 | 类型 | 说明 |
---|---|---|
registry | ConcurrentHashMap | 实时更新,类AbstractInstanceRegistry成员变量,UI端请求的是这里的服务注册信息 |
readWriteCacheMap | Guava Cache/LoadingCache | 实时更新,类ResponseCacheImpl成员变量,缓存时间180秒 |
readOnlyCacheMap | ConcurrentHashMap | 周期更新,类ResponseCacheImpl成员变量,默认每30s从readWriteCacheMap更新,Eureka client默认从这里更新服务注册信息,可配置直接从readWriteCacheMap更新 |
第一层缓存是 readOnlyCacheMap,readOnlyCacheMap 是采用 ConcurrentHashMap
来存储数据的,主要负责定时与 readWriteCacheMap进行数据同步,默认同步时间为 30 秒一次。
第二层缓存是 readWriteCacheMap,readOnlyCacheMap 主要用的是 Guava 的 LoadingCache 。缓存过期时间默认为 180 秒,当服务下线、过期、注册、状态变更等操作都会清除此缓存中的数据。
Client 获取服务实例数据时,会先从一级缓存中获取,如果一级缓存中不存在,再从二级缓存中获取,如果二级缓存也不存在,会触发缓存的加载,从 registry 拉取数据到缓存中,然后再返回给 Client。
Eureka 之所以设计二级缓存机制,也是为了提高 Eureka Server 的响应速度,缺点是会导致 Client 获取不到最新的服务实例信息,然后导致无法快速发现新的服务和已下线的服务,多级缓存也是导致数据一致性(C)很薄弱。
了解服务端的实现后,想要解决这个问题就变得很简单了,我们可以缩短 readOnlyCacheMap 的更新时间 eureka.server.response-cache-update-interval-ms
来让服务发现变得更加及时,或者直接将 readOnlyCacheMap 关闭 eureka.server.use-read-only-response-cache=false
。
Eureka Server 中会有定时任务去检测失效的服务,将服务实例信息从注册表中移除,也可以将这个时间缩短,eviction-interval-timer-in-ms
续期时间,即扫描失效服务的间隔时间(缺省为60*1000ms),这样服务下线后就能够及时从注册表中清除。
Eureka Client 缓存
Eureka Client 缓存
Eureka Client 负责跟 Eureka Server 进行交互,在 Eureka Client 中的 com.netflix.discovery.DiscoveryClient#initScheduledTasks
方法中,初始化了一个 CacheRefreshThread
定时任务专门用来拉取 Eureka Server 的实例信息到本地。
所以我们可以缩短这个定时拉取服务的时间间隔 eureka.client.lease-renewal-interval-in-seconds
来快速发现新的服务。
Ribbon 缓存
Ribbon 会从 Eureka Client 中获取服务下线,ServerListUpdater
是 Ribbon 中负责服务实例更新的组件,默认的实现是 PollingServerListUpdater
,通过现场定时去更新实例信息。定时刷新的时间间隔默认为 30 秒,当服务停止或者上线后,还需要 30 秒才能将实例信息更新成最新的。我们可以将这个时间调小一点,ribbon.ServerListRefreshInterval=1000
除此之外,还能开启 Ribbon 的重试功能,当路由的服务出现问题时,可以重试到另一个服务来保证这次请求的成功。
SpringCloud 各组件的超时时间
在SpringCloud 中,应用的组件较多,只要涉及通信,就有可能发生请求超时。那么如何设置超时时间?在 SpringCloud Netflix 中,超时时间只需要重点关注 Ribbon 和 Hystrix 即可。
Ribbon
如果采用的是服务发现方式,就可以通过服务名去进行转发,需要配置 Ribbon 的超时时间。Ribbon 的超时可以配置全局的 ribbon.ReadTimeout
和 ribbon.ConnectTimeout
,也可以在前面指定服务名,为指定服务单独配置,如: user-service.ribbon.ReadTimeout和ribbon.ConnectTimeout
Hystrix
Hystrix 的超时时间要大于 Ribbon 的超时时间,因为Hystrix将请求包装了起来,特别需要注意的是,如果Ribbon开启了重试机制,比如重试 3 次,Ribbon 的超时为 1 秒,那么 Hystrix 的超时时间应该大于 3 秒,否则就会出现 Ribbon 还在重试中,而 Hystrix 已经超时的现象。
Hystrix 全局超时配置就可以用 default 来代替具体的 command 名称。 hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000
如果想对具体的 command 进行配置,那么就需要知道 command 名称的生成规则,才能准确的配置。
如果我们使用 @HystrixCommand 的话,可以自定义 commandKey。如果使用 FeignClient 配置的话,可以为FeignClient来指定超时时间:hystrix.command.UserRemoteClient.execution.isolation.thread.timeoutInMilliseconds = 3000
如果想对 FeignClient 中的某个接口设置单独的超时,可以在FeignClient名称后加上具体的方法: hystrix.command.UserRemoteClient#getUser(Long).execution.isolation.thread.timeoutInMilliseconds = 3000
Feign
Feign 本身也有超时时间的设置,如果此时设置了 Ribbon 的时间就以 Ribbon 的时间为准,如果没设置Ribbon的时间但配置了 Feign 的时间,就以 Feign 的时间为准。Feign 的时间同样也配置了连接超时时间feign.client.config.服务名称.connectTimeout
和读取超时时间 feign.client.config.服务名称.readTimeout
。
建议,我们配置 Ribbon 超时时间和Hystrix超时时间即可。