AmazingTalker 的容器化之旅

AmazingTalker
AmazingTalker Tech
Published in
28 min readMar 30, 2022

前言

AmazingTalker 提供的是線上教育媒合平台的服務,從公司創立發展至今,隨著用戶越來越多,網站流量也隨之不斷上升;在此過程中,網站系統也因應實際需求 & 所面臨的挑戰,作過了不少的調整,過程中 AmazingTalker 工程團隊獲得了不少寶貴的經驗,而我們也認為這些經驗很值得對外分享,希望對於面臨相同問題的 IT 夥伴們有所幫助,因此有了此篇文章的產出。

本篇文章的內容主要在於分享 AmazingTalker 網站系統在發展之初,從原本在 VM 上運行的 Monolithic 架構,逐步拆分並邁向容器化的過程,以及在轉移過程中遇到的各式各樣的問題,並詳細的說明我們是如何因應 & 排除每一個問題,以完成服務容器化的目標。

最初,我們遇到了什麼問題?

在開始描述問題之前,首先要了解一下最原始的系統架構究竟是長什麼模樣,就會比較容易理解為什麼會有後續問題的產生。

原始的系統架構

在容器化之前的架構,所有服務都是構築在 AWS EC2 VM 上的,而在 VM-based 的架構時期還分成了兩個階段,以下進行詳細說明。

前後端服務介紹

前端服務的部份(後面簡稱 Nuxt),我們使用了 Nuxt [1] 作為開發框架,加上服務運行在 multi-core VM 上,因此我們搭配了 PM2 作為 Process Management 之用。
後端服務的部份(後面簡稱 Rails),我們使用了 RoR [2](Ruby on Rails,以下簡稱為 Rails) 作為開發框架,並搭配 Nginx + Passenger 作為 Web Application Server。

在此篇文章中,僅列出 AmazingTalker 網站系統中最重要的兩個前後端服務,為避免篇幅過長難以閱讀,其他先暫時不提到

EC2(frontend svc + backend svc)

AmazingTalker 在草創初期還沒有太多流量的時候,自然不可能花費龐大成本去建立一個複雜的網站系統,因此一直都是使用 MVP(Minimum Viable Product) 的方法,不斷地小步快跑並迭代發展,因此最早的系統架構,是將前後端服務都放在同一個 VM,如下圖:

在此架構下,會有以下幾個特性:

  1. 外部的 API request,可透過 ELB 進行分流,平均分散到多台機器的 Rails [2] (Backend Service)
  2. 當 request 從外部進來到 Nuxt [1](Frontend Service) 時, 所產生的內部 API 呼叫,都是在同一台 VM 完成

由於上述第二點特性的關係,自然也帶來了以下的缺點:

  1. 因應逐漸增加的流量 & 負載,每次擴展(scale out)的單位都必須是 Frontend Service(Nuxt) + Backend Service(Rails),即使某個服務負載很低,也是得跟著擴展,無形之中造成了資源的浪費
  2. 若 VM A 的 Rails service 出現問題,那 request 送進 VM A 的 nuxt 時,內部 API call 也會送進出問題的 Rails servie,那網站就出現 HTTP 5xx Error 了

因此,在 VM-based 的基礎下,我們繼續往下推進到下一個階段。

EC2(frontend svc) + EC2(backend svc)

在此階段中,我們將 Frontend Service(Nuxt) & Backend Service(Rails) 分別拆開到不同的 VM 存放,解決了放在一起所造成的問題,此時的系統架構圖如下:

隨著使用 AmazingTalker 平台的用戶越來越多,加上廣告的推廣,流量自然也越來越多,此時我們從監控系統觀察到每日流量的高低峰,有著高達 5 倍的差距,但我們為了因應最高峰的流量,一直維持著最大的 VM 數量,這也表示著在雲端資源使用成本上,有著巨大的改善空間。

