容器化RDS—計算儲存分離架構下的“Split-Brain”
沃趣科技 熊中哲
不管是架構選型還是生活,絕大多數時候都是在做 trade off,收穫了計算儲存分離帶來的好處,也意味著要忍受它帶來的一些棘手問題。本文嘗試結合 Kubernetes, Docker, MySQL和計算儲存分離架構, 分享我們遇到的“Split-Brain”問題。
2018年1月19號參加了阿里巴巴雙十一資料庫技術峰會,見到了好多老同事(各位研究員,資深專家),同時也瞭解到業界最新的資料庫技術發展趨勢:
● 資料庫容器化作為下一代資料庫基礎架構
● 基於編排架構管理容器化資料庫
● 採用計算儲存分離架構
這和我們在私有 RDS 上的技術選型不謀而合. 尤其是計算儲存分離架構.
在我們看來,其最大優勢在於:
● 計算資源 / 儲存資源獨立擴充套件,架構更清晰,部署更容易。
● 將有狀態的資料下沉到儲存層,Scheduler 排程時,無需感知計算節點的儲存介質,只需排程到滿足計算資源要求的 Node,資料庫例項啟動時,只需在分散式檔案系統掛載mapping volume 即可,可以顯著的提高資料庫例項的部署密度和計算資源利用率。
以阿里巴巴為例,考慮到今時今日它的規模,如果能夠實現資料庫服務的離線(ODPS)/線上叢集的混合部署,意義極其重大。關鍵問題在於,離線(ODPS)計算和線上計算對實時性要求不同,硬體配置也不同,尤其是本地儲存介質: ● 離線(ODPS)以機械磁碟為主 ● 線上以 SSD / Flash 為主 如果採用本地儲存作為資料庫例項的儲存介質, 試想一下, 一個 Storage Qos 要求是 Flash 的資料庫例項無法排程到離線計算叢集, 哪怕離線計算叢集 CPU, Memory 有大量空閒. 計算儲存分離為實現離線(ODPS)/線上叢集的混合部署提供了可能。 |
結合 Kubernetes,Docker 和 MySQL,進一步細化架構圖,如下圖所示:
同時,這套架構也帶給我們更加簡單、通用、高效的 High Availability 方案。當叢集中某個 Node 不可用後,藉助 Kubernetes 的原生元件Node Controller,Scheduler和原生API Statefulset即可將資料庫例項排程到其他可用節點,以實現資料庫例項的高可用。
一切是多麼的美好,不是可以得到這個結論:
藉助 Kubernetes 的原生元件Node Controller,cheduler和原生API Statefulset,上計算儲存分離架構,將成熟的分散式檔案系統整合到 Kubernetes 儲存系統, 就能提供私有RDS服務。
之前我們也是這麼想的, 直到遇到”Split-Brain”問題(也即是本文的主題)
回到上面的 High Availability 方案。
當叢集中某個 Node 不可用後, 藉助 Kubernetes 的原生元件Node Controller, Scheduler和原生API Statefulset即可將資料庫例項排程到其他可用節點, 以實現資料庫例項的高可用. |
判定 Node 不可用 將是後續觸發 Failover 動作的關鍵。
所以這裡需要對節點狀態的判定機制稍作展開:
● Kubelet 藉助 API Server 定期(node-status-update-frequency)更新 etcd 中對應節點的心跳資訊。
● Controller Manager 中的 Node Controller 元件定期(node-monitor-period) 輪詢 ETCD 中節點的心跳資訊。
● 如果在週期(node-monitor-grace-period)內,心跳更新丟失, 該節點標記為Unknown(ConditionUnknown)。
● 如果在週期(pod-eviction-timeout)內,心跳更新持續丟失, Node Controller 將會觸發叢集層面的驅逐機制。
● Scheduler將Unknown節點上的所有資料庫例項排程到其他健康(Ready)節點。
訪問架構圖如下所示:
補充一句,助 ETCD 叢集的高可用強一致,以保證 Kubernetes 叢集元資訊的一致性。
● ETCD 基於 Raft 演算法實現。 ● Raft 演算法是一種基於訊息傳遞(state machine replicated)且具有高度容錯(fault tolerance)特性的一致性演算法(consensus algorithm)。 ● Raft 是大名鼎鼎的 Paxos 的簡化版本 ● 如果對於 Raft 演算法的實現有興趣,可以看看
所有感興趣一致性演算法的同學,都值得花精力學習。基於 goraft/raft,我實現了Network Partition Failures/Recovery TestCase,收穫不小。 |
看上去合理的機制會給我們帶來兩個問題,
問題一 : 無法判定節點真實狀態
心跳更新是判斷節點是否可用的依據,但是,心跳更新丟失是無法判定節點真實狀態的(Kubernetes 中將節點標記為 ConditionUnknown 也說明了這點)。
Node 可能僅僅是網路問題,CPU 繁忙,”假死”,Kubelet bug 等原因導致心跳更新丟失,但節點上的資料庫例項還在執行中。
問題二 : 缺乏有效的 fence 機制
在這個情況下,藉助 Kubernetes 的原生元件Node Controller,Scheduler和原生API Statefulset實現的 Failover,將資料庫例項從 Unknown 節點驅逐到可用節點,但對原Unknown 節點不做任何操作。
這種”軟碟機逐”,將會導致新舊兩個資料庫例項同時訪問同一份資料檔案。
發生”Split-Brain”,導致 Data Corruption,資料丟失,損失無法彌補。
所以,必須藉助 WOQU RDS Operator 提供的 fence 機制,才能保障資料檔案的安全。
下面是枯燥的故障復現,透過日誌和程式碼分析驅逐的工作機制,總結”Split-Brain”整個過程。
測試過程:
● 使用 Statefulset 建立 MySQL 單例項 gxr-oracle-statefulset (這是一個Oracle DBA取的名字,請原諒他)
● Scheduler 將 MySQL 單例項排程到叢集中的節點 “k8s-node3”
● 透過 sysbench 對該例項製造極高的負載, “k8s-node3” load 飆升, 導致“k8s-node3” 上的 Kubelet 無法跟 API Server 通訊, 並開始報錯
● Node Controller 啟動驅逐
● Statefulset 發起重建
● Scheduler 將 MySQL 例項排程到 “k8s-node1” 上
● 新舊MySQL 例項訪問同一個 Volume
● 資料檔案被寫壞, 新舊MySQL例項都報錯, 並無法啟動
測試引數:
● kube-controller-manager 啟動引數
● kubelet 啟動引數
基於日誌, 整個事件流如下:
● 時間點 December 1st 2017,10:18:05.000 (最後一次更新成功應該是 10:17:42.000):
節點(k8s-node3)啟動資料庫壓力測試, 以模擬該節點”假死”, kubelet 跟 API Server 出現心跳丟失。
kubelet 日誌報錯,無法透過 API Server 更新 k8s-node3 狀態。
Kubelet 細節如下: ● 透過 API Server 更新叢集資訊 if kl.kubeClient != nil { // Start syncing node status immediately, this may set up things the runtime needs to run. go wait.Until(kl.syncNodeStatus, kl.nodeStatusUpdateFrequency, wait.NeverStop) }
● 定期(nodeStatusUpdateFrequency)更新對應節點狀態 nodeStatusUpdateFrequency 預設時間為 10 秒, 測試時設定的是8s obj.NodeStatusUpdateFrequency = metav1.Duration{Duration: 10 * time.Second}
● 更新如下資訊: func (kl *Kubelet) defaultNodeStatusFuncs() []func(*v1.Node) error { // initial set of node status update handlers, can be modified by Option's withoutError := func(f func(*v1.Node)) func(*v1.Node) error { return func(n *v1.Node) error { f(n) return nil } } return []func(*v1.Node) error{ kl.setNodeAddress, withoutError(kl.setNodeStatusInfo), withoutError(kl.setNodeOODCondition), withoutError(kl.setNodeMemoryPressureCondition), withoutError(kl.setNodeDiskPressureCondition), withoutError(kl.setNodeReadyCondition), withoutError(kl.setNodeVolumesInUseStatus), withoutError(kl.recordNodeSchedulableEvent), } } ● 透過 kubectl 可以獲得節點的資訊
|
● 時間點 December 1st 2017, 10:18:14.000:
● NodeController 發現 k8s-node3 的狀態有32s 沒有發生更新.
○ ready / outofdisk / diskpressure / memorypressue condition
● 將該節點狀態更新為 UNKNOWN
● 每隔NodeMonitorPeriod繼續節點狀態是否有更新
● 定期(nodeMonitorPeriod)檢視一次節點狀態 // Incorporate the results of node status pushed from kubelet to master. go wait.Until(func() { if err := nc.monitorNodeStatus(); err != nil { glog.Errorf("Error monitoring node status: %v", err) } }, nc.nodeMonitorPeriod, wait.NeverStop)
● nodeMonitorPeriod 預設 5秒,測試時4s NodeMonitorPeriod: metav1.Duration{Duration: 5 * time.Second},
● 當超過 NodeMonitorGracePeriod 時間後,節點狀態沒有更新將節點狀態設定成 unknown if nc.now().After(savedNodeStatus.probeTimestamp.Add(gracePeriod)) { // NodeReady condition was last set longer ago than gracePeriod, so update it to Unknown // (regardless of its current value) in the master. if currentReadyCondition == nil { glog.V(2).Infof("node %v is never updated by kubelet", node.Name) node.Status.Conditions = append(node.Status.Conditions, v1.NodeCondition{ Type: v1.NodeReady, Status: v1.ConditionUnknown, Reason: "NodeStatusNeverUpdated", Message: fmt.Sprintf("Kubelet never posted node status."), LastHeartbeatTime: node.CreationTimestamp, LastTransitionTime: nc.now(), }) } else { glog.V(4).Infof("node %v hasn't been updated for %+v. Last ready condition is: %+v", node.Name, nc.now().Time.Sub(savedNodeStatus.probeTimestamp.Time), observedReadyCondition) if observedReadyCondition.Status != v1.ConditionUnknown { currentReadyCondition.Status = v1.ConditionUnknown currentReadyCondition.Reason = "NodeStatusUnknown" currentReadyCondition.Message = "Kubelet stopped posting node status." // LastProbeTime is the last time we heard from kubelet. currentReadyCondition.LastHeartbeatTime = observedReadyCondition.LastHeartbeatTime currentReadyCondition.LastTransitionTime = nc.now() } } |
● 時間點 December 1st 2017, 10:19:42.000:
剛好過去 podEvictionTimeout,將該節點新增到驅逐佇列中。
● 在podEvictionTimeout 後,認為該節點上 pods 需要開始驅逐
if observedReadyCondition.Status == v1.ConditionUnknown { if nc.useTaintBasedEvictions { // We want to update the taint straight away if Node is already tainted with the UnreachableTaint if taintutils.TaintExists(node.Spec.Taints, NotReadyTaintTemplate) { taintToAdd := *UnreachableTaintTemplate if !util.SwapNodeControllerTaint(nc.kubeClient, []*v1.Taint{&taintToAdd}, []*v1.Taint{NotReadyTaintTemplate}, node) { glog.Errorf("Failed to instantly swap UnreachableTaint to NotReadyTaint. Will try again in the next cycle.") } } else if nc.markNodeForTainting(node) { glog.V(2).Infof("Node %v is unresponsive as of %v. Adding it to the Taint queue.", node.Name, decisionTimestamp, ) } } else { if decisionTimestamp.After(nc.nodeStatusMap[node.Name].probeTimestamp.Add(nc.podEvictionTimeout)) { if nc.evictPods(node) { glog.V(2).Infof("Node is unresponsive. Adding Pods on Node %s to eviction queues: %v is later than %v + %v", node.Name, decisionTimestamp, nc.nodeStatusMap[node.Name].readyTransitionTimestamp, nc.podEvictionTimeout-gracePeriod, ) } } } }
● 放到驅逐陣列中
// evictPods queues an eviction for the provided node name, and returns false if the node is already // queued for eviction. func (nc *Controller) evictPods(node *v1.Node) bool { nc.evictorLock.Lock() defer nc.evictorLock.Unlock() return nc.zonePodEvictor[utilnode.GetZoneKey(node)].Add(node.Name, string(node.UID)) }
|
● 時間點 December 1st 2017, 10:19:42.000:
開始驅逐
● 驅逐 goroutine
if nc.useTaintBasedEvictions { // Handling taint based evictions. Because we don't want a dedicated logic in TaintManager for NC-originated // taints and we normally don't rate limit evictions caused by taints, we need to rate limit adding taints. go wait.Until(nc.doNoExecuteTaintingPass, scheduler.NodeEvictionPeriod, wait.NeverStop) } else { // Managing eviction of nodes: // When we delete pods off a node, if the node was not empty at the time we then // queue an eviction watcher. If we hit an error, retry deletion. go wait.Until(nc.doEvictionPass, scheduler.NodeEvictionPeriod, wait.NeverStop) }
● 透過刪除 pods 的方式驅逐
func (nc *Controller) doEvictionPass() { nc.evictorLock.Lock() defer nc.evictorLock.Unlock() for k := range nc.zonePodEvictor { // Function should return 'false' and a time after which it should be retried, or 'true' if it shouldn't (it succeeded). nc.zonePodEvictor[k].Try(func(value scheduler.TimedValue) (bool, time.Duration) { node, err := nc.nodeLister.Get(value.Value) if apierrors.IsNotFound(err) { glog.Warningf("Node %v no longer present in nodeLister!", value.Value) } else if err != nil { glog.Warningf("Failed to get Node %v from the nodeLister: %v", value.Value, err) } else { zone := utilnode.GetZoneKey(node) evictionsNumber.WithLabelValues(zone).Inc() } nodeUID, _ := value.UID.(string) remaining, err := util.DeletePods(nc.kubeClient, nc.recorder, value.Value, nodeUID, nc.daemonSetStore) if err != nil { utilruntime.HandleError(fmt.Errorf("unable to evict node %q: %v", value.Value, err)) return false, 0 } if remaining { glog.Infof("Pods awaiting deletion due to Controller eviction") } return true, 0 }) } } |
● 時間點 December 1st 2017, 10:19:42.000:
statefulset controller 發現 default/gxr1-oracle-statefulset 狀態異常
● 時間點 December 1st 2017, 10:19:42.000:
scheduler 將 pod 排程到 k8s-node1
這樣舊的MySQL 例項在 k8s-node3 上,kubernetes 又將新的例項排程到 k8s-node1。
兩個資料庫例項寫同一份資料檔案,導致 data corruption,兩個節點都無法啟動。
老例項啟動報錯, 日誌如下:
2017-12-01 10:19:47 5628 [Note] mysqld (mysqld 5.7.19-log) starting as process 963 ... 2017-12-01 10:19:47 5628 [Note] InnoDB: PUNCH HOLE support available 2017-12-01 10:19:47 5628 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins 2017-12-01 10:19:47 5628 [Note] InnoDB: Uses event mutexes 2017-12-01 10:19:47 5628 [Note] InnoDB: GCC builtin __atomic_thread_fence() is used for memory barrier 2017-12-01 10:19:47 5628 [Note] InnoDB: Compressed tables use zlib 1.2.3 2017-12-01 10:19:47 5628 [Note] InnoDB: Using Linux native AIO 2017-12-01 10:19:47 5628 [Note] InnoDB: Number of pools: 1 2017-12-01 10:19:47 5628 [Note] InnoDB: Using CPU crc32 instructions 2017-12-01 10:19:47 5628 [Note] InnoDB: Initializing buffer pool, total size = 3.25G, instances = 2, chunk size = 128M 2017-12-01 10:19:47 5628 [Note] InnoDB: Completed initialization of buffer pool 2017-12-01 10:19:47 5628 [Note] InnoDB: If the mysqld execution user is authorized, page cleaner thread priority can be changed. See the man page of setpriority(). 2017-12-01 10:19:47 5628 [Note] InnoDB: Highest supported file format is Barracuda. 2017-12-01 10:19:47 5628 [Note] InnoDB: Log scan progressed past the checkpoint lsn 406822323 2017-12-01 10:19:47 5628 [Note] InnoDB: Doing recovery: scanned up to log sequence number 406823190 2017-12-01 10:19:47 5628 [Note] InnoDB: Database was not shutdown normally! 2017-12-01 10:19:47 5628 [Note] InnoDB: Starting crash recovery. 2017-12-01 10:19:47 5669 [Note] InnoDB: Starting an apply batch of log records to the database... InnoDB: Progress in percent: 89 90 91 92 93 94 95 96 97 98 99 2017-12-01 10:19:47 5669 [Note] InnoDB: Apply batch completed 2017-12-01 10:19:47 5669 [Note] InnoDB: Last MySQL binlog file position 0 428730, file name mysql-bin.000004 2017-12-01 10:19:47 5669 [Note] InnoDB: Removed temporary tablespace data file: "ibtmp1" 2017-12-01 10:19:47 5669 [Note] InnoDB: Creating shared tablespace for temporary tables 2017-12-01 10:19:47 5669 [Note] InnoDB: Setting file './ibtmp1' size to 12 MB. Physically writing the file full; Please wait ... 2017-12-01 10:19:47 5669 [Note] InnoDB: File './ibtmp1' size is now 12 MB. 2017-12-01 10:19:47 5669 [Note] InnoDB: 96 redo rollback segment(s) found. 96 redo rollback segment(s) are active. 2017-12-01 10:19:47 5669 [Note] InnoDB: 32 non-redo rollback segment(s) are active. 2017-12-01 10:19:47 5669 [Note] InnoDB: Waiting for purge to start 2017-12-01 10:19:47 0x7fcb08928700 InnoDB: Assertion failure in thread 140509998909184 in file trx0purge.cc line 168 InnoDB: Failing assertion: purge_sys->iter.trx_no <= purge_sys->rseg->last_trx_no InnoDB: We intentionally generate a memory trap. InnoDB: Submit a detailed bug report to InnoDB: If you get repeated assertion failures or crashes, even InnoDB: immediately after the mysqld startup, there may be InnoDB: corruption in the InnoDB tablespace. Please refer to InnoDB: http://dev.mysql.com/doc/refman/5.7/en/forcing-innodb-recovery.html InnoDB: about forcing recovery. 10:19:47 5669 - mysqld got signal 6 ; |
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/28218939/viewspace-2151253/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 容器化RDS|計算儲存分離架構下的 IO 優化架構優化
- 容器化RDS—— 計算儲存分離 or 本地儲存
- ClickHouse 存算分離架構探索架構
- 什麼是存算分離架構?架構
- 容器雲環境下如何設計儲存架構?架構
- 解構成為架構潮流的“存算分離”架構
- “雲端計算”時代 儲存架構如何變化架構
- 搜尋線上服務的儲存計算分離
- 計算儲存分離在訊息佇列上的應用佇列
- 容器化 RDS:藉助 CSI 擴充套件 Kubernetes 儲存能力套件
- 計算與儲存分離實踐—swift訊息系統Swift
- 雲端計算儲存之Ceph架構是怎麼樣的?架構
- Shopee ClickHouse 冷熱資料分離儲存架構與實踐架構
- 存算一體 VS 存算分離 ,IT發展下的技術迭代
- 架構中的分離架構
- 存算分離是否成為開源分散式資料庫主流架構?分散式資料庫架構
- 6月2日雲棲精選夜讀:儲存與計算分離:OSS構建表+計算引擎對接
- 線上課程|儲存與計算分離,京東在Elasticsearch上的實踐分享Elasticsearch
- 計算機儲存器的分類及其特性計算機
- 計算儲存分離在京東雲訊息中介軟體JCQ上的應用
- 聊聊大資料的存算分離大資料
- 計算機的層次化架構計算機架構
- 容器化RDS|排程策略
- 前後端分離架構中的介面設計後端架構
- 容器儲存架構比較:Kubernetes、Docker和MesosCompare架構Docker
- 金融行業聯機交易業務場景下的儲存架構設計行業架構
- 資料倉儲架構分層設計架構
- 前後分離架構的探索之路架構
- 如何進行雲端儲存架構框架設計?架構框架
- 二、儲存架構演變架構
- 雲端計算的架構架構
- 億級流量系統架構之如何支撐百億級資料的儲存與計算【石杉的架構筆記】架構筆記
- Clobotics 計算機視覺場景儲存實踐:多雲架構、 POSIX 全相容、低運維的統一儲存HB計算機視覺架構運維
- DAOS 分散式非同步物件儲存|架構設計分散式非同步物件架構
- 大型網站架構改進歷程:儲存的瓶頸(下)網站架構
- docker容器儲存Docker
- 億級流量系統架構之如何支撐百億級資料的儲存與計算架構
- [譯] 無容器下的雲端計算