用了几个小时读完了Gorilla这篇经典的 时序数据库论文
,prometheus的时序数据库在很多地方都参考了这篇论文。以此文总结一下读后感,非论文翻译。截图基本都出自于论文。本论文可以解答如下的普罗问题

为什么普罗不支持字符串类型,只支持double作为监控值

为了压缩数据,普罗使用了高效的用于double的压缩算法。

为什么普罗的默认的落盘间隔是2个小时

根据这篇论文,2个小时或以上的block的压缩比更小

普罗data盘里的文件都是用来干啥的?

有索引文件、数据文件、恢复日志等

顺带一提,Gorilla是大猩猩的意思,也是银魂中近藤勋的绰号。

Facebook因为从HBase读取时间序列太慢,再加上扩展性已经无法满足需求。Facebook对时延迟要求如此之低,Facebook否决了所有依赖磁盘做数据存储查询的方案,希望数据查询从内存返回。最终从论文看来,查询比HBase快了300多倍。

数据的编码方式

Facebook想要把数据都放到内存中,prometheus号称单机可处理数百万序列,如果按照业务代码的模式书写,三百万序列在1个小时内要占用多少内存呢?时间戳long值4byte,字符串名称加维度20byte,值算double类型,8byte,总共是32byte,两个小时,假设1分钟一个点,共有120个点。共1.2G内存。仅仅纯数据就占用了1.2G内存。Facebook基于如下两个监控数据的特点,对数据进行了高效压缩,缩小12倍原数据的大小

  • 大多数监控数据往往相差固定的时间间隔,而其他监控数据,虽然不是严格固定间隔,也基本接近固定的时间间隔。这个比例在Facebook的监控数据是24:1,即百分之96的数据都是固定间隔上报的
  • 大多数监控数据监控的值变化缓慢

基于这两点假设,它们对时间戳和值提出了两种压缩算法

时间戳

对时间戳的差值的差值进行存储,压缩空间大小。

image-20210313174810933

先存储这一块的起始时间2015 02:00:00,对于第一个数值2015 02:01:02记录差值62,对下一个值2015 02:02:02,其的差值是60,差值的差值是
60-62=-2,只存储-2即可,这样子大大节约了存储时间戳的空间

监控值

监控值采用异或的手法进行压缩,会得到0很多的二进制串,再通过合理的编码,降低总字节数。根据论文,有超过一半的数据相较上一个值没什么变化,使用一个字节即可存储

image-20210313175320885

那么这样一个block里面应该存储多久的数据呢?这是一个权衡,如果存储很久的数据,则每次查询都需要查询出很large的值,才能获得结果,如果存储数据较短,则难以达到很高的压缩比。最终他们选择了两小时。

block时长和压缩比的关系

对于普罗里的指标名称及维度,在普罗里,一个指标名称加上一组维度称为一个时间序列,Gorilla论文并没有提及维度的概念,仅仅使用名称。这块普罗实际和Gorilla都通过码表的方式,通过将字符串映射为一个longId,来大大降低存储的字节数。

对于普罗的查询流程,是 指标名称+维度=》一组时间序列,然后分别查询其中的值。Gorilla自身没有包装这一个流程,需要客户端组网自己想要查询的时间序列列表。

基于时序监控系统里的,新数据比旧数据关键,Gorilla也会将旧数据落盘。

Gorilla的高可用

单机可靠性

Facebook调查了自己之前的监控系统,发现百分之85以上请求都只查询了26个小时以内的数据。在Gorilla的第一版中,他们决定只支持26个小时数据的查询,将2小时的数据放在内存中。超过2小时的数据,会存储在高可用磁盘上,如GlusterFs、HDFS等。2小时以内的数据,有一个log用来做重启时恢复数据来保证可靠性。这样就保证了单机重启的可靠性
注:这个日志不保证能恢复所有的数据,允许异常场景下有数据丢失

Region宕机的可靠性

对于每一个Gorilla,都会主备部署一个对等的位于不同Region的实例,这两个实例都会存储数据,但数据并不完全一致。对于用户来说,他们接入距离他们最近的Gorilla实例。一旦其中一个Gorilla宕机,另一个Gorilla将会接管它的工作。为了保证数据的准确,待其恢复后26小时(拥有了它该拥有的全量数据),才可以接受业务请求。

Gorilla的扩展

Gorilla选择了水平扩展的方式,根据指标名做分区,分区到不同的主备Gorilla

论文还提到了一些其他有用的信息

监控时序数据的特点

写请求占大多数

读请求很少,人工读取,或者一些自动化告警系统

注重状态的变化

内存突然上升,乃至于一个指标值的导数突然上升等

监控系统的目标

高可用

出现问题的时候,监控系统和业务系统同时宕机是什么体验?是一把黑的体验。

低延迟

可容错

容忍单点故障灯

可扩展

随着业务系统的扩展,监控系统也需要扩展

RUM定理的背景

