滴滴Ceph分佈式存儲系統優化之鎖優化

1. 背景

在支撐一些延遲敏感的在線應用過程中,我們發現Ceph的尾延遲較差,當應用併發負載較高時,Ceph很容易出現延遲的毛刺,對延遲敏感的應用造成超時甚至崩潰。我們對Ceph的尾延遲問題進行了深入細緻的分析和優化。造成尾延遲的一個重要原因就是代碼中鎖的使用問題,下面根據鎖問題的類型分別介紹我們的優化工作。本文假設讀者已熟悉Ceph的基本讀寫代碼流程,代碼的版本爲Luminous。

2. 持鎖時間過長

2.1 異步讀優化

Ceph的osd處理客戶端請求的線程池爲osd_op_tp,在處理操作請求的時候,線程會先鎖住操作對應pg的lock。其中,處理對象讀請求的代碼如下圖所示,在鎖住對象所屬pg的lock後,對於最常用的多副本存儲方式,線程會同步進行讀操作,直到給客戶端發送返回的數據後,纔會釋放pg lock。

在進行讀操作時,如果數據沒有命中page cache而需要從磁盤讀,是一個耗時的操作,並且pg lock是一個相對粗粒度的鎖,在pg lock持有期間,其它同屬一個pg的對象的讀寫操作都會在加鎖上等待,增大了讀寫延遲,降低了吞吐率。同步讀的另一個缺點是讀操作沒有參與流量控制。

我們對線上集羣日誌的分析也驗證了上述問題,例如,一個日誌片段如下圖所示,圖中列舉了兩個op的詳細耗時信息,這兩個op均爲同一個osd的線程所執行,且操作的是同一個pg的對象。根據時間順序,第一個op爲read,總耗時爲56ms。第二個op爲write,總耗時爲69ms。圖中信息顯示,第二個op處理的一箇中間過程,即副本寫的完成消息在處理之前,在osd請求隊列中等待了36ms。結合上圖的代碼可以知道,這36ms都是耗在等待pg lock上,因爲前一個read操作持有pg lock,而兩個對象屬於相同pg。

我們的優化如下圖所示,我們創建了獨立的讀線程,負責處理讀請求,osd_op_tp線程只需將讀請求提交到讀線程的隊列即可返回解鎖,大大減少了pg lock的持有時間。讀線程完成磁盤讀之後,將結果放到finisher線程的隊列,finisher線程重新申請pg lock後負責後續處理,這樣將耗時的磁盤訪問放在了不持有pg lock的流程中,結合我們在流量控制所做的優化,讀寫操作可以在統一的框架下進行流量控制,從而精準控制磁盤的利用率,以免磁盤訪問擁塞造成尾延遲。

我們用fio進行了異步讀優化效果的測試,測試方法:對同一個pool的兩個rbd,一個做隨機讀,另一個同時做隨機寫操作,將pg number配置爲1,這樣所有對象讀寫會落到同一個osd的同一個pg。異步讀優化後,隨機寫平均延遲下降了 53% 。下圖爲某業務的filestore集羣異步讀上線前後讀吞吐率的數據,箭頭所指爲上線時間,可見上線之後,集羣承載的讀操作的吞吐率增加了 120%

上述優化在使用filestore存儲後端時取得了明顯的效果,但在使用bluestore存儲後端時,bluestore代碼中還存在持有pg粒度鎖同步讀的問題,具體見BlueStore::read的代碼。我們對bluestore的讀也進行了異步的優化,這裏就不詳細介紹了。

3. 鎖粒度過粗

3.1 object cache lock優化

Ceph在客戶端實現了一個基於內存的object cache,供rbd和cephfs使用。但cache只有一把大的互斥鎖,任何cache中對象的讀寫都需要先獲得這把鎖。在使用寫回模式時,cache flusher線程在寫回髒數據之前,也會鎖住這個鎖。這時對cache中緩存對象的讀寫都會因爲獲取鎖而卡住,使讀寫延遲增加,限制了吞吐率。

我們實現了細粒度的對象粒度的鎖,在進行對象的讀寫操作時,只需獲取對應的對象鎖,無需獲取全局鎖。只有訪問全局數據結構時,才需要獲取全局鎖,大大增加了對象間操作的並行。並且對象鎖採用讀寫鎖,增加了同一對象上讀的並行。測試表明,高併發下rbd的吞吐率增加了超過 20%

4. 不必要的鎖競爭

4.1減少pg lock競爭

Ceph的osd對客戶端請求的處理流程爲,messenger線程收到請求後,將請求放入osd_op_tp線程池的緩存隊列。osd_op_tp線程池的線程從請求緩存隊列中出隊一個請求,然後根據該請求操作的對象對應的pg將請求放入一個與pg一一對應的pg slot隊列的尾部。然後獲取該pg的pg lock,從pg slot隊列首部出隊一個元素處理。

