微服务架构中的链路超时分析

1、前言

1.1 现象(问题)

​ 微服务架构项目落地过程中,开发人员一般都遇到过调用超时问题,大部分时候会出现在feign接口调用上,这是微服务与单体服务最大的区别,单体从来不用考虑服务之间调用因为网络、序列化等因素导致的额外时间损耗问题。很多开发人员在微服务开发中通常会随手设置一个较长超时,原则就是别在feign接口调用超时,这个随手的超时时间可能是5分钟、10分钟,甚至1个小时不等,看似解决超时导致的问题,实际如果没有从整体微服务架构来考虑超时背后的因素,这样会导致给整个链路调用埋下隐患,可能会随机或者在高并发等情况下爆发。

​ 超时设置不正确会导致以下现象:

  • 应用不稳定,时不时出现小问题,问题复现困难,用户感知差。
  • 前端请求卡住,也不知道是网络问题,还是应用问题或者是数据库问题,排查问题费时费力。
  • 数据库资源浪费,出现空算。
  • 应用空算,引发资源浪费或内存溢出。
  • 高并发下,TPS和QPS同时出现异常下降。

1.2 原则(结论)

微服务架构(基于Spring Cloud)中,在行业应用中,超时设置都应满足以下条件:

  • 不应超过客户等待最大容忍度时间,这是一个弹性指标,通常在这个指标可以考虑在5s到30s之间;
  • 超时设置应大于API计算最大时间,如果和上一条冲突,API计算应转入异步计算;
  • 微服务链路超时各环节应保持一致性,并且从前端到后端到数据库(定义为从做左往右),越靠右超时设置应越短,链路超时应该是向右收敛。
  • 快速失败,节省核心资源,特别是数据库。

2、链路超时(细节)

  • 链路超时应满足向右收敛原则

    假设网关超时或服务超时,数据库还依然在执行客户端提交的的慢查询,等结果计算出来后,中间链路已经超时,这个时候数据是无法响应的,相当于数据库的计算被浪费。而数据库又是极为珍贵的资源,这种调用一旦过多很快就会导致与数据相关的API出现响应故障。

    默认情况下,整个微服务的调用链路是不符合这个要求的,所以一旦发生慢调用,很多时候会产生无效的计算,浪费资源或者直接影响服务的使用。

  • 快速失败,满足向右收敛原则后,发生慢调用的时候,靠右侧的核心资源会先超时,通过调用链传递,快速失败响应,这个时候服务无论是进行降级还是熔断都可以快速降低系统的压力,并且还能及时向开发团队或者运维团队反馈问题。

    通常情况下,资源越靠右侧,说明资源越珍贵,调用的代价也越大。

  • 缺省值,如果使用组件缺省值,一定要显式的设置参数。

    注意:不同版本,默认值可能是不同的。

  • 通常对链路入口(网关),服务入口(web中间件),服务调用(Feign),数据库调用(sql)等环节调整超时参数,遵守向右收敛原则。

2.1 网关关键配置

  • 全局超时配置
spring:
  cloud:
    gateway:     
      httpclient:
        connect-timeout: 45000 #毫秒
        response-timeout: 10000
        pool:
          type: elastic
  • 单路由配置
      - id: per_route_timeouts
        uri: https://example.org
        predicates:
          - name: Path
            args:
              pattern: /delay/{timeout}
        metadata:
          response-timeout: 200
          connect-timeout: 200

connect-timeout是指网关到目标路由的连接超时时间(缺省45秒)。

response-timeout是指服务给网关返回响应的时间(默认应该是无限时间,暂时没分析源码)。

网关默认使用弹性连接连接池,默认的连接数是Integer.MAX_VALUE

网关使用netty组件并且采用了响应式设计,大部分时候,网关不是整个链路的瓶颈。

官网:https://cloud.spring.io/spring-cloud-gateway/reference/html/#http-timeouts-configuration

  • 服务端超时
server:
  netty:
    connection-timeout: 60000

这个参数是外部连接与gateway建立连接的超时时间(应该是指tcp连接三次握手超时时间),目前该参数有争议。

在某些版本应该是固定是10s,配置参数无效。

https://stackoverflow.com/questions/53587611/how-to-configure-netty-connection-timeout-for-spring-webflux

https://github.com/spring-projects/spring-boot/issues/15368

https://github.com/spring-projects/spring-boot/issues/18473

2.2 Tomcat关键配置

  • 以内嵌Tomcat为例:
