跳轉到

Release It

Release It! 的封面
Release It! 的封面

這本書嘗試把一個非常複雜的東西,系統化地闡述出來,那就是:維運。

前言

維運是 維持運作 的簡寫,聽起來比起寫些服務的新功能更簡單, 但這不只要求對自己的服務暸解, 也需要對從底層的硬體機器到上層的軟體應用中間所有系統的行爲和之間的關係都要暸解。

當流量上升,或者節點越來越多,你不只需要單點的暸解,更需要全面性的去審視服務的架構。

關於二這個數字

「二」這個數字在網路服務中是個有趣的數字, 因為實際上我們通常會處理的數量級只有三個: 零個、一個和多個。

所以當你有一個以上的服務時,你需要處理的東西就會變很多

除此之外,之所以讓維運這麼困難的原因也在於每個企業甚至到每個應用, 他需要面臨的維運困難可能都不一樣,你很難找到一本書一句話,就可以解決你的問題。

最後,維運還有一個特色:你找不到正確答案,你通常都在一些權衡中做選擇。 在寫這份心得的時候,我也在思考要怎麼把這麼抽象的東西寫好, 這主要還是因為我希望自己寫的心得,不止是翻譯,更是一種新的詮釋。

這份心得主要分成三段(雖然書中是四段):

  • 案例分享, 透過一些案例分享,暸解哪些設計會讓服務難以維運。
  • 運行環境, 從實體、網路、單一節點、多節點再到調度工具的應用策略和維運注意事項。
  • 適應力, 從整體系統去解剖可以改善的點,包括企業架構、服務架構、資料架構。

案例分享

書中寫得有聲有色,但在這裡就僅以條列式的方式撰寫,幫助快速審視和回憶。

其中每個問題都有幾個對應的解法,可能是書中寫的也可能是自己的經驗和想法。

每一個案例都會有個引用,代表這個案例的出處和頁數(注意,頁數對應的是 Release it! 第二版)。

TCP 類型

TCP 導致的失能。

未正確關閉連線

請求方透過 VIP 和服務端建立連線,服務端做了 HA 之後,TCP 連線便自然被棄用。 但是對於請求方,因為 VIP 沒有變,所以仍然認為這條連線存在, 然而應用程式卻在連線發生錯誤時未處理這個例外狀況, 導致無法正確關閉連線,進而連線池中的所有連線都被迫停擺。

Case study, postmortem - p14

你可以:

  • 使用大家都在用(開源)的連線池管理工具;
  • 避免連鎖式反應影響其他服務,例如 艙壁設計原則(bulkheads)、每個資料庫用自己的連線池;
  • 調整資料架構,例如事件機制;
  • 監控各個依賴的連線(請求)數,出問題可以快速定位。
關於連線池

這通常出現在 Java 這類多線程且需要重複利用連線的程式, 故而在 PHP(快速生滅的子程序) 或 Node.js(單線程,libuv) 你很少會看到連線池的實作。

上面說沒有正確關閉連線的程式碼在這:

