跳转至

基于 API 的动态端点发现

当在 Envoy 配置中定义了上游集群后,Envoy 需要知道如何解析集群成员,这就是服务发现。端点发现服务(EDS)是 Envoy 基于 gRPC 或者用来获取集群成员的 REST-JSON API 服务的 xDS 管理服务。在本节我们将学习如何使用 REST-JSOn API 来配置端点的自动发现。

1. 介绍

在前面的章节中,我们使用文件来定义了静态和动态配置,在这里我们将介绍另外一种方式来进行动态配置:API 动态配置。

端点发现服务(EDS)是 Envoy 基于 gRPC 或者用来获取集群成员的 REST-JSON API 服务的 xDS 管理服务,集群成员在 Envoy 术语中成为端点,对于每个集群,Envoy 都从发现服务中获取端点。其中 EDS 就是最常用的服务发现机制,因为下面几个原因:

  • Envoy 对每个上游主机都有一定的了解(相对于通过 DNS 解析的负载均衡器进行路由),可以做出更加智能的负载均衡策略。
  • 发现 API 返回的每个主机的一些属性会将主机的负载均衡权重、金丝雀状态、区域等等告知 Envoy,这个额外的属性在负载均衡、统计数据收集等会被 Envoy 网格在全局中使用到
  • Envoy 项目在 JavaGolang 中都提供了 EDS 和其他服务发现的 gRPC 实现参考

接下来我们将更改配置来使用 EDS,从而允许基于来自 REST-JSON API 服务的数据进行动态添加节点。

2. EDS 配置

下面是提供的一个 Envoy 配置的初始配置 envoy.yaml,文件内容如下所示:

admin:
  access_log_path: /dev/null
  address:
    socket_address:
      address: 127.0.0.1
      port_value: 9000

node:
  cluster: mycluster
  id: test-id

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 10000 }
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        config:
          stat_prefix: ingress_http
          codec_type: AUTO
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route: { cluster: targetCluster }
          http_filters:
          - name: envoy.router

接下来需要添加一个 EDS 类型的集群配置,并在 eds_config 中配置使用 REST API:

clusters:
- name: targetCluster
  type: EDS
  connect_timeout: 0.25s
  eds_cluster_config:
    service_name: myservice
    eds_config:
      api_config_source:
        api_type: REST 
        cluster_names: [eds_cluster]
        refresh_delay: 5s

然后需要定义 eds_cluster 的解析方式,这里我们可以使用静态配置:

- name: eds_cluster
  type: STATIC
  connect_timeout: 0.25s
  hosts: [{ socket_address: { address: 172.17.0.4, port_value: 8080 }}]

然后同样启动一个 Envoy 代理实例来进行测试:

$ docker run --name=api-eds -d \
    -p 9901:9901 \
    -p 80:10000 \
    -v $(pwd)/manifests:/etc/envoy \
    envoyproxy/envoy:latest

然后启动一个如下所示的上游端点服务:

$ docker run -p 8081:8081 -d -e EDS_SERVER_PORT='8081' cnych/docker-http-server:v4

启动完成后我们可以使用如下命令来测试上游的端点服务:

$ curl http://localhost:8081 -i
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 36
Server: Werkzeug/0.15.4 Python/2.7.16
Date: Tue, 14 Apr 2020 06:32:56 GMT

355d92db-9295-4a22-8b2c-fc0e5956ecf6

现在我们启动了 Envoy 代理和上游的服务集群,但是由于我们这里启动的服务并不是 eds_cluster 中配置的服务,所以还没有连接它们。这个时候我们去查看 Envoy 代理得日志,可以看到如下所示的一些错误:

$ docker logs -f api-eds
[2020-04-14 06:50:07.334][1][warning][config] [source/common/config/http_subscription_impl.cc:110] REST update for /v2/discovery:endpoints failed
......

3. 启动 EDS

为了让 Envoy 获取端点服务,我们需要启动 eds_cluster,我们这里将使用 python 实现的一个示例 eds_server

使用如下所示的命令来启动 eds_server 服务:

$ docker run -p 8080:8080 -d cnych/eds_server

服务启动后,可以在服务日志中查看到如下所示的日志信息,表明一个 Envoy 发现请求成功:

* Serving Flask app "main" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 185-412-562
172.17.0.2 - - [14/Apr/2020 07:12:00] "POST /v2/discovery:endpoints HTTP/1.1" 200 -
 Inbound v2 request for discovery.  POST payload: {u'node': {u'user_agent_name': u'envoy', u'cluster': u'mycluster', u'extensions': [{......}], u'user_agent_build_version': {u'version': {u'minor_number': 14, u'major_number': 1, u'patch': 1}, u'metadata': {u'ssl.version': u'BoringSSL', u'build.type': u'RELEASE', u'revision.status': u'Clean', u'revision.sha': u'3504d40f752eb5c20bc2883053547717bcb92fd8'}}, u'build_version': u'3504d40f752eb5c20bc2883053547717bcb92fd8/1.14.1/Clean/RELEASE/BoringSSL', u'id': u'test-id'}, u'type_url': u'type.googleapis.com/envoy.api.v2.ClusterLoadAssignment', u'resource_names': [u'myservice'], u'version_info': u'v1'}
