• 日志中打印统计信息的方案


    概要

    目前维护的一套c++服务,在打印日志时会存在锁竞争。先用brpc定位锁竞争的环节,然后提出集中改进方案,以及它们的优缺点。

    contention火焰图

    使用brpc在压测环境下打印contention火焰图。

    左边的圆圈是处理服务的接入数据,会记录接收量,os类型,业务方类型等统计参数;

    右边那两个稍小圆圈是两处发送下游,会记录发出量等统计参数;

    它们都是用同一把锁,当需要写入统计数据时,会去竞争这把锁;

    从连接方框的边旁的数据大小,可以看到主要的锁竞争来自于处理接收数据的业务函数。原因是我们的服务接收数据是逐条接收的,但是发送数据是批量,因此数量是不对等的。

    改进方案

    每个统计指标对应一把锁

    好处:锁的作用域得到最小力度的控制

    坏处:当有多处需要写入的统计信息量较大时,这种场景不合适

    自旋锁

    好处:不会发生cs

    坏处:当临界区较长时,对cpu的浪费不能忽视

    使用atomic

    好处:基础类型在x86架构上是无锁的

    坏处:atomic变量虽然比加锁赋值更轻量,但比普通参数复制还是更重一些。当统计参数较多时,这部分时间开销累计起来也是很大的

    对比atomic和加锁的时间开销

    atomic

    1. #include<iostream>
    2. #include<atomic>
    3. #include<thread>
    4. using namespace std;
    5. atomic<int> a(0);
    6. void sum() {
    7. for(int i=0; i!=10000000; i++) {
    8. a++;
    9. }
    10. }
    11. int main() {
    12. // cout << a.is_lock_free() << endl;
    13. // 启动四个线程
    14. thread t1(sum);
    15. thread t2(sum);
    16. thread t3(sum);
    17. thread t4(sum);
    18. t1.join();
    19. t2.join();
    20. t3.join();
    21. t4.join();
    22. int ret = a.load();
    23. cout << ret << endl;
    24. return 0;
    25. }

    mutex

    1. #include<iostream>
    2. #include<thread>
    3. #include<mutex>
    4. using namespace std;
    5. mutex m;
    6. int a = 0;
    7. int b = 0;
    8. void sum() {
    9. for(int i=0; i!=10000000; i++) {
    10. m.lock();
    11. a++;
    12. m.unlock();
    13. }
    14. }
    15. int main() {
    16. // 启动四个线程
    17. thread t1(sum);
    18. thread t2(sum);
    19. thread t3(sum);
    20. thread t4(sum);
    21. t1.join();
    22. t2.join();
    23. t3.join();
    24. t4.join();
    25. cout << a << endl;
    26. return 0;
    27. }

    运行

    1. didi@bogon cpptest % time ./atomic
    2. 40000000
    3. ./atomic 3.41s user 0.01s system 395% cpu 0.865 total
    4. didi@bogon cpptest % time ./mutex
    5. 40000000
    6. ./mutex 2.13s user 4.70s system 317% cpu 2.153 total

    可见使用atomic后,系统调用的开销显著降低,性能得以提升

    双版本切换

    该方案的思路是使用双版本保存统计信息。当需要记录统计参数时,先获取当前版本的index,可以获得往哪个版本写。新开一个线程,每隔一个时间间隔切换index,同时打印之前的统计参数

    代码

    1. #include<iostream>
    2. #include<atomic>
    3. #include<vector>
    4. #include<thread>
    5. #include<unistd.h>
    6. using namespace std;
    7. struct Info {
    8. int a{0};
    9. void clear() {a=0;}
    10. };
    11. atomic<int> Index(0); // 指向当前的版本
    12. vector<Info*> vec_infos; // 保存双版本的信息
    13. void print_info(int idx) { // 打印idx版本的统计参数
    14. cout << vec_infos.at(idx)->a << endl;
    15. }
    16. void clear_info(int idx) { // 将idx版本的统计参数清零
    17. vec_infos.at(idx)->a = 0;
    18. }
    19. void printInfo() {
    20. int i=0; // 保证函数能退出
    21. int idx=0;
    22. while(i!=10) {
    23. sleep(1);
    24. int next_idx = (idx+1) % 2;
    25. clear_info(next_idx); // 将即将使用的Info清零
    26. Index.store(next_idx); // 切换index,之后的写入的统计数据会放到另一个Info中
    27. print_info(idx); // 打印之前的统计数据
    28. idx = next_idx; // 指向新的index
    29. i++;
    30. }
    31. }
    32. void writeInfo() { // 写入统计参数
    33. while(true) {
    34. int idx = Index.load(); // 获取当前的index
    35. vec_infos.at(idx)->a++;
    36. }
    37. }
    38. int main() {
    39. Info* info1 = new Info();
    40. Info* info2 = new Info();
    41. vec_infos.push_back(info1);
    42. vec_infos.push_back(info2); // 双版本统计参数初始化
    43. thread t1(printInfo); // 启动打印线程
    44. thread t2(writeInfo); // 启动模拟写入统计参数的线程
    45. t1.join();
    46. return 0;
    47. }

    这份代码其实并不严密,写入统计参数时,可能已经完成打印了,导致数据丢失。首先这种概率并不大,atomic的store操作的时间开销是大于普通变量赋值的;其次是对于统计信息,即使丢少量数据,问题不大;再则我们可以将intervel的时间分成两份,在store操作和print_info之前等待一段时间,确保之前版本的数据都被写入。

  • 相关阅读:
    AsynchronousFileChannel写数据
    vscode使用svn
    MySQL事务和锁
    CAD新手必练图形三
    Redis下载和安装(Windows系统)
    用于制作耳机壳的倒模专用UV树脂有什么特点?
    2.7 基本放大电路的派生电路
    Web服务器(01)——介绍web服务器
    vue+cesium 点击矢量图层获取geoserver信息
    2-SAT
  • 原文地址:https://blog.csdn.net/zhanglehes/article/details/125434641