见过整个运维部同时沉默的样子吗? 一封客户的投诉邮件,措辞客气,但句句见血。看完两段,头都麻了。

不是什么业务故障、代码死锁,就两件“部署小事”。但恰恰是这两件小事,让我觉得有必要写篇文章,提醒一下同行。

信里有两点特别扎眼,今天拆开来聊一聊。

第一件:Nginx 跑在 Windows 上

平台上线前部署代理操作系统为什么是 windows?我工作这么多年,多多少少接触了上百家公司,用 Linux 的至少 50 家,50 家里面你们是我见过第一家在 windows 上部署 nginx 的……

说实话,读到这句的时候,我愣了一下,然后笑了,苦笑。

Nginx 这玩意儿,生下来就是为 Linux 活的。它的灵魂是 epoll——Linux 内核里那个处理高并发的神器。Windows 上虽然也有 IOCP,但 Nginx 官方对 Windows 的支持,定位从来都是 “开发测试” ,官网上白纸黑字,生产环境强烈推荐 Linux。

为什么还会有人把 Nginx 装给 Windows?

无非三种情况:

  • 历史包袱太重,早期业务是 .NET/IIS,懒得迁,也不敢迁;
  • 团队里没人熟 Linux,招不到人,也舍不得花钱招;
  • 装上去能启动,没报错,就以为没事了。

但代价呢?

性能打折。 Windows 上的 Nginx 被迫退回到 select / poll 模型。虽然从 1.15.9 版本开始支持 poll 模型,可以绕过 select 的1024硬限制,但这只是“能跑”,远没到“跑得好”的程度。poll 在 Windows 上的实现本身就有缺陷,比如可能无法及时报告连接错误。你装了一台法拉利的引擎,结果跑起来跟三轮车似的。

玄学稳定性。 Nginx on Windows 的官方文档里至今仍列着“已知问题”——平时没事,流量一上来,什么诡异的超时都能遇到。根据社区讨论和实际测试,Windows 版 Nginx 的真实并发上限大约在 500 到 1500 连接,超过这个范围性能就会急剧下降。

那位前辈说“第一次见”,不是他见识少,是太离谱。世界还真是个草台班子。

第二件:connections 设了 20W,file-max 还是 1024

“贵司是否有专门的运维对 Linux 内核配置进行优化?我查看了部分机器的配置,nginx connections 设为 20W,但 Linux 的 file-max 还是默认值 1024。”

这条更狠,直接打在“根本没人管”的痛点上。

翻译一下就是:工程师在 Nginx 里写了 worker_connections 200000,觉得自己能扛 20 万并发。结果 Linux 系统冷冷地回了一句:“不,你最多开 1024 个文件。”

系统默认的 fs.file-max 通常是 1024(旧版)或者按内存自动算(新版),但无论如何,裸机默认值绝对到不了 20W。

Nginx 启动的时候一声不吭,你以为万事大吉。等流量一上来,日志里静悄悄出现 Too many open files,然后用户 502,老板拍桌子。

那位前辈说“查看了部分机器”——这个词最扎心。说明他随手查了几台,台台都是这样。

这不是某一个人的失误,是整个流程在裸奔。

一份只读脚本,帮你检查生产环境有没有裸奔

下面这份脚本是纯只读的,不会修改系统任何配置,放心在生产环境执行。它会扫描关键内核参数、limits.conf、常见服务进程的实际文件描述符限制,给出红黄绿状态。

将以下内容保存为 check_prod.sh,执行 sudo bash check_prod.sh

