Envoy 基礎教程:使用 Unix Domain Socket(UDS) 與上游集羣通訊

Envoy Proxy 在大多數狀況下都是做爲 Sidecar 與應用部署在同一網絡環境中,每一個應用只須要與 Envoy(localhost)交互,不須要知道其餘服務的地址。然而這並非 Envoy 僅有的使用場景,它自己就是一個七層代理,經過模塊化結構實現了流量治理、信息監控等核心功能,好比流量治理功能就包括自動重連、熔斷、全侷限速、流量鏡像和異常檢測等多種高級功能,所以 Envoy 也經常被用於邊緣代理,好比 Istio 的 Ingress Gateway、基於 Envoy 實現的 Ingress Controller(ContourAmbassadorGloo 等)。html

個人博客也是部署在輕量級 Kubernetes 集羣上的(實際上是 k3s 啦),一開始使用 Contour 做爲 Ingress Controller,暴露集羣內的博客、評論等服務。但好景不長,因爲我在集羣內部署了各類奇奇怪怪的東西,有些個性化配置 Contour 沒法知足個人需求,畢竟你們都知道,每抽象一層就會丟失不少細節。換一個 Controller 保不齊之後還會遇到這種問題,索性就直接裸用 Envoy 做爲邊緣代理,大不了手擼 YAML 唄。linux

固然也不全是手擼,雖然沒有所謂的控制平面,但儀式感仍是要有的,我能夠基於文件來動態更新配置啊,具體的方法參考 Envoy 基礎教程:基於文件系統動態更新配置nginx

1. UDS 介紹

說了那麼多廢話,下面進入正題。爲了提升博客的性能,我選擇將博客與 Envoy 部署在同一個節點上,而且所有使用 HostNetwork 模式,Envoy 經過 localhost 與博客所在的 Pod(Nginx) 通訊。爲了進一步提升性能,我盯上了 Unix Domain Socket(UDS,Unix域套接字),它還有另外一個名字叫 IPC(inter-process communication,進程間通訊)。爲了理解 UDS,咱們先來創建一個簡單的模型。git

現實世界中兩我的進行信息交流的整個過程被稱做一次通訊(Communication),通訊的雙方被稱爲端點(Endpoint)。工具通信環境的不一樣,端點之間能夠選擇不一樣的工具進行通訊,距離近能夠直接對話,距離遠能夠選擇打電話、微信聊天。這些工具就被稱爲 Socketgithub

同理,在計算機中也有相似的概念:docker

  • Unix 中,一次通訊由兩個端點組成,例如 HTTP 服務端和 HTTP 客戶端。
  • 端點之間想要通訊,必須藉助某些工具,Unix 中端點之間使用 Socket 來進行通訊。

Socket 本來是爲網絡通訊而設計的,但後來在 Socket 的框架上發展出一種 IPC 機制,就是 UDS。使用 UDS 的好處顯而易見:不須要通過網絡協議棧,不須要打包拆包、計算校驗和、維護序號和應答等,只是將應用層數據從一個進程拷貝到另外一個進程。這是由於,IPC 機制本質上是可靠的通信,而網絡協議是爲不可靠的通信設計的centos

UDS 與網絡 Socket 最明顯的區別在於,網絡 Socket 地址是 IP 地址加端口號,而 UDS 的地址是一個 Socket 類型的文件在文件系統中的路徑,通常名字以 .sock 結尾。這個 Socket 文件能夠被系統進程引用,兩個進程能夠同時打開一個 UDS 進行通訊,並且這種通訊方式只會發生在系統內核裏,不會在網絡上進行傳播。下面就來看看如何讓 Envoy 經過 UDS 與上游集羣 Nginx 進行通訊吧,它們之間的通訊模型大概就是這個樣子:api

2. Nginx 監聽 UDS

首先須要修改 Nginx 的配置,讓其監聽在 UDS 上,至於 Socket 描述符文件的存儲位置,就隨你的意了。具體須要修改 listen 參數爲下面的形式:bash

listen      unix:/sock/hugo.sock;複製代碼

固然,若是想得到更快的通訊速度,能夠放在 /dev/shm 目錄下,這個目錄是所謂的 tmpfs,它是 RAM 能夠直接使用的區域,因此讀寫速度都會很快,下文會單獨說明。微信

3. Envoy-->UDS-->Nginx

Envoy 默認狀況下是使用 IP 地址和端口號和上游集羣通訊的,若是想使用 UDS 與上游集羣通訊,首先須要修改服務發現的類型,將 type 修改成 static

type: static複製代碼

同時還需將端點定義爲 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 可以訪問 NginxSocket 文件,Kubernetes 中能夠將同一個 emptyDir 掛載到兩個 Container 中來達到共享的目的,固然最大的前提是 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: {}複製代碼

如今你又能夠愉快地訪問個人博客了,查看 Envoy 的日誌,成功將請求經過 Socket 轉發給了上游集羣:

