背景介紹
集羣備份:軟件爲了高可靠性,防止所在服務器down掉後業務中斷,使用了集羣,在多臺PC上跑同樣的代碼。但同一時間,只有一臺pc真正的處理業務,即Leader;其他的PC只是作爲熱備,當leader故障後纔會選舉中其中之一,處理業務。
模塊依賴:爲支持熱插拔,使用OSGI框架,整個軟件分爲多個模塊,可在運行軟件時,不重啓整個軟件,只卸載再加載不同模塊,即實現模塊的升級。這樣實現勢必會要求模塊之間是松耦合的,模塊之間的依賴需小心處理。
業務抽象:不能涉及到具體業務,所以本文記錄的內容會做一定抽象。當模塊加載完成(將自身註冊爲服務)後,會得到集羣事件,本服務器是否爲Leader,是才處理業務。出問題的模塊乾的事是,起個線程D,定時啓動一個固定的監控任務,並將任務的結果保存起來;同時主線程M等待隨時由用戶發起的實時監控任務。這些任務都應該有唯一的id標識,id自增是放在一個公共對象中,並統一使用一個加鎖的方法。實現時,公共對象簡化如圖(Java語言實現):
id是公共對象的一個動態成員變量,取任務id的方法如下:
public synchronized long getId(){
this.id++;
return this.id;
}
問題描述
發現保存的任務id順序不對,並不是遞增的。而是形如456,467,473,481,35,37,45,512,525,537,544,52,56,69,553,560,73,75……
能確定的是保存id的方式是直接將新的任務id加入數組的最後,並沒有使用會將輸入重排的數據結構。
問題定位過程
根據現象看id是沿兩條線456~560,35~75遞增的。這一點是拉出了所有的監控任務id,發現任務id兩條線的id有時會有重複,才最終確定的。在此之前,首先懷疑的是定時任務的線程D獲取到id後,在執行或者保存結果的時候,因爲什麼原因延遲,導致保存結果的時機推後了。
發現兩條線有時重複後,就由此斷定是出現線程安全問題了。
業務設計時,處理程序定爲唯一對象,即單例模式。自接手維護以來,這一塊的代碼就一直是單實例運行,所以最初想當然的認爲框架保證了對象唯一,即使用了單例模式。但因爲對OSGI本身不算熟悉,沒有細細的去找單例實現的地方。最終確定問題,是在處理邏輯那斷電,將對象toString打印,看到@後的數字不一樣(這裏的值時內存地址的hash值,不一樣則肯定是兩個對象),才確定對象產生了兩個。
最終原因是OSGI啓動本模塊(active階段)時,本模塊依賴的service還沒起,創建了一個對象,使註冊依賴服務時異常。實際上依賴的模塊只是在後續使用時需要,啓動時可以不關心,即屬於弱依賴關係。
解決
最終將依賴的服務的註冊時機延後到使用前,並處理此時仍沒有依賴服務的情況。實現兩模塊啓動階段的松耦合。
本次的教訓就是一句話:Talking is cheap,show me the code…