最近在技术交流群里看到一句话,让我一直琢磨了很久:Strata 确实可以封神了。第一眼望去,我以为是哪家又发布了一个叫 Strata 的新工具、新框架,正准备去看看它到底解决了什么痛点。结果在翻某团队的项目设计文档时才发现,Strata 说的根本不是一个具体产品,而是一种从名字就自带“分层”气质的架构思想。它来源于地层学里的复数词根,对应到软件开发里就是我们在系统设计中最常聊的那件事——分层。
分层这件事看起来没有技术含量,但说实话,我认真复盘过自己经手的大大小小项目后,最想给团队强调的反而就是一套清晰、稳定、能约束住所有人的 Strata 规则。这篇文章就聊聊为什么我把 Strata 看得这么重,它是怎么从数据仓库一路帮我理清应用架构和业务逻辑的,以及如果你也想用这套思路改造项目,应该从哪里下刀。
人会直觉地为复杂的东西寻找秩序。Strata 的核心秩序就是:按照时间、语义或者稳定性,把系统横向分割成若干层,让每一层只对上一层负责,只依赖自己的下一层。最早我是做数据集成项目的,表多到没人敢改上游结构;后来转应用开发,又看到业务规则到处散落的问题。两个场景看起来完全不同,但最后落地的解法都指向了同一个词——Strata。如果你正在为什么是分层、到底该分几层、怎么分层才不显得过度设计而纠结,希望这篇文章能给你一个可以直接抄作业的版本。
1. Strata 到底在说什么:它不是某个工具,而是一套秩序
1.1 从“地层”这个意象说起
Strata 是 stratum 的复数形式,地质学里用它描述一层层堆叠的岩石。每一层的成分和年代都可以完全不一样,但相邻层之间边界清楚,上下关系稳定。地质学家画一张地层图,不会让你去猜某个沉积层和某个熔岩层谁先谁后,因为那是按照形成时间自动排列的;你如果想研究侏罗纪的化石,只需要示意到对应深度的那一段,不用把整座山都翻开来看。
这套抽象对软件系统极其适用。最朴素的分层模型就是接入层、业务层、存储层。而数据工程里,体系会更完整:维度表、明细表、中间表、汇总表、应用表,哪张表该在哪一层,每一层有哪些产出,都有相对严格的定义。把“层”作为第一公民之后,我们交付的每一块代码或数据,都必须知道自己是属于哪一层的,并且在依赖方向上,只能从顶向下单向走。
很多初学者容易陷入一个误区,觉得分层不是“架构”而是“文件夹分类”。我在不少仓库里看到过按层建的目录,打开以后发现,从接入接口直接拼 SQL,甚至接入层里塞定时任务,目录是分层了,代码依然一锅炖。所以 Strata 不光是“看起来分成了几块”,更重要的是它包含约束:谁可以依赖谁,谁不能碰谁,每一层的输入和输出长什么样子。
1.2 分层的真实收益不是整洁,而是“可替换”
如果你只是为了美观去分层,那确实容易被人说是过度设计。我见过太多团队花一周时间把代码目录整理得井井有条,结果下一个迭代需求一来,又顺手把新逻辑塞进了最方便的位置。
Strata 真正值钱的地方,是它给了你“可替换”的能力。比如数据仓库里,上游业务库的表结构突然要改,传统做法是通知所有下游一起改脚本,谁晚一步谁出事。但如果你在中间加了一层公共明细层,把上游字段的差异全部在明细层做归一化处理,那么上游再怎么变,下游看到的接口始终是一致的。这种情况下,替换底层实现时,上层几乎无感。
应用到服务端也一样。你写了一个订单服务,开放给内部多个系统调用;只要接入层和数据访问层之间有一套稳定的接口定义,底层从关系型数据库迁到某个新型存储引擎,调用方完全不用跟着改。这种“可替换性”才是分层真正的护城河,它让系统内部可以持续演进,而不会因为一次重构就牵一发动全身。
1.3 层与层之间,靠什么来画边界
我早期画分层的时候,总是按“代码位置”来画,比如 controller 算一层、service 算一层、dao 算一层。后来发现这种分法其实很脆。因为某个业务规则,你可能从 controller 一路穿透到 dao,中间每层都只是“透传”,没有产生任何新价值,那分层就退化成了一次函数调用链。
真正务实的边界,应该按“职责变化频率”和“依赖方向”来画。变化最频繁的通常是靠近用户的表达层,最稳定的通常是底层的存储和基础设施。靠近上层的东西可以经常变,靠近下层的东西要尽量少变。层与层之间约定好接口,也就是这一层接收什么、吐出什么、错误怎么表达,然后内部怎么实现就是这层的自由。
一句话概括:Strata 是给系统内部立规矩,让每一层有自己的生命周期,而不是把所有代码按照三层目录一放了之。
2. 为什么 Strata 能封神:它帮我解决掉的三个老大难问题
2.1 一锅粥问题:所有逻辑混在一起,谁都怕动
很多项目一开始很小,小到你觉得分层是在浪费时间。订单数据直接从接口查库,算完金额直接返回给前端,状态流转逻辑也散落在各个接口里。前三个月确实很爽,后面就不对劲了。
一旦需求多起来,同一个订单字段会在十几个接口里各自算一遍。最典型的就是订单状态。A 接口里判断“已完成”是状态值为某几个枚举,B 接口里判断“已完成”连付款时间也要看,C 接口又只根据物流状态推。三个人写出三种口径,测试各测各的,产品一脸懵。
我后来用 Strata 思路去治理:先把订单从各业务入口接进来,统一落到一个订单明细层,字段含义在这一层定义清楚。之后不管是列表页、详情页还是对账报表,都不允许直接查原始业务表,只能查明细层和汇总层。收敛之后,口径终于只有一份,改逻辑只需要改明细层,下游全部自动生效。这个收益是立竿见影的,它比任何代码规范都更能服人。
2.2 爆炸半径问题:改一个字段,线上炸一家
没有分层的时候,上下游是紧紧耦合在一起的。底层表加一个字段,听起来是小事,实际上一堆下游脚本可能因为 insert 语句没列字段名而报了错;底层表删一个字段,更是灾难,哪个任务挂在依赖上,哪个报表就崩。
Strata 把爆炸半径明显缩小了。底层原始数据只管接入和简单清洗,不承诺对下游的稳定接口;真正对下游承诺的是上面的公共层。底层结构怎么改,我只要在公共层里做适配,下游拿到的还是那张熟悉的宽表。这样线上问题的影响面,从“影响几十个下游”缩小到“影响公共层这一个点”。遇到过线上事故的团队,一定会明白这个价值有多大。
2.3 什么时候查哪一层:数据需求被逼着说清楚
我还发现一个隐藏收益,就是分层会让需求方被迫想清楚自己的场景。以前同事过来要数据,直接说“把订单表给我”。其实他到底要明细、要汇总、还是要某个特定报表,自己也没想清楚。分层之后,大家有了共同语言:你要跑深度分析,给你明细层;你要看趋势看大盘,给你汇总层;你要直接接业务后台,给你应用层。
对数据开发来说,这也减少了大量“先拉出来看看,最后发现用错表”的返工。层就像书架上的分类标签,不仅是存储的容器,更是沟通的词汇表。
3. 数据工程中的 Strata 落地:一套可以直接复用的数仓分层设计
3.1 五层骨架:ODS、DWD、DWS、ADS、DIM
大部分数据团队到最后都会收敛到一套类似的分层模板,我用下来觉得最实用的是这五层。
- ODS 层:操作数据存储层,也叫接入层。它把各个业务库、日志、第三方接口的数据原样同步过来,做最小限度的格式统一。这一层不承诺数据质量,只负责“先拿过来再说”。
- DWD 层:明细数据层,也叫公共层。这是整个数仓最核心的一层。它做清洗、去重、维度退化、字段归一化,把 ODS 层的杂数据加工成干净、统一、好用的明细事实表和维度表。业务口径基本在这一层定义。
- DWS 层:汇总数据层,也叫服务层。它按照业务主题做轻度汇总,比如按日、按用户、按商品维度聚合订单指标。这一层主要服务于日常运营看板和分析人员。
- ADS 层:应用数据层,也叫报表层。它面向具体应用,比如后台报表、大屏、算法特征。它允许按应用定制,哪怕是同一个指标,不同应用可以有不同的加工粒度。
- DIM 层:公共维度层。它比较特殊,是配套 DWD 层的维表集合,比如用户维表、商品维表、日期维表,理论上谁都可以复用。
3.2 每一层的关键产出和验证方式
ODS 层的产出最粗暴直接:多张与业务源表结构对齐的镜像表。验证方式也比较简单,就是比对行数、主键是否唯一、时间戳是否最新。这层最忌讳的是在这里做复杂清洗或口径统一,因为一旦做了,ODS 就不再是“原样接入”,后续出问题你会分不清是源端问题还是加工问题。
DWD 层产出的是标准化的明细事实表和维表。这层要重点做三件事:统一命名、统一枚举、统一单位。比如金额到底是“分”还是“元”,必须在这一层定死;状态字段的枚举值,必须在这一层翻译成业务语言。这一步做完,下游基本可以停止争吵。
DWS 层的产出是各种“按 XX 维度汇总的 XX 表”,它偶读较高,查询也快。它需要重点验证的是汇总口径和指标定义。例如“成交金额”到底包含不包含退款订单、含不含未支付订单;这些口径如果不在这里对齐,就会在报表端重复打架。
ADS 层比较自由,可以按实际需求建设。这一层允许出现冗余,甚至允许直接用宽表,不用太担心规范化。开发时先和需求方确认“这个报表的刷新频率”“数据回溯窗口”“需要哪些筛选条件”,再决定用 DWS 汇总还是重新加工。
DIM 层要维护的是维度数据的版本和有效期。至少要有“拉链表”或者“快照表”的概念,否则历史数据回溯时,你会拿今天的城市归属去解释三个月前的订单,结论必然失真。
3.3 命名规范和字段设计,藏的全是细节
Strata 能不能长期执行下去,命名规范和字段设计占一半。我的习惯是每张表都用前缀标明层次,ODS 层的表叫 ods_业务域_业务表名,DWD 层叫 dwd_主题域_描述,DWS 层叫 dws_主题域_粒度_周期,ADS 层叫 ads_应用名_业务描述,DIM 层叫 dim_维度描述。这样任何人一看到表名就能判断它的位置和来源。
字段设计上,我强烈建议每张公共层表必须带上几个“审计字段”:数据写入时间、数据更新时间、业务日期、来源系统标识。特别是来源系统标识,多系统集成时没有它,出了问题你都不知道该找哪个团队确认。很多人一开始嫌字段冗余,等线上数据对不上账的时候,才会发现这几个字段给了你唯一的排查抓手。
分区策略也需要提前定。大多数按天分区就够了,少数按小时实时场景再加小时分区。不要盲目做小粒度分区,会带来大量小文件问题;也不要不分区,否则全表扫描会伴随系统越来越卡。
3.4 血缘追踪:Strata 顺手送的礼物
分层之后,数据血缘会变得异常清晰。从 ADS 往回追,你看到的是应用层到汇总层、汇总层到明细层、明细层到接入层。每一层都有据可查。有人问我做数据治理到底从哪入手,我通常说先把分层做清楚,血缘自动就有了。
没有分层的时候,数据之间的依赖像一团乱麻,知道结论出了问题,却不知道是哪一环造成的。分层之后,你可以在每一层设置校验规则,比如 DWD 层跑完先做断言,行数在合理范围才允许下游调度。这样即使最终数据异常,你也能通过血缘定位到具体是哪一层出错,而不是全线排查。
4. 实操记录:把一个订单模型从零改成 Strata 化的全过程
4.1 改造前的原始状态
模拟项目 X 是一个零售报表系统。前期开发很快,直接读写业务库的订单表、用户表、商品表,业务方要什么,开发就在一份超级查询里加条件。表面上看项目能跑,但问题积累很快:
- 同一个订单金额,销售看板和一个数据大屏算出来的结果不一样;
- 业务库做了分库分表,报表直连导致查询经常超时;
- 产品经理想要一份含退款、含取消状态的统计,开发只能现写一个临时脚本,用完即弃;
- 上游改了字段类型,报表端无感知,第二天大屏挂了。
这次改造的目标很明确:把“报表直连业务库”的模式,逐步切换成“数仓五层”模式。
4.2 第一刀:先做 ODS 接入层
第一步,我把业务库的订单表、用户表、商品表、售后表全部通过同步任务接入到 ODS 层。同步只做两件事:字段名统一成小写加下划线,以及增加一个数据写入时间和一个源系统标识。除此之外,一律不做处理。
这里要注意一个关键操作:ODS 表保留源端的物理主键,但不加数仓自己的自增主键。因为你要和源端做行数校验,如果有自增主键,源端删掉的数据重新插入时会产生新的自增编号,你很难判断这条记录到底是新增还是变更。
这一刀切下去,报表查询依然直连业务库,但至少有了一个“缓冲垫”。后续任何加工,从 ODS 读数据就行,不需要再挤到业务库上。
4.3 第二刀:沉淀 DWD 明细层
第二步,在 ODS 之上建设 DWD 层。这是整个改造中最耗功夫、回报也最大的一步。
我开始设计一张订单明细宽表,把所有订单基础字段、用户基础字段、商品基础字段都拉平到一张表里。宽表听起来冗余,但换来的好处是:下游查询少了几次 join,字段命名口径完全统一。订单金额字段,我只保留一个经过清洗的金额,币种统一,单位统一为元;订单状态字段,我把源端上百个状态枚举映射成业务认可的几大类:待支付、已支付、已发货、已完成、已关闭、已退款。
这一步还有一个很容易被忽略的动作:处理时区。源库存的是本地时间,但后面要做跨区域数据分析,我在 DWD 层统一转成标准时区,再用“业务日期”字段冗余一个当地时间,避免以后分析时还要到处转换。
DWD 层建好后,我要求所有下游查询禁止直连 ODS,全部改走 DWD。这个约束一开始会遇到阻力,因为大家已经习惯了直接查源表。我的做法是周会同步一次“违规查询清单”,谁在 ODS 上做聚合就看出来,然后一个个迁移。坚持两周后,直连源表的现象基本消失。
4.4 第三刀:做 DWS 汇总层和 ADS 应用层
第三步,建设汇总层。我先从核心指标开始,做了一张按“日 + 用户 + 商品”粒度的订单汇总表,里面放了订单数、成交金额、退款金额、成交件数等常用指标。这张表成了销售看板和大屏的共同数据来源。
之前在报表端出现的“销售额不一致”问题,根源是不同人写的 SQL 对状态的过滤条件不一样。有人在汇总时不排除已关闭订单,有人排除退款订单。DWS 层定死规则以后,所有下游拿到的结果天然一致。这也是 Strata 最有说服力的一点:口径不是靠嘴对,而是靠位置保证。
接着做 ADS 层。每个应用单独建一张结果表,比如大屏用一张每小时刷新的主题宽表,后台报表用一张每日全量指标表。ADS 层允许个性化,它是对 DWS 的二次加工,不允许直接跨过 DWS 去查明细。
最后还有 DIM 层。我抽了用户维表和商品维表,用拉链表结构保存历史变更。这样需要回溯历史订单的时候,能知道当时订单关联的用户、商品到底是什么状态,而不是用最新信息硬套历史数据。
4.5 改造前后对比
| 维度 | 改造前 | 改造后 |
|---|---|---|
| 查询性能 | 直连业务库,高峰期经常超时 | 读数仓层,查询稳定,可预测 |
| 口径一致性 | 各处各自算,结果经常对不上 | 在 DWD/DWS 统一定义,下游一致 |
| 上游变更影响 | 改字段就是事故 | 在 DWD 层适配,下游无感 |
| 排查问题 | 层层排查,耗时很长 | 通过血缘定位,快速找到错层 |
| 新需求响应 | 现场写临时脚本 | 查已有 DWD/DWS,缺什么才补什么 |
4.6 实时任务怎么融入 Strata
可能你会问,实时计算要不要也搞分层?我的经验是同步做,但比离线轻。
流式数据接入之后,先做一层流式明细,再做一层流式汇总,最后统一写到应用层。实时明细层的粒度可以和离线 DWD 对齐,这样做实时与离线对比时,两边对得上。千万不要让实时任务直接写业务库或者直接拼大屏,否则实时链路会成为整个系统里最不可控的角落。
我见过的典型反例是:实时任务为了满足临时的分析需求,在状态里存了一堆中间聚合值,越写越复杂,最后改一个指标要改整个作业。如果早期就按 Strata 思想把任务拆成接入、清洗、汇总、输出四段,每个算子只做一件事,改造代价会小得多。
5. 不止数仓,应用架构里的 Strata 也一样好用
5.1 应用代码的经典三段式
数据工程里的分层思路,完全可以平移到应用工程。我常用的应用层分法是:接入层、领域层、基础设施层。
接入层负责接收外部请求、参数校验、协议转换,不处理业务规则。领域层负责核心业务逻辑,比如状态机流转、金额计算、权限判断。基础设施层负责和关系型数据库、消息中间件、缓存、第三方服务打交道。
以前我写业务代码,喜欢把数据库操作直接写在接入层里,接口一多,每个接口都要关心如何拼 SQL。后来改成领域层统一管业务逻辑,基础设施层统一管数据访问,接入层变得很薄,改动也少了很多。业务规则要变,只改领域层;存储要换,只改基础设施层。分工明确后,团队协作不再互相踩脚。
5.2 循环依赖怎么破
分层之后最容易出的问题,就是“底层想调上层”。比如领域层发现需要获取当前登录用户的信息,直接调了接入层的一个方法,这就构成了反向依赖。乍一看没毛病,实际上会让层间的边界失效。
解决思路是依赖倒置:在领域层定义一个接口,由接入层或者外层去实现,然后通过依赖注入的方式传给领域层。这样领域层不再依赖某个具体上层实现,而是依赖一个抽象接口。依赖方向依然是“接入层 → 领域层”,只是具体实现被颠倒过来。这是 Strata 架构里我认为最关键的一个技巧。
5.3 小项目该不该分层
很多朋友会问,我一个人写后端,也搞这么多层,是不是太啰嗦。我的建议是:小项目不需要建粗粒度目录,但“分层意识”必须有。你至少可以先做两层:接入处理和核心业务分开,数据访问单独抽一层。等代码量上来、协作的人变多了,再逐步细分成三层以满足增长。
不要因为项目简单就完全不考虑分层。最怕的是项目一开始没分,等到业务复杂时再重构,成本高到团队不愿意动。轻量分层的关键是“接口先行”:哪怕你只写两个模块,先用清晰的方法签名把边界定下来,后面逐步填充实现。
5.4 用契约测试保住 Strata 边界
层与层之间的接口,本质上是一种契约。我强烈建议给关键接口加契约测试,特别是下游依赖多的接口。比如 DWD 层对外提供订单宽表,那至少要测试几项:字段不会丢、金额不为负、状态枚举都被翻译过。这样上游改逻辑时,只要跑一遍契约测试,就能立刻发现哪类接口语义变了。
契约测试不需要很复杂,也不需要覆盖全部字段,只要覆盖核心字段和数据格式就是增量收益。它能把“层间约定”从口头承诺变成自动化守护,防止某次临时需求把边界悄悄冲破。
6. 我踩过的坑与排查技巧实录
6.1 症状一:链路越来越长,一张报表要跑十层任务
分层做过头以后,最大的问题不是效率低,而是任务链路膨胀。本来一条数据从流入到出结果只需要三步,结果为了“严格分层”,中间加了很多个几乎没有加工的中间表。每一层都只是透传,还多跑一次调度,最终报表延迟从 1 小时变成 3 小时。
排查手段是看每个任务的产出字段变化率。如果一个任务读取了十列,输出还是那十列,只是少了两行,这个任务很可能是冗余的。我的原则是:每一层必须提供“增量价值”,可以是清洗、关联、聚合、命名统一,或者建好稳定的公共模型。如果只是复制粘贴,就合并掉或者直接删掉。
6.2 症状二:层与层互相调用,循环依赖出现
更隐蔽的是循环依赖。在数据任务里,DWD 层依赖 DWS 层算出来的某个维度值,同时 DWS 层又依赖 DWD 层,调度系统直接死锁。这种情况经常发生在“为了复用指标而提前下沉汇总需求”的时候。
正确做法是让底层尽量原子化,不要把上层指标下沉到下层。比如某指标只有在汇总层才有意义,那就不要让明细层去引用它。如果你发现必须引用,大概率是分层边界划分错了,先重新审视任务归属,而不是硬解环。
6.3 症状三:同一张业务表,ODS、DWD、DWS 里各建一份,字段还都对不齐
这个坑特别常见。业务表一多,不同时间不同人建设,很容易在每层都建了相似的物理表,但字段名、粒度、过滤条件各不相同。最后做报表时,到底用哪张表,只能靠猜。
我的解决方法是建立一个“表地图”文档,或者直接在元数据系统里登记。每张表必须有 owner、业务口径、清洗规则、目标层,没有登记的表约定不许被引用。很多团队觉得这是形式主义,实际上它救了无数次急。没有表地图,分层越大越混乱。
6.4 症状四:下游绕过汇总层直连明细层,查询把集群打崩
还有一种更现实的坑:应用方觉得自己的查询比较复杂,汇总层满足不了,或者根本不知道汇总层的信息,就直接绕过 DWS,甚至绕过 DWD,去查询 ODS 原始明细。结果某个大范围扫描几乎把集群资源打没。
我的经验是三条:一是在调度系统或者平台上把层访问权限控制起来,ODS 只允许 DWD 任务访问,DWD 只允许 DWS 或特定应用访问;二是定期扫任务的血缘和读表记录,发现跨层读就立刻和负责人对齐;三是把常用 DWS 表主动推给业务方,让它们有明确的查询入口。技术和管理结合起来,跨层查询才能有效根治。
6.5 分层健康度自检清单
| 检查项 | 通过标准 |
|---|---|
| 依赖方向 | 上层可依赖下层,下层禁止反向依赖 |
| 层间透传任务 | 每层加工至少变换字段、粒度或口径中的一种 |
| 口径一致性 | 每个核心指标在公共层只定义一次 |
| ODS 纯净性 | ODS 只做接入和格式统一,不做复杂清洗 |
| 应用隔离 | ADS 只依赖 DWS/DWD,不直连 ODS |
| 命名可读 | 表名前缀一致,看到名字知道层归属 |
| 数据血缘 | 任意任务可追溯上游输入与下游输出 |
| 修改影响面 | 修改底层时,受影响对象只限于公共层 |
6.6 别搞推倒重来,用“对话式重构”慢慢切
最后说一个大家都会犯的错:一上来就把旧系统推倒重写,中间状态维持很久,业务方怨声载道。我在改造模拟项目 X 时用的方法是“并行切换”,旧查询先继续用,新分层表建好后做数据对账,对账通过后再逐步让下游切换。每个报表切换完,旧逻辑才下线。整个过程没有出现过一次“查不到数”的窗口。
同时,我建议先挑三张最重要的表来做示范,打造出标杆。团队看到新的 DWD 宽表查起来更快、口径更准,自然会愿意迁移。如果要一次把所有报表都搬进新分层,很可能因改动面太大而中途放弃。
再分享一个小技巧:改造时在每个临时表中都加上“迁移前后标记”,比如新增一个来源字段记录是来自旧链路还是新链路。这样测试阶段发现问题,你能立刻知道应该去查哪条链路,避免两边数据对不上时无从下手。
这个内容后续还可以这样扩展:把分层的理念进一步延伸到组织协作上,比如不同小组负责不同层,每层都有明确的服务协议和负责人。我在实际项目中感受到,Strata 之所以能封神,不只是因为它让系统更稳定,还因为它让团队沟通有了共同坐标系。以后你再看到一个数据问题,不妨先问一句:它是哪一层的责任?边界清晰了,答案往往就清晰了。