Envoy Proxy 在大多數(shù)情況下都是作為 Sidecar 與應(yīng)用部署在同一網(wǎng)絡(luò)環(huán)境中,每個(gè)應(yīng)用只需要與 Envoy(localhost)交互,不需要知道其他服務(wù)的地址。然而這并不是 Envoy 僅有的使用場(chǎng)景,它本身就是一個(gè)七層代理,通過模塊化結(jié)構(gòu)實(shí)現(xiàn)了流量治理、信息監(jiān)控等核心功能,比如流量治理功能就包括自動(dòng)重連、熔斷、全局限速、流量鏡像和異常檢測(cè)等多種高級(jí)功能,因此 Envoy 也常常被用于邊緣代理,比如 Istio 的 Ingress Gateway、基于 Envoy 實(shí)現(xiàn)的 Ingress Controller(Contour、Ambassador、Gloo 等)。
我的博客也是部署在輕量級(jí) Kubernetes 集群上的(其實(shí)是 k3s 啦),一開始使用 Contour 作為 Ingress Controller,暴露集群內(nèi)的博客、評(píng)論等服務(wù)。但好景不長,由于我在集群內(nèi)部署了各種奇奇怪怪的東西,有些個(gè)性化配置 Contour 無法滿足我的需求,畢竟大家都知道,每抽象一層就會(huì)丟失很多細(xì)節(jié)。換一個(gè) Controller 保不齊以后還會(huì)遇到這種問題,索性就直接裸用 Envoy 作為邊緣代理,大不了手?jǐn)] YAML 唄。
當(dāng)然也不全是手?jǐn)],雖然沒有所謂的控制平面,但儀式感還是要有的,我可以基于文件來動(dòng)態(tài)更新配置啊,具體的方法參考 Envoy 基礎(chǔ)教程:基于文件系統(tǒng)動(dòng)態(tài)更新配置。
1. UDS 介紹
說了那么多廢話,下面進(jìn)入正題。為了提高博客的性能,我選擇將博客與 Envoy 部署在同一個(gè)節(jié)點(diǎn)上,并且全部使用 HostNetwork 模式,Envoy 通過 localhost 與博客所在的 Pod(Nginx) 通信。為了進(jìn)一步提高性能,我盯上了 Unix Domain Socket(UDS,Unix域套接字),它還有另一個(gè)名字叫 IPC(inter-process communication,進(jìn)程間通信)。為了理解 UDS,我們先來建立一個(gè)簡單的模型。
現(xiàn)實(shí)世界中兩個(gè)人進(jìn)行信息交流的整個(gè)過程被稱作一次通信(Communication),通信的雙方被稱為端點(diǎn)(Endpoint)。工具通訊環(huán)境的不同,端點(diǎn)之間可以選擇不同的工具進(jìn)行通信,距離近可以直接對(duì)話,距離遠(yuǎn)可以選擇打電話、微信聊天。這些工具就被稱為 Socket。

同理,在計(jì)算機(jī)中也有類似的概念:
- 在
Unix中,一次通信由兩個(gè)端點(diǎn)組成,例如HTTP服務(wù)端和HTTP客戶端。 - 端點(diǎn)之間想要通信,必須借助某些工具,Unix 中端點(diǎn)之間使用
Socket來進(jìn)行通信。
Socket 原本是為網(wǎng)絡(luò)通信而設(shè)計(jì)的,但后來在 Socket 的框架上發(fā)展出一種 IPC 機(jī)制,就是 UDS。使用 UDS 的好處顯而易見:不需要經(jīng)過網(wǎng)絡(luò)協(xié)議棧,不需要打包拆包、計(jì)算校驗(yàn)和、維護(hù)序號(hào)和應(yīng)答等,只是將應(yīng)用層數(shù)據(jù)從一個(gè)進(jìn)程拷貝到另一個(gè)進(jìn)程。這是因?yàn)椋?strong>IPC 機(jī)制本質(zhì)上是可靠的通訊,而網(wǎng)絡(luò)協(xié)議是為不可靠的通訊設(shè)計(jì)的。
UDS 與網(wǎng)絡(luò) Socket 最明顯的區(qū)別在于,網(wǎng)絡(luò) Socket 地址是 IP 地址加端口號(hào),而 UDS 的地址是一個(gè) Socket 類型的文件在文件系統(tǒng)中的路徑,一般名字以 .sock 結(jié)尾。這個(gè) Socket 文件可以被系統(tǒng)進(jìn)程引用,兩個(gè)進(jìn)程可以同時(shí)打開一個(gè) UDS 進(jìn)行通信,而且這種通信方式只會(huì)發(fā)生在系統(tǒng)內(nèi)核里,不會(huì)在網(wǎng)絡(luò)上進(jìn)行傳播。下面就來看看如何讓 Envoy 通過 UDS 與上游集群 Nginx 進(jìn)行通信吧,它們之間的通信模型大概就是這個(gè)樣子:

2. Nginx 監(jiān)聽 UDS
首先需要修改 Nginx 的配置,讓其監(jiān)聽在 UDS 上,至于 Socket 描述符文件的存儲(chǔ)位置,就隨你的意了。具體需要修改 listen 參數(shù)為下面的形式:
listen unix:/sock/hugo.sock;
當(dāng)然,如果想獲得更快的通信速度,可以放在 /dev/shm 目錄下,這個(gè)目錄是所謂的 tmpfs,它是 RAM 可以直接使用的區(qū)域,所以讀寫速度都會(huì)很快,下文會(huì)單獨(dú)說明。
3. Envoy-->UDS-->Nginx
Envoy 默認(rèn)情況下是使用 IP 地址和端口號(hào)和上游集群通信的,如果想使用 UDS 與上游集群通信,首先需要修改服務(wù)發(fā)現(xiàn)的類型,將 type 修改為 static:
type: static
同時(shí)還需將端點(diǎn)定義為 UDS:
- endpoint:
address:
pipe:
path: "/sock/hugo.sock"
最終的 Cluster 配置如下:
- "@type": type.googleapis.com/envoy.api.v2.Cluster
name: hugo
connect_timeout: 15s
type: static
load_assignment:
cluster_name: hugo
endpoints:
- lb_endpoints:
- endpoint:
address:
pipe:
path: "/sock/hugo.sock"
最后要讓 Envoy 能夠訪問 Nginx 的 Socket 文件,Kubernetes 中可以將同一個(gè) emptyDir 掛載到兩個(gè) Container 中來達(dá)到共享的目的,當(dāng)然最大的前提是 Pod 中的 Container 是共享 IPC 的。配置如下:
spec:
...
template:
...
spec:
containers:
- name: envoy
...
volumeMounts:
- mountPath: /sock
name: hugo-socket
...
- name: hugo
...
volumeMounts:
- mountPath: /sock
name: hugo-socket
...
volumes:
...
- name: hugo-socket
emptyDir: {}
現(xiàn)在你又可以愉快地訪問我的博客了,查看 Envoy 的日志,成功將請(qǐng)求通過 Socket 轉(zhuǎn)發(fā)給了上游集群:
[2020-04-27T02:49:47.943Z] "GET /posts/prometheus-histograms/ HTTP/1.1" 200 - 0 169949 1 0 "66.249.64.209,45.145.38.4" "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" "9d490b2d-7c18-4dc7-b815-97f11bfc04d5" "fuckcloudnative.io" "/dev/shm/hugo.sock"
嘿嘿,Google 的爬蟲也來湊熱鬧。
你可能會(huì)問我:你這里的 Socket 為什么在 /dev/shm/ 目錄下?。縿e急,還沒結(jié)束呢,先來補(bǔ)充一個(gè)背景知識(shí)。
4. Linux 共享內(nèi)存機(jī)制
共享內(nèi)存(shared memory),是 Linux 上一種用于進(jìn)程間通信(IPC)的機(jī)制。
進(jìn)程間通信可以使用管道,Socket,信號(hào),信號(hào)量,消息隊(duì)列等方式,但這些方式通常需要在用戶態(tài)、內(nèi)核態(tài)之間拷貝,一般認(rèn)為會(huì)有 4 次拷貝;相比之下,共享內(nèi)存將內(nèi)存直接映射到用戶態(tài)空間,即多個(gè)進(jìn)程訪問同一塊內(nèi)存,理論上性能更高。嘿嘿,又可以改進(jìn)上面的方案了。
共享內(nèi)存有兩種機(jī)制:
-
POSIX共享內(nèi)存(shm_open()、shm_unlink()) -
System V共享內(nèi)存(shmget()、shmat()、shmdt())
其中,System V 共享內(nèi)存歷史悠久,一般的 UNIX 系統(tǒng)上都有這套機(jī)制;而 POSIX 共享內(nèi)存機(jī)制接口更加方便易用,一般是結(jié)合內(nèi)存映射 mmap 使用。
mmap 和 System V 共享內(nèi)存的主要區(qū)別在于:
- System V shm 是持久化的,除非被一個(gè)進(jìn)程明確的刪除,否則它始終存在于內(nèi)存里,直到系統(tǒng)關(guān)機(jī)。
-
mmap映射的內(nèi)存不是持久化的,如果進(jìn)程關(guān)閉,映射隨即失效,除非事先已經(jīng)映射到了一個(gè)文件上。 -
/dev/shm是 Linux 下 sysv 共享內(nèi)存的默認(rèn)掛載點(diǎn)。
POSIX 共享內(nèi)存是基于 tmpfs 來實(shí)現(xiàn)的。實(shí)際上,更進(jìn)一步,不僅 PSM(POSIX shared memory),而且 SSM(System V shared memory) 在內(nèi)核也是基于 tmpfs 實(shí)現(xiàn)的。
從這里可以看到 tmpfs 主要有兩個(gè)作用:
- 用于
System V共享內(nèi)存,還有匿名內(nèi)存映射;這部分由內(nèi)核管理,用戶不可見。 - 用于
POSIX共享內(nèi)存,由用戶負(fù)責(zé)mount,而且一般 mount 到/dev/shm,依賴于CONFIG_TMPFS。
雖然 System V 與 POSIX 共享內(nèi)存都是通過 tmpfs 實(shí)現(xiàn),但是受的限制卻不相同。也就是說 /proc/sys/kernel/shmmax 只會(huì)影響 System V 共享內(nèi)存,/dev/shm 只會(huì)影響 POSIX 共享內(nèi)存。實(shí)際上,System V 與 POSIX 共享內(nèi)存本來就是使用的兩個(gè)不同的 tmpfs 實(shí)例。
System V 共享內(nèi)存能夠使用的內(nèi)存空間只受 /proc/sys/kernel/shmmax 限制;而用戶通過掛載的 /dev/shm,默認(rèn)為物理內(nèi)存的 1/2。