由於在 AWS 上使用資源,分分秒秒都是需要計費的,為了進一步更有效的利用硬體資源,我們開始思考架構的調整改善,這項決定也成為了讓我們著手開始評估將服務容器化的契機。

原始系統架構為何無法滿足需求?

需要手動擴展

在原始 VM-based 的架構中,VM 的 Scale out/in 都需要手動進行,這狀況造成了以下幾個問題:

  1. 對於突如而來的流量,無法很快速的因應
  2. 為了確保網站服務可以在每日在峰值流量時段可以正常運行,需要將 VM 的數量調整到可以服務最大使用者的數量;但從監控數據中可觀察出,實際上流量高峰 & 低峰的差距有 5 倍之多,這表示在流量低峰時,會有很多額外的資源浪費

缺少 Golden Image

在缺少 Golden Image 的情況下,即使 AWS 有提供 Auto-Scaling Group 的機制可以達成 VM auto scale out/in 的目的,也無法利用此機制來協助我們快速進行動態資源的調度,因此這也是造成上一個問題(需要手動擴展)的主因。

佈署流程中的 downtime

這問題來自於服務的進版流程,原本的進版流程如下:

  1. Developer 在本地開發完程式,送進 GitHub
  2. 在 CI 流程中完成測試
  3. 在 CD 流程中,將新版的程式佈署到所有 VM 上(搭配 Capistrano [3] ),並重啟服務

由於外部的 request 會不斷的透過 ELB 送進來,而上述第三個步驟中,在重啟服務前,並不會將 VM 從 ELB 中移除後再重新註冊,因此在重啟服務的過程中,可能正在使用網站的使用者,就會出現短暫 downtime 的狀況(約 1~2 秒)。

系統退版問題

退版流程跟服務進版很像,前兩個步驟更換成 revert PR,最後還是需要 Capistrano 將服務佈署到所有 VM 上並重啟服務,同樣也會造成系統 downtime 的問題發生。

當時我們有哪些選擇?

回顧上述提到的問題,當時思考解決方案時,我們先決定了預期希望達成的目標,共有下面幾個:

  1. 服務可自動擴展
  2. 服務進退版不會有 downtime
  3. 符合未來技術發展的潮流 & 方向
  4. 服務具備可攜性 (portability)
  5. 降低維運成本

當時內部在討論過程中,有曾經出現過以下三個解決方案:

  1. EC2 + Auto Scaling Group
  2. ECS (Elastic Container Service)
  3. Kubernetes

以下就每個解決方案的特性 & 決策結果進行詳細說明。

EC2 + Auto Scaling Group

這是以原有 VM 為基礎,搭配 AWS Auto Scaling Group 的設計,來達成自動擴展的目標。

關於預期目標的可達成率,我們進行了以下評估:

  1. 搭配 CloudWatch Monitoring,達到目標 1 是肯定沒問題的
  2. 目標 2 要讓服務進退板不會出現 downtime,那就跟 Auto Scaling Group 沒關係,需要在 CD 流程中實現 Target register & de-register 的功能才行,因此這部份的功能需要額外進行開發
  3. 目前業界潮流都走向容器化 & 微服務化,停留在以 VM 為基礎的環境中,不算是符合目標 3;既然不符合目標 3,那後續的目標也就沒必要繼續評估了

ECS (Elastic Container Service)

從這裡開始,正式進入了容器化的選項;當時評估這個選項時,大概有以下幾個理由:

  1. AWS 提供的 Serverless Container Orchestration Platfoem,可以不用考慮底層 infra 的維運,只要專注在服務的佈署 & 服務之間的相互關係即可
  2. 相較於 Kubernetes,算是較為容易上手的解決方案,學習成本相對較低

但缺點其實也顯而易見:

  1. 這是屬於 vendor-locking 的方案,當選擇了此方案,就表示被綁定在 AWS 平台上,失去了未來遷移到其他雲平台的彈性(即使更換雲平台的機會不高,但我們當時也希望可以保留此彈性)
  2. 在 ECS 上可疊加的工具,僅能使用 AWS 所提供的;相較於目前 Kubernetes 目前現存的工具生態系的規模,差異相當巨大
  3. 當前團隊中已經有同仁熟悉 Kubernetes,但卻沒有人會 ECS,因此這也代表選擇了 ECS 會需要從頭開始學習

