meinheld为何比gevent高效?

【前言】

两者都是高性能的WSGI兼容的web服务器。既然是同种东西,必然有对比,网上有挺多benchmark,我也做过对应的benchmark,不过没有整理,这里推荐一下网上的一篇benchmark,能够看出meinheld的性能确实好得令人意外。那么为什么meinheld会比gevent性能高这么多呢?我们从底层实现来看看,他究竟做了一些什么。

【meinheld和gevent】

两者实现很相似。
meinheld:greenlet(协程) + picoev(高性能网络库)
gevent:greenlet(协程) + libevent(高性能网络库)

【greenlet】

python中的yield和第三方库greenlet,都是用来实现协程的利器。
greenlet 提供了在协程中直接切换控制权的方式,比生成器(yield)更加灵活、简洁。

协程:又称微线程,纤程。
协程的这种“挂起”和“唤醒”机制实质上是将一个过程切分成了若干个子过程,给了我们一种以扁平的方式来使用事件回调模型。优点:共享进程的上下文,一个进程可以创建百万,千万的coroutine。

【libevent】

libevent是一个事件驱动的网络库,主要设计模式是Reactor(反应器)
程序通过Libevent框架注册相应的事件和回调函数;当这些事件发生时,Libevent会调用这些回调函数处理相应的事件(I/O读写、定时和信号)。整个过程都是异步高效的。想看具体源码实现的请移步这里。这里我们只看主要处理部分event_base_loop。

简要说明event_base_loop实现。
事件:首先loop中要处理的事件有3种,一种是计时事件(timeout触发),一种是普通I/O事件(select, poll,epoll),还有一种信号事件(signal),其中信号事件最终也是被转换成普通I/O事件被监听。

流程:

  1. 先通过Timer最小堆(以时间为排序的键)找出至少要等待的时间。(代码中的timeout_next()函数)。
  2. 通过select发送这些事件fd到内核并设置时间为1中所求的等待时间。然后把select返回的就绪事件放到就绪列表。(对应 evsel->dispatch(base, evbase, tv_p))。
  3. 然后把现在超时的计时事件放到就绪列表。(对应gettime(base, &base->tv_cache))。
  4. 最后调用处理函数处理就绪列表中的事件(timeout_process(base))。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
int event_base_loop(struct event_base *base, int flags)
{

const struct eventop *evsel = base->evsel;
void *evbase = base->evbase;
struct timeval tv;
struct timeval *tv_p;
int res, done;
// 清空时间缓存
base->tv_cache.tv_sec = 0;
// evsignal_base是全局变量,在处理signal时,用于指名signal所属的event_base实例
if (base->sig.ev_signal_added)
evsignal_base = base;
done = 0;
while (!done) { // 事件主循环
// 查看是否需要跳出循环,程序可以调用event_loopexit_cb()设置event_gotterm标记
// 调用event_base_loopbreak()设置event_break标记
if (base->event_gotterm) {
base->event_gotterm = 0;
break;
}
if (base->event_break) {
base->event_break = 0;
break;
}
// 校正系统时间,如果系统使用的是非MONOTONIC时间,用户可能会向后调整了系统时间
// 在timeout_correct函数里,比较last wait time和当前时间,如果当前时间< last wait time
// 表明时间有问题,这是需要更新timer_heap中所有定时事件的超时时间。
timeout_correct(base, &tv);

// 根据timer heap中事件的最小超时时间,计算系统I/O demultiplexer的最大等待时间
tv_p = &tv;
if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) {
timeout_next(base, &tv_p);
} else {
// 依然有未处理的就绪时间,就让I/O demultiplexer立即返回,不必等待
// 下面会提到,在libevent中,低优先级的就绪事件可能不能立即被处理
evutil_timerclear(&tv);
}
// 如果当前没有注册事件,就退出
if (!event_haveevents(base)) {
event_debug(("%s: no events registered.", __func__));
return (1);
}
// 更新last wait time,并清空time cache
gettime(base, &base->event_tv);
base->tv_cache.tv_sec = 0;
// 调用系统I/O demultiplexer等待就绪I/O events,可能是epoll_wait,或者select等;
// 在evsel->dispatch()中,会把就绪signal event、I/O event插入到激活链表中
res = evsel->dispatch(base, evbase, tv_p);
if (res == -1)
return (-1);
// 将time cache赋值为当前系统时间
gettime(base, &base->tv_cache);
// 检查heap中的timer events,将就绪的timer event从heap上删除,并插入到激活链表中
timeout_process(base);
// 调用event_process_active()处理激活链表中的就绪event,调用其回调函数执行事件处理
// 该函数会寻找最高优先级(priority值越小优先级越高)的激活事件链表,
// 然后处理链表中的所有就绪事件;
// 因此低优先级的就绪事件可能得不到及时处理;
if (base->event_count_active) {
event_process_active(base);
if (!base->event_count_active && (flags & EVLOOP_ONCE))
done = 1;
} else if (flags & EVLOOP_NONBLOCK)
done = 1;
}
// 循环结束,清空时间缓存
base->tv_cache.tv_sec = 0;
event_debug(("%s: asked to terminate loop.", __func__));
return (0);
}

