目录

问题描述

问题分析

问题解决


问题描述

经过长期观察发现,公司认证服务会间歇性的出现 502 http 状态码,导致上游报错。虽然用户可以通过重试来解决访问报错问题,但是还是需要排查一下为什么会间接性出现 502 http 状态码。

问题分析

502 HTTP 状态码是什么意思呢?

502 Bad Gateway 是一种 HTTP 协议的服务器端错误状态代码,它表示作为网关或代理角色的服务器,从上游服务器(如tomcat、php-fpm)中接收到的响应是无效的。

公司服务架构是:nginx + tomcat。结合上面对 502 http 状态码的描述,我们可以理解为用户访问网页,请求打到一台 nginx 服务器,nginx 服务器将请求转发到一台 tomcat 容器,tomcat 容器对 nginx 服务器的响应是无效的,nginx 服务器将 http 状态码标记为 502。

如何理解 tomcat 服务器对 nginx 服务器的响应是无效的?

nginx 服务器与 tomcat 应用服务器的交互有:建立连接、处理请求。所谓的无效是指在 nginx 服务器和 tomcat 应用服务器建立连接、 tomcat 应用服务器处理请求这条链路某一处出现了问题,导致 nginx 服务器认为响应是无效的,从而标记当前 http 请求的状态码为 502。

依据出现 502 状态码的时间点,我们排查到底是应用服务本身的问题还是建立连接问题。

如何排查不是应用服务本身的问题?

查看应用服务器 CPU、内存监控、JVM 监控。发现所有监控指标一切正常,初步排除是应用服务本身的问题。

如何排查是建立连接的问题?

经过和运维沟通,一致希望通过抓包来复现问题。通过抓包观察发现出现 502 的场景都是 nginx 复用连接发送请求的时候,tomcat 应用服务器主动 FIN 连接,导致 nginx 服务器认为响应无效,标记当前 http 请求为 502。

为什么 tomcat 应用服务器会主动 FIN 连接呢?

通过查阅资料发现 tomcat 应用服务器 FIN 连接与如下两个参数有关:KeepAliveTimeout、maxKeepAliveRequests。

keepAliveTimeout

The number of milliseconds this Connector will wait for another HTTP request before closing the connection. The default value is to use the value that has been set for the connectionTimeout attribute. Use a value of -1 to indicate no (i.e. infinite) timeout.

连接存活时间,connectionTimeout 是其默认值。

maxKeepAliveRequests

The maximum number of HTTP requests which can be pipelined until the connection is closed by the server. Setting this to -1 will allow an unlimited amount of pipelined or keep-alive HTTP requests. If not specified, this attribute is set to 100.

连接最大请求数目,默认值是 100。

源码分析:

KeepAliveTimeout 默认值为 20000 ms。

/**
 * Keepalive timeout, if not set the soTimeout is used.
 */
private Integer keepAliveTimeout = null;

public int getKeepAliveTimeout() {
    if (keepAliveTimeout == null) {
        return getConnectionTimeout();
    } else {
        return keepAliveTimeout.intValue();
    }
}

/**
 * Socket timeout.
 *
 * @return The current socket timeout for sockets created by this endpoint
 */
public int getConnectionTimeout() {
    return socketProperties.getSoTimeout();
}

public int getSoTimeout() {
    return soTimeout.intValue();
}

/**
 * SO_TIMEOUT option. default is 20000.
 */
protected Integer soTimeout = Integer.valueOf(20000);

KeepAliveRequests 默认值为 100。

/**
 * Max keep alive requests
 */
private int maxKeepAliveRequests=100; // as in Apache HTTPD server
public int getMaxKeepAliveRequests() {
    return maxKeepAliveRequests;
}
public void setMaxKeepAliveRequests(int maxKeepAliveRequests) {
    this.maxKeepAliveRequests = maxKeepAliveRequests;
}

从源码分析,对于当前连接来说,如果在20s内没有 http 请求或者该连接已经接受了100个请求,tomcat 应用服务器会主动发起 FIN 关闭 tcp 连接。

问题解决

从上述分析来看,可以得出结论是 nginx 复用已经建立的 tcp 连接,准备发送请求。nginx 对该连接是否有效是无感的,而此时 tomcat 正准备关闭该连接,无法正常响应请求,nginx 将该请求标记为 502

从数据统计来看,公司认证服务一天的请求量有 3千万左右,一天出现 502 的量大约为20个左右,所以说一旦出现一般都是极端情况。

为了减少极端情况发生,本着不影响服务器性能的原则,适当地调整参数来达到减少 502 的效果。

import org.apache.catalina.connector.Connector;
import org.apache.coyote.http11.Http11NioProtocol;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class WebServerConfiguration implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
    @Override
    public void customize(ConfigurableWebServerFactory configurableWebServerFactory) {
        //使用对应工厂类提供给我们的接口定制化我们的 tomcat connector
        ((TomcatServletWebServerFactory)configurableWebServerFactory).addConnectorCustomizers(new TomcatConnectorCustomizer() {
            @Override
            public void customize(Connector connector) {
                Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
                //设置30秒内没有请求则服务端自动断开连接
                protocol.setKeepAliveTimeout(30000);
                //当客户端发送超过1000个请求则自动断开链接
                protocol.setMaxKeepAliveRequests(1000);
            }
        });
    }
}

代码上线之后,经过一段时间的观察发现,一天出现 502 的量大约为 10 个左右。对比没上代码之前,每天 20 个左右,看来设置参数是有效的。