遺留系統的技術棧遷移

發表於2013-04-24

來源:張逸的部落格

什麼是遺留系統(Legacy System)?根據維基百科的定義,遺留系統是一種舊的方法、舊的技術、舊的計算機系統或應用程式[1]。這一定義事實上並沒有很好地揭露遺留系統的本質。我認為,遺留系統首先是一個還在執行和使用,但已步入軟體生命週期衰老期的軟體系統。它符合所謂的“奶牛規則”:奶牛逐漸衰老,最終無奶可擠;然而與此同時,飼養成本卻在上升。這意味著遺留系統會逐漸隨著時間的推移,不斷地增加維護成本。

維護一個軟體系統,就需要了解該軟體系統的知識。若知識缺失,就意味著這會給維護人員帶來極大的障礙和困難。從這個角度講,所謂“遺留系統”,就是缺少了一部分重要知識,使得維護人員“知其然而不知其所以然”的軟體系統。

若要讓遺留系統煥發青春,最徹底的做法自然是推倒重來,但這樣付出的代價太高;而且,即使對系統重新設計和開發,仍然免不了會重蹈遺留系統的覆轍。或者,可以對遺留系統進行重構,在不修改系統功能的情況下改善系統設計。只是這種重構常常是對系統進行重大擴充套件或修改的前奏,如無絕對必要,並不推薦這種償還“技術債務(Technical Debt)”的方式。重構應與開發同時進行,而不應將其作為債務推遲到最後,以至於支付高昂的利息。最後,還有一種方式,則是對遺留系統進行技術棧遷移。

一. 決策技術棧遷移的因素

那麼,為何要進行技術棧遷移呢?是否是原有技術無法滿足新的業務需求?對於遺留系統而言,這種情況總是存在,即需要擴充套件舊有系統的功能來滿足新的業務。然而,這一原因並不足以支援做出技術棧遷移的決策。因為,從技術實現的角度來看,無論採取何種技術,都可以實現各種業務功能,無非是付出的成本不同而已 。基本上,這種成本一定會低於技術棧遷移的成本。此外,當今的軟體開發,常常會將一個軟體系統看做是完整的生態系統,在這個生態系統圈中,完全允許有多種技術平臺(包括多種語言,甚至多種資料庫正規化)存在,只要我們能夠合理地劃定各個功能(或服務)的邊界。

牽涉到架構中的任何一個重大決策,都需要綜合考量和權衡,只有充分地識別了風險,才能制訂有效的設計決策。個人認為,只有在如下幾種情形出現時,才值得進行技術棧遷移。

· 原有技術不能保證新的質量需求

在一個系統的完整生命週期內,系統從誕生到發展,衰老和死亡,與人一樣,是不可規避的過程。對遺留系統進行技術棧遷移,無非是希望通過新的技術給舊有系統注入活力,就像器官移植一般,對腐朽的部分進行切除與替換。系統之所以會衰老,會腐朽,原因還在於需求的變化,從而導致系統結構變得龐大而混亂。我們在進行技術決策時,常常是根據當下的需求以及目前現有的技術,結合團隊技術能力做出的最符合當時場景的合理決策。因而,技術棧遷移的原因常常是是因為“此一時彼一時”。在當時場景下做出的明智決策,隨著時間的推移,會顯得不合時宜。這一點在質量需求的滿足上,體現得尤為明顯。例如,系統對可伸縮性、效能、安全的要求,都可能因為新的質量需求的提出發生變化。而這些質量屬性往往靠舊有技術無法解決。RackSpace對日誌處理的案例就屬於這一場景[2] 。RackSpace的架構對日誌的支援,先後經歷了三個大版本的演化,從檔案伺服器到中心資料庫,再到MapReduce,每次技術棧的遷移都是質量屬性的驅動,不得不為之。

· 出於戰略的考慮

