Redis连接数为何会偏高

2019-07-04 11:03:24 浏览数 (1)

本文介绍了ThinkPHP和YII2两个框架中对于redis的典型使用场景,通过连接数偏高的现象引出了长连接与短连接的概念,并且简单描述了几种网络连接状态,包括TIME_WAIT,ESTABLISHED,同时介绍了应用开发中Socket与TCP UDP的关联关系。

本文的大纲

问题描述 初步排查 TCP连接状态 ESTABLISHED TIME_WAIT 三次握手 Socket连接 长连接还是短链接 代码示例 结论

问题描述

运维收到线上服务器报警,反映Redis连接数过高,大量ESTABLISHED状态的连接,需要处理。 基础环境 PHP5.6 Think5 Redis

初步排查

首先查看Redis配置,发现缓存业务连接参数采用的是短连接,第一反应是代码是否没有做close。一查代码层面果然没有close,立马加上看效果,依然如此。

再次查看,Redis托管Session部分的Redis采用的默认连接,默认连接是长连接。后边示例中有代码说明。

TCP网络连接状态

检查TIME_WAIT 连接个数 128个,状态显示 ESTABLISHED

代码语言:javascript复制
netstat -na | grep 6379 | grep TIME_WAIT | wc -l

128
ESTABLISHED

大量ESTABLISHED状态代表什么,那我们往下看

ESTABLISHED的意思是建立连接。表示两台机器正在通信。

TIME_WAIT

这是 TCP 连接完全关闭前的最后一个状态,一个连接被关闭时,主动关闭的一端最后会进入 TIME_WAIT 状态,等待足够的时间以确保远程 TCP 接收到连接中断请求的确认,这个时间最大为四分钟,可调整。

如下图,图片来源于知乎

四次挥手断开连接

如上图,TCP中主动断开的一方确实会保持TIME_WAIT一段时间

两个状态都是TCP连接中的概念,说到TCP连接,我们不得不提三次握手和四次挥手以及Socket。三次握手用于建立连接,四次挥手用于断开连接。

三次握手

三次握手

三次握手

Socket连接

Socket连接到底是个什么概念? 1.完整的套接字格式{protocol,src_addr,src_port,dest_addr,dest_port}。 这常被称为套接字的五元组。其中protocol指定了是TCP还是UDP连接,其余的分别指定了源地址、源端口、目标地址、目标端口。

还有这么一个概念

TCP的连接端点称为 套接字(socket),根据TCP协议的规定,端口号拼接到IP地址即构成了套接字。

下面我们整理下TCP连接与Socket之间的关系。

TCP的连接端点实际上是一对儿客户端和服务器端,或者说是源地址和目标地址。

TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。

借助网络的一张图,我们看看Socket在整个网络协议上的位置

socket是在应用层和传输层之间的一个抽象层

由于socket是全双工的工作模式,一个socket的关闭,是需要四次挥手来完成的。

主动关闭连接的一方,调用close();协议层发送FIN包 被动关闭的一方收到FIN包后,协议层回复ACK;然后被动关闭的一方,进入CLOSE_WAIT状态, 主动关闭的一方等待对方关闭,则进入FIN_WAIT_2状态;此时,主动关闭的一方 等待 被动关闭一方的应用程序,调用close操作。

被动关闭的一方在完成所有数据发送后,调用close()操作;此时,协议层发送FIN包给主动关闭的一方,等待对方的ACK,被动关闭的一方进入LAST_ACK状态;

主动关闭的一方收到FIN包,协议层回复ACK;此时,主动关闭连接的一方,进入TIME_WAIT状态;而被动关闭的一方,进入CLOSED状态。 等待2MSL时间,主动关闭的一方,结束TIME_WAIT,进入CLOSED状态

下面我们了解下长连接和短连接的相关概念

长连接还是短链接

知乎找到两张图来解释长连接和短连接

长连接

长连接

长连接,也叫持久连接,在TCP层握手成功后,不立即断开连接,并在此连接的基础上进行多次消息(包括心跳)交互,直至连接的任意一方(客户端OR服务端)主动断开连接,此过程称为一次完整的长连接。HTTP 1.1相对于1.0最重要的新特性就是引入了长连接。

