分享知识、记录点滴

0%

开篇

Istio 流量劫持的文章其实目前可以在servicemesher社区找到一篇非常详细的文章,可查阅:Istio 中的 Sidecar 注入及透明流量劫持过程详解。特别是博主整理的那张“流量劫持示意图”,已经可以很清晰的看出来劫持流程。这里我借着那张图片解释一版该图片的文字版本。在开始文字版前如果对iptables命令如果不是非常了解的话建议先重点看下下面的两篇文章,深入浅出的解释了该命令的概念及用法:

  1. iptables概念 - 以通俗易懂的方式描述iptables的相关概念
  2. iptables指南 - iptables命令用法指南

这里引用iptables的一张报文流向图(版权归原博主所有)

iptables-routing

当客户端访问服务器的web服务时,客户端发送报文到网卡,而tcp/ip协议栈是属于内核的一部分,所以,客户端的信息会通过内核的TCP协议传输到用户空间中的web服务中,而此时,客户端报文的目标终点为web服务所监听的套接字(IP:Port)上,当web服务需要响应客户端请求时,web服务发出的响应报文的目标终点则为客户端,这个时候,web服务所监听的IP与端口反而变成了原点。 – 引用自 zsythink

上面这部分描述相当重要,它是理解sidecar在进行流量劫持的基础之一。

下面我们分析下昨天istio-init启动时执行的istio-iptables命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
nsenter -t 8533 -n iptables -t nat -S

# 默认
# 为PREROUTING/INPUT/OUTPUT/POSTROUTING链设置策略为接收数据包(ACCEPT)
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT

# 自定义4个istio的规则链
-N ISTIO_INBOUND
-N ISTIO_IN_REDIRECT
-N ISTIO_OUTPUT
-N ISTIO_REDIRECT

# 进入PREROUTING链tcp协议请求全部定向至 ISTIO_INBOUND 自定义链进行规则匹配
-A PREROUTING -p tcp -j ISTIO_INBOUND
# 进入OUTPUT链tcp协议请求全部定向至 ISTIO_OUTPUT 自定义链进行规则匹配
-A OUTPUT -p tcp -j ISTIO_OUTPUT

# 入口
# tcp协议请求且请求端口为22/15090/15021/15020的请求停止执行当前链中的后续Rules,并执行下一个链
-A ISTIO_INBOUND -p tcp -m tcp --dport 22 -j RETURN
-A ISTIO_INBOUND -p tcp -m tcp --dport 15090 -j RETURN
-A ISTIO_INBOUND -p tcp -m tcp --dport 15021 -j RETURN
-A ISTIO_INBOUND -p tcp -m tcp --dport 15020 -j RETURN
# tcp协议且端口不是22/15090/15021/15020的请求全部定向至 ISTIO_IN_REDIRECT
-A ISTIO_INBOUND -p tcp -j ISTIO_IN_REDIRECT
# 将重定向于此的tcp协议请求流量全部重定向至15006端口(envoy入口流量端口)
-A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15006

# 出口
# 源IP地址为localhost且数据包出口为 ”lo“ 的流量都返回到它的调用点中的下一条链执行(1)
-A ISTIO_OUTPUT -s 127.0.0.6/32 -o lo -j RETURN
# 目的地非localhost,数据包出口为 ”lo“,是istio-proxy用户的流量转发至 ISTIO_REDIRECT (2)
-A ISTIO_OUTPUT ! -d 127.0.0.1/32 -o lo -m owner --uid-owner 1337 -j ISTIO_IN_REDIRECT
# 数据包出口为 ”lo“,非istio-proxy用户的流量都返回到它的调用点中的下一条链执行(1)
-A ISTIO_OUTPUT -o lo -m owner ! --uid-owner 1337 -j RETURN
# istio-proxy 用户的流量都返回到它的调用点中的下一条链执行
-A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
# 目的地非localhost,数据包出口为 ”lo“,是istio-proxy用户组的流量转发至 ISTIO_REDIRECT(2)
-A ISTIO_OUTPUT ! -d 127.0.0.1/32 -o lo -m owner --gid-owner 1337 -j ISTIO_IN_REDIRECT
# 数据包出口为 ”lo“ 且非istio-proxy用户组流量都返回到它的调用点中的下一条链执行(1)
-A ISTIO_OUTPUT -o lo -m owner ! --gid-owner 1337 -j RETURN
# 到 istio-proxy 用户组的流量都返回到它的调用点中的下一条链执行(1)
-A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
# 所有目的地为localhost的流量都返回到它的调用点中的下一条链执行(1)
-A ISTIO_OUTPUT -d 127.0.0.1/32 -j RETURN
# 其他不满足上述rules的流量全部转发到 ISTIO_REDIRECT (2)
-A ISTIO_OUTPUT -j ISTIO_REDIRECT
# 将重定向于此的tcp协议请求流量全部重定向至15001端口(envoy出口流量端口)
-A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001

