写点什么

如何设计一个秒杀系统?

作者:Java随想录
  • 2024-06-16
    浙江
  • 本文字数:10704 字

    阅读完需:约 35 分钟

如何设计一个秒杀系统?

微信公众号:Java随想录


这篇分享源自之前购买的极客时间课程《如何设计一个秒杀系统》,以及书籍《亿级流量网站架构核心技术》。


这两个讲的都是关于高并发系统设计的,感觉收获颇多。


本篇内容对核心要点进行了摘录,也结合网上一些文章,希望能分享所得的收获。

动静分离

对页面进行彻底的动静分离,使得用户秒杀时不需要刷新整个页面,借此把页面刷新的数据降到最少。


用户看到的数据可以分为:静态数据动态数据


简单来说,"动态数据"和"静态数据"的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有 Cookie 等私密数据。


比如说:


  • 很多媒体类的网站,某一篇文章的内容不管是你访问还是我访问,它都是一样的。所以它就是一个典型的静态数据,但是它是个动态页面。

  • 我们如果现在访问淘宝的首页,每个人看到的页面可能都是不一样的,淘宝首页中包含了很多根据访问者特征推荐的信息,而这些个性化的数据就可以理解为动态数据了。


这里再强调一下,我们所说的静态数据,不能仅仅理解为传统意义上完全存在磁盘上的 HTML 页面,它也可能是经过 Java 系统产生的页面,但是它输出的页面本身不包含上面所说的那些因素。


也就是所谓"动态"还是"静态",并不是说数据本身是否动静,而是数据中是否含有和访问者相关的个性化数据

静态化改造

静态化改造就是要直接缓存 HTTP 连接


相较于普通的数据缓存而言,你肯定还听过系统的静态化改造。静态化改造是直接缓存 HTTP 连接而不是仅仅缓存数据,如下图所示,Web 代理服务器根据请求 URL,直接取出对应的 HTTP 响应头和响应体然后直接返回,这个响应过程简单得连 HTTP 协议都不用重新组装,甚至连 HTTP 请求头也不需要解析。





商详上静态化


高并发时候,商详页面是最先受到冲击的,通过商详静态化,可以帮助服务器挡掉 99.9%流量。


分类举例: 商品图片、商品详细描述等,所有用户看到的内容都是一样的,这一类数据就可以上静态化。


会员折扣、优惠券等信息具备个体差异性,就需要放在动态接接口中,根据入参信息实时查询。


我们从以下 5 个方面来分离出动态内容:


  • URL 唯一化:商品详情系统天然地就可以做到 URL 唯一化,比如每个商品都由 ID 来标识,那么 http://item.xxx.com/item.htm?id=xxxx 就可以作为唯一的 URL 标识。为啥要 URL 唯一呢?前面说了我们是要缓存整个 HTTP 连接,那么以什么作为 Key 呢?就以 URL 作为缓存的 Key,例如以 id=xxx 这个格式进行区分。

  • 分离浏览者相关的因素:浏览者相关的因素包括是否已登录,以及登录身份等,这些相关因素我们可以单独拆分出来,通过动态请求来获取。

  • 分离时间因素:服务端输出的时间也通过动态请求获取。

  • 异步化地域因素:详情页面上与地域相关的因素做成异步方式获取,当然你也可以通过动态请求方式获取,只是这里通过异步获取更合适。

  • 去掉 Cookie:服务端输出的页面包含的 Cookie 可以通过代码软件来删除,如 Web 服务器 Varnish 可以通过 unset req.http.cookie 命令去掉 Cookie。注意,这里说的去掉 Cookie 并不是用户端收到的页面就不含 Cookie 了,而是说,在缓存的静态数据中不含有 Cookie。


分离出动态内容之后,如何组织这些内容页就变得非常关键了。


动态内容的处理通常有两种方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。


  • ESI 方案(或者 SSI):即在 Web 代理服务器上做动态内容请求,并将请求插入到静态页面中,当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好。

  • CSI 方案。即单独发起一个异步 JavaScript 请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。

CDN

网站应用,静态资源占流量的多数。系统做了动静分离之后,就可以把静态资源通过 CDN 加速。


