突破云原生网关局限:面向海量 AI 沙箱的 Web VNC 动态路由与会话保持深度实践
在刚刚过去的这一年里,人工智能的发展轨迹正在发生一次深刻的范式转移:从仅仅停留在对话框里生成文本的大语言模型(LLM),大步迈向能够自主规划、调用工具、甚至操作计算机界面的自主智能体。当我们赋予 AI 操纵浏览器的权限去预订机票,或者赋予它终端权限去编写和调试代码时,一个严峻的基础设施挑战随之浮出水面——我们如何安全地运行它们,又如何实时地“看”到它们在干什么?
在现代云原生架构下,为每个 Agent 分配一个短暂、隔离的 Kubernetes 容器沙箱(Sandbox)是运行安全的基础;而通过 Web VNC(Virtual Network Computing)技术将沙箱内的图形化桌面流式传输到前端浏览器,则是实现可观测性的最佳手段。
然而,当系统规模从几十个实验性沙箱,暴增至成千上万个并发运行、IP 地址瞬息万变的动态沙箱时,一个经典的分布式网络难题横亘在了我们面前:如何通过一个统一的公共网关,将用户的复合 Web VNC 会话流量,精准、稳定、零遗漏地路由到后端随时在漂移的动态 Pod 上?
本文将全景式地复盘我们团队在构建 AI 沙箱可观测基础设施时的技术探索之路。我们将剖析通用 API 网关在处理此类复合会话时的致命短板,并详细拆解我们是如何基于 OpenResty 体系,通过巧妙的“首包驱动与状态转移”设计,从零打造出一个兼具极高可靠性与极致性能的专有动态路由网关。
01
业务困境:AI Agent 的“黑盒”危机与 GUI 观测的必然性
在传统的微服务架构中,系统的可观测性通常由日志(Logging)、指标(Metrics)和链路追踪(Tracing)三大支柱构成。但对于运行在隔离环境中的 AI Agent 而言,这三大支柱显得捉襟见肘。Agent 往往需要与复杂的图形用户界面(GUI)进行交互,比如自动化测试工具需要判断一个按钮是否发生了位移,或者一个 RPA(机器人流程自动化)Agent 在操作复杂的企业 ERP 软件时遭遇了未预期的弹窗阻挡。
在这些场景下,底层抛出的文本异常栈无法还原现场的真实视觉上下文。开发者和研究人员迫切需要一双“千里眼”,能够实时监控 Agent 的视觉输入和鼠标键盘输出。只有“看”得见,才能有效地对 Agent 的决策轨迹进行评测(Eval)和人工干预(Human-in-the-loop)。
赋予 AI 执行代码和操作环境的权限是极其危险的。为了防止恶意代码逸出、系统环境被破坏,或者不同 Agent 任务之间发生数据污染,基础设施团队通常会采用 Docker 容器、甚至是基于 Firecracker 的微虚拟机(MicroVM)来构建沙箱池。这些沙箱的生命周期特征极其鲜明:
1. 短寿且高频启停:一个 Agent 任务可能只持续几分钟甚至几十秒,任务结束沙箱立即被销毁回收。
2. 网络拓扑极度动态:每次沙箱启动,Kubernetes 集群的 CNI(容器网络接口)都会为其分配一个全新的、完全随机的内网 IP 地址。
为了实现低延迟的 GUI 观测,我们选用了基于 RFB(Remote Frame Buffer)协议的 VNC 技术,并结合 noVNC 这样的前端解决方案,将传统的 TCP 桌面数据流通过 websockify 桥接转换为 WebSocket 帧。
Web VNC 的优势在于它是完全“无客户端(Clientless)”的。开发者只需要一个现代浏览器,不需要安装任何插件,就能通过标准的 HTTP 和 WebSocket 协议直接渲染出远端沙箱的桌面画面。然而,正是这种基于 Web 协议栈的妥协,为后端的反向代理和负载均衡带来了空前的挑战。
02
协议深潜:Web VNC 复合会话的技术鸿沟
要彻底理解动态路由的难点,我们必须拿着显微镜去解剖 Web VNC 会话的流量特征。与简单的 RESTful API 调用截然不同,启动一个 Web VNC 监控窗口,在网络层面上是一个典型的**“多阶段、有状态、跨协议”的复合会话过程**。
当用户在控制台点击“监控沙箱(ID:37579)”的按钮时,浏览器实际上在极短的时间内执行了一套复杂的网络交互:
1. 第一阶段:入口 HTML 加载(首包请求)
浏览器向网关发起 GET /vnc/37579/ 的 HTTP 请求。此时,URL 中清晰地包含了我们想要访问的目标沙箱的唯一标识(Sandbox ID:37579)。网关根据这个 ID 找到后端实例,并返回 noVNC 的基础 HTML 框架页面。
2. 第二阶段:静态资源风暴(Asset Storm)
HTML 引擎开始解析页面,并立即并发发起数十个子请求,用于拉取 noVNC 运行必须的 JavaScript 引擎(如 rfb.js、ui.js)、CSS 样式表、多语言 JSON 文件以及 UI 图标。
注意这里的致命变化:这些由 HTML 相对路径触发的子请求,其 URL 形如 /vnc/app/ui.js,它们在 URL 路径中彻底丢失了原本的 Sandbox ID!
3. 第三阶段:协议升级与帧数据流(The Handshake)
当所有的前端 JS 脚本加载就绪后,引擎会向服务端发起一个特殊的 HTTP GET 请求:/vnc/websockify。这个请求的 Header 中携带着 Connection: Upgrade 和 Upgrade: websocket 标识,试图完成协议升级。一旦服务端返回 101 Switching Protocols 状态码,这条 TCP 连接就会蜕变为全双工的 WebSocket 长连接,开始源源不断地双向传输鼠标键盘指令和屏幕像素更新帧。同样,这个升级请求的 URL 中也是没有 Sandbox ID 的。
对于这三个阶段的所有流量,网络网关必须遵守一条绝对红线:属于同一个会话的所有 HTTP 资源请求和 WebSocket 长连接,必须精准无误、百分之百地被路由到同一个后端物理 Pod 上。
这是一项极其严苛的要求。如果静态资源(第二阶段)被错误地路由到了集群中另外一个版本不同的沙箱 Pod 上,前端可能会加载到不兼容的 JS 引擎;而如果最重要的 WebSocket 握手请求(第三阶段)发生了哪怕一次“路由漂移”,落到了非初始的 Pod 上,该 Pod 的 VNC 服务端会因为在内存中找不到前置的握手上下文,直接粗暴地断开 TCP 连接,最终在浏览器控制台抛出令前端工程师绝望的 Connection closed (code: 1006) 错误。
03
基础设施的碰撞:为什么云原生网关纷纷“折戟”?
理解了业务特性的苛刻要求后,我们再来审视现有的云原生基础设施,就会发现它们在面对此类需求时显得多么苍白无力。
最传统的 Nginx 代理方式是在 nginx.conf 中手写 upstream 块并填入后端 IP。但在 K8s 环境中,由于 Agent 沙箱可能每分钟都在成百上千地创建和销毁,IP 地址完全不可预知。如果通过自动化脚本频繁修改配置文件并触发 nginx -s reload,不仅会导致网关性能急剧下降,处于重载瞬间的已有 WebSocket 长连接也极易被强行切断。
很多运维工程师的第一反应是开启负载均衡器的 ip_hash 会话保持功能。其逻辑是:只要客户端的公网 IP 不变,网关就永远把流量发给同一个后端。然而,在现代网络环境中,这是一个极其脆弱的假设。
1. 移动办公网络的颠簸:研发人员在排查问题时,网络环境可能在公司内网 Wi-Fi 和手机 4G 热点之间切换,源 IP 发生改变,会话瞬间断裂。
2. 大型企业出口 NAT 的干扰:大型企业通常会配置多条运营商出口链路进行负载均衡(SNAT)。对于网关而言,同一个用户的并发请求可能呈现出来自几个完全不同的公网 IP。
3. 无法解决动态寻址问题:ip_hash 只能保证“把流量粘在某台机器上”,但它解决不了“第一次请求到底该发给哪一台机器”的问题。网关依然不知道 URL 里的 ID 对应的是集群深处的哪一个内网 IP。
那么,使用 APISIX、Kong 这类基于 OpenResty 封装的现代 API 网关,并编写自定义的 Lua 路由插件呢?这是我们团队早期尝试过的方案。我们编写了一个插件,在请求到达时拦截 URL,提取出 ID,然后去 Redis 里查真实 IP 并动态修改上游节点。
但我们很快撞上了“南墙”:这个方案只在“第一阶段(入口 HTML 加载)”有效。当浏览器发起不带 ID 的静态资源请求和 WebSocket 握手请求时,触发了同一个网关路由规则,但 Lua 插件提取不到 ID,瞬间失去了路由目标,只能返回 404 Not Found。
为了让 API 网关插件能继续工作,一种妥协的办法是“暴力重写前端源码”。我们需要深入 noVNC 的数百个 JavaScript 文件,修改它的资源加载器和 WebSocket 连接器,强行在所有生成的 URL 末尾拼上 ?sandbox_id=37579 参数。
这种侵入式修改不仅工作量巨大、极易引发难以排查的 Bug,更可怕的是,它彻底破坏了开源组件的可维护性。未来 noVNC 每次发布安全更新,我们都得重新进行一遍魔改。
我们需要一种更优雅、更底层的架构破局思路。
04
架构破局设计:“控制面解耦”与“数据面状态寄存”
经过深刻的反思,我们意识到:不能要求网关去理解复杂的业务组合,也不能强迫前端去适应死板的网关。必须在协议的最底层,利用 HTTP 原生机制构建一座桥梁。
我们重新设计了整体架构,将其清晰地划分为“控制面(Control Plane)”和“数据面(Data Plane)”。
在 Kubernetes 集群内,我们部署了一个名为 Sandbox Manager 的常驻核心微服务。
• 上帝视角的守望者:Manager 通过 K8s 的 List/Watch 机制深度监听集群内沙箱 Pod 的生命周期事件。
• 主动路由下发:一旦 Manager 确认某个 Agent 的沙箱 Pod 成功进入 Running 状态,并且 CNI 插件为其分配了健康的内网 IP,Manager 会立刻组装一条路由信息。
• 原子化存储:Manager 将映射关系(如 Sandbox_ID_37579 -> 10.18.124.21:6080)以 Hash 结构写入到中央高可用 Redis 集群中(键名:vnc_sandboxes)。当任务结束 Pod 销毁时,Manager 会同步从 Redis 中剔除该记录。
• 架构哲学:我们坚决摒弃了让微服务“启动时自行向网关注册”的常见做法。在计算资源极度受限、且可能运行不受信任 AI 代码的沙箱容器中,不应该部署任何控制面组件。由外部可信的 Manager 实施“旁路式”的状态同步,保证了路由数据的绝对干净和安全。
我们在数据接入层部署了基于 OpenResty 的 Nginx 网关,并通过自研的 Lua 脚本接管了请求处理的最早阶段(access_by_lua)。
为了解决子请求“丢失 ID”的核心痛点,我们设计了一套**“首包驱动 + 状态寄存”**的双信源接力决策树:
决策链条第一环:URL 强匹配与状态固化
当带有明确 Sandbox ID 的首包请求(GET /vnc/37579/)到达网关时:
1. Lua 脚本通过正则表达式 ^/vnc/(\d+)(.*)$ 迅速截获请求,成功提取出目标 ID 37579。
2. 脚本连接 Redis,毫秒级查出后端真实 IP,准备转发。
3. 点睛之笔:状态寄存。在 Nginx 将后端 HTML 响应返回给浏览器之前,Lua 脚本强行在 HTTP 响应头中植入了一颗“持久化种子”:
Set-Cookie: vnc_session_id=37579; Path=/vnc/; HttpOnly这一步是整个架构的灵魂。网关本身拒绝在内存中维护庞大且复杂的“用户-会话-沙箱”状态机,而是巧妙地利用了 HTTP 标准协议,将这份至关重要的状态数据,“寄存”到了用户的浏览器中。
决策链条第二环:Cookie 兜底与路由护航
当 HTML 加载完毕,浏览器在无形中触发数十个不带 ID 的静态资源请求(/vnc/app/ui.js)以及最终的 WebSocket 握手请求(/vnc/websockify)时,奇迹发生了。
根据 HTTP/1.1 标准规范,只要请求路径符合 Cookie 的 Path=/vnc/ 作用域,浏览器就会在每一个发往该域名的请求头中,强制且自动地携带上我们刚才种下的“种子”:Cookie: vnc_session_id=37579。
当这些残缺的子请求抵达 OpenResty 网关时:
1. Lua 脚本再次执行正则匹配,毫无意外地失败了(URL 中没有数字 ID)。
2. 脚本立即平滑回退到“第二信源”逻辑:读取 HTTP Request Header 中的 Cookie。
3. 脚本成功从 ngx.var.cookie_vnc_session_id 变量中提取出 ID 37579,重新接续上了断裂的上下文。
4. 网关使用提取出的 ID 再次查询 Redis,获取到与首包请求完全一致的后端 IP,稳稳地将 WebSocket 升级请求送达了目标沙箱。
通过这种“URL 探路,Cookie 护航”的机制,我们实现了逻辑闭环,在网关保持绝对无状态的前提下,完美达成了苛刻的“会话内路由一致性”。
05
底层引擎:基于 OpenResty 的极致工程化落地
有了理论框架,还需要经得起生产环境万级高并发考验的工程落地。在使用 Lua 编写扩展时,如果对 OpenResty 的底层内存模型和协程调度机制缺乏深刻理解,极易写出导致 Nginx 进程阻塞或内存泄漏的代码。
我们的网关脚本需要连接 Redis,但 Redis 集群的连接地址本身也是动态的(通过内部 DNS 或配置中心下发)。
在早期的低质量实现中,开发者往往在每次请求的 access_by_lua 阶段去发起域名解析并获取配置。这在高并发下是致命的:一旦 DNS 解析产生几毫秒的延迟,海量的并发请求会瞬间耗尽 Nginx Worker 的可用协程,引发灾难性的雪崩效应(Thundering Herd)。
为了打造工业级的性能,我们将配置发现逻辑前置到了 Nginx 的 init_worker_by_lua 阶段:
-- 仅在 Nginx worker 进程启动时触发
function _M.init_worker()
local worker_id = ngx.worker.id()
-- 严格限制只允许 0 号 worker 执行拉取任务,防止多进程重复请求配置中心
if worker_id == 0 then
-- 使用 ngx.timer.at 脱离当前执行上下文,创建一个异步的轻量级后台协程
ngx.timer.at(0, fetch_and_cache_redis_config)
end
end通过非阻塞的定时器配合 lua-resty-http 库,网关在后台静默地拉取最新配置,并将其序列化后存入。
由于只有 0 号 Worker 获取了配置,如何让其他 Worker 进程也能零延迟地拿到数据?Nginx 的多进程模型决定了 Worker 之间的内存是物理隔离的。
我们利用了 OpenResty 提供的重量级武器:lua_shared_dict。在 nginx.conf 的 http 上下文中开辟一块基于共享内存(Shared Memory)的字典区域,0 号 Worker 将拉取到的 Redis 账号密码、IP 端口写入该区域,其他所有处理用户请求的 Worker 直接从这块共享内存中极速读取。这种读写分离的架构,彻底消除了处理用户请求时的网络 I/O 阻塞,将路由决策的延迟压榨到了微秒级别。
在与真实存放路由数据的 Redis 通信时,每次建立 TCP 连接的成本极高。我们必须使用 resty.redis 提供的连接池功能。但需要特别注意,在使用完毕后,绝对不能调用 red:close()(这会物理断开 TCP 连接),而是必须严谨地调用 red:set_keepalive(10000, 100),将健康的长连接交还给 Nginx 的内置连接池,供下一次协程复用。这避免了网关在高负载下耗尽本地主机的临时端口范围(TIME_WAIT 爆炸)。
06
血泪排障实录:拨开 Nginx 配置的重重迷雾
在将这套系统推向生产环境的灰度阶段,我们遭遇了一系列极其诡异、极具欺骗性的故障。复盘这些血泪排障过程,或许比架构设计本身更有价值。
上线初期,部分用户反馈连接断开。抓包显示,网关成功返回了 HTML 页面,但在建立 WebSocket 连接时,前端抛出了 Connection closed (code: 1006)。
更诡异的是,如果在本地用 hosts 文件把域名直接绑死在一台后端的 IP 上,完全不经过公共前端网关,访问却是一切正常的。
这说明后端的 Lua 逻辑和 VNC Server 配置毫无问题,元凶必定出在前端公共 Nginx 网关上。我们反复审查了前端为 VNC 单独配置的 location /vnc/ 块:
location /vnc/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://sandbox_backend;
}这段配置是教科书级别的标准写法,似乎没有任何破绽。为了抓取底层真相,我们不得不登录前端机器,使用 Nginx 提供的配置诊断终极命令 nginx -T。这个命令会把 Nginx 在内存中展开合并后的完整、最终生效的配置树全部打印出来。
在成千上万行的合并配置中,我们顺藤摸瓜,终于在极其隐蔽的一个被 include 进来的公共配置文件 pub/server_pub.conf 中,发现了一行令人窒息的代码:
# 隐藏在黑暗角落的“幽灵”配置
proxy_set_header Connection "";真相大白。Nginx 的配置指令存在复杂的继承与覆盖规则。这个全局公共文件原本是为了在代理普通的 HTTP 无状态短连接接口时,强制掐断后端 Keep-Alive 以节省资源而设置的。由于它定义在更高层级的 server 块中,在 Nginx 合并配置的最后阶段,这个全局的“清空 Connection 头”指令,以极高的优先级,无情地抹杀、覆盖了我们在子 location /vnc/ 中精心配置的 Connection "upgrade"。
到达后端代理服务器的请求,变成了一个彻底失去“升级协议”意图的普通残疾 GET 请求,导致后端直接关闭了 TCP 握手。
破局手段:我们废弃了容易引发冲突的硬编码字符串,转而采用 Nginx 官方推荐的最健壮的 map 变量机制来动态处理协议升级头,并在 VNC 专属配置块中彻底隔绝了公共配置的污染。
解决了全局变量污染后,我们以为迎来了胜利,却发现当后端的 OpenResty 代理服务器从单节点扩展为多节点集群时,WebSocket 握手再次高概率失败。
这引发了我们对分布式系统“状态边界”的深层次思考。我们一直以为,既然路由映射关系保存在高可用的中央 Redis 中,后端的 OpenResty 网关就应该是完全“无状态(Stateless)”的,无论请求落到集群中的哪台服务器,都能去 Redis 里查到一致的结果。
然而,我们忽略了链路尽头的最终节点——运行在沙箱内部的 VNC Server 本身,它是一个强状态的重型应用。
当 Nginx 负载均衡器采用默认的“无状态轮询(Round-Robin)”策略时:
1. 用户请求 HTML 页面的 HTTP 流量,被轮询到了网关节点 A。网关节点 A 将请求转发给沙箱,沙箱 VNC Server 在内存中记录下:“我刚才给网关 A 发送了一个登录页面,我预期接下来建立 WebSocket 隧道的请求,也应该来自网关 A 的 IP”。
2. 几百毫秒后,浏览器发起的 WebSocket 升级请求,被负载均衡器不幸地轮询到了网关节点 B。
3. 网关节点 B 诚实地去 Redis 查到了正确的沙箱 IP,并满怀信心地将 WebSocket 握手请求拍在了沙箱的大门上。
4. 沙箱 VNC Server 一看源 IP,发现是陌生的网关 B,触发了内部的安全风控防劫持机制,直接将这个不在预期内的握手请求拒之门外。
破局手段:在前端负责流量分发的 Nginx upstream 块中,必须坚决地加上 ip_hash; 指令。这是一种妥协,也是必须的保证。它确保了在同一个物理用户的网络会话周期内,无论是 HTTP 预请求还是 WebSocket 长连接,都能被“粘滞(Sticky)”在同一个中间层网关节点上,维护了整条通信链路的上下文完整性。
在一次扩容部署中,新上线的后端网关节点始终报 404 Not Found 错误,Lua 脚本日志显示“在 Redis 中未找到 Sandbox ID 映射”。但这怎么可能?Manager 服务明明已经将数据写进去了,使用 redis-cli 工具在服务器终端手动查询也确实能看到数据。
同一个服务器,两套截然相反的结论,这是极其违背常理的。经过抽丝剥茧,问题最终定位在了 Nginx 的底层 DNS 解析机制上。
在企业的复杂混合云环境中,内网 DNS 经常根据不同的解析视图(View)返回不同的地址池。
# nginx.conf 里的解析器声明
resolver 186.20.35.21 12.19.21.11 valid=30s;当系统运维人员在命令行执行 redis-cli 测试时,使用的是 Linux 宿主机底层 /etc/resolv.conf 配置的 DNS,它把我们内部的缓存域名正确解析到了主库的地址。
然而,OpenResty 内部的 Lua 异步 HTTP 库(lua-resty-http),其底层通信完全不依赖操作系统的 DNS,而是强依赖于 nginx.conf 中手写配置的 resolver 指令。
因为旧配置文件中写死了一个陈旧的、已被废弃的 DNS 服务器 IP,导致 Nginx Worker 进程将 Redis 域名解析到了一个早已不再同步数据的废弃从库上。它在那个空空如也的从库里疯狂查询,自然永远返回 404。
经验教训:在云原生时代,务必保持 Nginx 的内部解析器与宿主机/容器平台环境(如 CoreDNS)的绝对一致性,任何硬编码的基础设施 IP 都是埋下的定时炸弹。
07
生产级调优:让远程监控如丝般顺滑
解决连通性只是第一步。要让 Web VNC 在网络波动下依然保持高帧率和低延迟,我们需要对 Nginx 进行深度的参数压榨。
在代理普通的 REST API 时,Nginx 的默认行为是开启 proxy_buffering on。网关会尽可能多地接收后端服务器吐出的数据,塞满本地的内存缓冲区,然后再批量发送给前端浏览器。这是一种极大提升网络吞吐吞吐量的优秀设计。
但在 VNC 场景下,这成了致命毒药。
WebSocket 隧道里流淌的是实时编码的屏幕像素数据流(帧)。开启缓冲意味着,沙箱画面发生变动时,网关会把几十帧画面“扣留”在内存里,直到填满 buffer 设定的阈值,才“哗啦”一下全部倒给浏览器。
这导致用户体验极度糟糕:画面停顿 3 秒,然后像快进一样瞬间播放完操作,毫无实时性可言。
必须在 VNC 的 location 中显式配置 proxy_buffering off;,强制 Nginx 变为一个透明的字节流搬运工,实现帧数据的毫秒级即时透传。
VNC 会话可能面临极端的场景:开发者打开了沙箱监控页面,然后去吃了个午饭,期间屏幕没有任何画面变化,也没有鼠标键盘输入。
对于中间网络设备(如云厂商的 NAT 网关)而言,一条超过 5 分钟没有任何数据包传输的 TCP 长连接,会被认为是“死连接”而遭到强制物理回收(RST 阻断)。
为保障稳定:
1. 调高读写阈值:将 proxy_read_timeout 和 proxy_send_timeout 大幅拉升至 3600s。
2. 应用层心跳护航:在 noVNC 客户端配置参数中激活 WebSocket 的底层 Ping/Pong Frame 机制。即便没有任何业务数据交互,底层协议每隔 15 秒也会发送微小的心跳包,向所有中间网络设备宣告“我还活着”,彻底杜绝静默断连。
08
安全防御纵深:构筑隔离的护城河
将承载了核心业务逻辑的网关暴露在公网或办公网下,安全性考量不容有丝毫懈怠。
在依靠正则表达式提取 Sandbox ID 时,我们实施了极其严苛的字符白名单策略。
local m_uri = ngx.re.match(uri, "^/vnc/(\\d+)(.*)$")通过强制要求 ID 必须为纯数字 \d+,我们从根源上粉碎了攻击者企图通过构造 /vnc/../../../etc/passwd 等恶意 Payload 绕过路由或探测后端文件系统的妄想。
我们用于维持路由状态的 vnc_session_id Cookie 是一把打开后门的钥匙。
• 严格约束域与路径:通过限定 Path=/vnc/,这把钥匙只在 VNC 专属通道内流通,绝不会被意外带入到公司主域名下的其他敏感业务接口中,缩小了攻击面。
• 物理隔离前端 JS:强制在后端附加 HttpOnly 标识。这意味着无论前端存在何种复杂的跨站脚本注入(XSS)漏洞,恶意代码都绝对无法通过 document.cookie API 窃取到路由凭证,构筑了一道物理级别的防火墙。
在最终架构中,我们的动态后端沙箱 Pod 并未绑定任何公网或对外网关的 Service。它们仅仅生存在 Kubernetes 内部的覆盖网络(Overlay Network)深处。
前端发出的任何流量,都必须且只能通过我们基于 OpenResty 打造的这一层网关,经过严格的 Lua 脚本逻辑校验和身份匹配后,才会被放行进入内部 6080 端口。这实现了真正意义上的网络边界隔离。
09
总结:寻找状态的最优“宿主”
在解决数以万计的动态 AI 沙箱路由难题的过程中,我们经历过技术选型的迷茫,也曾试图用最前沿的 Service Mesh 或重型 API 网关插件来强行填平协议间的鸿沟。但最终让我们走出泥潭的,是对网络协议底层逻辑的敬畏与回归。
在分布式系统架构设计中,我们常常被教导要“消灭状态,追求无状态(Stateless)”。但对于 Web VNC 这种天生具备强上下文依赖的会话而言,状态(State)其实永远无法被消灭,它只是在系统的不同层级之间不断地漂移与转移。
如果我们选择让 Nginx 网关在内存中记住状态,网关就会变得臃肿、难以横向扩容,并在宕机时带来灾难;如果我们强行要求前端剥离状态,前端代码就会变得千疮百孔,丧失开源项目的演进能力。
本方案中最具哲学意味的设计在于:我们利用古老但无比可靠的 HTTP Cookie 原语,将沉重的会话状态优雅地“寄存”在了距离用户最近、最适合保存会话的地方——客户端浏览器之中。
这使得我们的核心网关重获自由,化身为一个轻盈、极速、可以无限水平弹性的“无情翻译机器”。通过融合 OpenResty 的底层编排掌控力与协议本身的精妙机制,我们在极低的架构成本下,夯实了一座能够稳稳支撑未来更庞大 AI Agent 观测需求的基础设施灯塔。