mysql binlog in realtime



衆所周知,MySQL是最受歡迎的互聯網數據庫(沒有之一)———————爲開源而生。發展初期,很多公司都受益於其易用性和經濟性。隨着這些公司的成長,越來越多的公司投入到MySQL的開發中,因此MySQL的特性也越來越豐富,如:不同特性的存儲引擎、Binlog主從複製方案等。

今天我們要探討的就是如何實時解析MySQL Binlog,以及其所帶來的巨大的業務價值。我可以在一瞬間說出很多應用場景,如:主從複製、緩存系統、檢索引擎等。如下圖所示,MySQL的Binlog不僅僅能用於MySQL服務之間的主從複製,還能造福其他非MySQL服務:

binlog_app.png

在一個成熟的商業系統中我們有且僅有一份正確的全量數據(這裏指MySQL中的數據)。而面對現在應用豐富的功能以及用戶越來越高的要求,MySQL並不能一直很好的滿足我們。比如:我們需要使用緩存系統來加速服務,需要完備的檢索系統來提供搜索服務,需要事件觸發實時地發送通知等。但是擺在我們面前的問題是隻有一份數據,且我們直接操作的都是MySQL(本文用MySQL來代表所有數據庫系統),如何能讓除MySQL之外的其他系統擁有同樣的數據?且我們總是希望MySQL中的數據與其他系統的數據差異越小越好,越實時越好。當然我們可能會有一系列方案來解決這個問題,最典型的方法有兩種:

  • 雙寫, 當你在更新MySQL的同時更新緩存系統、更新檢索引擎,觸發事件發送通知。這麼做的確能解決問題,但是因爲都是異構系統,沒有一種機制能夠保證寫入成功。也就是說隨着時間的推移,數據差異會越來越大。不僅如此,如果外部系統很多,這麼做還會影響到主業務邏輯的響應時間。
  • 定時任務,在主業務邏輯中耦合更新其他系統的代碼是不OK的,外部系統只有幾個還行,如果要更新100個外部系統,這時候當如何處理呢?通過定時任務異步地去做這件事情看上去是一個更好的方案。的確如此,但是任務執行頻率是多少呢?每1分鐘?每10分鐘?當然我們可以根據業務需要制定這個頻率,乍想一下沒啥問題,不過再想一下,定時任務具體用來做什麼呢?
    • 定時導出全量:在數據量比較少的時候這是最省事兒的方法,當然下游每次必須清空自己的數據重新全量導入,數據量稍大一點代價就特別大。
    • 業務系統維護一個隊列(在業務庫中新建表當作隊列):在業務系統中操作數據的時候順便把自己操作的日誌寫到隊列裏面,然後由下游來消費這個隊列。

      這種方式由業務系統來維護操作記錄,好處是能保證數據的業務完整性,壞處是在業務端耦合了非業務相關的邏輯,每當數據有變更時都需要開啓事務保證業務操作和其操作日誌都能正確地寫入。如果以後因爲某些原因要直接操作DB,這種情況就很危險了,在操作DB時無法模擬複雜的業務邏輯計算和關係。

    • 定時掃表找出變更記錄:每條數據都會有一個updateTime (on update CURRENT_TIMESTAMP)字段,當該條記錄被修改時,MySQL會隱式地更新這個字段爲當前時間。這種方法使業務端遠離水火,以一種更解耦的方式來處理增量,非常適合無狀態緩存更新之類的場景,當然如果表記錄數過大可能會有慢查詢。但如果存在狀態流轉的的數據,這種方式會丟失狀態流轉方向。

如果你使用隊列方式,很可能會寫出下面這段代碼:

$db = getDI()->get("test_db");
$db->begin();
$db->update("// logic operate");
$db->insert("// operate logs");
$db->commit();

而且很可能你設計的操作日誌表看起來會是這樣子的:

 event_id   user_id   object_id    level   event_type   mcid   addtime             
      152 
      153 
      154 
      48 
      48 
      48 
 3007209739 
 3007209739 
 3007209739 
   200 
   202 
   201 
         23 
         24 
         24 
    0 
    0 
    0 
 2013-07-17 04:06:05 
 0000-00-00 00:00:00 
 0000-00-00 00:00:00 

如果你掃表的話可能會寫出下面這段SQL:

SELECT * FROM `products` WHERE updateTime > '2013-11-12 12:00:00' ORDER BY updateTime LIMIT 1000;

下面我來具體介紹下我們的應用場景,以及我們是如何利用MySQL Binlog解決這個問題的,先上圖:

app_arch.png

這是我們整個系統的架構圖,左邊可以看成用戶系統,右邊則是商業系統。瀏覽器向前端搜索服務集羣(圖中紅色部份)發送請求後,前端集羣會把用戶的請求數據發往很多個後端服務,而不同的後端之間並不是同構的,所以基本上都會有一個“長連接/協議轉換代理層”(圖中紅色部份),然後再由後端決定是否滿足用戶需求,最後再由前端集羣拼接來自不同後端服務的結果並呈現給網民。

