php与mysql通讯那点事 BUG、BUG,似乎末学的学习历程都是因为BUG引起的: 在我们的一款WebGame的生产环境中,一次无意的strace抓包时,发现了php与mysql大量通讯的数据。这种情况,在游戏服务器刚启动时,是正常的,但如果是运行一段时间之后,出现大量SELECT的SQL查询,绝对是有问题的,而且,所操作的数据库并不是配置库,那意味着,我们程序员的程序出现了违规的操作。具体结果大约如下: 如上图所示,php持续接收读取进程内描述符为3的响应包数据,描述符为3的为php与mysql建立的TCP通讯链接,这点也可以从313行的SELECT语句来确认。(原始数据丢失了,我模仿了一条。所以是配置库的SQL语句) 这是什么程序,想实现什么逻辑?为何要取这么多数据? 跟着这里的SELECT的sql语句,我定位到了相应的程序段: 我们从代码上来看,好像明白程序员想根据对应的role_id到role_items表里取一条想符合的数据,所以,他调用了row方法,来取一条。看上去,这里好像正常,我们都以为框架会给我们只取一条。但实际上,框架是如何处理的呢? 我们来看下框架的对应row方法的实现过程。对了,我们是CodeIgniter框架的一个较老的版本。 我们可以看到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给出如下改进: 在今年的4月末,鄙人写过另一篇关于CodeIgniter框架的设计缺陷问题,给我们游戏项目带来较大的影响,后来提交到github issues,并没得到回复,想了想,虽然官方的2.1.3版本中,也存在这个小问题。不过我觉得,这就不提交了,或许,我们的做法也符合他们的设计初衷。不过,我们还是在我们的项目中改进了。 如此改进之后,我们使用php的memory_get_usage()函数观察前后两个row()方法的结果时,果然发现内存使用情况有较大改善(改善幅度取决于SELECT的返回数据量)。 似乎,到这里就应该结束了,问题就这么被发现,被解决了。 但,我总觉得少了些什么呢?当我再次strace抓包时,发现仍然存在大量的数据通讯,就像文章开头的那副截图一模一样。然而,这又是什么原因呢? 我顺手写了个内存占用的测试代码如下: 看到结果时,我不禁XX一紧,什么?这你妈什么情况?查询完之后,内存大小居然只增加了不到1k?我那个表可是几十M的数据啊?遍历结果集之后,怎么突增几十M啊?尼玛这到底是什么情况?strace返回的大量数据到底存在哪的?算不算php进程申请的? 后来,我再次执行如上程序,再定时用free、/proc/PID/maps 之类系统工具,查看系统的内存使用情况,确认了当前进程的内存占用确实存在。那么可能的情况就是memory_get_usage()函数并没有获取到mysql_query之后的内存占用情况。由于比较怀疑,末学跟进了memory_get_usage()函数的源码,该函数直接交给zend_memory_usage函数处理。 php的内存管理 (中文地址:php-zend的内存管理中文版)这块,对于末学来说,太复杂了,只是稍微看懂直接 返回了mm_heap结构体的real_size/size的值。(两篇都是鸟哥写的,中文的地址也就是鸟哥博客最近一直打不开,抽风得厉害) 那mysql_query的结果集,存在哪的呢?如何申请内存的,莫非不是调用zend的_emalloc内存分配函数的?这得先明确mysql客户端类库问题,也就是我们使用哪个类库?libmysql还是mysqlnd,通过查看编译参数,发现(我的虚拟机)是libmysql,编译参数是这样的 有点乱: 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没有调用zend的内分分配函数_emalloc,就没法将内存的使用情况记录到mm_heap结构体中,也就是PHP的memory_get_usage()函数统计不到的原因。好了,虽然末学不是很能读懂源码,但似乎符合问题发生的现象了。 好像,末学又想到一个问题,如果libmysql保存的结果集所占用的内存的话,那么php的配置文件中的memory_limit也就无法限制他的内存使用情况了?也就是说,如果我们很理想的根据系统剩余内存分配了若干个php-fpm进程来启动运行的话,如果发生这情况,将会出现内存不够用的情况,libmysql占用的内存没有被统计到。。。结果是显然的,果然限制不了它。 那mysqlnd可以吗?mysqlnd的内存分配是使用zend的_emalloc函数吗?是的,没错mysqlnd 是我们的大救星。Mysqlnd_alloc.c line:77里代码中,明确看到了。各位SA在编译php时,一定要使用mysqlnd作为php连接mysql server的类库驱动哦。 Mysqlnd的好处可不止这么一点点啊。 内存还是内存: 末学苦于薄弱的英语,冒死翻过GFW,终于在“万恶的资本主义”国家的网站上找到了这些资料,mysqlnd将比libmysql节省将近40%的内存占用哦。如图: ,而且,memory_limit参数可以管的了它哦... 速度,速度: 国外友人给了一份测试结果,比较的API是mysql\mysqli,比较的驱动是libmysql\mysqlnd 使用mysqlnd驱动的ext\mysqli接口速度最快 使用libmysql驱动的ext\mysqli接口慢了6% 使用libmysql驱动的ext\mysql接口慢了3% 并且给出了mysqli在两个驱动下的执行时间: 还有,还有哦... 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更新] 末学对于本次学习经历中遇到的知识点,有大量的盲区,将会在以后的时间里,慢慢摸索熟悉,也欢迎各位前辈的点拨。 好像...好像...末学的问题太多了... 2012-12-15 +4 8 0
CodeIgniter框架中关于DB事务处理的设计缺陷 起因: 在我们线上的某个业务中,使用较老版本的CodeIgniter框架,其中的DB类中,对DB事物处理部分存在着一个设计上的缺陷,或许也算不上缺陷吧。但他却影响了我们生产环境,导致连锁反应。对业务产生较大影响,且不容易排查。这个问题,我在今年的3月中旬,曾向http://codeigniter.org.cn/的站长Hex 报告过,之后,我也忘记这件事情了。直到今天,我们线上业务又一次以为这个问题,害的我又排查一次。具体原因,各位且先听我慢慢说完。(这个问题同样存在于最新版本Version 2.1.0中) 分析: 以CodeIgniter框架Version 2.1.0为例,在system\database\DB_driver.php的CI_DB_driver类中第58行有个$_trans_status属性。 同时,这个类的query方法中,有赋值此属性的代码,见文件306、307行 这里也给了注释,告诉我们,如果使用了事物处理,那么这属性将成为一个回滚的决定条件。 在520行的事物提交方法trans_complete中,如下代码 在535行中,如果_trans_status属性如果是false,那么将发生回滚,并且返回false。 在我们的业务代码中,由于程序员疏忽,没有判断trans_complete()方法是否正确执行,直接告诉用户操作成功,但实际上,程序已经向DB下达回滚指令,并未成功更新DB记录。当用户执行下一步操作时,程序又发现相应记录并未更新,又提醒用户上个操作没有完成,通知用户重新执行。如此反复... 排查的过程,也是挺有意思的,起初从PHP代码中,总是不能确定问题所在,并没有把焦点放到trans_complete()方法的返回上。直到后来strace抓包分析,才知道是因为此属性而导致了回滚。 可以看到,在22:54:08.380085时间点处,发送更新SQL语句指令,在22:54:08.380089时间读取返回结果,得到SQL执行错误,不存在字段"cfc4n_user_lock";22:54:08.381791和22:54:08.382703两个时间点,PHP发送停止“自动提交”与“开始事务处理”指令,在 22:54:08.433954 发送“事务回滚”指令。 配合如上的代码分析,可以清楚的知道,因为“UPDATE `cfc4n_user_info` SET `cfc4n_user_lock` = 1 WHERE `cfc4n_user_id` = '6154' AND `cfc4n_user_lock` = 0”这句SQL执行错误,导致$_trans_status属性被设置为FALSE,当代码提交事务时,被trans_complete()方法判断,认为“上一个事务处理”(下面将仔细分析)中存在SQL语句执行失败,决定回滚事务,不提交。 刚刚提到“上一个事务处理”,可能有些朋友不能理解,我们先继续回到代码中来,继续看该属性,同样在trans_complete方法中,542-545行: 也可以很容易的从注释中看明白,设置CI的设计者,为了更严谨的处理 同一个脚本中,存在多个事务时,事务间彼此关系重要,一荣俱荣,一损俱损。这里的trans_strict属性,是个开关,当 trans_strict为false,便是非严格模式,意味着多个事务之间,彼此关系不重要,不影响。当前一个事务中有SQL语句执行失败,影响不到自己。便将_trans_status 设置为TRUE。 毫无疑问,这是个非常周全的考虑。考虑了多个事务之间的关系,保证业务跑在更严谨的代码上。 可是,我们的代码中,错误的SQL语句是执行在事务处理以外的,并不是事务之内。按照我们对事务的认识,可以很清晰的知道,事务之外的SQL相比事务之内的SQL来说,事务之内的SQL更重要,之外的可以允许出错,但事务之内的,务必要正确,不受外界干扰。但CI的框架中,因为一个事务以外的语句执行失败,却导致整个事务回滚...当然,我们的程序员没有对事务提交方法的返回做判断,这也是个问题。 问题已经很清晰了,那么解决方法想必对你来说,是多么的简单。 比如在trans_start方法中,对_trans_status 属性赋值,设置为TRUE,不理会事务外的问题。 结束: 在不明白对方设计意图的情况下,不能盲目的定义对方的代码评价,不管程序作者的水平如何。比自己强,也不能盲目崇拜;比自己弱,更不能乱加指责;理解读懂设计意图,学习他人优秀的设计思路、代码风格、算法效率,这才是一个好习惯。当然codeigniter框架是优秀的。 2012-04-30 +3 7 0