你真的很熟分散式和事務嗎?

foreach_break發表於2015-07-13

微吐槽

hello,world.

不想了,我等碼農,還是看看怎麼來處理分散式系統中的事務這個老大難吧!

  • 本文略長,讀者需要有一定耐心,如果你是高階碼農或者架構師級別,你可以跳過。
  • 本文注重實戰或者實現,不涉及CAP,略提ACID。
  • 本文適合基礎分散式程式設計師:
  • 本文會涉及叢集中節點的failover和recover問題.
  • 本文會涉及事務及不透明事務的問題.
  • 本文會提到微博和Twitter,並引出一個大資料問題.

由於分散式這個話題太大,事務這個話題也太大,我們從一個叢集的一個小小節點開始談起。

 

叢集中存活的節點與同步

分散式系統中,如何判斷一個節點(node)是否存活?

kafka這樣認為:

此節點和zookeeper能喊話.(Keep sessions with zookeeper through heartbeats.)
此節點如果是個從節點,必須能夠儘可能忠實地反映主節點的資料變化。也就是說,必須能夠在主節點寫了新資料後,及時複製這些變化的資料,所謂及時,不能拉下太多哦.
那麼,符合上面兩個條件的節點就可以認為是存活的,也可以認為是同步的(in-sync).

關於第1點,大家對心跳都很熟悉,那麼我們可以這樣認為某個節點不能和zookeeper喊話了:

 

關於第二點,要稍微複雜點了,怎麼搞呢?

來這麼分析:

  1. 資料 messages.
  2. 操作 op-log.
  3. 偏移 position/offset.

 

上面的節點的狀態管理一般由zookeeper來做,leader或者master節點也會維護那麼點狀態。

那麼應用中的leader或者master節點,只需要從zookeeper拉狀態就可以,同時,上面的實現是不是一定最佳呢?不是的,而且多數操作可以合起來,但為了描述節點是否存活這個事兒,我們們這麼寫沒啥問題。

節點死掉、失敗、不同步了,咋處理呢?

好嘛,終於說到failover和recover了,那failover比較簡單,因為還有其它的slave節點在,不影響資料讀取。

  1. 同時多個slave節點失敗了?沒有100%的可用性.資料中心和機房癱瘓、網路電纜切斷、hacker入侵刪了你的根,總之你rp爆表了.
  2. 如果主節點失敗了,那master-master不行嘛?keep-alived或者LVS或者你自己寫failover吧.高可用架構(HA)又是個大件兒了,此文不展開了。

我們來關注下recover方面的東西,這裡把視野開啟點,不僅關注slave節點重啟後追log來同步資料,我們看下在實際應用中,資料請求(包括讀、寫、更新)失敗怎麼辦?

大家可能都會說,重試(retry)唄、重放(replay)唄或者乾脆不管了唄!
行,都行,這些都是策略,但具體怎麼個搞法,你真的清楚了?

 

一個BigData問題

我們先擺個探討的背景:

問題:訊息流,比如微博的微博(真繞),源源不斷地流進我們的應用中,要處理這些訊息,有個需求是這樣的:

Reach is the number of unique people exposed to a URL on Twitter.

那麼,統計一下3小時內的本條微博(url)的reach總數。

怎麼解決呢?

把某時間段內轉發過某條微博(url)的人拉出來,把這些人的粉絲拉出來,去掉重複的人,然後求總數,就是要求的reach.

為了簡單,我們忽略掉日期,先看看這個方法行不行:

頂呱呱,無論如何,求出了Reach啊!

其實這又引出了一個很重要的問題,也是很多大談框架、設計、模式卻往往忽視的問題:效能和資料庫建模的關係。

 

1.資料量有多大?

不知道讀者有木有對這個問題的資料庫I/O有點想法,或者虎軀一震呢?

Computing reach is too intense for a single machine – it can require thousands of database calls and tens of millions of tuples.

在上面的資料庫設計中避免了JOIN,為了提高求大V粉絲的效能,可以將一批大V作為batch/bulk,然後多個batch併發讀,誓死搞死資料庫。

