Eureka服务发现慢有两个原因,一部分是因为服务端缓存导致,一部分是客户端缓存导致。
Eureka注册发现模型如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MZmth6ui-1622015677278)(http://106.75.145.234/upload/2021/05/image-4466447f9c174b1db882a89eef4a9d9e.png)]
1)服务端缓存
服务注册到注册中心后,服务实例是存储在注册表(数据存储层【registry】)中的,但是为了提高响应速度,内部加了两层缓存结构将client需要的实例缓存起来,获取的时候通过缓存响应给client。
- 第一层缓存readOnlyCacheMap
- readOnlyCacheMap是采用ConcurrentHashMap存储数据的,每30秒与readWriteCacheMap进行数据同步。
- 第二层缓存 readWriteCacheMap,
- 采用Guava来实现缓存。缓存过期时间默认为180秒,当服务下线、过期、注册、状态变更都会清楚缓存中的数据。
Client获取服务实例会先从一级缓存(readOnlyCacheMap)中获取,一级缓存不存在再从二级缓存(readWriteCacheMap)中获取,二级缓存不存在,会从存储层(Registry)中拉取数据存入缓存,再返回给Client。
Eureka二级缓存提高Eureka Server的响应速度,弊端是缓存会导致Client获取不到最新的服务实例信息,无法快速发现新服务和已下线服务。
服务端优化方案:
1、缩短只读缓存更新时间
eureka.server.response-cache-update-interval-ms= 10000 # 默认是30秒
2、关闭只读缓存(一级缓存)
eureka.server.use-read-only-response-cache=false
2)客户端缓存
Eureka Client 缓存
通过剖析源码了解到,EurekaClient负责与EurekaServer进行交互,在EurekaClient中的com.netflix.discovery.DiscoveryClient.initScheduledTasks() ⽅法中初始化了⼀
个 CacheRefreshThread 定时任务专⻔⽤来拉取 Eureka Server 的实例信息到本地。
@ImplementedBy(DiscoveryClient.class)
public interface EurekaClient extends LookupService {
...
}
public class DiscoveryClient implements EurekaClient {
...
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
Provider<BackupRegistry> backupRegistryProvider) {
//构造方法中
// 初始化调度任务(例如,群集解析器,心跳,实例信息复制器,提取
initScheduledTasks();
...
}
private void initScheduledTasks() {
if (clientConfig.shouldFetchRegistry()) {
// 对应eureka.client.registry-fetch-interval-seconds: 30 想注册表中获取实例信息的时间间隔
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread()
), registryFetchIntervalSeconds, TimeUnit.SECONDS);
}
...
}
}
Ribbon会从EurekaClient中获取服务信息,ServerListUpdater是Ribbon中负责服务实例更新的组件,默认实现是PollingServerListUpdater,当负载均衡器初始化的时候会调用PollingServerListUpdater的start(xxx)方法,方法内会获取PollingServerListUpdater的线程池,定期更新实例信息,默认30秒,通过下面命令调短时间,比如3秒。
#服务名.ribbon.ServerListRefreshInterval = xxx毫秒
user-service.ribbon.ServerListRefreshInterval=3000
# 更多参数设置可以通过类com.netflix.client.config.CommonClientConfigKey查看
Ribbon超时配置
# 全局ribbon超时配置
ribbon.ReadTimeout = 3000
ribbon.ConnectTimeout = 3000
# 单个服务超时配置
user-service.ribbon.ribbon.ReadTimeout = 3000
user-service.ribbon.ribbon.ConnectTimeout = 3000
Hystrix超时配置
Hystrix的超时配置要大于Ribbon的超市时间,因为Hystrix将请求进行了一层包装,特别注意,如果ribbon开启了重试机制,例如重试3次,Ribbon超时时间为1秒,则Hystrix超时时间必须大于3秒,否则会导致Ribbon还在重试,Hystrix就已经超时。
#全局配置
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000
#具体客户端配置
hystrix.command.user-service.execution.isolation.thread.timeoutInMilliseconds=3000
#只对某个方法进行配置
hystrix.command.user-service$getUser(Long).execution.isolation.thread.timeoutInMilliseconds=3000
@HystryxCommand注解指定超时时间
@HystrixCommand(
//线程池表示,要保持唯一,否则会共用同一个线程池
threadPoolKey = "getProviderPortTimeOutFallBack",
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "2"),//线程数
@HystrixProperty(name = "maxQueueSize", value = "20")//等待队列长度
},
commandProperties = {
//HystrixCommandProperties.HystrixCommandProperties(HystrixCommandKey, HystrixCommandProperties.Setter, String)
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
//hystric高级配置,定制工作过程细节,跳闸配置
/**
* 8秒内请求数达到2个,并且失败率达到50% hystrix跳闸
* 每隔3秒放一部分请求通过,如果成功重置断路器,如果失败,继续重试监听请求是否调用成功
*/
//统计时间窗口定义时长
, @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "8000")
//统计时间窗口内最小请求数
, @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "2")
//统计时间窗口内的错误数量百分比阈值
, @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
//自我修复时的活动窗口时长
, @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "3000")
}, fallbackMethod = "fallBack" //降级回调方法
)
Feign超时时间设置
Feign也可以配置超时时间,如果Ribbon也配了超时时间就以Ribbon的时间为准,如果没设置Ribbon时间设置了Feign时间,以Feign时间为准。
Feign的时间配置
feign.client.config.user-service.connectTimeout=1000
feign.client.config.user-service.readTimeout=1000