openstack分析-eventlet

最近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.py

eventlet实现细节

Spawn流程

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().

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值