文章较长,建议收藏! 硬核深挖Shopee如何进行数据库选型…

Shopee于2015年底上线,是东南亚地区领先的电子商务平台,覆盖东南亚和台湾等多个市场,在深圳和新加坡分别设有研发中心。本文系Shopee的分布式数据库选型思路漫谈。因为是『漫谈』,可能不成体系,但会着重介绍一些经验以及踩过的坑,提供给大家参考。

Shopee于2015年底上线,是东南亚地区领先的电子商务平台,覆盖东南亚和台湾等多个市场,在深圳和新加坡分别设有研发中心。本文系Shopee的分布式数据库选型思路漫谈。因为是『漫谈』,可能不成体系,但会着重介绍一些经验以及踩过的坑,提供给大家参考。

摄图网_500478552.jpg

1 Shopee的数据库使用情况

  Shopee在用哪些数据库?

  先说一下当前Shopee线上在用的几种数据库:

  在Shopee,我们只有两种关系数据库:MySQL和TiDB。目前大部分业务数据运行在MySQL上,TiDB集群的比重过去一年来快速增长中。

  Redis在Shopee各个产品线使用广泛。从DBA的角度看,Redis是关系数据库的一种重要补充。

  内部也在使用诸如HBase和Pika等多种NoSQL数据库,使用范围多限于特定业务和团队。不在本次讨论范围内。

  数据库选型策略

Sp1-20200113.jpg

  过去的一年里,我们明显感觉到数据库选型在DBA日常工作中的占比越来越重了。随着业务快速成长,DBA每周需要创建的新数据库较之一两年前可能增加了十倍不止。我们每年都会统计几次线上逻辑数据库个数。上图展示了过去几年间这个数字的增长趋势(纵轴表示逻辑数据库个数,我们把具体数字隐去了)。

  历史数据显示,逻辑数据库个数每年都有三到五倍的增长,过去的2019年增长倍数甚至更高。每个新数据库上线前,DBA和开发团队都需要做一些评估以快速决定物理设计和逻辑设计。经验表明,一旦在设计阶段做出了不当决定,后期需要付出较多时间和人力成本来补救。因此,我们需要制定一些简洁高效的数据库选型策略,确保我们大多数时候都能做出正确选择。

  我们的数据库选型策略可以概括为三点:

  默认使用MySQL。

  积极尝试TiDB。

  在必要的时候引入Redis用于消解部分关系数据库高并发读写流量。

  在使用MySQL的过程中我们发现,当单数据库体量达到TB级别,开发和运维的复杂度会被指数级推高。DBA日常工作会把消除TB级MySQL数据库实例排在高优先级。

  “积极尝试TiDB”不是一句空话。2018年初开始,我们把TiDB引入了到Shopee。过去两年间TiDB在Shopee从无到有,集群节点数和数据体积已经达到了可观的规模。对于一些经过了验证的业务场景,DBA会积极推动业务团队采用TiDB,让开发团队有机会获得第一手经验;目前,内部多数业务开发团队都在线上实际使用过一次或者多次TiDB。

  关于借助Redis消解关系数据库高并发读写流量,后面会展开讲一下我们的做法。

  分布式数据库选型参考指标

Sp2-2020011302315788979556825.jpg

  在制定了数据库选型策略之后,我们在选型中还有几个常用的参考指标:

  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分库分表方案在解决了主要的数据库横向扩展问题的同时,也导致了一些开发和运维方面的痛点。一方面,我们努力在MySQL分库分表框架内解决和缓解各种问题;另一方面,我们也尝试基于TiDB构建“不分库分表”的新型解决方案,并取得了一些进展。

2 MySQL在Shopee的使用情况

