跳转至

02 蓄势待发:秒杀系统架构设计和环境准备

你好,我是志东,欢迎和我一起从零打造秒杀系统。

我们知道,系统的设计是个由巨入细的过程,想去设计好它,那你首先得去了解清楚它。就像上节课我们对HTTP请求所走链路的介绍,学完后你就会明白,做秒杀系统设计时,会用到哪些层级系统,并且每个层级系统可以做什么事情。

今天我们要做的就是给每个层级系统做最合适的技术选型和职能边界划分,最终实现让各系统、技术做它们所擅长的事情,并在最后搭建起我们的开发依赖环境。

那如何给层级系统做技术选型和职能边界划分呢?我们通常都说,没有最好的技术,只有最契合当下业务场景的技术,所以我们得先了解一下,如果使用我们传统的架构系统来支持秒杀业务,可能会出现哪些问题。只有清楚了要面对的问题,我们才能做针对性的思考和优化。

所以这节课我们将重点分析传统架构设计的特点,接着介绍最新的秒杀系统架构,并做好技术选型和环境准备。

传统秒杀系统架构

下面先看一个大家常用的系统功能架构图:

这种功能结构以及系统架构,是我们非常熟悉的。在这种方式下,Nginx只做反向代理和负载均衡,甚至这层对我们做业务开发的研发人员来说,都是无感知的,一般运维同事在做生产环境搭建时,都会帮我们配好。研发人员更多的是在开发Web服务和RPC服务,我们把页面以及页面所依赖的静态资源都放到Web服务中,同时Web服务还提供业务接口,RPC服务提供一些支撑服务。

如果这是个ToB的运营管理系统,这样没有什么问题,因为请求量非常低,系统基本不会有太大的负载。但是对于ToC,且瞬时流量非常大的情况,问题就会暴露出来,那它究竟会有哪些问题呢?

域名与带宽问题

我们从最基础的讲起。如果Web服务既提供H5页面、静态资源,同时也提供业务接口,这就意味着所有的请求使用的都是同一个域名,在活动刚开始时,大家都点击抢购按钮进结算页,而结算页页面拉取静态资源,会占用很多带宽资源。

这在活动开始的瞬间,带宽资源很稀缺的情况下,可能会出现用户进不了结算页,或者进了结算页却不能正常渲染页面的问题,导致抢购体验大幅下降。

Web服务器性能问题

接着,讲一个关键问题。我们一般部署Web服务,都是使用Apache的Tomcat来部署的,Tomcat在处理请求的时候,是通过线程去处理的。

这样的问题就是如果瞬时的大量请求过来,线程池中的线程不够用,Tomcat就会瞬间新建很多线程,直至达到配置的最大线程数,如果线程数设置的过大,这个过程可能会直接将机器的CPU打满,导致机器死掉。即使没有挂掉,在高负载下,当设置的等待队列也满了之后,后面的请求都会被拒绝连接,直到有空出的资源去处理新请求。这时候你可能会想,我加机器分摊流量不就行了?可以是可以,但由此增加的活动成本不知道你的老板会不会买单?

当然了,这个过程中,还会伴有热点数据读写、库存超卖等问题,这些细节也非常重要,我会在后面的课程中一一给你展开说明。

新的秒杀系统架构

上面我们谈到了传统系统架构的局限性,那么我们新的系统该如何设计才能避免出现以上问题呢?结合上节课对于各链路层级的介绍,我画了一张新的功能结构与系统架构图:

首先新系统我们依然保留了HTTP服务常用的层级调用关系,即 Nginx->Web服务->RPC服务,这也是绝大部分公司都会使用的一种系统结构。

其次将原先由Web服务提供的静态资源放到了CDN(CDN是全国都有的服务器,客户端可以根据所处位置自动就近从CDN上拉取静态资源,速度更快),来大大减轻抢购瞬时秒杀域名的负担。

