容器化對資料庫的效能有影響嗎?

小猿姐聊技術發表於2024-02-02

引言

容器化是一種將應用程式及其依賴項打包到一個獨立、可移植的執行環境中的技術。容器化技術透過使用容器執行時引擎(比如Docker/Containerd)來建立、部署和管理容器。Kubernetes(通常簡稱為 k8s)是一個開源的容器編排和管理平臺,它提供了一個集中式的、可伸縮的平臺來自動化容器的部署、擴充套件、管理和排程。


容器化對資料庫的效能有影響嗎?

Fig. 1. Usage of containerized workloads by category [4]

資料庫容器化的趨勢已經非常明顯,如圖.1 所示,資料庫+分析類的 workload 已經佔據了半壁江山,但是依然有很多人在做技術選型時面臨一個難題:容器化是否對資料庫效能有影響?如果有,影響的因素是什麼?如何面對容器化帶來的效能甚至是穩定性的問題?

容器化優勢和技術原理

容器化的優勢

靈活性和可移植性:容器化技術提供了靈活性和可移植性的優勢,使得資料庫的部署和遷移變得更加簡單和可靠,容器化也是 IaC 的基礎。

資源隔離和可擴充套件性:容器化技術透過使用容器執行時引擎提供了資源隔離和可擴充套件性的優勢。每個容器都有自己的執行時環境和資源分配,因此資料庫例項可以在容器中獨立執行,相互之間影響降到最低。這種資源隔離使得資料庫例項能夠更好地利用計算資源,並提供更好的效能和可靠性。

更友好的排程策略:由於容器化後的資源粒度更小,對上層排程更為友好,可以在不同場景應用不同的排程策略,比如透過離線和線上混合部署來錯峰使用計算資源,多種引擎混部提升整體利用率,透過提升部署密度來降低計算成本。

容器化技術原理和分類

虛擬化

說到容器,那就不得不提虛擬化,虛擬化是一種將計算資源進行抽象和隔離的技術,使得多個虛擬例項可以在同一物理伺服器上同時執行。它透過在硬體和作業系統之間引入虛擬機器監視器(Hypervisor)的軟體層,將物理伺服器分割為多個虛擬機器,併為每個虛擬機器提供獨立的作業系統和資源。每個虛擬機器都可以執行完整的作業系統,並具有獨立的核心和資源,類似於在物理伺服器上執行一個完整的計算機。

容器化是一種更為輕量的虛擬化技術,它使用作業系統級別的虛擬化來隔離和執行應用程式及其依賴的環境。容器化和虛擬化一般搭配使用,以滿足使用者對不同隔離場景的需求。

虛擬化+容器化分類

根據容器執行時的資源隔離和虛擬化方式,可以將目前的主流虛擬化+容器技術分為這麼幾類:

  1. 標準容器,符合 OCI (Open Container Initiative)規範,如 docker/containerd,容器執行時為 runc,這是目前 k8s workload 的主要形態

  2. 使用者態核心容器,如 gVisor,也符合 OCI 規範,容器執行時為 runsc,有比較好的隔離性和安全性,但是效能比較差,適合比較輕量的 workload

  3. 微核心容器,使用了 hypervisor,如 Firecracker、Kata-Container,也符合 OCI 規範,容器執行時為 runc 或 runv,有比較好的安全性和隔離性,效能介於標準容器和使用者態核心容器之間

  4. 純虛擬機器,如 KVM、Xen、VMWare,是主流雲廠商伺服器的底層虛擬化技術,一般作為 k8s 中的 Node 存在,比容器要更低一個層次


Fig. 2. Comparison of system architecture of various lightweight virtualization methods. Orange parts are kernel space, while green parts are user space.[2]

OCI 主流容器技術實現

下面我們對符合 OCI 規範的幾款主流容器化技術做一下分析。

1.runc:

runc 是一個符合 OCI 標準的容器執行時,它是 Docker/Containerd 核心容器引擎的一部分。它使用 Linux 的名稱空間(Namespace)和控制組(Cgroup)技術來實現容器的隔離。
在執行容器時,runc 使用名稱空間隔離容器的程式、網路、檔案系統和 IPC(程式間通訊)。它還使用控制組來限制容器內程式的資源使用。這種隔離技術使得容器內的應用程式可以在一個相對獨立的環境中執行,與宿主機和其他容器隔離開來。
runc 的隔離技術雖然引入了一定開銷,但是這種開銷僅限於名稱空間對映、限制檢查和一些記賬邏輯,理論上影響很小,而且當 syscall 是長耗時操作時,這種影響幾乎可以忽略不計,一般情況下,基於 Namespace+Cgroup 的隔離技術對 CPU、記憶體、I/O 效能的影響較小。

