写在前面

我们知道在传统的Web服务器中,每个客户端连接需要一个单独的进程或者线程来处理,在切换任务的时候需要将CPU切换到新的任务并创建一个新的运行时上下文,这样不仅会消耗额外的内存还会花费一定的CPU时间。当并发请求增加时,服务器负担将会加重,进而对性能产生一定影响。而本文将学习的Nginx服务器则很好的解决了这个问题。

Nginx简介

Nginx是一个高性能的HTTP(处理静态文件)和反向代理(负载均衡)服务器,也是一个IMAP/POP3/SMTP代理服务器。现在大部分的Web服务器都使用Nginx作为负载均衡器。

Nginx特点

(1)速度更快。在单次请求下会得到更快的响应,而在高并发环境下,Nginx比其他Web服务器有更快的响应。
(2)高扩展性。Nginx基于模块化设计,由多个耦合度极低的模块组成,具有很高的扩展性。这种强扩展性,非常适合高流量的网站系统。
(3)高可靠性。Nginx的可靠性来源于其核心代码的优秀设计,模块设计的简单性。Nginx官方提供的常用模块非常稳定,每个worker进程相对独立,master进程在一个worker进程出错时可以快速拉起一个新的worker子进程以继续提供服务。
(4)低内存消耗。一般情况下,1万个非活跃的HTTP Keep-Alive连接在Nginx中仅消耗2.5MB的内存,这是Nginx支持高并发的基础。理论上来说Nginx支持的并发上限取决于机器的内存,只要内存足够,单机实现10万、100万以上的并发都是可能的,而实际上10万个并发已经都是基本操作了。
(5)热部署。前面提到master进程和worker进程是分离设计的,这使得Nginx支持热部署,也就是可以在不停掉当前服务的情况下,对Nginx可执行文件进行升级。除此之外,Nginx还支持不停止服务就可以更新配置文件中的配置项,和更换日志文件等功能。
(6)使用最自由的BSD协议。使用BSD协议就意味着用户不仅可以免费使用Nginx,还可以直接或者间接修改Nginx的源码,然后发布使用。
(7)CPU亲和。所谓的CPU亲和是指将CPU核心和Nginx工作进程绑定,将Nginx每个worker进程固定在一个CPU上执行,可以减少切换CPU的cache miss,能获得更好的性能。

Nginx社区分支

(1)Openresty。作者章宜春,其最大特点就是引入了ngx_lua 模块,支持使用Lua开发插件,且集合了很多丰富的模块以及 Lua库。
(2)Tengine。由淘宝团队开发,主要引入了淘宝自身业务需要的新特性。
(3)Nginx官方版本。它更新迭代较快,提供免费版本和商业版本。当然在官方版本中也有三种类型:Mainline Version是主线版,也就是开发版;Stable Version是最新稳定版,一般生产环境都使用这个;Legacy Version是历史的稳定老版本。

Nginx主要使用场景

(1)静态资源服务。通过使用本地文件系统来提供访问静态资源的服务。
(2)反向代理服务。这里面较为复杂,包括缓存、负载均衡等。
(3)API服务。主要是OpenResty。
(4)安全防御。
(5)灰度发布。
(6)静态化。
(7)消息推送。
(8)防盗链。
(9)智能路由,包括企业级的灰度测试、地图POI一键切流。
(10)图片实时压缩。

Nginx的企业应用场景

(1)作为Web服务软件使用;(2)作为反向代理或者负载均衡服务;(3)作为前端业务数据缓存服务。以上三个应用场景是Nginx在企业开发过程中比较常用的功能。

几个重要的概念

并发:指一个时间段内,有几个程序在同一个CPU上运行,但是任意时刻只有一个程序在CPU上运行。

并行:指在任意的时刻点上,有多个程序运行在多个CPU上面。(与CPU的数量保持一致)。

异步: 用于多任务情况, 多个任务之间执行没有先后顺序,可以同时运行,执行的先后顺序不会有什么影响,存在多条运行主线;(指代码调用IO操作时,不需要等待IO操作完成,就可以返回的调用方式)(多线程)。