Kubernetes

針對上面四個需求,Kubernetes 的確可以很輕鬆的滿足:

  1. 透過 HPA(Horizontal Pod Autoscaling) 的機制,達成自動擴展目標
  2. Deployment Rolling Upgrade 加上 Readiness/Liveness Probe 機制的設計,讓服務進退版的過程中不會產生 downtime 的狀況
  3. Kubernetes 是目前最火紅的 Container Orchestration Platform,圍繞著 Kubernetes 平台所衍生出來的工具生態系相當龐大,對於想要達成各種目標,大多都很容易可以找到對應的工具來完成
  4. Kubernetes 本身就是個獨立於各個雲平台的工具,在上面運行的服務都具有高度的可攜性,之後即使要轉移雲平台所需要額外花的成本也很小

由於 Kubernetes 確實可以滿足我們的需求,同時也解決當時遇到的痛點,因此內部討論後的結果,最後就是以 Kubernetes 作為承載服務容器化的運行平台。

我們最後的選擇

從上面的評估中,我們選擇了 Kubernetes 來承載服務,但最後其實我們選擇了 AWS EKS 並非自建 Kubernetes cluster,原因有以下幾個:

  1. EKS 是 AWS 提供的 managed Kubernetes 解決方案,讓使用者不需要管理 control plane 的部份,只要專注在服務本身即可;可以減少維運人力的負擔
  2. EKS Node Group 已經與 AWS Auto Scaling Group 直接整合,可以讓使用者直接透過 HPA 實現自動 worker node scale out/in
  3. 服務運行過程中難免會需要使用到 AWS 其他服務(例如:儲存資料到 S3),EKS 可以透過 IAM Role for service accounts [14] 的方式很方便取得 AWS 資源存取權限,服務不需要管理 AWS Access/Secret Key 就可以存取 AWS 相關資源,整體來說安全性更為提昇

最後,我們最後選定 AWS EKS 作為容器運行的平台,並開始逐一將服務進行轉移工作。

第一個容器化的服務:Rails

服務介紹

Rails 是 AmazingTalker 網站系統中屬於 Backend API service,它是由 Ruby 所開發的 Web 開發框架,用來提供內外部 API 需求,以及手機 App 所需要的 API 呼叫查詢。
與其他程式語言或開發框架比較的話,它已經設想好各種情境,讓開發者可以用簡短的語句來實現一個完整的功能。

曾經嘗試過的工具組合

由於 AWS EKS 有支援 Fargate,可以讓使用者不用管理 worker node,只要專注在服務需要耗費多少資源即可;這設計著實很吸引人,因此在評估如何運用 EKS 時花了些時間研究如何讓 Rails 運行在 Fargate 上。
但經過一段時間的研究並測試後,最後決定還是棄用 Fargate,改用一般的 worker node 的方式承載服務,原因有以下幾個:

  1. 考量到未來 Distributed Tracing & 其他網路相關的維運需求(例如:Rate Limiting、Circuit Breaking … 等功能),因此需要導入 Service Mesh 的解決方案
  2. 目前可用在 AWS Fargate 上的 Service Mesh 方案,僅有 AWS 提供的 Service Mesh & X-Ray 兩個服務,與目前 Kubernetes 常用的 Service Mesh 解決方案(例如:Istio、Linkerd … 等等)都不相容
  3. 考量到之前提到的服務可攜性,因此還是希望採用比較泛用的 Service Mesh 解決方案,因此最後並沒有選用 AWS Fargate 來承載服務

導入初期狀況

