我是山姆鍋

在經歷大大小小不同專案後,除了有特殊要求的系統外 (如需符合 PCI-DSS 的第三方支付服務、即時影音串流等), 大部分的 Web 應用架構其實都是大同小異。本文提供一個通用的 Web 應用架構作為參考,希望可以符合 80% 以上 Web 專案的需求。

山姆鍋是從解決實務需求為出發點,使用的一些名詞也許不符合教科書般的定義。例如:山姆鍋常常自己也搞不太清楚「系統架構」、「軟體架構」與「網路架構」該不該在同一份文件呈現?所以,可以預期山姆鍋使用的一些名詞可能會跟您認知的有所差異,為了減少混淆會先針對重要的名詞加以定義。

由於軟體架構的抽象性,多數參與開發的人員並不清楚架構的必要性。其實不管有沒有文件化,每個軟體都存在著架構。本文就是希望描述一個通用的 Web 軟體架構,協助對應用架構比較沒經驗的新創團隊有一個建構藍圖作為參考。另一方面,為了讓讀者對抽象的元件有較具體的概念,會盡可能描述該元件實作的常見選項,但不可能面面俱到,還煩請在留言處提供補充建議。

目錄

  1. 名詞定義
  2. 前提假設
  3. 設計目標
  4. 架構設計
    1. 應用客戶端 (Application clients)
    2. 域名服務 (Domain Name Service; DNS)
    3. 內容交付網路 (Content Delivery Network; CDN)
    4. 網路負載均衡器 (Network Load balancer)
    5. 邊緣代理 (Edge Proxy)
    6. 應用控制器 (Application Controller)
    7. 應用服務 (Application Service)
    8. 支援服務 (Backing Services)
    9. 支援存儲 (Backing Stores)
  5. 常見支援存儲
    1. 資料存儲 (Data Stores)
    2. 資料快取 (Data Cache)
    3. 搜尋索引 (Search Index)
    4. 共享檔案系統 (Shared Filesystem)
    5. 物件存儲 (Object Storage)
  6. 常見支援服務
    1. 訊息傳遞服務 (Messaging service)
    2. 郵件寄送服務 (Email Delivery Service)
  7. 其它支援服務
  8. 元件間通訊機制
    1. 客戶端 - 服務端
    2. 服務端 - 服務端
  9. 小結

名詞定義

山姆鍋對於軟體架構的定義很簡單:「說明系統中各個元件怎麼兜起來」。問題是:「什麼是系統?」、「什麼是元件?」,都是抽象化的名詞常常讓人搞不懂意思。

在軟體開發領域,「系統 (system)」就是子系統 (subsystem) 跟元件的組合,簡單地講:系統跟子系統只用用來將元件分組的,因為人類無法領解太過複雜的對象,傾向於將這樣整體巨觀的對象拆解成比較小的單元。以軟體開發來說,「運作中的 web 軟體」就是一個整體巨觀的對象。系統是不是一定要包含子系統?子系統要不要包含更小的子系統?這些都需要根據複雜度來考量,通常由架構師負責規劃出可以被理解的組成階層。

「元件 (component)」是另一個在不同場合被過度使用,抽象化也很高的名詞。隨著後期經驗的累積,在架構這個軟體層級,本文山姆鍋統一將元件視為可以獨立開發以及部署的單元。所以,MySQL 資料庫是個元件;微服務 (microservice) 也是元件。

「應用平台 (application platform)」提供應用所需的執行環境。以容器化應用 (containerized applications) 的話,通常會採用 Kubernetes 這樣的容器調度系統來作為應用平台基礎。

「部署環境」:根據軟體開發週期的不同階段以及使用者角色的不同,會有多個的應用平台作為部署的標的。常見的部署環境可分為「開發環境」、「測試環境 (QA environment)」、「生產環境 (production environment)」等等。

