报表连续三天对不上,前两天以为是统计口径改动,第三天顺着指标往上翻,才发现问题源头是某张基础表的某个字段从某一天开始出现空值,而这张表的下游挂着十几个模型。团队里其实有校验,规则写得也不算少,问题在于这些规则全部跑在数据已经落进湖里、已经被下游读过之后。规则本身没有写错,错的是它所在的位置。
这类事情之所以反复发生,是因为数据质量长期被当成一件整体工作来谈:要么说要加强校验,要么说要建立治理体系,很少有人把它拆成这条规则应该在链路的哪一段执行这样一个具体的架构问题。而恰恰是这个位置,决定了同样一处脏数据是被拦在一行日志里,还是要等三天之后被业务方发现。
下面把这件事拆成三个部分来讲:规则有哪些可检查的维度,这些维度分别适合放在哪一层执行,以及规则定下来之后还要补上哪些东西,它才不会退化成一批没人看的告警。
数据质量本身不是一个可以被检查的对象,能被检查的是规则。一条规则要能被程序判定,至少要说清楚三件事:查的是哪个字段或者哪个集合、判定真假的依据是什么、判定为假之后做什么。缺了第三条的规则在实际运行里基本等于没写,因为它最多产生一条没人认领的日志。
通常把规则分成六个维度。第一个是完整性,指必填字段是否有值、记录是否有缺失、分区是否到齐。它是最容易写也最容易被忽略的一类,因为空值在很多存储引擎里是合法的,不会自己报错,只有等到下游聚合结果发生偏移时才会被察觉。
第二个是唯一性,指主键或业务键是否出现重复。重复记录的来源通常不是同一个缺陷,而是好几种情况叠加:上游重放、双写、回补时没有先删后插,以及业务上本来就应该允许重复却被误判成唯一键。写这条规则之前必须先分清约束的是技术主键还是业务键,两者重复之后的处置方式完全不同。
第三个是有效性,覆盖值域、枚举、格式、类型、长度与单位。金额不能为负、状态码必须落在枚举表内、日期不能是未来时间、数值字段不能出现超出业务量程的极大值,都属于这一类。它的特点是单条记录就能判定,不需要看到其他记录,这一点决定了它可以被放在很靠前的位置。
第四个是一致性,指同一实体在不同表、不同系统之间的表达是否对得上,也包含父子记录之间的汇总关系是否成立。这类规则必须同时拿到多张表才能判,只看一条记录永远看不出问题,因此它对执行位置有硬性要求。
第五个是及时性,指数据是否在预期时间内到达、分区是否按时产出、延迟是否落在可接受范围。它的判定需要一条时间基线,而基线本身是一个会随业务节奏变化的配置项,不是写完就一劳永逸的东西。
第六个是准确性,指数据与权威源比对之后的偏差,比如财务系统的金额与业务系统的金额是否一致、库存快照与出入库流水能否对上。它需要一份被公认的参照,没有参照就无从谈起准确性,这也是这一类规则最难落地的原因。
这六类规则的判定代价差别很大:完整性和有效性只看单条记录,唯一性要看到完整集合,一致性要看到多张表,及时性要有一条时间基线,准确性要有一份外部参照。判定代价直接决定了它们能放在链路的哪一段,因为越靠前的环节能看到的数据越少,能够承担的规则种类也就越少。
修复一处脏数据的成本至少由三笔账组成:定位成本、重跑成本、以及纠错沟通成本。定位成本是查清这批脏数据出自哪一段链路所花的时间,重跑成本是把受影响的数据重新算一遍所消耗的计算与调度资源,纠错沟通成本是通知所有已经用错数据的下游并更正结论所花的精力。
在这三笔账里,前两笔都随发现时点往后移而增加,第三笔增加得最快。数据刚进湖时只有一条链路读过它,重跑只需要处理这一条;三天之后它已经进了十几个模型和若干张对外报表,重跑范围要顺着依赖树一路往下走,而且还要逐个通知曾经引用过这些数字的人,更正结论的解释成本往往比修数据本身高得多。
既然如此,把全部规则都堆到最上游是不是就对了?并不是。往上游放有两个代价。第一个代价是耦合:规则写在采集接入层,意味着它嵌进了上游业务的生产链路,规则的任何一次改动都要动这条链路,需要上游配合发版,协调成本远高于在下游加一条断言。第二个代价是覆盖有限,越靠前的环节看到的数据越少,跨表一致性与跨系统对账这类规则在接入层根本判不了,硬要判就得让接入层去查别的表,这会把一个轻量的校验逻辑变成一次分布式查询,得不偿失。
于是形成了一条比较实用的判断线:规则越稳定、越接近物理结构,越适合往上游放;规则越依赖业务上下文、越容易随业务变化,越适合往下游放。字段非空和类型这类规则可能几年都不会变一次,放在接入层很划算;而这个月的促销口径下这两张表应该对得上这类规则可能每个季度都要改一次,放在上游会不断制造协调成本,还会让上游团队对接入层的稳定性产生怀疑。
还有一条容易被忽略的约束:规则放得越靠前,它对写入性能的影响越直接。每一条接入层规则都要在每条记录上跑一遍,规则集如果无限膨胀,写入延迟会被拖长,而写入延迟上升又会影响上游业务的响应时间。所以即便是适合放在上游的规则,也需要按规则集分层管理,把高频核心规则和低频补充规则分开,而不是把所有规则无差别地挂在同一条链路上。
反过来看,越靠下游的规则虽然修复成本高,但它的改动成本极低,覆盖面也最广。同一个一致性问题可能同时存在于五条接入链路,在下游写一条断言就能全部覆盖,在五条链路上各写一遍则既重复又容易遗漏。这笔账应该在决定规则位置时一起算进去,而不是只看修复成本这一项。
入湖前指的是采集与接入这一段,数据在写入存储之前经过的那一层。这一层最适合结构性强、可枚举的规则:字段非空、类型正确、枚举值在范围内、长度不超限、格式匹配。这些规则的共同点是只需要看当前这一条记录,判定逻辑轻量,可以随写入流程同步执行,不需要额外的调度依赖。
这一层真正需要设计好的其实不是规则,而是失败动作。规则命中之后共有三种选择,每一种都有明确的适用条件,选错所带来的后果往往比规则本身写错更严重。
第一种是拒绝,直接不让这条数据写入。它最干净,脏数据根本进不了湖,但代价是阻塞上游写入链路:上游如果是一次性批次推送,被拒绝的这部分数据如果没有落盘留证,就等于丢数据;上游如果是一条同步业务链路,拒绝会让业务侧直接感知到失败。所以拒绝只适合上游能够重发、且被拒数据有留存的场景。
第二种是隔离,把命中规则的数据写入隔离区或者隔离表,主链路继续。它不阻塞上游,也保留了原始数据用于事后分析,代价是需要有人真的去看隔离区。很多团队建了隔离区却没有人看,隔离区就变成一个缓慢增长的堆积区,而且因为没人看,里面堆着的问题永远不会被修,直到某次磁盘告警才被想起。
第三种是放行并打标,数据照常写入,只在记录上打一个质量标记,由下游自己决定读不读这个标记。它对链路零影响,但成立的前提是下游确实会消费这个标记。如果下游的查询逻辑从头到尾没有读过标记字段,那么打标和没打标没有任何区别,只是多了一个字段而已。
选哪种失败动作,取决于两个问题:这条数据能不能重新采集,以及上游能不能重新发送。两者都能,可以考虑拒绝;能重新采集但上游不好重发,适合隔离;两者都不能,比如来自外部一次性投递的数据,就只能用放行并打标,把处置权交给下游,同时把这件事明确写进这个字段的数据字典里。
这一层还有一个硬性边界:它看不到全集。唯一性要在一个集合内部比对才能发现重复,跨表一致性要同时看到两张表,这两类规则在接入层判不了。想在这一层做唯一性判断,只能依赖外部存储记录已经出现过的键,本质上已经不是接入层自己的事了,而且会给每一条写入引入一次外部读写,代价相当高。
入湖后指的是数据已经写进存储、由批处理任务与调度系统接管的这一段。这一层的校验通常以任务级断言或分区级断言的形式存在:一个处理任务跑完之后,对它的输出做一次检查,通过才允许下游任务启动。
它能做的事情比接入层多得多,因为此时数据已经是一个完整集合。跨表一致性、主键与业务键重复、行数波动、分区完整性、父子记录的汇总关系是否成立,这些都只能在这里判。特别是行数波动,它需要一个历史基线做比较,而基线的计算本身就要跑在完整数据上。
分区完整性是这一层很典型的一类规则:预期应该有分区的时间段是否真的产生了分区、分区是否为空、是否存在重复分区、分区的水位时间是否落在预期窗口内。这类问题的表现往往不是报错,而是某张表静默地少了一天的数据,下游的环比指标因此整体偏移,而所有聚合计算都不会报错,看起来一切正常。
这一层的优点是不阻塞写入。规则可以按需增加,不影响上游业务链路的响应时间,改动也不需要上游配合发版,是三层里改动成本最低的一层。缺点是发现即已扩散:等断言跑完发现问题时,数据已经在存储里,可能被下游任务读过,也可能已经被某个对外接口取走,此时修复动作里必然包含一次向下游的解释。
因此这一层的关键设计是分级。断言失败之后是阻断下游调度还是只发告警继续跑,要由规则的严重级别决定,而不是统一处理。一律阻断会把一个数据质量问题变成一次生产停摆:某个非核心字段的枚举值异常,却让整条核心链路停摆半天,这是很常见的过度处置。一律告警则会让真正严重的问题被淹没在告警流里,等到有人注意到时已经扩散得更远。
分级的依据应当是下游受影响的范围和结果的可逆性。如果脏数据继续往下跑会产生不可逆的结果,比如对外的结算数据或者已经发出的对账文件,那么值得阻断;如果问题只是影响一张内部看板的次要维度,那么告警就够了,边修边跑更合理。同样重要的是,级别不是定完就不变的,业务重要性变化之后要回头复核一遍。
这一层还要处理断言自身的可观测性。断言跑了多少条、跳过多少条、失败多少条、失败中有多少是首次出现,这些数字需要单独统计。一个长期通过率百分之百的断言,很可能是因为它依赖的上游字段早就改名了,断言实际上一直在空跑。
消费时指的是查询与服务这一层,数据被取出来用于报表、接口或者对外交付的那一刻。这一层适合放对账类规则和业务语义校验:跨系统的金额是否一致、某个组合在业务上是否应该出现、对外发布的数字与源系统能否对得上。
它的代价是每次查询都要付一遍。同样一条一致性规则,放在批处理层一天跑一次,放在查询层则是每个请求跑一次,两者的计算开销完全不是一个量级。所以这一层的规则数量必须严格控制,只保留那些真正必须在出口处确认的校验,其余的都应该往上挪到批处理层。
放在这里的价值不在于修数据,而在于防止错误结论被发出去。数据已经错了很多天的情况下,修复需要时间,但在修复完成之前,出口处的一道断言可以阻止错误的数字继续流向业务方和外部。这是一道止损线,不是修复手段,把它当成修复手段会导致数据问题被长期掩盖。
实现方式上有一点需要特别注意:把校验做成断言,而不是静默修正。发现对不上时,正确的做法是把结果标记成未通过校验、或者拒绝返回,而不是在查询层悄悄改一个数让它看起来合理。静默修正会掩盖问题,让上游永远不知道自己的数据是错的,而且修正逻辑本身会逐渐成为一个新的、没人维护的真相来源,最后没人说得清哪个数字是对的。
这一层也是唯一能看见业务语义的一层。口径怎么定义、哪个组合算异常、什么样的数字业务方会认为不可信,这些判断只有靠近消费侧才能做出。前面两层能做的是结构上的正确性,业务上的合理性判断只有在这里才成立,所以即便成本高,这一层也不应该被完全省略。
执行这类校验的服务本身要占用计算与存储资源,而且往往和查询服务共享同一批机器,校验逻辑的重量会直接影响查询响应时间,因此在容量规划时要把它单独算进去。一万网络深耕 IDC 行业 19 年(成立于 2007 年),在华南、华东、华北及中国香港、海外多地整合自营节点与 BGP 多线资源;数据平台这类常驻组件该怎么配机型、存储与带宽,建议按自有数据量级向服务商核实。数据平台的机型、存储与带宽配置以官网实时信息或咨询确认为准。
规则定下来只是开始。规则数量超过几十条之后,如果没有配套的管理手段,它们会迅速退化成一批无法维护的脚本:不知道哪条还在生效,不知道哪条命中之后该找谁,也不知道某个阈值当初为什么定成这个值。
第一个要素是唯一编号。编号的作用是在告警里能立刻定位到具体规则。一条告警如果只说某张表校验失败,收到的人还要去翻代码找是哪条规则;如果告警里直接带上规则编号,就能立刻查到它的定义、责任人和历史记录。编号同时是统计的基础:哪条规则命中最多、哪条从来没命中过,都要靠编号来聚合。
第二个要素是所属域。规则要挂在明确的业务域或数据域下,这样在做影响分析时可以按域聚合,也能够在组织调整时把一批规则整批移交,而不是逐条重新认领。
第三个要素是责任人。这是最容易被跳过、也最致命的一项。没有责任人的规则,命中之后产生的是一条无人认领的告警。责任人也不应该写成某个人名就完事,最好是一个可轮值的角色,否则人员变动之后责任就落空了,规则还在跑但已经没有人会对它负责。
第四个要素是严重级别,它直接决定失败动作是阻断、告警还是隔离。级别应当在规则创建时就定好,并且随业务重要性的变化定期复核,而不是等到某次事故之后才被动调整。
第五个要素是阈值与阈值来源。行数波动超过多少算异常、延迟超过多久算不及时,这些数字必须有明确依据:是从历史分布算出来的,还是业务方约定的服务水平,还是某个对外部的承诺。记录阈值来源的意义在于,当业务规模增长之后,旧阈值会自然失效,知道来源才能判断该不该重新计算,而不是凭感觉改一个数。
第六个要素是历史变更记录。谁在什么时候把阈值从多少改成了多少、为什么改,这些记录决定了半年之后还能不能解释当前系统的行为,也决定了能不能把一次事故和某次阈值调整对应起来。
还需要提前考虑规则的腐化问题。业务变了而规则没有跟着改,会产生持续误报;误报多了,接收告警的人会开始忽略,真问题也就跟着一起被忽略了。应对方式是定期复核命中记录:长期零命中的规则,要么说明它已经失效,要么说明校验逻辑根本没跑起来;长期高命中的规则,要么是阈值不合理,要么是上游存在一个一直没修的老问题,两种情况都值得单独拿出来处理。
把前面六个维度按执行位置归并,可以得到四组,每一组对应一个主要的执行层。这个归并的意义在于给出一个起点,而不是一套硬性规定,具体落位还要结合自己链路上的实际约束调整。
第一组是结构类,包含完整性和有效性。它们的判定只需要单条记录,规则稳定、变化少,最适合放在入湖前的接入层,用最低的成本拦住最明显的脏数据。入湖后可以保留一批同样的规则做复核,用来发现接入层规则被绕过、被改坏或者因为发版遗漏而没有生效的情况,这种复核在多层架构里非常必要。
第二组是集合类,主要指唯一性。它必须在完整集合内部比对才能发现重复,因此只能放在入湖后,以分区级或任务级断言的形式执行。重复键一旦扩散到下游,去重工作要在每一张下游表上重做一遍,代价远高于在源头分区上就发现并处理。
第三组是关系类,即一致性。它需要同时看到多张表或多个系统的数据,放在入湖后,通常挂在某个任务完成之后,作为这个任务的下游依赖的前置条件。它也是三层里最容易被写成阻断的一类,因为一致性问题一旦往下扩散,下游的所有汇总都会跟着错。
第四组是时间与外部类,包含及时性和准确性。及时性依赖调度系统,天然属于入湖后的调度层,以分区是否按时产出、水位是否在窗口内这样的形式来检查;准确性需要一个权威参照,而参照往往来自另一个系统,因此更适合放在消费时的对账环节,或者放在一个专门的对账任务里按天执行,而不是按查询执行。
同一条规则在不同层各放一份是常见做法,但每放一份都要明确它的失败动作:接入层可以是放行并打标,入湖后可以是阻断下游,消费时可以是拒绝返回。三层都放了同一条规则,而三层的失败动作全都只是告警,那么这三层里其实没有一层真正拦住了问题,只是把同一个问题记录了三遍。
| 规则类型 | 最适合的执行层 | 延迟发现会造成什么 | 需要配套的元数据 |
|---|---|---|---|
| 完整性 | 入湖前为主,入湖后分区级复核 | 空值静默流入聚合结果,环比与占比整体偏移且不报错 | 字段必填定义、分区预期清单、允许为空的业务例外说明 |
| 有效性 | 入湖前,与完整性同一批规则集 | 越界与非法枚举值进入模型特征,异常被当作正常样本参与计算 | 枚举表与值域定义、字段类型与单位、格式规则的版本号 |
| 唯一性 | 入湖后,分区级或任务级断言 | 重复记录随下游扩散,去重要在每张下游表上各做一遍 | 主键与业务键的区分、分区边界定义、允许重复的业务场景清单 |
| 一致性 | 入湖后,挂在任务完成后作为下游前置条件 | 跨系统同一实体口径分裂,不同报表给出互相矛盾的结论 | 实体映射关系、参照完整性约束、两张表的口径对齐说明 |
| 及时性 | 入湖后调度层,按分区产出时间判定 | 分区静默缺失,下游按不完整数据出报表且无人察觉 | 预期产出时间窗口、调度依赖关系、上游到达时间的历史基线 |
| 准确性 | 消费时对账环节,或独立的按天对账任务 | 与权威源的偏差被当作事实发布,更正要追溯所有引用方 | 权威源标识与取数方式、可容忍偏差范围、对账频率与责任人 |
大多数团队不缺校验,缺的是校验之后的环节。规则命中、告警发出,然后就没有然后了:没有人被明确指派,没有人知道该重跑哪些分区,也没有人确认修完之后规则不再命中。校验产生的价值在这一环被全部损耗掉,最后形成有规则、有告警、但数据依旧出错的局面。
缺的第一段是告警路由。规则到责任人到通知通道,这条链路必须提前配置好,而不是等告警堆积之后再补。路由还要区分时间段:工作时间与夜间走不同通道,非核心域的告警不应该在深夜把人叫起来,否则值班的人会很快把所有告警通道设成免打扰。
缺的第二段是修复动作预案。每一类规则都该提前写好命中之后怎么办:主键重复怎么去重、分区缺失怎么补采、枚举值异常是改映射表还是改上游生成逻辑。没有预案的结果是每次命中都要临时讨论,而临时讨论的结论没有被记录下来,下次命中同样的问题又要重新讨论一遍。
缺的第三段是重跑范围的界定。修好源数据之后,从哪个分区开始重跑、下游哪些任务要跟着重跑、重跑期间对外提供的数据怎么处理,这些都要有明确规则。重跑范围定窄了,问题会残留一部分;定宽了,一次修复要消耗大量计算资源并且拖长恢复时间,还可能在重跑过程中影响正在运行的其他任务。
缺的第四段是回填与更正。历史数据被修好之后,基于它产生的历史报表要不要更正、已经发出去的数字要不要通知更正、更正时以什么口径对外解释,这些在多数团队里完全没有流程。结果是数据修好了,但各处的结论还停留在错误的版本上,过一段时间又会被翻出来当成新问题讨论一次。
缺的最后一段是复核。修完之后规则还会不会再命中,是判断修复是否真正完成的唯一标准。没有这一步,同样的问题会在下一个采集周期原样出现,团队则会陷入每周修同一张表的循环,最后把这条规则改成告警了事。
把这些环节串起来之后,才能形成可度量的闭环:从发现到修复的时长、误报在全部命中里的占比、同一条规则重复命中的次数、长期零命中规则的数量。这几个指标比规则总数更能说明一个数据质量体系的真实状态,规则总数多只说明写过多少次,不说明拦住了多少问题。
规则是不是越多越好?不是。规则的边际收益下降得很快,而维护成本是线性上升的,每加一条规则都意味着多一条需要复核、需要认领、需要在业务变化时重新评估的条目。更合理的做法是先按业务重要性排序,只对影响核心指标和对外交付的字段写规则,其余的等指标真正出问题之后再补。
入湖前的校验会不会拖慢写入?会,但幅度取决于规则的实现方式。单条记录上的字段判断开销很小,真正拖慢写入的是那些需要查询外部状态的规则。把需要查外部的规则从接入层挪走,是优化写入延迟的第一步,比调整规则条数有效得多。
告警太多没人看怎么办?通常不是告警太多,而是告警没有分级和路由。先把规则按严重级别分档,只把最高档做成实时通知,其余的收敛成日报;再检查长期高命中的规则,它们的阈值多半有问题,或者上游存在一个一直没人修的缺陷。
阈值到底定多少合适?取决于阈值的用途。用于发现异常的规则,阈值应当来自历史数据的分布;用于守住承诺的规则,阈值应当来自业务方明确约定的服务水平。两者混用会导致阈值既不能发现异常,也无法守住承诺,最后两边都不满意。
历史脏数据要不要一次性清掉?通常不建议。一次性清洗涉及的数据量大、影响面广,很容易在清理过程中引入新的问题,而且很难回滚。更稳妥的做法是先让新规则对增量数据生效,再按时间从近到远分批回补,每批回补后核对一次确认无误再继续。
小团队该从哪里开始?从对外发布的那一两个指标开始,倒推它依赖的表和字段,只为这一条链路写规则。范围小、见效快,也更容易用实际效果说服业务方配合后续的工作,比一上来铺开做全库治理要现实得多。
第一步是圈定范围。把对外发布和核心决策依赖的指标列出来,顺着依赖往上找出它们的源表,把这些表作为第一批要保护的对象。不要一上来就做全库规则,范围一大就没有重点,没有重点就没有责任人,最后每条规则都处于半维护状态。
第二步是上单记录可判的规则。在入湖前的接入层加完整性和有效性校验,覆盖第一步圈出来的字段。这一批规则的失败动作先统一设成放行并打标,不要一上来就阻断,先观察真实数据里到底有多少命中、命中集中在哪些字段和哪些上游。
第三步是把打标的数据变得可见。建一个隔离区或者一张质量看板,让命中记录能被查到。这一步不做,第二步就白做了,因为没有任何人能看到标记,标记只是一次无意义的写入。
第四步是在入湖后加分区级断言:分区是否按时产出、是否为空、是否重复、行数相对历史基线的波动是否超出阈值、主键与业务键是否重复。这批断言先全部设为只告警不阻断,跑一段时间收集基线数据,等分布稳定之后再讨论阈值设置。
第五步是补齐规则治理信息。给每一条已经在跑的规则补上编号、所属域、责任人、严重级别、阈值来源和失败动作。这一步做完之前,规则数量不应该继续扩张,否则新增的每一条都会立刻变成需要回头补信息的欠账。
第六步是接告警路由与工单,让每条严重级别的规则命中之后都能自动落到具体的人身上,并且形成可追踪的处理记录。有了记录,才能统计从发现到修复的时长,也才能看出哪些规则长期处于命中但无人处理的状态。
第七步是升级失败动作。观察一段时期之后,把那些命中次数少、影响范围明确的规则从放行打标升级为隔离,再逐步把其中影响不可逆的规则升级为阻断。升级要一条一条来,每升一条观察一段时间,不要批量切换。
第八步是在对外出口加对账断言,只保留最少的几条,目的是防止错误数字被发出去,而不是在这一层修复数据。断言失败时的处置方式要提前和消费方约定好,是拒绝返回、返回标记值,还是给出一个明确的不可用提示。
每一步的进入条件也值得写清楚:第三步做完之前不做第四步,因为没人看的标记加了没有意义;第五步做完之前不做第六步,因为没有责任人的告警路由不出去;第七步必须在前六步稳定运行一段时间之后再启动,因为失败动作升级是会真正影响链路的一步,仓促切换很容易把质量问题变成可用性问题。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品