返回

端口耗尽怎么办?原因分析及解决方案

mysql

如何防止端口耗尽

开发和部署应用程序,面临网络资源管理问题很正常。当程序与外部服务建立大量连接,没有正确释放或复用这些连接,很容易出现端口耗尽现象。具体表现为服务无法建立新的网络连接,导致服务停滞,无法响应新的请求。

一、端口耗尽现象及原因分析

现象: 应用在运行一段时间后出现运行缓慢、无法连接、无法接收新的请求、或者连接丢失。在使用 netstat 命令检查网络连接状态,能看到大量的 TIME_WAIT 状态的连接,它们通常指向一个特定端口,比如本例中出现的:3306(默认的Mysql数据库端口)。

原因分析: 出现端口耗尽,通常与连接管理不当相关。TCP连接在关闭,需要经过 TIME_WAIT 状态来保证数据完整地传输完毕以及防止旧连接的数据包对新连接造成干扰。此状态通常会持续一段时间(如2MSL,在一些系统上可能是1到4分钟)。

产生原因:

  • 频繁创建和关闭 TCP 连接: 大量短连接没有正确处理和复用,会导致大量 TIME_WAIT 状态连接积压。

  • 客户端的动态端口有限: 在每个连接上,本地都会选择一个动态端口。这些端口数量受限。在高并发连接下,若旧的 TIME_WAIT 连接尚未释放,而新连接又需动态端口,容易造成可用端口不足。

  • 程序中数据库连接未妥善关闭: 像提供的示例,连接可能没显式关闭,数据库的连接在某些场景未能得到释放,导致连接资源的累积。

二、问题排查以及解决方案

解决此类问题的核心: 合理地管理连接的生命周期, 尽可能复用已有的连接, 并在不需要连接时及时释放。针对出现的大量 TIME_WAIT 状态连接以及可能由未关闭数据库连接,需要着重排查和优化如下几个方面。

1. 检查数据库连接是否正确关闭

提供的示例显示,在 main 函数,使用了 defer config.GlobalDBPool.Close() 来关闭数据库连接池。当 main 函数正常退出时,Go 会执行此语句以释放连接池。 但是在每个请求处理函数,比如 AnotherEndpointPOST,没有显式关闭连接相关的 sql.Rowssql.Row。这些操作应及时清理关联的连接资源。

方案 : sql.DB 是一个数据库连接池, sql.Row 即使没有调用 Scan() 或者有错误, 也要确保调用了 rows.Close() 或者 row.Close()rows.Close() 是幂等的, 执行多次也不会有问题, sql.Row 只能执行 Scan() 不能执行 Close(), 所以推荐使用 QueryRow().Scan(...)QueryRow() 不支持传入上下文。如果查询语句有可能非常长的时间内无返回(例如人为错误)就会导致阻塞, 有死锁风险, 因此必须传入超时控制的上下文, 在连接异常或者语句存在问题时能快速释放连接。

操作步骤与代码示例 :

调整后的代码如下:

func (us *UserService) AnotherEndpointPOST() func(c *gin.Context) {
    return func(c *gin.Context) {
        // 使用 context 控制超时,如设置 5 秒超时
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()
    
        sqlStatement := `PLACEHOLDER-SQL-STATEMENT-FOR-PRIVACY`
    
        var var1, var2, var3 string
    
        // 注意:将 context 传入 QueryRowContext
        err := us.ServerConfig.GlobalDBPool.QueryRowContext(ctx, sqlStatement, "param1").Scan(&var1, &var2, &var3)
        if err != nil {
            c.JSON(400, gin.H{"code": "some-error-code-string", "error": err.Error()}) // 改进:返回错误详情给客户端
            return
        }
    
        outputVar := fmt.Sprintf("\\\\%s\\%s\\%s", var1, var2, var3)
    
        c.JSON(200, gin.H{"code": "outputVar"})
    }
}

改进:

  1. context.WithTimeout: 为数据库查询设定了一个截止期限, 查询无法在限制时间内完成, QueryRowContext 将会返回 context.DeadlineExceeded 错误, 可以避免查询由于程序缺陷等原因陷入无限等待状态, 连接资源也能尽快释放, 避免了阻塞, 提升了可用性。

  2. defer cancel(): 保证创建的 context.Context 得到妥善关闭,并立即关闭与其关联的所有资源, 包括 QueryRowContext 发起的查询以及与该查询关联的连接。及时执行资源清理工作。

2. 调整系统内核参数

操作系统对于 TIME_WAIT 状态的连接数有限制,并且回收时间可以调整。合理的调整这些参数能够在一定程度上缓解端口耗尽问题。

  • 注意: 系统内核参数的修改是比较底层的操作, 不同系统的参数不完全相同, 操作的时候请务必做好备份。盲目修改可能会引发其他系统稳定问题。

