Envoy 实践

作者: gaulzhw | 来源:发表于2018-05-15 23:23 被阅读0次

    Email:gaulzhw@gmail.com

    本文介绍Envoy的一些基本概念以及实践操作,以期通过本文的介绍让读者可以了解到Envoy的原理,帮助读者理解Istio的Data Panel层实现。

    写作本文的初衷是源于近期项目中需要做的微服务平台,平台需要针对微服务做控制。在技术选型的过程中比较了Envoy、Istio的实现,最终决定以Envoy来完成特定的业务需求。在使用Envoy的过程中,由于文档资料较少,实践中遇到了一些困难,故将实践中的一些理解和过程记录下来,方便大家查阅,减少弯路。

    1. Service Mesh

    关于Service Mesh不是本篇文章的重点,但是理解Service Mesh的概念、优势、发展,对理解本文有很大的帮助,此处罗列下Service Mesh的几篇文章,希望读者花一些时间先阅读一下文章的内容,对Service Mesh有个了解和认识。

    虽然目前的Service Mesh已经进入了以Istio、Conduit为代表的第二代,由Data Panel、Control Panel两部分组成。但是以Istio为例,它也没有自己去实现Data Panel,而是在现有的Data Panel实现上做了Control Panel来达成目标。

    所以说要掌握Istio,或者说要理解Service Mesh,首先需要掌握Data Panel的实现,而Envoy就是其中的一种实现方案。关于Envoy是什么,可以做什么,有什么优点,可以到Envoy的官网上查看详细信息,本文注重于Envoy的一些实践操作,重点关心怎么利用Envoy实现一些需求。

    2. Envoy术语

    要深入理解Envoy,首先需要先了解一下Envoy中的一些术语。

    • Host:能够进行网络通信的实体(如服务器上的应用程序)。

    • Downstream:下游主机连接到Envoy,发送请求并接收响应。

    • Upstream:上游主机接收来自Envoy连接和请求并返回响应。

    • Listener:可以被下游客户端连接的命名网络(如端口、unix套接字)。

    • Cluster:Envoy连接到的一组逻辑上相似的上游主机。

    • Mesh:以提供一致的网络拓扑的一组主机。

    • Runtime configuration:与Envoy一起部署的外置实时配置系统。

    envoy-term.png

    3. Envoy的启动

    官方提供了Envoy的Docker镜像,本文中使用的镜像名是envoyproxy/envoy-alpine

    镜像中已经将Envoy安装到/usr/local/bin目录下,可以先看看envoy进程的help信息。

    # /usr/local/bin/envoy --help
    USAGE: 
       /usr/local/bin/envoy  [--disable-hot-restart] [--max-obj-name-len
                             <uint64_t>] [--max-stats <uint64_t>] [--mode
                             <string>] [--parent-shutdown-time-s <uint32_t>]
                             [--drain-time-s <uint32_t>]
                             [--file-flush-interval-msec <uint32_t>]
                             [--service-zone <string>] [--service-node
                             <string>] [--service-cluster <string>]
                             [--hot-restart-version] [--restart-epoch
                             <uint32_t>] [--log-path <string>] [--log-format
                             <string>] [-l <string>]
                             [--local-address-ip-version <string>]
                             [--admin-address-path <string>] [--v2-config-only]
                             [--config-yaml <string>] [-c <string>]
                             [--concurrency <uint32_t>] [--base-id <uint32_t>]
                             [--] [--version] [-h]
    

    envoy进程启动的时候需要指定一些参数,其中最重要的是--config-yaml参数,用于指定envoy进程启动的时候需要读取的配置文件地址。Docker中配置文件默认是放在/etc/envoy目录下,配置文件的文件名是envoy.yaml

    所以在启动容器的时候需要将自定义的envoy.yaml配置文件挂载到指定目录下替换掉默认的配置文件。

    /usr/local/bin/envoy -c <path to config>.{json,yaml,pb,pb_text} --v2-config-only

    tip:envoy默认的日志级别是info,对于开发阶段需要进行调试的话,调整日志级别到debug是非常有用的,可以在启动参数中添加-l debug来将日志级别进行切换。

    4. Envoy的启动配置

    在介绍Envoy的配置文件之前,先介绍一下Envoy的API。Envoy提供了两个版本的API,v1和v2版本API。现阶段v1版本已经不建议使用了,通常都是使用v2的API。

    v2的API提供了两种方式的访问,一种是HTTP Rest的方式访问,还有一种GRPC的访问方式。关于GRPC的介绍可以参考官方文档,在后面的文章中只实现了GRPC的API。

    Envoy的启动配置文件分为两种方式:静态配置和动态配置。

    • 静态配置是将所有信息都放在配置文件中,启动的时候直接加载。

    • 动态配置需要提供一个Envoy的服务端,用于动态生成Envoy需要的服务发现接口,这里叫XDS,通过发现服务来动态的调整配置信息,Istio就是实现了v2的API。

    4.1 静态配置

    以一个最简化的静态配置来做示例,体验一下envoy。

    下面是envoy.yaml配置文件:

    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: 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: 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.1, port_value: 80 }}]
    

    在此基础上启动两个容器,envoyproxy容器和nginx容器,nginx容器共享envoyproxy容器的网络,以此来模拟sidecar。

    docker run -d -p 10000:10000 -v `pwd`/envoy.yaml:/etc/envoy/envoy.yaml --name envoyproxy envoyproxy/envoy-alpine
    ​
    docker run -d --network=container:envoyproxy --name nginx nginx</pre>
    

    根据配置文件的规则,envoy监听在10000端口,同时该端口也在宿主机的10000端口上暴露出来。当有请求到达监听上后,envoy会对所有请求路由到some_service这个cluster上,而该cluster的upstream指向本地的80端口,也就是nginx服务上。

    static.png

    4.2 动态配置

    动态配置可以实现全动态,即实现LDS(Listener Discovery Service)、CDS(Cluster Discovery Service)、RDS(Route Discovery Service)、EDS(Endpoint Discovery Service),以及ADS(Aggregated Discovery Service)。

    ADS不是一个实际意义上的XDS,它提供了一个汇聚的功能,以实现需要多个同步XDS访问的时候可以在一个stream中完成的作用。

    下面的图通过在静态配置的基础上,比较直观的表示出各个发现服务所提供的信息。

    xds.png

    由此,典型的动态配置文件如下:

    admin:
      access_log_path: /tmp/admin_access.log
      address:
        socket_address: { address: 127.0.0.1, port_value: 9901 }
    
    dynamic_resources:
      cds_config:
        ads: {}
      lds_config:
        ads: {}
      ads_config:
        api_type: GRPC
        cluster_names: [xds_cluster]
    
    static_resources:
      clusters:
      - name: xds_cluster
        connect_timeout: 0.25s
        type: STRICT_DNS
        lb_policy: ROUND_ROBIN
        http2_protocol_options: {}
        hosts: [{ socket_address: { address: envoy-server, port_value: 50051 }}]
    

    tip:动态配置和静态配置最大的区别在于,启动的时候一定要指定cluster和id,这两个参数表示该Envoy进程属于哪个cluster,id要求在相同的cluster下唯一,以表示不同的指向发现服务的连接信息。这两个参数可以在envoy的启动命令中添加--service-cluster--service-node,也可以在envoy.yaml配置文件中指定node.clusternode.id

    5. 深入实验

    接下来的实验主要以动态配置的方式来实现一个简单的需求,首先描述一下需求场景:

    有两个微服务,一个是envoy-web,一个envoy-server。

    • envoy-web相当于下图中的front-envoy作为对外访问的入口。

    • envoy-server相当于下图中的service_1和service_2,是内部的一个微服务,部署2个实例。

    demo.png

    envoy-server有3个API,分别是/envoy-server/hello、/envoy-server/hi、/envoy-server/self,目的是测试envoy对于流入envoy-server的流量控制,对外只允许访问/envoy-server/hello和/envoy-server/hi两个API,/envoy-server/self不对外暴露服务。

    envoy-web也有3个API,分别是/envoy-web/hello、/envoy-web/hi、/envoy-web/self,目的是测试envoy对于流出envoy-web的流量控制,出口流量只允许/envoy-web/hello和/envoy-web/self两个访问出去。

    最终的实验:外部只能访问envoy-web暴露的接口

    • 当访问/envoy-web/hello接口时返回envoy-server的/hello接口的数据,表示envoy-web作为客户端访问envoy-server返回服务响应的结果。

    • 当访问/envoy-web/hi接口时,envoy-web的envoy拦截住出口流量,限制envoy-web向envoy-server发送请求,对于前端用户返回mock数据。

    • 当访问/envoy-web/self接口时,envoy-web出口流量可以到达envoy-server容器,但是envoy-server在入口流量处控制住了此次请求,拒绝访问envoy-server服务,对于前端用户返回mock数据。

    5.1 静态配置

    首先以静态配置的方式先实现功能。

    5.1.1 编写服务代码

    服务代码分为envoy-web和envoy-server两个服务,采用SpringBoot的方式,下面记录一些重要的代码片段。

    • envoy-server
    @RestController
    public class HelloRest {
        private static final Logger LOGGER = LoggerFactory.getLogger(HelloRest.class);
    
        @GetMapping("/envoy-server/hello")
        public String hello() {
            LOGGER.info("get request from remote, send response, say hello");
            return "hello";
        }
    
        @GetMapping("/envoy-server/hi")
        public String hi() {
            LOGGER.info("get request from remote, send response, say hi");
            return "hi";
        }
    
        @GetMapping("/envoy-server/self")
        public String self() {
            LOGGER.info("get request from remote, send response, say self");
            return "self";
        }
    }
    
    • envoy-web
    @RestController
    public class HelloController {
        private static final Logger LOGGER = LoggerFactory.getLogger(HelloController.class);
    
        @Autowired
        private RestTemplate template;
    
        @GetMapping("/envoy-web/local")
        public String sayLocal() {
            LOGGER.info("get request, send response");
            return "local";
        }
    
        @GetMapping("/envoy-web/hello")
        public String sayHello() {
            String url = "http://127.0.0.1:10000/envoy-server/hello";
            LOGGER.info("get request, send rest template to {}", url);
            return getRemote(url, "mock value for hello");
        }
    
        @GetMapping("/envoy-web/hi")
        public String sayHi() {
            String url = "http://127.0.0.1:10000/envoy-server/hi";
            LOGGER.info("get request, send rest template to {}", url);
            return getRemote(url, "mock value for hi");
        }
    
        @GetMapping("/envoy-web/self")
        public String saySelf() {
            String url = "http://127.0.0.1:10000/envoy-server/self";
            LOGGER.info("get request, send rest template to {}", url);
            return getRemote(url, "mock value for self");
        }
    
        private String getRemote(String url, String mock) {
            try {
                ResponseEntity<String> response = template.getForEntity(url, String.class);
                return response.getBody();
            } catch (Exception e) {
                LOGGER.error("error happens: {}", e);
                return mock;
            }
        }
    }
    

    tip:为简化起见,代码只是介绍对出入流量的控制,直接在envoy-web上访问了本地的envoy端口进行转发流量,实际代码中可以用服务名:服务端口号访问,而此时为了使得envoy仍然可以拦截入和出的流量,可以配置iptables(Istio的实现中也是使用了iptables)。

    5.1.2 编写配置文件

    针对不同的服务,也配置了两份envoy.yaml配置文件。

    • envoy-server
    admin:
      access_log_path: /tmp/admin_access.log
      address:
        socket_address: { address: 0.0.0.0, port_value: 9900 }
    static_resources:
      listeners:
      - name: listener_ingress
        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: "/envoy-server/hello" }
                    route: { cluster: cluster_server }
                  - match: { prefix: "/envoy-server/hi" }
                    route: { cluster: cluster_server }
              http_filters:
              - name: envoy.router
      clusters:
      - name: cluster_server
        connect_timeout: 0.5s
        type: STATIC
        lb_policy: ROUND_ROBIN
        hosts: 
        - { socket_address: { address: 127.0.0.1, port_value: 8081 }}
    
    • envoy-web
    admin:
      access_log_path: /tmp/admin_access.log
      address:
        socket_address: { address: 0.0.0.0, port_value: 9900 }
    static_resources:
      listeners:
      - name: listener_ingress
        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: "/envoy-web/" }
                    route: { cluster: cluster_ingress }
                  - match: { prefix: "/envoy-server/hello" }
                    route: { cluster: cluster_egress }
                  - match: { prefix: "/envoy-server/self" }
                    route: { cluster: cluster_egress }
              http_filters:
              - name: envoy.router
      clusters:
      - name: cluster_ingress
        connect_timeout: 0.5s
        type: STATIC
        lb_policy: ROUND_ROBIN
        hosts:
        - { socket_address: { address: 127.0.0.1, port_value: 8080 }}
      - name: cluster_egress
        connect_timeout: 0.5s
        type: STATIC
        lb_policy: ROUND_ROBIN
        hosts:
        - { socket_address: { address: 172.17.0.2, port_value: 10000 }}
        - { socket_address: { address: 172.17.0.3, port_value: 10000 }}
    

    5.1.3 启动测试

    #envoy-server1
    docker run -d -v `pwd`/envoy-server.yaml:/etc/envoy/envoy.yaml --name envoyproxy-server1 envoyproxy/envoy-alpine /usr/local/bin/envoy --service-cluster envoy-server --service-node 1 -c /etc/envoy/envoy.yaml --v2-config-only
    
    docker run -d --network=container:envoyproxy-server1 --name envoy-server1 envoy-server:1.1
    
    #envoy-server2
    docker run -d -v `pwd`/envoy-server.yaml:/etc/envoy/envoy.yaml --name envoyproxy-server2 envoyproxy/envoy-alpine /usr/local/bin/envoy --service-cluster envoy-server --service-node 2 -c /etc/envoy/envoy.yaml --v2-config-only
    
    docker run -d --network=container:envoyproxy-server2 --name envoy-server2 envoy-server:1.1
    
    #envoy-web
    docker run -d -p 10000:10000 -v `pwd`/envoy-web.yaml:/etc/envoy/envoy.yaml --name envoyproxy-web envoyproxy/envoy-alpine /usr/local/bin/envoy --service-cluster envoy-web --service-node 1 -c /etc/envoy/envoy.yaml --v2-config-only
    
    docker run -d --network=container:envoyproxy-web --name envoy-web envoy-web:1.1
    

    当容器部署完毕之后,可以直接访问以下3个url,其中hi和self的访问返回的是mock数据,虽然同为mock数据,但是这两个url其实是不相同的,一个是在envoy出口流量处做的控制,一个是在envoy入口流量处做的控制,其中的细节可以再去品味品味。

    example.png

    5.2 动态配置

    动态配置需要实现发现服务,通过GRPC的方式获取相应。

    动态的配置文件在前面的内容中已经有过介绍,最重要的是需要提供一个发现服务,对外提供XDS服务,下面以其中的一个LDS作为介绍,其他XDS实现类似。

    • 服务端:既然作为服务,就需要对外提供接口服务。
    public class GrpcService {
        private Server server;
        private static final int PORT = 50051;
    
        private void start() throws IOException {
            server = ServerBuilder.forPort(PORT)
                    .addService(new LdsService())
                    .addService(new CdsService())
                    .addService(new RdsService())
                    .addService(new EdsService())
                    .addService(new AdsService())
                    .build()
                    .start();
            System.err.println("Server started, listening on " + PORT);
            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                System.err.println("*** shutting down gRPC server since JVM is shutting down");
                GrpcService.this.stop();
                System.err.println("*** server shut down");
            }));
        }
    
        private void stop() {
            if (server != null) {
                server.shutdown();
            }
        }
    
        private void blockUntilShutdown() throws InterruptedException {
            if (server != null) {
                server.awaitTermination();
            }
        }
    
        public static void main(String[] args) throws IOException, InterruptedException {
            final GrpcService server = new GrpcService();
            server.start();
            server.blockUntilShutdown();
        }
    }
    
    • XDS:通过GRPC生成服务端的stub文件,实现LdsServer继承自ListenerDiscoveryServiceGrpc.ListenerDiscoveryServiceImplBase,需要实现streamListeners方法。
    public class LdsService extends ListenerDiscoveryServiceGrpc.ListenerDiscoveryServiceImplBase {
        private static final Logger LOGGER = LogManager.getLogger();
    
        @Override
        public StreamObserver<Discovery.DiscoveryRequest> streamListeners(StreamObserver<Discovery.DiscoveryResponse> responseObserver) {
            return new StreamObserver<Discovery.DiscoveryRequest>() {
                @Override
                public void onNext(Discovery.DiscoveryRequest request) {
                    XdsHelper.getInstance().buildAndSendResult(request, responseObserver);
                }
    
                @Override
                public void onError(Throwable throwable) {
                    LOGGER.warn("Error happens", throwable);
                }
    
                @Override
                public void onCompleted() {
                    LOGGER.info("LdsService completed");
                }
            };
        }
    }
    

    6. 总结

    至此,基本介绍完Envoy使用的一些常见问题,在实现的时候也会有其他一些细节需要注意。

    比如,envoy作为一个服务之间网络请求的代理,如何拦截全部的入和出流量?

    Istio给了一个很好的解决方案,就是通过iptables。它会使用一个特定的uid(默认1337)用户运行envoy进程,iptables对于1337用户的流量不做拦截。下面就是参考Istio的iptables.sh做的一个实现:

    uname=envoy
    uid=1337
    iptalbes -t nat -F
    iptables -t nat -I PREROUTING -p tcp -j REDIRECT --to-ports 10000
    iptables -t nat -N ENVOY_OUTPUT
    iptables -t nat -A OUTPUT -p tcp -j ENVOY_OUTPUT
    iptables -t nat -A ENVOY_OUTPUT -p tcp -d 127.0.0.1/32 -j RETURN
    iptables -t nat -A ENVOY_OUTPUT -m owner --uid-owner ${uid} -j RETURN
    iptables -t nat -A ENVOY_OUTPUT -p tcp -j REDIRECT --to-ports 10000
    

    更多的实现细节则需要再研究挖掘了,同时也欢迎一起讨论。

    相关文章

      网友评论

        本文标题:Envoy 实践

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