由於 AmazingTalker 內部開發是以 Docker Compose 的方式建立本地開發環境的建置,因此將 Rails 的程式 & 環境打包成 container image 的過程中並沒有遇到任何問題,所有的問題都是將其放到 EKS 上後,開始將外部的 API 流量導入時所出現的,以下就針對我們當時所遇到的問題,逐一詳細說明。

為了確保線上服務不受到太大影響,在開發環境測試功能沒問題後,我們透過 Route53 提供了 Weighted Routing 功能,將 5% 左右的外部 API 流量轉到 EKS 上的服務進行驗證測試

此時的系統架構圖如下:

導入過程中遇到的問題 & 解決方案

Rails Pod OOM

當正式流量進入 EKS 後,很快我們遇到的第一個問題就是 Rails Pod OOM 的狀況。
Rails Pod 在短時間內就耗用了大量的記憶體,由於我們有設定 Memory Request & Limit 的關係,很快就遇到 Pod OOM(out of memory) 的狀況,因此就看到 Rails Pod 一段時間就會強制被 Kubernetes 砍掉重啟。
關於記憶體問題,當時在 EC2 上運行時其實就有發生,主因是 Rails 本身的設計以及開發的行為會大量在記憶體上操作,就會容易出現 memory fragmentation,我們找到的解決方案是使用 jemalloc [12],在 Rails API 有緩解不少,而在 Sidekiq 中更是有顯著的改善。雖然使用 jemalloc 在效能會稍微慢一點,但在記憶體管理方面會比較謹慎,以產品性價比而言,下降的記憶體是非常有感的。
那因為我們之前是使用 RVM(Ruby Version Manager) [13] 方式安裝 Rails 搭配jemalloc,而運行 Rails Pod 用的 container image 因為沒有將此工具打包進去,自然就失效了。
這問題的解決方法也很簡單,只要在建置 container image 的 Dockerfile 中指定安裝 libjemalloc2 後,再設定環境變數 ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2,就工具就可以在 EKS Pod 上生效,記憶體耗用的問題也就隨之解決了。

Elasticache Redis Network Latency

AmazingTalker 內部是透過 New Relic APM 來進行系統效能的調校 & 監控,在 Rails 嘗試轉移到 EKS 運行的過程中,API 的流量同時分散到原有的 EC2 & EKS 上,同時我們也有把相關的服務運行數據上傳到 New Relic 平台進行監控 & 觀察;然而我們發現,有某幾隻 API 在性能報告中,運行在 EKS 上比起運行在 EC2 慢了一倍以上,仔細觀察這些 API 的特性,其特徵都是呼叫 Redis 次數很高,平均都會呼叫 Redis 60~70次,且在報告中也明確指出兩者在 Redis花費的時間,EKS 比 EC2 總計多出了 150ms ~ 250ms,等於每呼叫一次就慢 2ms 以上。

我們認為這個多出的時間是不太合理,因此懷疑是底層架構的問題,實際上我們在EC2 以及 EKS Rails Pod 中各自安裝了redis-tools,在兩者同為 m5a.xlarge 規格下,做 redis-benchmark 後得到以下數據:
EC2為82142.27 requests per second
EKS為44187.18 requests per second

在同規格機器下,Pod in EKS 對 Redis 存取的 RPS 就比在 EC2 足足少了快一倍,也同時驗證了 New Relic 的數據報告是正確的。換句話說,要取得相同的 response time,程式碼得少呼叫一半的次數才能跟上在 EC2 上運行的速度,且若能少一半,自然在 EC2 上又能快一半,因此要解決的根本問題,還是得找出這個EC2 與 EKS 實際上的差異所在。
雖然我們知道,原本在 Kubernetes 上,Pod 之間的網路通訊是走一層 overlay network,效能比起從 TCP/IP Layer 4 直接連接自然會稍慢點,但慢到 2 倍左右則是一個很異常的現象,因此花了點時間研究了 AWS VPC CNI [6] 的設計,實際上這個 CNI 的設計並沒有多構築一層 overlay network,因此 EKS Pod 的網路效能應該要能夠接近 EC2 才對,因此並沒有往 CNI 的方向去追查。
接著為了追查問題的根本原因,我們嘗試的將 EKS Pod → Redis 這段的網路設定調整的盡量單純來進行測試。
由於當時我們已經安裝 Istio 作為 EKS 的 Service Mesh 解決方案,分析了 EKS Pod 的對外連線鏈路,唯一與其有關連的只有兩個 Istio 元件,分別是:

  1. Egress Gateway
  2. Pod sidecar Envoy proxy

