其实本文不知道算不算一个知识点分享,过程很美妙,但结果很失败。我们在利用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事件处理程序

 // solver是一个Solver对象,引擎入口
 solver.addEventListener(new SolverEventListener<TaskAssignmentSolution>() {
public void bestSolutionChanged(BestSolutionChangedEvent<TaskAssignmentSolution> event) {
if(solver.isEveryProblemFactChangeProcessed()) {
// TODO: 获取规划结果
}
}
});

调用引擎Solver对象提交变更 

 DeleteTaskProblemFactChange taskProblemChange = new DeleteTaskProblemFactChange(task);
if (solver.isSolving()) {
solver.addProblemFactChange(taskProblemChange);
} else {
taskProblemChange.doChange(scoreDirector);
scoreDirector.calculateScore();
}

ProblemFactChange接口的实现

 /**
* 添加任务到Workingsolution
* @author ZhangKent
*
*/
public class AddTaskProblemChange extends AbstractPersistable implements ProblemFactChange<TaskAssignmentSolution>{
private final Task task; public AddTaskProblemChange(Task task){
this.task = task;
} @Override
public void doChange(ScoreDirector<TaskAssignmentSolution> scoreDirector) { TaskAssignmentSolution taskAssignmentSolution = scoreDirector.getWorkingSolution(); scoreDirector.beforeEntityAdded(this.task);
taskAssignmentSolution.getTaskList().add(this.task);
scoreDirector.afterEntityAdded(this.task);
scoreDirector.triggerVariableListeners();
}
}

场景要求

  我们的项目其实挺符合实时作业的要求的,虽然我们也没有要求达到分钟级,或秒级的响应;但是如果能够每隔离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了。

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

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


本系列文章在公众号不定时连载,请关注公众号(让APS成为可能)及时接收,二维码:


如需了解更多关于Optaplanner的应用,请发电邮致:kentbill@gmail.com
或到讨论组发表你的意见:https://groups.google.com/forum/#!forum/optaplanner-cn
若有需要可添加本人微信(13631823503)或QQ(12977379)实时沟通,但因本人日常工作繁忙,通过微信,QQ等工具可能无法深入沟通,较复杂的问题,建议以邮件或讨论组方式提出。(讨论组属于google邮件列表,国内网络可能较难访问,需自行解决)

设计Optaplanner下实时规划服务的失败经历的更多相关文章

  1. phpnow 在win7下遇到“安装服务[apache_pn]失败”问题的一种解决办法

    安装PHPnow时如果遇到下列问题: 安装服务[apache_pn]失败.可能原因如下: 1. 服务名已存在,请卸载或使用不同的服务名. 2. 非管理员权限,不能操作 Windows NT 服务. 将 ...

  2. CentOS下,mysql服务启动失败

    mysql服务启动失败,可以使用排除法查找原因: 如果修改了my.cnf后重启mysql服务失败,大多数情况下都是配置文件有错误, 可以通过备份原来的配置文件,然后将配置文件清空,只剩下[mysqld ...

  3. 机械师实时调度示例(I) - 实时规划

    OptaPlanner创办人Geoffrey De Smet及其团队,在Red Hat 技术峰会上主题会场上,演示了一个通过OptaPlanner实现实时规划与调度的示例.Geoffrey及其团队专门 ...

  4. CentOS 7下MySQL服务启动失败的解决思路

    今天,启动MySQL服务器失败,如下所示: [root@spark01 ~]# /etc/init.d/mysqld start Starting mysqld (via systemctl): Jo ...

  5. Linux 下实时查看日志

    Linux 下实时查看日志 cat /var/log/*.log 如果日志在更新,如何实时查看 tail -f /var/log/messages 还可以使用 watch -d -n 1 cat /v ...

  6. Linux下部署Samba服务环境的操作记录

    关于Linux和Windows系统之间的文件传输,很多人选择使用FTP,相对较安全,但是有时还是会出现一些问题,比如上传文件时,文件名莫名出现乱码,文件大小改变等问题.相比较来说,使用Samba作为文 ...

  7. .netcore下的微服务、容器、运维、自动化发布

    原文:.netcore下的微服务.容器.运维.自动化发布 微服务 1.1     基本概念 1.1.1       什么是微服务? 微服务架构是SOA思想某一种具体实现.是一种将单应用程序作为一套小型 ...

  8. 大数据分析的下一代架构--IOTA架构设计实践[下]

    大数据分析的下一代架构--IOTA架构设计实践[下] 原创置顶 代立冬 发布于2018-12-31 20:59:53 阅读数 2151  收藏 展开 IOTA架构提出背景 大数据3.0时代以前,Lam ...

  9. SuperMap 9D 实时数据服务学习笔记

    SuperMap 在9月份发布了结合大数据技术的9D新产品,今天就和大家介绍下iServer9D中的实时数据服务. 1.技术框架 结合Spark的streaming流处理框架,将各种数据进行批量处理. ...

随机推荐

  1. 花了2小时写bug

    程序员的工作,写bug,修bug,改bug 写了2小时逻辑关系,没写明白 比昨天多了一个返回上一层的功能 也很简单,清除下数组内容即可 emm..明天继续深究吧 dic = { "植物&qu ...

  2. Sublime使用及配置C编译器

    一.环境配置 在安装了MinGW+Gcc的基础上做如下设置—— 新建编译系统c.sublime-build: { "cmd" : ["gcc", "$ ...

  3. python自动生成bean类

    近期在学习python,一直在和java做对比,目前没有发现有通过字段自动生成getter setter方法,故此自己写了一个类,可以通过__init__方法传入类名和字段数组,再调用内部的方法,就可 ...

  4. jdbc工具类的封装,以及表单验证数据提交后台

    在之前已经写过了jdbc的工具类,不过最近学习了新的方法,所以在这里重新写一遍,为后面的javaEE做铺垫: 首先我们要了解javaEE项目中,文件构成,新建一个javaEE项目,在项目中,有一个we ...

  5. 201771010141 周强《面向对象设计 java》第十五周实验总结

    理论部分 ◼ JAR文件◼ 应用程序首选项存储◼ Java Web Start JAR文件: 1.Java程序的打包:程序编译完成后,程序员将.class文件压缩打包为.jar文件后,GUI界面程序就 ...

  6. redis安装linux(二)

    官网地址:http://redis.io/ redis的安装 第一步:安装VMware,并且在VMware中安装centos系统(参考linux教程). 第二步:将redis的压缩包,上传到linux ...

  7. easyui获取选中行上一行的数据

    text: 'XX',            iconCls: 'icon-ok',            handler: function () {                var rowI ...

  8. react native原生模块引用本地jar包

    比如module目录结构是这样的: 然后libs中的目录是这样的: 只要在build.gradle中加入这段代码就行了 sourceSets { main { manifest.srcFile 'An ...

  9. cmd中运行maven -v提示JAVA_HOME的配置问题解决办法

    问题描述: 在安装maven之后,输入:mvn --version进行查询,结果是: The JAVA_HOME environment variable is not defined correct ...

  10. 集合或数组转成String字符串

    1.将集合转成String字符串 String s=""; for (int i = 0; i < numList.size(); i++) { if (s=="& ...