一直以来, Codementor / Arc 总是依赖著 data 来帮我们发现/验证各种假设。 这阵子我们使用 data 的方式渐渐有了改变。 Application 的变动、各个不同 team 流程的调整、再乘上各种人为犯错的可能, 即使 data 的量并不多,但各种变动带来不预期的错误却越来越多。 在这篇文章里面,我们想把我们切入问题的方式,评估的方式跟选择的策略跟大家分享, 希望可以对于在 data 这个领域要 “登大人” 的团队们有所帮助。

从前从前

打从我们的产品上线的第一天开始,我们总是在用各种 data 帮我们发现、验证各种事情。 这些 data 的来源,主要分成:

  • 我们各个 application DB 放的 data
  • 外部 tracking tool 收下来的 data
  • 有些流程会把 data 留在 3rd party 那边,像是sales使用的服务等

整体来说, 我们的 data 的特性是量不大,但是变化很快。 我们各种 data source 的 data 格式、逻辑每天几乎都在变化,需求也每天都在跑出来。

Stage 1

在过去很长的一段时间内,我们产出想要的数据 + 图表的流程是:

PM/Data Analyst 写 SQL query,搭配 metabase , Redash 等的工具把想看的数据呈现出来。

在这个阶段,我们其实就有预见潜在的问题是:

  • 【问题1】analytical query 是直接依赖 application db。所以当 application db schema 有任何改动(migration)的时候,analytical query 必须立刻跟上。但application这段的改动其实是很频繁的。
  • 【问题2】 application db 的 schema 是完全依照自己的需求( OLTP )而设计,这样的schema 在分析的情境下( OLAP )未必适合。

但在这个时间点,因为团队人数还很少,所有的沟通都很快速,所以这两个问题都还在掌握之内。

Stage 2 – Analyst 参战

接著,我们开始有 “对 data 细节比较不熟悉的” 伙伴加入了。同时,application data 和 3rd party 的 data 也变得比过去更复杂且多样。于是上述的【问题2】 开始变得明显。于是我们加入了 ETL,可以帮我们整理各种复杂的 query。这时候的 ETL 是以 减少分析端 query 的复杂度 为目标。

这时候的架构演进成这样:

Stage 3 – 到了最近

上述的做法一直持续到了最近,我们的团队渐渐又长大了。我们发现:

  • 【问题1】开始更明显了,application db migration 常常会有沟通漏接的状况。再加上 query 的量变多,对于维护 query 来说,要一个一个在 UI 上 update 是一件可怕的事情。
  • 【问题2】还是存在:时不时会有因为 application schema 太复杂,导致分析这端的 query 变得很复杂,甚至误解了 data 的意思。也就是说, stage 2 希望透过 ETL 解决的问题又跑出来了。
  • 【问题3】新的问题出现了:在 business logic 没有大概动的状况下,却有越来越多 query 会 同时需要 分析和 ETL 同时加入才能完成,而且似乎并没有减少的趋势。这个对我们来说是一种警讯: 如果同一件事还是总需要两边来完成,是不是本来的抽象出了什么问题呢?
  • 【问题4】新的问题:开始出现了 “分析端修改了 query,却影响到 ETL” 的状况。这个像是 circular dependency 的东西是之前没有预期到的。

在现行的流程下,上述的问题代表:我们可能会在没有人发现的状况下,根据错误的资讯做出决定。

虽然说各个发生的问题本身都是可以被修复的,但我们问自己: 有没有什么办法可以让 PM/Analyst 不用再等工程师,并且让以后类似的变动再发生的时候,相对应的数据不要坏掉呢?

于是我们觉得是时候好好从头检视这一切了。

其中【问题1】、【问题2】对应到上图 1、2 两个黄色的箭头:过去我们一直都没有认真有技术的方式去解决。而现在我们需要的应该是:

  • 【需求1】我们需要一个在分析端和application db/3rd party data 之间的中间层。 它要可以吸收各种变化,并且 expose 给分析端稳定并且直觉的介面。

而针对【问题3】、【问题4】则是对应到 3 号红色的箭头和灰色的虚线:过去我们没有一个很清楚的定义分析端和 ETL 之间的角色定位 – 谁该负责什么样的东西。

  • 【需求2】在分析端和 ETL 这边需要有一个明确的介面,定义谁该依赖谁,还有各自负责的东西是什么。

寻找解法

  • 【需求1】我们需要一个在分析端和application db/3rd party data 之间的中间层。 它要可以吸收各种变化,并且 expose 给分析端稳定并且直觉的介面。
  • 【需求2】在分析端和 ETL 这边需要有一个明确的介面,定义谁该依赖谁,还有各自负责的东西是什么。

针对【需求1】,我们有考虑过一些可能性:

1. 从 application 出发

由 Application 按照 domain model 定义/维护一层抽象。然后 ETL 根据这层抽象去把 data 拿下来。实现的方式之一会是:application 会把这组抽象做成 API 或者是 database 的view,借此把内部的实作细节包装起来。

这样的好处是因为这些抽象是做在 application 内部的,所以测试起来很快 – 在任何的 db migration 下,application 可以透过内部的 unit test 直接确定这个介面的行为没有坏掉。但坏处则是 application 会知道一大堆这样的抽象。这些抽象其实是为了分析而存在的,也就是说,当分析有新的需求的时候,会要同时改动到 application 和 ETL。