server:
  tomcat:   
    accept-count: 100
    threads:
      max: 200
    max-connections: 8192
    connection-timeout: 60000
    keep-alive-timeout: 60000
    max-keep-alive-requests: 100

threads.max:表示服务器最大有多少个线程处理请求,默认200,实际上这个参数超过大多数服务器核心数,实际会降低服务器cpu处理速度,所以在业务处理中,应让tomcat应该让业务快速响应。

max-connections:表示服务器与客户端可以建立多少个连接数,即持有的连接数。tomcat缺省是8192个连接数,cpu未必有时间给你处理,但是可以保持连接。这个参数是客户感知型参数。

accept-count: 与服务器内核相关,是客户端传入给服务器内核,请求的backlog值,该值与服务器内核参数net.core.somaxconn取小后的值为最终起效的TCP内核全队列值。它表示在max-connections值达到预设的值后,服务器内核还能建立的连接数,这个连接保存在内核,还未被上层应用(tomcat)取走。该值在tomcat中默认是100,在Centos7.x版本中内核net.core.somaxconn是128。如果超过max-connections和accept-count总和,新的连接会被拒绝,即直接拒绝服务(直接返回connection refused)。

connection-timeout: 连接超时,URI所请求的内容被呈现出来前的超时时间。在SpringBoot2.x中缺省是60秒,注意:如果是使用标准server.xml的tomcat,缺省是20秒,不同版本的SpringBoot,其内嵌tomcat的连接超时可能不同,所以,建议直接指定该值。

The number of milliseconds this Connector will wait, after accepting a connection, for the request URI line to be presented. Use a value of -1 to indicate no (i.e. infinite) timeout. The default value is 60000 (i.e. 60 seconds) but note that the standard server.xml that ships with Tomcat sets this to 20000 (i.e. 20 seconds). Unless disableUploadTimeout is set to false, this timeout will also be used when reading the request body (if any).

keep-alive-timeout: keepalive的超时时间,缺省与connection-timeout相同。

max-keep-alive-requests: 最大的保持keepalive的请求数量。

缺省情况下:tomcat可以保持8192个socket连接,系统内核帮忙保持100个连接。直至connection-timeout的时间。

同一个连接在保活期间可以多次请求和响应。

2.3 feign接口配置

feign接口配置影响的是链路中服务之间的调用。

  • feign全局服务超时

default配置项影响全局配置(是否只是影响缺省客户端待查)。在使用第三方客户端的时候,应是以第三个客户端为基准,例如httpclient或okhttp。

feign:  
  client:
    config:
      # 全局配置
      default:
        loggerLevel: basic # NONE(默认)、BASIC、HEADERS、FULL
        connectTimeout: 30000 #毫秒
        readTimeout: 30000 #毫秒
  # 开启httpClient客户端作为http连接池
  httpclient:
    enabled: true
    max-connections: 200
    max-connections-per-route: 50 # feign单个路径的最大连接数
    connection-timeout: 30000
    connection-timer-repeat: 3000
  • feign独立服务超时
feign:
  client:
    config:
      # 设置FooClient的超时时间
      FooClient:
        connectTimeout: 5000
        readTimeout: 3000
  • 单独给某接口设置超时时间

在feign接口里加入这个参数就可以单独为接口单独设置超时时间了

@FeignClient(name = "wood-system",contextId = "wood-system-holiday-feign")
public interface HolidayFeign {
    @GetMapping("/api/holiday/{id}")
    Result<SysHoliday> selectOne(Request.Options options,@PathVariable Long id);

    @PostMapping("/api/holiday/page/{pageNum}/{pageSize}")
    Result<Object> queryAllByPage(Request.Options options, @RequestBody SysHoliday holiday, @PathVariable int pageNum, @PathVariable int pageSize);
	
    ... ...	
}

调用的时候new 一下Options对象

   @Resource
    private HolidayFeign holidayFeign;

	@GetMapping("{id}")
    public Result<SysHoliday> selectOne(@PathVariable Long id) {
        Request.Options options = new Request.Options(10, TimeUnit.SECONDS, 10, TimeUnit.SECONDS, true);
        return holidayFeign.selectOne(options, id);
    }

    @PostMapping("/page/{pageNum}/{pageSize}")
    public Result<Object> queryAllByPage(@RequestBody SysHoliday holiday, @PathVariable int pageNum, @PathVariable int pageSize) {
        Request.Options options = new Request.Options(1, TimeUnit.SECONDS, 1, TimeUnit.SECONDS, true);
        Result<Object> result = holidayFeign.queryAllByPage(options, holiday, pageNum, pageSize);
        return result;
    }
  • 关于ribbon的超时

