Ryu eventlet学习总结

本文深入探讨了RYU控制器的运行原理,特别是其利用Eventlet库进行高性能并发处理的机制。通过分析RYU的启动流程,解释了Eventlet中的协程、GreenPool和GreenPile等关键概念,以及它们在RYU中的应用。同时,文章对比了RYU与ONOS的性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 前言

从OpenDaylight转到RYU以来一直都没有机会好好学习RYU的源码,只学会了编写简单的Application。但是如果要熟悉一个控制器,就要熟悉它的运行原理,熟悉它数据结构,熟悉它的设计模式等等。最近终于有时间好好看RYU的代码,但在看代码的过程中却发现RYU并不简单,其编码风格也非常优雅,非常值得学习。本篇博文主要讲述RYU中使用到的eventlet。

2 从RYU开始

运行ryu的时候,命令是:ryu-manager app.py。第一个要找到就是ryu-manager到底会触发什么程序。在/cmd中没有找到之后,在/bin中找到了两个可执行文件:ryu和ryu-manager。打开ryu-manager,显示如下:

Python

 

1

2

from ryu.cmd.manager import main

main()

找到/ryu/cmd/manager.py,发现这个文件中的main()函数是整个ryu的入口函数。这个main()函数的内容主要是完成了RYU的初始化配置和启动。Configure使用了oslo,这个在后续的博文中应该会提到。初始化的构成主要包括将app_list里面的内容加入App_Manager的列表中,然后开启协程去协调这些APP完成工作。hub是from ryu.lib import hub的。继续查看ryu/lib/hub.py。最终找到许多关于eventlet的内容。在hub.py中定义了Event,StreamServer和WSGIServer等类,还有一些重要的重要函数如spawn()等。为了更好地学习RYU,学习coroutine和eventlet就非常有必要了。

2.1 Coroutine

协程[coroutine]是一个程序组件。相比subroutine, coroutine更一般。coroutine相对与thread而言,又不一样。thread是资源抢占式的存在,而coroutine是通过yield来转移执行权,协程之间是平等的,没有等级关系。multi-thread一旦开始运行,就无法确定某一时刻到底是哪一个thread在占用cpu,临界资源也要加互斥锁。而coroutine则是需要程序员自己决定程序如何运行,同时也需要自己负责程序的风险。协程和线程一样,只共享堆,不共享栈。

2.2 Eventlet

eventlet是一个可以提供高性能并发处理能力的python库。我们可以在/usr/lib/python2.7/dist-packages/eventlet中找到对应的文件。为了更好的理解eventlet的内容,我花了半天认真地抄了一遍官网的Socket Connect例子。该实例源代码访问官网即可,本例中只是对将urls进行如下修改,urls = ['www.muzixing.com', 'www.baidu.com', 'www.python.org']。代码中对指定url发送了GET请求。重点在与eventlet.GreenPile()的使用。其中GreenPile类源码如下所示:

Python

 

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

class GreenPile(object):

    def __init__(self, size_or_pool=1000):

        if isinstance(size_or_pool, GreenPool):

            self.pool = size_or_pool

        else:

            self.pool = GreenPool(size_or_pool)

        self.waiters = queue.LightQueue()

        self.used = False

        self.counter = 0

 

    def spawn(self, func, *args, **kw):

        """Runs *func* in its own green thread, with the result available by

        iterating over the GreenPile object."""

        self.used =  True

        self.counter += 1

        try:

            gt = self.pool.spawn(func, *args, **kw)

            self.waiters.put(gt)

        except:

            self.counter -= 1

            raise

 

    def __iter__(self):

        return self

 

    def next(self):

        """Wait for the next result, suspending the current greenthread until it

        is available.  Raises StopIteration when there are no more results."""

        if self.counter == 0 and self.used:

            raise StopIteration()

        try:

            return self.waiters.get().wait()

        finally:

            self.counter -= 1

从__init__函数可以看出,GreenPile内部有一个GreenPool对象和一个Queue对象:waiters。GreenPool的作用相当与线程池的作用,这点后续会继续介绍。上述例子用到的spawn函数完成了协程(被称之为green thread)的启动。可以看出spawn函数的参数是(函数,参数),在上述例子中为: pile.spawn(geturl, x)。从spawn函数中,也可以看出spawn()方法的返回值被保存在waiters队列中。next()方法的实现使其具有迭代性质。

2.2.1 GreenPool

下面的例子使用到了GreenPool类,完成了一个非常暴力的迭代爬虫,理论上,如果你让他去爬取某一个网站,然后不去管它,它会从这个网站出发,找到所有的链接,然后跳到各自的链接,然后继续迭代,直到最后把整个互联网的网站都爬一遍。而且,它不尊重你网站的robot.txt,这意味这它什么都会爬取。

Python

 

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

from __future__ import with_statement

from eventlet.green import urllib2

import eventlet

import re

 

# http://daringfireball.net/2009/11/liberal_regex_for_matching_urls

url_regex = re.compile(r'\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))')

 

