原文地址 Read Consistency with Database Replicas
數(shù)據(jù)庫副本中的讀一致性
在Shopify, 我們一直使用數(shù)據(jù)圖復(fù)制進(jìn)行冗余及回復(fù),但是最近我們開始使用數(shù)據(jù)庫副本作為只讀庫(讀寫分離)∽喾颍可以緩解主庫對時間敏感性的讀寫操作的壓力月匣。
這會有一個不可避免的問題:復(fù)制延遲。簡單來說就是應(yīng)用讀取從庫的數(shù)據(jù)時有可能會讀到幾秒鐘或者幾分鐘之前的老數(shù)據(jù)脓鹃。根據(jù)英勇的需要這可能并不一定是一個問題逸尖。使用從庫的應(yīng)用必須要接收讀取到過期數(shù)據(jù)的可能性。這只是程度上的問題瘸右。當(dāng)這是原子性操作時娇跟,這就會變成一個問題。
有一種情況是太颤,當(dāng)一個查詢的數(shù)據(jù)是需要多個查詢結(jié)果合并而成的苞俘,這時多個查詢會路由到具有不同延遲程度的從庫副本中,這時查詢出來的數(shù)據(jù)是不可預(yù)測的龄章。
舉例來說吃谣,一個初始查詢可以查到的數(shù)據(jù)在第二次查詢中可能會查不到,因?yàn)槁酚傻搅艘粋€延遲更大的副本中做裙。很明顯這種情況對用戶體驗(yàn)非常不友好岗憋,如果這種查詢數(shù)據(jù)還會應(yīng)用到未來的寫入操作中,會是個很大的問題锚贱。這篇文章講述了Shopify 的數(shù)據(jù)連接管理團(tuán)隊(duì)是如何解決不同延遲引發(fā)的問題的仔戈。
緊湊一致性(Tight Consistency (類似 Strick consistency))
一種處理不同延遲問題的方法是執(zhí)行緊湊一致性,所有副本保證已經(jīng)與主庫同步才可以允許其他操作拧廊。這種方案代價很高而且使得使用副本的性能優(yōu)勢無效监徘。即使仍然可以降低主庫的負(fù)載,但是等待復(fù)制會影響性能卦绣。
因果一致性(Causal Consistency)
還有一種考慮到的方式就是使用GTID(global transaction identifier)的因果一致性方案(已經(jīng)開始實(shí)現(xiàn))耐量。主庫中的每個業(yè)務(wù)都會有一個GTID,這個GTID也會被保存到數(shù)據(jù)副本中滤港。這可以使每個請求根據(jù)GTID路由到各個從庫副本中廊蜒,這樣我們就可以確定從庫相對主庫的延遲程度趴拧,理論上這并不是緊湊一致性,但是實(shí)踐時可以達(dá)到同樣的效果山叮。
這種方式的主要弊端是在各個副本中必須有上報GTID給數(shù)據(jù)庫代理的應(yīng)用功能著榴,這樣數(shù)據(jù)庫代理才可以根據(jù)GTID 來路由到指定從庫副本中。最終我們決定不采用這種方式屁倔,因?yàn)槲覀儾恍枰@個級別的一致性保證及這種復(fù)雜度也是沒必要的脑又。
我們的一致性解決方案(Monotonic read Consistency)
其他一致性模型必然涉及某種妥協(xié),而我們選擇了單調(diào)讀一致性(monotonic read consistency)方案锐借,連續(xù)的讀請求即使不需要讀到最新的數(shù)據(jù)问麸,但也會沿著一個固定的時間線。最直接的方式就是一系列相關(guān)的讀請求都路由到相同的副本數(shù)據(jù)庫钞翔,這樣連續(xù)的請求數(shù)據(jù)與上一個請求相比都是距離主庫相同或更晚的數(shù)據(jù)严卖。
為了簡單的實(shí)現(xiàn)及避免沒必要的開銷,通史也希望避免應(yīng)用必須了解數(shù)據(jù)庫拓?fù)洳⑶夜芾砀北静冀巍砜匆幌挛覀兪窃趺醋龅南剩紫任覀兿瓤匆幌抡麄€流程。
首相應(yīng)用接入MySQL數(shù)據(jù)庫是通過ProxySQL的代理層使用的一種hostgroups概念:對于應(yīng)用來說就是一個單一的數(shù)據(jù)源汰扭,內(nèi)部是一個個變換服務(wù)器池稠肘。
當(dāng)一個客戶端應(yīng)用連接并獲得身份認(rèn)證后,代理會路由這一個單獨(dú)請求到在hostgroup中隨機(jī)的一個服務(wù)器中萝毛。(這一部分簡化了內(nèi)部的負(fù)載均衡等的一些算法项阴,為了更好的討論本文的主題)。為了提供讀一致性珊泳,我們修改了ProxySQL源碼中的這個服務(wù)選擇算法部分鲁冯。
任何應(yīng)用必須提供一個 UID(unique identifier),以鍵值對方式拼接在所有SQL 的最前面
/* consistent_read_id:<some unique ID> */ SELECT <fields> FROM <table>
首先色查,需要對這個UID進(jìn)行HASH算法得到一個integer值薯演。我們通過計算這個integer的模來獲得可用服務(wù)器中的一個。為了讓應(yīng)用在連續(xù)的請求中獲得相同的服務(wù)器秧了,它們必須傳遞相同的UID跨扮。這個解釋簡化了ProxySQL在權(quán)重方面的配置,以下是包含權(quán)重的這個算法的流程验毡。
遇到的一些坑
我已經(jīng)講了這個算法衡创,但是要想達(dá)到完美還需要解決一些問題。
第一個問題是在代碼審查期間出現(xiàn)的晶通,涉及到服務(wù)器在連續(xù)一致的讀請求之間變得不可用的情況璃氢。如果不可用的服務(wù)器是之前選中的服務(wù)器(因此可能會再次選中),則可能出現(xiàn)數(shù)據(jù)不一致——這是我們的方法的內(nèi)置限制狮辽。然而一也,即使不可用的服務(wù)器不是應(yīng)該被選擇的服務(wù)器巢寡,直接對可用服務(wù)器列表應(yīng)用選擇算法(就像ProxySQL對隨機(jī)服務(wù)器選擇所做的那樣)也可能導(dǎo)致不一致,但在這種情況下是不必要的椰苟。為了解決這個問題抑月,我們首先對主機(jī)組中已配置服務(wù)器的整個列表進(jìn)行索引,然后取消所選服務(wù)器的資格舆蝴,并在必要時重新選擇谦絮。這樣,僅當(dāng)所選服務(wù)器關(guān)閉時才會影響結(jié)果洁仗,而不會影響列表中其他服務(wù)器的索引层皱。關(guān)于這個問題在不同情況下的討論可以在 ebrary.net 上找到。
第二個問題是在極少數(shù)情況會出現(xiàn)的一致性bug中發(fā)現(xiàn)的京痢。ProxySQL會在初始服務(wù)器選擇后在進(jìn)行一次額外的負(fù)載均衡選擇奶甘。舉個例子,我們的目標(biāo)權(quán)重是 1:1 但是實(shí)際的鏈接分布會偏移到 3:1祭椰,任何請求都會被強(qiáng)制重新路由到權(quán)重不足的服務(wù)器,我們重寫了服務(wù)選擇方法并去掉了額外的負(fù)載均衡方法疲陕。
目前方淤,我們正在評估將靈活使用復(fù)制滯后測量作為可調(diào)因素的策略,我們可以使用它來修改讀取一致性的方法蹄殃。希望這個特性將繼續(xù)吸引我們的應(yīng)用程序開發(fā)人員携茂,并為每個人提高數(shù)據(jù)庫性能。
我們的解決方案的有點(diǎn)事簡單并且低開銷诅岩。它的主要缺點(diǎn)是服務(wù)停機(jī)()所產(chǎn)生的一致性問題難以檢測讳苦。If your application is tolerant of occasional failures in read consistency,這個解決方案應(yīng)該非常適合你吩谦。如果你需要更嚴(yán)格的一致性方案鸳谜,GTID 因果一致性方案應(yīng)該值得去探索。更詳細(xì)的內(nèi)容可以在這里查看 Adaptive query routing based on GTID tracking式廷。