Envoy 实践

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

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

推荐阅读更多精彩内容

  • 儿晚上拎着他的“公文包”回来,对我说,今天整理英语笔记,整整写了14张A4纸,且是正反两面。密密麻麻,整整...
    大爱无疆杨青阅读 201评论 0 6
  • 今天的体育课,同学们为了运动会认真的排练。虽然很晒,但是大家都在坚持。希望能在运动会上把方队走的整齐漂亮加油!
    青岛卫校15护3阅读 257评论 0 0
  • 图片网站:pexels以及wallhaven ,推荐前者,后者不容易访问~ 蒙版的制作方法:插入一个形状,接下来将...
    女静阅读 460评论 0 0
  • 当时我在医院里,看到这句话突然就很开心,什么也不怕了。
    月亮上的小酸奶阅读 61评论 0 0