由於一開始我們並沒有安裝 Istio Egress Gateway,因此決定移除 Istio sidecar Envoy proxy 來進行驗證;我們同時在 EC2 & EKS Pod 中進行 Redis benchmark,得到了以下的結果:

  1. sidecar Envoy proxy 存在時,Pod Redis benchmark 的數據僅有 EC2 的一半不到
  2. 移除 sidecar Envoy proxy 後,Pod Redis benchmark 的數據已經達到 EC2 的 95% 以上的數據

從上面的驗證測試過程中,我們找到了 EKS Pod 存取 Redis 效能不彰的主因,於是再到 Istio 官網中找到關於 Envoy proxy performance 相關的文件[7],其中提到一個重點:

In the default configuration of Istio 1.13.2 (i.e. Istio with telemetry v2), the two proxies add about 1.7 ms and 2.7 ms to the 90th and 99th percentile latency, respectively.

上面提到了多了 Envoy proxy 後,每一個 request 會增加 1.7ms ~ 2.7ms 的延遲。

因此我們找到了造成 redis 存取延遲的主要原因,Istio Envoy proxy 會讓每次存取 Redis 時,都其增加 1.7ms ~ 2.7ms 的延遲,雖然單次的延遲沒很大,但乘以數十次後,就呈現出高達 150ms ~ 250ms 的整體延遲。
我們最後選擇暫時先移除 Rails sidecar Envoy proxy 的方式來處理這個存取延遲問題,但回歸問題的本質,後續還是會安排調整程式,讓其調整存取 Redis 的行為,畢竟維持原本的存取模式,會有兩個問題:

  1. 無法啟用 sidecar Envoy proxy,會讓 Istio 某些功能失效(例如:Destination Rule)
  2. 無法讓 Rails Pod 跨 AZ 存放,因為跨 AZ 存取 Redis 會造成更大的存取延遲,那整體系統就無法進行 HA 的設計

Rails Passenger 轉移至 Puma

將 Rails 的 process management 工具從 Passenger 換到 Puma 是一直內部都有的討論,尤其 Puma 有提供 multi-thread 的設計,我們知道 multi-thread 在同一台機器上能更有效率的使用資源,若是僅使用 worker 的話,記憶體相對會消耗比較多,且同時能接受的 request 數量也無法太多。
加上因為 Passenger 需搭配 Nginx 一起安裝,相較於 Rails Puma 原生就有支援可以直接處理 http request,整個 Rails conainer image 中需要安裝的東西就相對比較少。
但是當服務搬移到 Puma 後,首先就遇到了效能問題,在尚未調校之前,我們採用的一個 m5a.xlarge(4 core, 16GB RAM) Worker Node ,上面啟動 了 3 個 Rails Puma Pod,每個 Pod 設定 Thread 15 + Worker 1 的方式;原先我們以為 1 個pod 跟 1 worker 是等價比的,而一個 node 多個 pod 在部署時會快很多,不用另外啟機器來替換,這也是當時我們這麼選擇的原因。
但這樣的規格,在我們採用 50/50 流量來比較 EC2 Passenger & EKS Puma 的情況時,發現效能差了不少,因此我們後續又繼續做了不少研究與調整。

