轉自簡書:https://www.jianshu.com/p/90f9ee98ce70
Email:[email protected]
本文介紹Envoy的一些基本概念以及實踐操作,以期通過本文的介紹讓讀者可以瞭解到Envoy的原理,幫助讀者理解Istio的Data Panel層實現。
寫作本文的初衷是源於近期項目中需要做的微服務平臺,平臺需要針對微服務做控制。在技術選型的過程中比較了Envoy、Istio的實現,最終決定以Envoy來完成特定的業務需求。在使用Envoy的過程中,由於文檔資料較少,實踐中遇到了一些困難,故將實踐中的一些理解和過程記錄下來,方便大家查閱,減少彎路。
1. Service Mesh
關於Service Mesh不是本篇文章的重點,但是理解Service Mesh的概念、優勢、發展,對理解本文有很大的幫助,此處羅列下Service Mesh的幾篇文章,希望讀者花一些時間先閱讀一下文章的內容,對Service Mesh有個瞭解和認識。
雖然目前的Service Mesh已經進入了以Istio、Conduit爲代表的第二代,由Data Panel、Control Panel兩部分組成。但是以Istio爲例,它也沒有自己去實現Data Panel,而是在現有的Data Panel實現上做了Control Panel來達成目標。
所以說要掌握Istio,或者說要理解Service Mesh,首先需要掌握Data Panel的實現,而Envoy就是其中的一種實現方案。關於Envoy是什麼,可以做什麼,有什麼優點,可以到Envoy的官網上查看詳細信息,本文注重於Envoy的一些實踐操作,重點關心怎麼利用Envoy實現一些需求。
2. Envoy術語
要深入理解Envoy,首先需要先了解一下Envoy中的一些術語。
-
Host:能夠進行網絡通信的實體(如服務器上的應用程序)。
-
Downstream:下游主機連接到Envoy,發送請求並接收響應。
-
Upstream:上游主機接收來自Envoy連接和請求並返回響應。
-
Listener:可以被下游客戶端連接的命名網絡(如端口、unix套接字)。
-
Cluster:Envoy連接到的一組邏輯上相似的上游主機。
-
Mesh:以提供一致的網絡拓撲的一組主機。
-
Runtime configuration:與Envoy一起部署的外置實時配置系統。
envoy-term.png
3. Envoy的啓動
官方提供了Envoy的Docker鏡像,本文中使用的鏡像名是envoyproxy/envoy-alpine。
鏡像中已經將Envoy安裝到/usr/local/bin目錄下,可以先看看envoy進程的help信息。
# /usr/local/bin/envoy --help
USAGE:
/usr/local/bin/envoy [--disable-hot-restart] [--max-obj-name-len
<uint64_t>] [--max-stats <uint64_t>] [--mode
<string>] [--parent-shutdown-time-s <uint32_t>]
[--drain-time-s <uint32_t>]
[--file-flush-interval-msec <uint32_t>]
[--service-zone <string>] [--service-node
<string>] [--service-cluster <string>]
[--hot-restart-version] [--restart-epoch
<uint32_t>] [--log-path <string>] [--log-format
<string>] [-l <string>]
[--local-address-ip-version <string>]
[--admin-address-path <string>] [--v2-config-only]
[--config-yaml <string>] [-c <string>]
[--concurrency <uint32_t>] [--base-id <uint32_t>]
[--] [--version] [-h]
envoy進程啓動的時候需要指定一些參數,其中最重要的是--config-yaml
參數,用於指定envoy進程啓動的時候需要讀取的配置文件地址。Docker中配置文件默認是放在/etc/envoy
目錄下,配置文件的文件名是envoy.yaml
。
所以在啓動容器的時候需要將自定義的envoy.yaml配置文件掛載到指定目錄下替換掉默認的配置文件。
/usr/local/bin/envoy -c <path to config>.{json,yaml,pb,pb_text} --v2-config-only
tip:envoy默認的日誌級別是info,對於開發階段需要進行調試的話,調整日誌級別到debug是非常有用的,可以在啓動參數中添加-l debug
來將日誌級別進行切換。
4. Envoy的啓動配置
在介紹Envoy的配置文件之前,先介紹一下Envoy的API。Envoy提供了兩個版本的API,v1和v2版本API。現階段v1版本已經不建議使用了,通常都是使用v2的API。
v2的API提供了兩種方式的訪問,一種是HTTP Rest的方式訪問,還有一種GRPC的訪問方式。關於GRPC的介紹可以參考官方文檔,在後面的文章中只實現了GRPC的API。
Envoy的啓動配置文件分爲兩種方式:靜態配置和動態配置。
-
靜態配置是將所有信息都放在配置文件中,啓動的時候直接加載。
-
動態配置需要提供一個Envoy的服務端,用於動態生成Envoy需要的服務發現接口,這裏叫XDS,通過發現服務來動態的調整配置信息,Istio就是實現了v2的API。
4.1 靜態配置
以一個最簡化的靜態配置來做示例,體驗一下envoy。
下面是envoy.yaml配置文件:
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: 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: 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.1, port_value: 80 }}]
在此基礎上啓動兩個容器,envoyproxy容器和nginx容器,nginx容器共享envoyproxy容器的網絡,以此來模擬sidecar。
docker run -d -p 10000:10000 -v `pwd`/envoy.yaml:/etc/envoy/envoy.yaml --name envoyproxy envoyproxy/envoy-alpine
docker run -d --network=container:envoyproxy --name nginx nginx</pre>
根據配置文件的規則,envoy監聽在10000端口,同時該端口也在宿主機的10000端口上暴露出來。當有請求到達監聽上後,envoy會對所有請求路由到some_service這個cluster上,而該cluster的upstream指向本地的80端口,也就是nginx服務上。
static.png
4.2 動態配置
動態配置可以實現全動態,即實現LDS(Listener Discovery Service)、CDS(Cluster Discovery Service)、RDS(Route Discovery Service)、EDS(Endpoint Discovery Service),以及ADS(Aggregated Discovery Service)。
ADS不是一個實際意義上的XDS,它提供了一個匯聚的功能,以實現需要多個同步XDS訪問的時候可以在一個stream中完成的作用。
下面的圖通過在靜態配置的基礎上,比較直觀的表示出各個發現服務所提供的信息。
xds.png
由此,典型的動態配置文件如下:
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 127.0.0.1, port_value: 9901 }
dynamic_resources:
cds_config:
ads: {}
lds_config:
ads: {}
ads_config:
api_type: GRPC
cluster_names: [xds_cluster]
static_resources:
clusters:
- name: xds_cluster
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
hosts: [{ socket_address: { address: envoy-server, port_value: 50051 }}]
tip:動態配置和靜態配置最大的區別在於,啓動的時候一定要指定cluster和id,這兩個參數表示該Envoy進程屬於哪個cluster,id要求在相同的cluster下唯一,以表示不同的指向發現服務的連接信息。這兩個參數可以在envoy的啓動命令中添加--service-cluster
和--service-node
,也可以在envoy.yaml配置文件中指定node.cluster
和node.id
。
5. 深入實驗
接下來的實驗主要以動態配置的方式來實現一個簡單的需求,首先描述一下需求場景:
有兩個微服務,一個是envoy-web,一個envoy-server。
-
envoy-web相當於下圖中的front-envoy作爲對外訪問的入口。
-
envoy-server相當於下圖中的service_1和service_2,是內部的一個微服務,部署2個實例。
demo.png
envoy-server有3個API,分別是/envoy-server/hello、/envoy-server/hi、/envoy-server/self,目的是測試envoy對於流入envoy-server的流量控制,對外只允許訪問/envoy-server/hello和/envoy-server/hi兩個API,/envoy-server/self不對外暴露服務。
envoy-web也有3個API,分別是/envoy-web/hello、/envoy-web/hi、/envoy-web/self,目的是測試envoy對於流出envoy-web的流量控制,出口流量只允許/envoy-web/hello和/envoy-web/self兩個訪問出去。
最終的實驗:外部只能訪問envoy-web暴露的接口
-
當訪問/envoy-web/hello接口時返回envoy-server的/hello接口的數據,表示envoy-web作爲客戶端訪問envoy-server返回服務響應的結果。
-
當訪問/envoy-web/hi接口時,envoy-web的envoy攔截住出口流量,限制envoy-web向envoy-server發送請求,對於前端用戶返回mock數據。
-
當訪問/envoy-web/self接口時,envoy-web出口流量可以到達envoy-server容器,但是envoy-server在入口流量處控制住了此次請求,拒絕訪問envoy-server服務,對於前端用戶返回mock數據。
5.1 靜態配置
首先以靜態配置的方式先實現功能。
5.1.1 編寫服務代碼
服務代碼分爲envoy-web和envoy-server兩個服務,採用SpringBoot的方式,下面記錄一些重要的代碼片段。
- envoy-server
@RestController
public class HelloRest {
private static final Logger LOGGER = LoggerFactory.getLogger(HelloRest.class);
@GetMapping("/envoy-server/hello")
public String hello() {
LOGGER.info("get request from remote, send response, say hello");
return "hello";
}
@GetMapping("/envoy-server/hi")
public String hi() {
LOGGER.info("get request from remote, send response, say hi");
return "hi";
}
@GetMapping("/envoy-server/self")
public String self() {
LOGGER.info("get request from remote, send response, say self");
return "self";
}
}
- envoy-web
@RestController
public class HelloController {
private static final Logger LOGGER = LoggerFactory.getLogger(HelloController.class);
@Autowired
private RestTemplate template;
@GetMapping("/envoy-web/local")
public String sayLocal() {
LOGGER.info("get request, send response");
return "local";
}
@GetMapping("/envoy-web/hello")
public String sayHello() {
String url = "http://127.0.0.1:10000/envoy-server/hello";
LOGGER.info("get request, send rest template to {}", url);
return getRemote(url, "mock value for hello");
}
@GetMapping("/envoy-web/hi")
public String sayHi() {
String url = "http://127.0.0.1:10000/envoy-server/hi";
LOGGER.info("get request, send rest template to {}", url);
return getRemote(url, "mock value for hi");
}
@GetMapping("/envoy-web/self")
public String saySelf() {
String url = "http://127.0.0.1:10000/envoy-server/self";
LOGGER.info("get request, send rest template to {}", url);
return getRemote(url, "mock value for self");
}
private String getRemote(String url, String mock) {
try {
ResponseEntity<String> response = template.getForEntity(url, String.class);
return response.getBody();
} catch (Exception e) {
LOGGER.error("error happens: {}", e);
return mock;
}
}
}
tip:爲簡化起見,代碼只是介紹對出入流量的控制,直接在envoy-web上訪問了本地的envoy端口進行轉發流量,實際代碼中可以用服務名:服務端口號
訪問,而此時爲了使得envoy仍然可以攔截入和出的流量,可以配置iptables(Istio的實現中也是使用了iptables)。
5.1.2 編寫配置文件
針對不同的服務,也配置了兩份envoy.yaml配置文件。
- envoy-server
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9900 }
static_resources:
listeners:
- name: listener_ingress
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: "/envoy-server/hello" }
route: { cluster: cluster_server }
- match: { prefix: "/envoy-server/hi" }
route: { cluster: cluster_server }
http_filters:
- name: envoy.router
clusters:
- name: cluster_server
connect_timeout: 0.5s
type: STATIC
lb_policy: ROUND_ROBIN
hosts:
- { socket_address: { address: 127.0.0.1, port_value: 8081 }}
- envoy-web
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9900 }
static_resources:
listeners:
- name: listener_ingress
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: "/envoy-web/" }
route: { cluster: cluster_ingress }
- match: { prefix: "/envoy-server/hello" }
route: { cluster: cluster_egress }
- match: { prefix: "/envoy-server/self" }
route: { cluster: cluster_egress }
http_filters:
- name: envoy.router
clusters:
- name: cluster_ingress
connect_timeout: 0.5s
type: STATIC
lb_policy: ROUND_ROBIN
hosts:
- { socket_address: { address: 127.0.0.1, port_value: 8080 }}
- name: cluster_egress
connect_timeout: 0.5s
type: STATIC
lb_policy: ROUND_ROBIN
hosts:
- { socket_address: { address: 172.17.0.2, port_value: 10000 }}
- { socket_address: { address: 172.17.0.3, port_value: 10000 }}
5.1.3 啓動測試
#envoy-server1
docker run -d -v `pwd`/envoy-server.yaml:/etc/envoy/envoy.yaml --name envoyproxy-server1 envoyproxy/envoy-alpine /usr/local/bin/envoy --service-cluster envoy-server --service-node 1 -c /etc/envoy/envoy.yaml --v2-config-only
docker run -d --network=container:envoyproxy-server1 --name envoy-server1 envoy-server:1.1
#envoy-server2
docker run -d -v `pwd`/envoy-server.yaml:/etc/envoy/envoy.yaml --name envoyproxy-server2 envoyproxy/envoy-alpine /usr/local/bin/envoy --service-cluster envoy-server --service-node 2 -c /etc/envoy/envoy.yaml --v2-config-only
docker run -d --network=container:envoyproxy-server2 --name envoy-server2 envoy-server:1.1
#envoy-web
docker run -d -p 10000:10000 -v `pwd`/envoy-web.yaml:/etc/envoy/envoy.yaml --name envoyproxy-web envoyproxy/envoy-alpine /usr/local/bin/envoy --service-cluster envoy-web --service-node 1 -c /etc/envoy/envoy.yaml --v2-config-only
docker run -d --network=container:envoyproxy-web --name envoy-web envoy-web:1.1
當容器部署完畢之後,可以直接訪問以下3個url,其中hi和self的訪問返回的是mock數據,雖然同爲mock數據,但是這兩個url其實是不相同的,一個是在envoy出口流量處做的控制,一個是在envoy入口流量處做的控制,其中的細節可以再去品味品味。
example.png
5.2 動態配置
動態配置需要實現發現服務,通過GRPC的方式獲取相應。
動態的配置文件在前面的內容中已經有過介紹,最重要的是需要提供一個發現服務,對外提供XDS服務,下面以其中的一個LDS作爲介紹,其他XDS實現類似。
- 服務端:既然作爲服務,就需要對外提供接口服務。
public class GrpcService {
private Server server;
private static final int PORT = 50051;
private void start() throws IOException {
server = ServerBuilder.forPort(PORT)
.addService(new LdsService())
.addService(new CdsService())
.addService(new RdsService())
.addService(new EdsService())
.addService(new AdsService())
.build()
.start();
System.err.println("Server started, listening on " + PORT);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.err.println("*** shutting down gRPC server since JVM is shutting down");
GrpcService.this.stop();
System.err.println("*** server shut down");
}));
}
private void stop() {
if (server != null) {
server.shutdown();
}
}
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
public static void main(String[] args) throws IOException, InterruptedException {
final GrpcService server = new GrpcService();
server.start();
server.blockUntilShutdown();
}
}
- XDS:通過GRPC生成服務端的stub文件,實現LdsServer繼承自ListenerDiscoveryServiceGrpc.ListenerDiscoveryServiceImplBase,需要實現streamListeners方法。
public class LdsService extends ListenerDiscoveryServiceGrpc.ListenerDiscoveryServiceImplBase {
private static final Logger LOGGER = LogManager.getLogger();
@Override
public StreamObserver<Discovery.DiscoveryRequest> streamListeners(StreamObserver<Discovery.DiscoveryResponse> responseObserver) {
return new StreamObserver<Discovery.DiscoveryRequest>() {
@Override
public void onNext(Discovery.DiscoveryRequest request) {
XdsHelper.getInstance().buildAndSendResult(request, responseObserver);
}
@Override
public void onError(Throwable throwable) {
LOGGER.warn("Error happens", throwable);
}
@Override
public void onCompleted() {
LOGGER.info("LdsService completed");
}
};
}
}
6. 總結
至此,基本介紹完Envoy使用的一些常見問題,在實現的時候也會有其他一些細節需要注意。
比如,envoy作爲一個服務之間網絡請求的代理,如何攔截全部的入和出流量?
Istio給了一個很好的解決方案,就是通過iptables。它會使用一個特定的uid(默認1337)用戶運行envoy進程,iptables對於1337用戶的流量不做攔截。下面就是參考Istio的iptables.sh做的一個實現:
uname=envoy
uid=1337
iptalbes -t nat -F
iptables -t nat -I PREROUTING -p tcp -j REDIRECT --to-ports 10000
iptables -t nat -N ENVOY_OUTPUT
iptables -t nat -A OUTPUT -p tcp -j ENVOY_OUTPUT
iptables -t nat -A ENVOY_OUTPUT -p tcp -d 127.0.0.1/32 -j RETURN
iptables -t nat -A ENVOY_OUTPUT -m owner --uid-owner ${uid} -j RETURN
iptables -t nat -A ENVOY_OUTPUT -p tcp -j REDIRECT --to-ports 10000
更多的實現細節則需要再研究挖掘了,同時也歡迎一起討論。
作者:gaulzhw
鏈接:https://www.jianshu.com/p/90f9ee98ce70
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。