返回文章列表

從 ECS 到 EKS:Kubernetes 遷移實戰分享

本文探討從 AWS ECS 遷移至 EKS 的完整過程,分享實戰經驗與效能最佳化策略,幫助團隊降低佈署成本並提升系統效能。

雲端運算

在過去十年間,容器化技術徹底改變了我們佈署和管理應用程式的方式。當我第一次接觸 Kubernetes 時,就被它強大的容器協調能力所吸引。最近,我帶領團隊完成了從 AWS ECS (Elastic Container Service) 遷移到 AWS EKS (Elastic Kubernetes Service) 的專案,這個過程帶給我許多寶貴的經驗與技術見解。

為何選擇 Kubernetes?

在決定遷移到 Kubernetes 之前,我們的系統架構採用 AWS ECS,主要包含三個核心元件:

  1. Apollo Router 叢集:負責 GraphQL 查詢路由與管理
  2. 主應用程式叢集:分為 API 服務與 WebSocket 服務
    • API 服務:主要消耗 CPU 資源
    • WebSocket 服務:著重記憶體使用
  3. 輔助應用程式叢集:
    • Monstache:負責 MongoDB 到 ElasticSearch 的即時同步
    • Changestream-to-Redis:搭配小型 Redis 例項處理資料變更

這個架構雖然運作正常,但隨著業務成長,我們發現幾個關鍵痛點:

  1. 營運成本持續上升
  2. 資源使用效率不佳
  3. 自動擴縮減能力受限
  4. 佈署流程複雜度增加

Kubernetes 帶來的轉變

經過深入評估,我認為 Kubernetes 能為我們解決這些問題:

# EKS 叢集基礎設定範例
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: production-cluster
  region: ap-northeast-1
nodeGroups:
  - name: standard-workers
    instanceType: t3.large
    desiredCapacity: 3
    minSize: 2
    maxSize: 5
    labels:
      role: worker
  • nodeGroups 定義工作節點群組設定
  • instanceType 指定 EC2 執行個體類別
  • desiredCapacity 設定期望的節點數量
  • minSizemaxSize 控制自動擴縮的範圍
  • 使用標籤(labels)方便資源管理與識別

這樣的設定讓我們能更有效地管理運算資源,並且提供更靈活的擴充套件能力。在實際佈署過程中,我發現 Kubernetes 的資源排程機制確實比 ECS 更為精準,特別是在處理不同負載特性的服務時。

從ECS到Kubernetes:AWS基礎設施轉型實戰分析

在非正式環境中,我們的服務數量翻了七倍,雖然部分服務是共用的,但反而讓情況更加複雜。我們使用AWS CDK以TypeScript定義大部分基礎設施,只有少數較早期的部分是用Terraform建置。

ECS架構選擇與挑戰

在ECS的使用上,我們需要在AWS Fargate和Amazon EC2之間做選擇。Fargate提供以CPU核心數和RAM容量為基礎的抽象硬體資源,而EC2則需要從眾多例項類別中選擇。考量到調整的靈活性,我們選擇了Fargate。

為了降低成本,我們採用了Spot執行類別,這能節省約65%的運算成本,但代價是容器可能隨時被AWS回收。然而實際運作時我們遇到了嚴重問題 - 每週二中午幾乎所有例項都會被中斷,更糟的是常收到「目前無可用容量」的錯誤訊息。

這導致應用程式當機,即使我們要求超過20個容器,實際執行的卻只有2個。我們嘗試過:

  • 切換回標準非Spot例項
  • 調整CPU與RAM的設定比例
  • 使用所有可用區域

但這些措施都無法徹底解決問題。

GitHub Actions效能最佳化需求

在進行架構遷移之前,我們面臨另一個挑戰:端對端測試套件執行時間過長。雖然已實施平行化測試,但隨著工作節點增加,額外開銷也隨之增長。

我們從GitHub標準執行器升級到大型執行器,效能確實顯著提升,但相對的成本也大幅增加。當帳單達到一定程度時,我們決定尋找替代方案。

自託管執行器看似理想解決方案,但存在資源閒置問題 - 夜間和週末時機器基本處於閒置狀態。這促使我們思考如何更有效地運用運算資源,進而開始考慮更全面的架構改造方案。