Fig. 3. Runc Architecture

2. Kata Containers:
Kata Containers 是一個使用虛擬機器技術實現的容器執行時,它提供了更高的隔離性和安全性。Kata Containers 使用了 Intel 的 Clear Containers 技術,並結合了輕量級虛擬機器監控器和容器執行時。
Kata Containers 在每個容器內執行一個獨立的虛擬機器,每個虛擬機器都有自己的核心和使用者空間。這種虛擬化技術能夠提供更嚴格的隔離,使得容器內的應用程式無法直接訪問宿主機的資源。然而,由於引入了虛擬機器的啟動和管理開銷,相對於傳統的容器執行時,Kata Containers 在系統呼叫和 I/O 效能方面可能會有一些額外的開銷。

Fig. 4. Kata Containers Architecture

3. gVisor:
gVisor 是一個使用使用者態虛擬化技術實現的容器執行時,它提供了更高的隔離性和安全性。gVisor 使用了自己的核心實現,在容器內部執行。
gVisor 的核心實現,稱為 "Sandboxed Kernel",在容器內部提供對作業系統介面的模擬和管理。容器內的應用程式和程式與宿主核心隔離開來,無法直接訪問或影響宿主核心的資源。這種隔離技術在提高安全性的同時,相對於傳統的容器執行時,可能會引入一些額外的系統呼叫和 I/O 效能開銷。

Fig. 5. gVisor Architecture

4. Firecracker:
Firecracker 是一種針對無伺服器計算和輕量級工作負載設計的虛擬化技術。它使用了微虛擬化技術,將每個容器作為一個獨立的虛擬機器執行。
Firecracker 使用 KVM(Kernel-based Virtual Machine)技術作為底層虛擬化技術。每個容器都在自己的虛擬機器中執行,擁有獨立的核心和根檔案系統,並使用獨立的虛擬裝置模擬器與宿主機通訊。這種隔離技術提供了較高的安全性和隔離性,但相對於傳統的容器執行時,Firecracker 可能會引入更大的系統呼叫和 I/O 效能開銷。

Fig. 6. Firecracker Architecture

實現原理對比:

Table. 1: Overview of implementations of virtualization and isolation in Containerization

Containerd-runc Kata-Container gVisor FireCracker-Containerd
隔離原理 Namespace + Cgroup Guest Kernel Sandboxed Kernel microVM
OCI Runtime runc Clear Container + runv runsc runc
虛擬化技術 Namespace QEMU/Cloud Hypervisor+KVM Rule-Based Execution rust-VMM + KVM
vCPU Cgroup Cgroup Cgroup Cgroup
Memory Cgroup Cgroup Cgroup Cgroup
Syscall Host Guest + Host Sentry Guest + Host
Disk I/O Host virtio Gofer virtio
Network I/O Host + veth tc + veth netstack tap + virtio-net

還有人對 Container Engine 的不同實現做了對比,比如 Containerd 和 CRI-O [3][5],這個對比也不在本文討論範圍內,留給感興趣的讀者自己去了解。

K8s + 容器化對資料庫的影響:

容器化對資料庫有很多正面的影響:比如容器化可以簡化資料庫的部署和管理、為資料庫提供標準的隔離執行環境、可以讓資料庫在不同的複雜環境中輕鬆部署和靈活遷移、對資料庫的版本管理也更加規範和方便。而且在 k8s 的加持下,資料庫中的多種角色和元件可以被靈活有機地編排在一起。

容器化對資料庫的挑戰:

但是,k8s+容器化對資料庫也帶來了很多挑戰,這和資料庫本身的特點也有很大關係,與普通的無狀態應用相比,資料庫有如下特點:

  • 資料庫是一個有多種角色的複雜應用:一個完整的資料庫有多種不同的角色,比如 MySQL 主備形態中,同樣是兩個 MySQL 容器,一個是主,一個是備,角色並不對等,這種不對等的關係需要被正確表達,而且在建立、重啟、刪除、備份、高可用等各種運維操作中都要被正確管理,本質上這是一種容器之間對於資料狀態的互相依賴,對於這種依賴目前的容器和 k8s 都沒有很好地抽象與解決。

  • 資料庫對資料的永續性和一致性有很高的需求:資料庫對儲存有很高的需求,簡單的容器化並不能滿足一個生產級別的 workload,還需要配套的 CSI 和 PersistentVolume,對儲存的選型也影響著資料庫可選的操作選項,比如雲盤有很高的 durability,提供 snapshot 備份功能,並能在不同的計算節點上 attach 和 detach,對資料庫的備份恢復和高可用操作非常友好;但在本地盤上,這種選擇就會更窄一些,比如 node 當機時我們可能就會永遠失去一個資料副本,高可用操作處理起來會更有挑戰,備份操作也只能選擇物理/檔案(physical)備份或邏輯(logical)的方式。不同的儲存方案對應著不同的持久化能力和不一樣的資料庫架構。

  • 資料庫對效能也有很高的需求:資料庫對效能的需求比較多樣,如果從 CPU、記憶體、網路、儲存這幾個方面來進行劃分,有 CPU + 儲存 I/O 密集型,如 OLAP 產品 ClickHouse、Greenplum 等;有記憶體 + 網路 I/O 密集型,如 Redis 和記憶體資料庫;有 CPU + 儲存 I/O 密集型,如 MySQL、Postgresql 等傳統 OLTP 資料庫。而且根據查詢場景不同,即使是同一個資料庫程式在不同的 SQL 中對資源的需求也大相徑庭。

  • 資料庫對安全性的要求:資料庫中的資料一般都比較核心和敏感,因此對執行環境隔離、資料訪問控制、日誌審計都有一定的規範化要求。

總而言之,將資料庫跑在容器+k8s 上,對資料庫是一種很大的挑戰,比如資料庫要去適應生命週期短暫的容器、浮動的 IP、頻繁更新的基礎設定、複雜的效能環境;對容器+k8s 也是很大的挑戰,比如角色的引入、容器之間的狀態和資料依賴、對效能的苛刻需求、對完整安全體系的合規要求。

關於上面提到的 1,2,4 這三點,在我們開發的 KubeBlocks 專案中已經有比較成體系的解決方案,感興趣的可以去圍觀  kubeblocks.io。回到本文的主題,在下面的部分會對容器化對資料庫效能的影響做進一步的深入分析。

K8s +容器化對資料庫效能的影響

如上所述,資料的效能主要受 CPU、記憶體、儲存、網路 這幾個因素的影響,我們將圍繞這幾個方面分析 k8s 和容器化對資料庫效能的影響,雖然 k8s 中的一些排程和親和性策略也會對效能有潛在影響,但這些策略和容器化無關,因此並不在本次討論範圍內。

我們從上述幾個維度來綜述一下容器化對應用(包括資料庫)效能的影響,在本次綜述中我們整理了業界近幾年的一些論文和測試資料,會對其中的一些測試資料和偏移項進行分析,指出其中的原因與不合理之處,針對一些缺乏的場景我們補了一些測試,比如 k8s CNI 對網路效能的影響。

CPU:

測試伺服器:Quad-Core Hyper Thread 4 Intel Core i5-7500 8GB RAM,1TB disk,Ubuntu 18.04 LTS

測試場景:這裡的資料和測試場景來自論文 [1],case1 是用 sysbench 4 併發做質數計算,最終彙報每秒發生的 events,屬於純計算場景,幾乎所有指令都跑在 user space, syscall 呼叫可忽略,所以理論上幾種容器技術的表現會差不多

測試結果:幾種容器的 CPU 表現差不多,相比裸金屬,其他場景效能下降都在 4% 左右

分析:這個 4% 的下降應該是 Cgroup 對 CPU 的限制所造成,當 Sysbench 併發等於 Hyper Thread 數量時,被 Cgroup Thottle 的機率非常高,當有 Cgroup Throttle 發生時,程式會被強制等待一個 jiffy(10ms),Cgroup 對資源的計算週期是以 jiffy 為粒度,不是以秒為粒度,所以 4 vCPU 的容器幾乎不可能跑到 400%,有一定 loss 是正常的,一般被 Throttle 的次數可以從 Cgroup 的 cpu.stat 檔案中查詢到


Fig. 7. CPU performance (Sysbench benchmark) (Xingyu Wang 2022)

測試場景:Davi1d 影片解碼,影片大小在幾百兆左右,在該測試中,由於需要讀取磁碟上的資料,所以會有大量的 syscall 呼叫,syscall 呼叫會對應用效能造成一部分影響

測試結果:runc 和 kata-qemu 損失 4% 左右,和質數測試結果類似;gVisor-ptrace 損失有 13%,而 gVisor-KVM 能和裸金屬持平

分析:影片解碼屬於順序讀取,順序讀取在 linux 中會有 read ahead 最佳化,所以絕大部分 I/O 都是直接從 page cache 中讀取資料,runc 主要還是受 Cgroup 影響,其他三個方案主要受 syscall 實現方案的影響,論文中並沒有就 gVisor-ptrace 和 gVisor-KVM 差別做進一步分析,gVisor 使用了 gofer 做檔案系統,gofer 還有自己一些的 cache 策略,進一步的分析可能要從 gVisor syscall 和 cache 策略入手