这样,静态资源的请求大部分通过就近部署的 CDN 服务器提供服务,用户的延迟也会有明显的提升。网站服务器专注于服务动态流量,带宽压力会小很多。


动静分离,部署时静态资源要给一个单独域名,这个域名是个 CNAME,CNAME 映射到 CDN 服务厂商提供的 DNS 服务器,CDN DNS 服务器会根据请求的 IP 地址所在区域和资源内容,返回就近的 CDN 缓存服务器 ip,后续用户对这个 DNS 的请求都会转到这个 IP 上来。


Tips:CNAME 简单来讲就是给域名起了个别名。


CDN 工作流程大致如下:



静态资源上 CDN 存在以下几个问题:


  • 失效问题。前面我们也有提到过缓存时效的问题,不知道你有没有理解,我再来解释一下。谈到静态数据时,我说过一个关键词叫“相对不变”,它的言外之意是“可能会变化”。比如一篇文章,现在不变,但如果你发现个错别字,是不是就会变化了?如果你的缓存时效很长,那用户端在很长一段时间内看到的都是错的。所以,这个方案中也是,我们需要保证 CDN 可以在秒级时间内,让分布在全国各地的 Cache 同时失效,这对 CDN 的失效系统要求很高。


失效需要一个失效系统来实现,一般有主动失效和被动失效。


主动失效需要监控数据库数据的变化然后转成消息来发送失效消息,这个实现比较复杂,阿里有个系统叫 metaq,可以网上参考下。


被动失效就是只缓存固定时间,然后到期后自动失效


  • 命中率问题。Cache 最重要的一个衡量指标就是“高命中率”,不然 Cache 的存在就失去了意义。同样,如果将数据全部放到全国的 CDN 上,必然导致 Cache 分散,而 Cache 分散又会导致访问请求命中同一个 Cache 的可能性降低,那么命中率就成为一个问题。

  • 发布更新问题。如果一个业务系统每周都有日常业务需要发布,那么发布系统必须足够简洁高效,而且你还要考虑有问题时快速回滚和排查问题的简便性。


部署方式如下图所示:



你可能会问,存储在浏览器或 CDN 上,有多大区别?我的回答是:区别很大!因为在 CDN 上,我们可以做主动失效,而在用户的浏览器里就更不可控,如果用户不主动刷新的话,你很难主动地把消息推送给用户的浏览器。




秒杀场景 CDN 应用


比如,1 元卖 iPhone,100 台,于是来了一百万人抢购。


我们把技术挑战放在一边,先从用户或是产品的角度来看一下,秒杀的流程是什么样的。


  • 首先,你需要一个秒杀的 landing page,在这个秒杀页上有一个倒计时的按钮。

  • 一旦这个倒计时的时间到了,按钮就被点亮,让你可以点击按钮下单。

  • 一般来说下单时需要你填写一个校验码,以防止是机器来抢。


从技术上来说,这个倒计时按钮上的时间和按钮可以被点击的时间是需要后台服务器来校准的,这意味着:


  • 前端页面要不断地向后端来请求,开没开始,开没开始……

  • 每次询问的时候,后端都会给前端一个时间,以校准前端的时间。

  • 一旦后端服务器表示 OK 可以开始,后端服务会返回一个 URL。

  • 这个 URL 会被安置在那个按钮上,就可以点击了。

  • 点击后,如果抢到了库存,就进入支付页面,如果没有则返回秒杀已结束。


很明显,要让 100 万用户能够在同一时间打开一个页面,这个时候,我们就需要用到 CDN 了。数据中心肯定是扛不住的,所以,我们要引入 CDN。


在 CDN 上,这 100 万个用户就会被几十个甚至上百个 CDN 的边缘结点给分担了,于是就能够扛得住。然后,我们还需要在这些 CDN 结点上做点小文章。


一方面,我们需要把小服务部署到 CDN 结点上去,这样,当前端页面来问开没开始时,这个小服务除了告诉前端开没开始外,它还可以统计下有多少人在线。每个小服务会把当前在线等待秒杀的人数每隔一段时间就回传给我们的数据中心,于是我们就知道全网总共在线的人数有多少。


假设,我们知道有大约 100 万的人在线等着抢,那么,在我们快要开始的时候,由数据中心向各个部署在 CDN 结点上的小服务上传递一个概率值,比如说是 0.02%。