「持續部署 (continuous deployment)」指將經過品質確保的應用元件持續且自動地部署到生產環境,每次元件部署就會產生一個新的版本與原先版本並存一段時間。透過 feature flag 等機制,完成部署不代表已經釋出 (released) 給終端使用者。讓生產環境的部署與釋出分開的作法,允許團隊使用 canary 或 blue green 等不同發行 (release) 策略來降低造成對終端用戶大規模衝擊的風險。

前提假設

本文所提的架構設計基於下列假設:

  1. Web 應用按照 The Twelve Factors App 原則設計。
  2. 採用 Client-server 網路架構。
  3. 採用 Kubernetes 作為應用平台,工程師有自己本機的開發環境。
  4. 團隊可以根據元件特性,自由選擇實作的程式語言、框架跟資料存儲。

設計目標

  • 自建應用平台只使用開源軟體。
  • 具備移植性 (portability) 可以自建應用平台或者選擇雲端託管服務 (EKS、GKE、AKS)。
  • 具備可用性 (availability) 與擴充性 (scalability)。

架構設計

雖然架構設計盡量抽象化,但一個務實的架構師其實在設計階段,心中或多或少已經有元件實作的選項。這點通常也沒什麼問題,甚至可以說是優點之一。畢竟,您不會希望架構設計只是設計,無法轉換成對應的實作吧!所以,山姆鍋其實都是從已經知道元件具體如何實現才加以抽象化描述,因為山姆鍋知道團隊一定會有人 (例如: IT 工程師) 提出某某元件應該採用哪個產品等等問題。

下圖是本文所提的 Web 應用架構圖:


應用客戶端 (Application clients)

由於本文描述 Web 應用架構,應用客戶端自然是透過 Web 瀏覽器來執行。應用客戶端按照實作方式不同可以分成下列幾種:

  1. 多頁式 Web 應用:藉由在後端動態產生網頁的 HTML 來實現使用者介面,使用者每次的點選動作都會發出新的請求來產生新頁面。

  2. 單頁式 Web 應用 (Single Page Application; SPA):透過 Ajax 機制在不換頁的情況下,動態部分修改網頁內容來呈現使用者介面。相較於傳統多頁式應用提高使用者介面的反應速度。

  3. 漸進式 Web 應用 (Progressive Web Application; PWA):比單頁式 Web 應用更進一步,利用新技術 (如 service worker, web push 等) 讓 Web 應用更接近原生 (行動) 應用的使用體驗。

對於新專案建議以 PWA 作為客戶端的實現方式,PWA 應用框架常見方案:

部署觀點
Web 應用的客戶端元件雖然在瀏覽器中執行,但所需的程式碼及資源檔 (e.g. CSS/images) 是以「應用服務」元件形式部署在平台,當瀏覽器存取才下載到客戶端呈現或執行。根據客戶端的特性,呈現頁面可能同時會有客戶端以及服務端邏輯。例如:多頁式應用,每頁都是由後端服務動態產生;新型態的 PWA 也可能為了搜尋引擎優化 (SEO) 目的而進行服務端渲染 (Server-Side Rendering; SSR)。

安全觀點
應用客戶端的程式碼與資源檔只能透過安全連線 (HTTPS) 被下載。同樣地,在瀏覽器執行的程式碼只透過安全連線 (HTTPS/WSS) 與服務端通訊。

域名服務 (Domain Name Service; DNS)

域名服務是使用者存取應用的第一個網路環節,應用客戶端要存取後端應用服務需要將域名解析 (resolve) 成 IP 位址才能實際透過網路存取。「域名服務」通常需要跟「內容交付網路」搭配,在客戶端解析域名時,根據客戶端所在的地點以及網路狀態來回傳適當的伺服器節點。

不在此架構涵括範圍,但如果應用部署在多個資料中心,DNS 也是實現全球負載均衡的必要機制。

雲端常見的方案:

  • CloudFlare
  • AWS Route 53

