遺留系統的技術棧遷移

>>>  技術話題—商業文明的嶄新時代  >>> 簡體     傳統

  什么是遺留系統(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)環境,提供快速反饋的通道。一旦發現新的修改破壞了系統功能,就需要馬上修復或者撤銷之前的提交。

  問題是我們該如何建立測試保護網?為遺留系統建立測試是一件非常痛苦的事情,為了減小工作量,我們首先應該根據技術遷移的目標,縮小和鎖定系統的范圍。例如,倘若我們要將系統從IBM MQ遷移到JBoss MQ,那么就只需要驗證那些與消息隊列通信的組件。若要將報表遷移到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


張逸 2013-06-24 09:06:04

[新一篇] 外語學習的真實方法及誤區

[舊一篇] 用LINQPad精通LINQ
回頂部
寫評論


評論集


暫無評論。

稱謂:

内容:

驗證:


返回列表