PHP API中,MYSQL与MYSQLI的持久连接区别 很久很久以前,我也是因为工作上的bug,研究了php mysql client的连接驱动mysqlnd 与libmysql之间的区别php与mysql通讯那点事,这次又遇到一件跟他们有联系的事情,mysqli与mysql持久链接的区别。写出这篇文章,用了好一个多月,其一是我太懒了,其二是工作也比较忙。最近才能腾出时间,来做这些事情。每次做总结,都要认真阅读源码,理解含义,测试验证,来确认这些细节。而每一个步骤都需要花费很长的时间,而且,还不能被打断。一旦被打断了,都需要很长时间去温习上下文。也故意强迫自己写这篇总结,改改自己的惰性。 在我和我的小伙伴们如火如荼的开发、测试时发生了“mysql server too many connections”的错误,稍微排查了一下,发现是php后台进程建立了大量的链接,而没有关闭。服务器环境大约如下php5.3.x 、mysqli API、mysqlnd 驱动。代码情况是这样: 从这个后台进程代码中,可以看出“$Property”变量以及“$Mounts”变量频繁被创建,销毁。而ROLE对象的getModule方法是这样写的 可以看出getModule方法只是模拟单例,new了一个新对象返回,而他们都继承了Afx_Module_Abstract类。Afx_Module_Abstract类大约代码如下: 类Afx_Module_Abstract中关键代码如上,跟DB相关的,就setAdapter一个方法,回到“后台进程A”,setAdapter方法是将Afx_Db_Factory::DbDriver($config['mysql']['driver'])的返回,作为参数传了进来。继续看下Afx_Db_Factory类的代码 一看就知道是个工厂类,继续看真正的DB Adapter部分代码 从上面的代码可以看到,我们已经启用长链接了啊,为何频繁建立了这么多链接呢?为了模拟重现这个问题,我在本地开发环境进行测试,无论如何也重现不了,对比了下环境,我的开发环境是windows7、php5.3.x、mysql、libmysql,跟服务器上的不一致,问题很可能出现在mysql跟mysqli的API上,或者是libmysql跟mysqlnd的问题上。为此,我又小心翼翼的翻开PHP源码(5.3.x最新的),终于功夫不负有心人,找到了这些问题的原因。 从mysql_pconnect的代码中,可以看到,当php拓展mysql api与mysql server建立TCP链接后,就立刻将这个链接存入persistent_list中,下次建立链接是,会先从persistent_list里查找是否存在同IP、PORT、USER、PASS、CLIENT_FLAGS的链接,存在则用它,不存在则新建。 而php的mysqli拓展中,不光用了一个persistent_list来存储链接,还用了一个free_link来存储当前空闲的TCP链接。当查找时,还会判断是否在空闲的free_link链表中存在,存在了才使用这个TCP链接。而在mysqli_closez之后或者RSHUTDOWN后,才将这个链接push到free_links中。(mysqli会查找同IP,PORT、USER、PASS、DBNAME、SOCKET来作为同一标识,跟mysql不同的是,没了CLIENT,多了DBNAME跟SOCKET,而且IP还包括长连接标识“p”) MYSQLI为什么要这么做?为什么同一个长连接不能在同一个脚本中复用? 在C函数mysqli_common_connect中看到了有个mysqli_change_user_silent的调用,如上代码,mysqli_change_user_silent对应这libmysql的mysql_change_user或mysqlnd的mysqlnd_change_user_ex,他们都是调用了C API的mysql_change_user来清理当前TCP链接的一些临时的会话变量,未完整写的提交回滚指令,锁表指令,临时表解锁等等(这些指令,都是mysql server自己决定完成,不是php 的mysqli 判断已发送的sql指令然后做响应决定),见手册的说明The mysqli Extension and Persistent Connections。这种设计,是为了这个新特性,而mysql拓展,不支持这个功能。 从这些代码的浅薄里理解上来看,可以理解mysqli跟mysql的持久链接的区别了,这个问题,可能大家理解起来比较吃力,我后来搜了下,也发现了一个因为这个原因带来的疑惑,大家看这个案例,可能理解起来就非常容易了。Mysqli persistent connect doesn't work回答者没具体到mysqli底层实现,实际上也是这个原因。 代码如下: 查看进程列表里是这样的结果: 这样看代码,就清晰多了,验证我的理解对不对也比较简单,这么一改就看出来了 如果你担心被close掉了,这是新建的TCP链接,那么你可以打印下thread id,看看是不是同一个ID,就清楚了。(虽然我没回复这个帖子,但不能证明我很坏。)以上是CLI模式时的情况。在FPM模式下时,每个页面请求都会由单个fpm子进程处理。这个子进程将负责维护php与mysql server建立的长链接,故当你多次访问此页面,来确认是不是同一个thread id时,可能会分别分发给其他fpm子进程处理,导致看到的结果不一样。但最终,每个fpm子进程都会分别维持这些TCP链接。 总体来说,mysqli拓展跟mysql拓展的区别是下面几条 持久链接建立方式,mysqli是在host前面增加“p:”两个字符;mysql使用mysql_pconnect函数;。 mysqli建立的持久链接,必须在mysqli_close之后,才会下面的代码复用,或者RSHOTDOWN之后,被下一个请求复用;mysql的长连接,可以立刻被复用 mysqli建立持久链接时,会自动清理上一个会话变量、回滚事务、表解锁、释放锁等操作;mysql不会。 mysqli判断是否为同一持久链接标识是IP,PORT、USER、PASS、DBNAME、SOCKET;mysql是IP、PORT、USER、PASS、CLIENT_FLAGS 好了,知道这个原因,那我们文章开头提到的问题就好解决了,大家肯定第一个想到的是在类似Property的类中,__destruct析构函数中增加一个mysqli_close方法,当被销毁时,就调用关闭函数,把持久链接push到free_links里。如果你这么想,我只能恭喜你,答错了,最好的解决方案就是压根不让它创建这么多次。同事dietoad同学给了个解决方案,对DB ADAPTER最真正单例,并且,可选是否新创建链接。如下代码: 看来,开发环境跟运行环境一致是多么的重要,否则就不会遇到这些问题了。不过,如果没遇到这么有意思的问题,岂不是太可惜了? (图:天冬) 2013-08-15 +2 6 0
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
webgame中Mysql Deadlock ERROR 1213 (40001)错误的排查历程 案例发现: 从我们正在运营的一款webgame的异常日志中看到一些程序执行MYSQL 语句的报错信息。比较多的是“Deadlock found when trying to get lock; try restarting transaction”,少部分是“Error number: 1205:Lock wait timeout exceeded; try restarting transaction”,如下: 从报错的英文上理解,大约是发生了“死锁”,以及“事务锁等待超时”两个错误异常。而且,都是我们后台PHP常驻进程遇到的问题。异常的代码对应行数上,大约可理解为执行SQL语句的一个指令,并无特殊的东西。有经验的程序员,很容易看出来,这不是程序的异常,这是MYSQL事务中,锁竞争的异常,客户端(PHP常驻进程)是没有语法上的错误的。那该如何排查呢? 一串疑问: 这是什么问题?如何排查?什么时候发生死锁? 我怎么知道他发生了? 发生之后去哪里排查?如何排查?怎么确定他们对应的事务中的所有SQL语句? 分别在哪几个事务中? 谁先锁的?谁后锁的?谁没锁到?谁报的死锁错误? 死锁是什么?为什么发生了? 如何避免?还有哪些因素影响? 毫无头绪: 程序间数据交互,上strace神器? 跟踪谁? 客户端(php)?你知道哪个客户端会发生这个问题?你知道啥时候会发生?在你开始抓包到抓到死锁的期间,这得是多大的数据量? 跟踪谁? 服务端(Mysql)?玩笑开大了吧?mysql以进程模式来处理客户端请求,每次都是一个新的进程,strace -ff参数的话,想想日志文件得被创建多少个,数据量会小么? "万军之中取上将首级"这本事我可没...strace排查这种错误,还是算了吧。 这是谁报的错?显然是mysql,那就从根源找起--MYSQL server。 抓获现场: 我们要还原案发现场,有幸的时,我们有监控记录BINLOG以及SHOW ENGINE INNODB STATUS。到对应mysql服务器上,执行“show engine innodb status”获取INNODB引擎当前信息,大约如下: 这是我精简之后的信息,我抓去了LATEST DETECTED DEADLOCK部分的数据,这部分的数据是INNODB的最后一次发生死锁的信息,更详细的说明见MYSQL官方手册对Standard Monitor and Lock Monitor Output返回结果的解释。 OK,发现一场案例,保存这个INNODB的状态数据备用。迅速到程序异常日志中查看相同时间点是否有死锁发生。果然,我们程序异常日志中记录了这起案例(文章开头的日志)。 再到binlog中抓去这个时间段前后10分钟(大约范围)的mysql sql语句执行日志。 案情分析: engine status中,大约看出MYSQL记录了两个事务之间发生锁竞争时,遗留的数据, 事务1“执行”(注意,这里加了双引号) 发现被修改资源已经被lock_mode X locks了(详情见:INNODB锁模式),准备等待该资源锁被释放。 事务2执行 也发现该资源被lock_mode X locks了。 最后部分,mysql给了很重要的一个数据“WE ROLL BACK TRANSACTION (1)” MYSQL回滚了事物1。既然mysql回滚了1,那么肯定是事务1的语句触发了死锁,被mysql回滚了,也就是应该为程序中的异常日志所记录的那部分。同时,MYSQL执行了事务2,那么事务2的SQL语句肯定被记录在BINLOG中了。 抽丝剥茧: 如何确定事务1、事务2执行了哪些SQL语句呢? 根据show engine innodb status的结果,确定事务2被执行的 SQL语句(业务逻辑的role_id唯一标识): REPLACE INTO `role_fight` (`role_id`, `life_max`, `mana_max`, `attack_physical`, `attack_internal`,****) VALUES ('53016', 4967, 3291, 350, 174, ***) 线程ID(mysql的唯一标识): MySQL thread id 122679850 执行时间(时间线):121128 15:59:46 根据这三个标识,以及BINLOG的起始表示“BEGIN、COMMIT”,几乎可以100%确定该事务所包含的SQL语句。 BINLOG信息大约如下: OK,事务2的SQL语句全部找齐了。那么事务1的呢?如何找? 根据php的异常报错,确定主要包含的语句SQL信息,以及程序跟踪的代码行数,根据代码逻辑去确定该事务的所有SQL语句。再去BINLOG中找到该用户的该业务的类似BINLOG: (请勿过于纠结上面binlog的thread_id跟show engine innodb status的thread_id一致的问题,这是因为我们程序是常驻进程,mysql连接不断开,不销毁,故一致了。而且,此日志是程序发现死锁之后,被mysql回滚之后,又重新提交的事务,算是不同时间点的相同事务) 案情还原: 根据案发现场的两个MYSQL INNODB事务的全部SQL语句,以及形成MYSQL INNODB 死锁的原因(感谢DBA组大雄哥的纠正),我们大约可以这么还原案情: 事务1: UPDATE `role_fight` SET `last_update_life` = '1354089587' WHERE `role_id` = '53016' UPDATE `role_state` SET `state` = 1 WHERE `role_id` = '53016' 事务2: UPDATE `role_state` SET `pet` = 0, `pet_level` = 0 WHERE `role_id` = '53016' REPLACE INTO `role_fight` (`role_id`, `life_max`, `mana_max`, `attack_physical`, `attack_internal`,****) VALUES ('53016', 4967, 3291, 350, 174, ***) 这四条语句构成了本次事务死锁的全部原因。 执行顺序肯定如下: 时间点 事务1 事务2 备注 1 begin 2 begin 3 UPDATE `role_state` SET `pet` = 0, `pet_level` = 0 WHERE `role_id` = '53016' 事务2 给 role_state表 role_id 53016记录上 X 锁 4 UPDATE `role_fight` SET `last_update_life` = '1354089587' WHERE `role_id` = '53016' 事务1 给 role_fight表 role_id 53016记录上 X 锁 5 REPLACE INTO `role_fight` (`role_id`, `life_max`, `mana_max`, `attack_physical`, `attack_internal`,****) VALUES ('53016', 4967, 3291, 350, 174, ***) 这里是重点,事务2给role_fight表role_id的记录上 X 锁,发现被其他人(事务1)上锁了,开始等待他人提交事务...等待... 6 UPDATE `role_state` SET `state` = 1 WHERE `role_id` = '53016' 事物1打算给role_state表role_id为53016记录上 X 排它锁,发现被其他事务上了,而且此事务居然还在等他提交,这时MYSQL立刻回滚事务1...(php发现MYSQL返回死锁信息,随记录该信息到异常日志...发送回滚指令...mysql已经“帮”他回滚了...) 7 【执行成功...】 事务2发现别人释放锁了,OK,获取X锁,修改成功 8 commit PHP程序发现上一条指令执行完毕,且执行无错,即,发送commit指令,提交事务。 好像有个参数%^&#: innodb_lock_wait_timeout参数是干啥的呢?从mysql官方手册上看,此参数是针对锁等待时,一个限定等待时间的参数。跟死锁并无关系,一旦mysql发现死锁,立刻回滚导致死锁的语句。并不会用到该参数。 规避方式: 缩小事务的语句数量 调整SQL语句执行顺序,变“死锁”为“锁等待”,等待一会,总比整个事务回滚掉,全部重新再执行这个流程要强. 其他.请补充 关于锁等待: 缩小事务间SQL语句的数量,减小规模吧。当然,提高检索速度,提高查询时间也是首要因素,我们就发现我们的SQL语句中,有几个没有用到索引,导致锁表,导致锁等待发生... 备注: 年底了,冲KPI的,各位见笑了. 2012-12-04 +2 6 0
nginx、php-fpm默认配置与性能–TCP socket还是unix domain socket 前几天看到一篇博客,提到php所在服务器在大并发情况下,频繁创建TCP短连接,而其所在服务器的2MSL时间过长,导致没有端口可用,系统无法创建TCP socket,而大量报错。博主在后面给的解决方案是减少2MSL的时间,尽快清除TIME_WAIT状态的TCP连接,回收端口。同时,文章结尾写了不用长连接的理由,但这真的是最好的解决办法吗?有其他办法可以更好的做法吗? 类似经历 之所以多这篇文章兴趣这么高,是因为在前段时间,末学也经历了一件类似的优化历程,先简短的描述下我们的服务器架构。如图 想必看到这幅架构图的同学,都比较熟悉吧,也比较简单。“最前面”的nginx 反代负责与玩家的http请求通讯,这里是长连接。在其与后端游戏大区通讯时,使用了短连接,也就是意味着,每处理用户的一个http请求,都要重新与后端的nginx建立一次TCP(http)请求,后端nginx处理完之后就关闭。后端的nginx与php在同一台服务器上,通讯配置如下: 在这个架构图中,反代承担这承上启下的作用,前面是用户,后面是web app服务器,长连接用户,短连接后端。若用户数在1W人,1W人与反代的连接,在反代服务器上都用80端口。但这1W人的每一个请求,反代都要重新使用服务器的端口与后端nginx的80创建tcp socket,来处理请求。若2MSL时间为1分钟以上,同时,玩家两次请求间隔很短,想我们游戏的服务器,两次请求间隔大约5秒(甚至2-3秒)。那么反代的端口,5秒用1W个,10秒2W,15秒3W,20秒4W,25秒5W,30秒6W!!!,前面已经用掉的端口还没被回收掉。那么将会有很多的用户请求被nginx反代接收之后,因没有端口资源而无法与后端nginx创建tcp连接。。。以游戏的其中一个大区为例: 关于这个问题,末学在新浪微博上请教过其他大牛,其中tengine负责人淘叔度前辈以及@120斤的大青蛙前辈告诉我,nginx将在1.1.4版本开始支持ngx_http_upstream_keepalive,3月1号左右,是1.1.16 dev版本,我们的运维团队稍微由于几天,之后在部分小区的服务器上适用了1.1.16,效果不错,Nginx反代上与目标服务器的80端口的TCP连接中处于ESTABLISHED状态的非常多,TIME_WAIT的几乎没有,很稳定。我们几乎计划为了这个性能的提升,决定用dev版了。接着nginx1.2Stable version也出来了,那么也就用这个版本了。至于反代上nginx与后端app server建立TCP连接个数,也就是端口数(满载情况下,维持ESTABLISHED状态)的为nginx worker_processes * ((upstream1 server * keepalive参数数量) + (upstream2 server * keepalive参数数量) + ...),并不多。 如上,端口占用数少了,那么还有其他有点吗?先看一副 socket创建,监听、数据接收发送、关闭与客户端连接的socket流程图: 如上图,nginx反代跟后端nginx的形式跟php-fpm与mysqld socket的模型一样的。 nginx反代跟php-fpm 相对来说,就是客户端,这种做法,他们的每个新socket 请求的发起,都会跟着左边的流程走一遍,一遍又一遍。。。。每次的socket创建,端口等资源申请,与服务端的三次握手,关闭时的四次握手,端口的回收。。。。 改用长连接之后,如下图: 只有php-fpm(或者nginx反代)的子进程分别与mysqld创建1次TCP连接,之后都是send、recv数据的事情了,端口占用也是为数不多的几个(每个fpm子进程将与mysqld维持一个TCP长连接)。 同样,后端服务器的nginx与php-fpm的通讯也是如此,只是请求两没有反代那么大。区别就是IP地址是回环地址127.0.0.1。 既然是回环地址,那么两个服务都是在同一台机器上跑的,既然是同一台机器,为何不用进程间通讯的socket--unix domain socket呢? socket是神马?摘抄一段描述: Socket 可以被定义描述为两个应用通信通道的端点。一个 Socket 端点可以用 Socket 地址来描述, Socket 地址结构由 IP 地址,端口和使用协议组成( TCP or UDP )。http协议可以通过socket实现,socket在传输层上实现。从这个角度来说,socket介于应用层和传输层之间。但是socket作为一种进程通信机制,操作系统分配唯一一个socket号,是依赖于通信协议的,但是这个通信协议不仅仅是 tcp或udp,也可以是其它协议。 在同一台服务器上,用tcp socket与unix domain socket有什么区别? 如图所示,对于进程间通讯的两个程序,unix domain socket的流程不会走到TCP 那层,直接以文件形式,以stream socket通讯。如果是TCP socket,则需要走到IP层。 对于非同一台服务器上,TCP socket走的就更多了。 至于localhost\127.0.0.1以及网络IP他们之间的区别,无意中找到一篇博客写的是以mysql作为验证,来说明localhost不走TCP/IP层,跟127.0.0.1不一样。末学认为他理解错了。他的理由如下 (以下截图均在linux上,windows的没有unix domain socket) mysql连接本机时,不加-h参数: mysql连接本机时,加-h参数且值是localhost: mysql连接本机时,加-h参数且值是127.0.0.1: 那位同学从mysql工具的使用方法、与结果的区别,来理解推导localhost与127.0.0.1的区别,这从方向上就存在问题,我更相信,这是mysql这个程序自己的行为,遇到-h参数没加,或者-h参数的值不是IP形式,且my.cnf里指定mysql的socket路径时,则直接使用unix domain socket来连接服务器,当然,这也是我的猜测,没有去验证,大家听听就好,别相信。 鉴于末学对以上的理解,将服务器的架构配置变更如下 至此,优化还为完毕,若php-fpm与mysql使用mysql_pconnect的话,那么php-fpm的子进程生成模式最好用static模式,若为dynamic模式,可能会出现mysql连接数被占满的情况,这也跟mysql服务的连接超时时间有关,适当调整也容易避免。 不过,我们目前还没用mysql_pconnect,主要原因是我们的代码中,有些事务处理开启之后,对于代码的失败处理,忘记写回滚语句,在短连接的情况下,这个连接的销毁,哪怕客户端没提交ROLLBACK或者COMMIT指令,mysql会自动回滚之前的事务。但使用长连接之后,不同请求会使用同一个MYSQL连接句柄,每个事务开启都会禁用MYSQL的自动提交,即SET AUTOCOMMIT=0语句,这语句会提交之前的事务。对于我们代码忘记写回滚,而直接返回结果的情况下,这是会出大问题的,也是我们目前唯一没有使用MYSQL_pconnect的原因。(计划近期找到没有写回滚语句的代码,修复,继续使用mysql_pconnect) 其实还有,我们php-fpm使用了APC来缓存php file,以及 变量数据等,这些也是有优化的地方(如果有时间的话,则待续)。 回过头来再理解下文章开头那位同学给的解决办法,我仍不能从他给的理由中,理解长连接的缺点,哪怕是解决了TIME_WAIT的问题,但每次创建TCP socket ,连接到服务器时三次握手,关闭TCP socket时的四次握手 这些也是开销。当然,缩短2MSL的时间,也是更好利用服务器资源的一个好方法。 最后,我们调整优化的服务器架构图如下: 好像有点偏离这篇文章的标题了,其实我更想说我不能理解为啥nginx跟php-fpm给的默认配置中,都是TCP socket通讯的,为啥不默认给unix domain socket的默认配置呢?如果说为了方便非同一台服务器时的情况,但给的默认IP也是回环地址呀。 而且,nginx给默认配置中,对于uri请求中的php文件的处理,匹配规则还是老的,之前发生因为NGINX与PHP的配置而导致的安全问题,虽然不是nginx的错,但nginx也可给出更严谨的范例,但仍没有。 值得欣慰的是,在UBUNTU 12.4中,nginx的默认配置有了很大的改进,不管是匹配uri的规则,还是nginx与php-fpm的交互方式: PS:末学只是个web程序员,这些只是末学学习研究如上知识的总结,理解上难免有错,各位请海涵。 2012-05-31 +18 22 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
php5.3.8中编译pdo_mysql的艰难历程 我们的一个项目,用了PDO_MYSQL拓展,准备迁移服务器,新环境需要编译安装环境。昨天,运维同事抽空编译了一下,一直编译不上pdo_mysql,同时,公司的一款新webgame临近上线,他们实在太忙,我这个三流运维技术的程序员来试试吧。 运维同事描述: 服务器系统版本:Linux version 2.6.32-71.el6.x86_64 (mockbuild@c6b6.centos.org) (gcc version 4.4.4 20100726 (Red Hat 4.4.4-13) (GCC) ) #1 SMP Fri May 20 03:51:51 BST 2011 编译PHP的参数 之后再次编译 pdo_mysql 拓展 之后, shell里执行 php -i 和 php -m都没有看到 pdo_mysql拓展。搜pdo_mysql,在将路径添加到php.ini中,仍找不到这个扩展,判断为编译失败。 这里是将pdo_mysql作为一个拓展引入使用的。在php5.3中,PHP开发组把mysqlnd作为默认的连接MYSQL的数据库驱动来使用,据官方描述,节省内存40%,速度更快,当然或许是为了解决许可协议的问题。之前PHP连接MYSQL,是调用MYSQL官方提供的C/C++编写的lib_mysql的dll/so,来实现。这个类库同样可以给PYTHON等脚本语言调用,只要按照API规范来。我们改用mysqlnd之后,就不用再为了lib_mysql去安装mysql client了。详情见:mysqlnd插件mysqlnd_ms的介绍。 两种方法都可以,运维同事都尝试了,由于时间关系,他们没做过多的尝试研究,就转向更紧急的项目了。 运维同事下载使用的PDO_MYSQL拓展的地址是 http://pecl.php.net/package/PDO_MYSQL ,里面用很耀眼的颜色,标注如下几行字 This package is not maintained anymore and has been superseded. Package has moved to channel http://svn.php.net/viewvc/php/php-src/trunk/ext/pdo_mysql/, package ext/pdo_mysql. 也就是说,早在2006年5月1(我是根据最后一个打包文件日期猜的,或许不准)之后,PHP已经将这个pdo拓展放到PHP源码的 ext/pdo_mysql下内置了。这里的这个包,将不会在更新维护了。 在PHP官方文档上对pdo_mysql使用mysqlnd的时候,是这么描述的 在php5.3中,已经支持mysqlnd作为数据库连接驱动了,无需再使用libmysql。而在将来的php5.4中,将变为默认的连接驱动。如图: 开启这个类库的 之后,我的编译参数如下 区别是使用php内置的pdo_mysql类库,使用mysqlnd作为连接驱动,不在使用之前的MYSQL官方提供的libmysql.so。 make之后,提示如下错误 这种错误,让我手足无措,GOOGLE搜了下,也没找到相关案例,而且,一直被墙,打不开国外网页。百度搜的结果,还是算了。(中文还行,E文的话,百度确实不行,尤其是程序相关) 之后,尝试make clean,清除之前的编译结果缓存之类。 再次make,有个小意外 再次搜索,这倒是很多网友遇到过,大部分的建议就是无视这个错误,不影响编译。照做。make install ,一路挺顺畅。 接着,php -i / php -m 也没发现pdo_mysql模块。 郁闷无比,决定看下web下的phpinfo结果,发现居然有了。问题终于解决了。。。万岁。。 可是!!! 1,为啥php -i 、php -m 的结果里看不到呢? 思考,为什么呢? 结合刚刚的报错,联想到CLI 模式下的php 脚本(以及相关的php.ini)跟刚刚从web下访问的php程序不是同一个。检查环境变量,以及切换到编译好的目录下执行php -i,发现pdo_mysql、mysqlnd等相关添加的模块了。 2,php.ini里没启用pdo_mysql拓展,为什么还能看的到呢? 这次使用的是PHP内置的类库,不是以新拓展方式加载运行的,所以,不用更改php.ini,再添加相关so路径。 综上所述,文章没有高深的东西,只是有几个需要细心的点。 1,以官方文档为准,一切跟着官方文档来,不轻易采信网络上网友提供的编译参数,包括这边博文。不论对方是老手、大牛,还是其他什么什么有威望的人。他们提供的方法或许跟你当前的环境不一致,时间也相差很大,或许相隔好几年了。 2,确认得到的结果是准确的,怎么说呢,文中的例子中php-i的路径不是我们新编译的,而是之前编译,或者yum安装的,一定要到自己编译的程序目录下,用自己新编译的脚本去执行测试,获得测试结果,下结论,不为了偷懒,不敲路径,直接写程序名进行测试。 3,遇到诡异的错误,我总会想要一个全新的系统,进行安装,以确保不被各种冗余、缓存等垃圾文件干扰。例子中用了make clean进行清除相关缓存,来解决文件缓存问题。(感谢@ivon_lee 的帮助) 4,自动安装脚本要及时更新,当然,不是意味着追求最新版本。例子中的pdo_mysql的拓展,官方提供了更好的方式,不论是效率,资源占用,都有更好的提升,为啥不使用呢。 备注:mysqlnd 的相关有点对比见http://developer.51cto.com/art/200903/115995.htm PS:文中出现的我搜索无果两次错误,如果有人知道,请告诉我,谢谢。 2011-11-25 4 0
coolcode转SyntaxHighlighter与Mysql正则表达式 blog的代码高亮插件原来是coolcode的,coolcode的高亮插件确实很酷,显示效果也很棒,但是占用的位子太大了。最近,我抽空改成SyntaxHighlighter。由于coolcode插件的开头标签是或者[coolcode]这样的,而SyntaxHighlighter是这样的(或者其他)。遂只能想办法把老的格式转化成新的格式。当然,肯定用到正则表达式了。 原来的代码高亮开头标识为 这种类型的, 而SyntaxHighlighter的标识为 那根据要求写的正则表达式为 解释一下 [a-z]+ 匹配 php,javascript,cpp,sql,css 等,后面的.*?中的 .表示任何除了换行之外的字符,而*表示0次或者无数次,*+这些表述次数的符号后面接的?标识非贪婪模式 看图,这个正则可以实现上述要求了。 但是,问题还没解决,我们还有一种情况没考虑,那就是后面不一定接的就是lang="php"这样的属性啊,有可能是download,也有可能是linenum="on/off"啊,所以,我们的正则还需要改。 CFC4N把正则改为 截图如下 细心的朋友可能看出来图中匹配的红色框内多出了,意思也就是说,前面的需要排除掉。如何排除呢?聪明的你肯定立刻想到.这个万能字符替换成非两个符号的规则,好,CFC4N立刻修改一下。 修改之后的正则为 果然,匹配正常了。结果见截图。 到这里,问题似乎解决了,可是,当初糊涂的我,把coolcode的两种开头标识都用了,那就是和[coolcode,那么,看官您认为这个正则该如何改写呢? 没错,无非就是开头,结尾的标识考虑两种情况 2010-06-02 +2 6 0