【AICon】 如何构建高效的 RAG 系统?RAG 技术在实际应用中遇到的挑战及应对策略?>>> 了解详情
写点什么

达达 - 高性能服务端优化之路

  • 2015-12-13
  • 本文字数:4715 字

    阅读完需:约 15 分钟

业务场景

达达是全国领先的最后三公里物流配送平台。 达达的业务模式与滴滴以及 Uber 很相似,以众包的方式利用社会闲散人力资源,解决 O2O 最后三公里即时性配送难题。 达达业务主要包含两部分:商家发单,配送员接单配送,如下图所示。

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

下图是达达过去 6 个月,每天的访问量变化趋图,可见增长极快

下图是达达过去 6 个月,高峰期请求 QPS 的变化趋势图,可见增长极快

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

最初的技术选型

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

读写分离

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

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

(点击放大图像)

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

Mysql 支持主从同步,实时将主库的数据增量复制到从库,而且一个主库可以连接多个从库同步(细节参考 Replication )。利用此特性,我们在应用服务端对每次请求做读写判断,若是写请求,则把这次请求内的所有 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 商家和订单表做查询,如下如示:

复制代码
select * from tb_order where supplier_id in (select id from supplier where name=‘上海海底捞’);

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

复制代码
supplier_ids  =   select id from supplier where name=‘上海海底捞’
select * from tb_order where supplier_id in (supplier_ids )

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

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

水平分库(sharding)

读写分离,通过从库水平扩展,解决了读压力;垂直分库通过按业务拆分主库,缓存了写压力,但系统依然存在以下隐患:

  • 单表数据量越来越大。如订单表,单表记录数很快将过亿,超出 MySql 的极限,影响读写性能。
  • 核心业务库的写压力越来越大,已不能再进一次垂直拆分,Mysql 主库不具备水平扩展的能力

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

水平分库面临的第一个问题是,按什么逻辑进行拆分。一种方案是按城市拆分,一个城市的所有数据在一个数据库中;另一种方案是按订单 ID 平均拆分数据。按城市拆分的优点是数据聚合度比较高,做聚合查询比较简单,实现也相对简单,缺点是数据分布不均匀,某些城市的数据量极大,产生热点,而这些热点以后可能还要被迫再次拆分。按订单 ID 拆分则正相反,优点是数据分布均匀,不会出现一个数据库数据极大或极小的情况,缺点是数据太分散,不利于做聚合查询。比如,按订单 ID 拆分后,一个商家的订单可能分布在不同的数据库中,查询一个商家的所有订单,可能需要查询多个数据库。针对这种情况,一种解决方案是将需要聚合查询的数据做冗余表,冗余的表不做拆分,同时在业务开发过程中,减少聚合查询。

反复权衡利弊,并参考了 Uber 等公司的分库方案后,我们最后决定按订单 ID 做水平分库。从架构上,我们将系统分为三层:

  • 应用层:即各类业务应用系统
  • 数据访问层:统一的数据访问接口,对上层应用层屏蔽读写分库、分库、缓存等技术细节。
  • 数据层:对 DB 数据进行分片,并可动态的添加 shard 分片。

水平分库的技术关键点在于数据访问层的设计,数据访问层主要包含三部分:

  • ID 生成器:生成每张表的主键
  • 数据源路由:将每次 DB 操作路由到不同的 shard 数据源上
  • 缓存: 采用 Redis 实现数据的缓存,提升性能

ID 生成器是整个水平分库的核心,它决定了如何拆分数据,以及查询存储 - 检索数据。ID 需要跨库全局唯一,否则会引发业务层的冲突。此外,ID 必须是数字且升序,这主要是考虑到升序的 ID 能保证 Mysql 的性能(若是 UUID 等随机字符串,在高并发和大数据量情况下,性能极差。对比性能测试数据可供参考 uuid-vs-int-insert-performance )。同时,ID 生成器必须非常稳定,因为任何故障都会影响所有的数据库操作。

我们的 ID 的生成策略借鉴了 Instagram 的 ID 生成算法( sharding-ids-at-instagram )。具体方案如下:

(点击放大图像)

  • 整个 ID 的二进制长度为 64 位
  • 前 36 位使用时间戳,以保证 ID 是升序增加
  • 中间 13 位是分库标识,用来标识当前这个 ID 对应的记录在哪个数据库中
  • 后 15 位为自增序列,以保证在同一秒内并发时,ID 不会重复。每个 shard 库都有一个自增序列表,生成自增序列时,从自增序列表中获取当前自增序列值,并加 1,做为当前 ID 的后 15 位

