2020年6月份天池举办的《中间件性能挑战赛》可谓是异常激烈,本人抽业余时间报名参与,感受比赛惨烈的同时,也有诸多感慨哈,总结一个多月的赛程,多少有一些心得与大家分享
赛题地址: https://tianchi.aliyun.com/competition/entrance/231790/information
赛题背景
本题目是另外一种采样方式(tail-based Sampling),只要请求的链路追踪数据中任何节点出现重要数据特征(错慢请求),这个请求的所有链路数据都采集。目前开源的链路追踪产品都没有实现tail-based Sampling,主要的挑战是:任何节点出现符合采样条件的链路数据,那就需要把这个请求的所有链路数据采集。即使其他链路数据在这条链路节点数据之前还是之后产生,即使其他链路数据在分布式系统的成百上千台机器上产生。
整体流程
用户需要实现两个程序,一个是数量流(橙色标记)的处理程序,该机器可以获取数据源的http地址,拉取数据后进行处理,一个是后端程序(蓝色标记),和客户端数据流处理程序通信,将最终数据结果在后端引擎机器上计算。具体描述可直接打开赛题地址 https://tianchi.aliyun.com/competition/entrance/231790/information。此处不再赘述
可将整体流程粗略分为三大部分
遵循原则:各部分协调好,可抽象为生成、消费模型,切勿产生数据饥饿;理想效果是stream流完,计算也跟着马上结束
最先想到的方案是以trace细粒度控制的方案
因题目中明确表明某个 trace 的数据出现的位置前后不超过2万上,故每2万行是非常重要的特征
BufferedReader.readLine()
此方案的跑分大致在 25s 左右,成绩不甚理想,总结原因大致可分为以下几种
基于上述原因,为了充分利用 2万行的数据特征,引入方案二
说明:为了更优雅处理数据过期及充分利用2万行特性,故衍生出此版本
因题目中明确表明某个trace的数据出现的位置前后不超过2万上,故每2万行数据可作为一个批次存储,过期数据自动删除
① 按行读取字符流
BufferedReader.readLine()
② 每2万行数据作为一个batch,并分配唯一的batchId(自增即可),此处涉及大量cpu计算,分为2部分
error=1
或http.status_code!=200
的数据并将其暂存List
中,方便后续 backend 节点拿取数据③ 上报 badTraceId
④ 通知2个 front 节点发送指定 traceIds 的全量数据
⑤ 计算结果
当前方案耗时在20s左右,统计发现字符流的读取耗时15s,其他耗时5s,且监控发现各个缓冲区没有发现饥饿、过剩的情况,所以当前方案的瓶颈还是卡在字符流的读取、以及cpu判断上,所以一套面向字节流处理的方案呼之欲出
BufferedReader
源码,发现其将字节流转换字符流做了大量的工作,也是耗时的源头,故需要将当前方案改造为面向字节的操作字符处理是耗时的根源,只能将方案改造为面向字节的方式,倘若如此,java 的大部分数据结构不能再使用
大层面的设计思想与方案二一致,不过面向字节处理的话,从读取流、截断行、判断是否为bad trace、数据组装等均需为字节操作
① 读取字节流
byte[]
,每个数组存放10M数据② 数据处理
int[20000]
替换之前动态分配内存的数据结构体 List
,只记录每行开始的 position③ 上报 badTraceId
④ 通知2个 front 节点发送指定 traceIds 的全量数据
int[20000]
,将符合要求的 trace 数据放入自定义规范的byte[]
注:刚开始设计的(快排+归并排序)的方案效果不明显,且线上的评测环境的2个 front 节点压力很大,再考虑到某个 trace 对应的 span 数据只有几十条,故此处将所有的排序操作都下放给 backend 节点,从而减轻 front 压力byte[]
存储数据的设计如下
byte[]
,来存储一个批次中的所有 bad trace 对应 span 数据byte[]
及有效长度⑤ 计算结果
byte[]
数据A线程还未下载完毕,那么B-Thread将被阻塞(io阻塞)Semaphore
,为 A-Thread 与 B-Thread 同步服务,A-Thread 产生数据后,对应 slot 的信号量+1,B-Thread 消费数据之前,需要semaphore.acquire()
volatile
及纳秒级睡眠Thread.sleep(0, 2)
实现高效响应。实际测试,某些场景中,该组合性能超过Semaphore
;C-Thread 发现 B-Thread 还未产出 next batch 的数据,那么进入等待状态volatile
及纳秒级睡眠Thread.sleep(0, 2)
打印gc输出日志时发现,程序会发生3-5次 full gc,导致性能欠佳,分析内存使用场景发现,流式输出的数据模型,在内存中只会存在很短的一段时间便会失效,真正流入老年代的内存是很小的,故应调大新生代占比
java -Dserver.port=$SERVER_PORT -Xms3900m -Xmx3900m -Xmn3500m -jar tailbaseSampling-1.0-SNAPSHOT.jar &
直接分配约 4g 的空间,其中新生代占 3.5g,通过观测 full gc 消失;此举可使评测快2-3s
此方案最优成绩跑到了5.7s,性能有了较大提升,总结快的原因如下:
奇技淫巧,俗称偷鸡,本不打算写该模块,不过很多上分的小技巧都源于此,真实的通用化场景中,可能本模块作用不大,不过比赛就是这样,无所不用其极。。。
因java语言设计缘故,凡事读取比 int 小的数据类型,统一转为 int 后操作,试想以下代码
while ((byteNum = input.read(data)) != -1) {
for (int i = 0; i < byteNum; i++) {
if (data[i] == 10) {
count++;
}
}
}
大量的字节对比操作,每次对比,均把一个 byte 转换为 4个 byte,效率可想而知
一个典型的提高字节数组对比效率的例子,采用万能的Unsafe
,一次性获取8个byte long val = unsafe.getLong(lineByteArr, pos + Unsafe.ARRAY_BYTE_BASE_OFFSET);
然后比较2个 long 值是否相等,提速是成倍增长的,那么怎么用到本次赛题上呢?
span数据是类似这样格式的
193081e285d91b5a|1593760002553450|1e86d0a94dab70d|28b74c9f5e05b2af|508|PromotionCenter|DoGetCommercialStatus|192.168.102.13|http.status_code=200&component=java-web-servlet&span.kind=server&bizErr=4-failGetOrder&http.method=GET
用"|"切割后,倒数第二位是ip,且格式固定为192.168.***.***
,如果采用Unsafe
,每次读取一个 int 时,势必会落在192.168.
中间,有4种可能192.
、92.1
、2.16
、.168
,故可利用此特性,直接进行 int 判断
int val = unsafe.getInt(data, beginPos + Unsafe.ARRAY_BYTE_BASE_OFFSET);
if (val == 775043377 || val == 825111097 || val == 909192754 || val == 943075630) {
}
此“技巧”提速1-2秒
提供2种遍历字节数组方式,哪种效率更高
方式1
byte[] data = new byte[1024 * 1024 * 2];
int byteNum;
while ((byteNum = input.read(data)) != -1) {
for (int i = 0; i < byteNum; i++) {
if (data[i] == 10) {
count++;
}
}
}
方式2
byte[] data = new byte[1024 * 1024 * 2];
int byteNum;
int beginIndex;
int endIndex;
int beginPos;
while ((byteNum = input.read(data)) != -1) {
beginIndex = 0;
endIndex = byteNum;
beginPos = 0;
while (beginIndex < endIndex) {
int i;
for (i = beginPos; i < endIndex; i++) {
if (data[i] == 124) {
beginPos = i + 1;
times++;
break;
} else {
if (data[i] == 10) {
count++;
beginIndex = i + 1;
beginPos = i + 1;
break;
}
}
}
if (i >= byteNum) {
break;
}
}
}
两种方式达到的效果一样,都是寻找换行符。方式2不同的是,每次找到换行符都 break 掉当前循环,然后从之前位置继续循环。其实这个小点卡了我1个星期,就是将字符流转换为字节流时,性能几乎没有得到提高,换成方式2后,性能至少提高一倍。为什么会呈现这样一种现象,我还没找到相关资料,有知道的同学,还望不吝赐教哈
这种cpu密集型的赛题,一向是 c/cpp 大展身手的舞台,前排几乎被其霸占。作为一名多年 crud 的 javaer,经过无数个通宵达旦,最终拿到了集团第6的成绩,虽不算优异,但自己也尽力了哈
最终比赛成绩贴上哈