因为 Feign 是基于 ribbon 来实现的,所以通过 ribbon 的超时时间设置也能达到目的。类似配置:

 ribbon:  
    ReadTimeout: 5000 #单位毫秒
    ConnectTimeout: 2000 #单位毫秒

实际上,在使用OpenFeign之后,ribbon已经无法直接配置超时,通常就是使用Feign来配置超时。

注意:ribbon 的默认 ConnectTimeout 和 ReadTimeout 都是 1000 ms。这里有两处默认值,见源码。

注意:@FeignClient 注解的 url 参数进行服务调用时是不走ribbon的。

2.4 数据库配置

  • 数据库连接池
  datasource:
    druid:    
      # 连接池属性
      initial-size: 15
      max-active: 100
      min-idle: 15
      # 配置从连接池获取连接等待超时的时间
      max-wait: 30000
      login-timeout: 30000

重点是关注max-wait参数:从连接池获取连接等待的时间,其他诸如连接时间、登录时间的超时对于一个正常的连接池反而不是重点,sql执行的时间不建议在连接池上设置超时,因为sql超时后的终止行为需要数据库引擎来执行,应该在数据库层面上设置时间。

  • 数据库引擎,sql查询执行超时设置
-- 默认是0,即无限
select @@max_execution_time;
show variables like 'max_execution_time';

-- 全局设置
SET GLOBAL MAX_EXECUTION_TIME=1000;
-- 对某个session设置
SET SESSION MAX_EXECUTION_TIME=1000;

-- 单独设定sql设置超时时间
SELECT /*+ MAX_EXECUTION_TIME(1000) */ sleep(3), a.* from project_info a;

sql执行超时抛出错误:Query execution was interrupted, maximum statement execution time exceeded。

  • 数据库事务超时
# 默认是50秒
select @@innodb_lock_wait_timeout;
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';

set innodb_lock_wait_timeout=30;
set global innodb_lock_wait_timeout=30;

注意global的修改对当前线程是不生效的,只有建立新的连接才生效

3、建议

3.1 超时时间设置太大的潜在危害

​ 可能会有人认为简单的把链路延迟都放大,比如5分钟,这样避免了链路超时的问题。在流量比较小的应用中,不会产生太大影响,在流量较大的微服务架构中,链路设定高延时的同时,如果遇到应用计算或数据库计算发生慢调用,会瞬间拉低TPS,甚至会造成QPS和TPS都为0的恶劣情况。这也是在压测中,某些慢接口会导致整个应用吞吐量急剧下降的原因。

原因:

  1. 应用的接入是有上限的,在tomcat下,默认就是8192+100,如果高并发下发生慢调用,并且超时时间较长,无法快速失败或降级熔断等,那么所有请求都将等待。
  2. cpu的核心数远远小于api的请求数,慢调用较多的时候,cpu应该被拉满,无法及时对正常调用做出响应。
  3. 慢调用越多,无效的,被废弃的请求就越多(包括正常调用),挤压的请求无法正常响应后,请求失败就会产生雪崩现象。

3.2 优化手段

  • 快慢分离:分库时,不但要考虑按业务分库,还需要考虑按响应和计算分库,例如统计操作一般比较耗时,考虑专门建立聚合统计库,专门的服务来支撑统计功能或慢查询功能。
  • 修改默认:数据库相关超时的默认时间都比较长,这些地方是优先需要修改的,在暂时没办法分析流量、响应、计算等要素的情况下,前期是可以考虑将数据库超时设置为30秒到120秒不等,左侧链路依次放大,然后在应用使用过程中观察流量和响应的实际情况,阶段性调整参数。
  • 链路优化:找到最右侧(一般是数据库)节点,设定可接受最小超时,然后往左侧逐步放大。
  • 计算理论:正常情况下,行业应用,流量流入以及数据计算量、API计算量是有理论最低和理论最高值的,可根据这些数据先预设超时限制。预先分析慢调用链路,在应用服务和数据库上区别设计,做到快慢分离。
  • 核心优先:整个微服务架构中,最核心的资源是数据库,它很难做到横向弹性,即使做到,也会有其他诸如分布式事务等因素拉低整体性能,所以,所有的设计都应优先保证数据库相关资源的高效合理利用。
  • 监控分析:数据库查询往往是整个微服务调用链路的性能瓶颈,在初期,性能监测阶段通过开启慢查询日志,开启性能分析profiles等手段定位性能问题和找出性能瓶颈,优化整体性能。

9、源码

9.1 SpringBoot中Tomcat的连接超时源码