在實務經驗中,我發現單純解決個別問題往往不夠全面。我們需要一個能同時處理資源排程、成本控制和效能最佳化的整體解決方案。這也是為什麼最終我們開始思考向Kubernetes轉型的可能性。

在多年管理雲端基礎設施的經驗中,玄貓深刻體會到:建立一個完善的Kubernetes叢集不僅需要技術能力,更需要深入理解每個元件的價值。今天就讓我分享如何建構一個功能完整的Kubernetes基礎設施。

核心基礎設施元件

我們的Kubernetes旅程始於Actions Runner Controller的自我託管需求。這個決定為後續的基礎設施擴充套件奠定了良好基礎。目前,我們的叢集包含多個重要元件,每個都扮演著獨特的角色:

持續佈署與設定管理

  • Argo CD作為叢集設定的前端介面,負責管理佈署流程。只要將期望版本推播到儲存函式庫就會自動處理後續佈署工作
  • Flux負責持續交付,處理所有不透過Argo佈署的專案,確保整個系統的自動化運作

安全與存取控制

  • Cert Manager專門處理憑證管理,確保所有服務的安全性
  • OAuth2 Proxy提供了簡單但強大的內部服務授權機制
  • External Secrets可直接存取AWS Secrets Manager,簡化了敏感資訊的管理

監控與可觀察性

在建構監控系統時,我選擇了業界公認的黃金組合:

  • Grafana提供強大的視覺化介面
  • Loki處理日誌收集與分析
  • Prometheus負責度量收集
  • Node Exporter收集節點層級的度量資料
  • fluentbit確保日誌的有效收集與轉發

資源排程與最佳化

  • Karpenter的加入徹底改變了我們的節點資源管理方式。它能夠即時分析並選擇最具成本效益的執行個體,這在管理大規模叢集時特別重要
  • Keda提供了進階的自動擴充套件功能,特別是在處理AWS SQS佇列大小等場景時非常有用

網路與快取最佳化

  • Ingress NGINX Controller作為反向代理和負載平衡器,確保流量的高效分配
  • Spegel提供Docker映像快取,大幅提升了容器佈署效率

系統整合的深層思考

在建置這套系統時,玄貓特別注意到幾個關鍵點:

  1. 元件間的協同運作至關重要。例如,Prometheus與Grafana的整合不僅提供了即時監控,還能協助我們做出更明智的資源排程決策

  2. 自動化程度的提升帶來了維運效率的顯著提升。透過Flux和Argo CD的配合,我們實作了真正的GitOps工作流程

  3. 成本效益的平衡。Karpenter的智慧排程機制幫助我們在保持效能的同時最佳化資源使用成本

這些基礎設施元件的數量確實龐大,需要大量的設定工作才能讓它們順利運作。當我們比較像Galaxy或Heroku這樣的託管平台時,就能理解它們在幕後為我們處理了多少複雜性。但透過自建基礎設施,我們獲得了更大的靈活性和控制力,這在特定場景下是無價的優勢。

在技術選型時,我特別注重元件的成熟度和社群活躍程度。例如,選擇Prometheus和Grafana這樣的業界標準工具,不僅確保了穩定性,還讓團隊能夠受益於龐大的社群資源和最佳實踐。

隨著應用程式的演進,我們也在持續最佳化這個基礎設施。目前正在評估加入Jaeger來增強分散式追蹤能力,這將進一步提升我們的可觀察性架構。在實際營運過程中,這些工具的價值遠超過其設定的複雜度。

建立這樣一個完整的Kubernetes基礎設施確實需要投入大量心力,但它為我們的應用提供了一個靈活、可擴充套件與高度自動化的執行環境。這些努力最終轉化為更好的服務品質和更高效的營運效率。

轉移過程中的重大挑戰

在非生產環境的遷移相對簡單,因為我們可以接受一定程度的停機時間。過程大致是先啟動 Kubernetes 服務,接著切換 DNS,最後關閉 ECS 服務。理論上這應該是一個零停機時間的安全切換流程,在非生產環境中確實也是如此。

然而,當我們進行生產環境遷移時,卻遇到了一些意料之外的嚴重問題。主要面臨兩大挑戰:防火牆設定與執行個體類別選擇。

防火牆設定的調整