Fig. 8. CPU performance (Dav1d benchmark) (Xingyu Wang 2022)

Memory:

測試場景:RAMSpeed,有 4 個子場景(Copy,Scale,Add,Triad),具體原理就不展開了

測試結果:幾種方案都差不多

分析:當記憶體分配好並做好缺頁中斷(page fault)後,理論上容器化對記憶體訪問沒有影響,真正影響記憶體效能的是 mmap 和 brk 之類的 syscall 呼叫,但是這個測試中,此類 syscall 佔比極小


Fig. 9. Memory access performance (Xingyu Wang 2022)

測試場景:Redis-Benchmark,測試了子場景 GET, SET, LPUSH, LPOP, SADD

測試結果:runc 和 kata-qemu 影響很小,gVisor 受影響很大,gVisor-ptrace 損失在 95% 左右,gVisor-KVM 損失在 56% 左右

分析:redis 是單執行緒重網路 I/O 的一種應用,網路 I/O 都是透過 syscall 進行,所以 gVisor 會有很大的效能損失,原論文中認為損失主要是由記憶體分配引起,這應該是一種誤解,Redis 內部使用使用者態記憶體管理工具 jemalloc,jemalloc 會透過呼叫 mmap syscall 來向 OS 批發大塊記憶體,然後再做本地小塊分配,由於 jemalloc 有比較成熟的記憶體分配和快取機制,所以呼叫 mmap 的機率會很小。當 redis 滿載時,網路 I/O 消耗的 CPU (CPU sys) 在 70% 左右,所以這裡 gVisor 的效能損耗主要由 syscall 劫持和內部的網路棧 netstack 引起,這個測試也說明 gVisor 目前並不適合重網路 I/O 的場景


Fig. 10. Redis performance for different container runtimes (Xingyu Wang 2022)

Disk I/O:

測試場景:IOZone 讀寫 16GB 檔案

測試結果:順序讀寫影響不大,kata-qemu 受影響較大,影響範圍為 12-16% 分析:大塊讀寫其實就是順序讀寫,如前所述,順序讀 OS 有 read ahead,順序讀寫其實操作的大部分是 page cache,原論文對 kata-qemu 做了分析,認為和 virtio-9p 檔案系統有關,virtio-9p 針對網路設計,對虛擬化並沒有做針對最佳化


Fig. 11. Disk read and write performance (Xingyu Wang 2022)

測試場景:直接基於 tmpfs(shared memory)做測試,單純用來衡量 syscall+記憶體 copy 對效能的影響

測試結果:除了 gVisor,其他都差不多

分析:gVisor syscall 成本比較高,結果和 redis-benchmark 類似


Fig. 12. Disk read and write performance (tmpfs overlay) (Xingyu Wang 2022)

測試場景:SQLite 單執行緒插入測試,耗時越少越好

測試結果:runc 和裸金屬相近,kata 耗時多出 17%,gVisor 耗時多出 125%

分析:資料庫 workload 比較複雜,是 CPU、記憶體、網路、Disk I/O 的綜合影響,對 syscall 的呼叫非常頻繁,gVisor 不是很適合此類場景


Fig. 13. Database record insertion performance (Xingyu Wang 2022)

Network I/O:

測試場景:TCP stream 吞吐測試,throughtput 越多越好

測試結果:gVisor 網路效能比較差,和在 redis-benchmark 中看到的類似,其他幾個影響不大

分析:gVisor 受限於 syscall 機制和 netstack 實現,整體吞吐較差


Fig. 14. TCP_STREAM network performance (Xingyu Wang 2022)

測試場景:測試 TCP_RR, TCP_CRR, UDP_RR,RR 是 request & response 的縮寫,指的是一個 TCP 請求來回,TCP 連結只建立一次,後續複用,CRR 是指每次測試都建立一條新的 TCP 連結,TCP_RR 對應著長連結的場景,TCP_CRR 對應著短連結的場景

測試結果:runc 和裸金屬相近,kata 有較小的損失,gVisor 損失很大,原理同上


Fig. 15. TCP_RR, TCP_CRR and UDP_RR performance (Xingyu Wang 2022)

CNI Network:

容器一般搭配 k8s 使用,基於 k8s 的容器編排已經是事實上的標準,在 k8s 環境中,網路一般由 CNI + 容器技術共同實現,常見的 CNI 有很多,比較主流的有 Calico, Flannel, Cilium...,在最新的版本中 Calico 和 Cilium 都大量使用了 eBPF 的技術,雖然具體實現不同,但是這兩個 CNI 在很多測試場景中效能表現相當,具體的測試資料見 [6]。

