惊群效应是什么

惊群效应是什么
惊群效应(thundering herd)是指多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群效应。

惊群效应消耗了什么
Linux 内核对用户进程(线程)频繁地做无效的调度、上下文切换等使系统性能大打折扣。上下文切换(context switch)过高会导致 CPU 像个搬运工,频繁地在寄存器和运行队列之间奔波,更多的时间花在了进程(线程)切换,而不是在真正工作的进程(线程)上面。直接的消耗包括 CPU 寄存器要保存和加载(例如程序计数器)、系统调度器的代码需要执行。间接的消耗在于多核 cache 之间的共享数据。
为了确保只有一个进程(线程)得到资源,需要对资源操作进行加锁保护,加大了系统的开销。目前一些常见的服务器软件有的是通过锁机制解决的,比如 Nginx(它的锁机制是默认开启的,可以关闭);还有些认为惊群对系统性能影响不大,没有去处理,比如 Lighttpd。

pm2是怎么工作的吗 pm2工作原理

Node Cluster
熟悉 js 的朋友都知道,js 是单线程的,在 Node 中,采用的是 多进程单线程 的模型。由于单线程的限制,在多核服务器上,我们往往需要启动多个进程才能最大化服务器性能。

Node 在 V0.8 版本之后引入了 cluster模块,通过一个主进程 (master) 管理多个子进程 (worker) 的方式实现集群。

通信
Node中主进程和子进程之间通过进程间通信 (IPC) 实现进程间的通信,进程间通过 send 方法发送消息,监听 message 事件收取信息,这是 cluster模块 通过集成 EventEmitter 实现的。

负载均衡
了解 cluster 的话会知道,子进程是通过 cluster.fork() 创建的。在 linux 中,系统原生提供了 fork 方法,那么为什么 Node 选择自己实现 cluster模块 ,而不是直接使用系统原生的方法?主要的原因是以下两点:

fork的进程监听同一端口会导致端口占用错误
fork的进程之间没有负载均衡,容易导致惊群现象
在 cluster模块 中,针对第一个问题,通过判断当前进程是否为 master进程,若是,则监听端口,若不是则表示为 fork 的 worker进程,不监听端口。

针对第二个问题,cluster模块 内置了负载均衡功能,master进程 负责监听端口接收请求,然后通过调度算法(默认为 Round-Robin,可以通过环境变量 NODE_CLUSTER_SCHED_POLICY 修改调度算法)分配给对应的 worker进程。

pm2的实现
pm2 基于 cluster模块 进行了封装,它能自动监控进程状态、重启进程、停止不稳定进程、日志存储等。利用 pm2 时,可以在不修改代码的情况下实现负载均衡集群。

这篇文章我们要关注的是 pm2 的 Satan进程、God Deamon守护进程 以及 两者之间的 进程间远程调用RPC。

撒旦(Satan),主要指《圣经》中的堕天使(也称堕天使撒旦),被看作与上帝的力量相对的邪恶、黑暗之源,是God的对立面。

其中 Satan.js 提供程序的退出、杀死等方法,God.js 负责维持进程的正常运行,God进程启动后一直运行,相当于 cluster 中的 Master进程,维持 worker 进程的正常运行。

RPC 是指远程过程调用协议,具体释义就不细讲了,感兴趣的自行查阅。在 pm2 中用于同一机器上的不同进程之间的方法调用。

https的握手过程是怎样的

1. Client-hello 阶段

浏览器中完成地址输入后, 解析域名获得 IP Host 地址, 浏览器会与此 Host 的443(默认, 如果指定其他端口则会连接此端口) 尝试连接, 也就是 TLS 握手协议的 Client-hello, 上图的第一步.

浏览器会将”支持的加密组件”/”尝试连接到Host头”等信息发送给服务器, 并会附上一份随机生成的 session ticket1.

2. Server-hello 阶段

服务器收到浏览器发送来的 TLS 握手请求后, 存储浏览器发送的session ticket2, 然后根据发送来的 host 寻找对于的服务器证书, 然后会将服务器证书, 服务器与浏览器妥协(均支持)的加密套件方法, 和一份随机生成的 session ticket 返回给浏览器.

3. Cipher-spec 阶段

浏览器收到服务器返回的证书后, 会验证证书有效性. 验证步骤大概如下:

验证证书有效期(起止时间)
验证证书域名(与浏览器地址栏中域名是否匹配)
验证证书吊销状态(CRL+OCSP), [见本文后”吊销检查”章节].
验证证书颁发机构, 如果颁发机构是中间证书, 在验证中间证书的有效期/颁发机构/吊销状态. 一直验证到最后一层证书, 如果最后一层证书是在操作系统或浏览器内置, 那么就是可信的, 否则就是自签名. [见本文后”签发者”章节]
以上验证步骤, 需要全部通过. 否则就会显示警告.

若检查通过, 随机生成一份 session ticket 3 (这是浏览器生成的第二份 ticket), 通过返回证书中的公钥, 用协商的”秘钥交换算法”加密, 返回给服务器.

同时浏览器用 session ticket 1(浏) & session ticket 2(服) & session ticket 3(浏) 组合成 session key.

服务器收到 Ciper-spec 后, 用配置的私钥, 解密出 session ticket3, 用 session ticket 1(浏) & session ticket 2(服) & session ticket 3(浏) 组合成 session key.

此处不难得知, 服务器与浏览器交换的最终秘钥, session key全等且未泄露(session ticket 1 和 session ticket 2可以抓包, 但session ticket 3是无法窃听的).

为什么session ticket 3无法窃听?

有个 webtrust 组织, 专门负责备案世界上各国商业与政府官方 CA 机构的公钥证书. 如果审计通过, 其他浏览器及操作系统/客户端才允许加入信任列表. 否则是不允许加入的. 如果中间人拦截了 session ticket 3 的响应密文, 没有私钥, 中间攻击人是解密不了的. 而要想拿到私钥, 攻击人可以做到, 就是在客户端和服务器中间搭建代理, 替换掉 SSL 证书, 以实现服务器返回证书时候中间替换自己的, 从而在中间拦截服务器和客户端两头的通信.

4. 内容传输阶段

至此, TLS 连接建立完成, 在连接销毁前, 浏览器与服务器彼此数据均通过 session key 来进行对称加密.

通过

上述过程, 其实是别有用心的, 因为非对称加密非常消耗 CPU. 所以只有在协商秘钥时候使用非对称加密, 而应用层数据交换就用协商成功的秘钥作为私钥对称加密传输(服务器响应的加密返回, 客户端提交的也加密提交).

题外话:
问: HTTPS 是否会拖慢性能?

答案: 看具体部署的情况.

浏览器在加密 session ticket3时, 和服务器在接受浏览器返回 session ticket3时, 是非对称加密中可能出现耗时的步骤. 但这个步骤顶多几毫秒, 并且现代化浏览器和 NGINX 已经支持 session 复用, 造成的性能损耗几乎可以忽略不计.

而真正可能拖慢性能的, 只可能是在吊销检查步骤中.

因为上面说了, 吊销状态检查只能是同步的, 那么受到 CA 厂商的部署限制, 极可能会将 CRL 服务器和 OCSP 服务器部署在遥远的小机房, 带宽/链路都是极差的, 这种, DNS 解析和连接 CRL/OCSP 服务器均需要耗时, 此过程的损耗, 是一大批在知乎的所谓专家所言的加密解密过程损耗的数十倍到数百倍.

问: 怎么规避吊销状态带来的损耗?

答案: 仁者见仁, 智者见智. 这里给出两个建议

1. 踩上大厂的顺风车. 如百度阿里腾讯和苹果微软操作系统各种常见网站和软体的服务器/代码签名证书, 均有 CRL 和 OCSP, 而 CRL 是操作系统层复用的, 只要在 TTL 时间内, 操作系统检查过对应 CA 的 CRL, 那么 CRL 均可避免二次下载, 用户访问就可实现加速. OCSP 也至少可以搭上一个免去 DNS 解析的红利. 例如 Symantec/GeoTrust/GlobalSign

2. 买国内 CA 的证书. 例如 Quantum CA.

问: 12306的证书部署, 除了 CA 不受信任外, 还有那些错误?

答: 除了 CA 不受信任, 还存在问题:

没有吊销状态声明, 根据最新的 webtrust 标准, 没有声明吊销状态的证书不受信任.
签名算法用了过期的 SHA-1.

HTTP缓存机制详解

什么是HTTP缓存
HTTP 缓存可以说是HTTP性能优化中简单高效的一种优化方式了,缓存是一种保存资源副本并在下次请求时直接使用该副本的技术,当 web 缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。
一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,节省网络流量,并且由于缓存文件可以重复利用,降低网络负荷,提高客户端响应。

缓存策略
在阐述HTTP不同缓存策略之前,我们需要知道用户刷新/访问行为 的手段分成三类:

在URI输入栏中输入然后回车/通过书签访问
F5/点击工具栏中的刷新按钮/右键菜单重新加载
Ctl+F5 (完全不使用HTTP缓存)
不同的刷新手段,会导致浏览器使用不同的缓存策略,我们下面会分析到

