You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

24 KiB

21 | 为什么用了负载均衡更加不均衡?

你好,我是胜辉。

咱们课程的第二个实战模块“应用层真实案例揭秘篇”已经进行到后半程了。前半程的四讲15到18都是围绕应用层特别是HTTP的相关问题展开排查。而在刚过去的两讲19和20我们又把TLS的知识和排查技巧学习了一遍。

基本上无论是网络还是应用引发的问题也无论是不加密的HTTP还是加密的HTTPS你应该都已经掌握了一定的方法论和工具集可以搞定不少问题了。

但是我们也要看到,现实世界里也有不少问题是混合型的,未必一定是跟网络有关。比如,你有没有遇到过类似下面这种问题:

  • ping正常抓包看也没有丢包或者乱序现象但是应用就是缓慢
  • Telnet端口能通但应用层还是报错。

其实这也说明了,掌握网络排查技能固然重要,但完全脱离操作系统和架构体系方面的知识,仅根据网络知识去做排查,也有可能会面临知识不够用的窘境。所以,作为一个技术人,我们任何时候都不要限制自己的学习和成长的可能,掌握得越多,相当于手里的牌越多,我们就越可能搞定别人搞不定的问题。

所以接下来的两节课,我会集中围绕系统方面的案例展开分析,希望可以帮助你构造这方面的能力。等以后你遇到网络和系统扯不清的问题时,也不会发怵,而是可以准确定位,高效推进了。

案例1高负载和不均衡

这也是我在公有云工作的时候处理的真实案例。当时一个客户是做垂直电商的,他们的体量还不大,所以并没有自建机房,而是放到公有云上运行。他们的架构大致是这样的:

LB运行在第四层设置的负载均衡策略是round robin轮询也就是新到达LB的请求依次派发给后端3个Nginx服务器使得每台机器获得的请求数相同。Nginx运行在第七层它作为Web服务器接收HTTP请求然后通过FastCGI接口传递给本机的php-fpm进程,进行应用层面的处理。

应该说这就是一个非常典型的负载均衡架构平时运行也正常。不过有一天客户忽然报告系统出问题了网站访问越来越慢甚至经常会抛出HTTP 504错误。如下图所示

图片

我们借助浏览器开发者工具可以看到这个响应耗时长达60秒然后抛出了504错误。事实上一般人浏览一个网站的时候等个十来秒肯定就没耐心了所以这次的问题确实很严重。

初步检查

这三台Nginx服务器的配置都是8核CPU。从服务端的监控来看它们的系统负载也就是CPU load出现了严重的不均衡。其中一台的负载值在7左右一台在20左右最后一台居然高达40左右远远超过它们的CPU核的数量。

我们知道,uptime或者top命令输出里的CPU load值表示的是待运行的任务队列的长度。比如8核的机器load到8那就是8CPU核处理8个任务全部用满了。不过能到用满的程度实际已经对应用的性能产生明显的影响了所以一般建议 load值不超过核数*0.7。也就是8核的机器建议在load达到5.6的时候,就需要重点关注了。

回到这三台Nginx机器我们看看具体的load。

Nginx 1的load大致在10到20之间

图片

Nginx 2的load在7左右

图片

Nginx 3就很高了在40以上

图片

联系前面我们提到的负载均衡你大概也明白为什么客户找过来了因为3台机器的负载不均衡啊难道不是你们的LB工作不正常导致的吗

这确实是一个形式上讲得通的逻辑,我们开工吧。

排查LB

首先需要检查网络状况。我们做了抓包分析,发现传输本身一切正常,没有丢包也没有特殊的延迟。

然后我们需要确认LB工作是否正常。从直观上看处于LB后端的3台机器的load不均似乎也意味着LB分发请求时做得不均衡要不然为啥后端这些机器的负载各不相同呢

不过这个load不均的问题也可能只是表象因为会影响到load的因素是比较多的。我们回到最初既然要证明LB工作是否正常最直接的方式还是查证LB是否做到了round robin也就是分发的请求数量是否均等。于是我们去查LB上的日志看看这三台后端机器分得的请求数量。

