最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

架构演进实践: 从 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:37893

评论

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

主机安全是什么意思?安全体检包含哪些方面?

行云管家

运维 服务器 主机 主机安全 安全体检

Linux用户密码管理

在即

9月日更

千万级数据迁移与分表的技术方案-企业产品实战

谙忆

Java 后端 分库分表 引航计划

作为一线技术人员,如何更好地提升自己

谙忆

管理 成长 引航计划

Tapdata 实时数据中台在智慧教育中的实践

tapdata

低代码应用:软件开发的一体化最新形态!

优秀

低代码

Go 语言网络库 getty 的那些事

apache/dubbo-go

dubbo Go 语言 Dubbo3

幻读是啥,会有什么问题?如何解决?

Java MySQL 数据库 面试 后端

网络攻防学习笔记 Day144

穿过生命散发芬芳

高可用 9月日更

国庆高质量出行,可视化开启智慧旅游

ThingJS数字孪生引擎

大前端 物联网 可视化 旅游 数字孪生

二十不惑的年纪,我简直走了狗屎运(4面拿字节跳动offer)

Java 程序员 架构 面试 计算机

金融级分布式事务解决方案DTC

tom

我愿意招什么样的产品经理?

石云升

产品经理 招聘 9月日更

直播预告|如何节省30%人工成本,缩短80%商标办理周期?

京东科技开发者

商标 企业服务 灵活用工

FunTester框架Redis性能测试之list操作

FunTester

redis 性能测试 测试框架 压力测试 FunTester

乘着汽车智能化的浪潮,“汽车人”的职业方向选择(三)

SOA开发者

软件定义汽车 车载控制单元

mydumper备份工具介绍与使用

Simon

MySQL

与springcloud整合的框架源码读取入口

Java 编程 架构 微服务

Alibaba内部“Java架构核心宝典”来袭,全新技术限时开源

Java 编程 程序员 架构 面试

深入理解掌握零拷贝技术

Linux服务器开发

网络协议 零拷贝 Linux服务器开发 Linux内核 用户态

2021年公有云市场的5大趋势

浪潮云

云计算

iOS开发面试拿offer攻略之数据结构与算法篇附加安全加密

iOSer

ios 数据加密 iOS面试 iOS逆向 iOS算法

基于Tensorflow + Opencv 实现CNN自定义图像分类

华为云开发者联盟

tensorflow KNN OpenCV CNN

银行数字化转型指南:《区域性银行数字化转型白皮书》完整版重磅发布

百度开发者中心

最佳实践 银行数字化转型

为什么不推荐Python初学者直接看项目源码

Felix

Python 编程 开发 Programing 阅读代码

一部好看过武侠小说的热血互联网史!

博文视点Broadview

Percolator模型及其在TiKV中的实现

vivo互联网技术

数据库 Percolator 分布式,

堡垒机作用之事后审计详细讲解-行云管家

行云管家

运维 网络安全 运维审计 事后审计

JavaScript进阶(七)call, apply, bind

Augus

JavaScript 9月日更

玩转TypeScript工具类型(下)

有道技术团队

typescript 大前端 网易有道

通俗易懂 即时通讯初学者入门 WhatsApp技术架构

OpenIM

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