Sp3-2020011304215788979887753.jpg

  Shopee的母公司SEA Group成立于2009年。我们从一开始就使用MySQL作为主力数据库,从早期的MySQL 5.1逐渐进化到现在的MySQL 5.7,我们已经用了十年MySQL。

  我们使用Percona分支,当前存储引擎以InnoDB为主。

  一主多从是比较常见的部署结构。我们的应用程序比较依赖读写分离,线上数据库可能会有多达数十个从库。一套典型的数据库部署结构会分布在同城多个机房;其中会有至少一个节点放在备用机房,主要用于定时全量备份,也会提供给数据团队做数据拉取等用途。

  如果应用程序需要读取Binlog,从库上会安装一个名为GDS(General DB Sync)的Agent,实时解析Binlog,并写入Kafka。

  应用程序透过DNS入口连接主库或从库。

  我们自研的数据库中间件,支持简单的分库分表。何为“简单的分库分表”?只支持单一分库分表规则,可以按日期、Hash或者某个字段的取值范围来分片;一条SQL最终只会被路由到单一分片上,不支持聚合或Join等操作。

  如何解决TB级MySQL数据库的使用?

Sp4-2020011307015788980059684.jpg

  根据我们的统计,Shopee线上数据库中80% 都低于50GB;此外,还有2.5% 的数据库体积超过1TB。上图列出了部分TB级别数据库的一个统计结果:平均体积是2TB,最大的甚至超过4TB。

  采用MySQL分库分表方案和迁移到TiDB是我们削减TB级MySQL数据库实例个数的两种主要途径。除此之外,还有一些办法能帮助我们对抗MySQL数据库体积膨胀带来的负面效应。

  旧数据归档。很多旧数据库占据了大量磁盘空间,读写却不频繁。换言之,这些旧数据很可能不是『热数据』。如果业务上许可,我们通常会把旧数据归档到单独的MySQL实例上。当然,应用程序需要把读写这些数据的流量改到新实例。新实例可以按年或按月把旧数据存入不同的表以避免单表体积过大,还可以开启InnoDB透明页压缩以减少磁盘空间占用。TiDB是非常理想的数据归档选项:理论上,一个TiDB集群的容量可以无限扩展,不必担心磁盘空间不够用;TiDB在计算层和存储层皆可水平弹性扩展,我们得以根据数据体积和读写流量的实际增长循序渐进地增加服务器,使整个集群的硬件使用效率保持在较为理想的水平。

  硬件升级(Scale-up)。如果MySQL数据体积涨到了1TB,磁盘空间开始吃紧,是不是可以先把磁盘空间加倍,内存也加大一些,为开发团队争取多一些时间实现数据库横向扩展方案?有些数据库体积到了TB级别,但业务上可能不太容易分库分表。如果开发团队能够通过数据归档等手段使数据体积保持在一个较为稳定(但仍然是TB级别)的水准,那么适当做一下硬件升级也有助于改善服务质量。

3 Redis和关系型数据库在Shopee的的配合使用

  前文中我们提到,使用Redis来解决关系数据库高并发读写流量的问题,下面我们就来讲讲具体的做法。

  先写缓存,再写数据库

Sp5-2020011338715788980255304.jpg

  比较常用的一种做法是:先写缓存,再写数据库。应用程序前端直接读写Redis,后端匀速异步地把数据持久化到MySQL或TiDB。这种做法一般被称之为“穿透式缓存”,其实是把关系数据库作为Redis数据的持久化存储层。如果一个系统在设计阶段即判明线上会有较高并发读写流量,把Redis放在数据库前面挡一下往往有效。

  在Shopee,一些偏社交类应用在大促时的峰值往往会比平时高出数十上百倍,是典型的“性能优先型应用”(Performance-critical Applications)。如果开发团队事先没有意识到这一点,按照常规做法让程序直接读写关系数据库,大促时不可避免会出现“一促就倒”的状况。其实,这类场景很适合借助Redis平缓后端数据库读写峰值。

  如果Redis集群整体挂掉,怎么办?一般来说,有两个解决办法:

  性能降级:应用程序改为直接读写数据库。性能上可能会打一个大的折扣,但是能保证大部分数据不丢。一些数据较为关键的业务可能会更倾向于采用这种方式。

  数据降级:切换到一个空的Redis集群上以尽快恢复服务。后续可以选择从零开始慢慢积累数据,或者运行另一个程序从数据库加载部分旧数据到Redis。一些并发高但允许数据丢失的业务可能会采用这种方式。

  先写数据库,再写缓存