java Connection conn = null; Statement stmt = null; try { // 如果連線池沒有連線可用,就會在這邊等待。 // 然後你通常會看到 HTTP 500 或 Timeout 特別高, // 但是使用的 CPU/Mem 卻很低 conn = connectionPool.getConnection(); stmt = conn.createStatement(); // Do the things } finally { if (stmt != null) { stmt.close(); } if (conn != null) { conn.close(); } }

有看到問題嗎?事實上,很多教科書上都是建議這樣寫。 但是如果你有注意過 JDBC 的文件(應該會和很多 IDE 整合),就會發現 stmt.close() 這個函示是會丟 Exception 的!

封包被丟棄

和資料庫已經建立好連線,在傳輸資料時,因為防火牆的 session table 重置, 導致原有的連線封包都被丟棄(注意,不是拒絕), 進而讓服務和請求端都認為連線仍存在,實際卻無法進行任何溝通。 所以在節點上會看到很高的 TCP Retransmission。

Integration Points, The 5 A.M. Problem - p38

你可以:

  • TCP keep-alive,收不到封包就重建;
  • 以 MongoDB 為例,它就有 keep-alive 這個機制的選項(預設沒開);
  • 監控各個依賴的連線(請求)數,出問題可以快速定位。

行為堆積

行為堆積導致的失能。

持續運作下的累積

在高可用性下的多個節點,當服務有著 會隨著流量上升而變明顯的錯誤 時(例如記憶體堆積), 單一節點的失能會加速其他節點的失能。 例如,原本有三個節點,每個節點分配三分之一的流量, 當其中一台因為記憶體堆積,導致失能時會加速剩下兩台的消亡。

在未獲得改善前(通常記憶體堆積的錯誤並不容易改善), 可能需要定期去重啟服務,或者人工的介入。

Chain Reactions, Searching... - p48

你可以:

  • 使用 艙壁設計原則
  • Auto Scaling;
  • 做完善的負載測試。

錯誤告知義務

服務端並未告知請求的錯誤是暫時性(例如鎖死)還是永久性(例如輸入格式錯誤), 所以請求端反覆重新發送,加速雙方的消亡。

Cascading Failures - p50

你可以:

  • 注意連線池中的連線生滅和停擺;
  • 明確告知請求方,即使重送一次,這個請求也不會被放行;
  • 提早把錯誤回給請求端,避免加重後端服務的負載,例如適應性並行處理

Session

前端使用者錯誤設置的代理者 (例如 Chrome 的優化設定) 會反覆發送請求, 並且這些請求不會去紀錄 session ID,導致反覆建置無意義的 session,加重服務負擔。

Unwanted Users - p57

你可以:

  • 建置防禦 DDoS 機制;
  • 讓前端(SPA 或 SSR)透過 JS 送請求來建立 session, 將低非人類請求去建立 session 的可能;
  • 艙壁設計原則,避免服務的失能。

服務重啟後的耗能高峰

高負載導致斷路器斷電,進而讓所有主機關機。 當要重新啟動機器時,每台主機在重開機的過程中請求比平時更多的電流,導致斷路器反覆跳電。

Dogpile - p79

你可以:

  • 逐台開機;
  • 插上螺絲起子避免斷路器跳電,同時加強冷氣機和風扇避免過熱。

錯誤配置

錯誤配置通常是服務失能的大部分原因。

自動化的暴衝

套件管理工具在服務升版過程把預期被關掉的 auto-scaler 機制啟動, 導致 auto-scaler 因為過時的錯誤狀態,認為應該關閉大部分節點。

Outage Amplification - p80

你可以:

  • 非預期時間降載時,通知管理者;
  • 降低自動化的權限,例如做大規模更動時,需要手動介入;
  • 確保服務有足夠時間暖身,例如先做好快取。

處理非預期的大量資料

資料庫中的資料本來是 metadata,並不預期他會有大量資料。 但意外就發生在這些理所當然的事情上,應用程式處理時沒有使用 pagination, 大量的資料吃光節點所有的記憶體,並導致失能。

Unbounded Result Sets - p87

你可以:

  • 不要相信所有外部資料;
  • 確保限制應用層協定傳輸的大小。

忽略應注意的錯誤

當物件運算錯誤時,需要丟出錯誤,而非繼續讓該物件接著進行後續工作後再丟錯。

We Got the Fax––It's All Black, p107

你可以:

  • 謹記 Throw early, catch late 原則;
  • 預先要到應有的資源,當無法得到時,就先丟錯,不進行運算。

資源配置的高度依賴

當其中一個外部依賴能處理的量變很低時,導致其他服務完全失能。 這是因為連線池的連線是共用的,其外顯特徵包括:頻寬很高、潛時很長、資源(CPU/Mem)使用率低。

你可以:

  • 分散連線池給不同服務;
  • 監控各個依賴的連線(請求)數,出問題可以快速定位。

低估可能的來源數

Trampled by Your Own Customers, p277

早期開發通常是一年四到五次更新,每一次更新都會是一大包, 作者也分享其中一次案例,一個長達一年多的開發。

這次更新主要是在重構整個線上購物網站,並提供一些個人化首頁。

那時的技術只有伺服器側渲染(server side render), 且服務是以單石(monolithic)網站呈現。 顯然大家都是有經驗的人,即使開發完、設置好線上環境,仍預先做了很多負載測試、QA 驗證等等。

關於單石式的網站

早期在開發的時候,通常讓一個服務負責全部的功能, 包括登入註冊、產品列表、購物車、金流等等。 雖然方便管理,但維運上卻會出現很多問題,這裡就不列舉了

現在大多都是分散式的微服務,但仍可能會透過一些程式上的手法(例如, service-weaver) 讓眾多的微服務保有單石的便利性,同時又有微服務的高可靠性。

另外,你可能會以為單石式的網站會讓前後端更有效的串接,但根據康威定律, 前後端的串接效率受制於公司內部溝通的頻率和管道而非使用上的技術和工具。 換句話說,不管是前後端分離、單石式, 如果公司環境上讓這兩個團隊分離而鮮少交流, 你仍可能面臨前後端串接困難的窘境。

他們的負載測試很嚴謹(至少就我個人來說,沒看過這麼嚴謹的負載測試), 使用實際使用者會用的機器(Windows 而非 Linux),且機器人會點擊頁面, 並模擬使用者滑動頁面,點擊產品細節等等, 並在數個(異步且使用不同連線)使用者中挑出幾個真的會走金流、購買物品的流程。

測試的量是同時有 1,200 個使用者在使用, 當然這些流量,都來源於真實的線上統計數據(這還是淡季的時候,很顯然它是個大網站)。

然後在測試的時候,壓力測試讓服務整個失能,各個節點因為高負載,被迫中斷。

這是好事,因為至少他們不是在線上的環境下開始修錯和找出有問題的依賴和服務。 經過三個月,反覆的調整,並應用一些絕妙的招式成功把負載撐高, 並允許承受當初測試的十倍,12,000 個使用者的量。 是時候迎來上線了!

九點上線,透過後台監控,服務在九點五分時的 session 量達到 10,000; 九點十分時,達到 50,000; 九點三十分時,達到 250,000,並讓整個服務失能了。

發生了什麼事?

即使這麼嚴謹,現實的情況卻永遠有你想不到的狀況發生:

  • 搜尋引擎(例如 Google)導流進來的量約佔四成,然而他們擁有的網址卻是舊的。 這時使用者進來的是 404 的畫面。但是!它仍然會建立 session。
  • 搜尋引擎注意到 404 後,開始清除快取,重新派出爬蟲尖兵去爬你的網站,加重負擔。
  • 競業的爬蟲來爬你的資料(主要是產品售價,以利做每日競業報表)。
  • 還有很大一部分的 session 不知從何而來,例如瀏覽器的套件錯誤,但就是手動把它移除。 就算讓一個使用者的體驗很糟,總好過讓大家都不能用。

我認為事件主要歸因於幾點:

  • 缺乏防衛機制,例如適應性並行處理
  • 缺乏舊網站的並行,並逐漸導流;
  • 過於寬鬆的 session 建立機制,可以試著把搜尋紀錄放在 URL 的 query 上。
我自己從中延伸,想做的事

做一台 server 和 client,他會模擬一些底層(網路連線上)運作的異常行為:

  • 拒絕連線(連線前或後),TCP RST 或其他可能;
  • 排得進序列裡,但是不會被實際執行;
  • 只會回 TCP SYNACK 的訊號;
  • 建立連線後不送任何資料;
  • 建立連線後一直不回 ACK,導致一直重新發送(retransmission);
  • 只會回 HTTP Header 而沒有 body;

然後確保過去的連線是能承受或至少不會讓這些錯誤的連線影響其他連線。

運行環境

當線上問題出錯時,有時不是在應用程式中加上日誌(log)就能找到的, 你可能會需要進到機器中進行診斷。 可以診斷的前提是:你要對你的應用程式和其運行環境有足夠的瞭解,以下分幾個面向:

電腦和網路

相關的基礎知識和注意事項, 主要分兩類,網路和電腦(或稱運行控制、節點):

  • 網路:內容包括釐清主機名(hostname)、IP 等等。
  • 運行控制(runtime control):介紹實體機、虛擬機、容器、雲端的差異。

網路

要釐清整個網路的運行,短短幾行字不太可能, 我建議就一點一點吸收,配上一些實務經驗會讓你更有感。

一般來說,會透過網路去走到指定的節點,其中辨識節點的方式有兩種:

  • Domain 是外部網址,透過 DNS 解析 IP 後路由到指定節點的名稱;
  • Hostname 是主機名稱,一台主機只會有一個名稱,你可以透過下指令 hostname 獲得。

兩者合在一起稱為 完整網域名稱(Fully Qualified Domain Name, FQDN)。 換句話說,一台機器可以有很多個 domain/FQDN,而且 Hostname 可能會和 domain 不一樣。

除此之外,在開發環境中 NIC 可能有多個,例如:

$ ifconfig 
lo0: <loopback>
gif0: <tunnel for ipv4,6>
stf0: <tunnel for ipv4,6>
ap1: <access-point mostly for WiFi>
en0: <ethernet>
en*: <more ethernet>
awdl0: <apple wireless direct link for somthing like AirDrop>
bridge0: <for virtual>
llw0: <low-latency WLAN>
utun0: <tunnel for VPN>
utun*: <more tunnel>

但在線上環境可能就只會有一個是給應用程式連線用、一個是給後台管理或資料備份用, 這兩個接口可能各自有獨立的 IP(這時很可能就對應到不同的 domain)。 也可能是一個應用程式兩個 NIC 但都是相同的 IP, 這時使用的技術稱為搭接(bonding/teaming),目的是分攤出去的流量。 也因為這樣,有時在建立應用程式的時候我們不能綁定所有的 NIC0.0.0.0:8080) 而是要指定 domain 或 IP(app.example.com:8080172.168.1.2)。

