在Google的DORA DevOps的研究中,测试数据管理被视为软件交付和组织绩效的重要能力之一[1],它被用于手动或者自动化测试当中,也可能用于生产环境的全链路压测,良好的测试数据可以快速验证不同逻辑分支的测试用例、重现缺陷或者模拟错误。
但伴随着业务的迅速迭代,数据库、表不断的增加,同时存量的表结构也频繁变更,测试数据的生成与管理也变得越发困难。传统的手动创建测试数据、接口造数方法低效且难以确保全面覆盖,数据的关联性、通用性和复用率也有所欠缺。虽然使用生产数据进行测试看似便捷,但却经常面临用户隐私安全事故的风险。同时,在使用脱敏或历史数据时,数据的有效性和关联性也容易丧失。
在这种情况下,怎样在保证维护测试数据的效率、质量和信息安全的同时,又能最大程度地降低测试数据生成的成本和管理的复杂性呢?
现如今,无论是人工造数还是使用生产数据,都有一种更便捷的解决方案—测试数据管理(TDM)与测试数据合成引擎(TDG)。
测试数据合成引擎 (TDG)是根据特定测试用例需求的测试数据场景按需生成合成测试数据。不使用生产数据库中的任何实际数据。此方案需要根据生产环境数据特征、日志特征等进行建模,基于这个模型由造数工厂构造生成测试数据。
在模型可靠的理想情况下,数据特征能够符合生产环境特征,且与生产环境无任何关联,数据是十分安全的。但对这个模型的要求其实是懂业务逻辑,而在一个成规模的业务体系下,这种情况下建模造数,数据关联性难以得到保障。我们可以对单张表、比较简单的业务范围内完成建模,但是要作为统一方案推出面向整个产品体系,难以企及。
测试数据管理 (TDM)[2] 系统通常用于管理生产数据库的副本, 涉及从生产环境同步多个数据源,创建版本副本,屏蔽或混淆感数据,以及在多云环境中分发测试数据,以支持敏捷开发和自动化测试,压测等。
这种做法简单直接,来自生产环境的真实数据包含有价值的测试用例,这些测试用例对于早期和频繁验证应用程序以在软件开发生命周期中发现问题非常重要,但对数据信息安全保障有较高要求。使用生产的测试数据需要在同步前期,需要对敏感字段配置再次加密或混淆的规则,但这种规则往往是可以重复使用和修改的,且可以与业务绑定,实现多张表的数据联动。
基于以上对比和权衡,我们倾向于较为直接的测试数据管理 (TDM) 方案,但从成本的角度来看,采用商业TDM系统可能很昂贵,所以我们选择自研了一套测试数据管理系统,并且基于六个指标来衡量测试数据管理的最终成效。每一个对于消除测试数据瓶颈和避免数据安全漏洞风险的最终目标都是必不可少的:
测试数据准备的时间主要取决于数据同步并进行混淆处理的时间,同步时间与数据库表的大小相关,理论上只需要确保同步的策略合理,在数据库可以承受的范围内尽快同步好数据,即可达到最优效率。
所以在同步策略上,我们使用的是通过设置限流阈值来动态控制同步速率,只要不触发阈值,即可按照一定速率稳定的提升速度,达到最高上限后停止增速,并在触发阈值时迅速降低速率,既可在保障数据库安全稳定的情况下尽快同步完数据。
具体的限流策略我们参考了AIMD, Vegas,Gradient v2等限流算法, 最终采用了AIMD算法作为我们的限流策略。
和性增长/乘性降低(英语:additive-increase/multiplicative-decrease、AIMD)[3]算法是一个反馈控制算法,最广为人知的用途是在TCP拥塞控制[4]。AIMD将拥塞窗口的线性增长与监测到拥塞时的指数降低相结合。使用AIMD拥塞控制的多个流将最终收敛到使用等量的共享链路。
Netflix的实现[5]使用以下参数:
initialConcurrency
: 初始化并发量maxConcurrency
: 最大并发量minConcurrency
: 最小并发量timeout
: 当MRT达到该值,就认定服务已经过载backoffRatio
: 当检测overload时,用于将当前并发数相乘的一个系数;一般在0.5 ~ 0.9之间它的步骤:
initialConcurrency
进行初始化maxConcurrency
或者触发overloadMRT
> timeout
),通过将当前并发乘以 backoffRatio
。最多降至minConcurrency
或者从overload中恢复同步时数据库的过载信号我们设置为:
过载阈值由业务负责团队自行设置,设置在生产可控的范围内。
经过上线测试,在单个实例最大读取写入并发量调为1750时,550万的数据需要一小时左右完成同步,cpu和iops使用率大概在上涨6个百分点后稳定。
自研的系统可以根据测试团队的需求和反馈进行界面和工作流程的优化,提高测试人员获取所需测试数据的便捷性,提升用户体验, 如可视化的偏移混淆规则的版本管理,数据申请流程的规范,以及历史数据的管理等。平台化后的操作相比传统脚本更便捷容易上手,也能大大降低使用者的出错率,同时快照的查看能增强版本的回溯性,防止降低配置错误,丢失上个版本的配置信息的情况。
隐私安全问题一向是测试数据的核心问题,我们有着严苛的数据安全管理规范,在建设遵守规范的数据管理系统时,我们需要解决以下问题。
与用户相关的敏感信息,平台会载入负责数据隐私的团队的插件,不涉及接口调用,避免了加解密报文的泄露。数据的解密和混淆之后的再次加密均在黑盒下进行,用户使用时不会获取到用户有关的真实数据。
系统稳定性主要体现在数据同步服务的高可用上,我们采用的工作模式是典型的多Master 多Worker模式,通过Etcd来实现实时的双向同步:任务发布、运行状态上报。控制面(CP)发布客户端发起的任务, 然后由数据面(DP)来处理; 而数据面(DP)发出的事件,将由控制面(CP)来处理
CP、DP分离后,两者的运行状态互不依赖、影响,因此重启、升级、甚至所有节点当宕机依然可以恢复(Etcd保存状态)
系统采用的是领域驱动模式设计(DDD), 主要分为任务域、监控域、流控域、数据源域,每个业务域有独立的应用服务层、领域层、基础设施层,将系统的不同领域纵向分离开来,使得各层之间的依赖关系更清晰,以便创建更具可维护性和可扩展性的系统。
经过脱敏混淆的数据常常会面临与其他表联接时候失效的问题,对于这个问题我们定义了业务字段这一抽象概念,将所有相关联的字段统一定义为业务字段,在配置规则时统一该字段的规则,从而确保各个业务方调取的字段保持一致关联性。
如果某字段需要根据其他字段的值来进行偏移混淆,可以根据字段规则的顺序编排,然后通过上下文来实现。
如:表字段 Cellphone、Params,Params中某个值需要使用到偏移后的Cellphone的值
因此存在执行顺序的要求(伪代码):
maskedCellphone = Masking(ctx, Cellphone)
ctx = context.WithValue(Cellphone, maskedCellphone)
maskedParams = Masking(ctx, Params)
降低成本可通过降低同步数据的规模来解决,但选择降低规模后有会面临一个问题,即如何保障数据间的关联性?
我们有两种解决方案:
如果选择第一种方式,只通过主键的范围来同步数据,同步完成后剔除失去关联的数据,只保留上下文连贯的数据,这样的办法会大大降低数据的可用率,最终获得可用于测试的数据会变得很少。
如果选择条件筛选数据,筛选条件势必会相当复杂,影响读性能,关系链的复杂多变以及生成的sql性能的不确定性会增加系统的复杂度,影响系统的性能,还会承担一定风险,得不偿失。
权衡之下我们选择只基于单独索引和关联索引来筛选数据,在保障数据可用性的同时保障数据库性能,降低系统风险。
在测试数据质量得到保障之后,意味着拥有高质量的测试数据集,开发和测试团队可以在生产部署之前验证应用程序的功能和性能,还可以用于确保应用程序在不同条件下都能够正常工作。这些测试用例可以在多个应用场景中充分发挥作用,比如单元测试、集成测试、功能测试、回归测试、冒烟测试和压力测试等,也可以用来构建预发布环境,模拟整个生产环境生态。
这将有助于提高开发流程的可靠性,减少在生产中发现的问题,从而提高产品质量,对整个软件开发生命周期产生积极影响。
总的来说,自研测试数据管理系统在充分考虑了成本因素的前提下,通过优化效率、提升易用性、加强安全性、保证质量、提升通用性等多个方面的考量,提供了一种可行的解决方案。合理的决策将有助于消除测试数据瓶颈,降低数据安全漏洞风险,并在长期内实现更低的成本。
雷斯涵,来自技术平台部
DevOps tech: Test data management: https://cloud.google.com/architecture/devops/devops-tech-test-data-management
[2]What is Test Data Management?: https://www.delphix.com/glossary/what-is-test-data-management
[3]和性增长/乘性降低: https://zh.wikipedia.org/wiki/%E5%92%8C%E6%80%A7%E5%A2%9E%E9%95%BF/%E4%B9%98%E6%80%A7%E9%99%8D%E4%BD%8E
[4]TCP拥塞控制: https://zh.wikipedia.org/wiki/TCP%E6%8B%A5%E5%A1%9E%E6%8E%A7%E5%88%B6
[5]AIMDLimit: https://github.com/Netflix/concurrency-limits/blob/master/concurrency-limits-core/src/main/java/com/netflix/concurrency/limits/limit/AIMDLimit.java