BasicAI SaaS 海量标注结果存储方案

BasicAI 作为一个支持多租户的 AI 标注 SaaS 平台,在人工手动标注和模型自动标注的过程中会产生海量的标注结果。产品功能上的需求导致单个用户操作就会加载和提交成千上万的记录,或者拷贝几十上百万的记录,对后端存储的挑战非常大。本文讲述了一种解决海量标注结果存储的方案,以供大家参考。

BasicAI SaaS 海量标注结果存储方案

需求背景

标注结果,也就是从点云和图片中通过人工手动或模型自动标注出来的对象(Object)信息,是以 JSON 字符串来存储,单个标注对象的信息大小从几百 Byte 到几百 KB。在点云分割场景下,由于需要保存分割区域里所有点的信息,因此对象信息会比较大,即便是存储点的索引,单个标注对象的信息大小也会达到几百 KB。传统的方式是使用数据库来存储标注结果,这样可以支持比较复杂的查询,但会面临以下挑战。

存储挑战

按 1000 个租户,每个租户 100 万 Data,每个 Data 里有 10 个 Object,那么总的 Object 数量就是 100 亿。按每个 Object 1KB 大小计算,则需要的存储空间为 10TB。这还只是数据流一种标注来源,如果考虑到工作流和模型标注,每个工作流 Task 都会复制一份某个来源的全部标注结果,并且会产生一份自己的标注结果,每次模型自动标注也会产生一份标注结果。保守估计,多标注来源情况下数据量相比数据流单来源会扩大 10 倍,也就是 1000 亿 Object,100TB 存储大小。

读写挑战

如果说存储上的挑战还勉强可以通过加节点和磁盘来解决,读写上的挑战就没法了,因为读写性能无法通过加节点来线性扩展。对于连续帧,每次在工具里打开就会同时加载几百个 Data 的几千个 Object,也就是会一次从数据库读取几千条记录(数据量几 MB 到几百 MB),提交时也会面临同样的压力。还有工作流里创建 Task,如果选择了整个数据集的数据,那么发布 Task 时需要拷贝该数据集下的所有 Data(即便只是引用,但也会产生大量关系记录),以及某个标注来源的全部标注结果(需求上要求重新为 Data 分配标注人员时允许重置标注结果)。如果说对于并发度不高的私有化部署场景,数据库还能勉强支撑,那么对于 SaaS,同时会有成千上万用户在线操作,数据库方案就变得完全不可行。

当前方案

当前设计中,为了满足查询某个数据集下所有 Object 的需求,标注结果是按 Object 粒度(具体来说是按更细的 Box)存放到数据库里的。当时初步评估了数据量,大概在 10 亿级别,认为 TiDB 这样的分布式数据库可以扛得住。但随着功能的迭代,发现数据量至少会高出两个数量级,并且当时也没有预见到连续帧和点云分割这样的大量读写数据的场景。这么大的数据量和读写量,即便是像 TiDB 这样的分布式关系数据库,或者 Elasticsearch、MongoDB 这样的 NoSQL 数据库,都难以抗住,或者说运维成本很高和难度极大。这些分布式数据库都是采用多副本(TiDB 和 MongoDB 默认为 3,Elasticsearch 默认为 2),相应的存储空间还会扩大到原始的 3 倍和 2 倍。

改进方案

要想扛住这么大数据量的存储和读写,只有放弃数据库存储方式,改为使用文件来存储标注结果。每个 Data 的每个标注来源的标注结果存为一个文件,每次读取和提交标注结果都按这个粒度来整体操作,标注结果文件数量跟 Data 数量能大致维持在一个数量级。对象存储作为一种能够支撑海量小文件存储的文件存储系统,就非常适合标注结果的存放,实现方案如下。

该方案下,浏览器直接从对象存储服务以文件的形式读写标注结果,极大降低了 API 服务和数据库的读写压力。在连续帧场景下,浏览器会同时并发下载和上传几百个文件,但浏览器的同源并发限制会拉长整体的下载和上传时间。由于对象存储服务不支持单个请求批量下载和上传多个文件,如果这里遇到了性能瓶颈,那么可以增加一个“批量请求代理服务”来支持批量下载和上传文件。相比数据库方案,其优势如下。

  1. 相比于数据库存储方案,文件存储方案性能提升的根本原因在于将随机的数据库读写转变为顺序的文件读写。按一个连续帧 300 个 Data,每个 Data 30 个 Object 计算,一次连续帧标注结果加载或提交,数据库存储方案需要随机读写 9000 个 Object,而文件存储方案只需要连续读写 300 个文件,这里的性能差异是巨大的;
  2. 将标注结果从数据库里移除之后,其记录数和存储大小会降低 1~2 个数量级,成本会显著降低,同时也避免了影响其它业务数据读写;
  3. 支持海量标注结果存储,如果使用公有云的对象存储服务(AWS S3、阿里云 OSS),那么近乎拥有无限的存储空间,成本也比数据库的 SSD 磁盘低,私有化场景可以使用开源的 MinIO 来无缝替换;
  4. 浏览器直接从对象存储服务读写标注结果,大大降低了 API 服务的带宽需求,公有云对象存储服务的带宽更大,价格也更便宜,还可以并发读写多个文件来加速;

限制

  1. 无法支持以 Object 为粒度的标注结果查询,比如展示某个数据集下的 Object 列表并根据某些属性过滤。如果一定要支持 Object 查询,可以使用专门的搜索系统来完成。如果团队没有开发搜索系统的能力,那么可以基于 Elasticsearch 这样的搜索系统来搭建,里面只存储搜需要被搜索的字段,这样能显著降低数据量大小,但记录数还是会一样多;
  2. 多人同时提交某个 Data 的标注结果,后者会覆盖掉前者,需要限制每个 Data 同时只允许一个人修改。数据库存储方案也有类似问题,只不过冲突的概率会小一些(在 Object 粒度上);

产品功能建议

即便把标注结果以文件形式存储到了对象存储里,Data 的数量级也是比较大的,随着日积月累,早晚也会达到数据库的瓶颈。如果在产品功能设计时多考虑一些影响性能的因素,是可以延缓甚至避免达到数据库瓶颈的。

避免单个操作产生大量数据,或者提高其操作门槛

以创建 Task 为例,目前单个数据集的 Data 数量上限为 10 万,如果用户选择了整个数据集和某个来源的标注结果,那么发布 Task 时就会一次插入 10 万条关系记录,以及几百万的 Object 记录(数据库方案),或者写入 10 万个文件(文件存储方案)。如果 Task 可以复用数据集的数据和标注结果,那么就可以避免这些数据的产生。如果需求上确实无法避免,那么还可以对 Task 数量进行限制。针对数据集的模型批量自动标注也是类似,每跑一次,就会产生几百万的记录,并且会消耗大量 GPU 资源,可以通过调用次数来限制或收费。

引导用户定期清理数据,避免数据只增不减

比如提高数据集的存储费用和限制数据量,引导用户及时删除已标注完成的数据集,删除的数据集先进入回收站,一定时期后再清理,包括相关的 Task 等工作流数据。对于一些需要长期保留的数据,可以备份到对象存储上。

参考资料

  1. 阿里云 Elasticsearch 规格容量评估