设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

Python异步IO的未来(从Web后端开发的角度)

2014-12-9 14:21| 发布者: joejoe0332| 查看: 11065| 评论: 0|原作者: douxingxiang, Ev4n, jk409, Ley, 开源中国七里香, magicoding|来自: oschina

摘要: 我是一个工程师,拥有10年以上的 WEB 后端开发经验,大部分职业生涯都在编写 Python代码。所以本文大部分文字描述可能跟软件开发的其他领域无关,同样的,也跟使用 JVM 或 CLR 的开发者无关,他们只是用不同的方式解 ...

  免责声明:我是一个工程师,拥有10年以上的 WEB 后端开发经验,大部分职业生涯都在编写 Python代码。所以本文大部分文字描述可能跟软件开发的其他领域无关,同样的,也跟使用 JVM 或 CLR 的开发者无关,他们只是用不同的方式解决问题。


  开发Web应用程序看起来与我们10年前做的有很大的不同。现在,我们用微服务建立的一切。它彻底改变了我们的应用程序的架构。

2014年,如果你还在构建完整巨大的 web 应用程序,你需要改变这一行为,否则很快会被解雇


  虽然我们的应用程序的设计发生了很大变化,但是我们的工具没有。这里我要介绍未来我会如何编写微服务。但是,首先,让我们来看看我们有什么。


全局解释锁

  对于声名狼籍的Python GIL,Python的支持者说的比较多的是其他的脚本语言也有(Ruby,Perl,Node.js,还有一些)。对于不好的语言解释器的设计,这是圣战的源头,但这对于web应用从来不是问题。我们总是以来许多进程共享一个数据库。


  在微服务面前,全局解释锁更加不是问题了。在大多数情况下,单个微服务甚至比十年前的典型web应用还小。尽管很小,它每秒也可以处理大量的请求,大多数是因为它针对查询的类型,进行了高度的定制化和很好的调整。


  而当构建的微服务需要等待其他服务回复的时间时,这开始成为一个问题。好戏由此开场。。。


“异步I / O,或非阻塞I/ O是输入/输出处理的一种形式,它允许其他进程继续在传输完成之前。”- 维基百科
 

  任何异步库基本是从左侧到右侧的流程来调整代码的:

  同步(左)与异步(右)请求进程对比图


  Python的异步I/O支持是相当的好。有一堆库可以做这个工作(Twisted, Tornado, Gevent, Eventlet,这里仅列举几个)。每个库都支持很多协议。你可以使用MySQL, Mongo, PostgreSQL, Redis, Memcache, ElasticSearch...,几乎每个DB,和许多其他得服务。一些奇异的协议,像SSH或者Beanstalk只在几个库中支持。不过这些都不是问题,写另一个协议或从一个I/O框架移植到另一个也不是很难。


  当然,每一个I/O库都支持客户端和服务端HTTP。想必,这就是为什么HTTP是最常见的用于微服务之间通信的协议。不过大多数框架也支持各种其他协议(msgpack-rpc, thrift, zeromq, ice,这里仅列举几个)。


  有很多框架存在,彼此在不同协议的使用便捷性和其他种类的并发抽象上的有所不同。当然对不同协议的支持已经变得越来越流行。直到Twisted在2002年发布,这种状况才有所改变。是的,即使当python支持了yield和stacklessgreenlets出现,便捷性确实大大增加了,但是这也仅仅是增加一点点的便捷性。真正的改变是在2002年。


  但是有些事情是大多数Python框架的弱项。当你在一个线程中操控很多个客户端的请求时,你有可能把它们管道进单个连接中。也就是说,如果你在前端有三个GET请求,你可能向MySQL数据库发送三次请求,但不等待回复。一旦收到(数据库)的数据就尽快回复客户端。就像上面图中表示的,但是使用了单个DB连接。大多数python框架现在,在请求开始时从连接池中拉取一个连接,在请求结束时释放连接,这样高效的保证连接数目与同时请求数目相等。


  对于多数数据库来说,成千上万个连接仍然是个问题。即使的典型的Sharding也无济于事,因为对每个shard来说也是相同数目的连接。很多用户采用了特殊的微服务(microservice)。这不仅仅是数据库的问题,许多微服务(microservice)也同样遇到这样问题。


  幸好asyncio架构允许更容易的构建流水线(pipelining),所以越来越多的asyncio协议采用这种技术。不幸的是连接代价巨大的数据库(比如MySQL和PostgreSQL)使用了不支持(该技术)的C库,而且没有人有足够的重视来写一个更好的。


