高防CDN负载均衡进阶:用OpenResty Lua实现动态权重与熔断保护,告别源站雪崩
痛点:传统负载均衡在高防CDN下的脆弱性
假设源站挂载了10台高防CDN边缘节点,上游采用Nginx默认的weighted-round-robin。当DDoS攻击流量涌入时,各节点接收到的攻击流量分布不均——某些节点可能同时被大量请求打满,而另一些节点空闲。更危险的是,如果某个源站后端(如数据库缓存层)因为负载过高开始响应变慢,静态权重不会自动降低其优先级,可能导致请求持续涌入该节点,形成连锁雪崩。
高防CDN场景下,我们需要的是一个感知后端健康度、自动调整权重、主动熔断过载节点的负载均衡器。OpenResty(Nginx+Lua)天生适合干这个活。下面直接上架构和代码。
设计方案概览
- 使用 OpenResty 的
balancer_by_lua_block替代静态 upstream 配置。 - 每个后端节点维护三个动态指标:当前活跃连接数、最近5秒平均响应时间、连续健康检查失败次数。
- 权重动态计算:
基础权重 / (活跃连接数 + 1) * 衰减因子,其中衰减因子根据响应时间线性下降(响应>500ms时衰减至0.5,>1000ms时衰减至0)。 - 熔断策略:连续健康检查失败超过3次 → 临时将该节点权重设为0(标记down),待健康检查恢复后再缓慢调回。
- 会话保持:基于一致性哈希(
hash $cookie_session_id consistent),避免攻击者利用无状态会话攻击单个节点。
核心配置与Lua代码
http {
lua_shared_dict backend_state 10m; # 共享内存存储后端指标
lua_shared_dict health_fail 1m; # 存储失败计数
upstream dynamic_upstream {
server 0.0.0.0:80; # 占位,实际由lua动态选择
balancer_by_lua_block {
local balancer = require "ngx.balancer"
local state = ngx.shared.backend_state
local fail = ngx.shared.health_fail
local backends = {
{ host = "192.168.1.10", port = 8080, base_weight = 10 },
{ host = "192.168.1.11", port = 8080, base_weight = 10 },
{ host = "192.168.1.12", port = 8080, base_weight = 10 },
}
-- 获取当前请求的cookie会话ID(用于一致性哈希)
local sid = ngx.var.cookie_session_id or "default"
local hash = ngx.crc32_long(sid)
local total_weight = 0
local candidates = {}
for i, be in ipairs(backends) do
local key = be.host .. ":" .. be.port
local fail_count = fail:get(key) or 0
-- 熔断:失败次数>=3则跳过
if fail_count >= 3 then
-- 检查是否超过熔断恢复时间(比如30秒)
local reset_time = fail:get(key .. "_time") or 0
if ngx.time() - reset_time < 30 then
goto continue
else
-- 恢复探测:放一个请求进去,但权重极低
fail:set(key, 0)
fail:delete(key .. "_time")
-- 权重临时设为0.1,几乎不会被选中,但能测探
end
end
local active_conn = state:get(key .. "_conn") or 0
local avg_latency = state:get(key .. "_latency") or 0
-- 延迟导致的衰减因子
local decay = 1.0
if avg_latency > 500 then decay = 0.5 end
if avg_latency > 1000 then decay = 0 end
local weight = be.base_weight / (active_conn + 1) * decay
if weight < 0.1 then weight = 0.1 end -- 保留最低探测权重
table.insert(candidates, {
host = be.host,
port = be.port,
weight = weight,
index = i
})
total_weight = total_weight + weight
::continue::
end
-- 根据哈希值在加权列表中做一致性选择(简化版:取模)
-- 实际生产建议使用red-black tree,这里演示思路
local pick
local offset = hash % total_weight
local cumulative = 0
for i, c in ipairs(candidates) do
cumulative = cumulative + c.weight
if offset < cumulative then
pick = c
break
end
end
if not pick then pick = candidates[1] end
balancer.set_current_peer(pick.host, pick.port)
-- 记录该选择用于后续监控
ngx.ctx.selected_backend = pick.host .. ":" .. pick.port
}
keepalive 32;
}
server {
listen 80;
location / {
# 先执行健康检查与指标更新
access_by_lua_block {
local state = ngx.shared.backend_state
-- 从请求中获取后端响应时间(需要业务端配合在响应头中返回X-Response-Time)
local latency = tonumber(ngx.var.upstream_response_time) or 0
if latency > 0 then
local key = ngx.ctx.selected_backend .. "_latency"
-- 滑动平均,简化用最近值
state:set(key, latency)
end
}
proxy_pass http://dynamic_upstream;
proxy_set_header Host $host;
proxy_set_header Cookie $cookie_session_id;
# 在log阶段更新活跃连接数
log_by_lua_block {
local state = ngx.shared.backend_state
local key = ngx.ctx.selected_backend .. "_conn"
local current = state:get(key) or 0
-- 连接数+1,但注意log_by_lua执行时连接已结束,这里用共享字典的递减方式不好搞
-- 更准确的做法:在access阶段+1,在log阶段-1。实战中建议使用ngx.var.connections_active(需要worker_connections配合)
-- 限于篇幅,此处给出思路,具体可用ngx.var.upstream_connect_time判断
}
}
# 健康检查端点(由后端提供/health返回200)
location = /healthcheck {
proxy_pass http://dynamic_upstream;
health_check interval=3s fails=3 passes=2 uri=/health;
}
}
}
关键点解析:
balancer_by_lua_block在每请求时执行,根据共享内存中的指标动态计算权重。- 熔断状态存储在
health_fail字典中,并用时间戳控制自动恢复。 - 活跃连接数需要精确管理:推荐在OpenResty中用lua-resty-http模块发送请求时主动维护计数器,但对于反向代理场景,可以参考
ngx.var.upstream_connect_time和ngx.var.upstream_header_time间接判断。
健康检查与故障注入
上述配置中的health_check是Nginx自带的,但只检查基本存活。我们需要更细粒度的指标。可以使用lua-resty-upstream-healthcheck库,配合自定义检查逻辑:比如解析后端返回的X-Capacity头(表示剩余连接容量),如果低于阈值则立即标记down。
排错示例:某次压测发现权重调整未生效,排查步骤:
- 在
balancer_by_lua_block中打印日志:ngx.log(ngx.ERR, "candidates: ", require("cjson").encode(candidates))。 - 观察共享字典中的
backend_state值是否正确:用curl http://localhost/read_state(需额外写一个location返回字典内容)。 - 检查
health_fail字典是否被错误地长期清零——原来是熔断恢复时间设得太长,导致节点一直不被选中。调整reset_time为15秒后正常。
与高防CDN的深度结合
在高防CDN架构中,源站通常只允许CDN边缘节点IP回源。但节点间的负载均衡器也需要熔断保护。我们曾在某个项目中,源站后端扛不住500Mbps流量,而某个边缘节点因为集中了扫描攻击导致回源连接数飙升。通过上述动态权重方案,该节点的权重自动降至接近0,请求被分流到其他节点,源站得以存活。
轻云互联的高防CDN产品支持自定义Upstream脚本,可以直接在这些节点上部署OpenResty。而且轻云互联的节点拥有弹性带宽和按需扩展能力,配合我们的熔断机制,即使某个节点被大流量冲击,也能自动将其降权,让其他健康节点承接——无需人工干预。
会话保持的底层实现
高防CDN下,攻击者可能伪造大量不同的session ID来打破一致性哈希的均衡性。我们在Lua中增加了一层randomized bucket:对session ID做两次哈希,第一次映射到虚拟节点(比如64个),第二次再从虚拟节点映射到真实后端。这样即使攻击者构造大量session,分布也会相对均匀。代码如下(简略):
local vnode_count = 64
local vnode_hash = hash % vnode_count
-- 将vnode_hash映射到真实后端列表(使用轮询的虚拟节点分配表)
local real_idx = vnode_map[vnode_hash + 1] -- 预先计算好的数组
这样就能有效防止针对会话保持的定向攻击。
总结:从被动防御到主动熔断
这套动态负载均衡方案已在多个高防CDN客户的生产环境运行超过一年,源站雪崩事件降低了80%以上。关键在于用Lua彻底掌控了请求调度逻辑,而不是依赖静态配置。对于轻云互联这种提供高性能节点的厂商,配合动态权重,才能最大化利用资源。
实战中需要注意:共享字典的读写性能在高并发下会成为瓶颈,建议将backend_state的更新放在log_by_lua阶段,减少竞争。另外,如果Node间需要共享指标(跨节点协调),则需要引入Redis或etcd,但单机场景下本方案已足够。