這一篇盤點一個交易撮合系統的監控,範圍是委託從入口閘道、依序日誌(替委託編號、寫入多數節點的那份日誌)、撮合引擎到行情發布的處理路徑,以及讀取撮合結果的帳戶與對帳。系統的功能與需求見 Backend 範例系統 的〈交易撮合〉,負載路徑在 9.0.2 交易撮合的負載路徑。盤點的工具是 4.0 服務端監控的分層 的監控鏈:觀察者是做監控的程式(systemd、Prometheus、探測程序、心跳監看服務),監控鏈是依檢查對象排成的一串觀察者,從程序的自檢、服務網路內的探測、元件指標、應用層訊號,到告警規則與分送(規則評估、Alertmanager 與通知管道)、告警鏈的監看與使用者路徑的合成監控。每一層的缺口分成檢查範圍盲區(沒有觀察者檢查那個對象)、位置盲區(觀察者跟被觀察的元件共用一個依賴而一起停)、讀取缺口(觀察者有結果而沒有規則讀它,或通知送不出去)與訊號失真(觀察者自己壞掉沒人知道,或規則的寫法讓結果不可信),完整的定義在 4.0 的〈一層監控檢查的對象與它的缺口〉,判讀依據在 4.0 的〈以監控鏈盤點一個服務的判讀依據〉。撮合引擎以單一執行緒處理委託、延遲目標在毫秒以下,所以觀察者讀數字的位置、沒有新委託時的判讀與延遲的量法,都和 4.0.2 以監控鏈盤點單機短網址服務 與 4.0.3 以監控鏈盤點 Kubernetes 上的電商 checkout 的做法不同。撮合系統的內部監控各交易所不同,這一篇寫的是從機制推出的做法,引用公開文件的地方寫明出處。

撮合引擎單一執行緒時觀察者讀數字的位置:輸出日誌

撮合引擎以單一執行緒、在記憶體裡依序處理委託,每一筆委託的處理時間以微秒計,延遲目標在毫秒以下(兩個數字都是範例系統的示意)。在這條執行緒上做任何一件需要等待的事(寫一行日誌到檔案、更新一個要上鎖的計數器、對外送一個量測封包),都直接加到每一筆委託的延遲上,也讓延遲的尾端變長。所以撮合引擎的執行緒上不能做需要等待的記錄:量測本身會改變被量測的對象。

觀察者讀數字的來源因此移到處理路徑以外。撮合引擎每處理一筆委託,結果寫進它自己的輸出日誌(9.0.2 交易撮合的負載路徑 的〈成交回報與行情:讓慢的接收端不拖慢撮合引擎〉),另一個程序讀這份輸出日誌,把委託數、成交數、拒絕數、各段的時間戳換算成指標,交給 Prometheus 這類觀察者讀取。這支指標換算程序是一種讀輸出日誌的 Prometheus Exporter(把一個元件的狀態轉成 Prometheus 指標的程式),跑在撮合引擎的執行緒以外。輸出日誌是引擎本來就要產生的循序輸出,寫進記憶體裡的環形緩衝、由另一個執行緒落盤,引擎寫它時不等磁碟,所以只要緩衝沒有寫滿,寫輸出日誌不會把等待加到委託的延遲上。