由於 DNS 對於應用服務的可用性扮演至關重要的地位,新創團隊沒有資源實現全球高可用的域名服務,應該選擇採用雲端服務。這裡的域名服務是提供給客戶端使用,跟應用平台內部的域名服務是分開的。

內容交付網路 (Content Delivery Network; CDN)

「內容交付網路」,也就是 CDN,負責將客戶端需要的資源檔案或程式碼快取到世界各地的邊緣 (edge) 資料中心來減少客戶端存取的延遲以及提高應用的處理容量跟可靠性。此外,CDN 服務供應商通常也提供 Web 應用防火牆 (web application firewall; WAF) 服務來強化 web 應用的安全性。應用如果是面向全球的使用者,則 CDN 的使用應該視為必要。

雲端常見的方案:

  • CloudFlare CDN
  • AWS CloudFront
  • Google Cloud CDN

在過往應用系統要能夠使用 CDN 是門檻相當高的事情,主要是費用問題。由於經濟型以及免費的 CDN 服務已經很常見,且 CDN 是實現應用高擴充性中重要的元件,因此被納入此架構中。

網路負載均衡器 (Network Load balancer)

支援第四層 (TCP/UDP) 的網路負載均衡,根據連線的 IP 與 Port 將請求平均轉送 (forward) 給應用平台內的各個「邊緣代理 (edge proxy)」。

開源常見的方案:

常見的雲端託管服務都有整合各家的網路負載服務器,只要在 K8S 中部署型態 (type) 為 LoadBalancer 的服務 (service) 資源就可以建立對應的負載均衡服務。

邊緣代理 (Edge Proxy)

負責應用平台請求的進入點,通常俱備下列功能:

  1. 提供與客戶端的加密安全連線 (SSL termination)。
  2. 將請求轉送到正確的上游 (upstream) 的應用控制器或服務元件。
  3. 請求限速 (Rate limit): 避免單一來源 (e.g. 同一 IP 或用戶) 同一時間發出太多請求,部分是為了防堵「阻斷服務 (DoS)」。
  4. 存取控制 (Access control):驗證請求中的 token 來決定是否允許存取。
  5. 標示請求識別碼:針對每個進入平台的請求給予唯一識別碼,讓追蹤系統可以關聯同一個請求在不同元件的資訊。

開源常見的方案:

此元件利用 Kubernetes 的 Ingress controller 實現。

應用控制器 (Application Controller)

此架構中的選擇性 (optional) 元件,負責:

  1. 將 HTTP 請求轉發給應用的不同元件。
  2. 根據負載來伸縮 (scale) 應用元件副本 (replica) 數量:實際上通常由另一個元件負責,但此架構將兩者合併。

開源常見的方案:

雖然習慣將這樣的方案稱為 Function as a Service (FaaS) 框架,此架構利用這些框架擴展 Kubernetes 對於應用元件的生命週期管理功能。如果需要更彈性的自動擴容 (autoscaling),包括從零擴容 (scale from zero) 的能力,則可以在平台中使用「應用控制器」。

應用服務 (Application Service)

一個 Web 應用由一個以上的「應用服務」組成,單體結構 (monolithic) 的應用只有一個應用服務,但並不是說有多個應用服務就是屬於微服務架構 (microservices)。在此架構並沒有對應用服務的數量以及結構加以限制,應用服務可以是一個「功能 (function)」、微服務、或者是包含整個應用邏輯的單體。應用服務包含不適合在客戶端實現的商務邏輯 (business logic) 以及領域模型 (domain model),通常由開發團隊內部開發,但就算是外包只要是應用特有 (application-specific) 的元件都屬於這一類,也應該採取相同架構規範。

實作應用服務常用的程式語言:

  • Python
  • Go
  • Java
  • JavaScript/TypeScript
  • Ruby
  • PHP
  • C#

除了實現應用邏輯所需的程式碼,應用服務也需要明確指定相依的套件名稱與版本,在 CI/CD 的 pipeline 就完成應用製品 (artifact) 的封裝工作,如產生容器的映像檔並推送到容器儲存庫 (container registry) 中。除了開發環境外,其餘的部署都只能使用封裝好的容器映像檔來進行。

