blog icon indicating copy to clipboard operation
blog copied to clipboard

关于 Service Mesh(三)--- 初战 Envoy

Open imjoey opened this issue 7 years ago • 2 comments
trafficstars

写在前面

本文主要以 Envoy 官方 Examples 来介绍如何部署Envoy,并详细解释各配置项。

Envoy 的编译和安装

官方提供了两种编译方式的指导:

  • 使用 Envoy 官方提供的具有完整Envoy 编译环境的 Container,详见指导文档
  • 完全自行编译,详见指导文档

同时官方也提供了已经编译好的 docker 镜像。

Envoy 示例

Envoy 官方有若干使用示例,本文就主要其中的 Front ProxyZipkin TracinggRPC Bridge 来解释 Envoy 各配置项的含义,为后续为自己的微服务部署 Envoy 做好知识储备。

Envoy 示例使用 local network 作为容器的网络,无法跨主机,后续我将写一篇使用 swarm network 的文章,介绍可以用于生产环境的 Envoy 部署方式。

Envoy 实例使用 docker compose 作为容器编排引擎,本文不涉及 docker compose 相关知识。

Envoy 官方示例中配置文件使用的 v1 版本,后续我会补充其对应的 v2 版本配置文件

Envoy 的配置可使用 Data Plane API 访问,也为 Istio (Control Plane)提供支持。

Front Proxy

此示例是将 Envoy 分别作为 Front Proxy(类似 API Gateway)和 Sidecar 模式部署。

部署图

部署架构图如下: Envoy-Front-Proxy

从此部署图可以看出:

  • 一共有3个 container,名称分别是:front-proxy、service1、service2;
  • front-proxy 中部署了 Envoy,配置文件使用 front-envoy.json,对外监听 HTTP 80端口,同时会将入向流量转发至 service1 和 service2;
  • serviceX 中部署了一个 Envoy 作为 Sidecar,同时部署了一个 Application。Envoy对外监听 HTTP 80端口,并将入向流量转发至application 的8080端口;

这种部署模式非常常用,Envoy 既作为微服务的唯一流量边界(API Gateway),又作为 Application 的 Sidecar 接管所有流量,从而组成 Service Mesh。

Front-Proxy Container 配置

front-proxy 容器的 Dockerfile 如下:(重要说明见注释)

FROM envoyproxy/envoy:latest          # 这里使用 Envoy 官方提供的预编译好的 docker 镜像

RUN apt-get update && apt-get -q install -y curl
# /etc/front-envoy.json 就是启动 envoy 时传入的配置文件,--service-cluser 表示集群名称
CMD /usr/local/bin/envoy -c /etc/front-envoy.json --service-cluster front-proxy

下面是重头戏,Envoy 的配置文件--- front-proxy.json:(各配置项说明见注释)

