跳到主要内容

第 15 章 联调与全链路走通

副题:把整条路串起来——一次请求的完整旅程,阶段 1 收官

第 8 到第 14 章,我们放大看了请求旅程的每一段:浏览器怎么渲染、网络怎么传、服务端怎么接、数据怎么存。每一段各自都能跑——现在把它们连起来。本章做两件事:第一,解决"连起来就不通"的联调问题;第二,把整条路完整走一遍。

请求旅程地图进度:浏览器站 ✅(8-9)→ 网络站 ✅(10-11)→ 服务端站 ✅(12-14)→ 本章:全链路收官——前面七站各自放大看完了,现在退远一步,看整条路。

本章要建立的认知

  1. 联调问题是三类问题的集合:契约问题(字段对不上)、环境问题(本地好的线上不行)、协作问题(各改各的);
  2. 一次请求的完整旅程:从老周点击到页面渲染,每一站都有前面章节的机制在管——本章把它们串成一条线;
  3. 联调走通 = 阶段 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、参数、返回、错误码、权限——联调时拿着这张表对,比凭记忆问快十倍。


基本盘二:环境分层。

本地、测试、生产三层,各有各的用途:

  • 本地:代码最新,数据随意,随便折腾
  • 测试:接近生产的配置,真实流程走查,联调的主战场
  • 生产:用户在用,改动要走流程
图15-1 新 图稿占位
联调环境示意
本地/测试/生产环境分层

"本地是好的,测试就不行",根源是环境差异。解法三件事:

第一,配置集中管理。 数据库地址、域名这些环境相关的值,不写死在代码里,走环境变量。测试环境的 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 章)
图15-2 一级#3完整放大 图稿占位
一次请求完整旅程
↻ 本章第 3 次复用,原始位置:图8-1(§8.x)
8-14 章拼成一张图

用老周"登录后发一条内容"走一遍这十站:

① 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 章决策记录里写的演进条件("首页变慢信号出现再上缓存"),将会在此刻触发。