当前位置:首页 > 科技  > 软件

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

来源: 责编: 时间:2024-01-03 17:21:47 133观看
导读背景图片metricserver2 (以下简称Agent)是与字节内场时序数据库 ByteTSD 配套使用的用户指标打点 Agent,用于在物理机粒度收集用户的指标打点数据,在字节内几乎所有的服务节点上均有部署集成,装机量达到百万以上。此外Agen

gt828资讯网——每日最新资讯28at.com

背景

图片图片gt828资讯网——每日最新资讯28at.com

metricserver2 (以下简称Agent)是与字节内场时序数据库 ByteTSD 配套使用的用户指标打点 Agent,用于在物理机粒度收集用户的指标打点数据,在字节内几乎所有的服务节点上均有部署集成,装机量达到百万以上。此外Agent需要负责打点数据的解析、聚合、压缩、协议转换和发送,属于CPU和Mem密集的服务。两者结合,使得Agent在监控全链路服务成本中占比达到70%以上,对Agent进行性能优化,降本增效是刻不容缓的命题。gt828资讯网——每日最新资讯28at.com

基本架构

图片图片gt828资讯网——每日最新资讯28at.com

  • Receiver 监听socket、UDP端口,接收SDK发出的metrics数据
  • Msg-Parser对数据包进行反序列化,丢掉不符合规范的打点,然后将数据点暂存在Storage中
  • Storage支持7种类型的metircs指标存储
  • Flusher在每个发送周期的整时刻,触发任务获取Storage的快照,并对其存储的metrics数据进行聚合,将聚合后的数据按照发送要求进行编码
  • Compress对编码的数据包进行压缩
  • Sender支持HTTP和TCP方式,将数据发给后端服务

我们将按照数据接收、数据处理、数据发送三个部分来分析Agent优化的性能热点。gt828资讯网——每日最新资讯28at.com

数据接收

Case 1

Agent与用户SDK通信的时候,使用 msgpack 对数据进行序列化。它的数据格式与json类似,但在存储时对数字、多字节字符、数组等都做了优化,减少了无用的字符,下图是其与json的简单对比:gt828资讯网——每日最新资讯28at.com

图片图片gt828资讯网——每日最新资讯28at.com

Agent在获得数据后,需要通过msgpack.unpack进行反序列化,然后把数据重新组织成 std::vector。这个过程中,有两步复制的操作,分别是:从上游数据反序列为 msgpack::object 和 msgpack::object 转换 std::vector。gt828资讯网——每日最新资讯28at.com

{ // Process Function    msgpack::unpacked msg;    msgpack::unpack(&msg, buffer.data(), buffer.size());    msgpack::object obj = msg.get();    std::vector<std::vector<std::string>> vecs;    if (obj.via.array.ptr[0].type == 5) {        std::vector<std::string> vec;        obj.convert(&vec);        vecs.push_back(vec);    } else if (obj.via.array.ptr[0].type == 6) {        obj.convert(&vecs);    } else {        ++fail_count;        return result;    }    // Some more process steps}

但实际上,整个数据的处理都在处理函数中。这意味着传过来的数据在整个处理周期都是存在的,因此这两步复制可以视为额外的开销。gt828资讯网——每日最新资讯28at.com

msgpack协议在对数据进行反序列化解析的时候,其内存管理的基本逻辑如下:gt828资讯网——每日最新资讯28at.com

图片图片gt828资讯网——每日最新资讯28at.com

为了避免复制 string,bin 这些类型的数据,msgpack 支持在解析的时候传入一个函数,用来决定这些类型的数据是否需要进行复制:gt828资讯网——每日最新资讯28at.com

图片图片gt828资讯网——每日最新资讯28at.com

因此在第二步,对 msgpack::object 进行转换的时候,我们不再转换为 string,而是使用 string_view,可以优化掉 string 的复制和内存分配等:gt828资讯网——每日最新资讯28at.com

// Define string_view convert struct.template <>struct msgpack::adaptor::convert<std::string_view> {    msgpack::object const& operator()(msgpack::object const& o, std::string_view& v) const {        switch (o.type) {        case msgpack::type::BIN:            v = std::string_view(o.via.bin.ptr, o.via.bin.size);            break;        case msgpack::type::STR:            v = std::string_view(o.via.str.ptr, o.via.str.size);            break;        default:            throw msgpack::type_error();            break;        }        return o;    }};static bool string_reference(msgpack::type::object_type type, std::size_t, void*) {    return type == msgpack::type::STR;}{     msgpack::unpacked msg;    msgpack::unpack(msg, buffer.data(), buffer.size(), string_reference);    msgpack::object obj = msg.get();    std::vector<std::vector<std::string_view>> vecs;    if (obj.via.array.ptr[0].type == msgpack::type::STR) {        std::vector<std::string_view> vec;        obj.convert(&vec);        vecs.push_back(vec);    } else if (obj.via.array.ptr[0].type == msgpack::type::ARRAY) {        obj.convert(&vecs);    } else {        ++fail_count;        return result;    }}

经过验证可以看到:零拷贝的时候,转换完的所有数据的内存地址都在原来的的 buffer 的内存地址范围内。而使用 string 进行复制的时候,内存地址和 buffer 的内存地址明显不同。gt828资讯网——每日最新资讯28at.com

图片图片gt828资讯网——每日最新资讯28at.com

Case 2

图片图片gt828资讯网——每日最新资讯28at.com

Agent在接收端通过系统调用完成数据接收后,会立刻将数据投递到异步的线程池内,进行数据的解析工作,以达到不阻塞接收端的效果。但我们在对线上数据进行分析时发现,用户产生的数据包大小是不固定的,并且存在大量的小包(比如一条打点数据)。这会导致异步线程池内的任务数量较多,平均每个任务的体积较小,线程池需要频繁的从队列获取新的任务,带来了处理性能的下降。gt828资讯网——每日最新资讯28at.com

因此我们充分理解了msgpack的协议格式(https://github.com/msgpack/msgpack/blob/master/spec.md)后,在接收端将多个数据小包(一条打点数据)聚合成一个数据大包(多条打点数据),进行一次任务提交,提高了接收端的处理性能,降低了线程切换的开销。gt828资讯网——每日最新资讯28at.com

static inline bool tryMerge(std::string& merge_buf, std::string& recv_buf, int msg_size, int merge_buf_cap) {    uint16_t big_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) {        // upper 4 digits are not 1001        // or merge_buf cannot hold anymore data        // or array 16 in the merge_buf cannot hold more objs (although not possible right now, but have to check)        return false;    }    // start merging    host_endian_len += cur_msg_len;    merge_buf.append(++recv_buf.begin(), recv_buf.begin() + msg_size);    // update elem cnt in array 16    big_endian_len = htons(host_endian_len);    memcpy((void*)&merge_buf[1], &big_endian_len, sizeof(big_endian_len));    return true;}{ // receiver function     // array 16 with 0 member    std::string merge_buf({(char)0xdc, (char)0x00, (char)0x00});    for(int i = 0 ; i < 1024; ++i) {        int r = recv(fd, const_cast<char *>(tmp_buffer_.data()), tmp_buffer_size_, 0);        if (r > 0) {            if(!tryMerge(merge_buf, tmp_buffer_, r, tmp_buffer_size_)) {                // Submit Task            }        // Some other logics    }}

从关键的系统指标的角度看,在merge逻辑有收益时(接收QPS = 48k,75k,120k,150k),小包合并逻辑大大减少了上下文切换,执行指令数,icache/dcache miss,并且增加了IPC(instructions per cycle)见下表:gt828资讯网——每日最新资讯28at.com

图片gt828资讯网——每日最新资讯28at.com

同时通过对前后火焰图的对比分析看,在合并数据包之后,原本用于调度线程池的cpu资源更多的消耗在了收包上,也解释了小包合并之后context switch减少的情况。gt828资讯网——每日最新资讯28at.com

Case 3

用户在打点指标中的Tags,是拼接成字符串进行纯文本传递的,这样设计的主要目的是简化SDK和Agent之间的数据格式。但这种方式就要求Agent必须对字符串进行解析,将文本化的Tags反序列化出来,又由于在接收端收到的用户打点QPS很高,这也成为了Agent的性能热点。gt828资讯网——每日最新资讯28at.com

早期Agent在实现这个解析操作时,采用了遍历字符串的方式,将字符串按|=分割成 key-value 对。在其成为性能瓶颈后,我们发现它很适合使用SIMD进行加速处理。gt828资讯网——每日最新资讯28at.com

原版gt828资讯网——每日最新资讯28at.com

inline bool is_tag_split(const char &c) {    return c == '|' || c == ' ';}inline bool is_kv_split(const char &c) {    return c == '=';}bool find_str_with_delimiters(const char *str, const std::size_t &cur_idx, const std::size_t &end_idx,    const Process_State &state, std::size_t *str_end) {    if (cur_idx >= end_idx) {        return false;    }    std::size_t index = cur_idx;    while (index < end_idx) {        if (state == TAG_KEY) {            if (is_kv_split(str[index])) {                *str_end = index;                return true;            } else if (is_tag_split(str[index])) {                return false;            }        } else {            if (is_tag_split(str[index])) {                *str_end = index;                return true;            }        }        index++;    }    if (state == TAG_VALUE) {        *str_end = index;        return true;    }    return false;}

SIMD gt828资讯网——每日最新资讯28at.com

#if defined(__SSE__)static std::size_t find_key_simd(const char *str, std::size_t end, std::size_t idx) {    if (idx >= end) { return 0; }    for (; idx + 16 <= end; idx += 16) {        __m128i v = _mm_loadu_si128((const __m128i*)(str + idx));        __m128i is_tag = _mm_or_si128(_mm_cmpeq_epi8(v, _mm_set1_epi8('|')),                                     _mm_cmpeq_epi8(v, _mm_set1_epi8(' ')));        __m128i is_kv = _mm_cmpeq_epi8(v, _mm_set1_epi8('='));        int tag_bits = _mm_movemask_epi8(is_tag);        int kv_bits = _mm_movemask_epi8(is_kv);        // has '|' or ' ' first        bool has_tag_first = ((kv_bits - 1) & tag_bits) != 0;        if (has_tag_first) { return 0; }        if (kv_bits) { // found '='            return idx + __builtin_ctz(kv_bits);        }    }    for (; idx < end; ++idx) {        if (is_kv_split(str[idx])) { return idx; }         else if (is_tag_split(str[idx])) { return 0; }    }    return 0;}static std::size_t find_value_simd(const char *str, std::size_t end, std::size_t idx) {    if (idx >= end) { return 0; }    for (; idx + 16 <= end; idx += 16) {        __m128i v = _mm_loadu_si128((const __m128i*)(str + idx));        __m128i is_tag = _mm_or_si128(_mm_cmpeq_epi8(v, _mm_set1_epi8('|')),                                     _mm_cmpeq_epi8(v, _mm_set1_epi8(' ')));        int tag_bits = _mm_movemask_epi8(is_tag);        if (tag_bits) {            return idx + __builtin_ctz(tag_bits);        }    }    for (; idx < end; ++idx) {        if (is_tag_split(str[idx])) { return idx; }    }    return idx;}

构建的测试用例格式为 gt828资讯网——每日最新资讯28at.com

。text 则是测试例子里的 str_size,用来测试不同 str_size 下使用 simd 的收益。可以看到,在 str_size 较大时,simd 性能明显高于标量的实现。gt828资讯网——每日最新资讯28at.com

str_size
gt828资讯网——每日最新资讯28at.com

simd
gt828资讯网——每日最新资讯28at.com

scalar
gt828资讯网——每日最新资讯28at.com

1
gt828资讯网——每日最新资讯28at.com

109
gt828资讯网——每日最新资讯28at.com

140
gt828资讯网——每日最新资讯28at.com

2
gt828资讯网——每日最新资讯28at.com

145
gt828资讯网——每日最新资讯28at.com

158
gt828资讯网——每日最新资讯28at.com

4
gt828资讯网——每日最新资讯28at.com

147
gt828资讯网——每日最新资讯28at.com

198
gt828资讯网——每日最新资讯28at.com

8
gt828资讯网——每日最新资讯28at.com

143
gt828资讯网——每日最新资讯28at.com

283
gt828资讯网——每日最新资讯28at.com

16
gt828资讯网——每日最新资讯28at.com

155
gt828资讯网——每日最新资讯28at.com

459
gt828资讯网——每日最新资讯28at.com

32
gt828资讯网——每日最新资讯28at.com

168
gt828资讯网——每日最新资讯28at.com

809
gt828资讯网——每日最新资讯28at.com

64
gt828资讯网——每日最新资讯28at.com

220
gt828资讯网——每日最新资讯28at.com

1589
gt828资讯网——每日最新资讯28at.com

128
gt828资讯网——每日最新资讯28at.com

289
gt828资讯网——每日最新资讯28at.com

3216
gt828资讯网——每日最新资讯28at.com

256
gt828资讯网——每日最新资讯28at.com

477
gt828资讯网——每日最新资讯28at.com

6297
gt828资讯网——每日最新资讯28at.com

512
gt828资讯网——每日最新资讯28at.com

883
gt828资讯网——每日最新资讯28at.com

12494
gt828资讯网——每日最新资讯28at.com

1024
gt828资讯网——每日最新资讯28at.com

1687
gt828资讯网——每日最新资讯28at.com

24410
gt828资讯网——每日最新资讯28at.com

数据处理

Case 1

Agent在数据聚合过程中,需要一个map来存储一个指标的所有序列,用于对一段时间内的打点值进行聚合计算,得到一个固定间隔的观测值。这个map的key是指标的tags,map的value是指标的值。我们通过采集火焰图发现,这个map的查找操作存在一定程度的热点。gt828资讯网——每日最新资讯28at.com

图片图片gt828资讯网——每日最新资讯28at.com

下面是 _M_find_before_node 的实现:gt828资讯网——每日最新资讯28at.com

图片图片gt828资讯网——每日最新资讯28at.com

这个函数作用是:算完 hash 后,在 hash 桶里找到匹配 key 的元素。这也意味着,即使命中了,hash 查找的时候也要进行一次 key 的比较操作。而在 Agent 里,这个 key 的比较操作定义为:gt828资讯网——每日最新资讯28at.com

bool operator==(const TagSet &other) const {        if (tags.size() != other.tags.size()) {            return false;        }        for (size_t i = 0; i < tags.size(); ++i) {            auto &left = tags[i];            auto &right = other.tags[i];            if (left.key_ != right.key_ || left.value_ != right.value_) {                return false;            }        }        return true;    }

这里需要遍历整个 Tagset 的元素并比较他们是否相等。在查找较多的情况下,每次 hash 命中后都要进行这样一次操作是非常耗时的。可能导致时间开销增大的原因有:gt828资讯网——每日最新资讯28at.com

  1. 每个 tag 的 key_ 和 value_ 是单独的内存(如果数据较短,stl 不会额外分配内存,这样的情况下就没有单独分配的内存了),存在着 cache miss 的开销,硬件预取效果也会变差;
  2. 需要频繁地调用 memcmp 函数;
  3. 按个比较每个 tag,分支较多。

图片图片gt828资讯网——每日最新资讯28at.com

因此,我们将 TagSet 的数据使用 string_view 表示,并将所有的 data 全部存放在同一块内存中。在 dictionary encode 的时候,再把 TagSet 转换成 string 的格式返回出去。gt828资讯网——每日最新资讯28at.com

// TagView #include <functional>#include <string>#include <vector>struct TagView {    TagView() = default;    TagView(std::string_view k, std::string_view v) : key_(k), value_(v) {}    std::string_view key_;    std::string_view value_;};struct TagViewSet {    TagViewSet() = default;    TagViewSet(const std::vector<TagView> &tgs, std::string&& buffer) : tags(tgs),         tags_buffer(std::move(buffer)) {}    TagViewSet(std::vector<TagView> &&tgs, std::string&& buffer) { tags = std::move(tgs); }    TagViewSet(const std::vector<TagView> &tgs, size_t buffer_assume_size) {        tags.reserve(tgs.size());        tags_buffer.reserve(buffer_assume_size);        for (auto& tg : tgs) {            tags_buffer += tg.key_;            tags_buffer += tg.value_;        }        const char* start = tags_buffer.c_str();        for (auto& tg : tgs) {            std::string_view key(start, tg.key_.size());            start += key.size();            std::string_view value(start, tg.value_.size());            start += value.size();            tags.emplace_back(key, value);        }    }    bool operator==(const TagViewSet &other) const {        if (tags.size() != other.tags.size()) {            return false;        }        // not compare every tag        return tags_buffer == other.tags_buffer;    }    std::vector<TagView> tags;    std::string tags_buffer;};struct TagViewSetPtrHash {    inline std::size_t operator()(const TagViewSet *tgs) const {        return std::hash<std::string>{}(tgs->tags_buffer);    }};

验证结果表明,当 Tagset 中 kv 的个数大于 2 的时候,新方法性能较好。gt828资讯网——每日最新资讯28at.com

图片图片gt828资讯网——每日最新资讯28at.com

数据发送

Case 1

早期Agent使用zlib进行数据发送前的压缩,随着用户打点规模的增长,压缩逐步成为了Agent的性能热点。gt828资讯网——每日最新资讯28at.com

因此我们通过构造满足线上用户数据特征的数据集,对常用的压缩库进行了测试:gt828资讯网——每日最新资讯28at.com

zlib使用cloudflaregt828资讯网——每日最新资讯28at.com

图片图片gt828资讯网——每日最新资讯28at.com

zlib使用1.2.11gt828资讯网——每日最新资讯28at.com

图片图片gt828资讯网——每日最新资讯28at.com

通过测试结果我们可以看到,除bzip2外,其他压缩算法均在不同程度上优于zlib:gt828资讯网——每日最新资讯28at.com

  • zlib的高性能分支,基于cloudflare优化 比 1.2.11的官方分支性能好,压缩CPU开销约为后者的37.5%
  • 采用SIMD指令加速计算
  • zstd能够在压缩率低于zlib的情况下,获得更低的cpu开销,因此如果希望获得比当前更好的压缩率,可以考虑zstd算法
  • 若不考虑压缩率的影响,追求极致低的cpu开销,那么snappy是更好的选择

结合业务场景考虑,我们最终执行短期使用 zlib-cloudflare 替换,长期使用 zstd 替换的优化方案。gt828资讯网——每日最新资讯28at.com

结论

上述优化取得了非常好的效果,经过上线验证得出:gt828资讯网——每日最新资讯28at.com

  • CPU峰值使用量降低了10.26%,平均使用量降低了6.27%
  • Mem峰值使用量降低了19.67%,平均使用量降低了19.81%

综合分析以上性能热点和优化方案,可以看到我们对Agent优化的主要考量点是:gt828资讯网——每日最新资讯28at.com

  • 减少不必要的内存拷贝
  • 减少程序上下文的切换开销,提高缓存命中率
  • 使用SIMD指令来加速处理关键性的热点逻辑

除此之外,我们还在开展 PGO 和 clang thinLTO 的验证工作,借助编译器的能力来进一步优化Agent性能。gt828资讯网——每日最新资讯28at.com

加入我们

本文作者赵杰裔,来自字节跳动 基础架构-云原生-可观测团队,我们提供日均数十PB级可观测性数据采集、存储和查询分析的引擎底座,致力于为业务、业务中台、基础架构建设完整统一的可观测性技术支撑能力。同时,我们也将逐步开展在火山引擎上构建可观测性的云产品,较大程度地输出多年技术沉淀。 如果你也想一起攻克技术难题,迎接更大的技术挑战,欢迎投递简历到 zhaojieyi@bytedance.comgt828资讯网——每日最新资讯28at.com

最 Nice 的工作氛围和成长机会,福利与机遇多多,在上海、杭州和北京均有职位,欢迎加入字节跳动可观测团队 !gt828资讯网——每日最新资讯28at.com

参考引用

  1. v2_0_cpp_unpacker:https://github.com/msgpack/msgpack-c/wiki/v2_0_cpp_unpacker#memory-management
  2. messagepack-specification:https://github.com/msgpack/msgpack/blob/master/spec.md
  3. Cloudflare fork of zlib with massive performance improvements:https://github.com/RJVB/zlib-cloudflare
  4. Intel® Intrinsics Guide:https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html
  5. Profile-guided optimization:https://en.wikipedia.org/wiki/Profile-guided_optimization
  6. ThinLTO:https://clang.llvm.org/docs/ThinLTO.html

本文链接:http://www.28at.com/showinfo-26-57279-0.html字节跳动百万级Metrics Agent性能优化的探索与实践

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 西瓜视频RenderThread引起的闪退问题攻坚历程

下一篇: 可能是最全的WinDbg命令和调试过程

标签:
  • 热门焦点
  • 天猫精灵Sound Pro体验:智能音箱没有音质?来听听我的

    天猫精灵Sound Pro体验:智能音箱没有音质?来听听我的

    这几年除了手机作为智能生活终端最主要的核心之外,第二个可以成为中心点的产品是什么?——是智能音箱。 手机在执行命令的时候有两种操作方式,手和智能语音助手,而智能音箱只
  • 5月iOS设备性能榜:M1 M2依旧是榜单前五

    5月iOS设备性能榜:M1 M2依旧是榜单前五

    和上个月一样,没有新品发布的iOS设备性能榜的上榜设备并没有什么更替,仅仅只有跑分变化而产生的排名变动,刚刚开始的苹果WWDC2023,推出的产品也依旧是新款Mac Pro、新款Mac Stu
  • 三言两语说透设计模式的艺术-单例模式

    三言两语说透设计模式的艺术-单例模式

    写在前面单例模式是一种常用的软件设计模式,它所创建的对象只有一个实例,且该实例易于被外界访问。单例对象由于只有一个实例,所以它可以方便地被系统中的其他对象共享,从而减少
  • .NET 程序的 GDI 句柄泄露的再反思

    .NET 程序的 GDI 句柄泄露的再反思

    一、背景1. 讲故事上个月我写过一篇 如何洞察 C# 程序的 GDI 句柄泄露 文章,当时用的是 GDIView + WinDbg 把问题搞定,前者用来定位泄露资源,后者用来定位泄露代码,后面有朋友反
  • JVM优化:实战OutOfMemoryError异常

    JVM优化:实战OutOfMemoryError异常

    一、Java堆溢出堆内存中主要存放对象、数组等,只要不断地创建这些对象,并且保证 GC Roots 到对象之间有可达路径来避免垃 圾收集回收机制清除这些对象,当这些对象所占空间超过
  • 雅柏威士忌多款单品价格大跌,泥煤顶流也不香了?

    雅柏威士忌多款单品价格大跌,泥煤顶流也不香了?

    来源 | 烈酒商业观察编 | 肖海林今年以来,威士忌市场开始出现了降温迹象,越来越多不断暴涨的网红威士忌也开始悄然回归市场理性。近日,LVMH集团旗下苏格兰威士忌品牌雅柏(Ardbeg
  • 2023年,我眼中的字节跳动

    2023年,我眼中的字节跳动

    此时此刻(2023年7月),字节跳动从未上市,也从未公布过任何官方的上市计划;但是这并不妨碍它成为中国最受关注的互联网公司之一。从2016-17年的抖音强势崛起,到2018年的&ldquo;头腾
  • 大厂卷向扁平化

    大厂卷向扁平化

    来源:新熵作者丨南枝 编辑丨月见大厂职级不香了。俗话说,兵无常势,水无常形,互联网企业调整职级体系并不稀奇。7月13日,淘宝天猫集团启动了近年来最大的人力制度改革,目前已形成一
  • 3699元!iQOO Neo8 Pro顶配版今日首销:1TB UFS 4.0同价位唯一

    3699元!iQOO Neo8 Pro顶配版今日首销:1TB UFS 4.0同价位唯一

    5月23日,iQOO推出了全新的iQOO Neo8系列,包含iQOO Neo8和iQOO Neo8 Pro两个版本,其中标准版搭载高通骁龙8+,而Pro版更是首发搭载了联发科天玑9200+旗舰
Top