以上就是LB上的统计红框圈出来的那一列是请求数3台Nginx对应这3行可见请求数都是1364这就说明LB的round robin机制运行正常。

接着跟之前课程里的很多案例类似我们做了一个简单的排除性测试我们绕过LB直接访问Nginx机器。结果发现

  • load为7的那台机器还勉强可以在10秒左右回复响应
  • 另外两台机器也就是load为20和40的机器的响应要慢很多超过了60秒。

补充如果只使用load为7的那台机器而禁用另外两台行不行呢当然是不行的一台肯定撑不住这个流量必须要三台一起服务。所以还是要把根因给找到并解决。

我们再来对比一下“通过LB”和“绕过LB”这两种场景下的问题现象

  • 通过LB耗时60秒的时候收到HTTP 504。
  • 绕过LB虽然收到了HTTP 200但是耗时长达70多秒。

其实这两个场景的根本问题是一致的都是后端服务器特别慢。这就再一次排除了LB本身的嫌疑。于是问题就变成了“为什么机器的负载变高了?

排查主机

那么到了系统排查这一步我们就没办法再用tcpdump结合Wireshark去排查了因为问题跟网络报文没有关系。具体点说我们要做下面这些事情

  • 分析所有进程找到具体是哪个进程引起了load升高。
  • 分析进程细节找到是什么Bug导致该进程变成了问题进程。

针对第一个问题,最常用的工具就是 top命令。通过top我们很快就找到了消耗CPU资源最多的进程发现是php-fpm进程。客户的电商程序框架本身是基于PHP开发的所以需要PHP解释器来运行程序。又因为Nginx本身不能处理PHP所以需要结合php-fpm才能正常工作。也就是下图这样

既然确定了问题进程接下来就是要排查进程导致load高的原因。排查这个问题大致也有两个思路。

  • 白盒检查:检查代码本身,找到根因。
  • 黑盒检查:不管代码怎么回事,我们从程序的外部表现来分析,寻求根因。

因为核心代码是客户的国外合作方维护的,客户自己并不十分清楚这里面所有的逻辑,所以白盒这条路,有点超出了他们的能力。那么自然的,我们要走第二条路。

排查操作系统

跟网络排查中有tcpdump这样强大的工具类似进程的排查也有相关的强大工具比如 strace。通过strace我们可以把排查工作从进程级别继续追查到更细的syscall系统调用级别。无论是系统调用读写文件时的问题还是系统调用本身的问题都可以在strace的帮助下现出原形。

不过我这里需要先介绍一些系统调用相关的知识方便你更好地理解strace。

什么是系统调用?

系统调用英文叫system call缩写是syscall。如果我们把操作系统视作一个巨大的应用程序那么系统调用相当于什么呢其实就相当于 API。利用各种系统调用,应用程序就可以做到各种跟操作系统有关的任务了。

如果没有系统调用让应用程序直接去操作文件系统、内存等资源会怎么样呢那一定是一场灾难。我们可以想象一个场景程序A往地址为100~300的内存段里写入数据然后程序B往地址为200~400的内存段里写入数据因为200~300这段内存被A和B所共用就很容易产生错乱这两个程序都可能因此而出错乃至崩溃。

所以,我们必须有一个“管理机构”来统筹安排,让所有的应用程序都向这个统一机构申请资源和办理业务。这个“管理机构”就是操作系统内核,而系统调用就是这个机构提供的“服务窗口”。

内核态和用户态

实际上从更底层的视角来说操作系统必须要提供系统调用的另一个原因是计算机体系结构本身的设计。为了避免前面例子里那种不合理的操作以及让不同安全等级的程序使用不同权限的指令集大部分现代CPU架构都做了保护环Protection Ring的设计。

比如x86 CPU实现了4个级别的保护环也就是ring 0到ring 3其中ring 0权限最大ring 3最小。就拿Linux来说它的内核态就运行在ring 0只有内核态可以操作CPU的所有指令。Linux的用户态运行在ring 3它就没办法操作很多核心指令。

