老子有钱娱乐lzyq58
  咨询电话:15555742862

老子有钱88APP

设计Optaplanner下实时规划服务的失败经历

  其实本文不知道算不算一个知识点分享,过程很美妙,但结果很失败。我们在利用Optaplanner的Real-Time planning(实时规则)功能,设计实时在线规划服务时,遇到一个属于Optaplanner7.8.0.Final版本的Bug。在实现实时在线规划服务的过程中,我做过很多尝试。因为需要实时在线的服务,因此,需要设计多线程并发为外界请求提供响应,需要实现消息队列来管理并发请求的时序等问题。这些Java方面的并发处理,我们暂时不详述,这方面的牛的人太多了,我只是新手,站在别人的肩膀上实现的代码而已。在本文我着重介绍一下,我在尝试使用Optaplanner的Real-Time Planning功能时遇到的问题,最终确认问题出自Optaplanner引擎自身, 并通过JIRA向Optaplanner 团队提交issue过程。

关于Optaplanner的Real-time planning

  先看看正常情况下,我们对Optaplanner的应用场景。平时我们使用Optaplanner时,不外乎以下几个, 构建Problem对象 + 构建Solver对象-> 启动引擎 -> 执行规划 -> 结束规划 -> 获得方案-> 获取结果方案,如下图。

  这种应用模式下,引擎处于一个非实时状态,只是一个调用 -> 获取规划结果的简单交互过程。

 

 

  但是有些对规划具的时间性要求较高,或在时间序列上,对规划的结果具有一定的延续性要求的情况下,这种规划方式是满足不了要求的。例如有些实时调度的场景;要求每个新的solution与上一个solution需要具有延续性,不可能每次给出的solution存在过大的差异,若产生过大的差异,这些规划出来的方案对于执行机构来说,是不可能按计划执行的。例如车辆调度系统(见下图),每隔一个时间段,就需要刷新一下车辆情况和环境情况,不可能每次刷新出来的调度方案跟前一次存在千差万别。每一次产生的方案,它必须尽最大程度上与上一次保持相近

 

  另外一个要求是实时性,如果按传的规划步骤,对于实时性有要求,或响应速度较高的场景,例如:车间作业的实时调度系统,可能每隔离10分钟就需要刷新一次计划,此时实时规则的作用就反映出来了。如下动图:

   Real-time planning, 顾名思义就是实时规划,它与传统的规划步骤区别在于,它并没有一个结束并退出规划的动作,面是一旦引擎启动,它将以守护进程的形式一直处于运行状态,而没有返回;当它满足规划结束条件时(例如找到符合条件的方案,或到达规划时限),会进入值守状态,不占用CPU资源。待激发事件对它发出重新启动的指令。因此,它的步骤是: [构建Problem对象] + [构建Solver对象] -> 启动引擎 -> 规划  -> 通过BestSolutionChange事件输出规则方案 -> 休眠 -> 接到重启指令 -> 规则(重重上述步骤),如下图:

 

  原来Optaplanner还有这种神操作,那么它的作用将进一步大增了,幻想一下大家看科幻或战争电影时,那里的指挥中心必然有一个大屏幕,上面显示了实时的战况或各方资源的部署情况,如果这些部署是需要通过规划来辅助实现的话,Optaplanner是不是可以作为后台超级计算机上不停运算规划的控制中枢系统呢?不过好像想多了。没那么神,做一下实时作业调度还是可以的。下面就看看我们的项目是如何考虑应用Real-time planning的。

  关于Real-Time Planning的具体开发步骤没办法在这里详述,在本系列的往后文章中,老农将会有一篇专门的文章介绍。它的基本步骤如下图。

 

  这里提供一下最重要的三个代码块,对应的场景是,当一个新的任务(Task)需要被添加进引擎的Problem中参与规则时,应该如何添加,添加完成之后,如何获得规划的结果。这三个代码块的功能分别是bestSolutionChanged事件处理程序,调用引擎Solver对象提交变更请求,和实现ProblemFactChange接口的实现,用于实现变更正在规划的Planning Entity.

 bestSolutionChanged事件处理程序

1 // solver是一个Solver对象,引擎入口

