envoy xDS 動態配置 java控制平面開發 支持restful grpc 動態endpoint配置
大綱
- 基礎概念
- Envoy 動態配置API
- 配置方式
- 動靜結合的配置方式
- 純動態配置方式
- 實戰
基礎概念
Envoy 的強大功能之一是支持動態配置,當使用動態配置時,我們不需要重新啟動 Envoy 進程就可以生效。Envoy 通過從磁盤文件或網絡接口讀取配置,動態地重新加載配置。動態配置使用所謂的發現服務 API,指向配置的特定部分。這些 API 也被統稱為xDS 即 (xxx discovery service)
注意:
Envoy的發現API開發模式是,按照Envoy指定的接口名稱,請求參數,響應值,自己開發,即需要滿足Envoy的規范
Envoy動態配置支持文件方式,grpc接口和, restful接口,其中 grpc接口/REST接口 的配置提供者(自己開發的項目)也被稱為控制平面
實現方式:
- 文件方式: 監聽文件的變化動態修改
- grpc接口: 使用的tcp長連接
- REST接口: 使用的http輪詢的方式實現
Envoy 動態配置API
API類型
Envoy 內部有多個發現服務 API (xDS):
- 監聽器發現服務(LDS listener discovery service) 使用 LDS,Envoy 可以在運行時發現監聽器,包括所有的過濾器棧、HTTP 過濾器和對 RDS 的引用。(即動態配置 listener 類似nginx配置虛擬主機)
- 擴展配置發現服務(ECDS) 使用 ECDS,Envoy 可以獨立于監聽器獲取擴展配置(例如,HTTP 過濾器配置)。
- 路由發現服務(RDS route discovery service) 使用 RDS,Envoy 可以在運行時發現 HTTP 連接管理器過濾器的整個路由配置。與 EDS 和 CDS 相結合,我們可以實現復雜的路由拓撲結構。(即動態配置路由)
- 虛擬主機發現服務(VHDS) 使用 VHDS 允許 Envoy 從路由配置中單獨請求虛擬主機。當路由配置中有大量的虛擬主機時,就可以使用這個功能。
- 寬泛路由發現服務(SRDS) 使用 SRDS,可以把路由表分解成多個部分。當有大的路由表時,就可以使用這個 API。
- 集群發現服務(CDS cluster discovery service ) 使用 CDS,Envoy 可以發現上游集群。Envoy 將通過排空和重新連接所有現有的連接池來優雅地添加、更新或刪除集群。Envoy 在初始化時不必知道所有的集群,因為我們可以在以后使用 CDS 配置它們。(即動態配置集群)
- 端點發現服務(EDS endpoint discovery service) 使用 EDS,Envoy 可以發現上游集群的成員。 (即動態配置后端服務類似nginx upstream)
- 秘密發現服務(SDS) 使用 SDS,Envoy 可以為其監聽器發現秘密(證書和私鑰,TLS 會話密鑰),并為對等的證書驗證邏輯進行配置。
- 運行時發現服務(RTDS) 使用 RTDS,Envoy 可以動態地發現運行時層。
API 版本
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_listeneraddress:socket_address: protocol: TCPaddress: 0.0.0.0port_value: 15200filter_chains:- filters:- name: envoy.filters.network.http_connection_managertyped_config:"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManagerstat_prefix: my-http-filterhttp_filters:- name: envoy.filters.http.routerstat_prefix: my_listener_httpcodec_type: AUTOroute_config:name: local_routevirtual_hosts:- name: local_servicedomains: ["*"]routes:- match: prefix: "/" route: cluster: user-service clusters:- name: user-servicetype: EDS #這里就是使用動態配置的方式實現endpoint的動態發現connect_timeout: 0.5seds_cluster_config: eds_config:resource_api_version: V3api_config_source:api_type: RESTtransport_api_version: V3cluster_names: [edscluster]refresh_delay: 10s - name: edsclustertype: STATICconnect_timeout: 0.5shosts: - socket_address: address: 192.168.0.218port_value: 7590
純動態配置方式
使用dynamic_resources 配置動態內容
例如
dynamic_resources:ads_config:api_type: GRPCtransport_api_version: V3grpc_services:- envoy_grpc:cluster_name: xds_clustercds_config:resource_api_version: V3api_config_source:api_type: GRPCtransport_api_version: V3grpc_services:- envoy_grpc:cluster_name: xds_clusterlds_config:resource_api_version: V3api_config_source:api_type: GRPCtransport_api_version: V3grpc_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鏡像部署
基于envoy xDS api v3版本 java restful實現
step1 配置 envoy.yaml
配置文件如下
node:cluster: myclusterid: test-id# 這是一段靜態配置
static_resources:listeners:- name: my_listeneraddress:socket_address: protocol: TCPaddress: 0.0.0.0port_value: 15200 #配置一個靜態的listener 監聽來自任意IP的請求15200端口的http請求filter_chains:- filters:- name: envoy.filters.network.http_connection_manager #注意指定filterstyped_config:"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManagerstat_prefix: my-http-filterhttp_filters:- name: envoy.filters.http.routerstat_prefix: my_listener_httpcodec_type: AUTOroute_config:name: local_routevirtual_hosts:- name: local_servicedomains: ["*"] #任意域名的請求routes:- match: prefix: "/" # 任意url的請求route: cluster: user-service # 路由到user-service 集群# 配置集群 clusters:- name: user-servicetype: 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: edsclustertype: STATICconnect_timeout: 0.5s # 配置連接超時時間# envoy會去請求 192.168.0.218:7590/v3/discovery:endpoints 這個接口 獲取endpoint配置信息# 代碼見 my-docker-demo-envoy-plane/DataPlaneEndpointControllerV3.javahosts: - socket_address: address: 192.168.0.218port_value: 7590
啟動 envoy 鏡像
docker run -p 5201:5201 -p 15200:15200 -v /ops/envoy:/etc/envoy envoyproxy/envoy:v1.16.0
envoy 啟動后可以看到開始調用 EDS接口,由于還沒啟動服務此時會報錯
step2 java 程序開發
EDS接口使用java springboot 開發
注意點如下:
- 1 接口必須是 /v3/discovery:endpoints
- 2 動態配置需要是一個json 字符串 并且滿足endpoint需要的格式
- 3 返回值必須是一個DiscoveryResponse service.discovery.v3.DiscoveryResponse
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包
<dependency><groupId>io.envoyproxy.controlplane</groupId><artifactId>api</artifactId><version>1.0.39</version></dependency>
這個包,里面有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 {/*** 以下資源類出自* * <dependency><groupId>io.envoyproxy.controlplane</groupId><artifactId>api</artifactId><version>1.0.39</version></dependency>* *///配置上游服務(類似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);
}
基于 envoy xDS api v3版本 java grpc實現
grpc的關鍵
- 1 使用envoy api包 實現對應的grpc 服務
- 2 返回值需要指定typeUrl
- 3 配置文件需要加入 http2_protocol_options 指定使用http2
沒使用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: myclusterid: test-id# 這是一段靜態配置
static_resources:listeners:- name: my_listeneraddress:socket_address: protocol: TCPaddress: 0.0.0.0port_value: 15200 #配置一個靜態的listener 監聽來自任意IP的請求15200端口的http請求filter_chains:- filters:- name: envoy.filters.network.http_connection_manager #注意指定filterstyped_config:"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManagerstat_prefix: my-http-filterhttp_filters:- name: envoy.filters.http.routerstat_prefix: my_listener_httpcodec_type: AUTOroute_config:name: local_routevirtual_hosts:- name: local_servicedomains: ["*"] #任意域名的請求routes:- match: prefix: "/" # 任意url的請求route: cluster: user-service # 路由到user-service 集群# 配置集群 clusters:- name: user-servicetype: 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: edsclustertype: STATICconnect_timeout: 0.5s # 配置連接超時時間# 這里是一個關鍵,必須指定http2_protocol_options 即使用http2http2_protocol_options: {}hosts: - socket_address: address: 192.168.0.218port_value: 7899
關鍵java代碼
public class EndpointDiscoveryServiceGrpcImpl extends EndpointDiscoveryServiceGrpc.EndpointDiscoveryServiceImplBase {/*** 這個接口是客戶端模式* */@Overridepublic io.grpc.stub.StreamObserver<io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest> streamEndpoints(io.grpc.stub.StreamObserver<io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse> responseObserver) {System.out.println("run grpc ...");/*** 創建StreamObserver<DiscoveryRequest>對象*/StreamObserver<DiscoveryRequest> so = new StreamObserver<DiscoveryRequest>() {@Overridepublic 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 ");}@Overridepublic void onError(Throwable t) {System.out.println("onError");t.printStackTrace();}@Overridepublic 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;