這常常是因為企業架構的因素。對於一個企業而言,應該將其IT系統看作是一個整體的生態系統。對於一個正在成長中的企業而言,必然會隨著整個企業組織結構、業務體系的變化而影響到IT系統。一般而言,企業IT系統的架構會存在兩種情況。第一種情況是從無到有,根據企業架構師與業務架構師的設計,嚴格按照設計藍圖來規劃所有的IT系統。第二種情況則可能是多種不同的系統並存(可能是因為企業採用了併購等方式兼併其他公司業務,也可能是因為不同的業務需要,購買了不同的軟體系統)。第一種情況看似美好,但仍有可能發生規劃藍圖不能滿足需求的可能。第二種情況則處於龍蛇混雜的局面,最後可能導致所謂的“煙囪系統(Stovepipe System)[3]”,需要花大力氣對各種系統進行整合。

無論是哪一種情況,一旦做出技術棧遷移的決定,都必然是企業戰略上的考慮。當然這種戰略指的是IT戰略,也可能是企業的整體戰略對IT系統產生影響。

我們的一個客戶是一家大型的金融企業,提供了多種品牌的保險與銀行業務。企業的戰略目標是在體現品牌價值的同時,整體展現企業的平臺作用。這對於IT系統而言,就意味著需要對各種業務系統進行整合、遷移。整個系統的主要核心是對客戶資料的管理,這些資料的管理會影響到整個企業的服務質量、市場推廣與產品維護。由於該企業在銀行業與保險業的發展壯大,是通過不斷的合併與兼併來促進自身的發展。因而在其IT系統中,事實上存在多種不同的系統。客戶資訊散落在不同系統的資料庫中。客戶資料的整合,不僅有利於對這些資訊的管理,保證資料的一致性,還在於從市場營銷角度考慮,可以通過一致的客戶資訊對客戶的情況做出全面瞭解,制定更好的推廣策略。

· 原有的技術提供者不再提供支援

這種情形最是無奈,卻時有發生。一種情況是使用的技術(平臺、框架)不再被供應商維護,這一點體現在開源專案上更為明顯。另一種情況則是所選的技術平臺進行了升級,卻沒有很好地提供向前相容,使得系統難以隨之而升級。在架構設計中,這種繫結具體平臺與技術的做法,實際上是反模式的一種,即“供應商鎖定(Vendor Lock-In)[4]”。

· 使用舊有技術的成本太高

IT技術並非一定是新技術成本高於舊技術,事實上,隨著技術的創新和發展,技術越新,成本越能得到更好的控制。當新舊技術的成本之差,遠遠高於技術棧遷移的成本,就值得做出遷移的決策了。例如,我們的一個專案需要處理的遺留系統,使用了某軟體公司的產品,該產品必須執行在大型伺服器上。該產品主要提供客戶資訊的處理。這是一個存在超過十年以上的產品,之後加入的子系統並未再使用該產品。如今,該產品所支援的客戶數量並不多,而每年的產品許可費用以及大型伺服器的維護成本都非常高。最後,我們對該產品提供的功能進行了遷移,以漸進地方式逐漸替換了該產品,降低了系統成本。

二. 引入風險驅動模型

George Fairbanks提出的風險驅動模型(Risk-Driven Model)非常適合遺留系統的技術棧遷移。所謂“風險驅動模型”,就是通過識別風險,對風險排定優先順序;然後根據風險選定相關技術,再對風險是否得到緩解進行評估的一種架構方法[5] 。在對遺留系統進行技術棧遷移時,如果未能事先對遷移過程的風險進行有效識別,就可能為系統引入新的問題,降低系統質量,或者導致遷移的成本過高。

根據我的經驗,在對遺留系統進行技術棧遷移時,可以識別的主要風險包括:

  • 遺留系統本身存在的質量問題,例如緊耦合、缺乏足夠的測試、系統可維護性差;
  • 缺乏足夠的知識來幫助我們理解整個遺留系統;
  • 成本、時間與人力的風險;
  • 對遷移的新技術缺乏充分認識;
  • 遷移能力的不足

三. 選擇緩解風險的技術

一旦識別出遷移過程中可能存在的風險,我們就可以有的放矢地選擇相關技術,制訂降低風險的解決方案。

· 尋找丟失的知識

只有體驗過去,才能謀劃未來。如果缺乏對遺留系統的足夠認識,這種技術棧的遷移就很難取得成功。通常來講,一個軟體系統的知識,主要體現在如下三個方面,如下圖所示:

遺留系統的技術棧遷移

 

在這三個方面中,團隊成員擁有的知識無疑是最值得寄予厚望的。在遷移過程中,若有了解該系統的團隊成員參與,無疑可以做到事半功倍。可惜,這部分知識又是最為脆弱的,它就好似儲存在記憶體中的資料一般,一旦斷電就會全盤丟失。遺留系統的問題恰在於此,由於系統過於陳舊,而人員的流動總是比較頻繁,在對系統進行遷移時,可能許多當年參與系統開發的成員,已經很難找到。

 

缺乏團隊成員在知識方面的傳承,就只能寄希望於文件與程式碼。文件的問題有目共睹,無論採用多麼嚴謹的文件管理辦法,文件與真實的實現總是存在偏差。正如“盡信書不如無書”,文件可以提供參考價值,但絕對不能完全依賴於文件。毫無疑問,程式碼是最為真實的知識。它不會說謊,但卻過於沉迷於細節,要通過程式碼來了解遺留系統的知識,一方面耗時耗力,另一方面也難免會產生“只見樹木不見森林”之嘆。

 

引入自說明的可執行文件,可以有效地將文件與程式碼結合起來。通過運用業務語言編寫功能場景來體現業務需求,完成文件的撰寫;同時,它又是可以執行的程式碼,通過直接呼叫程式碼實現,可以完全真實地驗證功能是否準確。目前,有許多框架和工具可以支援這種規格文件,例如Java平臺下的jBehave,Ruby語言編寫的Cucumber,支援HTML格式的Concordion,以及ThoughtWorks的產品Twist[6]。

在我們的一個專案中,需要完成系統從WebLogic到JBoss的技術棧遷移。該系統是一個長達十年以上時間的遺留系統。雖然有比較完整的文件說明,但許多具體的業務對於我們而言,還是像一個黑盒,不知道具體的互動行為。此時,我們和客戶一起為其建立了一個專門的專案,通過運用jBehave為該系統的業務行為編寫可以執行的Story。在編寫Story時,我們參考了系統的文件,並根據文件描述的功能建立場景,確定輸入和輸出,判斷系統的行為是否與文件描述一致。事實上,我們在編寫Story的過程中,確曾發現系統的真實行為與文件描述不一致的地方。這時,我們會判斷這種不一致究竟是缺陷,還是期待的真實行為。在編寫Story的過程中,我們尋找回了已經丟失的知識,並進一步熟悉了系統的結構,瞭解到系統元件的功能以及元件之間的關係。通過這些不斷完善的Story,我們逐漸建立起了一個完全反應了真實實現的可執行文件庫,它甚至可以取代原來的文件,成為系統的重要知識。

· 及時驗證,快速反饋

在對系統進行技術棧遷移時,我們常常會擔心修改會破壞原有的功能。尤其是對於大多數遺留系統,普遍存在測試不足,程式碼緊耦合,可維護性差的特點。雖然遺留系統會因為這些缺點而受人詬病,但不可否認的是,這些遺留系統畢竟經歷了長時間的考驗,在功能的正確性上已經得到了充分的驗證。在遷移到新的技術時,如果不慎破壞了原有功能,引入了新的缺陷,就可能得不償失了。

為了避免這種情況發生,我們就需要為其建立充分的測試,並通過建立持續整合(Continuous Integration)環境,提供快速反饋的通道。一旦發現新的修改破壞了系統功能,就需要馬上修復或者撤銷之前的提交。

問題是我們該如何建立測試保護網?為遺留系統建立測試是一件非常痛苦的事情,為了減小工作量,我們首先應該根據技術遷移的目標,縮小和鎖定系統的範圍。例如,倘若我們要將系統從IBMMQ遷移到JBossMQ,那麼就只需要驗證那些與訊息佇列通訊的元件。若要將報表遷移到JasperReport,就應該只檢測整個系統的報表元件。另一方面,我們應儘量從粗粒度的測試開始入手。一個好訊息是,在之前為了尋找失去的知識時建立的可執行文件,事實上可以看作是一種驗收測試。它不僅提供了自說明的文件,同時還建立了覆蓋率客觀的測試保護網。這種驗收測試是針對業務行為編寫的完整功能場景,更接近業務需求。它的抽象層次相對較高,並不會涉及太多程式設計細節。即使實現模組(包括類)是緊耦合的,沒有明顯的單元邊界,我們仍然可以為其編寫測試。這就可以省去對類與模組進行解耦這一難度頗高的工作。

