概述
本文将介紹
ASM入口網關(ingressgateway)支援協定轉碼的能力,使使用者及其用戶端可以使用HTTP/JSON通路服務網格内的gRPC服務。
ASM入口網關可将HTTP/JSON轉碼為gRPC。完整的http請求、grpc轉碼,以及grpc請求流程,如下圖所示。

- ASM控制平面下發用于grpc轉碼的EnvoyFilter、用于路由到gRPC服務端口的規則配置Gateway和VirtualService到ASM入口網關,入口網關接收後即時加載生效。
- 入口網關收到使用者或其用戶端http協定的請求後,将進行路由規則比對和協定轉換,然後以grpc協定請求服務網格内的gRPC服務。
- 入口網關收到後端服務的grpc響應,再将其轉換為http的響應傳回給請求方。
原理和目标
Envoy作為ServiceMesh資料平面的proxy元件,内置了多種http擴充過濾器,本文涉及的是其中的http到grpc的轉碼器。為了啟用這個過濾器,Envoy定義了相應的過濾器協定
config.filter.http.transcoder.v2.GrpcJsonTranscoder。為此,ServiceMesh的控制平面需要定義一個EnvoyFilter來聲明在什麼地方哪個階段啟用這個過濾器,然後下發這個EnvoyFilter在指定環節啟用轉碼器。另一方面,這個轉碼器需要知道grpc服務的具體協定,包括描述grpc服務的Proto Descriptors檔案、grpc服務全名清單。
Envoy是ServiceMesh入口網關的核心元件。是以,實作入口網關轉碼的關鍵任務就是生成這個用于grpc轉碼的EnvoyFilter。
由于EnvoyFilter的定義較為複雜,從頭寫一個這樣的EnvoyFilter比較費時費力且容易出錯,本文提供了自動化生成EnvoyFilter的工具grpc-transcoder(
https://github.com/AliyunContainerService/grpc-transcoder),一方面保證EnvoyFilter的定義正确,另一方面可以節省你的時間以及省去編寫EnvoyFilter的痛苦。
實戰
1 gRPC服務
如下圖所示,通常使用者的gRPC服務是從protobuf格式的gRPC服務協定檔案
.proto
的定義開始的。
grpc-service-project
封裝grpc接口并完成實作,後續步驟包括建構鏡像,編寫Deployment,最終通過ASM,将grpc服務以POD的形式部署到ACK叢集。
為了支援http轉碼grpc,需要确認
.proto
中是否定義了支援轉碼的聲明。這裡以
hello-servicemesh-grpc示例中的
proto為例,示意如下。
import "google/api/annotations.proto";
service LandingService {
//Unary RPC
rpc talk (TalkRequest) returns (TalkResponse) {
option(google.api.http) = {
get: "/v1/talk/{data}/{meta}"
};
}
...
}
message TalkRequest {
string data = 1;
string meta = 2;
}
其中,5-7行是為了支援轉碼新增的聲明,第1行是引入相應能力的
.proto
聲明。如果你的
.proto
中沒有相關聲明,請補充。這裡需要提示的是,補充這個
.proto
隻是為了生成pb(Proto Descriptors)檔案,補充後并不影響
grpc-service-project
及後續步驟。
2 生成Proto Descriptors檔案
這裡以
為例,使用
protoc
指令從
landing.proto
生成
landing.proto-descriptor
檔案。如果沒有安裝
protoc
請在
此處下載下傳。
# https://github.com/AliyunContainerService/hello-servicemesh-grpc
proto_path={path/to/hello-servicemesh-grpc}/proto
# https://github.com/grpc-ecosystem/grpc-gateway/tree/master/third_party/
proto_dep_path={path/to/third_party}
protoc \
--proto_path=${proto_path} \
--proto_path=${proto_dep_path} \
--include_imports \
--include_source_info \
--descriptor_set_out=landing.proto-descriptor \
"${proto_path}"/landing.proto
```
#### 3 生成EnvoyFilter
此時,我們已經有了Proto Descriptors檔案`landing.proto-descriptor`。接下來,通過[**grpc-transcoder**](https://github.com/AliyunContainerService/grpc-transcoder)生成EnvoyFilter。調用接口的示意請求如下。
```bash
grpc-transcoder \
--version 1.7 \
--service_port 9996 \
--service_name grpc-server-svc \
--proto_pkg org.feuyeux.grpc \
--proto_svc LandingService \
--descriptor landing.proto-descriptor
```
參數說明:
- `version`:由于ASM是托管叢集,是以需要保證版本更新對EnvoyFilter的影響。是以這個參數是必選值。
- `service_port`:對應的grpc服務(詳見下圖)端口。
- `service_name`:對應的grpc服務(詳見下圖)名稱。
- `proto_pkg`:對應的grpc服務`.proto`中包名的定義。
- `proto_svc`:對應的grpc服務`.proto`中服務名的定義。
- `descriptor`:Proto Descriptors檔案本地路徑。

使用上述請求自動生成的EnvoyFilter(`grpc-transcoder-envoyfilter.yaml`)如下。
```yaml
#Generated by ASM(http://servicemesh.console.aliyun.com)
#GRPC Transcoder EnvoyFilter[1.7]
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: grpc-transcoder-grpc-server-svc
spec:
workloadSelector:
labels:
app: istio-ingressgateway
configPatches:
- applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
portNumber: 9996
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
subFilter:
name: "envoy.filters.http.router"
proxy:
proxyVersion: ^1\.7.*
patch:
operation: INSERT_BEFORE
value:
name: envoy.grpc_json_transcoder
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
proto_descriptor_bin: Ctl4ChVnb29nbGUvYXBpL2h0dHAucHJ...
services:
- org.feuyeux.grpc.LandingService
print_options:
add_whitespace: true
always_print_primitive_fields: true
always_print_enums_as_ints: false
preserve_proto_field_names: false
```
#### 4 ASM入口網關啟用轉碼器
通過工具生成EnvoyFilter後,我們通過ASM管控台送出生成的EnvoyFilter(`grpc-transcoder-envoyfilter.yaml`),轉碼器将即時生效。

上述操作相當于執行指令`kubectl --kubeconfig mesh_config -f grpc-transcoder-envoyfilter.yaml -n istio-system`。到此,配置的步驟就完成了,完整的流程示意圖如下。

#### 5 envoy配置校驗
接下來我們校驗入口網關的envoy配置是否生效。依次執行如下指令,驗證envoy動态配置中是否包含解碼器GrpcJsonTranscoder的配置:
```bash
#擷取入口網關POD名稱
ingressgateway_pod=$(kubectl get pod -l app="istio-ingressgateway" -n istio-system -o jsonpath='{.items[0].metadata.name}')
#時間戳
timestamp=$(date "+%Y%m%d-%H%M%S")
#擷取envoy動态配置并儲存到dynamic_listeners-"$timestamp".json
kubectl -n istio-system exec $ingressgateway_pod \
-c istio-proxy \
-- curl -s "http://localhost:15000/config_dump?resource=dynamic_listeners" >dynamic_listeners-"$timestamp".json
#确認配置中存在GrpcJsonTranscoder
grep -B3 -A7 GrpcJsonTranscoder dynamic_listeners-"$timestamp".json
```
envoy動态配置中應包含類似如下的配置:
```json
{
"name": "envoy.grpc_json_transcoder",
"typed_config": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder",
"services": [
"org.feuyeux.grpc.LandingService"
],
"print_options": {
"add_whitespace": true,
"always_print_primitive_fields": true
},
...
```
#### 6 功能校驗
最後,我們進行功能校驗,驗證HTTP請求網格内GRPC服務。
我們再次給出grpc服務接口的定義:
```protobuf
rpc talk (TalkRequest) returns (TalkResponse) {
option(google.api.http) = {
get: "/v1/talk/{data}/{meta}"
};
}
```
為了驗證響應資訊,這裡給出`.proto`中定義的響應聲明:
```protobuf
message TalkResponse {
int32 status = 1;
repeated TalkResult results = 2;
}
message TalkResult {
//timestamp
int64 id = 1;
//enum
ResultType type = 2;
// id:result uuid
// idx:language index
// data: hello
// meta: serverside language
map kv = 3;
}
enum ResultType {
OK = 0;
FAIL = 1;
}
```
依次執行如下指令,驗證這個grpc服務接口可以通過HTTP請求入口網關實作調用:
```bash
#擷取入口網關IP
INGRESS_IP=$(k -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
#http請求入口網關的9996端口,/v1/talk/{data}/{meta}路徑
curl http://$INGRESS_IP:9996/v1/talk/0/java
我們應該得到如下響應:
{
"status": 200,
"results": [
{
"id": "699882576081691",
"type": "OK",
"kv": {
"data": "Hello",
"meta": "JAVA",
"id": "8c175d5c-d8a3-4197-a7f8-6e3e0ab1fe59",
"idx": "0"
}
}
]
}