也说load


要说运维面试最俗的问题,load的定义排第二的话,没有其他话题敢排第一了吧,load定义这个在我司运维面试中,基本是一个必考题目,可惜有很多面官自己也不是很清楚load计算中的各种细节,其中当然也包括我在内。


今天,我们更近一步,深入来看下load,究竟是怎么算出来,希望能探索到这个问题的根部,当然我们认为大家探讨的共同点是基于load值的来源是怎么一回事,基于哪些值计算出来的,简要的说,load值是基于目前可运行进程数与不可中断睡眠状态的进程数量做统计的,是一个衡量系统负载的值。


那么我们抛一个问题,如果初始时刻load为0,突然可运行状态进程计数变为16,并且长期持续下去,那么在1分钟的时候,load一分钟计数值是多少呢?


我相信可能大部分人都会回答是16吧,起初我也是这么认为的,业界有各种资料解释,load1分钟的值是1分钟内的统计值平均数,5分钟值是5分钟内的统计值平均数……但是,我不知道有人压测过没有,实际上在上面这个场景下,1分钟后的load1分钟值,大概在10左右。


是不是有点惊讶?天呐,为什么是这个样子?下来我们从源头的地方看下,为什么是这么一个情况。


首先放两个不错的还可以参考一下的链接,一个brendan gregg大神的介绍,当然这个侧重点也不在计算上,不过里面有一个图很不错,可能更能体现我上面描述的场景:http://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html


另一个是阿里大佬的一篇文章:https://yq.aliyun.com/articles/484253?utm_content=m_42447


关于我在寻找load含义的过程中,也仅有这两篇文章能给一些不错的启发,说实在的,其余市面搜索到的文章,99%都是错的,当然错误的根源实际上来自某大佬2011年的一篇翻译自国外的文章,这也让我认识到,试图阐释明白这个问题,其实是有点难度的,因此我尽量去解释明白这个问题,希望可以抛砖引玉吧


计算load的方法,可以参考这个链接的这段代码:

/*
 * a1 = a0 * e + a * (1 - e)
 *
 * a2 = a1 * e + a * (1 - e)
 *    = (a0 * e + a * (1 - e)) * e + a * (1 - e)
 *    = a0 * e^2 + a * (1 - e) * (1 + e)
 *
 * a3 = a2 * e + a * (1 - e)
 *    = (a0 * e^2 + a * (1 - e) * (1 + e)) * e + a * (1 - e)
 *    = a0 * e^3 + a * (1 - e) * (1 + e + e^2)
 *
 *  ...
 *
 * an = a0 * e^n + a * (1 - e) * (1 + e + ... + e^n-1) [1]
 *    = a0 * e^n + a * (1 - e) * (1 - e^n)/(1 - e)
 *    = a0 * e^n + a * (1 - e^n)
 *
 * [1] application of the geometric series:
 *
 *              n         1 - x^(n+1)
 *     S_n := \Sum x^i = -------------
 *             i=0          1 - x
 */

 


再参照下wiki:移动平均


关键的地方:a1 = a0 * e + a * (1 – e),可以看出,每一个当前的值a1,是前一个值a0乘以一个常量e加当前的可运行进程数乘以(1-e),e是一个介于0-1之间的数,e的取值,实际上是这个值:e-5/60R

这里很明显了,如果e越大,那么统计结果越接近上一个时刻的值,e越小,那么统计结果越接近当前的可运行进程数。事实上,load1,load5,load15,三种值的区别只是在于e的不同,所以导致三条曲线的增长趋势不同。

这里我们描述下计算load1,load5,load15的计算方式:

首先是三个常量的定义:

EXP_1 = e^(-5/(60*1)) //约等于0.9200
EXP_5 = e^(-5/(60*5)) //约等于0.9835
EXP_15 = e^(-5/(60*15)) //约等于0.9945

接下来是三个公式:

load1_1 = load1_0 * EXP_1 + c * (1 - EXP_1)
load5_1 = load5_0 * EXP_5 + c * (1 - EXP_5)
load15_1 = load15_0 * EXP_15 + c * (1 - EXP_15)

