不要在linux上启用net.ipv4.tcp_tw_recycle参数 本文为翻译英文BLOG《Coping with the TCP TIME-WAIT state on busy Linux servers》,但并非完整的翻译,译者CFC4N对原文理解后,进行了调整,增加了相关论点论据,跟原文稍有不同。翻译的目的,是为了加深自己知识点的记忆,以及分享给其他朋友,或许对他们也有帮助。文章比较长,没耐心请点关闭。 不要启用 net.ipv4.tcp_tw_recycle linux 内核文档中,对net.ipv4.tcp_tw_recycle的描述并不是很明确。 tcp_tw_recycle (Boolean; default: disabled; since Linux 2.4)[译者注:来自linux man tcp的描述] Enable fast recycling of TIME-WAIT sockets. Enabling this option is not recommended since this causes problems when working with NAT (Network Address Translation). 启用TIME-WAIT状态sockets的快速回收,这个选项不推荐启用。在NAT(Network Address Translation)网络下,会导致大量的TCP连接建立错误。 与其功能相似的参数net.ipv4.tcp_tw_reuse,手册里稍微有点描述,如下: tcp_tw_reuse (Boolean; default: disabled; since Linux 2.4.19/2.6) Allow to reuse TIME-WAIT sockets for new connections when it is safe from protocol viewpoint. It should not be changed without advice/request of technical experts. //从协议设计上来看,对于TIME-WAIT状态的sockets重用到新的TCP连接上来说,是安全的。(用于客户端时的配置) 这里的注释说明非常的少,我们发现,网上很多linux参数调整指南都建议把这些参数net.ipv4.tcp_tw_recycle 设置1「启用」,用于快速减少在TIME-WAIT状态TCP连接数。 但是,在TCP(7)手册中,参数net.ipv4.tcp_tw_recycle 非常蛋疼,尤其是在普通用户家中,有多台设备,或者网吧、公司等多台设备,共用同一个NAT设备环境下,TW回收选项是很有问题的面向公共服务器作为它不会把手连接两台不同的计算机上,这问题很难发现,无从下手。 Enable fast recycling of TIME-WAIT sockets. Enabling this option is not recommended since this causes problems when working with NAT (Network Address Translation). 启用TIME-WAIT状态sockets的快速回收,这个选项不推荐启用。在NAT(Network Address Translation)网络下,会导致大量的TCP连接建立错误。如果没有技术大神的指点的话,千万不要去改动他。 下文将给予更详细的解释,希望可以纠正互联网上错误的观点,尤其是转载比较多的内容,搜索时,往往排在前面,使用者往往接触到的都是不严谨的或者是错误的知识点。 正如此文,在 net.ipv4.tcp_tw_recycle 控制参数中,尽管很多地方写的是ipv4,但对ipv6同样实用。此外,我们这里聊的是Linux TCP协议栈,在linux上可能会受到Netfilter影响,稍微有差异。 关于TCP连接的TIME-WAIT状态,它是为何而生,存在的意义是什么? 让我们回忆一下,什么是TCP TIME-WAIT状态?如下图 这图中的流程不是很好理解,再看一张流程更清晰的图 当TCP连接关闭之前,首先发起关闭的一方会进入TIME-WAIT状态,另一方可以快速回收连接。 可以用ss -tan来查看TCP 连接的当前状态 TIME-WAIT状态的作用 对于TIME-WAIT状态来说,有两个作用 一、人尽皆知的是,防止上一个TCP连接的延迟的数据包(发起关闭,但关闭没完成),被接收后,影响到新的TCP连接。(唯一连接确认方式为四元组:源IP地址、目的IP地址、源端口、目的端口),包的序列号也有一定作用,会减少问题发生的几率,但无法完全避免。尤其是较大接收windows size的快速(回收)连接。RFC1137解释了当TIME-WAIT状态不足时将会发生什么。如果TIME-WAIT状态连接没有被快速回收,会避免什么问题呢?请看下面的例子: 缩短TIME-WAIT的时间后,延迟的TCP 包会被新建立的TCP连接接收。 二、另外一个作用是,当最后一个ACK丢失时,远程连接进入LAST-ACK状态,它可以确保远程已经关闭当前TCP连接。如果没有TIME-WAIT状态,当远程仍认为这个连接是有效的,则会继续与其通讯,导致这个连接会被重新打开。当远程收到一个SYN 时,会回复一个RST包,因为这SEQ不对,那么新的连接将无法建立成功,报错终止。 如果远程因为最后一个ACK包丢失,导致停留在LAST-ACK状态,将影响新建立具有相同四元组的TCP连接。 RFC 793中强调TIME-WAIT状态必须是两倍的MSL时间(max segment lifetime),在linux上,这个限制时间无法调整,写死为1分钟了,定义在include/net/tcp.h 曾有人提议将TCP TIME-WAIT时间改为一个可以自定义配置的参数,但被拒绝了,其实,这对TCP规范,对TIME-WAIT来说,是利大于弊的。 那么问题来了 我们来看下,为什么这个状态能影响到一个处理大量连接的服务器,从下面三个方面来说: 新老连接(相同四元组)在TCP连接表中的slot复用避免 内核中,socket结构体的内存占用 额外的CPU开销 而ss -tan state time-wait|wc -l的结果,并不能说明这些问题。 Connection table slot连接表槽 处于TIME-WAIT状态的TCP连接,在链接表槽中存活1分钟,意味着另一个相同四元组(源地址,源端口,目标地址,目标端口)的连接不能出现,也就是说新的TCP(相同四元组)连接无法建立。 对于web服务器来说,目标地址、目标端口都是固定值。如果web服务器是在L7层的负载均衡后面,那么源地址更是固定值。在LINUX上,作为客户端时,客户端端口默认可分配的数量是3W个(可以在参数net.ipv4.up_local_port_range上调整)。 这意味着,在web服务器跟负载均衡服务器之间,每分钟只有3W个端口是处于established状态,也就大约500连接每秒。 如果TIME-WAIT状态的socket出现在客户端,那这个问题很容易被发现。调用connect()函数会返回EADDRNOTAVAIL,程序也会记录相关的错误到日志。 如果TIME-WATI状态的socket出现在服务端,问题会非常复杂,因为这里并没有日志记录,也没有计数器参考。不过,可以列出服务器上当前所有四元组连接的数量来确认 解决办法是,增加四元组的范围,这有很多方法去实现。(以下建议的顺序,实施难度从小到大排列) 修改net.ipv4.ip_local_port_range参数,增加客户端端口可用范围。 增加服务端端口,多监听一些端口,比如81、82、83这些,web服务器前有负载均衡,对用户友好。 增加客户端IP,尤其是作为负载均衡服务器时,使用更多IP去跟后端的web服务器通讯。 增加服务端IP。 当然了,最后的办法是调整net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle。但不到万不得已,千万别这么做,稍后再讲。 内存 保持大量的连接时,当多为每一连接多保留1分钟,就会多消耗一些服务器的内存。举个栗子,如果服务器每秒处理了1W个新的TCP连接,那么服务器将会存货1W/s*60s = 60W个TIME-WAIT状态的TCP连接,那这将会占用多大的内存么?别担心,少年,没那么多。 首先,从应用的角度来看,一个TIME-WAIT状态的socket不会消耗任何内存:socket已经关了。在内核中,TIME-WAIT状态的socket,对于三种不同的作用,有三个不同的结构。 一、“TCP established hash table”的连接存储哈希表(包括其他非established状态的连接),当有新的数据包发来时,是用来定位查找存活状态的连接的。 该哈希表的bucket都包括在TIME-WAIT连接列表以及正在活跃的连接列表中(netstat -antp命令的结果中,没PID的TIME_WAIT状态连接,跟有PID的活跃连接两种)。 该哈希表的大小,取决于操作系统内存大小。在系统引导时,会打印出来,dmesg日志中可以看到。 这个数值,有可能被kernel启动参数thash_entries(设置TCP连接哈希表的最大数目)的改动而将其覆盖。 在TIME-WAIT状态连接列表中,每一个元素都是一个tcp_timewait_sock结构体,其他状态的连接都是tcp_sock结构体。 二、“death row”连接列表,是用来终止TIME-WAIT状态的连接的,这会在他们过期之前,开始申请。它占用的内存空间,跟在连接哈希表中的一样。这个结构体hlist_node tw_death_node是inet_timewait_sock的一个成员,如上代码的倒数第二行。 三、端口绑定的哈希表,用来存储绑定端口跟其他参数,用来确保当前端口没有被使用的,比如在listen监听时的指定端口,或者连接其他socket时,系统动态分配的端口。该哈希表的大小跟连接哈希表大小一样。 每个元素都是inet_bind_socket结构体。每个绑定的端口都会有一个元素。对于web服务器来说,它绑定的是80端口,其TIME-WAIT连接都是共享同一个entry的。另外,连接到远程服务器的本地连接,他们的端口都是随机分配的,并不共享其entry。 所以,我们只关心结构体tcp_timewait_sock跟结构体inet_bind_socket所占用的空间大小。每一个连到远程,或远程连到本地的一个TIME-WAIT状态的连接,都有一个tcp_timewait_sock结构体。还有个结构体inet_bind_socket,只会在连到远程的连接会存在,远程连过来的连接没这个结构体。 tcp_timewait_sock结构体的大小只有168 bytes,inet_bind_socket结构体为48bytes: 所以,当服务器上有4W个连进来的连接进入TIME-WAIT状态时,才用了10MB不到的内存。如果服务器上有4W个连接到远程的连接进入TIME-WAIT状态时,才用了2.5MB的内存。再来看下slabtop的结果,这里测试数据是5W个TIME-WAIT状态的连接结果,其中4.5W是连接到远程的连接: 命令执行结果原样输出,一个字符都没动。TIME-WAIT状态的连接占用内存非常的小。如果你的服务器上要处理每秒成千上万的新建TCP连接,你可能需要多一点的内存才能 正确无误的跟客户端做数据通信。但TIME-WAIT状态连接的内存占用,简直可以无视。 CPU 在CPU这边,查找一个空闲端口的操作,还是蛮珍贵的。这由inet_csk_get_port() 函数,加锁,遍历整个空闲端口列表实现。这个哈希表里条目数量大通常不是问题,如果服务器上存在大量连接到远程TIME-WAIT状态的连接(比如FPM连redis、memcache之类),都会同享相同的profile,这个特性会非常快的按照顺序找到一个新的空闲端口。 其他解决办法 如果你读了上面的章节后,仍对TIME-WAIT状态的连接存有疑问,那么接着看吧: 禁用socket延迟关闭「译者注1:以ubuntu 12.04为例,linger结构体定义在:/usr/src/linux-headers-3.2.0-23/include/linux/socket.h」 禁用net.ipv4.tcp_tw_reuse 禁用net.ipv4.tcp_tw_recycle 当close被调用时,SOCKET需要延迟关闭(lingering),在内核buffers中的残留数据将会发送到远程地址,同时,socket会切换到TIME-WAIT状态。如果禁用此选项,则调用close之后,底层也会关闭,不会将Buffers中残留数据未发送的数据继续发送。 不过呢,应用程序可以选择禁用socket lingering延迟关闭行为。关于socket lingering 延迟关闭,下面两个行为简单描述一下: 第一种情况,close函数后,并不会直接终止该四元组连接序号,而是在buffers任何残留数据都会被丢弃。该TCP连接将会收到一个RST的关闭信号,之后,服务端将立刻销毁该(四元组)连接。 在这种做法中,不会再有TIME-WAIT状态的SOCKET出现。第二种情况,如果当调用close函数后,socket发送buffer中仍然有残留数据,此进程将会休眠,直到所有数据都发送完成并确认,或者所配置的linger计时器过期了。非阻塞socket可以设置不休眠。如上,这些过程都都在底层发生,这个机制确保残留数据在配置的超时时间内都发送出去。 如果数据正常发送出去,close包也正常发送,那么将会转换为TIME-WAIT状态。其他异常情况下,客户端将会收到RST的连接关闭信号,同时,服务端残留数据会被丢弃。 这里的两种情况,禁用socket linger延迟关闭不是万金油。但在HAproxy,Nginx(反代)场景中,在TCP协议上层的应用上(比如HTTP),比较合适。同样,也有很多无可厚非的理由不能禁用它。 net.ipv4.tcp_tw_reuse TIME-WAIT状态是为了防止不相关的延迟请求包被接受。但在某些特定条件下,很有可能出现,新建立的TCP连接请求包,被老连接(同样的四元组,暂时还是TIME-WAIT状态,回收中)的连接误处理。RFC 1323 实现了TCP拓展规范,以保证网络繁忙状态下的高可用。除此之外,另外,它定义了一个新的TCP选项--两个四字节的timestamp fields时间戳字段,第一个是TCP发送方的当前时钟时间戳,而第二个是从远程主机接收到的最新时间戳。 启用net.ipv4.tcp_tw_reuse后,如果新的时间戳,比以前存储的时间戳更大,那么linux将会从TIME-WAIT状态的存活连接中,选取一个,重新分配给新的连接出去的TCP连接。 连出的TIME-WAIT状态连接,仅仅1秒后就可以被重用了。 如何确通讯安全性? TIME-WAIT的第一个作用是避免新的连接(不相关的)接收到重复的数据包。由于使用了时间戳,重复的数据包会因为timestamp过期而丢弃。 第二个作用是确保远程端(远程的不一定是服务端,有可能,对于服务器来说,远程的是客户端,我这里就用远程端来代替)是不是在LAST-ACK状态。因为有可能丢ACK包丢。远程端会重发FIN包,直到 放弃(连接断开) 等到ACK包 收到RST包 如果 FIN包接及时收到,本地端依然是TIME-WAIT状态,同时,ACK包也会发送出去。 一旦新的连接替换了TIME-WAIT的entry,新连接的SYN包会被忽略掉(这得感谢timestramps),也不会应答RST包,但会重传FIN包。 FIN包将会收到一个RST包的应答(因为本地连接是SYN-SENT状态),这会让远程端跳过LAST-ACK状态。 最初的SYN包将会在1秒后重新发送,然后完成连接的建立。看起来没有啥错误发生,只是延迟了一下。 另外,当连接被重用时,TWrecycled计数器会增加的。「译者注:见/proc/net/netstat 中TWrecycled的值」 net.ipv4.tcp_tw_recycle 这种机制也依赖时间戳选项,这也会影响到所有连接进来和连接出去的连接。「译者注:linux上tcp_timestamps默认开启」 TIME-WAIT状态计划更早的过期:它将会在超时重发(RTO)间隔后移除(底层会根据当前连接的延迟状况根据RTT来计算RTO值,上篇《PHP-FPM中backlog参数变更的一些思考》也有提到过,比较复杂的算法)。可以执行ss指令,获取当前存活的TCP连接状态,查看这些数据。「译者注:linux指令ss的结果中rto,rtt值单位均为ms」 Linux将会放弃所有来自远程端的timestramp时间戳小于上次记录的时间戳(也是远程端发来的)的任何数据包。除非TIME-WAIT状态已经过期。 当远程端主机HOST处于NAT网络中时,时间戳在一分钟之内(MSL时间间隔)将禁止了NAT网络后面,除了这台主机以外的其他任何主机连接,因为他们都有各自CPU CLOCK,各自的时间戳。这会导致很多疑难杂症,很难去排查,建议你禁用这个选项。另外,对方上TCP的LAST-ACK状态是体现本机net.ipv4.tcp_tw_recycle的最好数据。 总结 最合适的解决方案是增加更多的四元组数目,比如,服务器可用端口,或服务器IP,让服务器能容纳足够多的TIME-WAIT状态连接。在我们常见的互联网架构中(NGINX反代跟NGINX,NGINX跟FPM,FPM跟redis、mysql、memcache等),减少TIME-WAIT状态的TCP连接,最有效的是使用长连接,不要用短连接,尤其是负载均衡跟web服务器之间。尤其是链家事件中的PHP连不上redis。 在服务端,不要启用net.ipv4.tcp_tw_recycle,除非你能确保你的服务器网络环境不是NAT。在服务端上启用net.ipv4.tw_reuse对于连接进来的TCP连接来说,并没有任何卵用。 在客户端(尤其是服务器上,某服务以客户端形式运行时,比如上面提到的nginx反代,连接着redis、mysql的FPM等等)上启用net.ipv4.tcp_tw_reuse,还算稍微安全的解决TIME-WAIT的方案。再开启net.ipv4.tcp_tw_recycle的话,对客户端(或以客户端形式)的回收,也没有什么卵用,反而会发生很多诡异的事情(尤其是FPM这种服务器上,相对nginx是服务端,相对redis是客户端)。 最后引用一下W. Richard Stevens在《UNIX网络编程》的一句话 The TIME_WAIT state is our friend and is there to help us (i.e., to let old duplicate segments expire in the network). Instead of trying to avoid the state, we should understand it. 译者:存在即合理,勇敢面对,而不是逃避。 译者注: 译者根据原文做了翻译,并增加了相关论点论据,跟原文稍有不同。 linux的netstat命令ss命令的结果中,前者是TIME_WAIT之类,分个字符是下划线,而后者是TIME-WAIT的中划线。 残酷的案例: Clients behind NAT/Stateful FW will get dropped, 99.99999999% of time should never be enabledTuning TCP and nginx on ec2中30页内容 参考资料: Tuning TCP and nginx on ec2 -p30 TIME_WAIT and its design implications for protocols and scalable client server systems tcp_tw_recycle和tcp_timestamps导致connect失败问题 记一次TIME_WAIT网络故障 打开tcp_tw_recycle引起的一个问题 Transmission Control Protocol 2017年10月更新 在linux 4.1内核中,net.ipv4.tcp_tw_recycle参数将被移除。net.ipv4.tcp_tw_recycle has been removed from Linux 4.12. The tcp_tw_recycle was already broken for connections behind NAT, since the per-destination timestamp is not monotonically increasing for multiple machines behind a single destination address. After the randomization of TCP timestamp offsets in commit 8a5bd45f6616 (tcp: randomize tcp timestamp offsets for each connection), the tcp_tw_recycle is broken for all types of connections for the same reason: the timestamps received from a single machine is not monotonically increasing, anymore. Remove tcp_tw_recycle, since it is not functional. Also, remove the PAWSPassive SNMP counter since it is only used for tcp_tw_recycle, and simplify tcp_v4_route_req and tcp_v6_route_req since the strict argument is only set when tcp_tw_recycle is enabled. 2015-07-27 +7 11 0
ububtu server 14.04上,PHP5-FPM服务的启动方式变更 在一位前辈微博上看到,他总是给自己找点事做,让自己忙碌起来,严于律己。我甚有感触,相比之下自惭形秽,一直做不到,这应该就是大神跟我等小菜之间的差距吧。如今,事情主动找到我,而且是一而再再而三的找上来,我再去逃避,再不认真去面对这些问题,实在说不过去。 朋友在群里问了问题,ubuntu server 14.04版本,apt-get install的PHP5-FPM,自己编译了phpredis,却发现始终没成功加载phpredis这个扩展。 ubuntu@shnj-b-batch-30-222:~$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.2 LTS Release: 14.04 Codename: trusty 以及他的操作方式 而结果,却无任何回显。ps aux也看到所有FPM原来进程都还在,尝试sudo /etc/init.d/php5-fpm stop也无法停止掉进程,也无回显。无奈之下killall php5-fpmkill掉了所有FPM进程。当他再尝试sudo /etc/init.d/php5-fpm start,却发现PHP5-FPM启动不起来了。 那么问题来了,为什么sudo /etc/init.d/php5-fpm stop无法停止掉PHP5-FPM,是SIGNAL信号没传到FPM那边吗?为什么KILL之后不能重新启动了?是php加载了拓展时遇到意外,进程崩溃退出了吗?如何排查?群内有朋友建议上gdb来查看是不是php加载扩展出现意外了。但我觉得,php加载拓展前,还有很多事情要做,php加载扩展是/etc/init.d/php5-fpm 脚本执行/usr/sbin/php5-fpm去启动的,在执行之前,还有很多事情去做,比如获取系统shell变量,获取获取系统其他配置。执行后,还会去去读ini配置,读取拓展的so文件等等。我想先看下程序大约终止之前的文件操作走到了哪里。故我决定先用strace看下。 从strace结果可以看到,程序/etc/init.d/php5-fpm依次访问(open\stat)了 /usr/sbin/php5-fpm /etc/default/rcS /lib/init/vars.sh /lib/lsb/init-functions /sbin/initctl 之后,就以+++ exited with 1 +++结束了。并没有执行/usr/sbin/php5-fpm来操作对应的pidfile,执行stop指令。下面看下/etc/init.d/php5-fpm里如何写 大约可以看出,脚本执行到 init_is_upstart() 函数判断后,就退出了,exit 1 。从代码中文件访问(这里包括open、stat)顺序,以及退出是exit(1)的错误产生后退出。这也可以在执行echo $?来确认。既然知道这里退出的,那么修复方法就很简单了,注释掉 if init_is_upstart; then \ exit 1 \ fi这三行,就好了。 注释掉/etc/init.d/php5-fpm的27、28、29行之后,尝试/etc/init.d/php5-fpm start\stop\restart“果然”均正常... 注释掉/etc/init.d/php5-fpm的27、28、29行之后,就可以了???? 为什么代码里要加上那句判断,为什么判断后就退出了,不执行FPM的相关操作了?这么改之后,虽然FPM的启动、重启、停止都声场,但总觉得哪里不对,回到虚拟机上,我多测试了几遍,出现了更奇怪的事情。在上面的程序启动后,我随手敲了service php5-fpm stop返回了stop: Unknown instance:,嗯?service服务管理程序不能管理/etc/init.d/php5-fpm启动的程序?我记得是可以的,又立刻到服务器上做了测试,服务器是UBUNTU server 12.04 的,测试结果为他们可以互相管理对方启动的进程。 问题又来了,为什么ubuntu 12.04上都正常,而在ubuntu 14.04上不可以呢? 是不是我注释掉的那三行代码导致的?系统更新,变更了哪些内容?为此,我认真的看了service脚本的代码。 解释这段代码时,$SERVICE以php5-fpm为例,代码中写着,若系统中存在/etc/init/${SERVICE}.conf、同时initctl存在,且initctl version结果中包含upstart字符串,则执行start\stop\reload ${SERVICE} ${OPTIONS}。若不符合条件,则执行$SERVICEDIR/$SERVICE脚本来管理该程序。这里根据当前操作系统当前使用的init系统,来选择使用upstart还是sysvinit形式来管理服务。 同时,service在手册里的注释如下: DESCRIPTION service runs a System V init script or upstart job in as predictable environment as possible, removing most environment variables and with current working directory set to /. The SCRIPT parameter specifies a System V init script, located in /etc/init.d/SCRIPT, or the name of an upstart job in /etc/init. The existence of an upstart job of the same name as a script in /etc/init.d will cause the upstart job to take precedence over the init.d script. The supported values of COMMAND depend on the invoked script, service passes COMMAND and OPTIONS to the init script unmodified. For upstart jobs, start, stop, status, are passed through to their upstart equivilents. Restart will call the upstart 'stop' for the job, followed immediately by the 'start', and will exit with the return code of the start command. All scripts should support at least the start and stop commands. As a special case, if COMMAND is --full-restart, the script is run twice, first with the stop command, then with the start command. This option has no effect on upstart jobs. service --status-all runs all init scripts, in alphabetical order, with the status command. This option only calls status for sysvinit jobs, upstart jobs can be queried in a similar manner with initctl list'. FILES /etc/init.d The directory containing System V init scripts. /etc/init The directory containing upstart jobs. 根据man手册的解释,upstart是ubuntu特有的init管理系统,最初也是使用system v init系统来管理服务,现在debian好像也用了。system v init是最初大多数类unix系统上的init系统。后来system v init系统的性能问题,对热插拔的USB支持问题等诸多问题,大部分类unix系统改为了systemd。 但大多数linux都对system v init做了兼容。所以,service脚本为了支持两种init系统,做了封装,根据当前系统的init系统,来选择执行响应的操作。在ubuntu上, upstart系统的程序是initctl,而且initctl start\stop\restart\reload可简写为start\stop\restart\reload。 system v init系统的程序是start-stop-daemon(centos 上是daemon程序),service是对两种init系统执行的代码封装。/etc/init.d是System V init系统脚本的执行文件目录。/etc/init是upstart的配置文件目录。我特意对比虚拟机跟服务器上的service脚本的区别,只发现对upstart的判断更严格了,并没有太大区别...不过,我发现了一件奇葩的事情...就是service php5-fpm status指令跟service --status-all|php5-fpm的结果(在ubuntu 14.04上)并不一样,前者是upstart管理的服务状态数据,即upstart init系统的 status 指令来查看php5-fpm状态的;而后者是system v init的结果,即从$SERVICEDIR/$SERVICE下的脚本程序获取状态的,这...... 但是。。。也就是说知道service脚本的封装操作后,然而,这并没有解决我刚刚的疑问「为什么ubuntu 12.04上service脚本跟/etc/init.d/php5-fpm可以相互管理,而14.04上不可以?」,尽管我知道system v init跟upstart不能对同一个服务的状态互通,但为什么12.04上却可以?这到底是为什么呢?在我做了很多测试(走了很多弯路)之后,决定去服务器上再检查一遍环境。在我写出检查结果之前,我说下走弯路期间的发现吧。 之前提到过一个错stop: Unknown instance:,这也比较好理解,php5-fpm程序是用(注释那3行代码后)/etc/init.d/php5-fpm启动的,即system v init管理的。而使用service php5-fpm stop时,显然不能获取到PHP5-FPM服务的状态的,故返回stop: Unknown instance:。如果我反过来,使用system v init来关闭upstart启动的php5-fpm会发生什么呢? 细心的朋友会发现,原来的FPM主进程已经不存在,残留了2个子进程(父进程为1,是因为其父进程意外退出,被PID 为1 的init进程接管),而且, 从最后剩余的pid来看,也不是原来的子进程了。 同时,原来的pid file 跟 unix domain socket 监听文件都也残留....可以看出start-stop-daemon程序可以关闭原来的PHP5-FPM进程(直接执行kill指令,发送SIGTERM信号,当然可以关闭其他进程)。为什么会出现进程残留,原来子进程ID都不一样的问题。查看/var/log/upstart/php5-fpm.log中,有这么几条日志 system v init系统的start-stop-daemon调用kill指令,发送SIGTERM信号给FPM进程,让其退出。然而upstart发现进程退出,进行保护又启新的进程,然后又被start-stop-daemon 发送SIGKILL信号杀了。然后子进程还存活着...同时,原来的pid file 跟 unix domain socket 监听文件都也残留....也导致后来再启动时,发现sock文件依旧存在,无法再次启动。 回到刚刚的话题上,我去重新确认了配置,在ubuntu 12.04上,/lib/lsb/init-functions中,并没有init_is_upstart函数的定义。最主要的是,在/etc/init/目录下,根本没有/etc/init/php5-fpm.conf这文件的存在,也就是说,ubuntu 12.04上 是以system v init 来管理php5-fpm的。。。知道真相的我,眼泪都要留下来。。。 好了,最初的修改/etc/init.d/php5-fpm注释掉的3行代码,显然要还原。解决PHP5-FPM(包括但不限于)启动、停止、重启的方式,就用系统封装好的service吧。 总结本次获得的新技能 推荐使用service来管理服务,让service根据当前操作系统,自己确定选择用upstart、sysvinit、systemd来管理程序,而非我们主观上的认定。 ubuntu 1404 上 service XXXX status 跟 service --status-all|grep XXXX 的获取XXXX服务状态的方式不一样。前者是upstart,后者是sysvinit. UBUNTU 1404 使用sysvinit方式(/etc/init.d/php5-fpm stop)来关闭upstart启动(start php5-fpm或service php5-fpm start)的FPM进程时,会造成FPM进程无法全部杀死,直接发送SIGTERM信号给FPM主进程,然后upstart监控发现原来PID的进程不存在了,立刻创建。然后sysvinit(start-stop-daemon进程)二次检测,又继续kill。新启动的FPM子进程的所属主进程被kill了,他们的父ID没了,就被PID为1的init进程接管...发现类似的情况时,多数是主进程被意外终止掉了。 类似案例: upstart script ignores some php-fpm.conf parameters, cannot be bypassed in preference of SystemV init script. Ubuntu14.04重启网卡不生效 对于这些案例,相应的程序提供了一定的信息输出,比如“Stopping or restarting the networking job is not supported. ”起码告诉我们说不支持这种方式了,FPM应该echo一句话,告诉使用者不支持这种方式管理此服务了,这样,大家都方便了。 参考资料: 浅析 Linux 初始化 init 系统,第 1 部分: sysvinit 浅析 Linux 初始化 init 系统,第 2 部分: UpStart 浅析 Linux 初始化 init 系统,第 3 部分: systemd Operating system service management Linux init 2015-07-08 3 0
TCP SOCKET中backlog参数的用途是什么? 在前年时,业务中遇到好多次因为PHP-FPM的backlog参数引发的性能问题,一直想去详细研究一番,还特意在2013年总结里提到这事《为何PHP5.5.6中fpm backlog Changed default listen() backlog to 65535》,然而,我稍于懒惰,一拖再拖,直至今日,才动脑去想,动笔去写。 2013年12月14发布的PHP5.5.6中,changelog中有一条变更, FPM: Changed default listen() backlog to 65535. 这条改动,是在10月28日改的,见increase backlog to the highest value everywhere It makes no sense to use -1 for *BSD (which is the highest value there) and still use 128 for Linux. Lets raise it right to up the limit and let the people lower it if they think that 3.5Mb is too much for a process. IMO this is better than silently dropping connections. patch提交者认为,提高backlog数量,哪怕出现timeout之类错误,也比因为backlog满了之后,悄悄的忽略TCP SYN的请求要好。 当我最近开始想好好了解一下backlog的细节问题时,发现fpm的默认backlog已经不是65535了,现在是511了。(没写在changelog中,你没注意到,这不怪你。)我翻阅了github提交记录,找到这次改动,是在2014年7月22日,Set FPM_BACKLOG_DEFAULT to 511 It is too large for php-fpm to set the listen backlog to 65535. It is realy NOT a good idea to clog the accept queue especially when the client or nginx has a timeout for this connection. Assume that the php-fpm qps is 5000. It will take 13s to completely consume the 65535 backloged connections. The connection maybe already have been closed cause of timeout of nginx or clients. So when we accept the 65535th socket, we get a broken pipe. Even worse, if hundreds of php-fpm processes get a closed connection they are just wasting time and resouces to run a heavy task and finally get error when writing to the closed connection(error: Broken Pipe). The really max accept queue size will be backlog+1(ie, 512 here). We take 511 which is the same as nginx and redis. 其中理由是“backlog值为65535太大了。会导致前面的nginx(或者其他客户端)超时”,而且提交者举例计算了一下,假设FPM的QPS为5000,那么65535个请求全部处理完需要13s的样子。但前端的nginx(或其他客户端)已经等待超时,关闭了这个连接。当FPM处理完之后,再往这个SOCKET ID 写数据时,却发现连接已关闭,得到的是“error: Broken Pipe”,在nginx、redis、apache里,默认的backlog值兜是511。故这里也建议改为511。(后来发现,此patch提交者,是360团队「基础架构快报」中《TCP三次握手之backlog》的作者shafreeck) 对于backlog的参数的含义,在linux kernel手册中也给了注释说明listen - listen for connections on a socket #include /* See NOTES */ #include int listen(int sockfd, int backlog); Description listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2). The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET. The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow. If a connection request arrives when the queue is full, the client may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt at connection succeeds. Notes To accept connections, the following steps are performed: 1. A socket is created with socket(2). 2. The socket is bound to a local address using bind(2), so that other sockets may be connect(2)ed to it. 3. A willingness to accept incoming connections and a queue limit for incoming connections are specified with listen(). 4. Connections are accepted with accept(2). POSIX.1-2001 does not require the inclusion of , and this header file is not required on Linux. However, some historical (BSD) implementations required this header file, and portable applications are probably wise to include it. The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog. When syncookies are enabled there is no logical maximum length and this setting is ignored. See tcp(7) for more information. If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently truncated to that value; the default value in this file is 128. In kernels before 2.4.25, this limit was a hard coded value, SOMAXCONN, with the value 128. backlog的定义是已连接但未进行accept处理的SOCKET队列大小,已是(并非syn的SOCKET队列)。如果这个队列满了,将会发送一个ECONNREFUSED错误信息给到客户端,即 linux 头文件 /usr/include/asm-generic/errno.h中定义的“Connection refused”,(如果协议不支持重传,该请求会被忽略)如下: 在linux 2.2以前,backlog大小包括了半连接状态和全连接状态两种队列大小。linux 2.2以后,分离为两个backlog来分别限制半连接SYN_RCVD状态的未完成连接队列大小跟全连接ESTABLISHED状态的已完成连接队列大小。互联网上常见的TCP SYN FLOOD恶意DOS攻击方式就是用/proc/sys/net/ipv4/tcp_max_syn_backlog来控制的,可参见《TCP洪水攻击(SYN Flood)的诊断和处理》。 在使用listen函数时,内核会根据传入参数的backlog跟系统配置参数/proc/sys/net/core/somaxconn中,二者取最小值,作为“ESTABLISHED状态之后,完成TCP连接,等待服务程序ACCEPT”的队列大小。在kernel 2.4.25之前,是写死在代码常量SOMAXCONN,默认值是128。在kernel 2.4.25之后,在配置文件/proc/sys/net/core/somaxconn (即 /etc/sysctl.conf 之类 )中可以修改。我稍微整理了流程图,如下: 如图,服务端收到客户端的syn请求后,将这个请求放入syns queue中,然后服务器端回复syn+ack给客户端,等收到客户端的ack后,将此连接放入accept queue。 大约了解其参数代表意义之后,我稍微测试了一番,并抓去了部分数据,首先确认系统默认参数 在FPM的配置中,listen.backlog值默认为511,而如上结果中看到的Send-Q却是128,可见确实是以/proc/sys/net/core/somaxconn跟listen参数的最小值作为backlog的值。 apache ab这边的结果中,非2xx的http响应有271个,在NGINX日志数据如下: 从nginx结果中看出,本次压测总请求数为10000。http 200响应数量9729个;http 502 响应数量186个;http 504响应数量未85个;即非2xx响应总数为502+504总数,为271个。同时,也跟error.log中数据吻合。同时,也跟TCP数据包中的RST包数量吻合。 在nginx error中,错误号为111,错误信息为“Connection refused”的有186条,对应着所有http 502响应错误的请求;错误号为110,错误信息为“Connection timed out”的有85条,对应着所有http 504响应错误的请求。在linux errno.h头文件定义中,错误号111对应着ECONNREFUSED;错误号110对应着ETIMEDOUT。linux man手册里,对listen参数的说明中,也提到,若client连不上server时,会报告ECONNREFUSED的错。 Nginx error日志中的详细错误如下: 在压测的时候,我用tcpdump抓了通讯包,配合通信包的数据,也可以看出,当backlog为某128时,accept queue队列塞满后,TCP建立的三次握手完成,连接进入ESTABLISHED状态,客户端(nginx)发送给PHP-FPM的数据,FPM处理不过来,没有调用accept将其从accept quque队列取出时,那么就没有ACK包返回给客户端nginx,nginx那边根据TCP 重传机制会再次发从尝试...报了“111: Connection refused”错。当SYNS QUEUE满了时,TCPDUMP的结果如下,不停重传SYN包。 对于已经调用accept函数,从accept queue取出,读取其数据的TCP连接,由于FPM本身处理较慢,以至于NGINX等待时间过久,直接终止了该fastcgi请求,返回“110: Connection timed out”。当FPM处理完成后,往FD里写数据时,发现前端的nginx已经断开连接了,就报了“Write broken pipe”。当ACCEPT QUEUE满了时,TCPDUMP的结果如下,不停重传PSH SCK包。(别问我TCP RTO重传的机制,太复杂了,太深奥了 、TCP的定时器系列 — 超时重传定时器) 对于这些结论,我尝试搜了很多资料,后来在360公司的「基础架构快报」中也看到了他们的研究资料《TCP三次握手之backlog》,也验证了我的结论。 关于ACCEPT QUEUE满了之后的表现问题,早上IM鑫爷给我指出几个错误,感谢批评及指导,在这里,我把这个问题再详细描述一下。如上图所示 NO.515 client发SYN到server,我的seq是0,消息包内容长度为0. (这里的seq并非真正的0,而是wireshark为了显示更好阅读,使用了Relative SeqNum相对序号) NO.516 server回SYN ACK给client,我的seq是0,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。(请发后面的) NO.641 client发ACK给server,我是seq 1 ,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。 NO.992 client发PSH给server,我是seq 1 ,消息包内容长度是496,已经收到你发的seq 1 之前的TCP包。 ...........等了一段时间之后(这里约0.2s左右) NO.4796 client没等到对方的ACK包,开始TCP retransmission这个包,我是seq 1,消息包长度496,已经收到你发的seq 1 之前的TCP包。 ..........又...等了一段时间 NO.9669 client还是没等到对方的ACK包,又开始TCP retransmission这个包,我是seq 1,消息包长度496,已经收到你发的seq 1 之前的TCP包。 NO.13434 server发了SYN ACK给client,这里是tcp spurious retransmission 伪重传,我的seq是0,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。距离其上次发包给client是NO.516 已1秒左右了,因为没有收到NO.641 包ACK。这时,client收到过server的SYN,ACK包,将此TCP 连接状态改为ESTABLISHED,而server那边没有收到client的ACK包,则其TCP连接状态是SYN_RCVD状态。(感谢IM鑫爷指正)也可能是因为accept queue满了,暂时不能将此TCP连接从syns queue拉到accept queue,导致这情况,这需要翻阅内核源码才能确认。 NO.13467 client发TCP DUP ACK包给server,其实是重发了N0.641 ,只是seq变化了,因为要包括它之前发送过的seq的序列号总和。即..我的seq 497 ,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。 NO.16573 client继续重新发消息数据给server,包的内容还是NO.992的内容,因为之前发的几次,都没收到确认。 NO.25813 client继续重新发消息数据给server,包的内容还还是NO.992的内容,仍没收到确认。(参见下图中绿色框内标识) NO.29733 server又重复了NO.13434包的流程,原因也一样,参见NO.13434包注释 NO.29765 client只好跟NO.13467一样,重发ACK包给server。 NO.44507 重复NO.16573的步骤 NO.79195 继续重复NO.16573的步骤 NO.79195 server立刻直接回了RST包,结束会话 详细的包内容备注在后面,需要关注的不光是包发送顺序,包的seq重传之类,还有一个重要的,TCP retransmission timeout,即TCP超时重传。对于这里已经抓到的数据包,wireshark可以看下每次超时重传的时间间隔,如下图: RTO的重传次数是系统可配置的,见/proc/sys/net/ipv4/tcp_retries1 ,而重传时间间隔,间隔增长频率等,是比较复杂的方式计算出来的,见《TCP/IP重传超时--RTO》。 backlog大小设置为多少合适? 从上面的结论中可以看出,这跟FPM的处理能力有关,backlog太大了,导致FPM处理不过来,nginx那边等待超时,断开连接,报504 gateway timeout错。同时FPM处理完准备write 数据给nginx时,发现TCP连接断开了,报“Broken pipe”。backlog太小的话,NGINX之类client,根本进入不了FPM的accept queue,报“502 Bad Gateway”错。所以,这还得去根据FPM的QPS来决定backlog的大小。计算方式最好为QPS=backlog。对了这里的QPS是正常业务下的QPS,千万别用echo hello world这种结果的QPS去欺骗自己。当然,backlog的数值,如果指定在FPM中的话,记得把操作系统的net.core.somaxconn设置的起码比它大。另外,ubuntu server 1404上/proc/sys/net/core/somaxconn 跟/proc/sys/net/ipv4/tcp_max_syn_backlog 默认值都是128,这个问题,我为了抓数据,测了好几遍才发现。 对于测试时,TCP数据包已经drop掉的未进入syns queue,以及未进入accept queue的数据包,可以用netstat -s来查看: 经过相关资料查阅,技术点研究,再做一番测试之后,又加深了我对TCP通讯知识点的记忆,以及对sync queue、accept queue所处环节知识点薄弱的补充,也是蛮有收获,这些知识,在以后的纯TCP通讯程序研发过程中,包括高性能的互联网通讯中,想必有很大帮助,希望自己能继续找些案例来实践检验一下对这些知识的掌握。 参考资料 TCP三次握手之backlog linux里的backlog详解 tcp-queue-的一些问题 The meaning of listen(2)'s backlog parameter TCP 的那些事儿(上) TCP/IP重传超时--RTO TCP的定时器系列—超时重传定时器 Coping with the TCP TIME-WAIT state on busy Linux servers 2018年10月22日新增How TCP backlog works in Linux 2015-06-02 +8 12 0
nginx上,http状态200响应,PHP空白返回的问题 最近的大半年中,编程语言从PHP换到了Golang后,就很少接触PHP,当然,更多的还是恋恋不舍。尽管如此,每当有人在群里聊起PHP的话题时,我总是想插几句,怀念怀念,同时也温故温故知识点,可不能把她给忘了。 昨天朋友tywei问我一个关于PHP奇怪问题,查到原因解决后,没有详细的解释。夜里睡觉时,老是回想这事,早上醒来,决定还是认真记录一下这些问题。也让自己回归正常状态,多写点博客,总结自己,记录自己。 问题描述 PHP+nginx的环境,任何PHP处理的结果,都是空白页面。OS是ubuntu 14.10 ,nginx 1.6.2 ,PHP5.5.12, 问任何PHP的页面,返回的HTTP状态200,但页面内容是空的,什么都没有,不管PHP页面里写的是什么,正常响应。初步怀疑这是拓展的问题,处理请求后,输出内容有冲突,输出为空之类。想查看拓展列表,看看加载了哪些拓展,但任何PHP代码都返回空,万不得已,在CLI模式下运行,确定ini是同一个。粗略的看了加载的模块,列表如下: 大约如上的模块加载,怀疑xdebug跟opcache冲突,尝试关闭后,仍未解决。 strace看系统调用信息 觉得好简短,好奇怪,访问的是SCRIPT_NAME index.php,怎么都没 lstat \open 这个PHP文件呢?直接返回了?起码要判断SCRIPT_FILENAME是否存在吧,要读取SCRIPT_FILENAME,解析里面的代码吧? 等下.....SCRIPT_FILENAME全部地址是啥?发来的CGI协议包中怎么没有SCRIPT_FILENAME? 仔细看下CGI包的内容 缺少: 那么问题来了 PHP-FPM接收CGI请求时,如果没有SCRIPT_FILENAME怎么处理的? 发来的CGI协议包中,为啥没有SCRIPT_FILENAME? (SCRIPT_FILENAME是什么,干啥用的,这个就不要问了吧。) 问题1:PHP-FPM接收CGI请求时,如果没有SCRIPT_FILENAME怎么处理的? fpm源码里(PHP5.5.x 为例) 同样是 fpm_main.c中init_request_info函数的代码如下: 从代码里看出,script_path_translated变量就是cgi协议包中SCRIPT_FILENAME的结果,其中1115行左右,判断如果script_path_translated为空,并且env_path_translated不为空,则用env_path_translated赋值到script_path_translated上。 之后,对request_info的几个属性给予了默认值,包括request_method为null,以及http_response_code默认200的http响应。 在后面的代码中if (script_path_translated) ,因为CGI包中没有SCRIPT_FILENAME,也没有PATH_TRANSLATED,即script_path_translated为空,故没有对request_method进行赋值,其默认值为NULL。 回到main函数中1839行附近if (!SG(request_info).request_method) ,则直接goto到了fastcgi_request_done,直接结束当前request请求,由于之前有设置过默认的http 响应状态为200 ,也就导致了每次返回http状态200成功响应的空白页面的问题。 同时也解释了strace系统调用中,没出现lstat、open 操作SCRIPT_FILENAME的记录了。 问题2,发来的CGI协议包中,为啥没有SCRIPT_FILENAME? PHP-FPM接收到的CGI协议包都是来自前面nginx的,cgi协议包中没有这个,肯定是nginx没发来,查看nginx配置看到fastcgi_params中没有这项。加上后就可以了。 解决办法: 在nginx配置的 fastcgt_params中加上SCRIPT_FILENAME的配置(在ubuntu的apt-get形式安装nginx配置中,默认是有这条的),比如 题外话: 在FPM的fpm_main.c文件的main函数中,1848行对SG(request_info).path_translated的判断,晚于1849行SG(request_info).request_method的判断。而在init_request_info函数中,对SG(request_info).path_translated的赋值却是比SG(request_info).request_method早的。 而且,明明找不到需要执行的脚本,却还返回200的http响应,很奇怪,也不方便排查。我觉得把对SG(request_info).request_method的判断放到SG(request_info).path_translated后面更合适一些。或者若找不到SCRIPT_FILENAME的话,http响应状态改为404,同时,写入LOG日志,便于排查。 后记: 从fpm代码里可以看到,其实作者是有考虑到没有SCRIPT_FILENAME的问题的,只是判断顺序搞错了。所以,我觉得这应该是个bug,就提了一下:BUG #69625:php-fpm return http 200 response on nginx without SCRIPT_FILENAME,不知道官方是否认为这是个BUG。 新的问题: 为什么如果SCRIPT_FILENAME不存在时,用PATH_TRANSLATED来代替它?PATH_TRANSLATED是每个CGI前端都要发送的吗? 这个问题,后来认真看了下,感觉还挺复杂,跟CGI客户端有关,PHPFPM针对IIS\APACHE\NGINX的处理都不一样。以后再写吧。 参考资料: RFC3875 - The Common Gateway Interface (CGI) Version 1.1 如果你在阅读PHP源码,或者阅读PHP SAPI、PHP拓展源码,可以关注一下PHP源码执行流程图 关于FPM的执行流程,可以看下下面这幅图: 2018年07月09日更新: Fix bugs #75120 and #69625 #3226里修复了我在2015年3月提的 bug report Fix bug #69625 php-fpm return http 200 response without SCRIPT_FILENAME #1270 2015-05-05 +3 7 0
为什么不能在字符组中使用反向引用 有个网友,发一封邮件给我,让我帮看下一段“加密”字符串的解密问题,在线解密的解密不了。代码大约如下: 看到代码后,不禁惊讶起来,现在坏人们的想法真猥琐。利用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
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
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