如果你在项目列表或者某个技术讨论群里看到"Madeira"这个名字,第一反应很可能是葡萄牙那个火山岛,或者那种带着焦糖味的强化葡萄酒。但在我们团队,它是一个代号——一个从零搭建的轻量级自助式BI分析平台。我们花了大半年时间,把它从一张白纸做到业务部门真正在天天用,中间踩过的坑、推翻过的设计、总结出的经验,我想系统性地写一写。这篇文章适合那些准备做数据产品、或者在团队里负责搭建内部报表平台的人,尤其是资源不多、又想快速落地的中小团队。
1. 为什么我们给BI工具起名Madeira
1.1 这个代号从哪来
Madeira这个名字在软件行业其实有过一次很有分量的出场:微软早期做自助式BI产品时,内部项目代号就叫Madeira,也就是后来Power BI那条产品线的雏形。做技术的人多少都有点"代号情结",喜欢用地名、酒庄、甜品这类词给内部项目命名,既好记又有辨识度。我们立项时起过不少名字,什么SparkBoard、DataCanvas,翻了一圈不是被占用了就是太普通,最后有人翻到当年的项目代号表,说了一句"要不叫Madeira吧,有地缘特色,还带点酒香",就这么定了下来。
代号这东西,说重要也重要,说不重要也不重要。它最重要的作用是让团队内部讨论时有个清晰的指代词,而不是承担多少业务含义。真正决定项目命运的,是立项时你对问题本身的定义。
1.2 立项背景:我们要解决什么问题
当时我们公司有两套数据体系在并行运转:一套是数仓团队维护的正式报表,面向会写SQL的报表工程师,提出一个需求到上线基本要排两三周;另一套是业务部门自己用Excel维护的月度汇总,每个团队一张表,口径经常对不上。业务负责人想看清楚某个区域的销售趋势,得同时打开三个Excel文件自己比对,没人敢拍板"以哪个数为准"。
所以我们的目标从一开始就不是做一个Power BI的替代品,而是做一个中间层:让业务人员能自助连接经过治理的数据表,通过拖拽完成看板搭建,同时平台在背后自动记录每一次查询和指标口径。产品定位一句话就能说清——面向业务分析师的轻量级自助BI平台。它不服务专业的报表工程师,那些复杂需求该走数仓还是走数仓。
1.3 设计原则:少做功能,多做收敛
第一版我们砍掉了很多BI工具的标配功能:不做ETL编排、不做自定义SQL编辑器、不做报表的复杂分享权限,只保留三条链路:数据连接、模型配置、看板拖拽。
砍功能的原因是团队就五个人,前三个月要出可用版本。与其做一堆半成品功能堆在界面上,不如把三条核心链路彻底跑通。这个决策后来被反复证明是对的:业务方反馈最多的一句话是"你们的功能真不多,但我想做的事基本都能自己做出来"。这比一堆花哨但没人用得上的功能有价值得多。
2. 数据接入与关系建模:第一版架构的取舍
2.1 数据源适配器应该做到哪一层
第一版我们支持的数据源有四种:MySQL、PostgreSQL、ClickHouse,还有Excel上传。这四种数据源的能力差异非常大,如果你试图把它们抽象成一套完全统一的接口,后面会非常痛苦。
我直接用一张表说明这些差异:
| 数据源 | 查询下推能力 | 主要差异点 |
|---|---|---|
| MySQL | 支持 | 常见聚合函数齐全,语法标准 |
| PostgreSQL | 支持 | 支持JSON操作、窗口函数更丰富 |
| ClickHouse | 支持 | 日期函数、聚合函数与MySQL差异大 |
| Excel上传 | 不支持 | 只能全量导入后在内存里处理 |
如果强行把where和group by都下推到所有数据源,ClickHouse的语法差异会让你维护一套非常臃肿的方言层,而Excel又根本没法下推。我们最后把查询引擎拆成两层:SQL方言层和结果集操作层。
SQL方言层维护了一套轻量的方言映射,负责把常用操作(日期截断、字符串拼接、空值处理)翻译成目标数据源的语法。结果集操作层则兜底处理那些无法下推的部分——比如Excel数据源的过滤和聚合直接在应用内存里完成。这套设计换来的是"同一个看板在不同数据源上表现一致",业务人员不需要关心底层是Excel还是ClickHouse。
提示:不是所有数据源都需要做查询下推优化。Excel上传这种小数据量场景,内存聚合的延迟远小于维护方言解析的成本。做适配器之前,先想清楚每个数据源的量级和查询模式。
2.2 星型模型简化为宽表优先
一开始我们也想跟专业BI工具看齐,做一套完整的语义层:定义事实表、维度表、度量字段、计算字段、层级关系。做了一半发现,我们服务的业务团队,绝大多数场景其实就是一张宽表加上几个维度过滤条件,很少用到真正的星型模型。
最终我们做了一个简化版的关系模型,规则非常明确:
- 每个数据集由一到多张表组成
- 表之间最多支持两张关联键,关联方式限定为LEFT JOIN或INNER JOIN
- 度量字段必须来自事实表,维度字段可以来自任意关联表
- 关联键之外不做二次粒度控制
这个简化的核心好处是,前端拖拽时用户完全不需要理解"粒度"这个概念,系统通过主键约束保证聚合结果不会因为关联而翻倍。代价是如果业务确实需要多级明细分析,比如订单、订单明细、退款明细这种链路,就必须在数仓侧先做宽表预处理。
我们跟使用方明确了一条边界:宽表建设是数仓的责任,BI工具不做多级建模。这条边界省了无数沟通成本。
2.3 缓存刷新策略:查得快的代价
看板查询最快能做到几百毫秒,靠的不是什么高深的数据库调优,是缓存。我们用Redis存查询结果,缓存key由数据源ID、数据集ID和查询条件哈希组成,value是序列化后的表格结构。刷新策略有两种:
- 定时任务刷新:每天凌晨2点把常用数据集预跑一遍,写入缓存,上班打开就是热数据
- 手动刷新:业务人员点界面上的"刷新数据"按钮,清掉该数据集的所有缓存,下一次查询触发重建
这里有个非常容易被忽视的细节:手动刷新必须做缓存失效,而不是覆盖写。早期我们图省事直接覆盖写,结果两个用户同时触发刷新时出现了串数据的情况。排查了半天才定位到是缓存竞争问题。改成"先失效,后由查询触发重建"之后,问题彻底消失,代价是刷新后第一次打开会慢一些,但对报表场景来说完全可以接受。
3. 可视化渲染层的技术选型与性能优化
3.1 图表库选型:ECharts还是Recharts
可视化是BI工具的门面,业务人员第一眼感受到的"好不好用",基本都是靠图表交互。我们当时在ECharts、Ant Design Charts、Recharts三者之间做了对比,结论很直接:
| 图表库 | 包体影响 | 定制灵活性 | 社区活跃度 | 适合场景 |
|---|---|---|---|---|
| ECharts | 较大,可按需引入 | 强 | 高 | 复杂图表、深度定制 |
| Ant Design Charts | 中等 | 中 | 中 | 与Ant Design体系强绑定 |
| Recharts | 小 | 中 | 中 | React技术栈、图表形态简单 |
我们最终选了ECharts。原因是看板里总会出现一些比较特殊的图表形态——地图下钻、旭日图、多指标联动这种,ECharts的配置项最全,遇到边界问题时试错成本最低。而且它还有服务端渲染的能力,这对后面做报表分享和打印导出很有价值。
包体问题通过按需引入和CDN拆分解决,实际页面加载性能并没有成为瓶颈。
3.2 拖拽面板的数据结构:一切皆Schema
拖拽式操作的核心难点不在前端的拖拽交互,而在拖拽结果如何序列化成稳定、可迁移的JSON结构。我们为看板定义了一套Schema,它是前端交互和后端查询之间唯一的协议:
{ "version": 1, "dataSetId": "ds_12345", "metrics": [ {"field": "amount", "aggregation": "sum"} ], "dimensions": [ {"field": "province"} ], "filters": [ {"field": "date", "operator": "gte", "value": "2024-01-01"} ], "chartType": "bar", "layout": {"w": 6, "h": 4, "x": 0, "y": 0} }这套Schema的关键设计是:图表类型与查询逻辑完全解耦。用户拖拽时不管是选了柱状图还是折线图,metrics、dimensions、filters这部分完全不变,变化的只有chartType和layout。这个设计让看板的复制和迁移变得异常简单——从一个数据集复制到另一个数据集,只需要换dataSetId,然后做一次字段映射,查询逻辑天然兼容。
有了这套约定,后端查询的接口设计也简单了,前端把整个Schema作为参数提交,后端统一解析执行。这比传统的"前端传query参数、后端拼SQL"的方式规范得多,也方便做权限拦截。
3.3 数据量变大后的渲染优化:降采样与分批渲染
业务方最爱拖的图是"近三年每日订单量趋势",一拖就是上万甚至十万个点。ECharts直接渲染十万个SVG节点,浏览器会卡到没法滚动。我们做了两层优化:
- 采样压缩:当单图表点数超过5000时,用LTTB降采样算法把数据压缩到5000点以内。这个算法保留的是趋势极值点,人眼几乎分辨不出压缩前后的差异,但渲染压力直接降了一个量级。
- 分批渲染:图表先渲染首屏可见区域,等用户滚动或交互触发后再补全剩余部分,避免一次性阻塞主线程。
这两招配合下来,十万点级别的折线图也能稳定在60帧左右。建议所有做可视化的人提前掌握LTTB这个算法,它比简单等间隔采样实用得多,网上有现成的开源实现。
4. 权限模型:从"能用"到"敢用"
4.1 行级权限(RLS):怎么让不同的销售看不同的数
在BI工具里,行级权限不是加分项,是硬门槛。企业内部的数据看板,必然存在"销售A不能看到销售B的客户数据"这类需求。我们没有去做复杂的表达式规则引擎,而是用了一张字段映射表来落地:
- 数据集上配置一个权限维度,比如region
- 用户与权限值的关系维护在独立表里:user_id、region、access_type
- 查询时系统在模型层自动拼接过滤条件:
WHERE region IN (SELECT region FROM user_region WHERE user_id = ?)
这里面有一个非常关键的坑:权限条件拼接必须发生在服务端模型层,而不是前端传参。如果前端能够通过接口直接传入region参数,用户完全可以构造请求绕过权限看到不属于自己的数据。我们的做法是权限上下文在服务端Session中维护,前端只提交用户身份,可见范围完全由后端决定。
4.2 多租户隔离:缓存串号的事故复盘
SaaS部署方式下,多租户隔离是另一个容易出事的地方。我们第一版把租户ID当作普通查询参数处理,结果发生了一次真实的事故:两个租户看到了同一份数据。
排查过程不复杂,但教训很深刻。日志显示,租户A查询的数据集ID和租户B完全相同,缓存key又是按"数据源ID+数据集ID+查询条件哈希"生成的,完全没有租户维度。于是租户A先查了一次,缓存被写入;租户B发起相同查询,直接命中了A的缓存。
修复方案有三步:
- 所有缓存key强制以租户ID开头
- 数据库连接池按租户隔离,每个租户使用独立的数据源配置和连接
- 把"是否包含租户ID"写进代码评审的必查清单
最后一步是最重要的,因为这类问题在测试环境基本发现不了,只有并发租户多了才会暴露,而且一暴露就是数据安全事故。
4.3 审计日志:不为了追责,为了定位问题
审计模块我们一开始完全没做,直到有次业务负责人问"这个看板里的数字是谁改的",我们翻遍了数据库都没找到答案。后来补的日志又是SQL级的,业务人员根本看不懂。
现在我们的审计日志记录的是"人话事件":
- 谁在什么时间打开了哪个看板
- 谁修改了哪个数据集的指标口径:改了什么字段、从哪个聚合方式改成了哪个
- 谁导出了数据,导出了哪些维度范围的数据
实现上就是一个异步管道:操作事件推送到消息队列,消费者写入ClickHouse,查询端提供按人、按看板、按时间段的过滤。
注意:审计日志不只是合规要求,它更是产品迭代的依据。我们靠审计日志发现"导出Excel"是高频操作后,把导出功能做成了带水印的一键下载,反而减少了大量"为什么不能复制粘贴出来发给我"的客服咨询。
5. 上线之后踩过的坑与排查思路
5.1 慢查询排查:一个看板转圈十几秒的真正原因
项目上线两周后,销售团队反馈"某个看板打开要等十几秒"。我们的第一反应是渲染性能问题,结果用性能面板一测,渲染只花了300毫秒,剩下的时间全花在查询上。
完整的排查链路是这样的:
- 打开出问题的看板,在服务端日志里找到对应的query id
- 用query id反查到执行计划,发现命中的是一张几千万行的订单明细表,而不是数仓预聚合的表
- 检查数据集配置,发现字段的默认聚合选择的是"明细行数",导致查询退化成了全表扫描
修复分两步:把数据集的默认聚合改成sum(amount),同时把数仓侧的查询超时时间从60秒压到10秒。压超时时间这个操作很多人不理解,逻辑其实很简单:如果一个看板查询10秒都出不来,大概率是配置有问题,与其让用户无限等待然后超时,不如快速报错让问题提前暴露。
5.2 Excel上传导致的内存溢出
Excel上传功能是给业务临时导入数据用的,第一版做得很粗暴:文件直接读进内存转DataFrame。测试环境传几MB的文件完全没问题,生产环境有次用户传了一个80MB的Excel,每个sheet还有合并单元格,服务端直接OOM,连带着同一个节点上的其他租户查询一起挂了。
修复方案分三层:
- 上传文件先落盘,用流式方式逐行读取,边读边转,不在内存里放完整文件
- 行数超过50万的Excel直接拒绝导入,页面提示用户走数仓同步流程
- JVM堆内存增加监控告警,堆内存超过阈值自动告警并触发扩容
这里我有一条很深的体会:Excel处理的坑比数据查询的坑多得多。合并单元格、公式引用、日期格式被转成科学计数法、字符集乱码,每一个细节都能让报表数据错得离谱。我们的解决方案是在解析完成后做一次"数据体检":抽样打印前20行数据让用户确认后再入库。这个功能看起来不起眼,但上线后挡掉了90%的脏数据投诉。
5.3 看板白屏:一个ResizeObserver引发的兼容性问题
项目上线第三周,有客户反馈某个浏览器打开看板白屏。我们自己在Chrome上完全复现不了,后来才知道客户公司的内部浏览器是旧版Chromium内核。
排查链路的起点是浏览器控制台,报错信息是ResizeObserver is not defined。全局搜索代码里用到ResizeObserver的地方,发现是拖拽面板在窗口大小变化时用来重新计算布局的。查了一下兼容性,ResizeObserver要到Chrome 64才支持,客户的内网浏览器内核还是60多的版本。
修复本身很简单,加了个polyfill就解决了。但这个问题暴露的是企业级应用的一个通病:浏览器兼容性永远要当成一等公民对待。我们在产品文档里明确写了"支持Chrome 80及以上版本、Edge、Firefox",同时在打包层面统一引入core-js补齐polyfill,彻底断了这类问题的后路。
5.4 并发刷新导致的数据不一致
最后补一个并发问题。有次十几个业务人员同时点了同一个数据集的"手动刷新",结果出现了数据不一致:一部分人看到的是新数据,一部分人看到的是旧数据。
原因是刷新流程本身是"先删缓存再查库",并发请求全都在删完缓存后涌向数据库,但由于时序不同,查到的数据快照不是同一批。
我们改成双缓冲机制:新数据查询全量完成后再原子替换缓存,替换之前旧缓存继续对外服务。刷新接口加了一把互斥锁,同一数据集同一时刻只允许一个刷新任务在跑。这个方案上线后,再也没出现过刷新导致的脏读问题。类似的思路在缓存更新的很多场景都能复用,如果你的某个接口有类似问题,可以考虑同样的手段。
尾声:关于Madeira的几句话
项目上线八个月后回头看,Madeira真正的成功点不在于技术选型有多先进,而在于我们想清楚了一件事:给业务人员用的BI工具,核心不是功能堆叠,而是把数据链路的可信度做出来。每一次查询都有归属,每一个指标口径都有记录,每一层权限都清晰可见——做到这些,用户自然愿意放下Excel。
如果你们也打算做一个类似的自助分析项目,我的建议是:先用两周时间把产品定位和边界写在纸上,想清楚"哪些功能坚决不做",这比想清楚"要做哪些功能"更重要。选型上不要迷信大而全的框架,按自己的数据量级和使用场景来。另外,权限和审计模块一定要从第一天就开始设计,后期补的成本会翻好几倍。最后再分享一个小技巧:给内部项目起个像Madeira这样的地名代号,团队聊起来真的会更有归属感。