其中,load1_1表示load1的当前时刻值,load1_0表示load1的上一个时刻值(5s前),c表示当前可运行进程数。

三个公式可以泛化表示为:

loadx_1 = loadx_0 * EXP_x + c * (1 - EXP_x)

接下来来一个关键的推理过程,假设可运行进程数c不变,经过n个周期之后,新的load跟原来的load是什么关系呢?

f(z) = z * EXP_x + c * (1 - EXP_x) //第一次计算load,初始值为z
f(f(z)) = (z * EXP_x + c * (1 - EXP_x)) * EXP_x + c * (1 - EXP_x)  //第二次计算load
        = z * EXP_x ^ 2 + (1 - EXP_x ^ 2) * c
f(f(f(z))) = (z * EXP_x ^ 2 + (1 - EXP_x ^ 2) * n) * EXP_x + c * (1 - EXP_x)  //第三次计算load
           = z * EXP_x ^ 3 + (1 - EXP_x ^ 3) * c
......
f^n(z) = (z * EXP_x ^ n + (1 - EXP_x ^ n) * c) * EXP_x + c * (1 - EXP_x)  //迭代函数表示
       = z * EXP_x ^ n + (1 - EXP_x ^ n) * c
//反推求c的公式:
c = (f^n(z) - z * EXP_x ^ n) / (1 - EXP_x ^ n)

接下来我们验证下,现在我们有如下一个load序列(我们的监控粒度是10s一次,linux load取值是5s一次):

load1:    26.55    53.22    75.79
load5:    5.67     12.07    18.26
load15:   1.88     4.01     6.11

通过反推负载的公式计算出来,可运行进程数序列为:

200.27, 200.23
200.88, 200.88
194.64, 194.06

现实中我们确实是用200个sysbench线程去压测的,可以看出反推的公式也是非常靠谱的。

掌握了这个计算方式,就可以计算load跟可运行进程数了,但是,我们做了这么,其实不如直接监控可运行进程数更好一点呢,这个值也在/proc/loadavg里面哦,第四列的前半部分。

发表在 好玩的linux | 留下评论

丢包延时对带宽的影响


前几天线上发现一个丢包的场景,丢包率大概在0.3%左右,有同学认为这个丢包对网络没有影响,但是实际的测试结果,对带宽的影响已经很大了,所以这边测试了一下不同的丢包率跟延时对带宽的影响,测试的脚本如下:

#!/bin/env python
import sys
import math
import commands

loss = 0.01

for i in range(10):
    delay = 0.1
    for j in range(10):
        tc_add_command = "tc qdisc add dev bond0 root netem delay " + str(delay) + "ms loss " + str(loss)
        tc_del_command = "tc qdisc del dev bond0 root netem delay " + str(delay) + "ms loss " + str(loss)
        iperf_command = "iperf -c 10.0.3.47"
        status, tcout = commands.getstatusoutput(tc_add_command)
        status, iperfout = commands.getstatusoutput(iperf_command)
        status, tcout = commands.getstatusoutput(tc_del_command)
        iperf_last_line = iperfout.split("\n")[-1].split()
        bw = float(iperf_last_line[-2])
        if iperf_last_line[-1] == "Gbits/sec":
            bw = bw * 1000
        sys.stdout.write(str(loss) + ":" + str(delay) + ":" + str(bw) + "  ")
        sys.stdout.flush()
        delay = delay * 2
    print ""
    loss = loss * 2


注意其中需要一台服务器一个iperf server端,然后把脚本中的iperf命令的连接ip替换掉


测试使用了两台10G网卡的机器,网络带宽的上限为10G=10000Mbps,tcp拥塞算法是cubic


测试的结果数据如下:(行为延迟,单位ms,列为丢包率)

