为什么不能在字符组中使用反向引用 有个网友,发一封邮件给我,让我帮看下一段“加密”字符串的解密问题,在线解密的解密不了。代码大约如下: 看到代码后,不禁惊讶起来,现在坏人们的想法真猥琐。利用preg_replace函数的e修饰符(e修饰符在php5.5里,被取消了),来执行第二个参数里的内容,而第二个参数里的内容,不是常规的函数组成的字符串,其函数名都是ASCII码十六进制形式。从而避开了基于字符串形式正则匹配的“简易木马扫描器”的扫描。2010年时的在线解密功能,只支持eval gzinflate base64_decode三个函数的任意组合形式的字符串解密,且格式很严格,必须以<?php开头,以?>结尾,不支持嵌套解密。到了2012年,发现有好多网友在陆续使用,并反馈给我。我再次做了改进,支持嵌套解密,编码转换等功能。这次,看到这个加密方式,索性直接写一个支持任意以eval函数执行的合法php字符串解密,且支持同一份代码的多次出现、循环嵌套解密,格式也不用那么严格,只要被解密部分是合法的php代码即可,其他地方,可以有任意字符串,提高易用程度。 方向定了,实现起来,也应该没什么难度,目的也是基于字符串处理,正则匹配php语法函数,提取加密部分,解密,替换到原文本中。所有的难点,就是正则匹配php语法了。我在匹配eval(base64_decode('xxxxxxx'))遇到了一个问题。因为不能确定代码使用的是单引号还是双引号,故我打算捕获分组,再反向引用一下,那正则表达式就是(['"]).+?\1,正则捕获的第一个分组里的内容会是',那么\1的反向引用将是',看上去,匹配完成了,但会产生大量回溯,回溯次数取决于中间字符串长度。 作为性能洁癖的码农,不能接受正则里出现大量无用的回溯,决定使用字符组的脱字符^来对字符取非匹配。那么,表达式就是 regexbuddy的测试结果却是这样: 如图,调试信息中所示,绿色线条画出的部分,都是正常正确的匹配,表达式(['"])匹配字符串',并且分配到group 1中;表达式最后的\1反向引用,也匹配到了',这个可以在debug截图中得到确认。但红色部分,让我产生了疑惑,表达式[^\1]为何匹配到了字符串1230'''''cfc4n(解释一下:+为量词,先匹配,再将转动权交给表达式后的',再去匹配,其发现后面没有字符,那么再回溯到前一位。。一直到匹配到字符'),匹配到字符串1230跟cfc4n倒是正常,但group 1就是',[^\1]匹配到'''''实在不能理解。暂时改用其他办法实现,比如(['"]).*\1,但性能差,且不精确。或者用环视lookaround(也叫零宽断言)解决([\'"])(?:(?!\1).)+\1。 对于这个问题,我困惑很久,在stackoverflow上找到一个相似的案例General approach for (equivalent of) “backreferences within character class”?问题也是跟我类似的问题,解决方案,也是用环视解决,其实,环视的效率,比非贪婪的方式还多一个回溯。但回答者只是给了解决方案,也没说出真正的原因。然后,又看到了另一个相似案例Negating a backreference in Regular Expressions,解决方案也是一样,正则环视解决,回答中,还说了原理: Instead of a negated character class, you have to use a negative lookahead: \bvalue\s*=\s*(["'])(?:(?!\1).)*\1 (?:(?!\1).)* consumes one character at a time, after the lookahead has confirmed that the character is not whatever was matched by the capturing group, (["'']). A character class, negated or not, can only match one character at a time. As far as the regex engine knows, \1 could represent any number of characters, and there's no way to convince it that \1 will only contain " or ' in this case. So you have to go with the more general (and less readable) solution. 但这个原理说的是环视匹配的原理,并没有说“为什么字符组中反向引用匹配的字符串不正确”的真正原因。对于我的好奇心,显然不能轻易放过这个疑问,继续google中搜索,终于在regexbuddy的另一个官网找到了介绍:Parentheses and Backreferences Cannot Be Used Inside Character Classes,刚打开这个页面时,被配色、布局震惊了一下,这跟regexbuddy官网出奇的相似,仔细看了下,才发现这也是他们的网站之一。这里给出了说明: Round brackets cannot be used inside character classes, at least not as metacharacters. When you put a round bracket in a character class, it is treated as a literal character. So the regex [(a)b] matches a, b, ( and ). Backreferences also cannot be used inside a character class. The \1 in regex like (a)[\1b] will be interpreted as an octal escape in most regex flavors. So this regex will match an a followed by either \x01 or a b. 这里提到,正则表达式中,不能在字符组中使用反向引用,原因是正则表达式的\1在字符组中[\1],在大多数的正则流派中,会被正则引擎作为八进制转义,实际上的匹配结果将变成\x01。知道这个原因之后,就很好理解为什么之前的表达式[^\1]匹配到' ' ' ' '了。因为,这里的取非,是对字符\x01的取非,而不是对字符'的取非,我之前还天真的以为,取非的字符串是'呢。 除了不能在字符组中使用反向引用,还不能使用捕获分组,这里也提到了,正则表达式的元字符括号()在字符组中将被理解为普通的字符(),也就是说,在字符组character class中,不用再转义了,即[()]是合法的表达式,且可以匹配到(或者)。比如文章中给的例子:表达式[(a)b]匹配结果并不是a或者b,如果a匹配到,再将a分配到group 1中,而是可以匹配到a或b或(或)四个字符。所以,在字符组中使用反向引用,是不能实现的了。如图: 最后,解密的程序也写好了,支持eval base64_decode preg_replace gzinflate等各种函数的在线,目前除了自定义函数、字符串不支持解密以外,其他均支持,只要用eval函数执行的代码。跟http://ddecode.com/phpdecoder/相比,我的解密程序不支持变量函数、变量字符串解密,这是缺点。同时,优点是支持一段代码多个eval函数解密,且替换还原到原文。而phpdecoder中,只会保留最后解密的那个eval内代码解密情况,其他的都没了。 其实,基于字符串匹配的解密方式,都肯定存在不严谨、不正确的解密行为,最准确的,莫过于写一个php拓展,劫持eval函数,并且执行被解密代码,使用加密代码内原有变量,来实现最准确,严谨的解密。 PS:如果你对这里的正则表达式的术语有疑惑,请阅读这个PPT《正则表达式》PPT-匹配原理 PPS:后来,无意中看到360网站安全检测的后门查杀代码里的正则的写法 实在说不过去,作为国内一线安全厂商,这正则准确性、严谨性、误报、性能都有较大提升空间,忍不住的唱起“放只乌龟到处爬,放只螃蟹有点Zha,有点Zha”...如果他们不嫌弃的话,可以参考下《如何精确查找PHP WEBSHELL木马?》里的python版的webshell检测的正则,三年前的了,可能有很多东西已经变了。 2013/09/14更新: 我自己开发了一个项目,PHP版本的webshell扫描,基于语法分析的,名字叫 Pecker Scanner,欢迎使用。 2013-08-30 2 0
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
Nginx模块fastcgi_cache的几个注意点 在web项目中,大家都已经非常熟悉其架构流程了。都说Cache是万金油,哪里不舒服抹哪里。这些流程中,几乎每个环节都会进行cache。从浏览器到webserver,到cgi程序,到DB数据库,会进行浏览器cache,数据cache,SQL查询的cache等等。对于fastcgi这里的cache,很少被使用。去年年底,我对nginx的fastcgi_cache进行摸索使用。在我的测试过程中,发现一些WIKI以及网络上没被提到的注意点,这里分享一下。 这里是我的NGinx配置信息 配置这些参数时,注意每个参数的作用域,像fastcgi_cache_path参数,只能在http配置项里配置,而fastcgi_cache_min_uses这个参数,可以在http、server、location三个配置项里配置。这样更灵活的会每个域名、每个匹配的location进行选择性cache了。具体的参数作用域,参考FASTCGI模块的官方WIKI。我为了调试方便,添加了一个『X-Cache-CFC』的http响应头,$upstream_cache_status 变量表示此请求响应来自cache的状态,分别为: MISS 未命中 EXPIRED - expired, request was passed to backend Cache已过期 UPDATING - expired, stale response was used due to proxy/fastcgi_cache_use_stale updating Cache已过期,(被其他nginx子进程)更新中 STALE - expired, stale response was used due to proxy/fastcgi_cache_use_stale Cache已过期,响应数据不合法,被污染 HIT 命中cache 程序代码是Discuz!论坛, 随便开启测试了几下,发现/dev/shm/nginx_cache/下没有任何目录建立,也没有文件创建。调试的http header响应头里的X-Cache-CFC 结果一直是MISS。从服务器进程上来看,Nginx cache manager process 跟Nginx cache loader process 进程也正常运行: 不知道为何会这样,为何没有cache成功,我以为我配置参数有问题,只好阅读WIKI。发现fastcgi_ignore_headers 参数下解释有这么一段 fastcgi_ignore_headers Syntax: fastcgi_ignore_headers field ... Default: Context: http server location Reference: fastcgi_ignore_headers This directive forbids processing of the named headers from the FastCGI-server reply. It is possible to specify headers like "X-Accel-Redirect", "X-Accel-Expires", "Expires" or "Cache-Control". 也就是说这个参数的值,将会被忽略掉,同样被忽略掉的响应头比如"X-Accel-Redirect", "X-Accel-Expires", "Expires" or "Cache-Control",而nginx配置中并没有fastcgi_ignore_headers参数的设定,那么问题会不会出现在FASTCGI响应结果里包含了类似"X-Accel-Redirect", "X-Accel-Expires", "Expires" or "Cache-Control"这几个响应头呢?用strace抓包,看了下nginx与fpm进程通讯的数据 从抓取的数据包里可以看到,fpm确实返回了包含“Expires”、“Cache-Control”头的http 响应头信息。那么疑问来了: nginx的fastcgi_cache没缓存这条http响应,是因为响应头里包含“Expires”、“Cache-Control”的原因吗? 程序里并没有输出“Expires”、“Cache-Control” http header的代码,这是谁输出的呢? 既然是fpm响应的时候,就已经有了,那么是php的core模块,还是其他拓展模块输出的? “Expires:”时间为何是“Thu, 19 Nov 1981 08:52:00 GMT”? 疑问比较多,一个一个查起,先从Nginx的fastcgi_cache没缓存这条http响应查起。我根据测试环境nginx版本1.1.9(ubuntu 12.04默认的),到nginx官方下了对应版本的源码,搜索了fastcgi参数使用的地方,在http\ngx_http_upstream.c找到了。虽然不能很流程的读懂nginx的代码,但粗略的了解,根据了解的情况加以猜测,再动手测试实验,也得出了结论,确定了nginx的fastcgi_cache的规则。 也就是说,fastcgi响应http请求的结果中,响应头包括Expires、Cache-Control、Set-Cookie三个,都会可能不被cache,但不只有这些,别忘了nginx配置中fastcgi_ignore_headers参数设定的部分。以及ngxin的X-ACCEL X-Accel-Redirect、X-Accel-Expires、X-Accel-Charset、X-Accel-Buffering等nginx自定义的响应头。由于这几个不常用,我也没深入研究。通过对nginx的ngx_http_upstream模块代码模糊理解,加猜测,以及写了脚本测试验证,可以得到结论是正确的。即Nginx fastcgi_cache在缓存后端fastcgi响应时,当响应里包含“set-cookie”时,不缓存;当响应头包含Expires时,如果过期时间大于当前服务器时间,则nginx_cache会缓存该响应,否则,则不缓存;当响应头包含Cache-Control时,如果Cache-Control参数值为no-cache、no-store、private中任意一个时,则不缓存,如果Cache-Control参数值为max-age时,会被缓存,且nginx设置的cache的过期时间,就是系统当前时间 + mag-age的值。 到了这里,疑问1解决了。那么疑问2、3呢?程序里并没有输出“Expires”、“Cache-Control” http header的代码,这是谁输出的呢?既然是fpm响应的时候,就已经有了,那么是php的core模块,还是其他拓展模块输出的?我精简了代码,只输出一个“hello world”,发现也确实被缓存了。显然,php脚本程序中并没输出http header 的“Expires”、“Cache-Control”,多次测试,最终定位到session_start函数,翻阅源码找到了这些代码: 到了这里,知道原因了,是程序调用session_start时,php的session拓展自己输出的。session.cache_limit参数来决定输出包含哪种Expires的header,默认是nocache,修改php.ini的session.cache_limit参数为“none”即可让session模块不再输出这些http 响应头。或在调用session_start之前,使用session_cache_limiter函数来指定下该参数值。那为什么要在使用session时,发Expires、Cache-Control的http response header呢?我猜测了下,需要session时,基本上是用户跟服务器有交互,那么,既然有交互,就意味着用户的每次交互结果也可能不一样,就不能cache这个请求的结果,给返回给这个用户。同时,每个用户的交互结果都是不一样的,nginx也就不能把包含特殊Cache-Control的个人响应cache给其他人提供了。 还有一个无聊的问题“Expires:时间为何是Thu, 19 Nov 1981 08:52:00 GMT”?我翻阅了session.c这段代码的添加时间,版本,作者信息,在php官方版本库中找到了这次提交的信息: Revision 17092 - (view) (download) (as text) (annotate) - [select for diffs] Modified Sun Dec 12 14:16:55 1999 UTC (13 years, 2 months ago) by sas File length: 28327 byte(s) Diff to previous 16964 Add cache_limiter and cache_expire options. Rename extern_referer_check to referer_check. 对比session.c两个版本的变更,果然是这块代码。作者是sas,也就是Sascha Schumann, http://php.net/credits.php里可以看到他的大名。关于这个expires过期时间的问题,有人在stackoverflow也提问过,Why is “Expires” 1981?,别人说那天是他生日。这是真的么?如果那天是他生日的话,而他增加session.cache_limiter时是1999年,他才17岁,17岁呀。我17岁时在干嘛?还不知道电脑长啥样,正在玩『超级玛丽』呢。 好奇的不是我一个人,还有个帖子是epoch date -- Expires: Thu, 19 Nov 1981 08:52:00也问了。另外两个地址虽然没问,也有人提到那天是他生日了。http://boinc.berkeley.edu/dev/forum_thread.php?id=2514、https://github.com/codeguy/Slim/issues/157,这些帖子都提到说原帖是http://www.phpbuilder.com/lists/php3-list/199911/3159.php ,我无法访问,被跳转到首页了。用http://web.archive.org找到了历史快照,发现上下文关系不大,也不能证明是他生日。 我更是好奇的发了两封邮件到他的不同邮箱里问他,不过,目前他还没回复。或许他没收到、没看到,或许懒得回了。N年后,“Expires:时间为何是Thu, 19 Nov 1981 08:52:00 GMT”这个日期,会不会又成了一段奇闻佳话了呢? 2013-02-28 +14 18 0
手机通讯录,或许可以增加QQ、微信之类IM用户黏度 很多互联网公司,都努力抢占互联网的用户入口,从PC的操作系统Ghost(番茄花园、深度),到PC的浏览器入口(hao123),到PC的桌面程序入口(360安全卫士),再到移动平台的刷机程序(shendu、shuame),这些地方,都陆续成为他们的目标。 在手机上,不管各种刷机程序如何精简软件,都剪不掉通讯录这个程序。如果,在通讯录中,展示当前用户Instant Messenger即时通讯工具中好友的在线状况呢?如果你想给别人打电话,在通讯录里找这个人,找到之后,发下下面有个QQ或者WeChat微信在线的图标,如果事情不紧急的话,你会选择通过QQ语音通话吗? 这得从我去年年底,今年年初买的一部手机说起。 在2012年底,我换了一部Android手机Google Nexus 4,这是我的第一步智能手机,我打算在它上面,做一些移动应用上的尝试。但不知道该做什么样的功能,无意中,看到通讯录中联系人的列表里,如果对方gtalk在线的话,会直接展示出来,而且,还会同步个性签名,这太棒了。 Google Talk在国内,不是很流行,用户基数也少。在国内最流行的,莫过于腾讯的QQ了,以及手机专用的IM --WeChat微信,我也是腾讯的重度老用户。试想着,也把QQ联系人状态显示在通讯录里,一目了然,方便用户,也扩大了腾讯的魔爪。 程序上来说,我初次写android应用,java也已经5、6年没碰过,当初也只是会写个hello world。现在做起来,实在难。还好有google 官方手册,各大开源社区上也有现成的开源项目,照葫芦画瓢,勉强能跑起来。实现IM状态更新到通讯录这比较简单,但获取当前登录QQ、以及登录QQ的联系人在线情况,签名信息这极为困难。腾讯没有提供相应接口。我也不敢逆向手机QQ源码,来获取相应信息,哪怕敢了,也要花费大量精力。哪怕做成了,还是给腾讯做嫁衣。 这么一说,我的第一个移动应用,就这么OVER了。 抛开个人因素,单说这个创意,我认为,是令人震惊的... 如果你想给别人打电话,在通讯录里找这个人,找到之后,发下下面有个QQ在线的图标,如果事情不紧急的话,你会选择通过QQ语音通话吗? (微信没有在线状态一说) 打开对方信息详情页: 如果你输入了号码, 通话界面,直接提醒你 QQ、WeChat在线,可以通过其免费语音通话,那你选择吗?(紧急事情除外) 当用户选择使用QQ、微信图标时,即(在电话接通之前)挂断电话,直接弹出对应的语音通话面板,开始通话。(如果电话接通,则隐藏图标选择界面。) 腾讯,你还等什么...... 如果这样做了,那得劫持多少用户改用QQ啊? 移动、网通恨死IM了。 其实,移动,联通你们也可以的,但你们舍得么?敢吗?有勇气么?有胆识么?以后你们终究会从一张SIM电话卡沦落为移动的上网卡。 可能有人说,用户很少会在通讯录里填上对方的QQ号码,一般只会存个手机号,其他信息都会再输入了。但那是非智能手机的时候,填写联系人信息不方便的做法。现在可以在电脑上输入,云端同步到手机中。实在不行,腾讯不是有手机管家的通讯录管理功能么?腾讯自己就很容易帮用户关联好了吧? 上面提到,紧急事情可以不选择QQ、微信进行语音通话,但以后大家都喜欢这种通话方式,那么电话卡失去打电话功能,只能留下网络支持的功能了,那么移动电话卡变成移动网卡。 PS:就连Photoshop,老衲也用的很流畅,PS的界面也很惊艳,老衲不尽欣赏起自己一番。 2013-02-19 +3 7 0
再读《Rework重来》 去年,我读过《Rework》时却不以为然。如今再读,却产生很多共鸣。可能我在去年,并无太多相关产品的经历,也没有真正去尝试,所以那时没读懂书中的真谛。 从书的简介上看,是为创业者分享的经验,其实很适合做产品的所有人。150页的PDF,除去大幅图片,也就100页左右的文字。每次做公交车时,15分钟的路程上瞄上几眼,一周左右才读完,但也不乏对个别章节反复揣摩联想,标注评论。通篇下来,倒也标注不少,忍耐不住,便又要开始发个牢骚,做个读书笔记。这读书笔记也算是Nexus 4的第一个功劳,自我安慰一下,手机没有白买。以下是结合我做一年webgame产品的经历来讨论书中的部分章节。 Hire great writers 如果你要从一堆人中决定出一个职位的合适人选,雇那个写作最厉害的人。这个人不在乎是不是营销人员、推销员、设计师、程序员,无论是谁;他们的写作技巧会对此有益。 因为一个好的写手不单是有根好的笔杆子。清晰的写作体现其清晰的思路。优秀的写手懂得沟通。他们让事情易于理解。他们会站在别人的立场想事情。他们知道什么该省略。那是你在任何求职者中都想看到的品质。 在这一年里,我常自叹招人太难,招不到。一来是本来应聘者数量少,二来是我的要求高。然而前者是我已尽全力,已用尽所有社交关系。仍于事无补。这个问题,只能请HR花心思去解决了。后者是重点,此也是我较为赞同37signals的原因。招人要么就招合适的强人,要么不要。非常不愿意招人来将就一下,以后再换这种做法,不想浪费精力,浪费感情。不管是技术水平,还是工作态度,两者都要合适。有幸的是,在我的坚持下,我招到的同事,都是非常合适,非常尽心的“great writers”。 反观那些不合适的人,浪费我们精力去指导他,教会他。而他们只是为了工作而工作,为了薪水,不会关心你的产品做的怎么样。恰恰这种工作态度,在一次产品变更中被体现出来。在我们项目的生命周期的尾声,公司决定对此进行换皮化,并稍加修改,推出新产品。在产品做完2/3时,领导层突然推翻该项目,说要做一个全新的。通知一下来,很多人不解,不能接受,怨声载道,甚至决定离职。我倒是觉得,这些反映越是强烈的同事,越是对产品最负责,对产品贡献越大的人。这些人就是“great writers”。而那些毫无态度,觉得无所谓的人,几乎可认为他们是占着茅坑不拉屎,混日子型。 上一个项目的快速衰败,是否跟这些人有关,我也没法下结论,或者,我也不敢下结论。可能是我的要求太高了吧。 Let your customers outgrow you 不要被大客户牵着鼻子走 也许你曾经见过这样的场景:一个顾客让一家公司浪费了很多钱。这家公司想要尽可能的取悦那个顾客。因为一个顾客的一个要求而改变产品则会让产品远离初衷。 突然有一天大客户走了,公司则会背负一个包袱──这个产品是围绕着一个已经离开了的人设计的。而其他人没法用。 在你紧盯着现有的用户的任何困难时,你就会忽略新的客户。你的产品或者服务紧紧围绕着现有用户则不会吸引到新客户。那就是你公司慢慢走向死亡的原因。 在我们的第一个产品红了一段时间之后,我们开始收到一些老用户的强烈要求。他们说我们产品的功能开始不能满足他们的需求了。他们业务正在改变,希望我们改变我们的产品以适应他们新发现的复杂问题和要求。 我们说“不”。原因是:我们所希望的是顾客因为我们的产品而成长,绝不是我们优先去适应他们。为有些人增加一些高级用户需要的功能会威胁到还不是我们用户的人。吓跑新客户可比失去老客户要糟糕。 当用户的发展超过你时,你的产品是基础型的,那就很好了。小、简单、基础的需求是恒久的。对于真正需要它的用户来说,这既是无尽的补给。 不用你产品的人总是比你的用户要多。设法让这些人也成为你的用户。这就是你持续发展的可能性所在。 人和形式在变换。你不能满足所有人。公司需要满足的是一类用户,而不是为了一个特例而改变需求。 同样在这一年里,我的大部分工作时间,都是放在正在国内著名IM的开放平台上运营的产品上,在此期间,产品也经历过多次的版本迭代更新,也推出过许多新功能。新功能中,也有着大部分的功能,可认为是为那些大R(消费额度较高的RMB玩家)研发。我们策划,有句口头禅“你又不是大R,你怎么知道他们怎么想?”,从这句话里,也看出他把大R作为主要的服务对象,几乎所有的新功能都是为他们设计,尽其所能的取悦大R。游戏初期,功能为所有用户设计,玩家多了,人气旺了,才激发玩家充值,产生大R。而新上线的功能,多数为了给大R玩,为那些那群大人民币玩家而改进。导致普通玩家渐渐流失,更别提新玩家了。可能,他们的做法,短期上,从消费数字上看,有较为满意的结果。但从长期发展上来看,这无异于饮鸩止渴,自寻死路。普通玩家走了,大R优越感何在?大R为什么要花钱?不是虚荣心吗?不是想比别人强么?别人都走光了,他们还有何优越感而言? 现在,各大互联网厂商为何推出免费产品?他们要的是什么?流量!流量!有了流量,人气,就能盈利。我们的人都走玩了,人气都没了,光靠几个大R如何盈利?而大R们也是从大量普通用户中衍生出来的。普通用户越来越少,如何再衍生出新的大R?如何衍生出新的高端用户?普通用户走了,他们还会留下来吗?产品再去改来得及吗?还能改吗? web平台上的流量为王的总之,在webgame中,也同样适用。 当然,我说这些,不是批评指责我们策划同事的不对,我不是策划,我不专业,这只是我的一点看法。每个人有他自己的想法,他有他的道理。在一起做产品的日子里,我只能保持最大程度的理解、信任。也希望他在以后的项目里,可以坚持自己的观点,别被上级的意愿干扰,被迫为上级的想法而开发一个功能,此类做法,可对整个策划框架产生影响。取其精华,去其糟粕。该回绝就回绝,做好自己。 还有就是成功的项目并不是一定比我们更有才智和更幸运。他们只是在不断的尝试、不断的失败,直到成功。项目的每个新功能,在上线之后,再也没人去关心过,没有任何一个功能,都经过第二次的功能性优化,除非有bug。这是极为令人不解的。难道我们不该反思吗? 希望在我们的下一个产品里,能兼顾这些想法,把产品做的更好。 Sell your by-products: 注意你的副产品,也许可以卖。。 当你做某事时常常也附带了别的。人不能仅仅做一件事,所有事物都有其副产品。具有洞察力和创造力的商业头脑能发现那些副产品和机会。 木材工业贩卖曾经被浪费掉的- 锯屑、碎片、碎木头,而获得可观利润。你会发现这些副产品用于合成壁炉原料、混凝土、冰区加强器、根篱、碎木板、燃料等等。 但是你也许不能制造任何东西。发现你的副产品很难。木材公司的人清楚他们的废料是什么。他们不会忽略锯屑。但你不清楚你自己。也许你都不知道你已经制造出副产品了。这真缺乏远见。 我们的上一本书,Getting Real 就是个副产品。我们写了它都没意识到。创建一家公司和开发软件的经验是真的动手过程衍生出来的。我们一开始把那些知识分享到博客,然后是系列研讨会,再做成 pdf 文档,最后制成书。这本副产品让 37signals 直接收入超过一百万,间接又赚了一百万。你现在正在读的这本书也是副产品。 摇滚乐队 Wilco 发现在他们的录音过程中的副产品很有价值。他们将录制过程拍摄下来然后以 I Am Trying to Break Your Heart 发行。它呈现了乐队毫无保留的,迷人的创作过程,还有内讧。乐队用这部电影赚到钱,还有以此手段拓展了新的粉丝。 Henry Ford 将碎木头导入模子成型为块状木炭。他创建了木炭车间和 Ford Charcoal 公司(之后更名为 Kingsford Charcoal)。如今金斯福德木炭依然是全美领先的木炭制造商。 软件公司不是常常要去思考著书。乐队通常不会去想着拍摄录制过程。汽车制造商不会去销售木炭。也许你可以去销售一些你没想到过的东西。 我们的一款webgame,在国内知名IM的开放平台上运营效果并不是很理想,当然,也不是很差。公司里层有个推翻的新项目(上面也提到过),被推翻时,美术资源已经画完;被推翻后,项目组计划去做一款全新的webgame。这正在运营的项目代码、策划数值资源以及被推翻的项目的美术资源难道不算个非常珍贵的副产品吗?稍微加以修改,并不需要投入过大的人力物力,即可上线运营。谁又能保证,全新开发的一款webgame,会比这款已经预知肯定可以盈利的产品强呢?真的会强吗?又会投入多大的人力物力呢?或者,正在研发的其他几个项目,又有哪款可以保证盈利?或者比这款盈利高?投入产出高? 这只是个副产品,不是主产品,又并行不悖。对于高层的决策,我实在不解... No time is no excuse“没时间”不是借口 人们最常给的借口是:“时间不够用。”他们宣称很想开一家公司,学一件乐器,写一本书,随便什么吧,但时间不够用。 算了吧。如果你正确使用,时间总是足够的。也别想一定要放弃你的全日制工作。抓紧时间,晚上来做你的项目。 动手执行你的构想,而不是看电视,玩魔兽世界。11 点再睡觉,而不是 10 点就爬上床。我们不是在谈论通宵达旦或一天 16 小时都在工作,而是要你每星期挤出一些业余时间。这就足够做些事了。 一旦开始做,就知道你是否真的兴奋而感兴趣或只是过度期。如果没有结果,你就像以前一样继续工作。除了花些时间,这不会有风险或损失,这没什么大不了。 当你实在渴望你的理想,腾出时间,别管你别的责任。而真相是大多数人没那么渴望。然后拿时间借口来掩盖自己的内心。别给自己理由开脱。你的全部职责就是实现梦想。 此外,完美的时机从未有。你总会太年轻,太忙,太穷等等。若你一直为遇到一个完美时机而发愁,那绝不会发生。 回顾这些年,从大学时开始写博客,一直至今,除了较为忙的一段时间没有新的文章,其他时间里,几乎都是没有中断过。从开始的为了数量而写、复制、转载博客,到最近的为了质量,为了分享,为了磨练意志而写原创的BLOG。有幸的是,我坚持住了。人们最常给的借口是:“时间不够用”,这我也深有体会,尤其是儿子出生之后,挤出时间实在是不容易,而我又有些惰性,总想看看电视,打打游戏。但心里,总有个念头,强制自己去完成它。家里没有合适的环境,只好背着电脑来到公司里写博客。写完之后,心里就踏实多了。这恐怕又应验了一句话,时间是挤出来的,但每每跟他人提到这些,心里极为愧疚,总觉得,每年比大家增长比较快的,无非就是QQ等级了。 或许,等明年我再读Rework时,又有新的感悟、新的看法,也可能会否认我现在的观点。世界万物时刻都是在变化的,何况我呢? 附: 《Rework重来》在线阅读 《Rework重来》下载阅读 2013-01-20 4 0
google play购买Nexus 4手机小记(新增Nexus 5购买历程) google play海淘Nexus系列设备交流群:,包括nexus 4、nexus 5、nexus 6、nexus 7、nexus 9、nexus10 之类google已经发售的设备。加群验证为:nexus 、google play ----------------------------------------------分割线----2014 10 16 nexus 6发布-------------------------------- 2014年10月16日,google nexus 6发布,价格649$起,将近4300人民币,实在太贵,不打算买neuxs 6了... 2005年读大学时,我的手机是CECT,具体什么型号记不住了,在07年无法开机,200块钱在淘宝上买了黑莓7230,大约用了1年不到,硬件太差,频繁死机,又花了1500块,入手黑莓8800。从08年起,黑莓8800陪伴我到2012年的9月份,将近4年半的时间,由于很多潮流的社交软件都没有黑莓版,我也趁机冒充了一段时间的好男人。也就在9月份左右,然后,突然间每隔3-5分钟就死机一次。大家可能不知道,黑莓每开机启动一次,大约需要3-5分钟。这.... 新手机,选哪种好呢? Iphone系列:又贵,屏幕又小,又街机。 粗粮手机:从不正眼看过粗粮手机。 Lumia 920:计划当中,一直到10月份,知道价格起码4000多,遂打消念头。贵,而且不知道哪年开售。Windows 系统也挺有风险的。 华为荣耀2:得知微博上信息之后,曾列为备选目标,准备坐等其开售。 魅族MX2:我从华为荣耀2这个备选目标上又转移的一个方向。 Nexus4:google 亲儿子,android 4.2,4.7寸屏幕,价格只要2200左右(16G) 我有点花心,因为各种营销新闻、视频等影响,我的目标依次从Lumia 920 到 华为荣耀2 到 魅族MX2 到 Google Nexus4。就这么愉快的决定了。 浏览器代理: Nexus4 发售方式是google play网上购买,不在中国出售,故只能到美国官网或者澳洲欧洲等网站购买。以美国为例,google 的play是不对中国的IP服务的,也就是说中国IP无法访问 https://play.google.com/store/devices google devices这页。IT人基本功翻墙,我有美国机房的服务器,开了SSH,putty.exe弄了个socket 代理,正常访问了。 结算方式: google play的结算是google 电子钱包,付款方式可以用VISA、万事达等,不支持银联。国内信用卡有VISA付款功能的就可以。我的是浦发信用卡,绑定后,预扣1美元。同样,订单地址只能是美国,我用风雷转运(有坑,小心),google将会把手机寄送到转运公司的仓库,转运公司代收,再通过快递邮寄到中国。也就是说,你得拥有一个美国的收货地址。因为我使用转运公司的转运服务,转运公司会分配一个美国地址给我,我填写的就是它。还有一件事,美国的那么多州,有的地方是不用交税的。我为了不交税,选择了一个OR州的地址。之后由转运公司将货物再运到CA州。因为只有CA州能往中国发货。(现在,财付通有海外支付的功能,也可以使用。) 初次购买: 日期是美国时间11月13号。也就是北京时间14号凌晨1点多,开售时,google play多次发生宕机故障,无法正常服务,我有幸在30分钟后成功下了订单,扣了362.99$,也就是设备费用349$以及美国境内的快递费13.99刀。 google撤单: 北京时间11月14号,收到google 邮件,说我的订单被撤了,理由是我的信用卡认证有问题。我提交了身份证以及在paypal上购买windows 8 CD-KEY的信用卡账单截图,在16号收到google wallet认证通过的消息,可惜的是,这个订单已经被撤销,只能等下一次放货。 第二次购买: 开始,并不知道google 第二次放货是什么时间,28号早上起来,刷了下邮件,看到google发了一封通知邮件,告诉我已经放货了。迅速PC购买,很顺利的下单完成。预计发货时间是2-3周。 等等等等: 11月29日:等 11月30日:等 12月01日:等 12月02日:等 12月03日:等 12月04日:等 12月05日:等 12月06日:等 12月07日:等 12月08日:等 12月09日:等 12月10日:等 12月11日:上午收到信用卡的再次扣款信息(之前是预扣,这次是真扣),下午1点收到google发货邮件。 google使用的是UPS 2日达服务,2天内送到目的地。 12月12日:到达OR州转运公司仓库。 跳入风雷转运大坑: 12月11日:得知google发货,赶紧到风雷转运后台提交闪运请求,并充值200块钱。 12月12日:风雷转运的THOMAS签收了我的包裹。开始坐等OR州发往CA州,并入库,发往中国。 12月13日:等(很开心) 12月14日:等(继续开心) 12月15日:等(无表情) 12月16日:等(稍微不爽) 12月17日:等(没耐性了) 12月18日:等(有点生气) 12月19日:等(日) 12月20日:等(我草,尼玛....) 12月21日:等(草草草) 12月22日:等(狂草) 12月23日:等(草啊....)---- 一朝入了风雷坑,从此发货是路人。 12月24日:风雷CA州仓库收到货物,并入库. 12月25日:风雷扣了我199块,妈的,差点不够,差点没闪运。拿到USPS的转运号。开始去刷USPS。 从美国到中国: 12月26日:等 12月27日:等 12月28日:等 12月29日:到达上海,送交海关。(得知同批次的闵行网友当前拿到机器,草) 12月30日:等 12月31日:等 (得知另外一网友被收税拿到) ------------------------------尼玛,2013年了都------------------------- 1月1日:等 1月2日:等 1月3日:等 1月4日:等 (得知另外一网友被收税拿到) 1月5日:等 1月6日:尼玛王八蛋的X关,效率太高了,不等报税单了,直接冲去海关武定路458号(驻邮办)。 直奔海关: 打印了google的订单信息,带着身份证以及几张毛爷爷,直奔海关。7号线昌平路站2号口出来,走15分钟就到。 二楼领号,由于没有报税单,直接给了工作人员的EMS号码,对方直接给我现场打印一份税单。排队等叫号。 一会,叫号了,去一楼,一个着海关制服的大叔“接待”我,直接问:LG 手机?嗯,是的(Nexus 4是LG代工)。交税200。付钱,拿机器走人。 拆箱验货: 回来路上,去移动厅免费剪卡。到家安装。少废话,直接上图。 屏幕截图: 总体来说: 手机质量不错,性价比高,配置够强,有质感,大气。非常满意。 此次海淘,花费363.99$裸机成本 + 199¥转运费+200¥关税= 2660¥,(11月27号算开始)耗时1个月零10天。时间非常久,海淘虽好,但磨死人。需谨慎。。。 有必要提醒一下,在我填写风雷转运单时,我记得申报的物品类型是小家电。但我取到包裹之后,发下USPS的快递单上的Package type却是Gift,我估计就是因为这个的原因,导致我的包括被海关拦下,导致这么长时间没发给我。这个怀疑,后来在机锋网论坛上得到其他几个网友的确认,他们也是这个情况。 从9月份黑莓8800死机开始,一直到拿到新手机期间,感谢Y哥同学的临时手机的鼎力相助。 后来,我又在google上陆陆续续的买了两部手机,都是使用腾讯财付通的海外虚拟帐号功能,如图: Nexus 5购买历程 2013-10-23更新 对于打算自己在google play海淘Nexus 5的网友,如果有疑问,欢迎留言,我尽量一一解答。海淘,一定要先做好准备工作,比如翻墙方式、支付方式之类。 2013年11月1日,早上8点多醒来,从微博中得知Nexus 5已经上架到google play上开售,我立刻打开电脑,登陆google play,这次没有使用任何代理、VPN,直接可以访问购买页面,可能是因为之前已经成功购买nexus 4记录的原因吧。 腾讯财付通里早已充值了5000块人民币备用。迅速下单两部,一部白色,另外一部黑色,都是32G的。转运公司地址,还是用风雷转运的,付款时,发现冻结了5008.86元人民币。还好帐户里有点其他余额,差点都不够了。目前订单状态是处理中,如图: 关于风雷快递申报的物品类型,这里我稍微提一下,我一共在google play上购买过3部Nexus 4,分别是2012年12月11日、2013年3月5日、2013年9月13日,三部手机申报类型都是电子产品LG E960手机,但我收到的包裹上面打印的,都是Gift。其中第一部被海关抽到,缴纳税款200元,另外两部都没有被抽到。这好像也说明不了申报类型跟是否被海关抽到之间的关系,纯拼人品吧。 Google Play上订单状态 2013-11-01:处理中 2013-11-02:处理中 2013-11-03:处理中 2013-11-04:处理中 2013-11-05:已发货 11月5日早上8点,Nexus 4上提示三封邮件,立马猜到google 发货了。果然,有一封是发货提醒邮件,但google play上仍然显示一部2-3周发货,另外一部处理中。如下图: 拿到google 发货的单子,可以立刻到转运公司网站后台提交到货预报,或者直接提交闪运,闪运的前提是你的帐号内余额充足,不然,扔会被处理到正常预报、扣款发货的缓慢流程中。一般闪运比非闪运大约快2-3个工作日,节省的时间是转运公司承重报价、我们支付待批、他们批价发货这三个流程的时间。 我的三次风雷转运的所需转运费用,最开始价格还没涨,是199块人民币,后来涨到261了,只是一部Nexus 4的价格,按照重量来的(1.2磅)。对于想闪运的朋友,参考下这个价格,再存入适当的转运费,以免闪运时,发现余额不足的窘境。 UPS状态: 风雷转运状态: 2013-11-08:OR俄勒冈州THOMAS签收 2013-11-09:处理中...(OR州运输到CA州途中,看不到这个状态,猜的) 2013-11-10:处理中...(OR州运输到CA州途中,看不到这个状态,猜的) 2013-11-11:处理中...(OR州运输到CA州途中,看不到这个状态,猜的) 2013-11-12:处理中...(OR州运输到CA州途中,看不到这个状态,猜的) 2013-11-13:处理中...(OR州运输到CA州途中,看不到这个状态,猜的) 2013-11-14:处理中...(OR州运输到CA州途中,看不到这个状态,猜的) 2013-11-15:已入库...待发货... 2013-11-16:已发货...(USPS已接收请求)... 2013-11-17:USPS发货中,已经有快递单号 2013-11-18:美国CA到中国上海运输中... 2013-11-19:凌晨两点到达上海...15点发往经EMS片区营部,18点多到达片区经营部 2013-11-19:08:12安排投递,10点30妥投。收到货物 google play海淘Nexus 5交流群:177508087 ,加群验证为:nexus 、google play 2013-01-06 +84 88 0
2012壬辰年总结 早在之前,有人说2012是世界末日;晚在前几天,有人说2012世界已经毁灭,现在只不过是当时的一个分支。2012已成为每个人生命中最特殊的一年,尤其是守候12月21日那天的人。对我来说,2012也是我职业生涯中,最为特殊的一年。 年初,公司变动,项目的原创人员因不明原因,分道扬镳,另起炉灶,鄙人临时被任命带领众弟兄,负责对此项目的持续上线、版本更新等事宜。鄙人资历尚浅,经验薄弱,谢各位领导的提携,谢各位兄弟的帮助,谢原团队的热心交接。才使得队伍从无到有,项目从模糊变清晰,更新持续稳定,性能逐步提升,BUG越来越少。期间无不充满着困难、挑战、兴奋、成就。 在2012年的大部分时间里,所有的精力,几乎全部放在项目上,从技术架构上来说,在兄弟们的帮助下,没有丢技术人的脸,算是完成了自己的及格线。至于领导们如何看,那就是他们的事情了。这里,非常感谢Monster 、 dirtoad 、 神哥 以及后续加入的民哥、华哥,感谢各位辛苦的付出。 在前三个季度的摸索学习前辈们的项目过程中,鄙人学习他们的设计模式,代码风格,复杂业务实现方式,架构设计初衷等等,收益颇丰。期间也给了一些改进方案,比如《CodeIgniter框架中关于DB事务处理的设计缺陷》、《nginx、php-fpm默认配置与性能–TCP socket还是unix domain socket》、《使用APC来保护PHP代码》、《php与mysql通讯那点事》,鄙人觉得最为有成就感的就算APC 的BIN LOAD的创意了,即实现代码保护,又提高性能,方便运维发布,版本回滚。这些方案的实践过程中,感谢运维兄弟们支持,给与鄙人实践的机会,测试验证的机会。也感谢DBA的兄弟教导,指正,让我学习了很多MYSQL的底层知识。 鄙人拜谢。大家可以看到,这些日志的发布日期都是在第四季度,这里,比较惭愧的说,前三个季度实在挤不出写日志的时间,实在太忙了。或许,这是我的借口吧。 最后的冲刺,算是完成我去年《2011辛卯年总结》给自己下的指标,努力实现自己的承诺。亡羊补牢,犹未晚矣,总比那些重新改计划、定计划的人,要强的多。临渊慕渔不如退而结网,Just do it。 在第四季度,公司新的战略方向,老项目,由神哥与民哥继续负责更新维护,多个海外版以及一个简体版,也不是一件简单的事情,再次感谢他们。鄙人被委任到另外一个项目中(或者两个、三个,还是一个?),这里还充满挑战吗?我拭目以待。 新项目的策划正在进行中,后端的架构也在有序重构进行中。在新的RPG webgame项目里,计划全量使用NOSQL作为项目运行中的存储介质,以提高性能,所有的数据交互运算均在NOSQL里进行。Mysql作为背后最终的数据存储层,供数据仓库查询。框架的核心代码,主要有dietoad同学带头研发,monster同学辅助。鄙人就稍微偷懒了,只是盯一下架构方向,很少写核心代码,当然,偶尔觉得代码不符合自己的意图时,也会敲两行。这难免跟其他同学意见不相符,也难免被他们喷过,指出我的做法的不合理之处,给出更合理的方案。导致我也多次认输过,只好按照更合理的方案进行。对此,我也比较开心、欣慰。人生何不是这样? 今年读书甚少,尤其是技术类,寥寥几本,温习。反而文学类到多读了三五本,比如《朝花夕拾》、《城南旧事》、《乡关何处》,但至今不能理解,为什么《从百草园到三味书屋》为什么没被收录到《朝花夕拾》里;也不能理解,英子的玩伴妞儿到底是不是秀贞的孩子;更无法读懂“只是生活太粗砺,理想在里面很难左右逢源”的含义。 目前,正在读的是《目送》,但始终无法读下去... 母亲来这帮带儿子,父亲一个人在家... 身体是革命的本钱,今年我意识更为深重。一次菜饭骨头汤的梅菜扣肉,让我闹肚子整整一周,这绝对是史无前例的,从未有过的耻辱。像我这么强壮的人....还有就是 经常的在周一的下班时间,头晕,伴随着打哈欠,留眼泪。我网上搜了搜,多数说缺氧,想想也合理,周末公司关门,空气不流通,周一正好含氧量低,但我觉得,这应该是身体素质差的原因,明年,无论如何要加强锻炼,除非我是混蛋。 还有一件事,大事,我老了,表现就是我QQ、以及其他网络头像都从一个看上去活泼、可爱、幽默、搞笑的倒立QQ企鹅图标更换为松鸦的头像。我不再年轻。 去年还承诺一件事,就是继续做善事,哪怕很微薄... 多背一公斤: 其他公益: 败家的事情也不是没有,老婆的手机Nokia 7610s,2008年买的,9月份时,突然死机,无法再开机,换了一款索爱Lt18i,大洋2500左右。 我的手机也是,08年买的黑莓8800,9月份也彻底罢工,用了大约4年多,选一款新手机,这简直让我做了个无比艰难的决定。从Lumia 920到华为荣耀2,从MX2到Nexus4,最终还是在google官方买了Nexus 4(谢绝粗粮)。说起Nexus4的购买,简直是个绝对惊险刺激励志的历程。2012年11月14号开售,我当时就下好订单了,后来google wallet认为我的信用卡信息认证有问题,把我的订单给关闭了。16号进行申诉,提交了身份证照片以及visa卡购买windows 8的CDKEY截图,才通过认证。 在11月27号再次放货时,又成功的下了订单,预计2-3周发货,幸运的是,12月11号就发货了,发到OR免税州的某转运公司仓库。由转运公司帮转运到中国。收到美国黑色星期五的影响,此转运公司持续爆仓,导致我的货物一直到12月24号才被CA周仓库入库。25号发货,29号到上海,之后就发往海关,31号的工作日,仍看不到快递单的轨迹更新,2012年是拿不到手机了。一直到现在还在海关躺着。。。(Nexus4手机已经拿到手) (.....) 至于IT业界的见识,也是越发丰富。也能由于微博的原因,得到国内IT前辈贡献开源大作的消息也越来越多。也可能因为我自己眼界增长的原因,在github上看到国产的开源项目也越来越多。他们皆为我榜样。如去年一样,章宜春、鸟哥、算法帝以及微博上阿里淘宝系前辈,他们有着丰富的经验,更值得我学习。 2013呢? 2012-12-31 2 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
使用APC来保护PHP代码 问题就那么发生了: PHP的项目在生产服务器运行时,基本都是明文的文件,如果要保护代码的话,多数使用商业的加密软件,比如Zend Guard或者其他软件。zend也就是php程序语言Zend Engine语法分析引擎的公司出品。该产品会对php代码进行加密转换,费用大约是$600每年(大约4000人民币),按照公司授权。zend guard收费倒不是主要问题,如果单单购买加密功能的话,项目性能会奇差,相信很多朋友都遇到了,尤其是webgame中,刚开服时,几乎都是cpu 100%,load 奇高。使用时,需要服务端在装一个解密的程序,每个脚本文件,在每次被访问时,都需要解码、验证授权。这种繁琐的步骤,无疑浪费了时间、系统资源,也是我们一个项目当初在“友好邻邦”服务器上出现超高占用CPU的原因。(OS load占用高达100倍) 这些都是些什么玩意: 这产品是加密,如果想要程序执行流程优化的话,Zend又很“周到”的为我们提供另外一款产品 Zend Server,每年只需要最低支付$1695美元(大约11000人民币)(其他黄金版、白金版需要上万美元)。。。这样一来,我们的产品被绑架在zend上,如果我们需要定制功能的话,那是不可想象的。 如果是zend产品有bug,那我们只能干等着,等他们解决。除了zend这款,还有另外一款代码加密产品:ioncube,费用倒是不高,大约400美元每年。但用户量少,产品是否稳定不清楚,不开源,社区支持不是很好。 找寻: 有没有一款可以保护代码,社区支持好,用户量大,反馈处理及时,费用低的产品吗? 幸运的是,有这么一款开源产品APC:http://pecl.php.net/package/APC Alternative PHP Cache (APC)是一款php代码加速产品,该产品作为php程序的一个拓展,是一个开放自由的PHP opcode 缓存。它的目标是提供一个自由、 开放,和健全的框架用于缓存和优化PHP的中间代码。 早在3-4个月之前,鸟哥博客上一篇文章《关于PHP的编译和执行分离》中提到APC来作为PHP代码保护的方案。从文中可以看出,鸟哥的想法是每个php文件,导出一个opcode 的bin文件,加载时,也是挨个加载,这样也实现了代码保护,但一个项目几百个php文件的话,也得相应存在几百个bin文件,量比较大,操作比较复杂,管理不方便,不好做版本验证(以后会提到)。末学比较倾向于单个bin文件的导出,单个opcode bin文件的加载。而且,单个bin文件的加载,可以避免项目中出现部分文件跟整体版本不一致的情况发生,运维同事再也不用担心个别文件跟整个项目版本不一致的情况了。 原来是你: APC是替换了php zend引擎的zend_compile_file函数,来决定是否重新对php脚本文件的读取,扫描、解释、编译等步骤。启用apc之后,将跳过读取、扫描、解释、编译这几步,节省内存、CPU开销,直接执行OPCODE。Apc还提供对php源码的opcode的缓存,导出到一个二进制文件中,还提供加载这个二进制文件。起初,我们开始使用这个功能时,遇到该拓展的多个BUG,都已经提交到php官方,分别是: BUG #62757 、BUG #62765 、BUG #62825 ,并积极配合php内核组开发成员重现BUG,抓获coredump,提取bug demo代码,在PHP官方成员-APC拓展开发组长Xinchen Hui(以下称鸟哥 laruence)的帮助下,很快的修复了这些BUG 。 如此繁琐: 这样仍存在一个繁琐的步骤,即php-fpm每次启动时,没有自动加载bin文件,需要管理员去手动执行或者访问脚本,(该脚本内使用apc_bin_load/apc_bin_loadfile函数去加载bin文件)让php-fpm加载bin文件,对于单大区多台前端,以及单服务器运行多个php-fpm主进程来说,如何判断那个php-fpm主进程是否成功加载,这是个很麻烦的问题。对于程序执行opcode清除之后,又需要再次加载,这样是个非常复杂繁琐的过程,也容易出问题。 自力更生: 可否在fpm启动时,自动加载bin的功能呢?末学阅读了APC的源码,照葫芦画瓢的添加了自动加载的opcode bin文件的功能。 我们是在 APC SVN http://svn.php.net/viewvc/pecl/apc/trunk/ 的 r327454版本基础上,在apc_module_init函数中,模块初始化时,增加了opcode bin文件的自动加载功能,patch代码见:https://github.com/cfc4n/cnxct/blob/master/apc_r327454_add_preload_binfile.patch 。有兴趣的朋友可以git使用下。当然,末学水平有限,难免存在BUG,还请见谅。 后来,末学将此patch提交到PHP官方,请他们审核一下,是否可以收入apc官方中,免得我们以后都一直作为patch来使用。或者协助完善,或者给出更好的解决办法。 很伤心,未被采纳,原因是这是小众的需求。但末学表示不解,那apc_preload_path这个自动加载user cache的功能不也是小众需求吗?或许,这是他们委婉的拒绝方式。 (同时,还发现了APC自带的一个未公布的功能,自动加载user cache的功能,配置项为apc.preload_path,配置的值是目录地址,目录下可有多个文件,这些文件也应该是apc_bin_dump/apc_bin_dumpfile函数导出的user cache。看来,官方也有这么个打算。但为何没有实现 opcode bin文件 的自动加载呢?) 如何使用: opcode bin文件导出与导入: 导出的php脚本,末学提供一份,需要的朋友,可以参照修改一下即可: https://github.com/cfc4n/cnxct/blob/master/apc_dump.php 导出成功后,会在PROJECTROOT目录下面生成一个dumproot目录,dumproot目录中的所有文件,就是你需要上传到生产服务器上的文件。bin目录中,会生成两个文件,一个是opcode的bin文件,一个是生成之前的php文件的md5 hash值。其他目录就是项目被保护的目录,目录中文件都是空的php文件。将dumproot目录上传至生产服务器,保持项目路径正确,重启fpm即可。可通过apc拓展源码包内的apc.php查看被缓存文件数。如图: 版本更新、回滚: 遇到bug需要修复时,只需要在opcode bin文件导出服务器上,重新导出一个bin文件,将此bin文件上传到生产服务器,替换到之前的bin文件即可。偶尔会遇到项目因种种原因,需要对项目进行回滚,如果使用我们的方案之后,会发现所有php文件都已经是空的了。而我们的回滚更方便,只需要将之前需要回滚的版本的bin文件,替换到当前的文件即可。或者更改php.ini中apc.preload_binfile的路径。 版本检测 当怀疑个别文件不是某个版本的程序是,怎么办呢?apc提供了另外一个参数 apc.file_md5来存储被cache文件的md5 hash。但当使用已到处的opcode bin文件时,apc却没有重新抓去md5 hash,也没有重新存入apc中,导致我们看到的md5值是个错误的hash值。为此,末学斗胆做了修改,并提供了patch,且提交该BUG #63491到pecl apc官方,斗胆请官方采纳。当打上该patch之后,您可以在apc.php 看到这些信息。并且,跟导出bin文件时,生成的md5 hash文件中校对以确认哦。(2012/12/17 更新,官方已修复) 批量检测也是可以的,参加末学另一个脚本:https://github.com/cfc4n/cnxct/blob/master/check.php APC其他已知bug: 一、php5.4.x中,class中的array()静态成员属性在跨服务器使用时,产生core,已经提交到 BUG #63636(此bug由recoye发现) 临时解决方案如下: 使用php5.4.x时,不要在class中定义 $_user = array(),直接定义为 $_user; 不要使用php5.4.x 删除或注释apc_compile.c的1992、2003行(apc 3.1.13),即 zval_ptr_dtor(&src->default_static_members_table[i]); 字样的代码。 等待PHP官方解决,或微博@鸟哥,催鸟哥解决 二、web访问apc_dump.php 去生成的bin文件,在cli模式下使用时,会在RSHUTDOWN时,即请求结束时,回收系统资源时,会产生core。(该结果的php版本为php5.3.x,初步确认为上一个bug有密切关系,暂未提交至bugs.php.net) 这两个bug以末学的理解来看,与末学的patch无关,为apc的bug。 注意事项: 导出bin文件的服务器上,不要开启 apc.preload_binfile ,避免bin文件存在时,使用bin文件内的代码,而不使用php文件内的代码,导致导出的代码不是你想要的。 php5.4.x class中array 静态成员以及属性 操作系统32bit、64bit的导出的bin文件不能相互乱用。(乱用将产生core) 导出服务器的web路径跟目标生产服务器的web路径一致 生产服务器使用bin文件之后,必须存在同名的php文件,内容可以为空。 apc.ttl、apc.gc_ttl参数保持为0 apc.serializer 如果改用其他序列化函数发生问题,那么请保持为空,或者为 apc.serializer=php apc.num_files_hint的值务必大于等于cache files 的数量,可以先设置大点,再通过生产服务器上观察一段时间来确认 啰嗦一下: 关于32bit\64bit服务器上导出opcode bin文件的乱用问题,将产生如下core 因为long之类类型,在不同bit操作系统上的长度不一致导致内存越界的BUG。 末学不知道这算不算bug,或者您别混用,或者APC程序上也可以解决... 惊喜: 不光可以代码保护,性能提升,方便运维,还可以防黑客入侵哦。(项目目录不允许创建新文件,那么即使黑客改了php脚本,php仍会去apc里读,也不会去读取它。哪怕黑客执行了 apc_cache_clean清除opcode cache,我们的补丁仍会自动加载,那么黑客的行为还是阻拦了。) 备注: 此patch已经在我们的“友好邻邦”服务器上稳定运行3-4个月左右,无异常案例,各位可放心使用。 Apc不算一个php encoder,算是个cache,一个opcode cache,起到中间码缓存的作用,故本文中用“代码保护”,而不是“代码加密”。 但实质上实现了我们的目的,起到保护源代码的作用,那怕可以逆向,也是有一定难度的。 PS:感谢Ivon的openstack,感谢终极修炼师的协助测试。 PPS: 如果你能保证php文件不会被SAPI形式访问到的话,只会被include/require等访问到的话,甚至连同名空文件都不用放。比如单入口框架的项目,只要放个空的入口文件即可。 (感谢recoye纠正) PPPS: 更新日期:2014/10/17 近来,好多朋友问我要完整版,鉴于apc已经被官方删了,那个SVN地址无法正常checkout,故我在硬盘里找了个压缩包,应该是可用的版本,如果有其他异常,请留言,谢谢。 适用于PHP5.3的完整拓展:点击这里下载包含patch的完整版apc 2012-11-29 +39 43 0