其實本文不知道算不算一個知識點分享,過程很美妙,但結果很失敗。咱們在利用Optaplanner的Real-Time planning(實時規則)功能,設計實時在線規劃服務時,遇到一個屬於Optaplanner7.8.0.Final版本的Bug。在實現實時在線規劃服務的過程當中,我作過不少嘗試。由於須要實時在線的服務,所以,須要設計多線程併發爲外界請求提供響應,須要實現消息隊列來管理併發請求的時序等問題。這些Java方面的併發處理,咱們暫時不詳述,這方面的牛的人太多了,我只是新手,站在別人的肩膀上實現的代碼而已。在本文我着重介紹一下,我在嘗試使用Optaplanner的Real-Time Planning功能時遇到的問題,最終確認問題出自Optaplanner引擎自身, 並經過JIRA向Optaplanner 團隊提交issue過程。web
先看看正常狀況下,咱們對Optaplanner的應用場景。平時咱們使用Optaplanner時,不外乎如下幾個, 構建Problem對象 + 構建Solver對象-> 啓動引擎 -> 執行規劃 -> 結束規劃 -> 得到方案-> 獲取結果方案,以下圖。服務器
這種應用模式下,引擎處於一個非實時狀態,只是一個調用 -> 獲取規劃結果的簡單交互過程。微信
可是有些對規劃具的時間性要求較高,或在時間序列上,對規劃的結果具備必定的延續性要求的狀況下,這種規劃方式是知足不了要求的。例若有些實時調度的場景;要求每一個新的solution與上一個solution須要具備延續性,不可能每次給出的solution存在過大的差別,若產生過大的差別,這些規劃出來的方案對於執行機構來講,是不可能按計劃執行的。例如車輛調度系統(見下圖),每隔一個時間段,就須要刷新一下車輛狀況和環境狀況,不可能每次刷新出來的調度方案跟前一次存在千差萬別。每一次產生的方案,它必須盡最大程度上與上一次保持相近。網絡
另一個要求是實時性,若是按傳的規劃步驟,對於實時性有要求,或響應速度較高的場景,例如:車間做業的實時調度系統,可能每隔離10分鐘就須要刷新一次計劃,此時實時規則的做用就反映出來了。以下動圖:多線程
Real-time planning, 顧名思義就是實時規劃,它與傳統的規劃步驟區別在於,它並無一個結束並退出規劃的動做,面是一旦引擎啓動,它將以守護進程的形式一直處於運行狀態,而沒有返回;當它知足規劃結束條件時(例如找到符合條件的方案,或到達規劃時限),會進入值守狀態,不佔用CPU資源。待激發事件對它發出從新啓動的指令。所以,它的步驟是: [構建Problem對象] + [構建Solver對象] -> 啓動引擎 -> 規劃 -> 經過BestSolutionChange事件輸出規則方案 -> 休眠 -> 接到重啓指令 -> 規則(重重上述步驟),以下圖:併發
原來Optaplanner還有這種神操做,那麼它的做用將進一步大增了,幻想一下你們看科幻或戰爭電影時,那裏的指揮中心必然有一個大屏幕,上面顯示了實時的戰況或各方資源的部署狀況,若是這些部署是須要經過規劃來輔助實現的話,Optaplanner是否是能夠做爲後臺超級計算機上不停運算規劃的控制中樞系統呢?不過好像想多了。沒那麼神,作一下實時做業調度仍是能夠的。下面就看看咱們的項目是如何考慮應用Real-time planning的。ide
關於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 @Override 14 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了。
這又是一篇花費很多精力的東西,儘管最終沒實現實時規劃服務。
創做不易,歡迎轉載,請標明出處。
本系列文章在公衆號不定時連載,請關注公衆號(讓APS成爲可能)及時接收,二維碼:
如需瞭解更多關於Optaplanner的應用,請發電郵致:kentbill@gmail.com
或到討論組發表你的意見:https://groups.google.com/forum/#!forum/optaplanner-cn
如有須要可添加本人微信(13631823503)或QQ(12977379)實時溝通,但因本人平常工做繁忙,經過微信,QQ等工具可能沒法深刻溝通,較複雜的問題,建議以郵件或討論組方式提出。(討論組屬於google郵件列表,國內網絡可能較難訪問,需自行解決)