使用像Resque一样的发布-订阅

  当前,许多工程团队围绕着发布-订阅来构建微服务(microservices)架构。比如,他们运用RabbitMQ或者其竞争者之一的产品将所有内容发布成消息(message)。他们相信这是简化他们的建构:

  1. 单总线(Single Bus),无须再考虑这个

  2. 能够无需等待回复即可发布消息。不需要任何异步库即可高效的获取异步I/O

当部分工程师们以为这就是答案的时候,我认为这不适合普遍的情况。

我认为将我的设计决策限制在使用特定插件及特定的消息调度算法不会解决我的所有网络问题。

ZeromqNanomsg 方式

  另一个颇具魅力的微服务架构是Zeromq。如果你对它不熟悉,你应该尽快了解它。Zeromq只不过是巧合的使用MQ(消息队列)作为后缀,毕竟它不像 RabbitMQKafka以及其他的那样拥有中心消息队列。它是以socket形式工作在steroids。也就是说,它看起来就像常规的socket,确实会自动发送消息(分割TCP数据流为帧),重新连接,点对点平衡加载等等。

  在 Zeromq 世界里有三种方式供你的服务于其他连接:

  1. 发布-订阅,工作方式基本上和其他发布-订阅应用程序相同

  2. 请求-回复,基本与RPC工作方式相同

  3. Push-Pull,请求却不需要回复,或者发布-订阅发送消息到单一接收端(基于轮叫)


Nanomsg 做到了更多事情,它不仅支持上面提到的所有方式,而且增加了更多的通信模式(单就nanomsg而言):

  1. 监督者-应答者(Surveyor-respondent),允许向多点发送请求而且接收来至所有的请求

  2. 总线(Bus),允许向任何点发送消息

更多的是:nanomsg的前景在于通信模式是插件式的,也就是说,在未来更多的通信模式会被增加到库中。

我相信更多的通信模式会出现,而使用发布-订阅或者HTTP难逃厄运

像nanomsg和zeromq作为脚本语言的优势是:它们在一个单独的线程中操控I/O。所以当你使用python做一些事情操控全局解释器锁(GIL)时,你的zeromq线程保持你的连接,清空消息缓冲器,接收和建立连接等等。


真实世界中的微服务

  当聪明的黑客们创建了像 zeromq 、 nanomsg 和发布-订阅总线( publish-subscribe buses )这样优秀的项目的时候,实干的工程师们却仍然使用旧的技术干活。

到目前为止我还没有看到使用 zeromq 作为数据库存取通讯方式的数据库。嗯,现在是有很少一些使用了 zeromq 的开源服务。基本上所有现代的数据库在通讯方式的实现上目前分成了以下两个阵营:

  1. 创建并使用自身协议

  2. 使用 HTTP 协议

  在这个方面数据库算是个比较突出的例子。另外的例子比如 Docker , Docker 使用了基于 unix sockets 的 HTTP 协议作为其通讯协议,然而,当她需要使用全双工流( full-duplex streams )来替代请求-回应( request-reply )模式的时候,就只能很不优雅地打破了其使用的协议的语义( protocol semantics )。

HTTP协议

我不得不说一些关于HTTP的事情。

对HTTP的支持普遍存在,但是使用python会很低效。不仅是因为不能像zeromq那样在其他线程处理事务,对HTTP解析也很缓慢。常常持久连接(keep-alive connection)也不受支持。

