包包baobaolin.com
date
entry
012
topic
infrastructure
rev

一台機器跑七個專案:哪些是隔離的,哪些不是

把多個客戶的後端塞在同一台 EC2 上,對小團隊是合理的——省錢,而且一個人維護得動。真正的風險不在「共用」這件事,在於哪些東西共用跟直覺不一樣。

先說為什麼會這樣。七個專案七台機器,是七份 OS 更新、七份監控、七份憑證輪替、七份磁碟監控。對一個人維護的規模來說,那個運維成本比伺服器帳單貴得多。所以共用一台、用 nginx 依 domain 分流、每個服務一個容器,是合理的決定。

問題不在這個決定,在於做完之後大家會開始假設它們互不影響。有些是真的,有些不是,而不是的那幾項就是事故的來源。

真的隔離的部分

  • nginx vhost——一個 domain 一份 site config,改 A 的不會動到 B 的
  • 容器——各自的 process、檔案系統、port
  • 應用層的憑證與環境變數——各容器的 env 互相看不到
  • log group——各專案送去各自的 CloudWatch group,查的時候不會混

這些讓人產生「它們是獨立的」的印象。接著是不成立的部分。

不隔離的部分

這份清單才是重點。

  1. 磁碟空間。一個共用池。任何一個服務的 log 暴衝、或累積的 Docker image 沒清,會把整台的 / 填滿,然後所有服務同時開始失敗——寫不了 log、寫不了暫存檔、資料庫連線池報錯。
  2. Docker daemon。它自己出問題(或你重啟它)時,所有容器一起受影響。清理指令也是全域的,打錯一個參數就跨專案。
  3. nginx 的 reload。vhost 檔案是分開的,但reload 是一次全部。任何一份 conf 語法錯誤,整個 reload 失敗——你剛改的那個專案沒生效,而其他六個維持舊設定(如果 nginx 因此起不來,就是七個一起掛)。
  4. OS 與核心。安全更新、重開機、時區設定——一次影響全部。這代表「找一個所有客戶都能接受的維護時間」變成一件真實的排程問題。
  5. 公網 IP 與它的信譽。共用出口 IP。其中一個服務被列入某個黑名單、或被某個 API 供應商 rate limit,其他服務一起承受。
  6. CPU 與記憶體。沒設 limit 的話,一個跑歪的迴圈會把整台拖垮。這個最容易被忽略,因為平常負載都很低。

共用不是問題。把共用的部分當成隔離的,才是問題。

最常見的那一種事故:磁碟

我看過的跨專案事故裡,磁碟佔絕大多數,而且成因永遠是那兩個:沒有輪替的 log,和沒清的舊 image。

df -h /                      # 整體用量
docker system df             # image / container / volume / build cache 各佔多少
du -sh /var/lib/docker/containers/* | sort -h | tail   # 哪個容器的 log 最肥

Docker 的 container log 預設是沒有上限的 json-file driver。一個話多的服務可以在幾週內寫出好幾 GB。全域設定擋掉:

// /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": { "max-size": "50m", "max-file": "3" }
}

注意這個設定只對之後建立的容器生效,既有的要重建才會套用。所以改完要挑時間把容器逐一換掉,不然你以為擋住了、實際上舊的那幾個還在長。

舊 image 的清理要指定條件,不要無腦全清:

docker image prune -a --filter "until=720h"   # 只清 30 天前的
# 不要在共用機器上跑沒有 filter 的 docker system prune -a

然後把 df -h 的門檻做成告警。這件事的價值在於它是唯一一個會同時打死所有專案的原因,值得單獨監控。

改 nginx 設定的順序

因為 reload 是全域的,改任何一個專案的 vhost 都要照同一套順序:

  1. sudo cp site.conf /root/nginx-backup/site.conf.$(date +%s)——備份帶時間戳,而且放在 sites-enabled/ 外面
  2. sudo nginx -t——語法檢查是全域的,它會順便告訴你有沒有別人留下的地雷
  3. sudo systemctl reload nginx——reload 不要 restart

第 1 點的「放外面」不是潔癖。如果那個目錄的 include 樣式是 * 而不是 *.conf,備份檔會被一起載入,於是同一個 server_name 有兩份設定——這種壞法很難查,因為兩份檔案內容都是對的。

另外提醒:這些 vhost 檔案通常不在任何 git repo 裡。repo 裡那份 nginx.conf 多半是給容器內的前端用的,不是這台機器上真正生效的設定。改之前先確認你看的是哪一份。

什麼時候該拆出去

共用是預設,但有幾個情況我會直接拆一台:

  • 合規要求——客戶合約寫了資料隔離或指定區域,這不是技術判斷
  • SLA 差一個等級——如果 A 專案的維護時間必須是深夜、B 專案可以隨時停,綁在一起代表兩邊都拿到比較差的那個條件
  • 流量級距不同——一個服務的尖峰是其他六個的十倍時,資源競爭已經不是理論問題
  • 交接在即——要把某個專案移交給客戶自己維護時,它必須先能獨立存在

成本論述通常會擋下這幾件事,所以要講清楚:多一台機器的錢,跟一次全站事故的代價相比,前者是可預測的固定支出,後者不是。

還有一件事:不是每個服務都在 Docker 裡

共用機器跑久了,總會有一兩個服務是用 systemd 直跑的——當初趕、或那個技術棧不方便包容器。這造成的實際麻煩是偵錯流程不一致:其他服務用 docker logs 或 CloudWatch,這個要用 journalctl -u

問題不在哪種比較好,在於出事的時候你會先用錯的那個指令,然後看到空的輸出,然後懷疑服務死了。把「哪個服務用哪種方式查」寫下來放在一個所有人找得到的地方,這個成本是十分鐘,回報是每次事故省下的那五分鐘困惑。

如果只記得一件事

把「共用的資源」列成一張明確的清單,並且假設它們遲早會同時影響所有專案。 磁碟、daemon、reload、核心、出口 IP——這五樣是你的爆炸半徑。其他的隔離做得再好,也擋不住這五樣裡的任何一個。

修訂紀錄

  1. 首次發布