這裡將微博到轉發者表所在的庫,與粉絲庫分離,如果資料更大怎麼辦?

庫再分表…

OK,假設你已經非常熟悉傳統關係型資料庫的分庫分表及資料路由(讀路徑的聚合、寫路徑的分發)、或者你對於sharding技術也很熟悉、或者你良好的結合了HBase的橫向擴充套件能力並有一致性策略來解決其二級索引問題.

總之,儲存和讀取的問題假設你已經解決了,那麼分散式計算呢?

 

2.微博這種應用,人與人之間的關係成圖狀(網),你怎麼建模儲存?

而不僅僅對應這個問題,比如:某人的好友的好友可能和某人有幾分相熟?看看用storm怎麼來解決分散式計算,並提供流式計算的能力:

 

最多處理一次(At most once)

回到主題,引出上面的例子,一是為了引出一個有關分散式(儲存+計算)的問題,二是透漏這麼點意思:

碼農,就應該關注設計和實現的東西,比如Jay Kreps是如何發明Kafka這個輪子的 :

如果你還是碼農級別,我們來務點實吧,前面我們說到recover,節點恢復的問題,那麼我們恢復幾個東西?

基本的:

  • 節點狀態
  • 節點資料

本篇從資料上來討論下這個問題,為使問題再簡單點,我們考慮寫資料的場景,如果我們用write-ahead-log的方式來保證資料複製和一致性,那麼我們會怎麼處理一致性問題呢?

1.主節點有新資料寫入.

2.從節點追log,準備複製這批新資料。從節點做兩件事:
(1). 把資料的id偏移寫入log;
(2). 正要處理資料本身,從節點掛了。

那麼根據上文的節點存活條件,這個從節點掛了這件事被探測到了,從節點由維護人員手動或者其自己恢復了,那麼在加入叢集和小夥伴們繼續玩耍之前,它要同步自己的狀態和資料。

問題來了:

如果根據log內的資料偏移來同步資料,那麼,因為這個節點在處理資料之前就把偏移寫好了,可是那批資料lost-datas沒有得到處理,如果追log之後的資料來同步,那麼那批資料lost-datas就丟了。

在這種情況下,就叫作資料最多處理一次,也就是說資料會丟失。

 

最少處理一次(At least once)

好吧,丟失資料不能容忍,那麼我們換種方式來處理:

1.主節點有新資料寫入.

2.從節點追log,準備複製這批新資料。從節點做兩件事:
(1). 先處理資料;
(2). 正要把資料的id偏移寫入log,從節點掛了。

問題又來了:

如果從節點追log來同步資料,那麼因為那批資料duplicated-datas被處理過了,而資料偏移沒有反映到log中,如果這樣追,會導致這批資料重複。

這種場景,從語義上來講,就是資料最少處理一次,意味著資料處理會重複。

 

僅處理一次(Exactly once)

Transaction

好吧,資料重複也不能容忍?要求挺高啊。

大家都追求的強一致性保證(這裡是最終一致性),怎麼來搞呢?

換句話說,在更新資料的時候,事務能力如何保障呢?

假設一批資料如下:

現在要更新這批資料到庫裡或者log裡,那麼原來的情況是:

如果說可以保證如下三點:

  1. 事務ID的生成是強有序的.(隔離性,序列)
  2. 同一個事務ID對應的一批資料相同.(冪等性,多次操作一個結果)
  3. 單條資料會且僅會出現在某批資料中.(一致性,無遺漏無重複)

那麼,放心大膽的更新好了:

注意到這個更新是ID偏移和資料一起更新的,那麼這個操作靠什麼來保證:原子性。

你的資料庫不提供原子性?後文略有提及。

這裡是更新成功了。如果更新的時候,節點掛了,那麼庫裡或者log裡的id偏移不寫,資料也不處理,等節點恢復,就可以放心去同步,然後加入叢集玩耍了。

所以說,要保證資料僅處理一次,還是挺困難的吧?