0.1 0.2 0.4 0.8 1.6 3.2 6.4 12.8 25.6 51.2
0.01 9000 8900 8560 7100 4880 2970 2310 1160 867 612
0.02 9080 8320 8330 6100 3170 1900 1010 1300 979 535
0.04 9010 8190 6170 4240 2810 1580 1070 849 485 507
0.08 8030 6990 4490 3220 1840 1130 462 546 361 135
0.16 4270 4150 2980 2000 1170 692 431 201 147 235
0.32 2820 2210 1820 1190 438 337 152 43.9 83 6.01
0.64 972 829 809 556 322 183 117 56.4 78.4 5.12
1.28 462 435 277 237 178 106 37.1 12 7.27 2.61
2.56 94.1 61.2 64.4 83.1 68.2 40.5 17.8 7.53 4.19 13.2
5.12 30.8 19.2 14.7 13.4 15.6 19.1 12.1 4.38 2.64 1.45


我们可以看下据此数据绘出的图表:


可以看出在最低延时跟最低丢包率的场景下,基本带宽是可以跑满的,也就是最上面的一个小平台,而后面随着丢包或延时的增加,带宽迅速下降,当在长途链路上的时候,情况显然会更加恶化,所以,丢包其实是一个很可怕的事情


之前的文章也介绍过,带宽其实跟窗口跟延时有关系,带宽=窗口/延时,压测中也发现,窗口跟丢包率有一定的关系,一定丢包率会让窗口稳定在一个值上,但是其中的关系,目前还不是很明白,留待下一篇文章解释吧

发表在 好玩的linux, 网络 | 留下评论

fuse实现webfs

fuse是一个用户空间级别的文件系统接口,大家都知道文件系统通常在内核中实现,但是因为一些原因,有一些文件系统不方便在在内核中实现,比如版权的原因,或者开发者不想写C,于是fuse诞生了,总之fuse就是一个让你在用户空间实现文件系统的接口,虽然是在用户空间实现的,但是文件系统该有的接口,fuse也基本不拉,不过,看用户的需求,如果你只要实现一个很简单的功能,那么,也许一个只需要实现4-5个就可以满足你的需求,接下来我们来实现一个基于web的文件系统。
在linux下,我们跨机器传输文件经常会用到如下这个命令:

python -m SimpleHTTPServer

这个命令会在本地起一个端口,然后从另一个机器的访问这个http端口,就可以访问文件并且下载,相信很多同学对这个不陌生。
那么,有没有办法把这种类似的接口挂载到本地,像访问文件夹一样cp呢?
那我们用fuse就可以实现这个功能,基本的我们实现如下几个接口就可以:
getattr:获得文件属性,是普通文件还是目录,文件权限,在我们常见下文件都为444,文件夹都是555,因为要保证进入文件夹,需要x权限
read:读取文件内容的接口,需要支持offset
readdir:读取文件夹下的子文件,返回是一个列表
具体的代码如下:

    def getattr(self, path, fh=None):
        wh = self.getwebheader(path)
        if "Content-Length" in wh:
            st_size = int(wh["Content-Length"])
        else:
            st_size = 0
        tplist = path.split("/")
        ppath = "/".join([i for i in tplist if i != ""][0:-1]) + "/"
        w = self.getweb(ppath)
        if re.search('a href="' + path.lstrip("/") + '/"', w) or path == "/":
            st_mode = 0o40555
        else:
            st_mode = 0o100444
        attr_dict = {
            "st_mode": st_mode,
            "st_size": st_size,
                 }
        return attr_dict

    getxattr = None
    listxattr = None

    def open(self, path, flags):
        self.fd += 1
        return self.fd

    def read(self, path, size, offset, fh):
        w = self.getweb(path, size, offset)
        return w

    def readdir(self, path, fh):
        nulldir = ['.', '..']
        w = self.getweb(path)
        allfiles = re.findall('a href="(.*)"', w)
        dirs = [fs[0:-1] for fs in allfiles if fs.endswith("/")]
        files = [fs for fs in allfiles if not fs.endswith("/")]
        rsdir = files + dirs + nulldir
        return rsdir

    def getweb(self, path, size=None, offset=None):
        if size != None:
            headers = {"Range": "bytes=%d-%d" % (offset, offset + size - 1)}
        else:
            headers = {}
        r = requests.get(self.url + path, headers=headers)
        return r.content

    def getwebheader(self, path):
        r = requests.head(self.url + path)
        return r.headers