最后,同时也是我们所做的最大改变,就是将Nginx的职责放大,前置用来做Web网关,承担部分业务逻辑校验,并且增加黑白名单、限流和流控的功能,这其实也是考虑到我们的秒杀业务特点所做的调整。这种在Nginx里写业务的做法在很多大公司里都是很常见的,像京东是用来做商详、秒杀的业务网关,美团用来做负载均衡接入层,12306用来做车票查询等等,他们的共同特点都是要面对高并发的业务场景,这也说明在这种业务场景下,我们的设计是得到了真实实践和广泛认可的。

而这么做的目的,就是要充分利用Nginx的高并发、高吞吐能力,并且非常契合我们秒杀业务的特点,即入口流量大。但流量组成却非常的混杂,这些请求中,一部分是刷子请求,一部分是无效请求(传参等异常),剩下的才是正常请求,这个的比例可能是6:1:3,所以需要我们在网关层尽可能多地接收流量进来,并做精确地筛选,将真正有效的3成请求分发到下游,剩余的7成拦截在网关层。不然把这些流量都打到Web服务层,Web服务再新起线程来处理刷子和无效请求,这是种资源的浪费。

所以网关层对秒杀系统而言,至关重要,而Nginx刚好可以胜任此项任务。由此可见,Nginx在我们的系统设计中,扮演着非常重要的角色,但你对Nginx也许没那么了解,别急,接下来我就给你简单介绍一下Nginx,并带你解开Nginx在高并发下仍具有高性能的秘密。

Nginx介绍

Nginx最早被发明出来,就是来应对互联网高速发展下,出现的并发几十万、上百万的网络请求连接场景的,传统Apache服务器无法有效地解决这种问题,而Nginx却具有并发能力强、资源消耗低的特性。

总的来说,Nginx有5大优点,即模块化、事件驱动、异步、非阻塞、多进程单线程。以下是Nginx的架构原理图:

Nginx是由一个master进程和多个worker进程(可配置)来配合完成工作的。其中master进程负责Nginx配置文件的加载和worker进程的管理工作,而worker进程负责请求的处理与转发,进程之间相互隔离,互不干扰。同时每个进程中只有一个线程,这就省去了并发情况下的加锁以及线程的切换带来的性能损耗。

但一个线程能支持高并发的业务场景吗?

这就要说到Nginx的工作模型。以Linux为例,其采用的是epoll模型(即事件驱动模型),该模型是IO多路复用思想的一种实现方式,是异步非阻塞的,什么意思呢?就是一个请求进来后,会由一个worker进程去处理,当程序代码执行到IO时,比如调用外部服务或是通过upstream分发请求到后端Web服务时,IO是阻塞的,但是worker进程不会一直在这等着,而是等IO有结果了再处理,在这期间它会去处理别的请求,这样就可以充分利用CPU资源去处理多个请求了。

这里你还可以思考这样一个问题:Linux支持的以IO多路复用思想来实现的模型还有select和poll,为什么选择了epoll呢?因为epoll的效率更高。

举个例子,刚刚我们上面说到worker在处理请求到IO时,不会阻塞等待,而是去干其他事情,等IO有结果了再回头处理,那worker进程怎么知道刚刚的IO处理完毕了呢?

假设一个work process处理了1000个连接,但其中只有10个IO完成了,并可以继续往下执行,select/poll的做法是遍历这1000个FD(File Description,可以理解成每个建立了连接的一个标识),找到那10个就绪状态的,并把没做完的事情继续做完,这样检索的效率明显很低。所以epoll的做法是当这10个IO准备就绪时,通过系统的回调函数将FD放到一个专门的就绪列表中,这样系统只需要去找这个就绪列表就可以了,这大大提高了系统的响应效率。当然这只是epoll的其中一个优点,具体三种模型的对比,你可以自行去了解一下,网上相关的资料有很多,或者我们在留言区讨论也是可以的。

正是多进程+事件驱动的工作原理,才使得Nginx具有非常良好的性能表现,同时Nginx的模块化,也能够支撑强大的第三方自定义工具模块,让你的开发更加灵活自由。

OpenResty介绍