HTTP 缓存主要是通过请求和响应报文头中的对应 Header 信息,来控制缓存的策略。
响应头中相关字段为Expires、Cache-Control、Last-Modified、Etag。

HTTP缓存的类型很多,根据是否需要重新向服务器发起请求来分类包括两种:强制缓存 和 对比缓存

强制缓存的HTTP相关头部Cache-Control,Exipres(HTTP1.0),浏览器直接读本地缓存,不会再跟服务器端交互,状态码200。
对比缓存的HTTP相关头部Last-Modified / If-Modified-Since, Etag / If-None-Match (优先级比Last-Modified / If-Modified-Since高),每次请求需要让服务器判断一下资源是否更新过,从而决定浏览器是否使用缓存,如果是,则返回304,否则重新完整响应。

JavaScript中的Event Loop(事件循环)机制

javascript从诞生之日起就是一门单线程的非阻塞的脚本语言。这是由其最初的用途来决定的:与浏览器交互。

单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务。

而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。

单线程是必要的,也是javascript这门语言的基石,原因之一在其最初也是最主要的执行环境——浏览器中,我们需要进行各种各样的dom操作。试想一下 如果javascript是多线程的,那么当两个线程同时对dom进行一项操作,例如一个向其添加事件,而另一个删除了这个dom,此时该如何处理呢?因此,为了保证不会 发生类似于这个例子中的情景,javascript选择只用一个主线程来执行代码,这样就保证了程序执行的一致性。

浏览器环境下js引擎的事件循环机制
1.执行栈与事件队列
当javascript代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针。 但是我们这里说的执行栈和上面这个栈的意义却有些不同。

我们知道,当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。 而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。

当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。。这个过程反复进行,直到执行栈中的代码全部执行完毕。

js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。
.macro task与micro task
以上的事件循环过程是一个宏观的表述,实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task)。

以下事件属于宏任务:

setInterval()
setTimeout()
以下事件属于微任务

new Promise()
new MutaionObserver()
前面我们介绍过,在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈…如此反复,进入循环。

我们只需记住当当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

这样就能解释下面这段代码的结果:

setTimeout(function () {
console.log(1);
});

new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);
})
结果为:

2
3
1

node环境下的事件循环机制
1.与浏览器环境有何不同?
在node中,事件循环表现出的状态与浏览器中大致相同。不同的是node中有一套自己的模型。node中事件循环的实现是依靠的libuv引擎。我们知道node选择chrome v8引擎作为js解释器,v8引擎将js代码分析后去调用对应的node api,而这些api最后则由libuv引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。 因此实际上node中的事件循环存在于libuv引擎中。

2.事件循环模型
下面是一个libuv引擎中的事件循环的模型:

┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
注:模型中的每一个方块代表事件循环的一个阶段

这个模型是node官网上的一篇文章中给出的,我下面的解释也都来源于这篇文章。我会在文末把文章地址贴出来,有兴趣的朋友可以亲自与看看原文。

3.事件循环各阶段详解
从上面这个模型中,我们可以大致分析出node中的事件循环的顺序:

外部输入数据–>轮询阶段(poll)–>检查阶段(check)–>关闭事件回调阶段(close callback)–>定时器检测阶段(timer)–>I/O事件回调阶段(I/O callbacks)–>闲置阶段(idle, prepare)–>轮询阶段…

以上各阶段的名称是根据我个人理解的翻译,为了避免错误和歧义,下面解释的时候会用英文来表示这些阶段。

这些阶段大致的功能如下:

timers: 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()。
I/O callbacks: 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。
idle, prepare: 这个阶段仅在内部使用,可以不必理会。
poll: 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
check: setImmediate()的回调会在这个阶段执行。
close callbacks: 例如socket.on(‘close’, …)这种close事件的回调。
下面我们来按照代码第一次进入libuv引擎后的顺序来详细解说这些阶段:

poll阶段
当个v8引擎将js代码解析后传入libuv引擎后,循环首先进入poll阶段。poll阶段的执行逻辑如下: 先查看poll queue中是否有事件,有任务就按先进先出的顺序依次执行回调。 当queue为空时,会检查是否有setImmediate()的callback,如果有就进入check阶段执行这些callback。但同时也会检查是否有到期的timer,如果有,就把这些到期的timer的callback按照调用顺序放到timer queue中,之后循环会进入timer阶段执行queue中的 callback。 这两者的顺序是不固定的,收到代码运行的环境的影响。如果两者的queue都是空的,那么loop会在poll阶段停留,直到有一个i/o事件返回,循环会进入i/o callback阶段并立即执行这个事件的callback。