在接下來的測試中,我們選取 Cilium eBPF legacy host-routing 和 Cilium eBPF 兩種模式做對比,測試 CNI 對資料庫效能的具體影響。

  • legacy host-routing:
    在傳統主機路由模式(legacy host-routing)下,Cilium 使用 iptables 來進行包過濾和轉發,iptables 仍然是必需的,並且用於配置和管理網路流量的轉發規則,Cilium 透過 iptables 規則將流量引導到 Cilium 代理,然後由代理進行處理和轉發。
    在傳統主機路由模式下,Cilium 會利用 iptables 的 NAT 功能來修改源 IP 地址和目標 IP 地址,以實現網路地址轉換(NAT)和服務負載均衡。

  • eBPF-based host-routing:
    在新的 eBPF-based 路由模式下,Cilium 不再依賴 iptables,它使用 Linux 核心的擴充套件 BPF(eBPF)功能來進行包過濾和轉發。eBPF 主機路由允許繞過主機名稱空間中的所有 iptables 和上層棧開銷,以及在遍歷虛擬網路卡時的一部分上下文切換開銷。網路資料包儘早從面向網路的網路裝置中捕獲,並直接傳遞到 Kubernetes Pod 的網路名稱空間中。在出口方面,資料包仍然透過 veth pair 進行遍歷,被 eBPF 捕獲並直接傳遞到外部面向網路介面。路由表直接由 eBPF 查詢,因此此最佳化完全透明,並與系統上執行的任何其他提供路由分發的服務相容。

Fig. 16. Comparison of legacy and eBPF container networking [6]

測試環境:

Kubernetes: v1.25.6 CNI: cilium:v1.12.14

Node CPU: Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz RAM 128G

Redis: 7.0.6, 2 vCPU, Maxmemory: 2Gi

測試場景:

Table. 2. Overview of different service routing paths in K8s

Network Source Target
NodeLocal2HostPod Hostnetwork Node 本機 Pod
NodeLocal Ethernet Node 本機程式
PodLocal2Pod Pod Pod 本機 Pod
Node2HostPod Hostnetwork Node 遠端 Pod
NodeLocal2NodePort NodePort Node 本機 NodePort
Node2Node Ethernet Node 遠端程式
NodeLocal2Pod Pod Node 本機 Pod
Pod2Pod Pod Pod 遠端 Pod
Node2NodePort NodePort Node 遠端 NodePort
Pod2NodePort Pod + NodePort Pod 遠端 NodePort
Node2Pod Pod Node 遠端 Pod

測試結果:

Legacy host-routing with iptables:


Fig. 17. Redis benchmark under legacy host-routing with iptables


Fig. 18. Comparison between Host network and Pod network under legacy host-routing

eBPF-based host-routing


Fig. 19. Redis benchmark under eBPF-based host-routing


Fig. 20. Comparison between Host network and Pod network under eBPF-based host-routing

分析:legacy host-routing 對網路效能的影響較大,Pod 網路和 host 網路效能能差出 40%,eBPF-base host-routing 基本能將 Pod 網路延遲和 host 網路打平。eBPF-based host-routing 能將延遲做到和路由規則數量無關,並徹底消除 host 網路和 Pod 網路之間的差距,是一種普適性的提升,當然也非常適合 redis 之類的重網路 I/O 應用。

總結:

在 CPU、記憶體和 Disk I/O 幾個維度,runc 的效能最接近 bare metal,kata-containers 效能略低於 runc,但是在安全性和隔離性上有更好的表現,gVisor 由於受 syscall 實現的影響,效能表現最差,這可能和 gVisor 更關注安全特性有關,不過 gVisor 新版本也在一直不斷提升效能。

網路比較特殊,因為還需要考慮 k8s CNI 的影響,在 Cilium eBPF + runc 的組合測試中,容器網路能夠做到和 Host 網路一樣的效能,Cilium 也支援 kata-containers,不過對其他容器技術的支援較少。

總體而言,runc 在各個層面都能達到和裸金屬相當的效能表現,也是目前 k8s workload 最常用的選擇;kata-containers 效能略低於 runc,但是有比較好的隔離性,是效能和安全都能兼顧的一種選擇;gVisor 有比較靈活的隔離性,但是效能還比較差,比較適合對安全性要求很高,對效能要求不那麼苛刻的場景;Firecracker 的應用場景和 kata-containers 比較類似。

