秒杀其实主要解决两个问题,一个是并发读,一个是并发写前端
其实,秒杀的总体架构能够归纳为“稳、准、快”几个关键字java
而后就是“准”,就是秒杀 10 台 iPhone,那就只能成交 10 台,多一台少一台都不行。一旦库存不对,那平台就要承担损失,因此“准”就是要求保证数据的一致性。redis
最后再看“快”,“快”其实很好理解,它就是说系统的性能要足够高,不然你怎么支撑这么大的流量呢?不光是服务端要作极致的性能优化,并且在整个请求链路上都要作协同的优化,每一个地方快一点,整个系统就完美了。算法
因此从技术角度上看“稳、准、快”,就对应了咱们架构上的高可用、一致性和高性能的要求,咱们的专栏也将主要围绕这几个方面来展开,具体以下。数据库
高并发系统本质上就是一个知足大并发、高性能和高可用的分布式系统
这个就很直观了 , 请求数少并发量就少后端
高并发系统 1. 要下降系统依赖 , 防止由于依赖形成的各类问题 , 提升可用性 2. 下降流量入侵 , 大流量尽可能隔绝在外面浏览器
系统中的单点能够说是系统架构上的一个大忌,由于单点意味着没有备份,风险不可控 , 其次流量不能分发像redis这种会有热点数据问题缓存
其实构建一个高并发系统并无那么复杂 , 有一下的几个方法能够扛住比较高的并发性能优化
对电商来讲系统差很少是这种样子 :服务器
所谓动静分离 , 就是将一些不常变化 , 能够静态化 , 无状态 , 不须要逻辑处理的一些字段放在一个专门的系统或者地方 , 获取的时候不须要走后端系统的方法
常见的就三种 , 用户浏览器里、CDN 上 或者 在服务端的 Cache 中
Web 代理服务器根据请求URL查找缓存,直接取出对应的 HTTP 响应头和响应体而后直接返回,这个响应过程简单得连 HTTP 协议都不用从新组装,甚至连 HTTP 请求头也不须要解析
这个没有什么好办法 , 动态数据必定会将流量打到后端 , 因此尽量的减小这部分 , 若是不行就加机器
有 3 种方案可选:
就是使用内存缓存好比java的ehcache等
优势 | 缺点 |
---|---|
无网络开销 | 占用内存大 |
使用简单 | 同步机制须要使用其余方法保证 |
典型的就是redis集群
优势 | 缺点 |
---|---|
StartFragment单独一个 Cache 层,能够减小多个应用接入时使用 Cache 的成本。这样接入的应用只要维护本身的 Java 系统就好,不须要单独维护 Cache,而只关心如何使用便可 EndFragment | StartFragmentCache 层内部交换网络成为瓶颈 EndFragment |
StartFragment统一 Cache 的方案更易于维护,如后面增强监控、配置的自动化,只须要一套解决方案就行,统一块儿来维护升级也比较方便。 EndFragment | StartFragment缓存服务器的网卡也会是瓶颈; EndFragment |
StartFragment能够共享内存,最大化利用内存,不一样系统之间的内存能够动态切换,从而可以有效应对各类攻击。 EndFragment | StartFragment机器少风险较大,挂掉一台就会影响很大一部分缓存数据。 EndFragment |
要解决上面这些问题,能够再对 Cache 作 Hash 分组,即一组 Cache 缓存的内容相同,这样可以避免热点数据过分集中致使新的瓶颈产生。 好比redis 热点数据分组
在将整个系统作动静分离后,咱们天然会想到更进一步的方案,就是将 Cache 进一步前移到 CDN 上,由于 CDN 离用户最近,效果会更好
有如下几个问题须要解决
由于上面的这些问题 , 因此cdn的部署方法通常都是分网络分区域的中心化部署
首先,热点请求会大量占用服务器处理资源,虽然这个热点可能只占请求总量的亿分之一,然而却可能抢占 90% 的服务器资源,若是这个热点请求仍是没有价值的无效请求,那么对系统资源来讲彻底是浪费。
其次,即便这些热点是有效的请求,咱们也要识别出来作针对性的优化,从而用更低的代价来支撑这些热点请求
所谓“热点操做”,例如大量的刷新页面、大量的添加购物车、双十一零点大量的下单等都属于此类操做。对系统来讲,这些操做能够抽象为“读请求”和“写请求”,这两种热点请求的处理方式截然不同,读请求的优化空间要大一些,而写请求的瓶颈通常都在存储层,优化的思路就是根据 CAP 理论作平衡
热点数据”比较好理解,那就是用户的热点请求对应的数据。而热点数据又分为“静态热点数据”和“动态热点数据”
所谓“静态热点数据”,就是可以提早预测的热点数据。例如,咱们能够经过卖家报名的方式提早筛选出来,经过报名系统对这些热点商品进行打标。另外,咱们还能够经过大数据分析来提早发现热点商品,好比咱们分析历史成交记录、用户的购物车记录,来发现哪些商品可能更热门、更好卖,这些都是能够提早分析出来的热点
所谓“动态热点数据”,就是不能被提早预测到的,系统在运行过程当中临时产生的热点。例如,卖家在抖音上作了广告,而后商品一下就火了,致使它在短期内被大量购买
静态热点数据能够经过商业手段,例如强制让卖家经过报名参加的方式提早把热点商品筛选出来,实现方式是经过一个运营系统,把参加活动的商品数据进行打标,而后经过一个后台系统对这些热点商品进行预处理,如提早进行缓存 . 或者使用技术手段提早预测,例如对买家天天访问的商品进行大数据计算,而后统计出 TOP N 的商品,咱们能够认为这些 TOP N 的商品就是热点商品。
主要处理动态热点数据的 , 都是使用技术手段实现的
这里我给出了一个图,其中用户访问商品时通过的路径有不少,咱们主要是依赖前面的导购页面(包括首页、搜索页面、商品详情、购物车等)提早识别哪些商品的访问量高,经过这些系统中的中间件来收集热点数据,并记录到日志中。
咱们经过部署在每台机器上的 Agent 把日志汇总到聚合和分析集群中,而后把符合必定规则的热点数据,经过订阅分发系统再推送到相应的系统中。你能够是把热点数据填充到 Cache 中,或者直接推送到应用服务器的内存中,还能够对这些数据进行拦截,总之下游系统能够订阅这些数据,而后根据本身的需求决定如何处理这些数据。
打造热点发现系统时,我根据以往经验总结了几点注意事项。
处理热点数据一般有几种思路:一是优化,二是限制,三是隔离。
优化热点数据最有效的办法就是缓存热点数据,若是热点数据作了动静分离,那么能够长期缓存静态数据。可是,缓存热点数据更多的是“临时”缓存,即无论是静态数据仍是动态数据,都用一个队列短暂地缓存数秒钟,因为队列长度有限,能够采用 LRU 淘汰算法替换。
限制更多的是一种保护机制,限制的办法也有不少,例如对被访问商品的 ID 作一致性 Hash,而后根据 Hash 作分桶,每一个分桶设置一个处理队列,这样能够把热点商品限制在一个请求队列里,防止因某些热点商品占用太多的服务器资源,而使其余请求始终得不到服务器的处理资源。
高并发系统设计的第一个原则就是将这种热点数据隔离出来,不要让 1% 的请求影响到另外的 99%,隔离出来后也更方便对这 1% 的请求作针对性的优化。
具体到“秒杀”业务,咱们能够在如下几个层次实现隔离。
固然了,实现隔离有不少种办法。好比,你能够按照用户来区分,给不一样的用户分配不一样的 Cookie,在接入层,路由到不一样的服务接口中;再好比,你还能够在接入层针对 URL 中的不一样 Path 来设置限流策略。服务层调用不一样的服务接口,以及数据层经过给数据打标来区分等等这些措施,其目的都是把已经识别出来的热点请求和普通的请求区分开
咱们知道服务器的处理资源是恒定的,你用或者不用它的处理能力都是同样的,因此出现峰值的话,很容易致使忙处处理不过来,闲的时候却又没有什么要处理。可是因为要保证服务质量,咱们的不少处理资源只能按照忙的时候来预估,而这会致使资源的一个浪费
要对流量进行削峰,最容易想到的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间经过一个队列在一端承接瞬时的流量洪峰,在另外一端平滑地将消息推送出去
若是流量峰值持续一段时间达到了消息队列的处理上限,例如本机的消息积压达到了存储空间的上限,消息队列一样也会被压垮,这样虽然保护了下游的系统,可是和直接把请求丢弃也没多大的区别
消息队列,相似的排队方式还有不少,例如:
能够看到,这些方式都有一个共同特征,就是把“一步的操做”变成“两步的操做”,其中增长的一步操做用来起到缓冲的做用。
增长答题其实有不少目的
前面介绍的排队和答题要么是少发请求,要么对发出来的请求进行缓冲,而针对秒杀场景还有一种方法,就是对请求进行分层过滤,从而过滤掉一些无效的请求。分层过滤其实就是采用“漏斗”式设计来处理请求的
假如请求分别通过 CDN、前台读系统(如商品详情系统)、后台系统(如交易系统)和数据库这几层,那么:
分层过滤的核心思想是:在不一样的层次尽量地过滤掉无效请求,让“漏斗”最末端的才是有效请求。而要达到这种效果,咱们就必须对数据作分层的校验。
分层校验的基本原则是:
分层校验的目的是:在读系统中,尽可能减小因为一致性校验带来的系统瓶颈,可是尽可能将不影响性能的检查条件提早,如用户是否具备秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求、营销等价物是否充足等;在写数据系统中,主要对写的数据(如“库存”)作一致性检查,最后在数据库层保证数据的最终准确性(如“库存”不能减为负数)。
库存场景很是典型 , 是高并发状况下对数据进行读写操做的场景
先说一下场景
总结来讲,减库存操做通常有以下几个方式:
“下单减库存”在数据一致性上,主要就是保证大并发请求时库存数据不能为负数,也就是要保证数据库中的库存字段值不能为负数,通常咱们有多种解决方案
在交易环节中,“库存”是个关键数据,也是个热点数据,由于交易的各个环节中均可能涉及对库存的查询。可是,我在前面介绍分层过滤时提到过,秒杀中并不须要对库存有精确的一致性读,把库存数据放到缓存(Cache)中,能够大大提高读性能。
解决大并发读问题,能够采用 LocalCache(即在秒杀系统的单机上缓存商品相关的数据)和对数据进行分层过滤的方式,可是像减库存这种大并发写不管如何仍是避免不了,这也是秒杀场景下最为核心的一个技术难题。
所以,这里我想专门来讲一下秒杀场景下减库存的极致优化思路,包括如何在缓存中减库存以及如何在数据库中减库存。
好比使用redis , 其实咱们可使用lua脚原本保证一致性
若是你的秒杀商品的减库存逻辑很是单一,好比没有复杂的 SKU 库存和总库存这种联动关系,或者多组sku同时扣减的这种不涉及复琐事务的场景,我以为彻底能够.
若是涉及到多组扣减 , 若是有比较复杂的减库存逻辑,或者须要使用事务,仍是建议必须在数据库中完成减库存-> 或者缓存支持事务
因为 MySQL 存储数据的特色,同一数据在数据库里确定是一行存储(MySQL),所以会有大量线程来竞争 InnoDB 行锁,而并发度越高时等待线程会越多,TPS(Transaction Per Second,即每秒处理的消息数)会降低,响应时间(RT)会上升,数据库的吞吐量就会严重受影响
这就可能引起一个问题,就是单个热点商品会影响整个数据库的性能, 致使 0.01% 的商品影响 99.99% 的商品的售卖,这是咱们不肯意看到的状况。一个解决思路是遵循前面介绍的原则进行隔离,把热点商品放到单独的热点库中。可是这无疑会带来维护上的麻烦,好比要作热点数据的动态迁移以及单独的数据库等
而分离热点商品到单独的数据库仍是没有解决并发锁的问题,咱们应该怎么办呢?要解决并发锁的问题,有两种办法:
你可能有疑问了,排队和锁竞争不都是要等待吗,有啥区别?若是熟悉 MySQL 的话,你会知道 InnoDB 内部的死锁检测,以及 MySQL Server 和 InnoDB 的切换会比较消耗性能,淘宝的 MySQL 核心团队还作了不少其余方面的优化,如 COMMIT_ON_SUCCESS 和 ROLLBACK_ON_FAIL 的补丁程序,配合在 SQL 里面加提示(hint),在事务里不须要等待应用层提交(COMMIT),而在数据执行完最后一条 SQL 后,直接根据 TARGET_AFFECT_ROW 的结果进行提交或回滚,能够减小网络等待时间(平均约 0.7ms)。据我所知,目前阿里 MySQL 团队已经将包含这些补丁程序的 MySQL 开源。另外,数据更新问题除了前面介绍的热点隔离和排队处理以外,还有些场景(如对商品的 lastmodifytime 字段的)更新会很是频繁,在某些场景下这些多条 SQL 是能够合并的,必定时间内只要执行最后一条 SQL 就好了,以便减小对数据库的更新操做。
高并发系统为了保证系统的高可用,咱们必须设计一个 Plan B 方案来兜底
说到系统的高可用建设,它实际上是一个系统工程,须要考虑到系统建设的各个阶段,也就是说它其实贯穿了系统建设的整个生命周期,以下图所示:
具体来讲,系统的高可用建设涉及架构阶段、编码阶段、测试阶段、发布阶段、运行阶段,以及故障发生时。接下来,咱们分别看一下。
为何系统的高可用建设要放到整个生命周期中全面考虑?由于咱们在每一个环节中均可能犯错,而有些环节犯的错,你在后面是没法弥补的。例如在架构阶段,你没有消除单点问题,那么系统上线后,遇到突发流量把单点给挂了,你就只能干瞪眼,有时候想加机器都加不进去。因此高可用建设是一个系统工程,必须在每一个环节都作好。
那么针对秒杀系统,咱们重点介绍在遇到大流量时,应该从哪些方面来保障系统的稳定运行,因此更多的是看如何针对运行阶段进行处理,这就引出了接下来的内容:降级、限流和拒绝服务。
所谓“降级”,就是当系统的容量达到必定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。它是一个有目的、有计划的执行过程,因此对降级咱们通常须要有一套预案来配合执行。若是咱们把它系统化,就能够经过预案系统和开关系统来实现降级。
降级方案能够这样设计:当秒杀流量达到 5w/s 时,把成交记录的获取从展现 20 条降级到只展现 5 条。“从 20 改到 5”这个操做由一个开关来实现,也就是设置一个可以从开关系统动态获取的系统参数。
这里,我给出开关系统的示意图。它分为两部分,一部分是开关控制台,它保存了开关的具体配置信息,以及具体执行开关所对应的机器列表;另外一部分是执行下发开关数据的 Agent,主要任务就是保证开关被正确执行,即便系统重启后也会生效。
执行降级无疑是在系统性能和用户体验之间选择了前者,降级后确定会影响一部分用户的体验,例如在双 11 零点时,若是优惠券系统扛不住,可能会临时降级商品详情的优惠信息展现,把有限的系统资源用在保障交易系统正确展现优惠信息上,即保障用户真正下单时的价格是正确的。因此降级的核心目标是牺牲次要的功能和用户体验来保证核心业务流程的稳定,是一个不得已而为之的举措。
若是说降级是牺牲了一部分次要的功能和用户的体验效果,那么限流就是更极端的一种保护措施了。限流就是当系统容量达到瓶颈时,咱们须要经过限制一部分流量来保护系统,并作到既能够人工执行开关,也支持自动化保护的措施。
这里,我一样给出了限流系统的示意图。整体来讲,限流既能够是在客户端限流,也能够是在服务端限流。此外,限流的实现方式既要支持 URL 以及方法级别的限流,也要支持基于 QPS 和线程的限流。
首先,我之内部的系统调用为例,来分别说下客户端限流和服务端限流的优缺点。
在限流的实现手段上来说,基于 QPS 和线程数的限流应用最多,最大 QPS 很容易经过压测提早获取,例如咱们的系统最高支持 1w QPS 时,能够设置 8000 来进行限流保护。线程数限流在客户端比较有效,例如在远程调用时咱们设置链接池的线程数,超出这个并发线程请求,就将线程进行排队或者直接超时丢弃。
限流无疑会影响用户的正常请求,因此必然会致使一部分用户请求失败,所以在系统处理这种异常时必定要设置超时时间,防止因被限流的请求不能 fast fail(快速失败)而拖垮系统。
若是限流还不能解决问题,最后一招就是直接拒绝服务了。
当系统负载达到必定阈值时,例如 CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时,系统直接拒绝全部请求,这种方式是最暴力但也最有效的系统保护方式。例如秒杀系统,咱们在以下几个环节设计过载保护:
在最前端的 Nginx 上设置过载保护,当机器负载达到某个值时直接拒绝
HTTP 请求并返回 503 错误码,在 Java 层一样也能够设计过载保护。
拒绝服务能够说是一种不得已的兜底方案,用以防止最坏状况发生,防止因把服务器压跨而长时间完全没法提供服务。像这种系统过载保护虽然在过载时没法提供服务,可是系统仍然能够运做,当负载降低时又很容易恢复,因此每一个系统和每一个环节都应该设置这个兜底方案,对系统作最坏状况下的保护。
咱们讨论的主要是系统服务端性能,通常用 QPS(Query Per Second,每秒请求数)来衡量,还有一个影响和 QPS 也息息相关,那就是响应时间(Response Time,RT),它能够理解为服务器处理响应的耗时
正常状况下响应时间(RT)越短,一秒钟处理的请求数(QPS)天然也就会越多,这在单线程处理的状况下看起来是线性的关系,即咱们只要把每一个请求的响应时间降到最低,那么性能就会最高。
可是你可能想到响应时间总有一个极限,不可能无限降低,因此又出现了另一个维度,即经过多线程,来处理请求。这样理论上就变成了“总 QPS =(1000ms / 响应时间)× 线程数量”,这样性能就和两个因素相关了,一个是一次响应的服务端耗时,一个是处理请求的线程数。
对于大部分的 Web 系统而言,响应时间通常都是由 CPU 执行时间和线程等待时间(好比 RPC、IO 等待、Sleep、Wait 等)组成,即服务器在处理一个请求时,一部分是 CPU 自己在作运算,还有一部分是在各类等待。
理解了服务器处理请求的逻辑,估计你会说为何咱们不去减小这种等待时间。很遗憾,根据咱们实际的测试发现,减小线程等待时间对提高性能的影响没有咱们想象得那么大,它并非线性的提高关系,这点在不少代理服务器(Proxy)上能够作验证。
若是代理服务器自己没有 CPU 消耗,咱们在每次给代理服务器代理的请求加个延时,即增长响应时间,可是这对代理服务器自己的吞吐量并无多大的影响,由于代理服务器自己的资源并无被消耗,能够经过增长代理服务器的处理线程数,来弥补响应时间对代理服务器的 QPS 的影响。
其实,真正对性能有影响的是 CPU 的执行时间。这也很好理解,由于 CPU 的执行真正消耗了服务器的资源。通过实际的测试,若是减小 CPU 一半的执行时间,就能够增长一倍的 QPS。
也就是说,咱们应该致力于减小 CPU 的执行时间。
单看“总 QPS”的计算公式,你会以为线程数越多 QPS 也就会越高,但这会一直正确吗?显然不是,线程数不是越多越好,由于线程自己也消耗资源,也受到其余因素的制约。例如,线程越多系统的线程切换成本就会越高,并且每一个线程也都会耗费必定内存。
那么,设置什么样的线程数最合理呢?其实不少多线程的场景都有一个默认配置,即“线程数 = 2 * CPU 核数 + 1”。除去这个配置,还有一个根据最佳实践得出来的公式:
线程数 = [(线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间] × CPU 数量 => 这个公式的核心思想就行将等待的时间让给其余线程去处理
固然,最好的办法是经过性能测试来发现最佳的线程数。
对 Java 系统来讲,能够优化的地方不少,这里我重点说一下比较有效的几种手段,供你参考,它们是:减小编码、减小序列化。接下来,咱们分别来看一下。
Java 的编码运行比较慢,这是 Java 的一大硬伤。在不少场景下,只要涉及字符串的操做(如输入输出操做、I/O 操做)都比较耗 CPU 资源,无论它是磁盘 I/O 仍是网络 I/O,由于都须要将字符转换成字节,而这个转换必须编码。
每一个字符的编码都须要查表,而这种查表的操做很是耗资源,因此减小字符到字节或者相反的转换、减小字符编码会很是有成效。减小编码就能够大大提高性能。
那么如何才能减小编码呢?例如,网页输出是能够直接进行流输出的,即用 resp.getOutputStream() 函数写数据,把一些静态的数据提早转化成字节,等到真正往外写的时候再直接用 OutputStream() 函数写,就能够减小静态数据的编码转换。好比 把静态的字符串提早编码成字节并缓存,而后直接输出字节内容到页面,从而大大减小编码的性能消耗的,网页输出的性能比没有提早进行字符到字节转换时提高了 30% 左右。
序列化也是 Java 性能的一大天敌,减小 Java 中的序列化操做也能大大提高性能。又由于序列化每每是和编码同时发生的,因此减小序列化也就减小了编码。
序列化大部分是在 RPC 中发生的,所以避免或者减小 RPC 就能够减小序列化,固然当前的序列化协议也已经作了不少优化来提高性能。有一种新的方案,就是能够将多个关联性比较强的应用进行“合并部署”,而减小不一样应用之间的 RPC 也能够减小序列化的消耗。
所谓“合并部署”,就是把两个本来在不一样机器上的不一样应用合并部署到一台机器上,固然不只仅是部署在一台机器上,还要在同一个 Tomcat 容器中,且不能走本机的 Socket,这样才能避免序列化的产生。