cover_image

PHP配置化读取优化方案

卢阳@贝壳找房 贝壳产品技术
2021年01月06日 07:58
图片


1 背景


随着贝壳开通城市的增多,在贝壳app中每个城市会启用不同的功能,或同种功能表现出不同的交互方式。为了配置这么多城市和功能的组合,并能动态的修改配置,选用了apollo(携程框架部门研发的分布式配置中心)来记录和维护这些配置。


随着配置数据增多,当数据较大时,读取数据成了负担,会消耗较多的CPU和内存的资源。当前开通城市例如1000左右,每个城市的配置项有几十个项,每次请求需要读取如此大的一个配置数据,在极端情况下,例如并发较多时,首先会大量开销服务器资源,严重时会耗尽资源,其次会导致处理的时间变长,有请求挤占了连接一部分请求没能处理,导致整个链路的处理时间变长,最终导致一定时间服务不可用。


能否有效的降低这种资源开销,处理掉这个读取配置的瓶颈,保障服务的正常运行?


2 分析


配置数据和使用的整个过程即:

图片

图:配置数据读取和使用过程


为了处理读取配置的性能问题,可以从以下环节来入手:


  1. 避免多次重复读取和做最小的处理
  2. 读取的速度提升


解决多次重复读取和处理:在一次请求中,对配置做一次读取和解析并存储,在后续的执行过程中都使用这个存储的配置来完成执行的逻辑。


解决最小处理问题:读取配置后可以只处理需要的数据,不需要的数据可以不做处理,这样可以得到需要使用的最小配置。


解决访问读取速度的问题:这里已经使用了apollo + agent 读取的方式,读取的是本地内存,这个提升空间不大。但是可以对数据格式选择可以使用更小的格式,或者是使用更高压缩比的方式。


3 优化方案实施


3.1 原先场景


3.1.1 原先场景描述


存储在Apollo中原始的数据为json_encode以下数组之后的字符串。


由于apollo的数据存储用的是mysql,对每个value存储有长度限制,所以不能全部序列化当做一个字符串存储。这个数据定义为:二维数组,第一维度为每个city_id的key value 结构,value为一个json_encode之后的字符串,第二维度为city的各个属性。如此有1000个城市。


3.1.2 原先代码问题


由于历史原因等,之前的使用配置的方式较为简单粗暴:


首先,将读取city config封装为一个function,function中先对这个二维数组做json_decode,对第二维度循环做json_decode;

其次,在每个使到的地方直接调用这个function。这样会造成每个使用到的地方,都会去apollo读取配置,循环json_decode;

最终在一次访问量较大的情况下,部分机器由于这里的资源消耗占满了cpu,导致一段时间服务不可用;


3.2 下载及安装xhprof


为了更直观的看清性能的比对,我们需要先行安装xhprof。参考:https://github.com/longxinH/xhprof


3.3 避免重复解析


3.3.1 使用静态变量存储配置


场景构造:FuncA使用了读取apollo和解析其读取的配置,并调用了FuncB,FuncB中也使用了读取apollo和解析其读取的配置,同时也调用了FuncC,FuncC中也是用了了读取apollo和解析其读取的配置。

图片

图:FuncA FuncB FuncC调用关系


比对数据如下:

图片


Advantages

同种配置只会读取一次和在首次读取后的json_decode执行完就可以在随后的调用中直接读取static 变量,省去了再次读取apollo和json_decode的过程。


Disadvantage

读取的配置在本次请求的生命周期内不会更新,如果本次请求的生命周期较长,比如超过1min ,或者使用脚本执行较长时间的不会读取到新的配置。这里需要给出一个解决方法,比如可以在一个长时间的执行脚本中,这种脚本往往不考虑性能,执行起来是隔离的。每次都拉取新的配置来执行逻辑,避免由于配置更新了但并不被使用的场景。


3.3.2 最小json_decode


在完全解析这个json数据需要做K+1次json_decode,K为一维数组的个数。但实际场景不是每个一维数组都在被使用。如果只有第二维度的数组被消费了,只需要解析2次。


比对数据如下:

图片


代码调整如下:


App\Library\Apollo\ApolloHelper
public static function decode($string, $key = null)
{
$data = [];
$list = json_decode($string, true);
// key is null return all
if (empty($key)) {
foreach ($list as $k => $v) {
$data[$k] = json_decode($v, true);
}
return $data;
}
// key is not null only only return key pointed json data
if (!empty($list[$key])) {
$data[$key] = json_decode($list[$key], true);
}
return $data;
}


以上代码所示,会将目标city_id传入,只对这个目标key 做第二维度的解析。


Advantages

最少decode相比与原有decode 方式只针对目标key 做二级数组的decode,并返回目标key的数据。


Disadvantages

在目标key和整体数量相当的时候整个办法性能和原有等同。


3.4 降低解析成本


3.4.1 使用yac或 apcu 作为本地存储


能否从本地存储的角度来解决读取的效率问题,于是可以尝试yac和apcu。


yac:Yac (Yet Another Cache) - 无锁共享内存Cache

安装yac:https://github.com/laruence/yac