值得注意的是,poll阶段在执行poll queue中的回调时实际上不会无限的执行下去。有两种情况poll阶段会终止执行poll queue中的下一个回调:1.所有回调执行完毕。2.执行数超过了node的限制。

check阶段
check阶段专门用来执行setImmediate()方法的回调,当poll阶段进入空闲状态,并且setImmediate queue中有callback时,事件循环进入这个阶段。

close阶段
当一个socket连接或者一个handle被突然关闭时(例如调用了socket.destroy()方法),close事件会被发送到这个阶段执行回调。否则事件会用process.nextTick()方法发送出去。

timer阶段
这个阶段以先进先出的方式执行所有到期的timer加入timer队列里的callback,一个timer callback指得是一个通过setTimeout或者setInterval函数设置的回调函数。

I/O callback阶段
如上文所言,这个阶段主要执行大部分I/O事件的回调,包括一些为操作系统执行的回调。例如一个TCP连接生错误时,系统需要执行回调来获得这个错误的报告。

4.process.nextTick,setTimeout与setImmediate的区别与使用场景
在node中有三个常用的用来推迟任务执行的方法:process.nextTick,setTimeout(setInterval与之相同)与setImmediate

这三者间存在着一些非常不同的区别:

process.nextTick()
尽管没有提及,但是实际上node中存在着一个特殊的队列,即nextTick queue。这个队列中的回调执行虽然没有被表示为一个阶段,当时这些事件却会在每一个阶段执行完毕准备进入下一个阶段时优先执行。当事件循环准备进入下一个阶段之前,会先检查nextTick queue中是否有任务,如果有,那么会先清空这个队列。与执行poll queue中的任务不同的是,这个操作在队列清空前是不会停止的。这也就意味着,错误的使用process.nextTick()方法会导致node进入一个死循环。。直到内存泄漏。

那么合适使用这个方法比较合适呢?下面有一个例子:

const server = net.createServer(() => {}).listen(8080);

server.on(‘listening’, () => {});
这个例子中当,当listen方法被调用时,除非端口被占用,否则会立刻绑定在对应的端口上。这意味着此时这个端口可以立刻触发listening事件并执行其回调。然而,这时候on(‘listening)还没有将callback设置好,自然没有callback可以执行。为了避免出现这种情况,node会在listen事件中使用process.nextTick()方法,确保事件在回调函数绑定后被触发。

setTimeout()和setImmediate()
在三个方法中,这两个方法最容易被弄混。实际上,某些情况下这两个方法的表现也非常相似。然而实际上,这两个方法的意义却大为不同。

setTimeout()方法是定义一个回调,并且希望这个回调在我们所指定的时间间隔后第一时间去执行。注意这个“第一时间执行”,这意味着,受到操作系统和当前执行任务的诸多影响,该回调并不会在我们预期的时间间隔后精准的执行。执行的时间存在一定的延迟和误差,这是不可避免的。node会在可以执行timer回调的第一时间去执行你所设定的任务。

setImmediate()方法从意义上将是立刻执行的意思,但是实际上它却是在一个固定的阶段才会执行回调,即poll阶段之后。有趣的是,这个名字的意义和之前提到过的process.nextTick()方法才是最匹配的。node的开发者们也清楚这两个方法的命名上存在一定的混淆,他们表示不会把这两个方法的名字调换过来—因为有大量的node程序使用着这两个方法,调换命名所带来的好处与它的影响相比不值一提。

setTimeout()和不设置时间间隔的setImmediate()表现上及其相似。猜猜下面这段代码的结果是什么?

setTimeout(() => {
console.log(‘timeout’);
}, 0);

setImmediate(() => {
console.log(‘immediate’);
});
实际上,答案是不一定。没错,就连node的开发者都无法准确的判断这两者的顺序谁前谁后。这取决于这段代码的运行环境。运行环境中的各种复杂的情况会导致在同步队列里两个方法的顺序随机决定。但是,在一种情况下可以准确判断两个方法回调的执行顺序,那就是在一个I/O事件的回调中。下面这段代码的顺序永远是固定的:

const fs = require(‘fs’);

fs.readFile(__filename, () => {
setTimeout(() => {
console.log(‘timeout’);
}, 0);
setImmediate(() => {
console.log(‘immediate’);
});
});
答案永远是:

immediate
timeout
因为在I/O事件的回调中,setImmediate方法的回调永远在timer的回调前执行。