本文档第一部分介绍一下 Envoy v2 API,第二部分给出了一个 Java 编写的简陋 EDS Server 示例。
Envoy v2 API
Envoy 的 API 有 v1 和 v2 两个版本,目前主流版本是 v2。它具有以下特点:
- 通过 gRPC 流式传输对 xDS API 的更新,这减少了资源的需求并且可以降低更新延迟。
- 一种新的 REST-JSON API。其中 JSON/YAML 格式是通过 proto3 规范的 JSON 映射机制派生出来的。
- 通过文件系统、REST-JSON 或 gRPC 端点传递更新。
- 通过扩展端点分配 API 进行高级负载均衡,并向管理服务器报告负载以及资源的利用率。
- 当需要更强的一致性和排序属性时,Envoy v2 API 仍然可以保持基准最终一致性模型。
Envoy 的 Bootstrap 配置可以采用 完全静态、部分静态或者完全动态。
静态配置
下面提供了一个完全静态引导配置的例子:
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: 127.0.0.1, port_value: 10000 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["example.com"]
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.2, port_value: 1234 }}]
在上面的例子中,我们配置了一个 envoy实例,监听 127.0.0.1:10000,支持 http 协议访问,http 访问域名为:http://example.com。接收到的所有http流量,转发给 127.0.0.2:1234 的服务。这个例子中 some_service 这个cluster中 hosts 是固定的(127.0.0.2:1234),不利于扩展。
EDS为动态的配置
将配置 envoy 的配置调整如下。
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: 127.0.0.1, port_value: 10000 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["example.com"]
routes:
- match: { prefix: "/" }
route: { cluster: some_service }
http_filters:
- name: envoy.router
clusters:
- name: some_service
connect_timeout: 0.25s
lb_policy: ROUND_ROBIN
type: EDS
eds_cluster_config:
eds_config:
api_config_source:
api_type: GRPC
cluster_names: [xds_cluster]
- name: xds_cluster
connect_timeout: 0.25s
type: STATIC
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
hosts: [{ socket_address: { address: 127.0.0.3, port_value: 5678 }}]
这样就实现 some_service 这个 cluster 的 hosts 的动态配置了。新的配置中,some_service 这个 cluster 的 hosts 是 EDS(Endpoint Discovery Service) 的返回值决定的,就是说 EDS 会返回 some_service 这个 cluster 的 hosts 的列表。新配置中,EDS 服务的地址定义在 xds_cluster 这个 cluster中。
EDS 服务的地址是:127.0.0.3:5678,会返回 proto3 编码的响应格式如下:
version_info: "0"
resources:
- "@type": type.googleapis.com/envoy.api.v2.ClusterLoadAssignment
cluster_name: some_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.2
port_value: 1234
基于xDS的动态配置
下面提供了完全动态的 bootstrap 配置,其中属于管理服务器的所有资源都是通过 xDS 发现的。
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 127.0.0.1, port_value: 9901 }
dynamic_resources:
lds_config:
api_config_source:
api_type: GRPC
cluster_names: [xds_cluster]
cds_config:
api_config_source:
api_type: GRPC
cluster_names: [xds_cluster]
static_resources:
clusters:
- name: xds_cluster
connect_timeout: 0.25s
type: STATIC
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
hosts: [{ socket_address: { address: 127.0.0.3, port_value: 5678 }}]
这里我们假设 127.0.0.3:5678 提供完整的 xDS。
LDS服务的响应格式如下:
version_info: "0"
resources:
- "@type": type.googleapis.com/envoy.api.v2.Listener
name: listener_0
address:
socket_address:
address: 127.0.0.1
port_value: 10000
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
stat_prefix: ingress_http
codec_type: AUTO
rds:
route_config_name: local_route
config_source:
api_config_source:
api_type: GRPC
cluster_names: [xds_cluster]
http_filters:
- name: envoy.router
RDS 服务的响应格式如下:
version_info: "0"
resources:
- "@type": type.googleapis.com/envoy.api.v2.RouteConfiguration
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: some_service }
CDS 服务的响应如下:
version_info: "0"
resources:
- "@type": type.googleapis.com/envoy.api.v2.Cluster
name: some_service
connect_timeout: 0.25s
lb_policy: ROUND_ROBIN
type: EDS
eds_cluster_config:
eds_config:
api_config_source:
api_type: GRPC
cluster_names: [xds_cluster]
EDS 服务的响应如下:
version_info: "0"
resources:
- "@type": type.googleapis.com/envoy.api.v2.ClusterLoadAssignment
cluster_name: some_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.2
port_value: 1234
Envoy EDS实战示例
下面给出一个使用 Java 自行实现 EDS服务的示例,这里我们使用了 REST 接口。
实现 EDS-Server
首先创建一个 SpringBoot Web项目。实现 EDS 比较复杂的部分就是响应报文和请求报文的示例实现。
先看下请求报文(省略了setter 和 getter 部分)。
public class DiscoveryRequest {
private DiscoveryNode node;
@JsonProperty("resource_names")
private List<String> resourceNames;
@Override
public String toString() {
return "node = { " + this.node + "}, resource_names = " + this.resourceNames;
}
}
public class DiscoveryNode {
@JsonProperty("build_version")
private String buildVersion;
private String cluster;
private String id;
@Override
public String toString() {
return "build_version = " + this.buildVersion + ", cluster = " + this.cluster +
", id = " + this.id;
}
}
这样生成的请求报文类格式类似下面这个样子:
{
"node": {
"cluster": "myCluster",
"id": "test-id",
"build_version": "44f8c365a1f1798f0af776f6aa64279dc68f5666/1.12.1/Clean/RELEASE/BoringSSL"
},
"resource_names": [
"myservice"
]
}
请求报文格式比较简单,下面看下格式较为复杂的返回报文(依旧省略 setter 和 getter)。
public class DiscoveryResponse {
@JsonProperty("version_info")
private String versionInfo;
private List<ResponseResource> resources = new ArrayList<>();
}
public class ResponseResource {
@JsonProperty("@type")
private String type;
@JsonProperty("cluster_name")
private String clusterName;
@JsonProperty("endpoints")
private List<EndPoints> endPoints = new ArrayList<>();
}
public class EndPoints {
@JsonProperty("lb_endpoints")
private List<LbEndPoint> lbEndPoints;
public class LbEndPoint {
@JsonProperty("endpoint")
private EndPoint endPonit;
public class EndPoint {
private DiscoveryAddress address;
}
public class DiscoveryAddress {
@JsonProperty("socket_address")
private SocketAddress socketAddress;
public DiscoveryAddress() {
}
public DiscoveryAddress(SocketAddress socketAddress) {
this.socketAddress = socketAddress;
}
}
public class SocketAddress {
private String address;
@JsonProperty("port_value")
private int port;
public SocketAddress() {
}
public SocketAddress(String address, int port) {
this.address = address;
this.port = port;
}
}
生成的返回报文格式如下:
{
"resources": [
{
"endpoints": [
{
"lb_endpoints": [
{
"endpoint": {
"address": {
"socket_address": {
"address": "tcp://127.0.0.1",
"port": 8888
}
}
}
}
]
}
],
"@type": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
"cluster_name": "myService"
}
],
"version_info": "v1"
}
这里注意请求和返回报文的 json 格式一定不能错,否则无法和 envoy 进行交互。
再实现 host 实例用来保存服务地址。
public class DiscoveryHost {
// IP地址
@JsonProperty("ip_address")
private String ipAddress;
// 端口
private int port;
private DiscoveryTags tags;
@Override
public String toString() {
return "ip_address = " + this.ipAddress + ", port = " + this.port
+ ", tags = [" + this.tags + "]";
}
}
public class DiscoveryHosts {
private List<DiscoveryHost> hosts = new ArrayList<>();
private String service;
@Override
public String toString() {
return "service = " + this.service + ",hosts = " + this.hosts;
}
}
/**
* 服务发现中的元数据信息
*/
public class DiscoveryTags {
// 分区
@JsonProperty("az")
private String zone;
// 是否灰度
private boolean cannary;
// 权重
@JsonProperty("load_balancing_weight")
private int loadBalancingWeight;
@Override
public String toString() {
return "az = " + this.zone + ", cannary = " + this.cannary +
", load_balancing_weight = " + this.loadBalancingWeight;
}
}
这样所有的对象都生成完毕了,下面看下对外暴露的接口,包括服务发现的接口和操作服务的接口。
@RestController
public class DiscoveryController {
private static final Logger LOGGER = LoggerFactory.getLogger(DiscoveryController.class);
private int version = 1;
private Map<String, List<DiscoveryHost>> serviceHosts = new HashMap<>();
/**
* envoy eds服务接口
* @param request
* @return
*/
@PostMapping("/v2/discovery:endpoints")
public DiscoveryResponse discoveryEndpoints(@RequestBody DiscoveryRequest request) {
LOGGER.info("Dicovery Request: {}", request);
// 读取输入
DiscoveryNode node = request.getNode();
String id = node.getId();
String cluster = node.getCluster();
List<String> resourceNames = request.getResourceNames();
//构建返回
DiscoveryResponse response = new DiscoveryResponse();
response.setVersionInfo("v" + version);
ResponseResource resource = new ResponseResource();
List<EndPoints> endPointsList = new ArrayList<>();
List<LbEndPoint> lbEndPoints = new ArrayList<>();
for(String resourceName : resourceNames) {
if(serviceHosts.containsKey(resourceName)) {
List<DiscoveryHost> hosts = serviceHosts.get(resourceName);
for(DiscoveryHost host : hosts) {
EndPoint endPoint = new EndPoint();
endPoint.setAddress(new DiscoveryAddress(new SocketAddress(host.getIpAddress(), host.getPort())));
LbEndPoint lbEndPoint = new LbEndPoint();
lbEndPoint.setEndPonit(endPoint);
lbEndPoints.add(lbEndPoint);
}
}
resource.setType("type.googleapis.com/envoy.api.v2.ClusterLoadAssignment");
resource.setClusterName(resourceName);
if(lbEndPoints.size() > 0) {
EndPoints endPoints = new EndPoints();
endPoints.setLbEndPoints(lbEndPoints);
endPointsList.add(endPoints);
}
resource.setEndPoints(endPointsList);
response.getResources().add(resource);
return response;
}
return null;
}
/**
* 获取当前所有服务
* @return
*/
@GetMapping("/edsservice")
public List<DiscoveryHosts> getAllServices() {
List<DiscoveryHosts> hostsList = new ArrayList<>();
for(Map.Entry<String, List<DiscoveryHost>> entry : this.serviceHosts.entrySet()) {
DiscoveryHosts hosts = new DiscoveryHosts();
hosts.setService(entry.getKey());
hosts.setHosts(entry.getValue());
hostsList.add(hosts);
}
return hostsList;
}
/**
* 获取某个服务
* @param serviceName
* @return
*/
@GetMapping("/edsservice/{serviceName}")
public DiscoveryHosts getHosts(@PathVariable("serviceName") String serviceName) {
LOGGER.info("getHostsByServiceName: service={}", serviceName);
DiscoveryHosts hostsList = new DiscoveryHosts();
hostsList.setHosts(this.serviceHosts.get(serviceName));
return hostsList;
}
/**
* 添加某个服务
* @param serviceName
* @param hosts
*/
@PostMapping("/edsservice/{serviceName}")
public void addHosts(@PathVariable("serviceName") String serviceName,
@RequestBody DiscoveryHosts hosts) {
LOGGER.info("addHost: service={}, body={}", serviceName, hosts);
if (!this.serviceHosts.containsKey(serviceName)) {
this.serviceHosts.put(serviceName, hosts.getHosts());
this.version++;
}
}
/**
* 删除某个服务
* @param serviceName
*/
@DeleteMapping("/edsservice/{serviceName}")
public void removeHost(@PathVariable("serviceName") String serviceName) {
LOGGER.info("removeHost: service={}", serviceName);
this.serviceHosts.remove(serviceName);
}
/**
* 修改某个服务
* @param serviceName
* @param hosts
*/
@PutMapping("/edsservice/{serviceName}")
public void updateHost(@PathVariable("serviceName") String serviceName,
@RequestBody DiscoveryHosts hosts) {
LOGGER.info("updateHost: service={}, body={}", serviceName, hosts);
if(this.serviceHosts.containsKey(serviceName)) {
this.serviceHosts.put(serviceName, hosts.getHosts());
this.version++;
}
}
}
这样整个 EDS Server 的代码就开发完毕了。
实现上游服务
下面实现用来接收 Enovy 发出的请求的上游服务,依旧创建一个 SpringBoot Web 项目。
添加2个简单的接口。
@RestController
public class UpstreamController {
private static final String uuid = UUID.randomUUID().toString();
@RequestMapping("/")
public String index() {
return "Hello, My UUID is " + uuid;
}
@RequestMapping("/healthz")
public String health() {
return "OK";
}
}
添加配置文件 application.properties 。
server.port=${PORT:8081}
上游服务编写完毕,整体比较简单。
创建镜像
创建 SpringBoot 镜像的方法 Envoy 入门实战 和部署一个SpringBoot应用
都已经介绍过,这里不再赘述。这里创建一个 EDS服务镜像,2个上游服务镜像,上游服务镜像使用的端口分别是 8081 和 8082。
然后创建一个 envoy 镜像,envoy 镜像使用的配置文件 envoy-config.yaml 如下。
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address:
address: 127.0.0.1
port_value: 9000
node:
cluster: mycluster
id: testid
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: service_backend }
http_filters:
- name: envoy.router
clusters:
- name: service_backend
type: EDS
connect_timeout: 0.25s
eds_cluster_config:
service_name: myservice
eds_config:
api_config_source:
api_type: REST
cluster_names: [edscluster]
refresh_delay: 5s
- name: edscluster
type: STATIC
connect_timeout: 0.25s
hosts: [{ socket_address: { address: 127.0.0.1, port_value: 8080 }}]
其中需要注意的是由于 tomcat 不支持 URL 中存在"_",所以这里 cluster 名称使用了 edscluster。
envoy 镜像的 Dockerfile 内容如下:
FROM envoyproxy/envoy-alpine:latest
COPY envoy-config.yaml /etc/envoy/envoy.yaml
创建 envoy 镜像。
docker build -t envoytest2:v1.0.0 .
这样我们就拥有了如下镜像。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
envoy-discovery 1.0-SNAPSHOT ab2722bc242c 44 hours ago 122MB
envoytest2 v1.0.0 669ef4043e24 45 hours ago 40.9MB
upstream-service 1.0-SNAPSHOT b2e02f942699 45 hours ago 122MB
upstream-service2 1.0-SNAPSHOT 1eb7d9738765 45 hours ago 122MB
启动环境
下面依次启动镜像。
$ docker run -p 8081:8081 upstream-service:1.0-SNAPSHOT
$ docker run -p 8082:8082 upstream-service2:1.0-SNAPSHOT
$ docker run -p 10000:10000 --name envoytest2 envoytest2:v1.0.0
$ docker run --network=container:envoytest envoy-discovery:1.0-SNAPSHOT
现在在浏览器中中访问 http://192.168.99.100:10000/ (其中192.168.99.100是 Docker 虚拟机的地址)会看到如下结果。
找不到上游服务.png使用 Postman 向 http://192.168.99.100:8080/edsservice/myservice 发送如下 POST 请求,将上游服务注册到 Eds 服务之中。
{
"hosts": [
{
"ip_address": "192.168.99.100",
"port": 8081,
"tags": {
"server01": "8081",
"canary": false,
"load_balancing_weight": 50
}
},
{
"ip_address": "192.168.99.100",
"port": 8082,
"tags": {
"server01": "8082",
"canary": false,
"load_balancing_weight": 50
}
}
]
}
然后再次访问 http://192.168.99.100:10000/ ,会看到上游服务的页面。
发现上游服务1.png再次访问,可以看到另一个上游服务的页面。
发现上游服务2.png这是因为两个上游服务的权重一样, envoy 收到请求后会轮流定向到两个上游服务。
网友评论