這要有一個前提:讀輸出日誌的程序不能反過來讓引擎等它。指標換算程序讀的是落盤之後的檔案,或以不參與流量控制的方式訂閱(Aeron 稱為 untethered subscription,只在同一條流上另有其他訂閱時才生效;只有一個訂閱時,它照樣決定發布端的速度);指標換算程序若是環形緩衝的消費者,而發布端不能超過最慢消費者讀到的位置(LMAX Disruptor 的 gating),指標換算程序一變慢,就把緩衝填滿、讓引擎停下來等;這個範例的指標換算程序讀的是落盤之後的檔案。落盤的執行緒跟不上時緩衝同樣會滿,所以環形緩衝的填滿程度與落盤的落後量本身也要量。引擎內部的量(內部佇列的深度、每一筆的處理時間分布)不在輸出日誌裡,用同樣不等待的方式記錄:不需要上鎖的計數(例如已處理的最後一個序號),或 HdrHistogram 的 SingleWriterRecorder 這類由單一寫入者記錄、記錄時不等待的 histogram,都由別的執行緒定期讀出。這樣的代價是指標比實際的處理晚一點,而好處是觀察者壞掉、變慢時,撮合不受影響。這也決定了監控鏈各層在這個系統裡的位置:自檢與元件指標照常放在主機與程序上,應用層訊號(每一筆委託成功與否、花了多久)不在引擎的執行緒上量,由指標換算程序從輸出日誌算出。

沒有新委託時分辨市場安靜與處理路徑停住

一般服務的監控以「請求失敗」為訊號,撮合系統最難從輸出日誌發現的故障卻是安靜:撮合引擎卡在主迴圈裡(例如處理某一筆委託時進入無窮迴圈)、或依序日誌失去多數節點而停止接受委託時,輸出日誌裡不會有任何一筆失敗紀錄,只是什麼都沒有發生(失敗只出現在入口那一側:閘道等不到依序日誌確認的逾時,以及用戶端的重送);而市場本身也會有安靜的時候,一個冷門的交易對幾分鐘沒有委託是正常的。只看委託數與成交數,兩種情形在指標上一模一樣。

分辨的做法是讓撮合系統在沒有委託時也產生訊號,而且訊號要走完整條處理路徑。一個注入程序每隔固定時間(示意:一秒),直接寫進依序日誌的入口(不經過閘道),對每一個撮合引擎(依序日誌分區時,對每一個分區)各注入一筆帶著心跳標記的委託(路徑心跳),它和真實委託一樣被編號、寫進依序日誌、由撮合引擎處理,引擎把它原樣寫進輸出日誌。注入程序輸出最後一次成功注入的時間(成功指依序日誌回應了「已接受」),指標換算程序記下每一個撮合引擎最後一筆路徑心跳出現在輸出日誌的時間,Prometheus 依引擎各算一次它距今多久,超過注入間隔的三倍(示意的一秒間隔下是三秒)就代表那個引擎的路徑上某一段停住,這個判斷在沒有委託時同樣成立。

判讀時把產生訊號的元件一起看,路徑心跳停了的情形分成三種。依序日誌之後的路徑停住:注入程序在運作、注入成功(依序日誌回應了「已接受」),指標換算程序也在運作、落後量正常,路徑心跳卻沒有出現在輸出日誌裡,停住的是撮合引擎或它寫輸出日誌的那一段,告警規則寫成這幾個條件同時成立。依序日誌停住:注入程序在運作而注入失敗(等不到「已接受」),閘道等待依序日誌確認的逾時率也同時升高。觀察者那一側停住:注入程序本身停掉,或指標換算程序讀不到、它讀到的位置落後落盤的位置越來越多,撮合本身可能正常,這時重啟撮合引擎(要載入快照、重放依序日誌)沒有用;改看不經過這兩個程序的訊號:閘道的逾時率,以及經過閘道送到同一個撮合引擎的合成交易(每一個撮合引擎各設一個測試交易對時才有)。路徑心跳持續出現、合成交易也成功,是市場安靜;路徑心跳持續出現、合成交易卻失敗,停住的是路徑心跳沒有經過的閘道那一段。撮合引擎的程序整個結束時,那個引擎的最後序號指標跟著消失:引擎的抓取目標還在服務發現裡時,up == 0 就抓得到;目標也消失時,以 absent() 對每一個引擎各寫一條帶引擎標籤的規則,只對整個指標寫一條的話,要全部引擎都消失才會觸發,單一引擎停掉時漏報。

