繼MicroServices之後,ServiceMesh是又一個推動軟體工業的革命性技術。其服務治理的方法論,不僅改變了技術實作的方式和社會分工。
運作于資料平面的使用者服務與治理服務的各種規則徹底解耦。運作于控制平面的規則定義元件,将流量控制的具體規則推送給運作于資料平面的proxy,proxy通過對使用者服務的ingress和egress的實際控制,最終實作服務治理。
原本需要服務開發者程式設計實作的服務發現、容錯、灰階、流量複制等能力,被ServiceMesh非侵入的方式實作。此外,ServiceMesh還提供了通路控制、認證授權等功能,進一步減輕了使用者服務的開發成本。
阿裡雲提供的服務網格(
ASM )是基于容器服務( ACK )之上的托管版ServiceMesh,在提供完整的ServiceMesh能力的同時(ASM還在底層橫向拉通了阿裡雲雲原生的各種能力,不在本篇講述範圍),免去了使用者搭建和運維ServiceMesh平台istio的繁瑣工作。本篇将分享如何将我們自己的GRPC服務,托管到阿裡雲的服務網格中。1. grpc服務
grpc協定相比http而言,既具備http跨作業系統和程式設計語言的好處,又提供了基于流的通信優勢。而且,grpc逐漸成為工業界的标準,一旦我們的grpc服務可以mesh化,那麼更多的非标準協定就可以通過轉為grpc協定的方式,低成本地接入服務網格,實作跨技術棧的服務通信。
grpc服務的示例部分使用最普遍的程式設計語言Java及最高效的程式設計架構SpringBoot。示例的拓撲示意如下:

1.1 springboot
common——proto2java
示例工程包含三個子產品,分别是
common
、
provider
consumer
。其中,
common
負責将定義grpc服務的protobuf轉換為java的rpc模闆代碼;後兩者對其依賴,分别實作grpc的服務端和用戶端。
示例工程的protobuf定義如下,實作了兩個方法
SayHello
和
SayBye
。
SayHello
的入參是一個字元串,傳回一個字元串;
SayBye
隻有一個字元串類型的出參。
syntax = "proto3";
import "google/protobuf/empty.proto";
package org.feuyeux.grpc;
option java_multiple_files = true;
option java_package = "org.feuyeux.grpc.proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayBye (google.protobuf.Empty) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string reply = 1;
}
common
建構過程使用
protobuf-maven-plugin
自動生成rpc模闆代碼。
provider——grpc-spring-boot-starter
provider
依賴
grpc-spring-boot-starter
包以最小化編碼,實作grpc服務端邏輯。示例實作了兩套grpc方法,以在後文示範不同流量的傳回結果不同。
第一套方法示意如下:
@GRpcService
public class GreeterImpl extends GreeterImplBase {
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String message = "Hello " + request.getName() + "!";
HelloReply helloReply = HelloReply.newBuilder().setReply(message).build();
responseObserver.onNext(helloReply);
responseObserver.onCompleted();
}
@Override
public void sayBye(com.google.protobuf.Empty request, StreamObserver<HelloReply> responseObserver) {
String message = "Bye bye!";
HelloReply helloReply = HelloReply.newBuilder().setReply(message).build();
responseObserver.onNext(helloReply);
responseObserver.onCompleted();
}
}
第二套方法示意如下:
@GRpcService
public class GreeterImpl2 extends GreeterImplBase {
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String message = "Bonjour " + request.getName() + "!";
HelloReply helloReply = HelloReply.newBuilder().setReply(message).build();
responseObserver.onNext(helloReply);
responseObserver.onCompleted();
}
@Override
public void sayBye(com.google.protobuf.Empty request, StreamObserver<HelloReply> responseObserver) {
String message = "au revoir!";
HelloReply helloReply = HelloReply.newBuilder().setReply(message).build();
responseObserver.onNext(helloReply);
responseObserver.onCompleted();
}
}
consumer——RESTful
consumer
的作用有兩個,一個是對外暴露RESTful服務,一個是作為grpc的用戶端調用grpc服務端provider。示意代碼如下:
@RestController
public class GreeterController {
private static String GRPC_PROVIDER_HOST;
static {
GRPC_PROVIDER_HOST = System.getenv("GRPC_PROVIDER_HOST");
if (GRPC_PROVIDER_HOST == null || GRPC_PROVIDER_HOST.isEmpty()) {
GRPC_PROVIDER_HOST = "provider";
}
LOGGER.info("GRPC_PROVIDER_HOST={}", GRPC_PROVIDER_HOST);
}
@GetMapping(path = "/hello/{msg}")
public String sayHello(@PathVariable String msg) {
final ManagedChannel channel = ManagedChannelBuilder.forAddress(GRPC_PROVIDER_HOST, 6565)
.usePlaintext()
.build();
final GreeterGrpc.GreeterFutureStub stub = GreeterGrpc.newFutureStub(channel);
ListenableFuture<HelloReply> future = stub.sayHello(HelloRequest.newBuilder().setName(msg).build());
try {
return future.get().getReply();
} catch (InterruptedException | ExecutionException e) {
LOGGER.error("", e);
return "ERROR";
}
}
@GetMapping("bye")
public String sayBye() {
final ManagedChannel channel = ManagedChannelBuilder.forAddress(GRPC_PROVIDER_HOST, 6565)
.usePlaintext()
.build();
final GreeterGrpc.GreeterFutureStub stub = GreeterGrpc.newFutureStub(channel);
ListenableFuture<HelloReply> future = stub.sayBye(Empty.newBuilder().build());
try {
return future.get().getReply();
} catch (InterruptedException | ExecutionException e) {
LOGGER.error("", e);
return "ERROR";
}
}
}
這裡需要注意的是
GRPC_PROVIDER_HOST
變量,我們在
ManagedChannelBuilder.forAddress(GRPC_PROVIDER_HOST, 6565)
中使用到這個變量,以獲得provider服務的位址。相信你已經發現,服務開發過程中,我們沒有進行任何服務發現能力的開發,而是從系統環境變量裡擷取這個值。而且,在該值為空時,我們使用了一個hardcode值
provider
。沒錯,這個值将是後文配置在isito中的provider服務的約定值。
1.2 curl&grpcurl
本節将講述示例工程的本地啟動和驗證。首先我們通過如下腳本建構和啟動provider和consumer服務:
# terminal 1
mvn clean install -DskipTests -U
java -jar provider/target/provider-1.0.0.jar
# terminal 2
export GRPC_PROVIDER_HOST=localhost
java -jar consumer/target/consumer-1.0.0.jar
我們使用curl以http的方式請求consumer:
# terminal 3
$ curl localhost:9001/hello/feuyeux
Hello feuyeux!
$ curl localhost:9001/bye
Bye bye!
最後我們使用
grpcurl直接測試provider:
$ grpcurl -plaintext -d @ localhost:6565 org.feuyeux.grpc.Greeter/SayHello <<EOM
{
"name":"feuyeux"
}
EOM
{
"reply": "Hello feuyeux!"
}
$ grpcurl -plaintext localhost:6565 org.feuyeux.grpc.Greeter/SayBye
{
"reply": "Bye bye!"
}
1.2 docker
服務驗證通過後,我們制作三個docker鏡像,以作為deployment部署到kubernetes上。這裡以provider的dockerfile為例:
FROM openjdk:8-jdk-alpine
ARG JAR_FILE=provider-1.0.0.jar
COPY ${JAR_FILE} provider.jar
COPY grpcurl /usr/bin/grpcurl
ENTRYPOINT ["java","-jar","/provider.jar"]
建構鏡像和推送到遠端倉庫的腳本示意如下:
docker build -f grpc.provider.dockerfile -t feuyeux/grpc_provider_v1:1.0.0 .
docker build -f grpc.provider.dockerfile -t feuyeux/grpc_provider_v2:1.0.0 .
docker build -f grpc.consumer.dockerfile -t feuyeux/grpc_consumer:1.0.0 .
docker push feuyeux/grpc_provider_v1:1.0.0
docker push feuyeux/grpc_provider_v2:1.0.0
docker push feuyeux/grpc_consumer:1.0.0
本地啟動服務驗證,示意如下:
# terminal 1
docker run --name provider2 -p 6565:6565 feuyeux/grpc_provider_v2:1.0.0
# terminal 2
docker exec -it provider2 sh
grpcurl -v -plaintext localhost:6565 org.feuyeux.grpc.Greeter/SayBye
exit
# terminal 3
export LOCAL=$(ipconfig getifaddr en0)
docker run --name consumer -e GRPC_PROVIDER_HOST=${LOCAL} -p 9001:9001 feuyeux/grpc_consumer
# terminal 4
curl -i localhost:9001/bye
1.3 istio
驗證完鏡像後,我們進入重點。本節将完整講述如下拓撲的服務治理配置:
Deployment
consumer的deployment聲明示意如下:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: consumer
version: v1
...
containers:
- name: consumer
image: feuyeux/grpc_consumer:1.0.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9001
provider1的deployment聲明示意如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: provider-v1
labels:
app: provider
version: v1
...
containers:
- name: provider
image: feuyeux/grpc_provider_v1:1.0.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6565
provider2的deployment聲明示意如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: provider-v2
labels:
app: provider
version: v2
...
containers:
- name: provider
image: feuyeux/grpc_provider_v2:1.0.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6565
Deployment中使用到了前文建構的三個鏡像。在容器服務中不存在時(IfNotPresent)即會拉取。
這裡需要注意的是,provider1和provider2定義的
labels.app
都是
provider
,這個标簽是provider的唯一辨別,隻有相同才能被Service的Selector找到并認為是一個服務的兩個版本。
服務發現
provider的Service聲明示意如下:
apiVersion: v1
kind: Service
metadata:
name: provider
labels:
app: provider
service: provider
spec:
ports:
- port: 6565
name: grpc
protocol: TCP
selector:
app: provider
前文已經講到,服務開發者并不實作服務注冊和服務發現的功能,也就是說示例工程不需要諸如zookeeper/etcd/Consul等元件的用戶端調用實作。Service的域名将作為服務注冊的名稱,服務發現時通過這個名稱就能找到相應的執行個體。是以,前文我們直接使用了hardcode的
provider
grpc路由
服務治理的經典場景是對http協定的服務,通過比對方法路徑字首來路由不同的RESTful方法。grpc的路由方式與此類似,它是通過http2實作的。grpc的service接口及方法名與 http2的對應形式是
`Path : /Service-Name/{method name}
。是以,我們可以為Gateway的VirtualService定義如下的比對規則:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: grpc-gw-vs
spec:
hosts:
- "*"
gateways:
- grpc-gateway
http:
...
- match:
- uri:
prefix: /org.feuyeux.grpc.Greeter/SayBye
- uri:
prefix: /org.feuyeux.grpc.Greeter/SayHello
AB流量
掌握了grpc通過路徑的方式路由,定義AB流量便水到渠成:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: provider
spec:
gateways:
- grpc-gateway
hosts:
- provider
http:
- match:
- uri:
prefix: /org.feuyeux.grpc.Greeter/SayHello
name: hello-routes
route:
- destination:
host: provider
subset: v1
weight: 50
- destination:
host: provider
subset: v2
weight: 50
- match:
- uri:
prefix: /org.feuyeux.grpc.Greeter/SayBye
name: bye-route
...
到此,示例工程的核心能力簡單扼要地講述完畢。詳細代碼請clone
本示例工程。接下來,我将介紹如何将我們的grpc服務執行個體部署到阿裡雲服務網格。
2. 服務網格實踐
2.1 托管叢集
首先使用阿裡雲賬号登入,進入容器服務控制台(
https://cs.console.aliyun.com),建立Kubernetes叢集-标準托管叢集。詳情見幫助文檔:
快速建立Kubernetes托管版叢集2.2 服務網格
進入服務網格控制台(
https://servicemesh.console.aliyun.com/),建立服務網格執行個體。詳情見幫助文檔:
服務網格 ASM > 快速入門 > 使用流程服務網格執行個體建立成功後,確定資料平面已經添加容器服務叢集。然後開始資料平面的配置。
2.3 資料平面
kubeconfig
在執行資料平面的部署前,我們先确認下即将用到的兩個kubeconfig。
- 進入容器執行個體界面,擷取kubconfig,并儲存到本地
~/shop/bj_config
- 進入服務網格執行個體界面,點選連接配接配置,擷取kubconfig,并儲存到本地
~/shop/bj_asm_config
請注意,在資料平面部署過程中,我們使用
~/shop/bj_config
這個kubeconfig;在控制平面的部署中,我們使用
~/shop/bj_asm_config
這個kubeconfig。
設定自動注入
kubectl \
--kubeconfig ~/shop/bj_config \
label namespace default istio-injection=enabled
可以通過通路容器服務的
命名空間界面進行驗證。
部署deployment和service
export DEMO_HOME=
kubectl \
--kubeconfig ~/shop/bj_config \
apply -f $DEMO_HOME/istio/kube/consumer.yaml
kubectl \
--kubeconfig ~/shop/bj_config \
apply -f $DEMO_HOME/istio/kube/provider1.yaml
kubectl \
--kubeconfig ~/shop/bj_config \
apply -f $DEMO_HOME/istio/kube/provider2.yaml
可以通過通路容器服務的如下界面進行驗證:
- 無狀态應用 https://cs.console.aliyun.com/#/k8s/deployment/list
- 容器組 https://cs.console.aliyun.com/#/k8s/pod/list
- 服務 https://cs.console.aliyun.com/#/k8s/service/list
通過如下指令,确認pod的狀态是否符合預期:
$ kubectl \
--kubeconfig ~/shop/bj_config \
get pod
NAME READY STATUS RESTARTS AGE
consumer-v1-5c565d57f-vb8qb 2/2 Running 0 7h24m
provider-v1-54dbbb65d8-lzfnj 2/2 Running 0 7h24m
provider-v2-9fdf7bd6b-58d4v 2/2 Running 0 7h24m
入口網關服務
最後,我們通過ASM管控台配置入口網關服務,以對外公開
http
協定的
9001
端口和
grpc
6565
端口。
建立完成後,我們就有了公網的IP。餘文測試驗證環節将使用到這裡配置的入口網關IP
39.102.37.176
:
2.4 控制平面
部署Gateway
kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/gateway.yaml
部署完畢後,在ASM控制台的控制平面-服務網關界面下,可以看到這個Gateway執行個體。也可以直接使用該界面建立和删除服務網格的Gateway執行個體。
部署VirtualService
kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/gateway-virtual-service.yaml
kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/provider-virtual-service.yaml
kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/consumer-virtual-service.yaml
部署完畢後,在ASM控制台的控制平面-虛拟服務界面下,可以看到VirtualService執行個體清單。也可以直接使用界面建立和删除服務網格的VirtualService執行個體。
部署DestinationRule
kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/provider-destination-rule.yaml
kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/consumer-destination-rule.yaml
部署完畢後,在ASM控制台的控制平面-目标規則界面下,可以看到DestinationRule執行個體清單。也可以直接使用界面建立和删除服務網格的DestinationRule執行個體。
2.5 流量驗證
完成grpc服務在ASM的部署後,我們首先驗證如下鍊路的流量:
HOST=39.102.37.176
for ((i=1;i<=10;i++)) ;
do
curl ${HOST}:9001/hello/feuyeux
echo
done
最後再來驗證我如下鍊路的流量:
# terminal 1
export GRPC_PROVIDER_HOST=39.102.37.176
java -jar consumer/target/consumer-1.0.0.jar
# terminal 2
for ((i=1;i<=10;i++)) ;
do
curl localhost:9001/bye
echo
done