Release It! 2/e 讀後心得

fcamel
fcamel的程式開發心得
12 min readApr 19, 2020

除了滿足功能需求外,軟體設計階段你會額外考慮那些面項呢?

  • 執行效率
  • 記憶體使用量
  • concurrency
  • 可測性

對多數人來說,即使沒有要求,也會避免使用「顯然」沒效率或浪費空間的演算法或資料結構。例如需要排序時不會用 O(N²) 的 bubble sort,需要用 key 查詢資料時會優先想到 hash table。而時間和空間的效率分析也是面試時常問的問題。

再來,設計上可能會考慮 concurrency 問題,因為隨經驗累積,知道 concurency 很難作對且出錯後很難察覺。因此,面試時可能也會問如何正確的使用 lock或是架構上如何避開 concurrency 出錯的機會。開發時甚至會使用 Go、Rust 這類有考慮 concurrency 的程式語言。

有些人可能會在設計時額外考慮如何測式程式、如何用最小風險的方式更新程式。這些都是隨經驗累積,行有餘力後才能在設計階段考慮的事。經驗不足時,只好等發現錯誤才補救,此時得付出更高的成本。另一種經驗不足的結果是作過多的早期最佳化,增加不必要的複雜度和拖慢開發時間。

因此,隨著經驗的累積,會希望在同樣的開發時間下,善用累積的經驗多考慮每個階段「顯然」需要注意的事。這些事還包含:

  • 如何提早測出上線後會遇到的問題
  • 如何部署上線程式
  • 如何管理上線程式

現今流行的 continuous integration 和 continuous deployment 就是針對部署而作的 best practice。

綜合來說,本書主要在講產品上線前和上線後該作的事。內容滿雜的,個案分析很有趣,後面摘錄部份內容說明要考慮的面向。以此為契機留意這些主題,日後才有機會改善。

責任區分

Conway’s Law

設計系統的架構受制於產生這些設計的組織的溝通結構。

要小心此點的影響,例如將 DevOps 切成不同團隊後,會造成系統之間多一層介面讓開發團隊和 DevOps 對接。若開發團隊自行處理 DevOps,可以減少不必要的介面,降低系統複雜性。

作者覺得 DevOps 改叫 platform team 比較適合。DevOps team 該作的是開發工具和提供知識,讓各 app 團隊用他們的 know-how 作 deployment 和 operation。這和 Google 的 testing team 一樣,目的是作測試工具和散播知識。寫測試是 app 團隊成員的責任。只有這樣才能作好服務。

每個開發者都該在設計時考慮 product → development → quality assurance → deployment → operation,減少事後補救。

題外話,作者認為:

Microservices are techinological solution to an organizational problem.

因此,某個團隊想更新服務時,作好相容性處理,不用受其它團隊時程影響。滿有趣的觀點。

產品面的分析 (Control Panel)

整個系統透明度愈高,愈易維護。建立一個 control panel 有助於了解全貌,串起各個服務的狀況。

最重要的兩個問題:

  • 客戶是否高興?
  • 產品是否賺到符合預期的錢?

由此出發規劃各個子系統該提供什麼資訊,control panel 要怎麼串起各別的資訊,以便一眼看出這兩個問題的答案。

作者說這屬於 application performance management,並列舉一些相關服務:

  • New Relic
  • Datadog
  • AppDynamics

要監控的東西太多了,要練習思考 “economic value more than technical availability”,習慣後會更容易決定要監控的內容。

Development

穩定的系統需要 steady state,因此要用各種方法避免單一模組的服務量超載,還有避免局部錯誤擴散到整個系統。

確保 請求/占用資源 不會超載

  • shed load:超載時直接拒絕上游的 request,讓 load balancer 知道找別人。不能假設上游 producer 不會傳送超載的 requests。
  • 用 circut breaker 保護下游服務,大概如下圖所示。使用 circuit breaker 減輕 consumer 的壓力。Fail fast 比 slow timeout 好。有需要可以用成熟的實作,例如 Netflix 的 Hystrix
出自 Release It! Chapter 5
  • shed load 用來保護服務之間 (end-to-end),服務內部 message queue 滿載時,也可以藉由 block request 產生 back pressure,以阻止 producer 產生更多 request。
  • 凡是有產生資料的地方都要有明確的上限,包含 cache、message queue、logs 等。定好滿載時如何丟棄資料。例如 cache 使用 weak reference 在 OOM 時優先被回收;log rotation 定好每個檔案的 bytes,最多保留最近幾個檔案。