那么处于ring 3的应用程序也想要运行ring 0的指令的话该怎么办到呢就是让内核去间接地帮忙。而这个“忙”其实就是通过系统调用来“帮”的。

这样的话,用户空间程序就可以借系统调用之手,完成它原本没有权限完成的指令(进入内核态)。下面这张示意图就展示了用户空间、系统调用、内核空间这三者之间的关系:

既然系统调用要给用户空间的应用程序提供丰富而全面的接口无论是文件和网络等IO操作还是像申请内存等更加核心的操作都需要通过系统调用来完成那么系统调用的数量肯定是不少的。比如Linux的系统调用数量大约在300多个有的操作系统则会达到500个以上。

strace

了解了系统调用我们再来认识下strace。strace这个工具的s指的就是sycall所以strace就是对 syscall的trace。通过这个命令我们可以观测到一个进程访问的所有系统调用、给这些系统调用传入的参数以及系统调用的输出。可想而知这样充足的信息就给系统排查工作提供了极大的帮助。

你可以想象一下没有strace的时候你只是看到了程序的表象也就是程序想让你看到的你才能看到比如通过标准输出或者日志文件。而有了strace程序的一举一动就全在你的视野里了你就像有了火眼金睛程序在明里暗里干的所有事情都会被你知道。

夸奖了一番strace我们来了解一下它的具体用法。strace的用法一般有两种。

直接在命令之前加上strace。比如我们想知道curl www.baidu.com这个命令在系统调用层面具体发生了什么就可以执行strace curl www.baidu.com然后就能看到前后的几十个系统调用包括打开文件的openat()、关闭文件描述符的close()、建立TCP连接的connect()等等。

执行strace -p PID。这样的话你需要先找到进程的PID然后执行这条指令来完成追踪。这比较适合对持续运行的服务Daemon进行追踪。比如你可以先找到某个进程的进程号然后执行strace -p PID找到这个进程在系统调用方面的细节。当然你还可以加上各种其他参数来达到不同的追踪效果。

使用strace

好了聊完了strace的强大功能接下来看它的表现。我们用strace命令对php-fpm的进程号进行追踪也就是执行这个命令

strace -p 进程号

果然发现一个非常奇怪的现象整屏刷的都是gettimeofday()这个系统调用:

图片

看起来好像只有这个调用了,那有没有别的调用呢?

我们可以对strace命令加上 -c参数这样可以统计每个系统调用消耗的时间和次数看看这个奇怪的gettimeofday()系统调用占用了多少比例。我们在strace前面加上timeout 5就可以收集5秒钟的数据了命令如下

timeout 5 strace -cp 进程号

命令输出如下:

图片

可见gettimeofday()占用了这个进程高达97.91%的运行时间。在短短的5秒之内gettimeofday()调用次数达到了2万多次而其他正常的系统调用比如poll()、read()等,只有几十上百次。也就是说,非业务操作的耗时是业务操作耗时的50倍98%:2%。难怪进程这么卡原来全都花在执行getimeofday(),也就是收集系统时间数据上了。

那么为什么会有这么多gettimeofday()的调用呢这里的php-fpm有什么特殊性吗我们按这个方向去搜索发现有不少人也遇到了同样的问题比如Stack Overflow上这个人的“遭遇”。简而言之,原因多半是启用了某些性能监控软件。

我们把这个情况告诉了客户对方忽然想起来最近他们的国外团队确实有做一些性能监控的事情用的软件好像叫New Relic。果然我们在客户服务器上找到了这个New Relic。看下图

图片

原来问题的根因就是他们的国外团队部署了New Relic而这个软件发起了极为频繁的gettimeofday()系统调用。这些调用抢走了大部分的CPU时间这就导致业务代码基本没有机会被执行。因为LB有个60秒超时的机制所以它眼看着收不到后端Nginx服务器的返回就不得不返回HTTP 504了。也就是下面这样

