PostgreSQL 事件總線實作驗證 (後續)
這篇 「PostgreSQL 事件總線 (event bus) 實作驗證」文章意外地在臉書引起一些負面評價。究其原因,可能是山姆鍋對於實際使用情境跟與其搭配的產品特性說明不夠清楚所導致。雖然 “I know what I’m doing”, 但為了避免該篇文章被視為誤人子弟的負面教材,只好再來平反一下。
首先,山姆鍋正在開發的軟體產品對於稽核紀錄 (auditing log) 有特別的要求,其中一個需求是使用者對資料的所有修改動作都要有對應的稽核紀錄。概念上,資料庫中資料的修改都是一種「事件 (event)」,而這樣的事件,除了產生稽核紀錄外,也有很多其它潛在用途。所以,山姆鍋選擇以非同步方式將這些事件送給其它元件來負責後續處理,其中一個便是負責產生稽核紀錄,這也是為何前篇文章中,「產生安全稽核紀錄」被列在第一項應用的原因。
關於事件的傳遞,如果選擇在資料庫修改交易完成後,以訊息服務 (如 RabbitMQ/Kafka) 來傳遞事件,會先面臨一個不常發生但棘手的問題:資料修改了,但事件沒送出。事件沒送出,可能是臨時的網路問題或者傳送的程序異常終止,原因不一而足。為了避開這個問題,山姆鍋選擇將事件隨同資料修改在同一個交易 (transaction) 中完成,如此,確保事件的發生被正確紀錄,且這些事件也會長期存放在資料庫中 (最終太舊的資料會被清除,但不影響這裡的論述)。但接下來的問題便是:其它需要處理這些事件的元件 (也稱為事件消費者 /consumer) 要如何即時地收到這些事件通知?如何按照事件發生的時間順序,不遺漏地處理這些事件?
從上面的描述可知,事件的資料已經存在資料庫中,且歷史事件也可能因為新的需求而被重新處理。在說明如何解決上述問題前,就需要先說明這個 Event bus 的設計所要支援的產品特性。這個軟體產品是針對中小型企業內部使用的系統,實際使用者是 IT 工程師,且由 IT 工程師自行安裝以及運維。所以,可以假設這個產品的同時在線使用人數不高,對多數客戶來說可能是個位數。雖然此軟體可以跑在 3 台來實現高可用需求,但在最精簡的配置,整個系統的所有服務要能夠跑在一台伺服器上。所以,自然會希望這個軟體需要的服務元件越少越好,但其中一定有的服務元件是 PostgreSQL (主資料庫),跟 Redis (Cache)。
關於通知元件有新事件發生的作法,大致有考慮以下三個方案:
- Redis Streams
- RabbitMQ
- Kafka
基於上述架構精簡化的原因,加上不能假設企業的 IT 工程師有能力維護一套 Kafka 系統,Kafka 很早就被排除在方案之外。Redis 的 AOF persistence (預設 fsync 時間為 1 秒) 以及 replication 並無法保證在 Redis carsh 時資料不會遺失。不過這點在這裡不是問題,因為實際的事件資料已經存在 PostgreSQL, 這裡 Redis/RabbitMQ 只是用來分發 / 通知新的事件。
Event consumer 一開始執行 (或者跟訊息服務斷線) 會先確認自己取得對應 subscription 的處理資格 (透過 distributed lock), 然後便是從 PostgreSQL 處理歷史資料,等到歷史資料處理完畢,才會接收新事件處理。所以,Redis Streams 跟 RabbitMQ 其實都是可行的方案,不過細心的讀者應該已經猜到山姆鍋選擇的作法:就是 Redis Streams。原因很簡單,因為產品裡已經有 Redis 了!
不過,為何上篇文章提到的 Event bus 沒有用 Redis Streams 而是全部使用 PostgreSQL 提供的機制?背後的原因是為了完成上面 PostgreSQL + Redis Streams 的事件處理架構,還需要有個元件 (底下稱為 Pumper) 負責將新的事件資料轉發到 Redis Streams, 而完成 Pumper 這個元件的要求就跟上篇文章中的 Event consumer 相同,需要即時知道有新事件,事件要按照時間順序處理且不能遺漏。為了備援,可能會有多個 Pumper 實例同時在執行,但同時間只能有一個在處理事件,這個可以使用 Distributed lock 來達成。至於通知,簡單的 pubsub 機制就可以實現。
Distributed lock 以及 Pubsub 這兩個技術要求,Redis 以及 PostgreSQL 都可以提供支援。之所以選擇使用 PostgreSQL 方案,上篇文章作為方案驗證,單純使用 PostgreSQL 實作上比較簡單,但也適用在 consumer 數量不多的情況下。所以,PostgreSQL 自始至終都是作為 Event store 的用途,Event bus 的功能確實是投機取巧 (不用白不用?)。
下面針對臉書上的一些反饋,山姆鍋提供一下自己的看法。
“pg 這麼用,如果只是小貓二三隻的公司內部系統當然還死不了”
不好意思,目標客戶真的是小貓二三隻。
“db master 只有一台,要 Scale 一定要 down time”
Master 資料庫是這個產品的核心元件,如果主資料庫要硬體升級或者其他原因需要停機,整個服務都會停止。這跟有沒有把 PostgreSQL 作為 MQ 或者 locking server 對於可用性的結果都一樣。
“db 只用來存需要 long term persistance 的 data”
前面已經說明,PostgreSQL 用來長期存放事件資料。也許有讀者會認為:這不是跟 Event sourcing 的機制很像?沒錯,只不過在這個產品,Event store/bus 並不是要支援 Event sourcing。
“這玩法就是 outbox pattern 的誤用啊”
Transactional outbox 老實說是第一次聽到,雖然這樣的 Pattern 在 N 年前山姆鍋就已經知道,不過有了名稱,之後跟別人說明也比較方便了 :-)。
小結
作為一個工程師,山姆鍋常常跟團隊分享:選擇適合的方案來解決問題,而不是找尋 (自認) 完美的作法。了解工具的特性、分析優缺點、根據實際的使用情境挑選適當可行方案才是務實的作法。所謂「適當」,也就是知道自己的取捨是什麼,記得不存在所謂放諸四海皆準的通則。最後,如果 “You know what you are doing”, 不用擔心跟別人意見相左。