{
  // 监听器对象,一个 Envoy 进程(部署实例)可包含多个监听器,目前仅支持 TCP 类型的监听器。
  // 每个 listener 都可配置若干 filter。listener 配置可从远端的 LDS 服务获取。
  // https://www.envoyproxy.io/docs/envoy/v1.5.0/api-v1/listeners/listeners
  "listeners": [
    {
      // "name" 可选项,listener 名称,如果不设置将自动 UUID,如果使用 LDS 服务,则必须设置 name,
      // 其他可选项还包含:ssl_context/bind_to_port/use_proxy_proto/
      //    use_original_dst/per_connection_buffer_limit_bytes/drain_type 等
      
      // 必选项,目前仅支持 TCP
      "address": "tcp://0.0.0.0:80",

      // 必选项,array 类型。如果数组为空,则默认关闭网络连接。
      // https://www.envoyproxy.io/docs/envoy/v1.5.0/api-v1/listeners/listeners#config-listener-filters
      "filters": [
        {
          // filter名称,必选项,目前仅支持 http_connection_manager/client_ssl_auth/
          //    echo/mongo_proxy/ratelimit/redis_proxy/tcp_proxy 几种类型
          // https://www.envoyproxy.io/docs/envoy/v1.5.0/configuration/network_filters/network_filters.html#config-network-filters
          "name": "http_connection_manager",

          // filter 详细配置,必选项
          "config": {
            // http connection manager 使用的 codec类型,必选项,
            // 支持 http1/http2/auto,大多数情况下使用 auto 即可。
            "codec_type": "auto",
            // 统计使用的前缀,必选项,后面可紧跟较多的统计数据项
            "stat_prefix": "ingress_http",
            // https://www.envoyproxy.io/docs/envoy/v1.5.0/api-v1/route_config/route_config.html#config-http-conn-man-route-table
            // 路由配置
            "route_config": {
              // 路由表,必选项
              // https://www.envoyproxy.io/docs/envoy/v1.5.0/api-v1/route_config/vhost.html#config-http-conn-man-route-table-vhost
              "virtual_hosts": [
                {
                  // 节点名称,必选项
                  "name": "backend",
                  // 匹配的域名,必选项,支持正则,
                  // "*" 表示匹配所有域名,只能有一个virtual_host 的 domains 有"*"
                  // domains 中的项在多个 virtual_host 之间必须唯一,否则配置会出错
                  "domains": ["*"],
                  // 路由项,必选项,列表类型,按照顺序匹配
                  "routes": [
                    {
                      // 指定 route 的超时时间,单位是 s,不指定时默认15s
                      "timeout_ms": 0,
                      // 路由前缀,prefix/regrex/path 三者必须指定一个
                      "prefix": "/service/1",
                      // 指定 forwardto 的 upstream 集群
                      // 如果此 route 不是redirect 类型,cluster/cluster_header/weighted_clusters必须指定一个
                      // weighted_clusters 可用于 traffic splitting(分流)
                      "cluster": "service1"
                    },
                    {
                      "timeout_ms": 0,
                      "prefix": "/service/2",
                      "cluster": "service2"
                    }

                  ]
                }
              ]
            },
            // http connection manager 专用的 filter 列表
            // https://www.envoyproxy.io/docs/envoy/v1.5.0/api-v1/http_filters/router_filter.html#config-http-filters-router-v1
            // route filter 实现 HTTP forwarding,几乎所有的 HTTP Proxy 部署方式都会用到
            "filters": [
              {
                "name": "router",
                "config": {}
              }
            ]
          }
        }
      ]
    }
  ],
  // 指定 admin 接口,可以通过 HTTP 8001 查看 Envoy 信息
  "admin": {
    // access log 文件路径,如果不需要可指定 /dev/null
    "access_log_path": "/dev/null",
    // administration server 的监听地址
    "address": "tcp://0.0.0.0:8001"
  },
  // 内含所有配置了的 upstream 集群,可使用 CDS API 获取配置
  "cluster_manager": {
    // upstream 集群列表,必选项
    "clusters": [
      {
        // 集群名称,必选项
        "name": "service1",
        // 连接超时时间,必选项,单位:milliseconds
        "connect_timeout_ms": 250,
        // 集群内所有主机的发现方式,支持
        //    static:静态为每个 host 明确配置 IP 地址,不能配置域名
        //    strict_dns: 根据 dns 查询到所有主机,然后做 lb
        //    logical_dns: 类似 strict_dns,但只返回第一个主机的 IP,防止连接池被频繁重置
        //    orignal_dst: 适用于 redirect 场景
        //    sds: 使用 REST based API 访问发现集群内主机
        "type": "strict_dns",
        // 负载均衡策略,必选项,支持round_robin/least_request/ring_hash/random/
        //      original_dst_lb(只能配套 "type": "orignal_dst"使用)
        "lb_type": "round_robin",
        // upstream 集群支持的特征,目前仅支持 http2
        "features": "http2",
        // 集群内的主机列表
        "hosts": [
          {
            "url": "tcp://service1:80"
          }
        ]
      },
      {
        "name": "service2",
        "connect_timeout_ms": 250,
        "type": "strict_dns",
        "lb_type": "round_robin",
        "features": "http2",
        "hosts": [
          {
            "url": "tcp://service2:80"
          }
        ]
      }
    ]
  }
}


