相信很多人都会感觉到,springcloud服务发现很慢,特别是使用feign client作为通讯工具的时候,明明服务已经启动了,还要等30-90s左右才能被正常调用到。这个等待有点长!
这件事情也困扰了我很长时间,断断续续在网上搜索了不少资料,也没能改到令自己满意。
索性狠下心来花时间调试源码,彻底搞明白为什么!
经过一天时间的研究,总算有所收获,特地写下来,以备将来需要!
环境说明
- spring boot 2.1.1.RELEASE
- spring cloud Greenwich.RC1
- 服务注册中心:eureka
- 服务间通讯:feign client
- 负载均衡:ribbon
- 服务熔断:hystrix
原因分析
假设有两个服务(A,B),服务A调用服务B的过程大致是这样的:
- A调用feign
- feign发现启动了ribbon,于是从ribbon获取服务地址
- ribbon从eureka client获取所有服务地址
- eureka client 从 eureka server获取服务地址
- A得到B实际地址,建立连接
慢的原因在于步骤(2、3、4)都有缓存。缓存都是通过内置定时任务刷新,详细如下:
- ribbon 通过定时任务,定时从eureka client获取指定服务对应的地址列表。默认时间30s
- eureka client 通过定时任务,定时从eureka server获取服务列表。默认时间30s
- eureka server 通过定时任务,定时刷新本地服务列表缓存。默认时间30s
这3个30s加起来,最坏情况就是90s
源码配置说明
ribbon定时任务具体配置如下:
public class PollingServerListUpdater implements ServerListUpdater {
private static final Logger logger = LoggerFactory.getLogger(PollingServerListUpdater.class);
private static long LISTOFSERVERS_CACHE_UPDATE_DELAY = 1000; // msecs;
//这个是定时任务默认刷新时间,30s
private static int LISTOFSERVERS_CACHE_REPEAT_INTERVAL = 30 * 1000; // msecs;
//省略其他代码
}
eureka client具体代码如下:
@ImplementedBy(DefaultEurekaClientConfig.class)
public interface EurekaClientConfig {
/**
* Indicates how often(in seconds) to fetch the registry information from
* the eureka server.
*
* @return the fetch interval in seconds.
*/
int getRegistryFetchIntervalSeconds();
//省略其他代码
}
eureka server具体代码如下:
public class ResponseCacheImpl implements ResponseCache {
//...省略其他代码
ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
this.serverConfig = serverConfig;
this.serverCodecs = serverCodecs;
// 是否开启本地缓存
this.shouldUseReadOnlyResponseCache = serverConfig.shouldUseReadOnlyResponseCache();
this.registry = registry;
// 本地缓存刷新时间 默认30s
long responseCacheUpdateIntervalMs = serverConfig.getResponseCacheUpdateIntervalMs();
this.readWriteCacheMap =
CacheBuilder.newBuilder().initialCapacity(serverConfig.getInitialCapacityOfResponseCache())
.expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS)
.removalListener(new RemovalListener<Key, Value>() {
@Override
public void onRemoval(RemovalNotification<Key, Value> notification) {
Key removedKey = notification.getKey();
if (removedKey.hasRegions()) {
Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
}
}
})
.build(new CacheLoader<Key, Value>() {
@Override
public Value load(Key key) throws Exception {
if (key.hasRegions()) {
Key cloneWithNoRegions = key.cloneWithoutRegions();
regionSpecificKeys.put(cloneWithNoRegions, key);
}
Value value = generatePayload(key);
return value;
}
});
if (shouldUseReadOnlyResponseCache) {
timer.schedule(getCacheUpdateTask(),
new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
+ responseCacheUpdateIntervalMs),
responseCacheUpdateIntervalMs);
}
try {
Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register the JMX monitor for the InstanceRegistry", e);
}
}
//...省略其他代码
}
- ribbon 配置项可查看 DefaultClientConfigImpl
- eureka client 配置项可查看 EurekaClientConfigBean
- eureka server 配置项可查看 EurekaServerConfigBean
工程实际配置
在application.properties中添加相关配置
ribbon相关配置
# 设置连接超时时间,单位ms
ribbon.ConnectTimeout=5000
# 设置读取超时时间,单位ms
ribbon.ReadTimeout=5000
# 对所有操作请求都进行重试
ribbon.OkToRetryOnAllOperations=true
# 切换实例的重试次数
ribbon.MaxAutoRetriesNextServer=2
# 对当前实例的重试次数
ribbon.MaxAutoRetries=1
# 服务列表刷新频率 5s
ribbon.ServerListRefreshInterval=5000
ribbon.ConnIdleEvictTimeMilliSeconds=5000
ribbon.ConnIdleEvictTimeMilliSeconds=5000
eureka client相关配置
# eureka
eureka.client.instanceInfoReplicationIntervalSeconds:10
eureka.client.healthcheck.enabled=false
eureka.client.eureka-connection-idle-timeout-seconds=10
eureka.client.registry-fetch-interval-seconds=5
eureka.client.serviceUrl.defaultZone:http://localhost:8888/eureka/
eureka.instance.lease-renewal-interval-in-seconds=10
eureka.instance.lease-expiration-duration-in-seconds=10
eureka.instance.instance-id:${spring.cloud.client.ip-address}:${spring.application.name}:${spring.application.instance_id:${server.port}}
eureka.instance.prefer-ip-address: true
eureka.instance.hostname= ${spring.cloud.client.ip-address}
# 是否在注册中心注册
eureka.client.register-with-eureka:true
eureka server 相关配置
eureka:
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 4000
waitTimeInMsWhenSyncEmpty: 0
useReadOnlyResponseCache: false
上面配置中重点是这3个
- ribbon.ServerListRefreshInterval=5000;ribbon配置5s刷新一次服务列表
- eureka.client.registry-fetch-interval-seconds=5;eureka client配置5s从server同步一次服务列表
- eureka.server.useReadOnlyResponseCache=false; 关闭eureka server本地缓存
通过以上配置后,服务发现基本在10s以内,多数情况在5s左右,还算比较能接受。
注意事项
在研究配置过程中,发现一个巨坑,我在坑里折腾了好长时间才爬出来!
ribbon的配置是在首次使用的时候初始化的,同时初始化相关bean配置。
我的工程配置了shiro权限框架,在启动的时候从shiro相关服务读取角色、权限等数据;这时候也是用feign client建立连接获取数据的。
那么ribbon相关配置自然也就被初始化了。但是初始化早了,所有ribbon的自定义的配置全部没有被读取到,用的都是默认配置。
后来将shiro读取数据改成直连读取,不通过feign client就没问题了。
ribbon在工程完全启动后,首次使用被初始化,自定义的配置项就有效了。