sample-envoy-proxy

介绍

几个星期前我第一次遇到Envoy代理,当时我的一位博客读者建议我写一篇关于它的文章。我以前从未听说过,我的第一个想法是,这不是我的经验领域。事实上,这个工具并不像nginx或haproxy那样受欢迎,但它提供了一些有趣的功能,其中我们可以区分对MongoDB,Amazon RDS的开箱即用支持,发现和负载平衡的灵活性或生成很多有用的流量统计。好吧,我们对它的优势了解一点但是Envoy代理究竟是什么?'Envoy是一个开源边缘和服务代理,专为云原生应用而设计'。它最初由Lift开发,是一种高性能C ++分布式代理,专为独立服务和应用程序以及大型微服务服务网格而设计。现在听起来真的很棒。这就是为什么我决定仔细研究它并准备一个使用Envoy和基于Spring Boot的微服务实现的服务发现和分布式跟踪的示例。

特使配置

在以前基于Spring Cloud的大多数示例中,我们使用Zuul作为边缘和代理。Zuul是流行的Netflix OSS工具,在您的微服务架构中充当API网关。事实证明,它可以被Envoy代理成功取代。在Envoy中我真正喜欢的一件事就是创建配置的方法。默认格式为JSON,并根据JSON模式进行验证。这个JSON属性和模式记录良好,易于理解。正是您对现代解决方案的期望,推荐的开始使用方法是使用预先构建的Docker镜像。因此,在开始时我们必须创建Dockerfile以使用Envoy构建Docker镜像,并提供JSON格式的配置文件。这是我的Dockerfile。参数service-clusterservice-node 是可选的,与提供的服务发现配置有关,我将在一分钟内详细说明。

1

2

3

4

FROM lyft/envoy:latest

RUN apt-get update

COPY envoy.json /etc/envoy.json

CMD /usr/local/bin/envoy -c /etc/envoy.json --service-cluster samplecluster --service-node sample1

我假设你有关于Docker及其命令的基本知识,此时这是强制性的。提供envoy.json配置文件后,我们可以继续构建Docker镜像。

1

docker build -t envoy:v1 .

然后使用docker run命令运行它。有用的端口应暴露在外面。

1

docker run -d --name envoy -p 9901:9901 -p 10000:10000 envoy:v1

第一个非常有用的功能是本地HTTP管理员服务器。它可以在admin属性内的JSON文件中配置。出于示例目的,我选择了端口9901,您可能已经注意到我也在Envoy Docker容器外部暴露了该端口。现在,管理控制台可以在http://192.168.99.100:9901 /下找到。如果调用该地址,则会打印所有可用命令。对我来说最有用的是统计数据,它会打印与代理和日志记录相关的所有重要统计信息,我可以在这里动态更改某些已定义类别的日志记录级别。所以,首先,如果你有任何问题,Envoy尝试通过调用/logging?name=level并在运行docker logs envoy命令后在Docker容器上观察它们来更改日志记录级别。

1

2

3

4

"admin": {

    "access_log_path": "/tmp/admin_access.log",

    "address": "tcp://0.0.0.0:9901"

}

下一个必需的配置属性是listeners。在那里,我们定义路由设置和Envoy将侦听传入TCP连接的地址。符号tcp://0.0.0.0:10000是具有端口10000的任何IPv4地址的通配符匹配。此端口也在Envoy Docker容器外部公开。在这种情况下,它将是我们的API网关,可在http://192.168.99.100:10000/地址下获得。我们将在ltare阶段回到代理配置细节,现在让我们仔细研究一下这个示例的架构。

1

2

3

4

"listeners": [{

    "address": "tcp://0.0.0.0:10000",

    ...

}]

建筑

所示解决方案的架构在下图中可见。我们将Envoy代理作为API网关,这是我们系统的入口点。Envoy与Zipkin集成并向其发送跟踪消息,其中包含有关传入的HTTP请求和发回的响应的信息。两个示例微服务Person和Product在启动时在服务发现中注册,在注销时注销。它们隐藏在API网关后面的外部客户端。特使必须使用已注册服务的地址获取实际配置,并正确路由传入的HTTP请求。如果每个服务有多个实例可用,则应执行负载平衡。

sample-envoy-proxy

事实证明,Envoy不支持像Consul或Zookeeper这样众所周知的发现服务器,但是定义了自己的基于REST的通用API,需要实现它才能启用集群成员获取。此API的主要方法GET /v1/registration/:service用于获取当前已注册的服务实例列表。Lyft提供了Python的默认实现,但出于示例目的,我们使用Java和Spring Boot开发了自己的解决方案。GitHub上提供了示例应用程序源代码。除了服务发现实现,您还可以找到两个示例微服务。

服务发现

我们的自定义发现实现只是将基于REST的API暴露给注册,取消注册和获取服务实例的方法。GET方法需要返回与以下模式匹配的特定JSON结构。

1

2