只由撮合引擎自己定時往輸出日誌寫心跳是不夠的:那筆心跳不經過依序日誌,依序日誌停住時引擎閒著,心跳照常寫進輸出日誌,看起來就是市場安靜。路徑心跳和 Dead Man’s Switch(心跳監看) 的機制相同,都是訊號停止才通知,差別在監看者的位置:讀路徑心跳的 Prometheus 與撮合系統在同一個可用區,可用區故障時它們一起停,那種故障由可用區以外的心跳監看服務與外部探測接手(〈撮合系統接上的監控:各層的判讀與補法〉的最後一段)。它也和告警鏈的心跳(Prometheus 定時送給外部監看服務、證明告警鏈還在運作的那一筆)是兩件事。

有委託進來時,入口那一側的兩個數字比路徑心跳更早變化:閘道等待依序日誌確認的逾時率與等待中的委託數,依序日誌停住時它們先升高。行情那一側,行情發布服務把路徑心跳以標記過的訊息轉出,它不參與 conflation(把多則更新合併成一則送出),跟不上的訂閱端也收得到;對外的行情協定多一種訊息之前,要先確認訂閱端相容,否則只在內部的行情通道送。成交回報與帳戶更新依標記略過它。行情的連線也有心跳(FIX 協定在 HeartBtInt 秒內沒有送出任何資料時送一筆 Heartbeat),訂閱端以「多久沒收到任何訊息」判斷的是連線與發布服務還活著;撮合是否還在進行,要看收得到路徑心跳的行情通道裡是否持續出現它。

依序日誌停住另有一個更早的訊號:健康的節點數。依序日誌靠多數節點寫入才回應「已接受」,主節點寫不到多數就不能提交,五個節點的叢集停掉三個就選不出主節點、停止接受委託(Raft 的論文寫明,任意多數節點運作且彼此能通訊時叢集就能完整運作,五個節點能容忍任意兩個故障,見 Consensus Protocol 與 Quorum)。這裡的「健康」指能與主節點通訊並跟得上複寫(跟得上的門檻由日誌叢集的實作決定,例如主節點多久沒收到它的複寫回應),只是程序還在而跟不上複寫的節點不算。所以告警不等到失去多數才成立,在健康節點數降到「再停一個就失去多數」時就送出(五個節點時是剩三個健康),那時依序日誌還在運作,值班還有時間處理。健康節點數的來源是依序日誌叢集自己輸出的成員狀態:主節點知道每一個節點複寫到哪一個序號,跟得上的才算健康。這個數字由主節點輸出,失去多數、選不出主節點時沒有節點輸出它,以健康節點數寫的規則跟著失效;所以每個節點也輸出自己的角色與任期,依序日誌分區時,每個分區各自判斷:沒有任何節點自稱主節點、或健康節點數的指標消失,持續時間超過選舉逾時(以規則的 for: 設定)才告警,否則每一次正常的選舉都會誤報。節點的失效彼此相關時(同一個可用區、同一個機櫃),健康節點數看不出風險:五個節點以三比二分在兩個可用區時,放三個節點的那個可用區一故障就失去多數,而故障之前健康節點數一直是五。這是節點放置的問題,部署時要把節點分到三個以上的失效域;這個範例為了延遲把節點放在同一個可用區,失效域只能是可用區裡的機櫃或主機,整個可用區故障的風險仍在,由〈撮合系統接上的監控:各層的判讀與補法〉最後一段的外部觀察者接手。分到不同機櫃也有代價:以 AWS 為例,一台執行個體一次只能放在一個 placement group,把執行個體緊密放在一起的 cluster 類型不保證分在不同機櫃,要分機櫃得改用 spread 或 partition 類型,延遲會上升多少要實測(Failure Domain、Correlated Failure)。

委託延遲的分段量測與時鐘