总结

创业是与时间赛跑的过程,前期为了快速满足业务需求,我们采用简单高效的方案,如使用云服务、应用服务直接访问单点 DB;后期随着系统压力增大,性能和稳定性逐渐纳入考虑范围,而 DB 最容易出现性能瓶颈,我们采用读写分离、垂直分库、水平分库等方案。面对高性能和高稳定性,架构升级需要尽可能超前完成,否则,系统随时可能出现系统响应变慢甚至宕机的情况。

作者简介

杨 骏, 达达 CTO, 目前管理达达的大研发部门,负责产品,技术和数据。曾在 Google 和 Facebook 总部工作近 7 年,作为 Facebook 最早期的华人工程师之一加入并带领多个研发团队,负责过朋友推荐系统和多个广告产品和后台,通过机器学习和大数据分析进行广告优化。加入达达之前,在硅谷知名的移动支付公司 Square 带领 Growth 团队,负责公司的用户增长战略和实施。

毕业于浙江大学竺可桢学院,后获得卡耐基梅陇大学博士学位,从事机器学习和多媒体分析方向的研究。


感谢杜小芳对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。

2015-12-13 17:097106

评论

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

自我提升:方法探索

程序员架构进阶

自我管理 时间管理 自我提升 10月月更

12道Zookeeper高频面试题,你顶得住吗?

Java 架构 面试 分布式 后端

大数据作业Spark sql

Clarke

架构实战营模块九作业

maybe

惊艳!阿里出产的MyCat性能笔记,带你领略什么叫细节爆炸

Java 架构 面试 程序人生 编程语言

【Flutter 专题】36 图解 Flutter 基本动画 (一)

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 10月月更

在线RGB和HEX十六进制颜色互转工具

入门小站

工具

面试进阶必备:“阿里爸爸”高并发秒杀架构设计笔记(全彩版)

Java 架构 面试 程序人生 编程语言

为什么感觉假期还没开始就快要结束了?

脑极体

音视频全链路开发实践|引航计划|音视频

轻口味

音视频 引航计划 内容合集 技术专题合集

Android平台音视频实战|引航计划|音视频

轻口味

音视频 引航计划 内容合集 技术专题合集

私有云部署系列之动态IP获取(程序编写)

稻草鸟人

靠这份GitHub star过万的1121页图解算法成功杀进字节跳动

Java 架构 面试 程序人生 编程语言

【Vuex 源码学习】第八篇 - Vuex 对 State 状态的处理

Brave

源码 vuex 10月月更

linux之history命令

入门小站

Linux

从零开发一款Android Rtmp播放器

轻口味

android 音视频 引航计划 10月月更

009云原生之分布式事务模式

穿过生命散发芬芳

云原生 10月月更

Python代码阅读(第33篇):反转字典

Felix

Python 编程 Code Programing 阅读代码

模块五作业

Geek_fc100d

「架构实战营」

NodeJs 全栈创建多文件断点续传

devpoint

大前端 upload 引航计划 10月月更

金九银十旗开得胜!秋招字节正式批4面,顺利拿到offer

Java 架构 面试 程序人生 编程语言

🏆【Alibaba工具型技术系列】「EasyExcel技术专题」实战技术针对于项目中常用的Excel操作指南

洛神灬殇

Excel EasyExcel Alibaba Alibaba技术 10月月更

金九银十一线大厂Java面试题大全(整理版)1000+面试题附答案详解,最全面详细!

Java 程序员 架构 面试 后端

大前端:入门CSS的编程世界~

Bob

CSS 大前端 引航计划

【初恋系列】我轻轻的走了,正如我轻轻的来(微博评论实战)

人工智能~~~

高可用 高性能 微博评论架构实战

什么是机器学习, CNN TensorFlow by Google神经网络深度学习 易筋 ARTS 打卡 Week 69

John(易筋)

ARTS 打卡计划

Zabbix VS Prometheus :哪个更适合你

耳东@Erdong

后端 Prometheus zabbix 引航计划 10月月更

模块六作业

Geek_fc100d

「架构实战营」

Spark 系列教程(2)运行模式介绍

Se7en

这份笔记太牛了!手把手教你从零开始搭建Spring Cloud Alibaba!

Java 架构 面试 程序人生 编程语言

(深入篇)漫游语音识别技术—带你走进语音识别技术的世界

攻城先森

深度学习 音视频 nlp 语音识别

达达-高性能服务端优化之路_后端_杨骏_InfoQ精选文章