丁俊:京東商城K-V儲存產品的演化之路

儲存頻道發表於2018-11-28

   導語:本文根據丁俊老師於第十屆中國系統架構師大會(SACC 2018)的現場演講《JD K-V儲存產品演化之路》內容整理而成。

   講師介紹:

   丁俊 ,京東商城線上儲存部負責人,主要負責分散式儲存系統、分散式訊息系統、分散式服務框架等產品的開發和維護。

   正文:

  大家好,我今天講的題目是京東KV儲存產品的演進之路。我不會具體講每一個產品是怎麼做的,而是重點去講我們在開發中碰到的一些問題,以及我們是怎麼解決的。當然這些問題的解決方法不一定最優,也不一定完全適合大家。大家可以共同探討,作為一種解決思路。

  我今天主要講三個部分,一個部分是記憶體儲存(jimdb),另一塊是持久化儲存(sharkstore),還有一塊就是我們目前想要做的混合儲存。

  記憶體針對的是一個高吞吐、低延遲的場景;持續化儲存更強調的是可靠性和容量上面。我們後面想做一些混合儲存,主要是因為在業務發展過程中有很多資料慢慢從記憶體裡面沉澱下來,可能不常使用,而因為業務、成本等各方面的考慮,沒有將其從記憶體中挪出來,我們就想能不能透過平臺統一把這些事情處理一下。

  下面我大概介紹一下我們各個產品的時間節點。2014年我們記憶體儲存產品的第一個版本上線,當時是公司內部有各種各樣的一些開源產品的使用,每個小組也有自己的維護系統,包括開發一些管理介面,維護工具等等,公司決定把它統一起來,由一個獨立的團隊去提供這樣一個產品。

  2014年上線以後,業務量增長比較大,在2015年的時候,我們就面臨了一些新的挑戰。隨著業務資料的增長,比如在記憶體裡面,有一些業務資料可能今年的增長量是前面所有年份的總和,甚至還要多。2015年資料的擴容,包括我們服務的產品越來越多,搭建的叢集越來越多,它的故障恢復能力都給我們帶來了一些挑戰。第二個版本重點解決這些問題。

  2017年,我們有了異地的機房,就需要支援中介軟體的異地多活。現在的這個產品,我們也做了異地多活的解決方案。2018年記憶體方面的大部分事情,主要的矛盾解決差不多了,我們就做了持久化的儲存,去解決一些成本上的問題,包括和一些廠商去探索新的儲存,比如這個介質是不是可以節省一些記憶體,從降低成本上去進行一個考量。2018年當我們的儲存系統上線以後,就在這個基礎上,為一些業務提供了分散式鎖,包括配置分發的功能。再後面就是我們正在研發中的一個事情,希望能夠把混合儲存提上去。

   記憶體儲存的實踐與挑戰

  現在講第一部分,記憶體儲存。這裡先講運營資料,當然不是為了說明這個系統有多複雜,其實系統不是特別複雜,主要是我覺得一個系統碰到的主要問題和解決問題的思路,可能是隨著系統的資料量訪問壓力來的,所以說大家在挑選方案的時候,還是要結合自己的業務特點,從成本、實現的難度,維護人員的數量、緊急度等等一些方面去考量。平常我們整個平臺大概每秒鐘可能有上億次的訪問,記憶體的程式數大概在10萬級以上。

  下圖是我們記憶體儲存的架構圖。

  該架構的主要特點包括:高吞吐、低延遲,能夠自動故障恢復,可以線上伸縮,能夠進行廣域複製等等。

  下面我主要講一下故障檢測。說到故障檢測,大家在部署的系統當中其實都會面臨這個問題,都需要容災,對硬體、網路的故障等進行檢測,讓你的業務避免受到這些故障的影響,提升可用率。我們在做故障檢測的時候,如果說我本來就沒幾個例項,可能就是部署一個哨兵,或者有一些別的方案去探測。檢測一個服務是否存活,可能常用這幾種方法:一種是主動探測,另一塊就是由本身提供服務的人,去上報它的狀態是不是好的。

  如果故障檢測沒有做好,會有什麼問題?其實如果說你是一個沒有狀態的服務,探測錯了也不會引起太多的問題,最多可能就是導致效能的偶爾波動。作為一個有狀態的服務,如果檢測錯了,很可能導致的後果就是,可能你的資料會寫丟。比如說你檢測到一個master,它是一個寫入點,你認為它是故障的,有一部分節點還在往這個節點上寫入,另一部分節點認為它故障以後,可能會選出一個新的寫入點。如果你的業務同時往兩個寫入點進行寫入,勢必就會導致資料丟失。

  導致這種檢測誤判的主要原因有哪些呢?目前我們主要面臨的一些問題,一個就是網路的分割——當然如果部署的節點比較少,本身可能就這一個網路、一個機架裡面或者一臺交換機下面,這種故障的可能性會小一些。但是當你的服務部署在整個機房的各個角落,包括甚至可能在同城的多個機房裡面,這種網路的故障出現的機率就會大很多。另外其實我們還碰到一個問題,就是一個長任務的執行導致系統阻塞,探測的時候,可能會探測到它不是存活的,因為它沒有在一定的時間內給你一個響應。比如說你認為多少秒以後它就“死亡”了,如果說任務執行的時間超過這個時間,你也可能會認為它是死亡的。

  大家可以認為一個長期阻塞的任務是“死的”,當然也可以認為它是“活的”——因為本身程式還在,這個要看你的業務特點。但是我們認為,這種場景儘量不要把它“判死”,為什麼?我們發現往往你把它“判死”、“殺掉”、在別的地方恢復起來以後,業務往往還會執行相同的事情。相當於不停地“殺掉”-起來、“殺掉”-起來,進入這樣一個惡性迴圈中。

  對於這些問題,我們主要有這幾種解決辦法。第一個,我們把探測點部署在機房的各個角落裡,分佈在不同的機架和交換機下面,他們組成一組探測服務,共同去投票決定這個服務是“死亡”的、還是存活狀態。這是一點,解決網路的問題。第二就是,對於任務阻塞的情況,我們是在伺服器上面部署一個agent,因為本身伺服器上有agent去做一些指標採集等,也會做一個判斷程式是否存在的服務,結合這兩點去防止誤判。

  下面說一下故障的自動恢復。恢復其實很簡單,就是你探測到它不存活以後,可能透過一系列的手段,比如說slave“死了”,副本“死掉”以後,再加一個副本就可以了。如果說你的寫入節點master“死掉”了,從現有的副本里面、slave上面去提升一個,提升為master就可以了。我覺得在做業務系統的時候,為了提升使用率不一定要完全去依賴這種探測的服務,你還應該有一些別的手段,在客戶端可以做一些容錯的策略,比如說如果你是讀寫分離的,那能不能夠在slave“死掉”以後自動去讀一下master,恢復以後再去讀slave。比如說,如果我在同城有別的機房,有兩個對等的叢集,那我能不能在這一個叢集訪問不了的時候,業務客戶端去訪問另一個叢集。在這種規模比較小的時候,可能大家實際開發成本都是相對比較低的。

  還有一個問題就是,是不是所有的故障都一定要去恢復。這個場景其實不太經常碰到,但是如果碰到了,確實需要注意一下。因為我這麼多年做這種服務,偶爾也有碰到過那麼一兩次。就是說我發現一個大面積的故障,比如探測到某一個機房斷電了,那要不要去做故障恢復?也許這種場景下去做故障恢復帶來的後果可能更大,可能你還沒有完全把資料都自動恢復好的時候,機房可能已經幫你把電也已經通上了。而在自動恢復時,會佔用大量內部網路的流量,對現有業務產生影響。所以在這裡我主要想提一句,有一些場景可能需要你去做一個系統的決策,做一個智慧的判斷,但有時也不一定要做的這麼複雜,如果系統簡單,那做一個開關就好了。

  下面說一下遷移的過程。當系統的資料量越來越大時,原來分配的空間可能不夠了,需要擴容,系統需要升級,這時你也需要做資料遷移,因為資料在記憶體裡面,沒有辦法進行原地升級。我們知道在資料遷移中,通常第一步可能就做一個快照,做完快照以後再補增量。在這裡要提一句,就是說我們在補增量當中,實現完第一版以後,發現系統有時會卡頓一段時間。因為首先每個業務寫入的流量大小不一樣,有的業務可能剛好碰到寫入量比較大的那段時間,你會發現增量的資料比較多,這個時候就可能阻塞住。