2 solver.addEventListener(new SolverEventListener<TaskAssignmentSolution>() {3 public void bestSolutionChanged(BestSolutionChangedEvent<TaskAssignmentSolution> event) {4 if(solver.isEveryProblemFactChangeProcessed()) {5 // TODO: 获取规划结果6 }7 }8 });

 

调用引擎Solver对象提交变更 

1 DeleteTaskProblemFactChange taskProblemChange = new DeleteTaskProblemFactChange(task);2 if (solver.isSolving()) {3 solver.addProblemFactChange(taskProblemChange);4 } else {5 taskProblemChange.doChange(scoreDirector);6 scoreDirector.calculateScore();7 }

 

ProblemFactChange接口的实现

1 /** 2 * 添加任务到Workingsolution 3 * @author ZhangKent 4 * 5 */ 6 public class AddTaskProblemChange extends AbstractPersistable implements ProblemFactChange<TaskAssignmentSolution>{ 7 private final Task task; 8 9 public AddTaskProblemChange(Task task){10 this.task = task;11 }12 13 @Override14 public void doChange(ScoreDirector<TaskAssignmentSolution> scoreDirector) {15 16 TaskAssignmentSolution taskAssignmentSolution = scoreDirector.getWorkingSolution();17 18 scoreDirector.beforeEntityAdded(this.task);19 taskAssignmentSolution.getTaskList().add(this.task);20 scoreDirector.afterEntityAdded(this.task);21 scoreDirector.triggerVariableListeners();22 }23 }

 

场景要求

   我们的项目其实挺符合实时作业的要求的,虽然我们也没有要求达到分钟级,或秒级的响应;但是如果能够每隔离10分钟,通过实时规划的模式刷新一次计划,还是更能帮助生产调度人员更准确掌握生产情况的。事实上,我们对新的计划刷新条件,并不是按固定的时间间隔来进行,而是以触发事件的方式对进行变更规划的。

  即当一个新任务产生了,或一个已计划好的任务被生产完成了,或一个已计划好的任务无法按时执行生产作业而产生计划与实际情况存在差异时,或一个机台出现计划以外的停机等诸如此类对计划足以产生影响的事件,都将会作为触发重新规则的条件。因此,我将引擎程序做成Springboot程序,部署到服务器端,并将程序设计成多线程并发的模式,主线程负责侦听Springboot接收到的WebAPI请求,当接收到请求后,就从线程池中启用一个线程对请求进行处理,这些处理是更新规划的请求,并把传送过来的Planning Enitty, Problem Fact等信息按要求进行处理,并放入队列中。所有请求产生的重新规划信息,通过队列依次被送入引擎处理。当有新的solution产生时,将它输出指定位置,并通知客户端前往获取。

系统的构件结构如下图

 遗憾

  古语有云,理想很丰满,现实很骨感。上述的设计对于Optaplanner的使用领域来说,是比较先进的(起码在国内还没听说过有人这样用法)。对业务而言也是非常符合要求的。但是我对上述所有美妙的构想完成了设计,并实现了代码,并通过Springboot运行起来之后。程序确实如我意图那样运行起来了!启动引擎 -> 开始规则 -> 找到更佳方案 -> 输出方案 -> 满足停止条件 -> 引擎进入守值状态. 好了,我就通过http发出一个删除Planning Entity的请求。Springboot的Contoller成功接收,启动子线程处理数据,向引擎对象发送doChange请求,引擎检测到请求,分出一个线程(这个线程是引擎分出来处理我那个线程请求的)处理成功,并更新Problem对象中的Planning Entity列表;引擎继续运行。Duang~~~~引擎主线程竟然抛出一个异常并停止了!提示那个被请求删除的Planning Entity未被加入Planning Entity的列表中!这下我蒙了。为什么还会报出这个Planning Entity未被加进列表的错误?回想起Optaplanner的开发说明书里,关于Planning过程中,每个新的solution都是一个clone的情况,我坚信我的程序是遇到Race condition了,一定是我的程序考虑不周导致资源竞争。Optaplanner号称经过大量单元测试,压力测试,有良好的稳定性,不可能就这样被我把错误试出来的。但切切实实地抛出了这个异常,而我却没有任何办法。错误信息如下图,下图是我截取给Optaplanner团队的:

  然后,我花了两天时间,对每一个步骤进行调试分析,对每一个solution的clone进行核对,我确实没办法从我的程序中找到任何头绪。于是我唯有求助于Geoffrey大神。通过邮件讨论组我给他留了个贴子。很快Geoffrey大神就回复了(这个得给个赞,比利时跟我们的时区相差不少吧?每次提的问题,他都能及时回复)。回复见下图,这个回复令了心被泼了一大桶冷水。它竟然确实可能是一个bug! 当然也有可能是程序产生了race condition. 可我都找了两天了,实在没办法,才想到找Optaplanner团队。然后我就把这个问题的重现步骤在Optaplanner项目的JIRA中提交了一个issue,不知道这算不算我给Optaplanner作出的一点点贡献呢,期待处理结果呀。

  其实在这两天时间时,我并不仅仅是检查我自己的代码是否出现资源竞争问题,我还Debug进了Optaplanner的源代码里(7.8.0.Final版),并找到了异常的具体来源。发现确确实实是在我提交了ProblemFactChanged请求后,引擎也进行了处理,但因为引擎在处理了请求后,在新的Solution的clone中,并没有被成功更新,也就是新的Planning Entity并没有进入新的solution clone中,而导致处理程序无法识别新的Planning Entity, 就出错了。

 


 

 

 

  现在办法有两个,一个是等Optaplanner团队在JIRA上对我提交的issue进行处理,看是不是真的在Optaplanner中存在这么一个Bug. 另一种办法是我打算将我的程序进一步简化,将它与Springboot分离,跟Optaplanner的事件程序一样,通过其它方法启动线程来尝试Real-Time Planning.

  Optaplanner引擎程序被包装成一个Springboot程序,并设置为daemon模式(守卫进程),Springboot Application启动后,引擎执行程序被一个线程启动。主线程向外提供Restful webservice,当有Web请求到达时,就启动一个线程用于执行Optaplanner的ProblemFactChange对象中的doChange方法,对现有solution中的Planning Entity列表中的对象进行增删改操作;并触发VariableListeners. 引擎在处理这些调用时,会产生新的bestSolution,并触发BestSolutionChangedEvent事件,在事件处理方法中,将最新的Solution中的Planning Entity列表输出即可获得增删改Planning Entity后的最新solution了。

这又是一篇花费不少精力的东西,尽管最终没实现实时规划服务。

创作不易,欢迎转载,请标明出处。