“服务器宕机了”“网站打不开了”“用户全掉线了”。你慌慌张张登录上去,看到的第一行日志是:too many open files。再敲个ss -s,发现TCP连接数已经飙到了十几万,服务器的CPU软中断占满了两个核。而且奇怪的是,同样的问题在香港服务器、新加坡服务器上也会出现,但日本服务器——尤其是东京和大阪的机房——似乎更容易“崩溃”。为什么?因为日本互联网的特点是:带宽大、延迟低、用户密集,一个爆款手游或者一场直播活动,能在几分钟内把连接数从几百拉到几十万。今天就来聊聊,日本服务器在高并发下被TCP连接数压垮了怎么办。
一、先搞清楚“崩溃”到底是怎么发生的
很多人以为服务器崩溃就是CPU 100%或者内存用光了。但TCP连接数过多导致的崩溃,往往更隐蔽。它的典型症状是:系统负载不高,CPU只有30%,但新连接就是进不来;已经连上的用户正常,新用户一直转圈圈;重启服务就好了,跑一会儿又挂了。
根本原因是什么? 每一个TCP连接,不管有没有数据在传,都要占用内核资源。主要包括:文件描述符、socket缓冲区、连接跟踪表项、内存开销。
日本服务器尤其脆弱的原因:日本机房普遍采用高性能硬件(比如Xeon Gold 6240 + 256GB内存),运维人员容易产生“硬件够强就不怕”的错觉,软件层面的限制根本没调过。结果就是几千用户的时候跑得飞快,几万用户的时候直接躺平。
二、方法一:调大系统上限,别让内核卡住脖子
先别急着改代码,把操作系统的“出厂设置”改一改,往往能解决80%的问题。
第一步:改文件描述符上限
这个是最常见的坑。CentOS默认的nofile是1024,就是说你的进程最多只能同时打开1024个文件——而每个TCP连接也算一个“文件”。1024个连接,一个日活稍微高点的APP就撑爆了。
查看当前限制:
ulimit -n
临时改:
ulimit -n 1000000
永久改:编辑/etc/security/limits.conf,加上:
* soft nofile 1000000
* hard nofile 1000000
root soft nofile 1000000
root hard nofile 1000000
第二步:改系统级的TCP参数
编辑/etc/sysctl.conf:
# 最大文件句柄数(系统级)
fs.file-max = 2000000
# 增大本地端口范围
net.ipv4.ip_local_port_range = 1024 65535
# TIME_WAIT相关的优化
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
# 增加最大连接数(背压队列)
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 8192
# 内存相关的TCP缓冲
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
# 孤儿socket的最大数量(防止内存耗尽)
net.ipv4.tcp_max_orphans = 262144
# 已建立连接的哈希表大小(内存换性能)
net.ipv4.tcp_max_tw_buckets = 2000000
然后执行sysctl -p。
这些参数的逻辑是:让内核能处理更多连接,同时给每个连接分配更合理的内存。但注意,tcp_max_orphans设得太高可能导致内存被耗尽,要根据服务器的物理内存来调整——一个孤儿socket大约占用64KB内存,10万个就是6.4GB,心里要有数。
三、方法二:应用层加连接池,别来一个请求建一个连接
很多崩溃是“自找的”。代码里这么写的:
def get_user_info(user_id):
conn = mysql.connector.connect(host='db.jp.internal', user='app', password='xxx')
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM users WHERE id={user_id}")
result = cursor.fetchone()
conn.close() # 又关又开,造孽啊
return result
每个请求都新建一个数据库连接,请求完了又关掉。并发1000的时候还好,并发10000的时候,系统同时在处理新建连接和关闭连接的开销,CPU全花在握手和挥手上了。
正确的做法:用连接池。
Go语言的标准库自带连接池:
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func initDB() *sql.DB {
db, _ := sql.Open("mysql", "user:pass@tcp(db.jp.internal)/app")
db.SetMaxOpenConns(100) // 最大打开连接数,别超过数据库的配置
db.SetMaxIdleConns(50) // 最大空闲连接数
db.SetConnMaxLifetime(300 * time.Second)
return db
}
// 使用的时候,连接是复用的
func getUser(db *sql.DB, id int) {
row := db.QueryRow("SELECT name FROM users WHERE id=?", id)
// db不用close,连接池自动管理
}
Java用HikariCP,Node.js用generic-pool,Python用SQLAlchemy自带的连接池。不管用什么语言,核心原则是一样的:连接是昂贵的资源,建好了就留着用,别用完就扔。
对于HTTP服务之间的调用也一样。如果你的日本服务器作为API网关,需要调用下游的多个微服务,用HTTP客户端连接池:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
# 每个host保留20个连接
adapter = HTTPAdapter(pool_connections=50, pool_maxsize=100)
session.mount('http://', adapter)
session.mount('https://', adapter)
# 这个session可以被全局复用,底层连接会自动保持
四、方法三:负载均衡前置,别让单台服务器硬扛
日本服务器崩溃的另一个常见场景:你只有一台日本服务器,但用户来自东京、大阪、名古屋,甚至还有跨海从韩国、中国连过来的。一个高并发活动,比如日本的“メガ割”促销或者中国的双十一出海版,一台机器无论如何都扛不住。
这时候需要的不是优化单机性能,而是横向扩展。
方案A:用NLB/ALB做四层分发
在日本机房内部署多台应用服务器,前面放一个负载均衡器。负载均衡本身是经过优化的,单台NLB可以处理几百万并发连接,然后它把这些连接分发到后端。后端每台服务器只需要维持一部分连接。
方案B:边缘节点分流
日本用户很多走4G/5G移动网络,运营商有自己的代理和缓存。可以在这个层面做文章。比如在东京、大阪、名古屋三地部署边缘节点,用DNS智能解析把用户引导到距离最近的节点。
AWS在日本有东京和大阪两个Region,可以配合Global Accelerator做流量接入。Google Cloud的东京节点接入了NTT和KDDI的优质线路。Azure Japan East连接了多家运营商。利用云厂商的全球网络,单点崩溃的风险会低很多。
五、方法四:升级协议,从TCP换到UDP或者QUIC
这是最彻底的解法——既然TCP连接数太多会崩溃,那能不能不用TCP?
方案A:用UDP替代TCP
对于实时音视频、游戏位置同步、DNS查询这些业务,UDP天然就没有连接数的概念。每个客户端发数据包过来,服务器处理完就行,不用维护连接状态。
但UDP没有拥塞控制,自己实现一个可靠UDP库又太麻烦。业界通用的做法是用KCP——一个基于UDP的可靠传输协议,比TCP快30%-40%,而且没有TIME_WAIT、没有连接数爆炸的问题。
方案B:拥抱QUIC协议
QUIC是HTTP/3的基础,它把TCP的可靠性和TLS的安全加密都搬到了UDP之上。一个QUIC连接可以承载多个流,没有队头阻塞,而且连接迁移特性让手机从WiFi切到5G时连接不会断。
启用QUIC之后,连接管理的压力会显著下降。因为QUIC的连接是长生命周期的,不会像TCP短连接那样频繁建立销毁。而且QUIC内置了0-RTT握手,新用户建立连接的成本低得多。
六、日本服务器特有的坑:Idle timeout与NTT线路问题
在日本跑过高并发的都知道,日本运营商(尤其是NTT和KDDI)的NAT网关有个让人抓狂的特性:5分钟无数据就静默断连。客户端那边没收到任何通知,连接看起来还活着;服务器这边也以为连接正常。但实际上网关已经把映射表删了。
结果就是服务器上挂着几万个僵尸连接,客户端根本收不到数据。这些僵尸连接占着fd、占着内存,还占着连接表,直到服务器的keepalive探测发现对方不响应,才会慢慢清理。但那个清理速度太慢了,高并发下新的合法连接进不来,服务器就崩溃了。
解决方案:调整TCP keepalive参数
# 编辑 /etc/sysctl.conf
net.ipv4.tcp_keepalive_time = 60 # 60秒空闲后开始探测
net.ipv4.tcp_keepalive_intvl = 10 # 探测间隔10秒
net.ipv4.tcp_keepalive_probes = 3 # 探测3次失败就断开
这样配置后,僵尸连接最多60+30=90秒就会被清理掉,而不是之前默认的7200秒(2小时)。在大规模并发场景下,这能释放大量被占用的资源。
日本服务器在高并发下被TCP连接数压垮,本质上是个可预见的问题。你不需要等到凌晨两点被报警吵醒了再去翻sysctl.conf,而是在上线的第一天就该把这些参数调好。
还有一个容易被忽略的点:日本的用户行为模式和内地不太一样。日本用户对加载速度极其敏感,但对连接中断的容忍度很低。这意味着你的服务器不能因为连接数过多就拒绝新连接,也不能因为keepalive探测超时就粗暴断连。优化的时候要把握好度:太激进的服务端配置可能会误杀正常用户,太保守又扛不住并发。
如果实在扛不住了,记住一个原则:宁可让负载均衡器丢包,也别让核心数据库连接池爆掉。丢几个ping包用户感知不到,但数据库连不上就是真正的“崩溃”了。在设计高并发架构的时候,把连接数的天花板上限想清楚,比你事后花三天时间调内核参数要划算得多。
推荐文章
