雲棲號資訊:【點擊查看更多行業資訊】
在這裏您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!
繼MicroServices之後,ServiceMesh是又一個推動軟件工業的革命性技術。其服務治理的方法論,不僅改變了技術實現的方式,也將深入影響社會分工。
運行於數據平面的用戶服務與治理服務的各種規則徹底解耦。運行於控制平面的規則定義組件,將流量控制的具體規則推送給運行於數據平面的proxy,proxy通過對用戶服務的ingress和egress的實際控制,最終實現服務治理。
原本需要服務開發者編程實現的服務發現、容錯、灰度、流量複製等能力,被ServiceMesh非侵入的方式實現。此外,ServiceMesh還提供了訪問控制、認證授權等功能,進一步減輕了用戶服務的開發成本。
阿里雲提供的服務網格是基於容器服務之上的託管版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
可以通過訪問容器服務的命名空間界面https://cs.console.aliyun.com/#/k8s/namespace進行驗證。
部署deployment和service
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 39.102.37.176。
2.4 控制平面
部署Gateway
kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/gateway.yaml
部署Gateway的VirtualService
kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/gateway-virtual-service.yaml
部署VirtualService和DestinationRule
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/provider-destination-rule.yaml
kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/consumer-virtual-service.yaml
kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/consumer-destination-rule.yaml
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
到此,基於ASM的GRPC服務部署實踐分享完畢。歡迎技術交流。
【雲棲號在線課堂】每天都有產品技術專家分享!
課程地址:https://yqh.aliyun.com/live立即加入社羣,與專家面對面,及時瞭解課程最新動態!
【雲棲號在線課堂 社羣】https://c.tb.cn/F3.Z8gvnK