黄东旭解析 TiDB 的核心优势
408
2020-01-07
内容来源:http://mp.weixin.qq.com/s?__biz=MzI3NDIxNTQyOQ==&mid=2247490657&idx=1&sn=2895e6067e12c6085682272f21535d66&chksm=eb163b0bdc61b21d2466093bd005ea0e503e11aca9d63c70803e2ef2db9136d9b89d0a57d7da#rd
Shopee 于 2015 年底上线,是东南亚地区领先的电子商务平台,覆盖东南亚和台湾等多个市场,在深圳和新加坡分别设有研发中心。
Shopee 的数据库使用情况
Shopee 在用哪些数据库?
先说一下当前 Shopee 线上在用的几种数据库:
在 Shopee,我们只有两种关系数据库:MySQL 和 TiDB。目前大部分业务数据运行在 MySQL 上,TiDB 集群的比重过去一年来快速增长中。
Redis 在 Shopee 各个产品线使用广泛。从 DBA 的角度看,Redis 是关系数据库的一种重要补充。
数据库选型策略
分布式数据库选型参考指标
1TB:对于一个新数据库,我们会问:在未来一年到一年半时间里,数据库的体积会不会涨到 1TB?如果开发团队很确信新数据库一定会膨胀到 TB 级别,应该立即考虑 MySQL 分库分表方案或 TiDB 方案。
1000 万行或 10GB:单一 MySQL 表的记录条数不要超过 1000 万行,或单表磁盘空间占用不要超过 10GB。我们发现,超过这个阈值后,数据库性能和可维护性上往往也容易出问题(部分 SQL 难以优化,不易做表结构调整等)。如果确信单表体积会超越该上限,则应考虑 MySQL 分表方案;也可以采用 TiDB,TiDB 可实现水平弹性扩展,多数场景下可免去分表的烦恼。
每秒 1000 次写入:单个 MySQL 节点上的写入速率不要超过每秒 1000 次。大家可能觉得这个值太低了;许多开发同学也常举例反驳说,线上 MySQL 每秒写入几千几万次的实际案例比比皆是。我们为什么把指标定得如此之低呢?首先,上线前做估算往往有较大不确定性,正常状况下每秒写入 1000 次,大促等特殊场景下可能会陡然飙升到每秒 10000 次,作为设计指标保守一点比较安全。其次,我们允许开发团队在数据库中使用 Text 等大字段类型,当单行记录长度增大到一定程度后主库写入和从库复制性能都可能明显劣化,这种状况下对单节点写入速率不宜有太高期待。因此,如果一个项目上线前就预计到每秒写入速率会达到上万次甚至更高,则应该考虑 MySQL 分库分表方案或 TiDB 方案;同时,不妨根据具体业务场景看一下能否引入 Redis 或消息队列作为写缓冲,实现数据库写操作异步化。
P99 响应时间要求是 1 毫秒,10 毫秒还是 100 毫秒?应用程序要求 99% 的数据库查询都要在 1 毫秒内返回吗?如果是,则不建议直接读写数据库。可以考虑引入 Redis 等内存缓冲方案,前端直接面向 Redis 确保高速读写,后端异步写入数据库实现数据持久化。我们的经验是,多数场景下,MySQL 服务器、表结构设计、SQL 和程序代码等方面做过细致优化后,MySQL 有望做到 99% 以上查询都在 10 毫秒内返回。对于 TiDB,考虑到其存储计算分离和多组件协作实现 SQL 执行过程的特点,我们通常把预期值调高一个数量级到 100 毫秒级别。以线上某 TiDB 2.x 集群为例,上线半年以来多数时候 P99 都维持在 20 毫秒以内,偶尔会飙升到 200 毫秒,大促时抖动则更频繁一些。TiDB 执行 SQL 查询过程中,不同组件、不同节点之间的交互会多一些,自然要多花一点时间。
要不要分库分表?
内部的数据库设计评估清单里包含十几个项目,其中“要不要分库分表”是一个重要议题。在相当长时间里,MySQL 分库分表方案是我们实现数据库横向扩展的唯一选项;把 TiDB 引入 Shopee 后,我们多了一个“不分库分表”的选项。
从我们的经验来看,有几种场景下采用 MySQL 分库分表方案的副作用比较大,日常开发和运维都要付出额外的代价和成本。DBA 和开发团队需要在数据库选型阶段甄别出这些场景并对症下药。
难以准确预估容量的数据库。举例来讲,线上某日志数据库过去三个月的增量数据超过了之前三年多的存量体积。对于这类数据库,采用分库分表方案需要一次又一次做 Re-sharding,每一次都步骤繁琐,工程浩大。Shopee 的实践证明,TiDB 是较为理想的日志存储方案;当前,把日志类数据存入 TiDB 已经是内部较为普遍的做法了。
需要做多维度复杂查询的数据库。以订单数据库为例,各子系统都需要按照买家、卖家、订单状态、支付方式等诸多维度筛选数据。若以买家维度分库分表,则卖家维度的查询会变得困难;反之亦然。一方面,我们为最重要的查询维度分别建立了异构索引数据库;另一方面,我们也在 TiDB 上实现了订单汇总表,把散落于各个分片的订单数据汇入一张 TiDB 表,让一些需要扫描全量数据的复杂查询直接运行在 TiDB 汇总表上。
数据倾斜严重的数据库。诸如点赞和关注等偏社交类业务数据,按照用户维度分库分表后常出现数据分布不均匀的现象,少数分片的数据量可能远大于其他分片;这些大分片往往也是读写的热点,进而容易成为性能瓶颈。一种常用的解法是 Re-sharding,把数据分成更多片,尽量稀释每一片上的数据量和读写流量。最近我们也开始尝试把部分数据搬迁到 TiDB 上;理论上,如果 TiDB 表主键设计得高度分散,热点数据就有望均匀分布到全体 TiKV Region 上。
MySQL 在 Shopee 的使用情况
我们使用 Percona 分支,当前存储引擎以 InnoDB 为主。
一主多从是比较常见的部署结构。我们的应用程序比较依赖读写分离,线上数据库可能会有多达数十个从库。一套典型的数据库部署结构会分布在同城多个机房;其中会有至少一个节点放在备用机房,主要用于定时全量备份,也会提供给数据团队做数据拉取等用途。
如果应用程序需要读取 Binlog,从库上会安装一个名为 GDS(General DB Sync)的 Agent,实时解析 Binlog,并写入 Kafka。
应用程序透过 DNS 入口连接主库或从库。
我们自研的数据库中间件,支持简单的分库分表。何为“简单的分库分表”?只支持单一分库分表规则,可以按日期、Hash 或者某个字段的取值范围来分片;一条 SQL 最终只会被路由到单一分片上,不支持聚合或 Join 等操作。
如何解决 TB 级 MySQL 数据库的使用?
Redis 和关系型数据库在 Shopee 的的配合使用
先写缓存,再写数据库
比较常用的一种做法是:先写缓存,再写数据库。应用程序前端直接读写 Redis,后端匀速异步地把数据持久化到 MySQL 或 TiDB。这种做法一般被称之为“穿透式缓存”,其实是把关系数据库作为 Redis 数据的持久化存储层。如果一个系统在设计阶段即判明线上会有较高并发读写流量,把 Redis 放在数据库前面挡一下往往有效。
在 Shopee,一些偏社交类应用在大促时的峰值往往会比平时高出数十上百倍,是典型的“性能优先型应用”(Performance-critical Applications)。如果开发团队事先没有意识到这一点,按照常规做法让程序直接读写关系数据库,大促时不可避免会出现“一促就倒”的状况。其实,这类场景很适合借助 Redis 平缓后端数据库读写峰值。
如果 Redis 集群整体挂掉,怎么办?一般来说,有两个解决办法:
性能降级:应用程序改为直接读写数据库。性能上可能会打一个大的折扣,但是能保证大部分数据不丢。一些数据较为关键的业务可能会更倾向于采用这种方式。
数据降级:切换到一个空的 Redis 集群上以尽快恢复服务。后续可以选择从零开始慢慢积累数据,或者运行另一个程序从数据库加载部分旧数据到 Redis。一些并发高但允许数据丢失的业务可能会采用这种方式。
先写数据库,再写缓存
还有一种做法也很常见:先写数据库,再写缓存。应用程序正常读写数据库,Shopee 内部有一个中间件 DEC(Data Event Center)可以持续解析 Binlog,把结果重新组织后写入到 Redis。这样,一部分高频只读查询就可以直接打到 Redis上,大幅度降低关系数据库负载。
把数据写入 Redis 的时候,可以为特定的查询模式定制数据结构,一些不太适合用 SQL 实现的查询改为读 Redis 之后反而会更简洁高效。
此外,相较于“双写方式”(业务程序同时把数据写入关系数据库和 Redis),通过解析 Binlog 的方式在 Redis 上重建数据有明显好处:业务程序实现上较为简单,不必分心去关注数据库和 Redis 之间的数据同步逻辑。Binlog 方式的缺点在于写入延迟:新数据先写入 MySQL 主库,待其流入到 Redis 上,中间可能有大约数十毫秒延迟。实际使用上要论证业务是否能接受这种程度的延迟。
举例来讲,在新订单实时查询等业务场景中,我们常采用这种“先写数据库,再写缓存”的方式来消解 MySQL 主库上的高频度只读查询。为规避从库延迟带来的影响,部分关键订单字段的查询须打到 MySQL 主库上,大促时主库很可能就不堪重负。历次大促的实践证明,以这种方式引入 Redis 能有效缓解主库压力。
TiDB 在 Shopee 的使用情况
讲完 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 在 Shopee 的使用场景归纳为三类:
日志存储场景。
MySQL 分库分表数据聚合场景。
程序直接读写 TiDB 的场景。
从 MySQL 迁移到 TiDB:要适配,不要平移
把数据库从 MySQL 搬到 TiDB 的过程中,DBA 经常提醒开发同学:要适配,不要平移。关于这点,我们可以举一个案例来说明一下。
线上某系统最初采用 MySQL 分表方案,全量数据均分到 1000 张表;迁移到 TiDB 后我们去掉了分表,1000 张表合为了一张。应用程序上线后,发现某个 SQL 的性能抖动比较严重,并发高的时候甚至会导致整个 TiDB 集群卡住。分析后发现该 SQL 有两个特点:
该 SQL 查询频度极高,占了查询高峰时全部只读查询的 90%。
该 SQL 是一个较为复杂的扫表查询,不易通过添加索引方式优化。迁移到 TiDB 之前,MySQL 数据库分为 1000 张表,该 SQL 执行过程中只会扫描其中一张表,并且查询被分散到了多达二十几个从库上;即便如此,随着数据体积增长,当热数据明显超出内存尺寸后,MySQL 从库也变得不堪重负了。迁移到 TiDB 并把 1000 张表合为一张之后,该 SQL 被迫扫描全量数据,在 TiKV 和 SQL 节点之间会有大量中间结果集传送流量,性能自然不会好。
判明原因后,开发团队为应用程序引入了 Redis,把 Binlog 解析结果写入 Redis,并针对上述 SQL 查询定制了适当的数据结构。这些优化措施上线后,90% 只读查询从 TiDB 转移到了 Redis 上,查询变得更快、更稳定;TiDB 集群也得以削减数量可观的存储和计算节点。
TiDB 高度兼容 MySQL 语法的特点有助于降低数据库迁移的难度;但是,不要忘记它在实现上完全不同于 MySQL,很多时候我们需要根据 TiDB 的特质和具体业务场景定制出适配的方案。
总结
作者介绍:刘春辉,Shopee DBA,TiDB User Group Ambassador。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。