本文介绍了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环境下,连一次断一次,一个请求一次处理,挺好。
本篇文章涉及的网络协议比较多,由实际问题入手,层层深入,部分观点整理于网络,由于作者水平有限,文中的观点有不确切之处,欢迎评论讨论。请阅读原文获取更好的阅读体验。