许吉友 - 运维

流量转移与分流

Envoy 可以将流量拆分为两个或多个路由,分别将流量导向不同的集群,这里有两个常见的用例:

  1. 版本升级,路由的流量逐渐从一个集群转移到另一个集群。
  2. A/B 测试,同一服务,不同版本之间的测试,路由分配不同比例的流量进入不同版本的集群。

转移

先准备两个版本的 API 服务。

首先 v1 版本,main_v1.go :

package main

import (
    "github.com/emicklei/go-restful"
    "io"
    "log"
    "net/http"
)

// This example shows the minimal code needed to get a restful.WebService working.
//
// GET http://localhost:8081/hello

func main() {
    ws := new(restful.WebService)
    ws.Route(ws.GET("/hello").To(hello))
    restful.Add(ws)
    log.Println("server start in 8081")
    log.Fatal(http.ListenAndServe(":8081", nil))
}

func hello(req *restful.Request, resp *restful.Response) {
    log.Println("request hello")
    _, _ = io.WriteString(resp, "world v1")
}

然后 v2 版本,main_v2.go :

package main

import (
    "github.com/emicklei/go-restful"
    "io"
    "log"
    "net/http"
)

// This example shows the minimal code needed to get a restful.WebService working.
//
// GET http://localhost:8082/hello

func main() {
    ws := new(restful.WebService)
    ws.Route(ws.GET("/hello").To(hello))
    restful.Add(ws)
    log.Println("server start in 8082")
    log.Fatal(http.ListenAndServe(":8082", nil))
}

func hello(req *restful.Request, resp *restful.Response) {
    log.Println("request hello")
    _, _ = io.WriteString(resp, "world v2")
}

编译并运行 v1 版本:

$ go build main_v1.go
$ ./main_v1

编译并运行 v2 版本:

$ go build main_v2.go
$ ./main_v2

然后编写 envoy 配置文件 envoy-config.yaml :

admin:
  access_log_path: /dev/stdout
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 20000

node:
  cluster: hello-service
  id: node1

static_resources:
  listeners:
  - address:
      socket_address:
        address: 0.0.0.0
        port_value: 82
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
          codec_type: auto
          stat_prefix: ingress_http
          access_log:
            name: envoy.file_access_log
            typed_config:
              "@type": type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog
              path: /dev/stdout
          route_config:
            name: local_route
            virtual_hosts:
              - name: service
                domains:
                  - "*"
                routes:
                  - match:
                      prefix: "/hello"
                      runtime_fraction:
                        default_value:
                          numerator: 90
                          denominator: HUNDRED
                        runtime_key: routing.traffic_shift.hello
                    route:
                      cluster: hello-v1
                  - match:
                      prefix: "/hello"
                    route:
                      cluster: hello-v2
          http_filters:
          - name: envoy.filters.http.router
  clusters:
  - name: hello-v1
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: ROUND_ROBIN
    hosts:
    - socket_address:
        address: 127.0.0.1
        port_value: 8081
  - name: hello-v2
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: ROUND_ROBIN
    hosts:
    - socket_address:
        address: 127.0.0.1
        port_value: 8082

这里 runtime_key 是可以自定义的,denominator (分母)为 HUNDRED (一百),numerator (分子)为 90。

启动:

$ sudo getenvoy run standard:1.14.1 -- --config-path ./envoy-config.yaml

测试:

$ curl http://127.0.0.1:82/hello

访问 10 次,会访问一次 v2 版本。

介绍一下其中的流程:

Envoy 会对路由策略进行匹配,如果路由具有 runtime_fraction 配置,则会根据 runtime_fraction 值,另外匹配路由。

上面的示例中,两条路由有相同的匹配策略,第一条路由中指定了 runtime_fraction 对象,可以通过更改runtime_fraction值来实现流量转移。

以下是完成任务所需的大概操作顺序:

分流

继续使用上面的两个服务。

下面按比例在两个服务之间分配流量,比如分配 90% 的流量给 hello-v1,剩余 10% 的流量分配给 hello-v2。可以通过 weighted_clusters 来实现分流的效果。

在上面转移流量的示例中,需要配置多个路由,而在 weighted_clusters 中,只需要配置单个路由,就可以在多个 cluster 之间按权重分配流量。

修改 Envoy 的配置文件如下:

admin:
  access_log_path: /dev/stdout
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 20000

node:
  cluster: hello-service
  id: node1

static_resources:
  listeners:
  - address:
      socket_address:
        address: 0.0.0.0
        port_value: 82
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
          codec_type: auto
          stat_prefix: ingress_http
          access_log:
            name: envoy.file_access_log
            typed_config:
              "@type": type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog
              path: /dev/stdout
          route_config:
            name: local_route
            virtual_hosts:
              - name: service
                domains:
                  - "*"
                routes:
                  - match:
                      prefix: "/hello"
                    route:
                      weighted_clusters:
                        runtime_key_prefix: routing.traffic_split.hello
                        total_weight: 100
                        clusters:
                          - name: hello-v1
                            weight: 90
                          - name: hello-v2
                            weight: 10
          http_filters:
          - name: envoy.filters.http.router
  clusters:
  - name: hello-v1
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: ROUND_ROBIN
    hosts:
    - socket_address:
        address: 127.0.0.1
        port_value: 8081
  - name: hello-v2
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: ROUND_ROBIN
    hosts:
    - socket_address:
        address: 127.0.0.1
        port_value: 8082

默认情况下,权重之和必须恰好为 total_weight 指定的值,这里是 100。在 v2 API 中,总权重默认为100,如果想要更精细的粒度,可以修改这里的 total_weight

启动:

$ sudo getenvoy run standard:1.14.1 -- --config-path ./envoy-config.yaml

测试:

$ curl http://127.0.0.1:82/hello

和转移中的效果一致,访问 10 次这条 URL,会访问一次 v2 版本的服务。