Observability¶
說明¶
應用程式是需要維運的,分散式系統的維運和傳統上有什麼差異呢?
本章會依序介紹 Log、Tracing、Metric、Alert 和 Health Check。
Log¶
在本地端測試時,時常用 console.log
或 print
這類函示來確認請求在執行過程中沒有發生什麼意料之外的事。
若在線上部署後,我要怎麼從遠在他方的 server 中取得相關訊息?
下面會介紹 ELK(Elasticsearch
, Logstash
, and Kibana
)怎麼讓你拿到 server 的 log 資訊。
Tracing¶
若一個請求歷程會經過多項服務,我要怎麼區分不同的請求?
例如:發現 A 服務在和 B 服務請求資料時,得到錯誤資料。 於是接著查看 B 服務的 log,然而同一時間卻有很多請求,茫茫大海如何尋?
此時就可以使用 Tracing 的機制,範例中會使用 Zipkin。
request ID
代表各個請求的 ID,必須是不能重複的字串。
Metric¶
除了針對單一請求的流程檢閱,若我要做統計呢?
例如:機器的 CPU 使用率、回應 4xx/5xx 的比率、API 使用分佈
這一類需要累積的資料,我們便稱其為 Metric,範例中會使用 Graphite,StatsD 和 Grafana。
Alert¶
有了 Metric,我們便可以設定些閥值,來通知開發人員以及早得知服務的狀態。
例如 5xx 比率超過 3% 時,提出警告通知
範例中會使用 Grafana。
Health Check¶
若服務的依賴(例如 DB)連線斷掉或甚至服務本身斷線時,我們能否及早知道?同時自動重新建立連線,或甚至重新啟動該服務。
範例中會使用 HAProxy
比較¶
種類 | 多服務才有意義 | 功能 | 所屬(概略) |
---|---|---|---|
Log | X | Debug | Dev |
Tracing | O | Debug | Dev-Ops |
Metric | X | 統計 | Ops |
Alert | X | 分析 | Dev-Ops SE |
實作¶
接下來進入一系列實作。
Log¶
ELK
是 Elastic 開發的開源產品,其開發的不僅僅是 ELK
這三項產品。
本範例僅會討論 ELK
代表的 Elasticsearch、Logstash、Kibana 這三個,其中各產品定位和功能如下。
Elasticsearch¶
一種資料庫,並對外提供 API,會高效的搜尋相關資訊。並且有其標準的 query 語法。
Elasticsearch is fast. Really, really fast.
Elasticsearch 擁有很多功能,本次僅會示範幾種,若需要更多資訊,詳見。
Logstash¶
Elasticsearch 是一種資料庫,但是儲存的資料需要有人給他,這便是 Logstash 的功能。負責把收到的資料整合送給 Elasticsearch。
送什麼資料是由應用程式方決定的,可以是 log file、HTTP 等。 本範例是透過 HTTP on UDP 來傳送。
Kibana¶
Elasticsearch 是一種資料庫,但是並未提供 UI 介面,這時 Kibana 就是把 API 轉成可讓人透過網頁的方式來操作。
範例程式碼¶
對使用來說,不需要去考慮傳送到 Logstash 的邏輯
logger.info(
{
path: req.url,
method: req.method,
ip: req.ip,
ua: req.headers["user-agent"] || null,
},
"request-incoming"
);
無論用什麼格式撰寫 log,統一就好。
初始化 logger
const client = dgram.createSocket("udp4");
const stream = {
write: (msg: string) => {
client.send(msg, LS_PORT, LS_HOST);
},
};
const logger = pino({ level: "trace" }, stream);
Alternative¶
- Datadog
- Sumo Logic
- Splunk
- Cloud (AWS, GCP, ELK)
Metrics¶
其架構會和 Log 很像,Elasticsearch 也包含這類的 query 語法。 其重點差異便是在 log 處理對象為單一請求,而 metrics 處理的是服務在累積流量或時間後的狀態,如上所述。
範例中會使用的是 Grafana
、Graphite
和 StatsD
。
和 log 非常相似,這裡就不贅述其意義。
程式碼¶
初始化 client
const client = new StatsDClient({
host: "localhost",
port: 8125,
prefix: "web-api",
});
統計請求時間和次數
const begin = new Date();
await got(`http://${TARGET}/recipes/42`);
client.timing("outbound.recipe-api.request-time", begin);
client.increment("outbound.recipe-api.request-count");
統計系統資源
setInterval(() => {
client.gauge("server.conn", server.connections);
const m = process.memoryUsage();
client.gauge("server.memory.used", m.heapUsed);
client.gauge("server.memory.total", m.heapTotal);
const h = v8.getHeapStatistics();
client.gauge("server.heap.size", h.used_heap_size);
client.gauge("server.heap.limit", h.heap_size_limit);
// try to mock memory heap
fs.readdir(__dirname, (err, list) => {
if (err) return;
client.gauge("server.descriptors", list.length);
});
}, 10_000);
範例中,把記憶體等相關資訊放在應用程式中。 實際上,為了避免應用程式斷線,導致收集不到服務的資訊,這類的資料收集應在服務外使用。
Alternative¶
Graphite
-> Prometheus, InfluxDBStatsD
-> cAdvisor, MetricBeat
Tracing¶
在分散式系統中,多個服務間的溝通或請求都可能在過程中得到非預期的結果,為了能快速找到問題點,需要辨別同一支請求在各個服務中的位置。
透過建立 request ID 來辨別同一支請求在多個服務中的位置。
產出範例:
web-api 會傳送 task1
、fetch
和 task2
,recipe-api 會傳送 recipe
程式碼¶
const tracer = new Tracer({
ctxImpl,
recorder,
localServiceName: "web-api",
sampler: new sampler.CountingSampler(1),
});
紀錄請求開始時和結束時
app.use(expressMiddleware({ tracer }));
紀錄需要執行 100ms 的任務
await tracer.local<Promise<void>>(
'do_some_task',
() => new Promise((resolve) => setTimeout(resolve, 100)),
);
使用包裝後的 got
來去請求,在使用時不需要考慮其 tracing 邏輯。
await instance(`http://${TARGET}/recipes/42`).json();
const instance = got.extend({
hooks: {
init: [(opts) => (opts._zipkin.parentId = tracer.id)],
beforeRequest: [
(opts) => {
// ...
},
],
afterResponse: [
(res) => {
// ...
},
],
beforeError: [
(err) => {
// ...
return err;
},
],
},
});
Alert¶
Demo in production
Health Check¶
這大部分會和 Alert 一起實作,目的是持續打特定 route 去得知服務現有狀況。
但現有狀況會有些模糊地帶,例如:
有產品使用 cache 去達到 SLA。當 cache 失去運作,對應用程式來說仍能運行。但整體效率會降低,進而提高 Latency,這時服務的狀態算是健康還是不健康?
這種模糊地帶被稱作
degraded
,至於該怎麼處理,就和商務邏輯的實作有關了。
範例¶
範例中,會用 r-proxy 做 health check 且應用程式綁在一起。 但實作上,仍建議讓應用程式外的產品或甚至外單位的產品去做 health check。 例如:PagerDuty、Nagios、Pingdom。 除了 r-proxy,專做 health check 的開源軟體有:Cabot。
HAProxy 的設定黨
# ...
backend web-api
option httpchk GET /health
server web-api-1 localhost:3001 check
server web-api-2 localhost:3002 check
app.get("/health", (_req, res) => {
return res.send("OK");
});