于是,当秒杀开始的时候,这 100 万用户都在点下单按钮,首先他们请求到的是 CDN 上的这些服务,这些小服务按照 0.02% 的量把用户放到后面的数据中心,也就是 1 万个人放过去两个,剩下的 9998 个都直接返回秒杀已结束。于是,100 万用户被放过了 0.02% 的用户,也就是 200 个左右,而这 200 个人在数据中心抢那 100 个 iPhone,也就是 200 TPS,这个并发量怎么都应该能扛住了。

热点缓存

热点数据亦分 静态热点动态热点


所谓"静态热点数据",就是能够提前预测的热点数据。


例如,我们可以通过卖家报名的方式提前筛选出来,通过报名系统对这些热点商品进行打标。另外,我们还可以通过大数据分析来提前发现热点商品,比如我们分析历史成交记录、用户的购物车记录,来发现哪些商品可能更热门、更好卖,这些都是可以提前分析出来的热点。


所谓"动态热点数据",就是不能被提前预测到的,系统在运行过程中临时产生的热点。例如,卖家在抖音上做了广告,然后商品一下就火了,导致它在短时间内被大量购买。


静态热点比较好处理,所以秒级内自动发现热点商品就成为了热点缓存的关键。

动态热点发现

这里我给出一个动态热点发现系统的具体实现:


  1. 构建一个异步的系统,它可以收集交易链路上各个环节中的中间件产品的热点 Key,如 Nginx、缓存、RPC 服务框架等这些中间件(一些中间件产品本身已经有热点统计模块)。

  2. 建立一个热点上报和可以按照需求订阅的热点服务的下发规范,主要目的是通过交易链路上各个系统(包括详情、购物车、交易、优惠、库存、物流等)访问的时间差把上游已经发现的热点透传给下游系统,提前做好保护。比如,,对于大促高峰期,详情系统是最早知道的,在统一接入层上 Nginx 模块统计的热点 URL。热点的统计可以很简单的对访问的商品进行访问计数,然后排序。还有就是用通常的队列的淘汰算法如 LRU 等都可以实现。

  3. 将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商品会被频繁调用,然后做热点保护。


这里我给出了一个图,其中用户访问商品时经过的路径有很多,我们主要是依赖前面的导购页面(包括首页、搜索页面、商品详情、购物车等)提前识别哪些商品的访问量高,通过这些系统中的中间件来收集热点数据,并记录到日志中。



我们通过部署在每台机器上的 Agent 把日志汇总到聚合和分析集群中,然后把符合一定规则的热点数据,通过订阅分发系统再推送到相应的系统中。你可以是把热点数据填充到 Cache 中,或者直接推送到应用服务器的内存中,还可以对这些数据进行拦截,总之下游系统可以订阅这些数据,然后根据自己的需求决定如何处理这些数据。


热点发现要做到接近实时(3s 内完成热点数据的发现),因为只有做到接近实时,动态发现才有意义,才能实时地对下游系统提供保护。


对于缓存系统来讲,缓存命中率是最重要的指标,甚至都没有之一。时间拉的越长,不确定性越多,缓存命中率必然越低。比如如果 10s 内才发送热点就没意义了,因为 10s 内用户可以进行的操作太多了。时间越长,不可控元素越多,热点缓存命中率越低。


可以参考,京东开源的热点探测 Hot Key。




可以考虑建立实时热点发现系统。


具体步骤如下:


  1. 接入 Nginx 将请求转发给应用 Nginx。

  2. 应用 Nginx 首先该取本地缓存。如果命中,则直接返回,不命中会读取分布式缓存、回源到 Tomcat 进行处理。

  3. 应用 Nginx 会将请求上报给实时热点发现系统,如使用 UDP 直接上报请求,或者将请求写到本地 kafka,或者使用 flume 订阅本地 Nginx 日志。上报给实时热点发现系统后,它将进行热点统计(可以考虑 storm 实时计算)。

  4. 根据设置的阈值将热点数据推送到应用 Nginx 本地缓存。

热点限制

