09 July 2014

最近我們成功地將一個大型專案升級到 Java 8,面對這麼重大的改變,事前準備和測試是免不了的。不過很意外的,升級過程相當的順利,而且需要修改的地方非常的少,算是相當的幸運。

版本大升級

原來的專案主要是以 Spring 為主幹,再加上 Akka 組成:

  • Java 7 (1.7.0_25)
  • SpringFramework 3.0
  • Akka 1.3 (scala 2.9)
  • Netty 3.6
  • Tomcat 7
  • drivers for Cassandra, Redis, Elasticsearch...etc

程式碼總行數達 15 萬行,依賴的 jar 個數為 108 項。我想雖然不能算超大型的專案,但絕對不算小。這麼多的程式碼要遷徙,我們一開始就訂了目標:以改最少的方式升級。先順利轉移到 Java 8 後,後續再進行程式碼的重構。因此第一階段我們只升了兩大項:

  • Java 8 (1.8.0_05)
  • SpringFramework 4.0.5

原本只計畫升級 Java 8 而已,但是 Spring 3 無法在 Java 8 的環境下運作,因此只好跟著升上去。在測試過程中,我們發現 Java 8 完全不影響原來程式的運作,上千個 Unit Test 全部 pass。出問題的都是 Spring 升級造成的。而 Spring 的問題怎麼解決?改兩三個設定檔就搞定了 (大部份是 spring-mvc)。是的,只改設定檔就夠了,沒改到程式碼。

我們一開始最擔心的是 Akka,因為我們用的是舊版的,而它搭配的也是舊版的 Scala lib。Scala 是另一個 JVM 語言,我不確定它內部是否對 byte code 做了很多的修改,是否依賴 Java 7 的 byte code。Scala 這個 異物 卡在那裡一直是我們專案的隱憂。如果這關過不了,那升級大概就無望了,因為短期內不可能花幾週的時間去重寫 Akka 的程式 (Akka 2.x 和 1.x 天差地遠)。不過,我們相當的幸運,改用 Java 8 後測試完全沒問題。Scala 混進 Java 專案裡並非全然是破壞性的。

無痛上線

程式碼升級好了,接下來就是部署的問題。該專案的服務類型是分散式系統,同時間有好幾台伺服器互連,連線的傳輸格式大部份是純 Java serialization。如果 Java serialization 的格式在 Java7, Java8 間有任何不相容的變更,那麼我們在 rolling upgrade 的過程中, 勢必會出錯,造成只能關掉所有伺服器,一口氣更新。

一口氣升會有 down time ,這是我們極力避免的。一次就要全部升也代表我們無法做先行評估 -- 即更新部份的伺服器,看看新的 Java8 在正常的負載下有任何異常。很幸運的,在 stage 的階段,我們測試了 Java7/8 混合運作的環境,結果是傳輸完全相容。隨後就少量部署到正式機試運,試運的結果沒有出大問題。這兩階段測試過關後,我們就正式上線運作了,上線過程一台台正常 rolling,服務毫無間斷。

Java 8 Performance

以目前正式機收集到的資料,我們只有觀察到 Heap 的用量有些微的下降,大約5% (我們的 Heap Max 開 10GB),而 CPU 的使用量與之前相同。OS 的 interrupt 與 context switch 也無顯注的改變。沒有任何的大改進有點小失望,不過有可能是我們仍然沿用 Concurrent Mark Sweep GC 的原因。如果切換到新的 G1GC,相信有機會減少 CPU 的用量。但這是下一階段的課題。

Drop in replacement

這篇文章如果只寫升級那真是沒什麼料可寫,因為我們的升級太過順利。即使我們升級到 Java 8,這 Java 歷史上最大的一次改變。即使我們從 Spring 3.0 (2009) 升到 Spring 4.0 (2014),橫跨 5 年三個版本一次跳升上去。

我們只改了幾個設定檔