这个文件完整版本在这里:https://github.com/lishuai860113/fusepy/blob/master/examples/webfs.py
这样我们在远端起一个SimpleHTTPServer接口,然后本地这样挂载一下:

python webfs.py http://10.20.40.170:8000 /mnt/tmp

就可以在/mnt/tmp下读取到你web上的内容了。
那这样有一点美中不足的地方,在我们cp大文件的时候,会报错,这里是因为SimpleHTTPServer不支持下载的时候指定range,我们read的时候,一般都是按照一个block 4096去读取的,所以一定要web服务器支持range才可以,那我们也准备了一个支持range的server :https://github.com/lishuai860113/fusepy/blob/master/examples/RangeHTTPServer.py
用这个就没有问题了,另外go也写了一个简单的接口:
https://github.com/lishuai860113/UploadFileServer
go这个库原生是支持range的,所以没有问题的。
这样,就可以做出自己想要的文件系统了,祝玩的开心!

发表在 python, 好玩的linux | 留下评论

centos6.6 pidns_get空指针问题

某天数据库的同学抱怨服务器不断重启,登录发现均有crash的记录,查看之后发现是最近某支撑部门部署的agent导致,而且集中在centos6.6的系统中,crash中dmesg中有如下信息:

PID: 4361   TASK: ffff881024318aa0  CPU: 23  COMMAND: "java"
……
    [exception RIP: kref_get+12]
    RIP: ffffffff8128f42c  RSP: ffff881022f61e38  RFLAGS: 00010292
    RAX: 0000000000000000  RBX: 0000000000000000  RCX: 00000000fffffff3
    RDX: 0000000000000000  RSI: 0000000000000000  RDI: 0000000000000000
    RBP: ffff881022f61e48   R8: 0000000000000000   R9: 0000000000000000
    R10: 0000000000000000  R11: 0000000000000001  R12: ffffffff8161b040
    R13: 0000000000001001  R14: 00007f1bf4dfd2a0  R15: 0000000000000000
    ORIG_RAX: ffffffffffffffff  CS: 0010  SS: 0018
#9 [ffff881022f61e50] pidns_get at ffffffff810d69f6
#10 [ffff881022f61e70] proc_ns_readlink at ffffffff81203558

这里截取了关键的部分,发现是在pidns_get中调用了kref_get,但是kref_get获取到的参数,是空的指针,所以导致出错,我们查看了pidns_get的定义:

static void *pidns_get(struct task_struct *task)
{
        struct pid_namespace *ns;

        rcu_read_lock();
        ns = get_pid_ns(task_active_pid_ns(task));
        rcu_read_unlock();

        return ns;
}

这里因为task_active_pid_ns(task)可能会返回NULL,但是get_pid_ns是这么定义的:

static inline struct pid_namespace *get_pid_ns(struct pid_namespace *ns)
{
        if (ns != &init_pid_ns)
                kref_get(&ns->kref);
        return ns;
}

这里kref_get 去拿&ns->kref的时候,因为ns是NULL,所以会发生空指针访问异常。
正常是需要判断值为NULL的逻辑的,但是这里并没有,我们对比观察下没有问题的centos6.8系统上的代码:

static void *pidns_get(struct task_struct *task)
{
        struct pid_namespace *ns;

        rcu_read_lock();
        ns = task_active_pid_ns(task);
        if (ns)
                get_pid_ns(ns);
        rcu_read_unlock();

        return ns;
}

发现这里判断了当ns不为NULL的时候,才会去执行get_pid_ns(),这样自然不会有问题。

发表在 好玩的linux | 留下评论

linux glob_filename 解析超长字符串致夯死问题

0.背景

