Nginx基础教程(68)Nginx辅助设施之共享内存:别让Nginx workers变成“聋哑人”!共享内存:这个“小黑板”才是高并发背后的真·大腿

  • 时间:2025-12-02 21:56 作者: 来源: 阅读:4
  • 扫一扫,手机访问
摘要:嘿,朋友们!今天咱们来聊点Nginx里那些“不说不知道,一说吓一跳”的硬核知识。 你可能早就知道,Nginx能处理海量并发,靠的是它那经典的多worker进程模型:一个老大(Master Process)生了一堆能干的小弟(Worker Processes)。当海量的网络请求像春运人潮一样涌来时,这些小弟们被老大安排得明明白白,各自在自己的检票口(CPU核心)上疯狂检票,分工明确,效率极高。

嘿,朋友们!今天咱们来聊点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

第二章:怎么造这块“小黑板”?—— OpenResty 的 lua_shared_dict

OpenResty(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限流器

理论吹得再响,不如代码来得实在。下面,我们就用共享内存,实现一个完整的、基于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配置里,挂上第一块小黑板吧!

  • 全部评论(0)
手机二维码手机访问领取大礼包
返回顶部