2023 年 6 月,Chris Richardson 拜訪羅馬萬神殿(Pantheon,約建於西元 126 年):一座近 1900 年仍持續使用、保存最完整的羅馬古蹟之一,如今同時是教堂與觀光景點。軟體應用卻不像實體建築——即使需求永遠不變,也必須持續更新技術棧,彷彿昨天的建材今天突然停止運作。

可演化性(Evolvability) 反映「升級應用技術棧的容易程度」。它在某種意義上是可修改性的特例,但有獨特之處:

  • 技術棧升級往往是橫切性(cross-cutting)的變更,影響範圍可能極大。
  • 例如升級一個被多個子領域使用的函式庫,會牽動所有子領域的團隊。
  • 還有計畫外的升級,例如新發現的安全漏洞需要立即升級。
  • 升級的驅動力通常來自客戶需求以外的因素(供應商支援、市場演進、安全)。

認識技術棧#

應用的技術棧(technology stack)是用來建構與執行應用的所有技術的集合,主要分為:

  • 應用技術:函式庫、框架、程式語言執行時(runtime)。
  • 基礎設施服務:資料庫、message broker 等。
  • 開發工具:測試框架(JUnit)、建置工具(Gradle)、CI/CD 工作流(GitHub Actions)等。
  • 部署環境:雲端基礎設施(AWS/Azure)、容器技術(Docker、Kubernetes)、雲端服務(包括基礎設施服務與 logging/monitoring 等可觀測性工具)。

幾乎所有元素都有版本(例如以 semantic versioning 表示),所以技術棧本質上是一組「技術 + 版本」;同一種技術可能同時存在多個版本。

Figure 5.8: An application's technology stack — libraries, frameworks, languages, infrastructure, deployment environments

元件 vs 技術棧#

複雜應用通常擁有多個技術棧:

  • 每個元件可有自己的私有技術棧:函式庫、框架、語言執行時、私有的基礎設施服務、特定的開發工具。
  • 也可有跨元件共用的技術棧:例如共用的 message broker、開發工具、部署基礎設施等。

範例:

  • Customer Service 用 Java 22 + Spring Boot 3.5.3 + Postgres(新標準)。
  • Order Service 用 Java 17 + Spring Boot 3.1.2 + MySQL,額外用 Redis 做快取。
  • 兩者皆共用 Apache Kafka。

微服務的關鍵效益之一,正是讓每個元件可使用不同(版本的)技術

技術之間的三種關係#

  • Requires:某技術(版本)需要另一個技術(版本)。例如 Spring Boot 3.x 需要 Java 17。
  • Excludes:選擇某技術可能就排除了其替代品。例如同一元件只能有單一語言執行時、單一應用框架;classpath 也只能容納單一版本的函式庫。
  • Provides:某技術提供另一技術的特定版本。例如雲端供應商以 IaaS/PaaS 形式提供基礎服務,但可能只支援特定版本(甚至不是最新),日後也可能下架舊版本。

Figure 5.9: Three types of relationships between technologies — requires, excludes, provides

因為這些關係,組裝技術棧像在拼拼圖:Java 元件常有上百個傳遞依賴,你得找到一組彼此相容的版本——Maven 傳遞依賴尤其棘手。這個問題不只發生在 Java——例如 Kubernetes 升級可能會被某個 Terraform provider 卡住,因為它仍要求舊版。

技術會持續演化#

每個技術都遵循生命週期:

  • 引入(Introduction):早期採用者與遠見者使用。
  • 廣泛使用(Widely used):主流或某利基使用。
  • 淘汰(Phased out):完全消失,或淪為僅有少數使用者的「殭屍技術」。

Figure 5.10: Over time, technologies and technology versions come and go (introduction → widely used → phased out)

技術版本也有類似但更短的生命週期(Java 每 6 個月一個版本,各版本只獲得有限期間的廠商支援)。

對齊「組織重要度」與「市場重要度」#

一個技術在市場上的重要度,決定了支援、修補、學習資源、人才供給的可得性。

組織內部對某技術的重要度,應該與其在市場上的重要度對齊;不對齊就是風險。

