我正在为 appengine 开发作业调度程序,默认调度程序总是会启动 3-4 个实例来完成所有工作,一些溢出实例可能需要数千个任务,或者只有几个,然后坐在那里烧 CPU什么也不做。
我的任务涉及为许多不同大小的域处理作业,有时吞吐量很大,有时是一个用户有 10,000 个模型要更新;如果我松开普通的 appengine 任务调度程序,它会以两种方式失败:1)后端永远不会关闭,当内存达到上限时,java gc 会使实例抖动并表现得几乎是僵尸但从未关闭{并且仍然take/hold jobs},以及 2) 许多域有一个用户,其处理时间比其他所有用户要长得多,这使得后端在域的其余部分完成后很长时间内保持活动状态。
这些任务必须全天运行,并且需要多个后端来处理扇出,所以我不能将它们全部转储到 B8 上并收工。所以我们需要一个调度程序来管理如何将任务分配给后端。
现在,我不想为了节省几分钟的 cpu 时间而为每项任务支付数据存储操作费用,所以我的攻击计划{请批评}是在 RAM 中使用静态 ConcurrentHashMap,尝试启动每个 run() ,让每个延迟任务在启动时将其 [hashcode, startTime] 放入并在 finally 中放入 remove(hashcode)。每个正在运行作业的后端实例都会有一个这样的映射,包装在一个方法中,BackendCounter.addToLiveMap(this); 它的 .size() 用作该后端上有多少作业处于活动状态的运行总数{带有时间戳以检测运行 > 10 分钟的僵尸作业}。作业调度程序可以为每个实例触发一个工作线程,以监控在该实例中运行的作业(不包括其自身)的数量,并在内存缓存中保留一个排名列表,其中哪些实例有多少任务处于活动状态。如果一个实例低于 X 个活动任务的阈值,则选择一个溢出实例来推迟,然后让 BackendCounter.addToLiveMap(this) 方法抛出一个我可以捕获的异常,告诉作业只将自己安排到一个新实例 {ChangeInstanceException#获取新目标()}。通过这种方式,我可以防止几乎没有使用过的实例获得新作业,以便它们有机会关闭,只需为一些 memcache 操作付费,而扇出只需对静态映射进行写入和删除。
这解决了问题二,即实例小时杀手。至于问题一,即如何防止一个实例(通常是实例 0 和 1)达到内存峰值并开始转向黑暗面,我在两个选项之间徘徊。
一方面,我可以使用对 BackendCounter.addToLiveMap(this) 的预期调用 throws ChangeInstanceException 并简单地检查内存:
if (((float)Runtime.getRuntime().freeMemory() / Runtime.getRuntime().totalMemory())<0.9) throw new ChangeInstanceException(getOverflowInstance()); 这种天真的方法只会告诉任何接近其内存限制的实例将所有新工作发送到其他地方。
另一方面,我可以保留实例 0 和 1 来处理溢出{并在两者中的哪一个获得新作业以使它们有机会关闭},然后将扇出发送到实例 2+,它只会运行直到它们顺便说一下,并行 10 或 15 个工作。扇出非常一致,只需要几分钟,因此实例 2、3 和最多 4 个将需要打开,并在另一个实例受到更多负载时有时间关闭。我唯一担心的是,如果作业开始从一个实例跳到另一个实例,这可能可以通过重定向标头限制来克服,以跳过抛出 ChangeInstanceException。
非常感谢任何想法或建议。