嘿,朋友们!今天咱们来聊点Nginx里那些“不说不知道,一说吓一跳”的硬核知识。
你可能早就知道,Nginx能处理海量并发,靠的是它那经典的多worker进程模型:一个老大(Master Process)生了一堆能干的小弟(Worker Processes)。当海量的网络请求像春运人潮一样涌来时,这些小弟们被老大安排得明明白白,各自在自己的检票口(CPU核心)上疯狂检票,分工明确,效率极高。
但是!(注意,这里通常有个“但是”)
你有没有想过一个问题:这些小弟们,平时互相说话吗?
比如,Worker 1 今天检了10086个票,Worker 2 知道吗?有个“黄牛”(恶意IP)在Worker 3的窗口被发现了,Worker 4 能立刻把他拦在门外吗?你想在整个火车站搞个“瞬时人流统计”,该怎么把每个窗口的数据汇总起来?
答案是:默认情况下,他们不说!他们是一群“聋哑人”!
每个Worker进程都活在自己的小世界里,内存空间是隔离的。Worker 1 在自己笔记本上记的账,Worker 2 根本看不见。这就尴尬了,很多需要“全局协作”的事情就搞不定了。
那怎么办?总不能让他们靠吼吧?(进程间通信IPC可是很贵的!)
这时候,就需要我们今天的主角闪亮登场了——共享内存!
你可以把共享内存想象成办公室墙上那块谁都能看见、谁都能写的共享小黑板。
这块黑板被所有Worker小弟共用。任何一个小弟有什么需要通知大家的消息,比如“IP为 192.168.1.100 这家伙是黄牛,别让他进!”,就直接写在黑板上。其他小弟闲下来瞅一眼黑板,就知道了,下次这个IP再来,直接拒之门外。
这块“小黑板”牛在哪?
全局视野: 所有Worker看到的数据都是一致的,打破了信息孤岛。速度炸裂: 直接读写内存,速度比去数据库或者磁盘上查资料快了几个数量级。在高并发场景下,这就是“生死时速”的区别。持久存在: 只要Nginx不重启,这块黑板上的内容就会一直存在(除非你主动擦掉),非常适合存储一些全局的、需要跨请求的状态信息。那么,在Nginx的世界里,我们怎么搞出这么一块“小黑板”呢?这就不得不提两位超级英雄:
ngx_http_lua_module 和它的黄金搭档
lua-resty-core。
lua_shared_dictOpenResty(Nginx + LuaJIT)让我们能够用Lua脚本轻松操作Nginx。而创建共享内存,简单到只需要一行配置:
http {
lua_shared_dict my_shared_dict 10m;
}
来,咱们拆解一下这行“咒语”:
lua_shared_dict:就是命令,告诉Nginx:“喂,给我弄块小黑板!”
my_shared_dict:给这块小黑板起个名字,叫“我的共享字典”。后面我们用的时候就叫这个名字。
10m:这块黑板有多大?10兆字节。你可以根据要存的东西多少来定,比如存点简单的键值对,1m就够用很久了;如果要缓存大量HTML碎片,可能就得100m甚至更大。
这行配置写在
http 块里,意味着这块黑板对所有
server 和
location 都是全局可见的。瞧,黑板挂好了,所有Worker抬头就能看见。
光有黑板不行,得知道往上写什么。这块共享内存小黑板,简直是解决多种高并发痛点的“瑞士军刀”。
场景一:全局限流,专治“黄牛”和“DDoS攻击”
这是最经典的应用。想象一下,你要限制某个API接口,每个IP地址每秒只能请求10次。
没有小黑板时:Worker 1 给IP A记了8次,Worker 2 也给IP A记了7次,结果IP A实际请求了15次,谁也发现不了。
有了小黑板时:所有Worker都往黑板的同一个位置(比如 key 是
rate_limit:ip:A)记数。通过原子性的
incr 操作,IP A的请求次数被精准记录,一旦超过10,所有Worker都会知道并集体拒绝它。
场景二:跨Worker的缓存
你从数据库里费老劲查出来一个用户信息,想缓存起来。如果没有共享内存,每个Worker都得自己去查一遍数据库,然后缓存一份在自己屋里,浪费内存,而且缓存更新不同步。
有了小黑板,一个Worker查出来,直接写在黑板上:“用户123的信息是XXX”。其他Worker需要时,直接来黑板看就行了,省时省力,数据还一致。
场景三:实时状态统计
你想知道整个Nginx服务每秒处理多少请求?当前有多少活跃连接?用共享内存做个计数器,每个Worker处理完一个请求就去黑板上画个“正”字。然后你再用一个单独的接口去读取这个值,一个简陋却高效的实时监控就做好了。
理论吹得再响,不如代码来得实在。下面,我们就用共享内存,实现一个完整的、基于IP的限流功能。
目标: 对
/api/ 接口进行限流,每个IP每秒最多允许请求5次。
准备工作:
确保你安装了 OpenResty。它自带了Nginx、LuaJIT以及我们需要的所有Lua库。步骤一:修改Nginx配置文件 (
nginx.conf)
http {
# 1. 定义我们的“共享小黑板”,命名为‘limit_ip’,大小1m足够用了。
lua_shared_dict limit_ip 1m;
server {
listen 8080;
location /api/ {
# 2. 指定访问这个location时,要执行的限流逻辑
access_by_lua_file /path/to/your/rate_limiter.lua;
# 3. 你的正常业务逻辑(这里用echo模拟)
echo "Hello, API! You are allowed!";
}
# 4. 一个用于展示当前限流状态的接口(可选,用于调试和观察)
location /status {
content_by_lua_block {
local limit_ip = ngx.shared.limit_ip
ngx.say("小黑板 (limit_ip) 当前状态:")
local keys = limit_ip:get_keys(0) -- 获取所有key
for _, key in ipairs(keys) do
local val, flags = limit_ip:get(key)
ngx.say(string.format("Key: [%s] -> Value: %s", key, val))
end
}
}
}
}
步骤二:编写核心的限流Lua脚本 (
rate_limiter.lua)
-- rate_limiter.lua
-- 1. 获取我们的小黑板‘limit_ip’
local shared_dict = ngx.shared.limit_ip
-- 2. 获取客户端IP
local client_ip = ngx.var.remote_addr
-- 为了避免IP中的冒号等问题,可以简单处理一下key
local key = "rate_limit:" .. client_ip
-- 3. 限流参数
local limit = 5 -- 时间窗口内最大请求数
local window = 1 -- 时间窗口大小,单位:秒
-- 4. 核心逻辑:原子性计数
-- 参数:key, 每次增加的值, 初始化值, 过期时间(秒)
local newval, err, forcible = shared_dict:incr(key, 1, 0, window)
if not newval then
-- 如果incr失败(比如内存满了),出于安全考虑,我们选择拒绝请求
ngx.log(ngx.ERR, "Failed to incr key in shared dict: ", err)
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
-- 5. 判断是否超限
if newval > limit then
-- 超限了,返回429(Too Many Requests)
ngx.header["X-RateLimit-Limit"] = limit
ngx.header["X-RateLimit-Remaining"] = 0
ngx.header["X-RateLimit-Reset"] = ngx.now() + window
ngx.status = ngx.HTTP_TOO_MANY_REQUESTS
ngx.say("{"error": "Rate limited. Please slow down."}")
ngx.exit(ngx.status)
else
-- 没超限,允许通过,并在响应头中告诉客户端一些限流信息
ngx.header["X-RateLimit-Limit"] = limit
ngx.header["X-RateLimit-Remaining"] = limit - newval
ngx.header["X-RateLimit-Reset"] = ngx.now() + window
-- 脚本执行完毕,Nginx会继续执行后面的echo指令
end
步骤三:测试一下!
保存配置文件,并重启(或重载)OpenResty:
nginx -s reload打开你的终端,我们用
curl 来模拟疯狂请求。
# 快速连续请求6次
for i in {1..6}; do
echo "请求 $i:"
curl -i http://localhost:8080/api/
echo "------"
sleep 0.1
done
你会看到类似这样的结果:
前5次,你会收到
Hello, API! You are allowed! 以及响应头里的限流信息。
到第6次,你就会看到
{"error": "Rate limited. Please slow down."} 和 429 状态码!
http://localhost:8080/status,你可能会看到类似的内容:
小黑板 (limit_ip) 状态:
Key: [rate_limit:127.0.0.1] -> Value: 6
看,计数器清清楚楚地记着呢!等1秒过后,这个key会自动过期,计数器重置,那个IP就又可以重新访问了。
家规(注意事项):
内存规划:
lua_shared_dict 分配的内存是预分配的,一旦写满,再写入可能会失败(根据设置,可能会淘汰旧数据或直接失败)。所以一定要根据业务量合理设置大小。数据类型: 它只能存字符串和数字值。想存表(table)?先用
cjson.encode() 转换成JSON字符串再存,取出来再用
cjson.decode() 解析。原子性是生命线: 我们例子里的
incr 是原子操作,所以计数准确。但如果是
get -> 计算 ->
set 这种非原子操作,在高并发下会出大问题!务必使用
incr,
add,
replace 等原子方法,或者用
get/
set 时自己用
resty.lock(共享内存锁)来保护。
骚操作(高级技巧):
缓存失效风暴: 当缓存同时大量失效,所有请求都涌向数据库,就是“缓存雪崩”。你可以在共享内存里存一个比缓存本身稍长的“锁标记”,一个Worker去重建缓存时,先把标记写上,其他Worker看到有标记,就耐心等一会儿,而不是都去重建。简单的服务发现: 在微服务架构中,你可以把健康的 upstream 节点列表写在共享内存里,所有Worker都能第一时间感知到节点变化,实现非常轻量级的负载均衡和故障转移。看,就是这么一块看似简单的“共享小黑板”——共享内存,彻底改变了Nginx Worker们的工作方式。它让一群各自为战的“聋哑工人”,进化成了一支配合默契、信息同步的“超级战队”。
它解决了高并发服务中最棘手的状态共享和全局协调问题。限流、缓存、统计,这些曾经需要依赖外部组件(如Redis,虽然Redis也很强)的复杂功能,现在在Nginx内部就能以近乎极限的速度完成。
所以,下次当你设计一个高性能的Nginx服务时,别再让你的Worker们当“哑巴”了。给他们一块
lua_shared_dict 小黑板,你会发现,你的服务性能和高可用性,瞬间就能提升一个维度!
现在,就去你的Nginx配置里,挂上第一块小黑板吧!