隔離錯誤

不論我們如何小心,難免會有錯誤發生。在避免錯誤發生的同時,也要考慮隔離錯誤,讓它們發生時不會造成系統性的錯誤:

  • 借用 bulkheads 的概念,隔離使用的「服務」,避免出錯時擴及全船。例如服務佈到不同 AWS regions / availability zone。bulkheads 還可個別運用在不同資源上 (後述)。
  • 隔離使用空間:寫入 logs 到獨立的 partition,即使 logs 不小心寫爆了,只會影響 logs。
  • 隔離 CPU 使用:預留一個 thread 服務管理指令,確保 CPU 滿載時仍能下達系統指令。
  • 隔離網路頻寬:使用多張網卡連不同網段,特別是管理用的內網,確保外部灌入資料滿載時,不會影響內部頻寬。

Let it crash

  • 為了避免 error propogation,另一個選擇是let it crash,預期重開治百病。
  • 使用前提是重開到上線服務的時間極短,像是重開 container 符合此前提,但重開 VM 就太久了。還有要確保重開局部服務不會影響整個系統。
  • microservices 有助於用 let it crash 處理難以處理的錯誤。
  • 我個人滿喜歡重開治百病的解法,可以簡化一些難以處理的錯誤,避免為了處理罕見錯誤而增加別的錯誤。

其它

  • 使用 timeout 避免占用資源和 deadlock。
  • health checks 要作確實,反映能否真的提供服務,不是「機器還活著」。內容可包含機器 IP 資訊、目前 RPS、各種資源使用量等。可供 load balancer 決策。
  • 為方便測試,備好 testing server 提供真實資料的回應,還有 no response、slow response、garbage response 等各種異常回應。
  • 和 middleware 拖勾。服務之間用 synchronous/asynchronous 差異很大,盡可能在早期設計決定。基本上 async 的系統較可靠,但也比較難開發。
  • 完全自動化的壞處是出錯時會瞬間大爆炸。要適時地引入人為監控。比方說作 gradual rollout 可以先下指令自動更新一小批。觀察服務正常後再逐漸更新更大批機器。
  • 要正確地實作高效能和可靠的 connection pool 比我們想得難多了,因此使用廣為人知的成熟實作,不要自己作。同樣的,producer/consumer queue 也該使用別人成熟的實作。

以 session 為例,分析設計考量

  • 盡可能不要存資料在 session 裡。
  • 確保記憶體不足時會優先刪除 session data,例如在 Java 用 weak reference 保留 session data。
  • 更進一步是將 session data 移到程式外,使用 Redis 這類的服務。Redis 可同時作為 in-memory cache 和 persistent storage。

小結

設計時要考慮:

  • 限制每個子系統能處理的上限,同時不要對下游發出無限的請求。
  • 子系統內部針對重要的功能,將 CPU / disk / memory 隔離使用。
  • 子系統之間針對重要的功能,將網路隔離使用。
  • 子系統之間過載時快速拒絕請求。
  • 子系統出錯時能快速重開,避免擴散。

非同步系統是克服各種 stability antipatterns 的好方法。

Quality Assurance

  • longevity tests 有助於提早發現上線後才會發現的問題,不需要給予系統重高的負載,但要持續不斷有負載,包含某段時間給序極低的負載,才有機會找出 connection pool 或 firewall timeout 的問題。
  • 像是 load balancer 可能會在一段時間沒收到封包後,會丟掉連線相關資訊 (例如 NLB),導致連線失效了,但連線兩端的程式沒有查覺。作者遇到的例子是 firewall 丟掉連線資訊,導致 database connection pool 裡的連線在淩晨時段失效,但是上層程式不知道,操作都卡死。找出原因前,需要固定在離峰時段重啟才能運作。
  • end-to-end test 時,盡可能用真實資料,不管量還是種類,提早抓出上線後才會遇到的問題。
  • 盡可能快速且頻繁地制造難以處理的錯誤,才能快速修正。Netflix 的 Chaos Monkey 是個好例子。在不拖跨系統的前提下,局部地破壞子系統,有助於提升長期穩定性。

