一.概述
分布式系統(tǒng)存在網(wǎng)絡(luò),時(shí)鐘,以及許多不可預(yù)測的故障。分布式事務(wù),一致性與共識問題,迄今為止仍沒有得到很好的解決方案。要想完美地解決分布式系統(tǒng)中的問題不太可能,但是實(shí)踐中應(yīng)對特定問題仍有許多可靠的解決方案。本文不會談及諸如BASE, CAP, ACID 等空泛的理論,只基于實(shí)踐中遇到的問題提出可行的解決方案。
二.常見問題
1.讀自己的寫
現(xiàn)象: 用戶在發(fā)布頁發(fā)布了帖子,然后訪問自己的主頁查看帖子列表,并沒有馬上看到自己剛剛發(fā)布的帖子,等待1~2s后才看到
分析:后端db采取主從結(jié)構(gòu),復(fù)制任務(wù)在負(fù)載較高的情況下會有延遲。用戶讀取帖子列表查詢的是從節(jié)點(diǎn),所以無法及時(shí)看到剛剛發(fā)布的帖子。一般情況下延遲1~2s是可以接受的,但是為了更好的體驗(yàn),可以做一些改進(jìn)。
解決方案:
- 如果用戶讀取的是自己的主頁,就訪問主節(jié)點(diǎn)。如果訪問是他人的主頁,就訪問從節(jié)點(diǎn)。只需要在db層路由即可。
- 客戶端還可以記住最近更新時(shí)的時(shí)間戳,并附帶在讀請求中,據(jù)此信息,系統(tǒng)可以確保對該用戶提供讀服務(wù)時(shí)都應(yīng)該至少包含了該時(shí)間戳的更新。如果不夠新,要么交由另一個(gè)副本來處理,要么等待直到副本接收到了最近的更新
2.單調(diào)讀
現(xiàn)象:用戶查看某個(gè)帖子下面的評論,一會兒看到5條評論,一會兒看到6條評論。
分析:后端db采取主從結(jié)構(gòu),復(fù)制任務(wù)在負(fù)載較高的情況下會有延遲。用戶讀取評論列表查詢的是從節(jié)點(diǎn),但是兩次讀的是不同的從節(jié)點(diǎn),當(dāng)某個(gè)從節(jié)點(diǎn)具有明顯延遲就會出現(xiàn)數(shù)據(jù)反復(fù)的現(xiàn)象。
解決方案:
- 確保同一個(gè)用戶每次都是讀取同一個(gè)副本,可以在db層進(jìn)行路由。這是一種典型的sticky 請求路由。
????replica = hash(user_id) % number_of_replica
3.負(fù)載傾斜與熱點(diǎn)問題
現(xiàn)象:某個(gè)分區(qū)的數(shù)據(jù)明顯比其他分區(qū)多,并且訪問頻率高,負(fù)載壓力大。
分析:在某些特殊的業(yè)務(wù)場景下,比如官方或者名人賬號有百萬粉絲,當(dāng)這些賬號發(fā)布消息事件時(shí),人們會對該消息進(jìn)行評論,如果評論數(shù)據(jù)存儲使用事件id進(jìn)行hash,就會造成某個(gè)分區(qū)的負(fù)載產(chǎn)生傾斜。
解決:
- ??在關(guān)鍵詞,比如消息事件id,的開頭或者結(jié)尾添加一個(gè)隨機(jī)數(shù)。只需一個(gè)兩位數(shù)的十進(jìn)制隨機(jī)數(shù)就可以將關(guān)鍵字的寫做操作分布到100個(gè)不同的關(guān)鍵字上,從而分片到不同的分區(qū)上。這些特殊邏輯只應(yīng)用在一些特殊賬號上。
4.fencing令牌
現(xiàn)象:在采用分布式鎖的情況下,數(shù)據(jù)庫中的事務(wù)重復(fù)執(zhí)行。
分析:在分布式鎖環(huán)境中,客戶端A執(zhí)行事務(wù)超時(shí),分布式鎖被釋放??蛻舳薆執(zhí)行事務(wù)插入數(shù)據(jù)??蛻舳薃恢復(fù)后繼續(xù)執(zhí)行事務(wù),重復(fù)插入數(shù)據(jù)。
解決方案:
- 這不是分布式事務(wù)的范疇。可以采用fencing令牌來解決。我們假設(shè)每次鎖服務(wù)授予鎖或租約時(shí),同時(shí)還會返回一個(gè)fencing令牌,該令牌每授予一次就會遞增。然后,要求客戶端每次向存儲系統(tǒng)發(fā)生寫請求時(shí),都必須包含所持有的fencing令牌。當(dāng)使用zookeeper 作為鎖服務(wù)時(shí),可以用事務(wù)標(biāo)識zxid,或節(jié)點(diǎn)版本cversion來充當(dāng)fencing令牌,這兩個(gè)都可以滿足單調(diào)遞增的要求。
5.Lamport時(shí)間戳
現(xiàn)象:客戶端從兩個(gè)分區(qū)獲取兩條不同的數(shù)據(jù),比如事件a, b;a的序號小于b,但事實(shí)上b比a先發(fā)生。
分析:常見的有以下幾種非因果序列發(fā)生器,產(chǎn)生的序列號與因果關(guān)系并不嚴(yán)格一致。
- 每個(gè)節(jié)點(diǎn)單獨(dú)產(chǎn)生自己的一組序列號。
- 把墻上時(shí)間戳信息(物理時(shí)鐘)附加在每個(gè)操作上。
- 預(yù)先分配好序列號的區(qū)間范圍,比如節(jié)點(diǎn)A負(fù)責(zé)區(qū)間1~1000的序列號,節(jié)點(diǎn)B負(fù)責(zé)1001~2000。
解決方案:
- 使用Lamport時(shí)間戳。Lamport時(shí)間戳是一個(gè)kv對(計(jì)數(shù)器,節(jié)點(diǎn)ID)。核心流程:每個(gè)節(jié)點(diǎn)以及每個(gè)客戶端都跟蹤迄今為止所見到的最大計(jì)數(shù)器,并在每個(gè)請求中附帶該最大計(jì)數(shù)器值。當(dāng)節(jié)點(diǎn)收到請求(或者回復(fù))時(shí),如果發(fā)現(xiàn)請求內(nèi)嵌的最大計(jì)數(shù)器大于節(jié)點(diǎn)自身的計(jì)數(shù)器,則它立即把自己的計(jì)數(shù)器修改為該最大值。
????
6.端到端的重復(fù)消除問題
現(xiàn)象:消息重復(fù)是非常普遍的,比如
- 生產(chǎn)者發(fā)送消息到消費(fèi)者,消費(fèi)者消費(fèi)成功后宕機(jī),但是卻沒有更新消費(fèi)位置,消費(fèi)者重啟后就會重新消費(fèi)。
- 常見的rpc調(diào)用,調(diào)用方因?yàn)榫W(wǎng)絡(luò)問題沒有收到被調(diào)用方的響應(yīng),選擇重試。
- 2PC 分布式事務(wù)中,因?yàn)榫W(wǎng)絡(luò)問題,也可能出現(xiàn)重復(fù)事務(wù)的問題。
- 用戶在頁面重復(fù)提交POST請求。
分析:端到端的重復(fù)問題是非常普遍的,在TCP 網(wǎng)絡(luò)中也需要處理重復(fù)數(shù)據(jù)包的問題。有以下兩種解決辦法:
- 最有效的辦法之一是使操作滿足冪等性,即無論執(zhí)行一次還是多次,確保具有相同的結(jié)果。比如以下語句無論執(zhí)行多少次效果都是一致的。
???update table set v = v2 where v = v1
- 可以為操作生成一個(gè)唯一的標(biāo)識符如(UUID),服務(wù)端對此UUID 進(jìn)行去重校驗(yàn)。
??
- 在典型的電商下單接口中采用了以上兩種方法的結(jié)合:使用唯一標(biāo)識符來進(jìn)行去重,如果寫入異常返回之前的訂單。
create table order(
# ...
dedup_key varchar(60) not null comment 'key to pretend order duplication',
client_id,
# ...
unique uniq_dedup_key(dedup_key, client_id)
);
@Transactional
Order createOrder(Integer userId, String prodCode, Decimal amount, String dedupKey) {
try {
String orderId = createOrder(userId, prodCode, amount, deupKey); // insert a new order
Order order = getOrderById(orderId); // read order from db
order.setDuplicated(false); // 標(biāo)記是否有重復(fù)下單
return order;
} catch(UniqueKeyViolationException e) {
// if duplicated order has existed, return previous order
Order order = getOrderByDedupKey(dedupKey, clientId);
order.setDuplicated(true);
return order;
} catch (Exception e) {
// hanlde other errors and rollback transaction ...
}
}
7.唯一性約束
現(xiàn)象:在集群高并發(fā)的環(huán)境下,用戶A創(chuàng)建用戶marquezzzz,用戶B同時(shí)創(chuàng)建了用戶marquezzzz,兩者的用戶名相同,這違背了唯一性約束。
分析:創(chuàng)建用戶名的邏輯是,先去db中查詢是否有對應(yīng)的用戶名(步驟1),如果沒有就創(chuàng)建,如果存在就更新用戶的其他信息(步驟2)。用戶A執(zhí)行了步驟1, 用戶B執(zhí)行了步驟1和2,然后用戶A執(zhí)行了步驟2,這樣生成了兩個(gè)同名的用戶。
解決方案:
- 串行化請求,將創(chuàng)建用戶的請求串行化,比如發(fā)送到隊(duì)列中,這樣可以確保全局唯一性。
- 在db層進(jìn)行唯一性約束,比如使用唯一索引,考慮到龐大的數(shù)據(jù)量,性能會下降。如果做了分表,唯一索引的方法也不太可行。
- 使用分布式鎖,比如redis, zookeeper,redis偽代碼如下:
boolean r = redisClient.setnx("userName", currentThread, 10s); // 使用 setnx 原子命令
if (!r) {
return false;
}
// 步驟1 查找db確保沒有重名
// 步驟2 插入用戶
redisClient.delete("userName");
8.時(shí)鐘問題
現(xiàn)象:在許多app中,客戶端會上報(bào)事件,但是事件的發(fā)生時(shí)間不準(zhǔn)確
分析:app客戶端時(shí)鐘可能不準(zhǔn)確,或者用戶手動(dòng)調(diào)整過系統(tǒng)時(shí)鐘。
解決方案:
為了調(diào)整不正確的設(shè)備時(shí)鐘,一種方法是記錄三個(gè)時(shí)間戳:
- 根據(jù)設(shè)備的時(shí)鐘,記錄事件發(fā)生的時(shí)間, device_event_time
- 根據(jù)設(shè)備的時(shí)鐘,記錄將事件發(fā)生到服務(wù)器的時(shí)間, device_send_time
- 根據(jù)服務(wù)器時(shí)鐘,記錄服務(wù)器收到事件的時(shí)間, server_receive_time
事件真實(shí)發(fā)生時(shí)間 = device_event_time + (server_receive_time - device_send_time)
三.參考
《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)》文章來源:http://www.zghlxwxcb.cn/news/detail-442037.html
https://cloud.tencent.com/developer/article/1121727文章來源地址http://www.zghlxwxcb.cn/news/detail-442037.html
到了這里,關(guān)于分布式系統(tǒng)常見問題的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!