通常,我們會將這些測試作為持續整合的一個單獨pipeline。每次對原有系統的修改,都要觸發該pipeline的執行,以期獲得及時的反饋。這樣,就可以為原有系統建立一個覆蓋範圍廣泛的測試保護網,使得我們可以有信心地對系統進行技術棧遷移。

針對一些核心場景,我們還可以為遺留系統編寫整合測試。這種粗粒度的測試不需要對原有程式碼進行太多的調整或重構,唯一需要付出的努力是對整合測試環境的搭建。

對於遺留系統的整合測試,最好能夠支援本地構建。因為若能在本地開發環境執行整合測試,就可以通過在本地執行構建指令碼,快速地獲得反饋,避免一些整合錯誤流入到原始碼伺服器中,導致持續整合Pipeline頻繁出現錯誤。這種快速失敗的方式,可以更好地驗證錯誤,降低整合風險。在搭建本地整合環境時,可以選擇一些輕量級框架或容器,提高部署效能。例如我們可以在本地執行Jetty這種輕量級的Web伺服器,使用HSQL記憶體資料庫來準備資料。對於某些整合極為困難的情況,也可以適當考慮建立Stub。例如對外部服務的依賴,可以建立一個Stub的Web Service。這種方式雖然沒有真實地體現整合功能,但它卻可以快速地驗證系統內部的功能。

倘若因為一些外部約束,我們無法做到完全的本地構建,也應該提供足夠的整合環境,採取混合的方式執行構建指令碼。例如可以將正在進行遷移的系統執行在本地環境上,而將該系統需要訪問的中介軟體或者資料庫放到其他的整合環境下。我們還可以利用構建指令碼如Gradle,建立多種部署環境,例如Dev、Local、Stub、Intg等,使得開發人員或測試人員可以根據不同情況執行不同環境的構建指令碼。

· 做好充分的技術預研

所謂“技術棧遷移”,必然是指從一種技術遷移到另一種技術。在充分了解系統當前存在的問題後,還需要深思熟慮,選擇合理的目標技術。通常,我們會識別出待遷移模組(或系統)希望達到的質量屬性,然後就此功能給出候選技術,建立一個用於權衡的矩陣。接著,再對這些待選技術進行技術預研(Spike),預研的結果將作為最終判斷的依據。這種決策是有理有據的,可以有效地規避遷移中因為引入新技術帶來的風險。下圖是我們在一個專案中對文字搜尋進行的技術預研結果矩陣。

遺留系統的技術棧遷移

 

因為是技術棧遷移,必然要求目標技術一定要優於現有技術,否則就沒有遷移的必要了。通過技術預研,既可以提供可以量化的資料,保證這種遷移是值得的;同時也相當於預先開始對目標技術展開學習和了解,及早發現技術難點和遷移的痛點。

在我曾經參與的一個專案中,我們針對報告生成器模組編寫了自己的一個支援併發處理的Batch Job。但隨著系統使用者數量的逐步增加,在生成報告的高峰期,併發請求數超過了之前架構設計預見的峰值,且每個報告生成所耗費的時間較長。於是,我們計劃引入訊息佇列技術來替換現有的Batch Job。我們對一些候選技術進行了前期預研,這其中包括微軟的MSMQ、Apache ActiveMQ以及RabbitMQ,針對併發處理、可維護性、成本、部署、安全、分散式處理以及災備等多方面進行了綜合考慮,如下表所示:

遺留系統的技術棧遷移

技術選型從來都不是以單方面的高質量作為評價標準,即使某項技術在多個評判維度上都得到了最高的分數,也未必就是最佳選擇。我們必須結合當前專案的具體場景,實事求是地進行判斷,以期獲得一個恰如其分的遷移方案。

