分享一个系统抖动探测工具 sysjitter,github 主页。
工具具体是怎么用的,本文不赘述了,感兴趣的自行参考项目主页 README。
通俗点说,好比你写了一个如下代码:
while True: print now.nsec()
在一个无尽循环里打印当前的纳秒数。 理想情况下,此函数的任意前后两次输出,它们的时间间隔应该是比较稳定且固定的,比如打印如下:
12345
但实际上这个任务在运行期间,会随时被中断打断,或者被调度器调度走,那么会导致前后两次间隔打印出来的时间出现跳变,也就是抖动(jitter),比如:
12458
抖动肯定是不好的,具体来说,如果程序是个 server
,会导致对网络包的处理发生 lag
,进而导致 client
端出现卡顿之类的现象。
sysjitter
工具就是用来定量测量系统到底有多抖。裸金属场景下,可以用来衡量系统中断处理效率、内核态是否存在长路径、调度延迟等;虚拟化场景下,还可以用来衡量虚拟化开销。
经过第二章的铺垫,原理部分就非常好理解了: 在每个 core 上起一个线程,线程里起一个 while 1,while 1 里面不停地读 TSC(因为
TSC 精度才够高),如果发现前后两次 TSC 的差值超过了一个阈值,那么就视为一次异常抖动。 程序最后做 per-core 的抖动分析报表呈现。
当然,这个程序前提是 core 上都是 idle 的,如果 core 本身就很 busy,那么调度延迟本身就可能会很大。
static void *thread_main(void *arg)
{
/* 确保绑核 */
TEST(move_to_core(t->core_i) == 0 );
thread_init(t);
/* 测量本cpu 的 hz,也就是 1 秒多少兆的 cycles
* measure_cpu_mhz 函数原理,就是测量 1 秒内 TSC 走了多少,
* 会多次测量直到误差最小。 */
t->cpu_mhz = measure_cpu_mhz();
/*这个线程最多跑 g.runtime_secs 秒,起一个 alarm */
alarm(g.runtime_secs);
/* frc() 函数在 x86 平台上的实现就是 rdtsc
* frc_start 和 frc_stop 记录了 doit()函数开始和结束的 TSC */
frc(&t->frc_start);
doit(t,(cycles_t)g.threshold_nsec * t->cpu_mhz / 1000 );
frc(&t->frc_stop);
return NULL ;
}
这段代码略显蛋疼,但其本质很简单:
在 while (g.cmd == GO) 中,不断地调用 frc(),获取前后两次 TSC 的差值,如果 TSC的差值(i->diff)超过了一个阈值 threshold_cycles,则视其为一次抖动。
t->interruptions:记录本次 while循环中所采样的所有抖动(别被其命名误导了,以为是中断,实际上就是抖动 ,或者理解为程序被打断 —— interrupt 了 )。
t->c_interruption:本线程抖动的采样数组的最后一个有效采样点。 g.max_interruptions:是本次运行中(运行时长
g.runtime_secs)可能发生的抖动的最大次数,这个是通过算法估算出来的,下一小节说。
static void doit(struct thread *t, cycles_t threshold_cycles)
{
struct interruption *i = t->interruptions;
struct interruption *i_end = t->interruptions + g.max_interruptions;
stamp_t prev_ts;
frc(&prev_ts);
while (g.cmd == GO)
{
frc(&i->ts);
i->diff = i->ts - prev_ts;
prev_ts = i->ts; /* 前后两次 TSC 差值超过 threshold_cycles 阈值,则记为一次抖动。 */
if (i->diff >= threshold_cycles)
{
/* 这里 int_total 记录下总的抖动时间。
* 在函数的最后,赋值给 t->int_total,该值后面用于定量衡量抖动。 */
int_total += i->diff;
++i;
if (i == i_end)
break;
}
}
t->c_interruption = i;
}
代码实现中,为每个线程开了一个数组(t->interruptions)用来记录此线程的所有抖动采样。那么这个数组要多大呢(也就是上一小节中
g.max_interruptions 的估算问题)? 代码在运行之前会做一次估算:
/* run_expt() 底层就是调用了上面的 thread_main(),进而调用到 doit() * 入参的 1,表示跑 1 秒,换句话说,就是先只跑 1 秒的 doit(),看看 1 秒 * 内 doit() 会采样到多少次抖动,那后面再跑 runtime 秒,不就大概知道会 * 采样到多少次抖动了么。 */run_expt(threads, 1);
/* 根据 run_expt(threads, 1) 的结果,来估算跑 runtime 秒情况下,大 * 概会有多少次抖动。 */calc_max_interruptions(threads, runtime);
static void calc_max_interruptions(struct thread *threads, int runtime)
{
/* 这个官方注释已经讲的很清楚此函数是用来干啥的了。 */
/* Calculate how big max_interruptions needs to be for real run of * [runtime] seconds. */
struct thread *t;
int i, max = 0, per_sec;
/* t->c_interruption 是一个线程最后一个有效抖动采样点,那么
* t->int_n = t->c_interruption - t->interruptions,就是该线程
* 一共采样到了多少次抖动。
* 取所有线程中最大的次数 max。 */
for (i = 0; i < g.n_threads; ++i)
{
t = &(threads[i]);
t->int_n = t->c_interruption - t->interruptions;
if (t->int_n > max)
max = t->int_n;
}
/* per_sec 就是预估情况下,每秒最多会有多少次抖动。
* 这里 * 2 是为了保险起见,多给一倍的空间。 */
per_sec = max / g.runtime_secs;
g.max_interruptions = per_sec * 2 * runtime;
}
thread_main() 函数中,通过 t->frc_start 和 t->frc_stop 记录下 doit() 函数的总耗时,也即:t
->runtime = t->frc_stop - t->frc_start。 doit() 函数中,统计了总的抖动时间 t->int_total。
工具将 t->int_total /
t->runtime,也就是“程序运行期间,抖动大于一定阈值(threshold_cycles)的总时间,与程序运行总耗时之间的比例”作为衡量抖动的量化指标。
工具同时还做了抖动时间的各分位统计等,比较简单,不展开。
上一节算出来的抖动量化指标与 steal 是啥关系? 二者看起来很像,但从源码来看,二者完全不是一回事。
只能说,工具算出来的抖动量化指标可以用来定性的衡量 steal,但二者并不是一个概念,不能直接划等号。
通过采样前后两次 TSC 间隔,与指定阈值对比,定量评估系统的抖动。