另外,只要是熱門的程式語言,選擇哪一種則按照團隊成員已經熟悉為準,初期不用為了執行效能或為符合流行趨勢來選擇。

部署觀點
每個應用服務都是以容器映像 (container image) 形式包裝 (package) 跟分發 (delivery),容器已包含應用服務執行時期 (runtime) 以及相依的程式庫、執行檔等。一般原則是不建議在部署環境才建置 (build) 應用服務,一些 FaaS 方案(如 Fission) 支援在部署環境中才建置功能 (funtion) 容器用以執行對應的功能。雖然可以免除容器儲存庫 (container registry) 的需求,但並無法保證每次建置時相依的套件可以正常取得。如果應用有多個部署環境,也會增加組態飄移 (configuration drift) 的可能。

安全觀點
為了避免應用元件所在的主機 / 容器被入侵後將機密資料送出,應用服務不應該直接存取網路上的第三方服務。也就是說:應用服務元件所需要的服務都只能由「支援服務」或者「支援存儲」來提供。如此可以透過防火牆或者網路策略 (Network policy) 等機制來限制只有合法授權元件可以存取。

支援服務 (Backing Services)

泛指由第三方所提供套裝軟體或者雲端供應商提供的網路服務。通常「支援服務」的整備 (provision) 流程是運維團隊建立所需的「支援服務」實例後,將該服務實例與應用服務進行綁定 (binding)。理想上,應用元件不需要了解背後的整備流程。

以 MySQL 資料庫為例:「整備」包含部署 MySQL 服務軟體 (或者申請雲端資料庫服務)、建立資料庫,產生應用連線的帳密跟設定存取權限等;「綁定」則是將存取 MySQL 資料庫所需的服務位址、連線帳密等資訊以 ConfigMap 或者 Secret 資源形式讓應用服務可以取得。

簡單的整備流程可以手動安裝,也可以透過 Service Catalog 來自動進行。

支援存儲 (Backing Stores)

架構上同樣屬於支援服務,但由於負責提供應用狀態存放與查詢所需的機制,跟其它無狀態的支援服務在部署與安全策略明顯不同,架構上特別分開描述。「支援存儲」按照應用對資料的永續性 (persistence)、資料結構、查詢效能等不同需求會有不同適用的存儲方案。

  • 「資料存儲服務」提供應用永久性或半永久性資料的存放空間,同時需要提供應用透過網路協定來快速查詢與處理資料的功能。
  • 「資料快取服務」提供應用暫時性的資料存放空間,提供快速查詢常用資料機制以達到縮減請求處理時間的目的。
  • 「搜尋索引服務」提供全文檢索功能,對於想提供使用者 ad-hoc 資料查詢功能的應用特別需要。
  • 「共享檔案系統」提供可由多節點同時存取的網路檔案存取服務,傳統上透過 NFS 來提供。Web 應用常用來存放使用者上傳的媒體檔案。
  • 「物件存儲服務」由 AWS S3 服務引起流行的存儲模式,適合非結構化、大量的二進位物件 (blob) 檔案存取。由於物件儲存高可用性以及擴展性,在現代化的 Web 應用,常用來取代「共享檔案系統」的角色。

此架構設計允許應用元件選擇最適合的存儲方案,但是否真得需要根據資料特性來選擇?這要由團隊自行決定。極端而言,有些應用將所有資料包括檔案都存放在一個關聯式資料庫來集中管理,對效能、可用性以及擴充性當然有不好的作用,但是粗暴簡單。

常見支援存儲

應用服務為無狀態元件,所以 Web 應用總是需要某些「支援存儲」來存放狀態資料,本節提出幾種常見的支援存儲。

資料存儲 (Data Stores)

