大纲
Envoy 的强大功能之一是支持动态配置,当使用动态配置时,我们不需要重新启动 Envoy 进程就可以生效。Envoy 通过从磁盘文件或网络接口读取配置,动态地重新加载配置。动态配置使用所谓的发现服务 API,指向配置的特定部分。这些 API 也被统称为xDS 即 (xxx discovery service)
注意:
Envoy的发现API开发模式是,按照Envoy指定的接口名称,请求参数,响应值,自己开发,即需要满足Envoy的规范
Envoy动态配置支持文件方式,grpc接口和, restful接口,其中 grpc接口/REST接口 的配置提供者(自己开发的项目)也被称为控制平面
实现方式:
Envoy 内部有多个发现服务 API (xDS):
Envoy 的 API 有 v2 v3 目前主流版本是 v3
官方文档 https://www.envoyproxy.io/docs/envoy/latest/configuration/overview/xds_api
xDS API 可以使用restful接口和grpc接口开发,只要满足指定的接口名称和DiscoveryRequest,DiscoveryResponse参数和响应对象即可
例如以下就是一个EDS的接口
/v3/discovery:endpoints (即自己写的controller的mapping是/v3/discovery:endpoints)
@RequestMapping("/v3/discovery:endpoints")
静态配置与动态配置结合
例如
static_resources:
listeners:
- name: my_listener
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 15200
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: my-http-filter
http_filters:
- name: envoy.filters.http.router
stat_prefix: my_listener_http
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: user-service
clusters:
- name: user-service
type: EDS #这里就是使用动态配置的方式实现endpoint的动态发现
connect_timeout: 0.5s
eds_cluster_config:
eds_config:
resource_api_version: V3
api_config_source:
api_type: REST
transport_api_version: V3
cluster_names: [edscluster]
refresh_delay: 10s
- name: edscluster
type: STATIC
connect_timeout: 0.5s
hosts:
- socket_address:
address: 192.168.0.218
port_value: 7590
使用dynamic_resources 配置动态内容
例如
dynamic_resources:
ads_config:
api_type: GRPC
transport_api_version: V3
grpc_services:
- envoy_grpc:
cluster_name: xds_cluster
cds_config:
resource_api_version: V3
api_config_source:
api_type: GRPC
transport_api_version: V3
grpc_services:
- envoy_grpc:
cluster_name: xds_cluster
lds_config:
resource_api_version: V3
api_config_source:
api_type: GRPC
transport_api_version: V3
grpc_services:
- envoy_grpc:
cluster_name: xds_cluster
当envoy没有读取到配置时会一直使用默认的配置,所以如果控制平面宕机后还是会保持配置
每个 xDS API 都有给定的资源类型:
v2版本
LDS : envoy.api.v2.Listener
RDS : envoy.api.v2.RouteConfiguration
CDS : envoy.api.v2.Cluster
EDS :envoy.api.v2.ClusterLoadAssignment (EDS就是配置 endpoint)
v3版本
envoy.config.listener.v3.Listener
envoy.config.route.v3.RouteConfiguration,
envoy.config.route.v3.ScopedRouteConfiguration,
envoy.config.route.v3.VirtualHost
envoy.config.cluster.v3.Cluster
envoy.config.endpoint.v3.ClusterLoadAssignment (EDS endpoint 返回的resources 对象类型)
envoy.extensions.transport_sockets.tls.v3.Secret
envoy.service.runtime.v3.Runtime
即接口返回DiscoveryResponse 内部的resources 是以上类型
本次测试 envoy的版本为v1.16.0 使用docker镜像部署
配置文件如下
node:
cluster: mycluster
id: test-id
# 这是一段静态配置
static_resources:
listeners:
- name: my_listener
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 15200 #配置一个静态的listener 监听来自任意IP的请求15200端口的http请求
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager #注意指定filters
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: my-http-filter
http_filters:
- name: envoy.filters.http.router
stat_prefix: my_listener_http
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"] #任意域名的请求
routes:
- match:
prefix: "/" # 任意url的请求
route:
cluster: user-service # 路由到user-service 集群
# 配置集群
clusters:
- name: user-service
type: EDS #模式指定为EDS
connect_timeout: 0.5s # 配置连接超时时间
eds_cluster_config:
eds_config:
resource_api_version: V3 #指定使用V3版本接口
api_config_source:
api_type: REST #使用restful的方式
transport_api_version: V3 #指定使用V3版本接口
cluster_names: [edscluster]
refresh_delay: 10s # 配置刷新频率
# 这里配置的是envoy EDS接口的提供服务即控制平面
- name: edscluster
type: STATIC
connect_timeout: 0.5s # 配置连接超时时间
# envoy会去请求 192.168.0.218:7590/v3/discovery:endpoints 这个接口 获取endpoint配置信息
# 代码见 my-docker-demo-envoy-plane/DataPlaneEndpointControllerV3.java
hosts:
- socket_address:
address: 192.168.0.218
port_value: 7590
启动 envoy 镜像
docker run -p 5201:5201 -p 15200:15200 -v /ops/envoy:/etc/envoy envoyproxy/envoy:v1.16.0
envoy 启动后可以看到开始调用 EDS接口,由于还没启动服务此时会报错
EDS接口使用java springboot 开发
注意点如下:
DiscoveryResponse 格式如下
{
"version_info": ...,
"resources": [],
"type_url": ...,
"nonce": ...,
"control_plane": {...}
}
整体的返回值json字符串如下
{
"versionInfo": "1.0.0",
"resources": [{
"@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
"clusterName": "user-service",
"endpoints": [{
"lbEndpoints": [ {
"endpoint": {
"address": {
"socketAddress": {
"address": "10.244.1.203",
"portValue": 5588
}
}
}
}]
}]
}]
}
如果自己拼接json字符串感觉比较麻烦,可以使用envoy-api包
io.envoyproxy.controlplane
api
1.0.39
这个包,里面有xDS中的各种资源对象 以及grpc接口
也可以使用官方提供的 java控制面板项目 打包编译后得到api包,里面也有xDS中的各种资源对象
java 代码如下
@RequestMapping(value="/v3/discovery:endpoints" , produces = {"application/json;charset=UTF-8"})
public String discovery(HttpServletRequest req) throws Exception {
//json 字符串拼接
//String json = staticJson();
/**
* 构建返回EDS 配置json 字符串
*/
String json = useBean();
return json;
}
/**
* @return
*/
private String useBean() throws Exception {
/**
* 以下资源类出自
*
*
io.envoyproxy.controlplane
api
1.0.39
*
*/
//配置上游服务(类似nginx upstream)
SocketAddress sa1 = SocketAddress.newBuilder().setAddress("10.244.0.190").setPortValue(5588).build();
SocketAddress sa2 = SocketAddress.newBuilder().setAddress("10.244.1.203").setPortValue(5588).build();
Address address1 = Address.newBuilder().setSocketAddress(sa1).build();
Address address2 = Address.newBuilder().setSocketAddress(sa2).build();
Endpoint end1 = Endpoint.newBuilder().setAddress(address1).build();
Endpoint end2 = Endpoint.newBuilder().setAddress(address2).build();
LbEndpoint lb1 = LbEndpoint.newBuilder().setEndpoint(end1).build();
LbEndpoint lb2 = LbEndpoint.newBuilder().setEndpoint(end2).build();
LocalityLbEndpoints llb = LocalityLbEndpoints.newBuilder().addLbEndpoints(lb1).addLbEndpoints(lb2).build();
ClusterLoadAssignment cla = ClusterLoadAssignment.newBuilder()
.setClusterName("user-service")
.addEndpoints(llb).build();
DiscoveryResponse dr = DiscoveryResponse.newBuilder().setVersionInfo("1.0.0")
.addResources(Any.pack(cla))
.build();
JsonFormat.TypeRegistry typeRegistry = JsonFormat.TypeRegistry.newBuilder()
.add(ClusterLoadAssignment.getDescriptor())
.build();
JsonFormat.Printer printer = JsonFormat.printer()
.usingTypeRegistry(typeRegistry);
return printer.print(dr);
}
grpc的关键
没使用http2_protocol_options 配置会出现如下异常
io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception: Unexpected HTTP/1.x request: POST /envoy.service.endpoint.v3.EndpointDiscoveryService/StreamEndpoints
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception.connectionError(Http2Exception.java:109) ~[grpc-netty-shaded-1.48.1.jar:1.48.1]
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.readClientPrefaceString(Http2ConnectionHandler.java:302) ~[grpc-netty-shaded-1.48.1.jar:1.48.1]
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.decode(Http2ConnectionHandler.java:239) ~[grpc-netty-shaded-1.48.1.jar:1.48.1]
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler.decode(Http2ConnectionHandler.java:438) [grpc-netty-shaded-1.48.1.jar:1.48.1]
返回值未指定typeUrl
023-08-16 06:14:49.608][8][warning][config] [source/common/config/grpc_mux_impl.cc:155] Ignoring the message for type URL as it has no current subscribers.
关键配置 envoy.yaml 如下
# 指定集群名称
# 动态配置需要指定节点集群名称
node:
cluster: mycluster
id: test-id
# 这是一段静态配置
static_resources:
listeners:
- name: my_listener
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 15200 #配置一个静态的listener 监听来自任意IP的请求15200端口的http请求
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager #注意指定filters
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: my-http-filter
http_filters:
- name: envoy.filters.http.router
stat_prefix: my_listener_http
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"] #任意域名的请求
routes:
- match:
prefix: "/" # 任意url的请求
route:
cluster: user-service # 路由到user-service 集群
# 配置集群
clusters:
- name: user-service
type: EDS #模式指定为EDS
connect_timeout: 0.5s # 配置连接超时时间
eds_cluster_config:
eds_config:
resource_api_version: V3 #指定使用V3版本接口
api_config_source:
api_type: GRPC #使用grpc的方式
transport_api_version: V3 #指定使用V3版本接口
# 指定grpc_services 对应的集群
# 这里将使用下面定义的集群
grpc_services:
- envoy_grpc:
cluster_name: edscluster
# 这里配置的是envoy EDS接口的提供服务即控制平面
- name: edscluster
type: STATIC
connect_timeout: 0.5s # 配置连接超时时间
# 这里是一个关键,必须指定http2_protocol_options 即使用http2
http2_protocol_options: {}
hosts:
- socket_address:
address: 192.168.0.218
port_value: 7899
关键java代码
public class EndpointDiscoveryServiceGrpcImpl extends EndpointDiscoveryServiceGrpc.EndpointDiscoveryServiceImplBase {
/**
* 这个接口是客户端模式
*
*/
@Override
public io.grpc.stub.StreamObserver streamEndpoints(
io.grpc.stub.StreamObserver responseObserver) {
System.out.println("run grpc ...");
/**
* 创建StreamObserver对象
*/
StreamObserver so = new StreamObserver() {
@Override
public void onNext(DiscoveryRequest request) {
//接收客户端每一次发送的数据,返回给客户端
//showRequest(request);
SocketAddress sa1 = SocketAddress.newBuilder().setAddress("10.244.0.214").setPortValue(5588).build();
SocketAddress sa2 = SocketAddress.newBuilder().setAddress("10.244.0.201").setPortValue(5588).build();
Address address1 = Address.newBuilder().setSocketAddress(sa1).build();
Address address2 = Address.newBuilder().setSocketAddress(sa2).build();
Endpoint end1 = Endpoint.newBuilder().setAddress(address1).build();
Endpoint end2 = Endpoint.newBuilder().setAddress(address2).build();
LbEndpoint lb1 = LbEndpoint.newBuilder().setEndpoint(end1).build();
LbEndpoint lb2 = LbEndpoint.newBuilder().setEndpoint(end2).build();
LocalityLbEndpoints llb = LocalityLbEndpoints.newBuilder().addLbEndpoints(lb1).addLbEndpoints(lb2).build();
ClusterLoadAssignment cla = ClusterLoadAssignment.newBuilder()
/**
* 这里配置的ClusterName 应该是路由对应的cluster name 而不是 node中的cluster
* route: { cluster: user-service }
*/
.setClusterName("user-service")
.addEndpoints(llb).build();
final DiscoveryResponse dr = DiscoveryResponse.newBuilder().setVersionInfo("1.0.0")
.setTypeUrl("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment")
.addResources(Any.pack(cla))
.build();
/**
* 客户端模式这里不会去关闭StreamObserver
* 即不会调用 responseObserver.onCompleted();方法
*/
responseObserver.onNext(dr);
System.out.println("send DiscoveryResponse ");
}
@Override
public void onError(Throwable t) {
System.out.println("onError");
t.printStackTrace();
}
@Override
public void onCompleted() {
//当客户端数据发送完毕后调用此方法,返回客户端
SocketAddress sa1 = SocketAddress.newBuilder().setAddress("10.244.0.214").setPortValue(5588).build();
SocketAddress sa2 = SocketAddress.newBuilder().setAddress("10.244.0.201").setPortValue(5588).build();
Address address1 = Address.newBuilder().setSocketAddress(sa1).build();
Address address2 = Address.newBuilder().setSocketAddress(sa2).build();
Endpoint end1 = Endpoint.newBuilder().setAddress(address1).build();
Endpoint end2 = Endpoint.newBuilder().setAddress(address2).build();
LbEndpoint lb1 = LbEndpoint.newBuilder().setEndpoint(end1).build();
LbEndpoint lb2 = LbEndpoint.newBuilder().setEndpoint(end2).build();
LocalityLbEndpoints llb = LocalityLbEndpoints.newBuilder().addLbEndpoints(lb1).addLbEndpoints(lb2).build();
ClusterLoadAssignment cla = ClusterLoadAssignment.newBuilder()
.setClusterName("user-service")
.addEndpoints(llb).build();
final DiscoveryResponse dr = DiscoveryResponse.newBuilder().setVersionInfo("1.0.0")
.addResources(Any.pack(cla))
.build();
System.out.println("onCompleted");
responseObserver.onNext(dr);
responseObserver.onCompleted();
}
};
return so;