def fetch(url, seen, pool):

    '''Fetch A url, stick any found urls into the seen set,

    and dispatch any new  ones to te pool.'''

    print "fetching", url

    data = ''

    with eventlet.Timeout(5, False):

        data = urllib2.urlopen(url).read()

    for url_match in url_regex.finditer(data):

        new_url = url_match.group(0)

        # You can only send requests to muzixing.com so as not to destroy internet

        if new_url not in seen:  # and ’muzixing.com' in new_url:

            seen.add(new_url)

            # While this seems stack-recursive, it is actually not.

            # Spawned greenthreads start their own stacks

            pool.spawn_n(fetch, new_url, seen, pool)

 

def crawl(start_url):

    '''Recrusively crawl starting from *start_url*.Return a set of

    urls that were found.

    '''

    pool = eventlet.GreenPool()

    seen = set()

    fetch(start_url, seen, pool)

    pool.waitall()

    return seen

 

seen = crawl("http://www.muzixing.com")

print "I saw there urls:", seen

# print '\n'.join(seen)

&nbsp;

首先爬虫从http://www.muzixing.com网站开始搜索url。然后继续迭代寻找url,不断扩大查找范围。实验结果如下所示:

RYU-图1 迭代爬虫显示信息

图1:迭代爬虫显示信息

从上图可以看到爬虫抓取了www.muzixing.com的网页中存在的url如http://ikimi.net,然后我们可以看到爬虫又跳到了http://ikimi.net上爬取页面的其他url,如http://www.ikimi.net/wp-includes。如果将起始页面换成bbs.byr.cn会发现爬虫会以更快的速度在整个互联网蔓延开来!

上述例子中可以学习到GreenPool类的使用。GreenPool可以类比于线程池,这有利于理解。在GreenPool中的元素都是GreenThread。其中最重要的函数是spawn/spawn_n函数。

Python

 

1

2

3

4

5

6

def spawn(self, function, *args, **kwargs):

"""Run the *function* with its arguments in its own green thread.

        Returns the :class:`GreenThread <eventlet.greenthread.GreenThread>`

        object that is running the function, which can be used to retrieve the

        results.

"""

该函数启动了一个GreenThread,参数是需要执行的function和function对应的参数。返回值是执行该函数的GreenThread类。

Python

 

1

2

3

4

5

def spawn_n(self, function, *args, **kwargs):

        """Create a greenthread to run the *function*, the same as

        :meth:`spawn`.  The difference is that :meth:`spawn_n` returns

        None; the results of *function* are not retrievable.

        """

spawn_n函数功能上差不多,只是返回的是None。其他的函数举例简单说明如下:

  • waitall():等待所有greenthread执行完毕。
  • running(): 返回目前正在执行的greenthread。
  • imap():从迭代器中取出数据項作为func的参数去执行,并返回结果。
  • starmap(): 和imap类似,但是取参数的方式有所差异。从openstack nova 基础知识——eventlet中摘取举例如下:

 

 

1

2

imap(pow, (2,3,10), (5,2,3)) --> 32 9 1000

starmap(pow, [(2,5), (3,2), (10,3)]) --> 32 9 1000

 

  • free(): 返回当前可获取的greenthread的数目。

以上代码上的with语句是python中的一个非常方便的关键字。使用with关键字可以让代码更严谨且简洁。其封装了__enter__()函数和__exit__()函数,用于执行信息和退出处理。其等价于以下代码:

 

Python

 

1

2

3

4

try:

    __enter__()

finally:

    __exit__()

上述是关于GreenPool类的使用案例,使用该类可以高效完成并发操作。

2.2.2 Convenience

接下来再介绍一个更好玩的程序,多人群聊程序,可以让我们在学习eventlet的时候充满成就感。代码如下:

Python

 

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

import eventlet

from eventlet.green import socket

 

PORT = 3001

participants = set()

 

def read_chat_forever(writer, reader, address):

    line = reader.readline()

    while line:

        print('Chat:', line.strip())

        for p in participants:

            try:

                if p is not writer:  # Don't echo

                    msg = address[0] + ':'

                    msg += line

                    p.write(msg)

                    p.flush()

            except socket.error as e:

                # ignore broken pipes, they just mean the participant

                # closed its connection already

                if e[0] != 32:

                    raise

        line = reader.readline()

    participants.remove(writer)

    print("participant left chat")

 

try:

    print("ChatServer starting up on port %s" % PORT)

    server = eventlet.listen(('0.0.0.0', PORT))

    while True:

        new_connection, address = server.accept()

        print("Participant joined chat.")

        new_writer = new_connection.makefile('w')

        participants.add(new_writer)

        eventlet.spawn_n(

            read_chat_forever,

            new_writer,

            new_connection.makefile('r'),

            address)

except (KeyboardInterrupt, SystemExit):

    print("ChatServer exiting")

try语句块中完成了服务端socket的建立和监听。然后在while循环中完成了消息的处理。

首先关注第一个函数:eventlet.listen((addr,port))。在eventlet文件夹中,打开__init__文件可以查看到一些为了方便而初始化的定义,举例如下:

Python

 

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

version_info = (0, 9, 16)

__version__ = ".".join(map(str, version_info))

 

