最近openstack集群多次出现nova-network服务cpu占用100%的情况,跟进后发现是eventlet和qpid-python库配合上出现问题。因为对eventlet不熟悉,所以花了点时间了解了一下eventlet,将学习过程记录下来,以免时间一长就忘了。
eventlet在openstack中出镜率很高,基本上涉及并发的地方都用到了它。eventlet对greenlet进行了封装,学习eventlet之前有必要先了解一下greenlet。
Greenlet
最好的学习材料是greenlet官方文档,下面部分从官方文档翻译而来。
一个 “greenlet” 是一个很小的独立伪线程。可以把它想象成一个堆栈,栈底是初始调用,而栈顶是当前greenlet的暂停位置。使用greenlet的过程就是创建一些这样的堆栈,然后在他们之间进行跳转。跳转必须是显式的:一个greenlet必须选择跳转到另一个指定的greenlet,这会让前一个挂起,而后一个从上次挂起的位置恢复执行。两个greenlet之间的跳转称为切换(switch) 。
创建一个greenlet时,会得到一个初始化过的空堆栈;当第一次切换到这个greenlet时,它会启动指定的函数,这个函数可能会调用其他函数或者切换到另一个greenlet等等。当最终初始函数结束时,greenlet的堆栈又变成空,这个greenlet也就“死掉”了。greenlet也会因为一个未捕捉的异常而“死掉”。
看一个例子:from greenlet import greenlet
def test1():
print 12
gr2.switch()
print 34
def test2():
print 56
gr1.switch()
print 78
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
输出:
12
56
34
最后一行跳转到 test1() ,打印12,然后跳转到 test2() ,打印56,再跳转回 test1() ,打印34,然后 test1() 结束,gr1死掉。这时执行会回到原来的 gr1.switch() 调用。注意,78是不会被打印的。
父greenlet
现在看看一个greenlet“死掉”时执行点去了哪里。每个greenlet都有一个父greenlet。父greenlet即每个greenlet被创建时所在的greenlet(不过可以在任何时候改变)。当greenlet死掉时,执行点会回到父greenlet中原来的位置。这样,greenlet就被组织成一棵树,顶层的不在用户创建的 greenlet 中运行的代码会在被称为main greenlet的greenlet(隐式创建)中运行,也就是树根。
在上面的例子中,gr1和gr2都是把main greenlet作为父greenlet的。任何一个死掉,执行点都会回到主函数。
未捕获的异常会传播到父greenlet。如果上面的 test2() 包含一个打印错误(typo),会生成一个 NameError 并杀死gr2,然后执行点会回到主函数。traceback会显示 test2() 而不是 test1() 。记住,切换不是调用,而是执行点在并行的栈容器间进行交换,父greenlet即是逻辑上当前greenlet执行完后紧接着执行的下一个堆栈。
切换
greenlet之间的切换发生在greenlet的 switch() 方法被调用时,这会让执行点跳转到switch()函数被调用的greenlet上。或者在greenlet死掉时,跳转到父greenlet那里去。在切换时,一个对象或异常被发送到目标greenlet。这可以作为两个greenlet之间传递信息的方式。例如:
def test1(x,y):
z=gr2.switch(x+y)
print z
def test2(u):
print u
gr1.switch(42)
gr1=greenlet(test1)
gr2=greenlet(test2)
gr1.switch("hello"," world")
这会打印出 “hello world” 和42,跟前面的例子的输出顺序相同。注意 test1() 和 test2() 的参数并不是在 greenlet 创建时指定的,而是在第一次切换到这里时传递的。
这里是精确的调用方式:
g.switch(obj=None or *args)
切换到执行点greenlet g,发送给定的对象obj。在特殊情况下,如果g还没有启动,就会让它启动;这种情况下,会传递参数过去,然后调用 g.run(*args)。
垂死的greenlet
如果一个greenlet的 run() 结束了,他会返回值到父greenlet。如果 run() 是异常终止的,异常会传播到父greenlet(除非是 greenlet.GreenletExit 异常,这种情况下异常会被捕捉并返回到父greenlet)。
除了上面的情况外,目标greenlet会接收到发送来的对象作为 switch() 的返回值。虽然 switch() 并不会立即返回,而是在未来某一时刻(即其他greenlet切换回来时)返回。这时执行点恢复到 switch() 之后,看上去就像switch() 返回了调用者发送来的对象。这意味着 x=g.switch(y) 会发送对象y到g,然后等着一个其他greenlet发来的对象,并在这里赋值给x。
注意,任何尝试切换到死掉的greenlet的行为都会切换到死掉greenlet的父greenlet,或者父的父,等等。最终的父就是 main greenlet,永远不会死掉。
Eventlet
Eventlet对greenlet进行了封装,主要是实现了一个greenlet的调度器,称为hub,每个Python thread只能有一个hub。Eventlet会根据当前运行的系统选择hub的最优实现(epoll,poll,kqueue等)。另外eventlet还实现了一种event机制,主要用于获取greenlet运行函数的返回值和异常信息。
主要api
eventlet.spawn(func, *args, **kw):创建一个greenthread运行func,返回值是greenthread对象,可用于获取返回值和异常信息;
eventlet.spawn_n(func, *args, **kw):直接创建一个greenlet,无法获取返回值和异常信息,但是执行速度更快;
eventlet.spawn_after(seconds,func, *args, **kw):延迟seconds启动func,其他和spawn一样;
greenthread.wait():spawn后用此函数获取返回值和异常信息;
nova中的eventlet
nova中大量使用了eventlet,主要包含以下几个大类:
1. service主线程(compute、network、scheduler、consoleauth、osapi、metadata): nova/service.py
2. api thread pool(wsgi server1000): nova/wsgi.py
3. pipe_watcher(osapi、metadata):nova/service.py
4. qpid consumer: nova/openstack/common/rpc/impl_qpid.py
5. manager worker(eventlet greenpool 64): nova/openstack/common/rpc/amqp.py
6. periodic tasks: nova/service.py
7. report state: nova/service.pyeventlet实现细节
Spawn流程
def spawn(func, *args, **kwargs):
hub = hubs.get_hub()
g = GreenThread(hub.greenlet)
hub.schedule_call_global(0, g.switch, func, args, kwargs)
return g
spawn做了三件事,一是获取hub(如果没有则创建hub)。二是创建一个greenthread对象,参数hub.greenlet是父greenlet。Greenthread是eventlet对greenlet的封装,主要用于想要获取greenlet中func的返回值或异常的情况(用event获取)。三是调用hub.schedule_call_global将新生成的greenthread注册到hub中去。
Greenthread初始化:
def __init__(self, parent):
greenlet.greenlet.__init__(self, self.main, parent)
self._exit_event = event.Event()
self._resolving_links = False
greenthread初始化,可以看到封装在里面的greenlet,其中传给greenlet的func不是spawn参数里的func,而是greenthread的main函数。这样实现的目的是为了在main函数中接收func的返回值和异常。Func是作为参数传递给main的,具体见schedule_call_global函数的调用。self._exit_event是一个event对象,用于传递func的返回值和异常信息。
schedule_call_global函数代码实现:
def schedule_call_global(self, seconds, cb, *args, **kw):
t = timer.Timer(seconds, cb, *args, **kw)
self.add_timer(t)
return t
schedule_call_global函数会将传入的callback函数和参数封装到timer对象中,交给hub去调度,hub调度的基本单位即一个个timer。Callback函数在被调度到时执行,spawn传给schedule_call_global的参数cb是g.switch,调用g.switch的效果就是切换到g这个greenlet中执行,传给g.switch的参数是(func,args, kwargs),这些参数最终传给了初始化时指定的执行函数self.main。
main函数代码实现:
def main(self, function, args, kwargs):
try:
result = function(*args, **kwargs)
except:
self._exit_event.send_exception(*sys.exc_info())
self._resolve_links()
raise
else:
self._exit_event.send(result)
self._resolve_links()
在main函数中,最终调用了func,如果出现异常,使用send_exception函数将异常信息发送出去;没有异常则将func的返回值发送出去。如果有任何地方调用了该greenthread的wait函数,将会接收到异常信息或者返回值。
Hub mainloop
BaseHub类init函数实现
def __init__(self, clock=time.time):
self.listeners = {READ: {}, WRITE: {}}
self.secondaries = {READ: {}, WRITE: {}}
self.closed = []
self.clock = clock
self.greenlet = greenlet.greenlet(self.run)
self.stopping = False
self.running = False
self.timers = []
self.next_timers = []
self.lclass = FdListener
self.timers_canceled = 0
self.debug_exceptions = True
self.debug_blocking = False
self.debug_blocking_resolution = 1
核心是第6行,可以看到hub里也有一个greenlet,这个greenlet充当的角色即时前面说的main greenlet,因此spawn的时候g = GreenThread(hub.greenlet)会把这个greenlet传入,作为所有其他greenlet的父greenlet。这个父greenlet执行的函数是self.run,即官网上说的mainloop。
run函数代码实现:def run(self, *a, **kw):
"""Run the runloop until abort is called.
"""
# accept and discard variable arguments because they will be
# supplied if other greenlets have run and exited before the
# hub's greenlet gets a chance to run
if self.running:
raise RuntimeError("Already running!")
try:
self.running = True
self.stopping = False
while not self.stopping:
while self.closed:
# We ditch all of these first.
self.close_one()
self.prepare_timers()
if self.debug_blocking:
self.block_detect_pre()
self.fire_timers(self.clock())
if self.debug_blocking:
self.block_detect_post()
self.prepare_timers()
wakeup_when = self.sleep_until()
if wakeup_when is None:
sleep_time = self.default_sleep()
else:
sleep_time = wakeup_when - self.clock()
if sleep_time > 0:
self.wait(sleep_time)
else:
self.wait(0)
else:
self.timers_canceled = 0
del self.timers[:]
del self.next_timers[:]
finally:
self.running = False
self.stopping = False
run函数中的while循环即是mainloop。在mainloop中,如果有新创建的timer,则会调用timer的callback函数使得新的greenthread开始执行。之后mainloop会进入self.wait函数,处理各个greenthread的io。在我们的使用场景下eventlet使用epoll处理io。Eventlet对所有Python标准库中的io操作进行了重写,将以前阻塞的io调用改为非阻塞,在epoll上注册并切换到hub上(trampoline)。当io ready时epoll会通知hub,hub再切换回greenthread继续执行。
eventlet实现的并发和我们理解的通常意义上类似线程/进程的并发是不同的, eventlet实现的"并发" 更准确的讲, 是 IO多路复用 . 只有在被eventlet.spawn()的函数中存在可以 支持异步IO 相关的操作, 比如说读写socket/named pipe等时, 才能不用对被调用的函数做任何修改而实现 所谓的"并发".
如果被eventlet.spawn()的函数中存在大量的CPU计算或者读写普通文件, eventlet是无法对其 实现并发操作的. 如果想要在这样的greenthread间实现类似"并发"运行的效果, 需要手动的在函数中插入greenthread.sleep().