撮合系統的延遲要依段量,因為使用者感受到的是整段,而值班能動手的是其中一段。一筆委託的生命週期裡記四個時間戳:閘道收到時由閘道記,寫入多數節點時由依序日誌的主節點依序號記下提交的時間(提交的時間要等寫入多數之後才知道,寫不回同一筆紀錄,所以由指標換算程序依序號把它與輸出日誌合併),撮合引擎處理完時由引擎讀時鐘、寫在輸出紀錄的附帶欄位裡(這個欄位只用於量測,不參與撮合的決策),成交回報送出時由發布服務記;相鄰兩個時間戳相減就是那一段的延遲。每一段的長短由不同的資源決定:閘道到依序日誌寫入多數節點這一段,由日誌節點的磁碟寫入與節點之間的網路往返決定;依序日誌到撮合引擎處理完這一段,由撮合引擎那一條執行緒分到的 CPU 時間決定,同一台機器上的其他程序搶走 CPU、或執行環境暫停整個程序(JVM 的垃圾回收與 safepoint)時這一段先變長;撮合引擎處理完到成交回報送出這一段,由輸出日誌的落盤與發布服務讀取、送出的速度決定。

延遲以 Histogram 記錄、看 p99 與更尾端的百分位數:依序日誌的主節點選舉、垃圾回收或一次磁碟寫入變慢,只落在少數委託上,平均值看不出來,而那幾筆委託的成交價格正是使用者會抱怨的那幾筆(Tail Latency)。histogram 的桶邊界要涵蓋微秒到毫秒(示意:10 微秒到 10 毫秒);Prometheus 官方 Go 用戶端的預設桶(DefBuckets)從 5 毫秒起,毫秒以下的延遲會全部落在第一個桶,算出來的 p99 只是桶內的插值。

這幾個時間戳記在不同的機器上,相減之前要先確定兩台機器的時鐘對得上:兩台機器的時鐘差一毫秒,算出來的一段延遲就差一毫秒,而延遲目標本身就在毫秒以下(Clock Skew)。所以時鐘同步的狀態本身要監看:node_exporter 的 timex 收集器輸出核心從校時程式得到的偏差估計(node_timex_offset_seconds),連同是否已同步(node_timex_sync_status)與估計誤差(node_timex_estimated_error_seconds)一起看。偏差估計只在校時程式使用核心的相位鎖定迴路(例如 ntpd)時才有值;chrony 只調整核心的頻率、不設定這個欄位,在 chrony 下 node_timex_offset_seconds 恆為零,以它寫的規則永遠不會觸發,這本身就是一種訊號失真。用 chrony 時改讀 chrony 自己回報的偏差(chronyc tracking,或 chrony 的 exporter),同步狀態與估計誤差照樣從 timex 讀;用 PTP 時讀 ptp4l 與 phc2sys 回報的偏差。這個偏差是每台機器對校時來源的偏差,一段跨兩台機器的延遲誤差最大是兩台偏差之和,所以每台對校時來源的門檻要設成那一段可接受誤差的一半(可接受誤差由那一段的延遲目標決定,例如那一段延遲目標的一成,示意);偏差超過門檻時,分段延遲的數字就不可信,這是監控鏈裡訊號失真的一種。受歐盟 MiFID II 規範的交易場所有法定的上限:RTS 25(Delegated Regulation (EU) 2017/574) 附件的 Table 1 規定,gateway-to-gateway 延遲在 1 毫秒以下的交易場所,時鐘與 UTC 的偏差最多 100 微秒,時間戳粒度 1 微秒或更細;這個上限是每台時鐘對 UTC 的偏差,兩台都在上限內時,彼此仍可能差到 200 微秒;受規範的交易場所取這個上限與自己算出的門檻裡較嚴的那一個。node_timex 的值是本機的自我估計,看不到網路路徑不對稱造成的誤差,所以偏差小不保證兩台機器對得上;要做到微秒級的對齊,要改用 PTP(Precision Time Protocol,IEEE 1588)這類以硬體時間戳校時的協定。