針對 multi-Pod in 1 Worker Node 的規劃,我們花了一段時間研究後發現,Puma 本身針對多個 Worker 就有做一些優化跟共享,多個 Worker 跟多個 Pod,記憶體是差不少的,加上啟動每個 Pod 本身就會產生一些基本資源用量的消耗,這些都是重複浪費的,所以我們最終採用一個 1 Pod in 1 Worker Node 的設計。
針對 Puma Thread 與 Worker 數量這件事,Puma 的官方文件上有建議 Thread 預設 5 會是一個不錯的數字,沒特別情境不用特別調整,事實上我們測試後的結果也是顯示如此。
而 Worker 的數量則是經過不斷測試調整後,我們最終設定在 9,發現是對於機器的 CPU 使用率最好的數字,不管是在 Auto Scale out/in 的敏感度,或是效能方面;而這樣的設定也就代表一台機器最多能同時處理 9*5=45 個 request,比起原先 Passenger 設定 20 Worker 多了一倍以上。

Request 排隊問題

另外我們發現在同一個 API request下面,Passener 總是能維持一樣的速度,而使用 PUMA 時速度就有點飄忽不定,後來經拆解後發現 Passenger 本身就有實作一個排隊機制,能讓每一個 request 總是最快被執行,相比 Puma 就沒有這種行為,這會導致在流量瞬間變大的時候,會隨機排到很久的隊伍。

為了驗證這個推測是否正確,我們是利用了 Newrelic 提供的排隊監控,只要加上一個名稱為 X-Request-Start 的 HTTP header [15],New Relic 就能夠協助我們監控每個 API request 排隊的時間,而這個調整我們使用 Istio 加在 Ingress Request Header 的方式完成,在 Rails Puma 前加上去,而透過這個機制也驗證了我們的假設。

最後,解決方案是將 Puma 升級到 5.6 版本,它有提供 wait_for_less_busy_worker 的參數,能將 request 導入到比較不忙的 worker,雖然比起 Passenger 排隊機制還差一點,但那是因為 Passenger 本身並沒有 Thread 的因素,而後續我們也證明這樣的設定是有效的,也搭配我們在前面已經將調整為 1 Pod in 1 Worker Node 的架構,因此可以最大限度的增加 worker process,讓 Puma 更好的決定 request 要由誰來處理。

第二個容器化的服務:Nuxt

服務介紹

Nuxt 是 AmazingTalker 網站系統中屬於 Frontend service,我們選擇使用 Nuxt 這個 SSR Framework 進行開發,主要目的是透過伺服器端渲染的方式來呈現前端畫面之外,同時也可以提升 SEO 的成效。

導入初期狀況

與 Rails 相同,將程式打包成 container image 放到 EKS 上運行並沒有遭遇到太大的困難,因此我們很順利就將 Nuxt 放到 EKS 上開始運行。

而由於 Nuxt 是以 SSR(Server-Side Render) 的方式提供頁面,因此會產生一些內部的 API call,所以當 request 進入到 EKS Nuxt 後,就會有所調整。

在原本 EC2 的環境下,我們需要額外為 API service(Rails) 額外提供一個 ELB 的設定,來進行負載均衡,此時的架構如下:

當 Nuxt & Rails 都移入到 EKS 後,就可以直接利用 Kubernetes 內部的 Service 為 Rails 提供負載均衡的功能,因此架構就調整為:

透過上面的調整,就減少 ELB & Target Group 等相關設定的維運,並經過反覆測試驗證後確認此調整是可行的。

最後完整的架構圖如下:

接著我們開始將一部分流量透過 Route53 Weighted Routing 導入 EKS,經過一段時間的運行都很正常,因此我們也很放心的逐漸的加大流量比例,一直到所有流量導入 EKS 為止,但事情總是不會一直很美好的發展下去,過了一陣子開始出現了一些奇怪的現象,以下詳細說明我們當時遇到的狀況 & 解決方法。

導入過程中遇到的問題

偶發性的 Pod OOM

在 Nuxt 導入 EKS 運行一段時間後,網站三不五時會出現時好時壞的狀況,當時的特徵如下:

  1. 大約維持 5~10 分鐘左右,接著就會恢復正常
  2. 並非發生在特定的時間點,出現的時間並沒有規律性