運行控制

分為三類:實體機(physical host)、 虛擬機(virtual machine, VM)、 容器(container), 最後會在介紹一些雲端環境的注意事項。

實體機

一般來說和開發環境並不會相差太多,都是多核心、x86、64 位元、相似的時鐘晶片。 主要差異可能在於資料中心的主機儲存空間通常不會太大,他通常會透過 NASSAN 來擴充。 這是為了讓單台機器的成本降低,讓水平擴展可以節省地被達成。

除此之外,如果有特殊應用需要使用到 GPU 或高 RAM(例如機器學習、圖形運算) 才會額外賦予該應用特殊機器。

虛擬機

現在的網路應用節點毫無疑問是以虛擬機作為主導,雖然犧牲了一些資源都是卻換來了很大的管理方便。 但是虛擬機還是有些問題,例如它的效能是難預期的,這裡包括 CPU、記憶體、網路。 這是因為掛載虛擬機的主機(host),會為了資源調度而暫停這些虛擬機的運作。

這聽起來可以被接受,因為網路應用的潛時一直都是難以預期的, 但如果那些被迫暫停的節點是重要的服務,爆炸半徑可能就會很大了。 例如管理叢集的節點,如 auto-scaling、服務發現或共識演算。

除此之外,時鐘的偏移在這個環境下,產生了更大的變數。 虛擬機會為了和主機對齊時鐘而強制調整時鐘, 對應用來說,時間就可能會亂跳(會往前也會往後),如果應用是對時間敏感的,就需要注意。