9.1.1 web服务器工厂定制类自动配置EmbeddedWebServerFactoryCustomizerAutoConfiguration

注入TomcatWebServerFactoryCustomizer,该类用于定制具体的web server工厂。

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication
@EnableConfigurationProperties(ServerProperties.class)
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {
   	/**
	 * Nested configuration if Tomcat is being used.
	 */
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
	public static class TomcatWebServerFactoryCustomizerConfiguration {

		@Bean
		public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
				ServerProperties serverProperties) {
			return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
		}
	}
    
    ... ...
}

在自动配置类中创建tomcat自定义工厂配置类,这个类的目的就是通过tomcat工厂类ConfigurableTomcatWebServerFactory对tomcat的参数进行最后的设置或覆盖,它是通过后置处理器完成调用的。

tomcat工厂类TomcatWebServerFactoryCustomizerWebServerFactoryCustomizer接口的实现类。

以SpringBoot的回调机制,肯定是对WebServerFactoryCustomizer接口进行统一处理,通过查找WebServerFactoryCustomizer的接口调用或者查找customize()的调用都可以追溯到WebServerFactoryCustomizerBeanPostProcessor

9.1.2 tomcat工厂定制类TomcatWebServerFactoryCustomizer

用来定制tomcat工厂类,即对SpringBoot注入的TomcatServletWebServerFactory进行配置。

public class TomcatWebServerFactoryCustomizer
		implements WebServerFactoryCustomizer<ConfigurableTomcatWebServerFactory>, Ordered {

	private final Environment environment;

	private final ServerProperties serverProperties;

	public TomcatWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
		this.environment = environment;
		this.serverProperties = serverProperties;
	}

	@Override
	public void customize(ConfigurableTomcatWebServerFactory factory) {
		ServerProperties properties = this.serverProperties;
		
        ... ...
		
		propertyMapper.from(tomcatProperties::getConnectionTimeout).whenNonNull()
				.to((connectionTimeout) -> customizeConnectionTimeout(factory, connectionTimeout));
		propertyMapper.from(tomcatProperties::getMaxConnections).when(this::isPositive)
				.to((maxConnections) -> customizeMaxConnections(factory, maxConnections));
		propertyMapper.from(tomcatProperties::getAcceptCount).when(this::isPositive)
				.to((acceptCount) -> customizeAcceptCount(factory, acceptCount));
		... ...
		customizeStaticResources(factory);
		customizeErrorReportValve(properties.getError(), factory);
	}


	private void customizeConnectionTimeout(ConfigurableTomcatWebServerFactory factory, Duration connectionTimeout) {
		factory.addConnectorCustomizers((connector) -> {
			ProtocolHandler handler = connector.getProtocolHandler();
			if (handler instanceof AbstractProtocol) {
				AbstractProtocol<?> protocol = (AbstractProtocol<?>) handler;
				protocol.setConnectionTimeout((int) connectionTimeout.toMillis());
			}
		});
	}
}

在定制工厂类TomcatWebServerFactoryCustomizer中,获取ServerProperties属性,重新设置所有可配置项,超时时间的缺省值就是在这里被间接覆盖的。通过customizeConnectionTimeout函数,给TomcatServletWebServerFactory添加TomcatConnectorCustomizer定制连接器参数,在后续的使用TomcatServletWebServerFactory创建tomcat中会调用TomcatConnectorCustomizer来定制参数。同时,在customizeConnectionTimeout可以发现超时时间是在通讯协议里设置的,这点很重要,意味着,我们在跟踪源码时,需要跟踪到具体的HTTP协议创建的类中。

9.1.3 web服务器工厂配置类ServletWebServerFactoryConfiguration

用来注入工厂类TomcatServletWebServerFactory

@Configuration(proxyBeanMethods = false)
class ServletWebServerFactoryConfiguration {

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
	@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
	static class EmbeddedTomcat {

		@Bean
		TomcatServletWebServerFactory tomcatServletWebServerFactory(
				ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
				ObjectProvider<TomcatContextCustomizer> contextCustomizers,
				ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
			TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
			factory.getTomcatConnectorCustomizers()
					.addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
			factory.getTomcatContextCustomizers()
					.addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
			factory.getTomcatProtocolHandlerCustomizers()
					.addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
			return factory;
		}

	}
}    
9.1.4 tomcat工厂类TomcatServletWebServerFactory

真正用来创建tomcat的类。