3

4

6

7

{

    "hosts": [{

        "ip_address": "...",

        "port": "...",

        ...

    }]

}

这是带有发现API实现的REST控制器类。

1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

三十

31

32

33

34

35

36

37

38

39

@RestController

public class EnvoyDiscoveryController {

 

    private static final Logger LOGGER = LoggerFactory.getLogger(EnvoyDiscoveryController.class);

 

    private Map<String, List<DiscoveryHost>> hosts = new HashMap<>();

 

    @GetMapping(value = "/v1/registration/{serviceName}")

    public DiscoveryHosts getHostsByServiceName(@PathVariable("serviceName") String serviceName) {

        LOGGER.info("getHostsByServiceName: service={}", serviceName);

        DiscoveryHosts hostsList = new DiscoveryHosts();

        hostsList.setHosts(hosts.get(serviceName));

        LOGGER.info("getHostsByServiceName: hosts={}", hostsList);

        return hostsList;

    }

 

    @PostMapping("/v1/registration/{serviceName}")

    public void addHost(@PathVariable("serviceName") String serviceName, @RequestBody DiscoveryHost host) {

        LOGGER.info("addHost: service={}, body={}", serviceName, host);

        List<DiscoveryHost> tmp = hosts.get(serviceName);

        if (tmp == null)

            tmp = new ArrayList<>();

        tmp.add(host);

        hosts.put(serviceName, tmp);

    }

 

    @DeleteMapping("/v1/registration/{serviceName}/{ipAddress}")

    public void deleteHost(@PathVariable("serviceName") String serviceName, @PathVariable("ipAddress") String ipAddress) {

        LOGGER.info("deleteHost: service={}, ip={}", serviceName, ipAddress);

        List<DiscoveryHost> tmp = hosts.get(serviceName);

        if (tmp != null) {

            Optional<DiscoveryHost> optHost = tmp.stream().filter(it -> it.getIpAddress().equals(ipAddress)).findFirst();

            if (optHost.isPresent())

                tmp.remove(optHost.get());

            hosts.put(serviceName, tmp);

        }

    }

 

}

让我们回到Envoy配置设置。假设我们从Dockerfile构建了一个可见的图像,然后在默认端口上运行容器,我们可以在地址http://192.168.99.100:9200下调用它。该地址应放在envoy.json配置文件中。应在“Cluster Manager”部分中提供服务发现连接设置。

1

2

3

4

FROM openjdk:alpine

MAINTAINER Piotr Minkowski <[email protected]>

ADD target/envoy-discovery.jar envoy-discovery.jar

ENTRYPOINT ["java", "-jar", "/envoy-discovery.jar"]

EXPOSE 9200

这是envoy.json文件中的片段。应将服务发现的集群定义为全局SDS配置,必须在sdsproperty(1)中指定。最重要的是提供正确的URL(2),并在此基础上,Envoy自动尝试调用端点GET / v1 / registration / {service_name}。该部分的最后一个有趣的配置字段refresh_delay_ms是负责设置提取在发现服务器中注册的服务列表之间的延迟。那不是全部。我们还必须定义集群成员。它们由名称(4)标识。它们的类型是sds(5),这意味着该集群使用服务发现服务器来定位调用微服务的网络地址,其名称在service-name属性中定义。

1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

"cluster_manager": {

    "clusters": [{

        "name": "service1", (4)

        "type": "sds", // (5)

    "connect_timeout_ms": 5000,

    "lb_type": "round_robin",

    "service_name": "person-service" // (6)

    }, {

        "name": "service2",

        "type": "sds",

        "connect_timeout_ms": 5000,

        "lb_type": "round_robin",

        "service_name": "product-service"

    }],

    "sds": { // (1)

    "cluster": {

        "name": "service_discovery",

        "type": "strict_dns",

        "connect_timeout_ms": 5000,

        "lb_type": "round_robin",

        "hosts": [{

            "url": "tcp://192.168.99.100:9200" // (2)

        }]

    },

    "refresh_delay_ms": 3000 // (3)

    }

}

route_config属性内的每个侦听器定义路由配置(1)。第一条路由配置为人员服务,由集群服务1(2)处理,第二条路由服务 2集群进行产品服务处理。因此,我们的服务可在http://192.168.99.100:10000/personhttp://192.168.99.100:10000/product地址下找到。

1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

{

    "name": "http_connection_manager",

    "config": {

        "codec_type": "auto",

        "stat_prefix": "ingress_http",

        "route_config": { // (1)

            "virtual_hosts": [{

        "name": "service",

        "domains": ["*"],

        "routes": [{

            "prefix": "/person", // (2)

            "cluster": "service1"

        }, {

            "prefix": "/product", // (3)

            "cluster": "service2"

        }]

            }]

        },

    "filters": [{

        "name": "router",

        "config": {}

        }]

    }

}

构建微服务

