大秒系统:解决读数据热点问题的实践经验与思路

2024-06-17
来源:网络整理

概括

最初的秒杀系统雏形是淘宝详情页的限时上架功能,有些卖家为了吸引眼球,把价格压得很低,但这给详情系统带来了很大的压力,为了隔离这种突发流量,设计了秒杀系统。本文主要介绍大闪购系统以及这种典型的读取数据热点问题的解决思路和实践经验。

一些数据

还记得2013年小米的秒杀吗?三款小米手机每台限量11万台,全部采用大秒杀系统,三分钟后成为双11首家也是最快破亿的旗舰店。根据日志统计,双11当天前端系统峰值有效请求约60万QPS,而后端集群峰值近100万/s,单机近30万/s。但实际写入流量要小得多,当时最高降单tps由红米创造,达到1500/s。

一些概念

热点隔离。秒杀系统设计的第一原则就是隔离这些热点数据,让1%的请求不影响其他99%。隔离之后,也更容易针对这1%的请求做针对性的优化。我们针对秒杀做了多个级别的隔离:

业务隔离。我们把秒杀当成一个营销活动,卖家需要单独注册才能参与秒杀。从技术上讲,卖家注册后,就成为我们的已知热点,等到活动真正开始,我们就可以提前预热。

系统隔离。系统隔离更多的是运行时的隔离,可以通过分组部署的方式,把系统隔离和其他99%隔离开来。二手卖也申请一个单独的域名,目的是让请求落入不同的集群。

数据隔离。秒杀使用的数据大部分都是热点数据,比如会启用一个单独的集群或者数据库来存储热点数据,目前我们不希望0.01%的数据影响到其他99.99%的数据。

当然,实现隔离的方式有很多种,比如按用户区分,在接入层为不同用户分配不同的路由到不同的服务接口,在接入层针对不同的URL路径设置限流策略,服务层调用不同的服务接口;数据层可以通过特殊的标签来区分数据,目的是将识别出的热点与普通请求区分开来。

动静分离

上面在系统层面介绍的原则是隔离。接下来就是将动静态热点数据分离,这也是解决大流量系统的一个重要原则。如何将系统静态化做到动静分离呢?我之前写过一篇文章《高访问量系统静态架构设计》,详细介绍了淘宝商品系统的静态设计思路,有兴趣的可以去《程序员》杂志找找。我们的大秒系统是从商品详情系统发展而来的,所以已经做到了动静分离,如图1所示。

图1:大二次系统动静分离

此外,还有以下特点:

将整个页面放入用户的浏览器中

如果你强制刷新整个页面,你也会向 CDN 发出请求

实际有效请求只是“刷新宝藏”按钮

这样90%的静态数据就缓存在用户侧或者CDN上了,当有秒杀事件发生时,用户只需要点击一个特殊的按钮“刷新抢宝”,而不需要刷新整个页面。这样就只需要向服务器请求少量的有效数据,而不需要重复请求大量的静态数据。秒杀事件的动态数据比普通详情页要少,而性能却比普通详情页高3倍以上。所以“刷新抢宝”的设计思路解决了不刷新页面就能从服务器请求最新的动态数据的问题。

基于时间分片的削峰

熟悉淘宝秒杀的朋友都知道,第一版秒杀系统本身是没有问答功能的,后来才加入了秒杀问答功能。当然,秒杀问答功能的一个重要目的就是为了防止秒杀机器。2011年秒杀盛行的时候,秒杀机器也猖獗,没有达到全民参与营销的目的,所以才加入了问答功能来限制秒杀机器。加入问答功能之后,下单时间基本控制在2秒以内,秒杀机器的下单比例也下降到了5%以下。新的问答页面如图2所示。

图 2:秒答题页面

其实加入问答还有另外一个重要的作用,就是把下单请求的峰值从1s延长到2~10s左右。请求峰值是按照时间分片的,这个时间分片对于服务器处理并发非常重要,会大大减轻压力。另外由于请求是按顺序进行的,后面的请求自然是没有库存的,无法到达最后的下单步骤,所以真正的并发写入非常有限。其实这种设计思路目前也很常见,比如支付宝的“咻一咻”,微信的摇一摇。

除了在前端解答问题,减少用户侧的流量峰值外,服务器端一般会使用锁或者队列来控制瞬时请求。

数据层验证

图3 分层验证