容器

容器通常是開發者需要去設計和調整的運行控制,相對而言虛擬機則是系統管理員需要去處理的。 容器很像在雲端上管理虛擬機,你不會預期他的 IP 恆久不變也不會把重要的資料放進其檔案系統中, 因為它通常是短暫存在的。

容器減少了開發環境和線上環境的差異,但他仍有一些困境,然而隨著其發展,這些困境已經一一被解決了。 不過在使用上仍需要注意一些事情:

  • 容器預期是快速生滅的,服務應該避免過久的啟動和關閉。
  • 除錯是困難的,如果你有發生過線上問題,你會發現進去容器後簡直一籌莫展, 因為裡面的環境乾淨到很難做些什麼事情。
  • 網路是複雜的,由於在主機上又搭載了很多網路橋接(bridge),有時封包的流向很難追蹤。

你會需要一些時間去適應容器的除錯。

容器的困境

網路在容器世界是複雜的,因為你可能不會在 host 上暴露他的網路埠, 但卻需要讓他有能力對外連線(換句話說,只出不進)。 我們通常會使用 VLAN(或者說,overlay network)去橋接這個連線,並用軟體交換器去交換封包。

除此之外,容器通常是小而多的,所以你會需要一個自動化的管理系統(或者說,control plane)。

雲端

雖然你需要花些時間搬遷應用到雲端上,但是雲端環境提供很多優勢, 最主要的就是可用性(適合做 auto-scaling)和低成本。但是在雲端需要注意:

  • 你的虛擬機可能會因為營運方的管理因素被要求重新啟動
  • 就像容器一樣,你在雲端上的機器不會有固定的 IP,除非花錢去租賃。
  • 通常一台機器配上一個(虛擬)NIC,所以你只會拿到一個私有 IP,但有時應用的需求需要多張 NIC

雲端上的容器同時面臨著 容器雲端上的虛擬機 會有的困境。 但是隨著雲端服務的成熟,這些困難其實都不是困難,只是會需要你花點時間去研究和累積維運經驗。

單一節點

單一節點雖然只是一個龐大服務的基石,但是建構良好的節點,會幫助你在後續維運大型服務省下很多功夫。 而所謂的良好節點的設計,通常是在開發初期就考慮進去,包括後面提的服務的透明化

部署和設定總結幾點注意:

  • 程式碼要放在版本控制(version control)中,別塞機敏資訊。
  • 部署要自動化,並確保依賴和插件的安全性。
  • 比起運行控制隨著時間改變,每次改變都是從特定狀態延伸,更為安全有效:
    每次更新都從 base image 延伸,避免把狀態複雜化
    每次更新都從 base image 延伸,避免把狀態複雜化
  • 小服務設定檔用注入(檔案或環境變數); 大(多個微)服務可以用專門服務來替代(ConsulZooKeeperetcd)。

