2020年6月

背景

由于业务需要,我们在生产环境下利用rabbitmq作为消息通信的服务, 今天发现一个奇怪的现象, 服务端存在大量的Connections,同时有同等多的channels, 但是根据业务上分析, 我们完全用不到这么多的连接, 随后我们进行了排查问题的第一步:

初步思路

打开client所在的服务器

查看连接5672(amqp端口)的tcp数量以及状态

 ss -aoen state established | grep 5672
  ss -aoen state established | grep 5672 | wc -l

这个只是部分截图, 但是通过ss分析,可以肯定两件事情:

  1. 客户端维护的5672 tcp数量是很少的, 也是符合业务场景的
  2. 每一个tcp都维护了正常的keepalive

打开rabbitmq-server所在的服务器

查看连接5672(amqp端口)的tcp数量以及状态

 ss -aoen state established | grep 5672
 ss -aoen state established | grep 5672 | wc -l

通过分析发现:

  1. 服务端tcp数量很大
  2. 服务端tcp没有keepalive

进一步猜想

服务端存活着大量的dead connection, 由于客户端没有正确的关闭, 正常情况下客户端即使没有正常断开连接, 但是应该由服务端心跳检测, 或者借助tcp keepalive来实现服务端断开dead connection.但是这两种方式均为生效

由此有如下猜想:

1.服务端(rabbitmq-server)没有开启tcp keepalive?

  1. 客户端没有配置心跳?

证实猜想

翻阅官方文档, 阅读后, 可以总结为:

作为amqp协议, 鼓励你使用心跳来做conn的维护,客户端 捕获心跳异常来重连, 如果正确配置了心跳,那么服务端在心跳实际发现客户端无响应, 自然会断开.

