devops系列(一) Nginx 反向代理与负载均衡:一台服务器扛不住怎么办
devops系列(一) Nginx 反向代理与负载均衡一台服务器扛不住怎么办问题引入半夜被报警短信炸醒的滋味上个月有个周三凌晨两点我被钉钉报警震醒了。打开手机一看全是 “Tomcat 响应超时”、“接口 504 Gateway Timeout” 的告警。公司业务最近推广了一波日活从几千蹭蹭涨到了几万结果那台孤零零的 Tomcat 服务器开始扛不住了。白天用户访问慢还能忍到了高峰期直接频繁 502/504客服群里炸锅老板在群里 我“怎么回事修一下。”我爬起来看监控CPU 飙到 90%内存快满了GC 频率高得吓人。扩容机器可以但一台 8 核 16G 的服务器也不便宜啊。而且就算硬件升级了单点故障的问题还是没解决——这台 Tomcat 一挂整个服务就全凉了。说白了一台服务器就像一家只有一个厨师的餐厅饭点一到客人全堵在柜台前厨师累得手忙脚乱后面的客人等不及就开始骂娘。那怎么办多请几个厨师再找个机灵的服务员在门口统筹安排——客人来了服务员负责引导到不同的厨师那儿谁闲了给谁派活。这样既能分流压力某个厨师请假了其他厨师还能继续干活。这个机灵的服务员就是咱们今天要聊的Nginx。方案分析为什么选 Nginx我一开始也想过几个方案方案 A直接升级 Tomcat 服务器配置优点简单改个云服务器配置就行缺点贵而且单点故障没解决Tomcat 本身并发处理能力也有限方案 B用 Tomcat 集群 硬件负载均衡优点稳定缺点硬件 F5 贵得离谱小公司玩不起方案 CNginx 反向代理 多 Tomcat 节点优点免费开源、性能彪悍、配置灵活、社区生态成熟缺点需要学一点配置语法但其实不难对比下来Nginx 就是性价比之王。它不仅能做反向代理和负载均衡还能处理静态资源、做缓存、压缩、HTTPS 终止、限流防刷……简直就是运维界的瑞士军刀。但这里有个概念很多新手容易懵反向代理和正向代理到底啥区别用餐厅服务员来类比正向代理就像你客户端想打电话给某个明星但你不方便直接打于是找了个中间人代理帮你打。明星不知道电话是你打的只知道是中间人打的。正向代理代理的是客户端隐藏的是你。反向代理就像你去一家高档餐厅吃饭门口有个服务员接待你把你领到具体的厨师那儿。你根本不知道厨师是谁、在哪你只跟服务员打交道。反向代理代理的是服务端隐藏的是后厨。Nginx 就是那个站在门口的服务员。用户访问的是 Nginx 的 80 端口Nginx 再把请求转发给后端的 Tomcat 1、Tomcat 2、Tomcat 3……用户完全感知不到后端有几台服务器。实现过程Step by Step 上手好了概念聊完了咱们来点实在的。假设你现在有两台 Tomcat分别跑在192.168.1.101:8080和192.168.1.102:8080咱们用 Nginx 把它们代理起来。Step 1安装 Nginx略过编译安装的坑如果你用 CentOS直接 yum 装# 添加 EPEL 源后安装sudoyuminstallnginx-y# 启动并设置开机自启sudosystemctl start nginxsudosystemctlenablenginxUbuntu 的话用apt install nginx。编译安装太折腾新手建议先用包管理器把核心概念搞明白再说。Step 2配置反向代理Nginx 的核心配置文件一般在/etc/nginx/nginx.conf但咱们更常在/etc/nginx/conf.d/下创建独立的.conf文件方便管理。先来看一个最简版的反向代理配置# /etc/nginx/conf.d/myapp.conf server { listen 80; server_name api.example.com; location / { # 把所有请求转发到后端 Tomcat proxy_pass http://192.168.1.101:8080; # 关键把客户端真实 IP 传给后端不然 Tomcat 日志里全是 Nginx 的 IP proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这段要干嘛告诉 Nginx所有访问api.example.com:80的请求都帮我甩给192.168.1.101:8080。关键点在哪proxy_pass是核心指定后端地址proxy_set_header这三行非常重要否则后端拿不到用户的真实 IP 和原始 Host有些业务逻辑会出问题配置写完后一定要执行 reloadsudonginx-sreload很多新手改完配置发现没生效就是因为忘了这步。后面我会专门讲这个坑。Step 3负载均衡——一台不够多台一起扛现在咱们有两台 Tomcat 了总不能只代理一台吧Nginx 的upstream模块就是干这个的。# 先定义一个后端服务器池叫 tomcat_cluster upstream tomcat_cluster { server 192.168.1.101:8080; server 192.168.1.102:8080; } server { listen 80; server_name api.example.com; location / { proxy_pass http://tomcat_cluster; # 注意这里写的是 upstream 的名字 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这段要干嘛把请求轮流分发给两台 Tomcat实现最基本的负载均衡。默认策略是轮询round-robin就是请求 1 给 101请求 2 给 102请求 3 给 101依次循环。Step 4四种负载均衡策略怎么选Nginx 支持好几种负载均衡策略不同的场景用不同的策略别只会轮询。1. 轮询round-robin——默认策略upstream tomcat_cluster { server 192.168.1.101:8080; server 192.168.1.102:8080; }适用场景后端机器配置差不多请求处理时间也差不多。简单公平。2. 权重weight——能者多劳upstream tomcat_cluster { server 192.168.1.101:8080 weight3; server 192.168.1.102:8080 weight1; }适用场景两台机器配置不一样101 是 8 核 16G102 是 4 核 8G。那就让 101 多扛点活权重设为 3:1。3. ip_hash——同一个用户始终落在同一台机器upstream tomcat_cluster { ip_hash; server 192.168.1.101:8080; server 192.168.1.102:8080; }适用场景你的应用用了本地 Session 存储用户登录状态存在 Tomcat 内存里。如果请求被分配到不同机器用户就会频繁掉线。但要注意ip_hash 不是万能的如果某台机器挂了原本落在这台的请求会重新 hash 到其他机器。而且如果用户在公司内网出口 IP 相同可能会导致某一台机器压力特别大。4. least_conn——谁闲给谁upstream tomcat_cluster { least_conn; server 192.168.1.101:8080; server 192.168.1.102:8080; }适用场景接口处理时间差异很大有的请求 10ms 搞定有的要 10 秒。least_conn 会把新请求发给当前连接数最少的机器更智能一些。我的建议如果做了分布式 Session比如 Redis 存 Session优先用轮询或least_conn如果还是本地 Session临时用ip_hash过渡但长远看还是要上分布式 Session。Step 5静态资源分离——别让 Tomcat 干杂活Tomcat 处理动态请求还行但让它去传图片、CSS、JS那就是大材小用还拖累动态接口的响应速度。Nginx 传静态文件的能力是 Tomcat 的几十倍所以咱们要让 Nginx 直接处理静态资源动态请求才转发给 Tomcat。server { listen 80; server_name api.example.com; root /usr/share/nginx/html; # 静态资源直接由 Nginx 返回 location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2)$ { expires 30d; # 缓存 30 天 add_header Cache-Control public, immutable; access_log off; # 关闭静态资源访问日志减少磁盘 IO } # 动态请求转发给 Tomcat location /api/ { proxy_pass http://tomcat_cluster; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这段要干嘛图片、CSS、JS 这些文件Nginx 自己从磁盘读出来返回给用户只有/api/开头的接口请求才转发给 Tomcat。效果很明显我上次加了这个配置后Tomcat 的 CPU 使用率直接降了 30%页面加载速度也快了一截。Step 6Gzip 压缩——让传输更快现在的前端资源动不动就几百 KB开启 Gzip 压缩能省不少带宽。http { # 在 nginx.conf 的 http 块里加 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xmlrss text/javascript; gzip_min_length 1k; # 小于 1K 的文件不压缩省 CPU gzip_comp_level 4; # 压缩级别 1-94 是性价比平衡点 }为什么要设gzip_min_length 1k因为压缩本身也要消耗 CPU如果文件本来就几字节压缩后可能反而更大得不偿失。Step 7HTTPS 配置——现在没 HTTPS 都不好意思上线申请一个免费 SSL 证书Let’s Encrypt 或者阿里云免费证书配置很简单server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/ssl/api.example.com.crt; ssl_certificate_key /etc/nginx/ssl/api.example.com.key; ssl_protocols TLSv1.2 TLSv1.3; location / { proxy_pass http://tomcat_cluster; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } # HTTP 自动跳 HTTPS server { listen 80; server_name api.example.com; return 301 https://$server_name$request_uri; }关键提醒HTTPS 配完后一定要检查证书有效期设置自动续期。我有一次就因为证书过期了导致全站无法访问被老板在群里点名批评……Step 8限流防刷——给系统加个保险杠接口被爬虫狂刷怎么办Nginx 可以简单限流。这里介绍两种常用的限制单 IP 并发连接数# 在 http 块定义一个连接限制区域 limit_conn_zone $binary_remote_addr zoneaddr:10m; server { location /api/ { limit_conn addr 10; # 单个 IP 最多 10 个并发连接 proxy_pass http://tomcat_cluster; } }限制请求速率漏桶算法# 在 http 块定义 limit_req_zone $binary_remote_addr zoneone:10m rate10r/s; server { location /api/ { limit_req zoneone burst20 nodelay; # 每秒 10 个请求突发 20 个 proxy_pass http://tomcat_cluster; } }关键点rate10r/s是平均每秒 10 个请求burst20允许突发 20 个请求排队处理nodelay表示不延迟直接处理。如果超过了Nginx 会返回 503。限流这玩意儿不能设太死不然正常用户也可能被误伤。建议先设宽松一点观察日志再调整。一份生产级精简配置参考说了这么多我把上面这些整合成一份生产可用的精简配置你可以直接拿去改改 IP 就能用# /etc/nginx/nginx.conf user nginx; worker_processes auto; # 根据 CPU 核数自动调整 error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 4096; # 单个 worker 的最大连接数 use epoll; # Linux 高性能网络模型 multi_accept on; } http { include /etc/nginx/mime.types; default_type application/octet-stream; # 日志格式 log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for; access_log /var/log/nginx/access.log main; # 性能优化 sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; client_max_body_size 50m; # 允许上传的最大文件大小 # Gzip 压缩 gzip on; gzip_vary on; gzip_min_length 1k; gzip_comp_level 4; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xmlrss text/javascript; # 负载均衡 upstream upstream tomcat_cluster { least_conn; # 谁闲给谁比轮询更智能 server 192.168.1.101:8080 weight2; server 192.168.1.102:8080 weight1; # 健康检查失败 3 次认为不可用恢复 2 次认为可用 server 192.168.1.101:8080 max_fails3 fail_timeout30s; server 192.168.1.102:8080 max_fails3 fail_timeout30s; } # 虚拟主机配置 server { listen 80; server_name api.example.com; # HTTP 跳转 HTTPS如果不需要 HTTPS 可以注释掉 return 301 https://$server_name$request_uri; } server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/ssl/api.example.com.crt; ssl_certificate_key /etc/nginx/ssl/api.example.com.key; ssl_protocols TLSv1.2 TLSv1.3; root /usr/share/nginx/html; # 静态资源直接由 Nginx 处理 location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2)$ { expires 30d; add_header Cache-Control public, immutable; access_log off; } # 动态 API 转发给 Tomcat 集群 location /api/ { proxy_pass http://tomcat_cluster; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 连接超时设置 proxy_connect_timeout 5s; proxy_send_timeout 10s; proxy_read_timeout 30s; } } }这份配置的核心思路Nginx 负责 SSL 终止、静态资源、Gzip 压缩、负载均衡Tomcat 只专注处理动态业务逻辑通过least_conn和权重实现智能分流健康检查确保单点故障时自动剔除异常节点踩坑记录我踩过的两个大坑光讲配置没意思分享两个我真实踩过的坑说不定你正在踩或者即将踩。坑一改完配置没生效因为你没 reload这个坑我踩过不下三次。Nginx 的配置文件改完后必须执行nginx -s reload才能热加载新配置。如果你只改了文件就完事了Nginx 还在用旧的配置在跑。更坑的是有时候你执行了 reload但语法有错误Nginx 会拒绝加载新配置然后默默地继续用旧配置运行。你以为是新配置生效了实际上还是老样子。正确做法# 先检查语法是否正确sudonginx-t# 语法 OK 后再 reloadsudonginx-sreload养成nginx -t的习惯能救命。坑二location 路径匹配优先级搞错Nginx 的location匹配规则有点反直觉不是简单的谁在前面先匹配谁。它的优先级是这样的精确匹配最高优先级^~前缀匹配~和~*正则匹配按配置文件中的顺序普通前缀匹配/通用匹配最低优先级有一次我把静态资源的正则匹配写在了/api/的后面结果某些带.js后缀的 API 请求被 Nginx 当成静态资源处理了直接返回 404查了半天才发现是 location 顺序的问题。建议正则匹配的 location 尽量按精确度从高到低排列或者直接用^~做前缀匹配避免意外。验证效果改造前后对比咱们来验收一下成果。改造前单台 Tomcat 扛所有请求高峰期 CPU 90%频繁 502/504静态资源和动态请求混在一起互相拖累改造后两台 Tomcat 分担动态请求压力Nginx 直接处理静态资源Tomcat CPU 下降约 30%开启 Gzip 后静态资源体积减少 60%-70%单台 Tomcat 挂掉时Nginx 自动把流量切到另一台服务不中断虽然架构还是很简单但对于中小型项目来说这套方案性价比极高花半天时间配置能换来很长一段时间的安稳 sleep。总结今天咱们聊了怎么用 Nginx 解决一台服务器扛不住的问题反向代理就像是餐厅门口的服务员用户只跟 Nginx 打交道后端 Tomcat 被隐藏起来负载均衡让多台 Tomcat 一起干活策略有轮询、权重、ip_hash、least_conn按需选择静态资源分离能显著减轻 Tomcat 负担让专业的人干专业的事Gzip 压缩和HTTPS 配置是现代 Web 服务的基本操作限流防刷能给系统加一道保险杠防止被恶意流量冲垮当然这套方案也不是银弹。如果业务量继续增长后面你可能还要引入 Redis 做分布式 Session、用 Consul 做服务发现、上 Kubernetes 做容器编排……但那是后话了。千里之行始于一个靠谱的 Nginx 配置。你在实际项目中是怎么做负载均衡的用过 Nginx 的哪些高级功能或者踩过什么更奇葩的坑欢迎在评论区交流咱们一起进步如果这篇文章对你有帮助点个赞再走呗~