try:

    from eventlet import greenthread

    from eventlet import greenpool

    from eventlet import queue

    from eventlet import timeout

    from eventlet import patcher

    from eventlet import convenience

    import greenlet

 

    GreenPool = greenpool.GreenPool

    GreenPile = greenpool.GreenPile

 

    Queue = queue.Queue

 

    import_patched = patcher.import_patched

    monkey_patch = patcher.monkey_patch

 

    connect = convenience.connect

    listen = convenience.listen

    serve = convenience.serve

    StopServe = convenience.StopServe

    wrap_ssl = convenience.wrap_ssl

所以我们直接可以使用eventlet.listen调用convenience.listen函数。listen函数完成了一个server socket的绑定和监听。

Python

 

1

2

3

4

5

6

7

def listen(addr, family=socket.AF_INET, backlog=50):

    sock = socket.socket(family, socket.SOCK_STREAM)

    if sys.platform[:3] != "win":

        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    sock.bind(addr)

    sock.listen(backlog)

    return sock

socket.accept()函数将返回一个(connection,address)元组。socket.makefile([mode[, bufsize]])返回一个文件对象用于读写缓存。

eventlet.spawn_n函数将read_chat_forever函数及其三个参数作为参数,创建GreenThread去执行任务。eventlet主要完成的工作就是帮助你如何去协调你的任务,而不是去实现你的任务,这一点在这里得到体现。其实对比于线程池就容易理解读多了。

试验结果截图如下:

RYU-图2 多人群聊server运行界面

图2:多人群聊server运行界面

RYU-图3 多人群聊client运行界面

图3:多人群聊client运行界面

从client运行界面可以看出不同的用户发送的信息会以IP:message的形式展示出来,代码很简单,但是非常有趣。

以上例子均可以在官网找到,读者可以到官网去查看更多案例。

2.2.3 Patcher

Patch是eventlet中的一个重要模块。用于替换系统自带的模块。其中有import_patched和monkey_patch两个函数,后者可以提供运行时替换。具体例子可以查看openstack nova 基础知识——eventlet

3 回到RYU

前两行代码调用了hub.patch()函数,查看hub.py中发现patch = eventlet.monkey_patch,实现了运行时替换模块。

Python

 

1

2

from ryu.lib import hub

hub.patch()

接下来的CONF文件由于oslo的内容比较多,所以会在后续博文中详细介绍。首先关注main()函数的主要内容。

Python

 

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

def main(args=None, prog=None):

    try:

        CONF(args=args, prog=prog,

             project='ryu', version='ryu-manager %s' % version,

             default_config_files=['/usr/local/etc/ryu/ryu.conf'])

    except cfg.ConfigFilesNotFoundError:

        CONF(args=args, prog=prog,

             project='ryu', version='ryu-manager %s' % version)

 

    log.init_log()

 

    if CONF.pid_file:

        import os

        with open(CONF.pid_file, 'w') as pid_file:

            pid_file.write(str(os.getpid()))

 

    app_lists = CONF.app_lists + CONF.app

    # keep old behaivor, run ofp if no application is specified.

    if not app_lists:

        app_lists = ['ryu.controller.ofp_handler']

 

    app_mgr = AppManager.get_instance()

    app_mgr.load_apps(app_lists)

    contexts = app_mgr.create_contexts()

    services = []

    services.extend(app_mgr.instantiate_apps(**contexts))

 

    webapp = wsgi.start_service(app_mgr)

    if webapp:

        thr = hub.spawn(webapp)

        services.append(thr)

 

    try:

        hub.joinall(services)

    finally:

        app_mgr.close()

从CONF文件中取出app信息,存在app_lists内,若没有启动其他app,则默认启动ofp_handler应用,用于处理基础的事件,如协议协商等。然后声明一个AppManager的类,用于初始化和管理APP。load_apps函数完成了APP的加载。最后try语句块中的joinall()使得进程需要等待所有的services完成之后才能退出。至此RYU初始运行学习完成,后续的博文将分别介绍:oslo, 事件处理机制,RYUAPP类以及RYU数据结构和API使用等内容。

4 后语

Evenlet是个不错的python库,简单却很高效。相比于thread,coroutine的行为是可控的,切换成本也要更小。在单核情况下,coroutine要比thread开销小,但是multithread可以在多CPU的情况下发挥更大的能力。RYU是使用Python编写的控制器,比同样使用Python编写的POX,无论从代码的规范,优雅度,还是从性能上,都有很大的优势,此外,这个纯SDN控制器对OpenFlow协议的支持可以说是最稳定,最全面的。虽然我还会继续研究ONOS,学习大型分布式框架。但是RYU会成为我开发Application的利器。相比之下,Java编写的ODL,过于复杂和不稳定。新生儿ONOS相比之下用户体验更好,且没有使用YANG,大大降低了学习难度。周一的时候,还在Docker中安装了ONOS,并使用Cbench测试对比了ONOS和RYU的吞吐量。同样环境下,单节点的ONOS性能几乎是RYU的两倍,这让我有些忧伤。也许匕首只适合敏捷作战,而大刀才是开疆扩土的利器吧。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值