172.17.0.2 - - [14/Apr/2020 07:12:08] "POST /v2/discovery:endpoints HTTP/1.1" 200 -

现在我们就可以将上游的服务配置添加到 EDS 服务中去了,这样可以让 Envoy 来自动发现上游服务。

我们在 Envoy 配置中将服务定义为了 myservice,所以我们需要针对该服务注册一个端点:

$ curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{
  "hosts": [
    {
      "ip_address": "172.17.0.3",
      "port": 8081,
      "tags": {
        "az": "cn-beijing-a",
        "canary": false,
        "load_balancing_weight": 50
      }
    }
  ]
}' http://localhost:8080/edsservice/myservice

由于我们已经启动了上面注册的上游服务,所以现在我们可以通过 Envoy 代理访问到它了:

$ curl -i http://localhost
HTTP/1.1 200 OK
content-type: text/html; charset=utf-8
content-length: 36
server: envoy
date: Tue, 14 Apr 2020 07:33:04 GMT
x-envoy-upstream-service-time: 4

355d92db-9295-4a22-8b2c-fc0e5956ecf6

接下来我们在上游集群中运行更多的节点,并调用 API 来进行动态注册,使用如下所示的命令来向上游集群再添加4个节点:

for i in 8082 8083 8084 8085
  do
    docker run -d -e EDS_SERVER_PORT=$i cnych/docker-http-server:v4;
    sleep .5
done

然后将上面的4个节点注册到 EDS 服务上面去,同样使用如下所示的 API 接口调用:

$ curl -X PUT --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{
    "hosts": [
        {
        "ip_address": "172.17.0.3",
        "port": 8081,
        "tags": {
            "az": "cn-beijing-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        },
        {
        "ip_address": "172.17.0.5",
        "port": 8082,
        "tags": {
            "az": "cn-beijing-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        },
        {
        "ip_address": "172.17.0.6",
        "port": 8083,
        "tags": {
            "az": "cn-beijing-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        },
        {
        "ip_address": "172.17.0.7",
        "port": 8084,
        "tags": {
            "az": "cn-beijing-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        },
        {
        "ip_address": "172.17.0.8",
        "port": 8085,
        "tags": {
            "az": "cn-beijing-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        }
    ]
    }' http://localhost:8080/edsservice/myservice

注册成功后,我们可以通过如下所示的命令来验证网络请求是否与注册的节点之间是均衡的:

$ while true; do curl http://localhost; sleep .5; printf '\n'; done
d671262d-39b5-4150-9e25-94fb4f733959
dd1519ef-e03a-4708-bcd1-71890d38e40c
b0c218f0-99f4-43e4-87fc-8989d49fccec
355d92db-9295-4a22-8b2c-fc0e5956ecf6
d671262d-39b5-4150-9e25-94fb4f733959
34690963-0887-4d36-8776-c35cf37fa901
......

根据上面的输出结果可以看到每次请求的服务是不同的响应,我们一共注册了5个端点服务。

现在我们来通过 API 删除 EDS 服务上面注册的主机来测试下,执行如下所示的命令清空 hosts

$ curl -X PUT --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{
  "hosts": []
}' http://localhost:8080/edsservice/myservice

现在如果我们尝试向 Envoy 发送请求,我们将会看到如下所示的不健康的日志信息:

$ curl -v http://localhost
* Rebuilt URL to: http://localhost/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 80 (#0)
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 503 Service Unavailable
< content-length: 19
< content-type: text/plain
< date: Tue, 14 Apr 2020 07:50:06 GMT
< server: envoy
<
* Connection #0 to host localhost left intact
no healthy upstream

这是因为我们将端点服务的节点清空了,所以没有服务来接收 Envoy 的代理请求了。

接下来我们再来测试下 Envoy 和 EDS 服务器的连接断掉了会是一种什么样的情况。首先还是将前面的上游服务节点恢复:

$ curl -X PUT --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{
    "hosts": [
        {
        "ip_address": "172.17.0.3",
        "port": 8081,
        "tags": {
            "az": "cn-beijing-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        },
        {
        "ip_address": "172.17.0.5",
        "port": 8082,
        "tags": {
            "az": "cn-beijing-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        },
        {
        "ip_address": "172.17.0.6",
        "port": 8083,
        "tags": {
            "az": "cn-beijing-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        },
        {
        "ip_address": "172.17.0.7",
        "port": 8084,
        "tags": {
            "az": "cn-beijing-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        },
        {
        "ip_address": "172.17.0.8",
        "port": 8085,
        "tags": {
            "az": "cn-beijing-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        }
    ]
    }' http://localhost:8080/edsservice/myservice

然后同样用如下命令来验证节点是否正确响应:

$ while true; do curl http://localhost; sleep .5; printf '\n'; done

然后我们使用如下所示的命令来停止并删除 EDS 服务的容器:

$ docker ps -a | awk '{ print $1,$2 }' | grep cnych/eds_server  | awk '{print $1 }' | xargs -I {} docker stop {}
$ docker ps -a | awk '{ print $1,$2 }' | grep cnych/eds_server  | awk '{print $1 }' | xargs -I {} docker rm {}

这个时候我们可以看到上面的验证命令还是正常的收到相应,这就证明即使 Envoy 和 EDS 服务器断开了链接,也不会影响已经发现的集群节点。