public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory
		implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware {

	private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
    public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol";
    private String protocol = DEFAULT_PROTOCOL;
    ... ...

	@Override
	public WebServer getWebServer(ServletContextInitializer... initializers) {
		if (this.disableMBeanRegistry) {
			Registry.disableRegistry();
		}
		Tomcat tomcat = new Tomcat();
		File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
		tomcat.setBaseDir(baseDir.getAbsolutePath());
		for (LifecycleListener listener : this.serverLifecycleListeners) {
			tomcat.getServer().addLifecycleListener(listener);
		}
		Connector connector = new Connector(this.protocol);
		connector.setThrowOnFailure(true);
		tomcat.getService().addConnector(connector);
		customizeConnector(connector);
		tomcat.setConnector(connector);
		tomcat.getHost().setAutoDeploy(false);
		configureEngine(tomcat.getEngine());
		for (Connector additionalConnector : this.additionalTomcatConnectors) {
			tomcat.getService().addConnector(additionalConnector);
		}
		prepareContext(tomcat.getHost(), initializers);
		return getTomcatWebServer(tomcat);
	}        
    
}    

TomcatWebServerFactoryCustomizer中我们了解到连接超时的参数在通讯协议里,在上述源码里Connector接收协议名称,所以跟踪Connector可以定位到具体内容。

9.1.5 tomcat的连接超时
  • Connector
    /**
     * Defaults to using HTTP/1.1 NIO implementation.
     */
    public Connector() {
        this("HTTP/1.1");
    }

	public Connector(String protocol) {
        boolean apr = AprStatus.getUseAprConnector() && AprStatus.isInstanceCreated()
                && AprLifecycleListener.isAprAvailable();
        ProtocolHandler p = null;
        try {
            p = ProtocolHandler.create(protocol, apr);
        } catch (Exception e) {
            log.error(sm.getString(
                    "coyoteConnector.protocolHandlerInstantiationFailed"), e);
        }
        if (p != null) {
            protocolHandler = p;
            protocolHandlerClassName = protocolHandler.getClass().getName();
        } else {
            protocolHandler = null;
            protocolHandlerClassName = protocol;
        }
        // Default for Connector depends on this system property
        setThrowOnFailure(Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"));
    }
  • ProtocolHandler
    public static ProtocolHandler create(String protocol, boolean apr)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException,
            IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
        if (protocol == null || "HTTP/1.1".equals(protocol)
                || (!apr && org.apache.coyote.http11.Http11NioProtocol.class.getName().equals(protocol))
                || (apr && org.apache.coyote.http11.Http11AprProtocol.class.getName().equals(protocol))) {
            if (apr) {
                return new org.apache.coyote.http11.Http11AprProtocol();
            } else {
                return new org.apache.coyote.http11.Http11NioProtocol();
            }
        } else if ("AJP/1.3".equals(protocol)
                || (!apr && org.apache.coyote.ajp.AjpNioProtocol.class.getName().equals(protocol))
                || (apr && org.apache.coyote.ajp.AjpAprProtocol.class.getName().equals(protocol))) {
            if (apr) {
                return new org.apache.coyote.ajp.AjpAprProtocol();
            } else {
                return new org.apache.coyote.ajp.AjpNioProtocol();
            }
        } else {
            // Instantiate protocol handler
            Class<?> clazz = Class.forName(protocol);
            return (ProtocolHandler) clazz.getConstructor().newInstance();
        }
    }
  • Http11NioProtocol
public class Http11NioProtocol extends AbstractHttp11JsseProtocol<NioChannel> {

    private static final Log log = LogFactory.getLog(Http11NioProtocol.class);


    public Http11NioProtocol() {
        super(new NioEndpoint());
    }    
}    


public abstract class AbstractHttp11JsseProtocol<S>
        extends AbstractHttp11Protocol<S> {

    public AbstractHttp11JsseProtocol(AbstractJsseEndpoint<S,?> endpoint) {
        super(endpoint);
    }
    ... ...
}

public abstract class AbstractHttp11Protocol<S> extends AbstractProtocol<S> {

	... ...

    public AbstractHttp11Protocol(AbstractEndpoint<S,?> endpoint) {
        super(endpoint);
        setConnectionTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT);
        ConnectionHandler<S> cHandler = new ConnectionHandler<>(this);
        setHandler(cHandler);
        getEndpoint().setHandler(cHandler);
    }
    
    public void setConnectionTimeout(int timeout) {
        endpoint.setConnectionTimeout(timeout);
    }
}    

public final class Constants {

    public static final int DEFAULT_CONNECTION_TIMEOUT = 60000;
    ... ...
}    
  • AbstractEndpoint
