在服务网格的流量管理和可观测性实现上,Headers发挥着非常关键的作用。相比而言,HTTP协议的Headers实现较为容易,因为HTTP是同步阻塞式的请求响应模式,可以很容易在GET/POST/UPDATE/DELETE方法中定义和使用读写Header的API。GRPC协议的Headers则要复杂一些,各种编程语言在4种不同通信模型中,读写Header的形式的差异化很大,同时还要考虑流式和异步的编程实现。
本篇首先介绍4种编程语言的Headers编程实践,然后讲述在服务网格实践中,GRPC协议Headers的两个重要实践:流量管理和可观测性。
1 GRPC协议Headers编程实践
服务端获取Headers
GRPC通信模型编程语言 | Java | Go | NodeJs | Python |
---|---|---|---|---|
基本方法 | 实现拦截器ServerInterceptor 接口的interceptCall(ServerCall<ReqT, RespT> call,final Metadata m,ServerCallHandler<ReqT, RespT> h) 方法,通过String v = m.get(k) 获取header信息,get 方法入参类型为Metadata.Key<String> |
metadata.FromIncomingContext(ctx)(md MD, ok bool) ,MD 是一个map[string][]string |
call.metadata.getMap() ,返回值类型是[key: string]: MetadataValue ,MetadataValue 类型定义为string/Buffer |
context.invocation_metadata() ,返回值类型为2-tuple数组,2-tuple的形式为('k','v') ,使用m.key , m.value 遍历获取键值对 |
Unary RPC | 对Headers无感知 | 在方法中直接调用metadata.FromIncomingContext(ctx) ,上下文参数ctx 来自Talk的入参 |
在方法内直接调用call.metadata.getMap() |
在方法内直接调用context.invocation_metadata() |
Server streaming RPC | 同上 | 在方法中直接调用metadata.FromIncomingContext(ctx) ,上下文参数ctx 从TalkOneAnswerMore的入参stream 中获取:stream.Context() |
同上 | 同上 |
Client streaming RPC | 同上 | 在方法中直接调用metadata.FromIncomingContext(ctx) ,上下文参数ctx 从TalkMoreAnswerOne的入参stream 中获取:stream.Context() |
同上 | 同上 |
Bidirectional streaming RPC | 同上 | 在方法中直接调用metadata.FromIncomingContext(ctx) ,上下文参数ctx 从TalkBidirectional的入参stream 中获取:stream.Context() |
同上 | 同上 |
客户端发送Headers
GRPC通信模型编程语言 | Java | Go | NodeJs | Python |
---|---|---|---|---|
基本方法 | 实现拦截器ClientInterceptor 接口的interceptCall(MethodDescriptor<ReqT, RespT> m, CallOptions o, Channel c) 方法,实现返回值类型ClientCall<ReqT, RespT> 的start((Listener<RespT> l, Metadata h)) 方法,通过h.put(k, v) 填充header信息,put 方法入参k 的类型为Metadata.Key<String> ,v 的类型为String |
metadata.AppendToOutgoingContext(ctx,kv ...) context.Context |
metadata=call.metadata.getMap() metadata.add(key, headers[key]) |
metadata_dict = {} 变量填充 metadata_dict[c.key] = c.value 最终转为list tuple类型 list(metadata_dict.items()) |
Unary RPC | 对Headers无感知 | 在方法中直接调用metadata.AppendToOutgoingContext(ctx,kv) |
在方法内直接使用基本方法 | 在方法内直接使用基本方法 |
Server streaming RPC | 同上 | 同上 | 同上 | 同上 |
Client streaming RPC | 同上 | 同上 | 同上 | 同上 |
Bidirectional streaming RPC | 同上 | 同上 | 同上 | 同上 |
Propaganda Headers
由于链路追踪需要将上游传递过来的链路元数据透传给下游,以形成同一条请求链路的完整信息,我们需要将服务端获取的Headers信息中,和链路追踪相关的Headers透传给向下游发起请求的客户端。这就是"Propaganda Headers"的概念。
如上所述,除了Java语言的实现,其他语言的通信模型方法都对header有感知,因此可以将"服务端读取-传递-客户端发送"这三个动作顺序地在4种通信模型方法内部实现。
Java语言读取和写入Headers是通过两个拦截器分别实现的,因此Propaganda Headers无法在一个顺序的流程里实现,且考虑到并发因素,以及只有读取拦截器知道链路追踪的唯一ID,我们无法通过最直觉的缓存方式搭建两个拦截器的桥梁。
那么只能借助上下文了。幸好Java语言的实现提供了一种Metadata-Context Propagation的机制。
在服务器拦截器读取阶段,通过ctx.withValue(key, metadata)
将Metadata/Header存入Context,其中key
是Context.Key<String>
类型。然后在客户端拦截器中,通过key.get()
将Metadata从Context读出,get
方法默认使用Context.current()
上下文,这就保证了一次请求的Headers读取和写入使用的是同一个上下文。
有了Propaganda Headers的实现,基于GRPC的链路追踪就有了机制上的保证。
2 网格拓扑
完成GRPC的Headers处理后,我们进入部署和验证目录tracing。该目录下包含4种编程语言的部署脚本。我们以go版本为例,执行如下脚本进行部署和验证。
cd go
# 部署
sh apply.sh
# 验证
sh test.sh
部署后的服务网格拓扑如下图所示。
3 流量转移
在VirtualService中通过定义Header键值的匹配条件,可以实现根据请求动态地进行流量转移。如果再结合前一篇中讲述的按API切流、按版本切流的实践,就可以完成应用级的精细化流量管理。
进入服务网格(ASM)实例,新建VirtualService,将如下内容复制保存。这个VirtualService
定义了Header中server-version=go
的请求100%流量路由到go版本服务。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
namespace: grpc-best
name: grpc-server-vs
spec:
hosts:
- "*"
gateways:
- grpc-gateway
http:
- match:
- headers:
server-version:
exact: go
route:
- destination:
host: grpc-server-svc
subset: v2
weight: 100
4 链路追踪
进入服务网格(ASM)实例,在功能设置中勾选启用链路追踪,采样方式选择阿里云XTrace。
在本地执行如下请求脚本,向Ingressgateway发起多次请求。
USER_CONFIG=~/shop_config/ack_bj
alias k="kubectl --kubeconfig $USER_CONFIG"
INGRESS_IP=$(k -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
docker run -d --name grpc_client_node -e GRPC_SERVER="${INGRESS_IP}" registry.cn-beijing.aliyuncs.com/asm_repo/grpc_client_node:1.0.0 /bin/sleep 3650d
client_node_container=$(docker ps -q)
echo "Test in a loop:"
for i in {1..100}; do
docker exec -e GRPC_SERVER="${INGRESS_IP}" -it "$client_node_container" node mesh_client.js ${INGRESS_IP} 6666
done
在服务网格(ASM)实例的左侧菜单中点击"链路追踪",查看请求链路信息。如下图所示,完整的链路包括:本地请求端
-Ingressgateway
-grpc-server-svc1
-grpc-server-svc2
-grpc-server-svc3
。