我有以下任务,我的Java / Executors解决方案运行良好,但我想在Akka中实现相同的功能并寻找最佳实践建议。
问题:
并行获取/解析来自多个URL的数据,阻止直到获取所有数据并返回聚合结果。应该重试错误(IOException等)达到一定次数。
到目前为止,我的实现非常简单 - 创建Fetcher actor,它知道应该获取哪些URL,它创建一堆Worker actor并发送它们,每个消息一个。完成特定的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,但是当时正在处理的消息丢失了。即使我使用不同的SupervisorStrategy,结果也是一样的 - 一些消息有效地丢失了#39;当然我可以使用try / catch在Worker.onReceive()中包装代码,但我觉得这违背了Akka哲学。我想我可以使用持久性消息,但我不认为在这种情况下,消息持久性的复杂性是合理的。
我可能需要某种方式让Fetcher弄清楚工人未能获取一些URL并再次重新发送WorkUnit或检测到某些结果没有回来太久。处理这种情况的最佳方法是什么?
谢谢,
答案 0 :(得分:1)
我们在项目中遇到了类似的问题,我们找到了一个适合我们的解决方案 - 无论例外,工作人员失败,网络故障等都执行任务。虽然我必须承认代码最终变得有点复杂。
所以我们的设置如下:
WorkerControl
演员负责处理任务管理和与工作人员的沟通WorkerControl
接收一些要处理的数据,并在工作人员之间调度任务我们或多或少地尝试遵循here
所述的指南但我们也提高了设计的容错能力。
在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;
}
我们处理以下可能失败的列表
工作人员通过抛出异常(即您的情况下为IOException)来完成任务
我们向工作人员控件发送new Failure(caughtException)
消息。看到它后,工作人员控制增加failureCount
并将任务放在todoList
队列的头部。当达到给定数量的故障时,该任务被认为是永久失败的,并且从不重试。 (之后,可以以自定义方式记录,处理和处理永久失败的任务。)
工作人员在给定的时间内没有提供任何结果(例如,他陷入无限循环,工人机器上的资源争用,工人神秘消失,任务处理时间过长)
我们为此做了两件事
latestResultDelivery
的{{1}}字段,并将任务分配存储在taskInfo
地图中。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); } }
网络断开,工作人员进程死亡
我们再做两件事:
在工人注册后,我们开始观察工人
registeredWorkers.put(worker.path(), worker); context().watch(worker);
如果我们在工作人员控件中收到assignedTasks
消息,我们会增加Terminated
并将任务返回给restartCount
。重新启动太多次的任务最终会永久失败,永远不会再次重试。这是在任务本身成为远程工作人员死亡的原因(例如由于OutOfMemoryError导致远程系统关闭)的情况下完成的。我们为失败和重启保留了单独的计数器,以便能够更好地精确重试策略。
我们也尝试在工人本身中容忍失败。例如。工人控制他的任务的执行时间,并监控他最近是否做了什么。
根据您需要处理的故障类型,您可以实施列出的策略的子集。
结论:正如其中一条评论中提到的那样:为了重新安排任务,您需要在Fetcher中保留一些数据结构,以映射工作人员和分配的任务。
答案 1 :(得分:0)
由于没有人回答这个问题,这是我到目前为止所发现的。在我看来,对于我的情况,Mailbox with Explicit Acknowledgement将是合适的。以下是修改后的代码的样子。
首先,在classpath中的pee-dispatcher.conf文件中定义rssWorker的peek-dispatcher和部署:
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。
答案 2 :(得分:0)
为了让Fetcher能够知道失败的消息/任务是什么,你可以使用actor preRestart akka build-in hook。
您可以在此处查看详细信息: http://alvinalexander.com/scala/understand-methods-akka-actors-scala-lifecycle
根据Akka文档,当演员重新启动时, 当调用preRestart时,将通知old actor 导致重新启动的异常以及触发的消息 例外。如果未导致重新启动,则消息可能为“无” 通过处理消息。