科学数据规模化实践:Benchling迁移Schema模型以实现高性能扩展
Benchling是一个统一的科学数据平台。它使科学家能够在复杂的科学研究上进行协作,实现工作自动化,并为人工智能提供动力。客户在我们的平台上存储大量数据,并在Benchling内部及其自身基础设施的许多应用程序中加以利用。确保客户数据能够以高性能和可扩展的方式访问至关重要。
在本文中,我们将探讨我们在存储和检索客户数据方式上最近的一次转变。通过迁移到更紧凑的数据结构,我们解决了与数据量增加相关的关键性能挑战。这种转变需要在速度与灵活性之间取得谨慎的平衡,并采用分阶段的方法以最大限度地减少对用户的干扰。
Benchling Schemas
Benchling系统的核心是Schemas,这是一个允许Benchling内部团队和客户配置各种数据形态的产品,用于定义实体必须遵循的字段、属性和约束。这些数据结构代表了诸如设备、存储、生物分子、工作流、任务、实验记录以及科学测试记录结果等实体。Schemas位于我们称之为定义层的部分。
一个定义分子数据结构的示例模式
每个模式的实例,称为可模式化项目,代表了科学家输入的实际数据。我们称之为实例层。这些项目填充了符合模式定义字段的字段值。随着Benchling用户群的扩大以及平台每年摄入的模式化数据量的增加,优化字段值的存储变得至关重要。
分子的实际实例与其定义模式之间的关系
挑战:规模与性能
历史上,Benchling见证了从科学家手动上传数据到与实验室设备集成的转变,实现了数据自动采集。这显著提高了数据摄入的速度和量级。
测定结果,用于捕获实验数据,是Benchling中最常见的模式化项目。测定是用于测量样本中特定目标(如分子或生物实体)的存在、数量或活性的实验室程序。到2021年,测定结果摄入性能的下降使我们明显意识到,需要一个更具可扩展性的方法来存储字段值,以(1)提高数据摄入速度,(2)避免数据库扩展限制,特别是为了保持在PostgreSQL大小限制内,而不必采用分片技术。
旧世界:实体-属性-值模型
Benchling最初采用了实体-属性-值模型,这是一种灵活的数据模型,非常适合存储稀疏数据。在Benchling早期,当数据量尚可管理且访问模式不太明确时,该模型是有效的。
属性值过去使用EAV模型存储
然而,随着数据量的增加,EAV模型的几个缺点显现出来:
- 稀疏行:在EAV表中,每种数据类型都表示为单独的列,但每行通常只填充一列。这导致了存在未使用列的稀疏行。
- 元数据开销:Postgres每行的元数据在规模上造成了低效率。每个属性都需要自己的行,导致n个实体和k个属性产生O(n*k)行。Postgres每行23字节的开销加剧了空间低效性。
- 低效的访问模式:大多数读取操作需要一次性获取实体的所有属性。将这些属性分散在许多行中,迫使我们为读取单个实体而查询和返回许多行。
- 额外的连接:由于每个实体的属性存储在单独的行中,查询多个属性上的匹配项需要对EAV表进行多次连接。例如,在"名称"、“分子式"和"重量"上查找实体匹配项,将需要EAV表与自身连接两次。
这些限制使得EAV模型不太适合Benchling的需求。
新世界:使用PostgreSQL的JSONB
为了解决EAV的缺点,我们采用了PostgreSQL的JSONB数据类型,它支持高效的键值查询和索引。这使我们能够将实体的所有字段值压缩到一个存储在单行中的JSON blob。
我们现在使用JSONB列来存储属性值
这个新模型有几个优点:
- 紧凑的数据存储:JSONB格式通过将实体的所有属性存储在一行中,显著减少了行数。
- 改进的读写性能:由于大多数读取操作涉及检索实体的所有属性,查询单行被证明比跨多行查询属性要快得多。同样,在上传实体时,向数据库写入一个大行比写入许多稀疏行更快。
然而,这个模型也带来了新的挑战:
- 查询字段信息:回答某些问题效率较低。例如,检查某个字段是否有非空值需要更复杂的查询来深入JSON结构。键值索引可以缓解此问题。
- 锁争用:由于实体的所有字段都存储在一个JSONB文档中,如果一个进程更新"分子式"字段,另一个进程更新"重量"字段,它们都在修改同一行。这可能会造成瓶颈,因为一次只能有一个进程锁定该行。相比之下,在旧的EAV模型中,不同的字段存储在不同的行中,因此争用较少。这一权衡被认为是可接受的,因为在Benchling当前的使用模式中,这种同时写入的情况很少见。
为了缓解其中一些挑战,我们还考虑了其他替代方案,例如使用GIN索引在JSONB字段内实现更快的键值查找。具体来说,我们需要快速遍历实体链接,并回答诸如"哪些其他实体链接到给定实体?“之类的问题。
新的JSONB结构使得回答上述问题变得不那么直接
然而,虽然GIN索引加快了读取速度,但它们会减慢插入速度,因为PostgreSQL每次插入新的JSONB文档时都需要更新索引。由于我们要存储数百万条记录,不断更新GIN索引将成为瓶颈。鉴于我们在数据摄入速度上已经遇到瓶颈,我们选择了一种更简单的方法:添加一个表来跟踪实体之间的链接。
为每个链接插入一行确实会导致较高的开销,并且在空间效率上不如GIN索引,但这并不比我们之前将链接存储在EAV表之外的单独表中的旧模型更差。链接表允许我们对键进行微小的更新,而不会触发整个JSONB结构的重新索引,从而减少了不必要的开销。此外,通过在链接表中记录属性名称,我们不仅可以高效地查询哪些项目链接在一起,还可以查询建立链接的具体属性。
性能改进
推出新模型后,正如预期的那样,我们看到批量读取和写入的性能得到了改善。这改善了我们数据仓库和笔记本表格中结果摄入等领域的性能。
以下是我们内部测试以及生产环境汇总数据得出的部分指标样本:
- 测定结果摄入速度提升高达7倍
- 将项目从内部数据库映射到数据仓库模型的速度提升了33%
- 当序列更新时,在数据仓库中查找需要更新的项目的速度提升了60%
- 在我们Analysis产品中查询实体的速度提升了约2倍
性能结果可能因数据量、工作负载模式和系统配置而异。此处强调的改进在数据量较高的环境中最为显著。
保障数据完整性的分步推出
模式字段值在Benchling中无处不在,因此新旧系统之间需要保持数据一致性。我们首先将推出阶段应用于结果产品,因为这是Benchling规模扩展最多且开始遇到摄入缓慢的地方。为了克服书面测试的局限性——例如无法覆盖生产环境中遇到的所有可能边缘情况——我们分几个阶段在三年内推出了这次重构。
- 阶段1:双重写入和完整性检查:每次值更新都同时写入旧表和新表。但是,我们仍然从旧表读取数据。在此阶段,我们每晚从旧表到新表运行回填,并记录沿途发现的任何不一致之处。利用夜间完整性检查,我们修补了那些要么遗漏了新表写入,要么导致不一致写入的代码路径。
- 阶段2:切换到JSONB读取(仍进行完整性检查):在确信新表包含正确的字段值后,我们切换到从新表读取数据。在此阶段,我们仍然同时写入两个表,并继续在两个表之间进行夜间完整性检查。
- 阶段3:弃用旧的EAV表:这是不归路。我们弃用了旧的字段值表并停止向其写入数据。任何访问旧字段值表的尝试都会引发错误,因为与可能遗漏新表写入或从旧表读取损坏数据相比,这更可取。
挑战与解决方案
在整个迁移过程中,我们遇到了各种挑战,特别是在维护数据完整性和确保系统边界清晰方面。几个关键问题包括:
- 事务隔离:使用
READ-COMMITTED隔离级别有助于防止大多数竞争条件,而不会导致进程阻塞。然而,一些竞争条件需要在Postgres的行级锁之上添加额外的咨询锁。 - 死锁管理:在新系统中,我们必须管理可能出现的死锁,这些死锁源于多个进程更新重叠实体范围上的字段及相关数据时的锁争用。
- 类型系统灵活性:JSONB模型中不太严格的类型系统在输入验证和数据强制转换方面引入了新的复杂性。确保不同字段类型得到正确表示需要细致的关注、更彻底的测试以及产品与工程团队之间的紧密合作,以在预期行为上保持一致。
- 范围蔓延:在如此重大的重构中,存在将现有架构增强到超越一对一迁移的诱惑。为了避免项目范围无限扩大,我们专注于一些能带来重大效益的选定改进。一个例子是重构我们如何检查可模式化项目之间的唯一约束冲突。这种重构将重复检查的时间从几分钟减少到仅10-20毫秒,显著改善了拥有大型数据集的客户的性能。
为了克服这些挑战中的大部分,我们强调了全面测试和清晰错误日志记录的重要性,以便及时识别和修复问题。
展望未来:构建更具韧性和可扩展性的系统
为了跟上我们平台日益增长的需求,我们专注于几项增强功能,以使Benchling更加高效、有韧性并面向未来做好准备。以下是我们正在做的准备:
- 更清晰的性能监控:关于产品性能表现的透明度是打造卓越用户体验的第一步。我们当前的优先任务之一是建立更好的指标,使我们能够更深入地了解系统的性能,无论是在细粒度层面(例如单个字段值访问时间)还是在更广泛的应用程序层面。这将帮助我们快速识别任何瓶颈并主动解决它们。
- 重构字段访问模式:尽管我们的数据层和应用程序逻辑之间存在分离,但更改数据层的实现确实会影响应用层。随着将模式化字段值存储为JSON blob的转变,我们有机会优化Benchling内不同团队访问这些值的方式,以利用新的表示形式,这可以减少锁争用并提高整体系统性能。
- 强化系统边界:模块化代码是在不断壮大的团队和快速扩展的产品中进行快速开发的关键。我们计划构建更清晰的接口并在模块之间执行更严格的契约。这将有助于降低未来迁移的复杂性,并确保代码更具可维护性。
关键要点
从这次迁移项目中,我们学到了几个宝贵的经验:
- 数据建模中的权衡:虽然新的基于JSONB的模型解决了许多性能和存储问题,但它在查询和锁争用方面带来了挑战。仔细评估这些权衡以确保收益大于弊端非常重要。
- 分步推出以最小化风险:分多个阶段推出新的数据模型使我们能够处理边缘情况并确保数据完整性。这种分阶段的方法对于触及系统关键部分的大规模迁移至关重要。
- 全面测试与监控的重要性:单元测试、集成测试和实时完整性检查的结合是识别和解决系统问题的关键。
- 跨团队协作至关重要:由于字段值在各个团队和工作流中被访问和更新,与产品经理和其他工程团队合作是必不可少的。确保新模型顺利集成到系统的所有部分需要清晰的沟通和协调。
为扩展而构建
从实体-属性-值模型迁移到更紧凑的基于JSONB的表示,确保了Benchling能够随着其日益增长的数据摄入需求而扩展。虽然新模型显著提高了读写性能,但也需要仔细规划来管理所涉及的权衡,例如锁争用和查询复杂性。我们分阶段迁移的方法使我们能够在问题出现时发现并解决它们,确保了平稳过渡,并最大限度减少了对用户的影响。
展望未来,我们将通过改进性能指标、优化数据访问模式以及确保我们的基础设施能够支持Benchling的持续增长来不断完善系统。在此次迁移过程中学到的经验不仅加强了我们的工程实践,还为未来更具可扩展性和高效的数据模型奠定了基础。通过关注协作和持续改进,我们已经准备好应对未来的挑战。