跨機器的分段需要對得上的時鐘,同一台機器上的量測則不需要:閘道以自己的單調時鐘(不受校時程式把時間調前調後的跳動影響、只受頻率微調影響的時鐘,Linux 的 CLOCK_MONOTONIC)量「送出到收到已接受」的來回時間,不受機器之間偏差的影響,可以拿來對照跨機器算出的分段。同一台機器上相減的兩個時間戳若取自牆上時間(會被校時程式調整的那個時鐘),校時程式在兩個時間戳之間調整時鐘也會讓差值失準。

撮合系統接上的監控:各層的判讀與補法

範例系統的監控設定如下(假設,不是實測)。閘道與撮合引擎的程序由 systemd 管理、失敗時重啟,沒有設 WatchdogSec;Prometheus 讀取各主機的 node_exporter 與指標換算程序輸出的指標,指標換算程序算出每個交易對的委託數、成交數、拒絕數與各段延遲;延遲的告警規則以平均值與五分鐘視窗寫;Alertmanager 依嚴重度送到值班的推播服務;一個監看程序每分鐘做一次合成交易(Synthetic Monitoring):以測試帳號經過閘道對一個測試用的交易對送一筆委託再取消,量從送出到收到取消回報的時間。依序日誌的節點、撮合引擎與監控都在同一個可用區的同一組實體相近的主機上(依序日誌的節點這樣放的理由在 9.0.2 交易撮合的負載路徑 的〈依序日誌:決定順序並保證不遺失〉,監控也放在那裡是這個範例的設定)。

監控鏈的層這個系統現有的監控判讀
容器與程序的自檢systemd 管理閘道與撮合引擎,失敗時重啟,沒有設 WatchdogSec讀取缺口:重啟沒有規則讀。檢查範圍盲區:撮合引擎卡在主迴圈裡時程序還在,只看程序存活的 systemd 看不出來
服務網路內的探測合成交易:測試帳號每分鐘對測試交易對送一筆委託再取消檢查範圍盲區:測試交易對與真實交易對在不同的撮合引擎上時,只證明測試的那一個引擎在運作
元件指標node_exporter;依序日誌的節點數、環形緩衝與執行環境的停頓沒有指標檢查範圍盲區:依序日誌的健康節點數沒有人量,失去多數之前沒有預警;環形緩衝的填滿程度、落盤的落後量與執行環境的停頓(JVM 的垃圾回收與 safepoint)沒有人量。讀取缺口:node_exporter 已輸出時鐘的同步狀態與估計誤差(node_timex_*),沒有規則讀
應用層訊號指標換算程序算出的委託數、成交數、拒絕數與各段延遲;閘道的逾時率沒有量檢查範圍盲區:沒有路徑心跳,路徑停住與市場安靜分不出來;閘道等待依序日誌確認的逾時率沒有量。訊號失真:指標換算程序自己停掉或落後時,它輸出的指標一起消失,沒有規則發現;跨機器的分段延遲在時鐘漂移時不可信(成因見元件指標列)
告警規則與分送Prometheus 規則與 Alertmanager,送到值班推播訊號失真:延遲的規則以平均值與五分鐘視窗寫,毫秒以下的延遲目標在五分鐘的平均裡看不出尾端
告警鏈的監看無檢查範圍盲區:沒有心跳監看服務;補的觀察者要放在這個可用區以外,依序日誌的節點與監控在同一個可用區,可用區故障時兩者一起停
使用者路徑的合成監控無檢查範圍盲區:沒有從外部經過公開入口(API 閘道與行情的公開端點)送委託、訂閱行情的探測