· 新舊共存,小步前行

技術棧遷移的某些特徵與架構的演化不謀而合,我們絕對不能奢求獲得一個一蹴而就的完美方案,更不能盼望整個遷移過程能夠一步到位。尤其針對那些因為戰略調整而驅動的技術棧遷移,可能牽涉到架構風格或整個基礎設施的修改或調整,單就遷移這一項工作而言,就可能是一個浩大的工程。這時,我們必須要允許新舊共存,通過小步前行的方式逐步以新技術替換舊技術。我們必須保證前進的每一小步,都不會破壞系統的整體功能。這種新舊共存的局面,可能導致在一段時間會出現架構風格或解決方案的不一致,但只要做好整體規劃,最終仍能在一致性方面獲得完美的答案。

在我們工作的一個專案中,需要將一個獨立的系統徹底移除,並將該系統原有的功能整合到另一個系統。需要移除的目標系統目前以Web Service方式提供服務。我們選擇的解決方案是漸進地移除該系統。假設待移除的目標系統為Target,要整合的系統為Integration,我們採用瞭如下的遷移步驟:

  1. 修改Integration,為其建立與Target提供的Web Service一致的服務介面;
  2. 讓新建立的服務介面的實現呼叫Target提供的Web Service;
  3. 修改客戶端對Target服務的呼叫,改為指向新增的Integration服務介面;
  4. 如果執行一切正常,再將Target中的實現遷移到Integration中;
  5. 在遷移過程中,提供Toggle開關,可以隨時通過改變Toggle的值,選擇使用新或舊的呼叫方式;
  6. 再次確定採用新的呼叫方式是否正常,如果正常,徹底去掉原有的實現,移除Target系統。

新舊共存並非一種妥協,而是遷移過程中必須存在的中間狀態。Jez Humble介紹了ThoughtWorks產品GO的幾次技術棧遷移[7],包括從iBatis遷移到Hibernate,從Velocity和JsTemplate轉向JRuby on Rails的案例。文章提出了一種稱為Branch By Abstraction(抽象分支)的遷移方法,執行步驟如下圖所示:

遺留系統的技術棧遷移

 

圖中的抽象層將客戶端(Consumer)與被替換的實現進行了解耦,使得這種替換可以透明地進行。在對抽象層的實現進行替換時,可以規定替換紀律,例如對於新增功能,必須運用新技術提供實現;還可以通過持續整合的驗證門自動驗證,例如設定舊有技術在系統中的閾值,每次提交都不允許舊有技術的程式碼量超過這個閾值。整個遷移過程要保證這個閾值是不斷減少,絕不能增加。

· 理清思路,持續改進

要完成遺留系統的技術棧遷移,不可避免地需要對程式碼實現進行修改或重構。這或許是遷移難度最大的一部分內容。我的經驗是針對遺留系統進行處理時,不要從一開始就埋首於浩如煙海的程式碼段中,太多的細節可能會讓你迷失其中。若系統是可以執行的,可以首先執行該系統,通過實際操作了解系統的各個功能點、業務流程。這樣的直觀感受可以最快地幫助你瞭解該系統:它能夠做什麼?它能達成什麼目標?它的範圍是什麼?它存在什麼問題?

接下來,我們需要從系統架構出發,瞭解遺留系統的邏輯結構和物理分佈,最好能描繪出遺留系統的輪廓圖,這可以幫助你從技術的巨集觀角度剖析遺留系統的結構與組成;然後再結合你對該系統業務的理解,快速地掌握遺留系統。在閱讀原始碼時,最好能夠從主程式入口開始,找到一些主要的模組,瞭解其大體的設計方式與編碼習慣。由於之前對系統架構已有了解,閱讀程式碼時,不應在一開始就去理解程式碼實現的細節,而應結合架構文件,比對程式碼實現是否與文件的描述一致,並充分利用自己的技術與經驗,找到閱讀程式碼的終南捷徑。例如,如果我們知道該系統採用了MVC架構,就可以很容易地根據Url找到對應的Controller物件,並在該物件中尋找業務功能實現的脈絡。又例如我們知道系統引入了WCF來支援分散式處理,而我們又非常熟悉WCF,就可以基本忽略系統基礎設施的部分,直接瞭解系統的業務實現。如果系統基於EJB 2.0實現,則完全可以根據EJB提供的Bean的結構,快速地定位到對應的服務介面與實現。這是因為許多框架都規定了一些約束或規範,從這些約束與規範入手,可以做到事半功倍。