所以,如果是跑資料庫的 workload,優先推薦 runc 和 kata-containers。

常見的資料庫效能問題:

很多人常常受資料庫效能問題的困擾,在此,我們對常見的資料庫效能問題場景做了總結和原理分析,也可以讓大家一窺資料庫和基礎設施的複雜性以及我們努力的方向。

Disk IO hang:

當有大量 BufferedIO 下發時,比如 MySQL 寫外排臨時檔案的場景,寫的是 page cache,並會頻繁更新 Ext4 檔案系統的後設資料,此時 CPU 和 I/O 可能都會很忙,MySQL 程式會被頻繁 CPU Throttle,髒頁持續增多,然後觸發檔案系統 flush 髒頁,大量刷髒 I/O佔滿硬體通道,如果程式被 CPU Throttle 排程走時又恰巧持有了 Ext4 Journal Lock,那麼其他共享該 Ext4 檔案系統的程式都被掛起,當被掛起的次數和時間足夠久,就會造成 IO hang,這種現象常見於共享本地盤的場景,如 bare metal 和 hostpath CSI。主流的解決方案就是給 BufferedIO 限流,Cgroup V2 已支援該功能。

透過這個例子也可以看出,有時候瓶頸並不是某個單項因素所決定的,是由多個關聯的因素聯動產生,在 Disk IO hang 中,page cache 和記憶體、Disk I/O 相關,CPU Throttle 和 CPU 排程相關,Ext4 Journal 又和 Lock 有關,所以這些因素共同作用、互相影響才形成了一個完整的 IO hang。

值得一提的是,為了最佳化 I/O 操作,很多資料庫廠商都推薦將 XFS 作為檔案系統的第一選擇。對於 Disk I/O 對資料庫的深度影響,可以參考 《PosgreSQL@k8s 效能最佳化記》[7]。

Out Of Memory (OOM):

當使用 Cgroup 對記憶體進行隔離後,OS 的記憶體管理路徑會和 bare metal 變得不同,在記憶體分配(page allocation)和記憶體回收(page reclaim)上面臨的壓力會比 bare metal 更高。

比如有一個 Pod,記憶體 request 和 limit 都為 1G ,記憶體的分配和回收都要在 1G 的實體記憶體空間內進行,而資料庫又是一個對記憶體資源要求比較高的負載型別,僅僅啟動一個空的資料庫程式可能就要消耗數百兆的記憶體,所以其實留給實際應用的空間非常小,此時如果再搭配上監控或日誌採集之類的 sidecar,資料庫記憶體耗盡的機率就會非常高。

但是真正可怕的並不是 OOM,而是在 OOM 之前慢慢步入死亡的過程,這個過程可能會無比漫長,在真正觸發 OOM 之前,page reclaim 模組會嘗試一切辦法去回收足夠的記憶體,並呼叫耗時很久的 slow path,然後一遍又一遍地重複整個過程,直到超過限定次數退出,在這個過程中資料庫客戶端可能會觀測到大量事務超時退出。

Page reclaim slow path 還不僅僅影響一個 Cgroup Namespace,由於 OS 中的很多資料結構在 Host 這一層是共享的, 比如雖然 Pod 記憶體邏輯上屬於某個 Cgroup Namespace,但是在 Host Kernel 中,真實的記憶體管理還是基於同一個 Buddy System,對這些實體記憶體的管理需要全域性的鎖機制,所以一個記憶體壓力很大的 Pod 觸發的 page reclaim slow path 也會影響其它健康 Pod 的記憶體管理路徑,有的時候整個 Node 上的資料庫都變慢可能只是因為有一個 Pod 的 limit 記憶體太小了。

徹底解決此類問題就需要一些更好的隔離方案,比如基於微核心或 VM 的隔離方案,讓兩個 Pod 屬於不同的記憶體管理空間;還有一種改進方案是當記憶體回收變得不可避免時,儘量在資料庫的層面對各種執行指標進行判斷,做到 fail fast。

Too many Connections:

對於 OLTP 資料庫,一般都有專屬的預分配的 buffer pool,這部分記憶體相對是固定的,可變的部分主要來自 Connection 結構體、work mem 中間計算結果、頁表、page cache 等。

對於多程式模型的資料庫如 Postgresql 和 Oracle,一條 Connection 對應一個程式,當使用的 buffer pool 本身就很大時,fork 一個程式所需的頁表項也非常可觀,假設 page 4k,頁表項 8 位元組,那麼頁表和 buffer pool 的比例關係是 8/4k = 1/512,當有 512 條連結時,OS 需要的頁表記憶體就和 buffer pool 一樣大,這種多程式模型嚴重影響了資料庫的擴充套件性,在需要大併發量的場景會有比較高的額外記憶體成本,但是這種成本一般很容易被人忽略。

