Python专题之扩展与架构
Python专题之扩展与架构
前言
一个应用程序的可扩展性、并发性和并行性在很大程度上取决于它的初始架构和设计的选择。如你所见,有一些范例(如多线程)在Python中被误用,而其他一些技术(如面向服务架构)可以产生更好的效果。
多线程
这个可以参考python多线程相关概念及解释
由于Python中GIL存在,多线程并不是一个好的选择。你可以考虑其他选择。
- 如果需要运行后台任务,最容易的方式是基于事件循环构建应用程序。许多不同的Python模块都提供这一机制,甚至有一个标准库的模块–asyncore, 它是PEP 3156中标准化这一功能的成果。 有些框架就是基于这一概念构建的,如Twisted。最高级的框架应该提供基于信号量、计时器和文件描述符活动来访问事件。
- 如果需要分散工作负载,使用多进程会更简单有效。
多进程
这个可以参考Python多进程相关概念及解释
异步和事件驱动架构
事件驱动编程会一次监听不同的事件,对于组织程序流程是很好的解决方案,并不需要使用多线程的方法。
考虑这样一个程序,它想要监听一个套接字的连接,并处理收到的连接。有以下三种方式可以解决这个问题。
- 每次有新连接立时创建(fork)一个新进程,需要用到multiprocessing这样的模块。
- 每次有新连接建立时创建一个新线程,需要用到threading这样的模块。
- 将这个新连接加入事件循环(event loop)中,并在事件发生时对其作出响应。
众所周知的是,使用事件驱动方法对于监听数百个事件源的场景的效果要好于为每个事件创建一个线程的方式。
事件驱动架构背后的技术是事件循环的建立。程序调用一个函数,它会一直阻塞直到收到事件。其核心思想是令程序在等待输入输出完成前保持忙碌状态,最基本的事件通常类似于”我有数据就绪可被读取”或者”我可以无阻塞地写入数据”。
在Unix中,用于构建这种事件循环的标准函数是系统调用select(2)或者poll(2)。
它们会对几个文件描述符进行监听,并在其中之一准备好读或写时做出响应。
在Python中,这些系统调用通过select模块开放了出来。很容易用它们构造一个事件驱动系统,尽管这显得有些乏味。使用select的基本示例如下所示:
import select
import sockek
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(0)
server.bind(('localhost',10000))
server.listen(8)
while True:
inputs, outputs, excepts = select.select([server],[],[server])
if server in inputs:
connection, client_address = server.accept()
connection.send("hello!\n")
不久前一个针对这些底层的包装器被加入到了Python中,名为asyncore。
还有很多其他框架通过更为集成化的方式提供了这类功能,如Twisted或者Tornado。
Twisted多年来在这方面已经成为了事实上的标准。也有一些提供了Python接口的C语言库(如libevent、libev或者libuv)也提供了高效的事件循环。
最近,Guido Van Rossum开始致力于一个代号为tulip的解决方案,其记录在PEP3156中。这个包的目标就是提供一个标准的事件循环接口。将来,所有的框架和库都将与这个接口兼容,而且将实现互操作。
tulip已经被重命名并被并入了Python3.4的asyncio包中。如果不打算依赖Python3.4的话,也可以通过PyPI上提供的版本装在Python3.3上,只需通过pip install asyncio
即可安装。
建议:
- 只针对Python2,可以考虑基于libev的库,如pyev。
- 如果目标是同时支持Python2和Python3,最好使用能同时支持两个版本的库,如pyev。
- 如果只针对Python3, 那就用asyncio。
面向服务架构
Python在解决大型复杂应用的可扩展性方面的问题似乎难以规避。然而,Python在实现面向服务架构(Service-Oriented Architecture,SOA)方面的表现是非常优秀的。如果不熟悉这方面的话,线上有大量相关的文档和评论。
SOA是OpenStack所有组件都在使用的架构。组件通过HTTP REST和外部客户端(终端用户)进行通信,并提供一个可支持多个连接协议的抽象RPC机制,最常用的就是AMQP。
在你自己的场景中,模块之间沟通渠道的选择关键是要明确将要和谁通信。
当需要暴露API给外界时,目前最好的选择是HTTP,并且最好是无状态设计,例如REST风格的架构。这类架构非常容易实现、扩展、部署和理解。
然而,当在内部暴露和使用API时,使用HTTP可能并非最好的协议。有大量针对应用程序的通信协议存在,对任何一个协议的详尽描述都需要一整本书的篇幅。
在Python中,有许多库可以用来构建RPC(Remote Procedure Call)系统。Kombu与其他相比是最有意思的一个,因为它提供了一种基于很多后端的RPC机制。AMQ协议是主要的一个。但同样支持Redis、MongoDB、BeanStalk、Amazon SQS、CouchDB或者Zookeeper。
最后,使用这样松耦合架构的间接利益是巨大的。如果考虑让每个模块都提供并暴露API,那么可以运行多个守护进程暴露这些API。例如,Apache httpd将使用一个新的系统进程为每一个连接创建一个新的worker,因而可以将连接分发到同一个计算节点的不同worker上。要做的只是需要有一个系统在worker之间负责分发工作,这个系统提供了相应的API。每一块都将是一个不同的Python进程,正如我们在上面看到的,在分发工作负载时这样做要比用多线程好。可以在每个计算节点上启动多个worker。尽管不必如此,但是在任何时候,能选择的话还是最好使用无状态的组件。
ZeroMQ是个套接字库,可以作为并发框架使用。
示例如下:
import multiprocessing
import random
import zmq
def compute():
return sum(
[random.randint(1, 100) for i in range(1000000)]
)
def worker():
context = zmq.Context()
work_receiver = context.socket(zmq.PULL)
work_receiver.connect("tcp://0.0.0.0:5555")
result_sender = context.socket(zmq.PUSH)
result_sender.connect("tcp://0.0.0.0:5556")
poller = zmq.Poller()
poller.register(work_receiver, zmq.POLLIN)
while True:
socks = dict(poller.poll())
if socks.get(work_receiver) == zmq.POLLIN:
obj = work_receiver.recv_pyobj()
result_sender.send_pyobj(obj())
context = zmq.Context()
work_sender = context.socket(zmq.PUSH)
work_sender.bind("tcp://0.0.0.0:5555")
result_receiver = context.socket(zmq.PULL)
result_receiver.bind("tcp://0.0.0.0:5556")
processes = []
for x in range(8):
p = multiprocessing.Process(target = worker)
p.start()
processes.append(p)
for x in range(8):
work_sender.send_pyobj(compute)
results = []
for x in range(8):
results.append(result_receiver.recv_pyobj())
for p in processes:
p.terminate()
print("Results: %s" % results)
如你所见,ZeroMQ提供了非常简单的方式来建立通信信道。我这里选用了TCP传输层,表明我们可以在网络中运行这个程序。应该注意的是,ZeroMQ也提供了利用Unix套接字的inproc信道。
通过这种协议,不难想像通过网络消息总线(如ZeroMQ、AMQP等)构建一个完全分布式的应用程序通信。
最后,使用传输总线(transport bus)解耦应用是一个好的选择。它允许你建立同步和异步API,从而轻松地从一台计算机扩展到几千台。它不会将你限制在一种特定技术或语言上,现如今,没理由不将软件设计为分布式的,或者受任何一种语言的限制。