相信很多人都會感覺到,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在工程完全啓動後,首次使用被初始化,自定義的配置項就有效了。