idou老师教你学Istio 08: 调用链埋点是否真的“零修改”?

本文将结合一个具体例子中的细节详细描述Istio调用链的原理和使用方式。并基于Istio中埋点的原理解释来说明:为了输出一个质量良好的调用链,业务程序需根据自身特点做适当的修改,即并非官方一直在说的完全无侵入的做各种治理。另外还会描述Istio当前版本中收集调用链数据可以通过Envoy和Mixer两种不同的方式。

Istio一直强调其无侵入的服务治理,服务运行可观察性。即用户完全无需修改代码,就可以通过和业务容器一起部署的proxy来执行服务治理和与性能数据的收集。原文是这样描述的:

Istio makes it easy to create a network of deployed services with load balancing, service-to-service authentication, monitoring, and more, without any changes in service code. You add Istio support to services by deploying a special sidecar proxy throughout your environment that intercepts all network communication between microservices, then configure and manage Istio using its control plane functionality。

调用链的埋点是一个比起来记录日志,报个metric或者告警要复杂的多,根本原因是要能将在多个点上收集的关于一次调用的多个中间请求过程关联起来形成一个链。Dapper, a Large-Scale Distributed Systems Tracing Infrastructure 描述了其中的原理和一般性的机制,还是挺复杂的。也有很多实现,用的比较多的如zipkin,和已经在CNCF基金会的用的越来越多的Jaeger,满足Opentracing语义标准的就有这么多。

在Istio中大段的埋点逻辑在Sidecar中已经提供,业务代码不用调用以上这些埋点方式来创建trace,维护span等这些复杂逻辑,但是为了能真正连接成一个完整的链路,业务代码还是需要做适当修改。我们来分析下细节为什么号称不用修改代码就能搞定治理、监控等高级功能的Sidecar为什么在调用链埋点的时候需要改应用代码。

调用详细

服务调用关系

简单期间,我们以Istio最经典的Bookinfo为例来说明。Bookinfo的4个为服务的调用关系是这样:

调用链输出

从前端入口gateway那个envoy上进行一次调用,到四个不同语言开发的服务间完成调用,一次调用输出的调用链是这样:

简单看下bookinfo 中的代码,能看到并没有任何创建维护span这种埋点的逻辑,想也是,对于python、java、ruby、nodejs四种不同的语言采用不同的埋点的库在来实现类似的埋点逻辑也是非常头痛的一件事情。那我们看到这个调用链信息是怎么输出的?答案当然是应用边上的sidecar Envoy,Envoy对于调用链相关设计参照这里。sidecar拦截应用程序所有的进和出的网络流量,跟踪到所有的网络请求,像Service mesh的设计理念中其他的路由策略、负载均衡等治理一样,只要拦截到流量Sidecar也可以实现埋点的逻辑。

埋点逻辑

对于经过sidecar流入应用程序的流量,如例子中流入roductpage, details、reviews和ratings的流量,如果经过Sidecar时header中没有任何跟踪相关的信息,则会在创建一个span,Traceid就是这个spanId,然后在将请求传递给通pod的业务服务;如果请求中包含trace相关的信息,则sidecar从走回归提取trace的上下文信息并发给应用程序。

对于经过sidecar流出的流量,如例子中gateway调用productpage,或者productpage调用链details和reviews的请求。如果经过sidecar时header中没有任何跟踪相关的信息,则会创建根span,并将该跟span相关上下文信息放在请求头中传递给下一个调用的服务,当然调用前会被目标服务的sidecar拦截掉执行上面流入的逻辑;当存在trace信息时,sidecar从header中提取span相关信息,并基于这个span创建子span,并将新的span信息加在请求头中传递。

以上是bookinfo一个实际的调用中在proxy上生成的span主要信息。可以看到,对于每个app访问都经过Sidecar代理,inbound的流量和outbound的流量都通过Sidecar。图上为了清楚表达每个将对Sidecar的每个处理都分开表示,如productpage,接收外部请求是一个处理,给details发出请求是一个处理,给reviews发出请求是另外一个处理,因此围绕Productpage这个app有三个黑色的处理块,其实是一个Sidecar。为了不使的图上太凌乱,最终的Response都没有表示。其实图上每个请求的箭头都有一个反方向的response,在服务发起方的Sidecar会收到response时,会记录一个CR(client received)表示收到响应的时间并计算整个span的持续时间。