方案 : 增加可用的本地端口范围以及缩短 TIME_WAIT 状态持续时长

操作步骤与代码示例(以 Linux 为例) :

  1. 增加本地端口范围 :修改 /etc/sysctl.conf 文件,加入或修改以下两行:

    net.ipv4.ip_local_port_range = 1024 65535
    net.ipv4.ip_local_reserved_ports = 3306 # 根据程序自身需要预留特定端口
    

    这条指令指定了当建立新的连接时可以选择使用的端口的范围是 1024 到 65535。系统自动分配的端口,这增加了可供选择的端口的数量, 一定程度上预防了当连接速率快时出现耗尽。
    同时为MySQL 服务端的预留端口设置为 3306 可以避免连接复用,客户端选择本地端口时从预留端口列表中排出该端口。从而解决特定端口的 TIME_WAIT 连接过多的情况,比如大量到:3306的连接处于 TIME_WAIT 状态, 通过设置客户端不再复用目标服务的端口为新的连接, 可将问题进一步缩小排查的范围。
    通过执行 sudo sysctl -p 来使配置生效

  2. 调整 TIME_WAIT 状态参数 :

    可以快速地关闭 TIME_WAIT 连接,加快资源的释放。 修改 /etc/sysctl.conf 文件

    net.ipv4.tcp_tw_reuse = 1
    net.ipv4.tcp_tw_recycle = 1
    net.ipv4.tcp_fin_timeout = 30
    
    • net.ipv4.tcp_tw_reuse: 开启后,在主动关闭一个连接,处在TIME_WAIT 的连接在被后续发起的新连接需要分配连接的时候回收掉。连接状态的迁移以及连接的状态,系统处理的性能损失可以接受, 可以设置成 1 以安全快速复用处于 TIME_WAIT 状态的连接。
    • net.ipv4.tcp_tw_recycle: 它的效果和 net.ipv4.tcp_tw_reuse 的效果是基本类似的, 不同之处在于后者只会快速复用当前系统的客户端连接资源。在一些特定情况下这个选项的可用性存在不兼容的情况, tcp_tw_recycle 是开启系统快速回收快速处理处于 TIME_WAIT 的连接。但是不区分它到底是作为服务器端的还是客户端的连接, 通用使用时需要额外谨慎。如果系统同时也充当网络出口设备的功能, 该选项不适宜设置为 1, 例如 NAT 网络环境下开启可能会产生连接错误, 可以设置成 0
    • net.ipv4.tcp_fin_timeout:该设置项修改连接关闭方发送了最后一个 FIN 包并切换到 FIN_WAIT2 的最大时长。修改该选项,连接状态的更新更快更及时。因为正常情况只有一方会主动断开连接,不会出现两边几乎同时断开的异常, 设置了较短的FIN_WAIT2 的等待时限是可以被接受的。在一些流量较大以及并发较大的服务器,通过合理的评估连接情况进行缩短设置时长有明显作用, 可以从默认值减少到 30。注意在某些应用场景或者非常旧的操作系统,设置的数值较小可能不安全,因此最好能清楚认识自己的场景,修改的时候逐渐减少。

    同样,执行 sudo sysctl -p 使配置生效。
    注意: tcp_tw_recycle 在某些 NAT 环境可能会导致问题,生产环境慎用。根据具体的系统和网络情况按需调整。

  3. 如果是在Windows 下, 可以通过注册表进行类似的配置。

    例如,要增加本地端口范围:

    New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" -Name "MaxUserPort" -Value 65534 -PropertyType "DWORD"
    New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" -Name "TcpTimedWaitDelay" -Value 30 -PropertyType "DWORD"
    

    运行以上命令, 重启机器生效。

3. 连接池参数优化

示例的 createDBPool 函数使用了 db.SetMaxOpenConns(maxConnections) 设置数据库连接池最大连接数为 30。过小的最大连接数, 频繁打开和关闭连接导致不必要的 TIME_WAIT 状态的连接过多。较大的值能够在高负载下,提供更多的并发查询能力。 考虑应用程序实际的数据库访问负载以及数据库服务所能提供的服务能力,通过反复压测实验评估并选择一个最合适的连接数。

方案 : 调整数据库连接池配置,合理地配置最大空闲连接数、最大打开连接数等。防止过多的空闲连接不被及时关闭或无法创建新连接的问题。