我们知道,Nginx的底层模块一般都是用C语言写的,如果我们想在Nginx的基础之上写业务逻辑,还得借助OpenResty,它是Nginx的一个社区分支。这里我也简单介绍一下它。

OpenResty是一个基于 Nginx 与 Lua 的高性能 Web 平台,它使我们具备在Nginx上使用Lua语言来开发业务逻辑的能力,并充分利用 Nginx 的非阻塞 IO 模型,来帮助我们非常方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

这里插一句,为什么要用Lua语言来做Nginx开发呢?这就要说到Lua语言的特点了,Lua的线程模型是单线程多协程的模式,而Nginx刚好是单进程单线程,天生的完美搭档。同时Lua是一种小巧的脚本语言,语法非常的简单,很容易学习掌握,所以对于新语言你先不要有排斥心理,我会在后面的课程中慢慢向你展示讲解。

Web/RPC服务技术选型

以上我介绍完了Nginx服务层的技术选型,同时也讲解了为什么这么选的原因,下面就轮到Web服务和RPC服务了。

这里大的框架选择,其实就没有太多要求了,只要能提供我们需要的能力即可,比如基础框架是使用SpringMVC还是SpringBoot,持久层是喜欢用MyBatis还是JPA,数据库是用MySQL还是Oracle,这些都可以根据你的个人使用习惯或者所在公司的技术栈做灵活变通。

同时对于已经有秒杀系统,但是想要做优化的情况,也完全不用担心,跟着我学习之后,你只需将旧系统中的部分轻业务逻辑迁移到Nginx层来,体量最大的业务逻辑代码基本都不用动的,并且旧系统中的一些优化点,也都有单独的技术来实现,而这些都不需要太多的学习成本和迁移成本。

那么为了更好的本地开发教学,这里介绍一下我所使用的技术栈:Web服务和RPC服务的基础框架都是使用SpringMVC,RPC框架使用的是Dubbo,数据库使用免费开源的MySQL,分布式缓存数据库使用Redis,这应该也是大多数公司会使用的技术栈。

在技术选型和层级系统职能划分都确定了之后,接下来就让我们动起手来,先把开发的依赖环境准备好。

环境准备

以下是我在本地Mac上的安装方式,如果你是其他系统,可以找对应的安装方法,对于程序员来说应该是没难度的。如果Mac上没有安装过Homebrew,可以安装一下,这是一个Mac软件的工具包,很好用。以下操作都是在Mac的终端里输入相关命令来完成操作的。

Homebrew安装

ruby -e “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install))”

OpenResty安装

  1. brew install openresty/brew/openresty

从Homebrew安装OpenResty。

  1. export PATH=/usr/local/Cellar/openresty/1.19.3.2_1/nginx/sbin:$PATH

安装完成之后,默认的安装位置在 /usr/local/Cellar/openresty/1.19.3.2_1,这时我们设置下环境变量(即告诉终端输入的命令去哪里找)。

  1. Nginx -V

然后就可以查看OpenResty是否安装成功。如下图,执行红框内的命令,出现绿框的输出内容,即表示安装成功了。

图片

  1. 然后我们就可以测试下Nginx是否好用,所以我们在本地新建了个nginx.conf配置文件,就放在/Users/~/Documents/seckillproject/nginx/conf下,并且在nginx文件夹下新建logs文件夹,用于log日志的输出,新建后的文件目录结构如下:

图片

nginx.conf的内容就用官方的模板,输出个hello world:

worker_processes  1;
error_log logs/error.log;
events {
    worker_connections 1024;
}
http {
    server {
        listen 8080;
        location / {
            default_type text/html;
            content_by_lua_block {
                ngx.say("<p>hello, world</p>")
            }
        }
    }
}

5. cd /Users/wangzhangfei5/Documents/seckillproject/nginx

进入到我们新建的nginx文件夹下。

  1. nginx -p `pwd`/ -c conf/nginx.conf

启动Nginx服务,这时输入 `ps -ef|grep nginx` 可以查看起来Nginx进程,有两个,一个master ,一个worker。

图片

7. curl http://localhost:8080