監控總結幾點注意:

  • 在應用設計之初就要建立日誌(log)、測度(metrics)和監控(alert)的架構。
  • 避免日誌、測度的輸出和觀測之間的耦合化,也就是在調整觀測指標的時候,不需要改應用程式。
  • 寫日誌是最直接觀察應用的行為的方式,有幾點注意:
    • 日誌位置最好在應用的位置之外(/var/logs),容器的話單純輸出到 stdout 就可以。
    • 不要讓 error 層級的日誌常態出現(例如使用者輸入格式錯誤不應為 error)。
    • 寫清楚一點,因為緊急狀況會讓判斷力下降。
    • 同一個請求的日誌要有 trace ID(通常較 correlation ID)標示。
  • 暴露健康檢查接口,包括應用程式的連線狀況、IP、版本資訊。

叢集

隨著流量變多或為了高可用性(High Availability, HA),一個服務開始從單一節點成長為一個叢集。 這時我們看待服務就不是從節點的角度去看, 而是一個由服務發現(service discovery)、負載平衡(load balance)組成的叢集。

這類工具很多,從傳統的單一職責的 ZooKeeper、Consul、Nginx, 到現在全部整合的 Kubernetes(或有點過時但其實才出生十幾年的 Mesos)。 我們在使用這些工具的時候, 要考量從公司的規模、服務的架構到這些工具本身的迭代性和動態(自動化)性, 也就是說這工具是否方便被替換和升級。

這裡我們會談三種類型的負載平衡方式,DNSGSLB 和單純的 LB。 接著再談到相應的 資源管理網路設定 的注意事項。

DNS

只有人才能決定現在這個服務要用什麼領域名稱(domain name),而這個名稱通常是不會改變的。 當這個域名被決定了,就可以註冊進 DNS 中。 這裡要注意的是網域名稱可以是唯一,但是目的地位置可能是分散在世界各地。

但是對於服務提供者來說,即使送出多個 IP 位置,最終客戶選擇要使用哪個 IP,是客戶決定的。 它可能是傳統的循環比對(round robin),或者依照瀏覽器的邏輯去判斷, 這時要利用這個機制去做到彈性的負載平衡,就不太實際了,因為控制權不在你身上。

關於 DNS 的彈性

這裡所說的彈性,是指當服務降載甚至失能了,就需要避免客戶再送請求過來。 這時,彈性且快速的阻止每個 DNS 發送這個失能的 IP 是困難的。 因為 DNS 的運作是複雜 (可以說是網路世界中最複雜的一塊, 想想那些路由的協定,RIP、EIGRP、OSPF、BGP) 且多個 DNS server 並不是由一個統一的單位管理的,在設定上很可能出現分歧。

GSLB

對於這種不同地區的負載平衡,更常見的是提供多個 GSLBGSLB 可以做到檢查下游的健康狀況、彈性分配流量到不同節點, 最重要的是它通常歸你所管,所以你可以快速對它進行任何設定 (當然,設定錯了就會死很快)。

如果你有多個 GSLB(多個資料中心),你心裡要有個底: 並不是每次請求,客戶都會乖乖照著前次的 GSLB 進來。

負載平衡器

這其實和 GSLB 的工作職責很像,只是 GSLB 通常是任何外部的人都連得到, 但是這裡的負載平衡器是指某服務(或多個服務)前面的平衡器, 通常會被放進私有的網路環境中,並等待 GSLB 把流量導流進來。

如果這個平衡器是負責多個服務的,他就會有很多 VIP(s),然後每個 IP 對應一個服務(多節點)。

DNS, GSLB, LB 和服務的組合圖
DNS, GSLB, LB 和服務的組合圖

負載平衡器其實和 GSLB 一樣都需要設定:

  • 平衡負載的演算法選擇,例如循環比對(round robin)。
  • 如何檢查各個節點是否正常(health check)。
  • 是否要讓特定使用者連到特定節點,即所謂的 sticky-session。
  • 當服務失能時,要怎麼回應。

對於平衡器來說,不再是以主機名稱(hostname)來做搜尋名稱,而是以外部網址 (也就是 domain,其實本來就是這樣,只是這邊再強調一次 hostname 跟 domain 的差異)。

除此之外,有時他不是用來做「負載平衡」而是服務引導, 例如 HTTP 路徑為 /login 走這、/profile 走那。

資源的需求控制

流量增長可能會耗盡系統的資源,以網路為例,有幾點要注意:

  • Socket 會被耗盡,並需要等待舊的被關閉 (關閉前會需要進入 TIME_WAIT 狀態)。
  • 一部分封包等待著其他封包進來,這時記憶體就會被這些不完整的封包佔用, 稱這種現象為隊頭阻塞
  • 以 TCP 為例,建立連線前會進入 listen socket 的佇列, 只有成功建立連線的 socket 才會開始移交給應用程式端, 所以你可能會有很多正在佇列的連線。

