《Akka应用模式:分布式应用程序设计实践指南》读书笔记5
数据流
本章节介绍了Akka Streams的相关概念,它为流消息提供了一个领域专用术语。Akka Streams的一些设计理念非常重要,在分布式系统中很多技术都可以借鉴
吞吐量与延迟
测量代码的性能,通常会测量特定操作需要执行的时间,这就是延迟。吞吐量是固定时间内完成操作的次数,是一个频率。这两个指标关注的角度不同,有时候需要综合考虑。比如写数据库,单独一次写入数据库耗时10毫秒,那么写数据库这个操作延迟就是10毫秒。那是不是就意味着在100毫秒内,就可以写入10次呢?(只考虑单机情况)。好像也不一定,至少多线程情况下就不一定了。如果有2个线程那么100毫秒就可以写入20次。那200个线程是不是就可以写入2000次?显然不是的。说吞吐量的时候往往会考虑并发的问题。而吞吐量也会随着并发数的变化而变化
流
数据流是指数据经过一系列步骤处理,一条接一条地产生输出的过程。这个过程中隐含了一个顺序的问题。Akka中的actor使用邮箱系统,邮箱可以简单的理解为先进先出的队列,可以保证无论以什么顺序输入到一个actor内,输出都将按照预期顺序返回。
如果有多个actor,按照顺序串联,消息的延迟就等于每个阶段花费时间的总和。而系统的吞吐量则受到流中最慢的阶段。多个actor组成的其实是一个流水线,虽然某个消息在流水线中必须按照顺序被处理,但actor每个阶段却可以并行发生。
路由器
流可以提高系统的吞吐量,但流的能力受到系统操作的阶段数以及每个阶段需要耗费的时间限制的影响。那如果有多个流是不是可以提高吞吐量?显然是可以的,毕竟有多个流来处理消息了。那么就需要一个actor来接收消息,并将其路由到其他actor。这有点类似负载均衡的原理。路由器就是多条流水线并行处理时的一个分发器。就是在流水线的角度,提高系统的并发、吞吐量。
邮箱
actor使用邮箱保存、传递消息,那如果遇到了慢消费者,邮箱随着时间的推移越来越大时怎么办呢?邮箱可以分为两类:有界和无界。Akka默认的是无界邮箱。
无界邮箱
无界邮箱会一直保存消息,如果消息无限增长,一定会导致内存不足。那么为啥还要提供无界邮箱呢?给你个眼色自己慢慢体会。
如果你的Akka系统用无界邮箱导致内存不足,那么只能说明你的系统设计的有问题,或者至少说不符合Akka的设计模式,或者说没有理解Akka的精髓。
有界邮箱
有界邮箱可以设置容纳消息数量的上限,当消息数超过限制时,邮箱将执行某些操作来缓和问题。某些分布式系统就是用有界邮箱的方法来进行背压设计的。具体操作取决于邮箱的类型,分为阻塞性有界邮箱和非阻塞性有界邮箱。
新版本中已经移除了阻塞性有界邮箱,它存在误导开发者的问题和潜在的系统风险。因为毕竟会阻塞插入操作。其实简单的来说,阻塞性有界邮箱的真正问题是,它在actor之间创建了一个同步的依赖关系。这违反了Actor模型的核心思想:Actor模型通过异消息传递使actor彼此进行通信。在Akkka中要尽量使一切操作异步化。
非阻塞性有界邮箱怎么处理溢出呢?简单的丢弃消息!这有点狠,哈哈。
拉取的工作模式
解决慢消费者问题的一个方案就是让消费者主动拉取消息,而不是被动推送消息。因为消费者总是可以知道自己处理速率的,所谓能者多劳就是这个意思。这个方案非常重要也非常有用。比如某些actor主动的从数据库读取消息进行处理,可以大大的缓和actor的压力,提高系统稳定性。这种模式的一个很大的好处就是,可以根据负载对系统进行响应的扩、缩操作。
当然拉取的时候不要用一个死循环一直拉取,要通过接受某个消息实现。比如接收到X1消息时,从数据源拉取n条数据,发送对应的n条消息给actor,最后再发送下一个X1消息,这样就可以实现类似循环的功能,且不会阻塞当前的actor处理。这是一种很好的设计模式,希望大家自己体会。
背压
拉取工作模式的替代方案是为消费者提供一种支持背压的方法。类似于有水流动的管道,系统对发送方施加反向压力以减慢流速。当消费者数量达到系统的处理极限时,它会发送特殊形式的消息,或者由传输机制支持,提供透明的流量控制。
ack
实现背压的一个简单的方法就是让消费者给生产者发送一个确认消息,通常缩写为ack。消费者可以在1个或者n个消息后发送ack,生产者收到ack消息后才继续发送消息给消费者。其实这一点不太好实现。每条都发送ack还比较容易,那么如果批量发送ack发送就有点麻烦了。
高水位标记
与ack不同。只有当生产者应该减慢或停止生产时才发送消息,一般是在超过高水位标记值时。正常的ack表示已经处理完n条消息了,再发送一些吧。而高水位标记一般是指,当前处理的事情很多,快处理不过来了,发送方需要放缓速率。在Akka中,这其实也不太容易实现,除了记住高水位的位置,还需要发送消息给生产者放缓速率。那么既然都处理不过来了,哪有时间发送消息给生产者呢。这往往需要引入辅助actor实现这一功能。
队列长度监控
跟踪生产者和消费者之间的队列长度也是一种可行的选择。但要谨慎使用这种方法,这毕竟增加了系统的复杂性。如何监视、谁来监视,监视actor发生异常怎么办。这都是需要考虑的问题。
速率监控
这是我比较喜欢的一种方案。大概有两种实现方式。可以用之前的消费者(或者压力测试)作为参考,估算处理消息的近似速率,使用该速率让生产者控制生产速度的参考。比如压测时,消费者Y时间内最多处理X个消息,那么生产者在Y时间内就最多发送X个消息。这种方案简单可行,但最大的漏洞就是随着时间的变化、机器配置的不同、负载的不同,消费者处理消息的能力会有变化,就达不到预期的功能了。
当然还可以建立自调整系统,其中消费者向生产者定期发送消息,不时的更新其速率信息。这虽然复杂,但非常灵活,且消费者和生产者之间的沟通相对较少。关于这个方法,可以参考我另外一篇博文:https://www.cnblogs.com/gabry/p/9138507.html。当然我是动态调整发送速率的。
Akka数据流
Akka流是Akka实现的响应式流,它采用了流、路由器、背压等概念,将其转换为单一一致的DSL,允许编写类型安全的流,如交叉点和背压。同时提供与采用路由器作为分支点创建的actor流一样的所有优势。Akka Strems建立在几个基本概念之上,包括source/singk/flow/junction。很遗憾我对Akka Streams不熟悉,后面的介绍就不作介绍了。