當時觀察到網站發生問題時,所有的 Nuxt Pod 都處於 Kubernetes Liveness Check 無法通過的狀態,這表示當 request 進入到 Kubernetes 內部時,是不會被導入 Liveness Check 無法通過的 Pod,因此就倒站了。

當時緊急的應對方式就是暫時手動 scale out Nuxt,增加 Pod 數量,讓所有的 request 可以被處理,但過了約 5~10 分鐘後,這問題就消失了。

當時為了追查此問題,我們從以下兩個地方檢查 access log,分別是:

  1. CloudFront
  2. Nuxt sidecar Envoy proxy

後來發現在 Pod OOM 問題發生的那幾分鐘,有大量的 request 同時湧入,而且在 HTTP Request Header User-Agent 中顯示是 Facebook 爬蟲(例如:facebookexternalhit/1.1),當時證據都指出,我們似乎被 Facebook 爬蟲給爬到倒站了。

當時覺得有點不可思議,Facebook 爬蟲怎麼會有這樣的行為,當時一度認為可能是惡意爬蟲假冒 Facebook 爬蟲所造成的,但上傳查詢了相關資訊後,發現也是有其他人遇到相同的狀況 [9],接著又查到了 Facebook 分享關於其爬蟲資訊的文章 [8],並從 Log 撈出來源的 IP 與文章中透過 AS Number 查詢到的 Facebook IP 網段相比較,結果都是匹配的,這表示這些 request 的確都是由 Facebook 各地的機器所送出,只是有時候短時間的數量會多到前端服務扛不住,造成倒站的狀況發生。

突發性流量的應對

除了上面提到的 Facebook 爬蟲,我們也需要應對真實使用者所產生的突發性流量;在平常學生上課的峰值時段,偶發性的也會出現 HTTP 5xx Error,雖然發生的次數並不多,但也是個需要徹底解決的問題。

我們如何解決問題

釐清問題的本質

關於上面遇到的問題,其實從本質上來看,就是要處理瞬時流量這件事情,這件事情我們內部討論評估過後,就成本 & 合理性,進行了以下分析。

  1. 首先是從成本方面考量,由於 Nuxt 本身是個 SSR framework,運行時的資源消耗本來就比較大,若是希望完全由 Nuxt 來應付這件事情,肯定事先要多準備冗餘的資源來因應,需要更多的成本支出來解決此問題,這樣的處理一點都不划算,因此勢必借助其他方式解決。
  2. 第二點則是合理性,在第一個問題中,我們遇到了 Facebook 爬蟲的瞬時流量,但同樣也讓我們思考,有需要完全滿足 Facebook 爬蟲的所有 request 嗎? 主要的目標應該是放在不影響正常用戶使用,並以有限資源提供給 Facebook 爬蟲即可,因為我們其實也沒有想要封鎖所有 Facebook 爬蟲的流量。

預期達成的目標

釐清了問題的本質後,於是我們就擬定了下面幾個預期目標:

  1. 讓 Facebook 爬蟲的流量不要影響正常用戶使用 AmazingTalker 網站服務
  2. 為 Nuxt Pod 提供一個 Request Queue 的方案,當瞬時流量發生時,可以提供一個緩衝,讓 Nuxt Pod 可以慢慢消化,而不要造成負載過重導致連 Liveness Check 無法通過

執行解決方案

根據預期達成的目標,我們花了些時間進行了研究,後來決定透過以下方式來進行:

  1. 為 Nuxt Service 新增 Istio Destination Rule [10],透過 Connection Pool 的相關設定,讓 Nuxt sidecar Envoy proxy 提供了一個 Request Queue 的效果,在瞬時大流量進來的狀況下,不會讓 Nuxt Pod 照單全收,而是可以逐一消化,從此之後 Nuxt Pod OOM 的狀況就再也沒出現過了。
  2. 為爬蟲流量新增 Nuxt Bot Deployment,在這個 Kubernetes Deployment 中,只會有一個 Nuxt Bot Pod,接著我們透過調整 Istio Virtual Service [11] 的方式,讓 Facebook 爬蟲流量導向新的 Nuxt Pod,因此爬蟲的瞬時流量再大也不會影響到正常用戶使用 AmazingTalker 網站系統。
  3. 原本的 Nuxt Bot Pod,因為 Facebook 爬蟲的瞬時流量,初期也是會一直出現 Pod OOM 的狀況,後來比照第一個解決方案,我們為 Nuxt Bot Pod 也加上了 Destination Rule 的設定,後來也就再也沒有 Pod OOM 的狀況出現了。