操作步骤与代码示例 :

  1. 可以考虑调整的参数还包括:

    • db.SetMaxIdleConns(n):空闲连接池连接数, 空闲连接并不会实际关闭。数量要少于最大连接数量, 过多的空闲连接对降低连接本身的压力并无益处,甚至还可能会出现连接过期等意外。
    • db.SetConnMaxLifetime(d): 一个数据库的连接长时间没有发起新的数据库的请求,保持打开的状态可能会失效。它和连接是否正在被使用的过期管理和检测相关, 连接过期, 当连接重新拿出来并尝试被复用,需要做额外的一层无效检查的机制, 该机制由 Go 内部实现, 应用开发不需要关心内部逻辑,这个机制也是为了防止无限制长时间持有已过期的资源导致的其他问题。 只有明确知道连接本身有一定限制,且希望提前清理, 才建议设置此项。如果将该项配置为比最大打开连接的持续时长短的任意的长度, 将永远都不会使用到该功能。在多数连接无使用时长上限以及没有过期控制的要求的场景,配置 0 或者不设置此选项将更合适。
  2. 修改 createDBPool 函数:

func createDBPool(connectionString string, maxConnections int) (*sql.DB, error) {
    db, err := sql.Open("mysql", connectionString)
    if err != nil {
        return nil, err
    }
    db.SetMaxOpenConns(maxConnections)          // 根据实际情况调整
    db.SetMaxIdleConns(maxConnections / 2)     // 空闲连接数, 根据实际情况调整, 设置为最大连接数的一半是一个常用的设置。
    db.SetConnMaxLifetime(0)                   // 设置连接永不过期,不主动关闭。 如果需要主动关闭并重置, 可使用一个更合理的超时,比如 time.Minute*5
    return db, nil
}

4. HTTP 长连接

netstat 有大量来自 :80 端口的 TIME_WAIT 状态, 这是服务器绑定的 Web 端口,如果有面向外部服务的组件, 在确认服务器主动发起的连接并非预想情况并可能导致大量 TIME_WAIT,应当着手排查上游以及客户端方面存在的问题。

方案 : 在服务端的HTTP请求中开启 keep-alive,服务器不应主动断开客户端的连接请求。在作为客户端访问其他服务,也可以在 HTTP 请求中尽可能开启并复用连接。如果存在中间的反向代理组件例如 Nginx, 开启代理到服务器的反向连接池。这几种手段都可以减少 TCP 连接的频繁创建和关闭, 对减少端口消耗有效。

代码示例:

Gin 默认的行为已经是 HTTP keep-alive 模式, 一般不用主动配置 Gin。但是存在 HTTP keep-alive 下仍出现过多的 TIME_WAIT, 就应当考虑更复杂的应用场景以及针对其他网络组件做额外的优化。例如常见的有前端接入层 Nginx 与网关的连接,如果反向代理配置不合理, 仍然会出现连接建立与断开过于频繁的问题。通过在 Nginx 的配置文件中加入:

http {
    # ... 其他 http 的设置

    upstream backend {
        server 192.168.1.10:80; #你的应用服务器地址和端口
        keepalive 32; #配置连接池,根据你的后端实例数进行估算配置,并根据请求压力调整它,注意它不能超过任意后端的最大连接能力。
    }
    
    server {
        # ... 其他 server 的设置

        location / {
            # ...
            proxy_http_version 1.1;  #注意HTTP版本要求在1.1或更高
            proxy_set_header Connection "";
            proxy_pass http://backend; #代理转发,匹配上面定义的backend
        }
    }
}

配置 Nginx 的长连接, 在负载均衡以及连接健康检查做的比较好的情况下, Nginx 通过把这些来自客户端的短连接转变为与服务器通讯的长连接,能很大程度上降低连接开销。重启或热加载配置 nginx -s reload

这些策略实施后,应该能够较有效地降低 TIME_WAIT 状态的连接数量,从而解决端口耗尽问题,提高网络稳定性以及程序可靠性。请根据具体应用场景来评估实施的效果并持续优化这些参数, 这对最终能否解决实际问题是必须且非常重要的。

额外安全建议

保证你的所有依赖包都处于最新版本。旧版本的库可能存在已知的连接管理问题。同时保证数据库,操作系统补丁更新,新版本会对内核层做优化并提供新的工具帮助更好地进行连接监控以及排查错误。 运行端口扫描和安全审计。防止服务暴露不必要的端口到公网环境,遭受潜在的安全风险。配置防火墙策略,仅允许受信任的 IP 范围访问你的数据库服务以及 Gin 服务器的端口。监控日志,注意观察系统和数据库错误日志, 是否出现持续性的超时、连接错误以及可疑操作,快速做出响应与修复, 保证高可用。对资源占用情况做持续检测, 避免异常连接的建立,以及数据库和主机所在机器发生预期外的服务。根据硬件资源水平以及预测业务的发展选择合适数值。如果出现机器负载超过了合理的水位, 对运行期间参数做快速调整或者及早做出服务上的扩容准备。