【picoev】

picoev在项目下有把picoev和libevent这些库做对比,作者也提了一下为什么picoev的速度会这么快。主要有两个原因。

  1. picoev几乎所有顺序结构都是用数组实现的,索引访问速度比libevent的链表快很多。
  2. picoev采用了环形队列+vector+bitmap来实现定时事件的检测。

下面看看picoev的picoev_loop_once。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int picoev_loop_once(picoev_loop* loop, int max_wait) {
loop->now = time(NULL); //获取当前时间
// 最大等待事件不超过计时器的处理时间
if (max_wait > loop->timeout.resolution) {
max_wait = loop->timeout.resolution;
}
// 使用select去检测事件是否完成,如果完成就调对应的回调函数处理
if (picoev_poll_once_internal(loop, max_wait) != 0) {
return -1;
}
if (max_wait != 0) { //有指定时间则刷新当前时间
loop->now = time(NULL);
}
// 处理到时间的计时事件
picoev_handle_timeout_internal(loop);
return 0;
}

这里主要讲一下loop的结构,因为这是高效的原因。

loop结构图

  1. 对于timeout环形队列,每经过resolution时间就往后移动一块,当前队头永远指向刚刚到达时间的事件块,如图当前处理的是2,那么说明队列头在2,那么再经过resolution时间就会到3,根据时间不断后移,循环利用。
  2. 在处理每一块timeout里面注册的事件时,遍历所有不为0的vector,得出对应的fd。图中已经写的很清楚的,其实原理和16进制一样简单。插入一个事件的时间复杂度为O(1),遍历所有在timeout块的注册事件时间复杂度等价为O(n)[注:这里n为timeout里面注册事件的个数],对比libevent的最小堆O(logn)插入,每次处理一个后调整堆的复杂度O(logn)处理n个就为O(nlogn),确实是高效很多。
  3. 还有一个高效的地方在于,picoev是检测到有一个事件就马上处理(无阻塞),不像libevent挂起等待最小等待时间到达(阻塞),然后才对所有就绪事件队列里面的事件进行处理,不过这也导致了picoev不能设定事件处理的优先级。

缺点(对比libevent):作者在最后说picoev并没有libevent成熟,也没有很多功能,现在只支持select,epoll,kqueue,我们也可以看到没有信号事件的处理,优先级设定这些功能的支持。不过他简单快速,而且支持多线程。

【最后】

使用python做web开发的同学,可以尝试一下nginx + meinheld + gunicorn + flask。

文章目录
  1. 1. 【前言】
  2. 2. 【meinheld和gevent】
  3. 3. 【greenlet】
  4. 4. 【libevent】
  5. 5. 【picoev】
  6. 6. 【最后】
,