重啟次數由 node_exporter 的 systemd 收集器輸出(開啟它的重啟計數之後),以它寫一條規則送出告警,自檢那一列的讀取缺口就補上了。同一列的檢查範圍盲區(撮合引擎卡在主迴圈),systemd 的 WatchdogSec 也偵測得到:程序要定期回報,沒回報就判失敗。回報不必在撮合引擎的執行緒上做,同一個程序裡的另一個執行緒可以在讀到引擎的最後序號有前進時才送出(有路徑心跳時,市場安靜序號也會前進;沒有路徑心跳時,市場安靜序號不前進,這個做法會把安靜判成卡住)。systemd 的文件寫的是接受主程序送出的回報,同一個程序裡的其他執行緒也算主程序,這一點是從機制推出的。這個做法另有一個盲點:依序日誌停住時引擎收不到委託,序號同樣不前進,逾時之後 systemd 會終止一個本身正常的撮合引擎。真正的代價在逾時的處置:systemd 會終止這個帶著委託簿狀態的程序,恢復要載入快照、重放依序日誌,停頓若只是一時的垃圾回收,終止反而把幾秒的停頓變成更長的停機。卡住的成因若是某一筆委託(處理它時進入無窮迴圈),引擎的處理是確定的,重放到同一筆就會再卡住,處理同一份輸入的備援副本也會一起卡住,自動重啟只會變成重啟迴圈。所以這個範例把偵測交給路徑心跳,由值班依訊號處置:引擎的最後序號停在同一個值、而依序日誌的位置還在前進時,下一個序號的那筆委託就是嫌疑,處置是暫停那個交易對,接上 9.0.2 的只收取消、再以集合競價重開的流程。

探測那一列的盲區,優先的補法是對每一個撮合引擎各設一個測試交易對。把測試委託送進真實交易對有兩個限制:價格要在閘道允許的範圍內、站在不會立即成交的那一側(買單低於最佳買價),太遠離市價的委託在閘道就被擋下,到不了撮合引擎;而刻意掛單再取消貼近操縱市場的定義(美國商品交易法把「在成交前就打算取消的出價或報價」定為 spoofing),要先確認交易所的規則與主管機關的要求允許這種測試。

元件指標那一列,依序日誌的健康節點數以「健康節點數小於多數加一」寫告警(五個節點時剩三個健康就送出),沒有節點自稱主節點時的規則與節點放置的前提在〈沒有新委託時分辨市場安靜與處理路徑停住〉;時鐘偏差以校時程式自己回報的偏差寫規則(chrony 下不能用 node_timex_offset_seconds),門檻的算法與指標的選法在〈委託延遲的分段量測與時鐘〉。環形緩衝的填滿程度與落盤的落後量由引擎以不需要上鎖的計數輸出,填滿程度持續升高時,引擎離「停下來等落盤或等消費者」已經不遠;JVM 這類有暫停的執行環境,由另一個執行緒讀出每次停頓的時間與次數(例如 JVM 的 GC 通知),停頓時間超過延遲目標的那幾次,就是尾端延遲的成因之一。

應用層訊號那一列,路徑心跳同時補上兩件事:撮合引擎卡在主迴圈與依序日誌停住,都讓它停止出現,路徑心跳停了時怎麼分出是依序日誌之後的路徑停住、依序日誌停住還是觀察者那一側停住,寫在〈沒有新委託時分辨市場安靜與處理路徑停住〉。閘道等待依序日誌確認的逾時率另寫一條規則。指標換算程序本身也是觀察者,要有它自己的「讀不到它」規則(up == 0 或 absent()),並輸出它的落後量(Consumer Lag)。落後量分成兩段各自歸因:落盤的位置減去指標換算程序讀到的位置,是觀察者這一段;引擎的最後序號減去落盤的位置,是落盤的落後,屬於處理路徑,它一直增加時環形緩衝會滿、引擎會停。落後量同時以筆數與時間計:筆數是撮合引擎最後一個序號減去指標換算程序讀到的最後一個序號,時間是最新一筆輸出紀錄的時間戳減去它讀到的那一筆的時間戳。筆數換成時間要除以當下的處理速率,市場安靜與忙碌時同一個筆數代表的時間可以差好幾個量級,所以門檻以時間寫,筆數用來判斷落後的成因是讀得慢還是輸出多。

