php扩展 zval_copy_ctor,php缓存扩展频繁存储/读取数组引发CPU过高问题排查手记(php-memcache为例)...

最近进行性能排查的时候发现一个怪事:用php-memcache,缓存命中率越高CPU反而占用越大。

联想起之前用Xcache进行载入速度排除测试时也出现此问题,不禁疑惑了:不是说缓存命中率越高越好么?怎么变成烧CPU了?

今天周六总算空闲,决定硬着头皮去粗略浏览相关扩展源代码。现在写下来当手记。

HIGH_CPU_IN_X.jpg

(1)php缓存扩展共有的特性

得益于php的弱语言特性和统一的变量存储结构-zval[2],绝大多数php缓存扩展并不要求你存储的内容一定要是什么类型,它会自行进行处理。

cache_set('key','string_value', 500);//字符串、数字肯定OK

cache_set('key',array(), 500);//数组当然OK

cache_set('key',$anObject, 500);//对象也OK

所以,问题的关键,也许就是php缓存扩展在读取缓存或者存储缓存的时候,如何处理这些不同的类型数据,以及会带来什么样的性能问题。

顺着这条思路,一开始,以为只要浏览缓存读取的相关代码就知道是怎么一回事了,没想到绕了大半圈子之后才发现,从缓存存储相关代码读取,才是正道。555…

(2)php-memcache[1]源代码浏览简析

php-memcache的代码其实尚算简单,其中存储部分重点落在函数php_mmc_store和mmc_pool_store中。

摘要如下(memcache-2.2.6,memcache.c):

staticvoidphp_mmc_store(INTERNAL_FUNCTION_PARAMETERS,char*command,intcommand_len)/* {{{ */

{

//前面代码略

//读取参数,value即为我们要存储的缓存内容

if(mmc_object == NULL) {

if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"Osz|ll", &mmc_object, memcache_class_entry_ptr, &key, &key_len, &value, &flags, &expire) == FAILURE) {

return;

}

}

else{

if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"sz|ll", &key, &key_len, &value, &flags, &expire) == FAILURE) {

return;

}

}

//判断memcache状态,代码略

//重点,判断value类型,采取不同的策略

switch(Z_TYPE_P(value)) {

caseIS_STRING:

result = mmc_pool_store(

pool, command, command_len, key_tmp, key_tmp_len, flags, expire,

Z_STRVAL_P(value), Z_STRLEN_P(value) TSRMLS_CC);    //字符串,直接调用mmc_pool_store存储

break;

caseIS_LONG:

caseIS_DOUBLE:

caseIS_BOOL: {

zval value_copy;

/* FIXME: we should be using 'Z' instead of this, but unfortunately it's PHP5-only */

value_copy = *value;

zval_copy_ctor(&value_copy);

convert_to_string(&value_copy); //数字和布尔值,需要转换为字符串再存储

result = mmc_pool_store(

pool, command, command_len, key_tmp, key_tmp_len, flags, expire,

Z_STRVAL(value_copy), Z_STRLEN(value_copy) TSRMLS_CC);  //调用mmc_pool_store存储

zval_dtor(&value_copy);

break;

}

default: {

zval value_copy, *value_copy_ptr;

/* FIXME: we should be using 'Z' instead of this, but unfortunately it's PHP5-only */

value_copy = *value;

zval_copy_ctor(&value_copy);

value_copy_ptr = &value_copy;

//重点:数组、对象和其它类型的,一律序列化成字符串再存储

PHP_VAR_SERIALIZE_INIT(value_hash);

php_var_serialize(&buf, &value_copy_ptr, &value_hash TSRMLS_CC);

PHP_VAR_SERIALIZE_DESTROY(value_hash);

if(!buf.c) {

/* something went really wrong */

zval_dtor(&value_copy);

php_error_docref(NULL TSRMLS_CC, E_WARNING, "Failed to serialize value");

RETURN_FALSE;

}

flags |= MMC_SERIALIZED;    //重点:按位或运算flag,表明是序列化后的内容。MMC_SERIALIZED为1

zval_dtor(&value_copy);

result = mmc_pool_store(

pool, command, command_len, key_tmp, key_tmp_len, flags, expire,

buf.c, buf.len TSRMLS_CC);  //序列化后的字符串,调用mmc_pool_store存储

}

}

if(flags & MMC_SERIALIZED) {

smart_str_free(&buf);

}

if(result > 0) {

RETURN_TRUE;

}

RETURN_FALSE;

}

/* }}} */

intmmc_pool_store(mmc_pool_t *pool,constchar*command,intcommand_len,constchar*key,intkey_len,intflags,intexpire,constchar*value,intvalue_len TSRMLS_DC)/* {{{ */