Envoy代理上的路由已经配置。我们仍然没有运行微服务。它们的实现基于Spring Boot框架,除了公开REST API之外,它还提供对象列表上的简单操作以及在发现服务器上注册/取消注册服务。这是负责该注册的@Service bean。在正常关闭之前onApplicationEvent,应用程序启动和destroy方法之后会触发该方法。

1

2

3

4

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

三十

31

32

33

34

35

36

37

38

39

40

41

@Service

public class PersonRegister implements ApplicationListener<ApplicationReadyEvent> {

 

    private static final Logger LOGGER = LoggerFactory.getLogger(PersonRegister.class);

 

    private String ip;

    @Value("${server.port}")

    private int port;

    @Value("${spring.application.name}")

    private String appName;

    @Value("${envoy.discovery.url}")

    private String discoveryUrl;

 

    @Autowired

    RestTemplate template;

 

    @Override

    public void onApplicationEvent(ApplicationReadyEvent event) {

        LOGGER.info("PersonRegistration.register");

        try {

            ip = InetAddress.getLocalHost().getHostAddress();

            DiscoveryHost host = new DiscoveryHost();

            host.setPort(port);

            host.setIpAddress(ip);

            template.postForObject(discoveryUrl + "/v1/registration/{service}", host, DiscoveryHosts.class, appName);

        } catch (Exception e) {

            LOGGER.error("Error during registration", e);

        }

    }

 

    @PreDestroy

    public void destroy() {

        try {

            template.delete(discoveryUrl + "/v1/registration/{service}/{ip}/", appName, ip);

            LOGGER.info("PersonRegister.unregistered: service={}, ip={}", appName, ip);

        } catch (Exception e) {

            LOGGER.error("Error during unregistration", e);

        }

    }

 

}

正确关闭Spring Boot应用程序的最佳方法是通过其Actuator端点。为服务启用此类端点包括spring-boot-starter-actuator项目依赖项。默认情况下禁用关闭,因此我们应添加以下属性application.yml以启用它,并另外禁用默认安全性(endpoints.shutdown.sensitive=false)。现在,只需调用POST / shutdown,我们就可以停止Spring Boot应用程序并测试取消注册方法。

1

2

3

4

endpoints:

  shutdown:

    enabled: true

    sensitive: false

对于微服务我们也一样,我们也构建了docker镜像。这是人员服务Dockerfile,它允许覆盖默认服务和SDS端口。

1

2

3

4

6

FROM openjdk:alpine

MAINTAINER Piotr Minkowski <[email protected]>

ADD target/person-service.jar person-service.jar

ENV DISCOVERY_URL http://192.168.99.100:9200

ENTRYPOINT ["java", "-jar", "/person-service.jar"]

EXPOSE 9300

要使用自定义侦听端口构建映像并运行服务容器,请键入以下docker命令。

1

2

docker build -t piomin/person-service .

docker run -d --name person-service -p 9301:9300 piomin/person-service

分布式跟踪

现在是最后一块拼图的时候了 - Zipkin追踪。应在那里发送与所有传入请求相关的统计信息。Envoy代理中配置的第一部分是inside tracing属性,它指定HTTP跟踪器的全局设置。

1

2

3

4

6

7

8

9

10

11

"tracing": {

    "http": {

        "driver": {

            "type": "zipkin",

            "config": {

                "collector_cluster": "zipkin",

                "collector_endpoint": "/api/v1/spans"

            }

        }

    }

}

Zipkin连接的网络位置和设置应定义为集群成员。

1

2

3

4

6

7

8

9

10

11

"clusters": [{

    "name": "zipkin",

    "connect_timeout_ms": 5000,

    "type": "strict_dns",

    "lb_type": "round_robin",

    "hosts": [

      {

        "url": "tcp://192.168.99.100:9411"

      }

    ]

}]

我们还应该tracing在HTTP连接管理器配置中添加新的部分(1)。字段operation_name是必需的并设置范围名称。仅支持“入口”和“出口”值。

1

2

3

4

6

7

8

9

10

11

"listeners": [{

    "filters": [{

        "name": "http_connection_manager",

        "config": {

            "tracing": { // (1)

                "operation_name": "ingress" // (2)

            }

            // ...

        }

    }]

}]

Zipkin服务器可以使用其Docker镜像启动。

1

docker run -d --name zipkin -p 9411:9411 openzipkin/zipkin

概要

这是用于测试目的的运行Docker容器的列表。您可能还记得我们有Zipkin,Envoy,自定义发现,两个人员服务实例和一个产品服务实例。您可以通过调用POST / person添加一些人物对象,并通过调用GET / person显示所有人的列表。应根据服务发现中的条目在两个实例之间对请求进行负载平衡。

sample-envoy-proxy

有关每个请求的信息将发送到Zipkin,服务名称为-service-cluster Envoy代理运行参数。

sample-envoy-proxy

转载于:https://my.oschina.net/xiaominmin/blog/1840743