完成 DB migrate to RDS 後,現在的挑戰是要逐步拆解掉舊的 api server。這邊我們將會使用 docker 包裝原來的單塊系統 code base,並搭配 aws 的 ELB (Elastic Load Balancer) 中的 ALB (Application Load Balancer)。先讓我們來了解一下修改前的架構。
原始架構
這邊以簡單示意為主,原先架構有兩台 EC2,第一台會對外接收所有的 request,然後透過 nginx 去依照 path rule 來作分流,一部份本機處理掉,一部分往下送至第二台 EC2 處理。兩台 EC2 都使用相同的單塊 code base 透過 tmux 啟動多 process,每個 process 給定不不同的 port,讓 nginx 可以分流到對應的 process。
目前這樣的做法有幾個缺點:
- 使用 tmux 起 process 在前景,如果沒有一直盯著,很難知道他還是不是活著。如果死掉了必須要人工進去手動重啟
- tmux 等於負擔了所有進程的存活,如果他死了,全部都死了
- 有多少個 process 就有多少個 port,但沒留下 port 派發的規範。如果增加刪減 process,都必續到第一台手動修改 nginx configuration,並 reload 才能生效
- 起 process 的指令非常長,如果一時間死掉或是要調整,都需要回去看筆記
- 第一台重要性非常高,除了和第二台一樣負責 api server,他還負責分流,如果當機或被攻擊,後面的第二台並不能立刻補上,可用性不高
過渡期架構
於是在有限的修改時間內,我的計畫如下:
- 將原來的單塊系統 dockerize,使用 docker-compose 來啟動,把參數明文記錄下來並 git 版控
- 參考舊有的分流方法,重新設定 port 配置規範給各個 container
- 最終起至少兩台 EC2,都跑一模一樣的 containers。前面檔著 ALB (ELB 的一種),透過 path rule 來分流到不同的 containers
- 過渡期時,ALB 會導流到舊的和新的 EC2(一開始先只跑一台調參數),並逐步調大分流到新的 EC2 比例。
這樣設計有幾個好處:
- 新的兩台 EC2 起的 containers 完全一樣,比如說 new EC2 A 起了兩個 app api container,一個 web api container,另一台 new EC2 B 也是一樣的起法,如果其中一台掛掉,另一台還能繼續擋著,雖然處理能力剩下一半,但至少提供了緩衝時間讓我們來處理。
- 如果想要增加 app api container,只需要將 docker-compose yaml 中一樣的 setting 複製一塊再貼上,port 遞增即可。
- 如果需要手動重啟,只需要使用 docker-compose -f <yml file> restart 即可,統一且好記
- 如果 container 死掉,它會自動重啟復活。
- 前面使用 ALB 來作分流,一方面如果需要調整 path rule,只要透過 aws web console 就可以處理。明確且容易。
- 如果遇到 DDoS 攻擊,ALB 也可以先幫我們擋下來,且對外 ip 再也不是我們的 api server 的 EC2,可以增加安全性。
但也有幾個缺點:
- 如果想要增加 container,如 app api container,需要進去 EC2 裡面修改 yml 檔,並手動 docker-compose up -d,讓他建立新的 container,然後還需到 ALB 那邊增加 target group,如此 ALB 才會導流到新的 container。這些操作需要時間且人工。
- 雖然 ALB target group 有設定 health check,但無法再 check fail 的時候重啟 container,只能不分流過去。如此雖然可避免 request 導流到壞掉的 container,但負載能力卻會逐步下降,只能人工監控並手動重啟。
如果直接使用 kubernetes 來管理的話會有更多的好處:
- 他可以自動管理 container 要在哪個節點
- 他可以自動依照負載來伸縮 container 的數量
- 不需要再人工設定一堆 port 給 container,只需要使用他裡面的 service 類,就可以簡單無腦的直接下指令讓他伸縮
- 它內建 health check,如果發現 container 不健康,會直接殺掉並建立一個新的來取代他
其實我的做法也是參考了 kubernetes 的概念來設計,但時機因素無法架設 kubernetes,只好直接使用目前最熟悉的 docker-compose 來處理。
困難點與解法
不過原來的設計有幾個難處需要克服
- 兩台的設定檔不一致
- 據前人留下的說法,目前最外面的 EC2 還保留在 ubuntu 12.04 是因為 pdf 套件有系統依賴
- 有些使用者上傳的檔案還放在 local 沒有到 s3,所以有些 process 必須跑在前面的第一台 EC2,如果跑在第二台,就會找不到檔案
第一項,不知道是管理上的問題還是需求,兩台設定檔竟然不一致!所以我只能一行行比對,幸好只是缺漏並不是相衝的設定值,於是我取聯集,重新整理成一份設定檔,並透過 volume 的方式掛入 container。
第二項,關於 pdf 套件的問題,其實我之前也有使用過,就是 wkhtmltopdf,我的印像是他應該沒有依賴系統,於是我直接將單塊系統用 docker 包起來跑,發現並沒有異常,可以正常執行,看來有可能是多次傳達後的誤會。
第三項最為麻煩,依我的設計,新的兩台 EC2 必須要能夠互相替補,所以他們必須是無狀態且一致的。如果有些檔案放在 local,就變成有狀態的 conainer,將會大幅增加調配的難度!
所幸 kubernets 裡面也有為這種情況著想,他有一種 volume,是可以掛 AWS 的 EBS (Elastic Block Store) 或 EFS (Elastic File System),提供給有狀態的 container 使用,如 mongo 等等 db,可以將資料寫入 volume,就算他死掉了,重建後資料還會存在。
但 EBS 和 EFS 有所差異,EBS 只能依附在其中一台 EC2 上,意味著如果有多個 container 要存取,他們就只能跑在同一台 EC2,會降低可用性。而 EFS 概念就是網路硬碟,只要是在同一 region 下的 EC2 都可以存取!如此就能自由調度 container 到不同的 EC2 上來執行!
借鏡了這個概念,我調整了設計,同時掛載 EFS 到舊有和新的 EC2 host 上,docker 的部分 再透過 volume 的方式掛入 container,讓她們都能共享同樣的 local files。
經過實測,可行!只是在 ubuntu 12.04 要掛上 EFS 比較不容易,詳細可參考 EFS 攻略。
最後遇到的問題是,當時還有很多前端的 code 都是放在單塊系統,透過 nginx 來提供給 client。目標是要把前端的 code 放到 s3,但這部分需要更多的時間讓前端去調整,所以應急的方法是如下
- 同樣的 code base 另外使用 nginx image 包成一份 image 並起 container
- nginx 的設定檔從舊有的 copy 過來,但 root path 要調整成 container 裡面的路徑
- ALB 導流規則使用刪去法,先把 api 有關的路徑都過濾掉,剩下的就是前端 SPA 等路由,全部導入 nginx container,讓他去 try files 抓前端的 code 來處理
最終架構
不斷地踩坑和調整,最終的架構圖如下:
數月的實測結果,系統果然穩定下來,如果有錯誤我們也相當好調整,比之前的黑盒子好搞數倍。同時也能使用 jenkins 來做 CD,上版不需要再進入 EC2 裡面下指令,真是一個順暢愉快!
最後還是希望能夠完整轉移到 kubernetes 來自動化管理,釋放我們的管理時間!
延伸閱讀:拯救脆弱系統 – Route 53 設置
參考資料:
Create an Application Load Balancer
Target groups for your Application Load Balancers
使用 amazon-efs-utils 工具
安裝 Amazon EFS 文件系統自動
封面圖片備註:要維持系統的穩定度,架構真的很重要!