Java 的哲學一直以來就是推崇向前相容,相容性永遠掛在第一位。除了 Java 本身致力奉行這項原則之外,Spring Framework 也是力行者。這兩者是絕佳拍檔,是開發者的好選擇。當然也不是所有的 Java 的環境都這麼好,例如萬惡的 JEE。如果你選擇了 JEE5 啊,配上某版的 websphere 之類的,那大概就沒這麼好康了,你大概永遠只能鎖在某個 Java 版本而無法脫困。因此就算你選對了 Java,但選錯 container 、選錯技術也是白搭。我很慶幸我這次選對了 (之前選錯過,結果很慘...)

向前相容的重要性

技術債 (Technical debt) 是每個專案都要背的負債。如果你的專案不能隨著時代前進,那債務增加的速度將隨時間等比上升。做為一個開發者,你不能整天只想著追新潮的玩意兒,你不能看到 nodejs 潮了,你就想跟,不能見到現在所有人都往 golang 跑,你也跟著想鑽進去。你必須要衡量現在的專案在三、五年後,會需要多高的成本來維護。

一般來說,如果是 front end 的話,通常不必考慮該技術是否能不能順利升級。因為 UI 對流行度很敏感,而且從過去的記錄來看,一旦改變就只有重寫一途。例如 web 1.0 -> web 2.0 -> mobile 這十年來的三大革命,這種革命性的變化是一定需要重寫的。

但是 server side 就不同了。server 的程式會活很久,你也許有機會重寫某一部份的程式,但全部打掉重練的成本實在太高,基本上大型專案是不太可能的。server 程式最好的情況就是能夠順利升級,就像本篇中升 Java8 的例子。能夠升級,舊程式就能享有新功能,而且未來有機會一點一點的重構,汱舊換新。這應當你選擇 server 平台的第一項考量。

Java 在第五版時大改進,加了 generic,但被人垢病 erasure 超難用。但是如果那時做成破壞型的改進,那我看現在大部份的人都還在用 Java 1.4 。看看 python3 的慘狀,這是軟體業裡最可悲最沉痛的教訓,python3 推出 5, 6 年了,沒人願意切換,明明 python3 就超棒的 (unicode 啊!)。這證明破壞性演進在軟體業完全行不通。

我不是說你只能選擇 Java,盡管這是個好選擇。而是你必須觀察那些候選的平台,是否重視相容性。舉些例子來說說吧:過去 Scala 曾搞砸了二、三次,相容性通通沒了,造成使用者很多問題。我想也許他們不會再重蹈覆轍了,但過去的記錄讓我對他們實在沒啥信心。而 golang 的話,開發團隊是很重視相容性的,為什麼?因為每次 golang 版本升級他們都會備有自動 refactor 的工具幫你無痛升級,這做得比 Java 還過火啊!所以 golang 也是個可以信賴的平台。不過跟 Java 一樣,也不是選了 golang 就高忱無憂。他們的生態系還不是很成熟,選擇 3rd party lib/framework 得再三評估。

成熟技術的特徵

我們怎麼知道哪些 framework / library 未來會好好的做好相容性,讓使用者能升級?我前幾篇文章提到你可以看看該專案的貢獻人數,來判斷這專案是不是健康的。但健康的專案不代表他們會做好相容性。經過這麼多年的教訓,我的心法是:

文件寫的越好的,未來就越可靠

文件如果寫得很好,寫得他媽的完整,尤其是 1.0 版一出來就把文件寫得滿滿的專案。通常這種團隊也會很注重相容性,你可以壓寶在他們身上。很可惜我沒有辦法量化這兩者的關係,但我累積下來的經驗就是如此 (spring, gradle, mockito, jackson, golang ...etc)。也許這兩者有心理學的因素在吧,願意寫文件的人通常比較負責任這樣?

小結

寫這篇寫到一半有點離題了,一開始說說我們無痛升級到 Java 8 的經驗,後半則扯到相容性高的好處與它們的特徵。雖然我個人有些經驗法則,但能夠選到相容性好的平台其實還是有點運氣運氣的,畢竟沒人可以未卜先知。


回響

可以用 Tag <I>、<B>,程式碼請用 <PRE>