在嘗試理解程式碼的過程中,可以通過手工繪製或利用IDE自動生成包圖、時序圖等可視性強的UML圖,幫助我們理解程式碼結構。Michael Feathers提出可以為遺留程式碼繪製影響結構圖與特徵草圖[8],從而幫助我們去梳理程式中各個物件之間的關係,尤其是幫助我們識別依賴,進而利用接縫型別、隱藏依賴等手法去解除依賴。

瞭解了程式碼,還需要對程式碼進行修改。多數情況下,我們需要首先通過重構來改善程式碼質量。注意,技術棧的遷移並非重構,但重構可以作為遷移工具箱中一件最為重要的工具。例如,我們可以通過Extract Interface,並結合Use Interface Where Possible手法,對一些具體類進行介面提取,並改變對原來具體類物件的依賴。重構時,必須採取“分而治之,小步前進”的策略。可以首先選擇實現較為容易,或者獨立性較好的模組進行重構。將遺留系統逐步提取為一些可重用的模組與類。其中,對於原有類或模組的呼叫方,由於在重構時可能會更改介面,因而可以考慮引入Facade模式或Adapter模式,通過引入間接層對介面進行包裝或適配,逐漸替換系統,最後演化為一個結構合理的良好系統。需要注意的是,在重構時一定要時刻謹記,我們之所以進行重構,其目的是為了更好地遷移遺留系統的技術棧,而非為了重構而重構,從而偏離我們之前確定的目標。故而,重構與遷移應該是兩頂不同的帽子,不能同時進行。

四. 結束語

遺留系統的技術棧遷移可能是一個漫長艱苦的過程,它的難度甚至要高於新開發一個系統,這是因為我們常常會掙扎在新舊系統之間,並在不斷的妥協、權衡中緩步前行。

它是一個複雜工程,需要參與者瞭解遷移前後的技術棧知識,掌握或者至少善於分析與理解遺留系統。我們需要審慎地做出技術決策,通過識別遷移過程的風險來驅動整個遷移過程。在決定遷移選擇的技術時,要根據這些識別出來的風險對這些候選技術做充分的預研,獲得可供參考的度量矩陣。我們還可以引入BDD框架來編寫可執行的功能場景,以此來尋找失去的知識,同時兼得驗收測試的保護網。

我們可以通過引入持續整合,建立快速反饋環,以避免遷移時做出的改動對原有系統造成破壞。同時,還必須具備技術遷移的能力。我們可以考慮引入一些最佳實踐或遷移方法,例如抽象分支、影響結構圖、特徵草圖,運用設計模式和重構手法來改善遺留程式碼,以利於技術的遷移。當然,團隊協作、架構設計、組織管理、進度跟蹤等一系列技術與管理實踐同樣重要,只是這些實踐並非技術棧遷移所必須的,而是所有開發過程都必須經歷的過程,因而本文不再贅述這些內容。

參考文獻:

[1]:http://en.wikipedia.org/wiki/Legacy_system,原文為:“A legacy system is an old method, technology, computer system, or application program.”

[2]:文章How Rackspace Now Uses MapReduce And Hadoop To Query Terabytes Of Data

[3]:煙囪系統,一種反模式,http://sourcemaking.com/antipatterns/stovepipe-system。

[4]:供應商鎖定,一種反模式,參見http://sourcemaking.com/antipatterns/vendor-lock-in。

[5]:Gorge Fairbanks:Just Enough Software Architecture,參見第3章Risk Driven Model

[6]:以上所述皆為BDD框架或整體工具。

[7]:Jez Humble:Make Large Scale Changes Incrementally with Branch By Abstraction

[8]:Michael Feathers:Working Effectively with Legacy Code

相關文章