public abstract class AbstractEndpoint<S,U> {

   ... ... 

    public static long toTimeout(long timeout) {
        // Many calls can't do infinite timeout so use Long.MAX_VALUE if timeout is <= 0
        return (timeout > 0) ? timeout : Long.MAX_VALUE;
    }
    
    /**
     * Socket timeout.
     *
     * @return The current socket timeout for sockets created by this endpoint
     */
    public int getConnectionTimeout() { return socketProperties.getSoTimeout(); }
    public void setConnectionTimeout(int soTimeout) { socketProperties.setSoTimeout(soTimeout); }    
}

/**
*Properties that can be set in the <Connector> element in server.xml. 
*All properties are prefixed with "socket." and are currently only working for the Nio connector
*/
public class SocketProperties {
    ...
     /**
     * SO_TIMEOUT option. default is 20000.
     */
    protected Integer soTimeout = Integer.valueOf(20000);
}

最终的超时时间SO_TIMEOUT,体现在socket的read()上,并且在socket层面上,缺省超时是20秒,这个值会被tomcat创建Connector类实例化时的60秒常量参数覆盖。

  • SO_TIMEOUT
Enable/disable SO_TIMEOUT with the specified timeout, in milliseconds. With this option set to a non-zero timeout, a read() call on the InputStream associated with this Socket will block for only this amount of time. If the timeout expires, a java.net.SocketTimeoutException is raised, though the Socket is still valid. The option must be enabled prior to entering the blocking operation to have effect. The timeout must be > 0. A timeout of zero is interpreted as an infinite timeout.
9.1.6 SpringBoot创建tomcat服务
  • 入口refresh()
//org.springframework.context.support.AbstractApplicationContext#refresh
@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			......
			try {
				....       
				// Initialize other special beans in specific context subclasses.                    
				onRefresh();
                ....
				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);                    
			}
		}
	}

//org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh
@Override
protected void onRefresh() {
    super.onRefresh();
    try {
        createWebServer();
    }
    catch (Throwable ex) {
        throw new ApplicationContextException("Unable to start web server", ex);
   
    }
}

//org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#createWebServer
private void createWebServer() {
    WebServer webServer = this.webServer;
    ServletContext servletContext = getServletContext();
    if (webServer == null && servletContext == null) {
        ServletWebServerFactory factory = getWebServerFactory();
        this.webServer = factory.getWebServer(getSelfInitializer());
    }
    else if (servletContext != null) {
        try {
            getSelfInitializer().onStartup(servletContext);
        }
        catch (ServletException ex) {
            throw new ApplicationContextException("Cannot initialize servlet context", ex);
        }
    }
    initPropertySources();
}

//org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#getWebServer
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
    if (this.disableMBeanRegistry) {
        Registry.disableRegistry();
    }
    Tomcat tomcat = new Tomcat();
    File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
    tomcat.setBaseDir(baseDir.getAbsolutePath());
    //初始化
    Connector connector = new Connector(this.protocol);
    connector.setThrowOnFailure(true);
    tomcat.getService().addConnector(connector);
    customizeConnector(connector);
    tomcat.setConnector(connector);
    tomcat.getHost().setAutoDeploy(false);
    configureEngine(tomcat.getEngine());
    for (Connector additionalConnector : this.additionalTomcatConnectors) {
        tomcat.getService().addConnector(additionalConnector);
    }
    prepareContext(tomcat.getHost(), initializers);
    return getTomcatWebServer(tomcat);
}