當上述行為踩到限制,就會開始拒絕進來的請求, 進而促發請求端重新嘗試連線或重新發送資料的機制, 加重服務的負擔。 除此之外,一個連線會歷經很多階段的處理,這時如果應用層端的服務已經滿載了, 我們當然會希望請求在很早的階段就被回拒

一個健全的負載平衡器,就很適合在最一開始就拒絕請求,減輕服務的負擔。 這時,應用程式的健康檢查就很重要了, 讓應用程式提供完整的資訊,進而讓負載平衡器有能力判斷丟進去的量。

除此之外,也可以使用一些適應性並行處理的機制, 然後這個回應可以是單純的 HTTP 503 Service Unavailable

網路相關的注意事項

這裡有幾個面向可以考慮:

  • 路由,路由是網路世界最複雜的一塊,不是三言兩語說得盡 (當然有很多相關的書)。
    • 隨著節點的增加,需要維護路由表,不管這個路由表是資料庫或是一個表格。
    • 近期也慢慢興起全部由軟體控制的架構,
    • 例如虛擬交換器K8s、 VLAN tagging 等。
    • 這些路由設定包括單一節點有多個出路口(一個給服務、一個給維運等等)的複雜狀況。
  • 服務發現,每個提供這個機制的軟體(ZooKeeper、etcd 等等)都有自己的權衡機制, 適合不同場景,不要認為這些軟體都是一樣的。
  • VIP 的轉換,負載器會透過 VIP 來指定服務要走的節點(HA, active/standby 的機制), 這時要注意,當 VIP 進行切換時,每個 TCP 連線都可能在傳遞下一個封包時失能, 包括那些重要的資料庫連線。

叢集管理

叢集管理並不是一個簡單的公式,把數字代進去就可以得到結果。 我們需要考量自己的需求和環境(迭代率、即時性、維運性),來決定我們需要補足哪個面向的不足。

叢集管理工具是用來減輕人類負擔, 所以當一個人類因為錯誤操作導致叢集失能, 我們應該歸咎於工具的偵錯性和管理性的失能。 但這也回應到叢集管理並不是魔法, 他仍然是一行一行的程式碼, 所以在看到一些文章或新聞在推廣某個管理工具(例如 K8s)時, 應首先思考這工具是否符合需求, 建置、維運、拆除(工具一定會有迭代)的成本和其帶來的效益。

AWS 的失能案例

AWS 2017 年發生的 S3 失能事件, 其中的屍檢報告可以看到:

一位授權的管理者根據指南進行操作,在執行一個關閉單一節點的指令時, 因為指令的錯誤,導致關閉了大部分的節點。

在此我們可以反思幾件事情:

  • 文中從沒出現「人為錯誤」這類相關訊息,因為人類犯錯是可以被預期和接受的, 身為一個管理工具卻沒能感知到這件事情,所以文中強調的是管理工具的失能。
  • 操作指南代表以前有人照著這些指令執行,但是為什麼以前沒有發生意外? 我們常常檢視那些失敗的案例,但我們也可以去檢視那些成功的案例。 例如之前有人也寫錯過,但他在提交前的某個流程中有其他人審核出來了,該怎麼強化這些流程?
  • 自動化的反應是快速的,在屍檢報告中 AWS 最終降低了移除節點的速度和增加一些保安系統 (當減少的量低於系統當前承載的量時提出警告)。

我們會先釐清「叢集管理和被管理的服務」之間的關係, 接著試著讓「服務透明化」也就是利於管理。 當服務需要的節點數越來越多時, 需要一些「備置和部署」的自動化工具協助管理, 最後有些服務不適合快速重啟,需要一個「控制管理的介面」。

平臺和使用者的關係

我們首先分別以 監控系統資料庫 來檢視一下現在的軟體環境中 平台建置者和開發人員之間的關係

現在有很多開源的監控系統,當你把平台建置(可能是系統工程師)起來之後,是怎麼讓開發人員使用的? 早期可能是開發人員填單子,請相關人員做好應用程式的監控。 但隨著發展(DevOps),開發者也慢慢開始傾向自己設定和調整相關監控。 這有點像是寫程式時的介面(interface), 建置人員做好一個彈性很高的平台後,讓開發人員填好自己的實作。

這代表之中的責任移轉了,平台建置人員專注於多樣化、穩定和有效率的平台, 開發人員專注於應用邏輯,調整水位、示警閥值、 服務指標(SLA)等等。

同樣的狀況發生在資料庫中, DBA(Database Architecture)的工作應該是建立一個高效率和穩定的資料庫。 但是早期 RDBMS 的系統下,應用程式的設計邏輯會大大影響資料庫的穩定度, 慢慢的 DBA 就變成 DBA(Database Administrator),開始要管理應用程式的資料庫邏輯。