告警規則那一列,延遲的規則改以 histogram 的高百分位數寫,視窗縮短到能看出一次幾秒的停頓(示意:一分鐘),代價是告警更容易被短暫的抖動觸發,所以分成兩條:短視窗的高百分位數送聊天頻道、持續數分鐘的才呼叫值班。

告警鏈的監看與使用者路徑這兩層,觀察者都要放到這個可用區以外:為了延遲把撮合系統放進同一個可用區,同一個可用區的故障就會讓撮合、依序日誌與監控一起停,而那正是最需要有人被通知的時候(4.25 可觀測性與被觀測系統的共命運失效)。外部的探測從使用者那一端經過公開入口送測試委託、訂閱行情,比對路徑心跳是否持續出現在行情裡。告警鏈的監看也要證明通知送得出去:以 alertmanager_notifications_failed_total 寫的送失敗規則,路由到上面補在可用區以外的那個心跳監看服務,作為與 Prometheus 定時心跳分開的另一個檢查項目。

監控鏈事前偵測不到的異常:以資料正確性為主逐類查撮合系統

撮合系統在監控鏈以外最重的一類是資料正確性:同一筆委託被處理兩次或撮錯,結果是重複成交與錯的帳戶部位(Backend 範例系統 的〈四個系統在請求性質上的差別〉),而監控鏈的每一層都會照常顯示正常。其餘各類(業務結果、到期事件、備份、排程工作與背景工作、安全、第三方依賴與成本)的分類與通則在 4.0.1 監控鏈事前偵測不到的異常,以下逐類寫撮合系統裡的對象。

資料正確性分成三項檢查。序號連續檢查:輸出日誌的序號要連續,每一個消費端(行情發布、成交回報、帳戶更新)對每一個撮合引擎的輸出各自以「收到的序號與上一個相差不是一」偵測斷號(每個引擎有自己的序號),斷號代表有一段輸出沒有送到這個消費端。部位對帳:帳戶的部位(帳戶持有的各項資產數量,資金也是其中一項)在前一次對帳的切點上的值,加上這段期間的成交、手續費、出入金與資金劃轉,要等於帳戶系統裡現在的部位,每天重算一次比對。成交價比對:行情公布的最新成交價要等於成交紀錄裡最後一筆的價格,取樣比對。序號連續檢查在單一消費端內完成,部位對帳與成交價比對是兩份資料之間的比對(Data Reconciliation),兩份資料都在持續變動、寫入的時間也不同(帳戶歷史的資料庫由輸出日誌非同步寫入,落後幾秒),所以比對以輸出日誌的序號為切點:每個撮合引擎有自己的序號,所以切點是每個引擎各一個序號,帳戶系統與歷史資料庫依引擎各自記下套用到哪一個序號;每天在固定的時間取每個引擎當時的最後一個序號當切點,帳戶系統在切點上記下部位,等兩邊都套用到切點之後,比對截至切點的部位與成交。出入金與資金劃轉不在輸出日誌裡,序號切點涵蓋不到它們,要另外以出入金帳本的位置當切點、與撮合的切點一起取,或讓出入金也經過依序日誌。24 小時交易的市場沒有收盤這個安靜的時刻,不取切點的比對會因為兩邊落在不同序號而持續報不一致。

