如何通过 OpenResty 和 Redis 实现高效动态 IP 黑名单同步?
- 内容介绍
- 文章标签
- 相关推荐
本文共计959个文字,预计阅读时间需要4分钟。
直接封锁IP本体不难,困难的是封锁了即时生效。多worker不重复查Redis,解锁自动触发且不漏。这三点处理不好,就容易出现封锁了还通、解封不及时、高并发下Redis手忙脚乱的问题。
access_by_lua_file 里必须用 shared dict 缓存黑名单结果
每次请求都去 Redis 查 banned:1.2.3.4,QPS 上千时 Redis 连接和延迟会成瓶颈。OpenResty 的 shared dict 是跨 worker 共享的内存字典,适合缓存「封禁状态 + 过期时间」。
- 在
http块里定义:lua_shared_dict ip_ban_cache 10m; - Lua 脚本里先查
ip_ban_cache:get(ip),命中就直接ngx.exit(403) - 未命中再查 Redis;若 Redis 返回被封,就用
ip_ban_cache:set(ip, true, ttl)写入(ttl 要比 Redis 的 TTL 小 1–2 秒,避免缓存比 Redis 多留几秒) - 注意:shared dict 的过期是惰性删除,不能依赖它自动清理,得靠 Redis 的 TTL 主动控制生命周期
获取真实客户端 IP 必须按 X-Real-IP → X-Forwarded-For → remote_addr 顺序 fallback
反向代理(如 SLB、CDN、Nginx 前置)会覆盖 remote_addr 为上一跳地址,直接用它封错 IP 是最常见失误。
- 在 Lua 脚本里写:
local ip = ngx.var.http_x_real_ip or (ngx.var.http_x_forwarded_for and string.match(ngx.var.http_x_forwarded_for, "([^,]+)")) or ngx.var.remote_addr - 确保前置代理已设置
proxy_set_header X-Real-IP $remote_addr;和proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - 如果只信任某几个代理 IP(比如你自己的 Nginx 集群),加一层校验:
if not is_trusted_proxy(ngx.var.realip_remote_addr) then ip = ngx.var.remote_addr end
Redis 连接必须复用 + 设置合理超时
每个请求新建 Redis 连接,1000 QPS 就是 1000 个 socket,很快打满连接数或触发 TIME_WAIT 拥塞。
- 用
resty.redis:new()创建 client 后,调用red:set_keepalive(60000, 100)把连接放回连接池(第一个参数是空闲超时毫秒,第二个是最大空闲连接数) -
red:set_timeout(500)比默认 1000 更激进,防慢查询拖垮整个 worker - 连接失败时不要静默吞掉错误:
if not ok then ngx.log(ngx.WARN, "redis connect failed: ", err) return end,否则你会以为没封成功其实是连不上 Redis - 别用
red:close()—— 它会强行断开,破坏连接复用;让set_keepalive自己管理就行
自动解封必须靠 Redis TTL,不能只靠 shared dict
shared dict 的过期不可靠,尤其在低流量时段,key 可能长期滞留内存。真正解封动作必须由 Redis 的过期事件驱动。
- 封禁时用
red:setex("banned:"..ip, ttl_seconds, "1"),不是set+expire两步(非原子) - 如果需要记录封禁原因或时间,值可以是 JSON 字符串,但别太大(
- 运维接口(如
POST /api/block?ip=1.2.3.4&ttl=3600)要同步写 Redis 和清空 shared dict:ip_ban_cache:delete(ip),否则新请求仍走旧缓存 - 别忘了日志:
ngx.log(ngx.WARN, "Blocked IP ", ip, " for ", ttl_seconds, "s"),error log 是唯一可信的审计依据
共享内存和 Redis 的 TTL 协同是核心,但最容易被忽略的是「连接复用」和「真实 IP 提取逻辑」——这两个点一旦出错,整个动态黑名单就形同虚设,看着在跑,其实根本没生效。
本文共计959个文字,预计阅读时间需要4分钟。
直接封锁IP本体不难,困难的是封锁了即时生效。多worker不重复查Redis,解锁自动触发且不漏。这三点处理不好,就容易出现封锁了还通、解封不及时、高并发下Redis手忙脚乱的问题。
access_by_lua_file 里必须用 shared dict 缓存黑名单结果
每次请求都去 Redis 查 banned:1.2.3.4,QPS 上千时 Redis 连接和延迟会成瓶颈。OpenResty 的 shared dict 是跨 worker 共享的内存字典,适合缓存「封禁状态 + 过期时间」。
- 在
http块里定义:lua_shared_dict ip_ban_cache 10m; - Lua 脚本里先查
ip_ban_cache:get(ip),命中就直接ngx.exit(403) - 未命中再查 Redis;若 Redis 返回被封,就用
ip_ban_cache:set(ip, true, ttl)写入(ttl 要比 Redis 的 TTL 小 1–2 秒,避免缓存比 Redis 多留几秒) - 注意:shared dict 的过期是惰性删除,不能依赖它自动清理,得靠 Redis 的 TTL 主动控制生命周期
获取真实客户端 IP 必须按 X-Real-IP → X-Forwarded-For → remote_addr 顺序 fallback
反向代理(如 SLB、CDN、Nginx 前置)会覆盖 remote_addr 为上一跳地址,直接用它封错 IP 是最常见失误。
- 在 Lua 脚本里写:
local ip = ngx.var.http_x_real_ip or (ngx.var.http_x_forwarded_for and string.match(ngx.var.http_x_forwarded_for, "([^,]+)")) or ngx.var.remote_addr - 确保前置代理已设置
proxy_set_header X-Real-IP $remote_addr;和proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - 如果只信任某几个代理 IP(比如你自己的 Nginx 集群),加一层校验:
if not is_trusted_proxy(ngx.var.realip_remote_addr) then ip = ngx.var.remote_addr end
Redis 连接必须复用 + 设置合理超时
每个请求新建 Redis 连接,1000 QPS 就是 1000 个 socket,很快打满连接数或触发 TIME_WAIT 拥塞。
- 用
resty.redis:new()创建 client 后,调用red:set_keepalive(60000, 100)把连接放回连接池(第一个参数是空闲超时毫秒,第二个是最大空闲连接数) -
red:set_timeout(500)比默认 1000 更激进,防慢查询拖垮整个 worker - 连接失败时不要静默吞掉错误:
if not ok then ngx.log(ngx.WARN, "redis connect failed: ", err) return end,否则你会以为没封成功其实是连不上 Redis - 别用
red:close()—— 它会强行断开,破坏连接复用;让set_keepalive自己管理就行
自动解封必须靠 Redis TTL,不能只靠 shared dict
shared dict 的过期不可靠,尤其在低流量时段,key 可能长期滞留内存。真正解封动作必须由 Redis 的过期事件驱动。
- 封禁时用
red:setex("banned:"..ip, ttl_seconds, "1"),不是set+expire两步(非原子) - 如果需要记录封禁原因或时间,值可以是 JSON 字符串,但别太大(
- 运维接口(如
POST /api/block?ip=1.2.3.4&ttl=3600)要同步写 Redis 和清空 shared dict:ip_ban_cache:delete(ip),否则新请求仍走旧缓存 - 别忘了日志:
ngx.log(ngx.WARN, "Blocked IP ", ip, " for ", ttl_seconds, "s"),error log 是唯一可信的审计依据
共享内存和 Redis 的 TTL 协同是核心,但最容易被忽略的是「连接复用」和「真实 IP 提取逻辑」——这两个点一旦出错,整个动态黑名单就形同虚设,看着在跑,其实根本没生效。

