Day 18 / 共 20 天 · 第 4 周 高级子系统与运维

stream 子系统(L4 代理)

前面都是 HTTP(L7)代理。但 APISIX 还能代理 L4(TCP/UDP)流量——比如 MySQL、Redis、MQTT。今天看 stream 子系统:它和 HTTP 子系统共享架构,但工作在更底层。

📍 你在整条链的位置(第 4 周 高级子系统与运维)
Admin API D17 stream 子系统(L4 代理) SSL/secret/wasm D19 部署收官 D20
L01

L4 vs L7

🤔 痛点:我想给 MySQL / Redis / MQTT 也做统一入口 前 17 天全是 HTTP(L7)。可数据库、消息队列不是 HTTP——它们跑在裸 TCP/UDP 上。想给它们做负载均衡、限流、mTLS,HTTP 那套用不上,难道另起一个代理?
💡 本质:L4 只看信封,L7 拆开读信 L7(HTTP)像邮局拆开信读内容——能按 uri/header 智能路由。L4(TCP/UDP)像快递只看信封上的地址——看不懂里面装的是 MySQL 协议还是加密流,只管把字节流从 A 搬到 B。APISIX 大部分是 L7,但也提供 L4 代理(stream 子系统),给数据库/消息队列做负载均衡与 mTLS。底层还是 OpenResty(Nginx 的 stream 模块本就支持 L4)。
网络分层:L4 管"连接",L7 管"内容" L7(应用层)= HTTP,网关能看懂 uri/header/body,按内容路由。L4(传输层)= TCP/UDP,网关只看到"字节流",看不懂内容(因为内容可能是 MySQL 协议、Redis 协议、加密的 TLS)。APISIX 大部分能力是 L7(HTTP),但也提供 L4 代理——用来给 TCP/UDP 服务(数据库、消息队列、DNS)做负载均衡/限流/mTLS。底层还是 OpenResty(Nginx 的 stream 模块也支持 L4)。
L02

stream 阶段

init.lua 里有一套平行的 stream 阶段函数(对应 Nginx stream 模块的阶段):

  • stream_init:1066)/ stream_init_worker:1082):初始化(类比 http_init)
  • stream_preread_phase:1120):主处理(类比 http_access_phase)
  • stream_balancer_phase:1239):选上游
  • stream_log_phase:1251):日志
HTTP (L7) stream (L4) 平行对称 http_init_worker stream_init_worker http_access_phase stream_preread_phase balancer stream_balancer_phase header/body_filter (无:L4 没头/体) log stream_log_phase ↔ 一一对应
两套子系统结构对称,只差 L4 没有 header/body_filter;core 的 ctx/router/balancer 两边共用。
读法:结构和 HTTP 子系统平行对称——init/init_worker/主处理/balancer/log。只是 L4 没有 header_filter/body_filter(没有 HTTP 头/体的概念)。APISIX 把两套子系统(http/stream)用同一套代码风格实现,很多 core 组件(config/router/plugin/balancer)两边共用。
L03

stream_preread_phase

init.lua:1120-1160(和 http_access_phase 神似):

function _M.stream_preread_phase()
    local api_ctx = core.tablepool.fetch("api_ctx", 0, 32)   -- 一样用 tablepool
    ngx_ctx.api_ctx = api_ctx
    if not verify_tls_client(api_ctx) then return ngx_exit(1) end
    core.ctx.set_vars_meta(api_ctx)                           -- 一样的 ctx 机制
    router.router_stream.match(api_ctx)                       -- stream 路由匹配
    local matched_route = api_ctx.matched_route
    if not matched_route then return ngx_exit(1) end
    -- 取上游(upstream_id / service_id)...
end
"复用 HTTP 那套"很明显 tablepool 取 api_ctx、set_vars_meta、router.match——全是 HTTP 子系统学过的东西,stream 直接复用!只是路由器换成 router_stream(按 L4 条件匹配,如源 IP、SNI、端口)。这就是为什么 core/ 里的 ctx/config/router/plugin/balancer 设计成"子系统无关"——http 和 stream 都能用。省了大量重复代码。
L04

preread 的含义