这个根因有点令人哭笑不得不过对见怪不怪的技术支持团队来说心里早已云淡风轻了。接下来就是客户卸载New Relic应用立刻恢复正常。用strace再次检查显示系统调用也恢复正常

图片

可以看到read()和poll()系统调用上升到进程CPU时间的60%以上而gettimeofday()降低到不足1%,所以已经彻底解决问题了。

案例2LB特性和不均衡

我再给你讲一个案例。还有一个电商客户,也遇到了一次负载不均衡的问题。这是在双十一期间,客户发起了促销活动,随之而来的访问压力的上升也十分明显,而他们的后端服务器也出现了负载不均的现象,有时候访问十分卡顿,于是我们再次介入排查。

这次的架构是一台LB后面接了两台服务器其中一台的CPU load高达50以上也是远远超过了8个的CPU核数。另外一台情况要好一些load在10左右当请求被分配到这台服务器上的时候还算勉强可以访问。

因为CPU load在两台机器上有比较大的差异从访问速度来说大约是一半请求会非常慢一半请求略快一些所以客户就怀疑LB的请求分配做得不均衡导致其中一台处理不过来。

真的是这样吗我们检查了LB的访问日志发现一个有意思的现象

  • 大部分的访问请求是到了/api.php这个URL上。
  • 按HTTP请求的源IP来分开统计某些IP的访问量远远大于其他IP。

我们来看一下根据源IP分开统计的访问次数

很明显图中第一行的源IP对/api.php这个URL有3470次访问而其他源IP的访问量比它低了一个数量级只有小几百。那么这跟问题有什么关系呢

我们通过对LB日志做进一步的分析后发现同样是源IP的请求要么去了Nginx 1要么去了Nginx 2。再次检查了这台LB的配置后我们发现客户在LB上开启了“会话保持Session Persistence。这就造成了下面这种分布不均的现象

你可能也知道会话保持是LB的常见功能它主要是起到了维持会话状态的作用。在开启了会话保持功能后LB会维护一张映射表供每次分发请求之前做匹配。如果LB查到某个请求属于之前的某个会话就会把这个请求转发给上一次会话所选择的后端服务器否则就按默认的负载均衡算法来选择一个后端服务器。

那么LB怎么确定一个请求属于之前的会话呢这就要说到会话保持的类型了。我们用的LB是HAProxy它的会话保持有两种。

  • 源IP凡是源IP相同的请求都去同一个后端服务器。
  • Cookie凡是HTTP Cookie值相同的请求都去同一个后端服务器。

而客户启用的是第一种基于源IP的会话保持。不巧的是这次访问量的源IP本身的分布就很不均匀。就像前面提到的某个IP发起了10倍于其他IP的请求量那么这个源IP所分配到的后端服务器就不得不服务着10倍的请求了然后就导致了负载不均的问题出现。

所以,我们让客户关闭了会话保持,两台后端服务器的负载很快就恢复平衡了。我们也给出了进一步的建议:最好把/api.php这个服务跟其他的服务隔离开比如使用另外的域名做另一套负载均衡。这是因为/api.php的访问量和作用跟其他接口的区别很大通过对它做独立的负载均衡既可以隔离互相之间的干扰也有利于提供更加稳定的访问质量。

而且这次的问题也是无法用tcpdump来排查的它需要我们对LB的特点很熟悉以及对LB和应用结合的场景都有一个全面的认识。

实验

我们已经学习了strace现在做一个简单的小实验来巩固一下学到的知识吧。我们可以这样做

  1. 这里下载并执行一个Nginx服务的Docker镜像也就是执行
docker run --cap-add=SYS_PTRACE -p 80:80 -it docker.io/moonlightysh/v_nginx bash

注意,这里必须加上 --cap-add=SYS_PTRACE否则容器内的strace将不能正确运行。

  1. 进入容器后启动里面的Nginx服务执行
systemctl start nginx

  1. 还是在容器内启动strace
strace -p $(pidof nginx | awk '{print $1}')

此时strace就开始监听Nginx worker进程了。