線上遷移流程圖

  為了保證前後、新舊服務的資料一致性,你可能需要把老的寫入給停了。後面發現一個業務特點就是,讀比較多一些。我們可以把讀給放開,讓它在遷移的過程當中,只阻止寫——也就是變化的地方。

  下面說一下記憶體儲存的廣域複製的問題。我可能在華東、華北有兩個機房,我的服務部署在華北,有一個master,也有一個slave。現在我可能需要在華東也提供一個服務,常用的就是把一個資料複製過去就可以了。這裡有一個前提,因為它是在記憶體裡面,所有的資料都在記憶體裡面,我能不能夠直接在華東掛一個副本到我的master去。

  個人認為在業務量比較小、資料比較少的時候,這種方案是可以的。但是大家都知道,廣域網路上面可能網路延遲比較高,網路的質量比較差一些。這就帶來一個問題,可能會有中斷。那我要快取的、要同步的資料,可能在記憶體裡面會積攢比較長的一段。這樣,當我資料量比較少的時候,能不能把緩衝區直接調大,就可以快取更多的增量資料,避免這種存量。不過平臺、服務的叢集很多的時候,如果把每一個例項記憶體都調大,可能浪費的資源就比較大了。

  我們採用的方案如下圖,在華北新建一個同步的模組去模擬slave,把master的資料同步下來,儲存在本地的機房,再把資料傳送給華東的叢集。這裡我們會有兩個master,意味著就可以接受兩個地方寫入,目前我們線上也有這樣的一些服務在跑。比如,大部分是華北作為一個主要的叢集進行寫入,華東是不接受寫的,只有讀,但也有一些業務可能需要在兩地同時多寫,我們也提供在華東接受寫入。

  這裡可能會面臨一個問題,就是華北寫入的key複製到了華東,如果華東也要往回複製的話,同一個key是不是就在這裡“轉圈”?其實我們在key裡面打了一個標,它從哪個地方寫入就打哪裡的標,當sync服務在複製這些資料的時候,會跳過這些key,這樣就實現了兩個叢集之間的相互複製。當然這裡也有一個問題是沒有解決的,需要業務去解決,比如說華東和華北同時寫同一個key,這可能就面臨一些問題。第一,複製是延遲的,第二就是一致性等問題。我們這裡其實沒有去解決,還是靠上層的業務去規避。就是說在華東寫入的key不會在華北寫入,華北寫入的key不會在華東寫入。或者業務上能夠接受這種混寫,就是說以誰為準都可以。

   持久化儲存的實踐與挑戰

  下面講一下持久化儲存。其實持久化的KV儲存其實有很多開源的實現,大家的一些實現思路也都大同小異。我們也一樣,選用了一些常用的開源元件,在這基礎之上進行開發。自己開發這個系統的原因,一方面是為了更好地和記憶體儲存的API等等相容,還有一些自己的業務特性在裡面。

  持久化儲存的特點包括:1、分散式強一致;2、支援線上分裂、自動故障恢復;3、支援schema,海量資料;4、支援範圍查詢,單表操作。

  下圖是持久化儲存的邏輯檢視,其實KV的儲存裡面,大家的key和value不一定是列的,那我們選用這樣一種方案,一方面為了方便,比如說後面我們要相容MySQL的協議,還有就是從我們自己的業務特點來看,目前這種跟MySQL一樣的這種結構可能對我們來說已經能夠滿足需求。相對來講,我覺得這種方案可能還有一定的靈活性,也有一定的約束,比較折中一點。然後key是可以由多列去組成的。

  下圖是持久化儲存的結構圖,其實這種持久化儲存,包括物件儲存等等,大家的結構可能都差不多,有一個接入層,有一個master去管理後設資料,有儲存資料的地方。

  這裡可能需要介紹一下,我們的data servers是基於rocksdb去實現的。其實大家上網去查這種持久化儲存,可能首屈一指的就是rocksdb,它各方面效能,寫、讀都非常優秀,所以我們也選擇了這麼一種方案。但後面我們在一個場景上測試的時候就碰到了一個問題,主要是rocksdb compact帶來的影響。這裡我簡單介紹一下compact產生的原因,對於rocksdb來講,它就是一個基於日誌結構的合併樹。假設我的左手邊是一棵無序寫入的、按寫入順序進行儲存的樹,而我的右手邊是一顆有序的樹,然後不停地把無序寫入的資料往右手這邊的有序樹上去合併。一個是為了保證讀的效能,另外一塊就是說它有一個特點,比如我的刪除,其實在無序的樹裡面去寫入一個Key,做一個標記它是刪除的,並沒有真正在我的右手邊有序的樹裡面去刪除,只是打了個標,然後透過後臺的GC把這些資料給清理掉。基於這兩點,它需要不停地把無序的樹和有序的樹進行一個檔案的合併,合併的動作就是從無序的樹裡面挑一個檔案,看Key的分佈,在有序的樹裡面也挑選相同或者一定跨度範圍內的Key的一個檔案進行合併。

  另一塊就是說因為每一層資料不能夠保證Key寫得太大,它會一層一層往下寫,下面的雖然說每一層不是有序的,為了查詢的效率,包括GC掉一些刪除的Key等等,它會往下進行合併,每一層也會合並。這裡面其實大家就看到一個問題,比如說我有一個Key,從寫入以後就再也沒有改變過,也沒有刪除,在這個過程當中,可能我就會被來回搬運很多次,被寫入很多次,這就導致了一個寫的放大。大概就是這麼一個流程。

  我們會發現在一段時間內,當我一個rocksdb的程式、資料量在100多近200G左右的時候,它其實效能還維持的比較好(這裡提一下,我們使用的是基於NVMe的SSD)。當它超過一定的量以後,你會發現寫入的效能就有一些“尖刺”了,有的可能就直接掉到0。作為一個線上服務來講,這種問題是我們無法接受的。舉個例子,大家作為消費者,可能都不希望在大促銷、“秒殺”的那一刻,服務如果出現延遲,那麼可能促銷的時間點已經過去了。當然如果是一個離線服務,我覺得是可以接受這種短暫波動的。

  下面是系統長時間跑的一個圖,大家可以看到,越到後面波動就越大。

  針對這個問題,我們目前採用了一個Key-value分離的方案。我們是基於rocksdb裡面blobdb功能的完善和改造。這裡簡單介紹一下Key-value分離是怎麼實現的,就是說我有一個Key寫進來以後,還是和原來一樣儲存到rocksdb這一套結構裡面,同時我把我的value寫在一個順序追加的檔案當中,然後把這個順序追加的檔案的位置,比如檔案編號、基於這個檔案的偏移量等等,把它寫在一個索引裡面,把索引資訊和Key儲存在一起,索引資訊作為一個原來Key對應的value儲存起來,相當於中間加了一層。這樣做的一個好處就是說我在做一個有序整理的時候,就是排序合併的時候,不用先去搬弄我的value。

  其實這裡面也隱藏了一個問題,就是說它並沒有解決所有的場景,可能只對Key-value比例比較大的場景比較合適。比如說key可能就幾十個位元組,而value可能上千或更大,這種場景其實是非常合適的。如果value本身就比較小,可能幾十個位元組,和key差不多,還多出個索引來,其實這種場景也是解決不了的。

  然後,如果說你的比例能達到一比幾十的話,你一塊盤幾個T的容量,記憶體裡面保留幾百G的key的資料,也許就能夠很好的去解決這個問題。包括你這對一臺物理機進行多個例項的部署,也可以緩解這些問題。當然業界也有一些別的探討,比如說結合SSD磁碟的一些特性,SSD磁碟本身也會在後臺做一個GC,也許就可以和rocksdb的GC合併起來。

  下面就是我們改動以後業務測試的一個效能表現,相對來講就比原來平滑很多。

  講完儲存那一塊碰到的問題以後,下面就說一下raft成員變更。因為我們的資料,包括雲資料、業務資料和後設資料都是基於raft複製的,它可以線上擴容,也可以進行一些故障恢復,勢必就會涉及raft成員的變更。當你在做資料遷移負載均衡的時候,成員就需要變更。比如說一個磁碟快滿了,我需要把一個副本從A機器搬到B機器去,我們的做法就是在B機器上面加一個節點,加上以後再把A機器上的副本刪掉。

  這裡面就碰到一個問題,如果這個時候剛好A機器有故障,你就會發現raft就沒辦法正常工作了,因為它現在的成員是四個,新加入的成員和有故障A映象的副本同時不能工作,這個時候其實你是有兩個節點是壞的,兩個節點是好的,它沒有辦法保證大多數的成功。

  大家可能就問,那我能不能先把A機器上的節點刪了,讓我的成員從三個變成兩個,然後再把一個別的節點加進來,是不是就能解決這個問題?其實在一定程度上是能緩解這個問題,但這裡面可能也會碰到新的問題,就是說當你剛好把A映象的副本給刪掉以後,三個節點刪了一個還有兩個,如果再有個節點出現故障的話,你整個資料可能都沒有辦法自己去恢復了,你就需要強行去幹預它,這可能就涉及資料的一個安全。

  那麼我們現有的方案是怎麼解決這個問題的?其實我們大概思路就是,首先讓新增的raft成員只複製資料,不參與到投票裡面,同時也不會發起Leader選舉,讓它作為一個“學習節點”。當這一個新增的節點複製完資料以後,我的Leader會知道它複製到哪了,就認為它已經跟上進度了,再把它提交到成員裡面去,同時把另一個節點刪掉,這樣就能夠保證這個工作比較順利地進行。如果這個時候即使有一個新的節點有故障的話,也能保證有兩個節點在。

  這裡提到其實它有個特點,就是新增一個成員的時候,是會重新發起Leader選舉的。還有一個就是,如果我這個節點(follower)資料落後了很多,斷開網路以後重新加進來,也會發起一次Leader的選舉。那Leader的這種選舉、切換,其實是需要時間的,對效能會有干擾。

  當然raft的作者也有提到怎麼解決這些問題。就是新增的節點加進raft組以後,先詢問一下,跟現實中的選舉一樣,你在正式選舉之前,可能需要去各個社群拜個票,後面真的選舉了,讓人家選你。它也一樣,就是說加進來以後,先不發起Leader選舉,而是先去拜個票,跟每一個成員溝通一下,能不能給我一個機會當Leader。如果大多數的人反饋你可以,那就進行選舉。如果說現有的Leader挺好的,我不能讓你當,或者是說你的資料落後於我,你不會成為Leader,那就沒必要再發起leader選舉了。作者提出了Pre-Candidate演算法,在發起之前就先進行一次預選舉。如果預選舉時能得到大多數的投票,再增加term,進行正常的選舉。這樣就是大大的降低了因為一些網路,包括資料的遷移平衡等因素導致的效能波動。

  前面有提到,我們在持久化化儲存的基礎之上實現了分散式鎖和配置服務。企業當中或多或少都有這種需求,最初如果說Redis/Memcached沒有升級之前,可能更多基於資料庫去做;或者說我的場景比較簡單,就基於資料庫去做。我在做一個任務的時候,先把這個任務打個標,在資料庫裡面標識這個任務是分配狀態或執行狀態。你做完任務後,你可能就去把它解鎖,把任務標識為執行完成,打上標的資料不會被別的服務搶到。

  這裡其實可能就帶來一個問題,假設打上標以後你的服務就掛了,誰來解?可能你會引入比如超時時間,一個定時的服務,去檢測這些長時間在執行中的任務,將其解鎖,讓它重新可以交給別人去執行。另一種方案就是基於Redis/Memcached這種服務,如果你只部署了一個節點,我們知道進入Redis/Memcached都是非同步的複製,如果你的鎖服務加上以後剛好碰到了你的master“死”掉了,這把鎖是不是意味著就丟失了?redis作者提出了redlock演算法,透過去多個redis裡面同時寫個鎖,類似於這種分散式的協商一樣。這裡面其實有一個問題是什麼?如果說寫入以後,你的服務“死”了,可能你就需要根據你的業務給它設一個超時時間。對大部分的業務來講,其實我覺得是能滿足的。當然一些比較嚴格的服務裡面,可能會面臨一個問題,就是我們這個時間設多長合適?如果你設的時間不是很合適,可能到這個時間以後,服務還沒有執行完就過期了。