可見,如果osd_op_tp線程池的請求緩存隊列中連續兩個請求操作的對象屬於相同的pg,則一個osd_op_tp線程出隊前一個請求加入pg slot隊列後,獲取pg lock,從pg slot隊列首部出隊一個請求開始處理。另一個osd_op_tp線程從請求緩存隊列出隊第二個請求,因爲兩個請求是對應相同的pg,則它會加入相同的pg slot隊列,然後,第二個線程在獲取pg lock時會阻塞。這降低了osd_op_tp線程池的吞吐率,增加了請求的延遲。

我們的優化方式是保證任意時刻每個pg slot隊列只有一個線程處理。因爲在處理pg slot隊列中的請求之前需要獲取pg lock,因此同一個pg slot隊列的請求是無法並行處理的。我們在每個pg slot隊列增加一個標記,記錄當前正在處理該pg slot的請求的線程。當有線程正在處理一個pg slot的請求時,別的線程會跳過處理該pg slot,繼續從osd_op_tp線程池的請求緩存隊列出隊請求。

4.2 log lock優化

Ceph的日誌系統實現是有一個全局的日誌緩存隊列,由一個全局鎖保護,由專門的日誌線程從日誌緩存隊列中取日誌打印。工作線程提交日誌時,需要獲取全局鎖。日誌線程在獲取日誌打印之前,也需要獲取全局鎖,然後做一個交換將隊列中的日誌交換到一個臨時隊列。另外,當日志緩存隊列長度超過閾值時,提交日誌的工作線程需要睡眠等待日誌線程打印一些日誌後,再提交。鎖的爭搶和等待都增加了工作線程的延遲。

我們爲每個日誌提交線程引入一個線程局部日誌緩存隊列,該隊列爲經典的單生產者單消費者無鎖隊列。線程提交日誌直接提交到自己的局部日誌緩存隊列,該過程是無鎖的。只有隊列中的日誌數超過閾值後,纔會通知日誌線程。日誌線程也會定期輪詢各個日誌提交線程的局部日誌緩存隊列,打印一些日誌,該過程也是無鎖的。通過上述優化,基本避免了日誌提交過程中因爲鎖競爭造成的等待,降低了日誌的提交延遲。測試在高併發日誌提交時,日誌的提交延遲可降低接近 90%

4.3 filestore apply lock優化

對於Ceph filestore存儲引擎,同一個pg的op需要串行apply。每個pg有一個OpSequencer(簡稱osr),用於控制apply順序,每個osr有一個apply lock以及一個op隊列。對於每個待apply的op,首先加入對應pg的osr的隊列,然後把osr加到filestore的負責apply的線程池op_tp的隊列,簡稱爲apply隊列。op_tp線程從apply隊列中取出一個osr,加上它的apply lock,再從osr的隊列裏取出一個op apply,邏輯代碼如下圖左所示。可見,每個op都會把其對應的osr加入到apply隊列一次。如果多個op是針對同一個pg的對象,則這個pg的osr可能多次加入到apply隊列。如果apply隊列中連續兩個osr是同一個pg的,也就是同一個osr,則前一個op被一個線程進行apply時,osr的apply lock已經加鎖,另一個線程會在該osr的apply lock上阻塞等待,降低了併發度。

這個問題也體現在日誌中。一個線上集羣日誌片段如下圖,有兩個op_tp線程6700和5700,apply隊列裏三個對象依次來自pg: 1.1833, 1.1833. 1.5f2。線程6700先拿到第一個對象進行apply, 線程5700拿第二個對象進行apply時卡在apply lock上,因爲兩個對象都來自pg 1.1833,直到6700做完纔開始apply。而6700拿到第三個對象,即1.5f2的對象進行apply即寫page cache只用了不到1ms,但實際apply延遲234ms,可見第三個對象在隊列裏等待了233ms。如果5700不用等待apply lock,則第二和第三個對象的apply延遲可以大大縮短。

我們優化後的邏輯代碼如上圖右所示,同一個osr只加入apply隊列一次,取消apply lock,利用原子操作實現無鎖算法。上面的算法可以進一步優化,在將一個osr出隊之後,可以一次從它的隊列中取m(m>1)個op進行apply,在op apply完成階段,改爲如果atomic::fetch_sub(osr->queue_length, m) > m,則將osr重新入隊以提高吞吐率。

我們用fio進行了apply lock優化效果測試,方法爲建兩個pool,每個pool的pg number爲1,每個pool一個rbd, 對兩個rbd同時進行隨機寫的操作,一個pool寫入數據的量爲31k*10k,另一個pool寫入數據的量爲4k*100k, 衡量所有請求apply的總耗時。優化前總耗時434ks, 優化後總耗時45ks,減少 89.6%

作者介紹

汪黎,滴滴首席工程師

負責滴滴在線非結構化存儲研發,曾任國防科技大學計算機學院副研究員,教研室主任,天河雲存儲負責人

本文轉載自公衆號滴滴技術(ID:didi_tech)。

原文鏈接

滴滴Ceph分佈式存儲系統優化之鎖優化

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