indirection - layer

08. 间接层:反向代理、网关与所有中间人

0. 本章先解决什么问题

计算机科学有句流传很广的老话(出自 David Wheeler):

计算机科学里的任何问题,都可以靠增加一个间接层来解决。
后半句常被省略: 除了”间接层太多”这个问题本身。

反向代理、网关、负载均衡的网络层基础在 代理、网关、负载均衡与 CDN 已经讲过。
本章把视野拉开:这个结构在从硬件到框架的每个尺度上反复出现,页表、DNS、VIP、NGINX、注册中心、槽位表、Spring 的动态代理、DispatcherServlet、消息队列,全是同一句话的化身。
本章把每个化身单独拆开讲清机制,最后收拢它们共享的收益和代价。

间接层的众多化身

这张图怎么读

中间是抽象结构:调用方拿着一个稳定的名字,间接层把名字翻译成真实位置,真实位置可以随时变。四周是这个结构在各层的化身,从硬件的页表到业务的网关。

1. 抽象结构:名字和位置解耦

所有间接层做的是同一件事:

  • 没有间接层:调用方 -> 真实目标
    地址写死,目标一动,全体调用方改代码
  • 有间接层:调用方 -> 稳定的名字 -> [翻译] -> 真实目标
    目标搬家只改翻译表,调用方无感

由此白送三种能力,后面每个化身都能对号入座:

  1. 位置透明: 目标可以搬家、扩容、替换、故障切换
  2. 集中控制: 所有流量过一个咽喉,鉴权、限流、观测、路由策略在这一点做
  3. 平滑变更: 翻译规则可以逐步改,灰度、迁移、AB 有了拨流量的支点

2. 硬件和 OS 里的间接层

2.1 页表:地址的间接层

上上章拆过分页机制,这里换间接层视角再看一眼它买到的三样东西,和第 1 节的三种能力逐条对应:

位置透明: 物理页可以换出、搬迁、整理,程序里的指针值不变
集中控制: 权限位在翻译表上(只读/可执行/内核态),
越权访问在翻译这一步被拦下(段错误的来源)
平滑变更: 写时复制、按需加载全靠”先改表、访问时再兑现”
它的翻译表查询太频繁,于是配了专用缓存 TLB。
记住这个组合: 间接层 + 它的翻译表缓存,后面反复出现

2.2 文件描述符与 FTL

  • 文件描述符 fd:进程里看到的整数 3 先映射到内核打开文件表,再指向 inode。重定向 2>&1 本质上只是改这张映射表中的一个格子,应用程序无需感知。
  • SSD 的 FTL:把逻辑块地址映射到闪存物理位置。磨损均衡、坏块屏蔽和 GC 搬运都在翻译层内部完成,上层始终以为自己在读写固定地址;第 2 章关于擦除块的伏笔也在这里归位。

3. 网络入口的间接层:DNS 和 VIP

3.1 DNS:最早的名字服务

域名到 IP 的翻译链在 DNS 讲过,间接层视角下补两个工程结论:

  • 它顺便是最粗的负载均衡:一个域名挂多条 A 记录轮流答,
    或按地理位置返回就近机房(CDN 调度的第一跳)
  • 它的失效收敛极慢:翻译结果被浏览器、OS、运营商逐级缓存,
    TTL 说 60 秒,总有不守规矩的缓存拖到几小时。
    所以 DNS 适合做”机房级”的粗切换,
    秒级的故障转移要靠下面更快的层

3.2 VIP + keepalived:IP 层的可漂移名字

对外只暴露一个虚拟 IP(VIP),当前活着的主机持有它
keepalived 跑 VRRP 协议: 主机周期组播心跳,
备机收不到心跳就按优先级接管 VIP,
并广播免费 ARP 让交换机更新”这个 IP 在哪个网口”
效果: 故障转移对客户端完全透明,连接串永远是那个 VIP
局限: 上一章说过,两台机器没有多数派,脑裂要靠额外 fencing

DNS 管”名字到机房”,VIP 管”机房里哪台活着”,两层间接各管一段收敛速度。

4. 流量层的间接层:反向代理、网关、注册中心

4.1 NGINX 反向代理:翻译表叫 upstream