onRefresh()完成tomcat服务器创建,并赋予默认参数。

  • 创建bean以及后置处理finishBeanFactoryInitialization(beanFactory)
	/**
	 * Finish the initialization of this context's bean factory,
	 * initializing all remaining singleton beans.
	 */
	protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
		
        ... ...

		// Instantiate all remaining (non-lazy-init) singletons.
		beanFactory.preInstantiateSingletons();
	}



	public <T> T getBean(String name, @Nullable Class<T> requiredType, @Nullable Object... args)
			throws BeansException {

		return doGetBean(name, requiredType, args, false);
	}


	protected <T> T doGetBean(
			String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
			throws BeansException {

			... ...
			return createBean(beanName, mbd, args);
         	... ...
	}


	/**
	 * Central method of this class: creates a bean instance,
	 * populates the bean instance, applies post-processors, etc.
	 * @see #doCreateBean
	 */
	@Override
	protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
			throws BeanCreationException {
        
        
			Object beanInstance = doCreateBean(beanName, mbdToUse, args);
			...
			return beanInstance;

			... ...
	}

	/**
	 * Actually create the specified bean. Pre-creation processing has already happened
	 * at this point, e.g. checking {@code postProcessBeforeInstantiation} callbacks.
	 * <p>Differentiates between default bean instantiation, use of a
	 * factory method, and autowiring a constructor.
	 * @param beanName the name of the bean
	 * @param mbd the merged bean definition for the bean
	 * @param args explicit arguments to use for constructor or factory method invocation
	 * @return a new instance of the bean
	 * @throws BeanCreationException if the bean could not be created
	 * @see #instantiateBean
	 * @see #instantiateUsingFactoryMethod
	 * @see #autowireConstructor
	 */
	protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
			throws BeanCreationException {

		// Instantiate the bean.
	    ... ...
		Object bean = instanceWrapper.getWrappedInstance();
		... ...

		// Initialize the bean instance.
		Object exposedObject = bean;
        // Populate the bean instance in the given BeanWrapper with the property values from the bean definition.
		populateBean(beanName, mbd, instanceWrapper);
        
		exposedObject = initializeBean(beanName, exposedObject, mbd);
		... ...
	}


	/**
	 * Initialize the given bean instance, applying factory callbacks
	 * as well as init methods and bean post processors.
	 * <p>Called from {@link #createBean} for traditionally defined beans,
	 * and from {@link #initializeBean} for existing bean instances.
	 * @param beanName the bean name in the factory (for debugging purposes)
	 * @param bean the new bean instance we may need to initialize
	 * @param mbd the bean definition that the bean was created with
	 * (can also be {@code null}, if given an existing bean instance)
	 * @return the initialized bean instance (potentially wrapped)
	 * @see BeanNameAware
	 * @see BeanClassLoaderAware
	 * @see BeanFactoryAware
	 * @see #applyBeanPostProcessorsBeforeInitialization
	 * @see #invokeInitMethods
	 * @see #applyBeanPostProcessorsAfterInitialization
	 */
	protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
		if (System.getSecurityManager() != null) {
			AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
				invokeAwareMethods(beanName, bean);
				return null;
			}, getAccessControlContext());
		}
		else {
			invokeAwareMethods(beanName, bean);
		}

		Object wrappedBean = bean;
		if (mbd == null || !mbd.isSynthetic()) {
			wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
		}

		try {
			invokeInitMethods(beanName, wrappedBean, mbd);
		}
		catch (Throwable ex) {
			throw new BeanCreationException(
					(mbd != null ? mbd.getResourceDescription() : null),
					beanName, "Invocation of init method failed", ex);
		}
		if (mbd == null || !mbd.isSynthetic()) {
			wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
		}

		return wrappedBean;
	}

真正的bean后置处理在initializeBean中完成。

9.2 Gateway连接数源码

  • 网关自动配置GatewayAutoConfiguration,连接相关主要是HttpClient,注意:它是基于netty的响应式客户端,不是apache的HttpClient。
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@EnableConfigurationProperties
@AutoConfigureBefore({ HttpHandlerAutoConfiguration.class,
		WebFluxAutoConfiguration.class })
@AutoConfigureAfter({ GatewayLoadBalancerClientAutoConfiguration.class,
		GatewayClassPathWarningAutoConfiguration.class })
@ConditionalOnClass(DispatcherHandler.class)
public class GatewayAutoConfiguration {
    ... ...
    
    @Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(HttpClient.class)
	protected static class NettyConfiguration {

		... ...

		@Bean
		@ConditionalOnMissingBean
		public HttpClient gatewayHttpClient(HttpClientProperties properties,
				List<HttpClientCustomizer> customizers) {

			// configure pool resources
			HttpClientProperties.Pool pool = properties.getPool();

			ConnectionProvider connectionProvider;
			if (pool.getType() == DISABLED) {
				connectionProvider = ConnectionProvider.newConnection();
			}
			else if (pool.getType() == FIXED) {
				connectionProvider = ConnectionProvider.fixed(pool.getName(),
						pool.getMaxConnections(), pool.getAcquireTimeout(),
						pool.getMaxIdleTime(), pool.getMaxLifeTime());
			}
			else {
				connectionProvider = ConnectionProvider.elastic(pool.getName(),
						pool.getMaxIdleTime(), pool.getMaxLifeTime());
			}

			HttpClient httpClient = HttpClient.create(connectionProvider)
                	...
					// TODO: move customizations to HttpClientCustomizers
					.tcpConfiguration(tcpClient -> {

						if (properties.getConnectTimeout() != null) {
							tcpClient = tcpClient.option(
									ChannelOption.CONNECT_TIMEOUT_MILLIS,
									properties.getConnectTimeout());
						}

					
			... ...

			return httpClient;
		}

		... ...

	}    
    
}
                                      
static ConnectionProvider elastic(String name, @Nullable Duration maxIdleTime, @Nullable Duration maxLifeTime) {
		return builder(name).maxConnections(Integer.MAX_VALUE)
		                    .pendingAcquireTimeout(Duration.ofMillis(0))
		                    .pendingAcquireMaxCount(-1)
		                    .maxIdleTime(maxIdleTime)
		                    .maxLifeTime(maxLifeTime)
		                    .build();
}                                      

在elastic类型下,连接数是Integer.MAX_VALUE

9.3 Gateway连接超时源码