兩種風險:

  • 使用已被淘汰或不再受廠商支援的技術。
  • 沒有採用已成為主流、已被良好支援的技術。

Figure 5.11: A technology's importance in your organization should align with its importance in the market

必須持續升級#

至少需要做到:

  • 持續升級函式庫、框架版本。
  • 升級語言執行時、基礎設施服務、部署環境。
  • 定期評估是否該替換成新一代技術。

升級工作量差異極大:

  • 微小:完全向後相容的小版本升級。
  • 中等:Spring Boot 2 → 3,需要連同 Java 17、Jakarta EE 9,把所有 import javax. 改成 import jakarta.,瑣碎但量大。
  • 巨量:整個語言或框架的更換。Python 2 在 2020 年 EOL 後,組織不得不遷移到 Python 3,部分仰賴遷移工具,但若依賴的函式庫尚未支援 Python 3,就要等待或設法移除依賴。

漏洞需要立即行動:2021 年 Log4j 漏洞(CVE-2021-44228)允許遠端程式執行,全球組織被迫立即升級。這類事件無法預期,只能仰賴「平時就有快速升級能力」。

採用新技術需要可控的實驗#

升級函式庫小版本風險低,採用全新技術風險高——LinkedIn 上對某技術的吹捧無法替代你的實際驗證。

採用 Rust 的三種選項:

  1. 寫幾個玩具應用:不夠真實,沒有上線經驗。
  2. 花一年打造複雜應用上線:真實但風險巨大,可能花了 6 人年才發現自己更喜歡有 GC 的語言。
  3. 用 Rust 實作並部署一個子領域:真實且風險低,只需投入幾個人月。

第三種選項幾乎永遠是最佳選擇——這也是多元件架構在「實驗新技術」上能帶來的具體好處。

可演化性的場景#

四種典型的技術棧升級:

  1. 為了實作功能,團隊主動升級。
  2. 為了與市場對齊,定期升級。
  3. 漏洞修補,沒得商量,必須快
  4. 為了評估新技術,做低成本但真實的實驗。

第 1、2 種升級若會造成多個團隊的工作堆積,組織可能會遲遲不做。第 4 種需要特定架構支援。

通用要求:

  • 團隊能升級自家子領域所用的技術棧元素,不必經常與其他團隊協調
  • 組織能快速、輕鬆地把函式庫升到受支援版本。
  • 組織能快速、輕鬆地修補含漏洞的函式庫。
  • 升級工作能漸進式分批進行
  • 團隊能進行低成本但真實的新技術實驗。

具體場景:

  • Orders team 想升級 Order Management 用的某函式庫,不需與其他團隊協調
  • 安全團隊收到 0-day 漏洞通報,需升級函式庫(無程式碼變更),一天內完成測試與部署——隱含著對可測試性與可部署性的要求。
  • 全應用範圍的函式庫升級,可逐團隊進行,每個團隊改、測、部各自子領域。
  • Oracle → Postgres 遷移,各團隊把自家子領域改用 Postgres,僅需與營運(管理資料庫基礎設施)協調。
  • 某團隊想評估 Rust:用 Rust 實作或重寫一個子領域,部署上線評估,整體成本控制在幾個人月

設計高可演化性的架構#

對「由多團隊開發的大型應用」而言,高可演化性需要多元件架構(multi-component architecture)

多元件架構的好處:

  • 限縮多數技術棧升級的影響範圍:私有技術棧的升級只影響該元件。
  • 降低漏洞影響:若漏洞函式庫只被部分元件使用,只需升級這些元件。
  • 拆解大型升級:框架或語言升級可被切成「每個元件各自升級」的小任務,可與其他 flow item 交錯進行。
  • 支援低成本、真實的技術實驗:用候選技術實作一個子領域,部署為獨立服務即可。

Figure 5.12: A multi-component architecture significantly improves an application's evolvability

多元件架構的代價#

同樣的升級,有時要對每個元件做一遍——例如 Spring framework 小版本升級。

緩解做法:

  • 寫腳本自動化跨 Git repo 的變更。
  • Microservice Chassis 模式(第 17 章):集中化建置邏輯、共通基礎設施程式碼與依賴版本管理,讓某些升級簡化為改一處即可。