写点什么

架构演进实践: 从 0 到 4000 高并发请求背后的努力!

  • 2020-06-20
  • 本文字数:4501 字

    阅读完需:约 15 分钟

架构演进实践:从0到4000高并发请求背后的努力!

来自:即时通讯网


http://www.52im.net/thread-2141-1-1.html


达达创立于 2014 年 5 月,业务覆盖全国 37 个城市,拥有 130 万注册众包配送员,日均配送百万单,是全国领先的最后三公里物流配送平台。达达的业务模式与滴滴以及 Uber 很相似,以众包的方式利用社会闲散人力资源,解决 O2O 最后三公里即时性配送难题(2016 年 4 月,达达已经与京东到家合并)。


达达的业务组成简单直接——商家下单、配送员接单和配送,也正因为理解起来简单,使得达达的业务量在短时间能实现爆发式增长。而支撑业务快速增长的背后,正是达达技术团队持续不断的快速技术迭代的结果,本文正好借此机会,总结并分享了这一系列技术演进的第一手实践资料,希望能给同样奋斗在互联网创业一线的你带来启发。

技术背景

达达业务主要包含两部分:


  • 商家发单;

  • 配送员接单配送;


达达的业务逻辑看起来非常简单直接,如下图所示:



达达的业务规模增长极大,在 1 年左右的时间从零增长到每天近百万单,给后端带来极大的访问压力。压力主要分为两类:读压力、写压力。读压力来源于配送员在 APP 中抢单,高频刷新查询周围的订单,每天访问量几亿次,高峰期 QPS 高达数千次/秒。写压力来源于商家发单、达达接单、取货、完成等操作。达达业务读的压力远大于写压力,读请求量约是写请求量的 30 倍以上。


下图是达达在成长初期,每天的访问量变化趋图,可见增长极快:



下图是达达在成长初期,高峰期请求 QPS 的变化趋势图,可见增长极快:



极速增长的业务,对技术的要求越来越高,我们必须在架构上做好充分的准备,才能迎接业务的挑战。接下来,我们一起看看达达的后台架构是如何演化的。

最初的技术架构:简单直接

作为创业公司,最重要的一点是敏捷,快速实现产品,对外提供服务,于是我们选择了公有云服务,保证快速实施和可扩展性,节省了自建机房等时间。在技术选型上,为快速的响应业务需求,业务系统使用 Python 做为开发语言,数据库使用 MySQL。


如下图所示,应用层的几大系统都访问一个数据库:


中期架构优化:读写分离

数据库瓶颈越来越严重

随着业务的发展,访问量的极速增长,上述的方案很快不能满足性能需求:每次请求的响应时间越来越长,比如配送员在 app 中刷新周围订单,响应时间从最初的 500 毫秒增加到了 2 秒以上。业务高峰期,系统甚至出现过宕机,一些商家和配送员甚至因此而怀疑我们的服务质量。在这生死存亡的关键时刻,通过监控,我们发现高期峰 MySQL CPU 使用率已接近 80%,磁盘 IO 使用率接近 90%,Slow Query 从每天 1 百条上升到 1 万条,而且一天比一天严重。数据库俨然已成为瓶颈,我们必须得快速做架构升级。


如下是数据库一周的 qps 变化图,可见数据库压力的增长极快:


我们的读写分离方案

当 Web 应用服务出现性能瓶颈的时候,由于服务本身无状态(stateless),我们可以通过加机器的水平扩展方式来解决。而数据库显然无法通过简单的添加机器来实现扩展,因此我们采取了 MySQL 主从同步和应用服务端读写分离的方案。


MySQL 支持主从同步,实时将主库的数据增量复制到从库,而且一个主库可以连接多个从库同步。


利用 MySQL 的此特性,我们在应用服务端对每次请求做读写判断:


  • 若是写请求,则把这次请求内的所有 DB 操作发向主库;

  • 若是读请求,则把这次请求内的所有 DB 操作发向从库。



实现读写分离后,数据库的压力减少了许多,CPU 使用率和 IO 使用率都降到了 5%内,Slow Query 也趋近于 0。