看似一個海量訪問的服務,其實是由無數個小服務組成的,圖中所示就是我們組做的小服務,在具體場景中藍色部分做的事情是把前端服務集羣的協議轉換成FASTCGI協議,並轉發給後端服務UI/PHP-FPM。雖然名稱叫UI,但是做的不是我們所理解的UI的工作,而是對於後面的檢索和數據流來說它是最前面的一部分,所以才叫UI。它承擔的工作是解析代理層發來的數據,並根據請求數據和參數構造結構化的查詢條件,當然這個構造的過程可能仍需要請求很多外部服務,然後向檢索系統(BS)發出請求,BS返回的是一個實體ID集合(你可以理解爲商品ID),UI根據實體ID到數據庫(這裏省略了緩存系統)裏面查詢實體詳情,整個完程就是這樣。

我們這個架構中也存在上面描述的異構系統,BS(Basic Search)模塊是對Lucene的二次開發,它需要DB中的數據來建立索引,而客戶會時不時地往DB裏面寫一些數據,爲了能讓UI實時地從BS中檢索到新數據,DB的所有變更要能實時地體現到BS中,這是用雙寫/定時任務無法完成的。所以我們開發了基於MySQL Binlog的異步事件框架(AdPipe)。

AdPipe由以下幾部分組成:

  • BIZ framework: 大家都知道PHP是最快的,所以經常需要變更的事件邏輯使用PHP寫。
  • PHP ext: 爲了能在PHP用戶空間寫Binlog事件邏輯,不可避免地需要PHP擴展。
  • Binlog listener: 與MySQL Server連接,實現Binlog議協。

來看看BIZ框架的代碼:代碼包下載

首先是啓動腳本, binlog_task.php:

上面代碼主要職責是連接MySQL Server,並且設置Binlog的位置,建立連接後根據接收到的事件調用BinlogEvent相應的方法。而BinlogEvent所有方法僅僅返回一些狀態碼(IGNORE, SUCCESS, L_EXIT, SIGNAL_QUIT),這些狀態碼控制着消息該如何處理。

其次是事件處理類,BinlogEvent:

看171行,getHandleClass方法通過獲取一個業務對象來處理事件,如果沒有找到處理某個DB事件的類,那麼默認會調用DefaultEvent來處理。

請看DefaultEvent類:DefaultEvent.php


由於Binlog事件和DB的scheme是息息相關的,所以在DefaultEvent中也調用了Model類來獲取表字段。每個事件處理類都有三個方法,onWrite, onUpdate, onDelete,你可以在方法中獲取DB變更的數據,然後根據業務需求做各種變換,然後打包成消息。

接下來就說到消息類,這裏的消息是指邏輯消息,是經過業務代碼變換後準備發往BS的消息,Message:Message.php

以上就是AdPipe系統中biz framework的代碼,當然代碼要能運行,需要安裝binlog listener和php-binlog擴展,請點擊下面鏈接查看:

具體安裝過程可以分別查看它們的readme。

如果biz framework掛了,會影響其他系統嗎?

這是個好問題,如果biz framework掛了,我們也不用擔心,因爲在我們的架構中AdPipe的上游和下游都是隊列,上游是binlog隊列,而且文件存在MySQL服務器上:

     filename           position    
 mysql-bin.000001 
 mysql-bin.000002 
      ...         
    422655739    
    124544114    
      ...        

而我們的下游是AMQ之類的消息隊列,BS通過AMQ來獲取消息。上下游均通過隊列解耦,所以biz framework不幸掛了,不會影響其他系統。

如果biz framework掛了,能快速恢復運行嗎?

當然可以,biz framework每次處理完事件後,都會保存該事件的filename和position,所以如果服務掛掉,可以在連接MySQL服務器之後顯示地設置filename和position,如:binlog_set_position($link, $filename, $position)。

下游(BS)如果掛了,能快速恢復嗎?

這是個非常關鍵的問題,如果BS掛了它需要重建索引。隨着系統的運行,增量數據越來越多,如果給BS一份最原始的基準基量文件,那麼BS要消費從系統啓動至今的所有增量消息,這可能需要幾個月甚至一年。如果我們有一份最新的全量基準文件,情況就大不一樣了,所以AdPipe另一個功能是定時生成最新的全量基準文件,以供下游恢復數據使用,這樣下游只需追有限的增量消息便能跟上DB的數據。

所以這進一步引發我們對這個事情的思考,能不能設計一個通用的事件觸發系統呢,這裏我們只考慮 文件(inotify)/MySQL(binlog)/MongoDB(oplog),其他類似於redis也有keyspace notifications功能。

generic_binlog_arch.png






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章