转载:http://mp.weixin.qq.com/s?__biz=MjM5NDMwNjMzNA==&mid=204189034&idx=1&sn=73f71a4af3dfd625d8b662290a82091a&scene=2&from=timeline&isappinstalled=0&key=b2574200810f04e80d9ee5e073181c50f9c0f7a0e374521a9ec44a25fc30420b78d3f57fd5bcf8d04b1335cf9b2089c0&ascene=2&uin=OTUwODIwNTYw&devicetype=android-19&version=26010049&nettype=WIFI&pass_ticket=eMX%2F3miIbznwbCaEf%2F%2FVE2P75QbHsWljmnHjzQqE0az%2B8AINTRRm4faGpTcd2qjh

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

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

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

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

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

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

640?tp=webp&wxfrom=5
我们可以看到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给出如下改进:

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

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

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

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

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

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

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

640?tp=webp&wxfrom=5
php的内存管理 (中文地址:php-zend的内存管理中文版)这块,对于末学来说,太复杂了,只是稍微看懂直接 返回了mm_heap结构体的real_size/size的值。(两篇都是鸟哥写的)

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

640?tp=webp&wxfrom=5
有点乱:
mysql、mysqli、pdo-mysql、libmysql、mysqlnd 好多名词,有点乱,没关系,一张图让你清晰起来:

640?tp=webp&wxfrom=5


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申请内存的代码:

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

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

640?tp=webp&wxfrom=5


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

内存还是内存:

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

640?tp=webp&wxfrom=5

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


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

  • 使用mysqlnd驱动的extmysqli接口速度最快

  • 使用libmysql驱动的extmysqli接口慢了6%

  • 使用libmysql驱动的extmysql接口慢了3%

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

640?tp=webp&wxfrom=5

还有,还有哦…
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哦。

via:CFC4N