访问本地8080端口,可以看到,输出了"<p>hello, world</p>",也可以在浏览器输入 http://localhost:8080,看到 hello,world 的返回。

图片

8. nginx -p `pwd` -s stop

停止Nginx服务。

MySQL安装

brew install mysql@5.7

也可以通过官网下载,根据系统版本,下载好对应包之后直接安装,完成安装之后可以测试下。

1. export PATH=/usr/local/opt/mysql@5.7/bin:$PATH

默认的安装位置在 /usr/local/opt/mysql@5.7 这时我们设置下环境变量(即告诉终端输入的命令去哪里找)。

2. mysql.server start

启动MySQL。

3. mysql_secure_installation

设置数据库密码,按照对应的提示,让选择Y/N时,输入Y,然后会让选择密码等级,一共三个级别 0,1,2 强度由低到强,选择后设置密码,并记住刚设的密码。

4. mysql -uroot -p

登录数据库,这时会提示你输入密码,输入刚设置的密码即可进入。

5. show databases;

查看下当前的所有库,到这里也说明我们的MySQL准备好了,可以建库建表了。

图片

6. mysql.server stop

control+z退出MySQL后,执行该命令,关闭MySQL。

Redis安装

brew install redis

安装完成之后 ,依次执行以下命令进行测试。

  1. /usr/local/opt/redis/bin/redis-server /usr/local/etc/redis.conf

启动Redis服务端,如下图绿框,在安装好后提示我们如何启动Redis,按照提示输入命令,便可以看到启动成功等待连接。

图片

  1. redis-cli -h 127.0.0.1 -p 6379

这时新建一个终端窗口,模拟客户端连接Redis服务,如下图所示,可以set一个值,并get查询出来,说明Redis也正常安装成功并可以使用。

图片

  1. 服务端停止,可以直接control+z退出,也可以 sudo pkill redis-server 客户端断开连接 redis-cli shutdown。

到这我们主要的依赖环境都已经搭好了,而项目的搭建与开发,我会在下节课为你讲解。

小结

在这节课里,针对秒杀系统,我们将传统的架构设计与我们新的架构设计做了一个对比,可以看出传统架构设计的局限性,其中仅列举了域名带宽问题和Tomcat服务器性能问题,这也是我们从宏观上做技术选型时,就需要去认真思考的问题。

而针对这两点,我们也给出了我们的答案,即利用Nginx在高并发下仍具有高性能的特性,将Web网关职能前置,尽量在流量入口处拦截掉风险流量以及缩短请求链路,保护下游系统,并提高服务的响应速度。

有了大的方向指导,我们便可以针对一些更细的技术点去做优化和设计,比如哪种限流算法更好,怎么能在高并发下保证库存不超卖等等这些。

同时在做技术选型时,我也尽可能地使用了多数公司都在使用的技术栈,以降低你的学习成本。但是用Lua语言来做Nginx业务开发,或许还有不少同学是第一次用,不过不着急,后面跟着我慢慢学,相信你自己一定可以,或许这还将成为你比别人厉害的法宝!

思考题

关于Tomcat的思考,为什么Tomcat也支持NIO,但性能却比Nginx差那么多呢?

以上就是这节课的全部内容,欢迎你在评论区和我讨论问题,交流想法!

