分类 "Php" 的存档.

一次线上业务跨IDC高延迟问题解决的案例分享

最近在梳理某项目上各服务接口的性能情况,遇到两个问题。以下是定位和解决问题的一个思路,分享给大家。
业务之前并没有详细的性能日志记录,仅在电信机房(T机房)进行了性能测试,结果是各接口满足预期,服务上线。
在进一步对接口进行性能分析时,对各业务接口的关键路径添加了日志统计,通过日志进行分析,将接口的延迟进行统计,接入Grafana,观察数据后,发现两类问题。

  1. 连接MongoDB的服务,网通机房(C机房)延迟比电信机房(T机房)要高。
  2. 连接Mysql的服务,网通机房(C机房)延迟比电信机房(T机房)高。

    NOTE: 这些服务接口,都是只读,没有写操作。

    对两类问题分别进行排查:

MongoDB

简单的排查后发现,MongoDB实例有过一次迁移,并且迁移后只保留了电信机房(T机房)的实例,网通机房(C机房)没有从库,所以网通机房(C机房)延迟比电信机房(T机房)高。对网通机房(C机房)部署了从库实例后,却意外发现电信机房(T机房)的延迟比网通机房(C机房)高了。再次排查后发现,代码中配置的MongoDB的读策略是secondary(从库优先),所以网通机房(C机房)有从库后,电信机房(T机房)也去网通机房(C机房)读取,导致了电信机房(T机房)的延迟变高。更改读策略为nearest(就近优先),有所好转,但并没有预想的效果那么好。仔细看下官方文档

The driver reads from a random member of the set that has a ping time that is less than 15ms slower than the member with the lowest ping time. Reads in the MongoClient::RP_NEAREST mode do not consider the member’s type and may read from both primaries and secondaries.

就会发现,nearest是在客户端维护一个到各个实例延迟小于15ms的集合,而我们电信机房(T机房)到网通机房(C机房)是光纤直连,延迟在12ms左右,所以,每次客户端可能会连接到电信机房(T机房),也可能到网通机房(C机房)。
这点在以后的应用中,大家可以注意下。

Mysql

在所有的服务中,只有一个服务接口是读mysql实现的,而这个接口的表现更是奇怪,网通机房(C机房)的延迟比电信机房(T机房)多100 ms+。

开始时猜测有可能业务内做了某些写主库的操作,比如写mysql,或写redis之类的,跨机房写导致的延迟高。
实际分析后发现,业务内并没有写操作,多出的时间就是读mysql的时间。
mysql是有网通机房(C机房)的从库的,为什么读取从库的数据,延迟还会这么高呢。在我们服务端ping 网通机房(C机房)的mysql ip,发现延迟正常,只有零点几毫秒,不存在网络问题。
下一步就是通过抓包,分析下我们服务端跟mysql间到底有哪些交互,到底是哪个环节慢了。

根据抓包结果发现,正常的select查表请求很快能得到响应,但当从我们服务端发送一个 “Describe tableName”的请求到mysql 服务端时,服务端等待了较长时间(30ms+)才返回结果,而且一次接口服务请求中,有多次Describe的请求,这样,导致网通机房(C机房)最终延迟很高。
问题定位后,开始尝试解决。
解决问题前需要先理清思路:

  1. Describe TableName 这个命令是干什么用的,业务里并没有显式调用。这个请求能不能去掉。
  2. 如果不能去掉,那它的延迟为什么这么高,能不能优化?

第一个问题比较简单,Describe 命令是现在ORM中比较通用的做法,通过获取数据库的表结构,来动态的创建Model。如果不调用Describe命令,当然也可以做到,那样就需要自己业务端对每个model进行声明,这样开发成本会大大增加,这个方式不可取。所以需要保留Describe命令。

第二个问题,延迟为什么高,这个命令是很简单的一个命令,没有任何复杂的操作,而且主库上都没有这个问题。结合DBA同学在Mysql上使用了Atlas中间件,可以大胆猜想下,应该是这个中间件搞的鬼,把select请求分配到从库执行,但是把Describe分配到了主库执行,有可能是因为Atlas中间件只考虑了一些读的SQL,把这类请求分配到从库,而其它各种请求,可能由于过于复杂,就默认分配到主库去执行。当然这只是猜测,没有查看过Atlas的源码,所以不能妄下结论。结合已经整理到的线索,跟DBA同学进行了确认,确定 Describe命令确实被Atlas中间件发送到主库去执行了,至于这么做的原因,是为了避免主从结构不一致时,从库拿到的表结构错误。这种情况下,我们也不能评价说中间件做的到底合理不合理,所以我们需要从自己的角度再思考下能不能优化。如果说希望避免每次请求都执行Describe命令,除了说刚才提到的自己声明,另外一个方式就是cache了,因为表结构变化的频次太低了,我们完全可以设置一个较长时间的cache,来避免频繁的这种请求。业务使用的是Phalcon框架,这个框架中已经提供了这种meta-Data cache的方案,Yii中也有类似的实现: schemaCaching
当启用这种cache后,效果就很明显,可以看到:
网通机房(C机房)延迟从原来的120ms降到7ms, 电信机房(T机房)延迟从原来的10ms降低到5ms.