資料存儲或者說資料庫 (database) 按照資料模型的不同可分為「關聯式資料庫 (RDBM)」與非關聯式資料庫 (NoSQL) 兩大類。

Wikipedia 對於關聯式資料庫的定義:

關聯式資料庫(英語:Relational database),是建立在關聯模型基礎上的資料庫,藉助於集合代數等數學概念和方法來處理資料庫中的資料。現實世界中的各種實體以及實體之間的各種聯絡均用關聯模型來表示。關聯模型是由埃德加・科德於 1970 年首先提出的,並配合「科德十二定律」。現如今雖然對此模型有一些批評意見,但它還是資料儲存的傳統標準。標準資料查詢語言 SQL 就是一種基於關聯式資料庫的語言,這種語言執行對關聯式資料庫中資料的檢索和操作。

Wikipedia 對於 NoSQL 的簡短說明:

NOSQL (Not Only SQL) 是對不同於傳統的關聯式資料庫的資料庫管理系統的統稱。

允許部分資料使用 SQL 系統儲存,而其他資料允許使用 NOSQL 系統儲存。其資料儲存可以不需要固定的表格模式以及中介資料 (metadata),也經常會避免使用 SQL 的 JOIN 操作,一般有水平可延伸性的特徵。

選擇哪類型來作為應用的資料庫可以根據需求與運維能力而定。作為一般原則:由於關聯式資料庫技術最為成熟以及使用廣泛,新創團隊應該優先採用。

開源常見的方案:

Redis 適用不需要 Ad-hoc 查詢,可以接受短時間 (如 1 秒)資料丟失風險的應用情境。

資料快取 (Data Cache)

資料快取服務提供應用元件暫時性 (transient) 資料的存放服務,用來減少需要存取資料庫的次數以達到提高服務請求的效率。例如使用者的會期資料 (session data) 在每個網頁請求都需要存取,就可以放在資料快取中減少查詢時間。

開源常見方案:

由於 Redis 具備多樣性功能,山姆鍋傾向於使用 Redis, 在某些情境甚至可以作為 key-value 資料庫使用。

搜尋索引 (Search Index)

如果應用需要提供全文檢索 (full-text search) 功能,由於資料庫對於全文檢索的支援相對陽春,應用也許需要使用專門的搜尋引擎 (search engine) 軟體來符合查詢效能的需求。

開源常見的方案:

共享檔案系統 (Shared Filesystem)

在分散式應用架構中,常見的一個需求是:使用者上傳的檔案 (通常是圖檔) 要讓應用的多個節點可以存取,傳統上可以透過網路檔案系統 (如 NFS) 來提供。使用共享檔案系統的主要好處在於熟悉以及方便開發,讀寫本地跟遠端掛載的檔案操作差異不大。

開源常見的方案

由於雲端供應商較少提供共享檔案系統服務,如果使用共享檔案系統就需要自行部署。因此,如果可行建議採用「物件存儲」代替。

物件存儲 (Object Storage)

相對於共享檔案系統雖然需要透過 API 來進行檔案操作,由於難度不高並不會增加太多額外負擔 (overhead)。使用雲端物件存儲服務還具備高可用以及高可靠的優勢,檔案存放在雲端物件存儲通常也方便與 CDN 整合 (e.g. AWS CloudFront + S3)。在本地開發上,MinIO 提供相容的功能可以方便整合測試。

開源常見的方案:

由於雲端物件存儲服務按量計費,且有經濟的選項,如 DigitalOcean 的 Spaces。原則上,即使是自建應用平台也建議採用雲端物件存儲服務。

常見支援服務

不同 Web 應用需要的支援服務差異頗大,但還是有幾種「支援服務」常需要用到。

訊息傳遞服務 (Messaging service)

