mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 14:13:46 +08:00
mod
This commit is contained in:
125
极客时间专栏/透视HTTP协议/探索篇/34 | Nginx:高性能的Web服务器.md
Normal file
125
极客时间专栏/透视HTTP协议/探索篇/34 | Nginx:高性能的Web服务器.md
Normal file
@@ -0,0 +1,125 @@
|
||||
<audio id="audio" title="34 | Nginx:高性能的Web服务器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c2/b4/c244add2f3ad05a959d875e667c336b4.mp3"></audio>
|
||||
|
||||
经过前面几大模块的学习,你已经完全掌握了HTTP的所有知识,那么接下来请收拾一下行囊,整理一下装备,跟我一起去探索HTTP之外的广阔天地。
|
||||
|
||||
现在的互联网非常发达,用户越来越多,网速越来越快,HTTPS的安全加密、HTTP/2的多路复用等特性都对Web服务器提出了非常高的要求。一个好的Web服务器必须要具备稳定、快速、易扩展、易维护等特性,才能够让网站“立于不败之地”。
|
||||
|
||||
那么,在搭建网站的时候,应该选择什么样的服务器软件呢?
|
||||
|
||||
在开头的几讲里我也提到过,Web服务器就那么几款,目前市面上主流的只有两个:Apache和Nginx,两者合计占据了近90%的市场份额。
|
||||
|
||||
今天我要说的就是其中的Nginx,它是Web服务器的“后起之秀”,虽然比Apache小了10岁,但增长速度十分迅猛,已经达到了与Apache“平起平坐”的地位,而在“Top Million”网站中更是超过了Apache,拥有超过50%的用户([参考数据](https://w3techs.com/technologies/cross/web_server/ranking))。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c5/0b/c5df0592cc8aef91ba961f7fab5a4a0b.png" alt="unpreview">
|
||||
|
||||
在这里必须要说一下Nginx的正确发音,它应该读成“Engine X”,但我个人感觉“X”念起来太“拗口”,还是比较倾向于读做“Engine ks”,这也与UNIX、Linux的发音一致。
|
||||
|
||||
作为一个Web服务器,Nginx的功能非常完善,完美支持HTTP/1、HTTPS和HTTP/2,而且还在不断进步。当前的主线版本已经发展到了1.17,正在进行HTTP/3的研发,或许一年之后就能在Nginx上跑HTTP/3了。
|
||||
|
||||
Nginx也是我个人的主要研究领域,我也写过相关的书,按理来说今天的课程应该是“手拿把攥”,但真正动笔的时候还是有些犹豫的:很多要点都已经在书里写过了,这次的专栏如果再重复相同的内容就不免有“骗稿费”的嫌疑,应该有些“不一样的东西”。
|
||||
|
||||
所以我决定抛开书本,换个角度,结合HTTP协议来讲Nginx,带你窥视一下HTTP处理的内幕,看看Web服务器的工作原理。
|
||||
|
||||
## 进程池
|
||||
|
||||
你也许听说过,Nginx是个“轻量级”的Web服务器,那么这个所谓的“轻量级”是什么意思呢?
|
||||
|
||||
“轻量级”是相对于“重量级”而言的。“重量级”就是指服务器进程很“重”,占用很多资源,当处理HTTP请求时会消耗大量的CPU和内存,受到这些资源的限制很难提高性能。
|
||||
|
||||
而Nginx作为“轻量级”的服务器,它的CPU、内存占用都非常少,同样的资源配置下就能够为更多的用户提供服务,其奥秘在于它独特的工作模式。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3e/c1/3e94fbd78ed043e88c443f6416f99dc1.png" alt="">
|
||||
|
||||
在Nginx之前,Web服务器的工作模式大多是“Per-Process”或者“Per-Thread”,对每一个请求使用单独的进程或者线程处理。这就存在创建进程或线程的成本,还会有进程、线程“上下文切换”的额外开销。如果请求数量很多,CPU就会在多个进程、线程之间切换时“疲于奔命”,平白地浪费了计算时间。
|
||||
|
||||
Nginx则完全不同,“一反惯例”地没有使用多线程,而是使用了“**进程池+单线程**”的工作模式。
|
||||
|
||||
Nginx在启动的时候会预先创建好固定数量的worker进程,在之后的运行过程中不会再fork出新进程,这就是进程池,而且可以自动把进程“绑定”到独立的CPU上,这样就完全消除了进程创建和切换的成本,能够充分利用多核CPU的计算能力。
|
||||
|
||||
在进程池之上,还有一个“master”进程,专门用来管理进程池。它的作用有点像是supervisor(一个用Python编写的进程管理工具),用来监控进程,自动恢复发生异常的worker,保持进程池的稳定和服务能力。
|
||||
|
||||
不过master进程完全是Nginx自行用C语言实现的,这就摆脱了外部的依赖,简化了Nginx的部署和配置。
|
||||
|
||||
## I/O多路复用
|
||||
|
||||
如果你用Java、C等语言写过程序,一定很熟悉“多线程”的概念,使用多线程能够很容易实现并发处理。
|
||||
|
||||
但多线程也有一些缺点,除了刚才说到的“上下文切换”成本,还有编程模型复杂、数据竞争、同步等问题,写出正确、快速的多线程程序并不是一件容易的事情。
|
||||
|
||||
所以Nginx就选择了单线程的方式,带来的好处就是开发简单,没有互斥锁的成本,减少系统消耗。
|
||||
|
||||
那么,疑问也就产生了:为什么单线程的Nginx,处理能力却能够超越其他多线程的服务器呢?
|
||||
|
||||
这要归功于Nginx利用了Linux内核里的一件“神兵利器”,**I/O多路复用接口**,“大名鼎鼎”的epoll。
|
||||
|
||||
“多路复用”这个词我们已经在之前的HTTP/2、HTTP/3里遇到过好几次,如果你理解了那里的“多路复用”,那么面对Nginx的epoll“多路复用”也就好办了。
|
||||
|
||||
Web服务器从根本上来说是“I/O密集型”而不是“CPU密集型”,处理能力的关键在于网络收发而不是CPU计算(这里暂时不考虑HTTPS的加解密),而网络I/O会因为各式各样的原因不得不等待,比如数据还没到达、对端没有响应、缓冲区满发不出去等等。
|
||||
|
||||
这种情形就有点像是HTTP里的“队头阻塞”。对于一般的单线程来说CPU就会“停下来”,造成浪费。而多线程的解决思路有点类似“并发连接”,虽然有的线程可能阻塞,但由于多个线程并行,总体上看阻塞的情况就不会太严重了。
|
||||
|
||||
Nginx里使用的epoll,就好像是HTTP/2里的“多路复用”技术,它把多个HTTP请求处理打散成碎片,都“复用”到一个单线程里,不按照先来后到的顺序处理,而是只当连接上真正可读、可写的时候才处理,如果可能发生阻塞就立刻切换出去,处理其他的请求。
|
||||
|
||||
通过这种方式,Nginx就完全消除了I/O阻塞,把CPU利用得“满满当当”,又因为网络收发并不会消耗太多CPU计算能力,也不需要切换进程、线程,所以整体的CPU负载是相当低的。
|
||||
|
||||
这里我画了一张Nginx“I/O多路复用”的示意图,你可以看到,它的形式与HTTP/2的流非常相似,每个请求处理单独来看是分散、阻塞的,但因为都复用到了一个线程里,所以资源的利用率非常高。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4c/59/4c6832cdce34133c9ed89237fb9d5059.png" alt="">
|
||||
|
||||
epoll还有一个特点,大量的连接管理工作都是在操作系统内核里做的,这就减轻了应用程序的负担,所以Nginx可以为每个连接只分配很小的内存维护状态,即使有几万、几十万的并发连接也只会消耗几百M内存,而其他的Web服务器这个时候早就“Memory not enough”了。
|
||||
|
||||
## 多阶段处理
|
||||
|
||||
有了“进程池”和“I/O多路复用”,Nginx是如何处理HTTP请求的呢?
|
||||
|
||||
Nginx在内部也采用的是“**化整为零**”的思路,把整个Web服务器分解成了多个“功能模块”,就好像是乐高积木,可以在配置文件里任意拼接搭建,从而实现了高度的灵活性和扩展性。
|
||||
|
||||
Nginx的HTTP处理有四大类模块:
|
||||
|
||||
1. handler模块:直接处理HTTP请求;
|
||||
1. filter模块:不直接处理请求,而是加工过滤响应报文;
|
||||
1. upstream模块:实现反向代理功能,转发请求到其他服务器;
|
||||
1. balance模块:实现反向代理时的负载均衡算法。
|
||||
|
||||
因为upstream模块和balance模块实现的是代理功能,Nginx作为“中间人”,运行机制比较复杂,所以我今天只讲handler模块和filter模块。
|
||||
|
||||
不知道你有没有了解过“设计模式”这方面的知识,其中有一个非常有用的模式叫做“**职责链**”。它就好像是工厂里的流水线,原料从一头流入,线上有许多工人会进行各种加工处理,最后从另一头出来的就是完整的产品。
|
||||
|
||||
Nginx里的handler模块和filter模块就是按照“职责链”模式设计和组织的,HTTP请求报文就是“原材料”,各种模块就是工厂里的工人,走完模块构成的“流水线”,出来的就是处理完成的响应报文。
|
||||
|
||||
下面的这张图显示了Nginx的“流水线”,在Nginx里的术语叫“阶段式处理”(Phases),一共有11个阶段,每个阶段里又有许多各司其职的模块。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/41/30/41318c867fda8a536d0e3db6f9987030.png" alt="">
|
||||
|
||||
我简单列几个与我们的课程相关的模块吧:
|
||||
|
||||
- charset模块实现了字符集编码转换;([第15讲](https://time.geekbang.org/column/article/104024))
|
||||
- chunked模块实现了响应数据的分块传输;([第16讲](https://time.geekbang.org/column/article/104456))
|
||||
- range模块实现了范围请求,只返回数据的一部分;([第16讲](https://time.geekbang.org/column/article/104456))
|
||||
- rewrite模块实现了重定向和跳转,还可以使用内置变量自定义跳转的URI;([第18讲](https://time.geekbang.org/column/article/105614))
|
||||
- not_modified模块检查头字段“if-Modified-Since”和“If-None-Match”,处理条件请求;([第20讲](https://time.geekbang.org/column/article/106804))
|
||||
- realip模块处理“X-Real-IP”“X-Forwarded-For”等字段,获取客户端的真实IP地址;([第21讲](https://time.geekbang.org/column/article/107577))
|
||||
- ssl模块实现了SSL/TLS协议支持,读取磁盘上的证书和私钥,实现TLS握手和SNI、ALPN等扩展功能;([安全篇](https://time.geekbang.org/column/article/108643))
|
||||
- http_v2模块实现了完整的HTTP/2协议。([飞翔篇](https://time.geekbang.org/column/article/112036))
|
||||
|
||||
在这张图里,你还可以看到limit_conn、limit_req、access、log等其他模块,它们实现的是限流限速、访问控制、日志等功能,不在HTTP协议规定之内,但对于运行在现实世界的Web服务器却是必备的。
|
||||
|
||||
如果你有C语言基础,感兴趣的话可以下载Nginx的源码,在代码级别仔细看看HTTP的处理过程。
|
||||
|
||||
## 小结
|
||||
|
||||
1. Nginx是一个高性能的Web服务器,它非常的轻量级,消耗的CPU、内存很少;
|
||||
1. Nginx采用“master/workers”进程池架构,不使用多线程,消除了进程、线程切换的成本;
|
||||
1. Nginx基于epoll实现了“I/O多路复用”,不会阻塞,所以性能很高;
|
||||
1. Nginx使用了“职责链”模式,多个模块分工合作,自由组合,以流水线的方式处理HTTP请求。
|
||||
|
||||
## 课下作业
|
||||
|
||||
1. 你是怎么理解进程、线程上下文切换时的成本的,为什么Nginx要尽量避免?
|
||||
1. 试着自己描述一下Nginx用进程、epoll、模块流水线处理HTTP请求的过程。
|
||||
|
||||
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4c/3d/4c7bceb80a8027389705e9d6ec9eb43d.png" alt="unpreview">
|
||||
|
||||
|
||||
130
极客时间专栏/透视HTTP协议/探索篇/35 | OpenResty:更灵活的Web服务器.md
Normal file
130
极客时间专栏/透视HTTP协议/探索篇/35 | OpenResty:更灵活的Web服务器.md
Normal file
@@ -0,0 +1,130 @@
|
||||
<audio id="audio" title="35 | OpenResty:更灵活的Web服务器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7d/68/7d395f67094c2bfa140ee2d100996168.mp3"></audio>
|
||||
|
||||
在上一讲里,我们看到了高性能的Web服务器Nginx,它资源占用少,处理能力高,是搭建网站的首选。
|
||||
|
||||
虽然Nginx成为了Web服务器领域无可争议的“王者”,但它也并不是没有缺点的,毕竟它已经15岁了。
|
||||
|
||||
“一个人很难超越时代,而时代却可以轻易超越所有人”,Nginx当初设计时针对的应用场景已经发生了变化,它的一些缺点也就暴露出来了。
|
||||
|
||||
Nginx的服务管理思路延续了当时的流行做法,使用磁盘上的静态配置文件,所以每次修改后必须重启才能生效。
|
||||
|
||||
这在业务频繁变动的时候是非常致命的(例如流行的微服务架构),特别是对于拥有成千上万台服务器的网站来说,仅仅增加或者删除一行配置就要分发、重启所有的机器,对运维是一个非常大的挑战,要耗费很多的时间和精力,成本很高,很不灵活,难以“随需应变”。
|
||||
|
||||
那么,有没有这样的一个Web服务器,它有Nginx的优点却没有Nginx的缺点,既轻量级、高性能,又灵活、可动态配置呢?
|
||||
|
||||
这就是我今天要说的OpenResty,它是一个“更好更灵活的Nginx”。
|
||||
|
||||
## OpenResty是什么?
|
||||
|
||||
其实你对OpenResty并不陌生,这个专栏的实验环境就是用OpenResty搭建的,这么多节课程下来,你应该或多或少对它有了一些印象吧。
|
||||
|
||||
OpenResty诞生于2009年,到现在刚好满10周岁。它的创造者是当时就职于某宝的“神级”程序员**章亦春**,网名叫“agentzh”。
|
||||
|
||||
OpenResty并不是一个全新的Web服务器,而是基于Nginx,它利用了Nginx模块化、可扩展的特性,开发了一系列的增强模块,并把它们打包整合,形成了一个**“一站式”的Web开发平台**。
|
||||
|
||||
虽然OpenResty的核心是Nginx,但它又超越了Nginx,关键就在于其中的ngx_lua模块,把小巧灵活的Lua语言嵌入了Nginx,可以用脚本的方式操作Nginx内部的进程、多路复用、阶段式处理等各种构件。
|
||||
|
||||
脚本语言的好处你一定知道,它不需要编译,随写随执行,这就免去了C语言编写模块漫长的开发周期。而且OpenResty还把Lua自身的协程与Nginx的事件机制完美结合在一起,优雅地实现了许多其他语言所没有的“**同步非阻塞**”编程范式,能够轻松开发出高性能的Web应用。
|
||||
|
||||
目前OpenResty有两个分支,分别是开源、免费的“OpenResty”和闭源、商业产品的“OpenResty+”,运作方式有社区支持、OpenResty基金会、OpenResty.Inc公司,还有其他的一些外界赞助(例如Kong、CloudFlare),正在蓬勃发展。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/01/9f7b79c43c476890f03c2c716a20f301.png" alt="unpreview">
|
||||
|
||||
顺便说一下OpenResty的官方logo,是一只展翅飞翔的海鸥,选择海鸥是因为“鸥”与OpenResty的发音相同。另外,这个logo的形状也像是左手比出的一个“OK”姿势,正好也是一个“O”。
|
||||
|
||||
## 动态的Lua
|
||||
|
||||
刚才说了,OpenResty里的一个关键模块是ngx_lua,它为Nginx引入了脚本语言Lua。
|
||||
|
||||
Lua是一个比较“小众”的语言,虽然历史比较悠久,但名气却没有PHP、Python、JavaScript大,这主要与它的自身定位有关。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4f/d5/4f24aa3f53969b71baaf7d9c7cf68fd5.png" alt="unpreview">
|
||||
|
||||
Lua的设计目标是嵌入到其他应用程序里运行,为其他编程语言带来“脚本化”能力,所以它的“个头”比较小,功能集有限,不追求“大而全”,而是“小而美”,大多数时间都“隐匿”在其他应用程序的后面,是“无名英雄”。
|
||||
|
||||
你或许玩过或者听说过《魔兽世界》《愤怒的小鸟》吧,它们就在内部嵌入了Lua,使用Lua来调用底层接口,充当“胶水语言”(glue language),编写游戏逻辑脚本,提高开发效率。
|
||||
|
||||
OpenResty选择Lua作为“工作语言”也是基于同样的考虑。因为Nginx C开发实在是太麻烦了,限制了Nginx的真正实力。而Lua作为“最快的脚本语言”恰好可以成为Nginx的完美搭档,既可以简化开发,性能上又不会有太多的损耗。
|
||||
|
||||
作为脚本语言,Lua还有一个重要的“**代码热加载**”特性,不需要重启进程,就能够从磁盘、Redis或者任何其他地方加载数据,随时替换内存里的代码片段。这就带来了“**动态配置**”,让OpenResty能够永不停机,在微秒、毫秒级别实现配置和业务逻辑的实时更新,比起Nginx秒级的重启是一个极大的进步。
|
||||
|
||||
你可以看一下实验环境的“www/lua”目录,里面存放了我写的一些测试HTTP特性的Lua脚本,代码都非常简单易懂,就像是普通的英语“阅读理解”,这也是Lua的另一个优势:易学习、易上手。
|
||||
|
||||
## 高效率的Lua
|
||||
|
||||
OpenResty能够高效运行的一大“秘技”是它的“**同步非阻塞**”编程范式,如果你要开发OpenResty应用就必须时刻铭记于心。
|
||||
|
||||
“同步非阻塞”本质上还是一种“**多路复用**”,我拿上一讲的Nginx epoll来对比解释一下。
|
||||
|
||||
epoll是操作系统级别的“多路复用”,运行在内核空间。而OpenResty的“同步非阻塞”则是基于Lua内建的“**协程**”,是应用程序级别的“多路复用”,运行在用户空间,所以它的资源消耗要更少。
|
||||
|
||||
OpenResty里每一段Lua程序都由协程来调度运行。和Linux的epoll一样,每当可能发生阻塞的时候“协程”就会立刻切换出去,执行其他的程序。这样单个处理流程是“阻塞”的,但整个OpenResty却是“非阻塞的”,多个程序都“复用”在一个Lua虚拟机里运行。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/c6/9fc3df52df7d6c11aa02b8013f8e9bc6.png" alt="">
|
||||
|
||||
下面的代码是一个简单的例子,读取POST发送的body数据,然后再发回客户端:
|
||||
|
||||
```
|
||||
ngx.req.read_body() -- 同步非阻塞(1)
|
||||
|
||||
local data = ngx.req.get_body_data()
|
||||
if data then
|
||||
ngx.print("body: ", data) -- 同步非阻塞(2)
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
代码中的“ngx.req.read_body”和“ngx.print”分别是数据的收发动作,只有收到数据才能发送数据,所以是“同步”的。
|
||||
|
||||
但即使因为网络原因没收到或者发不出去,OpenResty也不会在这里阻塞“干等着”,而是做个“记号”,把等待的这段CPU时间用来处理其他的请求,等网络可读或者可写时再“回来”接着运行。
|
||||
|
||||
假设收发数据的等待时间是10毫秒,而真正CPU处理的时间是0.1毫秒,那么OpenResty就可以在这10毫秒内同时处理100个请求,而不是把这100个请求阻塞排队,用1000毫秒来处理。
|
||||
|
||||
除了“同步非阻塞”,OpenResty还选用了**LuaJIT**作为Lua语言的“运行时(Runtime)”,进一步“挖潜增效”。
|
||||
|
||||
LuaJIT是一个高效的Lua虚拟机,支持JIT(Just In Time)技术,可以把Lua代码即时编译成“本地机器码”,这样就消除了脚本语言解释运行的劣势,让Lua脚本跑得和原生C代码一样快。
|
||||
|
||||
另外,LuaJIT还为Lua语言添加了一些特别的增强,比如二进制位运算库bit,内存优化库table,还有FFI(Foreign Function Interface),让Lua直接调用底层C函数,比原生的压栈调用快很多。
|
||||
|
||||
## 阶段式处理
|
||||
|
||||
和Nginx一样,OpenResty也使用“流水线”来处理HTTP请求,底层的运行基础是Nginx的“阶段式处理”,但它又有自己的特色。
|
||||
|
||||
Nginx的“流水线”是由一个个C模块组成的,只能在静态文件里配置,开发困难,配置麻烦(相对而言)。而OpenResty的“流水线”则是由一个个的Lua脚本组成的,不仅可以从磁盘上加载,也可以从Redis、MySQL里加载,而且编写、调试的过程非常方便快捷。
|
||||
|
||||
下面我画了一张图,列出了OpenResty的阶段,比起Nginx,OpenResty的阶段更注重对HTTP请求响应报文的加工和处理。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/36/df/3689312a970bae0e949b017ad45438df.png" alt="">
|
||||
|
||||
OpenResty里有几个阶段与Nginx是相同的,比如rewrite、access、content、filter,这些都是标准的HTTP处理。
|
||||
|
||||
在这几个阶段里可以用“xxx_by_lua”指令嵌入Lua代码,执行重定向跳转、访问控制、产生响应、负载均衡、过滤报文等功能。因为Lua的脚本语言特性,不用考虑内存分配、资源回收释放等底层的细节问题,可以专注于编写非常复杂的业务逻辑,比C模块的开发效率高很多,即易于扩展又易于维护。
|
||||
|
||||
OpenResty里还有两个不同于Nginx的特殊阶段。
|
||||
|
||||
一个是“**init阶段**”,它又分成“master init”和“worker init”,在master进程和worker进程启动的时候运行。这个阶段还没有开始提供服务,所以慢一点也没关系,可以调用一些阻塞的接口初始化服务器,比如读取磁盘、MySQL,加载黑白名单或者数据模型,然后放进共享内存里供运行时使用。
|
||||
|
||||
另一个是“**ssl阶段**”,这算得上是OpenResty的一大创举,可以在TLS握手时动态加载证书,或者发送“OCSP Stapling”。
|
||||
|
||||
还记得[第29讲](https://time.geekbang.org/column/article/111940)里说的“SNI扩展”吗?Nginx可以依据“服务器名称指示”来选择证书实现HTTPS虚拟主机,但静态配置很不灵活,要编写很多雷同的配置块。虽然后来Nginx增加了变量支持,但它每次握手都要读磁盘,效率很低。
|
||||
|
||||
而在OpenResty里就可以使用指令“ssl_certificate_by_lua”,编写Lua脚本,读取SNI名字后,直接从共享内存或者Redis里获取证书。不仅没有读盘阻塞,而且证书也是完全动态可配置的,无需修改配置文件就能够轻松支持大量的HTTPS虚拟主机。
|
||||
|
||||
## 小结
|
||||
|
||||
1. Nginx依赖于磁盘上的静态配置文件,修改后必须重启才能生效,缺乏灵活性;
|
||||
1. OpenResty基于Nginx,打包了很多有用的模块和库,是一个高性能的Web开发平台;
|
||||
1. OpenResty的工作语言是Lua,它小巧灵活,执行效率高,支持“代码热加载”;
|
||||
1. OpenResty的核心编程范式是“同步非阻塞”,使用协程,不需要异步回调函数;
|
||||
1. OpenResty也使用“阶段式处理”的工作模式,但因为在阶段里执行的都是Lua代码,所以非常灵活,配合Redis等外部数据库能够实现各种动态配置。
|
||||
|
||||
## 课下作业
|
||||
|
||||
1. 谈一下这些天你对实验环境里OpenResty的感想和认识。
|
||||
1. 你觉得Nginx和OpenResty的“阶段式处理”有什么好处?对你的实际工作有没有启发?
|
||||
|
||||
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c5/9f/c5b7ac40c585c800af0fe3ab98f3449f.png" alt="unpreview">
|
||||
|
||||
|
||||
182
极客时间专栏/透视HTTP协议/探索篇/36 | WAF:保护我们的网络服务.md
Normal file
182
极客时间专栏/透视HTTP协议/探索篇/36 | WAF:保护我们的网络服务.md
Normal file
@@ -0,0 +1,182 @@
|
||||
<audio id="audio" title="36 | WAF:保护我们的网络服务" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f5/0f/f53e1775cb784eeb10197f3bd6fa1b0f.mp3"></audio>
|
||||
|
||||
在前些天的“安全篇”里,我谈到了HTTPS,它使用了SSL/TLS协议,加密整个通信过程,能够防止恶意窃听和窜改,保护我们的数据安全。
|
||||
|
||||
但HTTPS只是网络安全中很小的一部分,仅仅保证了“通信链路安全”,让第三方无法得知传输的内容。在通信链路的两端,也就是客户端和服务器,它是无法提供保护的。
|
||||
|
||||
因为HTTP是一个开放的协议,Web服务都运行在公网上,任何人都可以访问,所以天然就会成为黑客的攻击目标。
|
||||
|
||||
而且黑客的本领比我们想象的还要大得多。虽然不能在传输过程中做手脚,但他们还可以“假扮”成合法的用户访问系统,然后伺机搞破坏。
|
||||
|
||||
## Web服务遇到的威胁
|
||||
|
||||
黑客都有哪些手段来攻击Web服务呢?我给你大概列出几种常见的方式。
|
||||
|
||||
第一种叫“**DDoS**”攻击(distributed denial-of-service attack),有时候也叫“洪水攻击”。
|
||||
|
||||
黑客会控制许多“僵尸”计算机,向目标服务器发起大量无效请求。因为服务器无法区分正常用户和黑客,只能“照单全收”,这样就挤占了正常用户所应有的资源。如果黑客的攻击强度很大,就会像“洪水”一样对网站的服务能力造成冲击,耗尽带宽、CPU和内存,导致网站完全无法提供正常服务。
|
||||
|
||||
“DDoS”攻击方式比较“简单粗暴”,虽然很有效,但不涉及HTTP协议内部的细节,“技术含量”比较低,不过下面要说的几种手段就不一样了。
|
||||
|
||||
网站后台的Web服务经常会提取出HTTP报文里的各种信息,应用于业务,有时会缺乏严格的检查。因为HTTP报文在语义结构上非常松散、灵活,URI里的query字符串、头字段、body数据都可以任意设置,这就带来了安全隐患,给了黑客“**代码注入**”的可能性。
|
||||
|
||||
黑客可以精心编制HTTP请求报文,发送给服务器,服务程序如果没有做防备,就会“上当受骗”,执行黑客设定的代码。
|
||||
|
||||
“**SQL注入**”(SQL injection)应该算是最著名的一种“代码注入”攻击了,它利用了服务器字符串拼接形成SQL语句的漏洞,构造出非正常的SQL语句,获取数据库内部的敏感信息。
|
||||
|
||||
另一种“**HTTP头注入**”攻击的方式也是类似的原理,它在“Host”“User-Agent”“X-Forwarded-For”等字段里加入了恶意数据或代码,服务端程序如果解析不当,就会执行预设的恶意代码。
|
||||
|
||||
在之前的[第19讲](https://time.geekbang.org/column/article/106034)里,也说过一种利用Cookie的攻击手段,“**跨站脚本**”(XSS)攻击,它属于“JS代码注入”,利用JavaScript脚本获取未设防的Cookie。
|
||||
|
||||
## 网络应用防火墙
|
||||
|
||||
面对这么多的黑客攻击手段,我们应该怎么防御呢?
|
||||
|
||||
这就要用到“**网络应用防火墙**”(Web Application Firewall)了,简称为“**WAF**”。
|
||||
|
||||
你可能对传统的“防火墙”比较熟悉。传统“防火墙”工作在三层或者四层,隔离了外网和内网,使用预设的规则,只允许某些特定IP地址和端口号的数据包通过,拒绝不符合条件的数据流入或流出内网,实质上是**一种网络数据过滤设备**。
|
||||
|
||||
WAF也是一种“防火墙”,但它工作在七层,看到的不仅是IP地址和端口号,还能看到整个HTTP报文,所以就能够对报文内容做更深入细致的审核,使用更复杂的条件、规则来过滤数据。
|
||||
|
||||
说白了,WAF就是一种“**HTTP入侵检测和防御系统**”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e8/a3/e8369d077454e5b92e3722e7090551a3.png" alt="">
|
||||
|
||||
WAF都能干什么呢?
|
||||
|
||||
通常一款产品能够称为WAF,要具备下面的一些功能:
|
||||
|
||||
- IP黑名单和白名单,拒绝黑名单上地址的访问,或者只允许白名单上的用户访问;
|
||||
- URI黑名单和白名单,与IP黑白名单类似,允许或禁止对某些URI的访问;
|
||||
- 防护DDoS攻击,对特定的IP地址限连限速;
|
||||
- 过滤请求报文,防御“代码注入”攻击;
|
||||
- 过滤响应报文,防御敏感信息外泄;
|
||||
- 审计日志,记录所有检测到的入侵操作。
|
||||
|
||||
听起来WAF好像很高深,但如果你理解了它的工作原理,其实也不难。
|
||||
|
||||
它就像是平时编写程序时必须要做的函数入口参数检查,拿到HTTP请求、响应报文,用字符串处理函数看看有没有关键字、敏感词,或者用正则表达式做一下模式匹配,命中了规则就执行对应的动作,比如返回403/404。
|
||||
|
||||
如果你比较熟悉Apache、Nginx、OpenResty,可以自己改改配置文件,写点JS或者Lua代码,就能够实现基本的WAF功能。
|
||||
|
||||
比如说,在Nginx里实现IP地址黑名单,可以利用“map”指令,从变量$remote_addr获取IP地址,在黑名单上就映射为值1,然后在“if”指令里判断:
|
||||
|
||||
```
|
||||
map $remote_addr $blocked {
|
||||
default 0;
|
||||
"1.2.3.4" 1;
|
||||
"5.6.7.8" 1;
|
||||
}
|
||||
|
||||
|
||||
if ($blocked) {
|
||||
return 403 "you are blocked.";
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Nginx的配置文件只能静态加载,改名单必须重启,比较麻烦。如果换成OpenResty就会非常方便,在access阶段进行判断,IP地址列表可以使用cosocket连接外部的Redis、MySQL等数据库,实现动态更新:
|
||||
|
||||
```
|
||||
local ip_addr = ngx.var.remote_addr
|
||||
|
||||
local rds = redis:new()
|
||||
if rds:get(ip_addr) == 1 then
|
||||
ngx.exit(403)
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
看了上面的两个例子,你是不是有种“跃跃欲试”的冲动了,想自己动手开发一个WAF?
|
||||
|
||||
不过我必须要提醒你,在网络安全领域必须时刻记得“**木桶效应**”(也叫“短板效应”)。网站的整体安全不在于你加固的最强的那个方向,而是在于你可能都没有意识到的“短板”。黑客往往会“避重就轻”,只要发现了网站的一个弱点,就可以“一点突破”,其他方面的安全措施也就都成了“无用功”。
|
||||
|
||||
所以,使用WAF最好“**不要重新发明轮子**”,而是使用现有的、比较成熟的、经过实际考验的WAF产品。
|
||||
|
||||
## 全面的WAF解决方案
|
||||
|
||||
这里我就要“隆重”介绍一下WAF领域里的最顶级产品了:ModSecurity,它可以说是WAF界“事实上的标准”。
|
||||
|
||||
ModSecurity是一个开源的、生产级的WAF工具包,历史很悠久,比Nginx还要大几岁。它开始于一个私人项目,后来被商业公司Breach Security收购,现在则是由TrustWave公司的SpiderLabs团队负责维护。
|
||||
|
||||
ModSecurity最早是Apache的一个模块,只能运行在Apache上。因为其品质出众,大受欢迎,后来的2.x版添加了Nginx和IIS支持,但因为底层架构存在差异,不够稳定。
|
||||
|
||||
所以,这两年SpiderLabs团队就开发了全新的3.0版本,移除了对Apache架构的依赖,使用新的“连接器”来集成进Apache或者Nginx,比2.x版更加稳定和快速,误报率也更低。
|
||||
|
||||
ModSecurity有两个核心组件。第一个是“**规则引擎**”,它实现了自定义的“SecRule”语言,有自己特定的语法。但“SecRule”主要基于正则表达式,还是不够灵活,所以后来也引入了Lua,实现了脚本化配置。
|
||||
|
||||
ModSecurity的规则引擎使用C++11实现,可以从[GitHub](https://github.com/SpiderLabs/ModSecurity)上下载源码,然后集成进Nginx。因为它比较庞大,编译很费时间,所以最好编译成动态模块,在配置文件里用指令“load_module”加载:
|
||||
|
||||
```
|
||||
load_module modules/ngx_http_modsecurity_module.so;
|
||||
|
||||
```
|
||||
|
||||
只有引擎还不够,要让引擎运转起来,还需要完善的防御规则,所以ModSecurity的第二个核心组件就是它的“**规则集**”。
|
||||
|
||||
ModSecurity源码提供一个基本的规则配置文件“**modsecurity.conf-recommended**”,使用前要把它的后缀改成“conf”。
|
||||
|
||||
有了规则集,就可以在Nginx配置文件里加载,然后启动规则引擎:
|
||||
|
||||
```
|
||||
modsecurity on;
|
||||
modsecurity_rules_file /path/to/modsecurity.conf;
|
||||
|
||||
```
|
||||
|
||||
“modsecurity.conf”文件默认只有检测功能,不提供入侵阻断,这是为了防止误杀误报,把“SecRuleEngine”后面改成“On”就可以开启完全的防护:
|
||||
|
||||
```
|
||||
#SecRuleEngine DetectionOnly
|
||||
SecRuleEngine On
|
||||
|
||||
```
|
||||
|
||||
基本的规则集之外,ModSecurity还额外提供一个更完善的规则集,为网站提供全面可靠的保护。这个规则集的全名叫“**OWASP ModSecurity 核心规则集**”(Open Web Application Security Project ModSecurity Core Rule Set),因为名字太长了,所以有时候会简称为“核心规则集”或者“CRS”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ad/48/add929f8439c64f29db720d30f7de548.png" alt="">
|
||||
|
||||
CRS也是完全开源、免费的,可以从GitHub上下载:
|
||||
|
||||
```
|
||||
git clone https://github.com/SpiderLabs/owasp-modsecurity-crs.git
|
||||
|
||||
```
|
||||
|
||||
其中有一个“**crs-setup.conf.example**”的文件,它是CRS的基本配置,可以用“Include”命令添加到“modsecurity.conf”里,然后再添加“rules”里的各种规则。
|
||||
|
||||
```
|
||||
Include /path/to/crs-setup.conf
|
||||
Include /path/to/rules/*.conf
|
||||
|
||||
```
|
||||
|
||||
你如果有兴趣可以看一下这些配置文件,里面用“SecRule”定义了很多的规则,基本的形式是“SecRule 变量 运算符 动作”。不过ModSecurity的这套语法“自成一体”,比较复杂,要完全掌握不是一朝一夕的事情,我就不详细解释了。
|
||||
|
||||
另外,ModSecurity还有强大的审计日志(Audit Log)功能,记录任何可疑的数据,供事后离线分析。但在生产环境中会遇到大量的攻击,日志会快速增长,消耗磁盘空间,而且写磁盘也会影响Nginx的性能,所以一般建议把它关闭:
|
||||
|
||||
```
|
||||
SecAuditEngine off #RelevantOnly
|
||||
SecAuditLog /var/log/modsec_audit.log
|
||||
|
||||
```
|
||||
|
||||
## 小结
|
||||
|
||||
今天我们一起学习了“网络应用防火墙”,也就是WAF,使用它可以加固Web服务。
|
||||
|
||||
1. Web服务通常都运行在公网上,容易受到“DDoS”、“代码注入”等各种黑客攻击,影响正常的服务,所以必须要采取措施加以保护;
|
||||
1. WAF是一种“HTTP入侵检测和防御系统”,工作在七层,为Web服务提供全面的防护;
|
||||
1. ModSecurity是一个开源的、生产级的WAF产品,核心组成部分是“规则引擎”和“规则集”,两者的关系有点像杀毒引擎和病毒特征库;
|
||||
1. WAF实质上是模式匹配与数据过滤,所以会消耗CPU,增加一些计算成本,降低服务能力,使用时需要在安全与性能之间找到一个“平衡点”。
|
||||
|
||||
## 课下作业
|
||||
|
||||
1. HTTPS为什么不能防御DDoS、代码注入等攻击呢?
|
||||
1. 你还知道有哪些手段能够抵御网络攻击吗?
|
||||
|
||||
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/24/b9e48b813c98bb34b4b433b7326ace24.png" alt="unpreview">
|
||||
|
||||
|
||||
120
极客时间专栏/透视HTTP协议/探索篇/37 | CDN:加速我们的网络服务.md
Normal file
120
极客时间专栏/透视HTTP协议/探索篇/37 | CDN:加速我们的网络服务.md
Normal file
@@ -0,0 +1,120 @@
|
||||
<audio id="audio" title="37 | CDN:加速我们的网络服务" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/44/4f/44e3c0a62c765e9df59d0447ec226a4f.mp3"></audio>
|
||||
|
||||
在正式开讲前,我们先来看看到现在为止HTTP手头都有了哪些“武器”。
|
||||
|
||||
协议方面,HTTPS强化通信链路安全、HTTP/2优化传输效率;应用方面,Nginx/OpenResty提升网站服务能力,WAF抵御网站入侵攻击,讲到这里,你是不是感觉还少了点什么?
|
||||
|
||||
没错,在应用领域,还缺一个在外部加速HTTP协议的服务,这个就是我们今天要说的CDN(Content Delivery Network或Content Distribution Network),中文名叫“内容分发网络”。
|
||||
|
||||
## 为什么要有网络加速?
|
||||
|
||||
你可能要问了,HTTP的传输速度也不算差啊,而且还有更好的HTTP/2,为什么还要再有一个额外的CDN来加速呢?是不是有点“多此一举”呢?
|
||||
|
||||
这里我们就必须要考虑现实中会遇到的问题了。你一定知道,光速是有限的,虽然每秒30万公里,但这只是真空中的上限,在实际的电缆、光缆中的速度会下降到原本的三分之二左右,也就是20万公里/秒,这样一来,地理位置的距离导致的传输延迟就会变得比较明显了。
|
||||
|
||||
比如,北京到广州直线距离大约是2000公里,按照刚才的20万公里/秒来算的话,发送一个请求单程就要10毫秒,往返要20毫秒,即使什么都不干,这个“硬性”的时延也是躲不过的。
|
||||
|
||||
另外不要忘了, 互联网从逻辑上看是一张大网,但实际上是由许多小网络组成的,这其中就有小网络“互连互通”的问题,典型的就是各个电信运营商的网络,比如国内的电信、联通、移动三大家。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/41/b9/413605355db69278cb137b318b70b3b9.png" alt="">
|
||||
|
||||
这些小网络内部的沟通很顺畅,但网络之间却只有很少的联通点。如果你在A网络,而网站在C网络,那么就必须“跨网”传输,和成千上万的其他用户一起去“挤”连接点的“独木桥”。而带宽终究是有限的,能抢到多少只能看你的运气。
|
||||
|
||||
还有,网络中还存在许多的路由器、网关,数据每经过一个节点,都要停顿一下,在二层、三层解析转发,这也会消耗一定的时间,带来延迟。
|
||||
|
||||
把这些因素再放到全球来看,地理距离、运营商网络、路由转发的影响就会成倍增加。想象一下,你在北京,访问旧金山的网站,要跨越半个地球,中间会有多少环节,会增加多少时延?
|
||||
|
||||
最终结果就是,如果仅用现有的HTTP传输方式,大多数网站都会访问速度缓慢、用户体验糟糕。
|
||||
|
||||
## 什么是CDN?
|
||||
|
||||
这个时候CDN就出现了,它就是专门为解决“长距离”上网络访问速度慢而诞生的一种网络应用服务。
|
||||
|
||||
从名字上看,CDN有三个关键词:“**内容**”“**分发**”和“**网络**”。
|
||||
|
||||
先看一下“网络”的含义。CDN的最核心原则是“**就近访问**”,如果用户能够在本地几十公里的距离之内获取到数据,那么时延就基本上变成0了。
|
||||
|
||||
所以CDN投入了大笔资金,在全国、乃至全球的各个大枢纽城市都建立了机房,部署了大量拥有高存储高带宽的节点,构建了一个专用网络。这个网络是跨运营商、跨地域的,虽然内部也划分成多个小网络,但它们之间用高速专有线路连接,是真正的“信息高速公路”,基本上可以认为不存在网络拥堵。
|
||||
|
||||
有了这个高速的专用网之后,CDN就要“分发”源站的“内容”了,用到的就是在[第22讲](https://time.geekbang.org/column/article/108313)说过的“**缓存代理**”技术。使用“推”或者“拉”的手段,把源站的内容逐级缓存到网络的每一个节点上。
|
||||
|
||||
于是,用户在上网的时候就不直接访问源站,而是访问离他“最近的”一个CDN节点,术语叫“**边缘节点**”(edge node),其实就是缓存了源站内容的代理服务器,这样一来就省去了“长途跋涉”的时间成本,实现了“网络加速”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/46/5b/46d1dbbb545fcf3cfb53407e0afe9a5b.png" alt="">
|
||||
|
||||
那么,CDN都能加速什么样的“内容”呢?
|
||||
|
||||
在CDN领域里,“内容”其实就是HTTP协议里的“资源”,比如超文本、图片、视频、应用程序安装包等等。
|
||||
|
||||
资源按照是否可缓存又分为“**静态资源**”和“**动态资源**”。所谓的“静态资源”是指数据内容“静态不变”,任何时候来访问都是一样的,比如图片、音频。所谓的“动态资源”是指数据内容是“动态变化”的,也就是由后台服务计算生成的,每次访问都不一样,比如商品的库存、微博的粉丝数等。
|
||||
|
||||
很显然,只有静态资源才能够被缓存加速、就近访问,而动态资源只能由源站实时生成,即使缓存了也没有意义。不过,如果动态资源指定了“Cache-Control”,允许缓存短暂的时间,那它在这段时间里也就变成了“静态资源”,可以被CDN缓存加速。
|
||||
|
||||
套用一句广告词来形容CDN吧,我觉得非常恰当:“**我们不生产内容,我们只是内容的搬运工。**”
|
||||
|
||||
CDN,正是把“数据传输”这件看似简单的事情“做大做强”“做专做精”,就像专门的快递公司一样,在互联网世界里实现了它的价值。
|
||||
|
||||
## CDN的负载均衡
|
||||
|
||||
我们再来看看CDN是具体怎么运行的,它有两个关键组成部分:**全局负载均衡**和**缓存系统**,对应的是DNS([第6讲](https://time.geekbang.org/column/article/99665))和缓存代理([第21讲](https://time.geekbang.org/column/article/107577)、[第22讲](https://time.geekbang.org/column/article/108313))技术。
|
||||
|
||||
全局负载均衡(Global Sever Load Balance)一般简称为GSLB,它是CDN的“大脑”,主要的职责是当用户接入网络的时候在CDN专网中挑选出一个“最佳”节点提供服务,解决的是用户如何找到“最近的”边缘节点,对整个CDN网络进行“负载均衡”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/ca/6c39e76d58d9f17872c83ae72908faca.png" alt="">
|
||||
|
||||
GSLB最常见的实现方式是“**DNS负载均衡**”,这个在[第6讲](https://time.geekbang.org/column/article/99665)里也说过,不过GSLB的方式要略微复杂一些。
|
||||
|
||||
原来没有CDN的时候,权威DNS返回的是网站自己服务器的实际IP地址,浏览器收到DNS解析结果后直连网站。
|
||||
|
||||
但加入CDN后就不一样了,权威DNS返回的不是IP地址,而是一个CNAME( Canonical Name )别名记录,指向的就是CDN的GSLB。它有点像是HTTP/2里“Alt-Svc”的意思,告诉外面:“我这里暂时没法给你真正的地址,你去另外一个地方再查查看吧。”
|
||||
|
||||
因为没拿到IP地址,于是本地DNS就会向GSLB再发起请求,这样就进入了CDN的全局负载均衡系统,开始“智能调度”,主要的依据有这么几个:
|
||||
|
||||
1. 看用户的IP地址,查表得知地理位置,找相对最近的边缘节点;
|
||||
1. 看用户所在的运营商网络,找相同网络的边缘节点;
|
||||
1. 检查边缘节点的负载情况,找负载较轻的节点;
|
||||
1. 其他,比如节点的“健康状况”、服务能力、带宽、响应时间等。
|
||||
|
||||
GSLB把这些因素综合起来,用一个复杂的算法,最后找出一台“最合适”的边缘节点,把这个节点的IP地址返回给用户,用户就可以“就近”访问CDN的缓存代理了。
|
||||
|
||||
## CDN的缓存代理
|
||||
|
||||
缓存系统是CDN的另一个关键组成部分,相当于CDN的“心脏”。如果缓存系统的服务能力不够,不能很好地满足用户的需求,那GSLB调度算法再优秀也没有用。
|
||||
|
||||
但互联网上的资源是无穷无尽的,不管CDN厂商有多大的实力,也不可能把所有资源都缓存起来。所以,缓存系统只能有选择地缓存那些最常用的那些资源。
|
||||
|
||||
这里就有两个CDN的关键概念:“**命中**”和“**回源**”。
|
||||
|
||||
“命中”就是指用户访问的资源恰好在缓存系统里,可以直接返回给用户;“回源”则正相反,缓存里没有,必须用代理的方式回源站取。
|
||||
|
||||
相应地,也就有了两个衡量CDN服务质量的指标:“**命中率**”和“**回源率**”。命中率就是命中次数与所有访问次数之比,回源率是回源次数与所有访问次数之比。显然,好的CDN应该是命中率越高越好,回源率越低越好。现在的商业CDN命中率都在90%以上,相当于把源站的服务能力放大了10倍以上。
|
||||
|
||||
怎么样才能尽可能地提高命中率、降低回源率呢?
|
||||
|
||||
首先,最基本的方式就是在存储系统上下功夫,硬件用高速CPU、大内存、万兆网卡,再搭配TB级别的硬盘和快速的SSD。软件方面则不断“求新求变”,各种新的存储软件都会拿来尝试,比如Memcache、Redis、Ceph,尽可能地高效利用存储,存下更多的内容。
|
||||
|
||||
其次,缓存系统也可以划分出层次,分成一级缓存节点和二级缓存节点。一级缓存配置高一些,直连源站,二级缓存配置低一些,直连用户。回源的时候二级缓存只找一级缓存,一级缓存没有才回源站,这样最终“扇入度”就缩小了,可以有效地减少真正的回源。
|
||||
|
||||
第三个就是使用高性能的缓存服务,据我所知,目前国内的CDN厂商内部都是基于开源软件定制的。最常用的是专门的缓存代理软件Squid、Varnish,还有新兴的ATS(Apache Traffic Server),而Nginx和OpenResty作为Web服务器领域的“多面手”,凭借着强大的反向代理能力和模块化、易于扩展的优点,也在CDN里占据了不少的份额。
|
||||
|
||||
## 小结
|
||||
|
||||
CDN发展到现在已经有二十来年的历史了,早期的CDN功能比较简单,只能加速静态资源。随着这些年Web 2.0、HTTPS、视频、直播等新技术、新业务的崛起,它也在不断进步,增加了很多的新功能,比如SSL加速、内容优化(数据压缩、图片格式转换、视频转码)、资源防盗链、WAF安全防护等等。
|
||||
|
||||
现在,再说CDN是“搬运工”已经不太准确了,它更像是一个“无微不至”的“网站保姆”,让网站只安心生产优质的内容,其他的“杂事”都由它去代劳。
|
||||
|
||||
1. 由于客观地理距离的存在,直连网站访问速度会很慢,所以就出现了CDN;
|
||||
1. CDN构建了全国、全球级别的专网,让用户就近访问专网里的边缘节点,降低了传输延迟,实现了网站加速;
|
||||
1. GSLB是CDN的“大脑”,使用DNS负载均衡技术,智能调度边缘节点提供服务;
|
||||
1. 缓存系统是CDN的“心脏”,使用HTTP缓存代理技术,缓存命中就返回给用户,否则就要回源。
|
||||
|
||||
## 课下作业
|
||||
|
||||
1. 网站也可以自建同城、异地多处机房,构建集群来提高服务能力,为什么非要选择CDN呢?
|
||||
1. 对于无法缓存的动态资源,你觉得CDN也能有加速效果吗?
|
||||
|
||||
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bc/51/bc1805a7c49977c7838b29602f3bba51.png" alt="unpreview">
|
||||
|
||||
|
||||
151
极客时间专栏/透视HTTP协议/探索篇/38 | WebSocket:沙盒里的TCP.md
Normal file
151
极客时间专栏/透视HTTP协议/探索篇/38 | WebSocket:沙盒里的TCP.md
Normal file
@@ -0,0 +1,151 @@
|
||||
<audio id="audio" title="38 | WebSocket:沙盒里的TCP" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5e/e2/5e9e27f590f3fd65f21975e334447ee2.mp3"></audio>
|
||||
|
||||
在之前讲TCP/IP协议栈的时候,我说过有“TCP Socket”,它实际上是一种功能接口,通过这些接口就可以使用TCP/IP协议栈在传输层收发数据。
|
||||
|
||||
那么,你知道还有一种东西叫“WebSocket”吗?
|
||||
|
||||
单从名字上看,“Web”指的是HTTP,“Socket”是套接字调用,那么这两个连起来又是什么意思呢?
|
||||
|
||||
所谓“望文生义”,大概你也能猜出来,“WebSocket”就是运行在“Web”,也就是HTTP上的Socket通信规范,提供与“TCP Socket”类似的功能,使用它就可以像“TCP Socket”一样调用下层协议栈,任意地收发数据。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ee/28/ee6685c7d3c673b95e46d582828eee28.png" alt="">
|
||||
|
||||
更准确地说,“WebSocket”是一种基于TCP的轻量级网络通信协议,在地位上是与HTTP“平级”的。
|
||||
|
||||
## 为什么要有WebSocket
|
||||
|
||||
不过,已经有了被广泛应用的HTTP协议,为什么要再出一个WebSocket呢?它有哪些好处呢?
|
||||
|
||||
其实WebSocket与HTTP/2一样,都是为了解决HTTP某方面的缺陷而诞生的。HTTP/2针对的是“队头阻塞”,而WebSocket针对的是“请求-应答”通信模式。
|
||||
|
||||
那么,“请求-应答”有什么不好的地方呢?
|
||||
|
||||
“请求-应答”是一种“**半双工**”的通信模式,虽然可以双向收发数据,但同一时刻只能一个方向上有动作,传输效率低。更关键的一点,它是一种“被动”通信模式,服务器只能“被动”响应客户端的请求,无法主动向客户端发送数据。
|
||||
|
||||
虽然后来的HTTP/2、HTTP/3新增了Stream、Server Push等特性,但“请求-应答”依然是主要的工作方式。这就导致HTTP难以应用在动态页面、即时消息、网络游戏等要求“**实时通信**”的领域。
|
||||
|
||||
在WebSocket出现之前,在浏览器环境里用JavaScript开发实时Web应用很麻烦。因为浏览器是一个“受限的沙盒”,不能用TCP,只有HTTP协议可用,所以就出现了很多“变通”的技术,“**轮询**”(polling)就是比较常用的的一种。
|
||||
|
||||
简单地说,轮询就是不停地向服务器发送HTTP请求,问有没有数据,有数据的话服务器就用响应报文回应。如果轮询的频率比较高,那么就可以近似地实现“实时通信”的效果。
|
||||
|
||||
但轮询的缺点也很明显,反复发送无效查询请求耗费了大量的带宽和CPU资源,非常不经济。
|
||||
|
||||
所以,为了克服HTTP“请求-应答”模式的缺点,WebSocket就“应运而生”了。它原来是HTML5的一部分,后来“自立门户”,形成了一个单独的标准,RFC文档编号是6455。
|
||||
|
||||
## WebSocket的特点
|
||||
|
||||
WebSocket是一个真正“**全双工**”的通信协议,与TCP一样,客户端和服务器都可以随时向对方发送数据,而不用像HTTP“你拍一,我拍一”那么“客套”。于是,服务器就可以变得更加“主动”了。一旦后台有新的数据,就可以立即“推送”给客户端,不需要客户端轮询,“实时通信”的效率也就提高了。
|
||||
|
||||
WebSocket采用了二进制帧结构,语法、语义与HTTP完全不兼容,但因为它的主要运行环境是浏览器,为了便于推广和应用,就不得不“搭便车”,在使用习惯上尽量向HTTP靠拢,这就是它名字里“Web”的含义。
|
||||
|
||||
服务发现方面,WebSocket没有使用TCP的“IP地址+端口号”,而是延用了HTTP的URI格式,但开头的协议名不是“http”,引入的是两个新的名字:“**ws**”和“**wss**”,分别表示明文和加密的WebSocket协议。
|
||||
|
||||
WebSocket的默认端口也选择了80和443,因为现在互联网上的防火墙屏蔽了绝大多数的端口,只对HTTP的80、443端口“放行”,所以WebSocket就可以“伪装”成HTTP协议,比较容易地“穿透”防火墙,与服务器建立连接。具体是怎么“伪装”的,我稍后再讲。
|
||||
|
||||
下面我举几个WebSocket服务的例子,你看看,是不是和HTTP几乎一模一样:
|
||||
|
||||
```
|
||||
ws://www.chrono.com
|
||||
ws://www.chrono.com:8080/srv
|
||||
wss://www.chrono.com:445/im?user_id=xxx
|
||||
|
||||
```
|
||||
|
||||
要注意的一点是,WebSocket的名字容易让人产生误解,虽然大多数情况下我们会在浏览器里调用API来使用WebSocket,但它不是一个“调用接口的集合”,而是一个通信协议,所以我觉得把它理解成“**TCP over Web**”会更恰当一些。
|
||||
|
||||
## WebSocket的帧结构
|
||||
|
||||
刚才说了,WebSocket用的也是二进制帧,有之前HTTP/2、HTTP/3的经验,相信你这次也能很快掌握WebSocket的报文结构。
|
||||
|
||||
不过WebSocket和HTTP/2的关注点不同,WebSocket更**侧重于“实时通信”**,而HTTP/2更侧重于提高传输效率,所以两者的帧结构也有很大的区别。
|
||||
|
||||
WebSocket虽然有“帧”,但却没有像HTTP/2那样定义“流”,也就不存在“多路复用”“优先级”等复杂的特性,而它自身就是“全双工”的,也就不需要“服务器推送”。所以综合起来,WebSocket的帧学习起来会简单一些。
|
||||
|
||||
下图就是WebSocket的帧结构定义,长度不固定,最少2个字节,最多14字节,看着好像很复杂,实际非常简单。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/29/c4/29d33e972dda5a27aa4773eea896a8c4.png" alt="">
|
||||
|
||||
开头的两个字节是必须的,也是最关键的。
|
||||
|
||||
第一个字节的第一位“**FIN**”是消息结束的标志位,相当于HTTP/2里的“END_STREAM”,表示数据发送完毕。一个消息可以拆成多个帧,接收方看到“FIN”后,就可以把前面的帧拼起来,组成完整的消息。
|
||||
|
||||
“FIN”后面的三个位是保留位,目前没有任何意义,但必须是0。
|
||||
|
||||
第一个字节的后4位很重要,叫**“Opcode**”,操作码,其实就是帧类型,比如1表示帧内容是纯文本,2表示帧内容是二进制数据,8是关闭连接,9和10分别是连接保活的PING和PONG。
|
||||
|
||||
第二个字节第一位是掩码标志位“**MASK**”,表示帧内容是否使用异或操作(xor)做简单的加密。目前的WebSocket标准规定,客户端发送数据必须使用掩码,而服务器发送则必须不使用掩码。
|
||||
|
||||
第二个字节后7位是“**Payload len**”,表示帧内容的长度。它是另一种变长编码,最少7位,最多是7+64位,也就是额外增加8个字节,所以一个WebSocket帧最大是2^64。
|
||||
|
||||
长度字段后面是“**Masking-key**”,掩码密钥,它是由上面的标志位“MASK”决定的,如果使用掩码就是4个字节的随机数,否则就不存在。
|
||||
|
||||
这么分析下来,其实WebSocket的帧头就四个部分:“**结束标志位+操作码+帧长度+掩码**”,只是使用了变长编码的“小花招”,不像HTTP/2定长报文头那么简单明了。
|
||||
|
||||
我们的实验环境利用OpenResty的“lua-resty-websocket”库,实现了一个简单的WebSocket通信,你可以访问URI“/38-1”,它会连接后端的WebSocket服务“ws://127.0.0.1/38-0”,用Wireshark抓包就可以看到WebSocket的整个通信过程。
|
||||
|
||||
下面的截图是其中的一个文本帧,因为它是客户端发出的,所以需要掩码,报文头就在两个字节之外多了四个字节的“Masking-key”,总共是6个字节。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c9/94/c91ee4815097f5f9059ab798bb841594.png" alt="">
|
||||
|
||||
而报文内容经过掩码,不是直接可见的明文,但掩码的安全强度几乎是零,用“Masking-key”简单地异或一下就可以转换出明文。
|
||||
|
||||
## WebSocket的握手
|
||||
|
||||
和TCP、TLS一样,WebSocket也要有一个握手过程,然后才能正式收发数据。
|
||||
|
||||
这里它还是搭上了HTTP的“便车”,利用了HTTP本身的“协议升级”特性,“伪装”成HTTP,这样就能绕过浏览器沙盒、网络防火墙等等限制,这也是WebSocket与HTTP的另一个重要关联点。
|
||||
|
||||
WebSocket的握手是一个标准的HTTP GET请求,但要带上两个协议升级的专用头字段:
|
||||
|
||||
- “Connection: Upgrade”,表示要求协议“升级”;
|
||||
- “Upgrade: websocket”,表示要“升级”成WebSocket协议。
|
||||
|
||||
另外,为了防止普通的HTTP消息被“意外”识别成WebSocket,握手消息还增加了两个额外的认证用头字段(所谓的“挑战”,Challenge):
|
||||
|
||||
- Sec-WebSocket-Key:一个Base64编码的16字节随机数,作为简单的认证密钥;
|
||||
- Sec-WebSocket-Version:协议的版本号,当前必须是13。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8f/97/8f007bb0e403b6cc28493565f709c997.png" alt="">
|
||||
|
||||
服务器收到HTTP请求报文,看到上面的四个字段,就知道这不是一个普通的GET请求,而是WebSocket的升级请求,于是就不走普通的HTTP处理流程,而是构造一个特殊的“101 Switching Protocols”响应报文,通知客户端,接下来就不用HTTP了,全改用WebSocket协议通信。(有点像TLS的“Change Cipher Spec”)
|
||||
|
||||
WebSocket的握手响应报文也是有特殊格式的,要用字段“Sec-WebSocket-Accept”验证客户端请求报文,同样也是为了防止误连接。
|
||||
|
||||
具体的做法是把请求头里“Sec-WebSocket-Key”的值,加上一个专用的UUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,再计算SHA-1摘要。
|
||||
|
||||
```
|
||||
encode_base64(
|
||||
sha1(
|
||||
Sec-WebSocket-Key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' ))
|
||||
|
||||
```
|
||||
|
||||
客户端收到响应报文,就可以用同样的算法,比对值是否相等,如果相等,就说明返回的报文确实是刚才握手时连接的服务器,认证成功。
|
||||
|
||||
握手完成,后续传输的数据就不再是HTTP报文,而是WebSocket格式的二进制帧了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/84/03/84e9fa337f2b4c2c9f14760feb41c903.png" alt="">
|
||||
|
||||
## 小结
|
||||
|
||||
浏览器是一个“沙盒”环境,有很多的限制,不允许建立TCP连接收发数据,而有了WebSocket,我们就可以在浏览器里与服务器直接建立“TCP连接”,获得更多的自由。
|
||||
|
||||
不过自由也是有代价的,WebSocket虽然是在应用层,但使用方式却与“TCP Socket”差不多,过于“原始”,用户必须自己管理连接、缓存、状态,开发上比HTTP复杂的多,所以是否要在项目中引入WebSocket必须慎重考虑。
|
||||
|
||||
1. HTTP的“请求-应答”模式不适合开发“实时通信”应用,效率低,难以实现动态页面,所以出现了WebSocket;
|
||||
1. WebSocket是一个“全双工”的通信协议,相当于对TCP做了一层“薄薄的包装”,让它运行在浏览器环境里;
|
||||
1. WebSocket使用兼容HTTP的URI来发现服务,但定义了新的协议名“ws”和“wss”,端口号也沿用了80和443;
|
||||
1. WebSocket使用二进制帧,结构比较简单,特殊的地方是有个“掩码”操作,客户端发数据必须掩码,服务器则不用;
|
||||
1. WebSocket利用HTTP协议实现连接握手,发送GET请求要求“协议升级”,握手过程中有个非常简单的认证机制,目的是防止误连接。
|
||||
|
||||
## 课下作业
|
||||
|
||||
1. WebSocket与HTTP/2有很多相似点,比如都可以从HTTP/1升级,都采用二进制帧结构,你能比较一下这两个协议吗?
|
||||
1. 试着自己解释一下WebSocket里的”Web“和”Socket“的含义。
|
||||
1. 结合自己的实际工作,你觉得WebSocket适合用在哪些场景里?
|
||||
|
||||
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/5b/4b81de6b5c57db92ed7808344482ef5b.png" alt="unpreview">
|
||||
|
||||
|
||||
Reference in New Issue
Block a user