最近在自己的vps上搭了一个vpn,但是在windows上使用自带的vpn客户端连接后,vpn服务器经常会跑到cpu 100%(服务器的配置是一核)。按照常理说偶尔的cpu飙高是可以理解的,但是持续性的飙到100%就有点莫名其妙了。
处于好奇,我想确定下究竟是什么地方导致了这种现象的产生以及这种现象是不是正常的。下面的内容叙述了探究这个问题的详细过程,
使用下面的命令可以看到vpn进程对应的所有线程占用cpu资源情况,
top -H -p 5060
从上图中可以看到线程4677占用了绝大多数的cpu资源。但是目前还不能确定这个线程做了什么,目前常用的用于黑盒跟踪线程(进程)执行情况的命令有strace, pstack, ltrace等。
我们试一试pstack看一下数据,
pstack 5072
可见占用高的线程运行在lipthread的recvfrom函数上,但是由于pstack只能打印出瞬时间的调用栈,并没有长时间的跟踪,所以不能说明就是这个函数导致的cpu占用高。
使用strace可以统计进程一段时间的调用信息,
strace -c -p 5072
运行上面命令一段时间后停止这个命令可以看到如下的输出结果,
可见80%的时间都用于了调用recvfrom方法,可见的确是recvfrom的调用导致的。有了pstack的输出。
但是里面具体的调用位置目前还不是很清楚,我们需要进一步的去追踪。我们知道使用gdb可以对程序进行调试,所以下面轮到gdb出场了,
gdb -p pid 5072
使用bt命令查看当前的调用栈,奇怪的是只有很简短的调用栈,没有任何有用的信息,同时info locals和disas这些命令也都不能用。看gdb启动的时候的输出,
/opt/vpnserver: No such file or directory
去opt目录下的确没有这个文件,这时候突然意识到vpn是通过docker镜像启动的,镜像里面的程序所在的目录由docker统一管理和宿主机的目录相隔离。那么我们进到镜像内部看看,
docker exec -ti vpn /bin/bash
由于镜像的pid和宿主机的pid是隔离的,也就是说我们上面查到的pid在宿主机内部是不存在的,所以我们需要使用top重新定位,
top
奇怪的是出现了如下的信息,而不是我们想要的进程信息。
TERM environment variable not set.
在网上搜索了一下这个错误找到了对应的解决方案,
/usr/bin/top -bcn 1
下面需要定位到具体的线程,
/usr/bin/top -bcn 1 -H -p 116
再次使用gdb进行调试,
gdb -p 128
可以看到gdb成功的读取到了进程的数据,使用bt命令可以看到当前的调用栈,
bt
,但这个调用栈并不是我们想要的,我们需要定位到NatThreadMain这个函数上。使用disas命令可以看到对应函数的汇编指令,
disas RecvFrom
(gdb) disas RecvFrom Dump of assembler code for function RecvFrom: 0x000000000048f220 <+0>: push %r13 0x000000000048f222 <+2>: push %r12 0x000000000048f224 <+4>: push %rbp 0x000000000048f225 <+5>: push %rbx 0x000000000048f226 <+6>: mov %rdi,%rbx 0x000000000048f229 <+9>: sub $0x28,%rsp 0x000000000048f22d <+13>: test %rdi,%rdi 0x000000000048f230 <+16>: je 0x48f25d <RecvFrom+61> 0x000000000048f232 <+18>: test %rsi,%rsi 0x000000000048f235 <+21>: mov %rsi,%rbp 0x000000000048f238 <+24>: movl $0x0,0x1e8(%rdi) 。。。。。。省略 0x000000000048f290 <+112>: lea 0xc(%rsp),%r9 0x000000000048f295 <+117>: lea 0x10(%rsp),%r8 0x000000000048f29a <+122>: xor %ecx,%ecx 0x000000000048f29c <+124>: movl $0x10,0xc(%rsp) 0x000000000048f2a4 <+132>: callq 0x405e80 <recvfrom@plt> ##### 0x000000000048f2a9 <+137>: test %eax,%eax 0x000000000048f2ab <+139>: mov %rax,%r13 。。。。。。省略 ---Typeto continue, or q to quit--- 0x000000000048f350 <+304>: cmp $0x4,%edx 0x000000000048f353 <+307>: je 0x48f365 <RecvFrom+325> 0x000000000048f355 <+309>: cmp $0xb,%eax 0x000000000048f358 <+312>: sete %al 0x000000000048f35b <+315>: movzbl %al,%eax 0x000000000048f35e <+318>: neg %eax 0x000000000048f360 <+320>: jmpq 0x48f25f <RecvFrom+63> 0x000000000048f365 <+325>: movl $0x1,0x1e8(%rbx) 0x000000000048f36f <+335>: jmpq 0x48f25d <RecvFrom+61> End of assembler dump. </pre> 在带有#####的地方加上断点,  然后继续运行, 断点停在了Breakpoint 1上,  使用bt查看当前调用栈,  和我们之前用pstack看到的类似。使用info locals无法找到对应的临时变量,没有办法看到当前栈的具体状况。同时栈只显示了对应的函数,却没有显示对应的文件。 我们需要重新编译代码,将调试信息写入到执行文件中去。找到vpn对应的项目,https://github.com/siomiz/SoftEtherVPN 找到这个文件,https://github.com/siomiz/SoftEtherVPN/blob/master/copyables/build.sh 将其修改为: #!/bin/bash echo "clean_requirements_on_remove=1" >> /etc/yum.conf yum -y update \ && yum -y install unzip \ && yum -y groupinstall "Development Tools" \ && yum -y install readline-devel ncurses-devel openssl-devel iptables \ && yum -y install gdb \ && yum -y install strace \ && yum -y install net-tools # git clone --depth 1 https://github.com/SoftEtherVPN/SoftEtherVPN.git /usr/local/src/vpnserver cd /usr/local/src/vpnserver cp src/makefiles/linux_64bit.mak Makefile patch -p1 < ../AES-256-CBC.patch # 这里强制开启-g,具体实现可以参考https://github.com/SoftEtherVPN/SoftEtherVPN.git中的linux_64bit.mak make DEBUG=YES cp bin/vpnserver/vpnserver /opt/vpnserver cp bin/vpnserver/hamcore.se2 /opt/hamcore.se2 cp bin/vpncmd/vpncmd /opt/vpncmd # 不删除 # rm -rf /usr/local/src/vpnserver gcc -o /usr/local/sbin/run /usr/local/src/run.c # 不删除 # rm /usr/local/src/run.c yum -y remove readline-devel ncurses-devel openssl-devel \ && yum -y groupremove "Development Tools" \ && yum clean all rm -rf /var/log/* /var/cache/yum/* /var/lib/yum/* exit 0将Dockerfile文件修改为,FROM centos:centos7 MAINTAINER Tomohisa Kusano <siomiz@gmail.com> # 下载https://github.com/SoftEtherVPN/SoftEtherVPN.git, 并且拷贝到镜像的对应目录 COPY SoftEtherVPN /usr/local/src/vpnserver COPY copyables / RUN chmod +x /entrypoint.sh /gencert.sh RUN bash /build.sh \ && rm /build.sh WORKDIR /opt ENTRYPOINT ["/entrypoint.sh"] EXPOSE 500/udp 4500/udp 1701/udp 1194/udp 5555/tcp CMD ["/usr/local/sbin/run"]进入到对应目录,执行如下的命令, > docker build -t test:test . 可以得到一个带有调试信息的镜像。