精选留言(15)
  • Geek_5b2ab1 👍(2) 💬(1)

    踩坑之一: nginx -p pwd/ -c conf/nginx.conf 应为nginx -p ·pwd·/ -c conf/nginx.conf pwd左右的是反引号,表示执行pwd命令,获取当前工作目录路径

    2021-09-30

  • 👍(8) 💬(4)

    文中说的epoll是异步非阻塞的结论我提出挑战,所谓的多路复用器,不管是select、poll、epoll不都是同步非阻塞么?

    2021-11-13

  • 黄序 👍(7) 💬(1)

    1)两者的使用侧重点不同,Nginx主要是反向代理以及负载均衡,Tomcat是一个servlet容器,需要处理各种动态请求,比如说网络IO以及和数据库的交互; 2)机制不同:作者大大提及到,lua脚本底层用到了携程,Tomcat使用的是线程,性能上也会存在差距

    2021-10-01

  • superyins 👍(5) 💬(0)

    对于M1 Mac的坑,安装位置不对。会是:/opt/homebrew/Cellar/openresty/版本号/...

    2021-10-29

  • Z.G 👍(4) 💬(0)

    老师你好,问下如果是基于现代云原生的K8s基础设施,还需要Nginx吗?又应该怎么处理呢?

    2021-09-30

  • nana👄 👍(4) 💬(0)

    麻烦速更啊啊啊,完全不够看

    2021-09-28

  • nana👄 👍(3) 💬(0)

    tomcat,多个请求会开启多个线程来处理情书,涉及到线程的切换和锁的开销。nginx是单线程多进程,请求过来就少了那部分开销。不知道回答对不对,希望作者每期都回答下上期问题,谢谢。

    2021-10-12

  • 陈强 👍(2) 💬(0)

    为什么tomcat也支持NIO但是慢的原因。 1、tomcat的请求接入使用的是Java NIO的selectable模型,也是异步事件监听的模式,并不慢。(并不清楚selectable模型是不是基于epoll实现的) 2、tomcat同时需要对请求进行处理,默认情况下(不提前加载servlet和jsp模板)第一次处理请求会涉及类的加载和jsp文件的模板翻译,还有本身的业务处理,这些过程可能比较慢。 3、selectable是否是Java语言对epoll的抽象?不太清楚。不过无论是不是,在监听到事件后,selectionKey集合本身还是要交给线程池去执行的,这涉及大量的对象创建以及线程池是否预热。 4、毕竟Java语言,底层还是要掉C和C++的库,不知道这算不算一点,哈哈哈(不会C语言,具体调啥我也不知道...这就是半路出家的痛苦...)。 感觉自己回答的有些不严谨,假设不涉及业务处理,单从请求接入这个角度的话,感觉JavaNIO应该很能打,希望老师点评。

    2021-11-09

  • 送过快递的码农 👍(2) 💬(1)

    我觉得可能是tomcat 和 Nginx的侧重点是不一样,nginx讲究主要管这个请求,从哪儿来,到哪儿去。它不求解决所有问题,是一个专业的中转站。而tomcat是一个servlet容器,它讲究怎么吃下这些请求,并且都处理好,因此,它需要适配各种消息体,处理不同的五花八门的请求,甚至于还得考虑,cookie,session,上下文等问题。虽然tomcat很伟大的用servlet和servlet容器的方式来解耦各个业务请求,减少了开发成本,但是性能成本确实挥之不去了

    2021-09-27

  • Nullrable 👍(1) 💬(0)

    以上工具用docker,管理比较方便

    2022-05-02

  • pc 👍(1) 💬(1)

    有一个概念没有理解:(上节课也有提到)“Web 服务既提供 H5 页面、静态资源,同时也提供业务接口”。这是指前后端没有分离吗?相当于是一个服务又提供页面,又提供接口的意思吗?

    2022-02-03

  • Feng 👍(1) 💬(0)

    Nginx只做请求和响应的转发,以及轻业务处理,大部分时间都花在IO上;Nginx的IO采用的是单线程、异步非阻塞的模式,避免打开IO通道等待数据传输的过程,缩短了线程调度和IO处理的时间。 Tomcat的IO线程一般控制在400以内,如果每个请求的处理时间为300ms,一个线程3QPS,总体上一个Tomcat的并发能力为1000QPS。

    2021-12-04

  • intomymind 👍(1) 💬(3)

    谁能帮忙解答下,在IO多路复用情况下,客户端发起请求之后是在等着返回结果吗,如果不是,那结果是如何接受到的

    2021-11-27

  • 酱紫的小白兔 👍(1) 💬(0)

    为什么H5和html不放在nginx或者cdn呢?

    2021-10-18

  • 到不了的塔 👍(0) 💬(0)

    这是因为tomcat对每一个请求,都会起一个线程去处理,而不像Nginx,一个线程可以处理多个请求

    2024-03-05