同时HTTP是复杂的,如果你认为不是这样,你就错了。来看个简单的例子吧。你可能写出下面的代码:

1
2
3
4
5
def simple_app(environ, start_response):
    status = '304 Not Modified'
    headers = [('Content-Length''5')]
    start_response(status, headers)
    return [b'hello']

理论上服务器会返回一个写有“hello”的页面(在wsgiref下测试),但是实际上:

The 304 response MUST NOT contain a message-body, and thus is always terminated by the first empty line after the header fields 
(304响应必须不包含消息体,因此通常会被头域后的第一个空行终止)

意思是说“hello”行会被客户端在下次请求时识别成响应的第一行。这在一些设置中会导致缓存污染和安全漏洞。有多少使用HTTP的程序员意识到了这个现象呢?还有更多莫名其妙的细节。

HTTP不要在内部消息中使用,因为它容易使用但不简单,甚至复杂到使用了5个RFC也只描述了基本内容。即使误解最简单的内容可能会导致安全漏洞。

说实话,大多数微服务(microservice)使用了HTTP的子集,比如只认可200的响应码(其他的都作为失败),不使用特别的数据头或类似的,这可能不会出问题。当然这不是真正意义上的HTTP(但是经常用来负载均衡的代理则需要真正的HTTP,比如HAProxy),而且需要对HTTP特性非常熟悉的人才能构建安全的HTTP子集。


那么Zeromq怎么样

首先,它不是那种多功能的软件:

  1. 它很难拓展(hack on)(使用了复杂的C++ Actor模型)

  2. 嵌入进一些程序的效果欠佳(也就是说它没有使用好fork)

  3. 对故障切换(failover)和服务发现(service discovery)整合欠佳

  4. 对非幂等(non-idempotent)请求和有状态路由(stateful routing)操控欠佳

Nanomsg在(1)表现相对要好但远未达到完美。而在当前的设计中(2)还不能解决。(3)nanoconfig库为nanomsg解决,但却比nanomsg本身受到了更少的关注。(4)在nanomsg中可能最终解决但现在还没有。


  第二个大问题是工程师们还不太适应它的思考方式,例如对同样的连接,redis协议使用发布-订阅(pub-sub)和请求-响应(req-rep),mongo使用push-pull和请求-响应(req-rep),而zeromq不允许。nonomsq特别想修正工程师们头脑中的这种想法,但这条路还很长。


  别误解我,zeromq很好。nanomsq从这个失误中学到了很多,当它可用于生产环境时,它将是我用于服务间消息传递的第一选择。


但是微服务怎么了?

  好吧,最简单的原因是,仅仅使用zeromq你甚至不能构建一个很小的服务。但是如果你的DB支持HTTP,你就可以使用HTTP在客户端和服务端两端构建服务。听起来很简单(但是记住HTTP很复杂)。


  另外一个问题是I/O模型。当你的代码是单线程的,你就不能在不使用连接的情况下,依然保持连接心跳。即使你使用异步循环,它也可能因做其他运算而停顿很久。


  有时你想给连接发请求,但是连接实际上已经关闭了。有种广泛使用的方法,读取连接数据,检出是否可用,因为通常发送请求后很难恢复:

1
2
3
4
if s.read() == b'':
  self.reconnect()
s.write(request)
response = s.read()


  这意味着连接仅当你发送请求时才开始建立,而不是当zeromq或nanomsg中那样只要准备好了。


  而且这样也不好做服务发现,现在你有三种简单的选择:


  1. 每次请求前检查服务名字(如解析DNS)

  2. 下次连接请求时解析服务名字

  3. 永不更新服务(即,直到进程重启)


  大部分用户选择(3)。有时(2)可以直接用(work out of the box, 开箱即可用),但是它只有机器不可达后故障切换时才会发生。(1)相当低效,几乎不可用。



酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部