  • 缺省值45秒,是硬代码
public abstract class TcpClient {
    ... ...
	/**
	 * Block the {@link TcpClient} and return a {@link Connection}. Disposing must be
	 * done by the user via {@link Connection#dispose()}. The max connection
	 * timeout is 45 seconds.
	 *
	 * @return a {@link Mono} of {@link Connection}
	 */
	public final Connection connectNow() {
		return connectNow(Duration.ofSeconds(45));
	}

	/**
	 * Block the {@link TcpClient} and return a {@link Connection}. Disposing must be
	 * done by the user via {@link Connection#dispose()}.
	 *
	 * @param timeout connect timeout
	 *
	 * @return a {@link Mono} of {@link Connection}
	 */
	public final Connection connectNow(Duration timeout) {
		Objects.requireNonNull(timeout, "timeout");
		try {
			return Objects.requireNonNull(connect().block(timeout), "aborted");
		}
		catch (IllegalStateException e) {
			if (e.getMessage().contains("blocking read")) {
				throw new IllegalStateException("TcpClient couldn't be started within "
						+ timeout.toMillis() + "ms");
			}
			throw e;
		}
	}    
}
  • 参数覆盖

GatewayAutoConfiguration

public class GatewayAutoConfiguration {
    ... ...
    
    @Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(HttpClient.class)
	protected static class NettyConfiguration {

		... ...

		@Bean
		@ConditionalOnMissingBean
		public HttpClient gatewayHttpClient(HttpClientProperties properties,
				List<HttpClientCustomizer> customizers) {


			HttpClient httpClient = HttpClient.create(connectionProvider)
                	...
					
					.tcpConfiguration(tcpClient -> {

						if (properties.getConnectTimeout() != null) {
							tcpClient = tcpClient.option(
									ChannelOption.CONNECT_TIMEOUT_MILLIS,
									properties.getConnectTimeout());
						}

					
			... ...

			return httpClient;
		}

		... ...

	}    
    
}
     

HttpClient是抽象类,其实现类HttpClientConnect聚合了TcpClient的实现类HttpTcpClient

9.4 ribbon超时源码

ribbon 的默认配置在 DefaultClientConfigImpl

    public static final int DEFAULT_READ_TIMEOUT = 5000;

    public static final int DEFAULT_CONNECTION_MANAGER_TIMEOUT = 2000;

    public static final int DEFAULT_CONNECT_TIMEOUT = 2000;

在使用 ribbon 请求接口时,第一次会构建一个 IClienConfig 对象,这个方法在 RibbonClientConfiguration 类中,此时,重新设置了 ConnectTimeout、ReadTimeout等。

public class RibbonClientConfiguration {

    /**
     * Ribbon client default connect timeout.
     */
    public static final int DEFAULT_CONNECT_TIMEOUT = 1000;

    /**
     * Ribbon client default read timeout.
     */
    public static final int DEFAULT_READ_TIMEOUT = 1000;

    /**
     * Ribbon client default Gzip Payload flag.
     */
    public static final boolean DEFAULT_GZIP_PAYLOAD = true;

    @RibbonClientName
    private String name = "client";

    @Autowired
    private PropertiesFactory propertiesFactory;

    @Bean
    @ConditionalOnMissingBean
    public IClientConfig ribbonClientConfig() {
        DefaultClientConfigImpl config = new DefaultClientConfigImpl();
        config.loadProperties(this.name);
        config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
        config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
        config.set(CommonClientConfigKey.GZipPayload, DEFAULT_GZIP_PAYLOAD);
        return config;
    }
    
    //...
}

ribbon 的默认 ConnectTimeout 和 ReadTimeout 都是 1000 ms。

posted @ 2023-03-27 16:34  我是属车的  阅读(644)  评论(0编辑  收藏  举报