概括一下:
-
POSIX共享內(nèi)存與System V共享內(nèi)存在內(nèi)核都是通過tmpfs實(shí)現(xiàn),但對(duì)應(yīng)兩個(gè)不同的tmpfs實(shí)例,相互獨(dú)立。 - 通過
/proc/sys/kernel/shmmax可以限制System V共享內(nèi)存的最大值,通過/dev/shm可以限制POSIX共享內(nèi)存的最大值。
5. Kubernetes 共享內(nèi)存
Kubernetes 創(chuàng)建的 Pod,其共享內(nèi)存默認(rèn) 64MB,且不可更改。
為什么是這個(gè)值呢?其實(shí),Kubernetes 本身是沒有設(shè)置共享內(nèi)存的大小的,64MB 其實(shí)是 Docker 默認(rèn)的共享內(nèi)存的大小。
Docker run 的時(shí)候,可以通過 --shm-size 來設(shè)置共享內(nèi)存的大?。?/p>
?? → docker run --rm centos:7 df -h |grep shm
shm 64M 0 64M 0% /dev/shm
?? → docker run --rm --shm-size 128M centos:7 df -h |grep shm
shm 128M 0 128M 0% /dev/shm
然而,Kubernetes 并沒有提供設(shè)置 shm 大小的途徑。在這個(gè) issue 里社區(qū)討論了很久是否要給 shm 增加一個(gè)參數(shù),但是最終并沒有形成結(jié)論,只是有一個(gè) workgroud 的辦法:將 Memory 類型的 emptyDir 掛載到 /dev/shm 來解決。
Kubernetes 提供了一種特殊的 emptyDir:可以將 emptyDir.medium 字段設(shè)置為 "Memory",以告訴 Kubernetes 使用 tmpfs(基于 RAM 的文件系統(tǒng))作為介質(zhì)。用戶可以將 Memory 介質(zhì)的 emptyDir 掛到任何目錄,然后將這個(gè)目錄當(dāng)作一個(gè)高性能的文件系統(tǒng)來使用,當(dāng)然也可以掛載到 /dev/shm,這樣就可以解決共享內(nèi)存不夠用的問題了。
使用 emptyDir 雖然可以解決問題,但也是有缺點(diǎn)的:
- 不能及時(shí)禁止用戶使用內(nèi)存。雖然過 1~2 分鐘
Kubelet會(huì)將Pod擠出,但是這個(gè)時(shí)間內(nèi),其實(shí)對(duì)Node還是有風(fēng)險(xiǎn)的。 - 影響 Kubernetes 調(diào)度,因?yàn)?
emptyDir并不涉及 Node 的Resources,這樣會(huì)造成 Pod “偷偷”使用了 Node 的內(nèi)存,但是調(diào)度器并不知曉。 - 用戶不能及時(shí)感知到內(nèi)存不可用。
由于共享內(nèi)存也會(huì)受 Cgroup 限制,我們只需要給 Pod 設(shè)置 Memory limits 就可以了。如果將 Pod 的 Memory limits 設(shè)置為共享內(nèi)存的大小,就會(huì)遇到一個(gè)問題:當(dāng)共享內(nèi)存被耗盡時(shí),任何命令都無法執(zhí)行,只能等超時(shí)后被 Kubelet 驅(qū)逐。
這個(gè)問題也很好解決,將共享內(nèi)存的大小設(shè)置為 Memory limits 的 50% 就好。綜合以上分析,最終設(shè)計(jì)如下:
- 將 Memory 介質(zhì)的
emptyDir掛載到/dev/shm/。 - 配置 Pod 的
Memory limits。 - 配置
emptyDir的sizeLimit為Memory limits的 50%。
6. 最終配置
根據(jù)上面的設(shè)計(jì),最終的配置如下。
Nginx 的配置改為:
listen unix:/dev/shm/hugo.sock;
Envoy 的配置改為:
- "@type": type.googleapis.com/envoy.api.v2.Cluster
name: hugo
connect_timeout: 15s
type: static
load_assignment:
cluster_name: hugo
endpoints:
- lb_endpoints:
- endpoint:
address:
pipe:
path: "/dev/shm/hugo.sock"
Kubernetes 的 manifest 改為:
spec:
...
template:
...
spec:
containers:
- name: envoy
resources:
limits:
memory: 256Mi
...
volumeMounts:
- mountPath: /dev/shm
name: hugo-socket
...
- name: hugo
resources:
limits:
memory: 256Mi
...
volumeMounts:
- mountPath: /dev/shm
name: hugo-socket
...
volumes:
...
- name: hugo-socket
emptyDir:
medium: Memory
sizeLimit: 128Mi
7. 參考資料
微信公眾號(hào)
掃一掃下面的二維碼關(guān)注微信公眾號(hào),在公眾號(hào)中回復(fù)?加群?即可加入我們的云原生交流群,和孫宏亮、張館長、陽明等大佬一起探討云原生技術(shù)