解析下具体数据,结合实际调用中生成的数据来看下前面proxy埋点的逻辑会更清楚些。

1.从gateway开始,gateway作为一个独立部署在一个pod中的envoy进程,当有请求过来时,它会将请求转给入口的微服务productpage。Gateway这个Envoy在发出请求时里面没有trace信息,会生成一个根span:spanid和traceid都是f79a31352fe7cae9,parentid为空,并记录CS时间,即Client Send;

2.请求从入口gateway这个envoy进入productpage前先讲过productpage pod内的envoy,envoy处理请求头中带着trace信息,则记录SR,Server received,并将请求发送给Productpage业务容器处理,productpage在处理请求的业务方法中需要接收这些header中的trace信息,然后再调用Details和Reviews的微服务。

Python写的 productpage在服务端处理请求时,先从request中提取接收到的header。然后再调用details获取details服务时,将header转发出去。

app.route(‘/productpage‘)
def front():
product_id = 0 # TODO: replacedefault value
headers = getForwardHeaders(request)

detailsStatus, details = getProductDetails(product_id, headers)
reviewsStatus, reviews = getProductReviews(product_id, headers)
return…
可以看到就是提取几个trace相关的header kv

def getForwardHeaders(request):
headers = {}
incoming_headers = [ ‘x-request-id‘,
‘x-b3-traceid‘,
‘x-b3-spanid‘,
‘x-b3-parentspanid‘,
‘x-b3-sampled‘,
‘x-b3-flags‘,
‘x-ot-span-context‘
]

for ihdr in incoming_headers:
val = request.headers.get(ihdr)
if val is not None:
headers[ihdr] = val
return headers
其实就是重新构造一个请求发出去,可以看到请求中包含收到的header。

def getProductReviews(product_id, headers):
url = reviews[‘name‘] + "/" + reviews[‘endpoint‘] + "/" + str(product_id)
res = requests.get(url, headers=headers, timeout=3.0)
3.从ProductPage出的请求去请求Reviews服务前,又一次通过同Pod的envoy,envoy埋点逻辑检查header中包含了trace相关信息,在将请求发出前会做客户端的调用链埋点,即以当前span为parent span,生成一个子span:即traceid:保持一致9a31352fe7cae9, spanid重新生成cb4c86fb667f3114,parentid就是上个span: f79a31352fe7cae9。

4.请求在到达Review业务容器前,先经过Review的Envoy,从Header中解析出trace信息存在,则发送Trace信息给Reviews。Reviews处理请求的服务端代码中需要解析出这些包含trace的Header信息。

reviews服务中java的rest代码如下:

@GETbr/>@Path("/reviews/{productId}")
public Response bookReviewsById(@PathParam("productId") int productId,
@HeaderParam("end-user") String user,
@HeaderParam("x-request-id") String xreq,
@HeaderParam("x-b3-traceid") String xtraceid,
@HeaderParam("x-b3-spanid") String xspanid,
@HeaderParam("x-b3-parentspanid") String xparentspanid,
@HeaderParam("x-b3-sampled") String xsampled,
@HeaderParam("x-b3-flags") String xflags,
@HeaderParam("x-ot-span-context") String xotspan)
即在服务端接收请求的时候也同样会提取header。调用Ratings服务时再传递下去。其他的productpage调用Details,Reviews调用Ratings逻辑类似。不再复述。

以一实际调用的例子了解以上调用过程的细节,可以看到Envoy在处理inbound和outbound时的埋点逻辑,更重要的是看到了在这个过程中应用程序需要配合做的事情。即需要接收trace相关的header并在请求时发送出去,这样在出流量的proxy向下一跳服务发起请求前才能判断并生成子span并和原span进行关联,进而形成一个完整的调用链。否则,如果在应用容器未处理Header中的trace,则Sidecar在处理outbound的请求时会创建根span,最终会形成若干个割裂的span,并不能被关联到一个trace上。