2. 由 ETL 出发

Application 这端完全不知道分析端要用的抽象,而是由 ETL 来概括承受。 也就是说,由 ETL 去依赖 application 的 schema,而在每一次 schema 改动的时候,去修改内部的实作来吸收这些改动,维持分析端的一致。这样的作法好处是当分析有新的需求,application 可以完全不用知道。但坏处则是 ETL 这边要可以找到自动测试的方法。这在各种 data source 的来源多样的状况下可能不一定是件单纯的事。

上述的”抽象”举一个例子来说明的话像是:想像我们 application 内部记录 payment 的 db schema 很复杂,因为要考虑到各种像是 isolation level 的实作细节。但在这个状况下,其实分析端在乎的”抽象”可能只是 “某一个 user 在什么时候付了多少钱”,而根据这个 “抽象”,分析端可以延伸出各种 business 上在乎的资讯:好比说 “付费跟时间的关系”,”付费跟 user 所在位置的关系” 等。

后来我们选择了第二种方法。主要原因是我们觉得当 application 变动的快的时候,让 application 维护这样的抽象沟通成本太高了。而以目前我们 data source 的复杂度,ETL 要进行自动的测试并不是太难的事情。

针对【需求2】

在想过上面的东西之后,其实【需求2】的答案就呼之欲出了。我们想要的 “ETL跟分析端之间的介面” 其实就是上面的 “抽象”。当定义了适合的抽象作为介面之后,分析端才可以跟据这些简单的抽象,去排列组合出各种 business insight。这边的判断条件是: 当 ETL expose 出某个结果给分析端的时候,我们问自己一个问题:ETL 放出的这组 data 代表了 domain model 中的哪个部份呢?如果可以顺利回答的话,通常没什么问题。

但客观来说,其实在很多时候这是一个要取平衡点的问题,没有标准的答案。但可以确定的是,绝大多数的时候不会发生分析端说:”欸帮我用ETL建一个我分析要看的 table,schema 长这样”。而是双方应该要讨论 “这个要看的东西要怎么用现有的 domain model 来呈现”,而根据这个来建立 ETL 的 output。

如上图,如果 ETL 抽象太靠近 Application DB 的话,理论上分析可以做出很多变化。但抽象的意义就失去了:对于分析端来说,schema 还是很复杂。并且分析端承受 application 变化的能力也变弱了。反之,如果太靠近分析端的话,对于分析端理论上 query 可以变得很简单,也把 application 的 schema 包装得很完整,但这时候可以用这个抽象来做的变化就少了,同时也代表分析端跟 ETL 会多出很多不必要的沟通。

Layered structure

综合以上,新的架构变成:

首先,我们发现常见的 layered architecture 很适合来表达这个概念:每一个 layer 包装位于自己下方的 layer,并且提供介面给上方的 layer。下方的 layer 不可以依赖上方的 layer。 在这样的架构下,我们有一个 ETL layer 介于分析跟 application data 的中间,提供具有 domain model 意义的资讯给上方,但本身并不用知道上方是怎么使用它的。 最上方的分析端,在”某些特殊的时机” (好比说一些还很不确定的实验等等) 可以跳过中间的ETL这层,直接取用 application/3rd party 的 data schema,但同时也要承受 query 会不预期坏掉的风险。

其实整体来说,实作上的改动并不大,主要是透过这个过程让我们更清楚了各种类型的 data 扮演的角色和彼此之间的关系:

  • ETL 这层不应该去知道上方分析端的细节,更不该依赖它。
  • ETL 放出的 data 必需要具有 domain model 的意义,而不能只具有分析的意义。

但有了这两个规范之后,从理解整个工作流程到看懂 code 进而维护它,都因此变得直觉许多。

写在后面

这一切其实都还是个进行中的过程,我们还是持续中过程/错误中去学习。 到目前为止,整理出来我们认为学到的功课有:

  • 在写程式的时候我们会有 code smell:一些小地方作为警讯,让我们发现可能潜在的问题。而在不同 team 的沟通上,可能也有类似的 “smell”:当有一些事情好像总是很不顺利,也许是背后的东西有什么误会。
  • 如同工程的日常,大多数的事情没有绝对的好坏,而是看我们怎么做 trade-off。像是 “ETL 该靠 application/分析多近” 就是一个好的例子。但往往难的是 “发现 阿!原来是这个地方要做出 trade-off 阿” 的这个过程。
  • 像 refactor code 一样,在调整各种流程/架构的时候,”如果某个地方有变动,那状况会变成怎样” 是个评估流程/架构的改动好不好的标准。
  • 也像是写 code 一样,domain model 会贯穿整个流程:确保所有人,包括 PM/工程师/分析师、甚至是 marketing/sales 团队的人,对于 domain model 都有共同的认知之后,大部份的事情才有办法顺利运作。
  • 不同领域的概念,有时候可以交替使用:像是一开始的遇到【问题3】其实让人联想到 shutgun surgery 、layered architecture 等。当这些本来很不同的概念连结起来之后,就有可能可以变成很有趣的 mental model 让我们在思考上跳关。
相关文章