心得 & 總結

過往看過不少其他公司導入 Kubernetes 的心得,大多數都是發生在以下幾類的問題:

  1. 整個 CD 的流程需要因應 Kubernetes 的特性重新調整
  2. Kubernetes 是個相當複雜的平台工具,學習成本高,因此導入過程中需要有熟悉此工具的工程師協同合作
  3. 長期來看,除了 Ops 之外,屬於 Dev 的角色也需要了解 Kubernetes 的架構 & 運作特性,才可以有效發揮 Kubernetes 可帶來的優勢

從維運人員的角度來看待 Kubernetes 這工具,除了學習成本高之外,它在維運方面可帶來的優勢可謂相當的大,例如:

  • Auto Scaling
  • Self Healing
  • Auto Service Discovery
  • Service Load Balancing

光是上面所提的幾項功能,在 VM-based 的架構下,要實現以上功能可是要花相當多的功夫並搭配不少工具才能完成,更別說還有很多其他沒提到的部份。

加上 Kubernetes 是目前最熱門的 Container Orchestration Platform,圍繞此平台所發展出來的生態系統極為龐大,很多開發 & 維運所需要的功能很容易就可以找到對應的解決方案;因此我們當時才會決定導入此工具。

在導入 Kubernetes 之前,其實也認知到一定會遇到不少問題,還好在整個導入過程中,AmazingTalker 的工程師夥伴們也都相當的幫忙,同心協力一同排除導入過程中遇到的種種問題,從沒遇過任何推諉搪塞的狀況,大家都是為同一個目標一起努力,讓整個導入過程中遇到的問題可以迎刃而解。

最後,總是要工商一下,在 AmazingTalker有一群為公司的長期目標一起努力的工作夥伴們,未來還有很長的一段路要走,過程中我們也會不斷的迭代進步並內化到公司的所有夥伴身上。

🔥若是您對 AmazingTalker 有興趣,並有很多想法與熱情來改善 AmazingTalker 的線上服務品質,歡迎到我們的徵才網頁看看。

References

  1. Nuxt — The Intuitive Vue Framework
  2. Ruby on Rails — A web-app framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern
  3. capistrano/rails: Official Ruby on Rails specific tasks for Capistrano
  4. PM2 — Advanced, Production Process Manager For Node.JS
  5. Passenger — Enterprise grade web app server for Ruby, Node.js, Python
  6. aws/amazon-vpc-cni-k8s: Networking plugin repository for pod networking in Kubernetes using Elastic Network Interfaces on AWS
  7. Istio / Performance and Scalability
  8. Facebook Crawler — Sharing
  9. web crawler — why facebook is flooding my site? — Stack Overflow
  10. Istio / Destination Rule
  11. Istio / Virtual Service
  12. jemalloc memory allocator — GitHub
  13. RVM: Ruby Version Manager — RVM Ruby Version Manager
  14. IAM roles for service accounts — Amazon EKS
  15. Configure request queue reporting | New Relic Documentation

--

--

AmazingTalker
AmazingTalker Tech

AmazingTalker 致力建立一個全方位的線上教學平台,讓任何人都可以找到最適合自己的家教,以不斷提昇線上教師收入為使命。始於 2017 年,目前平台上約有 8000 名,超過 100 萬名學生,以及進行 550 萬堂語言、升學等課程,橫跨 190 個地區。