两种交通方向,一台设备
欢迎
大多数架构图显示的交通只有一种方向:客户端在上方,服务器在下方,箭头指向下方。现实中交通却有两种方向。
入口:外部客户端通过这个路径访问您的服务。网络边缘的反向代理终止TLS, 路由请求并强制执行访问策略。
出口:您的服务通过这个路径访问外部服务。调用支付处理器的API,获取 webhook 目标,向伙伴发送请求。通常通过一个允许列表的前向代理或NAT网关。
许多架构从一个盒子开始,同时处理两者。它起初有效,直到有一天不再有效。这种故障模式很微妙,只有在内部服务足够多时才会出现,并且教会了一个关于分担关注的重要课堂。
在本课程结束时,您将理解:
- 为什么入口与出口代表着根本不同的交通模式,具有不同的扩展轴线和故障模式
- Hairpin NAT 和为什么一个代理试图连接到自己会失败
- 架构分叉:一个盒子变成了两个,每个盒子独自拥有一些专属的东西
- 安全隔离的收益:每个侧可以锁定到其真正允许的同行
- 如何识别您的单盒设计已经跨越了界限,在这种情况下拆分是必要的
为什么方向需要不同的工具
在一个网络边界上有两种不同的工作负载
入口交通特征:
- 由外部方(整个互联网)发起
- 量级与您的用户基数成比例
- TLS终止,请求路由,每个来源的限制速率
- 深度防御关注:DDoS,滥用,抓取
- 公共IP需要接受来自任何人的连接
出口交通特征:
- 由您自己的服务发起(一个已知的小客户集)
- 量级与您的服务之间调用和外部-API调用模式成比例
- 源 IP 允许列表在远程端点 (您有一个固定的出bounds IP, 合作伙伴信任)
- 深度防御的关注点: 数据外泄, 被损坏的内部服务调用出bounds
- 应该拒绝来自除您自己的服务之外的任何连接
关键不对称性: 入gress 接受来自世界的流量; 出gress 只接受来自您自己的服务的流量。将它们放在同一台机器上意味着这台机器必须同时从世界(用于入gress)和从您的服务(用于出gress)处被访问。满足一个的防火墙规则会损害另一个。
增长路径: 一個微小的项目可以在一个 IP 和一个工具后面隐藏,因为体积小,合作伙伴-IP-允许列表短。随着项目的增长,两个角色的摩擦会增加,有一天特定的故障模式(回环NAT)会迫使分裂。
那个bug迫使分裂的Bug
清洁停机的故事
想象一下在生产集群中发生的实际架构分叉。下面的名字已经被更改;形状与团队在野外遇到的形状相同。
一个组织在 203.0.113.5 上运行单个代理服务器。它处理入gress (用户端口 443)和出gress (内部服务调用出bounds的 SOCKS5 端口 1080)。内部服务位于私有子网,并将所有出bounds流量路由到 203.0.113.5:1080 的 SOCKS5 代理。
在同一个 203.0.113.5 后面的服务之一是 api.example.com。公共 DNS 将 api.example.com 解析为 203.0.113.5。
现在一个不同的内部服务需要调用 api.example.com。其出bounds路径:
1. 内部服务解析 api.example.com → 203.0.113.5
2. 内部服务通过 SOCKS5 出gress 代理发送请求 203.0.113.5:1080
3. 代理尝试从自身打开一个连接到 203.0.113.5:443
4. 连接被拒绝。 包括在同一NAT中进入和退出的数据包,大多数网络堆栈拒绝。代理无法通过其公共 IP 连接到自身。
这是一种 hairpin NAT: 一个从 NAT 出口的数据包需要重新进入相同的 NAT 才能到达目的地。没有在路由层中特别支持 hairpin 的情况下,数据包会丢失。
为什么会在后期出现
项目刚开始的时候,所有内部服务要么通过私有主机名(internal-api.local)与其他内部服务通信,要么不调用自己的组织的公共服务。hairpin 路径根本不存在。
然后,一个新功能要求服务 A 调用 api.example.com (一个公共主机名)。hairpin 路径被激活。连接被拒绝。出错。
修复只解决了症状(强制解析器给出 api.example.com 的私有 IP 而不是公共 IP)。根本原因:一个机器承担了太多的工作。
架构分叉
一个盒子变成了两个
干净的修复:将代理分成两个机器。
入口服务器 (公共 IP 203.0.113.5):
- Caddy / 反向代理端口 80, 443
- 公共 DNS 记录指向这里
- 主机 api.example.com、app.example.com 等
出入口服务器 (不同公共 IP 203.0.113.99):
- SOCKS5 / 转发代理端口 1080
- 防火墙限制仅允许来自内部子网的 IP 进行入站连接
- 内部服务将所有出站流量路由到此地址
这买了什么:
1. hairpin 已解决。 内部服务调用 api.example.com 通过 203.0.113.99 (出入口)路由到出站,然后正常连接到 203.0.113.5 (入口,一个不同的 IP)。NAT 循环消失因为这两个 IP 分别位于不同的机器上。
2. 安全隔离。 出入口服务器的防火墙可以限制到一个有限的内部 IP 集合。入口服务器的防火墙保持开放给世界。两个单独的规则集,每个都清晰地表达了一个角色。
3. 独立扩展。 入口带宽随用户增长;出入口带宽随内部服务活动增长。同时升级一个没有影响另一个。
4. 故障隔离。 出入口配置错误不再破坏公共网站。对公共网站的 DDoS 攻击也不再导致出入口带宽不足。
5. 更清晰的 mental model. 每台机器都有一份工作。工程师不用考虑 ingress concerns 而同时考虑 egress, & vice versa.
两个轴、两个-sizing决策
独立扩展
在拆分之前,两者方向的增长都会对同一台机器产生压力。在拆分之后,每个方向都有自己的配置。
ingress sizing: 根据用户数量扩展。容量决策位于公众面向的层次(更多的反向代理实例,更大的虚拟机,在前面有 CDN)。根据峰值用户流量计算带宽预算。
egress sizing: 根据内部服务到外部 API 调用量扩展。通常由 webhook 发送,支付处理器调用或第三方数据查询决定。根据内部调用模式计算带宽预算。
故障隔离: 公共 ingress 的 DDoS 攻击不再消耗 egress 带宽(那些支付处理器调用通过那些机器)。egress proxy 崩溃不再导致公众网站下线(用户可以继续访问网站;只有内部出bounds 调用失败)。
不同的 SLO: ingress 可用性对用户重要(可见的网站中断);egress 可用性对操作员重要(可能需要更长时间发现故障)。每个侧都可以承担自己的 SLO。
多个 egress 服务器
一旦 egress 角色变成自己的机器,接下来最明显的动作就是在 HA 的负载均衡后运行多个 egress 机器。每个新的内部服务都指向 egress 主机名(解析到负载均衡池)而不是指向单个 IP。
与分布式系统的其它部分一样:一旦一层变成无状态且有自己的角色,它便可以廉价地增加。
一个新的合作集成
您的组织按照设计运行 ingress / egress 拆分。egress 服务器具有固定的公众 IP (203.0.113.99),您已经将其 allowlisted 与三个现有的合作 API(支付处理器,短信网关,电子邮件提供者)。
一个产品团队想要添加一个第四个集成:一个回调交付系统,调用全球客户端的端点。预估流量:每分钟10,000个调用,峰值到达30,000。
为一个成长的服务设计网络边界
合成
你已经了解了为什么入口和出口需要不同的工具、在真实集群中,hairpin NAT失败迫使拆分以及在拆分之后,独立扩展、安全隔离和故障隔离会得到积累。
应用所有四个。
一个中等规模的SaaS公司为他们的用户提供三个产品子域(app、api、admin),另外还有四个出bounds集成(Stripe、Twilio、SendGrid、一个客户端回调系统)。今天,everything都在一个单一的代理机器后面,公有IP只有一个。他们已经开始收到关于内部服务尝试调用api.example.com时的间歇性hairpin故障的报告。他们想要设计一个永久的解决方案。
Where This Course Goes Next
Where This Course Goes Next
你现在已经看到分布式系统中一个最清晰的分担责任重构:一个盒子变成了两个,每个都有明确的角色,系统在此过程中继承了扩展、安全性和故障隔离的优点。
下一个课程(cs_distsys_failure_modes_and_blast_radius)会扩展故障隔离的推理。你将阅读一个消毒的DNS-SERVFAIL后事录,识别蔓延故障模式,并撰写无罪的行动项目,针对系统而不是人来进行。
伴侌课: geometry_of_ingress_egress_separation 将分裂视为双分图,并探讨切点、网络分区以及图论对网络边界的启示。
很好。继续前进。