上面的保障“僅處理一次”這個語義的實現有什麼問題呢?

效能問題。

這裡已經使用了batch策略來減少到庫或磁碟的Round-Trip Time,那麼這裡的效能問題是什麼呢?

考慮一下,採用master-master架構來保證主節點的可用性,但是一個主節點失敗了,到另一個主節點主持工作,是需要時間的。

假設從節點正在同步,啪!主節點掛了!因為要保證僅處理一次的語義,所以原子性發揮作用,失敗,回滾,然後從主節點拉失敗的資料(你不能就近更新,因為這批資料可能已經變化了,或者你根本沒快取本批資料),結果是什麼呢?

老主節點掛了, 新的主節點還沒啟動,所以這次事務就卡在這裡,直到資料同步的源——主節點可以響應請求。

如果不考慮效能,就此作罷,這也不是什麼大事。

你似乎意猶未盡?來吧,看看“銀彈”是什麼?

 

Opaque-Transaction

現在,我們來追求這樣一種效果:

某條資料在一批資料中(這批資料對應著一個事務),很可能會失敗,但是它會在另一批資料中成功。
換句話說,一批資料的事務ID一定相同。

來看看例子吧,老資料不變,只是多了個欄位:prevReach。

這種情況,新事務的ID更大、更靠後,表明新事務可以執行,還等什麼,直接更新,更新後資料如下:

現在來看下另外的情況:

這種情況怎麼處理?是跳過嗎?因為新資料的事務ID和庫裡或者log裡的事務ID相同,按事務要求這次資料應該已經處理過了,跳過?

不,這種事不能靠猜的,想想我們有的幾個性質,其中關鍵一點就是:

給定一批資料,它們所屬的事務ID相同。

仔細體會下,上面那句話和下面這句話的差別:

給定一個事務ID,任何時候,其所關聯的那批資料相同。

我們應該這麼做,考慮到新到資料的事務ID和儲存中的事務ID一致,所以這批資料可能被分別或者非同步處理了,但是,這批資料對應的事務ID永遠是同一個,那麼,即使這批資料中的A部分先處理了,由於大家都是一個事務ID,那麼A部分的前值是可靠的。

所以,我們將依靠prevReach而不是Reach的值來更新:

你發現了什麼呢?

不同的事務ID,導致了不同的值:

  1. 當事務ID為4,大於儲存中的事務ID3,Reach更新為3+5 = 8.
  2. 當事務ID為3,等於儲存中的事務ID3,Reach更新為2+5 = 7.

這就是Opaque Transaction.

這種事務能力是最強的了,可以保證事務非同步提交。所以不用擔心被卡住了,如果說叢集中:

Transaction:

  • 資料是分批處理的,每個事務ID對應一批確定、相同的資料.
  • 保證事務ID的產生是強有序的.
  • 保證分批的資料不重複、不遺漏.
  • 如果事務失敗,資料來源丟失,那麼後續事務就卡住直到資料來源恢復.

Opaque-Transaction:

  • 資料是分批處理的,每批資料有確定而唯一的事務ID.
  • 保證事務ID的產生是強有序的.
  • 保證分批的資料不重複、不遺漏.
  • 如果事務失敗,資料來源丟失,不影響後續事務,除非後續事務的資料來源也丟了.

其實這個全域性ID的設計也是門藝術:

  • 冗餘關聯表的ID,以減少join,做到O(1)取ID.
  • 冗餘日期(long型)欄位,以避免order by.
  • 冗餘過濾欄位,以避免無二級索引(HBase)的尷尬.
  • 儲存mod-hash的值,以方便分庫、分表後,應用層的資料路由書寫.

這個內容也太多,話題也太大,就不在此展開了。

你現在知道twitter的snowflake生成全域性唯一且有序的ID的重要性了。

 

兩階段提交

現在用zookeeper來做兩階段提交已經是入門級技術,所以也不展開了。

如果你的資料庫不支援原子操作,那麼考慮兩階段提交吧。

 

結語

To be continued.

相關文章