分布式系统

Dustin是一个开源的软件开发者,同时也是Mozilla的一名发布工程师。他参与的项目包括在Puppet中配置主机系统,一个基于Flask的Web框架,为防火墙配置做单元测试,还有一个在Twisted Python下开发的持续集成系统框架。你可以通过GitHub或者dustin@mozillar.com联系他。

介绍

在这一章,我们将会一起探索如何实现一个网络协议用于可靠的分布式计算。正确实现一个网络协议并不简单,因此我们会采用一些技巧来尽可能的减少、查找和修复漏洞。要建立一个可靠地软件,同样需要一些特别的开发和调试技巧。

情景思考

这一章的重点在于网络协议的实现,但是首先让我们以简单的银行账户管理服务为例做一个思考。在这个服务中,每一个账户都有一个当前余额,同时每个账户都有自己的账号。用户可以通过“存款”、“转账”、“查询当前余额”等操作来连接账户。“转账”操作同时涉及了两个账户——转出账户和转入账户——并且如果账户余额不足,转账操作必须被驳回。

如果这个服务仅仅在一个服务器上部署,很容易就能够实现:使用一个操作锁来确保“转账”操作不会同时进行,同时对转出账户的进行校验。然而,银行不可能仅仅依赖于一个服务器来储存账户余额这样的关键信息,通常,这些服务都是被分布在多个服务器上的,每一个服务器各自运行着相同代码的实例。用户可以通过任何一个服务器来操作账户。

在一个简单的分布式处理系统的实现中,每个服务器都会保存一份账户余额的副本。它会处理任何收到的操作,并且将账户余额的更新发送给其他的服务器。但是这种方法有一个严重的问题:如果两个服务器同时对一个账户进行操作,哪一个新的账户余额是正确的?即使服务器不共享余额而是共享操作,对一个账户同时进行转账操作也可能造成透支。

从根本上来说,这些错误的发生都是由于服务器使用它们本地状态来响应操作,而不是首先确保本地状态与其他服务器相匹配。比如,想象服务器A接到了从账号101向账号202转账的操作指令,而此时服务器B已经处理了另一个把账号101的钱都转到账号202的请求,却没有通知服务器A。这样,服务器A的本地状态与服务器B不一样,即使会造成账户101透支,服务器A依然允许从账号101进行转账操作。

分布式状态机

为了防止上述情况发生我们采用了一种叫做“分布式状态机”的工具。它的思路是对每个同样的输入,每个服务器都运行同样的对应的状态机。由于状态机的特性,对于同样的输入每个服务器的输出都是一样的。对于像“转账”、“查询当前余额”等操作,账号和余额也都是状态机的输入。

这个应用的状态机比较简单:

  1. def execute_operation(state, operation):
  2. if operation.name == 'deposit':
  3. if not verify_signature(operation.deposit_signature):
  4. return state, False
  5. state.accounts[operation.destination_account] += operation.amount
  6. return state, True
  7. elif operation.name == 'transfer':
  8. if state.accounts[operation.source_account] < operation.amount:
  9. return state, False
  10. state.accounts[operation.source_account] -= operation.amount
  11. state.accounts[operation.destination_account] += operation.amount
  12. return state, True
  13. elif operation.name == 'get-balance':
  14. return state, state.accounts[operation.account]

值得注意的是,运行“查询当前余额”操作时虽然并不会改变当前状态,但是我们依然把它当做一个状态变化操作来实现。这确保了返回的余额是分布式系统中的最新信息,并且不是基于一个服务器上的本地状态来进行返回的。

这可能跟你在计算机课程中学习到的典型的状态机不太一样。传统的状态机是一系列有限个状态的集合,每个状态都与一个标记的转移行为相对应,而在本文中,状态机的状态是账户余额的集合,因此存在无穷多个可能的状态。但是,状态机的基本规则同样适用于本文的状态机:对于同样的初始状态,同样的输入总是有同样的输出。

因此,分布式状态机确保了对于同样的操作,每个主机都会有同样的相应。但是,为了确保每个服务器都允许状态机的输入,前文中提到的问题依然存在。这是一个一致性问题,为了解决它我们采用了一种派生的Paxos算法。