天天看點

開源C語言庫Melon:使用者态動态追蹤與控制回報

作者:碼哥比特

前言

本文根據開源C語言庫Melon的最新特性,講述使用該庫做使用者态動态追蹤,以及根據追蹤内容進行計算,并将結果用于回報給程式,同時對程式的處理流程進行影響。

說到動态追蹤,大家可能第一印象是bpf、dtrace、systemtap等等,但是本文介紹的動态追蹤不依賴于這些内容。Melon中提供的功能更加傾向于讓程式在使用者态内完成對自身的動态追蹤,而不依賴于核心态,也不依賴于uprobe和usdt等内容。

關于 Melon 庫,這是一個開源的 C 語言庫,它具有:開箱即用、無第三方依賴、安裝部署簡單、中英文文檔齊全等優勢。

Github: github.com/Water-Melon/Melon

原理

簡單來說,Melon庫的動态追蹤也是在程式中加入跟蹤點。隻是在Melon中,一個應用程式被劃分為兩個層面(但都運作在同一個程序中):

  • C代碼層
  • Melang腳本代碼層

兩個代碼層面可以運作在同一線程,也可以運作在不同線程中,但需要運作于同一程序下。

換言之:跟蹤點資訊會在C層被抛出,傳入給指定的腳本任務,然後腳本任務接收資訊并進行處理。

這麼看,似乎和在程式中記錄日志,然後額外寫一個程式來讀取日志進行處理沒有什麼差別,那麼這麼做的好處是什麼呢?

優勢:

  • 不需要對日志的格式進行解析,即可直接拿到對應類型的資料
  • 腳本側運算後可進行遠端傳輸或者入庫(腳本庫函數保證)
  • 即使在同一線程下,腳本任務執行也不會長期中斷C層邏輯,腳本和C邏輯是自動分時處理的
  • 利用腳本側的回報機制,可以将運算結果傳回C層,讓C代碼層的執行邏輯根據結果進行變更,例如:服務降級

示例

下面來看一個非常簡單的例子:

#include <stdio.h>
#include "mln_log.h"
#include "mln_core.h"
#include "mln_trace.h"
#include "mln_conf.h"
#include "mln_event.h"

int timeout = 100;

static void timeout_handler(mln_event_t *ev, void *data)
{
    mln_trace("sir", "Hello", getpid(), 3.1);
    mln_event_timer_set(ev, timeout, NULL, timeout_handler);
}

static int recv_handler(mln_lang_ctx_t *ctx, mln_lang_val_t *val)
{
    timeout += val->data.i;
    return 0;
}

int main(int argc, char *argv[])
{
    mln_event_t *ev;
    struct mln_core_attr cattr;

    cattr.argc = argc;
    cattr.argv = argv;
    cattr.global_init = NULL;
    cattr.main_thread = NULL;
    cattr.master_process = NULL;
    cattr.worker_process = NULL;

    if (mln_core_init(&cattr) < 0) {
       fprintf(stderr, "Melon init failed.\n");
       return -1;
    }

    if ((ev = mln_event_new()) == NULL) {
        mln_log(error, "event new error\n");
        return -1;
    }

    if (mln_trace_init(ev, mln_trace_path()) < 0) {
        mln_log(error, "trace init error\n");
        return -1;
    }
    mln_trace_recv_handler_set(recv_handler);

    mln_event_timer_set(ev, 1000, NULL, timeout_handler);

    mln_event_dispatch(ev);

    return 0;
}           

簡單來描述下程式流程:

  1. 對Melon庫進行全局初始化(mln_core_init)
  2. 初始化事件對象
  3. 初始化跟蹤腳本
  4. 設定用于處理腳本層發來資料的函數
  5. 設定逾時事件
  6. 事件分發,逾時事件會被觸發

在逾時處理函數timeout_handler中,我們利用mln_trace向腳本任務發送了三個不同類型的資料,然後繼續設定逾時事件。

逾時時長是一個全局變量timeout,初始為100,即100毫秒。

當腳本層發來資料時,這裡我們約定腳本層一定發來的是一個整數,那麼在接收函數recv_handler中,我們将這一數值與timeout進行累加,作為随後的逾時時長。

由此,可以猜測,程式中每秒向腳本層投遞的資料量會越來越少。

下面給出腳本層代碼:

sys = Import('sys');
if (MASTER)
    sys.print('master process');
else
    sys.print('worker process');

Pipe('subscribe');
while (1) {
    ret = Pipe('recv');
    if (ret) {
        for (i = 0; i < sys.size(ret); ++i) {
            sys.print(ret[i]);
        }
        Pipe('send', 100);
    } fi
    sys.msleep(1000);
}
Pipe('unsubscribe');           

簡單描述下腳本層邏輯就是,每秒鐘從C層接收一批資料,然後向終端輸出,且在輸出後,向C層發送一個整數100。

下面來看下程式運作結果:

...
[Hello, 72173, 3.100000, ]
[Hello, 72173, 3.100000, ]
[Hello, 72173, 3.100000, ]
...           

會看到很多上述輸出,但是讀者若自己運作則會發現,每秒鐘的輸出行數會越來越少,這與我們的程式邏輯是相符合的。

結語

從這一例子中,我們可以看到,我們既可以在腳本側做跟蹤統計,也可以向C層施加影響。而最關鍵的三點是:

  1. C層中不需要增加額外的統計變量和結構
  2. C層與腳本層在代碼管理層面上是分離的,互不幹擾
  3. 兩個層面的代碼運作在同一個線程下

為了簡化示範代碼,上面的例子中沒有給出在腳本層做網絡通信和資料入庫,但這些功能Melang腳本全部支援,感興趣的讀者可以參考Melang官網(melang.org)。

最後,對于Melon庫感興趣的讀者可以通路其Github倉庫(github.com/Water-Melon/Melon)擷取更多資訊。

感謝閱讀!