主从同步、读写分离给我们主要带来如下两个好处:


  • 减轻了主库(写)压力:达达的业务主要来源于读操作,做读写分离后,读压力转移到了从库,主库的压力减小了数十倍;

  • 从库(读)可水平扩展(加从库机器):因系统压力主要是读请求,而从库又可水平扩展,当从库压力太时,可直接添加从库机器,缓解读请求压力。


如下是优化后数据库 QPS 的变化图:


读写分离前主库的 select QPS



读写分离后主库的 select QPS


新状况出现:主从延迟问题

当然,没有一个方案是万能的。


读写分离,暂时解决了 MySQL 压力问题,同时也带来了新的挑战:


  • 业务高峰期,商家发完订单,在我的订单列表中却看不到当发的订单(典型的 read after write);

  • 系统内部偶尔也会出现一些查询不到数据的异常。


通过监控,我们发现,业务高峰期 MySQL 可能会出现主从延迟,极端情况,主从延迟高达 10 秒。


那如何监控主从同步状态?在从库机器上,执行 show slave status,查看 Seconds_Behind_Master 值,代表主从同步从库落后主库的时间,单位为秒,若同从同步无延迟,这个值为 0。MySQL 主从延迟一个重要的原因之一是主从复制是单线程串行执行。


那如何为避免或解决主从延迟?我们做了如下一些优化:


  • 优化 MySQL 参数,比如增大 innodb_buffer_pool_size,让更多操作在 MySQL 内存中完成,减少磁盘操作;

  • 使用高性能 CPU 主机;

  • 数据库使用物理主机,避免使用虚拟云主机,提升 IO 性能;

  • 使用 SSD 磁盘,提升 IO 性能。SSD 的随机 IO 性能约是 SATA 硬盘的 10 倍;

  • 业务代码优化,将实时性要求高的某些操作,使用主库做读操作。

主库的写操作变的越来越慢

读写分离很好的解决读压力问题,每次读压力增加,可以通过加从库的方式水平扩展。但是写操作的压力随着业务爆发式的增长没有很有效的缓解办法,比如商家发单起来越慢,严重影响了商家的使用体验。我们监控发现,数据库写操作越来越慢,一次普通的 insert 操作,甚至可能会执行 1 秒以上。



可见磁盘 IO 使用率已经非常高,高峰期 IO 响应时间最大达到 636 毫秒,IO 使用率最高达到 100%


同时,业务越来越复杂,多个应用系统使用同一个数据库,其中一个很小的非核心功能出现 Slow query,常常影响主库上的其它核心业务功能。


我们有一个应用系统在 MySQL 中记录日志,日志量非常大,近 1 亿行记录,而这张表的 ID 是 UUID,某一天高峰期,整个系统突然变慢,进而引发了宕机。监控发现,这张表 insert 极慢,拖慢了整个 MySQL Master,进而拖跨了整个系统。(当然在 MySQL 中记日志不是一种好的设计,因此我们开发了大数据日志系统。另一方面,UUID 做主键是个糟糕的选择,在下文的水平分库中,针对 ID 的生成,有更深入的讲述)。

进一步对主库进行拆分,优化主库写操作慢的问题

这时,主库成为了性能瓶颈,我们意识到,必需得再一次做架构升级,将主库做拆分:


  • 一方面以提升性能;

  • 另一方面减少系统间的相互影响,以提升系统稳定性;


这一次,我们将系统按业务进行了垂直拆分。


如下图所示,将最初庞大的数据库按业务拆分成不同的业务数据库,每个系统仅访问对应业务的数据库,避免或减少跨库访问:



下图是垂直拆分后,数据库主库的压力,可见磁盘 IO 使用率已降低了许多,高峰期 IO 响应时间在 2.33 毫秒内,IO 使用率最高只到 22.8%:



未来是美好的,道路是曲折的。


垂直分库过程,也遇到不少挑战,最大的挑战是: 不能跨库 join,同时需要对现有代码重构 。单库时,可以简单的使用 join 关联表查询;拆库后,拆分后的数据库在不同的实例上,就不能跨库使用 join 了。


比如在 CRM 系统中,需要通过商家名查询某个商家的所有订单,在垂直分库前,可以 join 商家和订单表做查询,如下如示:



分库后,则要重构代码,先通过商家名查询商家 id,再通过商家 Id 查询订单表,如下所示:



垂直分库过程中的经验教训,使我们制定了 SQL 最佳实践,其中一条便是 程序中禁用或少用 join,而应该在程序中组装数据,让 SQL 更简单 。一方面为以后进一步垂直拆分业务做准备,另一方面也避免了 MySQL 中 join 的性能较低的问题。


经过一个星期紧锣密鼓的底层架构调整,以及业务代码重构,终于完成了数据库的垂直拆分。拆分之后,每个应用程序只访问对应的数据库,一方面将单点数据库拆分成了多个,分摊了主库写压力;另一方面,拆分后的数据库各自独立,实现了业务隔离,不再互相影响。

为未来做准备,进一步升级架构:水平分库(sharding)

通过上一节的分享,我们知道:


  • 读写分离,通过从库水平扩展,解决了读压力;

  • 垂直分库通过按业务拆分主库,缓存了写压力。


但技术团队是否就此高枕无忧?答案是:NO。


上述架构依然存在以下隐患:


  • 单表数据量越来越大:如订单表,单表记录数很快将过亿,超出 MySQL 的极限,影响读写性能;

  • 核心业务库的写压力越来越大:已不能再进一次垂直拆分,MySQL 主库不具备水平扩展的能力;


以前,系统压力逼迫我们架构升级,这一次,我们需提前做好架构升级,实现数据库的水平扩展(sharding)。我们的业务类似于 Uber,而 Uber 在公司成立的 5 年后(2014)年才实施了水平分库,但我们的业务发展要求我们在成立 18 月就要开始实施水平分库。



水平分库面临的第一个问题是,按什么逻辑进行拆分:


  • 一种方案是按城市拆分,一个城市的所有数据在一个数据库中;

  • 另一种方案是按订单 ID 平均拆分数据;


按城市拆分的优点是数据聚合度比较高,做聚合查询比较简单,实现也相对简单,缺点是数据分布不均匀,某些城市的数据量极大,产生热点,而这些热点以后可能还要被迫再次拆分。


按订单 ID 拆分则正相反,优点是数据分布均匀,不会出现一个数据库数据极大或极小的情况,缺点是数据太分散,不利于做聚合查询。比如,按订单 ID 拆分后,一个商家的订单可能分布在不同的数据库中,查询一个商家的所有订单,可能需要查询多个数据库。针对这种情况,一种解决方案是将需要聚合查询的数据做冗余表,冗余的表不做拆分,同时在业务开发过程中,减少聚合查询。


反复权衡利弊,并参考了 Uber 等公司的分库方案后,我们最后决定按订单 ID 做水平分库


从架构上,我们将系统分为三层:


  • 应用层:即各类业务应用系统;

  • 数据访问层:统一的数据访问接口,对上层应用层屏蔽读写分库、分库、缓存等技术细节;

  • 数据层:对 DB 数据进行分片,并可动态的添加 shard 分片。


水平分库的技术关键点在于数据访问层的设计。


数据访问层主要包含三部分:


  • ID 生成器:生成每张表的主键;

  • 数据源路由:将每次 DB 操作路由到不同的 shard 数据源上;

  • 缓存:采用 Redis 实现数据的缓存,提升性能。


ID 生成器是整个水平分库的核心,它决定了如何拆分数据,以及查询存储-检索数据:


  • ID 需要跨库全局唯一,否则会引发业务层的冲突;

  • 此外,ID 必须是数字且升序,这主要是考虑到升序的 ID 能保证 MySQL 的性能;

  • 同时,ID 生成器必须非常稳定,因为任何故障都会影响所有的数据库操作;


我们的 ID 的生成策略借鉴了 Instagram 的 ID 生成算法。



如上图所示,方案说明如下:


  • 整个 ID 的二进制长度为 64 位;

  • 前 36 位使用时间戳,以保证 ID 是升序增加;

  • 中间 13 位是分库标识,用来标识当前这个 ID 对应的记录在哪个数据库中;

  • 后 15 位为 MySQL 自增序列,以保证在同一秒内并发时,ID 不会重复。每个 shard 库都有一个自增序列表,生成自增序列时,从自增序列表中获取当前自增序列值,并加 1,做为当前 ID 的后 15 位。

