Nginx 高可用与 HTTPS 落地:Keepalived 双机主备 + 证书改造实战

大促前做架构巡检时发现一个致命问题:整条链路里 Nginx 入口只有一台,它一挂全站就没了。同时安全合规也要求全站上 HTTPS。两件事凑一起做了一次改造:双机主备入口 + 证书统一下放到入口层。这篇记录配置过程和几个只有实际部署才会遇到的坑。

一、Keepalived 怎么实现高可用

Keepalived 基于 VRRP 协议做主备切换,核心是一台”虚拟路由器”:

  • 两台机器(master/backup)组成一个组,对外暴露一个虚拟 IP(VIP);
  • 域名解析指向 VIP;
  • master 每秒发心跳,backup 收不到心跳就认为自己该接管,把 VIP 抢过来;
  • 用户侧无感知——地址没变,只是背后换了台机器。

用户不用改任何配置,因为 VIP 和它的虚拟 MAC 在 ARP 缓存里是稳定的,这一点比”手动把 DNS 切到备机”可靠得多。

二、主备配置

1
yum install -y keepalived    # 两台都装

master(lb01):

1
cat /etc/keepalived/keepalived.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
global_defs {
router_id lb01 # 标识本机身份
}

vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 50 # 主备必须一致,同网多组要不冲突
priority 150 # 优先级,主高备低
advert_int 1 # 心跳间隔 1s
authentication {
auth_type PASS
auth_pass 1234 # 主备一致
}
virtual_ipaddress {
10.0.8.3 # VIP
}
}

backup(lb02)只需改三处:router_id lb02、state BACKUP、priority 100。

1
2
systemctl start keepalived && systemctl enable keepalived
ip addr | grep 10.0.8.3 # 验证 VIP 在主上

切换测试:停掉 master 的 keepalived,看 VIP 是否漂到 backup;再启动 master,看是否抢回。

抢占与非抢占

默认是抢占式:master 恢复后会把 VIP 抢回去。频繁抢来抢去对连接是有损的,追求稳定可以把两边都配成 BACKUP + nopreempt:

1
2
3
4
5
vrrp_instance VI_1 {
state BACKUP # 两台都写 BACKUP
priority 150 # 仅靠优先级区分
nopreempt
}

非抢占模式下的规则:两台 state 都是 BACKUP、都加 nopreempt、优先级一高一低——谁先拿到就谁持有,直到它真挂。

脑裂:两台都以为自己是主

“脑裂”指主备之间的心跳断了,但两台机器都还活着,各自认为自己是 master,VIP 就冲突了。常见诱因:网线/交换机抖动、主备之间被防火墙拦了心跳。

预防和检测:在备机放一个巡查脚本,能 ping 通主、同时自己又持有 VIP,就说明脑裂了。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh
vip=10.0.8.3
master_ip=10.0.8.5
while true; do
ping -c 2 $master_ip &>/dev/null
if [ $? -eq 0 -a `ip add | grep "$vip" | wc -l` -eq 1 ]; then
echo "ha is split brain.warning." # 该告警了
else
echo "ha is ok"
fi
sleep 5
done

生产上更稳妥的做法是把仲裁和告警接进监控(Zabbix/钉钉),脚本只负责发现问题,处理动作走人工确认。

三、Keepalived 要能感知 Nginx 死活

只装 Keepalived 有个问题:Nginx 挂了但 Keepalived 还活着,VIP 不会漂移,用户请求照样打过来报错。必须写个探测脚本,Nginx 没了就主动退出 keepalived 触发切换:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
nginxpid=$(ps -C nginx --no-header | wc -l)

if [ $nginxpid -eq 0 ]; then
systemctl start nginx # 先尝试拉起
sleep 3
nginxpid=$(ps -C nginx --no-header | wc -l)
if [ $nginxpid -eq 0 ]; then
systemctl stop keepalived # 拉不起来就让位,让 VIP 漂走
fi
fi
1
chmod +x /root/check_web.sh

在 keepalived 配置里挂上:

1
2
3
4
5
6
7
8
9
10
11
vrrp_script check_web {
script "/root/check_web.sh"
interval 5 # 每 5 秒执行一次
}

vrrp_instance VI_1 {
...
track_script {
check_web
}
}

注意:抢占式只在 master 上配即可;非抢占式两台都要配。脚本执行时间要小于 interval,否则会互相打断。

四、全站 HTTPS 改造

1. 证书从哪来、放哪里

正式环境用 CA 签发的证书(云厂商有免费 DV 证书)。测试环境用 openssl 自签:

1
2
3
4
5
6
7
8
mkdir -p /etc/nginx/ssl_key && cd /etc/nginx/ssl_key