: 对于 client 和 upstream cluster 均支持 http2 时,一定要将 cluster_manager.clusters.features属性显式设置为:http2,否则会报错:WARNING: RPC failed: Status{code=UNAVAILABLE, description=HTTP status code 503 invalid content-type: text/plain headers: Metadata(:status=503,content-length=57,content-type=text/plain,date=Thu, 04 Jan 2018 07:27:28 GMT,server=envoy)

上面配置文件的核心是:监听器监听80端口,然后将 /service/1 转发到 service1 集群的80端口,将 /service/2 转发到 service2 集群的80端口,具体如何实现的可参见上面的注释。

如果此时运行 docker-compose scale service1=3将 service1 的 container 实例扩充到3个,此时访问/service/1可以看到,Envoy 自动upstream 集群做了负载均衡,这其中很重要的原因就是使用了 service1 而不是 IP 地址,作为 route/cluster 的定义。

serviceX Container 配置

还是首先看一下 serviceX 的 Dockerfile 如下:(重要说明见注释)

# 使用 Envoy 官方提供的二进制 docker 镜像
FROM envoyproxy/envoy:latest                

RUN apt-get update && apt-get -q install -y \
    curl \
    software-properties-common \
    python-software-properties
RUN add-apt-repository ppa:deadsnakes/ppa
RUN apt-get update && apt-get -q install -y \
    python3 \
    python3-pip
RUN python3 --version && pip3 --version
RUN pip3 install -q Flask==0.11.1
RUN mkdir /code
ADD ./service.py /code
ADD ./start_service.sh /usr/local/bin/start_service.sh
RUN chmod u+x /usr/local/bin/start_service.sh

# 启动 Python flask 8080 Web 服务,同时启动给 Envoy 传递 service-envoy.json 配置文件
ENTRYPOINT /usr/local/bin/start_service.sh

那么下面咱们看一下 service-envoy.json 这个配置文件,与上面 front-proxy.json 不同的配置项会用注释解释说明。

{
  "listeners": [
    {
      // 监听80端口
      "address": "tcp://0.0.0.0:80",
      "filters": [
        {
          "name": "http_connection_manager",
          "config": {
            "codec_type": "auto",
            "stat_prefix": "ingress_http",
            "route_config": {
              "virtual_hosts": [
                {
                  "name": "service",
                  "domains": ["*"],
                  "routes": [
                    {
                      "timeout_ms": 0,
                      // 路由可匹配 /service/1 或 /service/2
                      "prefix": "/service",
                      "cluster": "local_service"
                    }
                  ]
                }
              ]
            },
            "filters": [
              {
                "name": "router",
                "config": {}
              }
            ]
          }
        }
      ]
    }
  ],
  "admin": {
    "access_log_path": "/dev/null",
    "address": "tcp://0.0.0.0:8001"
  },
  "cluster_manager": {
    "clusters": [
      {
        "name": "local_service",
        "connect_timeout_ms": 250,
        "type": "strict_dns",
        "lb_type": "round_robin",
        "hosts": [
          {
            "url": "tcp://127.0.0.1:8080"
          }
        ]
      }
    ]
  }
}

Zipkin Tracing

此示例是在上面 Front-Proxy 例子的基础上, 添加了 Zipkin 分布式调用追踪,展示如何在 Envoy 中集成 Zipkin。

部署图

部署图如下图所示:

四个容器:front-proxy,service1,service2,zipkin

Front-Proxy Envoy 配置

front-proxy 容器中 Envoy 其实是一个反向代理服务器,其配置如下。

{
  "listeners": [
    {
      "address": "tcp://0.0.0.0:80",
      "filters": [
        {
          "name": "http_connection_manager",
          "config": {
            // 如果设为 true,则 x-request-id http header 不存在时自动创建
            // Envoy 默认为为所有的外部/内部请求生成 x-request-id,所以需要
            // 应用层代码转发 x-request-id 保证 trace 的可追溯性
            "generate_request_id": true,
            // 如果设置,则开启 tracing 功能
            "tracing": {
              // 必选项,支持 egress/ingress,span的名称源于此值
              "operation_name": "egress"
            }, 
            "codec_type": "auto",
            "stat_prefix": "ingress_http",
            "route_config": {
              "virtual_hosts": [
                {
                  "name": "backend",
                  "domains": ["*"],
                  "routes": [
                    // 这个地方需要注意,front-proxy 只转发到 service1
                    {
                      "prefix": "/",
                      "cluster": "service1",
                      // 可为 Match 的 request 附加额外信息
                      "decorator": {
                        // 为 Match 的 request 附加此信息
                        // 当开启 tracing 时,此名称会用作此 request 的 span 的名称
                        // NOTE: For ingress (inbound) requests, or egress (outbound) responses,
                        // this value may be overridden by the x-envoy-decorator-operation header.
                        "operation": "checkAvailability"
                      }
                    }
                  ]
                }
              ]
            },
            "filters": [
              {
                "name": "router",
                "config": {}
              }
            ]
          }
        }
      ]
    }
  ],
  // 全局的 tracing 设置,目前仅支持 HTTP 类型的 tracer,
  // 未来会增加其他类型配置
  "tracing": {
    // HTTP 类型 tracer 的配置
    "http": {
      // driver 设置,下面是 Zipkin 类型的 driver,还支持 LightStep
      "driver": {
        "type": "zipkin", 
        "config": {
          // Cluster manager 中 zipkin 所在 cluster 的名称
          "collector_cluster": "zipkin",
          // Zipkin 提供的 span 提交地址
          // Zipkin 标准安装时地址是"/api/v1/spans",也是此项的默认值
          "collector_endpoint": "/api/v1/spans"
        }
      }
    }
  }, 
  "admin": {
    "access_log_path": "/dev/null",
    "address": "tcp://0.0.0.0:8001"
  },
  "cluster_manager": {
    "clusters": [
      {
        "name": "service1",
        "connect_timeout_ms": 250,
        "type": "strict_dns",
        "lb_type": "round_robin",
        "features": "http2",
        "hosts": [
          {
            "url": "tcp://service1:80"
          }
        ]
      },
      {
        // 在 Cluster manager 中定义 zipkin 所在的 cluster 名称
        // 用于以上的 traceing 中的 Zipkin 配置
        "name": "zipkin", 
        "connect_timeout_ms": 1000, 
        "type": "strict_dns", 
        "lb_type": "round_robin", 
        "hosts": [
          {
            "url": "tcp://zipkin:9411"
          }
        ]
      } 
    ]
  }
}

Service1 Envoy 配置

service1 容器中 Envoy 有两个监听器:

  • 监听 80 端口,route 规则是 "/",request 装饰为:"operation"
  • 监听 9000 端口,route 规则是 "/trace/2",request 装饰为:"checkStock"

因为与上面各 Envoy 配置无新增配置项,所以这里就不贴配置了,详见这里

Service2 的 Envoy 配置无新增配置项,所以这里就不贴配置了,详见这里

Service1 的 Application

上面提到,tracing 是 Envoy 中少有的需要 Application 配合的几个功能之一,所以这里就看一下 service1 是如何配合 Envoy 完成 tracing 功能的。

先贴 service1 的代码:

from flask import Flask
from flask import request
import socket
import os
import sys
import requests

app = Flask(__name__)

########### 这里非常重要,当tracing 后端接的是 Zipkin 时,Zipkin 
########### 需要这些 header 来更精确的tracing
########### 详见 https://www.envoyproxy.io/docs/envoy/v1.5.0/intro/arch_overview/tracing.html#arch-overview-tracing
TRACE_HEADERS_TO_PROPAGATE = [
    'X-Ot-Span-Context',
    'X-Request-Id',
    'X-B3-TraceId',
    'X-B3-SpanId',
    'X-B3-ParentSpanId',
    'X-B3-Sampled',
    'X-B3-Flags'
]

@app.route('/service/<service_number>')
def hello(service_number):
    return ('Hello from behind Envoy (service {})! hostname: {} resolved'
            'hostname: {}\n'.format(os.environ['SERVICE_NAME'], 
                                    socket.gethostname(),
                                    socket.gethostbyname(socket.gethostname())))

@app.route('/trace/<service_number>')
def trace(service_number):
    headers = {}
    # call service 2 from service 1
    if int(os.environ['SERVICE_NAME']) == 1 :
        ################ service1 在接收到/trace/1 请求后,会继续调用 service2 的 /trace/2 接口,
        ################ 调用时,将传入的 tracing 相关 header 都添加到新的请求中,实现 span 之间的追溯
        for header in TRACE_HEADERS_TO_PROPAGATE:
            if header in request.headers:
                headers[header] = request.headers[header]
        ret = requests.get("http://localhost:9000/trace/2", headers=headers)
    return ('Hello from behind Envoy (service {})! hostname: {} resolved'
            'hostname: {}\n'.format(os.environ['SERVICE_NAME'], 
                                    socket.gethostname(),
                                    socket.gethostbyname(socket.gethostname())))

if __name__ == "__main__":
    app.run(host='127.0.0.1', port=8080, debug=True)

gRPC Bridge

此示例是在 Envoy 中使用 gPRC Bridge filter,实现 HTTP/1.1 Client 访问 HTTP/2 Server。

部署图

从如下部署图可以看出:

  • 一共有两个 Container,第一个是 Python Client,第二个是 Go gRPC Server;
  • Python Client Container 中的 Envoy 实例监听9001端口,将 Python Client 的 HTTP/1.1 请求转换为 HTTP/2;
  • Go Server Container 中的 Envoy 实例监听9211端口,将 HTTP/2 gRPC 请求转发至 Go Server;
+--------------------+             +-------------------+
|                    |             |                   |
|  +--------------+  |             | +---------------+ |
|  | Python Client|  |             | |Go gRPC Server | |
|  |              |  |             | |               | |
|  +-----+--------+  |             | +------+--------+ |
|        |           |             |        ^          |
|        | 9001      |             |        |8081      |
|        v           |             |        |          |
|                    |             |        |          |
|  +--------------+  |             | +---------------+ |
|  |    Envoy     |  |             | |    Envoy      | |
|  |              |  |             | |               | |
|  | lisenter:    |  |   9211      | | lisenter:     | |
|  |    tcp/9001  +--------------> | |    tcp/9221   | |
|  +--------------+  |             | +---------------+ |
+--------------------+             +-------------------+

Python Client Container 中的 Envoy 配置

这里直接看 Envoy 的配置文件,新出现的配置项将在注释中说明。

{
  "listeners": [
    {
      "address": "tcp://127.0.0.1:9001",
      "filters": [
        {
          "name": "http_connection_manager",
          "config": {
            "codec_type": "auto",
            // 可选项,如果设为 true,则 http connection manager 则会将 http header 
            //  中的 user_agent 设置为 --service-cluster 的值(命令行运行 Envoy 时)
            "add_user_agent": true,
            // 单位:秒。当 http 连接在一段时间内无任何请求时
            //      HTTP/1.1 -- 将被关闭。
            //      HTTP/2 -- 将优先“a drain sequence will occur”,其次才会被关闭
            "idle_timeout_s": 840,
            // HTTP access log 的记录配置
            "access_log": [
              {
                "path": "/var/log/envoy/egress_http.log"
              }
            ],
            "stat_prefix": "egress_http",
            // 如果是 true,则使用真正的 client remote address
            // 如果是 false,则使用x-forward-for http header
            "use_remote_address": true,
            "route_config": {
              "virtual_hosts": [
                {
                  "name": "grpc",
                  "domains": [
                    "grpc"
                  ],
                  "routes": [
                    {
                      "prefix": "/",
                      "cluster": "grpc"
                    }
                  ]
                }
              ]
            },
            "filters": [
              {
                // HTTP filters 中的一种,支持 HTTP/1.1 到 HTTP/2 的转换
                // 额外要求 client 端发送的 HTTP/1.1 请求必须满足:
                //  :method: POST
                //  :path: <gRPC-METHOD-NAME>
                //  :content-type: application/grpc
                "name": "grpc_http1_bridge",
                "config": {}
              },
              {
                "name": "router",
                "config": {}
              }
            ]
          }
        }
      ]
    }
  ],
  "admin": {
    "access_log_path": "/var/log/envoy/admin_access.log",
    "address": "tcp://0.0.0.0:9901"
  },
  "cluster_manager": {
    "clusters": [
      {
        "name": "grpc",
        "type": "logical_dns",
        "lb_type": "round_robin",
        "connect_timeout_ms": 250,
        "features": "http2",
        "hosts": [
          {
            "url": "tcp://grpc:9211"
          }
        ]
      }
    ]
  }
}

Go gRPC Server Container 中的 Envoy 配置

还是直接看 Envoy 配置和注释。

{
  "listeners": [
    {
      "address": "tcp://0.0.0.0:9211",
      "filters": [
        {
          "name": "http_connection_manager",
          "config": {
            "codec_type": "auto",
            "stat_prefix": "ingress_http",
            "route_config": {
              "virtual_hosts": [
                {
                  "name": "local_service",
                  "domains": [
                    "*"
                  ],
                  "routes": [
                    {
                      "timeout_ms": 0,
                      "prefix": "/",
                      // 指定必须匹配的 http header 项,
                      // 如果不指定 value,则必须 http header 中必须存在
                      "headers": [
                        {"name": "content-type", "value": "application/grpc"}
                      ],
                      "cluster": "local_service_grpc"
                    }
                  ]
                }
              ]
            },
            "filters": [
              {
                "name": "router",
                "config": {}
              }
            ]
          }
        }
      ]
    }
  ],
  "admin": {
    "access_log_path": "/var/log/envoy/admin_access.log",
    "address": "tcp://0.0.0.0:9901"
  },
  "cluster_manager": {
    "clusters": [
      {
        "name": "local_service_grpc",
        "connect_timeout_ms": 250,
        "type": "static",
        "lb_type": "round_robin",
        "features": "http2",
        "hosts": [
          {
            "url": "tcp://127.0.0.1:8081"
          }
        ]
      }
    ]
  }
}

总结

本文详细介绍了 Envoy 的三个示例中的部署方式以及对应的配置文件,大家可根据这三个示例去构造自己需要的 Envoy 配置。接下来我还会去研究 Envoy 的更多功能和配置,比如如何支持灰度发布、A/B 测试、限流、熔断等。

imjoey avatar Dec 28 '17 01:12 imjoey

thanks

lwhile avatar Apr 29 '18 07:04 lwhile

@lwhile 😄

imjoey avatar Apr 29 '18 16:04 imjoey