Envoy微服務實踐—簡書博客

原文鏈接:https://www.jianshu.com/p/90f9ee98ce70

轉自簡書:https://www.jianshu.com/p/90f9ee98ce70 

Email:[email protected]

本文介紹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

更多的實現細節則需要再研究挖掘了,同時也歡迎一起討論。



作者:gaulzhw
鏈接:https://www.jianshu.com/p/90f9ee98ce70
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章