-m = –match. istio-proxy 用户身份运行, uid-owner 1337 为用户ID / gid-owner 1337 为用户组,即 sidecar 所处的用户空间,这也是 istio- proxy 容器默认使用的用户。

注意文中打括号的地方

(1) 代表流量会直接执行下一个拦截链,本文中下一个拦截链为POSTROUTING

(2) 代表流量会被重定向至envoy出口流量端口

根据上面的规则小结一下:

ISTIO_INBOUND 链:所有进入Pod但非指定端口(如22)的流量全部重定向至15006端口(envoy入口流量端口)进行拦截处理。

ISTIO_OUTPUT 链:所有流出Pod由 istio-proxy 用户空间发出且目的地不为localhost的流量全部重定向至15001端口(envoy出口流量端口),其他流量全部直接放行至下一个POSTROUTING链,不用被envoy拦截处理。

其实仔细思考下可以看到,流量拦截主要发生在两个地方:

  1. 用户请求到达Pod时对应流量会被拦截至sidecar进行处理,由sidecar请求业务服务
  2. 当业务服务响应用户请求时该响应再次被拦截至sidecar,由sidecar响应用户

再看下iptables nat 表的规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
nsenter -t 8533 -n iptables -t nat -L -v


Chain PREROUTING (policy ACCEPT 3435 packets, 206K bytes)
pkts bytes target prot opt in out source destination
3435 206K ISTIO_INBOUND tcp -- any any anywhere anywhere (1)

Chain INPUT (policy ACCEPT 3435 packets, 206K bytes) (5)
pkts bytes target prot opt in out source destination

Chain OUTPUT (policy ACCEPT 599 packets, 54757 bytes)
pkts bytes target prot opt in out source destination
22 1320 ISTIO_OUTPUT tcp -- any any anywhere anywhere

Chain POSTROUTING (policy ACCEPT 599 packets, 54757 bytes) (8)
pkts bytes target prot opt in out source destination

Chain ISTIO_INBOUND (1 references) (2)
pkts bytes target prot opt in out source destination
0 0 RETURN tcp -- any any anywhere anywhere tcp dpt:22
1 60 RETURN tcp -- any any anywhere anywhere tcp dpt:15090
3434 206K RETURN tcp -- any any anywhere anywhere tcp dpt:15021
0 0 RETURN tcp -- any any anywhere anywhere tcp dpt:15020
0 0 ISTIO_IN_REDIRECT tcp -- any any anywhere anywhere (3)

Chain ISTIO_IN_REDIRECT (3 references)
pkts bytes target prot opt in out source destination
0 0 REDIRECT tcp -- any any anywhere anywhere redir ports 15006 (4)

Chain ISTIO_OUTPUT (1 references) (6)
pkts bytes target prot opt in out source destination
0 0 RETURN all -- any lo 127.0.0.6 anywhere
0 0 ISTIO_IN_REDIRECT all -- any lo anywhere !localhost owner UID match 1337
0 0 RETURN all -- any lo anywhere anywhere ! owner UID match 1337
22 1320 RETURN all -- any any anywhere anywhere owner UID match 1337
0 0 ISTIO_IN_REDIRECT all -- any lo anywhere !localhost owner GID match 1337
0 0 RETURN all -- any lo anywhere anywhere ! owner GID match 1337
0 0 RETURN all -- any any anywhere anywhere owner GID match 1337
0 0 RETURN all -- any any anywhere localhost
0 0 ISTIO_REDIRECT all -- any any anywhere anywhere

Chain ISTIO_REDIRECT (1 references)
pkts bytes target prot opt in out source destination
0 0 REDIRECT tcp -- any any anywhere anywhere redir ports 15001

让我们一起再来仔细看下面这个图片(版权归原博主所有),同步观察上面iptables chain的规则。这里的分析主要针对红色的数字一对一解释:

envoy-sidecar-traffic-interception

  1. productpage 服务对reviews 服务发送 TCP 连接请求
  2. 请求进入reviews服务所在Pod内核空间,被netfilter拦截入口流量,经过PREROUTING链然后转发至ISTIO_INBOUND
  3. 在被ISTIO_INBOUND链被这个规则-A ISTIO_INBOUND -p tcp -j ISTIO_IN_REDIRECT拦截再次转发至ISTIO_IN_REDIRECT
  4. ISTIO_IN_REDIRECT链直接重定向至 envoy监听的 15006 入口流量端口
  5. 在 envoy 内部经过一系列入口流量治理动作后,发出TCP连接请求 reviews 服务,这一步对envoy来说属于出口流量,会被netfilter拦截转发至出口流量OUTPUT
  6. OUTPUT链转发流量至ISTIO_OUTPUT
  7. 目的地为localhost,不能匹配到转发规则链,直接RETURN到下一个链,即POSTROUTING
  8. sidecar发出的请求到达reviews服务9080端口
  9. reviews服务处理完业务逻辑后响应sidecar,这一步对reviews服务来说属于出口流量,再次被netfilter拦截转发至出口流量OUTPUT
  10. OUTPUT链转发流量至ISTIO_OUTPUT
  11. 发送非localhost请求且为istio-proxy用户空间的流量被转发至ISTIO_REDIRECT
  12. ISTIO_REDIRECT链直接重定向至 envoy监听的 15001 出口流量端口
  13. 在 envoy 内部经过一系列出口流量治理动作后继续发送响应数据,响应时又会被netfilter拦截转发至出口流量OUTPUT
  14. OUTPUT链转发流量至ISTIO_OUTPUT
  15. 流量直接RETURN到下一个链,即POSTROUTING

针对上文其实我还有两个疑问点,还请大家不吝指教:

  • 上面的理解没有写第16点,博主的图中的16点还会再进ISTIO_REDIRECT链,我们可以看到ISTIO_REDIRECT链中只有一个改写端口转发的规则,这样岂不是会进入一个死循环?或者是我还没有理解清楚
  • envoy 转发流量是不是自己新建立的tcp 连接请求还是通过修改请求报文地址来实现的。因为对c++了解有限,无法查阅其源码去一探究竟

从整个流量拦截流程大家也可以看出,路径这么长, 在大并发场景下肯定会损失转发性能。目前业界有一些框架在试着缩短这个拦截路径,让大家拭目以待吧。

参考文献

https://www.servicemesher.com/blog/sidecar-injection-iptables-and-traffic-routing/

https://www.frozentux.net/iptables-tutorial/cn/iptables-tutorial-cn-1.1.19.html#REDIRECTTARGET

http://www.zsythink.net/archives/1199

开篇

通过上一篇 Istio Sidecar注入原理 文章可以发现,在应用提交到kubernate部署时已经同时注入了Sidecar应用。

细心的话应该还可以发现,除了注入了istio-proxy应用外,另外还有注入一个istio-initInit Containers。接下来一起来看看在这两个注入的容器中分别都有做一些什么操作。

istio-init

istio-init init 容器用于设置 iptables 规则,以便将入站/出站流量通过 sidecar 代理。初始化容器与应用程序容器在以下方面有所不同:

  • 它在启动应用容器之前运行,并一直运行直至完成。
  • 如果有多个初始化容器,则每个容器都应在启动下一个容器之前成功完成
阅读全文 »

概念

简单来说,Sidecar 注入会将额外容器的配置添加到 Pod 模板中。这里特指将Envoy容器注应用所在Pod中。

Istio 服务网格目前所需的容器有:

istio-init 用于设置 iptables 规则,以便将入站/出站流量通过 Sidecar 代理。

初始化容器与应用程序容器在以下方面有所不同:

  • 它在启动应用容器之前运行,并一直运行直至完成。
  • 如果有多个初始化容器,则每个容器都应在启动下一个容器之前成功完成。

因此,您可以看到,对于不需要成为实际应用容器一部分的设置或初始化作业来说,这种容器是多么的完美。在这种情况下,istio-init 就是这样做并设置了 iptables 规则。

istio-proxy 这个容器是真正的 Sidecar 代理(基于 Envoy)。

阅读全文 »

简介

Jaeger是 Uber 推出的一款开源分布式追踪系统(已从CNCF毕业),兼容 OpenTracing API。 它用于监视和诊断基于微服务的分布式系统,功能包括:

  1. 分布式上下文传播
  2. 分布式链路跟踪
  3. 服务依赖分析

示例

Trace列表视图

traces-jaeger-index

Trace明细视图

trace-detail-jaeger

技术栈

架构

Jaeger可以作为单个进程进行部署,也可以作为可扩展的分布式系统进行部署。

Jaeger 主要由以下几部分组成,架构非常清晰:

  • Jaeger Client - 为不同语言实现了符合 OpenTracing 标准的 SDK。应用程序通过 API 写入数据,client library 把 trace 信息按照应用程序指定的采样策略传递给 jaeger-agent.
  • Agent - 它是一个监听在 UDP 端口上接收 span 数据的网络守护进程,它会将数据批量发送给 collector。它被设计成一个基础组件,部署到所有的宿主机上。Agent 将 client library 和 collector 解耦,为 client library 屏蔽了路由和发现 collector 的细节.
  • Collector - 接收 jaeger-agent 发送来的数据,然后将数据写入后端存储。Collector 被设计成无状态的组件,因此您可以同时运行任意数量的 jaeger-collector。 当前,我们的管道会分析数据并为其建立索引,执行任何转换并最终存储它们。 Jaeger的存储设备是一个可插拔组件,目前支持 Cassandra, Elasticsearch and Kafka 存储.
  • Query - 接收查询请求,然后从后端存储系统中检索 trace 并通过 UI 进行展示.
  • Ingester - 后端存储被设计成一个可插拔的组件,支持将数据写入 Cassandra, Elasticsearch.

Jaeger包含两种架构方案:

一、收集器数据直接写入存储架构(tracing数据直接写入存储)

architecture-v1

二、收集器数据缓冲后异步写入存储架构(tracing数据通过kafka缓冲后再异步消费写入存储)

architecture-v2

个人推荐采用第二种架构方式部署

部署

为了快速搭建Jaeger环境,这里安装基于Helm部署(需要先搭建 Kubernetes 集群),可以参考前面写的文章来搭建。从 https://github.com/jaegertracing/helm-charts/tree/master/charts/jaeger 这里可以找到详细的部署流程,可以一步一步跟着执行部署。这里采用 收集器数据直接写入存储架构 部署

1
helm install jaeger jaegertracing/jaeger

官方推荐使用jaeger-operator来部署,可参考: https://www.jaegertracing.io/docs/1.17/operator/

安装完成后查看服务状态

1
2
3
4
5
6
kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
jaeger-agent ClusterIP 10.97.3.215 <none> 5775/UDP,6831/UDP,6832/UDP,5778/TCP,14271/TCP 7m45s
jaeger-cassandra ClusterIP None <none> 7000/TCP,7001/TCP,7199/TCP,9042/TCP,9160/TCP 7m45s
jaeger-collector ClusterIP 10.111.141.231 <none> 14250/TCP,14267/TCP,14268/TCP,14269/TCP 7m45s
jaeger-query ClusterIP 10.97.103.64 <none> 80/TCP,16687/TCP 7m45s

要访问jaeger ui 需要查看jaeger-query项目对外暴露的端口,我们看到通过helm安装,我们采用的默认配置,这里的网络类型是ClusterIP,如果想外网访问可以先临时改成NodePort的方式,执行如下命令编辑对应配置:

1
kubectl edit service jaeger-query

找到最下面的ClusterIP改成NodePort保存即可,保存后会自动生效

1
2
3
4
5
6
kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
jaeger-agent ClusterIP 10.97.3.215 <none> 5775/UDP,6831/UDP,6832/UDP,5778/TCP,14271/TCP 8m38s
jaeger-cassandra ClusterIP None <none> 7000/TCP,7001/TCP,7199/TCP,9042/TCP,9160/TCP 8m38s
jaeger-collector ClusterIP 10.111.141.231 <none> 14250/TCP,14267/TCP,14268/TCP,14269/TCP 8m38s
jaeger-query NodePort 10.97.103.64 <none> 80:31067/TCP,16687:31381/TCP 8m38s

可以发现现在jaeger-query的网络类型已经变成了NodePort,现在可以通过流量访问Jaeger Ui了

这里的地址是 http://47.57.100.110:31067/search (注意,IP地址及端口根据自己控制台的实际输出填入就行)

进入页面后可以到刚才部署的UI界面,并查询jaeger-query项目本身的tracing信息。

我在列表页面找到一个trace_id: 73c00aa573bf1ed0 临时保存下它,后面分析会用到,打开后界面如下。

jaeger-query-ui

traces存储结构

我们可以在jaeger源代码中找到后端cassandra的存储结构,具体信息可以看这里,位置比较隐蔽:

https://github.com/jaegertracing/jaeger/blob/master/plugin/storage/cassandra/schema/v001.cql.tmpl

不过我们可以登录Pod查看创建后的数据结构信息(cassandra)。让我们一探究竟,首先登入cassandra对应的docker镜像,然后通过cql 连接cassandra集群。

如果对cql不了解的可以查看对应文档: https://cassandra.apache.org/doc/latest/cql/

1
2
3
4
5
6
kubectl exec -it jaeger-cassandra-0 --container jaeger-cassandra -- /bin/bash
cqlsh

Connected to jaeger at 127.0.0.1:9042.
[cqlsh 5.0.1 | Cassandra 3.11.6 | CQL spec 3.4.4 | Native protocol v4]
Use HELP for help.

进入对应的space,查看里面对应的表信息

1
2
3
4
5
6
7
8
9
10
cqlsh> desc keyspaces;    #查看有哪些keyspaces

jaeger_v1_test system_auth system_distributed
system_schema system system_traces

cqlsh> use jaeger_v1_test; #切换到jaeger对应的space
cqlsh:jaeger_v1_test> desc tables; #查看jaeger space下面的表信息

service_name_index service_names service_operation_index traces
dependencies_v2 tag_index duration_index operation_names_v2

我们可以一个一个的表信息查看。这里我们主要看下保存我们trace信息的表 service_name_index

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
cqlsh:jaeger_v1_test> desc traces;

CREATE TABLE jaeger_v1_test.traces (
trace_id blob,
span_id bigint,
span_hash bigint,
duration bigint,
flags int,
logs list<frozen<log>>,
operation_name text,
parent_id bigint,
process frozen<process>,
refs list<frozen<span_ref>>,
start_time bigint,
tags list<frozen<keyvalue>>,
PRIMARY KEY (trace_id, span_id, span_hash)
) WITH CLUSTERING ORDER BY (span_id ASC, span_hash ASC)
AND bloom_filter_fp_chance = 0.01
AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'}
AND comment = ''
AND compaction = {'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy', 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'max_threshold': '32', 'min_threshold': '4'}
AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'}
AND crc_check_chance = 1.0
AND dclocal_read_repair_chance = 0.0
AND default_time_to_live = 172800
AND gc_grace_seconds = 10800
AND max_index_interval = 2048
AND memtable_flush_period_in_ms = 0
AND min_index_interval = 128
AND read_repair_chance = 0.0
AND speculative_retry = 'NONE';

还记得我们开始保存的那个trace_id: 73c00aa573bf1ed0 么,现在我们可以在这个表中查看它是如何保存的,我们可以使用下面的cql进行查询,查询前需要对界面上的trace_id进行补位填充0x0000000000000000 ,这里一定要注意,最终在cql里面查询的trace_id为:0x000000000000000073c00aa573bf1ed0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cqlsh:jaeger_v1_test> expand on;
Now Expanded output is enabled
cqlsh:jaeger_v1_test> select * from traces where trace_id=0x000000000000000073c00aa573bf1ed0;

@ Row 1
----------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
trace_id | 0x000000000000000073c00aa573bf1ed0
span_id | 2300299680491247480
span_hash | 1417161953846781420
duration | 204491
flags | 1
logs | [{ts: 1589363774632310, fields: [{key: 'event', value_type: 'string', value_string: 'searching', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'trace_id', value_type: 'string', value_string: '4c423cfb69721367', value_bool: False, value_long: 0, value_double: 0, value_binary: null}]}]
operation_name | readTrace
parent_id | 0
process | {service_name: 'jaeger-query', tags: [{key: 'jaeger.version', value_type: 'string', value_string: 'Go-2.22.1', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'hostname', value_type: 'string', value_string: 'jaeger-query-55c77745b5-ff8tt', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'ip', value_type: 'string', value_string: '192.168.61.148', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'client-uuid', value_type: 'string', value_string: '363a86b295da9842', value_bool: False, value_long: 0, value_double: 0, value_binary: null}]}
refs | [{ref_type: 'child-of', trace_id: 0x000000000000000073c00aa573bf1ed0, span_id: 8340678215617945296}]
start_time | 1589363774627367
tags | [{key: 'db.statement', value_type: 'string', value_string: '\n\t\tSELECT trace_id, span_id, parent_id, operation_name, flags, start_time, duration, tags, logs, refs, process\n\t\tFROM traces\n\t\tWHERE trace_id = ?', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'db.type', value_type: 'string', value_string: 'cassandra', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'component', value_type: 'string', value_string: 'gocql', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'internal.span.format', value_type: 'string', value_string: 'proto', value_bool: False, value_long: 0, value_double: 0, value_binary: null}]

此处省略4个Row....
(5 rows)

因为找到这个trace_id包含了5个span,所以这里查询出来了5条记录,可以通过这段文本及上面的图片进行一一观察,可以发现存储结构还是非常清晰的,UI界面需要展示的信息基本都可以很容易从里面取到。

我们再回过头来看看jaeger client 库thrift的结构(源码见:jaeger.thrift)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 标签
struct Tag {
1: required string key
2: required TagType vType
3: optional string vStr
4: optional double vDouble
5: optional bool vBool
6: optional i64 vLong
7: optional binary vBinary
}

# 日志
struct Log {
1: required i64 timestamp
2: required list<Tag> fields
}

enum SpanRefType { CHILD_OF, FOLLOWS_FROM }

# Span 之间的关系
struct SpanRef {
1: required SpanRefType refType
2: required i64 traceIdLow
3: required i64 traceIdHigh
4: required i64 spanId
}

# Span
struct Span {
1: required i64 traceIdLow # the least significant 64 bits of a traceID
2: required i64 traceIdHigh # the most significant 64 bits of a traceID; 0 when only 64bit IDs are used
3: required i64 spanId # unique span id (only unique within a given trace)
4: required i64 parentSpanId # since nearly all spans will have parents spans, CHILD_OF refs do not have to be explicit
5: required string operationName
6: optional list<SpanRef> references # causal references to other spans
7: required i32 flags # a bit field used to propagate sampling decisions. 1 signifies a SAMPLED span, 2 signifies a DEBUG span.
8: required i64 startTime
9: required i64 duration
10: optional list<Tag> tags
11: optional list<Log> logs
}

基本上可以跟存储的数据结构一一对应上。

采样策略

Jaeger客户端支持4种采样策略,分别是:

  1. Constant (sampler.type=const) 采样率的可设置的值为 0 和 1,分别表示关闭采样和全部采样
  2. Probabilistic (sampler.type=probabilistic) 按照概率采样,取值可在 0 至 1 之间,例如设置为 0.5 的话意为只对 50% 的请求采样
  3. Rate Limiting (sampler.type=ratelimiting) 设置每秒的采样次数上限 。 例如,当sampler.param = 2.0时,它将以每秒2条迹线的速率对请求进行采样。
  4. Remote (sampler.type=remote) 此为默认策略。 采样遵循远程设置,取值的含义和 probabilistic 相同,都意为采样的概率,只不过设置为 remote 后,Client 会从 Jaeger Agent 中动态获取采样率设置。

为了最大程度地减少开销,Jaeger默认采用 0.1% 的采样策略采集数据 (1000次里面采集1次)。

客户端

所有Jaeger客户端库都支持OpenTracing API ,下面这些都是官方支持的客户端库

语言 GitHub Repo
Go jaegertracing/jaeger-client-go
Java jaegertracing/jaeger-client-java
Node.js jaegertracing/jaeger-client-node
Python jaegertracing/jaeger-client-python
C++ jaegertracing/jaeger-client-cpp
C# jaegertracing/jaeger-client-csharp

其他语言的客户端库还在开发中,具体进展可以来这里查看 issue #366

参考文献

https://www.jaegertracing.io/docs/

https://github.com/jaegertracing/jaeger

概念

当我们把系统微服务化后,想查询某个接口一次请求的耗时信息,需要登录多台机器查询相关日志才行。 如下图所示架构,当对应服务集群化部署后,想要查询到某一次请求信息更是难上加难。那我们有什么办法可以解决这个问题么?

答案当然是有的,分布式追踪系统正是为了解决这个问题而生。分布式跟踪为描述和分析跨进程事务提供了一种解决方案。如Google Dapper论文 (业界的分布式追踪系统基本都是以这篇论文为基础进行实现)所述,分布式跟踪的一些使用场景包括:

  1. 异常检测,问题诊断
  2. 分布式系统内各组件的调用情况
  3. 性能/延迟优化
  4. 服务依赖性分析

micro-arch

阅读全文 »

交易中台技术全景图

交易中台技术栈全景

上图是个人根据之前的一些工作积累描绘出来的,当然这些只是冰山一角。其中大部分组件都有在公司实际使用过,里面都是开源组件,因为平常工作都是使用Java,所以基本都是从Java里面做的选型。

至于最终技术栈的选择,每个人有不同的认知及经验差异,可能会有其他的一些更好的想法,这个非常好。没有最好的,只有更合适的。可以结合公司的需求,团队的成员熟知度等因素综合考量后,完成这个技术体系的搭建即可。

阅读全文 »

微服务概念

微服务是一种用于构建应用的架构方案。微服务架构有别于更为传统的单体式方案,可将应用拆分成多个核心功能。每个功能都被称为一项服务,可以单独构建和部署,这意味着各项服务在工作和出现故障时不会相互影响。

monolithic-vs-microservices

微服务组件

下图为搭建微服务平台常用到的一些生态组件

微服务架构组件

阅读全文 »

Istio Sidecar

概念及示例

Sidecar描述了sidecar代理的配置。默认情况下,Istio 让每个 Envoy 代理都可以访问来自和它关联的工作负载的所有端口的请求,然后转发到对应的工作负载。您可以使用 Sidecar 配置去做下面的事情:

  • 微调 Envoy 代理接受的端口和协议集。
  • 限制 Envoy 代理可以访问的服务集合。

您可能希望在较庞大的应用程序中限制这样的 sidecar 可达性,配置每个代理能访问网格中的任意服务可能会因为高内存使用量而影响网格的性能。

您可以指定将 sidecar 配置应用于特定命名空间中的所有工作负载,或者使用 workloadSelector 选择特定的工作负载。例如,下面的 sidecar 配置将 bookinfo 命名空间中的所有服务配置为仅能访问运行在相同命名空间和 Istio 控制平面中的服务。

每个名称空间只能有一个没有任何配置 workloadSelectorSidecar配置 , 如果Sidecar给定名称空间中存在多个不使用选择器的配置,则系统的行为是不确定的。

阅读全文 »

Istio ServiceEntry 外部服务引入

概念及示例

使用服务入口Service Entry来添加一个入口到 Istio 内部维护的服务注册中心。添加了服务入口后,Envoy 代理可以向服务发送流量,就好像它是网格内部的服务一样。配置服务入口允许您管理运行在网格外的服务的流量,它包括以下几种能力:

  • 为外部目标 redirect 和转发请求,例如来自 web 端的 API 调用,或者流向遗留老系统的服务。
  • 为外部目标定义重试、超时和故障注入策略。
  • 添加一个运行在虚拟机的服务来扩展您的网格。
  • 从逻辑上添加来自不同集群的服务到网格,在 Kubernetes 上实现一个多集群 Istio 网格
阅读全文 »

Istio DestinationRule 目标规则

概念及示例

VirtualService一样,DestinationRule也是 Istio 流量路由功能的关键部分。您可以将虚拟服务视为将流量如何路由到给定目标地址,然后使用目标规则来配置该目标的流量。在评估虚拟服务路由规则之后,目标规则将应用于流量的“真实”目标地址。

特别是,您可以使用目标规则来指定命名的服务子集,例如按版本为所有给定服务的实例分组。然后可以在虚拟服务的路由规则中使用这些服务子集来控制到服务不同实例的流量。

目标规则还允许您在调用整个目的地服务或特定服务子集时定制 Envoy 的流量策略,比如您喜欢的负载均衡模型、TLS 安全模式或熔断器设置。在目标规则参考中可以看到目标规则选项的完整列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: my-destination-rule
spec:
host: my-svc
trafficPolicy:
loadBalancer:
simple: RANDOM
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
- name: v3
labels:
version: v3

每个子集都是基于一个或多个 labels 定义的,在 Kubernetes 中它是附加到像 Pod 这种对象上的键/值对。这些标签应用于 Kubernetes 服务的 Deployment 并作为 metadata 来识别不同的版本。

除了定义子集之外,目标规则对于所有子集都有默认的流量策略,而对于该子集,则有特定于子集的策略覆盖它。定义在 subsets 上的默认策略,为 v1v3 子集设置了一个简单的随机负载均衡器。在 v2 策略中,轮询负载均衡器被指定在相应的子集字段上。

host字段

使用 Kubernetes Service的短名称。含义同VirtualService 中destination 的 host字段一致。服务 一定要存在于对应的服务注册中心中,否则会被忽略。

loadBalancer字段

默认情况下,Istio 使用轮询的负载均衡策略,实例池中的每个实例依次获取请求。Istio 同时支持如下的负载均衡模型,可以在 DestinationRule 中为流向某个特定服务或服务子集的流量指定这些模型。

  • 随机:请求以随机的方式转到池中的实例。
  • 权重:请求根据指定的百分比转到实例。
  • 最少请求:请求被转到最少被访问的实例。

subsets字段

subsets是服务端点的集合,可以用于 A/B 测试或者分版本路由等场景。可以将一个服务的流量切分成N份供客户端分场景使用。name字段定义后主要供 VirtualService 里destination 使用。 每个子集都是在host对应服务的基础上基于一个或多个 labels 定义的,在 Kubernetes 中它是附加到像 Pod 这种对象上的键/值对。这些标签应用于 Kubernetes 服务的 Deployment 并作为 metadata 来识别不同的版本。

DestinationRule配置

Field Type Description Required
host string 表示规则的适用对象,取值是在服务注册中心注册的服务名,可以是网格内的服务,也可以是以 ServiceEnrty 方式注册的网格外的服务。如果这个服务名在服务注册中心不存在,则这个规则无效。host 如果取短域名,则会根据规则所在的命名空间进行解析。 Yes
trafficPolicy TrafficPolicy 流量策略,包括负载均衡、连接池策略、异常点检查等 No
subsets Subset[] 是定义的一个服务的子集,经常用来定义一个服务版本,结合 VirtualService 使用 No
exportTo string[] 当前destination rule要导出的 namespace 列表。 应用于 service 的 destination rule 的解析发生在 namespace 层次结构的上下文中。 destination rule 的导出允许将其包含在其他 namespace 中的服务的解析层次结构中。 此功能为服务所有者和网格管理员提供了一种机制,用于控制跨 namespace 边界的 destination rule 的可见性
如果未指定任何 namespace,则默认情况下将 destination rule 导出到所有 namespace
. 被保留,用于定义导出到 destination rule 被声明所在的相同 namespace 。类似的值*保留,用于定义导出到所有 namespaces
NOTE:在当前版本中,exportTo值被限制为.*(即, 当前namespace或所有namespace)
No

subsets配置

Field Type Description Required
name string 服务名和 subset 名称可以用于路由规则中的流量拆分 Yes
labels map 使用标签对服务注册表中的服务端点进行筛选 No
trafficPolicy TrafficPolicy 应用到这一 subset 的流量策略。缺省情况下 subset 会继承 DestinationRule 级别的策略,这一字段的定义则会覆盖缺省的继承策略 No

具体细节的参数明细可查阅:https://preliminary.istio.io/zh/docs/reference/config/networking/destination-rule/#DestinationRule

参考文献

https://preliminary.istio.io/zh/docs/concepts/traffic-management/#destination-rules

https://preliminary.istio.io/zh/docs/reference/config/networking/destination-rule/

https://preliminary.istio.io/zh/docs/reference/config/networking/destination-rule/#DestinationRule