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. 参考资料

微信公众号

扫一扫下面的二维码关注微信公众号,在公众号中回复◉加群◉便可加入咱们的云原生交流群,和孙宏亮、张馆长、阳明等大佬一块儿探讨云原生技术

相关文章
相关标签/搜索