11.4 面向服务架构

考虑到前面阐述的问题和解决方案,Python在解决大型复杂应用的可扩展性方面的问题似乎难以规避。然而,Python在实现面向服务架构(Service-Oriented Architecture,SOA)方面的表现是非常优秀的。如果不熟悉这方面的话,线上有大量相关的文档和评论。

SOA是OpenStack所有组件都在使用的架构。组件通过HTTP REST和外部客户端(终端用户)进行通信,并提供一个可支持多个连接协议的抽象RPC机制,最常用的就是AMQP。

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

在你自己的场景中,模块之间沟通渠道的选择关键是要明确将要和谁进行通信。

当需要暴露API给外界时,目前最好的选择是HTTP,并且最好是无状态设计,例如REST(Representational state transfer)风格的架构。这类架构非常容易实现、扩展、部署和理解。

然而,当在内部暴露和使用API时,使用HTTP可能并非最好的协议。有大量针对应用程序的通信协议存在,对任何一个协议的详尽描述都需要一整本书的篇幅。

在Python中,有许多库可以用来构建RPC(Remote Procedure Call)系统。Kombu(http://kombu.readthedocs.org/en/latest/)与其他相比是最有意思的一个,因为它提供了一种基于很多后端的RPC机制。AMQ协议(http://www.amqp.org/)是主要的一个。但同样支持Redis(http://redis.io/)、MongoDB(https://www.mongodb.org/)、BeanStalk(http://kr.github.io/beanstalkd/)、Amazon SQS(http://aws.amazon.com/cn/sqs/)、CouchDB(http://couchdb.apache.org/)或者ZooKeeper(http://zookeeper.apache.org/)。

最后,使用这样松耦合架构的间接收益是巨大的。如果考虑让每个模块都提供并暴露API,那么可以运行多个守护进程暴露这些API。例如,Apache httpd将使用一个新的系统进程为每一个连接创建一个新worker,因而可以将连接分发到同一个计算节点的不同worker上。要做的只是需要有一个系统在worker之间负责分发工作,这个系统提供了相应的API。每一块都将是一个不同的Python进程,正如我们在上面看到的,在分发工作负载时这样做要比用多线程好。可以在每个计算节点上启动多个worker。尽管不必如此,但是在任何时候,能选择的话还是最好使用无状态的组件。

ZeroMQ(http://zeromq.org/)是个套接字库,可以作为并发框架使用。下面的例子实现了和前面例子中同样的worker,但是利用了ZeroMQ作为分发和通信的手段。

使用ZeroMQ的Worker

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:5555")
  result_sender = context.socket(zmq.PUSH)
  result_sender.connect("tcp://.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()
# Build a channel to send work to be done
work_sender = context.socket(zmq.PUSH)
work_sender.bind("tcp://.0:5555")
# Build a channel to receive computed results
result_receiver = context.socket(zmq.PULL)
result_receiver.bind("tcp://.0:5556")
# Start 8 workers
processes = []
for x in range(8):
  p = multiprocessing.Process(target=worker)
  p.start()
  processes.append(p)
# Start 8 jobs
for x in range(8):
  work_sender.send_pyobj(compute)
# Read 8 results
results = []
for x in range(8):
  results.append(result_receiver.recv_pyobj())
# Terminate all processes
for p in processes:
  p.terminate()
print("Results: %s" % results)

如你所见,ZeroMQ提供了非常简单的方式来建立通信信道。我这里选用了TCP传输层,表明我们可以在网络中运行这个程序。应该注意的是,ZeroMQ也提供了利用Unix套接字的inproc信道。显然在这个例子中,基于ZeroMQ构造的通信协议是非常简单的,这是为了保持本书中的例子尽量清晰和简洁,不难想象基于其上建立一个更为复杂的通信层。

通过这种协议,不难想象通过网络消息总线(如ZeroMQ、AMQP等)构建一个完全分布式的应用程序间通信。

注意,类似HTTP、ZeroMQ或者AMQP这样的协议是同语言无关的。可以使用不同的语言和平台构建系统的各个部分。尽管我们都认同Python是一门优秀的语言,但其他团队也许有他们的偏好,或者对于问题的某个部分,其他语言可能是更好的选择。

最后,使用传输总线(transport bus)解耦应用是一个好的选择。它允许你建立同步和异步API,从而轻松地从一台计算机扩展到几千台。它不会将你限制在一种特定技术或语言上,现如今,没理由不将软件设计为分布式的,或者受任何一种语言的限制。


1或者如果多个CPU不存在的话,依次在某一个处理器上。
2用C语言开发的Python参考实现,也就是通常在shell中输入python之后运行的那个。
3尽管是使用最普遍的。
4关于这个的进一步阅读,可以看看C10K问题(http://www.kegel.com/c10k.html#nb.kqueue)。
5 Asynchronous IO Support Rebooted: “asyncio” Module, Guido van Rossum, 2012