后续需要考虑的就是,如果表结构发生变化,如何在不影响业务的情况下进行更新。这个也可以有多种实现的方案,大家可以自己想下。

总结

解决问题的思路就在于,遵循最小化原则,先对可能产生这种问题原因进行大胆猜测,然后快速验证,逐步缩小范围,将问题定位到一个最小可复现的范围内,再深入分析具体原因。当然这一切都要有数据说话,如果平时开发中,能提供足够丰富的日志数据,就可以很快的定位问题,甚至提前发现问题。

PHP安装时libiconv错误问题解决

/var/tools/php-5.6.17/ext/iconv/iconv.c:2512: undefined reference to `libiconv_open’
ext/xmlrpc/libxmlrpc/encodings.o: In function `convert’:
/var/tools/php-5.6.17/ext/xmlrpc/libxmlrpc/encodings.c:73: undefined reference to `libiconv_open’
/var/tools/php-5.6.17/ext/xmlrpc/libxmlrpc/encodings.c:81: undefined reference to `libiconv’
/var/tools/php-5.6.17/ext/xmlrpc/libxmlrpc/encodings.c:101: undefined reference to `libiconv_close’
collect2: ld returned 1 exit status
make: *** [sapi/cli/php] Error 1

在阿里云安装php时,make的时候,发生了libiconv错误,通过安装libiconv,指定with-iconv-dir也没解决。最终是在make时加了一个参数,然后顺利编译通过的。
make ZEND_EXTRA_LIBS='-liconv'

怎么修改ulimit,开启php的coredump输出

大家应该都知道,开启php的coredump输出,修改ulimit -c就可以了,但是很多情况下,会提示权利受限,无法修改

[fukun@10.16.29.xxx]$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 30678
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 32768
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 10240
cpu time               (seconds, -t) unlimited
max user processes              (-u) 1024
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

$ ulimit -c unlimited
-bash: ulimit: core file size: cannot modify limit: Operation not permitted

解决方案:
1.检查配置
看看shell配置里有没有 ulimit -c 0 这种类似的关闭的操作,例如 $HOME/.bash_profile 或者 $HOME/.bashrc 之类的,如果有,注释掉。

#
# Do not produce core dumps
#
# ulimit -c 0

Read more…

如何在PHP扩展中引用第三方的C++类库

还是360指数项目,需要用到Query归一化,但是公司内只有c++版本的,数据分析的同事用的都是那个二进制可执行文件来处理,但是我们前端又不可能跑system()方法吧,太不安全了,所以要来了源代码,打算封装成一个php扩展。
下边就讲一下我的封装过程。

首先,要到了Query归一化的C++版本源代码。

query归一化

query归一化

引用第三方的类库有两种方法,一种是静态引用,一种是动态引用,推荐使用静态引用,因为静态引用的情况下,会把类库打包到php的扩展.SO文件中,这样我们不必担心依赖关系,带着类库到处跑了。

Read more…

Unknown: Input variables exceeded 1000;nginx在post大量字段时部分字段被丢弃

今天有运营同事反馈在使用一个内部的运营工具时,有些操作失败,后来抓包发现,post到服务端的数据是正常的,在服务端接受到的数据却并不完整,有缺失。很是奇怪。看了下nginx的error log,发现问题所在了,原来是php有设置最大接受变量个数。

2014/04/02 18:06:33 [error] 23623#0: *1115 FastCGI sent in stderr: "PHP message: PHP Warning:  Unknown: Input variables exceeded 1000. To increase the limit change max_input_vars in php.ini. in Unknown on line 0" while reading response header from upstream, client: 10.18.120.25,

所以,需要做的就是修改下php.ini中的设置:max_input_vars ,默认没有开启,默认值是1000,修改为自己合适的值,就可以了。

undefined symbol: itoa

itoa不是c的一个标准输入输出,所以可能不在stdlib.h里。
解决办法很简单,不就是要把整型转为字符串么。sprintf 格式化输出一下就好了。

举个例子:

int val = 177;
char *val_s ;
spprintf(&val_s, 0, "%d", val);

libcurl.so.3: cannot open shared object file

php配置扩展时遇到了个这个问题:PHP Warning: PHP Startup: Unable to load dynamic library ‘/home/s/apps/php-5.2.6/extensions/curl.so’ – libcurl.so.3: cannot open shared object file: No such file or directory in Unknown on line 0

google了下,都说解决办法是:

ln -s /usr/lib/libcurl.so.4 /usr/lib/libcurl.so.3

我看了下自己的lib目录,发现也没有 libcurl.so.4,然后又查没有libcurl.so.4怎么样,答案是:

ln -s /usr/lib/libcurl.so.3 /usr/lib/libcurl.so.4

啊哈哈哈哈哈哈,乐死我了。

我个人觉得是curl.so不靠谱,从别的地方搞了一个拷贝过来,ok了。

php代码规范检查 PHP_CodeSniffer

http://pear.php.net/package/PHP_CodeSniffer/

使用pear安装,然后使用phpcs命令检查.

sudo pear install PHP_CodeSniffer
phpcs application/controllers/Index.php

默认的规则比较严格,doc是必须的,doc的空格个数,格式都有严格要求。。

Best checked for with a tool: PHP Code Sniffer

Best checked for with a tool: PHP Code Sniffer

如何在不重新make install PHP的情况下编译出PHP扩展.so文件

生成一个PHP扩展.so文件,一般就是这两种方法,第一种是在php源码内configure –[with|enable]-extension_name,然后make && make install,新扩展就会生成并保存在PHP的环境中了。
不过多半会有这种情况,我们跟别人共用一台开发机,开发机已经安装了标准的php开发环境,如果我们在用这种重新编译PHP的情况,如果我们有调试php源码,或者调试扩展,很有可能会损坏原有的标准环境,所以我们就需要另一种方法,只生成一个.so文件,然后把so手动放到标准环境下,就可以不影响大局了。
这种方式呢,也很简单,跟第一种方法一样,三步搞定。

一、phpize
进入你开发php扩展的扩展根目录,比如我想做的扩展是 fkhelloworld,目录在这里 /home/s/www/fukun/clang/php-5.2.6/ext/fkhelloworld.
在这里执行phpize,如果提示找不到此命令,就找到你php的bin目录,执行全路径的命令,比如我的phpize在这里:/home/s/apps/php-5.2.6/bin/phpize

If you look in the current directory at this point, you’ll notice a lot more files than you had there a moment ago. The phpize program combined the information in your extension’s config.m4 file with data collected from your PHP build and laid out all the pieces necessary to make a compile happen. This means that you don’t have to struggle with makefiles and locating the PHP headers you’ll be compiling against. PHP has already done that job for you.

phpize会结合你的config.m4文件中描述的信息以及你的php构建相关数据,制定合并所需的部分数据,这意味着你不必再在意makefile文件和定位要编译的php文件头,PHP已经把这些都做好了。

Read more…

PHP扩展:如何在Zend Api中对参数进行获取:zend_parse_parameters()和zend_parse_parameters_ex()

首先要知道, ZEND_NUM_ARGS()宏定义可以得到参数的个数。

参数解析函数:
int zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, …);
第一个参数num_args表明了我们想要接收的参数个数,一般都使用ZEND_NUM_ARGS() 来表示对传入的参数“有多少要多少”,如果有特殊需求,可以自己直接指定。第二参数应该总是宏 TSRMLS_CC 。第三个参数 type_spec 是一个字符串,用来指定我们所期待接收的各个参数的类型,有点类似于 printf 中指定输出格式的那个格式化字符串。剩下的参数就是我们用来接收PHP参数值的变量的指针。需要说明的一点是:zend_parse_parameters中如果声明了一个字符串类型,那么后面对应的参数将是两个,第一个代表字符串,第二个代表字符串长度。如果声明了一个指定类型的对象实例,那么后面对应的参数也是两个,第一个代表对象,第二个代表对象的类型。

type_spec 的各类型表示如下:
• l – 长整数
• d – 双精度浮点数
• s – 字符串(也可能是空字节)和其长度
• b – 布尔值
• r – 资源, 保存在 zval*
• a – 数组, 保存在 zval*
• o – (任何类的)对象, 保存在 zval*
• O – (由class entry 指定的类的)对象, 保存在 zval*
• z – 实际的 zval*
| – 表明剩下的参数都是可选参数。
/ – 表明参数解析函数将会对剩下的参数以 SEPARATE_ZVAL_IF_NOT_REF()
的方式来提供这个参数的一份拷贝, 除非这些参数是一个引用。
! – 表明剩下的参数允许被设定为 NULL(仅用在a、o、O、r和z身上)。

下边给几个例子,自己体会下:

Read more…