[!quote]- 一键检测生产环境内核配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#!/bin/bash
# ============================================================
# 文件名: check_production_sysctl.sh
# 用途: 检查生产环境内核参数是否符合推荐值(只读)
# 适用: Debian/Ubuntu/CentOS/RHEL
# ============================================================

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# ---------- 推荐值定义(可根据实际内存调整) ----------
declare -A RECOMMENDED=(
["fs.file-max"]="2097152"
["fs.nr_open"]="1048576"
["kernel.pid_max"]="65536"
["net.core.somaxconn"]="65535"
["net.ipv4.tcp_max_syn_backlog"]="65535"
["net.core.netdev_max_backlog"]="65535"
["net.ipv4.tcp_syncookies"]="1"
["net.ipv4.tcp_tw_reuse"]="1"
["net.ipv4.tcp_fin_timeout"]="30"
["net.ipv4.ip_local_port_range"]="1024 65535"
["net.ipv4.tcp_max_tw_buckets"]="2000000"
["net.core.rmem_max"]="16777216"
["net.core.wmem_max"]="16777216"
["net.ipv4.tcp_rmem"]="4096 87380 16777216"
["net.ipv4.tcp_wmem"]="4096 65536 16777216"
["vm.swappiness"]="10"
["vm.overcommit_memory"]="1"
["net.netfilter.nf_conntrack_max"]="2097152"
)

echo -e "${BLUE}=============================================${NC}"
echo -e "${BLUE} 生产环境内核参数检查报告 (只读)${NC}"
echo -e "${BLUE} 时间: $(date)${NC}"
echo -e "${BLUE} 主机: $(hostname)${NC}"
echo -e "${BLUE}=============================================${NC}"

# ---------- 1. 内核参数检查 ----------
echo -e "\n${BLUE}>>> 1. 内核参数检查 (sysctl)${NC}"
echo -e "参数名 | 当前值 | 推荐值 | 状态"
echo -e "-------|--------|--------|------"

for param in "${!RECOMMENDED[@]}"; do
current=$(sysctl -n "$param" 2>/dev/null || echo "N/A")
rec="${RECOMMENDED[$param]}"

if [[ "$current" == "N/A" ]]; then
echo -e "$param | N/A | $rec | ${RED}NOT FOUND${NC}"
continue
fi

if [[ "$current" == "$rec" ]]; then
echo -e "$param | $current | $rec | ${GREEN}OK${NC}"
else
# 数值类型:当前值 >= 推荐值算通过
if [[ "$current" =~ ^[0-9]+$ ]] && [[ "$rec" =~ ^[0-9]+$ ]]; then
if [[ "$current" -ge "$rec" ]]; then
echo -e "$param | $current | $rec | ${GREEN}OK (>=推荐)${NC}"
else
echo -e "$param | $current | $rec | ${RED}WARN (偏小)${NC}"
fi
elif [[ "$current" =~ ^[0-9]+\ [0-9]+\ [0-9]+$ ]] && [[ "$rec" =~ ^[0-9]+\ [0-9]+\ [0-9]+$ ]]; then
cur_max=$(echo "$current" | awk '{print $3}')
rec_max=$(echo "$rec" | awk '{print $3}')
if [[ "$cur_max" -ge "$rec_max" ]]; then
echo -e "$param | $current | $rec | ${GREEN}OK${NC}"
else
echo -e "$param | $current | $rec | ${RED}WARN (最大值偏小)${NC}"
fi
else
echo -e "$param | $current | $rec | ${YELLOW}UNKNOWN${NC}"
fi
fi
done

# ---------- 2. limits.conf 检查 ----------
echo -e "\n${BLUE}>>> 2. /etc/security/limits.conf${NC}"
grep -E "^[^#]*nofile" /etc/security/limits.conf 2>/dev/null || echo "无有效配置"
soft_cnt=$(grep -E "^[^#]+\s+soft\s+nofile\s+65535" /etc/security/limits.conf 2>/dev/null | wc -l)
hard_cnt=$(grep -E "^[^#]+\s+hard\s+nofile\s+65535" /etc/security/limits.conf 2>/dev/null | wc -l)
if [[ "$soft_cnt" -gt 0 ]] && [[ "$hard_cnt" -gt 0 ]]; then
echo -e "${GREEN}✓ 已配置 nofile 65535${NC}"
else
echo -e "${RED}✗ 未正确配置 nofile 65535${NC}"
fi

