Shopee 于 2015 年底上线,是东南亚地区领先的电子商务平台,覆盖东南亚和中国台湾等多个市场,在深圳和新加坡分别设有研发中心。 本文系 Shopee 的分布式数据库选型思路漫谈。因为是『漫谈』,可能不成体系,但会着重介绍一些经验以及踩过的坑,提供给大家参考。
先说一下当前 Shopee 线上在用的几种数据库:
过去的一年里,我们明显感觉到数据库选型在 DBA 日常工作中的占比越来越重了。随着业务快速成长,DBA 每周需要创建的新数据库较之一两年前可能增加了十倍不止。我们每年都会统计几次线上逻辑数据库个数。上图展示了过去几年间这个数字的增长趋势(纵轴表示逻辑数据库个数,我们把具体数字隐去了)。
历史数据显示,逻辑数据库个数每年都有三到五倍的增长,过去的 2019 年增长倍数甚至更高。每个新数据库上线前,DBA 和开发团队都需要做一些评估以快速决定物理设计和逻辑设计。经验表明,一旦在设计阶段做出了不当决定,后期需要付出较多时间和人力成本来补救。因此,我们需要制定一些简洁高效的数据库选型策略,确保我们大多数时候都能做出正确选择。
我们的数据库选型策略可以概括为三点:
在使用 MySQL 的过程中我们发现,当单数据库体量达到 TB 级别,开发和运维的复杂度会被指数级推高。DBA 日常工作会把消除 TB 级 MySQL 数据库实例排在高优先级。
“积极尝试 TiDB”不是一句空话。2018 年初开始,我们把 TiDB 引入了到 Shopee。过去两年间 TiDB 在 Shopee 从无到有,集群节点数和数据体积已经达到了可观的规模。对于一些经过了验证的业务场景,DBA 会积极推动业务团队采用 TiDB,让开发团队有机会获得第一手经验;目前,内部多数业务开发团队都在线上实际使用过一次或者多次 TiDB。
关于借助 Redis 消解关系数据库高并发读写流量,后面会展开讲一下我们的做法。
在制定了数据库选型策略之后,我们在选型中还有几个常用的参考指标:
内部的数据库设计评估清单里包含十几个项目,其中“要不要分库分表”是一个重要议题。在相当长时间里,MySQL 分库分表方案是我们实现数据库横向扩展的唯一选项;把 TiDB 引入 Shopee 后,我们多了一个“不分库分表”的选项。
从我们的经验来看,有几种场景下采用 MySQL 分库分表方案的副作用比较大,日常开发和运维都要付出额外的代价和成本。DBA和开发团队需要在数据库选型阶段甄别出这些场景并对症下药。
总体来说,MySQL 分库分表方案在解决了主要的数据库横向扩展问题的同时,也导致了一些开发和运维方面的痛点。一方面,我们努力在 MySQL 分库分表框架内解决和缓解各种问题;另一方面,我们也尝试基于 TiDB 构建“不分库分表”的新型解决方案,并取得了一些进展。
Shopee 的母公司 SEA Group 成立于 2009 年。我们从一开始就使用 MySQL 作为主力数据库,从早期的MySQL 5.1 逐渐进化到现在的 MySQL 5.7,我们已经用了十年 MySQL。
根据我们的统计,Shopee 线上数据库中 80% 都低于 50GB;此外,还有 2.5% 的数据库体积超过 1TB。上图列出了部分 TB 级别数据库的一个统计结果:平均体积是 2TB,最大的甚至超过 4TB。
采用MySQL 分库分表方案和迁移到 TiDB 是我们削减 TB 级MySQL数据库实例个数的两种主要途径。除此之外,还有一些办法能帮助我们对抗 MySQL 数据库体积膨胀带来的负面效应。
前文中我们提到,使用Redis来解决关系数据库高并发读写流量的问题,下面我们就来讲讲具体的做法。
比较常用的一种做法是:先写缓存,再写数据库。应用程序前端直接读写 Redis,后端匀速异步地把数据持久化到 MySQL 或 TiDB。这种做法一般被称之为“穿透式缓存”,其实是把关系数据库作为 Redis 数据的持久化存储层。如果一个系统在设计阶段即判明线上会有较高并发读写流量,把 Redis 放在数据库前面挡一下往往有效。
在 Shopee,一些偏社交类应用在大促时的峰值往往会比平时高出数十上百倍,是典型的“性能优先型应用”(Performance-critical Applications)。如果开发团队事先没有意识到这一点,按照常规做法让程序直接读写关系数据库,大促时不可避免会出现“一促就倒”的状况。其实,这类场景很适合借助 Redis 平缓后端数据库读写峰值。
如果 Redis 集群整体挂掉,怎么办?一般来说,有两个解决办法:
还有一种做法也很常见:先写数据库,再写缓存。应用程序正常读写数据库,Shopee 内部有一个中间件 DEC(Data Event Center)可以持续解析 Binlog,把结果重新组织后写入到 Redis。这样,一部分高频只读查询就可以直接打到 Redis上,大幅度降低关系数据库负载。
把数据写入 Redis 的时候,可以为特定的查询模式定制数据结构,一些不太适合用 SQL 实现的查询改为读 Redis 之后反而会更简洁高效。
此外,相较于“双写方式”(业务程序同时把数据写入关系数据库和 Redis),通过解析 Binlog 的方式在 Redis 上重建数据有明显好处:业务程序实现上较为简单,不必分心去关注数据库和 Redis 之间的数据同步逻辑。Binlog 方式的缺点在于写入延迟:新数据先写入 MySQL 主库,待其流入到 Redis 上,中间可能有大约数十毫秒延迟。实际使用上要论证业务是否能接受这种程度的延迟。
举例来讲,在新订单实时查询等业务场景中,我们常采用这种“先写数据库,再写缓存”的方式来消解 MySQL 主库上的高频度只读查询。为规避从库延迟带来的影响,部分关键订单字段的查询须打到 MySQL 主库上,大促时主库很可能就不堪重负。历次大促的实践证明,以这种方式引入 Redis 能有效缓解主库压力。
讲完MySQL和Redis,我们来接着讲讲TiDB。
我们从 2018 年初开始调研 TiDB,到 2018 年 6 月份上线了第一个 TiDB 集群(风控日志集群,版本 1.0.8)。2018 年 10 月份,我们把一个核心审计日志库迁到了 TiDB上,目前该集群数据量约 7TB,日常 QPS 约为 10K ~ 15K。总体而言,2018 年上线的集群以日志类存储为主。
2019 年开始我们尝试把一些较为核心的线上系统迁移到 TiDB 上。3 月份为买家和卖家提供聊天服务的 Chat 系统部分数据从 MySQL 迁移到了 TiDB 。最近的大促中,峰值 QPS 约为 30K,运行平稳。今年也有一些新功能选择直接基于 TiDB 做开发,比如店铺标签、直播弹幕和选品服务等。这些新模块的数据量和查询量都还比较小,有待持续观察验证。
TiDB 3.0 GA 后,新的 Titan (https://github.com/tikv/titan) 存储引擎吸引了我们。 在Shopee,我们允许 MySQL 表设计中使用 Text 等大字段类型,通常存储一些半结构化数据。但是,从 MySQL 迁移到 TiDB 的过程中,大字段却可能成为绊脚石。一般而言,TiDB 单行数据尺寸不宜超过 64KB,越小越好;换言之,字段越大,性能越差。Titan 存储引擎有望提高大字段的读写性能。目前,我们已经着手把一些数据迁移到 TiKV 上,并打开了 Titan,希望能探索出更多应用场景。
目前 Shopee 线上部署了二十多个 TiDB 集群,约有 400 多个节点。版本以 TiDB 2.1 为主,部分集群已经开始试水 TiDB 3.0。我们最大的一个集群数据量约有 30TB,超过 40 个节点。到目前为止,用户、商品和订单等电商核心子系统都或多或少把一部分数据和流量放在了 TiDB 上。
我们把 TiDB在 Shopee 的使用场景归纳为三类:
第一种使用场景是日志存储。前面讲到过,我们接触 TiDB 的第一年里上线的集群以日志类存储为主。通常的做法是:前端先把日志数据写入到 Kafka,后端另一个程序负责把 Kafka 里的数据异步写入 TiDB。由于不用考虑分库分表,运营后台类业务可以方便地读取 TiDB 里的日志数据。对于 DBA 而言,可以根据需要线性增加存储节点和计算节点,运维起来也较MySQL分库分表简单。
第二种使用场景是 MySQL 分库分表数据聚合。Shopee 的订单表和商品表存在 MySQL上,并做了细致的数据分片。为了方便其他子系统读取订单和商品数据,我们做了一层数据聚合: 借助前面提到的 DEC 解析 MySQL Binlog,把多个 MySQL 分片的数据聚合到单一 TiDB 汇总表。这样,类似BI系统这样的旁路系统就不必关注分库分表规则,直接读取 TiDB 数据即可。除此之外,订单和商品子系统也可以在 TiDB 汇总表上运行一些复杂的 SQL 查询,省去了先在每个 MySQL 分片上查一次最后再汇总一次的麻烦。
第三种就是程序直接读写 TiDB。像前面提到的 Chat 系统,舍弃了 MySQL,改为直接读写 TiDB。优势体现在两个方面:不必做分库分表,应用程序的实现相对简单、直接;TiDB 理论上容量无限大,且方便线性扩展,运维起来更容易。
前面提到过,在 Shopee 内部使用GDS(General DB Sync)实时解析 MySQL Binlog,并写入 Kafka 提供给有需要的客户端消费。TiDB 上也可以接一个 Binlog 组件,把数据变化持续同步到 Kafka 上。需要读取 Binlog 的应用程序只要适配了 TiDB Binlog 数据格式,就可以像消费 MySQL Binlog 一样消费 TiDB Binlog 了。
把数据库从 MySQL 搬到 TiDB 的过程中,DBA 经常提醒开发同学:要适配,不要平移。关于这点,我们可以举一个案例来说明一下。
线上某系统最初采用 MySQL 分表方案,全量数据均分到 1000 张表; 迁移到 TiDB 后我们去掉了分表,1000 张表合为了一张。应用程序上线后,发现某个 SQL 的性能抖动比较严重,并发高的时候甚至会导致整个 TiDB 集群卡住。分析后发现该 SQL 有两个特点:
判明原因后,开发团队为应用程序引入了 Redis,把 Binlog 解析结果写入 Redis,并针对上述 SQL 查询定制了适当的数据结构。这些优化措施上线后,90% 只读查询从 TiDB 转移到了 Redis 上,查询变得更快、更稳定;TiDB 集群也得以削减数量可观的存储和计算节点。
TiDB 高度兼容 MySQL 语法的特点有助于降低数据库迁移的难度;但是,不要忘记它在实现上完全不同于 MySQL,很多时候我们需要根据 TiDB 的特质和具体业务场景定制出适配的方案。
本文回顾了 Shopee 在关系数据库选型方面的思路,也附带简单介绍了一些我们在MySQL、TiDB 和 Redis 使用方面的心得,希望能为大家提供一点借鉴。
简单来说,如果数据量比较小,业务处于早期探索阶段,使用 MySQL 仍然是一个很好的选择。 Shopee 的经验是不用过早的为分库分表妥协设计,因为当业务开始增长,数据量开始变大的时候,可以从MySQL平滑迁移到TiDB,获得扩展性的同时也不用牺牲业务开发的灵活性。另一方面,Redis 可以作为关系型数据库的很好的补充,用来加速查询,缓解数据库压力,使得数据库能够更关注吞吐以及强一致场景。
作者介绍:
刘春辉,Shopee DBA,TiDB User Group Ambassador
领取专属 10元无门槛券
私享最新 技术干货