补充:这里用 awk '{print $1}' 是为了追踪Nginx worker进程它出现在pidof命令输出的左边而最右边是Nginx master进程。跟踪Nginx master进程是看不到HTTP处理过程的。

  1. 从你的本机发起HTTP请求也就是执行 curl localhost:80此时在容器内Nginx在处理这次请求时就会发起很多系统调用而strace就全面展示了这些调用涉及的文件和参数细节。比如下面这样的strace输出
root@48fc1221a03a:/# strace -p $(pidof nginx | awk '{print $1}')
strace: Process 14 attached
epoll_wait(9, [{EPOLLIN, {u32=4072472592, u64=140681431285776}}], 512, -1) = 1
accept4(6, {sa_family=AF_INET, sin_port=htons(60070), sin_addr=inet_addr("172.17.0.1")}, [112->16], SOCK_NONBLOCK) = 3
epoll_ctl(9, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=4072473289, u64=140681431286473}}) = 0
epoll_wait(9, [{EPOLLIN, {u32=4072473289, u64=140681431286473}}], 512, 60000) = 1
recvfrom(3, "GET / HTTP/1.1\r\nHost: localhost\r"..., 1024, 0, NULL, NULL) = 73
stat("/var/www/html/", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
stat("/var/www/html/", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
stat("/var/www/html/index.html", 0x7ffcb86eac80) = -1 ENOENT (No such file or directory)
stat("/var/www/html", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
stat("/var/www/html/index.htm", 0x7ffcb86eac80) = -1 ENOENT (No such file or directory)
stat("/var/www/html/index.nginx-debian.html", {st_mode=S_IFREG|0644, st_size=612, ...}) = 0
stat("/var/www/html/index.nginx-debian.html", {st_mode=S_IFREG|0644, st_size=612, ...}) = 0
openat(AT_FDCWD, "/var/www/html/index.nginx-debian.html", O_RDONLY|O_NONBLOCK) = 11
fstat(11, {st_mode=S_IFREG|0644, st_size=612, ...}) = 0
setsockopt(3, SOL_TCP, TCP_CORK, [1], 4) = 0
writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1"..., iov_len=247}], 1) = 247
sendfile(3, 11, [0] => [612], 612)      = 612
write(4, "172.17.0.1 - - [07/Mar/2022:14:2"..., 87) = 87
close(11)                               = 0
setsockopt(3, SOL_TCP, TCP_CORK, [0], 4) = 0
epoll_wait(9, [{EPOLLIN|EPOLLRDHUP, {u32=4072473289, u64=140681431286473}}], 512, 65000) = 1
recvfrom(3, "", 1024, 0, NULL, NULL)    = 0
close(3)                                = 0
epoll_wait(9,

小结

我们的课程主旨是网络排查但因为网络跟系统又是密不可分的关系所以在掌握Wireshark等技能以外我们最好要熟悉操作系统排查的常规步骤。

在这节课里我们通过一个典型的系统问题导致服务慢的案例学习了内核空间内核态、用户空间用户态、系统调用这些操作系统的概念。另外我们还重点学习了strace这个工具知道它有两种使用方式直接在命令之前加上strace执行strace -p PID。

那么在案例中,我们也用 strace-cp PID中的-c参数,对进程发起的各种系统调用进行了执行次数和执行时间的统计,这对于分析进程耗时的去向会很有帮助。

在这里我们也再一次温习下HTTP 504的概念。在后端服务不能在LB的时限内回复HTTP响应的时候LB就会用HTTP 504来回复给客户端告诉它是后端服务超时潜台词我LB可没问题

而在第二个案例里,我们了解了会话保持在某些情况下会“放大”负载不均衡的问题。所以你要知道,在启用会话保持功能时,你需要根据实际情况,做通盘的考虑。

思考题

给你留两道思考题:

  • 在案例1里面LB回复了HTTP 504。那在什么情况下这个LB会回复HTTP 503呢
  • 除了strace你还知道哪些trace类的工具可以帮助排查呢

欢迎你在留言区分享自己的经验,我们一同成长。