系列定位:这是 16 篇「从普及到源码」系列的第 9 篇,进入阶段三(源码深潜)。第 8 篇我们聊了 AI 网关的「灵魂三件套」------审计、脱敏、计费。这一篇往下走一层:这些能力到底跑在一个什么样的运行时上? 一个请求进到数据面,底层到底发生了什么?
读完这篇,你会得到一个完整的心智模型:为什么 AI 网关几乎清一色选 OpenResty?一个 worker 怎么扛几万连接?Lua 协程是什么、为什么它能让同步写法跑出异步性能?以及这套模型的三条边界------CPU 密集、生态、适用流量。
一、为什么先说「阶段」,现在才说「运行时」
前几篇我们把请求生命周期拆成 rewrite/access/content 等阶段,把插件执行顺序讲透了。但一直有个问题没说破:这些「阶段」和「插件」到底跑在一个什么样的底座上?
打个比方:阶段和插件是「业务流程」,运行时是「操作系统」。只懂业务流程,你永远不知道为什么某个插件会拖垮整个 worker、为什么流式响应会让 body_filter 被调用几十次、为什么「在 access 阶段等响应」是个灾难。
所以这一篇,我们把镜头怼到最底层。
二、原理剖析:事件循环、非阻塞 IO 与 Lua 协程
2.1 为什么 AI 网关普遍选 OpenResty,而不是纯 Nginx 或纯 Go
这个问题被问烂了,但很少有人答到点子上。标准答案通常是「Nginx 性能好 + Lua 灵活」。这只说对了一半。真正的核心是:
AI 网关的本质,是「把 LLM 的字节流按语义转发」。它需要既有 Nginx 级别的网络吞吐,又有「逐帧解析 SSE、跨块拼接工具调用」这种业务灵活性。纯 Nginx 做不到后者,纯 Go 做不到前者(或者说做起来成本极高)。
用一张图看三家方案的定位差异:
#mermaid-svg-dnyByTROc6mir6qg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dnyByTROc6mir6qg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dnyByTROc6mir6qg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dnyByTROc6mir6qg .error-icon{fill:#552222;}#mermaid-svg-dnyByTROc6mir6qg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dnyByTROc6mir6qg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dnyByTROc6mir6qg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dnyByTROc6mir6qg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dnyByTROc6mir6qg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dnyByTROc6mir6qg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dnyByTROc6mir6qg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dnyByTROc6mir6qg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dnyByTROc6mir6qg .marker.cross{stroke:#333333;}#mermaid-svg-dnyByTROc6mir6qg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dnyByTROc6mir6qg p{margin:0;}#mermaid-svg-dnyByTROc6mir6qg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} OpenResty (Nginx+LuaJIT) 纯 Go 网关 纯 Nginx + C 模块 网络吞吐弱网络吞吐强业务灵活性低业务灵活性高 网关技术选型定位图
当然,这张图是简化后的示意,实际选型还要考虑团队栈、生态成熟度、运维成本。但「吞吐 × 灵活性」这两个轴,基本框定了 AI 网关的选型空间。
OpenResty 的答案很直接:Nginx 的事件循环 + LuaJIT 的动态逻辑。Nginx 负责扛连接、扛吞吐,Lua 负责在关键钩子上写业务逻辑------协议识别、字段清洗、token 计费、SSE 分帧。两者各司其职,这就是 OpenResty 的全部秘密。
2.2 事件驱动 + 非阻塞 IO:一个 worker 为什么能扛几万连接
先纠正一个常见误解:「一个 worker 扛几万连接」不是 Lua 的功劳,是 Nginx 事件循环的功劳。
传统 BIO(Blocking IO)模型下,一个连接需要一个线程。1 万连接 = 1 万线程。每个线程 1MB 栈,内存直接爆炸。而且线程切换有内核开销,CPU 大部分时间在「切线程」而不是「干活」。
Nginx 的解法是事件驱动:一个线程(worker 进程)+ 一个事件循环(epoll)+ 几万个「挂起」的连接。 连接没有数据到达时,它只是 epoll 等待队列里的一个 fd,不占 CPU、不占线程。数据到了,事件循环唤醒对应的处理逻辑,处理完立刻回到等待状态。
#mermaid-svg-M0OWAi2ET3nI7qRJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-M0OWAi2ET3nI7qRJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-M0OWAi2ET3nI7qRJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-M0OWAi2ET3nI7qRJ .error-icon{fill:#552222;}#mermaid-svg-M0OWAi2ET3nI7qRJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-M0OWAi2ET3nI7qRJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-M0OWAi2ET3nI7qRJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-M0OWAi2ET3nI7qRJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-M0OWAi2ET3nI7qRJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-M0OWAi2ET3nI7qRJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-M0OWAi2ET3nI7qRJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-M0OWAi2ET3nI7qRJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-M0OWAi2ET3nI7qRJ .marker.cross{stroke:#333333;}#mermaid-svg-M0OWAi2ET3nI7qRJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-M0OWAi2ET3nI7qRJ p{margin:0;}#mermaid-svg-M0OWAi2ET3nI7qRJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-M0OWAi2ET3nI7qRJ .cluster-label text{fill:#333;}#mermaid-svg-M0OWAi2ET3nI7qRJ .cluster-label span{color:#333;}#mermaid-svg-M0OWAi2ET3nI7qRJ .cluster-label span p{background-color:transparent;}#mermaid-svg-M0OWAi2ET3nI7qRJ .label text,#mermaid-svg-M0OWAi2ET3nI7qRJ span{fill:#333;color:#333;}#mermaid-svg-M0OWAi2ET3nI7qRJ .node rect,#mermaid-svg-M0OWAi2ET3nI7qRJ .node circle,#mermaid-svg-M0OWAi2ET3nI7qRJ .node ellipse,#mermaid-svg-M0OWAi2ET3nI7qRJ .node polygon,#mermaid-svg-M0OWAi2ET3nI7qRJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-M0OWAi2ET3nI7qRJ .rough-node .label text,#mermaid-svg-M0OWAi2ET3nI7qRJ .node .label text,#mermaid-svg-M0OWAi2ET3nI7qRJ .image-shape .label,#mermaid-svg-M0OWAi2ET3nI7qRJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-M0OWAi2ET3nI7qRJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-M0OWAi2ET3nI7qRJ .rough-node .label,#mermaid-svg-M0OWAi2ET3nI7qRJ .node .label,#mermaid-svg-M0OWAi2ET3nI7qRJ .image-shape .label,#mermaid-svg-M0OWAi2ET3nI7qRJ .icon-shape .label{text-align:center;}#mermaid-svg-M0OWAi2ET3nI7qRJ .node.clickable{cursor:pointer;}#mermaid-svg-M0OWAi2ET3nI7qRJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-M0OWAi2ET3nI7qRJ .arrowheadPath{fill:#333333;}#mermaid-svg-M0OWAi2ET3nI7qRJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-M0OWAi2ET3nI7qRJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-M0OWAi2ET3nI7qRJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M0OWAi2ET3nI7qRJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-M0OWAi2ET3nI7qRJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M0OWAi2ET3nI7qRJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-M0OWAi2ET3nI7qRJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-M0OWAi2ET3nI7qRJ .cluster text{fill:#333;}#mermaid-svg-M0OWAi2ET3nI7qRJ .cluster span{color:#333;}#mermaid-svg-M0OWAi2ET3nI7qRJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-M0OWAi2ET3nI7qRJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-M0OWAi2ET3nI7qRJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-M0OWAi2ET3nI7qRJ .icon-shape,#mermaid-svg-M0OWAi2ET3nI7qRJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M0OWAi2ET3nI7qRJ .icon-shape p,#mermaid-svg-M0OWAi2ET3nI7qRJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-M0OWAi2ET3nI7qRJ .icon-shape .label rect,#mermaid-svg-M0OWAi2ET3nI7qRJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M0OWAi2ET3nI7qRJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-M0OWAi2ET3nI7qRJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-M0OWAi2ET3nI7qRJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 事件驱动模型:1 worker = 1 epoll
epoll 事件循环
fd 1
fd 2
fd N
BIO 模型:1 连接 = 1 线程
线程 1
连接 1
线程 2
连接 2
线程 N
连接 N
事件循环的核心思想用一句话概括:不要让 CPU 等待,让等待的连接排队。 一个 worker(单线程)就能并发处理几万个连接,因为绝大多数连接在绝大多数时刻都处于「等数据」状态,事件循环只需要在「数据到达」的时刻调度处理逻辑。
2.3 Nginx 的 11 个处理阶段,以及 OpenResty 暴露出来的那几个
Nginx 内部有 11 个处理阶段,从 POST_READ 到 LOG 阶段。OpenResty 暴露出来给我们写 Lua 的,是其中几个关键钩子。
先看全貌:
#mermaid-svg-AEusrhbqF6K3Ux52{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AEusrhbqF6K3Ux52 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AEusrhbqF6K3Ux52 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AEusrhbqF6K3Ux52 .error-icon{fill:#552222;}#mermaid-svg-AEusrhbqF6K3Ux52 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AEusrhbqF6K3Ux52 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AEusrhbqF6K3Ux52 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AEusrhbqF6K3Ux52 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AEusrhbqF6K3Ux52 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AEusrhbqF6K3Ux52 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AEusrhbqF6K3Ux52 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AEusrhbqF6K3Ux52 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AEusrhbqF6K3Ux52 .marker.cross{stroke:#333333;}#mermaid-svg-AEusrhbqF6K3Ux52 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AEusrhbqF6K3Ux52 p{margin:0;}#mermaid-svg-AEusrhbqF6K3Ux52 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AEusrhbqF6K3Ux52 .cluster-label text{fill:#333;}#mermaid-svg-AEusrhbqF6K3Ux52 .cluster-label span{color:#333;}#mermaid-svg-AEusrhbqF6K3Ux52 .cluster-label span p{background-color:transparent;}#mermaid-svg-AEusrhbqF6K3Ux52 .label text,#mermaid-svg-AEusrhbqF6K3Ux52 span{fill:#333;color:#333;}#mermaid-svg-AEusrhbqF6K3Ux52 .node rect,#mermaid-svg-AEusrhbqF6K3Ux52 .node circle,#mermaid-svg-AEusrhbqF6K3Ux52 .node ellipse,#mermaid-svg-AEusrhbqF6K3Ux52 .node polygon,#mermaid-svg-AEusrhbqF6K3Ux52 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AEusrhbqF6K3Ux52 .rough-node .label text,#mermaid-svg-AEusrhbqF6K3Ux52 .node .label text,#mermaid-svg-AEusrhbqF6K3Ux52 .image-shape .label,#mermaid-svg-AEusrhbqF6K3Ux52 .icon-shape .label{text-anchor:middle;}#mermaid-svg-AEusrhbqF6K3Ux52 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AEusrhbqF6K3Ux52 .rough-node .label,#mermaid-svg-AEusrhbqF6K3Ux52 .node .label,#mermaid-svg-AEusrhbqF6K3Ux52 .image-shape .label,#mermaid-svg-AEusrhbqF6K3Ux52 .icon-shape .label{text-align:center;}#mermaid-svg-AEusrhbqF6K3Ux52 .node.clickable{cursor:pointer;}#mermaid-svg-AEusrhbqF6K3Ux52 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AEusrhbqF6K3Ux52 .arrowheadPath{fill:#333333;}#mermaid-svg-AEusrhbqF6K3Ux52 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AEusrhbqF6K3Ux52 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AEusrhbqF6K3Ux52 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AEusrhbqF6K3Ux52 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AEusrhbqF6K3Ux52 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AEusrhbqF6K3Ux52 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AEusrhbqF6K3Ux52 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AEusrhbqF6K3Ux52 .cluster text{fill:#333;}#mermaid-svg-AEusrhbqF6K3Ux52 .cluster span{color:#333;}#mermaid-svg-AEusrhbqF6K3Ux52 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AEusrhbqF6K3Ux52 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AEusrhbqF6K3Ux52 rect.text{fill:none;stroke-width:0;}#mermaid-svg-AEusrhbqF6K3Ux52 .icon-shape,#mermaid-svg-AEusrhbqF6K3Ux52 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AEusrhbqF6K3Ux52 .icon-shape p,#mermaid-svg-AEusrhbqF6K3Ux52 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AEusrhbqF6K3Ux52 .icon-shape .label rect,#mermaid-svg-AEusrhbqF6K3Ux52 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AEusrhbqF6K3Ux52 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AEusrhbqF6K3Ux52 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AEusrhbqF6K3Ux52 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求
客户端
POST_READ
连接建立初期
SERVER_REWRITE
server 级 rewrite
PREACCESS
限流、限速前置检查
ACCESS
认证、鉴权、准入控制
CONTENT
生成响应或内部转发
LOG 阶段
访问日志、审计日志
客户端
这张图里藏着 AI 网关的关键设计。注意 ACCESS 和 CONTENT 两个阶段在 AI 网关里的分工:
- ACCESS 阶段:AI 网关在这里做认证、鉴权、限流。这个阶段在「转发给上游之前」,所以它叫「准入控制」。在这里能拿到请求 body,做协议识别、内容审核、脱敏、计费初始化。
- CONTENT 阶段 :AI 网关在这里做「生成响应或内部转发」。AI 网关的流式转发逻辑主要跑在这个阶段,
ngx.print/ngx.flush/ngx.sleep都在这里编排。 - LOG 阶段:所有处理结束后的「善后」。AI 网关在这里做 token 计费、审计日志落盘。注意这个阶段是「请求已经结束后」才跑的,所以它不能用来「等响应」------但可以用来「事后记账」。
2.4 Lua 协程:让同步写法跑出异步性能的那块拼图
事件循环解决了「并发连接数」的问题,但还有一个问题没解决:在单线程事件循环里,怎么写「读上游 → 处理 → 写下游」这种多步异步流程?
传统写法有两种烂方案:
- 回调地狱:每一步异步操作都挂一个回调,业务逻辑被切得粉碎。
- 同步阻塞写法 :直接
socket:read()。但事件循环是单线程的,一阻塞,整个 worker 全停。
Lua 协程是第三条路。用一张图看它和回调的区别:
#mermaid-svg-UPnThmjUnA6jgpNt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UPnThmjUnA6jgpNt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UPnThmjUnA6jgpNt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UPnThmjUnA6jgpNt .error-icon{fill:#552222;}#mermaid-svg-UPnThmjUnA6jgpNt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UPnThmjUnA6jgpNt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UPnThmjUnA6jgpNt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UPnThmjUnA6jgpNt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UPnThmjUnA6jgpNt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UPnThmjUnA6jgpNt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UPnThmjUnA6jgpNt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UPnThmjUnA6jgpNt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UPnThmjUnA6jgpNt .marker.cross{stroke:#333333;}#mermaid-svg-UPnThmjUnA6jgpNt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UPnThmjUnA6jgpNt p{margin:0;}#mermaid-svg-UPnThmjUnA6jgpNt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UPnThmjUnA6jgpNt .cluster-label text{fill:#333;}#mermaid-svg-UPnThmjUnA6jgpNt .cluster-label span{color:#333;}#mermaid-svg-UPnThmjUnA6jgpNt .cluster-label span p{background-color:transparent;}#mermaid-svg-UPnThmjUnA6jgpNt .label text,#mermaid-svg-UPnThmjUnA6jgpNt span{fill:#333;color:#333;}#mermaid-svg-UPnThmjUnA6jgpNt .node rect,#mermaid-svg-UPnThmjUnA6jgpNt .node circle,#mermaid-svg-UPnThmjUnA6jgpNt .node ellipse,#mermaid-svg-UPnThmjUnA6jgpNt .node polygon,#mermaid-svg-UPnThmjUnA6jgpNt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UPnThmjUnA6jgpNt .rough-node .label text,#mermaid-svg-UPnThmjUnA6jgpNt .node .label text,#mermaid-svg-UPnThmjUnA6jgpNt .image-shape .label,#mermaid-svg-UPnThmjUnA6jgpNt .icon-shape .label{text-anchor:middle;}#mermaid-svg-UPnThmjUnA6jgpNt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UPnThmjUnA6jgpNt .rough-node .label,#mermaid-svg-UPnThmjUnA6jgpNt .node .label,#mermaid-svg-UPnThmjUnA6jgpNt .image-shape .label,#mermaid-svg-UPnThmjUnA6jgpNt .icon-shape .label{text-align:center;}#mermaid-svg-UPnThmjUnA6jgpNt .node.clickable{cursor:pointer;}#mermaid-svg-UPnThmjUnA6jgpNt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UPnThmjUnA6jgpNt .arrowheadPath{fill:#333333;}#mermaid-svg-UPnThmjUnA6jgpNt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UPnThmjUnA6jgpNt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UPnThmjUnA6jgpNt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UPnThmjUnA6jgpNt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UPnThmjUnA6jgpNt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UPnThmjUnA6jgpNt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UPnThmjUnA6jgpNt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UPnThmjUnA6jgpNt .cluster text{fill:#333;}#mermaid-svg-UPnThmjUnA6jgpNt .cluster span{color:#333;}#mermaid-svg-UPnThmjUnA6jgpNt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UPnThmjUnA6jgpNt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UPnThmjUnA6jgpNt rect.text{fill:none;stroke-width:0;}#mermaid-svg-UPnThmjUnA6jgpNt .icon-shape,#mermaid-svg-UPnThmjUnA6jgpNt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UPnThmjUnA6jgpNt .icon-shape p,#mermaid-svg-UPnThmjUnA6jgpNt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UPnThmjUnA6jgpNt .icon-shape .label rect,#mermaid-svg-UPnThmjUnA6jgpNt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UPnThmjUnA6jgpNt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UPnThmjUnA6jgpNt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UPnThmjUnA6jgpNt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 协程风格:逻辑是连续的
发起请求
读取响应
处理并转发
flush
回调风格:逻辑被切碎
发起请求
回调 1:解析响应
回调 2:转发下游
回调 3:flush
协程的本质:把「挂起」和「恢复」做成语言级原语。 写代码的人用同步写法,编译器/运行时自动把「等 IO」翻译成「挂起协程,让事件循环去处理别的连接;IO 就绪后恢复协程,接着执行」。这就是 OpenResty 的核心编程模型:同步写法 + 异步性能。
三、实践落地:一个 AI 请求在事件循环里的完整旅程
原理讲完了,带大家走一遍真实链路。一个 AI 请求从客户端到达,到响应流式送回,会经历以下几步:
上游 LLM 网关(access/rewrite 阶段) 客户端 上游 LLM 网关(access/rewrite 阶段) 客户端 #mermaid-svg-JaVRRlywj3AVCQts{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JaVRRlywj3AVCQts .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JaVRRlywj3AVCQts .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JaVRRlywj3AVCQts .error-icon{fill:#552222;}#mermaid-svg-JaVRRlywj3AVCQts .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JaVRRlywj3AVCQts .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JaVRRlywj3AVCQts .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JaVRRlywj3AVCQts .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JaVRRlywj3AVCQts .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JaVRRlywj3AVCQts .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JaVRRlywj3AVCQts .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JaVRRlywj3AVCQts .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JaVRRlywj3AVCQts .marker.cross{stroke:#333333;}#mermaid-svg-JaVRRlywj3AVCQts svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JaVRRlywj3AVCQts p{margin:0;}#mermaid-svg-JaVRRlywj3AVCQts .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JaVRRlywj3AVCQts text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-JaVRRlywj3AVCQts .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JaVRRlywj3AVCQts .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-JaVRRlywj3AVCQts .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-JaVRRlywj3AVCQts .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-JaVRRlywj3AVCQts #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-JaVRRlywj3AVCQts .sequenceNumber{fill:white;}#mermaid-svg-JaVRRlywj3AVCQts #sequencenumber{fill:#333;}#mermaid-svg-JaVRRlywj3AVCQts #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-JaVRRlywj3AVCQts .messageText{fill:#333;stroke:none;}#mermaid-svg-JaVRRlywj3AVCQts .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JaVRRlywj3AVCQts .labelText,#mermaid-svg-JaVRRlywj3AVCQts .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-JaVRRlywj3AVCQts .loopText,#mermaid-svg-JaVRRlywj3AVCQts .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-JaVRRlywj3AVCQts .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JaVRRlywj3AVCQts .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-JaVRRlywj3AVCQts .noteText,#mermaid-svg-JaVRRlywj3AVCQts .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-JaVRRlywj3AVCQts .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JaVRRlywj3AVCQts .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JaVRRlywj3AVCQts .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JaVRRlywj3AVCQts .actorPopupMenu{position:absolute;}#mermaid-svg-JaVRRlywj3AVCQts .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-JaVRRlywj3AVCQts .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JaVRRlywj3AVCQts .actor-man circle,#mermaid-svg-JaVRRlywj3AVCQts line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-JaVRRlywj3AVCQts :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} POST /v1/chat/completions(body 含 messages)认证、鉴权、限流协议识别、模型名改写转发请求(协议转换后)SSE 流(逐块到达)分帧 → 解析 → 转码 → 合帧SSE 流(逐块下发)log 阶段:token 计费、审计日志
3.1 从「生命周期函数」看数据面的真实骨架
这套源码里,数据面核心 server 块的骨架长这样:
nginx
server {
listen 80;
server_name ai-gateway.internal;
# ① 处理 AI 请求的主入口
location /ai/ {
content_by_lua_block {
local ai_core = require "ai_core"
ai_core.handle_ai_request()
}
}
# ② 处理 MCP 协议请求的入口
location /mcp/ {
content_by_lua_block {
local mcp_core = require "mcp_core"
mcp_core.handle_mcp_request()
}
}
# ③ Stream(L4 TCP/UDP)子系统
# 用于需要 TCP/UDP 级代理的场景
}
关键点:数据面的「主入口」是一个 content_by_lua_block。 这意味着 AI 网关的流式转发逻辑、协议转换逻辑、计费逻辑,全都跑在这个 Lua 块里。它不是跑在 rewrite/access 阶段,而是跑在 content 阶段------因为 AI 请求的「响应生成」本身就是 content 阶段的职责(生成响应或内部转发)。
3.2 转发前:access 阶段里到底发生了什么
在 content 阶段之前,access 阶段里 AI 网关会做这些事:
- 认证:验证 API key 或 JWT。
- 限流:按 key、按 IP、按路由维度限流。
- 计费初始化:给请求打上「计费上下文」的标记,便于后续按 token 计费。
- 协议识别 :
detect()函数按顺序问每个协议「这像你吗?」,第一个点头的胜出。 - 内容审核/脱敏:识别出协议后,提取正文和系统提示,交给审核插件和脱敏插件。
- 转发决策:根据模型名查路由表,决定转发给哪个供应商、哪个实例。
- 出站准备 :
prepare_outgoing_request()补头、注入字段、剥字段。
3.3 AI 流式请求:body_filter 会被调用几十次,为什么
这是 AI 网关和普通 API 网关最大的区别。普通网关处理「请求 → 完整响应」的短事务,AI 网关处理的是「逐 token 吐出的 SSE 流」。
body_filter 是 Nginx 的一个钩子,它在「响应体数据经过网关时」被调用。对普通响应,它可能只被调用一次(响应体一次性到达)。但对 SSE 流,上游每吐一个 chunk,body_filter 就被调用一次。一条几千 token 的长流,body_filter 可能被调用几十上百次。
这就是为什么 AI 网关的插件必须「按增量」工作:审计插件要逐块累积正文,脱敏插件要跨块拼接敏感词,计费插件要跨块统计 token。流式场景下,插件的工作模式从「一次性处理」变成了「增量处理」。
3.4 日志与收尾:log 阶段做完了所有「善后」
log 阶段是请求生命周期最末端的钩子,请求已经结束、客户端已经收到响应,log 阶段跑在「请求已结束」的上下文里。AI 网关在这里做 token 计费、审计日志落盘。
关键点:log 阶段不能用来「等响应」,因为请求已经结束了。但它可以用来「事后记账」------在 log 阶段把整个请求的 token 用量、耗时、路由路径、审核结果全部汇总落盘。这也是为什么 AI 网关的计费和审计总是「落后」响应一个身位:它们跑在 log 阶段,而响应已经走了。
另一个关键点:log 阶段有独立的内存池(request pool)。 在 log 阶段做的任何 Lua 代码,都跑在请求的内存池里。在 log 阶段里做内存操作很安全,但如果你在 log 阶段里做阻塞操作,依然会拖垮整个 worker。
四、阶段与插件:一张图看懂执行顺序
前面讲了阶段,也讲了插件。两者的关系用一张图说清楚:
#mermaid-svg-Vp92OtmmBpnlnru0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Vp92OtmmBpnlnru0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Vp92OtmmBpnlnru0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Vp92OtmmBpnlnru0 .error-icon{fill:#552222;}#mermaid-svg-Vp92OtmmBpnlnru0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Vp92OtmmBpnlnru0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Vp92OtmmBpnlnru0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Vp92OtmmBpnlnru0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Vp92OtmmBpnlnru0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Vp92OtmmBpnlnru0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Vp92OtmmBpnlnru0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Vp92OtmmBpnlnru0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Vp92OtmmBpnlnru0 .marker.cross{stroke:#333333;}#mermaid-svg-Vp92OtmmBpnlnru0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Vp92OtmmBpnlnru0 p{margin:0;}#mermaid-svg-Vp92OtmmBpnlnru0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Vp92OtmmBpnlnru0 .cluster-label text{fill:#333;}#mermaid-svg-Vp92OtmmBpnlnru0 .cluster-label span{color:#333;}#mermaid-svg-Vp92OtmmBpnlnru0 .cluster-label span p{background-color:transparent;}#mermaid-svg-Vp92OtmmBpnlnru0 .label text,#mermaid-svg-Vp92OtmmBpnlnru0 span{fill:#333;color:#333;}#mermaid-svg-Vp92OtmmBpnlnru0 .node rect,#mermaid-svg-Vp92OtmmBpnlnru0 .node circle,#mermaid-svg-Vp92OtmmBpnlnru0 .node ellipse,#mermaid-svg-Vp92OtmmBpnlnru0 .node polygon,#mermaid-svg-Vp92OtmmBpnlnru0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Vp92OtmmBpnlnru0 .rough-node .label text,#mermaid-svg-Vp92OtmmBpnlnru0 .node .label text,#mermaid-svg-Vp92OtmmBpnlnru0 .image-shape .label,#mermaid-svg-Vp92OtmmBpnlnru0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Vp92OtmmBpnlnru0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Vp92OtmmBpnlnru0 .rough-node .label,#mermaid-svg-Vp92OtmmBpnlnru0 .node .label,#mermaid-svg-Vp92OtmmBpnlnru0 .image-shape .label,#mermaid-svg-Vp92OtmmBpnlnru0 .icon-shape .label{text-align:center;}#mermaid-svg-Vp92OtmmBpnlnru0 .node.clickable{cursor:pointer;}#mermaid-svg-Vp92OtmmBpnlnru0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Vp92OtmmBpnlnru0 .arrowheadPath{fill:#333333;}#mermaid-svg-Vp92OtmmBpnlnru0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Vp92OtmmBpnlnru0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Vp92OtmmBpnlnru0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Vp92OtmmBpnlnru0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Vp92OtmmBpnlnru0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Vp92OtmmBpnlnru0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Vp92OtmmBpnlnru0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Vp92OtmmBpnlnru0 .cluster text{fill:#333;}#mermaid-svg-Vp92OtmmBpnlnru0 .cluster span{color:#333;}#mermaid-svg-Vp92OtmmBpnlnru0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Vp92OtmmBpnlnru0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Vp92OtmmBpnlnru0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Vp92OtmmBpnlnru0 .icon-shape,#mermaid-svg-Vp92OtmmBpnlnru0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Vp92OtmmBpnlnru0 .icon-shape p,#mermaid-svg-Vp92OtmmBpnlnru0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Vp92OtmmBpnlnru0 .icon-shape .label rect,#mermaid-svg-Vp92OtmmBpnlnru0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Vp92OtmmBpnlnru0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Vp92OtmmBpnlnru0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Vp92OtmmBpnlnru0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 插件(纵向)priority 3000 → priority -1(从高到低执行)
审计插件 priority 3000
脱敏插件 priority 2000
协议识别
计费插件
流式转发
Nginx 处理阶段(横向)
POST_READ
SERVER_REWRITE
PREACCESS
ACCESS
CONTENT
LOG
看图说话:横向是 Nginx 的处理阶段,纵向是插件的执行顺序。每个插件在某个阶段被调用,插件的 priority 决定了在同一阶段内谁先谁后。
比如审计插件(priority 3000)在 access 阶段最先跑,因为「先记账、再处理」是审计的第一原则。脱敏插件(priority 2000)跑在审计之后,因为它要处理的是「已经过审计的正文」。协议识别(priority 1000)跑在脱敏之后,因为它需要「已脱敏的正文」来做识别。
而流式转发(priority -1,最低)跑在所有插件之后,因为它要「等所有插件都处理完请求,才把请求转发给上游」。
这就是 AI 网关的「阶段 × 插件」执行模型:阶段决定「什么时候」,插件决定「做什么」,priority 决定「谁先谁后」。
五、踩坑与边界:这套模型不是万能药
5.1 坑一:Lua 协程里禁止阻塞操作,否则一个插件拖垮整个 worker
这是 OpenResty 开发中最容易踩的坑。在 Lua 协程里做阻塞操作(比如 os.execute()、阻塞式文件 IO、阻塞式 socket),会拖垮整个 worker。
原因回到 2.2 节:事件循环是单线程的。你在协程里一阻塞,事件循环就停了,这个 worker 上所有并发连接都跟着停。
生产级实践:所有 IO 必须走 OpenResty 的非阻塞 API(ngx.socket.tcp、ngx.sleep、cosocket)。 如果确实需要阻塞操作,放到独立进程里做(比如用 ngx.thread.spawn 把阻塞操作丢到后台线程,或干脆用外部服务)。如果你在源码里看到某个插件用了 io.open(阻塞文件读),那它大概率会在高并发下出问题。
5.2 坑二:body_filter 可能多次触发,别假设响应体「一次到位」
对普通响应,body_filter 只触发一次。但对流式响应,body_filter 会被触发几十上百次(上游每吐一个 chunk 触发一次)。
生产级实践:插件必须按增量处理。 如果你写一个「脱敏插件」,不能假设「响应体一次性到达」------你必须跨 chunk 拼接敏感词。跨块拼接的正确姿势是:在请求上下文里维护一个状态机,每次 body_filter 触发时,把当前 chunk 喂给状态机,状态机输出「已确认安全的部分」。
5.3 坑三:流式响应与阶段模型的关系------别在 access 阶段做「等响应」的事
在 access 阶段做「等响应」的事,是个灾难。 因为access 阶段在「转发给上游之前」,你在这里等响应,等于把请求卡在半路------上游还没收到请求,响应自然无从谈起。
生产级实践:如果你需要在响应阶段做处理,用 body_filter 钩子;如果你需要在请求转发前做处理,用 access 阶段。 每个钩子有它该干的事,别越界。
5.4 边界一:CPU 密集计算场景,OpenResty 不是好选择
LuaJIT 很快,但它跑在 worker 进程里。任何 CPU 密集计算(大 JSON 反复序列化、正则回溯、加密算法),都会占住 worker,拖累同进程的所有并发请求。
生产级实践:CPU 密集逻辑外移。 要么下沉到 C 模块,要么放到独立服务里,网关只做「转发 + 轻量改写」。这也是为什么 AI 网关的 tokenizer、语义缓存这类重逻辑,往往做成旁路服务而不是网关内嵌插件------这个权衡会在第 15 篇详细展开。
5.5 边界二:生态边界------Lua 生态远小于 Go/Java
Lua 的生态远小于 Go/Java。当你的网关需要对接复杂的业务系统(数据库、消息队列、内部 RPC),Lua 生态的缺口就会显现。
生产级实践:明确「网关层做什么、业务层做什么」的边界。 网关层做协议、路由、限流、审计、脱敏、计费;业务层做用户管理、订单、复杂业务逻辑。网关一旦开始「长业务」,就离失控不远了。
5.6 边界三:适用流量------AI 网关的流量模型和普通网关不一样
AI 网关的流量模型是「长连接 + 低 QPS + 高时长 + 大流量」。一个流式对话可能持续几十秒甚至几分钟,token 逐个吐出,SSE 帧持续下发。
这和普通 API 网关的「短连接 + 高 QPS + 低时长 + 小流量」完全相反。这意味着 AI 网关的容量规划、超时设置、限流策略都要重新设计------比如超时不能设 30 秒(长对话会被掐断),限流不能只看 QPS(还要看并发连接数和 token 吞吐)。
六、高维总结:三层心智模型
把这篇的内容压缩成一张图:
#mermaid-svg-BG1FvArSvkD1DCSA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BG1FvArSvkD1DCSA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BG1FvArSvkD1DCSA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BG1FvArSvkD1DCSA .error-icon{fill:#552222;}#mermaid-svg-BG1FvArSvkD1DCSA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BG1FvArSvkD1DCSA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BG1FvArSvkD1DCSA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BG1FvArSvkD1DCSA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BG1FvArSvkD1DCSA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BG1FvArSvkD1DCSA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BG1FvArSvkD1DCSA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BG1FvArSvkD1DCSA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BG1FvArSvkD1DCSA .marker.cross{stroke:#333333;}#mermaid-svg-BG1FvArSvkD1DCSA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BG1FvArSvkD1DCSA p{margin:0;}#mermaid-svg-BG1FvArSvkD1DCSA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BG1FvArSvkD1DCSA .cluster-label text{fill:#333;}#mermaid-svg-BG1FvArSvkD1DCSA .cluster-label span{color:#333;}#mermaid-svg-BG1FvArSvkD1DCSA .cluster-label span p{background-color:transparent;}#mermaid-svg-BG1FvArSvkD1DCSA .label text,#mermaid-svg-BG1FvArSvkD1DCSA span{fill:#333;color:#333;}#mermaid-svg-BG1FvArSvkD1DCSA .node rect,#mermaid-svg-BG1FvArSvkD1DCSA .node circle,#mermaid-svg-BG1FvArSvkD1DCSA .node ellipse,#mermaid-svg-BG1FvArSvkD1DCSA .node polygon,#mermaid-svg-BG1FvArSvkD1DCSA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BG1FvArSvkD1DCSA .rough-node .label text,#mermaid-svg-BG1FvArSvkD1DCSA .node .label text,#mermaid-svg-BG1FvArSvkD1DCSA .image-shape .label,#mermaid-svg-BG1FvArSvkD1DCSA .icon-shape .label{text-anchor:middle;}#mermaid-svg-BG1FvArSvkD1DCSA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BG1FvArSvkD1DCSA .rough-node .label,#mermaid-svg-BG1FvArSvkD1DCSA .node .label,#mermaid-svg-BG1FvArSvkD1DCSA .image-shape .label,#mermaid-svg-BG1FvArSvkD1DCSA .icon-shape .label{text-align:center;}#mermaid-svg-BG1FvArSvkD1DCSA .node.clickable{cursor:pointer;}#mermaid-svg-BG1FvArSvkD1DCSA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BG1FvArSvkD1DCSA .arrowheadPath{fill:#333333;}#mermaid-svg-BG1FvArSvkD1DCSA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BG1FvArSvkD1DCSA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BG1FvArSvkD1DCSA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BG1FvArSvkD1DCSA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BG1FvArSvkD1DCSA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BG1FvArSvkD1DCSA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BG1FvArSvkD1DCSA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BG1FvArSvkD1DCSA .cluster text{fill:#333;}#mermaid-svg-BG1FvArSvkD1DCSA .cluster span{color:#333;}#mermaid-svg-BG1FvArSvkD1DCSA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BG1FvArSvkD1DCSA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BG1FvArSvkD1DCSA rect.text{fill:none;stroke-width:0;}#mermaid-svg-BG1FvArSvkD1DCSA .icon-shape,#mermaid-svg-BG1FvArSvkD1DCSA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BG1FvArSvkD1DCSA .icon-shape p,#mermaid-svg-BG1FvArSvkD1DCSA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BG1FvArSvkD1DCSA .icon-shape .label rect,#mermaid-svg-BG1FvArSvkD1DCSA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BG1FvArSvkD1DCSA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BG1FvArSvkD1DCSA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BG1FvArSvkD1DCSA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第 1 层:事件循环(epoll)
解决「几万并发连接」
第 2 层:Lua 协程
解决「单线程里的多步异步流程」
第 3 层:阶段 × 插件模型
解决「业务逻辑怎么组织」
第 1 层(事件循环):Nginx 用 epoll 实现「一个 worker 扛几万连接」。这是网络吞吐的来源。代价是单线程------任何阻塞操作都会拖垮整个 worker。
第 2 层(Lua 协程):协程让开发者用同步写法跑出异步性能。「等 IO」被翻译成「挂起协程,让事件循环去处理别的连接」。这是业务灵活性的来源。
第 3 层(阶段 × 插件):阶段决定「什么时候」,插件决定「做什么」,priority 决定「谁先谁后」。这是 AI 网关业务逻辑的组织方式。
一句话总结:OpenResty = Nginx 的事件循环(吞吐)+ LuaJIT 协程(灵活),AI 网关在这套运行时上,用「阶段 × 插件」模型组织协议识别、脱敏、计费、流式转发等所有能力。
理解了这三层,你就掌握了 AI 网关运行时的完整心智模型。后面几篇讲配置同步、计费、流式处理时,会反复回到这套模型------因为所有上层能力,最终都要落到「在哪个阶段、以什么 priority、跑在哪个 worker」这三个问题上。
小结与预告
这一篇我们下探到了 AI 网关的运行时层:OpenResty 的事件循环让一个 worker 扛几万连接,Lua 协程让同步写法跑出异步性能,「阶段 × 插件」模型让业务逻辑有序组织。同时我们也划出了三条边界:CPU 密集、生态、适用流量。
下一篇我们进入配置管理领域:配置同步:etcd 的 watch 机制与一致性。当路由规则、模型映射、限流阈值这些配置分散在多个 worker、多个实例上时,怎么保证它们的一致性?etcd 的 watch 机制是怎么工作的?为什么配置同步不能依赖轮询?这些问题,下一篇逐一拆解。
