对于银行后台系统来说,稳定性是非常重要的;而对于移动银行来说,还需要面对数以亿计的客户。Monzo 遇到的正是这种挑战,客户一方面需要 7*24 小时能够管理他们的钱,因此数据延时、单点故障等是不可接受的;另一方面又希望他们的需求能够很快实现。
正因为如此,Monzo 的后台系统从一开始就设计成分布式微服务架构。一些大型互联网公司(例如亚马逊、Netflix、Twitter 等)的实践已经证明,独立大型应用在用户数量快速增长或者开发人员数量增长之后,会导致扩容、需求响应等一系列问题。同时,Monzo 的业务发展方向,需要针对不同地区的客户进行定制化,为了提高开发效率,必须减少开发团队之间的耦合。
Monzo 后台系统开发人员最初只有 3 个人,必须从务实开始。最初的团队选择了他们相对熟悉的技术,确定了方向就开始构建产品,但是技术时刻在改变。
Monzo 开发工作室
当 Monzo beta 版本发布的时候,他们的后台服务已经有将近 100 个,同时开发团队也在快速增长。此时,他们开始考虑现有系统的架构问题,梳理出以下几个着重关心的领域:
- 集群管理:随着服务数量的增加,需要有个自动化的方式管理大量服务器、分布式任务和服务器故障;
- 多语种服务:在 Monzo 主要使用 Go 语言进行开发,但是没有一种语言的生态环境可以满足整个银行后台系统的需要,因此整个后台需要能够承载多种语言环境的服务;
- 远程调用框架:由于有大量离散的服务,分布在不同的主机、数据中心、甚至不同大陆,后台系统必须有一个强大的远程服务调用层,以处理模块故障,降低延迟,理清调用链路。
- 异步消息:为了提高后台系统的性能和可伸缩性,Monzo 通过异步消息队列将业务逻辑放入“后台”运行,消息队列必须保证极强的可靠性,以避免消息丢失的发生。
下面我们就来逐一了解这些系统的详细设计和选型。
集群管理
Monzo 系统包含大量微服务,如果一台主机上只部署一个服务,会造成对服务器的大量浪费。按照传统方式将服务器归类,会增加服务扩容的难度。因此,一个支持快速伸缩的集群调度系统,应该抽象出应用程序的运行环境,将运行环境和底层硬件进行隔离。调度算法根据应用程序负载和可用资源,对其进行扩容和缩容。另外,Monzo 在设计集群管理系统时,希望能够让所有的应用程序共享一个调度器,因此它不仅需要能够调度无状态服务,还需要能够调度有状态服务。
容器化的提出,特别是 Docker 的兴起,将前面提到的抽象层进行了标准化。首先,通过将应用程序及其运行时依赖打包成一个镜像,使得应用程序运行环境和主机解耦;然后,通过整合 Linux 内核的隔离特性( cgroup 、 namespace ),使得主机上运行的多个服务之间能够相互隔离,减少干扰。这样,集群管理服务对应用程序可以做到黑盒,将重心放到服务编排上去。
Monzo 最初使用 Mesos + Marathon 的架构,随后切换到了运行在 CoreOS 上的 Kubernetes 。下图展示了在切换到 Kubernetes 之后,整体开销的变化:
从上图可以看见,使用 Kubernetes 之后,整体开销开始下降。这是因为所有系统都共用资源池。例如,之前 Monzo 的构建服务搭建在独立的 Jenkins 服务器上,使用了 Kubernetes 之后,构建服务由 Kubernetes 根据机器可用资源分配到现有机器上。Kubernetes 的资源分配和限制模块可以尽可能的利用机器空闲资源,并且保证低优先级的任务不会影响高优先级任务。
另外,通过在AWS 上将Kubernetes 以 HA 模式部署之后,系统稳定性有了明显的增强,Monzo 开始在 AWS 上部署类似 Netflix 的 Simian Army ,对生产环境服务进行破坏性操作,以验证生产环境对服务失败的容错能力。
多语种服务
Monzo 后台系统主要使用 Go 语言编写,因为 Go 语言在构建低延时高并发应用程序上有先天的优势。但是一种语言很难完成整个银行后台系统的搭建,因为系统中可能会用到大量不同语言编写的开源软件,同时技术人员也会有不同的语言偏重。
Docker 的出现解决了多语种应用部署的问题,它将应用程序及其运行环境一并打包,调度系统无需关注其内部使用的具体语言。但是多种编程语言的混用,会降低代码的复用程度,因为无法再抽取公共代码成为类库相互引用。
为了解决这个问题,Monzo 将大量基础服务采用 RPC 的方式暴露出来,将“代码共享”变成了“基础服务共享”。例如,如果要获取一个分布式锁,无需通过客户端访问 etcd ,而只需要通过封装好的 RPC 接口即可。通过将基础服务(例如数据库、消息队列等)RPC 化,每种新的语言只需要发起 RPC 调用,即可使用现有的基础服务。通过这种访问,Monzo 完成了对 Java、Python 和 Scala 等语言的兼容。
RPC 框架
上一节提到了,Monzo 通过 RPC 的方式将基础服务暴露出去,因此它们的基础架构需要一个强大的 RPC 框架,以支撑其微服务架构。
首先是传输协议,为了能够让多种语言构建的服务之间能够方面交互,HTTP 协议是首选的传输协议。几乎每种语言都有实现 HTTP 协议的标准库,这能够降低使用门槛。
另外,要能够支撑整个微服务架构,RPC 框架还应该有以下这些特性:
- 负载均衡:大部分 HTTP 库都实现了基于 DNS 的轮询负载均衡,但是这个方式比较生硬。理想的负载均衡应该能够选择最合适的目标服务器,以达到较低的失败率,较小的延时。这样即使集群中出现因为故障而进行复制的副本,也不会影响整个系统的性能。
- 自动重试:对于分布式系统来说,故障是难以避免的。如果一个幂等调用失败,RPC 系统应该要能够自动请求集群中的其他副本,以确保集群中存在少量故障节点时,系统整体仍然可用。
- 连接池:如果每个请求都需要重新创建连接,远程调用的延迟会大大增加。理想情况下每个远程调用请求都应该尽可能复用之前已经创建的连接。
- 路由:对于一个 RPC 系统来说,能够运行时修改目标机器是非常有必要的。例如一个新版本服务上线,可能需要一定的灰度过程。从新上限到 100% 使用,期间需要通过路由功能逐步将流量引到新版本服务上。
基于上述这些特性,Monzo 最终选择了 Finagle 。它拥有上述所有特性,并且自身的模块化设计也降低了学习成本。另外,Twitter 已经使用该框架多年,说明它经受了实战的考验。刚好,在今年 linkerd 发布了,它是基于 Finagle 的进程外代理,这意味着那些不运行在 JVM 上的语言也能够使用 Finagle 的这些特性。
Monzo 将 linkerd 在 Kubernetes 上以守护集(daemon set)方式部署,这样每台服务器上都会运行linkerd 服务。应用服务首先和本机上的linkerd 服务进行交互,然后linkerd 再通过 Power of Two Choices + Peak EWMA(exponentially-weighted moving average)方式做负载均衡。该算法通过请求往返时间和请求次数加权计算均线,然后从其中选择最优的 n 个结果,再从结果集中随机挑选。这种负载均衡方式兼顾了选取最优后端,又避免了因为后端失效导致的抖动。
异步消息
Monzo 的大部分业务逻辑在后台都是通过异步消息完成的。虽然有些操作本身耗时很短,但是通过异步消息,可以更快的向用户反馈任务状态。
由于大量核心逻辑都采取了异步化,每个消息都非常重要,一个完整业务操作每个步骤的消息都不能跳过,即使发生了无法恢复的异常,整个流程也应该能够在故障修复之后继续执行下去。因此,对于异步消息架构来说,必须满足以下特性:
- 高可用:消息发送者在发出消息之后,无需再关注消息的消费情况。即使消费者节点或者消息系统本身出现故障,该消息也必须确保被最终消费者处理。
- 可扩展性:由于整个系统中会有大量消息流转,消息系统本身必须能够水平扩展。当业务压力过大时,可以像其他微服务一样简单的扩展消息系统的容量。
- 持久化:在任何情况下,消息系统必须保证消息不会丢失。即使出现消息系统服务器故障,或者消费者出现故障时,消息最终也能够被消费者消费。
- 可回放:消息系统支持回放,可以方便的重现每一个消息的处理流程,对于排查问题、故障恢复等场景非常有用。
- 至少一次投递:消息系统首要保证消息必须被正确消费,但是要确保投递且仅投递一次基本上是不可能的,因此消息系统需要在通常情况下尽可能确保不会重复投递。
基于上述特性,在比较了多重消息中间件之后,Monzo 最终选择了 Kafka 。虽然从架构上来看,Kafka 和一般的消息中间件有着很大的不同,它更像是一个可复制的提交日志(commit log),但是它的特性刚好可以满足 Monzo 对于异步消息架构的要求。首先 Kafka 的复制和分区特性,可以满足对高可用和可扩展性的要求。其次,Kafka 的消费者仅仅维护了一个消息日志中的游标,这样可以降低发布/ 订阅模型模式的成本,另外还可以通过修改游标来重新处理过去的消息。
总结
Kubernetes、Docker、linkerd、Kafka,Monzo 通过一系列开源软件,构建了他们的 7*24 小时银行后台系统,其中的选型、运用对于其他系统也有一定的借鉴意义。
感谢郭蕾对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论