大清早的同事过来找,说有个机器无法登录之后无法通过sudo su -切换到root,上去看了一下,发现确实如此,于是开始愉(dan)快(teng)的排障之旅。

1.思路

确定应该不是shell本身的原因导致,因为普通权限的用户可以登录,并且没有问题,但是切换到root的时候会卡住,于是怀疑bash的rc文件或者profile文件导致问题,重点排查这个环节

2.步骤

2.0

首先,现在切不到root,很多不方便的地方,虽然root bash不可以用,但是还有其他很多shell哦,没有装?装一个,sudo yum install csh,然后sudo csh,可以获得root权限,后续就方便很多了

2.1

开启另一个终端做调试,执行sudo su – ,夯住:

htop查看下负载,单核跑满:

确认是bash进程跑满单核,并且内存占用会逐步上升

2.2

查看了bashrc跟profile.d下面所有文件,并未发现异常于其他机器的,开始怀疑特殊的场景,诱发了bash,或者某些rc文件的bug,尝试用perf top -p pid 分析下,查看下函数cpu占用情况:

2.3

搜索资料发现,上面三个占用cpu最高的函数,是处理字符串的函数,怀疑某种特殊字符的字符串导致bash僵死,后来证明这个猜测是错误的,不过还好方向没走太偏。

要找到bash内部的问题,目前看来只能尝试gdb去跟踪了,先sudo gdb bash跟踪了一次,发现输出都是汇编,还是很头疼的,为了将汇编跟源代码对应起来,需要安装debuginfo的包,安装的debuginfo包方法在这里:

https://www.jianshu.com/p/5b4ef8112b97

在这个场景,我们需要安装的是bash,glibc的debuginfo包。

装好之后,gdb走起

2.4

sudo gdb bash 执行之后,run一下,hang住之后,等一段时间,大约10s,ctrl c掉

然后执行bt查看下栈信息,装了debuginfo终于可以看到源码跟参数信息了,泪目。上下翻下栈的详细信息

可以看到,栈深度已经到达49,正常不会有这么多的,事后复现发现,栈深度也会持续增加,从第1个栈帧到34个,基本都是雷同的内容,猜测有递归调用,而其中的内容,应该是应用层的内容,开始排查profile.d 下面的各个文件发现有一行:

这段逻辑是要把history中上一条执行记录提取出来,发送到日志中,但是在echo $y的时候,命令行中的参数会被解释!

2.5

把这个文件移走之后,发现可以正常切换了,断定问题根源在这个文件

3.复现

3.0

原理,linux在shell中遇到形如 /dir/* 的参数的时候,会解析展开其中的内容如:/dir/f1 /dir/f2 /dir/f3 ……

如果某个目录下文件太多,那会导致这个解析过程处理的字符串过大,由于解析逻辑中有递归的过程,也导致调用不断增加,内存不断增加,最后导致夯住,持续下去,进程也会耗光内存,导致OOM

3.1

如果希望复现,我们可以构建一个较长的目录名称,然后在这个目录下创建大量文件:

mkdir /tmp/123456789012345678901234567890123456789012345678901234567890
cd /tmp/123456789012345678901234567890123456789012345678901234567890
for i in `seq 1 10000`
do
touch $i
done

这样,我们在执行如下命令的时候:

echo /tmp/123456789012345678901234567890123456789012345678901234567890/* >/dev/null

参数就会被解析,最终会展开一个几十万byte的字符串

以上这个场景,bash会处理十几分钟,最后会报错太长参数退出。

大家可以尝试构建更多的文件,夯更久一些,直到OOM,玩的开心

4.总结

总体来说,排障的思路还是没有走入太多的歪路,gdb还是神器,后续可能还要继续了解使用下,后续类似这种问题,其实简单来说可以把profile.d跟bashrc移走先,基本可以快速定位问题文件。其中也使用strace以及bash -x 也可以发现一些端倪,如果思路清晰对位,最终一定能找到真正的问题所在。

发表在 debug, 好玩的linux | 留下评论