最重要的设计原则是对大流量系统的数据进行分层验证。所谓分层验证,就是对大量请求做一个“漏斗”设计,如图3所示:无效请求被不同层级尽可能过滤,“漏斗”的末端才是有效请求。要达到这个效果,必须对数据进行层层验证。以下是一些原则:

首先分离动态和静态数据

在客户端浏览器中缓存 90% 的数据

动态请求的读取数据在Web端

读取数据无强一致性检查

合理的按时间分片写入数据

为写请求提供限流保护

写入数据强一致性检查

闪购系统的系统架构就是按照这个原则设计的,如图4所示。

图4:闪购系统分层架构

将大量不需要验证的静态数据放在最靠近用户的地方;在前端读取系统验证一些基本信息,比如用户是否符合秒杀资格、商品状态是否正常、用户的回答是否正确、秒杀是否结束等;在数据写入系统检查一些其他的东西,比如是否是非法请求、是否有足够的营销等价物(淘宝币等),以及写入数据的一致性,比如检查是否有库存等;最后在数据库层面保证数据最终的准确性,比如库存不能降为负数。

实时热点发现

淘宝软件开发_淘宝产品开发_淘宝**小程序开发思路

其实闪购系统的本质还是数据读取的热点问题,而且是最简单的一个。因为正如文章中提到的,通过业务隔离,我们可以提前识别出这些热点数据,可以提前做一些防护。提前识别出来的热点数据处理起来比较简单,比如分析历史交易记录可以发现哪些商品比较畅销,分析用户的购物车记录也可以发现哪些商品可能比较容易卖出去,这些都是可以提前分析出来的热点。比较难的是,那些我们无法提前发现的突然成为热点的商品,就成为了热点。这就需要实时的热点数据分析,目前我们的设计可以在3秒之内发现交易链路上的实时热点数据,然后根据实时发现的热点数据对各个系统进行实时防护。具体实现如下:

构建异步热点Key,可以统计交易链路上各类中间件产品的热点统计,例如Tair 、HSF等(Tair 等中间件产品已经有热点统计模块)。

建立可按需订阅的热点上报及热点服务投递规范。主要目的是通过交易链路上各个系统(详情、购物车、交易、折扣、库存、物流)的访问时间差,将上游发现的热点透传到下游系统,提前防患于未然。比如大促高峰期,详情系统最先知道,通过统计接入层的模块统计热点URL。

上游系统收集热点数据,发送到热点服务台,下游系统(比如交易系统)就能知道哪些产品被频繁调用,从而进行热点防护,如图5所示。

图5 实时热点数据背景

关键部分包括:

热点服务后台最好能异步抓取热点数据日志,一方面容易实现通用性,另一方面也不会影响业务系统和中间件产品的主流程。

热点服务后端与现有的中间件、应用不存在替代关系,各个中间件、应用需要自我保护,热点服务后端提供了统一的热点数据采集规范和工具,提供热点订阅服务,便于实现各系统热点数据透明化。

热点发现必须实时完成(3秒内)。

关键技术优化点

上面介绍了一些设计高流量阅读系统的原则,但是当这些方法都用了之后,如果仍然有大量流量涌入,该怎么办呢?闪购系统需要解决几个关键问题。

优化Java对大并发动态请求的处理

其实 Java 在处理大并发 HTTP 请求方面比一般的 Web 服务器要弱,所以我们对于访问量大的 Web 系统通常会做静态改造,让大部分请求和数据直接在服务器或者 Web 代理服务器(等)上返回(可以减少数据的序列化和反序列化),不让请求落到 Java 层,让 Java 层只处理数据量较小的动态请求。当然针对这些请求也有一些优化方法:

直接使用处理请求。避开传统的MVC框架,可能会绕过很多复杂无用的处理逻辑,节省1ms,当然这个取决于你对MVC框架的依赖。

直接输出流数据。使用 resp.() 代替 resp.() 可以省去一些不变的字符数据编码,提高性能。同时建议在输出数据时使用 JSON 而不是模板引擎(一般解释执行)来输出页面。

同一产品并发读取问题

你可能会说这个问题很好解决,放到Tair缓存里就可以了。集中式的Tair缓存为了保证命中率,一般采用一致性哈希,所以同一个key会落在一台机器上。虽然我们的Tair缓存机器可以支撑30w/s的请求,但是对于大秒杀这样的热门商品来说,远远不够。如何彻底解决这个单点瓶颈呢?答案就是使用应用层,也就是在秒杀系统的单台机器上缓存商品相关数据。如何缓存数据呢?也有动态和静态的:

产品标题、描述等不变的内容将在闪购开始前完整推送到闪购机器,并缓存到闪购结束。

库存等动态数据会采用被动失效的方式缓存一段时间(一般是几秒),失效之后再从Tair缓存中拉取最新的数据。

你可能会想,如果数据不一致,像库存这种频繁更新数据的时候,会不会导致超卖呢?其实这就要用到我们前面介绍的读取数据分层校验原则了。读取场景可以允许一定量的脏数据,因为这里的误判只会导致少量原本缺货的订单请求被误认为有货。在真正写入数据的时候保证最终的一致性。这样平衡了数据的高可用和一致性,解决了高并发数据读取的问题。

同一数据并发更新问题

为了解决大并发读取的问题,采用了数据分层校验的方式,但诸如减库存这样的大并发写入无论如何都无法避免,这也是秒杀场景下最核心的技术难点。

同一条数据在数据库中是按行存储的(),因此会有大量线程争用行锁,当并发量较高时,等待线程会增多,TPS 会下降,RT 会上升,严重影响数据库吞吐量。这里就会出现一个问题,就是单一的热点商品会影响整个数据库的性能,我们不会希望看到 0.01% 的商品影响 99.99% 的商品。因此一个思路就是按照上面介绍的第一条原则,把热点商品隔离出来,放到单独的热点数据库中。

但无疑会带来维护的麻烦(热数据的动态迁移、数据库分离等)。

将热点商品分离到单独的数据库依然不能解决并发锁的问题,解决并发锁的方法有两种。

应用层进行排队,按照商品维度设置排队顺序,可以减少同一台机器对同一行数据库记录的操作的并发量,也可以控制单个商品占用的数据库连接数,防止热点商品占用过多的数据库连接。

队列是数据库层做的,应用层只能做单机排队,但是应用机器数量很大,这种排队方式对并发的控制还是有限的,所以如果能在数据库层做全局排队就再好不过了。淘宝数据库团队为此开发了一个层,可以在数据库层做单行记录的并发排队,如图6所示。

图6 数据库层单行记录的并发排队

你可能会问,队列和锁竞争不是需要等待吗?有什么区别?如果熟悉的话就会知道,内部死锁检测和在 和 之间的切换会很耗性能。淘宝核心团队还做了其他很多优化,比如 和 ,在SQL中加入hint,使得事务不需要等待应用层提交,而是在数据执行完最后一条SQL后直接根据结果提交或者回滚,这样可以减少网络等待时间(平均0.7ms左右)。据我所知,阿里巴巴团队已经将这些 和 提交到官方审核中了。

大促热点问题的思考

根据多年的经验,针对秒杀这种典型系统所代表的热点问题,我总结了一些通用的原则:隔离、动态分离、分层验证。每个环节都要从整个链路的角度去考虑和优化,除了优化系统提升性能,限流和防护也是必须做的。

除了上面提到的热点问题,淘宝还有很多其他的数据热点问题:

数据访问热点,比如某些热点产品的访问量非常高。甚至Tair的本身也存在瓶颈问题,一旦请求数达到单台机器的极限,就会出现热点防护问题。有时候看似很好解决,比如限流,但是想想,一旦某个热点触发了某台机器的限流,那么这台机器的数据就失效了,间接导致应用层数据库崩溃,请求雪崩。这类问题需要结合具体的产品,才有比较好的解决方案。这里介绍一个通用的解决思路,就是在端本地做,发现热点数据就直接进来,而不是去请求。

数据更新热点,除了上面提到的热点隔离、排队处理,还有一些场景,比如产品字段的频繁更新。有些场景下,可以将这多条 SQL 合并,在一定时间内只执行最后一条 SQL,这样可以减少数据库操作。另外,热点产品的自动迁移,理论上也可以在数据路由层完成,利用上面介绍的热点实时发现,将热点从通用数据库自​​动迁移到单独的热点数据库中。

按照某个维度建索引会产生热点数据,比如实时搜索中,评价数据和商品维度关联,某些热点商品的评价非常多,导致搜索系统按照商品ID对评价数据建索引时内存溢出。交易维度和订单信息关联时也会出现同样的问题。这类热点数据需要进行哈希处理,增加另一个维度重新组织数据。

开源中国征文活动已经开始!

开源中国是受关注度高、影响力大的开源技术社区,拥有超过200万开源技术精英,传播开源理念,推广开源项目,为IT开发者提供发现、使用、交流开源技术的平台。

分享