限制更多的是一种保护机制,限制的办法也有很多,例如对被访问商品的 ID 做一致性 Hash,然后根据 Hash 做分桶,每个分桶设置一个处理队列,这样可以把热点商品限制在一个请求队列里,防止因某些热点商品占用太多的服务器资源,而使其他请求始终得不到服务器的处理资源。

多级缓存

使用 Java 堆内存来存储缓存对象。使用堆缓存的好处是不需要序列化/反序列化,是最快的缓存。缺点也很明显,当缓存的数据量很大时,GC(垃圾回收)暂停时间会变长,存储容量受限于堆空间大小。


一般通过软引用/弱引用来存储缓存对象,即当堆内存不足时,可以强制回收这部分内存释放堆内存空间。一般使用堆缓存存储较热的数据。可以使用 Caffeine Cache 实现。


可以参考文章:Caffeine Cache-高性能Java本地缓存组件


现在应用最多的是多级缓存方案,就好比 CPU 也有 L1,L2,L3。


Nginx 缓存 → 分布式 Redis 缓存(可以使用 Lua 脚本直接在 Nginx 里读取 Redis)→堆内存。


整体流程如下:


  1. 接入 Nginx 将请求负载均衡到应用 Nginx,此处常用的负载均衡算法是轮询或者一致性哈希。轮询可以使服务器的请求更加均衡,而一致性哈希可以提升应用 Nginx 的缓存命中率。

  2. 应用 Nginx 读取本地缓存(本地缓存可以使用 LuaShared Dict、Nginx Proxy Cache(磁盘/内存)、LocalRedis 实现)。如果本地缓存命中,则直接返回,使用应用 Nginx 本地缓存可以提升整体的吞吐量,降低后端压力,尤其应对热点问题非常有效。

  3. 如果 Nginx 本地缓存没命中,则会读取相应的分布式缓存(如 Redis 缓存,还可以考虑使用主从架构来提升性能和吞吐量),如果分布式缓存命中中,则直接返回相应数据(并回写到 Nginx 本地缓存)。

  4. 如果分布式缓存也没有命中,则会回源到 Tomcat 集群,在回源到 Tomcat 集群时,也可以使用轮询和一致性哈希作为负载均衡算法。

  5. 在 Tomcat 应用中,首先读取本地堆缓存。如果有,则直接返回(并会写到主 Redis 集群)。

  6. 作为可选部分,如果步骤 4 没有命中,则可以再尝试一次读主 Redis 集群操作,目的是防止当从集群有问题时的流量冲击。

  7. 如果所有缓存都没有命中,则只能查询 DB 或相关服务获取相关数据并返回。

  8. 步骤 7 返回的数据异步写到主 Redis 集群,此处可能有多个 Tomcat 实例同时写。

流量削峰

秒杀答题

添加秒杀答题。有以下两个目的:


  • 第一个目的是防止部分买家使用秒杀器在参加秒杀时作弊。

  • 第二个目的其实就是延缓请求,起到对请求流量进行削峰的作用,从而让系统能够更好地支持瞬时的流量高峰。这个重要的功能就是把峰值的下单请求拉长,从以前的 1s 之内延长到 2s~10s。这样一来,请求峰值基于时间分片了。

限流

请求排队

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

  • 数据库层做排队。应用层只能做到单机的排队,但是应用机器数本身很多,这种排队方式控制并发的能力仍然有限,所以如果能在数据库层做全局排队是最理想的。阿里的数据库团队开发了针对这种 MySQL 的 InnoDB 层上的补丁程序(patch),可以在数据库层上对单行记录做到并发排队。


你可能有疑问了,排队和锁竞争不都是要等待吗,有啥区别?


如果熟悉 MySQL 的话,你会知道 InnoDB 内部的死锁检测,以及 MySQL Server 和 InnoDB 的切换会比较消耗性能。




对于分布式限流,目前遇到的场景是业务上的限流,而不是流量入口的限流。流量入口限流应该在接入层完成,而接入层笔者一般使用 Nginx。业务的限流一般用 Redis + Lua 脚本。

库存扣减

千万不要超卖,这是大前提。超卖直接导致的就是资损。

库存扣减方式

