本期作者
彭良友
哔哩哔哩资深测试开发工程师
负责B站基础架构存储/微服务质量保障,一直从事中间件的质量工程建设工作,专注于分布式系统测试方案设计,应用和推广。
01 背景
之前我们介绍了B站分布式KV存储在B站的探索实践(←点击回顾前文)。本文主要介绍对于高可靠、高可用、高性能、高扩展的B站分布式KV存储系统来如何保障其可靠性以及混沌工程的落地实践。
02 大型分布式存储难点
众所周知大型分布式系统很难设计,开发和测试[1],这种挑战有多方面因素:意料外的节点故障,不同模块的异步交互逻辑,网络通信故障造成的数据丢失,多核心CPU多线程代码和各种客户端逻辑等,所有这些因素都很容易造成非常严重的线上事故,这些极端情况非常难去发现,判别和修复,这些问题可能是一段并行事务中一行非常简单的代码,很小概率才会触发,但是最终产生的结果可能就是灾难性的。
基于此传统大型分布式系统的可靠性测试需要非常完备的测试用例,以覆盖各种可能的影响因素,通常基于分层的思想,根据开发测试运维经验和历史的线上case来建立可靠性的场景集合并且正交覆盖各种并发和边界值情况,在测试环境逐一验证,可靠性设计是否符合预期并完善,但是难免会遗漏一些预期外或者多因素互相干扰下的故障。
在大型分布式存储系统中更是如此,进一步需要关注数据一致性和持久性,任何的数据错误和丢失对于核心业务来说都无法接受,在已有的分布式系统可靠性场景之外,还需要设计数据一致性和持久性的相关场景以及测试验证方案。分布式存储业界已经发展几十年,各个商用存储团队也有对应的开源测试框架,比较知名的比如P#[2]和Jepsen[3],但是这些框架应用成本高,在非商用存储团队中已有的迭代开发过程中很难有额外的人力应用实施,并且对于操作人员要求高,往往需要特殊的编程语言和框架学习成本。
03 混沌工程的意义
混沌理论[4]在各个研究领域都有实践,混沌并不是指事务的复杂性超过某个临界点变得无法预测,而是一种思考和量化分析的方法,在探讨一个动态系统必须用整体和连续的关系才能加以解释以及预测其行为,对于一个分布式系统的可靠性也可以利用此方法对其整体和连续性进行思考和分析,对应产生了混沌工程。
混沌工程特别适用于分布式环境,分布式系统是一组通过网络连接并共享资源的节点。对于大型分布式系统,组件通常具有复杂且不可预知的依赖性,很难排除错误或预测何时会发生错误。
分布式系统有许多方式可能会失效,系统越大、越复杂,其行为就越不可预测和混沌。
混沌工程实验故意在分布式系统中产生各种故障异常,以测试系统并找出可靠性优化点,可能发现的一些示例包括:
-
盲点:监控遗漏或者很难触及的地方。
-
隐藏的错误:小概率和极端场景下故障,比如多场景叠加。
-
性能瓶颈:效率和性能问题,以及实现不合理。
2008年8月Netfilx因为数据库存储故障造成了长达三天的停机,之后他们开发了对应的测试工具,并在2015年发布了《混沌工程原则》[5],通过在复杂系统中采用混沌方式进行实验,增强存储产品可靠性方案处理混乱现象的能力和信心,这些实验遵循四个步骤:
-
用系统的正常行为下一些可测量输出来定义“稳定状态”。
-
假设这个在控制组和实验组会继续保持稳定状态。
-
在实验组中注入各种真实线上实际可能发生的事件因子。
-
通过控制组和实验组之间的状态差异来验证该因子的影响。
同时提出了5个高级原则,实际在应用实践混沌工程方法时需要结合各个系统的实际使用环境和组织形态会有所区别,这里会补充一些我们实践中的变化:
-
建立一个围绕稳定状态行为的假设。定义业务是否稳态,并不是系统资源指标,这里的指标是直接衡量系统服务质量的业务指标,比如任务是否执行成功/执行时间,请求时延,错误率等,甚至自行设计冒烟回归用例进行检测。
-
模拟生产环境中真实或者可能存在的故障场景。比如网络延迟,服务异常,还有存储系统的副本异常等。
-
在生产环境中运行实验。并不是必须要在线上生产环境中运行,而是实验环境要尽可能真实。在混沌工程实践中,各个团队各个成员职责不一样或者系统的可靠性不足以像Netflix一样在生产环境中运行。要保证测试环境足够真实,混沌工程的实践才更有价值,所以在大型分布式存储系统中,我们需要保证测试环境可以等比例的复制线上真实环境,以及各种类型不同的业务和负载。
-
持续自动化运行实验。这里面有两方面原因,一方面是单次执行对于一个不断变化的真实系统意义有限,因为系统代码可能在不断迭代,用户场景也在不断变化,只有持续的运行才能降低故障重新出现的回归问题;另外一个方面很多故障场景是有一定概率才会出现甚至于运行成百上千次才会触发,只有依赖持续自动化运行才能保证这些故障的覆盖。
-
最小化爆炸半径。这点需要故障注入工具提供细粒度的配置和控制能力,因为实验环境可能有多种用途甚至实际生产,要防止对其它测试和业务的干扰。
04 混沌工程实践
4.1 建立稳态假设
B站分布式KV存储是随着业务需求不断迭代开发,功能需求逐渐覆盖到全公司各个业务线,性能和可靠性也是逐步完善优化的过程,在实践中需要对于稳态的标准不断更新和优化。
4.2 真实用户场景
为了更好的测试效果,测试环境必须尽可能的模拟真实环境,逻辑部署和架构图等比例的复制线上真实环境:
-
同等规格硬件配置,接入层容器部署,数据层物理机部署。
-
两套KV存储集群,模拟多机房部署物理隔离。
-
单套集群部署多region分区。
-
单个region分区中包含同等规模的raft group。
-
包含各种不同的存储引擎,适应社区,直播,游戏,账号等各种不同的业务应用
-
构建不同的负载模型和数据模型
4.3 基于真实线上场景设计和持续运行实验
以数据迁移场景来说明混沌实验场景设计的主要组成部分,通过将功能测试和异常测试中识别的真实用户场景用例和不同故障Monkey随机组合之后得到该场景的混沌实验并在测试环境中持续运行,发现尽可能的可靠性故障点,提升组内研发人员对于系统的信心。以下以存储的数据迁移场景为实例进行说明:
4.3.1 稳态指标
通过业务监控采集各种数据操作PUT/GET/DEL的操作时延,请求成功率,检测是否出现数据丢失,以及资源监控等,约定该实验场景下系统各个指标的阈值。对于存储系统来说还需要校验数据的持久性和一致性,以及内部任务和节点状态都可以通过监控采集以及脚本进行检查。
4.3.2 用户场景
用户场景的构造一方面需要模拟线上流量进行,可以使用线上请求的录制和重放或者使用测试自定义规则的流量,另外一个方面模拟各种任务以及运维操作,例如在迁移场景,需要覆盖集群的扩容和缩容,跨区域的迁移以及任务取消等场景并投放在实验过程中,此时完成了一个混沌实验所需要的一个真实的“稳态”用户场景建立。
– 背景流量封装以及检查请求返回值
-
单条业务请求PUT/GET/DEL
-
批量请求PUT/GET/DEL
– 封装数据迁移场景以及检查任务状态
-
本区域扩容
-
本区域缩容
-
跨区域迁移
-
跨区域迁移cancel
-
…
//封装业务请求PUT/GET/DEL并持续检查数据状态
go func() {
common.PutGetDelLoop(t, true, b.Client, 1000000, 300)
close(done1)
}()
//封装批量请求PUT/GET/DEL并持续检查数据状态
go func() {
common.PutGetDelBatch(t, true, b.Client)
close(done2)
}()
//封装用户场景:数据迁移并检查任务状态
resp := common.RebalanceTable(base.RemoteServer, common.REBALANCE_TABLE, Table, "0:50%,9:50")
assert.Contains(t, resp, "OK")
log.Info("Rabalance plan: %s", "0:50%,9:50")
4.3.3 故障注入封装
在系统故障注入方面,一方面需要在目标节点上安装agent操作各个存储节点,各种存储引擎的数据表对象,比如机器节点,数据表信息,另外一方面需要对各种故障类型Monkey进行封装,比如CPUMonkey,MemMonkey,副本Monkey等,并在实验构建ChaosTest中将实验对象和各种Monkey进行组合,可以采用固定顺序或者随机组合的方式持续运行该实验,实现实验的可重放和可复用。
-
各种故障类型封装并绑定实验对象
class Monkeys {
public:
Monkeys() {
// 定义目标节点
m_hosts.push_back("172.22.12.25:8000");
m_hosts.push_back("172.22.12.31:8000");
m_hosts.push_back("172.22.12.37:8000");
srand(time(0));
// 定义各种目标实验对象
m_tables.push_back("test_granite");
m_tables.push_back("test_quartz");
m_tables.push_back("test_pebble");
m_tables.push_back("test_marble_k16");
// ...
}
// 封装 CPU monkey 注入CPU类型异常
void cpu_monkey() {
std::string host = m_hosts[rand() % m_hosts.size()];
cpu_load(host);
LOG_INFO "CPU MONKEY:"
}
// 封装 mem monkey 注入内存类型异常
void mem_monkey() {
std::string host = m_hosts[rand() % m_hosts.size()];
mem_load(host);
LOG_INFO "MEM MONKEY:"
}
// 封装 replica monkey 注入副本丢失异常
void replica_monkey() {
std::string table = m_tables[rand() % m_tables.size()];
drop_replica(table);
LOG_INFO "Replica MONKEY:"
}
// 封装各种类型 monkey ...
}
-
定义混沌实验bind各个monkey并持续运行
class ChaosTest : public ::testing::Test {
protected:
ChaosTest() {
m_monkeyVec.push_back(std::bind(&Monkeys::cpu_monkey, &m_monkeys));
m_monkeyVec.push_back(std::bind(&Monkeys::mem_monkey, &m_monkeys));
m_monkeyVec.push_back(std::bind(&Monkeys::disk_monkey, &m_monkeys));
m_monkeyVec.push_back(std::bind(&Monkeys::network_monkey, &m_monkeys));
m_monkeyVec.push_back(std::bind(&Monkeys::kill_node_monkey, &m_monkeys));
m_monkeyVec.push_back(std::bind(&Monkeys::stop_node_monkey, &m_monkeys));
m_monkeyVec.push_back(std::bind(&Monkeys::stop_meta_monkey, &m_monkeys));
m_monkeyVec.push_back(std::bind(&Monkeys::replica_monkey, &m_monkeys));
...
4.4 实验结果的记录和分析
在实验开始前,实验中,实验后采集各种指标和数据,用于最后的数据分析并落地稳定性提升优化。并且通过混沌实验的无人值守和持久运行,持续的发现更多的概率性问题和优化系统稳定性。
-
实验运行系统运行日志监控
-
实验后服务功能用例回归验证
-
实验运行指标数据和监控数据通过Prometheus进行数据采集和持久化
-
实验看板Grafana实现可视化和告警
05 结果收益
在整套工程实践持续运行的近1年半时间中,拦截了多起场景叠加的严重系统问题,例如在大数据压力下叠加副本丢失场景会出现内部异步线程竞争造成raft节点异常,类似问题传统可靠性故障场景很难发现,同时相比传统分布式测试框架而言其投入产出比具有诸多优势:
-
相比传统存储测试框架维护成本低
-
可以覆盖更多的真实业务场景,避免复杂场景遗漏。
-
切合产品成熟度发展和迭代进度逐步发展优化演进。
-
增量开发和维护成本低,符合开闭原则,新增场景对于原实验无干扰。
06 标准化与服务化
6.1 标准化
2021年中国信通院发布混沌工程实践指南[6],可用于评估组织架构实践混沌工程实践的能力,反映混沌工程实践的可行性,有效性和安全性。混沌工程实验需要随着系统架构能力不断提升标准化建设。
6.2 服务化
混沌工程工具种类繁多,大部分故障注入工具已开源,如Chaos Blade和Chaos Mesh。但是不同公司的系统架构不一样,在实践中需要进一步集成和应用各种故障注入工具形成自己的服务化平台。
参考
[1] https://www.microsoft.com/en-us/research/wp-content/uploads/2016/04/paper-1.pdf
[2] https://github.com/p-org/PSharp
[3] https://github.com/jepsen-io/jepsen
[4] https://baike.baidu.com/item/混沌理论
[5] https://principlesofchaos.org/
[6] http://www.caict.ac.cn/kxyj/qwfb/ztbg/202112/P020211223588643401747.pdf
本文为从大数据到人工智能博主「xiaozhch5」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://lrting.top/backend/5948/