在過去十年間,容器化技術徹底改變了我們佈署和管理應用程式的方式。當我第一次接觸 Kubernetes 時,就被它強大的容器協調能力所吸引。最近,我帶領團隊完成了從 AWS ECS (Elastic Container Service) 遷移到 AWS EKS (Elastic Kubernetes Service) 的專案,這個過程帶給我許多寶貴的經驗與技術見解。
為何選擇 Kubernetes?
在決定遷移到 Kubernetes 之前,我們的系統架構採用 AWS ECS,主要包含三個核心元件:
- Apollo Router 叢集:負責 GraphQL 查詢路由與管理
- 主應用程式叢集:分為 API 服務與 WebSocket 服務
- API 服務:主要消耗 CPU 資源
- WebSocket 服務:著重記憶體使用
- 輔助應用程式叢集:
- Monstache:負責 MongoDB 到 ElasticSearch 的即時同步
- Changestream-to-Redis:搭配小型 Redis 例項處理資料變更
這個架構雖然運作正常,但隨著業務成長,我們發現幾個關鍵痛點:
- 營運成本持續上升
- 資源使用效率不佳
- 自動擴縮減能力受限
- 佈署流程複雜度增加
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
設定期望的節點數量minSize
和maxSize
控制自動擴縮的範圍- 使用標籤(
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映像快取,大幅提升了容器佈署效率
系統整合的深層思考
在建置這套系統時,玄貓特別注意到幾個關鍵點:
元件間的協同運作至關重要。例如,Prometheus與Grafana的整合不僅提供了即時監控,還能協助我們做出更明智的資源排程決策
自動化程度的提升帶來了維運效率的顯著提升。透過Flux和Argo CD的配合,我們實作了真正的GitOps工作流程
成本效益的平衡。Karpenter的智慧排程機制幫助我們在保持效能的同時最佳化資源使用成本
這些基礎設施元件的數量確實龐大,需要大量的設定工作才能讓它們順利運作。當我們比較像Galaxy或Heroku這樣的託管平台時,就能理解它們在幕後為我們處理了多少複雜性。但透過自建基礎設施,我們獲得了更大的靈活性和控制力,這在特定場景下是無價的優勢。
在技術選型時,我特別注重元件的成熟度和社群活躍程度。例如,選擇Prometheus和Grafana這樣的業界標準工具,不僅確保了穩定性,還讓團隊能夠受益於龐大的社群資源和最佳實踐。
隨著應用程式的演進,我們也在持續最佳化這個基礎設施。目前正在評估加入Jaeger來增強分散式追蹤能力,這將進一步提升我們的可觀察性架構。在實際營運過程中,這些工具的價值遠超過其設定的複雜度。
建立這樣一個完整的Kubernetes基礎設施確實需要投入大量心力,但它為我們的應用提供了一個靈活、可擴充套件與高度自動化的執行環境。這些努力最終轉化為更好的服務品質和更高效的營運效率。
轉移過程中的重大挑戰
在非生產環境的遷移相對簡單,因為我們可以接受一定程度的停機時間。過程大致是先啟動 Kubernetes 服務,接著切換 DNS,最後關閉 ECS 服務。理論上這應該是一個零停機時間的安全切換流程,在非生產環境中確實也是如此。
然而,當我們進行生產環境遷移時,卻遇到了一些意料之外的嚴重問題。主要面臨兩大挑戰:防火牆設定與執行個體類別選擇。
防火牆設定的調整
在防火牆方面,我們原本採用了相當嚴格的 ModSecurity 設定。這個決定雖然出發點是好的,但實際執行時卻帶來了許多問題。經過仔細評估後,我們只需要停用少數幾個規則,主要是與傳入資料相關的部分。這讓我深刻體會到,在安全性與可用性之間找到平衡點的重要性。
執行個體效能的最佳化
在執行個體類別方面的問題則花費了我們數天時間來調整。在 ECS 中,資源設定相對簡單,只需指定所需的 CPU 與記憶體數量。我們在 Karpenter 中採用了相同的設定方式,系統也確實依照成本最佳化的原則進行了資源分配。
但問題在於,並非所有的 CPU 效能都是相同的。這個差異對應用程式的效能影響相當顯著。建議開發者可以參考 Vantage 的執行個體比較表,以更全面地瞭解不同執行個體類別的特性。
經驗教訓
回顧這次遷移過程,我認為我們應該:
- 保留舊叢集更長時間,讓流量轉移更平緩
- 一開始就採用較寬鬆的防火牆規則,再逐步調整
- 更謹慎地評估執行個體類別對效能的影響
持續最佳化與調校
成功完成遷移後,玄貓開始進行系統最佳化。Kubernetes 提供了數千個可調整的引數,但調校過程就像玩蛇梯棋一樣充滿變數。有些調整能讓應用程式效能提升 20%,有些卻可能導致系統當機。
檢視設定歷史記錄,在第一個月內我們進行了超過百次的調整。儘管調整的幅度隨時間逐漸縮小,但這個數字仍然相當驚人。最令人欣慰的是,透過精確的資源調校,我們同時達成了降低雲端費用與改善回應時間的目標。
關鍵設定最佳化專案
在大規模轉移過程中,有幾個重要的設定專案最初被我們忽略:
Karpenter 中斷機制的設定,這對於避免高峰時期的嚴重變動至關重要。
與 MongoDB Atlas 的 VPC 對等連線設定。這個疏忽導致較長的網路往往返時間和較高的資料傳輸費用。
環境變數中的打字錯誤修正,這是一個常見但容易被忽視的問題。
自動擴充套件的上限調整,確保系統能夠適應流量峰值。 在容器化環境中佈署 Meteor 應用程式,玄貓在處理多個大型專案時,發現記憶體管理與 WebSocket 設定是兩大關鍵。讓我們探討這些重要的技術細節。
容器記憶體指標的最佳化
在初期建置階段,我們常直接將非正式環境的設定套用到正式環境,這是一個常見的錯誤。經過多次專案經驗,玄貓建議將記憶體監控指標從 container_memory_usage_bytes
改為 container_memory_working_set_bytes
。這個改變有其重要原因:
container_memory_usage_bytes
包含了可被回收的快取記憶體container_memory_working_set_bytes
才是真正會觸發記憶體不足錯誤的指標- 使用正確的指標能夠更準確地進行自動擴縮
Meteor 應用程式的容器化佈署
基礎建置流程
容器化佈署 Meteor 應用程式需要注意幾個重要環節:
- 建立 Docker 映像檔
- 使用 Helm 圖表 而非直接建立 Kubernetes 服務
- 實作優雅停機制
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 升級協定支援
效能最佳化建議
- 設定
DISABLE_SOCKJS=1
環境變數,除非需要支援舊版瀏覽器 - 整合
@meteorjs/ddp-graceful-shutdown
實作平滑關機 - 實作批次斷開使用者連線,避免流量尖峰
經過多個專案實踐,這些設定能有效提升應用程式的穩定性與效能。對於現代網頁應用程式來說,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 預設記憶體管理機制的限制:
- 記憶體釋放需要消耗 CPU 資源
- 系統傾向於保留記憶體而非立即釋放
- 只有在接近記憶體上限(預設 2GB)時才會強制進行垃圾回收
- 當強制垃圾回收時,約 80% 的 CPU 資源會被佔用,導致服務反應遲緩
jemalloc 解決方案
在嘗試多種最佳化方案後,玄貓決定匯入 jemalloc 記憶體設定器。這個決定帶來了立竿見影的效果:
- 平均記憶體使用降低約 20%
- 系統能根據流量自動調整記憶體使用
- 夜間低峰期可以有效縮減資源使用
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
內容解密
RUN apk add --no-cache jemalloc
:- 使用 Alpine 的套件管理器安裝 jemalloc
- –no-cache 選項確保不會在映像檔中保留快取檔案
ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2
:- 設定環境變數使系統預載 jemalloc 函式庫 - 這會覆寫系統預設的記憶體設定器
這個最佳化過程讓玄貓深刻體會到,有時候系統效能的提升不一定要透過複雜的程式碼改寫,選擇合適的基礎元件同樣能帶來顯著的效能提升。在實際生產環境中,這個改動不僅提升了系統的穩定性,還降低了維運成本。
記憶體管理最佳化是一個持續性的過程,選擇合適的記憶體設定器只是第一步。在實際應用中,還需要持續監控系統效能,根據實際負載情況進行調整。經過這次最佳化經驗,更加確信在處理大型 Node.js 應用時,基礎架構的選擇與設定同樣重要。