即虽然Istio一直是讲服务治理和服务的可观察性对业务代码0侵入。但是要获得一个质量良好的调用链,应用程序还是要配合做些事情。在官方的distributed-tracing 中这部分有描述:“尽管proxy可以自动生成span,但是应用程序需要在类似HTTP Header的地方传递这些span的信息,这样这些span才能被正确的链接成一个trace。因此要求应用程序必须要收集和传递这些trace相关的header并传递出去”

? x-request-id

? x-b3-traceid

? x-b3-spanid

? x-b3-parentspanid

? x-b3-sampled

? x-b3-flags

? x-ot-span-context

调用链阶段span解析:

前端gateway访问productpage的proxy的这个span大致是这样:

"traceId": "f79a31352fe7cae9",
"id": "f79a31352fe7cae9",
"name": "productpage-route",
"timestamp": 1536132571838202,
"duration": 77474,
"annotations": [
{
"timestamp": 1536132571838202,
"value": "cs",
"endpoint": {
"serviceName": "istio-ingressgateway",
"ipv4": "172.16.0.28"
}
},
{
"timestamp": 1536132571839226,
"value": "sr",
"endpoint": {
"serviceName": "productpage",
"ipv4": "172.16.0.33"
}
},
{
"timestamp": 1536132571914652,
"value": "ss",
"endpoint": {
"serviceName": "productpage",
"ipv4": "172.16.0.33"
}
},
{
"timestamp": 1536132571915676,
"value": "cr",
"endpoint": {
"serviceName": "istio-ingressgateway",
"ipv4": "172.16.0.28"
}
}
],
gateway上报了个cs,cr, productpage的那个proxy上报了个ss,sr。

分别表示gateway作为client什么时候发出请求,什么时候最终受到请求,productpage的proxy什么时候收到了客户端的请求,什么时候发出了response。

Reviews的span如下:

"traceId": "f79a31352fe7cae9",
"id": "cb4c86fb667f3114",
"name": "reviews-route",
"parentId": "f79a31352fe7cae9",
"timestamp": 1536132571847838,
"duration": 64849,
Details的span如下:

"traceId": "f79a31352fe7cae9",
"id": "951a4487642c0966",
"name": "details-route",
"parentId": "f79a31352fe7cae9",
"timestamp": 1536132571842677,
"duration": 2944,
可以看到productpage这个微服务和detail和reviews这两个服务的调用。

一个细节就是traceid就是第一个productpage span的id,所以第一个span也称为根span,而后面两个review和details的span的parentid是前一个productpage的span的id。

Ratings 服务的span信息如下:可以看到traceid保持一样,parentid就是reviews的spanid。

"traceId": "f79a31352fe7cae9",
"id": "5aac176b61ec8d84",
"name": "ratings-route",
"parentId": "cb4c86fb667f3114",
"timestamp": 1536132571889086,
"duration": 1449,
当然在Jaeger里上报会是这个样子:

根据一个实际的例子理解原理后会发现,应用程序要修改代码根本原因就是调用发起方,在Isito里其实就是Sidecar在处理outbound的时生成span的逻辑,而这个埋点的代码和业务代码不在一个进程里,没法使用进程内的一些类似ThreadLocal的方式(threadlocal在golang中也已经不支持了,推荐显式的通过Context传递),只能显式的在进程间传递这些信息。这也能理解为什么Istio的官方文档中告诉我们为了能把每个阶段的调用,即span,串成一个串,即完整的调用链,你需要修你的代码来传递点东西。

当然实例中只是对代码侵入最少的方式,就是只在协议头上机械的forward这几个trace相关的header,如果需要更多的控制,如在在span上加特定的tag,或者在应用代码中代码中根据需要构造一个span,可以使用opentracing的StartSpanFromContext 或者SetTag等方法。

调用链数据上报

Envoy上报

一个完整的埋点过程,除了inject、extract这种处理span信息,创建span外,还要将span report到一个调用链的服务端,进行存储并支持检索。在Isito中这些都是在Envoy这个sidecar中处理,业务程序不用关心。在proxy自动注入到业务pod时,会自动刷这个后端地址。如:

即envoy会连接zipkin的服务端上报调用链数据,这些业务容器完全不用关心。当然这个调用链收集的后端地址配置成jaeger也是ok的,因为Jaeger在接收数据是兼容zipkin格式的。

Mixers上报

除了直接从Envoy上报调用链到zipkin后端外,和其他的Metric等遥测数据一样通过Mixer这个统一面板来收集也是可行的。

即如tracespan中描述,创建一个tracespan的模板,来描述从mixer的一次访问中提取哪些数据,可以看到trace相关的几个ID从请求的header中提取,而访问的很多元数据有些从访问中提取,有些根据需要从pod中提取(背后去访问了kubeapiserver的pod资源)

apiVersion: "config.istio.io/v1alpha2"
kind: tracespan
metadata:
name: default
namespace: istio-system
spec:
traceId: request.headers["x-b3-traceid"]
spanId: request.headers["x-b3-spanid"] | ""
parentSpanId: request.headers["x-b3-parentspanid"] | ""
spanName: request.path | "/"
startTime: request.time
endTime: response.time
clientSpan: (context.reporter.local | true) == false
rewriteClientSpanId: false
spanTags:
http.method: request.method | ""
http.status_code: response.code | 200
http.url: request.path | ""
request.size: request.size | 0
response.size: response.size | 0
source.ip: source.ip | ip("0.0.0.0")
source.service: source.service | ""
source.user: source.user | ""
source.version: source.labels["version"] | ""
最后

在这个文章发出前,一直在和社区沟通,督促在更明晰的位置告诉大家用Istio的调用链需要修改些代码,而不是只在一个旮旯的位置一小段描述。得到回应是1.1中社区首页第一页what-is-istio/已经修改了这部分说明,不再是1.0中说without any changes in service code,而是改为with few or no code changes in service code。提示大家在使用Isito进行调用链埋点时,应用程序需要进行适当的修改。当然了解了其中原理,做起来也不会太麻烦。

参照

https://thenewstack.io/distributed-tracing-istio-and-your-applications/

https://github.com/istio/old_mixer_repo/issues/797

http://www.idouba.net/opentracing-serverside-tracing/

原文地址:http://blog.51cto.com/14051317/2345885

时间: 2024-08-29 04:34:22

idou老师教你学Istio 08: 调用链埋点是否真的“零修改”?的相关文章

idou老师教你学Istio 07: 如何用istio实现请求超时管理

前言 在前面的文章中,大家都已经熟悉了Istio的故障注入和流量迁移.这两个方面的功能都是Istio流量治理的一部分.今天将继续带大家了解Istio的另一项功能,关于请求超时的管理. 首先我们可以通过一个简单的Bookinfo的微服务应用程序来动手实践一下Istio是如何实现请求超时的管理.看过idou老师前面文章的老司机应该都已经对Bookinfo这个实例驾轻就熟了,当然还存在部分被idou老师的文采刚吸引过来的新同学. 下面先简单的介绍一下Bookinfo这个样例应用整体架构,以便我们更好地

idou老师教你学Istio 20 : Istio全景监控与拓扑

根据Istio官方报告,Observe(可观察性)为其重要特性.Istio提供非侵入式的自动监控,记录应用内所有的服务. 我们知道在Istio的架构中,Mixer是管理和收集遥测信息的组件.每一次当请求到达的时候,Envoy会调用Mixer进行预检查,在请求处理完毕后也会将过程上报给Mixer. 今天我们会结合开源监控插件(Jaeger)与嵌入Istio服务的应用性能管理服务来为大家展示部分Istio的全景监控能力. 1Jaeger Istio结合Jaeger使用可以解决端到端的分布式追踪问题.

idou老师教你学istio:监控能力介绍

经过了一年多的开发和测试,Istio于北京时间7月31日发布了1.0版本,并且宣布1.0版本已经可以成熟的应用于生产环境.对于istio的各项主要功能,之前的文章已经介绍的非常详细,并且还会有更多的文章来分析原理和实践功能.今天我们主要介绍的服务是istio流量监控能力.我们知道每个pod内都会有一个Envoy容器,其具备对流入和流出pod的流量进行管理,认证,控制的能力.Mixer则主要负责访问控制和遥测信息收集.如拓扑图所示,当某个服务被请求时,首先会请求istio-policy服务,来判定

idou老师教你学Istio 04:Istio性能及扩展性介绍

Istio的性能问题一直是国内外相关厂商关注的重点,Istio对于数据面应用请求时延的影响更是备受关注,而以现在Istio官方与相关厂商的性能测试结果来看,四位数的qps显然远远不能满足应用于生产的要求.从发布以来,Istio官方也在不断的对其性能进行优化增强.同时,Istio控制面的可靠性是Istio用于生产的另一项重要考量标准,自动伸缩扩容,自然是可靠性保证的重要手段.下面我们先从性能测试的角度入手,了解下Istio官方提供的性能测试方法与基准,主要分为以下四个方面展开. 一.函数级别测试

idou老师教你学Istio 16:如何用 Istio 实现微服务间的访问控制

摘要使用 Istio 可以很方便地实现微服务间的访问控制.本文演示了使用 Denier 适配器实现拒绝访问,和 Listchecker 适配器实现黑白名单两种方法. 使用场景 有时需要对微服务间的相互访问进行控制,比如使满足某些条件(比如版本)的微服务能够(或不能)调用特定的微服务. 访问控制属于策略范畴,在 Istio 中由 Mixer 组件实现. Mixer拓扑图,来源官方文档 如上图所示,服务的外部请求会被 Envoy 拦截,每个经过 Envoy 的请求都会调用 Mixer,为 Mixer

idou老师教你学Istio 19 : Istio 流量治理功能原理与实战

一.负载均衡算法原理与实战 负载均衡算法(load balancing algorithm),定义了几种基本的流量分发方式,在Istio中一共有4种标准负载均衡算法. ?Round_Robin: 轮询算法,顾名思义请求将会依次发给每一个实例,来共同分担所有的请求. ?Random: 随机算法,将所有的请求随机分发给健康的实例 ?Least_Conn: 最小连接数,在所有健康的实例中任选两个,将请求发给连接数较小的那一个实例. 接下来,我们将根据以上几个算法结合APM(应用性能管理)的监控拓扑图来

idou老师教你学Istio 27:解读Mixer Report流程

1.概述 Mixer是Istio的核心组件,提供了遥测数据收集的功能,能够实时采集服务的请求状态等信息,以达到监控服务状态目的. 1.1 核心功能 ?前置检查(Check):某服务接收并响应外部请求前,先通过Envoy向Mixer(Policy组件)发送Check请求,做一些access检查,同时确认adaptor所需cache字段,供之后Report接口使用: ?配额管理(Quota):通过配额管理机制,处理多请求时发生的资源竞争: ?遥测数据上报(Report):该服务请求处理结束后,将请求

idou老师教你学Istio 14:如何用K8S对Istio Service进行流量健康检查

Istio利用k8s的探针对service进行流量健康检查,有两种探针可供选择,分别是liveness和readiness: liveness探针用来侦测什么时候需要重启容器.比如说当liveness探针捕获到程序运行时出现的一个死锁,这种情况下重启容器可以让程序更容易可用. readiness探针用来使容器准备好接收流量.当所有容器都ready时被视为pod此时ready.比如说用这种信号来控制一个后端服务,当pod没有到ready状态时,服务会从负载均衡被移除. 使用场景: liveness

idou老师教你学Istio 15:Istio实现双向TLS的迁移

在Istio中,双向TLS是传输身份验证的完整堆栈解决方案,它为每个服务提供可跨集群的强大身份.保护服务到服务通信和最终用户到服务通信,以及提供密钥管理系统.本文阐述如何在不中断通信的情况下,把现存Istio服务的流量从明文升级为双向TLS. 使用场景 在部署了Istio的集群中,使用人员刚开始可能更关注功能性,服务之间的通信配置的都是明文传输,当功能逐渐完善,开始关注安全性,部署有sidecar的服务需要使用双向TLS进行安全传输,但服务不能中断,这时,一个可取的方式就是进行双向TLS的迁移.