繁体   English   中英

处理Akka演员异常的最佳实践

[英]Best practices for dealing with exceptions in Akka actors

我有以下任务,我的Java / Executors解决方案运行良好,但我想在Akka中实现相同的功能并寻找最佳实践建议。

问题:

从多个URL并行获取/解析数据,阻塞直到获取所有数据并返回聚合结果。 应该重试错误(IOException等)达到一定次数。

到目前为止,我的实现非常简单 - 创建Fetcher actor,它知道应该获取哪些URL,它创建一堆Worker actor并发送它们URL,每个消息一个。 完成特定的URL Worker后,将结果发送回Fetcher。 Fetcher保持结果状态,工人无国籍。 以下简化代码。

提取程序:

class Fetcher extends UntypedActor {
  private ActorRef worker;

  public void onReceive(Object message) throws Exception {
    if (message instanceof FetchMessage) {
      this.worker = context().actorOf(SpringExtension.SpringExtProvider.get(actorSystem).props("Worker")
              .withRouter(new RoundRobinPool(4)), "worker");
      for(URL u: urls) {
        this.worker.tell(new WorkUnit(u), getSelf());
      }
   }
   else if (message instanceof Result) {
     // accumulate results
   }
}

工人:

class Worker extends UntypedActor {

  public void onReceive(Object message) throws Exception {
    if (message instanceof WorkUnit) {
      // fetch URL, parse etc
      // send result back to sender
      getSender().tell(new Result(...), null);
    }
}

到目前为止,如此好,没有例外,一切都按预期工作。

但是如果在Worker中获取URL时发出IOException,那么Akka会重新启动Worker actor,但是当时Worker正在处理的消息将丢失。 即使我使用不同的SupervisorStrategy,结果也是一样的 - 有些消息实际上“丢失”了。 当然我可以使用try / catch在Worker.onReceive()中包装代码,但我觉得这违背了Akka哲学。 我想我可以使用持久性消息传递,但我不认为在这种情况下消息持久性的复杂性是合理的。

我可能需要某种方式让Fetcher弄清楚工人是否未能获取一些URL并再次重新发送WorkUnit或检测到某些结果没有回来太长时间。 处理这种情况的最佳方法是什么?

谢谢,

我们在项目中遇到了类似的问题,我们找到了一个适合我们的解决方案 - 无论例外,工作人员失败,网络故障等都执行任务。虽然我必须承认代码最终变得有点复杂。

所以我们的设置如下:

  1. 有一个WorkerControl actor可以处理任务管理并与worker进行通信
  2. 有许多工作者演员住在不同的VM(可能在不同的物理机器上)
  3. WorkerControl接收一些要处理的数据,并在工作者之间分派任务

或多或少,我们尝试遵循此处描述的准则

但我们也提高了设计的容错能力。

在WorkerControl中,我们保留以下数据结构:

Map<ActorPath, ActorRef> registeredWorkers // registry of workers
Deque<TaskInfo> todoList                   // tasks that have not been yet processed
Map<ActorRef, TaskInfo> assignedTasks      // tasks assigned to the workers
Map<ActorPath, ActorRef> deadWorkers       // registry of dead workers

对于要执行的每个任务,我们保留数据结构

class TaskInfo {
    private final WorkerTask task;
    private int failureCount = 0;
    private int restartCount = 1;
    private Date latestResultDelivery;
}

我们处理以下可能的故障列表

Worker通过抛出异常(即您的情况下为IOException)来完成任务

我们向worker控件传递一个new Failure(caughtException)消息。 看到它后,工作人员控制会增加failureCount并将任务放在todoList队列的头部。 当达到给定数量的故障时,该任务被认为是永久失败的,并且从不重试。 (之后,可以以自定义方式记录,处理和处理永久失败的任务)。

工人在给定的时间内没有提供任何结果 (例如,他陷入无限循环,工人机器上的资源争用,工人神秘地消失,任务处理花费太长时间)

我们为此做了两件事