分散式鎖流程

  另外一種就是基於Etcd/zk的方案,它有心跳機制。這裡面有一個場景,就是網路中斷以後,臨時節點不存在了,但是服務本身還在跑,那麼它要不要中斷?因為你沒有終止的話,還是有可能發生重複執行的現象。這裡我們提供了一些業務可選的方案,根據場景去配置。比如,我們可以是合理的超時時間+心跳機制,讓這個時間可以續期,如果服務沒有“死”,還在執行,就可以加時間;還有一個就是分佈安全超時時間+報警檢測,設比較長的時間,我發現一段時間以後沒有執行完,就給那個業務去報警;還有一種就是心跳過期+回撥,是保證你的任務是能夠取消掉的。

  然後是配置服務,基於剛才講的,我們KV儲存是多range的,一臺伺服器、一個程式有多個range服務,意味著它是多點的寫入。在range分裂的時候,樹型結構的深度是有限制的,防止分離時group分組被割裂。

   混合儲存的實踐與挑戰

  下面給講一下混合儲存。我們希望能做到冷、溫、熱的三層儲存,冷資料儲存在持久化急群眾。其中冷和溫資料在同一個程式裡面去解決,這可能就涉及到一個Key的淘汰問題,將不常訪問的資料淘汰到磁碟。淘汰有很多種演算法,比如LRU、HIT DENSITY等等,這時又面臨一個問題,Key要不要從記憶體裡刪除?其實記憶體本身就是需要低延遲的場景,這裡面又關聯到第二個問題,如果Key刪掉以後,在訪問Key的時候需要去磁碟訪問,這樣會不會阻塞掉記憶體訪問的其他客戶端的請求,拖慢整體效能?還有一個問題就是,記憶體裡面可能有一些大的資料結構,要不要把它淘汰到磁碟上去?淘汰以後可能就意味著一些功能的閹割,作為一個平臺,可能有些功能沒有辦法閹割的,需要進行一個取捨。

  我們目前的做法是,作為一個冷資料,是採用非同步去訪問的(如下圖)。比如說我有A、B、C三個客戶端,A客戶端訪問到的是一個冷資料,key儲存在記憶體裡打個標,然後它需要訪問磁碟,我們暫時把這個請求再丟給一個佇列,把這一個CPU執行緒給釋放出來,去處理別的客戶端的請求。如果在我們以前其實沒有按這種方式,就會導致就是說我訪問磁碟,比如說要50毫秒,可能整個後面的任務都被阻塞50毫秒。

  下圖這是一個冷熱分離的訪問流程,沒有太複雜的事情。

  還有一個就是混合儲存,如下,這一塊我們還在實踐當中。

  今天我大概就講了這三部分的內容,記憶體儲存、持久化儲存和混合儲存,希望能夠給大家帶來一些共鳴,謝謝。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31545805/viewspace-2222021/,如需轉載,請註明出處,否則將追究法律責任。

相關文章