相信很多人都会感觉到,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在工程完全启动后,首次使用被初始化,自定义的配置项就有效了。