一、背景
1. 业务上云带来性能收益
公司从去年全面推动业务上云,而以往 IDC 架构部署上,接入层采用典型的 4 层 LVS 多机房容灾架构,在业务高峰时期,扩容困难(受限于物理机资源和 LVS 内网网段的网络规划),且抵挡不住 HTTPS 卸载引发的高 CPU 占用。
而经过压力测试发现,使用腾讯云 7 层 CLB 负载均衡进行 HTTPS 卸载,性能得到极大提升。测试数据也表明,IDC 旧架构中,启用 HTTPS 会带来 90% 以上的性能损耗。
2. 架构调整引发多次故障
引入腾讯云 7 层 CLB 负载均衡产品,带了了巨大的性能提升,却也给业务带来了痛苦,主要核心问题是获取客户端的真实 IP 上。
当前现状是业务语言异构(PHP + Go),多数业务已经历服务化改造,但缺乏服务发现机制,服务与服务之间的调用依赖域名和 DNS 解析,大部分都是 HTTP 服务。
在架构调整后,由于未能 100% 覆盖测试,导致漏测的服务经常拿到错误的客户端 IP 地址,造成的后果是损失大量的用户。这些用户会因为短信验证码发送限制、IP 登录频次过高而无法登录、充值,给公司带来巨大损失。
3. 未来的路应该怎么走?
更进一步讲,当前业务如何抵挡外界的 DDoS 攻击、请求机器人、SQL 注入等等,最简单的是接入高防 IP、WAF 应用防火墙,而请求经过多轮转发,同样也有获取客户端真实 IP 的问题。
再者,业务也在逐步容器化,享受 Kubernetes 弹性扩容的便利,怎么平滑迁移也是非常值得深思的。
假设有一天某个同学,不小心配置有误——应用层拿到的,很有可能是高防 IP 或者 WAF 的 IP,业务绝对无法忍受。
显然,确定一个业务无感知的方案并成功落地迫在眉睫。
然而翻遍整个互联网,几乎没有文章能把这些看起来很简单的事情捋清楚、讲明白,更不用说最佳实践。
大多数人都是抄抄配置,潦潦草草上线,方案并没有普适性。
这篇文章也是我在这段时间的研究中总结出来的宝贵经验,希望对读者能有些许帮助。文章篇幅较长,难免有错误之处,还请各位看官斧正,感激不尽:)
二、名词释义
1. REMOTE-ADDR
- Nginx + PHP 模式下,REMOTE-ADDR 为远端的 IP 地址,可通过
$_SERVER['REMOTE-ADDR']
获取; - 它代表与上一层建立 TCP 连接的 IP 地址;
- 网站无代理时(客户端->服务端),WEB服务器(Nginx,Apache等)会设置该值为客户端 IP;
- 网站存在代理时(客户端->代理->服务端),该值为代理的 IP。
proxy_set_header REMOTE-ADDR $remote_addr;
2. X-Forwarded-For
X-Forwarded-For 是一个 HTTP 扩展头部。HTTP/1.1(RFC 2616)协议并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实 IP。如今它已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。
-
格式为英文逗号 + 空格隔开,例如:X-Forwarded-For: IP0(client), IP1(proxy), IP2(proxy);
-
中间经过的代理,会逐层追加至末尾;
-
IP0 离服务端最远,然后是每一级代理设备的 IP,IP2 直连服务端。
-
如果客户端伪造 IP 地址,格式为:X-Forwarded-For: 伪造的 IP 地址 1, [伪造的 IP 地址 2…], IP0(client), IP1(proxy), IP2(proxy)。
3. X-Real-IP
注:CLB <=> SLB,为腾讯云和阿里云不同产品的称呼,均为负载均衡。
典型的调用链路:
client --> ① [CLB-7]gateway --域名--> ② [CLB-7]server(③ nginx + ④ go/php)
- X-Real-IP 为建立 TCP 连接的上一跳的 IP 地址;
- 对于 ④ 而言,X-Real-IP 为 ① 网关的 NAT 公网出口 IP 地址,或 gateway 的内网 IP 地址,该结论通过生产环境 tcpdump 抓包验证得到;
- 公网调用下,① 网关 调用 ② 7 层 CLB,再到应用层 ③④,此时 ④ 拿到的 X-Real-IP 为 ① 的 NAT 公网出口地址(7 层 CLB 会重写 X-Real-IP 头部,并追加 X-Forwarded-For 头部);
- 内网环境中,原理相似,只不过拿到的是 gateway 的内网 IP 地址;
- 中间可能被 ③ nginx 重写,此时等同于 REMOTE-ADDR。
比如以下最常见的 nginx 配置:
proxy_set_header REMOTE-ADDR $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
REMOTE-ADDR 和 X-Real-IP 都是 nginx 的 $remote_addr 变量,再传递给下游。
三、面临困境
1. 运维侧
- 业务线配置五花八门,没有统一。具体表现在 nginx.conf 和 vhost 配置在不同的业务线有很大区别;
- vhost 成千上万,nginx 内部存在多重转发,外部也有网关转发过来的流量,且网关不止一套,捋不清链路容易导致线上故障;
- 缺乏完善的 QA 验证流程,变更没办法 100% 覆盖测试,最终结果就是尽可能少变更,但这不是长久之计;
- 存在开发自行维护信任 IP 的情况,所以运维不敢随便变更,因为变更前需要通知开发整改,开发有自己的时间排期,处理起来效率极其低下;
- 为了尽可能少修改原先的配置,部分机器组接入了腾讯云的 TOA 模块,用来获取客户端真实 IP 地址,而阿里云没有相似的产品,如果没有统一的方案,没办法上线阿里云,实现不了双云双活的目标等等。
2. 开发侧
各个业务线使用的技术栈不统一,存在多种获取客户端 IP 的方案,需要找到一种尽可能少修改代码,或者一点都不需要修改代码的方案。
PHP 以 Laravel 框架为例(底层是 Symfony 框架),发现内部取了 $_SERVER[‘REMOTE_ADDR’] 变量:
public function getClientIp()
{
$ipAddresses = $this->getClientIps();
return $ipAddresses[0]; // 1. 取第一个 IP 地址。
}
public function getClientIps()
{
$ip = $this->server->get('REMOTE_ADDR');
if (!$this->isFromTrustedProxy())</