Deployment

Configs

上線用的 configs 要用另外的 repository 管理,確保大家使用同一份組態,並且能追踪改變。作者提及他曾加入一個上線延遲一年的 300+ 人團隊,加入後第一件事就是和各組負責人問清楚他們用的設定,然後收集到一個 repository。不然無法想像服務要如何上線。

使用 Container

使用 container 有許多好處,確保 app 由一個明確的 image 開始擴增,然後保持不變。日後需要作新功能時,再重建一個 image,確保掌握變化。

https://12factor.net/ 提供 app 和 app 管理者之間的責任分界,說明 app 該作什麼。例如:

  • logs 寫到 stdout 由 process/container 的管理程式處理。意即 log rotations 由 operation 管理,而非 process 本身。
  • 由管理程式處理程式重啟、回收 exit code 等事,不要作 daemontize 和寫入 damon PID 到檔案。
  • 同一個 app 有不同部署時,從環境變數讀入變化的部份。

12factor 似乎沒有特別強調用 container,不過用 container 可以少作一些事,例如使用 docker logging driver 將 logs 導到不同服務。

使用 container 後,部署的主要的困難是:

  • container 之間的網路組態。
  • 讓每種服務有足夠的資源可用,也就是分配 container 執行的服務和所在的主機。

Kubernetes, Mesos, Docker Swarm 等服務就是用來克服上述兩大困難。服務太多不易管理時,可考慮使用。

CI、CD

這大家都在談,不需多說。

Data migration

簡單的作法是跑一次性的 schema migration + data migration 再重啟服務,但這樣 downtime 會拉長。實務的作法是 app 讀取舊資料時,順手作 migration。全作完再移掉 migration code。

即使是 SQL-based database 也可以這麼作,比較安全且通用的作法是對 table T 開新的 table T2,將資料由 T1 轉到 T2。轉移過程中可能仍有 app 寫入 T,此時可透過設定 trigger 自動將寫入 T 的資料轉到 T2。

Operation

  • 發生意外時,第一優先是回復服務減少損失,其次才是找出出錯原因。全部 thread 的 backtrace 是除錯的好資料,事先要備好這個機制在重啟服務前自動留下此資訊。
  • 一般來說希望透過 kill & restart 的方式快速重啟子系統,可以自動修正。避免要 restart the world 才能更新修正。對於不能快速重開的服務,仍需要線上設定的參數,像是 reset circuit breaker、調整 connection pool size 和 timeout、開關功能等。
  • 管理者比開發者更常看 logs,內容要寫清楚,盡可能提供 context。比方說 “file not found” 無法告知管理者要怎麼處理,但是 “failed to load config file XXX: file not found” 就清楚許多。盡可能 wrap 底層 error,提供更清楚的 context 回傳給上層,讓上層邏輯能提供更多訊息到 logs 裡。
  • logs 和 metrics 不同,要提供最低限度的 metrics 了解系統狀況。包含流量、線上和過去使用人數、資源使用狀況 (disk、memory、connection pool 等)、各個服務的連接點 (integration point) 健康狀況。留意和過去相比異常的變化。
  • 將日常服務和需管理權限的功能分成不同程式,避免誤用或安全漏洞。

其它

  • HTTP-based 的 load balancer 可用 cookie 作 sticky sesison。TCP-based 可用 source IP 作 sticky session。
  • 若有使用 virtual IP,在 failover 時切換 virtual IP 對到的機器,會造成 TCP 傳到一半流水號出錯。因此 app 要假設會發生這種 IO 錯誤並重建連線。
  • 收集 logs 的主要服務有付費的 Splunk 和自己架 Elastic Stack。
  • OWASP Top 10 是了解常見安全漏洞的好地方。例如不要提供不同的錯誤訊息讓攻擊者找出有提供管理者權限的 API,回應 404 會比 403 安全。還sensitve data 要加密後再存起來。
  • 團隊隨時維持效率 100% 不見得是好事,要留一些餘地處理突發狀況。人事上和系統相似,高效率伴隨著降低彈性。

結語

這本書實在太雜了,點了很多問題,有些也沒提供解法。我閱讀的主要目的是擁有正確的觀念 (mindset)。能先處理就處理,即使沒處理到,之後能有系統地記取教訓,下次再提前作好準備。

--

--