写点什么

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

  • 2023-03-31
    湖南
  • 本文字数:17334 字

    阅读完需:约 57 分钟

一、前言

1.1 现象(问题)

微服务架构项目落地过程中,开发人员一般都遇到过调用超时问题,大部分时候会出现在 feign 接口调用上,这是微服务与单体服务最大的区别,单体从来不用考虑服务之间调用因为网络、序列化等因素导致的额外时间损耗问题。


很多开发人员在微服务开发中通常会随手设置一个较长超时,原则就是别在 feign 接口调用超时,这个随手的超时时间可能是 5 分钟、10 分钟,甚至 1 个小时不等,看似解决超时导致的问题,实际如果没有从整体微服务架构来考虑超时背后的因素,这样会导致给整个链路调用埋下隐患,可能会随机或者在高并发等情况下爆发。


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

  • 应用不稳定,时不时出现小问题,问题复现困难,用户感知差。

  • 前端请求卡住,也不知道是网络问题,还是应用问题或者是数据库问题,排查问题费时费力。

  • 数据库资源浪费,出现空算。

  • 应用空算,引发资源浪费或内存溢出。

  • 高并发下,TPS 和 QPS 同时出现异常下降。

1.2 原则(结论)

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

  • 不应超过客户等待最大容忍度时间,这是一个弹性指标,通常在这个指标可以考虑在 5s 到 30s 之间;

  • 超时设置应大于 API 计算最大时间,如果和上一条冲突,API 计算应转入异步计算;

  • 微服务链路超时各环节应保持一致性,并且从前端到后端到数据库(定义为从左往右),越靠右超时设置应越短,链路超时应该是向右收敛。

  • 快速失败,节省核心资源,特别是数据库。

二、链路超时(细节)

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


假设网关超时或服务超时,数据库还依然在执行客户端提交的的慢查询,等结果计算出来后,中间链路已经超时,这个时候数据是无法响应的,相当于数据库的计算被浪费。而数据库又是极为珍贵的资源,这种调用一旦过多很快就会导致与数据相关的 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 组件并且采用了响应式设计,大部分时候,网关不是整个链路的瓶颈。


官网:cloud.spring.io/spring-clou…


  • 服务端超时

server:  netty:    connection-timeout: 60000
复制代码

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


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


stackoverflow.com/questions/5…

github.com/spring-proj…

github.com/spring-proj…

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.1 超时时间设置太大的潜在危害

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


原因:

  1. 应用的接入是有上限的,在 tomcat 下,默认就是 8192+100,如果高并发下发生慢调用,并且超时时间较长,无法快速失败或降级熔断等,那么所有请求都将等待。

  2. cpu 的核心数远远小于 api 的请求数,慢调用较多的时候,cpu 应该被拉满,无法及时对正常调用做出响应。

  3. 慢调用越多,无效的,被废弃的请求就越多(包括正常调用),挤压的请求无法正常响应后,请求失败就会产生雪崩现象。

3.2 优化手段

  • 快慢分离:分库时,不但要考虑按业务分库,还需要考虑按响应和计算分库,例如统计操作一般比较耗时,考虑专门建立聚合统计库,专门的服务来支撑统计功能或慢查询功能。

  • 修改默认:数据库相关超时的默认时间都比较长,这些地方是优先需要修改的,在暂时没办法分析流量、响应、计算等要素的情况下,前期是可以考虑将数据库超时设置为 30 秒到 120 秒不等,左侧链路依次放大,然后在应用使用过程中观察流量和响应的实际情况,阶段性调整参数。

  • 链路优化:找到最右侧(一般是数据库)节点,设定可接受最小超时,然后往左侧逐步放大。

  • 计算理论:正常情况下,行业应用,流量流入以及数据计算量、API 计算量是有理论最低和理论最高值的,可根据这些数据先预设超时限制。预先分析慢调用链路,在应用服务和数据库上区别设计,做到快慢分离。

  • 核心优先:整个微服务架构中,最核心的资源是数据库,它很难做到横向弹性,即使做到,也会有其他诸如分布式事务等因素拉低整体性能,所以,所有的设计都应优先保证数据库相关资源的高效合理利用。

  • 监控分析:数据库查询往往是整个微服务调用链路的性能瓶颈,在初期,性能监测阶段通过开启慢查询日志,开启性能分析 profiles 等手段定位性能问题和找出性能瓶颈,优化整体性能。

四、源码

4.1 SpringBoot 中 Tomcat 的连接超时源码

4.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 工厂类 TomcatWebServerFactoryCustomizer 是 WebServerFactoryCustomizer 接口的实现类。

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

4.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 协议创建的类中。

4.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; }
}}
复制代码
4.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 可以定位到具体内容。

4.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.
复制代码
4.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@Overrideprotected 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#createWebServerprivate 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@Overridepublic 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 中完成。

4.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。

4.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。

4.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。


作者:我是属车的

链接:https://juejin.cn/post/7215608036395483196

来源:稀土掘金

用户头像

还未添加个人签名 2021-07-28 加入

公众号:该用户快成仙了

评论

发布
暂无评论
微服务架构中的链路超时分析_Java_做梦都在改BUG_InfoQ写作社区