Sp6-2020011331915788980375371.jpg

  还有一种做法也很常见:先写数据库,再写缓存。应用程序正常读写数据库,Shopee内部有一个中间件DEC(Data Event Center)可以持续解析Binlog,把结果重新组织后写入到Redis。这样,一部分高频只读查询就可以直接打到Redis上,大幅度降低关系数据库负载。

  把数据写入Redis的时候,可以为特定的查询模式定制数据结构,一些不太适合用SQL实现的查询改为读Redis之后反而会更简洁高效。

  此外,相较于“双写方式”(业务程序同时把数据写入关系数据库和Redis),通过解析Binlog的方式在Redis上重建数据有明显好处:业务程序实现上较为简单,不必分心去关注数据库和Redis之间的数据同步逻辑。Binlog方式的缺点在于写入延迟:新数据先写入MySQL主库,待其流入到Redis上,中间可能有大约数十毫秒延迟。实际使用上要论证业务是否能接受这种程度的延迟。

  举例来讲,在新订单实时查询等业务场景中,我们常采用这种“先写数据库,再写缓存”的方式来消解MySQL主库上的高频度只读查询。为规避从库延迟带来的影响,部分关键订单字段的查询须打到MySQL主库上,大促时主库很可能就不堪重负。历次大促的实践证明,以这种方式引入Redis能有效缓解主库压力。

4 TiDB在Shopee的使用情况

  讲完MySQL和Redis,我们来接着讲讲TiDB。

Sp7-2020011389615788980527992.jpg

  我们从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,希望能探索出更多应用场景。

  集群概况

Sp8-2020011324515788980661703.jpg

  目前Shopee线上部署了二十多个TiDB集群,约有400多个节点。版本以TiDB 2.1为主,部分集群已经开始试水TiDB 3.0。我们最大的一个集群数据量约有30TB,超过40个节点。到目前为止,用户、商品和订单等电商核心子系统都或多或少把一部分数据和流量放在了TiDB上。

  TiDB在Shopee的使用场景

  我们把TiDB在Shopee的使用场景归纳为三类:

  日志存储场景

  MySQL分库分表数据聚合场景

  程序直接读写TiDB的场景

Sp9-2020011341415788980860945.jpg

  第一种使用场景是日志存储。前面讲到过,我们接触TiDB的第一年里上线的集群以日志类存储为主。通常的做法是:前端先把日志数据写入到Kafka,后端另一个程序负责把Kafka里的数据异步写入TiDB。由于不用考虑分库分表,运营后台类业务可以方便地读取TiDB里的日志数据。对于DBA而言,可以根据需要线性增加存储节点和计算节点,运维起来也较MySQL分库分表简单。

Sp10-2020011318315788980985762.jpg

  第二种使用场景是MySQL分库分表数据聚合。Shopee的订单表和商品表存在MySQL上,并做了细致的数据分片。为了方便其他子系统读取订单和商品数据,我们做了一层数据聚合:借助前面提到的DEC解析MySQL Binlog,把多个MySQL分片的数据聚合到单一TiDB汇总表。这样,类似BI系统这样的旁路系统就不必关注分库分表规则,直接读取TiDB数据即可。除此之外,订单和商品子系统也可以在TiDB汇总表上运行一些复杂的SQL查询,省去了先在每个MySQL分片上查一次最后再汇总一次的麻烦。

Sp11-2020011324615788981109094.jpg

  第三种就是程序直接读写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:要适配,不要平移

Sp12-2020011366915788981225034.jpg

  把数据库从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的特质和具体业务场景定制出适配的方案。

5总结

  本文回顾了Shopee在关系数据库选型方面的思路,也附带简单介绍了一些我们在MySQL、TiDB和Redis使用方面的心得,希望能为大家提供一点借鉴。

  简单来说,如果数据量比较小,业务处于早期探索阶段,使用MySQL仍然是一个很好的选择。Shopee的经验是不用过早的为分库分表妥协设计,因为当业务开始增长,数据量开始变大的时候,可以从MySQL平滑迁移到TiDB,获得扩展性的同时也不用牺牲业务开发的灵活性。另一方面,Redis可以作为关系型数据库的很好的补充,用来加速查询,缓解数据库压力,使得数据库能够更关注吞吐以及强一致场景。

(来源:微信公众号InfoQ  作者: 刘春辉)
1
收藏0
行业动态跨境微聊新政/功能
评论0分享至
参与评论
后参与评论
暂无数据

简介: 作者很懒,还未填写简介