几个月前,我接到一个任务,要拿出一个集中式的事件系统,可以让我们的各种后端组件相互通信。我们讨论了后端活动流、渲染、数据转换、建筑信息模型(BIM)、个性身份(identity)、日志报表、分析等等。找寻其中是否有真正通用的可变负载、使用模式和扩展性配置。或许是其他的某样东西,能使我们的开发团队轻松接口。当然,系统的每个部分都应该具备自我扩展的能力。
由于我没有时间写太多的代码,所以我选择了Kafka作为我们的存储核心,因为它稳定且被广泛使用,而且表现良好(请注意,我没有说非得用它不可,可以用其他东西替代)。当然,现在我还不能直接将Kafka暴露出去,得在前端用一些API去实现这些。没想太多,我就放弃了在我的后端管理偏移量的想法,因为这在为实例处理失败时,会给人带来太多的约束。
所以我的结论就是,将系统分为独立的两层:第一层是API层,负责处理请求流(incoming traffic);第二层是后端层,负责处理长期的、有状态的、与Kafka交互的流处理进程(比如,实现生产者和消费者)。这两层都可以独立扩展,只要求它们之间保持一致的路由,以保证客户端可以与相同的后端流处理进程,保持通信。
两层代码均100%使用Scala实现,并使用Play!框架。同时,它们重度依赖Akka actor系统(每个节点通常跑数百个actor)。后端层实现了一组自定义的Kafka生产者和消费者,并使用一组专用的actor来管理预读与写缓冲区。一切都被实现为嵌套的有限状态机(我超爱这个概念)。分析数据存储到Splunk,同时,度量数据存储到Librato(collectd是在容器中运行的)。
在此,哥文艺地展示下系统发布后的模样如何实现两层之间的路由呢?只需使用RabbitMQ即可,它有很好的持久性和弹性,甚至没什么趣味可言。AMQP队列是实现这一简单的“电话交换机”模式的伟大方法。通过使用一些逻辑分片技术(比如对一些存在于每个事务中的cookie值进行散列或者类似的技术)将一组固定的后端节点关联到某个RabbitMQ broker,就能够让路由支持扩展,这简直是小事一桩。
为什么我没有对RabbitMQ的broker做集群?哇哦,我能说我懒吗,关键是我真心觉得没啥必要。在每个broker之间分流实际上是为了高效,而在我看来,更重要的是以更轻松的方式去控制。为此付出的额外工作相比收成毫无意义。
所以,总之,给定容器拓扑,我们的请求会保持一条特定路径,这条路径依赖于后端节点所处理的流会话。我们所需的每一层可独立扩展远比整体可扩展重要。唯一的实际限制将是虚拟网络适配器和带宽。
虚线表示给定的会话将保持的特定路径。
现在,我们开始有意思的部分吧:如何确保可靠的通信,避免拜占庭式的失败呢?我要说,这非常简单,只需使用简单的两阶段提交协议,在客户端和后端同时建立状态机镜像模型(例如,他们总是同步的)。读写操作需要显式的确认请求。当你尝试读取并且失败的时候,你只需重新尝试,直到获得确认,然后这个确认将改变后端的状态(比如,向前移动Kafka的偏移量或者下发定时任务将事件发布)。所以我的客户端和后端之间的传输流实际上是这样的:“allocate session”、“read”、“ack”、“read”、“ack”......“dispose”。
这一系统的巨大优势是有效地实现了操作幂等,而且没有任何烦人的声明语句,就可以编码状态机中的所有逻辑(我告诫自己:我要提供一个纯粹的功能实现,没别的,只为了炫技)。应对任何网络故障,当然是优雅地重新尝试。通过这种方式,还可以得到自由的控制流和背压技术。
这个系统的API会暴露为Apache Thrift(当前通过HTTPS协议使用压缩技术工作,计划在某个时间点迁移到普通的TCP层)。我提供了Python、Scala、.NET和Ruby语言的客户端 SDK,以应对我们在Autodesk所使用的、花样百出的技术)。请注意,Kafka偏移量是由客户端管理的(虽然这样不透明),这使得后端控制更简单。
稍等,我听见你说,如何处理后端节点宕掉的情况?哇哦,因为我们有两阶段协议,所以我们在读取数据的时候并不会真的获取:如果客户端不断地失败,将使用当前的偏移量,重新分配一个新的流会话。当向 Kafka写入数据时,麻烦来了,因为这个过程是异步并且潜在地受下行流背压(比如你的节点失败了并且Kafka broker也出现问题了)。我会将后端节点配置为正常关机,在等待任何写挂起时,传入的任何请求都会快速失败。最后一招,我们甚至可以将任何待处理的数据刷新到磁盘上(之后,重新将数据注入)。
稍等,我听见你又说,如果基础设施的一部分挂掉会怎样?同样的。与流会话处理的实际后端节点之间的通信中断,当然会慢下来,但不会再有什么讨厌的影响,这又要归功于两阶段协议喽。
哦,我忘了说,在将数据写入Kafka日志前是自动加密的(使用AES 256)。Kafka的生产者和消费者使用同一套密钥共享。在涉密的主题上,我可以让流会话通过OAUTH2来验证,对每个请求验证MD5-HMAC,走TLS到后端集群。
现在你又问,那么,我们如何完整地部署这个时尚最时尚的系统呢?嗯,我们将整套系统100%地运行在一个普通的Mesos/Marathon集群上(现在用的不是DCOS哦,但我们可以切过去,因为这样可以用上他们超赞的仪表盘)。集群托管在AWS EC2上,我们基本上整体复用了一些 c3.2xlarge实例(在给定的区域内,10到20个小型部署足够了)。请注意,我们可以在Kubernetes上(无论是EC2还是GCE)做同样的事情。
小秀一下我们技术栈的结构
每处部署都使用了我们的开源技术Ochopod(自集群容器)。操作被降低到绝对最小值。例如,我们要做一次优雅的构建推送,就拿API层来说,无非是分配一些新的容器,等待他们启动好,然后逐步替换旧部署,这样就可以了。所有这一切都是通过集群中的一个专用的Jenkins slave(它本身也是一个Ochopod容器)完成的!
实际上,我开发了一个叫Ochothon的迷你PaaS系统,只是为了能够快速开发/运维所有的容器。
(点击放大图像)
Ochothon CLI显示了我的一个预发布集群的情况
为了能让你感受Ocho-*平台是多么好用,我这样说吧,一个人(比如我)就能够管理跨2个区域的5个部署系统,包括所有的基础设施副本……此外,还能有时间写写博客、码码程序!
所以,总体来说,此间的设计和编码,整件事情都充满乐趣,再加上它现在已经在生产运行,是我们云基础架构任务的关键部分(这是一个还不错的打赏)。如果你想对这个奇葩的流系统有更多的了解,告诉我们哦!