美文网首页
Envoy 学习笔记之创建 EDS 动态配置

Envoy 学习笔记之创建 EDS 动态配置

作者: 简单一点点 | 来源:发表于2020-01-10 22:14 被阅读0次

    本文档第一部分介绍一下 Envoy v2 API,第二部分给出了一个 Java 编写的简陋 EDS Server 示例。

    Envoy v2 API

    Envoy 的 API 有 v1 和 v2 两个版本,目前主流版本是 v2。它具有以下特点:

    • 通过 gRPC 流式传输对 xDS API 的更新,这减少了资源的需求并且可以降低更新延迟。
    • 一种新的 REST-JSON API。其中 JSON/YAML 格式是通过 proto3 规范的 JSON 映射机制派生出来的。
    • 通过文件系统、REST-JSON 或 gRPC 端点传递更新。
    • 通过扩展端点分配 API 进行高级负载均衡,并向管理服务器报告负载以及资源的利用率。
    • 当需要更强的一致性和排序属性时,Envoy v2 API 仍然可以保持基准最终一致性模型。

    Envoy 的 Bootstrap 配置可以采用 完全静态、部分静态或者完全动态。

    静态配置

    下面提供了一个完全静态引导配置的例子:

    admin:
      access_log_path: /tmp/admin_access.log
      address:
        socket_address: { address: 127.0.0.1, port_value: 9901 }
    
    static_resources:
      listeners:
      - name: listener_0
        address:
          socket_address: { address: 127.0.0.1, port_value: 10000 }
        filter_chains:
        - filters:
          - name: envoy.http_connection_manager
            config:
              stat_prefix: ingress_http
              route_config:
                name: local_route
                virtual_hosts:
                - name: local_service
                  domains: ["example.com"]
                  routes:
                  - match: { prefix: "/" }
                    route: { cluster: some_service }
              http_filters:
              - name: envoy.router
      clusters:
      - name: some_service
        connect_timeout: 0.25s
        type: STATIC
        lb_policy: ROUND_ROBIN
        hosts: [{ socket_address: { address: 127.0.0.2, port_value: 1234 }}]
    

    在上面的例子中,我们配置了一个 envoy实例,监听 127.0.0.1:10000,支持 http 协议访问,http 访问域名为:http://example.com。接收到的所有http流量,转发给 127.0.0.2:1234 的服务。这个例子中 some_service 这个cluster中 hosts 是固定的(127.0.0.2:1234),不利于扩展。

    EDS为动态的配置

    将配置 envoy 的配置调整如下。

    admin:
      access_log_path: /tmp/admin_access.log
      address:
        socket_address: { address: 127.0.0.1, port_value: 9901 }
    
    static_resources:
      listeners:
      - name: listener_0
        address:
          socket_address: { address: 127.0.0.1, port_value: 10000 }
        filter_chains:
        - filters:
          - name: envoy.http_connection_manager
            config:
              stat_prefix: ingress_http
              route_config:
                name: local_route
                virtual_hosts:
                - name: local_service
                  domains: ["example.com"]
                  routes:
                  - match: { prefix: "/" }
                    route: { cluster: some_service }
              http_filters:
              - name: envoy.router
      clusters:
      - name: some_service
        connect_timeout: 0.25s
        lb_policy: ROUND_ROBIN
        type: EDS
        eds_cluster_config:
          eds_config:
            api_config_source:
              api_type: GRPC
              cluster_names: [xds_cluster]
      - name: xds_cluster
        connect_timeout: 0.25s
        type: STATIC
        lb_policy: ROUND_ROBIN
        http2_protocol_options: {}
        hosts: [{ socket_address: { address: 127.0.0.3, port_value: 5678 }}]
    

    这样就实现 some_service 这个 cluster 的 hosts 的动态配置了。新的配置中,some_service 这个 cluster 的 hosts 是 EDS(Endpoint Discovery Service) 的返回值决定的,就是说 EDS 会返回 some_service 这个 cluster 的 hosts 的列表。新配置中,EDS 服务的地址定义在 xds_cluster 这个 cluster中。

    EDS 服务的地址是:127.0.0.3:5678,会返回 proto3 编码的响应格式如下:

    version_info: "0"
    resources:
    - "@type": type.googleapis.com/envoy.api.v2.ClusterLoadAssignment
      cluster_name: some_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.2
                port_value: 1234
    

    基于xDS的动态配置

    下面提供了完全动态的 bootstrap 配置,其中属于管理服务器的所有资源都是通过 xDS 发现的。

    admin:
      access_log_path: /tmp/admin_access.log
      address:
        socket_address: { address: 127.0.0.1, port_value: 9901 }
    
    dynamic_resources:
      lds_config:
        api_config_source:
          api_type: GRPC
          cluster_names: [xds_cluster]
      cds_config:
        api_config_source:
          api_type: GRPC
          cluster_names: [xds_cluster]
    
    static_resources:
      clusters:
      - name: xds_cluster
        connect_timeout: 0.25s
        type: STATIC
        lb_policy: ROUND_ROBIN
        http2_protocol_options: {}
        hosts: [{ socket_address: { address: 127.0.0.3, port_value: 5678 }}]
    

    这里我们假设 127.0.0.3:5678 提供完整的 xDS。

    LDS服务的响应格式如下:

    version_info: "0"
    resources:
    - "@type": type.googleapis.com/envoy.api.v2.Listener
      name: listener_0
      address:
        socket_address:
          address: 127.0.0.1
          port_value: 10000
      filter_chains:
      - filters:
        - name: envoy.http_connection_manager
          config:
            stat_prefix: ingress_http
            codec_type: AUTO
            rds:
              route_config_name: local_route
              config_source:
                api_config_source:
                  api_type: GRPC
                  cluster_names: [xds_cluster]
            http_filters:
            - name: envoy.router
    

    RDS 服务的响应格式如下:

    version_info: "0"
    resources:
    - "@type": type.googleapis.com/envoy.api.v2.RouteConfiguration
      name: local_route
      virtual_hosts:
      - name: local_service
        domains: ["*"]
        routes:
        - match: { prefix: "/" }
          route: { cluster: some_service }
    

    CDS 服务的响应如下:

    version_info: "0"
    resources:
    - "@type": type.googleapis.com/envoy.api.v2.Cluster
      name: some_service
      connect_timeout: 0.25s
      lb_policy: ROUND_ROBIN
      type: EDS
      eds_cluster_config:
        eds_config:
          api_config_source:
            api_type: GRPC
            cluster_names: [xds_cluster]
    

    EDS 服务的响应如下:

    version_info: "0"
    resources:
    - "@type": type.googleapis.com/envoy.api.v2.ClusterLoadAssignment
      cluster_name: some_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.2
                port_value: 1234
    

    Envoy EDS实战示例

    下面给出一个使用 Java 自行实现 EDS服务的示例,这里我们使用了 REST 接口。

    实现 EDS-Server

    首先创建一个 SpringBoot Web项目。实现 EDS 比较复杂的部分就是响应报文和请求报文的示例实现。

    先看下请求报文(省略了setter 和 getter 部分)。

    public class DiscoveryRequest {
    
        private DiscoveryNode node;
        @JsonProperty("resource_names")
        private List<String> resourceNames;
    
        @Override
        public String toString() {
            return "node = { " + this.node + "}, resource_names = " + this.resourceNames;
        }
    }
    
    public class DiscoveryNode {
        @JsonProperty("build_version")
        private String buildVersion;
        private String cluster;
        private String id;   
    
        @Override
        public String toString() {
            return "build_version = " + this.buildVersion + ", cluster = " + this.cluster +
                    ", id = " + this.id;
        }
    }
    

    这样生成的请求报文类格式类似下面这个样子:

    {
        "node": {
            "cluster": "myCluster",
            "id": "test-id",
            "build_version": "44f8c365a1f1798f0af776f6aa64279dc68f5666/1.12.1/Clean/RELEASE/BoringSSL"
        },
        "resource_names": [
            "myservice"
        ]
    }
    

    请求报文格式比较简单,下面看下格式较为复杂的返回报文(依旧省略 setter 和 getter)。

    public class DiscoveryResponse {
    
        @JsonProperty("version_info")
        private String versionInfo;
        private List<ResponseResource> resources = new ArrayList<>();
    
    }
    
    public class ResponseResource {
    
        @JsonProperty("@type")
        private String type;
        @JsonProperty("cluster_name")
        private String clusterName;
        @JsonProperty("endpoints")
        private List<EndPoints> endPoints = new ArrayList<>();
    
    }
    
    public class EndPoints {
    
        @JsonProperty("lb_endpoints")
        private List<LbEndPoint> lbEndPoints;
    
    
    public class LbEndPoint {
    
        @JsonProperty("endpoint")
        private EndPoint endPonit;
    
    
    public class EndPoint {
    
        private DiscoveryAddress address;
    
    }
    
    public class DiscoveryAddress {
    
        @JsonProperty("socket_address")
        private SocketAddress socketAddress;
    
        public DiscoveryAddress() {
    
        }
    
        public DiscoveryAddress(SocketAddress socketAddress) {
            this.socketAddress = socketAddress;
        }
    
    }
    
    public class SocketAddress {
    
        private String address;
        @JsonProperty("port_value")
        private int port;
    
        public SocketAddress() {
    
        }
    
        public SocketAddress(String address, int port) {
            this.address = address;
            this.port = port;
        }
    
    }
    

    生成的返回报文格式如下:

    {
        "resources": [
            {
                "endpoints": [
                    {
                        "lb_endpoints": [
                            {
                                "endpoint": {
                                    "address": {
                                        "socket_address": {
                                            "address": "tcp://127.0.0.1",
                                            "port": 8888
                                        }
                                    }
                                }
                            }
                        ]
                    }
                ],
                "@type": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
                "cluster_name": "myService"
            }
        ],
        "version_info": "v1"
    }
    

    这里注意请求和返回报文的 json 格式一定不能错,否则无法和 envoy 进行交互。

    再实现 host 实例用来保存服务地址。

    public class DiscoveryHost {
    
        // IP地址
        @JsonProperty("ip_address")
        private String ipAddress;
        // 端口
        private int port;
        private DiscoveryTags tags;
    
        @Override
        public String toString() {
            return "ip_address = " + this.ipAddress + ", port = " + this.port
                    + ", tags = [" + this.tags + "]";
        }
    }
    
    public class DiscoveryHosts {
    
        private List<DiscoveryHost> hosts = new ArrayList<>();
        private String service;
    
        @Override
        public String toString() {
            return "service = " + this.service + ",hosts = " + this.hosts;
        }
    }
    
    /**
     * 服务发现中的元数据信息
     */
    public class DiscoveryTags {
    
        // 分区
        @JsonProperty("az")
        private String zone;
        // 是否灰度
        private boolean cannary;
        // 权重
        @JsonProperty("load_balancing_weight")
        private int loadBalancingWeight;
    
        @Override
        public String toString() {
            return "az = " + this.zone + ", cannary = " + this.cannary +
                    ", load_balancing_weight = " + this.loadBalancingWeight;
        }
    }
    

    这样所有的对象都生成完毕了,下面看下对外暴露的接口,包括服务发现的接口和操作服务的接口。

    @RestController
    public class DiscoveryController {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(DiscoveryController.class);
    
        private int version = 1;
        private Map<String, List<DiscoveryHost>> serviceHosts = new HashMap<>();
    
        /**
         * envoy eds服务接口
         * @param request
         * @return
         */
        @PostMapping("/v2/discovery:endpoints")
        public DiscoveryResponse discoveryEndpoints(@RequestBody DiscoveryRequest request) {
            LOGGER.info("Dicovery Request: {}", request);
            // 读取输入
            DiscoveryNode node = request.getNode();
            String id = node.getId();
            String cluster = node.getCluster();
            List<String> resourceNames = request.getResourceNames();
    
            //构建返回
            DiscoveryResponse response = new DiscoveryResponse();
            response.setVersionInfo("v" + version);
            ResponseResource resource = new ResponseResource();
            List<EndPoints> endPointsList = new ArrayList<>();
            List<LbEndPoint> lbEndPoints = new ArrayList<>();
            for(String resourceName : resourceNames) {
                if(serviceHosts.containsKey(resourceName)) {
                    List<DiscoveryHost> hosts =  serviceHosts.get(resourceName);
                    for(DiscoveryHost host : hosts) {
                        EndPoint endPoint = new EndPoint();
                        endPoint.setAddress(new DiscoveryAddress(new SocketAddress(host.getIpAddress(), host.getPort())));
                        LbEndPoint lbEndPoint = new LbEndPoint();
                        lbEndPoint.setEndPonit(endPoint);
                        lbEndPoints.add(lbEndPoint);
                    }
                }
    
                resource.setType("type.googleapis.com/envoy.api.v2.ClusterLoadAssignment");
                resource.setClusterName(resourceName);
                if(lbEndPoints.size() > 0) {
                    EndPoints endPoints = new EndPoints();
                    endPoints.setLbEndPoints(lbEndPoints);
                    endPointsList.add(endPoints);
                }
                resource.setEndPoints(endPointsList);
                response.getResources().add(resource);
                return response;
            }
            return null;
        }
    
        /**
         * 获取当前所有服务
         * @return
         */
        @GetMapping("/edsservice")
        public List<DiscoveryHosts> getAllServices() {
            List<DiscoveryHosts> hostsList = new ArrayList<>();
            for(Map.Entry<String, List<DiscoveryHost>> entry : this.serviceHosts.entrySet()) {
                DiscoveryHosts hosts = new DiscoveryHosts();
                hosts.setService(entry.getKey());
                hosts.setHosts(entry.getValue());
                hostsList.add(hosts);
            }
            return hostsList;
        }
    
        /**
         * 获取某个服务
         * @param serviceName
         * @return
         */
        @GetMapping("/edsservice/{serviceName}")
        public DiscoveryHosts getHosts(@PathVariable("serviceName") String serviceName) {
            LOGGER.info("getHostsByServiceName: service={}", serviceName);
            DiscoveryHosts hostsList = new DiscoveryHosts();
            hostsList.setHosts(this.serviceHosts.get(serviceName));
            return hostsList;
        }
    
        /**
         * 添加某个服务
         * @param serviceName
         * @param hosts
         */
        @PostMapping("/edsservice/{serviceName}")
        public void addHosts(@PathVariable("serviceName") String serviceName,
                             @RequestBody DiscoveryHosts hosts) {
            LOGGER.info("addHost: service={}, body={}", serviceName, hosts);
            if (!this.serviceHosts.containsKey(serviceName)) {
                this.serviceHosts.put(serviceName, hosts.getHosts());
                this.version++;
            }
    
        }
    
        /**
         * 删除某个服务
         * @param serviceName
         */
        @DeleteMapping("/edsservice/{serviceName}")
        public void removeHost(@PathVariable("serviceName") String serviceName) {
            LOGGER.info("removeHost: service={}", serviceName);
            this.serviceHosts.remove(serviceName);
        }
    
        /**
         * 修改某个服务
         * @param serviceName
         * @param hosts
         */
        @PutMapping("/edsservice/{serviceName}")
        public void updateHost(@PathVariable("serviceName") String serviceName,
                               @RequestBody DiscoveryHosts hosts) {
            LOGGER.info("updateHost: service={}, body={}", serviceName, hosts);
            if(this.serviceHosts.containsKey(serviceName)) {
                this.serviceHosts.put(serviceName, hosts.getHosts());
                this.version++;
            }
        }
    
    }
    

    这样整个 EDS Server 的代码就开发完毕了。

    实现上游服务

    下面实现用来接收 Enovy 发出的请求的上游服务,依旧创建一个 SpringBoot Web 项目。

    添加2个简单的接口。

    @RestController
    public class UpstreamController {
    
        private static final String uuid = UUID.randomUUID().toString();
    
        @RequestMapping("/")
        public String index() {
            return "Hello, My UUID is " + uuid;
        }
    
        @RequestMapping("/healthz")
        public String health() {
            return "OK";
        }
    }
    

    添加配置文件 application.properties 。

    server.port=${PORT:8081}
    

    上游服务编写完毕,整体比较简单。

    创建镜像

    创建 SpringBoot 镜像的方法 Envoy 入门实战部署一个SpringBoot应用
    都已经介绍过,这里不再赘述。这里创建一个 EDS服务镜像,2个上游服务镜像,上游服务镜像使用的端口分别是 8081 和 8082。

    然后创建一个 envoy 镜像,envoy 镜像使用的配置文件 envoy-config.yaml 如下。

    admin:
      access_log_path: /tmp/admin_access.log
      address:
        socket_address:
          address: 127.0.0.1
          port_value: 9000
    
    node:
      cluster: mycluster
      id: testid
    
    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: service_backend }
              http_filters:
              - name: envoy.router
    
      clusters:
      - name: service_backend
        type: EDS
        connect_timeout: 0.25s
        eds_cluster_config:
          service_name: myservice
          eds_config:
            api_config_source:
              api_type: REST
              cluster_names: [edscluster]
              refresh_delay: 5s
      - name: edscluster
        type: STATIC
        connect_timeout: 0.25s
        hosts: [{ socket_address: { address: 127.0.0.1, port_value: 8080 }}]
    

    其中需要注意的是由于 tomcat 不支持 URL 中存在"_",所以这里 cluster 名称使用了 edscluster。

    envoy 镜像的 Dockerfile 内容如下:

    FROM envoyproxy/envoy-alpine:latest
    COPY envoy-config.yaml /etc/envoy/envoy.yaml
    

    创建 envoy 镜像。

    docker build -t envoytest2:v1.0.0 .
    

    这样我们就拥有了如下镜像。

    $ docker images
    REPOSITORY                      TAG                 IMAGE ID            CREATED             SIZE
    envoy-discovery                 1.0-SNAPSHOT        ab2722bc242c        44 hours ago        122MB
    envoytest2                      v1.0.0              669ef4043e24        45 hours ago        40.9MB
    upstream-service                1.0-SNAPSHOT        b2e02f942699        45 hours ago        122MB
    upstream-service2               1.0-SNAPSHOT        1eb7d9738765        45 hours ago        122MB
    

    启动环境

    下面依次启动镜像。

    $ docker run -p 8081:8081 upstream-service:1.0-SNAPSHOT
    $ docker run -p 8082:8082 upstream-service2:1.0-SNAPSHOT
    $ docker run -p 10000:10000 --name envoytest2 envoytest2:v1.0.0
    $ docker run --network=container:envoytest envoy-discovery:1.0-SNAPSHOT
    

    现在在浏览器中中访问 http://192.168.99.100:10000/ (其中192.168.99.100是 Docker 虚拟机的地址)会看到如下结果。

    找不到上游服务.png

    使用 Postman 向 http://192.168.99.100:8080/edsservice/myservice 发送如下 POST 请求,将上游服务注册到 Eds 服务之中。

    {
      "hosts": [
        {
          "ip_address": "192.168.99.100",
          "port": 8081,
          "tags": {
            "server01": "8081",
            "canary": false,
            "load_balancing_weight": 50
          }
        },
        {
          "ip_address": "192.168.99.100",
          "port": 8082,
          "tags": {
            "server01": "8082",
            "canary": false,
            "load_balancing_weight": 50
          }
        }
      ]
    }
    

    然后再次访问 http://192.168.99.100:10000/ ,会看到上游服务的页面。

    发现上游服务1.png

    再次访问,可以看到另一个上游服务的页面。

    发现上游服务2.png

    这是因为两个上游服务的权重一样, envoy 收到请求后会轮流定向到两个上游服务。

    相关文章

      网友评论

          本文标题:Envoy 学习笔记之创建 EDS 动态配置

          本文链接:https://www.haomeiwen.com/subject/ltnmactx.html