AKKA 学习(二)--- AKKA 2.2 Actor 系统
程序员文章站
2022-05-09 12:15:55
...
Actor是封装了状态和行为的对象,它们仅仅通过交换消息(messages)实现相互通信。这些消息被放置到收件人的邮箱(mailbox)中。从某种意义上说,Actor是面向对象编程的最严格的形式,同时它让人更好理解它们:考虑使用Actor的解决方案,假设一群人,并指派子任务给他们,按照他们的职能作用将他们划分到一个组织结构中,并且需要思考如何处理失败的场景(所有不实际与人打交道的,这意味着我们不需要因为他们的情绪状态或道德问题涉及到己的利益)。这个结果可以作为软件实现的心理脚手架。
注:Actor 系统是一个重量级的结构,将分配1。 。 N个线程,为每一个逻辑应用程序创建一个线程。
2.2.1层次结构 (Hierarchical Structure)
像一个经济组织,actors自然的形成层次结构。一个actors在系统监督下完成特定的功能,比如将它的任务分解成更小,更易于管理。为达到目的,它启动一个它监督下的子actor(child actors )。监管的细节会在这里(见2.4,暂时缺失)解释,在本节中我们会集中考虑基本概念。唯一需要提前知道的是,每个actor 都有一个确切的监管者,也就是创建它的Actor。
Actor 系统的典型特点是任务拆分和委派,直到他们足够小,可以在一个分片中进行处理。这样做,不仅需要任务本身结构清晰,而且作为结果的Actor也必须经得起他们要处理的消息的仔细推敲,他们应该如何处理正常逻辑,又如何处理失败都需要仔细的推敲。如果一个Actor不具备处理某种特殊场景的能力,它会发送一个相应的故障信息给监管者来寻求帮助。递归结构,允许他们在合适层次上处理失败的消息。
与分层软件设计相比,它轻松地实现了防御性编程,不遗落任何错误的目标:如果错误信息传达给正确的人,可以有更好的解决方案,而不是试图把一切都“在地毯下”。
现在,设计这样一个系统的难点是如何决定谁应该监督什么。当然没有一个最佳的解决方案,但也有一些可能会有帮助的指引:
- 如果一个Actor管理另一个Actor做的工作,例如:传递子任务,那么管理者应监督孩子。其原因是由于管理者知道可能出现的错误,以及如何处理它们。
- 如果一个Actor含有非常重要的数据(即其状态,应该尽量避免丢失),这个Actor分配任何可能有危险的子任务给的子Actor,同时也负责监督和恰当的处理这些子Actor失败的情况。根据请求的性质,最好为每个请求创建子Actor,这会简化收集响应的状态管理。这在Erlang里称为“错误的内核模式(Error Kernel Pattern”)。
- 如果一个Actor依赖于另一个Actor,它就应该监听其他Actor的活跃度,并且在接收它的终止通知。这不同于监管,监听不会对监管策略产生影响,并且应当注意的是,单纯的功能性依赖不是衡量一个子Actor在层级结构中位置的标准。
2.2.2 配置容器
Actor 系统是多个协同actors的协作, 它自然需要管理共享设施单元,如调度服务,配置,日志等。同一个JVM中可以存在多个不同配置的Actor 系统,AKKA本身没有全局共享的状态。还有一点,在系统中一个节点内或跨网络的多个Actor 系统的通信连接是透明的,取决于Actor 系统自身所在的功能层次结构。
2.2.3 Actor最佳实践
- Actor应该像很好的同事:高校的完成自己的工作,而不会毫无必要打扰其他人,并且避免占用资源。翻译为程序,这意味着通过事件驱动的方式处理事件并产生响应(或多个请求) 。Actors不应该阻塞(如被动地等待,同时占用一个线程)在一些可能被锁的外部实体上,或者一个网络接口等,除非是不可避免的;在下文中可见后面这种情况。
- 不要在Actor之间传递可变对象。为了确保,宁可传递不可改变的消息。如果暴露自己的可变状态到外面,Actor的封装就被打破了,你又要面临正常Java并发所有的缺点。
- Actors是行为和状态的封装,了解这个意味着不要经常在消息内包含行为(这可能对使用Scala闭包很有诱惑力)。其中一个风险就是不小心在多个Actors之间共享了可变的状态,而这将不幸的打破actor模型给编程者带来的好的体验。
- *是actors是你的错误内核(Error Kernel)的最深处,所以需要有节制地创建他们并且放到真正的层级结构中。这有利于错误处理(考虑配置的粒度和性能),并且也减少了对监护actor的依赖,如果过度使用,会产生单点问题。
2.2.4阻塞需要谨慎的管理
在某些情况下,阻塞操作是不可避免的,比如:把一个线程sleep不固定的时间,来等待某项外部事件。例如传统的关系型数据库的驱动程序或消息API,而更深层的原因是背后典型的(网络)I / O。面对这个问题,你可能自然的想到,包装阻塞调用道Future代替内部调用,但这个策略实在太简单了:当应用程序负载增加的时候,你很可能会很快的发现瓶颈或内存耗尽。
“阻塞问题”的解决方案,有以下几点建议,但这并不是全部的方案:
- 将阻塞调用放入一个Actors(或由一个路由器[Java,Scala]管理的一组Actors)中,并确保给他们配置一个专门的足够大的线程池。
- 将阻塞调用放入一个Future,确保在任何时间点这样的操作有一个上限(无限数量的提交这类任务的会耗尽你的内存或线程限制)。
- 将阻塞调用放入一个Future,提供一个有线程数量上限的线程池,跟进应用程序运行的硬件设置数量。
- 用一个单独的线程来管理一组阻塞的资源(如NIO选择器(selector)驱动多个管道(channels)),并且调度actors的事件消息。
对资源来说最适合的是单线程,如传统的数据库句柄,一次只能执行一个未完成的查询,并且使用一个内部同步确保这一点。更常见的模式是为N个Actors创建一个路由器(router),并且为每一个Actor封装一个单独的DB 连接,查询处理发送到路由器(router)。N的取值必须设置为最大的吞吐量,这将取决于数据库管理系统部署在什么硬件上。
注:配置线程池最佳的方式是委托给AKKA,简单地配置在application.conf,然后实例化一个Actor系统[Java,Scala]