课程大纲 · 选择一课
在每个边界处明确所有权、信任、网络与含义
将系统调用链转化为边界契约,明确责任归属、权限校验、批准的路径以及明确的领域含义。通过一个具有误导性的客户 ID 关联案例和一个依赖调度案例来练习,而不是把箭头当作保证。
28 分钟
作者:FDEInterviews · 已更新
课程文字稿
这是《前沿部署工程基础》的第八课。对于每一条连接,都要说明谁拥有变更权、适用哪些身份与权限、批准的网络路径是什么,以及数据的含义。边界值得显式校验。组件内部的故障同样是调查的一部分。
上一课我们追踪了一条记录,并标注了其证据的局限。现在,我们将重要的跨越点转化为契约——能够指导实现、评审和恢复的契约。
从箭头开始。架构图可以很好地展示所有权、信任和网络边界——前提是作者画了这些内容。只包含方框的图可能会让这些问题悬而未决。
解决办法是标注或补充架构图。不要假设每张图都隐藏了真相。组件也可能在内部发生故障,例如逻辑错误、资源耗尽或存储错误。边界则带来另一种风险:本来各自正常工作的部分之间假设不匹配。所以两者都要调查。
在你的服务与对方的系统之间存在三个边界:所有权,它定义了协调与修正的权利;信任,它既涉及权限也涉及身份;网络,连通性是必要条件,但不等于许可。它们是有用的视角,但并非所有故障模式的详尽清单。
所有权排在第一位。一个由其他团队拥有的数据源,仍然可能支持经批准的修正 API、映射层或联合修复。拥有所有权并不意味着你只能等待。
它意味着你需要知道谁能授权每一次变更,以及数据源与其派生副本之间如何保持一致。以一个格式错误的标识符为例,可能的处理方式包括:契约批准的规范化、显式映射、隔离、拒绝整批数据,或在源头进行修正。
根据含义和后果来选择。如果格式错误的行会使整批数据不安全,那么继续处理其余数据可能是错误的;但如果它们是孤立的,且契约允许部分处理,那么阻塞所有工作可能是不必要的。
在合适的情况下,记录原始值、转换规则及其版本,以及对账证据。不要仅仅因为某个转换能让关联成功,就悄悄修复一个标识符。也不要仅仅因为某个有效的规范化规则改变了数据源的格式,就拒绝它。
接下来是含义。一个共享的词可能掩盖不同的模型。限界上下文是一个显式边界,在其内部领域模型是自洽的,并且模型之间存在映射。
而且,限界上下文并不只是团队所有权边界或网络分段的另一种说法。在这个合成的货运示例中,计费模块的“客户”是指支付发票的法律主体,而调度模块的客户字段标识的是交付地点。两者都使用小整数,但相等的整数并不代表相同的实体。
SQL 会毫无怨言地连接任意两个整数列,因为类型系统只认识整数,对付款方或交付地点一无所知。语义存在于列名之中,而连接条件却忽略了它们。
本课附带一个可运行的 SQLite 测试数据。包含三张表:计费客户、交付地点和货运单。第一个查询直接将货运单与计费客户连接,条件是付款方 ID 等于地点 ID。第二个查询则经由交付地点表,以地点的付款方 ID 与计费客户连接。两者在语法上都有效,但只有一个有实际意义。
以下是它的输出。形状相同的连接将货运单 S1 关联到 North Retail,而显式映射则将 S1 关联到 South Wholesale。S2 也以同样的方式翻转。两个查询都返回两行,因此仅凭行数检查无法发现这个错误。
这个测试数据中的映射是人为提供的领域证据。在真实系统中,应核实其所有者、当前有效性、租户范围和变更历史,并记住该示例并不能证明每个地点永远恰好只有一个付款方。
同样的结构还隐藏着第二个故障。如果某个货运单的地点完全没有映射行,内连接就不会为它返回任何结果,该货运单会无声无息地从报告中消失。在两个步骤都使用左连接可以保留它,旁边带一个空的付款方,这样缺口就成了一行可见的数据,行数也仍然能对上。只在第一步使用左连接,货运单仍会在第二个连接处丢失。
接下来是信任问题,它既涉及身份,也涉及权限。认证用于识别调用者或服务。授权则决定该身份可以访问哪些对象和执行哪些操作。
宽泛的服务凭证在返回结果时,绝不能抹去终端用户的限制。对于检索,必须在未授权内容进入模型上下文或响应之前,强制执行适用的权限。摄取时的过滤、租户专属索引以及查询时的实时检查可以协同工作。正确的设计取决于访问权限如何变化,以及服务路径是什么。
因此,“在查询时而非摄取时处理”是一条不完整的规则,因为缓存、嵌入向量、直接获取和已撤销的授权同样需要控制。追踪授权来源及其新鲜度,然后至少测试以下场景:一个被允许的用户、一个被拒绝的用户、一个已撤销的授权,以及一次缓存命中。
一份未在界面上显示的文档仍然可能泄露,如果它的文本进入了模型或某个可下载的端点。并且,授权绝不能依赖模型生成的声明。
接下来是网络。连通性是必要的,但它不等于权限。应描述源、目标、端口和协议、DNS、代理和 TLS 要求、凭证,以及审批负责人。
从笔记本电脑发起的成功调用并不能证明部署的工作负载可以打通,某条路由在技术上可达也不意味着可以授权将客户数据通过它发送。
如果外部服务不可用或被禁止,队列可以解耦时序,但无法让路由变得被允许;缓存可以减少调用,但同时会引入存储、数据陈旧和访问权限方面的问题。可能需要一个经过批准的本地替代方案,或者采用不同的工作流程。对于被永久禁止的目标,不要把“永远排队”作为解决方案。
在预期的环境中、在真实的网络流程下进行有边界的检查,并记录失败属于哪一类:DNS、连接、TLS、身份验证、授权,还是应用行为。这些层面看起来可能很相似,但它们需要不同的负责人和不同的修复方式。
接下来计算依赖路径。最慢的请求并不自动等于整个项目的时间线。有些前置条件可以并行进行,而集成验证可能要等几项都完成之后才能开始。
在这个假设的计划中,路由审查需要五个工作日,身份审查需要三个,实施需要四个,并且我们假设这三项可以在分配了各自独立人力的情况下同时开始。端到端检查需要两项批准和实施都完成,然后需要两天时间。
最早完成时间取三者中最长的一个,即五天的路由审查,再加上两天,在这些假设下就是七个工作日。现在假设身份审查必须等路由审查完成后才能开始。路径就变成五加三加二,即十天,前提是实施仍然并行完成。
而如果同一名工程师既要实施又要执行另一项任务,人力安排可能再次改变进度。因此,要记录依赖关系和人力假设,而不是把最大的单个数字当作答案来报告。
最后,编写边界登记表。对于货运路径,每个重要的跨越点占一行,并为每项决策链接其来源。
按顺序填写每一行:从生产者和消费者,到变更的负责人、适用的身份和权限、经过批准的路由、数据在每一侧的含义、失败时的行为,以及证据状态及其来源。
一个有用的未解决行应这样写:付款方映射历史未提供,计费数据负责人需在历史报表生成前确认变更。它不会说计费是阻碍因素。前者识别出一个可测试的依赖项,后者则把不确定性变成了指责。
请在此暂停视频。运行 join fixture,修改 site-to-payer 映射,并验证显式 join 是否遵循这些映射。尝试交叉映射,以及每个 payer ID 恰好等于其 site ID 的映射。同构的 join 会忽略映射,因此它可能碰巧返回正确答案,但规则本身仍是错误的。添加一个缺失的 site 映射,并决定真实工作流应如何暴露该未解决的 shipment。为信任边界编写三个负面检查,包括已撤销的访问权限和缓存的响应。对于一条提议的外部路由,说明它在连接性和数据使用方面各需要什么,并解释为什么引入队列并不能免除其中任何一项。最后,计算七天和十天计划,并指出区分这两个计划的关键假设。你的陈述在点出决策、契约和证据时才算通过,而不是图中的一个框标签或一次握手。
回顾一下。边界需要为职责、含义、权限和连接性建立契约,而所有权边界允许在不为静默转换或自动部分处理辩护的前提下进行协同修复。相同的字段类型和相同的行数并不能证明领域含义等价。将权限控制应用于所有服务路径,包括缓存和当前授予的权限,并基于依赖关系和容量进行规划,而不仅仅是看单个最长请求。
这就是第八课。本系列下一课:在增加负载之前,先测试重放、慢依赖和变化的数据。完整课程及其练习和演练见 fdeinterviews.com。
TL;DR:对于每个连接,说明谁拥有变更权、适用哪种身份和权限、批准了什么网络路由,以及数据的含义。边界值得显式检查,而组件内部的故障同样属于调查范围。
你现在的位置。上一课追踪了一条记录并标注了证据的局限。现在,把那些重要的跨越点转化为能够指导实现、评审和恢复的契约。
箭头需要契约
当架构图的作者愿意标注时,架构图可以很好地展示所有权、信任和网络边界。一张只有框的图可能让这些问题悬而未决,补救方法是为其添加注释或补充说明,而不是假设每张图都隐藏了真相。
组件可能因逻辑错误、资源耗尽或存储错误而在内部发生故障,而边界还会增加各部分之间假设不匹配的风险——即使这些部分各自运行良好——所以要两者都调查。下面三种边界类型是有用的视角,并非所有故障模式的详尽列表。
所有权定义了协调与纠正的权利
由另一个团队拥有的数据源可能仍然支持经批准的纠正 API、映射层或联合修复。所有权并不意味着你只能等待;它意味着你需要知道谁能授权每次变更,以及源数据和派生副本如何保持一致。
对于格式错误的标识符,可能的处理方式包括:按契约批准的规范化、显式映射、隔离(quarantine)、拒绝整个批次,或修正源头。应根据含义和后果来选择。如果格式错误的行导致整个批次不安全,继续处理其余部分可能是错误的;如果这些行是孤立的且契约允许部分处理,那么阻塞所有工作可能是不必要的。
在适当的情况下,应记录原始值、转换规则/版本以及对账证据。不要仅仅因为某个转换能让连接(join)成功,就悄悄“修复”一个标识符。同样,也不应仅仅因为某个有效的规范化规则改变了源数据的格式,就拒绝该规则。
同一个词可能隐藏不同的模型
Martin Fowler 对限界上下文(bounded context)的解释描述了显式的边界,域模型在这些边界内是连贯的,并且模型之间存在映射。限界上下文并不只是团队所有权边界或网络段的同义词。
在这个合成的货运示例中,计费(billing)系统的“客户”是支付发票的法律主体,而调度(dispatch)系统的“客户”字段标识的是交付站点。两者都使用小整数,但相等的整数并不代表相同的实体。SQL 会毫无怨言地连接任意两个整数列,因为类型系统只了解整数,而不了解付款方或站点;含义存在于列名之中,而连接条件却忽略了它们。
运行这个 SQLite 测试数据,可以看到一个语法上有效但语义上错误的查询:
import sqlite3
conn = sqlite3.connect(':memory:')
conn.executescript('''
CREATE TABLE billing_customer (payer_id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE delivery_site (site_id INTEGER PRIMARY KEY, payer_id INTEGER, name TEXT);
CREATE TABLE shipment (shipment_id TEXT PRIMARY KEY, site_id INTEGER);
INSERT INTO billing_customer VALUES (1, 'North Retail'), (2, 'South Wholesale');
INSERT INTO delivery_site VALUES (1, 2, 'Harbor depot'), (2, 1, 'Hill depot');
INSERT INTO shipment VALUES ('S1', 1), ('S2', 2);
''')
wrong = conn.execute('''
SELECT s.shipment_id, b.name
FROM shipment s JOIN billing_customer b ON b.payer_id = s.site_id
ORDER BY s.shipment_id
''').fetchall()
correct = conn.execute('''
SELECT s.shipment_id, b.name
FROM shipment s
JOIN delivery_site d ON d.site_id = s.site_id
JOIN billing_customer b ON b.payer_id = d.payer_id
ORDER BY s.shipment_id
''').fetchall()
print('same-shaped IDs:', wrong)
print('explicit mapping:', correct)
assert correct == [('S1', 'South Wholesale'), ('S2', 'North Retail')]
# same-shaped IDs: [('S1', 'North Retail'), ('S2', 'South Wholesale')]
# explicit mapping: [('S1', 'South Wholesale'), ('S2', 'North Retail')]两个查询都返回两行,因此仅靠行数检查无法发现错误。此测试夹具中的映射是提供的领域证据;在真实系统中,应核实其所有者、当前有效性、租户范围和变更历史。该示例也并未证明每个站点永远恰好只有一个付款方。同样的结构还隐藏着第二个失败。如果某次出货的站点根本没有映射行,内连接将不会为其返回任何内容,该出货就会在毫无警告的情况下从报告中消失。在两个步骤都使用左连接可以保留它,旁边带一个空的付款方,这样缺口就成了一行可见的数据,行数也仍然对得上;而仅在第一步使用左连接,出货仍会在第二个连接处丢失。
信任既涉及权限,也涉及身份
身份验证识别调用方或服务;授权决定该身份可以访问哪些对象和操作。在返回结果时,宽泛的服务凭证不得抹去最终用户的限制。
对于检索,在未授权内容进入模型上下文或响应之前,应强制执行适用的权限。摄取过滤、租户专用索引和实时查询检查可以协同工作;正确的设计取决于访问权限的变化和服务路径。“查询时而非摄取时”是一条不完整的规则,因为缓存、嵌入、直接获取和已撤销的授权也需要控制。
追踪授权来源及其新鲜度,并至少测试以下场景:一个被允许的用户、一个被拒绝的用户、一个已撤销的授权以及一次缓存命中。在 UI 中隐藏的文档,如果其文本到达了模型或可下载的端点,仍然可能泄露,因此授权不应依赖模型生成的声明。
网络连通是必要条件,但不等于权限
描述源、目标、端口/协议、DNS、代理、TLS 要求、凭证和审批负责人。从笔记本电脑成功发起调用并不能证明已部署的工作负载也能成功,技术上可达的路由也不意味着被授权通过它发送客户数据。
如果外部服务不可用或被禁止,队列可以解耦时序,但无法使该路由变得被允许。缓存可以减少调用,但会带来自身的存储、过期和访问问题。可能需要一个经过批准的本地替代方案或不同的工作流程。不要把“永远排队”作为对永久禁止目标的一种解决方案。
在真实的网络进程下,从目标环境执行有边界的检查。记录失败属于 DNS、连接、TLS、身份验证、授权还是应用行为,因为这些层面会产生相似的症状,却需要不同的负责人和修复方式。
依赖路径,经过计算
最慢的请求并不自动等于整个项目的时间线:有些前置条件可以并行运行,而集成验证可能要等若干任务完成后才能开始。
在此模拟计划中,路线审核需要五个工作日,身份审核需要三个,实施需要四个,且三者可以同时开始,各自分配独立的资源。端到端检查需要两项审批和实施都完成,然后需要两天。在这些假设下,最早完成时间为 max(5, 3, 4) + 2 = 7 个工作日。
max(5, 3, 4) + 2 = 7
working days under those assumptions.
如果身份审核必须等路线审核完成后才能开始,路径变为 5 + 3 + 2 = 10 天,前提是实施仍并行完成。如果同一名工程师既要实施又要执行另一项任务,资源情况可能再次改变进度。请记录依赖关系和人员配置假设,而不是把最大的单个数字当作答案报告出来。
5 + 3 + 2 = 10
days, assuming implementation still finishes in parallel. If the same engineer must implement and also do another task, capacity may change the schedule again. Record dependency and staffing assumptions rather than reporting the largest single number as the answer.
边界登记表
对于货运路径,为每个重要的跨越点使用一行:生产者和消费者、数据含义、当前所有者、允许的身份/操作、路由、契约/版本、失败行为和证据状态。将每项决策的来源链接起来。
一条有用的未解决行应写“未提供付款方映射历史;计费数据所有者需在历史报表生成前确认变更。”而不是写“计费是阻塞项”,因为前者指出了一个可测试的依赖关系,而后者把不确定性变成了指责。
上面的主干是边界登记表中的一行,按你填写它的顺序绘制:从箭头的两端到其上每一项声明背后的证据。
在继续之前请完成以下操作
运行连接测试用例,修改站点到付款方的映射,并验证显式连接是否遵循这些映射。尝试交叉映射以及每个付款方 ID 恰好等于其站点 ID 的映射:基于相同形状 ID 的连接会忽略映射,因此它可能碰巧返回正确答案,但规则本身仍然是错误的。添加一个缺失的站点映射,并决定真实工作流应如何暴露该未解决的货运记录;内连接会静默地将其省略,因此可能需要对账计数或在两个步骤都使用左连接。
为信任边界编写三个负面检查,包括已撤销的访问权限和缓存的响应。对于提议的外部路由,同时说明连接性和数据使用方面的前提条件,并解释为什么添加一个队列并不能免除其中任何一项。
最后,计算七天和十天的依赖计划,并找出区分二者的假设。当你的边界登记表能够指明决策、契约和证据,而不是依赖架构图中的方框标签或握手符号时,它才算合格。
深入探索
- 多租户隔离是被当作架构需求对待的信任边界。
- VPC 部署是当你必须在网络边界内部署时它所呈现的样子。
- PII 处理涵盖数据跨越所有权边界进入你的系统那一刻所发生的变化。
- 数据质量是针对你无法在源头修复的数据的学科。
- 对一千万份文档进行权限感知检索是作为面试题出现的信任边界,而且是一道常见的筛选题。
- 参考架构是你在审视时需要逐层核对的清单,包括各层与信任边界。
核心要点
- 边界需要就职责、语义、权限和连接性建立契约。
- 所有权边界允许协同修复,但不能成为静默转换或自动部分处理的理由。
- 字段类型和行数相同,并不意味着领域语义等价。
- 在所有服务路径上应用权限控制,包括缓存和现有授权。
- 从依赖关系和容量出发进行规划,而不仅仅是单个最长请求。
自我检测
先作答再看答案。回忆才能加深记忆,单纯认出来不行。
- 1两个连接查询都返回两行。这是否足以证明付款方归属是正确的?
- 2某个外部端点被禁止。把请求放入队列是否就能让这种集成变得可接受?
- 3三个并行任务分别需要五天、三天和四天,之后是两天的检查。在给定的独立容量假设下,最早完成时间是多少?
免费注册以保存进度并返回本课程。