[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 的爬蟲也來湊熱鬧。

你可能會問我:你這裏的 Socket 爲何在 /dev/shm/ 目錄下啊?別急,還沒結束呢,先來補充一個背景知識。

4. Linux 共享內存機制

共享內存(shared memory),是 Linux 上一種用於進程間通訊(IPC)的機制。

進程間通訊可使用管道,Socket,信號,信號量,消息隊列等方式,但這些方式一般須要在用戶態、內核態之間拷貝,通常認爲會有 4 次拷貝;相比之下,共享內存將內存直接映射到用戶態空間,即多個進程訪問同一塊內存,理論上性能更高。嘿嘿,又能夠改進上面的方案了。

共享內存有兩種機制:

  • POSIX 共享內存(shm_open()、shm_unlink()
  • System V 共享內存(shmget()、shmat()、shmdt()

其中,System V 共享內存歷史悠久,通常的 UNIX 系統上都有這套機制;而 POSIX 共享內存機制接口更加方便易用,通常是結合內存映射 mmap 使用。

mmapSystem V 共享內存的主要區別在於:

  • System V shm 是持久化的,除非被一個進程明確的刪除,不然它始終存在於內存裏,直到系統關機。
  • mmap 映射的內存不是持久化的,若是進程關閉,映射隨即失效,除非事先已經映射到了一個文件上。
  • /dev/shm 是 Linux 下 sysv 共享內存的默認掛載點。

POSIX 共享內存是基於 tmpfs 來實現的。實際上,更進一步,不只 PSM(POSIX shared memory),並且 SSM(System V shared memory) 在內核也是基於 tmpfs 實現的。

從這裏能夠看到 tmpfs 主要有兩個做用:

  • 用於 System V 共享內存,還有匿名內存映射;這部分由內核管理,用戶不可見。
  • 用於 POSIX 共享內存,由用戶負責 mount,並且通常 mount 到 /dev/shm,依賴於 CONFIG_TMPFS

雖然 System V 與 POSIX 共享內存都是經過 tmpfs 實現,可是受的限制卻不相同。也就是說 /proc/sys/kernel/shmmax 只會影響 System V 共享內存,/dev/shm 只會影響 POSIX 共享內存。實際上,System VPOSIX 共享內存原本就是使用的兩個不一樣的 tmpfs 實例。

System V 共享內存可以使用的內存空間只受 /proc/sys/kernel/shmmax 限制;而用戶經過掛載的 /dev/shm,默認爲物理內存的 1/2

歸納一下:

  • POSIX 共享內存與 System V 共享內存在內核都是經過 tmpfs 實現,但對應兩個不一樣的 tmpfs 實例,相互獨立。
  • 經過 /proc/sys/kernel/shmmax 能夠限制 System V 共享內存的最大值,經過 /dev/shm 能夠限制 POSIX 共享內存的最大值。

5. Kubernetes 共享內存

Kubernetes 建立的 Pod,其共享內存默認 64MB,且不可更改。

爲何是這個值呢?其實,Kubernetes 自己是沒有設置共享內存的大小的,64MB 實際上是 Docker 默認的共享內存的大小。

Docker run 的時候,能夠經過 --shm-size 來設置共享內存的大小:

🐳 → 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 並無提供設置 shm 大小的途徑。在這個 issue 里社區討論了好久是否要給 shm 增長一個參數,可是最終並無造成結論,只是有一個 workgroud 的辦法:將 Memory 類型的 emptyDir 掛載到 /dev/shm 來解決。

Kubernetes 提供了一種特殊的 emptyDir:能夠將 emptyDir.medium 字段設置爲 "Memory",以告訴 Kubernetes 使用 tmpfs(基於 RAM 的文件系統)做爲介質。用戶能夠將 Memory 介質的 emptyDir 掛到任何目錄,而後將這個目錄看成一個高性能的文件系統來使用,固然也能夠掛載到 /dev/shm,這樣就能夠解決共享內存不夠用的問題了。

使用 emptyDir 雖然能夠解決問題,但也是有缺點的:

  • 不能及時禁止用戶使用內存。雖然過 1~2 分鐘 Kubelet 會將 Pod 擠出,可是這個時間內,其實對 Node 仍是有風險的。
  • 影響 Kubernetes 調度,由於 emptyDir 並不涉及 Node 的 Resources,這樣會形成 Pod 「偷偷」使用了 Node 的內存,可是調度器並不知曉。
  • 用戶不能及時感知到內存不可用。

因爲共享內存也會受 Cgroup 限制,咱們只須要給 Pod 設置 Memory limits 就能夠了。若是將 Pod 的 Memory limits 設置爲共享內存的大小,就會遇到一個問題:當共享內存被耗盡時,任何命令都沒法執行,只能等超時後被 Kubelet 驅逐。

這個問題也很好解決,將共享內存的大小設置爲 Memory limits50% 就好。綜合以上分析,最終設計以下:

  1. 將 Memory 介質的 emptyDir 掛載到 /dev/shm/
  2. 配置 Pod 的 Memory limits
  3. 配置 emptyDirsizeLimitMemory limits 的 50%。

6. 最終配置

根據上面的設計,最終的配置以下。

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. 參考資料

微信公衆號

掃一掃下面的二維碼關注微信公衆號,在公衆號中回覆◉加羣◉便可加入咱們的雲原生交流羣,和孫宏亮、張館長、陽明等大佬一塊兒探討雲原生技術

相關文章
相關標籤/搜索