同步: 用于多任务情况, 多个任务之间执行的时候要求有先后顺序,必须一个先执行完成之后,另一个才能继续执行, 只有一个主线;(指代码调用IO操作时,必须等待IO操作完成,才返回的调用方式)。

阻塞:从调用者的角度出发,如果在调用的时候,被卡住,不能再继续向下运行,需要等待,就说是阻塞;(调用函数的时候,当前线程被挂起,不能立即返回)。

非阻塞: 从调用者的角度出发, 如果在调用的时候,没有被卡住,能够继续向下运行,无需等待,就说是非阻塞。(调用函数的时候,当前线程不会被挂起,而是立即返回)。

HTTP的请求与响应报文

HTTP请求报文

下面是HTTP请求报文的基本语法:

请求行:①是请求方法,GET和POST是最常见的HTTP方法,除此以外还包括DELETE、HEAD、OPTIONS、PUT和TRACE;②是请求对应的URL地址,它和报文头的Host属性组成完整的请求URL;③是协议名称及版本号。

请求头:④是HTTP的报文头,报文头包含若干个属性,格式为“属性名:属性值”,服务端据此获取客户端的信息。请注意与缓存相关的规则信息,均包含在header中。

请求体:⑤是报文体,它将一个页面表单中的组件值通过param1=value1&param2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。请注意,不仅报文体可以传递请求参数,请求URL也是可以通过类似于/envythink/index.html? param1=value1&param2=value2的方式来传递请求参数。

将下来简单介绍一下HTTP请求报文头属性:
(1)Accept请求报文可通过一个“Accept”报文头属性告诉服务端,客户端可以接受什么类型的响应。举个例子,Accept:text/plain表示告诉服务器,客户端只能接收响应类型仅为纯文本的数据。

实际上Accept属性的值可以为一个或多个MIME类型的值(这些值用于描述消息内容类型的因特网标准, 消息能包含文本、图像、音频、视频以及其他应用程序专用的数据)。

(2)cookie。客户端的Cookie就是通过这个报文头属性传给服务端。这里通常会有一个sessionId,用于告诉服务端如何辨别哪些请求来自于同一个客户端发起的。其实就是通过HTTP请求报文头的Cookie属性中sessioId的值来关联的。其实也可以通过重写URL的方式将会话ID附带在每个URL的后面,不过这种方式不太安全,不建议这么操作。

(3)Referer。Referer表示这个请求是从哪个URL过来的。

(4)Cache-Control。表示对缓存进行控制,如一个请求希望响应返回的内容在客户端要被缓存一年,或不希望被缓存就可以通过设置这个报文头来达到目的。

HTTP响应报文

HTTP响应报文也是由三部分组成,不同于请求报文中的请求行、请求头和请求体;响应报文包括响应行、响应头和响应体,如下所示:

响应行:其中①报文协议及版本;②状态码及状态描述;

响应头:其中③响应报文头,也是由多个属性组成;

响应体:其中④响应报文体,即服务器返回给客户端的数据。

将下来简单介绍一下常见的HTTP响应报文头属性:
(1)Cache-Control。又看到这个属性,这里表示响应输出到客户端之后,服务端通过该报文头属性来告诉客户端如何控制响应内容的缓存。

Cache-Control这一属性常见的取值有private、public、no-cache、max-age、no-store,默认值为private。private表示客户端可以缓存;public表示客户端和代理服务器都可以缓存;no-cache表示需要使用对比缓存来验证缓存数据;max-age表示缓存的内容将在max-age秒之后失效;no-store表示所有的内容都不会缓存。

前面也说了默认值为private,缓存时间为31536000秒,也就是365天,也就是说在365天之内,再次请求这条数据,都会直接从缓存数据库中获取数据并使用。
(2)ETag。它代表一个响应服务端资源,如页面版本的报文属性,如果某个服务端资源发生了变化,那么这个ETag就会发生变化。可以将这个ETag作为Cache-Control的补充,它可以让客户端更智能的知道,什么时候从服务端获取资源,什么时候可以直接从缓存中返回响应。
(3)Location。如果你之前使用过JSP,且需要让A页面Redirect到B页面,此时其实就是让客户端再发起一个请求到A页面,那么就需要Redirect到A页面的URL,说白了就是通过响应报文头的Location属性来告知客户端。举个例子,下面的报文属性就是将客户端Redirect到envythink的首页:

1
Location: http://www.envythink.com 

(4)Set-Cookie。服务端可以设置客户端的Cookie,其原理就是通过这个响应报文头属性来实现的。

(5)Cookie机制。客户端请求服务器,如果服务器需要记录该用户状态,就可以使用Response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来,当浏览器再次请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器会检查该Cookie并据此来辨认用户状态。当然服务器还可以根据需要修改Cookie的内容。

Cookie的maxAge决定Cookie的有效期,单位为秒(Second)。Cookie可以通过getMaxAge()setMaxAge(int maxAge)方法来读写maxAge属性。

如果maxAge属性为正数,则表示该Cookie会在maxAge秒之后自动失效。如果maxAge为负数,则表示该Cookie仅在本浏览器窗口以及本窗口打开的子窗口内有效,关闭窗口后该Cookie立即失效。如果maxAge为0,则表示删除该Cookie。

请注意,Cookie并不提供修改和删除操作,如果需要修改某个Cookie,只需要新建一个同名的Cookie,然后添加到Response中来覆盖之前的Cookie。如果要删除某个Cookie,只需要新建一个同名的Cookie,并将maxAge设置为0,并添加到到Response中来覆盖之前的Cookie。举个例子,如下代码:

1
2
3
4
5
6
# 新建一个Cookie
Cookie cookie = new Cookie("username","envy");
# 设置Cookie的生命周期为0,注意不能为负数
cookie.setMaxAge(0);
# 将cookie输出到客户端
response.addCookie(cookie);

简单请求和非简单请求

在学习了上述HTTP请求和响应报文之后,接下来就来了解简单请求和非简单请求。为什么需要区分简单请求和非简单请求,那是因为浏览器处理简单请求和非简单请求的方式不一样。

如果某个请求同时满足下面两个条件,那么该请求就属于简单请求:
(1)请求方法是GET、POST或者HEAD三者中的任意一个;
(2)HTTP头信息不超过下面几个字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type。请注意Last-Event-IDContent-Type只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

除此之外,凡是不同时满足这两个条件的,都属于非简单请求。

前面说过浏览器会针对这两种请求采取不同的处理方式,那么具体的处理方式又是怎样的呢?

简单请求

对于简单请求来说,浏览器会在其头信息中增加Origin字段后直接发出,Origin字段用来说明本次请求来自哪个源(协议+域名+端口)。

如果服务器发现Origin指定的源不在许可范围内,服务器会返回一个正常的HTTP回应,浏览器取到回应之后发现回应的头信息中没有包含Access-Control-Allow-Origin字段,就抛出一个错误给XHR的error事件;

如果服务器发现Origin指定的域名在许可范围内,那么服务器返回的响应会多出几个以Access-Control-开头的头信息字段。

非简单请求

说完了简单请求,接下来开始学习非简单请求,非简单请求是指对服务器有特殊要求的请求。举个例子,如请求方法是PUT、DELETE或者Content-Type值为application/json中的某种时,浏览器会在正式通信之前,发送一次HTTP预检OPTIONS请求,先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP请求方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的XHR请求,否则报错。

跨域

所谓的跨域请求是指在浏览器上当前访问的网站向另一个网站发送请求获取数据的过程。

跨域是浏览器的同源策略决定的,它是一个重要的浏览器安全策略,用于限制一个Origin的文档或者它加载的脚本与另一个源的资源进行交互,它能够帮助阻隔恶意文档,减少可能被攻击的媒介,如果开发者不需要跨域这个安全设置,那么可以使CORS配置来解除这个限制。

关于跨域大家可以参看MDN的 <浏览器的同源策略> 文档,这里仅仅列举几个同源和不同源的例子,来加深大家的理解和印象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 同源的例子
http://example.com/app1/index.html # 只是路径不同
http://example.com/app2/index.html