解決方案一般分為兩種,一是在資料庫前面增加一層 proxy,透過 proxy 來承接大量的連結,proxy 和資料庫之間只建立比較少的連結,比如 proxy 和後端 db 之間只建立 P 條連結,proxy 從應用側承接 C 條連結(C >> P),透過這種連結複用來降低後端 db 的連結壓力;還有一種是採用 Hugepage 的方案,假設 Hugepage size 為 2M,那麼頁表和 buffer pool 的比例關係就是 8/2M = 1/256k,頁表成本幾乎可以忽略不計,多程式模型可承載的連結數也大大增加,但是 Hugepage 也是一種副作用很多的技術方案,給資源管理也帶來了不小的負擔。所以 proxy 方案一般是更友好的選擇。

多執行緒模型又分兩種,一種是一個 Connection 對應一個執行緒,當 Connection 增多時雖然沒有頁表 copy 的問題,但是也會導致資源爭搶、context switch 過多等問題,這些問題又會導致效能持續惡化,當然這種問題也可以透過加 proxy 來解決;一種是 C 條連結對應 P 個執行緒(C >> P),這種方案一般叫作執行緒池(Thread Pool),比如 Percona MySQL 就採用了此類方案。

Proxy 和 Thread Pool 本質上是相同的,都是做連結複用,只是實現的地方不同,而且這兩個方案也可以搭配使用,進一步提升容量和降低負載。

Table. 3. Overview of different database process-connection models

連結數:程式數 頁表 備註
多程式 Proxy C:P *P C >> P
多程式 直連 C:C *C
多執行緒 Thread Pool C:P *1 C >> P
多執行緒 Per Thread C:C *1

TCP Retran:

網路對資料庫的影響主要體現在兩個方面:

延遲:網路延遲會影響資料的傳輸時長,進而會影響客戶端的整體響應時間,當客戶側的請求延遲變高時,單位時間內完成相同請求數量所需要的連結數會變多,客戶端連結數變多又會導致記憶體消耗變大、context switch 變多、爭搶更加劇烈,最終導致效能的逐步下降。

頻寬:無論是單個 TCP 連結的有效頻寬,還是網路卡和交換機網口的最大傳輸頻寬,都對網路傳輸質量和延遲有重大的影響,當某個 TCP 連結、交換機網口或網路卡佇列變得擁塞時,會在 OS Kernel 或硬體層面觸發丟包行為,丟包又會觸發重傳和亂序,重傳和亂序又導致延遲上升,進而引發後續一系列的效能問題。

網路問題觸發的不僅是效能問題,還有可用性和穩定性的問題,比如因網路延遲過大心跳超時導致的主備切換、主備之間的複製延遲過大等問題。

CPU schedule wait:

在一些基於 VM 的容器化方案中,容器中的程式和 Host Kernel 中的程式並不是能一一對應的,在 Host Kernel 看來,看到的只有 VM 虛擬化相關的程式,當你在 VM 內部看到一個 process 處於 running 狀態,並不意味著它已經在Host 上獲取到資源並執行,Host 和 VM 是兩套獨立的 CPU 排程系統,只有當 VM 內部 process 處於就running 並且所在 Host 上對應的 VM process 也處於 running 狀態時,VM 內部 process 才真正得到執行。

從 process 變成 running 狀態到真正被執行到的這段時間就是額外的排程等待時間,這個等待時間對資料庫的效能也會產生影響,對效能要求比較苛刻的場景可以採取降低 Host 負載或設定 VM CPU affinity 的方法來降低影響。

Lock & Latch:

在資料庫領域,Lock 一般保護的是資源(Resource),Latch 保護的是臨界區(Critical Region),但是兩種技術最終在 OS 層面的內部實現是相同的,在 Linux 中,一般用 futex 來實現上層的互斥鎖和等待變數。

當 CPU、I/O、記憶體都無限供應的時候,資料庫的擴充套件性一般受限於自己內部的事務+鎖機制,比如在 TPC-C 測試中,大部分單機資料庫的擴充套件性一般都在 32 Core (64 Hyper Threads) ~ 64 Core (128 Hyper Threads) 之間,超過 32 Core 之後,CPU 數量對資料庫效能的邊際貢獻會非常低。

這個話題和容器的關係不是那麼密切,所以在本文中也就不做展開。

幾種資料庫的效能瓶頸分析:

Table. 4. Overview of different database performance bottlenecks

