仓库源文站点原文


layout: post title: 論程式的可觀測性和自動化監控 categories: [dev]

description: 如何從可觀測性的角度出發,來改善程式和系統的品質,以及快速定位異常點

Introduction

對於工程師而言,程式上線後最大的痛苦大概就是發生了異常狀況,又沒有辦法即時修改程式增加log來dump上下文資料或分析程式執行路徑,看著單調的錯誤訊息猜謎。要想未卜先知的猜測可能發生異常的地方,事先加上log卻又太過雜亂且發散。藉由可觀測性的概念可以協助我們理清思路,並埋設正確的觀測點,來輔助監控和發現異常。

在軟體工程中,可觀測性是指你能夠有效地監視和記錄應用程式的運行狀態和問題,以便更容易地進行故障排除和性能優化。

可觀測性的 logging、metric 和 tracing 是軟體系統中用於監控和了解系統運作的三個重要方面:

  1. 日誌 (Logging):

  2. 指標 (Metrics):

  3. 追蹤 (Tracing):

這些可觀測性的方法通常在大型複雜的軟體系統中使用,以幫助開發者監控、診斷和改進系統。在使用 Go 語言開發應用程式時,你可以使用相應的庫和工具來實現這些可觀測性功能,例如使用 Logrus 進行日誌記錄、Prometheus 進行指標監控、以及 OpenTelemetry 進行分布式追蹤。

這三者之間是存在交集的,意味著某個異常或問題或許可以透過 logging 和 metrics 兩種方式呈現,具體取決於你想要監控和了解的資訊。

舉例來說:logging像是回報高速公路上發生的每個車禍。metrics像是偵測每個區間的車流量。他們負責的面向雖然不一樣,但在功能上都同樣可以呈現車流不順暢的狀況。我們也有可能同時觀察兩種資訊,來更精確的了解目前發生的狀況。

因為我幾乎沒有使用Tracing來觀測系統,大多是以logging和metrics結合為主,以下就只寫前兩個。

logging

現在的log形式多半是以結構化的方式儲存。白話來說就是存成一個json物件,物件內可能包含時間(timestamp)、檔案名稱(file)、行號(line)、函式名稱(func)、錯誤訊息(msg)、程度(severity)和其他輔助資訊。輸出時再根據格式format成單行的純文字檔。

其中我們最常用來觀察程式執行狀況的是程度或等級(severity or level),也就是:error, warning, info...,通常的共識是:

將log以統一的方式進行程度分級是有必要的,特別是維運時不一定有辦法得知該訊息的嚴重程度,誤植就會帶來噪音。我的分類方法是:

使用的方法則是:

  1. 編譯Release版時移除trace的log,避免影響效能
  2. 建立三種log處理後端,分別是stdout/err, file, logging system。其中logging system是用來自動化監控用的,可以使用如elk或efk等三件套。
  3. 只把error(和以上)的log直接輸出到警告或工單系統,因此要謹慎決定是否使用error level。

metrics

metrics分成了多個層面,從維運的角度來看可以在這塊蒐集很多資訊,而開發者則需要聚焦在如何利用metrics暴露程式內的執行狀況。通常來說不同層面的metrics會有連動性,舉例來說CPU使用率非常高可能代表服務尖峰,或是程式出現異常狀況,所以需要同時觀察多個metrics來判斷原因。

開發者可以從這幾個角度來思考應該提供的metrics:

  1. 輸入
  2. 輸出
  3. queue長度
  4. 耗時
  5. 活動量

輸入/輸出也可以代換成請求/回應,藉由紀錄正常和異常的數量並判斷比例來初步評估一個服務是否處於正常運行的狀態。舉例來說像是HTTP狀態為5xx的比例如果過高,很有可能是服務遭遇某種嚴重的異常。

耗時和queue長度是另一組相關的數據。由於我們可能利用message queue來控制請求的併發數量,或是利用job queue來將工作異步化處理,如果消費者端發生問題導致來不及消化,症狀就會顯現在queue長度不斷增加。而耗時便是判斷在消費者端是否處理請求時發生異常,比如網路問題導致無法讀寫,或是上游api回應太慢。這個queue可以是分散式系統內的mq,也可以是程式內部交換資料的queue。如果開發者沒有使用類似job queue的方式,而是使用per job per thread的方式,也可以監控thread數量。

活動量是最後一類數據,比如連線數、訂閱數、目前執行的工作數量...。這些在開發環境中可能不太會製造問題,但在線上環境中有可能因為大量的連線或工作導致denied of service。