现在的基础设备复杂,多种多样。数据的保存和查询也多种多样,很多时候会为了很小的差异重新设计数据结构。这篇论文主要是指出了无论如何设计数据结构,都不可能在Read(读取)、Update(更新)、Memory(存储)三个方向上都做到最优,也希望指导接下来其他数据结构的设计,更希望能有一种自适应的系统,可以根据数据的查询、数据的写入、配置的硬件、人工的配置在Read、Update、Memory之间权衡。

RUM开销介绍

我们将存储在数据中心的数据称作基础数据。那些用来辅助写入,辅助查询的数据成为辅助数据

读开销 RO

也称作读放大,通过读取到的辅助数据加上基础数据除以基础数据来计算。举个例子,在mysql中查询数据,中间经过的B树层级就是读放大

更新开销 UO

也叫做写放大,实例物理上写入磁盘的大小除以逻辑上需要更新的大小。

内存开销 MO

也叫做空间放大,全部的基础数据加上全部的辅助数据除以基础数据。

RUM不可能达成举例

我们选择一个有代表性的基础数据:一个整数数组。我们将这个数据集合组织到N个块中的固定大小的元素,每一个持有一个数值。每一个块可以用一个单调递增的ID来指示。工作负载使用数据的方式有 点查询、点更新、插入和删除。

最小化RO

那我们就把bk的Id当做我们数据结构数组的下标,举例子,{1, 17}是两个元素的id,我们就开辟大小为17的数组array,然后通过array[i]来得到i的数据。现在已经达成了RO最小,但我们的索引非常稀疏,理论上我们的数组是无限大的。更新需要操作两次,将旧的数组元素置空,然后将新的数据存放在新的block中。

RO: 1 UO: 2 MO: 无穷大

最小化UO

为了最小化UO,我们将每次更新的数据直接插入到日志的最尾端,就算更新完成。查询需要遍历原来的数据和整个log文件。

UO: 1 RO: 无穷大 MO: 无穷大

最小化MO

最小化MO时,不存储辅助数据,而将基础数据密集地存储起来。 读取需要进行全表扫描,如果任意更新,也需要进行全表扫描

MO: 1 RO: N UO: 1

数据结构的RUM

image-20210314164113082

参数 N m B P T MEM
含义 数据集大小 查询结果大小 块大小 分区大小 LSM级别比例 内存
单位 元组 元组
批量创建 索引大小 点查询 范围查询(大小m) 查询/更新/删除
读取方式
B+树
完美哈希索引
ZoneMaps
层级LSM树
排序列
未排序列

image-20210314164933209

论文作者对将来系统的设想

作者认为,将来的系统应该是RUM可调的,来满足大多数场景的需要。通过一套可以轻松适应不同优化目标的访问方法来展望未来的数据系统。例如:

• 具有动态调整参数(包括树高,节点大小和拆分条件)的B +树,以便在运行时调整树大小,读取成本和更新成本。

• Approximate (tree) indexing that supports updates with low read performance overhead, by absorbing them in updatable probabilistic data structures (like quotient filters).

• Morphingaccessmethods,combiningmultipleshapesatonce. Adding structure to data gradually with incoming queries, and building supporting index structures when further data reorganization becomes infeasible.

• Update-friendly bitmap indexes, where updates are absorbed using additional, highly compressible, bitvectors which are gradually merged.

• Accessmethodswithiterativelogsenhancedbyprobabilistic data structures that allows for more efficient reads and up- dates by avoiding accessing unnecessary data at the expense of additional space.

论文中其他有意思的点

  • 现代数据系统,通常在压缩的数据上运行,并尽可能晚地解压缩

前言

iptables和ipvs都是常见的转发工具,可以进行报文的转发,比如从 IPa,Port1的消息,经过转发机器发向IPb,Port2,不管是在iptables,抑或是ipvs,进行过一次转发之后,就会留下一条转发记录,iptables是在nf_conntrack,ipvs是在ipvs自己的会话管理里,后面的来自IPa,Port1的消息,就会直接发向IPb,Port2。这个时候,假如强行有一个报文,试图把IPc,Port1的消息,转发到IPb,Port2该怎么办?在四元组冲突的情况下,该转发给谁?返程的报文是什么走向。是这篇文章试图分析的问题

本文基于Linux内核版本5.4.0

出场网元介绍

image-20210311221120239

  • S: 缩写服务的意思

  • G: 缩写Gateway的意思

Iptables和Iptables冲突

准备工作

假设S1和S2都需要经过G使用33333端口发送消息到V的33333端口,我们先配置G的iptables规则:

1
2
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -I POSTROUTING -p udp -s ${S的子网} -o eth0 --sport 33333 -j SNAT --to-source ${G的IP}:33333

接下来在两台S上,把发往V的报文的默认路由指向V

1
ip route add ${V的IP}/32 via ${G的IP}

实验前先执行如下命令,收集输出

1
2
~# conntrack -S
cpu=n found=0 invalid=0 ignore=0 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=0

这个命令的输出跟您的cpu相关,有几个cpu就会输出几行。接下来在V上开启抓包

1
tcpdump -port 33333 -ann