代码块JAVA · 7 行收起展开
upstream backend {
   least_conn;                    # 翻译策略
   server 10.0.0.1:8080 weight=3;
   server 10.0.0.2:8080;
   server 10.0.0.3:8080 backup;   # 平时不用,全挂才上
}
server { location /api/ { proxy_pass http://backend; } }

调度策略逐个点名,各有归属场景:

轮询(默认)/ 加权轮询: 机器同构/异构时的均分
least_conn: 请求耗时方差大时,避免长请求堆在同一台
ip_hash: 按来源 IP 固定后端,会话粘性的粗方案
(移动网络下 IP 会变,正经会话还是外置到 Redis)
hash $key consistent: 一致性哈希(上一章的环在这里上岗),
按业务键路由,本地缓存亲和
健康检查: 开源版是被动式(转发失败计数,超阈值临时摘除),
探活式主动检查是商业版/第三方模块的活

代理会吃掉客户端来源 IP,标准补丁是逐跳追加 X-Forwarded-For、传递 X-Real-IP。安全细节:这两个头客户端可以伪造,取”真实 IP”必须从可信代理层数起,别直接信第一个值。

4.2 网关:带业务语义的代理

Spring Cloud Gateway 的模型三件套,本质是把翻译规则做成了可编程的:

  • Route:一条翻译规则(id + 目标 + 谓词 + 过滤器链)
  • Predicate:什么请求匹配这条路(按路径/方法/Header/权重…)
  • Filter:匹配后过路要交的税(改写路径、鉴权、限流、日志)

放在网关做的事,全是第 1 节”集中控制”的兑现:

  • 统一鉴权:解 JWT、查会话,合法才放行,
    后端服务从此不用各自实现一遍登录校验
  • 限流:令牌桶(常配 Redis + Lua 保证集群一致,
    板子在 Lua脚本)
  • 灰度:按 Header/用户尾号/权重把 5% 流量拨给新版本
    (“平滑变更”能力的直接兑现)
  • 和 NGINX 的分工:NGINX 站最外层管接入
    (TLS 终结、静态资源、粗限流),
    网关站微服务门口管业务策略,两层各司其职

4.3 注册中心:翻译表本身活起来了

前面的翻译表都靠人改配置,微服务实例动态扩缩后,表必须自动维护:

服务启动 -> 向 Nacos 注册”订单服务 @ 10.0.0.5:8080”
存活 -> 周期心跳续约;停止心跳 -> 摘除
调用方 -> 订阅”订单服务”的实例列表,
变更由推送 + 定时拉取兜底,本地缓存一份做路由

注意结构:调用方本地缓存的实例列表,就是翻译表的缓存,注册中心挂了一阵,调用方靠缓存照常路由(牺牲新鲜度保可用)。“间接层 + 翻译表缓存”的组合第三次出现(页表配 TLB、DNS 配逐级缓存、注册中心配本地列表)。

5. 数据层的间接层

上一章已经拆过机制的,这里只按间接层视角归位:

Redis Cluster 槽位表: key 到节点之间垫了 16384 个槽,
迁移只改”槽到节点”这半张表。smart client 的本地槽位表
又是翻译表缓存,MOVED 就是缓存失效通知
分库分表中间件: 逻辑表名到真实库表的翻译。
两种部署位置(JDBC 内嵌 / 独立代理)上一章已对比,
那道选择题的通用形态见本章第 8 节
Redis 集群方案演化史刚好把间接层的三个位置走了一遍:
客户端分片(翻译逻辑内嵌调用方)
-> twemproxy/Codis(独立代理层)
-> Cluster(服务端重定向 + 客户端缓存,两头分摊)

6. 框架里的间接层:Spring 全家桶的老底

6.1 动态代理:方法调用级的间接层

Spring AOP 给 Bean 包一层代理对象,调用先过代理再到真身,事务、日志、鉴权切面全垫在这层:

  • JDK 动态代理:目标实现了接口时用。运行时生成一个
    实现同接口的代理类,方法体统一进 InvocationHandler
  • CGLIB:无接口时用。生成目标类的子类,覆写方法织入逻辑
    (所以 final 类和 final 方法切不进去)

著名的”事务自调用失效”用间接层视角一句话就通了:

代码块JAVA · 10 行收起展开
@Service
public class OrderService {
    public void a() {
        this.b();   // this 是真身,不是代理!
    }               // 调用没经过代理层,@Transactional 的翻译逻辑没机会垫入
    @Transactional
    public void b() { ... }
}
// 修法本质都是"把调用重新引导回代理":
// 注入自己的代理引用再调、AopContext.currentProxy()、或拆到另一个 Bean

同理还有 @Async、@Cacheable 的自调用失效,一个病根。机制展开在 事务 - Transactional

6.2 DispatcherServlet:应用内部的网关

所有请求 -> DispatcherServlet(唯一入口)
-> HandlerMapping: URL 到 Controller 方法的翻译表
-> HandlerAdapter: 参数绑定、执行
-> 拦截器链: 过路的税(登录检查、日志)

结构和 4.2 的网关逐格同构:一个咽喉入口、一张路由表、一串过滤器。前端控制器模式就是”应用进程内的网关”。

6.3 MyBatis Mapper:接口到 SQL 的翻译

Mapper 接口没有实现类,运行时 MapperProxy 动态代理把方法调用翻译成”namespace.methodId 对应的 SQL”再交给 SqlSession 执行。你写的 orderMapper.selectById(1) 全程是一次查表翻译。

7. 消息队列:时间维度上的间接层

有一个化身容易被漏掉,因为它间接的维度不同。MQ 把”调用方直连被调方”改成”调用方 -> 队列 -> 被调方”:

空间解耦: 生产者不知道消费者是谁、有几个、在哪(位置透明)
时间解耦: 消费者可以暂时不在线,消息先躺在队列里
速率解耦: 生产洪峰被队列吸收,消费者按自己的节奏拉
(第三条是缓冲的老本行,MQ 同时占了间接层和缓冲两个思想的便宜,
这解释了它为什么无处不在)
RabbitMQ 的 exchange 到 queue 的 binding 就是它的翻译表:
direct 按 routing key 精确翻、topic 按通配翻、fanout 广播,
改 binding 就是改路由,生产者一行代码不动

展开见 MQ基础

8. 位置三选:间接层放哪,一道反复出现的选择题

同一个间接层,物理上放哪,是从 Redis 到分库分表到 RPC 都要答的题:

  1. 内嵌调用方(smart client / JDBC 中间件 / 客户端 LB 如 Ribbon)
    优点: 零额外网络跳数
    缺点: 翻译逻辑复制在每个调用方里,升级要全体动,跨语言要重写
  2. 独立中间层(twemproxy / MyCat / NGINX / 网关)
    优点: 调用方零改造、集中管控、语言无关
    缺点: 多一跳延迟;代理自身成为要做高可用的对象
  3. 下沉服务端(Redis Cluster 的 MOVED + 客户端缓存)
    优点: 服务端权威,客户端只是缓存,两头分摊
    缺点: 协议要客户端配合(收到 MOVED 会刷新表的才叫 smart client)

做选型时认出”这是间接层位置问题”,讨论直接跳到这张利弊表,不用从头吵。

9. 代价:后半句名言的具体形状

每加一层间接,固定支付四种成本:

  1. 延迟: 每层至少多一跳网络或一次查表,链路延迟是逐层累加的
  2. 新单点: 咽喉挂了全体遭殃。
    所以 NGINX 前面要 keepalived,网关要集群部署
    (为高可用加的层,自己又成了要做高可用的对象,
    层就是这么一层层多起来的)
  3. 排障变长: 来源 IP 要靠 X-Forwarded-For 逐跳传,
    一次请求跨五层,没有全链路 trace 就是盲人摸象;
    502/504 要先回答”这个错误码是哪一层返回的”
  4. 翻译表不一致: 槽位表过期、DNS 缓存没跟上、注册中心推送延迟,
    全是”翻译表的缓存”和现实脱节。
    第 3 章的缓存一致性问题原样适用: 思想之间是嵌套的,不是并列的

10. 联系实际:什么时候该加层,什么时候该删层

该加层的信号:
调用方硬编码目标地址,目标一变全体改配置发版
同一段横切逻辑(鉴权/限流/日志)在每个服务里各写一份
想灰度、想迁移,找不到能拨流量的支点
该删层的信号:
一次请求过五层代理,每层只做了转发
排障时没人说得清这一跳为什么存在
某层的翻译表从上线起就没变过(静态得不需要翻译)
判断标准一句话: 这层的翻译规则会变吗?有人利用它集中做事吗?
两个都答否,它就只剩延迟贡献。

11. 学完本章你能解决什么问题

  1. 间接层白送的三种能力是什么,页表怎么逐条兑现它们?
  2. DNS 和 VIP 各管哪一段收敛速度,为什么秒级切换轮不到 DNS?
  3. NGINX 五种调度策略各配什么场景,X-Forwarded-For 为什么不能直接信?
  4. 网关的 Route/Predicate/Filter 对应翻译结构的哪三件,和 NGINX 怎么分工?
  5. “间接层 + 翻译表缓存”的组合出现在哪四个地方?
  6. 事务自调用失效的病根一句话是什么,三种修法共同在做什么?
  7. DispatcherServlet 和网关的同构点在哪?
  8. MQ 同时占了哪两个思想的便宜,它的翻译表是什么?
  9. 间接层位置三选的利弊表怎么背,Redis 方案史怎么走完三格的?
  10. 四种固定成本是什么,第四种和第 3 章什么关系?

核心一句话:间接层用一次翻译换位置透明、集中控制和平滑变更;它的翻译表本身又是缓存、它自己又可能成为新单点,模式互相嵌套,层就是这样长出来的;判断一层存废,看翻译规则变不变、有没有人在这层集中做事。

延伸阅读