spring cloud 服務發現時效配置(服務發現慢優化)

相信很多人都會感覺到,springcloud服務發現很慢,特別是使用feign client作爲通訊工具的時候,明明服務已經啓動了,還要等30-90s左右才能被正常調用到。這個等待有點長!

這件事情也困擾了我很長時間,斷斷續續在網上搜索了不少資料,也沒能改到令自己滿意。

索性狠下心來花時間調試源碼,徹底搞明白爲什麼!

經過一天時間的研究,總算有所收穫,特地寫下來,以備將來需要!

環境說明

  • spring boot 2.1.1.RELEASE
  • spring cloud Greenwich.RC1
  • 服務註冊中心:eureka
  • 服務間通訊:feign client
  • 負載均衡:ribbon
  • 服務熔斷:hystrix

原因分析

假設有兩個服務(A,B),服務A調用服務B的過程大致是這樣的:

  1. A調用feign
  2. feign發現啓動了ribbon,於是從ribbon獲取服務地址
  3. ribbon從eureka client獲取所有服務地址
  4. eureka client 從 eureka server獲取服務地址
  5. A得到B實際地址,建立連接

慢的原因在於步驟(2、3、4)都有緩存。緩存都是通過內置定時任務刷新,詳細如下:

  1. ribbon 通過定時任務,定時從eureka client獲取指定服務對應的地址列表。默認時間30s
  2. eureka client 通過定時任務,定時從eureka server獲取服務列表。默認時間30s
  3. 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在工程完全啓動後,首次使用被初始化,自定義的配置項就有效了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章