云服务器在高流量下被打死的技术细节

719 阅读6分钟

背景

近期,有一个同事反馈一个技术问题,细节如下:

  • 用户使用AWS MSK(Kafka)作为MQ,并使用AWS EC2作为消费者消费这个MQ里面的消息
  • 用户在进行极限压测的时候,遇到了AWS EC2被打死,导致服务器无响应
    • SSH 无法登陆AWS EC2
    • SpringBoot程序作为MQ的消费者失去响应,一小时内都似乎没有恢复
    • AWS CloudWatch面板上观察到AWS EC2 CPU其实利用率随着压测压力的释放,利用率不到20%
    • 等待很长时间后发现AWS EC2即使利用率不高也没发恢复到SSH可登陆状态

所以综合来看,似乎是AWS EC2这边出现了某种资源不够导致的OS级别的问题

分析

一般而言,云服务商的服务器都是久经考验的,经过了无数客户在各种场景下的压力考验,出现这样的情况我们可以分析到可能是如下的几种情况

  • CPU被打满:当CPU被打满后,服务器过载导致失去响应,但是如果压力下降,CPU利用率下降后,应该会自动恢复
  • Memory被打满:当Memory被耗尽后,OS进程被挂起,包括SSH在内的一些进程出现memory资源抢占或者无法申请的问题。这时会导致这些程序无法正常的运行。但是如果memory被释放,有足够的memory后一般而言这些进程会恢复工作
  • 网络被打满:当网络带宽被打满后,新的流量无法进入到服务器里面,导致发生大量的丢包等问题

结合上面的分析,我们可以看到这个同事的问题应该是Memory被打满的问题,但是要具体分析

排查

  1. 首先排除网络问题,AWS EC2的带宽在一众云服务商中是很有领先优势的,我们可以在network bandwidth看到具体机型的带宽上限,AWS EC2的带宽最小都是5G以上,也就是可以达到500MB/S,这是非常大的数字,所以网络部分问题不大,而且我们可以在AWS CloudWatch上看到网络是否被打满。当时的情况是网络并为被看到打满的情况
  2. 其次排除CPU的问题,AWS EC2的CPU客户使用的M5系列,CPU资源强劲,在AWS CloudWatch上我们看到压测压力释放后,CPU利用率下降了,但是AWS EC2依然没有响应。因此排除CPU资源不够的问题
  3. 最后定位Memory的问题,AWS EC2默认不监控Memory的利用率,这可能是考虑到客户的信息安全问题的设计,我们可以安装CloudWatch agent来监控memory,并把memory指标发送到CloudWatch上去,Monitor memory and disk metrics for Amazon EC2 Linux instances

当我们安装了CloudWatch agent后继续进行压测,发现Memory到利用率快速的上升,并在利用率达到92%后,复现上述的现象。这是我们发现AWS EC2失去了响应,SSH无法登陆,CloudWatch Agent停止了汇报内存利用率,估计这个agent这时也挂起了。

因此我们确认是内存资源导致的问题,那么多种监控手段都用了,但是AWS EC2已经出现了停止响应的问题,那么我们怎么继续定位呢,有下面的几种方式

  1. 使用其他方式尝试链接
    1. 使用AWS上自带的Serial Console尝试连接到AWS EC2上
    2. 使用AWS System Manager上的session manager连接
  2. 将压测停止,然后进行reboot,安装atop,尝试监控更多的信息
    1. 这些监控组件安装好后进行有梯度的缓慢的压力提升,尽量在问题边界定位问题
    2. 尝试获取Amazon Linux的日志,定位问题
  3. 检查服务器的swap是否打开,My EC2 Linux instance failed the instance status check due to over-utilization of its resources. How do I troubleshoot this?

当我们尝试前面2个方式都不太凑效的时候,那么就可以考虑检查swap,swap能在服务物理内存不够的时候,使用磁盘做Memory的虚拟化扩展,当物理内存不够用的时候,操作系统会从内存中取出一部分暂时不用的数据,放在交换分区中,从而为当前运行的程序腾出足够的内存空间,保证程序的正常运行,但是swap由于磁盘的IO性能限制,远不如实际的物理内存的性能,一般云服务商都没有打开swap

AWS EC2 默认没有打开SWAP,除了极少数低memory机型:

Aliyun ECS默认没有打开SWAP:

当时客户在一台AWS EC2上运行了多个java Spring程序,EC2的物理内存为8G(实际可用内存不到8G),每个java程序配置的-xms为4096M;那么我们假设他运行了至少2个不同java程序A、B,表面上每个程序的内存消耗都不到物理内存的一半,但是在压测期间JVM会随着压力不断的尝试申请内存(每个最大4096M),那么JVM合计的内存消耗就大于EC2的实际内存了,这才是根本原因!!!

在压测时,出现的上述情况,在Java上体现的最明显

  • JVM在管理内存的时候,其实它只会管理自己堆上的内存,其实实际的内存需求是 堆外内存+堆内存,当我们设置-xms 4096M时实际的内存消耗是大于4096M的
  • 不同的java进程间时无感知的,合计的内存大于了实际的物理内存
  • 如果你使用Java 8以及更老的版本,JVM并不会自动的在空闲时段将不用的内存释放给Linux OS;Java 11以及更新的版本,可以考虑使用ZGC或者Shenandoah GC释放内存给OS

解决

我们可以就这个问题,直接打开SWAP缓解物理内存不够的问题。没打开SWAP在JVM耗尽内存后,由于Linux没有额外的手段做非热点内存数据的腾挪,那么被卡死,当我们打开SWAP后,Linux把闲置内存数据临时放在SWAP,优先满足繁忙的应用程序的内存需求,这也就避免的上述的问题

注意:

  • 开启SWAP只能保证Linux最大化的因为内存空间不足导致的卡死,不能无限的扩展内存大小
  • 开启SWAP后,并不能降低JVM向Linux提交的内存需求的大小
  • 开启SWAP后,会有一定程度的性能损失