找回密码
 会员注册
查看: 22|回复: 0

字节跳动百万级MetricsAgent性能优化的探索与实践

[复制链接]

8

主题

0

回帖

25

积分

新手上路

积分
25
发表于 2024-10-1 06:55:08 | 显示全部楼层 |阅读模式
背景metricserver2 (以下简称Agent)是与字节内场时序数据库 ByteTSD 配套使用的用户指标打点 Agent,用于在物理机粒度收集用户的指标打点数据,在字节内几乎所有的服务节点上均有部署集成,装机量达到百万以上。此外Agent需要负责打点数据的解析、聚合、压缩、协议转换和发送,属于CPU和Mem密集的服务。两者结合,使得Agent在监控全链路服务成本中占比达到70%以上,对Agent进行性能优化,降本增效是刻不容缓的命题。本文将介绍我们在Agent性能优化上的探索和实践。基本架构Receiver 监听socket、UDP端口,接收SDK发出的metrics数据Msg-Parser对数据包进行反序列化,丢掉不符合规范的打点,然后将数据点暂存在Storage中Storage支持7种类型的metircs指标存储Flusher在每个发送周期的整时刻,触发任务获取Storage的快照,并对其存储的metrics数据进行聚合,将聚合后的数据按照发送要求进行编码Compress对编码的数据包进行压缩Sender支持HTTP和TCP方式,将数据发给后端服务我们将按照数据接收、数据处理、数据发送三个部分来分析Agent优化的性能热点。数据接收Case 1Agent与用户SDK通信的时候,使用 msgpack 对数据进行序列化。它的数据格式与json类似,但在存储时对数字、多字节字符、数组等都做了优化,减少了无用的字符,下图是其与json的简单对比:Agent在获得数据后,需要通过msgpack.unpack进行反序列化,然后把数据重新组织成 std::vector。这个过程中,有两步复制的操作,分别是:从上游数据反序列为 msgpack:bject 和 msgpack:bject 转换 std::vector。{//ProcessFunctionmsgpack::unpackedmsg;msgpack::unpack(&msg,buffer.data(),buffer.size());msgpack:bjectobj=msg.get();std::vector>vecs;if(obj.via.array.ptr[0].type==5){std::vectorvec;obj.convert(&vec);vecs.push_back(vec);}elseif(obj.via.array.ptr[0].type==6){obj.convert(&vecs);}else{++fail_count;returnresult;}//Somemoreprocesssteps}但实际上,整个数据的处理都在处理函数中。这意味着传过来的数据在整个处理周期都是存在的,因此这两步复制可以视为额外的开销。msgpack协议在对数据进行反序列化解析的时候,其内存管理的基本逻辑如下:为了避免复制 string,bin 这些类型的数据,msgpack 支持在解析的时候传入一个函数,用来决定这些类型的数据是否需要进行复制:因此在第二步,对 msgpack:bject 进行转换的时候,我们不再转换为 string,而是使用 string_view,可以优化掉 string 的复制和内存分配等://Definestring_viewconvertstruct.templatestructmsgpack::adaptor::convert{msgpack:bjectconst&operator()(msgpack:bjectconst&o,std::string_view&v)const{switch(o.type){casemsgpack::type::BIN:v=std::string_view(o.via.bin.ptr,o.via.bin.size);break;casemsgpack::type::STR:v=std::string_view(o.via.str.ptr,o.via.str.size);break;default:throwmsgpack::type_error();break;}returno;}};staticboolstring_reference(msgpack::type:bject_typetype,std::size_t,void*){returntype==msgpack::type::STR;}{msgpack::unpackedmsg;msgpack::unpack(msg,buffer.data(),buffer.size(),string_reference);msgpack:bjectobj=msg.get();std::vector>vecs;if(obj.via.array.ptr[0].type==msgpack::type::STR){std::vectorvec;obj.convert(&vec);vecs.push_back(vec);}elseif(obj.via.array.ptr[0].type==msgpack::type::ARRAY){obj.convert(&vecs);}else{++fail_count;returnresult;}}经过验证可以看到:零拷贝的时候,转换完的所有数据的内存地址都在原来的的 buffer 的内存地址范围内。而使用 string 进行复制的时候,内存地址和 buffer 的内存地址明显不同。Case 2Agent在接收端通过系统调用完成数据接收后,会立刻将数据投递到异步的线程池内,进行数据的解析工作,以达到不阻塞接收端的效果。但我们在对线上数据进行分析时发现,用户产生的数据包大小是不固定的,并且存在大量的小包(比如一条打点数据)。这会导致异步线程池内的任务数量较多,平均每个任务的体积较小,线程池需要频繁的从队列获取新的任务,带来了处理性能的下降。因此我们充分理解了msgpack的协议格式(https://github.com/msgpack/msgpack/blob/master/spec.md)后,在接收端将多个数据小包(一条打点数据)聚合成一个数据大包(多条打点数据),进行一次任务提交,提高了接收端的处理性能,降低了线程切换的开销。staticinlinebooltryMerge(std::string&merge_buf,std::string&recv_buf,intmsg_size,intmerge_buf_cap){uint16_tbig_endian_len,host_endian_len,cur_msg_len;memcpy(&big_endian_len,(void*)&merge_buf[1],sizeof(big_endian_len));host_endian_len=ntohs(big_endian_len);cur_msg_len=recv_buf[0]&0x0f;if((recv_buf[0]&0xf0)!=0x90||merge_buf.size()+msg_size>merge_buf_cap||host_endian_len+cur_msg_len>0xffff){//upper4digitsarenot1001//ormerge_bufcannotholdanymoredata//orarray16inthemerge_bufcannotholdmoreobjs(althoughnotpossiblerightnow,buthavetocheck)returnfalse;}//startmerginghost_endian_len+=cur_msg_len;merge_buf.append(++recv_buf.begin(),recv_buf.begin()+msg_size);//updateelemcntinarray16big_endian_len=htons(host_endian_len);memcpy((void*)&merge_buf[1],&big_endian_len,sizeof(big_endian_len));returntrue;}{//receiverfunction//array16with0memberstd::stringmerge_buf({(char)0xdc,(char)0x00,(char)0x00});for(inti=0;i(tmp_buffer_.data()),tmp_buffer_size_,0);if(r>0){if(!tryMerge(merge_buf,tmp_buffer_,r,tmp_buffer_size_)){//SubmitTask}//Someotherlogics}}从关键的系统指标的角度看,在merge逻辑有收益时(接收QPS = 48k,75k,120k,150k),小包合并逻辑大大减少了上下文切换,执行指令数,icache/dcache miss,并且增加了IPC(instructions per cycle)见下表:同时通过对前后火焰图的对比分析看,在合并数据包之后,原本用于调度线程池的cpu资源更多的消耗在了收包上,也解释了小包合并之后context switch减少的情况。Case 3用户在打点指标中的Tags,是拼接成字符串进行纯文本传递的,这样设计的主要目的是简化SDK和Agent之间的数据格式。但这种方式就要求Agent必须对字符串进行解析,将文本化的Tags反序列化出来,又由于在接收端收到的用户打点QPS很高,这也成为了Agent的性能热点。早期Agent在实现这个解析操作时,采用了遍历字符串的方式,将字符串按| 和 =分割成 key-value 对。在其成为性能瓶颈后,我们发现它很适合使用SIMD进行加速处理。原版inlineboolis_tag_split(constchar&c){returnc=='|'||c=='';}inlineboolis_kv_split(constchar&c){returnc=='=';}boolfind_str_with_delimiters(constchar*str,conststd::size_t&cur_idx,conststd::size_t&end_idx,constProcess_State&state,std::size_t*str_end){if(cur_idx>=end_idx){returnfalse;}std::size_tindex=cur_idx;while(index=end){return0;}for(;idx+16=end){return0;}for(;idx+16#include#includestructTagView{TagView()=default;TagView(std::string_viewk,std::string_viewv):key_(k),value_(v){}std::string_viewkey_;std::string_viewvalue_;};structTagViewSet{TagViewSet()=default;TagViewSet(conststd::vector&tgs,std::string&buffer):tags(tgs),tags_buffer(std::move(buffer)){}TagViewSet(std::vector&tgs,std::string&buffer){tags=std::move(tgs);}TagViewSet(conststd::vector&tgs,size_tbuffer_assume_size){tags.reserve(tgs.size());tags_buffer.reserve(buffer_assume_size);for(auto&tg:tgs){tags_buffer+=tg.key_;tags_buffer+=tg.value_;}constchar*start=tags_buffer.c_str();for(auto&tg:tgs){std::string_viewkey(start,tg.key_.size());start+=key.size();std::string_viewvalue(start,tg.value_.size());start+=value.size();tags.emplace_back(key,value);}}booloperator==(constTagViewSet&other)const{if(tags.size()!=other.tags.size()){returnfalse;}//notcompareeverytagreturntags_buffer==other.tags_buffer;}std::vectortags;std::stringtags_buffer;};structTagViewSetPtrHash{inlinestd::size_toperator()(constTagViewSet*tgs)const{returnstd::hash{}(tgs->tags_buffer);}};验证结果表明,当 Tagset 中 kv 的个数大于 2 的时候,新方法性能较好。数据发送Case 1早期Agent使用zlib进行数据发送前的压缩,随着用户打点规模的增长,压缩逐步成为了Agent的性能热点。因此我们通过构造满足线上用户数据特征的数据集,对常用的压缩库进行了测试:zlib使用cloudflarezlib使用1.2.11通过测试结果我们可以看到,除bzip2外,其他压缩算法均在不同程度上优于zlib:zlib的高性能分支,基于cloudflare优化 比 1.2.11的官方分支性能好,压缩CPU开销约为后者的37.5%采用SIMD指令加速计算zstd能够在压缩率低于zlib的情况下,获得更低的cpu开销,因此如果希望获得比当前更好的压缩率,可以考虑zstd算法若不考虑压缩率的影响,追求极致低的cpu开销,那么snappy是更好的选择结合业务场景考虑,我们最终执行短期使用 zlib-cloudflare 替换,长期使用 zstd 替换的优化方案。结论上述优化取得了非常好的效果,经过上线验证得出:CPU峰值使用量降低了10.26%,平均使用量降低了6.27%Mem峰值使用量降低了19.67%,平均使用量降低了19.81%综合分析以上性能热点和优化方案,可以看到我们对Agent优化的主要考量点是:减少不必要的内存拷贝减少程序上下文的切换开销,提高缓存命中率使用SIMD指令来加速处理关键性的热点逻辑除此之外,我们还在开展 PGO 和 clang thinLTO 的验证工作,借助编译器的能力来进一步优化Agent性能。加入我们本文作者赵杰裔,来自字节跳动 基础架构-云原生-可观测团队,我们提供日均数十PB级可观测性数据采集、存储和查询分析的引擎底座,致力于为业务、业务中台、基础架构建设完整统一的可观测性技术支撑能力。同时,我们也将逐步开展在火山引擎上构建可观测性的云产品,较大程度地输出多年技术沉淀。 如果你也想一起攻克技术难题,迎接更大的技术挑战,欢迎投递简历到 zhaojieyi@bytedance.com最 Nice 的工作氛围和成长机会,福利与机遇多多,在上海、杭州和北京均有职位,欢迎加入字节跳动可观测团队 !参考引用v2_0_cpp_unpacker:https://github.com/msgpack/msgpack-c/wiki/v2_0_cpp_unpacker#memory-managementmessagepack-specification:https://github.com/msgpack/msgpack/blob/master/spec.mdCloudflare fork of zlib with massive performance improvements:https://github.com/RJVB/zlib-cloudflareIntel Intrinsics Guide:https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.htmlProfile-guided optimization:https://en.wikipedia.org/wiki/Profile-guided_optimizationThinLTO:https://clang.llvm.org/docs/ThinLTO.html
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-13 11:02 , Processed in 0.695239 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表