Heim  >  Artikel  >  Datenbank  >  php与mysql通讯那点事

php与mysql通讯那点事

WBOY
WBOYOriginal
2016-06-07 16:34:19944Durchsuche

BUG、BUG,似乎末学的学习历程都是因为BUG引起的: 在我们的一款WebGame的生产环境中,一次无意的strace抓包时,发现了php与mysql大量通讯的数据。这种情况,在游戏服务器刚启动时,是正常的,但如果是运行一段时间之后,出现大量SELECT的SQL查询,绝对是有

BUG、BUG,似乎末学的学习历程都是因为BUG引起的:
在我们的一款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++_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之间关系

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


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

继续:

libmysql类库是MYSQL官方提供的类库,每次PHP编译都是指定参数来确定mysql\mysqli\pdo-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之间的关系

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%的内存占用

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

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

速度,速度:
国外友人给了一份测试结果,比较的API是mysql\mysqli,比较的驱动是libmysql\mysqlnd

  • 使用mysqlnd驱动的ext\mysqli接口速度最快
  • 使用libmysql驱动的ext\mysqli接口慢了6%
  • 使用libmysql驱动的ext\mysql接口慢了3%

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

mysqli_select_varchar_buffered

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更新]

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

好像…好像…末学的问题太多了…

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Vorheriger Artikel:高性能MySQLNächster Artikel:实现图书的增删查改(CRUD)