在防火牆方面,我們原本採用了相當嚴格的 ModSecurity 設定。這個決定雖然出發點是好的,但實際執行時卻帶來了許多問題。經過仔細評估後,我們只需要停用少數幾個規則,主要是與傳入資料相關的部分。這讓我深刻體會到,在安全性與可用性之間找到平衡點的重要性。

執行個體效能的最佳化

在執行個體類別方面的問題則花費了我們數天時間來調整。在 ECS 中,資源設定相對簡單,只需指定所需的 CPU 與記憶體數量。我們在 Karpenter 中採用了相同的設定方式,系統也確實依照成本最佳化的原則進行了資源分配。

但問題在於,並非所有的 CPU 效能都是相同的。這個差異對應用程式的效能影響相當顯著。建議開發者可以參考 Vantage 的執行個體比較表,以更全面地瞭解不同執行個體類別的特性。

經驗教訓

回顧這次遷移過程,我認為我們應該:

  • 保留舊叢集更長時間,讓流量轉移更平緩
  • 一開始就採用較寬鬆的防火牆規則,再逐步調整
  • 更謹慎地評估執行個體類別對效能的影響

持續最佳化與調校

成功完成遷移後,玄貓開始進行系統最佳化。Kubernetes 提供了數千個可調整的引數,但調校過程就像玩蛇梯棋一樣充滿變數。有些調整能讓應用程式效能提升 20%,有些卻可能導致系統當機。

檢視設定歷史記錄,在第一個月內我們進行了超過百次的調整。儘管調整的幅度隨時間逐漸縮小,但這個數字仍然相當驚人。最令人欣慰的是,透過精確的資源調校,我們同時達成了降低雲端費用與改善回應時間的目標。

關鍵設定最佳化專案

在大規模轉移過程中,有幾個重要的設定專案最初被我們忽略:

  1. Karpenter 中斷機制的設定,這對於避免高峰時期的嚴重變動至關重要。

  2. 與 MongoDB Atlas 的 VPC 對等連線設定。這個疏忽導致較長的網路往往返時間和較高的資料傳輸費用。

  3. 環境變數中的打字錯誤修正,這是一個常見但容易被忽視的問題。

  4. 自動擴充套件的上限調整,確保系統能夠適應流量峰值。 在容器化環境中佈署 Meteor 應用程式,玄貓在處理多個大型專案時,發現記憶體管理與 WebSocket 設定是兩大關鍵。讓我們探討這些重要的技術細節。

容器記憶體指標的最佳化

在初期建置階段,我們常直接將非正式環境的設定套用到正式環境,這是一個常見的錯誤。經過多次專案經驗,玄貓建議將記憶體監控指標從 container_memory_usage_bytes 改為 container_memory_working_set_bytes。這個改變有其重要原因:

  • container_memory_usage_bytes 包含了可被回收的快取記憶體
  • container_memory_working_set_bytes 才是真正會觸發記憶體不足錯誤的指標
  • 使用正確的指標能夠更準確地進行自動擴縮

Meteor 應用程式的容器化佈署

基礎建置流程

容器化佈署 Meteor 應用程式需要注意幾個重要環節:

  1. 建立 Docker 映像檔
  2. 使用 Helm 圖表 而非直接建立 Kubernetes 服務
  3. 實作優雅停機制

WebSocket 連線管理

關於 WebSocket 的處理,玄貓提供以下建議:

# K8s設定範例
apiVersion: v1
kind: Service
metadata:
  name: meteor-websocket
spec:
  selector:
    app: meteor
    component: websocket
  ports:
    - port: 80
      targetPort: 3000
  • 這個服務設定專門處理 WebSocket 流量
  • 使用選擇器區分 WebSocket 元件
  • 將標準 HTTP 埠對映到 Meteor 預設的 3000 埠

流量分離策略

玄貓建議將 WebSocket 容器與其他服務分開處理:

# Nginx Ingress設定範例
location /websocket {
    proxy_pass http://meteor-websocket;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

location / {
    proxy_pass http://meteor-api;
}
  • 將 /websocket 路徑導向專門的 WebSocket 服務
  • 其他 API 請求導向一般服務容器
  • 啟用 WebSocket 升級協定支援

效能最佳化建議

  1. 設定 DISABLE_SOCKJS=1 環境變數,除非需要支援舊版瀏覽器
  2. 整合 @meteorjs/ddp-graceful-shutdown 實作平滑關機
  3. 實作批次斷開使用者連線,避免流量尖峰

經過多個專案實踐,這些設定能有效提升應用程式的穩定性與效能。對於現代網頁應用程式來說,WebSocket 支援已相當普及,因此可以放心停用 SockJS。透過適當的流量分離與優雅停機制,能夠確保系統在擴縮減時保持穩定運作。

每個專案的需求可能不同,但這套設定方案提供了一個紮實的基礎,讓開發團隊能夠根據實際需求進行調整。記住,系統穩定性與使用者經驗才是最終目標,技術選擇都應該服務於這個核心目標。

在容器化環境中執行 Meteor 應用程式其實並不複雜,關鍵在於理解各個元件的作用,並且根據實際需求做出合適的設定選擇。透過正確的監控指標、適當的服務分離,以及完善的優雅停機制,我們能夠建立一個穩定與高效的生產環境。

在多年的系統最佳化經驗中,玄貓發現許多 Node.js 開發者往往忽略了一個關鍵的效能最佳化點:記憶體設定器的選擇。這個看似基礎的元件,實際上對應用程式的效能和穩定性有著深遠的影響。

記憶體設定器的重要性

在處理大型 Node.js 應用程式時,選擇合適的記憶體設定器至關重要。目前市面上有多種優秀的選擇:

  • jemalloc:Facebook 開發的記憶體設定器
  • mimalloc:微軟開發的高效能記憶體設定器
  • tcmalloc:Google 開發的執行緒快取記憶體設定器

WebSocket 服務的記憶體回收問題

在建置複雜的 WebSocket 服務時,玄貓遇到了一個棘手的問題:WebSocket 容器的記憶體始終無法有效回收。雖然 API 容器運作正常,但 WebSocket 服務的記憶體使用呈現持續增長的趨勢。

// 典型的 WebSocket 服務設定
const ws = new WebSocket.Server({
    port: 8080,
    maxPayload: 1024 * 1024 // 1MB
});

ws.on('connection', function connection(ws) {
    // 連線處理邏輯
    ws.on('message', function incoming(message) {
        // 訊息處理邏輯
    });
});

記憶體問題的深層原因

經過深入分析,發現這並非記憶體洩漏問題,而是 Node.js 預設記憶體管理機制的限制:

  1. 記憶體釋放需要消耗 CPU 資源
  2. 系統傾向於保留記憶體而非立即釋放
  3. 只有在接近記憶體上限(預設 2GB)時才會強制進行垃圾回收
  4. 當強制垃圾回收時,約 80% 的 CPU 資源會被佔用,導致服務反應遲緩

jemalloc 解決方案

在嘗試多種最佳化方案後,玄貓決定匯入 jemalloc 記憶體設定器。這個決定帶來了立竿見影的效果:

  1. 平均記憶體使用降低約 20%
  2. 系統能根據流量自動調整記憶體使用
  3. 夜間低峰期可以有效縮減資源使用

Docker 環境中的 jemalloc 設定

在 Alpine Linux 的 Docker 環境中設定 jemalloc 相當直接:

# Dockerfile
FROM alpine:latest

# 安裝 jemalloc
RUN apk add --no-cache jemalloc

# 設定 jemalloc 為預設記憶體設定器
ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2

# 應用程式設定
COPY . /app
WORKDIR /app

內容解密

  1. RUN apk add --no-cache jemalloc

    • 使用 Alpine 的套件管理器安裝 jemalloc
    • –no-cache 選項確保不會在映像檔中保留快取檔案
  2. ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2

    • 設定環境變數使系統預載 jemalloc 函式庫 - 這會覆寫系統預設的記憶體設定器

這個最佳化過程讓玄貓深刻體會到,有時候系統效能的提升不一定要透過複雜的程式碼改寫,選擇合適的基礎元件同樣能帶來顯著的效能提升。在實際生產環境中,這個改動不僅提升了系統的穩定性,還降低了維運成本。

記憶體管理最佳化是一個持續性的過程,選擇合適的記憶體設定器只是第一步。在實際應用中,還需要持續監控系統效能,根據實際負載情況進行調整。經過這次最佳化經驗,更加確信在處理大型 Node.js 應用時,基礎架構的選擇與設定同樣重要。