# ---------- 3. 常见服务进程实际限制 ----------
echo -e "\n${BLUE}>>> 3. 常见服务进程实际限制${NC}"
for svc in nginx java redis-server mysqld postgres httpd; do
pids=$(pgrep -x "$svc" 2>/dev/null | head -2)
for pid in $pids; do
if [[ -f /proc/$pid/limits ]]; then
hard=$(grep "Max open files" /proc/$pid/limits | awk '{print $4}')
echo -e " $svc (PID $pid): 硬限制 = $hard $([ "$hard" -ge 65535 ] && echo "${GREEN}${NC}" || echo "${RED}✗ 建议>=65535${NC}")"
fi
done
done

# ---------- 4. systemd 服务检查 ----------
echo -e "\n${BLUE}>>> 4. systemd 服务 LimitNOFILE${NC}"
for unit in $(systemctl list-units --type=service --state=running 2>/dev/null | grep -E "nginx|java|redis|mysql|postgres" | awk '{print $1}'); do
nofile=$(systemctl show -p LimitNOFILE "$unit" 2>/dev/null | cut -d= -f2)
echo -e " $unit : LimitNOFILE = ${nofile:-未设置} $([ "${nofile:-0}" -ge 65535 ] 2>/dev/null && echo "${GREEN}${NC}" || echo "${RED}${NC}")"
done

echo -e "\n${BLUE}=============================================${NC}"
echo -e "${GREEN}检查完成。请关注标记为 WARN 或 ✗ 的项目。${NC}"
echo -e "${BLUE}=============================================${NC}"

最后说几句

那封投诉信里提到的两件事,单独拎出来看,每一件都不算“致命失误”——毕竟 Windows 跑 Nginx 也能转,file-max 没改也不一定会立刻崩。

但合在一起,暴露的是一个更根本的问题:这个团队的生产环境部署,没有标准,没有检查清单,没有敬畏心。

那位老前辈真正愤怒的,不是具体哪个选择做错了,而是这些错误“低级到只要有人在上线前随手查一下就能发现”。

系统能跑,不等于能活。能活过流量高峰,才叫生产环境。

生产环境不是玩具,是战场。

最近在公司内部翻到一封“技术投诉信”,字里行间的锐利让我出了一身冷汗。投诉对象是某部门上线的系统,投诉人显然是位见过世面的前辈。信里有两点特别扎眼,今天就借这两把“飞刀”,来聊聊生产环境部署的那些“不是玩具”的事。

💡投诉原文

(2)平台上线前部署代理操作系统为什么是 windows?我工作这么多年,多多少少接触了上百家公司,用 Linux 的至少 50 家,50 家里面你们是我见过第一家在 windows 上部署 nginx 的……
(3)贵司是否有专门的运维对 Linux 内核配置进行优化?我查看了部分机器的配置,nginx connections 设为 20W,但 Linux 的 file-max 还是默认值 1024。

两句话,两个巴掌,打在了两个不同的“常识”上。我们来一个个拆。

第一巴掌:Nginx + Windows,是什么神仙组合?

Nginx 诞生于 Linux 世界,它的 epoll(Linux) 和 kqueue(BSD/macOS) 事件模型是高并发的基石。Windows 上虽然也有 IOCP,但 Nginx 官方对 Windows 的支持定位是 “开发测试”,生产环境强烈推荐 Linux。

那为什么有些团队会选 Windows?

  • 历史包袱:早期业务跑在 .NET/IIS 上,懒得迁;
  • 运维技能树单一:团队只熟 Windows,没 Linux 人手;
  • 反正能跑:启动后没报错,就以为万事大吉。

但代价是什么?

  • 性能折损:Windows 上的 Nginx 用 select/poll 模型,单进程并发上限被锁死在 1024 级别;
  • 稳定性玄学:Nginx on Windows 的 release notes 常年写着 “known issues with large number of connections”;
  • 生态割裂:要调优内核参数?Windows 的注册表调优和 Linux 的 /etc/sysctl.conf 完全两个宇宙。

第二巴掌:connections=20W,file-max=1024,这比“渣男”还渣

这巴掌更狠,直接打在了内核参数没人管的痛点上。