「訊息傳遞服務」提供應用元件間一對ㄧ (queue)、一對多 (publish-subscribe) 等多種通訊模式的訊息傳遞機制。此服務的應用場景之一是應用將實現請求不需要立即完成的工作透過佇列 (queue) 派送給背景程序執行,如此可以減少客戶端等待時間。幾乎沒有一個高擴充性 (highly scalable) 的系統找不到訊息傳遞服務的存在。所以,熟悉跟了解訊息互動模式,更重要的是事件驅動 (event-driven) 概念,對於設計高可用以及高擴充性的應用相當關鍵。

開源常見的方案:

Redis 真的是多用途的軟體,在小規模的應用也常看到作為訊息傳遞服務。山姆鍋不負責任建議:如果沒有特別需要的話,可以選擇 NATS Streaming Server。

郵件寄送服務 (Email Delivery Service)

此服務提供應用發送電子郵件所需的功能,讓 Web 應用發送諸如:「使用者信箱確認信件」、「密碼重置信件」等電子郵件。由於垃圾信氾濫,自建郵件服務信件送達機率越來越低,建議採用雲端服務。

雲端常見的方案:

應用需透過 SMTPS 協定與郵件寄送服務溝通避免供應商鎖定 (vendor lock-in) 以及方便測試。開發環境會需要利用 MailSluper 這類工具來進行模擬跟除錯。

安全觀點
對應 “應用服務元件不能直接存取第三方服務 “這個限制,須在應用平台 (生產環境) 部署對應的代理 (proxy) 服務,與雲端服務連線只能由此代理進行。對應用元件而言,郵件寄送服務是由此代理所提供,因為代理也是平台所管理的元件,存取控制可以一致地使用應用平台的機制實現 (如 Network policy, Service Mesh)。

其它支援服務

實務上應用平台至少還要部署應用監控、日誌等其它所需的服務,但由於不是應用元件執行所直接相依的服務,這裡就不特別說明。除此之外,就運維一個應用平台而言,服務網格 (service mesh)、分散式追蹤 (distributed tracing) 等用來提高應用系統可觀察性 (observability) 以及可靠性 (reliability) 的技術,都是團隊值得考慮投資的項目。

元件間通訊機制

應用元件並非獨立存在,彼此間免不了需要進行溝通,例如客戶端需要呼叫後端 API 來查詢資料;微服務需要呼叫另一個微服務來完成客戶端過來的請求。元件之間要能夠順利溝通,自然就需要有規範的通訊協定,本節針對幾個情境提出常見選項。

客戶端 - 服務端

雖然可以做到一定離線 (offline) 操作能力,但 Web 應用的客戶端總是免不了需要跟後端服務溝通的。下面幾個通訊協定以及適用的情境:

  • REST (HTTPS): 適合由客戶端發起進行查詢或者操作的 API 請求。
  • Websocket (WSS): 適合客戶端與服務端需要即時雙向的通訊,如即時股價回報。
  • gRPC (HTTP/2): 適合客戶端是行動應用或者其它 Thick clients

服務端 - 服務端

應用服務元件之間的通訊機制如果是同步的請求回應 (request-response) 互動模式,雖然看起來 gRPC 是不二選擇,但 REST API 或者 JSON-RPC 其實也是簡單務實的作法。

除非對網路流量優化或者訊息 schema 驗證有特別的需求,不然應用服務元件間非同步通訊則建議以 JSON 為訊息的格式,夠彈性也方便找問題。

小結

山姆鍋希望本文所提的 Web 應用架構剛好足夠 (just enough) 適用多數 Web 專案的需要。新創團隊想要一開始就使用先進 (state-of-the-art) 的技術框架作為賣點之一,雖然可以理解但同時也會增加不必要的風險。畢竟軟體的價值發生在實際交付使用者的時候,初期投資太多在酷炫技術對使用者並沒有實際效益。針對新創團隊,即使目標是微服務架構,山姆鍋仍建議從單體式應用服務作為開始,在實際掌握領域 (domain) 如何適當劃分的經驗後,再過渡到微服務架構應該是比較保險的作法。採用微服務架構的話,可以評估是否以及哪個時間點導入「服務網格 (service mesh)」機制。