短连接

短连接,顾名思义,与长连接的区别就是,客户端收到服务端的响应后,立刻发送FIN消息,主动释放连接。也有服务端主动断连的情况,凡是在一次消息交互(发请求-收响应)之后立刻断开连接的情况都称为短连接。缺点是每个连接都需要经过三次握手和四次握手的过程,耗时大大增加。

注:短连接是建立在TCP协议上的,有完整的握手挥手流程,区别于UDP协议。

作者:南阳居士 链接:https://www.zhihu.com/question/22677800/answer/382380580 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

短连接

Redis本身提供了两种对外连接访问接口pconnect和connect,也就是说应用程序有两种连接Redis方式,长连接(pconnect)和短连接(connect)。

这里说的长连接是指多次请求之间可以对redis连接进行复用,即只在第一次执行请求是建立连接,以后每次请求只是从连接池中将连接取出,不再重新建立连接;而短连接表示连接在多次请求之间不可复用,每次请求都需要重新建立连接。与上文描述的长连接和短连接含义一致。

对于PHP使用长连接,业界有一个观点

长连接只会在PHP-FPM进程结束之后结束,连接的生命周期就是PHP-FPM进程的生命周期。

如果代码中使用pconnect, close的作用仅是使当前php不能再进行redis请求,但无法真正关闭redis长连接,连接在后续请求中仍然会被重用,直至fpm进程生命周期结束。而这个连接数量由php-fpm的最大连接数决定 如: ps.maxChild=128,那么最大连接数就是128

疑问

使用connect需要显式调用close方法,会不会自动断开连接,是否需要显式设置连接超时时间?

我们在生产环境下遇到使用redis长连接方式连接数过高问题,改为短连接后,针对连接数偏高的想象,连接数立刻恢复正常。

既然pconnect可以 重用连接,什么场景下应该使用pconnect建立连接?

PHP生态下,没有真正意义的基于连接池的连接

代码示例

项目使用的ThinkPHP5,有两处使用Redis,一处是Redis托管登录Session信息,一处是其它业务Redis缓存。

参数配置中 persistent很关键,用于设置采用长连接还是短连接,生产环境的问题就是因为托管登录Session信息的配置中没有显式指定persistent=>false 造成的

Redis托管Session信息

我们先看Session相关的设置和执行

Session相关的Redis配置参数

代码语言:javascript复制
     //  ----------------------------------------------------------------------
    // | 会话设置
    //  ----------------------------------------------------------------------
    'session' => [
        'id' => '',
        // SESSION_ID的提交变量,解决flash上传跨域
        'var_session_id' => '',
        // SESSION 前缀
        'prefix' => 'etcp',
        // 驱动方式 支持redis memcache memcached
        'type' => 'redis',
        // redis主机
        'host' => 'host',
        // redis端口
        'port' => 6379,
        // 密码
        'password' => '',
        'select' => 14,
        // 是否自动开启 SESSION
        'auto_start' => true,
        'time'
    ],

驱动文件路径 thinkphp/library/think/session/driver/Redis.php

代码语言:javascript复制
    /**
     * 打开Session
     * @access public
     * @param string $savePath
     * @param mixed  $sessName
     * @return bool
     * @throws Exception
     */
    public function open($savePath, $sessName)
    {
        // 检测php环境
        if (!extension_loaded('redis')) {
            throw new Exception('not support:redis');
        }
        $this->handler = new Redis;

        // 建立连接
        $func = $this->config['persistent'] ? 'pconnect' : 'connect';
        $this->handler->$func($this->config['host'], $this->config['port'], $this->config['timeout']);

        if ('' != $this->config['password']) {
            $this->handler->auth($this->config['password']);
        }

        if (0 != $this->config['select']) {
            $this->handler->select($this->config['select']);
        }

        return true;
    }

依据配置,通过Open函数可以看到默认通过persistent建立了长连接。

Redis缓存业务信息

配置参数

