秒杀场景下带来的海量用户所造成的流量突增对系统冲击力可想而知,瞬时流量之高一方面造成的读写冲突,数据库锁会非常严重,另一方面应用服务器能否撑住也是一个问题。同时由于秒杀业务一般是各种活动带来,需要快速上下线,这在开发上也会提出更高的标准,快速支持需求而不出错。从系统上讲我们要做到高可用和高并发;从开发效率上我们要做到敏捷开发以支持产品快速迭代。接下来让我们从产品架构和流量控制这两个方面来讨论并解决这个问题。如果把解决秒杀问题看成一种武林秘籍,则产品架构是内功,流量控制是招法,上乘内力搭配制胜招法则无往而不利。
内功:产品架构解决之道向一站式架构说"NO"
上图是一个典型的一站式架构,未经过良好的设计的系统随着时间的推移逐渐会转换成上图这样,一方面是产品业务的压力,另一方面也是先天的设计不足。一站式架构存在很多问题。
首先就是不易扩展,难以维护。随着业务的发展,对系统进行扩容难以避免,而一站式架构的可扩展性却令人堪忧,对其扩容则相当于对系统中的所有功能进行扩容,从代码确认到测试再到环境都是一项艰巨的任务。一站式架构中的扩容往往建立在“既然什么都没改,则运行应该正常”的假设之上,而扩容迁移中的各种BUG则隐藏在这种假设之中。系统所需的各种权限也随着规模而不断增多,连接的各种组件也越来越多,由此带来的维护性问题也会持续加重。
其次是代码难以理解,开发质量得不到保证。随着系统规模的扩大,即便是最初经过良好设计的代码,经过人员迭代,需求压力,也难免会逐渐走向混乱。渐渐的,各种跨层调用,不合适的封装,有副作用的函数等都会出现在代码中,这些设计侵入到代码中的每个角落,每个人都能见,而每个人都会选择性忽略,因为错综复杂的调用难以调整,也很难找到人员来进行整体测试。而这些都是BUG产生的温床。
最后在一站式架构下项目的可靠性是无法得到保证的,由于业务调整而修改一处逻辑,往往会影响到代码中很多功能的逻辑,而这些额外受到影响的功能则通常不会得到测试,只是在假定这些都是正常的,而这种假定也通常正确,直接潜在的问题爆发。该出错的总会出错,墨菲定律往往这时候是最有效的。
综上可以看出一站式架构设计与敏捷开发格格不入,持续开发,持续集成,持续部署也就只能变成空谈了。
微服务架构
上图是优化后的微服务架构,将整个系统拆分成订单、推送、折扣、产品、个人信息等各个微服务,每个服务都有自己的数据库和缓存等,并且不会互相交叉。各服务之间使用消息队列、RPC调用等传输数据。目前大部分系统设计可能处于一站式架构和微服务架构之间,即上层应用可能已经服务化,但是数据库层面还是使用同一个库。但个人以为系统应该朝完全微服务化努力,隔离数据库层面的共用,以获得更高的系统可靠性。
微服务架构下的系统更加容易进行扩展,可以只针对需要扩容的系统来进行扩容,例如订单量增大,可以只扩容订单服务,而对于其他服务例如个人信息、折扣中心等都都不进行调整,这样一方面减少了系统扩容而对整体稳定性带来的变化,即只需测试新环境中的订单服务即可,随着微服务拆分的越细,这种优势也就越大。
在这种微服务架构下,开发人员可以更加集中精力,将重点放到少量的代码和明确的业务上,这样能够产出更加优雅的代码和良好的设计,在代码优化调整中,也不会由于到处调用而畏手畏脚。每个微服务可以安排2-3人的小组专门维护,这样也会减少一个微服务内部的沟通成本,而进一步提高生产力。每个微服务小组可以独立工作,无需过多协调即可实践新功能或想法。
随着技术的不断发展,项目所用的技术架构总会过时,在系统技术革新上,对于传统的一站式架构,甚至是之前提到的服务化架构在应用新技术上都会遇到不小的困难,牵一发动全身,技术改革往往除了推倒重来而没有其他办法。对于微服务架构,由于系统的完全拆分,公共组件依赖只剩下异步的消息队列,在新技术应用这方面则有了天然优势。微服务基于组件开发设计,提供了在开发过程中技术选型的最大灵活性,甚至是编程语言的变化都可以进行尝试。
最后要提到的,就是系统稳定性和可用性方面的考虑。在业务需求的持续推动下,持续部署不可避免,在线系统随时都需要进行上线。随着业务的增长、系统的复杂,系统部署时造成问题的潜在可能性会大大提高。而微服务在这方面极大的提高了系统的可靠性。由于微服务的划分,故障天然被隔离,某个服务的故障,不会造成系统的整体瘫痪。而发布的时间由于只需要发布更新相关的服务而大大缩短,这也提高了整体系统的稳定性。当面临问题需要回滚时,也只需要回滚更新相关服务即可,而这在一站式架构中将会是一个灾难。
良好的架构可以更好的支持快速迭代。高内聚的设计将开发人员精力集中到相对集中的领域以设计更优雅的代码实现。随着技术的演进,项目架构也可以跟着一起迭代升级。也可以更好的支持持续集成、持续部署。总之,微服务可以渗透到开发中的每个领域为业务迭代提供更好的支持。微服务这方面建议可以参考Spring Cloud和Docker。
招法:流量控制解决方案内功的修炼固然重要,不过并非一朝一夕可成,是需要长期的努力和不断的沉淀。在武学中固然有高神内力,同时也存在一些致胜招法,一旦练成即可功力猛进,下来就让我们看一下支持秒杀业务中的一些致胜招法:流量控制解决方案。
流量控制解决总览
前端流量控制,页面可以设计为动静分离,将尽可能多的数据使用CDN进行缓存,以减少到自己服务器上的流量。同时可以加入验证问题,拉长用户下单时间并防止刷单。对较核心逻辑担心用户破译验证方式,可以再增加服务器端的用户ID访问限制,例如同一用户5s内只能触发1次相关操作等。
反向代理流量控制 反向代理(Nginx)流量控制,很多页面或者接口响应数据都可以进行静态化处理,应用侧(Tomcat)可以定时生成这些资源,发到内容分发服务上,内容分发服务可以将这些静态化资源分发到所有的反向代理上。这些数据可以根据需要按照一定时间间隔进行更新。同时,也可以利用Nginx中的缓存配置功能,对热点接口进行缓存。
借助Nginx中Nginx-lua插件的功能,一些简单逻辑在Nginx中直接实现会比较容易,使用恰当,能够大幅减少到应用服务器的请求。例如倒计时,取系统当前时间等逻辑就非常适合在Nginx中使用Lua实现。对于存在缓存中的商品数量等信息也可由Nginx直接访问缓存返回,而不将请求再转发给应用服务器。
访问 数据分析流量控制在秒杀中,令人头疼的问题不只是正常流量突增。由于秒杀活动一般都带有优惠性质,恶意访问也会增多,对于恶意请求在Nginx层也可以做很多工作,进行一次拦截。Nginx配置中有限制用户访问频率的配置可以根据需要进行配置。同时,也可以使用nginx-lua完成一些简单的封禁逻辑,例如调用接口可以封掉指定IP或者UA的访问请求。之后利用日志分析程序,对访问日志进行分析,将需要封掉的IP或者UA等信息调用Nginx上提供的接口进行封杀。
应用服务器流量控制经过之前的处理,能够到达应用服务器的流量已经少了很多。对于秒杀活动这类的需求,可以准备专门的活动服务器,专门处理相关逻辑。可以使用不同域名进行分流,或者按照一定URL规则在反向代理服务器上进行分流。同时,对于到数据库进行操作的请求可以使用阻塞队减少到数据库的访问以减少行锁等。对于明显超出处理范围的请求可以直接返回秒杀已经结束。例如商品总量300,剩余量100,每台 应用服务器 上待处理的值超过总量的值则可以直接返回秒杀结束。剩余量可以存入缓存服务中,剩余量可以不那么精确,确保缓存中的剩余量>=实际剩余量即可。超出剩余量的请求也可以直接返回秒杀结束。
缓存存储多点部署以提高系统的整体可用性,避免缓存问题导致系统出错的情况。Redis会是一个目前比较好的选择,主从自动切换等功能可以更好的增强系统整体可用性。对于使用单点Memcached等系统,建议可以配置Keepalived,使得发生机器故障时,IP可以自动转移到另外一台健康Memcached服务器上,虽然数据不可恢复,但是缓存组件依然可用。宕机的组件对系统造成的影响往往是不可预料的,尤其是在未良好设计的系统中,整体架构中应任一组件完全宕机的可能性。
对于秒杀活动需求,可能是一系列的活动,相关逻辑可以判断是否可以做到一个专门活动库中,并进行读写分离设计。有条件的情况下,数据库中间件会完成其中一部分工作。在没有相关条件的时候,即使没有数据库中间件,相关逻辑也可以直接在代码中实现。
对于数据库行锁问题,如有需要可以进一步优化,例如上图,将行锁问题通过拆分具体商品,增加分配ID标志,去掉行锁。这个变动的缺点是可能对现有业务逻辑影响较大。不过优点也很明显,在数据库层面消除了行锁。
流量控制总结一图胜千言,以上讨论的流量控制总结方案可以总结到一张图上:
结合微服务架构,我们最终的架构图可以是这样的:
平台应用构建技术选型如下,供大家参考:
-
在虚拟化方面,可以按照物理机->虚拟机->Docker三层架构来实现虚拟化,在物理机上按照应用实例、缓存实例、消息队列实例等建立虚拟机,再分别在虚拟机上启动不同实例的Docker。
-
在应用构建方面,推荐使用Spring Cloud解决方案(由于历史原因,目前还保留部分Dubbo接口)。
-
消息队列选型目前比较广泛,这里推荐一下阿里开源的RocketMQ,目前已经捐赠给Apache。我们内部也针对RocketMQ开发了一套易用client,当然也在开源计划之中,相信不久就会和大家见面。目前我们系统每分钟接口请求量高峰在几十万左右,系统整体支撑再多的访问量相信也没有问题,各层、各服务根据需要都可以进行横向扩容以支撑更高的访问量。