這也是 NoSQL 運動的背景因素之一,嘗試要把資料庫管理和應用邏輯抽離。

這些歷史知識,都可以幫助我們暸解,一個叢集管理工具應該要長成什麼樣子, 我們不仿把 K8s 套用在這個關係之中,然後思考一下它現在的樣子是一個理想的樣子嗎?

怎麼知道現行工具不適合?

作者提供一個思考點:

如果你發現你的工作是每天(或每隔幾天)都有個固定事情要做,例如重啟服務。 這就是一個很強的論點說明現行的工具已經不適合使用了。

服務的透明化

要怎麼知道你的服務或節點現在的健康狀況怎麼樣了? 透明化你的服務。

監控系統百百種,早期每種類型的監控都需要付上大筆鈔票來購買企業的服務, 但現在開源服務遍地開花,我們選擇的基準是什麼?

  • 是否給服務使用者(不是服務開發者)帶來好的體驗
  • 能否替公司賺錢(省錢)

以這些為出發點,找到那些應用程式需要注意的地方,例如:

  • 服務的瓶頸(bottleneck),例如最常用的請求是什麼, 可以 cache 嗎?驗證授權邏輯可以 wildcard 嗎?
  • 連線是否有佇列排隊現象?
  • 前端使用的連線行為是否過度壅塞?

依照這些東西,就可以去設計我該收集哪些日誌、指標和示警。 同時還要考量各種成本,包括:開發、建置、基礎設施、維運和效率(設定優化)。 這些都是以目標(賺錢、省錢,好用、穩定)為思考點,回扣到做法, 而非從技術層面為立足點。

另外在選擇服務中哪些資訊要暴露時,通常是所有東西都暴露出來, 實際在做維運的時候,當發生想要的資訊沒有暴露時, 就只能看到兩個工程師相視而笑,兩手一攤。

拼圖還缺了哪一塊

就像尚未出現的整合資料的服務一樣, 對於應用程式來說,我們還是痛苦於整合所有驗證授權系統和監控介面。

服務的多面向

要注意每個團隊想要看的東西可能不一樣:

  • 開發人員可能想看日誌,檢查奇怪的程式行為;
  • 分析人員可能想看指標,暸解使用者整體的行為;
  • 專案的管理人員可能想看功能的使用狀況。

身為一個監控系統要怎麼滿足這些東西? 你可以使用串流處理

可能重要的資訊

這裡特別列出一些重要的資訊並做簡單的分類:

  • 流量,請求數、併行數。
  • 使用者,登入失敗、在線數。
  • 商務邏輯,成交數、入帳額。
  • 資料庫,連線、請求失敗數、回應時間。
  • 運算資源,連線池、線程池、阻塞狀態。
  • 儲存資源,記憶體、快取。

這些資訊通常都會搭配時間軸(例如,近兩個小時的狀態)和閥值(超過 80% 就開始通知管理者)來服用。

備置和部署

隨著應用程式或者節點數量的增加,需要讓每次更新或異動可以快速、便捷。

在服務備置(provision)時期,你通常會有推(push)或者拉(pull)這兩種模式。

「推」會是一個中央服務,把資訊(組態設定、鏡像檔、執行檔等等)送到指定節點或服務中, 這種做法比較單純,可以透過 SSH 等機制快速達成驗證授權行為。

「拉」則是讓各個節點去拉取指定位置的資訊,這通常對於快速生滅的環境(例如容器化)很適合, 對於擴增性(scalability)也有很好的輔助。 但缺點就是需要設計好驗證授權的機制。

除了早期靜態設定(例如 env file), 你可以透過提供組態設定的服務(通常是 ZooKeeper 或 etcd)來達到動態設定。 在這之中,需要注意幾個要點:

  • 當組態服務失能時,應用程式和節點應該要可以正常運作(但是新的應用程式要部署可能會失敗)。
  • 確保組態服務沒有能力可以快速終結大量節點的能力。
  • 資料要備份。

除此之外,在備置時通常會希望環境是乾淨的,可重複再現的。 例如:當從新版本(v2)直接退版(v1)時,其結果要和當初升到舊版時一樣(v1), 可能可以透過 Lock 檔或檢查 sha(或 eTag 等等)值。

檢查 eTag 失敗的案例分享

有碰過一個案例,就是開發者每次升版,會覆寫舊版的備置檔(例如壓縮檔)。

這導致今天新版本出問題要退版時, 程式發現舊版本 eTag 對應的壓縮檔(latest.zip)已經改變了, 導致退版失敗,需要讓應用程式重新打包一次再部署,造成失能時間拉長。