代码语言:javascript复制
 'redis_server' => [
        'host' => 'REDIS_RW',
        'port' => 6379,
        'persistent' => false,
        'database' => 0,
        'profiler' => true,
        'password' => 'password',
    ]

设置与读取示例 借助github上一个开源redis操作包 "shen2/easy-redis": "dev-master"

代码语言:javascript复制
class RedisClient
{
    private static $config;

    private static $_instance;

    /**
     * __construct
     *
     * @return mixed
     */
    public function __construct()
    {
        if (self::$config == null) {
            self::$config = Config::get('redis_server');
        }
    }

    /**
     * GetInstance
     *
     * @return mixed
     */
    public static function getInstance()
    {
        if (!self::$_instance instanceof self) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    /**
     * redis
     *
     * @return mixed
     */
    public static function getMainInstance()
    {
        if (!self::$_instance instanceof self) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }


    /**
     * 设置key值
     *
     * @param mixed $key    键
     * @param mixed $value  值
     * @param mixed $expire 过期时间
     *
     * @return mixed
     */
    public function setCommond($key, $value, $expire)
    {
        $redisManager = new EasyRedisManager(self::$config);
        return $redisManager->call('set', $key, $value, $expire);
    }
    /**
     * 获取value
     *
     * @param mixed $name
     *
     * @return mixed
     */
    public function getCommond($name)
    {
        $redisManager = new EasyRedisManager(self::$config);
        return $redisManager->call('get', $name);
    }
    /*

YII2 Redis

驱动包github地址 https://github.com/yiisoft/yii2-redis

官方文档 Redis Cache, Session and ActiveRecord for Yii 2

从题目中就可以看出这个扩展的三块使用场景,缓存数据,托管Session,操作ActiveRecord。

可以在这里查看到更多使用信息 https://github.com/yiisoft/yii2-redis/blob/master/docs/guide/README.md

配置

代码语言:javascript复制
[
    'class' => 'yiiredisConnection',
    'hostname' => 'REDIS_PHP',
    'port' => 6379,
    'password' => 'password',
    'database' => 0,
];

使用

代码语言:javascript复制
/**
     * CreateLoginToken
     *
     * @param mixed $userId
     * @param mixed $timeStamp
     * @param mixed $token_ttl 3600
     * @param mixed $data      缓存数据
     *
     * @return mixed
     */
    public function createLoginToken($userId, $timeStamp, $token_ttl=3600, $data=[])
    {
        $token = md5($userId . self::SALT . $timeStamp);
        $value=json_encode(array("uid"=>$userId,"data"=>$data));
        $redis = Yii::$app->redis;
        $this->_ttl = $token_ttl;
        $result = $redis->setex($token, $this->_ttl, $value);
        return $token;
    }

    /**
     * Remove LoginToken
     *
     * @param mixed $token Token
     *
     * @return mixed
     */
    public function removeLoginToken($token)
    {
        $redis = Yii::$app->redis;
        $result = $redis->del($token);
        return $result;
    }

连接池

连接池的作用是复用连接,省去了每次都要建立连接的通信成本。使用连接池,就得研究长连接。

PHP是否使用连接池

php作为脚本语言,上文提到的观点

长连接只会在PHP-FPM进程结束之后结束,连接的生命周期就是PHP-FPM进程的生命周期

或者换一种说法,真正基于连接池的长连接,并且能实现重用连接,达到降低系统消耗的目的,PHP环境下做不到。

如果TIME_WAIT数太多,并不是将连接改为长连接即可,PHP环境下,可以改为短连接验证一下,是否能够满足业务场景需要。一切以能满足业务场景为最终目的。

结论

都说长连接可以降低系统消耗,公用连接,这里的公用连接是有代价的,并不是只要长连接都可以降低系统资源。PHP环境下,连一次断一次,一个请求一次处理,挺好。

本篇文章涉及的网络协议比较多,由实际问题入手,层层深入,部分观点整理于网络,由于作者水平有限,文中的观点有不确切之处,欢迎评论讨论。请阅读原文获取更好的阅读体验。

0 人点赞