为了使多核提升读写效率,使用了不加锁的读,避免了加锁带来的竞争和效率的降低。效率相当于本地的memcached。


图片

图:Yac无锁读取和有锁读取比对


不加锁的读, 拿到以后做数据校验, 如果校验成功, 增说明查询成功, 否则就认为查询失败, 这是一种常用的采用CPU来换锁的方法. 对于目前的服务器, 大部分都是多核的, 如果是加锁, 那么对CPU是一个极大的浪费。


apcu:https://github.com/krakjoe/apcu

APCU的前身是APC(Alternative PHP Cache),APC的主要用途有两项:


  1. 将PHP代码编译之后所产生的bytecode暂存在共享内存内供重复使用,以提升应用的运行效率。

    (Opcode Cache)

  2. 提供用户数据缓存功能。

    (User Data Cache)


注:它的作用和redis和memcached重合,测试表明, APC的User Data Cache的效率和本地memcached几乎相当(在单机性能上,APCu通常比Memcached更高。memcached本身的设计就是为了分布式应用,大规模内存缓存,集群,易扩展等,如果只有一台服务器且内存足够缓存用户数据时推荐apcu)。


安装apcu:https://github.com/krakjoe/apcu/blob/master/INSTALL

图片

图:yac/apcu 与 apollo集成读取配置流程


yac / apcu 中的数据应该如何更新,可以在启动一个常驻进程的脚本,订阅apollo agent的拉取事件,当apollo agent 拉取完成后,将配置更新到yac / apcu中。


比对数据如下:

图片
图片


Advantages

yac 和 apcu在内存使用上略小于与 apollo + json,执行时间只有其方式的约四分之一。


Disadvantages

引入了扩展和扩展数据管理的环节,增加了同步数据的常驻进程脚本,需要对该做监控,如果这里出问题,会带来程序的问题。


3.4.2 逆向工程生成数组配置


实现流程:


首先,判断需要读取的文件是否存在,如果存在则读取,如果不存在则拉取。
其次,读取的时候判断是否过期,过期也重新拉取。
再次,拉取的时候使用php var_export方法写入文件,同时会写入文件的过期是时间。
最后,如此往复。
图片

图:require file组合apollo读取数据方式流程


比对数据如下:

图片


Advantages

使用require file 的方式并使用了opcache来提升读取配置的过程,读取配置效率是有了较大提升。


Disadvantages

增加了维护file的环节。


注:这里如果使用submodule 的方式来读取配置文件,优势:效率等同于require file的方式;劣势:实时性较差,更新需要更新submodule的代码和重新部署。


3.5 其它格式


3.5.1 使用message pack格式存储和解析


message pack 介绍:


MessagePack is an efficient binary serialization format. It lets you exchange data among multiple languages like JSON. But it's faster and smaller. Small integers are encoded into a single byte, and typical short strings require only one extra byte in addition to the strings themselves.


下载及安装地址:https://github.com/msgpack/msgpack-php


原理:


1)true、false 之类的:这些太简单了,直接给1个字节,(0xc3 表示true,0xc2表示false)


2)不用表示长度的:就是数字之类的,他们天然是定长的,是用一个字节表示后面的内容是什么,比如用(0xcc 表示这后面,是个uint 8,用oxcd表示后面是个uint 16,用 0xca 表示后面的是个float 32)。对于数字做了进一步的压缩处理,根据大小选择用更少的字节进行存储,比如一个长度<256的int,完全可以用一个字节表示。


3)不定长的:比如字符串、数组、二进制数据(bin类型),类型后面加 1~4个字节,用来存字符串的长度,如果是字符串长度是256以内的,只需要1个字节,MessagePack能存的最长的字符串,是(2^32 -1 ) 最长的4G的字符串大小。


4)高级结构:MAP结构,就是k-v 结构的数据,和数组差不多,加1~4个字节表示后面有多少个项


5) Ext结构:表示特定的小单元数据。也就是用户自定义数据结构。

图片


实验步骤:


首先:将json_encode数据和msgpack_pack的数据存储到某种存储中,比如redis 或者 file;
其次:读取这个配置;
最后:比对这这两种存储方式的解析效率;


比对数据如下:

图片


Advantages

Message pack在是二进制的存储,在使用内存上略小于json ,相同存储格式下执行时间约相当于json的60%,有了明显的提升。


Disadvantages

需要引入新的扩展。


4 总结


以上方式是在不同环节解决读取一个较大配置的性能问题,每个方案都与apollo + json_decode 的原始方案做比对,在不同环节做优化并具有替代性。这些方案也可以组合使用,同时每个方案也会有引入该方案的成本。目前,我们使用apollo + yac + static变量的方式在读取,解析,消费三个环节上了很大程度提升了性能,避免了资源耗尽的风险。


当然在实际项目实施过程中,依据场景各有不同,可以组合以上提到的一种或几种方案来使用,达到性能的提升。


图片
后端 · 目录
上一篇复杂订阅条件下,如何实时准确的向用户推送新上房源?下一篇记一次MySQL死锁排查过程
继续滑动看下一个
贝壳产品技术
向上滑动看下一个