  1. 我们初始化latestResultDelivery基于场taskInfo并存储在任务分配assignedTasks地图。
  2. 我们会定期对工作人员控制执行“健康检查”,以确定工人是否已经处理某项任务太长时间。
for (ActorRef busyWorker : assignedTasks.keySet()) {
        Date now = new Date();
        if (now.getTime()
                - assignedTasks.get(busyWorker).getLatestResultDeliveryTime() >= 0) {
            logger.warn("{} has failed to deliver the data processing result in time", nameOf(busyWorker));
            logger.warn("{} will be marked as dead", nameOf(busyWorker));
            getSelf().tell(new Failure(new IllegalStateException("Worker did not deliver any result in time")),
                    busyWorker);
            registeredWorkers.remove(busyWorker.path());
            deadWorkers.put(busyWorker.path(), busyWorker);
        }
    }

网络断开,工人进程死亡

我们再做两件事:

  1. 在工人注册工人控制后,我们开始观察工人演员

      registeredWorkers.put(worker.path(),worker);\n 。上下文()看(工人); 

  2. 如果我们在worker控件中收到Terminated消息,我们递增restartCount并将任务返回给todoList 重新启动太多次的任务最终会永久失败,永远不会再次重试。 这是在任务本身成为远程工作人员死亡的原因(例如由于OutOfMemoryError导致远程系统关闭)的情况下完成的。 我们为失败和重启保留单独的计数器,以便能够更好地精确重试策略。

我们也尝试在工人本身中容忍失败。 例如,工作人员控制他的任务的执行时间,并且还监视他最近是否一直在做任何事情。

根据您需要处理的故障类型,您可以实施所列策略的子集。

底线:正如其中一条评论中提到的那样:为了重新安排任务,您需要在Fetcher中保留一些数据结构,以映射工作人员和分配的任务。

由于没有人回答这个问题,这是我到目前为止所发现的。 在我看来,对于我的情况, 具有显式确认邮箱将是合适的。 以下是修改后的代码的外观。

首先,在classpath中的pee-dispatcher.conf文件中为rssWorker定义peek-dispatcher和deployment:

peek-dispatcher {
  mailbox-type = "akka.contrib.mailbox.PeekMailboxType"
  max-retries = 10
}

akka.actor.deployment {
  /rssFetcher/rssWorker {
    dispatcher = peek-dispatcher
    router = round-robin
    nr-of-instances = 4
  }
}

使用上面的配置创建ActorSystem:

ActorSystem system = ActorSystem.create("Akka", ConfigFactory.load("peek-dispatcher.conf"));

Fetcher几乎保持不变,只有在我们在配置文件中定义路由器时才能简化Worker actor的创建

this.worker = getContext().actorOf(SpringExtension.SpringExtProvider.get(actorSystem).props("worker"), "worker");

另一方面,工作人员会在处理结束时添加额外的行以确认消息。 如果出现任何错误,消息将无法得到确认,并将保留在收件箱中,以便再次重新传送到'max-retries'次,如config中所指定:

class Worker extends UntypedActor {

  public void onReceive(Object message) throws Exception {
    if (message instanceof WorkUnit) {
      // fetch URL, parse etc
      // send result back to sender
      getSender().tell(new Result(...), null);
      // acknowledge message
      PeekMailboxExtension.lookup().ack(getContext());
    }
}

注意:我不确定PeekMailboxExtension.lookup()。ack(getContext()); 是正确的方式来调用确认,但它似乎工作

这也可能与Worker的SupervisorStrategy.resume()结合使用 - 因为Worker没有状态它只能在错误后恢复消息消耗,我认为没有必要重新启动Worker。

为了让Fetcher能够知道失败的消息/任务是什么,你可以使用actor preRestart akka内置钩子。

您可以在此处查看详细信息: http//alvinalexander.com/scala/understand-methods-akka-actors-scala-lifecycle

根据Akka文档,当一个actor重新启动时,旧的actor会在调用preRestart时被告知进程,导致重启的异常以及触发异常的消息。 如果重新启动不是由处理消息引起的,则消息可以是None。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM