PHP与MySQL通讯那点事

在我们的一款WebGame的生产环境中,一次无意的strace抓包时,发现了php与mysql大量通讯的数据。这种情况,在游戏服务器刚启动时,是正常的,但如果是运行一段时间之后,出现大量SELECT的SQL查询,绝对是有问题的,而且,所操作的数据库并不是配置库,那意味着,我们程序员的程序出现了违规的操作。具体结果大约如下:

strace跟踪php进程与mysql通讯的日志(点击可查看大图)

如上图所示,php持续接收读取进程内描述符为3的响应包数据,描述符为3的为php与mysql建立的TCP通讯链接,这点也可以从313行的SELECT语句来确认。(原始数据丢失了,我模仿了一条。所以是配置库的SQL语句)

这是什么程序,想实现什么逻辑?为何要取这么多数据?

跟着这里的SELECT的sql语句,我定位到了相应的程序段:

/*
**  业务逻辑的代码
*/
public function SItem($roleId,$baseId) {
    //...
    // ############写出下面这种代码的人都得死.##################
    $this->dbrRole->select(‘*‘);
    $this->dbrRole->from(‘role_items‘);
    $this->dbrRole->where(‘role_id‘,$roleId);
    $this->dbrRole->where(‘baseId‘,$baseId);
    $result = $this->dbrRole->get()->row(); //看上去,这里好像正常,我们都以为框架会给我们只取一条。
    //...
}

我们从代码上来看,好像明白程序员想根据对应的role_id到role_items表里取一条想符合的数据,所以,他调用了row方法,来取一条。看上去,这里好像正常,我们都以为框架会给我们只取一条。但实际上,框架是如何处理的呢?

我们来看下框架的对应row方法的实现过程。对了,我们是CodeIgniter框架的一个较老的版本。

/*
**  框架中,DB drive中,row相关方法的代码
**
*/
public function row($n = 0,$type = ‘array‘){
    if(!is_numeric($n)){
        if(! is_array($this->_rowData)){
            $this->_rowData = $this->rowArray(0);
        }
        if(isset($this->_rowData[$n])){
            return $this->_rowData[$n];
        }
        $n = 0;
    }
    return ($type == ‘object‘) ? $this->rowObject($n) : $this->rowArray($n);
}

//继续跟进rowArray方法
public function rowArray($n = 0){
    $result = $this->resultArray();
    if(count($result) == 0){
        return $result;
    }

    if($n != $this->_current && isset($result[$n])){
        $this->_current = $n;
    }

    return $result[$this->_current];
}

//继续跟进resultArray方法 ###这个方法是重点###
public function resultArray(){
    if(count($this->resultArray) > 0){
        return $this->resultArray;
    }

    if(false === $this->resulter || 0 == $this->recordCount()){
        return array();
    }

    $this->_dataSeek(0);
    while($row = $this->_fetchAssoc()){
        $this->resultArray[] = $row;    //###########这个数组每次都增加_fetchAssoc()结果的内存大小数量#########################
    }
    return $this->resultArray;
}

//继续跟进_fetchAssoc方法
/*
** 对应driver的_fetchAssoc方法的代码
*/
protected function _fetchAssoc(){
    return mysql_fetch_assoc($this->resulter);
}

我们可以看到CodeIgniter框架的resultArray方法使用mysql(我们的php调用mysql的api用的是mysql函数,有点绕,后面解释)的mysql_fetch_assoc函数对缓冲区的数据进行遍历转换。将所有缓冲区的数据全部复制给$this->resultArray属性,再判断row方法中所需要的key的结果是否存在,再与返回的。

也就是说,框架层并没有只从mysql server(潜意识上的mysql server)那边取一条给我们调用者,而是取了所有结果,再返回一条。(先别喷,后面解释) 当然,CI这种做法,也不是错。但我觉得有更好的改进方法。

这个问题,我们组的dietoad (征婚) 发现了这个问题,并给了修复方案。有些同学认为,这是程序员的错,程序员的SELECT语句没有加limit来限制条数。这我绝对赞同,而且,觉得写出这种代码的人都得死。

  • 业务层:为这种业务需求的SQL语句加上limit限制
  • 框架层:框架对于这种需求,自动控制,发现这种情况,直接返回1条

对于解决方案1,我写了一个正则,匹配select()方法被调用之后,row()方法被调用之前,中间没有使用limit()方法的所有代码,结果,发现量并不小。后来,我们决定两种方案同时实施,防止第二种出现漏掉的情况。

dietoad给出如下改进:

/*
**  //改进为当_rowData不存在时,从_rowData的数量开始取,取小于$n条记录,避免 上面 resultArray方法中从缓冲区取所有数据,复制双倍数据,占用内存的情况
*/
public function row ($n = 0, $type = ‘array‘)
{
    if(isset($this->_rowData[$n]))
    {
        return $this->_rowData[$n];
    }
    if (! is_numeric($n))
    {
        return $this->rowObject($n);
    }

    $ln=count($this->_rowData);
    //继续上次位置
    while($ln++<=$n&&$r=$this->_fetchAssoc())
    {
       $this->_rowData[]=$r;
    }
    //需要几条就读几条
    //防止记录集为空报warning
    return isset($this->_rowData[$n])?$this->_rowData[$n]:array();
}

在今年的4月末,鄙人写过另一篇关于CodeIgniter框架的设计缺陷问题,给我们游戏项目带来较大的影响,后来提交到github issues,并没得到回复,想了想,虽然官方的2.1.3版本中,也存在这个小问题。不过我觉得,这就不提交了,或许,我们的做法也符合他们的设计初衷。不过,我们还是在我们的项目中改进了。

如此改进之后,我们使用php的memory_get_usage()函数观察前后两个row()方法的结果时,果然发现内存使用情况有较大改善(改善幅度取决于SELECT的返回数据量)。

似乎,到这里就应该结束了,问题就这么被发现,被解决了。

但,我总觉得少了些什么呢?当我再次strace抓包时,发现仍然存在大量的数据通讯,就像文章开头的那副截图一模一样。然而,这又是什么原因呢?

我顺手写了个内存占用的测试代码如下:

$db = mysql_connect(‘192.168.xx.xx‘,‘xxxx‘,‘xxxx‘);
$sql = ‘SELECT * from items‘;
mysql_select_db(‘jv01‘,$db);
echo ‘SELECT_DB: ‘,convert(memory_get_usage()),"\n";     //619.26 kb

$r = mysql_query($sql,$db);
echo ‘QUERY_SQL: ‘,convert(memory_get_usage()),"\n";    //619.98 kb  ###什么?查询完之后,内存大小居然只增加了不到1k?我那个表可是几十M的数据啊

//sleep(50);  // hold住进程,别销毁,留着看当前进程的内存分配1
$arr = array();
while ($rs = mysql_fetch_assoc($r))
{
    $arr[]=$rs;
}
echo ‘FETCH_RS: ‘,convert(memory_get_usage()),"\n";    //27.11 mb  ###什么?刚刚不是只增加了1k吗?这里的遍历的结果集怎么突增几十M啊?尼玛这到底是什么情况?

unset($arr);
echo ‘UNSET: ‘,convert(memory_get_usage()),"\n";    //620.12 kb  #### $arr z占了 几十M

mysql_free_result($r);
echo ‘FREE_R: ‘,convert(memory_get_usage()),"\n";    //620 kb    ### 结果集居然只有0.12 k?这不扯淡么? 莫非。。。莫非缓冲区的数据php统计不到?莫非不是调用zend 内存申请函数来申请内存的?

//sleep(50);  // hold住进程,别销毁,留着看当前进程的内存分配2
function convert($size)
{
$unit=array(‘b‘,‘kb‘,‘mb‘,‘gb‘,‘tb‘,‘pb‘);
return @round($size/pow(1024,($i=floor(log($size,1024)))),2).‘ ‘.$unit[$i];
}
/*
//返回结果如下:
SELECT_DB: 619.26 kb
QUERY_SQL: 619.98 kb
FETCH_RS: 27.11 mb
UNSET: 620.12 kb
FREE_R: 620 kb
*/