写在最后

创业是与时间赛跑的过程,前期为了快速满足业务需求,我们采用简单高效的方案,如使用云服务、应用服务直接访问单点 DB。


后期随着系统压力增大,性能和稳定性逐渐纳入考虑范围,而 DB 最容易出现性能瓶颈,我们采用读写分离、垂直分库、水平分库等方案。


面对高性能和高稳定性,架构升级需要尽可能超前完成,否则,系统随时可能出现系统响应变慢甚至宕机的情况。


2020-06-20 18:371109

评论

发布
暂无评论
发现更多内容

全球云市场增势迅猛,数据安全进入法治化的强监管时代

行云管家

云计算 网络安全 数据安全

Apache APISIX Meetup 南京站!我们 7.30 见!

API7.ai 技术团队

API网关 APISIX Meetup Workshop

什么是真正的HTAP?(一)背景篇

StoneDB

MySQL OLAP OLTP HTAP StoneDB

【森城市】GIS数据漫谈(四)— 坐标系统

ThingJS数字孪生引擎

用对工具,CI事半功倍

龙智—DevSecOps解决方案

ci 持续集成 ⾃动化构建 ⾃动化部署

【计算讲谈社】第六讲|三星堆奇幻之旅:只有云计算才能带来的体验

大咖说

云计算 三星堆 数字空间 阿里云大咖说 计算讲谈社

Gartner:无需数据中台,API就能胜任连接前端和后端的工作

雨果

数据中台 API

大数据培训 Hive 相关知识的全面总结

@零度

hive 大数据开发

华为影像XMAGE:求尽世间像,终见菩提心

脑极体

直播带货系统源码

开源直播系统源码

软件测试 APP开发 直播系统源码 直播带货系统源码

红象云腾大数据基础平台与龙蜥社区操作系统再次完成联合测试

OpenAnolis小助手

开源 操作系统 龙蜥社区 红象云腾 兼容性互认证

加盟自助洗车真的不用招人吗

共享电单车厂家

自助洗车加盟 车白兔自助洗车 无人自助洗车

没有可观测性,DataOps 注定失败|TheNewStack

观测云

【7.8-7.15】写作社区精彩技术博文回顾

InfoQ写作社区官方

优质创作周报

24小时共享自助洗车店你见过吗

共享电单车厂家

24小时共享自助洗车 自助洗车加盟 自助洗车店

无需CORS,用nginx解决跨域问题,轻松实现低代码开发的前后端分离

葡萄城技术团队

nginx 前后端分离 cros

Java实现有getMin功能的栈

工程师日月

Java 算法 7月月更

Python|类与对象

AXYZdong

Python 7月月更

5分钟快速梳理你的HTTP体系

程序员海军

前端 HTTP 7月月更

助力开发者,全方位解读 APISIX 测试案例

API7.ai 技术团队

开源 测试 APISIX 网关

游戏有什么用?| 游戏应用价值研究案例征集

易观分析

游戏

24小时自助共享洗车店要多少钱

共享电单车厂家

自助洗车加盟 车白兔洗车 24小时自助共享洗车 自助共享洗车店

知识干货:基础存储服务新手体验营

hum建应用专家

数据库

Dimitra 和 Ocean Protocol 解读农业数据背后的秘密

股市老人

代码合规性:开发人员使用Helix QAC的5大原因

龙智—DevSecOps解决方案

静态代码分析 Helix QAC 静态代码分析器

MySQL 添加用户并授予只能查询权限

叫练

网络安全网格概念以及特点简单普及

行云管家

网络安全 网络安全网格

一文搞懂│什么是跨域?如何解决跨域?

前端 经验分享 跨域 7月月更

阿里云E-MapReduce 极客大赛开放报名 数十万奖金等你挑战

Lily

比赛

24小时自助共享洗车有人洗吗

共享电单车厂家

24小时无人自助洗车 自助洗车加盟 车白兔自助洗车 自助洗车店

【用户文章】P4合并实践指南之实例拆解Resolve

龙智—DevSecOps解决方案

P4合并 解决冲突

架构演进实践:从0到4000高并发请求背后的努力!_文化 & 方法_技术琐话_InfoQ精选文章