{

//前面代码略

/* autocompress large values *///此处的有关内容,请参考Memcache::setCompressThreshold[4]

if(pool->compress_threshold && value_len >= pool->compress_threshold) {

flags |= MMC_COMPRESSED;

}

//检测flag是否指定要进行压缩存储,是则压缩。MMC_COMPRESSED为2(即二进制10)

if(flags & MMC_COMPRESSED) {

unsigned longdata_len;

if(!mmc_compress(&data, &data_len, value, value_len TSRMLS_CC)) {

/* mmc_server_seterror(mmc, "Failed to compress data", 0); */

return-1;

}

//检测是否达到压缩比,否就丢弃此次压缩。默认压缩比为0.2,即如果10K不能压缩到8K以下就丢弃

/* was enough space saved to motivate uncompress processing on get */

if(data_len min_compress_savings)) {

value = data;

value_len = data_len;

}

else{

flags &= ~MMC_COMPRESSED;

efree(data);

data = NULL;

}

}

//下面有关发送命令到memcache的代码略

}

/* }}} */

从上面可以看到,php-memcache对数组和对象等,采取了php默认的serialize方法变成字符串;发送到memcached服务器前,再进行压缩(如果指定了压缩或者指定了压缩比的话)。其中flags值至关重要,保存着是否为序列化数据和压缩数据的双重任务。换句话讲,序列化和压缩都是在web服务器运行的。

那么反推,解压和反序列化也是在web服务器运行的:php-memcache从memcached服务器中获取时,会先根据flags值判断是否进行解压,有的话就解压缩(话说回来,Memcache::get中传递的flags似乎在源代码中没什么作用,奇怪了);然后再根据flags值判断是否需要反序列化,有则反序列化。相关函数如下,篇幅关系不贴源代码了:

//类比php_mmc_store,Memcache::get的主要实现代码,缓存读取和返回调度代码

intmmc_exec_retrieval_cmd(mmc_pool_t *pool,constchar*key,intkey_len, zval **return_value, zval *return_flags TSRMLS_DC)

//类比mmc_pool_store,从服务器中读取缓存内容并解压缩

staticintmmc_read_value(mmc_t *mmc,char**key,int*key_len,char**value,int*value_len,int*flags TSRMLS_DC)

//类比php_mmc_store中的数组和对象等资源序列化片段,这段代码是反序列化

staticintmmc_postprocess_value(zval **return_value,char*value,intvalue_len TSRMLS_DC)

(3)验证:serialize是否导致php-memcache CPU占用异常

众所周知,serialize占用的资源是挺大的[5],而项目中的缓存基本就是数组。它会不会就是造成php-memcache CPU占用异常的原因呢?

为此进行进行测试,分压缩和不压缩、序列化和不序列化4种可能性进行缓存读取相交测试(代码和下载请看最后)。结果如下:

A)php-memcache存储数组后进行5000次读取(相当于unserialize 5000次)+ MEMCACHE_COMPRESSED

PROCESS COUNT:5000. PORCESS ALL TIME:10.8274896145s. EACH TIME:0.0021654979229

PROCESS COUNT:5000. PORCESS ALL TIME:10.6685016155s. EACH TIME:0.0021337003231

0_MC_COMPRESS_ARRAY.jpg

B)php-memcache存储数组后进行5000次读取(相当于unserialize 5000次)+ 非COMPRESSED

PROCESS COUNT:5000. PORCESS ALL TIME:9.95206928253s. EACH TIME:0.00199041385651

PROCESS COUNT:5000. PORCESS ALL TIME:8.75112223625s. EACH TIME:0.00175022444725

1_MC_NO_COMPRESS_ARRAY.jpg

C)php-memcache存储字符串(var_export(数组, true)而来)后进行5000次读取(没有unserialize)+ MEMCACHE_COMPRESSED

PROCESS COUNT:5000. PORCESS ALL TIME:5.49585962296s. EACH TIME:0.00109917192459

PROCESS COUNT:5000. PORCESS ALL TIME:4.89061260223s. EACH TIME:0.000978122520447

2_MC_COMPRESS_STRING.jpg

D)php-memcache存储字符串(var_export(数组, true)而来)后进行5000次读取(没有unserialize)+ 非COMPRESSED

PROCESS COUNT:5000. PORCESS ALL TIME:3.33534455299s. EACH TIME:0.000667068910599

PROCESS COUNT:5000. PORCESS ALL TIME:5.05854201317s. EACH TIME:0.00101170840263

3_MC_NO_COMPRESS_STRING.jpg

从上面数据和图可以看到几个现象:

a)serialize确实引发了php-memcache占用资源异常的问题:对比B)和D)结果,无论从运行时间还是CPU占用都显著增加不少

b)compress也会导致php-memcache占用资源异常:对比C)和D)结果,主要在于显著的CPU占用率升高

c)不启用compress,对I/O和memcached服务端的要求比较高:对比C)和D)结果,php的I/O成倍升高,memcached服务端的CPU占用也较高

从上述现象可以总结:

a)导致php-memcache CPU占用资源异常有两个影响因素:数组序列化/反序列化,压缩。其中序列化/反序列化引发的CPU占用率问题相对较高。