Linux 系统默认的 fs.file-max 通常是 1024(旧版)或按内存计算(新版),但无论如何,裸机默认值绝不会是 20W。而 Nginx 配置里设置了 worker_connections 200000,意味着每个 worker 进程希望打开 20 万个文件描述符。

结果呢?系统说:不,你最多只能开 1024 个。 Nginx 启动时不会报错,但一旦流量上来,日志里就会 quietly 出现 Too many open files,然后连接被拒绝,用户 502,老板拍桌。

这不是技术问题,这是运维基本素养的缺失,更可怕的是,这类疏忽往往不是孤例——投诉人“查看了部分机器”就发现,说明批量部署脚本里根本没有内核参数检查步骤。

用一份检查脚本,检查你的生产环境是否“裸奔”

下面这份脚本是纯只读的检查脚本,不会修改系统任何配置,可以放心在生产环境执行。它会扫描所有关键内核参数、limits.conf 配置、正在运行的常见服务进程的实际文件描述符限制,并给出红黄绿状态评估。

将以下内容保存为 check_prod.sh,然后执行 sudo bash check_prod.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#!/bin/bash
# ============================================================
# 文件名: check_production_sysctl.sh
# 用途: 检查生产环境内核参数是否符合推荐值(只读)
# 适用: Debian/Ubuntu/CentOS/RHEL
# ============================================================

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# ---------- 推荐值定义(可根据实际内存调整) ----------
declare -A RECOMMENDED=(
["fs.file-max"]="2097152"
["fs.nr_open"]="1048576"
["kernel.pid_max"]="65536"
["net.core.somaxconn"]="65535"
["net.ipv4.tcp_max_syn_backlog"]="65535"
["net.core.netdev_max_backlog"]="65535"
["net.ipv4.tcp_syncookies"]="1"
["net.ipv4.tcp_tw_reuse"]="1"
["net.ipv4.tcp_fin_timeout"]="30"
["net.ipv4.ip_local_port_range"]="1024 65535"
["net.ipv4.tcp_max_tw_buckets"]="2000000"
["net.core.rmem_max"]="16777216"
["net.core.wmem_max"]="16777216"
["net.ipv4.tcp_rmem"]="4096 87380 16777216"
["net.ipv4.tcp_wmem"]="4096 65536 16777216"
["vm.swappiness"]="10"
["vm.overcommit_memory"]="1"
["net.netfilter.nf_conntrack_max"]="2097152"
)

echo -e "${BLUE}=============================================${NC}"
echo -e "${BLUE} 生产环境内核参数检查报告 (只读)${NC}"
echo -e "${BLUE} 时间: $(date)${NC}"
echo -e "${BLUE} 主机: $(hostname)${NC}"
echo -e "${BLUE}=============================================${NC}"

# ---------- 1. 内核参数检查 ----------
echo -e "\n${BLUE}>>> 1. 内核参数检查 (sysctl)${NC}"
echo -e "参数名 | 当前值 | 推荐值 | 状态"
echo -e "-------|--------|--------|------"

for param in "${!RECOMMENDED[@]}"; do
current=$(sysctl -n "$param" 2>/dev/null || echo "N/A")
rec="${RECOMMENDED[$param]}"

if [[ "$current" == "N/A" ]]; then
echo -e "$param | N/A | $rec | ${RED}NOT FOUND${NC}"
continue
fi

if [[ "$current" == "$rec" ]]; then
echo -e "$param | $current | $rec | ${GREEN}OK${NC}"
else
# 数值类型:当前值 >= 推荐值算通过
if [[ "$current" =~ ^[0-9]+$ ]] && [[ "$rec" =~ ^[0-9]+$ ]]; then
if [[ "$current" -ge "$rec" ]]; then
echo -e "$param | $current | $rec | ${GREEN}OK (>=推荐)${NC}"
else
echo -e "$param | $current | $rec | ${RED}WARN (偏小)${NC}"
fi
elif [[ "$current" =~ ^[0-9]+\ [0-9]+\ [0-9]+$ ]] && [[ "$rec" =~ ^[0-9]+\ [0-9]+\ [0-9]+$ ]]; then
cur_max=$(echo "$current" | awk '{print $3}')
rec_max=$(echo "$rec" | awk '{print $3}')
if [[ "$cur_max" -ge "$rec_max" ]]; then
echo -e "$param | $current | $rec | ${GREEN}OK${NC}"
else
echo -e "$param | $current | $rec | ${RED}WARN (最大值偏小)${NC}"
fi
else
echo -e "$param | $current | $rec | ${YELLOW}UNKNOWN${NC}"
fi
fi
done