在正常的电商平台购物场景中,用户的实际购买过程一般分为两步:下单和付款。你想买一台 iPhone 手机,在商品页面点了“立即购买”按钮,核对信息之后点击“提交订单”,这一步称为下单操作。下单之后,你只有真正完成付款操作才能算真正购买,也就是俗话说的“落袋为安”。


  • 下单减库存,即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。但是你要知道,有些人下完单可能并不会付款。

  • 下单减库存有多种方式保证不超卖:一种是在应用程序中通过事务来判断,即保证减后库存不能为负数,否则就回滚;另一种办法是直接设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行 SQL 语句来报错;再有一种就是使用 CASE WHEN 判断语句,例如这样的 SQL 语句:


UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
复制代码


  • 付款减库存,即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。

  • 预扣库存,这种方式相对复杂一些,买家下单后,库存为其保留一定的时间(如 10 分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。在买家付款前,系统会校验该订单的库存是否还有保留:如果没有保留,则再次尝试预扣;如果库存不足(也就是预扣失败)则不允许继续付款;如果预扣成功,则完成付款并实际地减去库存。


先说第一种,"下单减库存",可能导致恶意下单


正常情况下,买家下单后付款的概率会很高,所以不会有太大问题。但是有一种场景例外,就是当卖家参加某个活动时,此时活动的有效时间是商品的黄金售卖时间,如果有竞争对手通过恶意下单的方式将该卖家的商品全部下单(雇几个人下单将你的商品全都锁了),让这款商品的库存减为零,那么这款商品就不能正常售卖了。要知道,这些恶意下单的人是不会真正付款的,这正是"下单减库存"方式的不足之处。


既然,从而影响卖家的商品销售,那么有没有办法解决呢?你可能会想,采用"付款减库存"的方式是不是就可以了?的确可以。但是,"付款减库存"又会导致另外一个问题:库存超卖。


假如有 100 件商品,就可能出现 300 人下单成功的情况,因为下单时不会减库存,所以也就可能出现下单成功数远远超过真正库存数的情况,这尤其会发生在做活动的热门商品上。这样一来,就会导致很多买家下单成功但是付不了款,买家的购物体验自然比较差。


超卖情况可以区别对待:对普通的商品下单数量超过库存数量的情况,可以通过补货来解决;但是有些卖家完全不允许库存为负数的情况,那只能在买家付款时提示库存不足


预扣库存方案确实可以在一定程度上缓解上面的问题。但没有彻底解决,比如针对恶意下单这种情况,虽然把有效的付款时间设置为 10 分钟,但是恶意买家完全可以在 10 分钟后再次下单,或者采用一次下单很多件的方式把库存减完。针对这种情况,解决办法还是要结合安全和反作弊的措施来制止。


例如,给经常下单不付款的买家进行识别打标(可以在被打标的买家下单时不减库存)、给某些类目设置最大购买件数(例如,参加活动的商品一人最多只能买 3 件),以及对重复下单不付款的操作进行次数限制等。

库存扣减操作异步化,更新操作转化为插入操作

方案的核心思路:将库存扣减异步化,库存扣减流程调整为下单时只记录扣减明细(DB 记录插入),异步进行真正库存扣减(更新)。


大量请求对同一数据行的的竞争更新,会导致数据库的性能急剧下降,甚至发生数据库分片的连接被热点单商品扣减。


前置校验库存,从 db 更换为 redis,库存扣减操作,从更新操作,直接修改为插入操作(性能角度,插入锁比更新锁的性能高)


热点发现系统(中间件)会通过消息队列的方式通知应用,应用对库存进行热点打标。一但库存不再是热点(热点失效),则会进行库存热点重置。

库存分段

将商品库存分开放,分而治之。例如,原来的秒杀商品的 id 为 10001,库存为 1000 件,在 Redis 中的存储为(10001, 1000),我们将原有的库存分割为 5 份,则每份的库存为 200 件,此时,我们在 Redia 中存储的信息为(10001_0, 200),(10001_1, 200),(10001_2, 200),(10001_3, 200),(10001_4, 200)。将 key 分散到 redis 的不同槽位中,这就能够提升 Redis 处理请求的性能和并发量。

隔离

单个热点商品会影响整个数据库的性能,导致 0.01%的商品影响 99.99%的商品的售卖,这是我们不愿意看到的情况。一个解决思路是遵循前面介绍的原则进行隔离,把热点商品放到单独的热点库中。但是这无疑会带来维护上的麻烦,比如要做热点数据的动态迁移以及单独的数据库等。

线程隔离

线程隔离主要是指线程池隔离,在实际使用时,我们会把请求分类,然后交给不同的线程池处理。当一种业务的请求处理发生问题时,不会将故障扩散到其他线程池,从而保证其他服务可用。



随着对系统可用性的要求,会进行多机房部署,每个机房的服务都有自己的服务分组,本机房的服务应该只调用本机房服务,不进行跨机房调用。其中,一个机房服务发生问题时,可以通过 DNS/负载均衡将请求全部切到另一个机房,或者考虑服务能自动重试其他机房的服务,从而提升系统可用性。



核心业务以及非核心业务可以放在不同的线程池。


可以使用 Hystrix 来实现线程池隔离,参考文章: Hystrix实现熔断、降级、服务隔离

降级

所谓“降级”,就是当系统的容量达到一定程度时,是为了保证核心服务的稳定而牺牲非核心服务的做法。


降级方案可以这样设计:当秒杀流量达到 5w/s 时,把成交记录的获取从展示 20 条降级到只展示 5 条。“从 20 改到 5”这个操作由一个开关来实现,也就是设置一个能够从开关系统动态获取的系统参数。


降级无疑是在系统性能和用户体验之间选择了前者,降级后肯定会影响一部分用户的体验,例如在双 11 零点时,如果优惠券系统扛不住,可能会临时降级商品详情的优惠信息展示,把有限的系统资源用在保障交易系统正确展示优惠信息上,即保障用户真正下单时的价格是正确的。所以降级的核心目标是牺牲次要的功能和用户体验来保证核心业务流程的稳定,是一个不得已而为之的举措

拒绝服务

如果限流还不能解决问题,最后一招就是直接拒绝服务了。


当系统负载达到一定阈值时,例如 CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求,这种方式是最暴力但也最有效的系统保护方式。


在最前端的 Nginx 上设置过载保护,当机器负载达到某个值时直接拒绝 HTTP 请求并返回 503 错误码,在 Java 层同样也可以设计过载保护。

负载均衡

在项目的架构中,我们一般会同时部署 LVS 和 Nginx 来做 HTTP 应用服务的负载均衡。也就是说,在入口处部署 LVS,将流量分发到多个 Nginx 服务器上,再由 Nginx 服务器分发到应用服务器上


为什么这么做呢?


主要和 LVS 和 Nginx 的特点有关,LVS 是在网络栈的四层做请求包的转发,请求包转发之后,由客户端和后端服务直接建立连接,后续的响应包不会再经过 LVS 服务器,所以相比 Nginx,性能会更高,也能够承担更高的并发。


可 LVS 缺陷是工作在四层,而请求的 URL 是七层的概念,不能针对 URL 做更细致地请求分发,而且 LVS 也没有提供探测后端服务是否存活的机制;而 Nginx 虽然比 LVS 的性能差很多,但也可以承担每秒几万次的请求,并且它在配置上更加灵活,还可以感知后端服务是否出现问题。


因此,LVS 适合在入口处,承担大流量的请求分发,而 Nginx 要部在业务服务器之前做更细维度的请求分发


我给你的建议是,如果你的 QPS 在十万以内,那么可以考虑不引入 LVS 而直接使用 Nginx 作为唯一的负载均衡服务器,这样少维护一个组件,也会减少系统的维护成本。


但对于 Nginx 来说,我们要如何保证配置的服务节点是可用的呢?


这就要感谢淘宝开源的 Nginx 模块 nginx_upstream_check_moduule 了,这个模块可以让 Nginx 定期地探测后端服务的一个指定的接口,然后根据返回的状态码,来判断服务是否还存活。当探测不存活的次数达到一定阈值时,就自动将这个后端服务从负载均衡服务器中摘除。


它的配置样例如下:


upstream server {    server 192.168.1.1:8080;    server 192.168.1.2:8080;    check interval=3000  rise=2  fall=5  timeout=1000  type=http  default_down=true    check_http_send "GET /health_check HTTP/1.0\r\n\n\n\n"; //检测URL    check_http_expect_alivehttp_2xx; //检测返回状态码为 200 时认为检测成功}
复制代码


不过这两个负载均衡服务适用于普通的 Web 服务,对于微服务多架构来说,它们是不合适的。因为微服务架构中的服务节点存储在注册中心里,使用 LVS 就很难和注册中心交互,获取全量的服务节点列表。


另外,一般微服务架构中,使用的是 RPC 协议而不是 HTTP 协议,所以 Nginx 也不能满足要求。


所以,我们会使用另一类的负载均衡服务,客户端负载均衡服务,也就是把负载均衡的服务内嵌在 RPC 客户端中

DNS 负载均衡

当我们的应用单实例不能支撑用户请求时,此时就需要扩容,从一台服务器扩容到两台、几十台、几百台。


然而,用户访问时是通过如 http://www.jd.com 的方式访问,在请求时,浏览器首先会查询 DNS 服务器获取对应的 IP,然后通过此 IP 访问对应的服务。


因此,一种方式是 www.jd.com 域名映射多个 IP,但是,存在一个最简单的问题,假设某台服务器重启或者出现故障,DNS 会有一定的缓存时间,故障后切换时间长,而且没有对后端服务进行心跳检查和失败重试的机制。

Nginx 负载均衡

对于一般应用来说,有 Nginx 就可以了。但 Nginx 一般用于七层负载均衡,其吞吐量是有一定限制的。为了提升整体吞吐量,会在 DNS 和 Nginx 之间引入接入层,如使用 LVS(软件负载均衡器)、F5(硬负载均衡器)可以做四层负载均衡,即首先 DNS 解析到 LVS/F5,然后 LVS/F5 转发给 Nginx,再由 Nginx 转发给后端 Real Server。



对于一般业务开发人员来说,我们只需要关心到 Nginx 层面就够了,LVS/F5 一般由系统/运维工程师来维护。Nginx 目前提供了 HTTP (ngx_http_upstream_module)七层负载均衡,而 1.9.0 版本也开始支持 TCP(ngx_stream_upstream_module)四层负载均衡。


参考文章:Nginx负载均衡配置


一致性 hash 算法最好在 lua 脚本里指定。


Nginx 商业版还提供了 least_time,即基于最小平均响应时间进行负载均衡。


Nginx 的服务检查是惰性的,Nginx 只有当有访问时后,才发起对后端节点探测。如果本次请求中,节点正好出现故障,Nginx 依然将请求转交给故障的节点,然后再转交给健康的节点处理。所以不会影响到这次请求的正常进行。但是会影响效率,因为多了一次转发,而且自带模块无法做到预警。


参考文章:Nginx被动健康检查和主动健康检查


  • Nginx 服务器是服务端的负载均衡,而分布式服务实现是客户端的负载均衡。

  • Nginx 是集中式的负载均衡,分布式服务是消费者内部线程实现的负载均衡。

数据异构

比如对于订单库,当对其分库分表后,如果想按照商家维度或者按照用户维度进行查询,那么是非常困难的,因此可以通过异构数据库来解决这个问题。可以采用下图的架构。



异构数据主要存储数据之间的关系,然后通过查询源库查询实际数据。不过,有时可以通过数据冗余存储来减少源库查询量或者提升查询性能。


针对这类场景问题,最常用的是采用“异构索引表”的方式解决,即采用异步机制将原表的每一次创建或更新,都换另一个维度保存一份完整的数据表或索引表,拿空间换时间。


也就是应用在插入或更新一条订单 ID 为分库分表键的订单数据时,也会再保存一份按照买家 ID 为分库分表键的订单索引数据,其结果就是同一买家的所有订单索引表都保存在同一数据库中,这就是给订单创建了异构索引表。


参考文章:阿里巴巴中台战略--数据库分库分表之异构索引表


感谢阅读,希望文章对你有些帮助。

发布于: 刚刚阅读数: 4
用户头像

Java随想录

关注

Java开发工程师 2021-12-26 加入

Java工程师,写博客的初衷是为了沉淀我所学习,累积我所见闻,分享我所体验。希望和更多的人交流学习

评论

发布
暂无评论
如何设计一个秒杀系统?_Java_Java随想录_InfoQ写作社区