随后, 去查看php amqp的源码, 发现如下:

    public function __construct(
        $host,
        $port,
        $user,
        $password,
        $vhost = '/',
        $insist = false,
        $login_method = 'AMQPLAIN',
        $login_response = null,
        $locale = 'en_US',
        $connection_timeout = 3.0,
        $read_write_timeout = 3.0,
        $context = null,
        $keepalive = false,
        $heartbeat = 0,
        $channel_rpc_timeout = 0.0,
        $ssl_protocol = null

if ($this->keepalive) {
    $this->enable_keepalive();
}
    protected function enable_keepalive()
    {
        if ($this->protocol === 'ssl') {
            throw new AMQPIOException('Can not enable keepalive: ssl connection does not support keepalive (#70939)');
        }
        .......
        .......

        $socket = socket_import_stream($this->sock);
        socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
    }

由上面的源码可以分析到, php的这个lib, 在默认情况下, 完全禁用了heartbeats以及keepalive, 由此证明猜想.

A zero value indicates that a peer suggests disabling heartbeats entirely. To disable heartbeats, both peers have to opt in and use the value of 0. This is highly recommended against unless the environment is known to use TCP keepalives on every host.

官方文档中明确说明, 除非特殊情况, 否则强烈建议使用心跳.

遗留的疑问

  1. tcp keepalive 还需要服务端应用程序额外开启?

根据php的源码socket_set_option中可以看到需要客户端明确开启keepalive,底层调用 ` socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
` 实现.

额外补充一个golang版本的代码:

这是客户端:

func main() {
    var tcpAddr *net.TCPAddr
    tcpAddr,_ = net.ResolveTCPAddr("tcp","127.0.0.1:9999")

    conn,err := net.DialTCP("tcp",nil,tcpAddr)
    // 这里设置和不设置影响客户端tcp keepalive
    conn.SetKeepAlive(true)

通过ss查看:

上面是未设置SetKeepAlive, 由此可以看出, 客户端需要明确指定keepalive来使用tcp keepalive机制

总结

整体写了心跳以及tcp keepalive的问题, 也发现了线上php amqplib的默认值导致的该问题, 所以这里修改php的初始化方法, 给心跳分配一个时间(golang版本默认值是15s)

tips

这里涉及到后续要删掉那些死连接, 这里提供一个小小的tips

rabbitmqctl list_connections pid port state user recv_cnt send_cnt send_pend name client_properties | more

这里可以获取client属性,然后匹配php版本,全量关闭,然后重连即可.

Too many open files 是提供http服务的应用程序很容易遇到的问题, 通常是由于系统配置不当或者程序本身打开太多文件导致的, 于是网上有两种提示你解决该类问题的办法.

  1. 你的系统配置的最大文件打开数太低了
  2. 你的程序df泄露了

本篇内容主要围绕第一个原因展开描述,问题先从这个issue开始说起

这里有人回复:

Check your hard and soft limits for file descriptors (respectively ulimit -Hn and ulimit -Hs) and your (linux) kernel settings (cat /pro/sys/fs/file-max) as these may need to be adjusted.

这里提到3个概念

  1. ulimit -Hn
  2. ulimit -Hs
  3. /pro/sys/fs/file-max

带着这3个指标, 我们开始查阅资料

什么是ulimit

help ulimit # 注意这里的命令,请切换到bash上操作,而不是zsh

修改 shell 资源限制
在允许此类控制的系统上,提供对于 shell 及其创建的进程所可用的资源的控制。

由此,我们可以看出, 我们跑在linux上的大部分程序, 都是有shell所创建进程, 那么ulimit就如同一个配置文件, 限制了我们使用系统资源.

如果你使用help ulimit, 可以查看到如下参数配置项

选项:
  -S    使用 `soft'(软)资源限制
  -H    使用 `hard'(硬)资源限制
  -a    所有当前限制都被报告
  -b    套接字缓存尺寸
  -c    创建的核文件的最大尺寸
  -d    一个进程的数据区的最大尺寸
  -e    最高的调度优先级(`nice')
  -f    有 shell 及其子进程可以写的最大文件尺寸
  -i    最多的可以挂起的信号数
  -l    一个进程可以锁定的最大内存尺寸
  -m    最大的内存进驻尺寸
  -n    最多的打开的文件描述符个数
  -p    管道缓冲区尺寸
  -q    POSIX 信息队列的最大字节数
  -r    实时调度的最大优先级
  -s    最大栈尺寸
  -t    最大的CPU时间,以秒为单位
  -u    最大用户进程数
  -v    虚拟内存尺寸
  -x    最大的锁数量
  

这其中, 有我们关心的内容, 软资源和硬资源

什么是soft和hard

这里通过查阅资料整理如下:

  1. 理解soft和hard, 首先要想到是权限需求
  2. soft 必须小于hard
  3. hard严格的设定,必定不能超过这个设定的数值
  4. soft警告的设定,可以超过这个设定值,但是若超过则有警告信息
  5. 一个process可以修改当前process的soft或hard

ulimit的修改与生效

  1. ulimit的值总是继承父进程的设置。
  2. 可以利用ulimit设置当前shell进程
  3. 增加hard, 只能由root来

下面通过几个例子来践行上面的理论知识:

  1. 修改/etc/security/limits.conf
一条记录包含4️列,分别是范围domain(即生效的范围,可以是用户名、group名或*代表所有非root用户);t类型type:即soft、hard,或者-代表同时设置soft和hard;项目item,即ulimit中的资源控制项目,名字枚举可以参考文件中的注释;最后就是value。
* hard nofile 1000000 
* soft nofile 1000

Question: 修改后不重启当前shell, limits的修改是否生效
A: 否

  1. 当前shell配置ulimit
#!/bin/bash
ulimit -Sn 2045
nc -l 8888

然后ps -ef 查看nc的进程, 然后 cat /proc/{$pid}/limits

Max open files 2045 1000000 files

可以看到这里已经通过shell修改了soft nofile

修改系统最大的df数

除了ulimit控制外,/proc/sys/fs/file-max这个文件控制了系统内核可以打开的全部文件总数。所以,即便是ulimit里nofile设置为ulimited,也还是受限的。

系统级别: /proc/sys/fs/file-max

用户级别: /etc/security/limits.conf

单个进程: fs.nr_open

ulimit 常见命令

  • ulimit -a # 查看所有soft值
  • ulimit -Ha # 查看所有hard值
  • ulimit -Hn # 查看nofile的hard值
  • ulimit -Sn 1000 # 将nofile的soft值设置为1000
  • ulimit -n 1000 # 同时将nofiles的hard和soft值设置为1000

参考资料:
https://juejin.im/post/5d4cf32f6fb9a06b1d21312c
https://blog.csdn.net/liuleinevermore/article/details/83473919