然后在两台S上执行命令发送报文,这里有个小技巧就是使用不一样长度的内容,这样tcpdump会打印报文的长度,一眼就可以看出来送达的报文是谁的

1
echo "Hello world"| nc -4u -p 33333 V的IP 33333

你会观察到,只有先发送的报文抵达了V。随后再执行conntrack -S,收集输出的时候,会发现insert_faileddrop都增加了1。

结论

iptables和iptables冲突的场景,先发送的先生效,后生效的没有发送(这一点你可以通过在G上抓包证实)。观察点就是命令conntrack -S的输出,insert_faileddrop都有所增加。

Iptables流程图

我在做这个实验的时候,顺手打开了iptables的trace功能,记录一下报文的流程

image-20210311222756033

发送失败的也会走完这个流程,然后在插入iptables规则的时候失败。

Iptables和Ipvs冲突

准备工作

因为ipvs不会和ipvs冲突,所以我们尝试构造一下ipvs和iptables冲突的场景,让我们添加上允许V经过G发送报文到S1,让我们在G上配置ipvs所需的转发规则

1
2
ipvsadm -A -u ${G的IP}:33333 -s rr
ipvsadm -a -u ${G的IP}:33333 -r ${S1的IP}:33333 -m

实验流程

从V发送报文转发到S1,然后S2通过SNAT发送报文到V,观察情况

实施前的准备观察项

  • ipvsadm -lnc观察会话

  • ipvsadm -ln –stats 观察报文统计

  • conntrack -L|grep 33333 观察会话

  • 当V发送报文到S1后,

Ipvsadm -ln -stats统计值增加了,只有ipvs的会话表里有内容,iptables会话表里没有内容。

  • 然后S2通过SNAT发送报文到V后,

ipvsadm -ln -stats统计值没有变化,iptables会话表出现内容。(这里我试过S1先返回报文做几次交互,但是是一样的结果)

但是在ipvs有效期间,通过S1不断发送报文,还是可以发送到V节点的。这个时候,会出现S1和S2同时都能发送报文到V。

  • 让我们看看报文返程(即V发送到G的报文)会发送给谁?是S1还是S2。答案是S2。

返程的报文优先匹配了会话表,发送给了S2。如果conntrack老化,那么才会发送给S1

V上来的流程

image-20210311224826893

参考

https://blog.sourcerer.io/writing-a-simple-linux-kernel-module-d9dc3762c234

编码

C文件书写

首先,先书写一个C文件,命名为kernel_first.c

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Robert W. Oliver II");
MODULE_DESCRIPTION("A simple example Linux module.");
MODULE_VERSION("0.01");
static int __init

/**
* Load时候触发的函数
*/
example_init(void) {
printk(KERN_INFO
"Hello, World!\n");
return 0;
}

static void __exit

/**
* Unload时候触发的函数
*/
example_exit(void) {
printk(KERN_INFO
"Goodbye, World!\n");
}

module_init(example_init);
module_exit(example_exit);
  • 请注意使用printk而不是printf。 另外,printk与printf共享的参数不同。 例如,KERN_INFO是一个标志,用于声明应为此行设置日志记录的优先级,并且不带逗号。
    内核在printk函数中对此进行了分类,以节省堆栈内存。
  • 在文件末尾,我们调用module_init和module_exit告诉内核哪些函数是在load时候执行,那些在unload的时候执行

Makefile书写

1
2
3
4
5
obj-m += kernel_first.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

注 make前面应该是Tab键

测试

执行如下命令加载模块到内核sudo insmod kernel_first.ko执行dmesg|grep -i hello,将会看到Hello world的输出。接下来卸载内核模块
sudo rmmod kernel_first,接下来运行dmesg,你将会看到Goodbye world的输出

参考

  • 数据密集型系统

Quorum介绍

Quorum模式常用于分布式场景,保证数据的一致性。其中有两个核心参数

  • Qw 代表数据写入(包括更新、删除)需要的节点数
  • Qr 代表数据读取需要的节点数

如果你总共有N个节点,那么很容易得出只要 W+R>N,那么你的读请求和写请求一定有重叠的节点,这就保证了一致性,你总是能找到最新的那个写入请求

Quorum的不一致场景

但Quorum模式并不一定是万无一失的,他在如下场景会导致不一致

  • 如果两个写操作同时发生,则无法明确先后顺序,最终需要额外的修复手段
  • 如果写操作和读操作同时发生,写操作可能仅在一部分副本上完成。此时,读取时返回旧值还是新值存在不确定性
  • 如果某些副本上已经写入成功,而其他一些副本发生写入失败(如磁盘已满),且总的成功副本数少于w,那些已成功的副本上不会做回滚。这意味着尽管这样的写操作被视为失败,后续的读操作仍可能返回新值

还有两个更加边界的场景

Sloppy Quorum

也叫做宽松的Quorum模式,就是说当N不够的情况下,可以把集群的其他节点当作Qw节点。如果采用了sloppy
quorum,写操作的w节点和读取的r节点可能完全不同,因此无法保证读写请求一定存在重叠的节点

数据的恢复场景

进行数据的恢复在所难免,如cassandra就有读修复等,如果具有新值的节点后来发生失效,但恢复数据来自某个旧值,则总的新值副本数会低于w,这就打破了之前的判定条件

前言

在ipvs中,最小连接算法是一种负载均衡算法,常见的还有轮询算法,加权轮询算法等。让我们先做个基本的假设,每个UDP会话连接上的请求量大概一致。让LB无需观测后端服务的状态,仅仅根据会话信息,做出转发到哪个后端Service的判断,事实上,lvs也目前不能根据后端服务的cpu、内存或者是其他信息做出判断。直观上来说,最小连接数很符合大家的直观感受,保证了每个工作负载上承受的业务连接数是最少的。

ipvs-lb-udp

但是,在LVS保留会话时间稍微较长的情况下,最小连接算法在扩容、升级(升级前后IP改变)、重启(重启前后IP改变)会有一些问题。

简而言之,就是LVS向后端转发UDP消息的时候,后端服务没有很好的拒绝手段,在LC模式下,导致LVS可能转发给后端服务,超过它处理能力的消息数,等到这些会话老化之后,LVS又开始转发给后端服务,超过它处理能力的消息数,如此反复,始终造成大量消息呼损,极难自愈

详细数据推导

以扩容为例,设

  • 每秒消息量 m
  • 保活时间 t
  • 旧的节点数 a
  • 新增节点数 b
  • 使用新IP的请求占比 c (0<c<1)
  • 在一段保活时间内的IP总数 d
  • 单节点处理能力为x

那么,扩容时刻,老的节点上的会话数是d/a

扩容的时候,由于老的节点存在mt/a的会话,那么新IP上来的请求都会转发向新节点,直到把新节点的连接数冲到mt/a为止,

新节点接收请求的速率: (m * c)/b
新节点连接数和老节点持平的时间点: **(d * b)/(m * c a)*

如果新节点连接数和老节点持平的时间点远远小于保活时间,这就会有问题。其他节点上的会话都是相对离散的,所以在保活时间t内,一直不断有消息进来,但新的节点一瞬间接收到了大量请求,又会在同一时间老化。在下一个周期t,又接收到大量请求,如此反复,极难自愈。这也因为lvs的udp转发不关心后端服务器是否成功处理报文,只要转发过去就算了。就是lvs无视后端的状态转发,相比tcp,至少还有是否接收tck连接,后端主动拆链等手段。

虽然LVS能限制后端服务器的连接数,但连接数限制在这个场景是不起作用的。如果您的服务满足上述的这个模式,还是建议您修改为rr算法更为适合。

Entry Log File

背景

测试环境上出现了一些entryLog解析异常的问题,想分析一下磁盘上.log文件的格式,分析分析我们的文件是否有问题

解析代码地址

https://github.com/protocol-laboratory/bookkeeper-codec-java/blob/main/src/main/java/com/github/protocol/EntryLogReader.java

正文

我们采用的配置是singleEntryLog模式,就是说很多ledger的信息都会放在一个log文件内部。

插一句话:这种log文件,其实和LSM相似,属于不可变的数据结构,这种数据结构,得益于不可变,所以内容可以安排的非常紧凑,不像B树结构,需要预留一定空间给原地更新,随机插入等。

bookkeeper-entry-log-format

如上图所示,接下来,我们沿着解析的流程,解读每个部分的详细格式

解析头部

首先,我们解析文件的头部字段,bookkeeper的设计中,文件头部预留了1024字节,目前只使用了20个字节
前四个字节是BKLO的文件魔数
然后紧跟着的4个字节是bk文件的版本号,这里我们仅分析版本号1
然后8字节的long类型代表ledgersMap的开始位置,称为ledgersMapOffset
然后4字节的int类型代表ledgersMap的总长度。

解析ledgerMap部分

最前面四个字节,代表这部分的大小

然后开始的ledgerId和entryId分别为-1,-2,随后是一个ledger的count大小,后面的ledgerId和size才是有效值

随后的部分非常紧凑,由一个个ledgerId,size组成

读取完ledgerMap,可以知道,这个文件包含了多少ledger,总大小是多少?

注:size代表这一段ledger占用的磁盘空间大小

解析body内容

body内容也非常紧凑.
最前面4个字节,代表这个entry的大小。
然后8个字节,ledgerId
然后8个字节,entryId
剩下的内容,就是pulsar写数据的编码,不再属于bookkeeper的格式范畴了

Txn Log File

解析代码地址

https://github.com/protocol-laboratory/bookkeeper-codec-java/blob/main/src/main/java/com/github/protocol/TxnLogReader.java

简述

bookkeeper中的journal log,和大部分基于LSM的数据结构一样,是用来保证文件一定被写入的。会在数据写入的时候,写入journal log,崩溃恢复的时候从journal log里面恢复。

bookkeeper-txn-log-format

解析头部

首先,我们解析文件的头部字段
前四个字节是BKLG的文件魔数
然后紧跟着的4个字节是bk文件的版本号

1
2
3
4
5
6
7
8
9
private TxnHeader readHeader(FileChannel fileChannel) throws Exception {
final ByteBuf headers = Unpooled.buffer(HEADER_SIZE);
final int read = fileChannel.read(headers.internalNioBuffer( index: 0, HEADER_SIZE));
headers.writerIndex(read);
final byte[] bklgByte = new byte[4];
headers.readBytes(bklgByte, dstIndex: 0, length: 4);
final int headerVersion = headers.readInt();
return new TxnHeader(headerVersion);
}

解析内容

内容非常紧凑,由ledgerId,entryId和内容组成。ledgerId一定大于0,entryId在小于0的情况下代表特殊的数据。如

  • -0x1000即4096 代表ledger的masterKey
  • -0x2000即8192 代表ledger是否被fence
  • -0x4000即16384 代表ledger的force
  • -0x8000即32768 代表ledger的显示LAC

回放流程

当bookkeeper启动的时候,他会从data路径下取得lastMark文件,该文件一定为16个字节,前八个字节代表落盘的最新journal log文件,后八个字节代表文件的位置。会从这个位置开始回放。
值得一提的是,lastId文件,代表下一个dataLog该使用什么文件名。

前言

读《数据库系统内幕》有感,个人感觉分槽页是个很难理解的概念,也是很实用的知识。

正文

原始的B树论文描述了一种简单的,用于定长数据的页组织方式:

image-20210214170013416

这种页有这样两个缺点

  • 除非往最右侧插入数据,否则需要移动前面的数据
  • 无法有效地管理变长地字段

所以这里自然而然地思考,因为要存储变长的数据。

变长的数据需要回收。

回收完的数据需要移动。

但是对外的偏移量不能变,这个变动会非常麻烦,至少聚簇索引需要变动,根据实现不同,甚至二级索引也要跟着更新

我们的需求是

  • 最小开销存储变长记录
  • 回收已删除记录占用地空间
  • 引用页中地记录,无论记录在哪

image-20210214165732666

分槽页通过加了一层结构,页外指针的引用都通过前面的指针引用,包括二分查找也通过前面的指针引用,来解决这个问题。如果不涉及页的变动,一切变化都在分槽页内完成。

分槽页如何解决上述问题:

  • 最小开销:分槽页唯一的额外开销是一个指针数组,用于保存记录实际所在位置的偏移量
  • 空间回收:通过对页进行碎片整理和重写,就可以回收空间
  • 动态布局:从页外部,只能通过槽ID来引用槽,而确切的位置是由页内部决定的

前言

基于k8s部署的微服务,健康检查已经成为其中非常重要的一环,无论是k8s自带的域名负载均衡,或是istio的负载均衡。都把健康检查(即Readiness)是否通过看为是微服务是否正常的标志。

如果正常,才会给应用程序转发报文请求。反之,则不会转发请求

即就是,在微服务功能正常的时候,健康检查返回正常,当微服务进程异常的时候,健康检查返回异常。

假设的数据流向

及时地让上游的服务/网关不转发给你。这里自然反应做不到及时,立刻的反应(健康检查是定期间隔探测,探测的周期在k8s
yaml中配置),如果要达到整个系统无失败,是要依赖一定程度的重试机制,如下图流程所示

image-20210115130302091

image-20210115133626133

image-20210115133655090

如果服务有多个功能,一个好使,一个不好使怎么办?

这种健康检查自检无法通过,一般是系统依赖的某个资源不好使了,如数据库无法写入,消息中间件无法写入等,抑或是进程死锁等进程内部的故障。以上图的微服务B1举例,假如同时运行着tomcat和kafka生产者,那么也许当tomcat挂掉的时候,他的
kafka生产者还能正常运行。这个时候,可能有一些业务是不重要的,可以在异常的时候失败。比如元数据的创建、新用户的开户等。

这里健康检查的结果就可以以核心功能是否正常为准,次级功能通过上报告警
的方式及时处理。并且,除了一些网关模块,成熟的微服务框架都有熔断的功能,可以保证调用B1实例次级功能失败太多,后面都向B2实例调用。

如果我的两个功能都是核心功能怎么办

我觉得这里有三种方式可以探讨一下

健康检查做到接口级别

把健康检查做到接口级别,调用方根据你的接口级别的状态是否ok,决定是否调用这个实例

优点 :健康检查做到接口级别,不出错,可以应对任何多个功能依赖不同资源的场景。

缺点 :健康检查复杂,几乎很难有开源组件支持,基本上要自研

健康检查依旧通过,上游通过熔断处理功能故障

健康检查依旧通过,故障通过上报告警的方式及时处理,一些业务的呼损通过熔断机制解决。

不过,大部分的4层ELB都不支持熔断,Nginx也支持的有限,如果我们的服务挂在4层ELB,7层ELB的后面,就无法通过熔断机制搞定业务呼损的问题

健康检查不通过,上游可以放通

健康检查不通过,但是上游如果发现下游所有的实例都处于不健康状态,这种情况下,把他们当作健康状态处理,试一试能否发通。像servicecomb这个微服务框架就支持这个特性,但
istio还不支持。

总结

  • 最好还是能做到一个微服务只有一个核心功能。
  • 当你的服务挂在ELB、Nginx、K8sService、Istio的后端时,就把他定位成网关服务,核心功能单一,接收请求向下游转发,健康检查准确可靠(LB几乎无熔断能力)
  • 如果服务处于架构的内侧,只有一个核心功能,其他功能在异常场景下可失败,健康检查的结果就以核心功能是否正常为准
  • 如果服务处于架构的内侧,并且有两个核心功能。如服务B1,在上游有熔断的情况下,三种方式均可
  • 如果服务处于架构的外侧,并且有两个核心功能。只能通过报告警的方式

Step1 加入SpringWeb的依赖

1
2
3
4
5
6
7

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.9.RELEASE</version>
<scope>provided</scope>
</dependency>

Step2 书写一个RestController

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
package com.github.hezhangjian.demo.agent.test.controller;

import com.github.hezhangjian.demo.agent.test.module.rest.CreateJobReqDto;
import com.github.hezhangjian.demo.agent.test.module.rest.CreateJobRespDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
* @author hezhangjian
*/
@Slf4j
@RestController
public class TestController {

/**
* @param jobId
* @param createJobReqDto
* https://docs.aws.amazon.com/iot/latest/apireference/API_CreateJob.html
* curl -X PUT -H 'Content-Type: application/json' 127.0.0.1:8080/jobs/111 -d '{"description":"description"}' -iv
* @return
*/
@PutMapping(path = "/jobs/{jobId}")
public ResponseEntity<CreateJobRespDto> createJob(@PathVariable("jobId") String jobId, @RequestBody CreateJobReqDto createJobReqDto) {
final CreateJobRespDto jobRespDto = new CreateJobRespDto();
createJobReqDto.setDescription("description");
createJobReqDto.setDocument("document");
return new ResponseEntity<>(jobRespDto, HttpStatus.CREATED);
}

}

ReqDto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.github.hezhangjian.demo.agent.test.module.rest;

import lombok.Data;

/**
* @author hezhangjian
*/
@Data
public class CreateJobReqDto {

private String description;

private String document;

public CreateJobReqDto() {
}

}

RespDto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.github.hezhangjian.demo.agent.test.module.rest;

import lombok.Data;

/**
* https://docs.aws.amazon.com/iot/latest/apireference/API_CreateJob.html
* @author hezhangjian
*/
@Data
public class CreateJobRespDto {

private String description;

private String jobArn;

private String jobId;

public CreateJobRespDto() {
}
}

Step3 在AgentTransformer中织入切面的逻辑

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
package com.github.hezhangjian.demo.agent;

import com.github.hezhangjian.demo.agent.interceptor.RestControllerInterceptor;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;

/**
* @author hezhangjian
*/
public class AgentTransformer implements AgentBuilder.Transformer {

private static final Logger log = LoggerFactory.getLogger(AgentTransformer.class);

@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {
try {
//包名放在com.github.hezhangjian.demo.agent.test.controller下视为controller代码
if (typeDescription.getTypeName().startsWith("com.github.hezhangjian.demo.agent.test.controller")) {
final Advice advice = Advice.to(RestControllerInterceptor.class);
return builder.visit(advice
.on(ElementMatchers.isAnnotatedWith(RequestMapping.class)
.or(ElementMatchers.isAnnotatedWith(GetMapping.class))
.or(ElementMatchers.isAnnotatedWith(PostMapping.class))
.or(ElementMatchers.isAnnotatedWith(PutMapping.class))
.or(ElementMatchers.isAnnotatedWith(DeleteMapping.class))
.or(ElementMatchers.isAnnotatedWith(PatchMapping.class))));
}
} catch (Exception e) {
log.error("error is ", e);
}
return builder;
}

}

Step4 先添加一个打印日志工具类

Interceptor中不能出现和controller一样的字段,我们先写一个工具类用来agent打印日志

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
package com.github.hezhangjian.demo.agent.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;

/**
* @author hezhangjian
*/
public class AgentUtil {

private static final Logger log = LoggerFactory.getLogger(AgentUtil.class);

/**
* Log a message at the TRACE level.
*
* @param msg the message string to be logged
*/
public static void trace(String msg) {
log.trace(msg);
}

/**
* Log a message at the TRACE level according to the specified format
* and argument.
* <p/>
* <p>This form avoids superfluous object creation when the logger
* is disabled for the TRACE level. </p>
*
* @param format the format string
* @param arg the argument
*/
public static void trace(String format, Object arg) {
log.trace(format, arg);
}

/**
* Log a message at the TRACE level according to the specified format
* and arguments.
* <p/>
* <p>This form avoids superfluous object creation when the logger
* is disabled for the TRACE level. </p>
*
* @param format the format string
* @param arg1 the first argument
* @param arg2 the second argument
*/
public static void trace(String format, Object arg1, Object arg2) {
log.trace(format, arg1, arg2);
}

/**
* Log a message at the TRACE level according to the specified format
* and arguments.
* <p/>
* <p>This form avoids superfluous string concatenation when the logger
* is disabled for the TRACE level. However, this variant incurs the hidden
* (and relatively small) cost of creating an <code>Object[]</code> before invoking the method,
* even if this logger is disabled for TRACE. The variants taking {@link #trace(String, Object) one} and
* {@link #trace(String, Object, Object) two} arguments exist solely in order to avoid this hidden cost.</p>
*
* @param format the format string
* @param arguments a list of 3 or more arguments
*/
public static void trace(String format, Object... arguments) {
log.trace(format, arguments);
}

/**
* Log an exception (throwable) at the TRACE level with an
* accompanying message.
*
* @param msg the message accompanying the exception
* @param t the exception (throwable) to log
*/
public static void trace(String msg, Throwable t) {
log.trace(msg, t);
}

/**
* Log a message at the DEBUG level.
*
* @param msg the message string to be logged
*/
public static void debug(String msg) {
}

/**
* Log a message at the DEBUG level according to the specified format
* and argument.
* <p/>
* <p>This form avoids superfluous object creation when the logger
* is disabled for the DEBUG level. </p>
*
* @param format the format string
* @param arg the argument
*/
public static void debug(String format, Object arg) {
}

/**
* Log a message at the DEBUG level according to the specified format
* and arguments.
* <p/>
* <p>This form avoids superfluous object creation when the logger
* is disabled for the DEBUG level. </p>
*
* @param format the format string
* @param arg1 the first argument
* @param arg2 the second argument
*/
public static void debug(String format, Object arg1, Object arg2) {
}

/**
* Log a message at the DEBUG level according to the specified format
* and arguments.
* <p/>
* <p>This form avoids superfluous string concatenation when the logger
* is disabled for the DEBUG level. However, this variant incurs the hidden
* (and relatively small) cost of creating an <code>Object[]</code> before invoking the method,
* even if this logger is disabled for DEBUG. The variants taking
* {@link #debug(String, Object) one} and {@link #debug(String, Object, Object) two}
* arguments exist solely in order to avoid this hidden cost.</p>
*
* @param format the format string
* @param arguments a list of 3 or more arguments
*/
public static void debug(String format, Object... arguments) {
}

/**
* Log an exception (throwable) at the DEBUG level with an
* accompanying message.
*
* @param msg the message accompanying the exception
* @param t the exception (throwable) to log
*/
public static void debug(String msg, Throwable t) {
}

/**
* Log a message at the INFO level.
*
* @param msg the message string to be logged
*/
public static void info(String msg) {
}

/**
* Log a message at the INFO level according to the specified format
* and argument.
* <p/>
* <p>This form avoids superfluous object creation when the logger
* is disabled for the INFO level. </p>
*
* @param format the format string
* @param arg the argument
*/
public static void info(String format, Object arg) {
}

/**
* Log a message at the INFO level according to the specified format
* and arguments.
* <p/>
* <p>This form avoids superfluous object creation when the logger
* is disabled for the INFO level. </p>
*
* @param format the format string
* @param arg1 the first argument
* @param arg2 the second argument
*/
public static void info(String format, Object arg1, Object arg2) {
}

/**
* Log a message at the INFO level according to the specified format
* and arguments.
* <p/>
* <p>This form avoids superfluous string concatenation when the logger
* is disabled for the INFO level. However, this variant incurs the hidden
* (and relatively small) cost of creating an <code>Object[]</code> before invoking the method,
* even if this logger is disabled for INFO. The variants taking
* {@link #info(String, Object) one} and {@link #info(String, Object, Object) two}
* arguments exist solely in order to avoid this hidden cost.</p>
*
* @param format the format string
* @param arguments a list of 3 or more arguments
*/
public static void info(String format, Object... arguments) {
}

/**
* Log an exception (throwable) at the INFO level with an
* accompanying message.
*
* @param msg the message accompanying the exception
* @param t the exception (throwable) to log
*/
public static void info(String msg, Throwable t) {
}

/**
* Log a message at the WARN level.
*
* @param msg the message string to be logged
*/
public static void warn(String msg) {
}

/**
* Log a message at the WARN level according to the specified format
* and argument.
* <p/>
* <p>This form avoids superfluous object creation when the logger
* is disabled for the WARN level. </p>
*
* @param format the format string
* @param arg the argument
*/
public static void warn(String format, Object arg) {
}

/**
* Log a message at the WARN level according to the specified format
* and arguments.
* <p/>
* <p>This form avoids superfluous string concatenation when the logger
* is disabled for the WARN level. However, this variant incurs the hidden
* (and relatively small) cost of creating an <code>Object[]</code> before invoking the method,
* even if this logger is disabled for WARN. The variants taking
* {@link #warn(String, Object) one} and {@link #warn(String, Object, Object) two}
* arguments exist solely in order to avoid this hidden cost.</p>
*
* @param format the format string
* @param arguments a list of 3 or more arguments
*/
public static void warn(String format, Object... arguments) {
}

/**
* Log a message at the WARN level according to the specified format
* and arguments.
* <p/>
* <p>This form avoids superfluous object creation when the logger
* is disabled for the WARN level. </p>
*
* @param format the format string
* @param arg1 the first argument
* @param arg2 the second argument
*/
public static void warn(String format, Object arg1, Object arg2) {
}

/**
* Log an exception (throwable) at the WARN level with an
* accompanying message.
*
* @param msg the message accompanying the exception
* @param t the exception (throwable) to log
*/
public static void warn(String msg, Throwable t) {
}

/**
* Log a message at the ERROR level.
*
* @param msg the message string to be logged
*/
public static void error(String msg) {
}

/**
* Log a message at the ERROR level according to the specified format
* and argument.
* <p/>
* <p>This form avoids superfluous object creation when the logger
* is disabled for the ERROR level. </p>
*
* @param format the format string
* @param arg the argument
*/
public static void error(String format, Object arg) {
}

/**
* Log a message at the ERROR level according to the specified format
* and arguments.
* <p/>
* <p>This form avoids superfluous object creation when the logger
* is disabled for the ERROR level. </p>
*
* @param format the format string
* @param arg1 the first argument
* @param arg2 the second argument
*/
public static void error(String format, Object arg1, Object arg2) {
}

/**
* Log a message at the ERROR level according to the specified format
* and arguments.
* <p/>
* <p>This form avoids superfluous string concatenation when the logger
* is disabled for the ERROR level. However, this variant incurs the hidden
* (and relatively small) cost of creating an <code>Object[]</code> before invoking the method,
* even if this logger is disabled for ERROR. The variants taking
* {@link #error(String, Object) one} and {@link #error(String, Object, Object) two}
* arguments exist solely in order to avoid this hidden cost.</p>
*
* @param format the format string
* @param arguments a list of 3 or more arguments
*/
public static void error(String format, Object... arguments) {
}

/**
* Log an exception (throwable) at the ERROR level with an
* accompanying message.
*
* @param msg the message accompanying the exception
* @param t the exception (throwable) to log
*/
public static void error(String msg, Throwable t) {
}

}

Step5 书写Interceptor切入方法

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
53
54
55
package com.github.hezhangjian.demo.agent.interceptor;

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.hezhangjian.demo.agent.util.AgentJacksonUtil;
import com.github.hezhangjian.demo.agent.util.AgentUtil;
import net.bytebuddy.asm.Advice;
import org.springframework.http.ResponseEntity;

import java.lang.reflect.Method;

/**
* @author hezhangjian
*/
public class RestControllerInterceptor {

private static final ThreadLocal<Long> costThreadLocal = new ThreadLocal<>();

/**
* #t Class名 ex: com.github.hezhangjian.demo.agent.test.controller.TestController
* #m Method名 ex: createJob
* #d Method描述 ex: (Ljava/lang/String;Lcom/github/hezhangjian/demo/agent/test/module/rest/CreateJobReqDto;)Lorg/springframework/http/ResponseEntity;
* #s 方法签名 ex: (java.lang.String,com.github.hezhangjian.demo.agent.test.module.rest.CreateJobReqDto)
* #r 返回类型 ex: org.springframework.http.ResponseEntity
*
* @param signature
*/
@Advice.OnMethodEnter
public static void enter(@Advice.Origin("#t #m") String signature) {
AgentUtil.info("[{}]", signature);
}

/**
* @param method 方法名
* @param args
* @param result
* @param thrown
*/
@SuppressWarnings("rawtypes")
@Advice.OnMethodExit(onThrowable = Throwable.class)
public static void exit(@Advice.Origin Method method, @Advice.AllArguments Object[] args,
@Advice.Return Object result, @Advice.Thrown Throwable thrown) {
AgentUtil.debug("method is [{}]", method);
final ArrayNode arrayNode = AgentJacksonUtil.createArrayNode();
for (Object arg : args) {
arrayNode.add(AgentJacksonUtil.toJson(arg));
}
final ObjectNode objectNode = AgentJacksonUtil.createObjectNode();
objectNode.set("args", arrayNode);
ResponseEntity responseEntity = (ResponseEntity) result;
AgentUtil.info("status code is [{}] args is [{}] result is [{}]", responseEntity.getStatusCode(), objectNode, responseEntity.getBody());
}


}

curl命令调用查看效果

1
2
2020-12-31,17:52:01,528+08:00(6121):INFO{}[http-nio-8080-exec-1#39]com.github.hezhangjian.demo.agent.util.AgentUtil.info(AgentUtil.java:167)-->[com.github.hezhangjian.demo.agent.test.controller.TestController createJob]
2020-12-31,17:52:01,544+08:00(6137):INFO{}[http-nio-8080-exec-1#39]com.github.hezhangjian.demo.agent.util.AgentUtil.info(AgentUtil.java:200)-->status code is [201 CREATED] args is [{"args":["\"111\"","{\"description\":\"description\",\"document\":\"document\"}"]}] result is [CreateJobRespDto(description=null, jobArn=null, jobId=null)]
0%