--- 摄于 2017 年 9 月 藏川线前段
众所周知,踩坑后必须及时回顾和记录,这不仅能防止日后重蹈覆辙,更是一种良好的习惯。本文将分享我作为项目技术负责人,在一个小而全的项目中,从需求调研到技术选型,再到解决实际问题所经历的挑战与思考。尽管项目尚未结束,但作为分享案例,它确实提供了丰富的素材。
N周前,我接到上级通知,X团队某项目需要技术支持。一天后,X团队成员前来沟通,表示他们需要开发一个类似 CKB Probe 的数据统计后端,为 Fiber 团队提供数据。我们这边需要支持后端数据构建、整理和 API 接口。
本人内心 OS(简评):“很好,需求还算比较清晰,类似于需要做一个 HTTP 后端,基本就两块东西,一个是数据整理入库,一个是 API 接口,前端展示不用管,技术栈无限制,只需要完成任务就行。”
接着,我们讨论了更多细节。与 CKB Probe 不同的是,fiber 不止需要节点信息,还需要 channel 信息,以及一些简单的数据规整内容。
随后,我大致浏览了Fiber项目的代码,发现 Fiber 获取信息的方式与 CKB 不同。它无需像 CKB 那样通过随机漫步获取全网信息,而是在网络协议层就已经完成了整个网络内信息的收集工作,并提供了相应的 RPC 接口,方便用户获取这些数据。
接下来,我与Fiber团队确认了数据来源,包括数据的类型和具体含义等。
通过第一轮主动与两个团队沟通后,我整理了需求,并提出了两个方案:
如果这些信息足以支持前端展示,其实只需要做一个 RPC 转 HTTP 的 Proxy 就行,项目也就可以此完结。
双方对直接从 RPC 获取的方案表示一致,但 X 团队表示,直接做 RPC Proxy 并不能满足需求,数据统计需求不仅限于瞬时数据,还需要记录和统计历史数据。这意味着需要引入“历史数据”的概念,并需要一个数据库后端来承载这些瞬时数据,使其形成历史数据。
接着,我单独与 X 团队进行沟通,对数据的采集频率、历史数据保留时长等问题进行了核对。确认最终决定是历史数据最好一直保存,但对数据采集频率没有严格要求,一天一次即可。
项目需求:
这是一个经典的数据采集整理项目流程。项目的重点应该是确保采集数据的完整性,并使数据存储尽可能精细化(颗粒化),以便于分析。特别是,最好支持 SQL 查询,方便数据库数据的聚合。KV 数据存储虽然灵活,但在设计上很难做到无冗余读取或颗粒化读取,这需要精心设计每一对 KV 所表达的内容。
思路整理:
带着这些思路,我首先排除了 KV 数据库方案;接着,考虑到扩展性问题,又排除了传统 SQL 数据库方案。接下来就是寻找一个相对好用的时序数据库。
经典的时序数据库是 InfluxDB,我甚至写过它的Rust客户端。但在参考了其他几个时序数据库的实现后,最终选择了 QuestDB。原因是这款数据库整体性能优于 InfluxDB,支持更多的 SQL 语法和聚合分析,并且我知道有金融公司在生产环境中使用它。虽然学习新数据库需要投入一定精力,但这个成本相对可控。
至此,前期沟通工作和项目技术方案选型工作基本完成。不出意外的话,接下来的推进会很快,因为项目本身的难度并不高,只需将任务分成几个模块分别实现即可:
当我投入精力,将数据采集和数据库适配写得差不多的时候,意想不到的事情发生了:我突然发现一个非常严重的问题——来源数据中存在多对多关系,而令人非常困扰的是,几乎所有时序数据库都无法很好体现多对多关系。
绝大多数时序数据库的核心是时间关系,其他数据都需要扁平化(拍平)处理。跨表连接功能虽然有,但相对较弱。尤其无法完成一个致命的操作,即传统关系表和时序表的兼容处理。绝大部分时序数据库仅有时序表,无法在脱离时间因素的前提下创建关系表。
在这个项目中,多对多关系是必须表达的,因为这种关系是“节点(node)<-> 用户定义类型(UDT)”和“通道(channel)-> 用户定义类型(UDT)”,这是 Fiber 项目的独特性质,肯定需要正反向查询。无论项目前期是否上线这个功能,后期肯定都会需要。接下来有两条路:
首先,我与Fiber团队确认了这种关系是否是多对多且没有时间序列概念的,得到确认后,再与X团队沟通,询问是否可以直接扁平化处理,从而无法支持反向查询功能。
对,读者们,你们没有看错,虽然已经有思路去弥补多对多的问题,但是,这次沟通还是先将最坏结果告知对方,并确认是否能够接受这个结果。无论是调整数据库还是调整方案都是需要时间的,思路、方案、时间、技术需要综合考虑代价,不是说纯净完美的方案就是最好的,需要看代价。
沟通结果是,暂时来说,多对多的反查功能可以不存在,后期加是可以接受的。
看上去沟通完成了,但最终如何选择方向还是要由我来考量。要知道,这仍是项目方案验证阶段,实际上并没有任何数据迁移和兼容问题。那么,如果能让项目的技术栈相对纯净,将对后期项目的维护有很大帮助。每增加一个组件,项目的维护成本就可能翻倍,尤其是多个组件之间的同步问题,很可能会是导致项目“腐败”的最常见原因
于是,我暂停了当前进度,去寻找一个更适合的数据库。我查阅了许多时序数据库的文档,发现它们都不支持纯粹的关系表。最后,我发现了TimescaleDB(PostgreSQL + 插件),这是目前唯一同时支持两种表的数据库。简单研究后,我决定更换数据库。
我花了大量时间重新研读 TimescaleDB 的文档,过程中发现了一些新特性,包括列式存储和压缩存储、持续集成数据整理功能等。这些功能对于优化数据查询性能和降低存储成本大有裨益。然后,通过对项目数据关系的梳理,我写了一个符合 TimescaleDB 的 SQL 建表语句。这一次,我需要同时与运维、Fiber和X三个团队进行沟通。
沟通内容很简单:
我将这些信息抛给其他团队评审,同时继续进行适配调整。由于中途更换数据库,导致项目估时超出,我需要尽快完成 MVP 后端,加快进度让前端尽快进入适配阶段。只有这样,才能带着整个前后端项目去寻求更多人的评审,从而推动项目从验证 MVP 阶段走向更多接口上线。
在完善 API 接口的过程中,意外发现 TimescaleDB 的持续集成表(Continuous Aggregate)似乎并没有按照预期行为进行工作,这可不是小事,我设置了表的刷新策略为:
SELECT add_continuous_aggregate_policy(
'online_nodes_hourly',
start_offset => INTERVAL '3 hour',
end_offset => INTERVAL '10 minute',
schedule_interval => INTERVAL '5 minute'
);
然而,它并没有将 20 分钟之前的数据刷入持续集成表,咨询 AI 也说这个设置是正常的,但实际行为却不符合预期。通过观察刷新任务的时间和触发状态,确认任务确实正常执行,调整任务的时间间隔也同样不生效。但通过手动强制刷新数据,数据却能正常刷入。
这下我真的急了,要是再换一次数据库和方案,项目恐怕会面临巨大压力,甚至可能“爆炸”。随后,无意发现创建持续集成表的语句是以小时为 时间桶(bucket) 的:
CREATE MATERIALIZED VIEW online_nodes_hourly
WITH (timescaledb.continuous) AS
SELECT
-- time bucket by hour
time_bucket('1 hour', time) AS bucket,
node_id,
...
FROM node_infos
-- group by node ID to ensure each node appears only once
GROUP BY bucket, node_id
WITH NO DATA;
猜测是否是整个小时完结之后,才会进入数据统计流程?根据这个猜测进行实际验证后发现果然如此。这真是有惊无险!原来,持续聚合表的刷新机制中,最重要的规则是其归集时间桶的大小,其次才是归集任务的触发频率。要想获得更高的实时性,就需要设置更小的时间桶,并更频繁地触发任务。
从更换数据库,适配完成,到后端 MVP 程序出来,编写代码不到两天时间。项目本身的复杂度不高,但沟通内容确实不少。因为这里包含的每一步操作,都需要与各团队进行确认,而实际上并没有其他人帮忙,都需要我自己主动去沟通联系。
项目虽然还在进展中,但这些前期的经验已经可以做一个简单的分享了:在前期获取需求的过程中,我并没有很敏锐地发现数据中存在多对多关系,导致在方案验证阶段出现了一次切换数据库的决策。这极大地拖延了整个项目的进度,算是一个比较大的失误。
这个项目不算大,但整个开发过程与我以往的许多项目已经不同,或者说,我负责的职能不一样了。代码编写时间的占比与以往相比大幅度降低,取而代之的是:每一步需求的确认、上下游团队之间的对接工作等等。
请登录后评论
评论区
加载更多