看到结果时,我不禁XX一紧,什么?这你妈什么情况?查询完之后,内存大小居然只增加了不到1k?我那个表可是几十M的数据啊?遍历结果集之后,怎么突增几十M啊?尼玛这到底是什么情况?strace返回的大量数据到底存在哪的?算不算php进程申请的?

后来,我再次执行如上程序,再定时用free、/proc/PID/maps 之类系统工具,查看系统的内存使用情况,确认了当前进程的内存占用确实存在。那么可能的情况就是memory_get_usage()函数并没有获取到mysql_query之后的内存占用情况。由于比较怀疑,末学跟进了memory_get_usage()函数的源码,该函数直接交给zend_memory_usage函数处理。

//这个是php的memory_get_usage()函数的 相关代码,见Zend_alloc.c  line:2640
ZEND_API size_t zend_memory_usage(int real_usage TSRMLS_DC)
{
if (real_usage) {
return AG(mm_heap)->real_size;
} else {
size_t usage = AG(mm_heap)->size;
#if ZEND_MM_CACHE
usage -= AG(mm_heap)->cached;
#endif
return usage;
}
}

//这个是Zend内存分配函数的代码
//Zend_alloc.c  line:2418
ZEND_API void *_emalloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
TSRMLS_FETCH();

if (UNEXPECTED(!AG(mm_heap)->use_zend_alloc)) {
return AG(mm_heap)->_malloc(size);
}
return _zend_mm_alloc_int(AG(mm_heap), size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}

php的内存管理 (中文地址:php-zend的内存管理中文版)这块,对于末学来说,太复杂了,只是稍微看懂直接 返回了mm_heap结构体的real_size/size的值。(两篇都是鸟哥写的,中文的地址也就是鸟哥博客最近一直打不开,抽风得厉害)

那mysql_query的结果集,存在哪的呢?如何申请内存的,莫非不是调用zend的_emalloc内存分配函数的?这得先明确mysql客户端类库问题,也就是我们使用哪个类库?libmysql还是mysqlnd,通过查看编译参数,发现(我的虚拟机)是libmysql,编译参数是这样的:

./configure‘  ‘--prefix=/services/php_5.3.19‘ ‘--with-config-file-path=/services/php_5.3.19/etc‘ ‘--with-pdo-mysql=/usr/bin/mysql_config‘ ‘--with-mysql=/usr/bin/mysql_config‘ ‘--with-mysqli=/usr/bin/mysql_config‘ ‘--enable-bcmath‘ ‘--enable-fpm

//生产服务器如下:
./configure‘  ‘--prefix=/services/php‘ ‘--with-config-file-path=/services/php/etc‘ ‘--with-pdo-mysql=mysqlnd‘ ‘--with-mysql=mysqlnd‘ ‘--with-mysqli=mysqlnd‘ ‘--enable-bcmath‘ ‘--enable-fpm

有点乱:
mysql、mysqli、pdo-mysql、libmysql、mysqlnd 好多名词,有点乱,没关系,一张图让你清晰起来:

mysql、mysqli、pdo-mysql、libmysql、mysqlnd之间关系

mysqlnd跟libmysql一样,都是直接与mysql server通讯的驱动类库。 而php程序员使用的mysql、mysqli、pdo-mysql是面向程序员调用的API接口。。

继续:

libmysql类库是MYSQL官方提供的类库,每次PHP编译都是指定参数来确定mysqlmysqlipdo-mysql所使用的连接驱动是哪个。并且,前提你的得先装好mysql的客户端(libmysql类库),以确保有libmysqlclient.so ,

末学抱着试试看的心态,心情沉重的打开了libmysql的源码,终于在Safemalloc.c的line:120附近找到类似libmysqlclient申请内存的代码:

//libmysql客户端库Safemalloc.c  line:120
/* Allocate some memory. */

void *_mymalloc(size_t size, const char *filename, uint lineno, myf MyFlags)
{
  ...
  /*
    Test for memory limit overrun.
    If compiled with DBUG, test for error injection. Described in my_sys.h.
  */
  if ((size + sf_malloc_cur_memory > sf_malloc_mem_limit)
      IF_DBUG(|| my_malloc_error_inject))
  {
    IF_DBUG(if (my_malloc_error_inject)
              errno= ENOMEM;
            my_malloc_error_inject= 0);
    irem= 0;
  }
  else
  {
    /* Allocate the physical memory */
    irem= (struct st_irem *) malloc (ALIGN_SIZE(sizeof(struct st_irem)) +
     sf_malloc_prehunc +
     size + /* size requested */
     4 + /* overrun mark */
     sf_malloc_endhunc);    //系统的内存分配函数 malloc
  }
  ...
  }

//下面是mysqlnd驱动的代码,为了省的再弄一个代码高亮的区块,特意放一起了.
// Mysqlnd客户端库Mysqlnd_alloc.c line:77
/* {{{ _mysqlnd_emalloc */
void * _mysqlnd_emalloc(size_t size MYSQLND_MEM_D)
{
...
ret = _emalloc(REAL_SIZE(size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_ORIG_RELAY_CC);    //调用zend的内存分配函数 _emalloc
...
if (ret && collect_memory_statistics) {
*(size_t *) ret = size;
MYSQLND_INC_GLOBAL_STATISTIC_W_VALUE2(STAT_MEM_EMALLOC_COUNT, 1, STAT_MEM_EMALLOC_AMOUNT, size);
}
TRACE_ALLOC_RETURN(FAKE_PTR(ret));
}
/* }}} */

也就是说,libmysql没有调用zend的内分分配函数_emalloc,就没法将内存的使用情况记录到mm_heap结构体中,也就是PHP的memory_get_usage()函数统计不到的原因。好了,虽然末学不是很能读懂源码,但似乎符合问题发生的现象了。

好像,末学又想到一个问题,如果libmysql保存的结果集所占用的内存的话,那么php的配置文件中的memory_limit也就无法限制他的内存使用情况了?也就是说,如果我们很理想的根据系统剩余内存分配了若干个php-fpm进程来启动运行的话,如果发生这情况,将会出现内存不够用的情况,libmysql占用的内存没有被统计到。。。结果是显然的,果然限制不了它。

libmysql与mysqlnd跟memory_limit之间的关系

那mysqlnd可以吗?mysqlnd的内存分配是使用zend的_emalloc函数吗?是的,没错mysqlnd 是我们的大救星。Mysqlnd_alloc.c line:77里代码中,明确看到了。各位SA在编译php时,一定要使用mysqlnd作为php连接mysql server的类库驱动哦。
Mysqlnd的好处可不止这么一点点啊。

内存还是内存:

末学苦于薄弱的英语,冒死翻过GFW,终于在“万恶的资本主义”国家的网站上找到了这些资料,mysqlnd将比libmysql节省将近40%的内存占用哦。如图:

mysqlnd比libmysql节省40%的内存占用

,而且,memory_limit参数可以管的了它哦…

速度,速度:
国外友人给了一份测试结果,比较的API是mysqlmysqli,比较的驱动是libmysqlmysqlnd

  • 使用mysqlnd驱动的extmysqli接口速度最快
  • 使用libmysql驱动的extmysqli接口慢了6%
  • 使用libmysql驱动的extmysql接口慢了3%

并且给出了mysqli在两个驱动下的执行时间:

mysqli_select_varchar_buffered

还有,还有哦…
mysqlnd还支持各种debug调试哦,各种strace跟踪哦…还支持….算了,你自己下载mysqlnd相比libmysql的优点看吧。末学可是搜了很久才搜到这个ppt。

推荐:

1,再推荐一片关于mysqlnd持久链接的文章:PHP 5.3: Persistent Connections with ext/mysqli

2,你的应用的cache的存储是程序员自己根据DB数据结果,查询条件,hash取值,存到memcache中的吗?想不想尝试下自动实现的?mysqlnd的插件可以尝试下:PHP: Client side caching for all MySQL extensions ,支持memcached,apc,sqlit哦。

回到开始:

有人说,当php调用mysql_query时,mysql server会返回本次查询的结果到php所在服务器的缓冲区中。当程序调用mysql_fetch_assoc/mysql_fetch_row/mysql_fetch_array/mysql_fetch_object之类函数时,都是调用php_mysql_fetch_hash函数去缓冲区读取数据的。我要是用mysql_unbuffered_query()函数呢?让结果集不直接在查询之后返回,当调用mysql_fetch_x函数时,再拉回来呢? 这…你让mysql server的缓冲区来存储这些数据么?你以为客户端就你自己么?其他的客户端也要连的啊,尤其是php,如果用mysql_unbuffered_query()函数,他们都会将结果集放到mysql server的缓冲区的,mysql server的内存占用岂不是成本增长…你想让DBA砍死你?

手册上也说了,mysql_unbuffered_query返回的结果集之上不能使用 mysql_num_rows() 和 mysql_data_seek()。我几乎没用过这个函数,这算非主流的函数么?

有人说我们方案1节省了从结果集取出,遍历赋值给新数组的内存占用,并没有减少网络数据的传输。没错,你说的对,一点都没错。也就是说,我们的解决方案2只能稍微缓解这种问题的负面效果,彻底解决的话,还得程序层上去正确的调用,取回该要的数据。(其实,如果使用mysqlnd驱动的话,我们的改动基本没有优势,节省不了内存。mysqlnd时,结果集的读取只是引用缓冲区的数据。libmysql的话,有明显效果。)我更加鉴定的赞同的那句话“写出这种代码的人都得死”。不使用mysqlnd作为php连接驱动的SA都是耍流氓

结论:
api推荐mysqli,驱动推荐mysqlnd.

温故而知新?

在回家之后,末学刷了几局《保卫萝卜》,除了几个需要养成才解锁的关卡之外,均可耻的”全清”+”金萝卜”,玩着玩着,突然想起一件事情,就是末学在去年写过一篇博客php5.3.8中编译pdo_mysql的艰难历程中,之前运维的编译参数中,mysqli使用的是mysqlnd,而mysql使用的是libmysql,后来再装的pdo-mysql也使用了libmysql了….3个api,指定两个连接驱动,莫非上次的错误是因为这个?而末学的编译参数虽然巧合的解决了问题,当初并没有理解真正的原因?下周验证一下… [2012/12/15 23:31更新]

知耻而后勇?

今天刚写完这篇学习笔记后,回家玩游戏时,想起鸟哥曾提到过mysqlnd,再次回去看看,看鸟哥如何讲解mysqlnd的,我理解的是否有误,才发现鸟哥这里已经有了个Ulf Wendel博客的链接,末学却在网络搜索N久才找到那篇文章,同时,发现其blog上有大量mysqlnd的文章,还暗自偷笑,以为自己发现了大金矿,现在才发现….哎,惭愧惭愧…[2012/12/15 23:58更新]

末学对于本次学习经历中遇到的知识点,有大量的盲区,将会在以后的时间里,慢慢摸索熟悉,也欢迎各位前辈的点拨。

原文:

http://blog.jobbole.com/38762/

时间: 2024-10-08 08:15:56

PHP与MySQL通讯那点事的相关文章

MySQL 通讯协议

Client/Server 通讯协议用于客户端链接.代理.主备复制等,支持 SSL.压缩,在链接阶段进行认证,在执行命令时可以支持 Prepared Statements 以及 Stored Procedures . 当打算编写数据库代理.中间件.对 MySQL 数据包进行审核时,都需要了解底层的通信协议.在本文中,主要介绍 MySQL 通讯协议相关的内容. 简介 服务器启动后,会使用 TCP 监听一个本地端口,当客户端的连接请求到达时,就会执行三段握手以及 MySQL 的权限验证:验证成功后,

MySQL字符集这点事

今天,一开发同事使用jdbc连接数据库执行一条语句无结果集,但是通过sqlyou执行相同的语句有返回结果. 执行的语句where条件中含有中文,这应该是字符集引起的 查看当前数据库的字符集 mysql> show variables like '%charac%'; +--------------------------+----------------------------------+ | Variable_name | Value | +-------------------------

信息系统实践手记8-两模块通讯的一些事

说明:信息系统实践手记系列是系笔者在平时研发中先后遇到的大小的问题,也许朴实和细微,但往往却是经常遇到的问题.笔者对其中比较典型的加以收集,描述,归纳和分享. 摘要:此文描述了笔者接触过的部分信息系统或平台之间的对接构型和情况,挂一漏万的总结分享之. 正文 系列随笔目录:信息系统实践手记 (http://www.cnblogs.com/taichu/p/5305603.html) 作者:太初 转载说明:请指明原作者,连接,及出处. 正文 在信息系统开发和对接的过程中,免不了要遇到两个模块之间的消

PHP API中,MYSQL与MYSQLI的持久连接区别

留存再测试验证 PHP API中,MYSQL与MYSQLI的持久连接区... 很久很久以前,我也是因为工作上的bug,研究了php mysql client的连接驱动mysqlnd 与libmysql之间的区别php与mysql通讯那点事,这次又遇到一件跟他们有联系的事情,mysqli与mysql持久链接的区别.写出这篇文章,用了好一个多月,其一是我太懒了,其二是工作也比较忙.最近才能腾出时间,来做这些事情.每次做总结,都要认真阅读源码,理解含义,测试验证,来确认这些细节.而每一个步骤都需要花费

mysql协议分析2---认证包

主人看到navicat和mysql在那嘻嘻哈哈,眉来眼去的,好不快乐,忽然也想自己写个程序,直接去访问Mysql,虽然现在已经有很多现成的中间件可以直接拿来用了,程序只要负责写sql语句就行了,但是主人想要自己通过mysql协议直接和mysql通讯,一窥究竟.于是主人找到Mysql说:亲爱的mysql,我以前和你交流总要通过第三方的驱动在中间传话,总感觉我们之间还有一个隔阂,有些话也不方便说,我现在有些心里话想直接和你交流...你说行吗? mysql说:当然行啊,mysql受宠若惊,要和我打交道

mysql基础补充

前言 本篇的主要内容为整理mysql的基础内容,分享的同时方便日后查阅,同时结合python的学习整理python操作mysql的方法以及python的ORM. 一.数据库初探 在开始mysql之前先了解下什么是数据库: 数据库(Database)是按照数据结构来组织.存储和管理数据的仓库,它产生于距今六十多年前,随着信息技术和市场的发展,特别是二十世纪九十年代以后,数据管理不再仅仅是存储和管理数据,而转变成用户所需要的各种数据管理的方式.数据库有很多种类型,从最简单的存储有各种数据的表格到能够

Atlas+keepalived实现mysql读写分离

本文主要介绍atlas安装.mysql读写分离的配置.mysql主从配置及利用keepalived解决atlas单点故障. Atlas介绍 Atlas是基于mysql-proxy做的二次开发.Atlas是一个位于应用程序与MySQL之间中间件.在后端DB看来,Atlas相当于连接它的客户端,在前端应用看来,Atlas相当于一个DB.Atlas作为服务端与应用程序通讯,它实现了MySQL的客户端和服务端协议,同时作为客户端与MySQL通讯.它对应用程序屏蔽了DB的细节,同时为了降低MySQL负担,

MySQL网上资源

博客 xiaohuazi http://www.cnblogs.com/MYSQLZOUQI/p/3994166.html?utm_source=tuicool 姜承尧 http://insidemysql.blog.163.com/ 姜承尧的 innomysql博客 http://www.innomysql.net/ 何登成 http://hedengcheng.com/ 淘宝MySQL数据库组网站 http://mysql.taobao.org/index.php?title=%E9%A6%

mysql 中间件研究 (Atlas,cobar,TDDL)

mysql中间件研究(Atlas,cobar,TDDL) mysql-proxy是官方提供的mysql中间件产品可以实现负载平衡,读写分离,failover等,但其不支持大数据量的分库分表且性能较差.下面介绍几款能代替其的mysql开源中间件产品,Atlas,cobar,tddl,让我们看看它们各自有些什么优点和新特性吧. Atlas Atlas是由 Qihoo 360, Web平台部基础架构团队开发维护的一个基于MySQL协议的数据中间层项目.它是在mysql-proxy 0.8.2版本的基础