第 15 章 联调与全链路走通
副题:把整条路串起来——一次请求的完整旅程,阶段 1 收官
第 8 到第 14 章,我们放大看了请求旅程的每一段:浏览器怎么渲染、网络怎么传、服务端怎么接、数据怎么存。每一段各自都能跑——现在把它们连起来。本章做两件事:第一,解决"连起来就不通"的联调问题;第二,把整条路完整走一遍。
请求旅程地图进度:浏览器站 ✅(8-9)→ 网络站 ✅(10-11)→ 服务端站 ✅(12-14)→ 本章:全链路收官——前面七站各自放大看完了,现在退远一步,看整条路。
本章要建立的认知
- 联调问题是三类问题的集合:契约问题(字段对不上)、环境问题(本地好的线上不行)、协作问题(各改各的);
- 一次请求的完整旅程:从老周点击到页面渲染,每一站都有前面章节的机制在管——本章把它们串成一条线;
- 联调走通 = 阶段 1 的验收前置:第 4 章的验收标准,逐项在这里兑现——三周 MVP 的最后一公里(从"各自能跑"到"一起能跑")。
15.1 各自能跑,连起来就不通
三周 MVP 的第二十天,明天上线。前端、后端、数据库各自本地跑得好好的,今天部署到测试环境,走一遍完整流程。
下午三点出现问题,列表页白屏。
第一个问题:字段名对不上。 控制台报错 Cannot read properties of undefined (reading 'author_name')。前端按契约要的是 author_name,后端实现时手滑写成了 authorName。契约上明明写着下划线,代码里变成了驼峰——契约和实现之间差了两个字节,联调时就差了一个下午。
第二个问题:"我本地是好的啊"。 小明本地数据库有一堆测试数据,页面渲染正常;测试环境是空库,列表接口返回空数组,前端拿到空数组渲染空页面,看着像白屏。代码没错,环境的数据不一样。
第三个问题:跨域。 浏览器报 No 'Access-Control-Allow-Origin' header。本地开发时前后端用一个域名跑,没触发跨域;测试环境分了前端域和 API 域,CORS 配置没跟上——又是本地没遇到、部署后才暴露。
三个问题,一个下午。修完天黑了。小明把它们写在便签上:字段对不上、环境不一样、配置没配齐——共同的名字叫联调。 三周 MVP 里,联调占了三到四天,超过十分之一工期。"减少联调时间"是实打实的工期优化。 第 7 章花整章讲契约,就是为了砍这笔账。
还有第四个隐藏问题:时间格式。 列表页要显示"3 分钟前",后端返回 ISO 字符串,前端要的是时间戳——契约里没写清楚。类似的"小事"还有分页从 0 还是从 1 开始、空值返回 null 还是空串。契约里看起来不重要的细节,联调时每一件都是半小时。
联调分两种形态。 开发期每写完一个接口就前后端对一次,问题摊薄在日常;上线前集中联调,问题堆到最后一个下午全部爆发。本章的清单,两种形态都适用。
上线前夜的纪律: 只修联调暴露的问题,不做任何顺手优化。修字段时顺手"优化"别的逻辑,是上线前事故的头号来源。新功能和重构,留给上线以后再说。
15.2 联调问题是什么问题
联调的痛,一句话就能说清:各自跑都对,连起来就错。
上面遇到的问题可以归到三类:
第一类:契约问题(七成以上)
字段名、参数格式、错误码、分页规则——两边各写各的,合起来对不上。第 7 章说过"约定不清楚,联调就是灾难",现在看到了灾难现场:文档里写着 author_name,代码里手滑写成 authorName;文档没写时间格式,前端猜 ISO,后端猜时间戳;同一句话两个理解——"删除"是软删还是硬删?
契约问题有三种形态:
- 文档没写:空值怎么表示、分页从 0 还是从 1 开始
- 代码没照写:文档写了,实现时手滑或想当然
- 两边理解不同:同一句话,各自脑补了不同版本
三种形态的解法都一样:回到契约,把话说死。 第 7 章的"契约是唯一真相",在这里就是操作手册。
联调问题里契约问题占大头,而契约问题只能靠人事前消灭。 环境不一致可以用配置管理压下去,协作问题可以用流程缓解,但字段对不上、格式不一致,每一条都要人坐下来改。
第二类:环境问题
"我本地是好的"——本地有测试数据,测试环境是空库;本地用一个域名,测试环境分两个域;本地依赖版本 2.1,测试环境是 2.0。代码对环境的假设,在另一个环境里不成立。
第三类:协作问题
前后端各自开发,联调时才第一次见面。一个人写接口时随便命名,第二个人接入时冲突爆发——联调是之前欠的账,集中结算的时刻。
三个问题有一个共同的根:接口是两边之间的承诺,联调问题就是承诺违约。 承诺没写清(契约问题)、承诺的语境变了(环境问题)、承诺双方没对过(协作问题)。所以解法是把承诺在联调之前就管好,联调时自然不用多加班。
联调还有一个收尾动作:复盘。
每次联调结束,把暴露的"契约没说清"的新问题补进文档,两边同步。第一轮联调三天的项目,第二轮可能只要一天——契约越用越厚,联调越用越短。
15.3 联调的三个基本盘
知道了问题是什么,解法就是三个基本盘——都是前面章节已经铺好的。
基本盘一:契约先行(第 7 章)。
接口清单、字段命名、错误码、分页规则——在写第一行代码之前就定好,前后端照着同一份契约写。联调时发现对不上,第一反应是查契约:是代码没照契约写(改代码),还是契约本身有歧义(改契约,两边同步)。
第 7 章的接口清单,本身就是联调清单的骨架。每个接口一行:方法、URL、参数、返回、错误码、权限——联调时拿着这张表对,比凭记忆问快十倍。
基本盘二:环境分层。
本地、测试、生产三层,各有各的用途:
- 本地:代码最新,数据随意,随便折腾
- 测试:接近生产的配置,真实流程走查,联调的主战场
- 生产:用户在用,改动要走流程
"本地是好的,测试就不行",根源是环境差异。解法三件事:
第一,配置集中管理。 数据库地址、域名这些环境相关的值,不写死在代码里,走环境变量。测试环境的 CORS 就按生产的样子配好——本地用一个域名不触发跨域,测试用两个域名才能暴露问题。测试环境是生产的彩排,不是本地的复制品。
第二,测试数据要固定。 用造数脚本(seed)灌入固定的测试账号、固定的内容和评论。测试环境的数据要"已知",不必"真实"——数据已知,才能判断接口返回对不对。数据被改乱了,一键重置,脚本重跑一遍。
第三,锁死依赖版本。 "我本地 Node 18,测试环境 Node 16"——这种版本漂移不报错,只是行为不一样。用 lockfile 把每个依赖的精确版本锁进仓库,部署时照单安装。
基本盘三:联调清单。
从第 7 章的 12 个接口里,裁剪出一条必须走通的主干道:
登录 → 发内容 → 看列表 → 评论 → 看详情 → 删除(只有作者能删)→ 登出
七步覆盖了核心接口、401/403 权限、数据库读写。联调从"想到哪调到哪"变成"照着单子走"——漏掉的路径,就是上线后用户替我们发现的 bug。
清单的进化路径:阶段 1 先用手工清单,后面章节中我们会补上契约测试(把"字段对得上、状态码对得上"写成自动化断言,每次改动后自动跑)。
后端没好时,前端可以先 Mock。 按契约造假数据开发,接口好了再切真。但记住:mock 是"按契约造数据",绕不开契约——mock 和真实接口都该以同一份契约为准,否则 mock 省的时间,联调阶段加倍还。
最后一个小动作:联调前互相看一遍代码。 后端看前端怎么调接口,前端看后端返回什么——先看代码后联调,能提前发现一半的字段问题。
15.4 一次请求的完整旅程
联调的三天里,小明把请求从头到尾走了无数遍。现在,把这条完整的路画出来(图 15-2)——图 8-1 那张旅程图第一次全部点亮,每一站都标上它的机制(和它所在的章节):
老周在浏览器打开首页
│
├─ ① 浏览器:输入网址 → DNS 问路(第 11 章:递归解析找到 IP)
├─ ② 网络:TCP 三次握手 + HTTPS 加密(第 10 章:连接四段)
├─ ③ CDN:静态资源就近返回(第 11 章);动态接口回源
├─ ④ 服务端入口:中间件管道(第 12 章:日志→解析→鉴权)
├─ ⑤ 认证门禁:凭证验证(第 13 章:Session 查表,无效 401)
├─ ⑥ 路由+控制器:URL → 处理函数(第 12 章:第 7 章的契约在这里落地)
├─ ⑦ 业务层:决策与编排(第 12 章:校验、敏感词、调数据层)
├─ ⑧ 数据层:查询走索引(第 14 章:EXPLAIN 过的复合索引)
├─ ⑨ 响应原路返回:DTO 组装 → 中间件 → 网络 → 浏览器
└─ ⑩ 浏览器:渲染管线(第 8 章:解析→树→布局→绘制)+ 框架更新界面(第 9 章)
用老周"登录后发一条内容"走一遍这十站:
① DNS 问路(第 11 章)→ 浏览器问 api.example.com 在哪
② 握手加密(第 10 章)→ TCP 三次握手 + TLS 握手,建立安全通道
③ 就近取静、远路取动(第 11 章)→ CSS/图片走 CDN,接口请求继续向源站
④ 中间件管道(第 12 章)→ 请求进服务端,先过日志、解析、鉴权
⑤ 查凭证(第 13 章)→ Session 表验明正身,req.userId = 1
⑥ 路由分发(第 7 章)→ POST /v1/contents 送到对应控制器
⑦ 业务编排(第 12 章)→ 校验标题、敏感词检查、决定发不发通知
⑧ 数据落盘(第 14 章)→ INSERT 走约束和事务,保证写对
⑨ 组装返回(第 12 章)→ DTO 把结果包成第 7 章约定的格式
⑩ 界面更新(第 8/9 章)→ 浏览器收到响应,前端框架重渲染,老周看到"发布成功"
请求是有去有回的。 去程验身份、查数据库、做决策;回程轻一些——凭证已验过,响应可以直接走缓存,最后在浏览器里渲染成像素。
每一站都有"卡住"的方式: DNS 找不到、握手超时、中间件顺序错、凭证失效、全表扫描……第 8-15 章每一章的事故,都是这条路上某个站点的故障。联调排障的本质,就是判断卡在哪一站。
性能视角:2 秒预算摊到各站
| 站点 | 耗时 | 主人 |
|---|---|---|
| DNS | ~50ms | 第 11 章(本地缓存后更快) |
| 连接 | ~100ms | 第 10 章(Keep-Alive 复用后省掉) |
| TTFB | ~200ms | 第 12/14 章(中间件 10 + 业务 20 + 数据 150) |
| 下载/渲染 | ~1.5s | 第 8/11 章(CSS 拆分前 2MB,拆分后达标) |
旅程里其实有两条路。
静态资源(HTML/CSS/JS/图片)走 CDN 就近返回,几乎不走完整链路。 动态接口(内容/评论/下单)必须走完全程——要鉴权、要查库、要新鲜数据。
GET 和 POST 的旅程也不同。 GET 可以缓存,浏览器和 CDN 都能截胡,可以抄近道;POST 必须到服务端、必须过事务,不能缓存。读的旅程可以偷懒,写的旅程必须走完全程——所以列表页总比发布按钮快。
15.5 旅程排查法:卡在哪一站
第一问:网络段断在哪?
打开 DevTools 的 Network 面板,看请求停在哪一段:
- DNS 段失败 → 域名问题
- 连接段失败/超时 → 网络或防火墙
- TTFB 慢 → 服务端处理慢
- 下载慢 → 响应体太大
第二问:服务端卡在哪一层?
TTFB 慢时,按第 12 章的分层从上到下查:
- 先看数据访问层(EXPLAIN)
- 再看业务层(逻辑是否复杂、循环是否过多)
- 最后看中间件(鉴权查表慢、日志阻塞)
分层既是代码结构,也是排障地图。
第三问:数据查询卡在哪?
数据层三件套:
- EXPLAIN 看执行计划 → Seq Scan 就是索引问题
- rows 估算 离谱 → 统计信息该更新
- 慢查询日志 → 有没有被记录、频率多高
三问走完,90% 的联调问题能定位到"某一站的某一个机制"。
三问法的边界:不管安全。
接口"看起来正常但数据泄露"——越权、SQL 注入——三问定位不了。它管的是"通不通、快不快",不管"对不对"。
上线后的进化:从人工到自动。
联调期,三问是每次报错都手动走一遍。上线后,这套逻辑交给监控:
- 接口延迟报警 → 第一问的自动化
- 链路追踪 → 第二问的自动化
- 慢查询自动记录 → 第三问的自动化
人工排查和自动化监控,是同一套方法的前后两种形态。
用三问法重走 15.1 的三个问题:
| 问题 | 排查过程 | 结论 |
|---|---|---|
| 字段对不上 | 第一问:请求到了服务端,TTFB 正常,不是网络段。 第二问:控制器返回正常,是 DTO 组装时字段名手滑。 | 契约问题,改代码 |
| "本地是好的" | 第一问:请求完全正常,没有断。 直接跳到环境比对。 | 环境问题,查配置/数据/版本 |
| CORS 报错 | 第一问:请求发出去了,但浏览器在 Console 拦截。 属于浏览器安全模型(旅程之外)。 | 配置问题,补响应头 |
两个实战工具:
request_id。 前后端对不上时,前端把报错里的 request_id 丢给后端,后端按这个 ID 一搜,就能看到那次请求完整经过:中间件几点几分接进来的、业务层做了什么决策、数据层哪条 SQL 耗时多少。
复现纪律。 联调报错,先复现再修。复现三要素凑齐,问题才算"稳定":什么接口什么参数、哪条数据触发的、在哪个环境。三要素缺一个,"偶尔出现"的 bug 最难修。排查的第一步,是让问题能稳定地再次出现。
15.6 MVP 验收与上线
联调走通,最后一天:验收。第 4 章定的三周 MVP,到了结算时刻。
功能验收,对照需求清单逐项过。
登录、发内容、看列表、评论、看详情、删除、登出——联调清单的七步就是走查路径,全部通过。第 7 章的 12 个接口,每一个都联调通过。接口清单就是功能验收的代码侧。
非功能验收,拿预算逐段量。
- 首页 P95 < 2 秒:用第 15.5 的排查法量了一遍,网络段、服务端、数据层全在预算内。第 12 章层内预算、第 14 章数据层 150 毫秒,都在线内。
- 4G 首屏 3 秒:第 8 章关键渲染路径优化(2MB CSS 拆分)兑现了承诺。
约束验收。
- 数据不能丢:第 14 章事务 + 备份机制在不在?
- 登录态 7 天:第 13 章滑动过期生效没有?
- 交易后置:第 6 章
order/payment表先建好、安静地等着阶段 3。
约束验收,"没出过事"不算通过,"机制在"才算数。
验收单长什么样?
一行一项:需求 → 验收动作 → 结果(通过/不通过)。
不通过的项,要么修(代码没覆盖到),要么回到第 4 章砍掉(需求本身不可行)。
需求清单的逐行对照也走一遍——这是最容易被跳过的动作:
- "发内容" → 联调七步里点一遍
- "内容不能丢" → 看事务和备份
- "首页要快" → 拿 P95 预算量
- "登录 7 天" → 隔天再看登录态在不在
用户验收:**老周作为用户验收人。**他不懂契约、不懂索引,只看两件事:能不能用、卡不卡。 功能清单过完,让老周自己点一遍——他点的路径可能和联调清单不一样。**他在清单外发现的每个问题,都是清单的盲区。**这就是"用户验收"和"技术验收"的区别:技术验收证明系统符合契约,用户验收证明契约符合需求。
上线后的第一天,看三样东西。
- 错误日志:有没有联调没覆盖的路径报错?
- 接口延迟:之前旅程账,哪一站超预算?
- 用户行为:老周在用吗?他卡在哪一步?
上线之后,问题从已知变成未知。 联调时问题是我们列出来的,上线后问题是用户替我们发现的。上线本身只有几分钟。 它把"我们以为对的"变成"用户检验的",从这一刻起接受真实用户检验。
最后记一个历史时刻。老周的第一条内容,写进了 content 表的第一行。第 6 章建的表、第 14 章的约束和事务,在这一刻第一次被真实数据填上。
15.7 本章对应表
| 业务诉求 | 技术选择 | 为什么 | 代价/取舍 |
|---|---|---|---|
| 各自能跑连起来不通 | 联调(契约/环境/协作三归类) | 联调问题=接口承诺违约(第 7 章回扣) | 联调本身是流程成本 |
| 字段/参数对不上 | 契约先行(接口清单=联调清单) | 第 7 章事前投资,本章事后结算 | 契约要维护(版本化) |
| "本地是好的"线上不行 | 环境分层(本地/测试/生产) | 代码对环境的假设要显式管理 | 三层环境要维护(配置/数据) |
| 漏掉路径上线才暴露 | 联调清单(七步走查) | 从"想到哪调到哪"到"照着单子走" | 清单要跟着接口演进 |
| 一次请求怎么完整走 | 旅程集中展示(图 15-2) | 第 8-14 章拼成一条路,排障有地图 | 每站机制要回指原章复习 |
| MVP 怎么算完成 | 验收清单(功能/非功能/约束) | 第 4 章验收标准逐项兑现 | 非功能要"拿预算量"不"凭感觉" |
本章小结
-
联调问题是三类问题的集合(契约/环境/协作)——本质是第 7 章"接口是承诺"的违约;
-
一次请求的完整旅程:十站、七章、一张图——第 8-14 章每一站的机制,在这里串成一条路;
-
旅程排查法三问:断在哪一段 → 卡在哪一层 → 数据哪个环节——零件都在前面章节,本章拼成顺序;
-
MVP 验收:第 4 章的验收标准逐项兑现——功能走查、非功能拿预算量、约束验机制;
-
阶段 1 结束,MVP 上线——下一站,用户开始增长(第 16 章性能优化,阶段 2 开篇:首页变慢的演进条件正式触发)。
下一章
三周 MVP 上线了。老周用得很开心,把产品介绍给了朋友,朋友又介绍给朋友。注册用户开始涨——从几十到几百到几千。
之前定的验收预算(首页 P95 < 2s)开始被突破:内容多了、图片多了、同时在线的人多了——首页从"秒开"变成"转圈"。第 14 章决策记录里写的演进条件("首页变慢信号出现再上缓存"),将会在此刻触发。