即時控制你的應用程式

有時候服務沒辦法快速啟動, 例如服務啟動時需要從快取暖機、 啟動前需要先建置程式碼的虛擬機(例如 JVM)、 服務住在虛擬機上(例如 AWS EC2)等等。

這時,你需要一個方式可以在外部影響應用程式,而不需要重新啟動服務。 可能可以即時控制的行為有:

  • 重置迴圈;
  • 調整連線池的數量和逾時時間;
  • 暫停和某個服務的連線;
  • 重新讀取設定檔;
  • 某個功能的開關;
  • 開始、關閉對外服務。

但是不要在這裡去暴露 「改動資料庫的綱目」或 「清除快取」的接口, 因為這種即時調動大量持續性狀態的行為,通常都會造成一些意想不到的邊際狀況。

還有個問題是這個接口要怎麼暴露? 通常會選擇打開一個 HTTP 端口,透過打 HTTP API 來達成目的。 但如果服務的節點有五百個,難道要對五百個節點打 HTTP 請求嗎?

這時我們可以使用事件佇列機制,讓這五百個節點去監聽某個事件, 然後管理人員發出這個事件,讓每個節點一批一批(一次性讓大家做事會增加大量負荷)的去處理這個事件。

有時大家會寫一個 GUI 讓大家可以輕鬆操作,聽起來好像很合理, 但是這個介面只能提供簡單而高層次的操作, 例如讓審核者同意這次自動化的操作(例如 autoscaling)等等。 否則任何細部的行為都允許讓人去操作,而非使用固定腳本,這無疑是增加失誤的風險。

適應力

時間在走,適應環境變化的能力是必不可少的。 要讓服務有適應力,優良的架構是必然的,其中又分成三個面向:

公司層級的架構

書中各個例子都會闡明一些狀況和討論,這裡以條列式的方式列出:

  • 擁有架構團隊(DevOps),這個團隊需要對服務架構清楚,可具有教育意義的團隊。
  • 無痛發布,一次大的發佈比不上多個小的發佈。
  • 不要建立太大的團隊,團隊再大也只需要兩個披薩就能餵飽,麻雀雖小五臟俱全的概念。
  • 效率的抉擇,大家可能會覺得效率越高越好,但是高效率通常代表低靈活性,這是需要取捨的。 例如汽車工廠機械的高效率低彈性,對比於技師的低效率高彈性。

SRE 和 DevOps 的差異

詳細可以參考 Site Reliable Workbook 的第二章 How SRE Relates to DevOps

服務層級的架構

強調三種健康的服務架構:

  • 微服務,書種以 Design Rules 為原則, 闡述六種模組化設計原則。
  • 訊息佇列,高彈性但是除錯較難,且需要思想上的轉變。
  • 嵌入式擴充,適合單一服務的擴充。

資料層級的架構

這段可以參考:Design Data-Intensive Application, 裡面就會談得很詳細。

主要概念是要把資料結構和邏輯抽離, 例如不要傳 user ID,而是傳 URN, 這要下游或其他服務在使用的時候,才不會因為不同邏輯,而讓資料結構轉來轉去。 其實 AWS 在這塊就玩得很好,他每個服務都是一個 ARN 而不是 ID。

結語

總的來說,隨著各大企業逐漸累積一些維運需要注意的事項, 即使這些知識天生就會零零散散的, 但至少也有了一些系統化的認識。

如果未來要更近一步的深化,可以參考 Google SRE 這個網站, 裡面講了非常多類似的東西, 但他著重在建立 SLO, 並以此作為決策的基礎,這也是下一份心得想寫的內容。

字詞解釋

關於虛擬 IP(VIP

Virtual IP;虛擬 IP 位置,會需要有個服務管理 VIP 對應真實 IP 的表格。 在 Linux 中,通常是 conntrack(Netfilter)

什麼是 conntrack?

有狀態防火牆(stateful firewall)是相對於早期的無狀態防火牆(stateless firewall)而言的: 早期防火牆只能 drop syn to port 443 或者 allow syn to port 80 這種非常簡單的規則, 沒有 flow 的概念, 因此無法實現諸如「如果這個 ack 之前已經有 syn, 就 allow,否則 drop」這樣的規則, 使用非常受限。

顯然,要實現有狀態防火牆,就必須紀錄 flow 和狀態,這正是 conntrack 做的事情。

虛擬 IP 通常有幾個功能:

  • 用作單一服務多個節點的唯一路口,
  • HA 機制,
  • 單純在私有網路遮罩下的 IP 分配。

由於功能很多,需要注意上下文中其代表的意義。