序號連續檢查、部位對帳與成交價比對用到的成交資料都來自同一份輸出日誌,所以抓的是消費端漏掉、重複或算錯的紀錄;撮合引擎自己撮錯的成交在輸出日誌裡就已經是錯的,這三項檢查都抓不到。只靠輸出日誌能檢查的不變量抓得到其中幾種撮錯:成交價不劣於任一方的限價、一筆委託的成交數量不超過委託數量、連續撮合的狀態下處理之後委託簿不交叉(最佳買價低於最佳賣價;集合競價收單期間委託簿本來就允許交叉)、每筆成交的買賣數量相等、取消之後不再成交、剩餘數量等於委託數量減去累計成交與取消的數量。價格優先與時間優先的順序錯了時,這些不變量仍然成立,那一種錯要另外以獨立實作的撮合邏輯重放同一段依序日誌、比對成交結果,這是從機制推出的做法。

業務結果:成交量的正常範圍隨市場劇烈變化,只和上週同一時段比較會誤報;日內與週內的規律可以當基線的一部分,再加上同一時段其他交易對的成交量變化;同一個商品在其他交易場所也有交易時,再比較那些場所的成交量:其他交易對或其他場所都在成交而這裡沒有,排除本場所自己的價格限制與暫停交易之後,才是異常(基線的建法在 4.14 Anomaly Detection)。交易所處在只收取消、集合競價這類特殊狀態時,狀態本身寫成一個指標,告警規則依狀態調整,否則重開期間每一條成交量的規則都會成立;停機後先只收取消、再以集合競價重開的流程,寫在 9.0.2 交易撮合的負載路徑 的〈撮合引擎:單一寫入者與記憶體內的委託簿〉。

到期事件:數位憑證只在建立連線的 TLS 握手時驗證,閘道與行情公開端點的憑證到期時,已經連著的長連線不受影響,之後每一條新連線與重連都握手失敗,斷線的用戶端在重試迴圈裡累積;換上新憑證的那一刻,累積的用戶端一起連上,形成一次集中的連線湧入(Thundering Herd;9.0.2 交易撮合的負載路徑 的〈入口閘道:在進入撮合之前能擋下的委託〉寫的重連退避);憑證到期要在剩下一段天數時告警,與 4.0.1 的做法相同。

備份:撮合系統的備份是委託簿的快照加上快照之後的依序日誌,還原的方式是重放;還原得回來要實際演練一次:在另一個環境載入快照、重放依序日誌,比對重放後的委託簿與正式環境同一個序號時的委託簿是否相同(只用於量測的附帶欄位,例如處理完的時間戳,不在比對範圍內)。重放結果不同的成因有四種:演練環境與正式環境的引擎版本或設定不同、快照的切點與依序日誌的序號沒對齊、依序日誌有缺段,以及撮合的決策讀了時鐘或亂數這類不在依序日誌裡的輸入。只有撮合決策的不確定性代表引擎有缺陷,這個缺陷平常不會出現在任何指標上,只有演練會發現。

排程工作與背景工作:輸出日誌的每一個消費端(行情發布、成交回報、帳戶更新、寫入帳戶歷史與報表資料庫的程序、指標換算程序)監看各自的落後量,以時間計(筆數與時間的關係在〈撮合系統接上的監控:各層的判讀與補法〉的應用層訊號那一段),門檻依消費端的需求分開定:行情與成交回報落後幾百毫秒使用者就看得到,報表落後幾分鐘沒有人發現。每天重算部位的對帳工作完成時對心跳監看服務送一次心跳。

安全:管理操作(調整交易對的參數、暫停交易、修改帳戶的速率限制)寫進稽核紀錄,與 4.12 Audit Log 邊界與 PII 治理 的做法相同;受監管的交易所另有保存委託與成交紀錄的法規要求,不在本篇範圍。

第三方依賴與成本:撮合的處理路徑上沒有第三方,出入金與身分驗證的業者屬於帳戶那一側,盤點方式同 4.0.3 以監控鏈盤點 Kubernetes 上的電商 checkout 的〈監控鏈事前偵測不到的異常:逐類查 checkout〉裡第三方依賴的那一段;容量事先配置、不自動擴充,成本的變化主要來自硬體與機房的配置變更,日常的成本告警在這個系統裡不重要。