b)如何才能正确使用缓存?理想的情况下,不序列化不压缩,缓存存储和读取对资源的消耗似乎是最低的,但会引发I/O、流量以及memcached服务端资源消耗增大的问题;那么相对平衡的做法,在开启压缩的情况下尽量存储字符串内容,也应该可以显著的降低web服务器系统响应时间。也就是说无论如何,“存储渲染好的片段html而非渲染前的原始数组”,之前记得有前辈分享过,只是一时忘了出处了。

(4)其它缓存系统的简略浏览分析

其它立足于本地缓存的php扩展(APC、XCache、WinCache等),除了wincache对对象的处理也是进行了序列化/反序列化的方式外,其它对数组的的处理方式,基本没有看到序列化/反序列化的身影。限于水平所限,无法完全弄懂,只觉得好像是对数组变量本身的zval体进行分块拆分/拼装处理。

但是这样的处理方式,在面对大数组似乎也是无能为力。以下这是APC的测试结果:

APC循环5000次读取数组:

PROCESS COUNT:5000. PORCESS ALL TIME:5.98700547218s. EACH TIME:0.00119740109444

APC_ARRAY.jpg

APC循环5000次读取字符串(var_export(数组, true)而来):

PROCESS COUNT:5000. PORCESS ALL TIME:0.0747337341309s. EACH TIME:1.49467468262E-5

APC_STRING.jpg

这个结果……唉,不多说了……

=====================================================

限于水平所限,本文肯定错误多多,希望有大牛指点一下APC、XCache、WinCache对数组究竟是如何处理的。

最后说一句,wincache的代码中发现许多goto,让人犯晕。不得不吐槽一句:分多几个函数,不会增加你工作量啊……

goto_in_wincache.jpg

代码下载:

备注:

[1]php-memcache代码:

[2]laruence. 深入理解PHP原理之变量(Variables inside PHP):

[3]laruence. php的hash算法:

[4]Volcano. 启用memcached压缩注意事项:

[5]Volcano. 关于“facebook的memcached实战”小记:

以下是对提供的参考资料的总结,按照要求结构化多个要点分条输出: 4G/5G无线网络优化与网规案例分析: NSA站点下终端掉4G问题:部分用户反馈NSA终端频繁掉4G,主要因终端主动发起SCGfail导致。分析显示,在信号较好的环境下,终端可能因节能、过热保护等原因主动释放连接。解决方案建议终端侧进行分析处理,尝试关闭节电开关等。 RSSI算法识别天馈遮挡:通过计算RSSI平均值及差值识别天馈遮挡,差值大于3dB则认定有遮挡。不同设备分组规则不同,如64T和32T。此方法可有效帮助现场人员识别因环境变化引起的网络问题。 5G 160M组网小区CA不生效:某5G站点开启100M+60M CA功能后,测试发现UE无法正常使用CA功能。问题原因在于CA频点集标识配置错误,修正后测试正常。 5G网络优化与策略: CCE映射方式优化:针对诺基亚站点覆盖农村区域,通过优化CCE资源映射方式(交织、非交织),提升RRC连接建立成功率和无线接通率。非交织方式相比交织方式有显著提升。 5G AAU两扇区组网:与三扇区组网相比,AAU两扇区组网在RSRP、SINR、下载速率和上传速率上表现不同,需根据具体场景选择适合的组网方式。 5G语音解决方案:包括沿用4G语音解决方案、EPS Fallback方案和VoNR方案。不同方案适用于不同的5G组网策略,如NSA和SA,并影响语音连续性和网络覆盖。 4G网络优化与资源利用: 4G室分设备利旧:面对4G网络投资压减与资源需求矛盾,提出利旧多维度调优策略,包括资源整合、统筹调配既有资源,以满足新增需求和提质增效。 宏站RRU设备1托N射灯:针对5G深度覆盖需求,研究使用宏站AAU结合1托N射灯方案,快速便捷地开通5G站点,提升深度覆盖能力。 基站与流程管理: 爱立信LTE基站邻区添加流程:未提供具体内容,但通常涉及邻区规划、参数配置、测试验证等步骤,以确保基站间顺畅切换和覆盖连续性。 网络规划与策略: 新高铁跨海大桥覆盖方案试点:虽未提供详细内容,但可推测涉及高铁跨海大桥区域的4G/5G网络覆盖规划,需考虑信号穿透、移动性管理、网络容量等因素。 总结: 提供的参考资料涵盖了4G/5G无线网络优化、网规案例分析、网络优化策略、资源利用、基站管理等多个方面。 通过具体案例分析,展示了无线网络优化中的常见问题及解决方案,如NSA终端掉4G、RSSI识别天馈遮挡、CA不生效等。 强调了5G网络优化与策略的重要性,包括CCE映射方式优化、5G语音解决方案、AAU扇区组网选择等。 提出了4G网络优化与资源利用的策略,如室分设备利旧、宏站RRU设备1托N射灯等。 基站与流程管理方面,提到了爱立信LTE基站邻区添加流程,但未给出具体细节。 新高铁跨海大桥覆盖方案试点展示了特殊场景下的网络规划需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值