为什么叫 "preread"(预读)? L4 是字节流,网关默认看不懂内容。但有时想"偷看"开头几个字节来做路由决策——比如 TLS 握手的 SNI(想访问哪个域名)、或某协议的标识。Nginx stream 的 preread 阶段就是"在把流转发给上游前,先预读一小段"。APISIX 在这个阶段做路由匹配(可能基于预读的内容,如 SNI)。这是 L4 代理能做"基于内容路由"的关键——虽然是 L4,但偷看一眼开头就能智能路由。比如 MQTT 网关按 client-id 路由、TLS 按 SNI 分流。
📝 举个例子(preread 偷看 SNI) 客户端要连 db.example.com:443。TLS 第一个包 ClientHello 里带 SNI=db.example.com
stream_preread_phase 偷看这个 SNI → router_stream.match 匹配到一条 stream_route(sni=db.example.com)→ 转发到对应数据库集群。全程没解密内容,只瞄了一眼握手信封。

👶 小白:L4 都看不懂内容,为什么还能"基于内容路由"?不矛盾吗?

👨‍🏫 老师:关键在"偷看开头"。它不解析整个流,只在 preread 阶段读最前面几个字节——TLS 的 SNI、MQTT 的 client-id 都在协议开头。够做路由决策就行,剩下的字节照样原样转发、不拆包。这就是 preread(预读)这个名字的由来。

L05

stream 路由与上游

stream 有自己的抽象对象 stream_route(schema_def.lua:960),匹配条件是 L4 的(remote_addr、server_addr、server_port、sni 等),上游还是复用 upstream(Day 08/10 的负载均衡、健康检查全能用)。preread 匹配后,stream_balancer_phase:1239)选上游实例,转发。

读法:stream_route 和 http route 是平行的(/apisix/stream_routes/{id})。上游(upstream)两边共用——所以一个数据库集群的负载均衡、健康检查配置,L4 代理直接享用 Day 10 学的那套。Day 06 的 merge_service_stream_route(Day 13 见过)就是 stream 版的 service 合并。
L06

stream 插件

stream 也有插件(Day 11 plugin.load 里的 load_streamstream_filter :564)——但因为 L4 没有 HTTP 语义,stream 插件较少(如 mqtt-proxyip-restriction、限流类)。

L4 插件能做什么? 不能做"改 HTTP header"这种(没有 header),但能做 IP 黑白名单、L4 限流、协议解析(mqtt-proxy 解析 MQTT)、日志。插件机制和 HTTP 一样(priority、阶段方法),只是可用的阶段和能力受 L4 限制。plugin.lua 里 http 和 stream 插件分开加载(local_stream_plugins),但共用同一套运行引擎。
L07

xrpc 协议扩展

Day 03 的 http_init 末尾有 xrpc.init()xRPC 是 APISIX 的"L4 之上的协议框架"——让你为特定协议(如 Redis、Dubbo)写"协议感知"的代理逻辑(超越纯字节转发)。

读法:纯 L4 只转发字节;xRPC 让网关"理解"某个 L7-over-TCP 协议(如 Redis 命令),从而做协议级的路由/限流/观测。比如 xrpc 的 redis 模块能识别 Redis 命令、按 key 路由、统计命令级指标。这是 APISIX 在 L4 和 L7 之间的补充能力,用于那些"跑在 TCP 上但有自己协议"的服务。属于进阶话题,了解即可。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • L4 和 L7 代理的区别?APISIX 为什么要支持 L4?
  • stream 阶段和 HTTP 阶段怎么对称?复用了哪些 core 组件?
  • "preread"是什么意思?为什么 L4 也能基于内容路由?
  • stream 插件相比 HTTP 插件受什么限制?xRPC 补充了什么?

✋ 动手

cd /Users/bitmart/work/codes/github/apisix
sed -n '1120,1160p' apisix/init.lua
sed -n '960,999p' apisix/schema_def.lua       # stream_route
ls apisix/stream/
明天预告 · Day 19SSL/secret/wasm/pubsub——动态证书(ssl_phase 按 SNI 选证书)、密钥管理(secret 对接 Vault)、Wasm 插件支持、以及 pubsub 发布订阅等剩余子系统。
← Day 17 Day 19 · SSL/secret/wasm →