http://Example.com:80 # 只是大小写差异
http://example.com

# 不同源的例子
http://example.com/app1 # 协议不同
https://example.com/app2

http://example.com # host 不同
http://www.example.com
http://myapp.example.com

http://example.com # 端口不同
http://example.com:8080

正向代理和反向代理

正向代理和反向代理这两个概念看起来挺高大上的,其实用户在无形中肯定都使用过这两种代理。

正向代理

正向代理(Forward Proxy):通常来说,一般的访问流程都是客户端直接向目标服务器发送请求并获取内容。如果使用正向代理,那么客户端不再直接向目标服务器发送请求,而是向代理服务器发送请求,并指定目标服务器(目标服务器是原始服务器),然后由目标服务器和原始服务器进行通信,转交请求并获得内容,之后再将内容返回给客户端。也就是说,正向代理隐藏了真实的客户端,为客户端收发请求,使真实的客户端对服务器不可见。

举一个常见的例子,你的浏览器无法直接访问谷歌,但是你通过某科学上网工具就可以连接了,那么这里的工具其实就是一个正向代理服务器:

反向代理

反向代理(Reverse Proxy):与一般的访问流程相比,使用反向代理后,直接收到请求的服务器是代理服务器,然后代理服务器将请求转发给内部网络上真正进行处理的服务器,并将得到的结果返回给客户端。也就是说,反向代理隐藏了真实的服务器,为服务器收发请求,使真实服务器对客户端不可见。反向代理一般用在需要处理跨域请求的时候,现在基本上大型的网站都设置了反向代理。

举个例子,当你拨打10086客服电话的时候,其实这个10086号码就相当于是一个代理,而真正提供服务的是话务员,对于客户来说,他不关心具体是哪个话务员提供的服务,他只需要记住10086这个号码即可。也就是说,当所有的请求都到10086这一号码的时候,它会将这些请求转发给某一个具体的话务员去处理,因此这里的10086号码其实就是一个代理,不过它代理的不是客户端,而是话务员,这种形式的代理我们称之为反向代理:

关于正向代理和反向代理的主要区别,可以参看如下图片:

负载均衡

一般情况下,客户端发送多个请求到服务器,服务器将处理这些请求,不过其中一部分请求可能要操作一些资源,如数据库、静态资源等,服务器处理完成请求后,再将结果返回给客户端。

对于早期的系统来说,由于功能不复杂,且并发请求相对较少,因此上述模式可以适用。但是随着业务不断更迭、访问量和数据量也在飞速发展,此时这种模式已经无法满足实际要求,当并发量比较大的时候,服务器非常容易发生宕机和崩溃。

很明显这就是服务器性能瓶颈带来的访问过载,除了不断加机器之外,还需要使用负载均衡来解决这一问题。

在请求数量达到指数级增长的时候,单个机器性能再优秀也无法满足需求,这时就必须采用集群的方式,将请求分发到多个机器上,其实就是将负载分发到多个机器上,这就是负载均衡。负载均衡的核心就是分摊压力。一般来说,Nginx实现负载均衡就是将请求转发给多个服务器,也就是服务器集群。

举一个司空见惯的例子,当你早高峰乘坐地铁的时候,工作人员会拿着大喇叭告诉你:“请往车厢两边走,两边人少,便于您乘车。”这里工作人员的作用就是负载均衡,非常形象生动,便于理解:

动静分离

一般来说,为了加快网站的解析速度,开发者可以将动态页面和静态页面交由不同的服务器来解析,以降低原来单个服务的压力。常见的静态资源有html、img、js、css等;动态资源有jsp、servlet、ftl等。

在一些大型的网站架构中,都会将动态资源和静态资源分离开,然后借助于Nginx的高并发和静态资源缓存等特性,将静态资源部署在Nginx上。这样如果请求的是静态资源,则会直接去静态资源目录获取资源;如果请求的是动态资源,则利用反向代理将请求转发到对应的后台应用去处理,进而实现动静分离。

使用前后端分离架构,可以在很大程度上提升静态资源的访问速度,同时即使动态服务不可用,静态资源的访问也不会受到影响。