# ---------- 2. limits.conf 检查 ----------
echo -e "\n${BLUE}>>> 2. /etc/security/limits.conf${NC}"
grep -E "^[^#]*nofile" /etc/security/limits.conf 2>/dev/null || echo "无有效配置"
soft_cnt=$(grep -E "^[^#]+\s+soft\s+nofile\s+65535" /etc/security/limits.conf 2>/dev/null | wc -l)
hard_cnt=$(grep -E "^[^#]+\s+hard\s+nofile\s+65535" /etc/security/limits.conf 2>/dev/null | wc -l)
if [[ "$soft_cnt" -gt 0 ]] && [[ "$hard_cnt" -gt 0 ]]; then
echo -e "${GREEN}✓ 已配置 nofile 65535${NC}"
else
echo -e "${RED}✗ 未正确配置 nofile 65535${NC}"
fi

# ---------- 3. 常见服务进程实际限制 ----------
echo -e "\n${BLUE}>>> 3. 常见服务进程实际限制${NC}"
for svc in nginx java redis-server mysqld postgres httpd; do
pids=$(pgrep -x "$svc" 2>/dev/null | head -2)
for pid in $pids; do
if [[ -f /proc/$pid/limits ]]; then
hard=$(grep "Max open files" /proc/$pid/limits | awk '{print $4}')
echo -e " $svc (PID $pid): 硬限制 = $hard $([ "$hard" -ge 65535 ] && echo "${GREEN}${NC}" || echo "${RED}✗ 建议>=65535${NC}")"
fi
done
done

# ---------- 4. systemd 服务检查 ----------
echo -e "\n${BLUE}>>> 4. systemd 服务 LimitNOFILE${NC}"
for unit in $(systemctl list-units --type=service --state=running 2>/dev/null | grep -E "nginx|java|redis|mysql|postgres" | awk '{print $1}'); do
nofile=$(systemctl show -p LimitNOFILE "$unit" 2>/dev/null | cut -d= -f2)
echo -e " $unit : LimitNOFILE = ${nofile:-未设置} $([ "${nofile:-0}" -ge 65535 ] 2>/dev/null && echo "${GREEN}${NC}" || echo "${RED}${NC}")"
done

echo -e "\n${BLUE}=============================================${NC}"
echo -e "${GREEN}检查完成。请关注标记为 WARN 或 ✗ 的项目。${NC}"
echo -e "${BLUE}=============================================${NC}"

系统不是能用就可以,生产环境不是玩具

投诉信里的那位老哥,真正愤怒的,不是“Windows 跑 Nginx”这个选择本身——毕竟每个团队有自己的历史包袱。他愤怒的是:你们在选择了一个非常规方案后,没有给出任何额外的论证、测试或补偿措施。

他愤怒的,也不是“file-max 没改”这个具体失误——毕竟谁都会忘。他愤怒的是:这个失误如此低级,以至于只要有人在上线前执行一次 sysctl -a | grep file-max 就能发现,但显然没有人做过。

这两件事合并起来,暴露了一个更严重的问题:这个团队的生产环境部署,没有标准,没有检查清单,没有敬畏心。

我们做技术的,往往迷恋代码里的优雅设计,却忽略了下线后的运行环境。但真正决定系统上限的,从来不是那几行业务逻辑,而是操作系统配置、网络栈参数、文件句柄限制这些看不见的基石。

一块基石歪了,整个大厦就摇摇欲坠。而生产环境里,没有重启试试这种后悔药。生产环境不是玩具,是战场。 而我们应该做个合格的士兵,不是马马虎虎的炮灰。