隨着騰訊自研上雲及公有云用戶的迅速增加,一方面,騰訊雲容器服務TKE服務數量和核數大幅增加, 另外一方面咱們提供的容器服務類型(TKE託管及獨立集羣、EKS彈性集羣、edge邊緣計算集羣、mesh服務網格、serverless knative)也愈來愈豐富。各種容器服務類型背後的核心都是K8s,K8s核心的存儲etcd又統一由咱們基於K8s構建的etcd平臺進行管理。基於它咱們目前管理了千級etcd集羣,背後支撐了萬級K8s集羣。git
在萬級K8s集羣規模下的咱們如何高效保障etcd集羣的穩定性? github
etcd集羣的穩定性風險又來自哪裏?golang
咱們經過基於業務場景、歷史遺留問題、現網運營經驗等進行穩定性風險模型分析,風險主要來自舊TKE etcd架構設計不合理、etcd穩定性、etcd性能部分場景沒法知足業務、測試用例覆蓋不足、變動管理不嚴謹、監控是否全面覆蓋、隱患點是否能自動巡檢發現、極端災難故障數據安全是否能保障。web
前面所描述的etcd平臺已經從架構設計上、變動管理上、監控及巡檢、數據遷移、備份幾個方面程度解決了咱們管理的各種容器服務的etcd可擴展性、可運維性、可觀測性以及數據安全性,所以本文將重點描述咱們在萬級K8s場景下面臨的etcd內核穩定性及性能挑戰,好比:算法
本文將簡易描述咱們是如何發現、分析、復現、解決以上問題及挑戰,以及從以上過程當中咱們得到了哪些經驗及教訓,並將之應用到咱們的各種容器服務存儲穩定性保障中。數據庫
同時,咱們將解決方案所有貢獻、回饋給etcd開源社區, 截止目前咱們貢獻的30+ pr已所有合併到社區。騰訊雲TKE etcd團隊是etcd社區2020年上半年最活躍的貢獻團隊之一, 爲etcd的發展貢獻咱們的一點力量, 在這過程當中特別感謝社區AWS、Google、Ali等maintainer的支持與幫助。後端
從GitLab誤刪主庫丟失部分數據到GitHub數據不一致致使中斷24小時,再到號稱"不沉航母"的AWS S3故障數小時等,無一例外都是存儲服務。穩定性對於一個存儲服務、乃至一個公司的口碑而言相當重要,它決定着一個產品生與死。穩定性優化案例咱們將從數據不一致的嚴重性、兩個etcd數據不一致的bug、lease內存泄露、mvcc 死鎖、wal crash方面闡述,咱們是如何發現、分析、復現、解決以上case,並分享咱們從每一個case中的得到的收穫和反思,從中汲取經驗,防患於未然。api
談到數據不一致致使的大故障,就不得不詳細提下GitHub在18年一次因網絡設備的例行維護工做致使的美國東海岸網絡中心與東海岸主要數據中心之間的鏈接斷開。雖然網絡的連通性在43秒內得以恢復,可是短暫的中斷引起了一系列事件,最終致使GitHub 24小時11分鐘的服務降級,部分功能不可用。數組
GitHub使用了大量的MySQL集羣存儲GitHub的meta data,如issue、pr、page等等,同時作了東西海岸跨城級別的容災。故障核心緣由是網絡異常時GitHub的MySQL仲裁服務Orchestrator進行了故障轉移,將寫入數據定向到美國西海岸的MySQL集羣(故障前primary在東海岸),然而美國東海岸的MySQL包含一小段寫入,還沒有複製到美國西海岸集羣,同時故障轉移後因爲兩個數據中心的集羣如今都包含另外一個數據中心中不存在的寫入,所以又沒法安全地將主數據庫故障轉移回美國東海岸。安全
最終, 爲了保證保證用戶數據不丟失,GitHub不得不以24小時的服務降級爲代價來修復數據一致性。
數據不一致的故障嚴重性不言而喻,然而etcd是基於raft協議實現的分佈式高可靠存儲系統,咱們也並未作跨城容災,按理數據不一致這種看起來高大上bug咱們是很難遇到的。然而夢想是美好的,現實是殘酷的,咱們不只遇到了難以想象的數據不一致bug, 還一踩就是兩個,一個是重啓etcd有較低的機率觸發,一個是升級etcd版本時若是開啓了鑑權,在K8s場景下較大機率觸發。在詳細討論這兩個bug前,咱們先看看在K8s場景下etcd數據不一致會致使哪些問題呢?
首先第一個不一致bug是重啓etcd過程當中遇到的,人工嘗試復現屢次皆失敗,分析、定位、復現、解決這個bug之路幾經波折,過程頗有趣並充滿挑戰,最終經過我對關鍵點增長debug日誌,編寫chaos monkey模擬各類異常場景、邊界條件,實現復現成功。最後的真兇居然是一個受權接口在重啓後重放致使鑑權版本號不一致,而後放大致使多版本數據庫不一致, 部分節點沒法寫入新數據, 影響全部v3版本的3年之久bug。
隨後咱們提交若干個相關pr到社區, 並所有合併了, 最新的etcd v3.4.9[1],v3.3.22[2]已修復此問題, 同時google的jingyih也已經提K8s issue和pr[3]將K8s 1.19的etcd client及server版本升級到最新的v3.4.9。此bug詳細可參考超凡同窗寫的文章三年之久的 etcd3 數據不一致 bug 分析。
第二個不一致bug是在升級etcd過程當中遇到的,因etcd缺乏關鍵的錯誤日誌,故障現場有效信息很少,定位較困難,只能經過分析代碼和復現解決。然而人工嘗試復現屢次皆失敗,因而咱們經過chaos monkey模擬client行爲場景,將測試環境全部K8s集羣的etcd分配請求調度到咱們復現集羣,以及對比3.2與3.3版本差別,在可疑點如lease和txn模塊增長大量的關鍵日誌,並對etcd apply request失敗場景打印錯誤日誌。
經過以上措施,咱們比較快就復現成功了, 最終經過代碼和日誌發現是3.2版本與3.3版本在revoke lease權限上出現了差別,3.2無權限,3.3須要寫權限。當lease過時的時候,若是leader是3.2,那麼請求在3.3節點就會因無權限致使失敗,進而致使key數量不一致,mvcc版本號不一致,致使txn事務部分場景執行失敗等。最新的3.2分支也已合併咱們提交的修復方案,同時咱們增長了etcd核心過程失敗的錯誤日誌以提升數據不一致問題定位效率,完善了升級文檔,詳細說明了lease會在此場景下引發數據不一致性,避免你們再次採坑。
從這兩個數據不一致bug中咱們得到了如下收穫和最佳實踐:
衆所周知etcd是golang寫的,而golang自帶垃圾回收機制也會內存泄露嗎?首先咱們得搞清楚golang垃圾回收的原理,它是經過後臺運行一個守護線程,監控各個對象的狀態,識別而且丟棄再也不使用的對象來釋放和重用資源,若你遲遲未釋放對象,golang垃圾回收不是萬能的,不泄露才怪。好比如下場景會致使內存泄露:
接下來看看咱們遇到的這個etcd內存泄露屬於哪一種狀況呢?事情起源於3月末的一個週末起牀後收到現網3.4集羣大量內存超過安全閾值告警,馬上排查了下發現如下現象:
此內存泄露bug屬於內存數據結構管理不周致使的,問題修復後,etcd社區當即發佈了新的版本(v3.4.6+)以及K8s都當即進行了etcd版本更新。
從這個內存泄露bug中咱們得到了如下收穫和最佳實踐:
死鎖是指兩個或兩個以上的goroutine的執行過程當中,因爲競爭資源相互等待(通常是鎖)或因爲彼此通訊(chan引發)而形成的一種程序卡死現象,沒法對外提供服務。deadlock問題由於每每是在併發狀態下資源競爭致使的, 通常比較難定位和復現, 死鎖的性質決定着咱們必須保留好分析現場,不然分析、復現及其困難。
那麼咱們是如何發現解決這個deadlock bug呢?問題起源於內部團隊在壓測etcd集羣時,發現一個節點忽然故障了,並且一直沒法恢復,沒法正常獲取key數等信息。收到反饋後,我經過分析卡住的etcd進程和查看監控,獲得如下結論:
這個bug也隱藏了好久,影響全部etcd3版本,在集羣中寫入量較大,某落後的較多的節點執行了快照重建,同時此時又偏偏在作歷史版本壓縮,那就會觸發。我提交的修復PR目前也已經合併到3.3和3.4分支中,新的版本已經發布(v3.3.21+/v3.4.8+)。
從這個死鎖bug中咱們得到了如下收穫和最佳實踐:
panic是指出現嚴重運行時和業務邏輯錯誤,致使整個進程退出。panic對於咱們而言並不陌生,咱們在現網遇到過幾回,最先遭遇的不穩定性因素就是集羣運行過程當中panic了。
雖然說咱們3節點的etcd集羣是能夠容忍一個節點故障,可是crash瞬間對用戶依然有影響,甚至出現集羣撥測鏈接失敗。
咱們遇到的第一個crash bug,是發現集羣連接數較多的時候有必定的機率出現crash, 而後根據堆棧查看社區已有人報grpc crash(issue)[4], 緣由是etcd依賴的組件grpc-go出現了grpc crash(pr)[5],而最近咱們遇到的crash bug[6]是v3.4.8/v3.3.21新版本發佈引發的,這個版本跟咱們有很大關係,咱們貢獻了3個PR到這個版本,佔了一大半以上, 那麼這個crash bug是如何產生以及復現呢?會不會是咱們本身的鍋呢?
雖然這個bug是社區用戶反饋的,但從這個crash bug中咱們得到了如下收穫和最佳實踐:
etcd面對一些大數據量的查詢(expensive read)和寫入操做時(expensive write),如全key遍歷(full keyspace fetch)、大量event查詢, list all Pod, configmap寫入等會消耗大量的cpu、內存、帶寬資源,極其容易致使過載,乃至雪崩。
然而,etcd目前只有一個極其簡單的限速保護,當etcd的commited index大於applied index的閾值大於5000時,會拒絕一切請求,返回Too Many Request,其缺陷很明顯,沒法精確的對expensive read/write進行限速,沒法有效防止集羣過載不可用。
爲了解決以上挑戰,避免集羣過載目前咱們經過如下方案來保障集羣穩定性:
多維度的集羣告警在咱們的etcd穩定性保障中發揮了重要做用,屢次幫助咱們發現用戶和咱們自身集羣組件問題。用戶問題如內部某K8s平臺以前出現bug, 寫入大量的集羣CRD資源和client讀寫CRD QPS明顯偏高。咱們自身組件問題如某舊日誌組件,當集羣規模增大後,因日誌組件不合理的頻繁調用list Pod,致使etcd集羣流量高達3Gbps, 同時apiserver自己也出現5XX錯誤。
雖然經過以上措施,咱們能極大的減小因expensive read致使的穩定性問題,然而從線上實踐效果看,目前咱們仍然比較依賴集羣告警幫助咱們定位一些異常client調用行爲,沒法自動化的對異常client的進行精準智能限速,。etcd層因沒法區分是哪一個client調用,若是在etcd側限速會誤殺正常client的請求, 所以依賴apiserver精細化的限速功能實現。社區目前已在1.18中引入了一個API Priority and Fairness[7],目前是alpha版本,期待此特性早日穩定。
etcd讀寫性能決定着咱們能支撐多大規模的集羣、多少client併發調用,啓動耗時決定着咱們當重啓一個節點或因落後leader太多,收到leader的快照重建時,它從新提供服務須要多久?性能優化案例剖析咱們將從啓動耗時減小一半、密碼鑑權性能提高12倍、查詢key數量性能提高3倍等來簡單介紹下如何對etcd進行性能優化。
當db size達到4g時,key數量百萬級別時,發現重啓一個集羣耗時居然高達5分鐘, key數量查詢也是超時,調整超時時間後,發現高達21秒,內存暴漲6G。同時查詢只返回有限的記錄數的場景(如業務使用etcd grpc-proxy來減小watch數,etcd grpc proxy在默認建立watch的時候,會發起對watch路徑的一次limit讀查詢),依然耗時很高且有巨大的內存開銷。因而週末空閒的時候我對這幾個問題進行了深刻調查分析,啓動耗時到底花在了哪裏?是否有優化空間?查詢key數量爲什麼如何耗時,內存開銷如此之大?
帶着這些問題對源碼進行了深刻分析和定位,首先來看查詢key數和查詢只返回指定記錄數的耗時和內存開銷極大的問題,分析結論以下:
再看啓動耗時問題太高的問題,經過對啓動耗時各階段增長日誌,獲得如下結論:
某內部業務服務一直跑的好好的,某天client略微增多後,忽然現網etcd集羣出現大量超時,各類折騰,切換雲盤類型、切換部署環境、調整參數都不發揮做用,收到求助後,索要metrics和日誌後,通過一番排查後,獲得如下結論:
本文簡單描述了咱們在管理萬級K8s集羣和其餘業務過程當中遇到的etcd穩定性和性能挑戰,以及咱們是如何定位、分析、復現、解決這些挑戰,並將解決方案貢獻給社區。
同時,詳細描述了咱們從這些挑戰中收穫了哪些寶貴的經驗和教訓,並將之應用到後續的etcd穩定性保障中,以支持更大規模的單集羣和總集羣數。
最後咱們面對萬級K8s集羣數, 千級的etcd集羣數, 10幾個版本分佈,其中很多低版本包含重要的潛在可能觸發的嚴重bug, 咱們還須要投入大量工做不斷優化咱們的etcd平臺,使其更智能、變動更加高效、安全、可控(如支持自動化、可控的集羣升級等), 同時數據安全也相當重要,目前騰訊雲TKE託管集羣咱們已經全面備份,獨立集羣的用戶後續將引導經過應用市場的etcd備份插件開啓定時備份到騰訊雲對象存儲COS上。
將來咱們將繼續緊密融入etcd的社區,爲etcd社區的發展貢獻咱們的力量,與社區一塊提高etcd的各個功能。
[1]v3.4.9: https://github.com/etcd-io/et...
[2]v3.3.22: https://github.com/etcd-io/et...
[3]K8s issue和pr: https://github.com/kubernetes...
[4]grpc crash(issue): https://github.com/etcd-io/et...
[5]grpc crash(pr): https://github.com/grpc/grpc-...
[6]crash bug : https://github.com/etcd-io/et...
[7]API Priority and Fairness: https://github.com/kubernetes...
【騰訊雲原生】雲說新品、雲研新術、雲遊新活、雲賞資訊,掃碼關注同名公衆號,及時獲取更多幹貨!!