# 生成自签证书(有效期 10 年,CN 填域名,邮箱填自己的)
openssl req -days 36500 -x509 -sha256 -nodes \
-newkey rsa:2048 -keyout server.key -out server.crt

# 确认 Nginx 编译带了 ssl 模块
nginx -V | grep with-http_ssl_module

证书文件权限设 600,私钥泄露等于证书作废。

2. 入口层统一终止 HTTPS

证书只配在负载均衡层,后端继续跑 HTTP(内网传输),这是最常见的架构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
upstream website {
server 172.18.1.8:80;
server 172.18.1.9:80;
}

server {
listen 443 ssl;
server_name www.example.com;
ssl_certificate ssl_key/server.crt;
ssl_certificate_key ssl_key/server.key;

location / {
proxy_pass http://website;
proxy_set_header Host $http_host;
include proxy_params;
}
}

# 80 强制跳 443
server {
listen 80;
server_name www.example.com;
return 302 https://$server_name$request_uri;
}

3. 上线后出现”破图”和跳转死循环——两个真实坑

破图:WordPress 早期用 http 安装,站点地址和文章里的图片链接都写进了数据库(http://www.example.com/...)。上 HTTPS 后浏览器判定”HTTPS 页面加载 HTTP 资源”不安全,图片全裂。处理办法:

  • 用 WP 后台”设置 → 常规”把站点地址改成 https;
  • 数据库里的历史链接做一次批量替换(改前先备份库);
  • 长期方案是在加载时统一由 define('WP_HOME', ...) 强制协议。

PHP 不认识自己跑在 HTTPS 下:入口是 HTTPS,但后端 Nginx 到 PHP-FPM 是 HTTP,PHP 判断协议出错,重定向时把自己甩回 http,形成跳转循环。要在后端 fastcgi 参数里显式声明:

1
2
3
4
5
6
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTPS on; # 关键:告诉 PHP 前置是 HTTPS
include fastcgi_params;
}

这个问题排查花了我们不少时间:现象是登录页无限跳转,最后靠翻 PHP 框架源码里 is_ssl() 的判断逻辑才定位到。

4. SSL 参数优化

1
2
3
4
5
ssl_session_cache shared:SSL:10m;      # 会话复用,减少重复握手
ssl_session_timeout 1440m;
ssl_protocols TLSv1.2 TLSv1.3; # 生产禁用 TLS 1.0/1.1
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:!NULL:!MD5:!RC4;
ssl_prefer_server_ciphers on;

另外可以配合入口层的两个强化项:HSTS(add_header Strict-Transport-Security "max-age=31536000" always;)和证书到期监控(openssl s_client | openssl x509 -noout -enddate,到期前 30 天必须告警——证书过期导致全站警告这件事,每年都能在朋友圈看到别人家的翻车)。

五、顺手做的性能优化

高可用改造的同时,把入口层参数也调了一轮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 系统层:文件句柄
echo '* soft nofile 65535
* hard nofile 65535' >> /etc/security/limits.conf

# Nginx 层
worker_processes auto;
worker_cpu_affinity auto; # worker 绑核,减少 CPU 切换损耗
worker_rlimit_nofile 30000;

events {
worker_connections 10240;
}

http {
sendfile on; # 零拷贝传静态文件
tcp_nopush on;
gzip on; # 文本资源压缩,省 60% 以上流量
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript;
server_tokens off; # 隐藏版本号
client_max_body_size 200m; # 覆盖上传限制
}

代理到后端的连接池也会配(否则每个用户请求都跟后端新握手一次):

1
2
3
4
5
6
7
8
9
upstream backend {
server 172.18.1.8:8080;
keepalive 32;
}
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}

我们压测对比过:开 keepalive 之后同一并发下后端的连接数下降一大半;而静态文件交给 Nginx 直接服务时,同样的并发下 QPS 能到 Tomcat 的四倍以上(九千多对两千左右)——这就是动静分离的收益。

六、巡检和证书管理

改完之后给运维组加了几条日常巡检动作:

1
2
3
4
5
6
systemctl status nginx                 # 服务状态
ss -lntp | grep nginx # 80/443 监听
tail -100 /var/log/nginx/error.log # 错误日志
grep -c "connect() failed" /var/log/nginx/error.log # 502 前兆
df -h | grep /var/log # 日志盘空间
openssl x509 -enddate -noout -in /etc/nginx/ssl/www.pem # 证书到期

每周看一次慢接口 TOP 和状态码分布,把问题发现在用户投诉之前——这套巡检清单沿用到现在。

七、总结

高可用和 HTTPS 这两件事,配置本身都不复杂,真正决定成败的是细节:Keepalived 要能感知 Nginx 死活、非抢占式要两台都改、HTTPS 要处理应用层的协议识别。做完这一轮,入口层从”最脆弱的一环”变成了整个链路里最稳的一层。