設計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     @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郵件列表,國內網絡可能較難訪問,需自行解決)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM