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

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

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

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

背景

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

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

基本架构

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

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

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

数据接收

Case 1

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

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

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

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

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

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

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

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

因此在第二步,对 msgpack::object 进行转换的时候,我们不再转换为 string,而是使用 string_view,可以优化掉 string 的复制和内存分配等:Jrv28资讯网——每日最新资讯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 的内存地址明显不同。Jrv28资讯网——每日最新资讯28at.com

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

Case 2

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

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

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

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

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

Case 3

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

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

原版Jrv28资讯网——每日最新资讯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 Jrv28资讯网——每日最新资讯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;}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

数据处理

Case 1

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

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

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

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

这个函数作用是:算完 hash 后,在 hash 桶里找到匹配 key 的元素。这也意味着,即使命中了,hash 查找的时候也要进行一次 key 的比较操作。而在 Agent 里,这个 key 的比较操作定义为:Jrv28资讯网——每日最新资讯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 命中后都要进行这样一次操作是非常耗时的。可能导致时间开销增大的原因有:Jrv28资讯网——每日最新资讯28at.com

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

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

因此,我们将 TagSet 的数据使用 string_view 表示,并将所有的 data 全部存放在同一块内存中。在 dictionary encode 的时候,再把 TagSet 转换成 string 的格式返回出去。Jrv28资讯网——每日最新资讯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 的时候,新方法性能较好。Jrv28资讯网——每日最新资讯28at.com

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

数据发送

Case 1

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

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

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

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

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

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

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

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

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

结论

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

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

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

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

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

加入我们

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

最 Nice 的工作氛围和成长机会,福利与机遇多多,在上海、杭州和北京均有职位,欢迎加入字节跳动可观测团队 !Jrv28资讯网——每日最新资讯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命令和调试过程

标签:
  • 热门焦点
  • vivo TWS Air开箱体验:真轻 臻好听

    在vivo S15系列新机的发布会上,vivo的最新款真无线蓝牙耳机vivo TWS Air也一同发布,本次就这款耳机新品给大家带来一个简单的分享。外包装盒上,vivo TWS Air保持了vivo自家产
  • JavaScript 混淆及反混淆代码工具

    介绍在我们开始学习反混淆之前,我们首先要了解一下代码混淆。如果不了解代码是如何混淆的,我们可能无法成功对代码进行反混淆,尤其是使用自定义混淆器对其进行混淆时。什么是混
  • 线程通讯的三种方法!通俗易懂

    线程通信是指多个线程之间通过某种机制进行协调和交互,例如,线程等待和通知机制就是线程通讯的主要手段之一。 在 Java 中,线程等待和通知的实现手段有以下几种方式:Object 类下
  • 2023 年的 Node.js 生态系统

    随着技术的不断演进和创新,Node.js 在 2023 年达到了一个新的高度。Node.js 拥有一个庞大的生态系统,可以帮助开发人员更快地实现复杂的应用。本文就来看看 Node.js 最新的生
  • 分布式系统中的CAP理论,面试必问,你理解了嘛?

    对于刚刚接触分布式系统的小伙伴们来说,一提起分布式系统,就感觉高大上,深不可测。而且看了很多书和视频还是一脸懵逼。这篇文章主要使用大白话的方式,带你理解一下分布式系统
  • 中国家电海外掘金正当时|出海专题

    作者|吴南南编辑|胡展嘉运营|陈佳慧出品|零态LT(ID:LingTai_LT)2023年,出海市场战况空前,中国创业者在海外纷纷摩拳擦掌,以期能够把中国的商业模式、创业理念、战略打法输出海外,他们依
  • 腾讯VS网易,最卷游戏暑期档,谁能笑到最后?

    作者:无锈钵来源:财经无忌7月16日晚,上海1862时尚艺术中心。伴随着幻象的精准命中,硕大的荧幕之上,比分被定格在了14:12,被寄予厚望的EDG战队以绝对的优势战胜了BLG战队,拿下了总决
  • 消息称小米汽车开始筛选交付中心:需至少120个车位

    IT之家 7 月 7 日消息,日前,有微博简介为“汽车行业从业者、长三角一体化拥护者”的微博用户 @长三角行健者 发文表示,据经销商集团反馈,小米汽车目前
  • OPPO K11采用全方位护眼屏:三大护眼能力减轻视觉疲劳

    日前OPPO官方宣布,全新的OPPO K11将于7月25日正式发布,将主打旗舰影像,和同档位竞品相比,其最大的卖点就是将配备索尼IMX890主摄,堪称是2000档位影像表
Top