儲存引擎 Disk I/O I/O unit 程式模型 效能瓶頸
MySQL InnoDB DirectIO + BufferedIO Page 多執行緒 I/O bandwidth + Lock + Connections
PostgreSQL HeapTable BufferedIO Page 多程式 I/O bandwidth + Lock + Connections
MongoDB WiredTiger BufferedIO/DirectIO Page 多執行緒 I/O bandwidth + Lock + Connections
Redis RDB + Aof BufferedIO Key-Value 單執行緒* CPU Sys(網路)
  • MySQL 需要特別關注外排臨時檔案,由於臨時檔案使用的是 BufferedIO,如果沒有 Cgroup 限制,會很快觸發 OS 大量的髒頁刷髒,這個刷髒過程會佔用儲存裝置的幾乎所有通道,造成正常請求卡住,這種現象是比較經典的 Disk IO hang。

  • PostgreSQL 是多程式模式,所以需要十分關注連結數和頁表大小,雖然使用 Hugepage 方案可以降低頁表的負擔,但是 Hugepage 本身還是有比較多的副作用,利用 pgBouncer 之類的 proxy 做連結複用是一種更好的解法;當開啟 full page 時,PostgreSQL 對 I/O 頻寬的需求非常強烈,此時的瓶頸為 I/O 頻寬;當 I/O 和連結數都不是瓶頸時,PostgreSQL 在更高的併發下瓶頸來自內部的鎖實現機制。具體可以參考 《Postgresql@k8s 效能最佳化記》[7]。

  • MongoDB 整體表現比較穩定,主要的問題一般來自 Disk I/O 和連結數,WiredTiger 在 cache 到 I/O 的流控上做得比較出色,雖然有 I/O 爭搶,但是 IO hang 的機率比較小,當然 OLTP 資料庫的 workload 會比 MongoDB 更復雜一些,也更難達到一種均衡。

  • Redis 的瓶頸主要在網路,所以需要特別關注應用和 Redis 服務之間的網路延遲,這部分延遲由網路鏈路決定,Redis 滿載時 70%+ 的 CPU 消耗在網路棧上,所以為了解決網路效能的擴充套件性問題,Redis 6.0 版本引入了網路多執行緒功能,真正的 worker thread 還是單執行緒,這個功能在大幅提升 Redis 效能的同時也保持了 Redis 簡單優雅的特性。

總結:

本文在綜合業界研究成果的基礎上,補足了容器 + 網路 CNI 部分的測試,對容器化在 CPU、Memory、Disk I/O、Network 幾個方面的影響做了進一步的分析,藉機闡明瞭容器化對效能的影響機制和解決方法,並且透過分析測試資料,我們發現 runc + cilium eBPF 是一種和 bare metal 效能幾乎持平的容器化方案,如果考慮到更好的安全性和隔離性,kata-containers 也是一種很好的選擇。

然後在容器化的基礎上,對資料庫的常見的效能瓶頸做了原理分析,並指出資料庫這種 heavy workload 對 Host kernel 的複雜依賴,引導人們重新關注頁表、Journal Lock、TCP Retran、CPU schedule wait 這些容易被忽視的因素,當然這裡的很多問題和容器化無關,是一種普遍的存在。最後我們對幾款流行的資料庫做了定性分析,也根據我們團隊多年的運維經驗對一些常見問題做了總結,希望這些問題能被持續關注並從架構層面得到解決。

資料庫容器化是最近經常被提起的話題,to be or not to be 也是縈繞在每個決策者心中的問題,在我們看來,資料庫容器化面臨的效能、穩定性、有狀態依賴等關鍵問題都在被一一解決,每個問題都會有一個完美的答案,只要有需求在。

參考文獻

[1] Wang, Xing et al. “Performance and isolation analysis of RunC, gVisor and Kata Containers runtimes.” Cluster Computing 25 (2022): 1497-1513.

[2] Goethals, Tom et al. “A Functional and Performance Benchmark of Lightweight Virtualization Platforms for Edge Computing.” 2022 IEEE International Conference on Edge Computing and Communications (EDGE) (2022): 60-68.

[3] Espe, Lennart et al. “Performance Evaluation of Container Runtimes.” International Conference on Cloud Computing and Services Science (2020).

[4] 10 insights on real-world container use.


[5] Kube container Performance CRI-O vs containerD maybe alternatives .


[6] CNI Benchmark: Understanding Cilium Network Performance:  https://cilium.io/blog/2021/05/11/cni-benchmark/.


[7] A testing report for optimizing PG performance on Kubernetes:  https://kubeblocks.io/blog/A-testing-report-for-optimizing-PG-performance-on-Kubernetes.


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

相關文章