当前位置: 首页 > 工具软件 > Yac > 使用案例 >

PHP多进程 - Yac扩展的基本原理及其使用教程(cli和fpm)

苗信鸥
2023-12-01

一、概述

关于Yac扩展的介绍 https://www.laruence.com/2020/03/25/5657.html
github地址 https://github.com/laruence/yac/blob/master
php官方文档 https://www.php.net/manual/zh/yac.get.php

Linux安装 pecl install yac
docker安装 pecl install yac && /usr/local/bin/docker-php-ext-enable yac
记得开启 yac.enable_cli

Yac的设计初衷:PHP进程之间共享一些简单的数据。

阐明三个关键点:
1、PHP进程之间
实现运行中的同族的PHP进程之间,也就是有亲缘关系的进程之间的数据共享,无关的进程不会共享。
同一台机器内部。
类似于 APC,APCu,Swoole-Table。

2、数据共享
只是数据的共享,并不能当数据库来使用。

3、简单的数据
简单的数据缓存,并不具备memcache, redis这样的强大能力。

使用场景
php-fpm模式下的一级缓存,比如:页面缓存,用户信息缓存,系统配置项等。用来取代本机的memcache缓存,文件缓存,因为不需要经过网络IO,所以并发和速度都会快很多,但是有极小的概率会发生key冲突,可见非常重要的数据不适合使用yac。

Yac是鸟哥为php-fpm模式而设计的,可以很方便的实现php-fpm进程之间的数据共享,它同样可以用在cli的master-worker模式下worker与worker之间,worker与master之间的数据共享,比如workerman框架。

二、实践

示例:

demo1.php

<?php

$yac = new Yac();

$key = 'name';
$data = $yac->get($key);
var_dump($data);

while(true){
    $yac->set($key, date('Y-m-d H:i:s'));
    var_dump($yac->get($key));

    sleep(1);
}

demo2.php

<?php

$yac = new Yac();
var_dump($yac->dump(10));

使用 curl 先访问 demo1.php, 再访问 demo2.php。可以获取到缓存。

还是上面的两个文件,在cli模式下,先执行 php demo1.php,再开一个窗口执行 php demo2.php,发现读取不到缓存了。这个现象应该是第一次使用yac的同学都会遇到,并且都会一脸懵,其实yac实现PHP进程间数据共享的两个条件是:同族进程,且至少有一个PHP进程在运行中。

1、同族进程
正如上面的php-fpm进程,master和worker都算同族的进程,所以要想在cli下使用yac,那么应该是父子进程的关系,这样father与child,child与child之间才能实现共享。
开启了yac扩展之后,每启动一个PHP进程,yac内部会发起mmap系统调用使得进程之间通过映射匿名文件实现共享内存,虽然是匿名的,但是同族的进程会指向同一个匿名文件,不同族的进程指向不同的匿名文件,从而保护了当前共享内存不受外部不相干的进程的干扰。
所以,上面的例子,两个进程是不同的族,隐射了不同的内存地址,自然不能共享数据。

2、至少有一个PHP进程在运行中
mmap会将虚拟内存映射到进程的地址空间,这样同族的进程就都包含了这个虚拟的内存地址,都可以访问它。同时操作系统会记录这个虚拟的内存被哪些进程引用,一旦引用数为0,这个内存将被释放,所以说一旦该族的所有PHP进程都结束了。yac的数据将会消失,再来想想php-fpm模式下,由于master进程几乎不会结束,哪怕所有的worker都结束了,数据还是在的,鉴于此,yac只适合用来做缓存,丢了就丢了,无所谓,再次生成缓存就好了。

改进后的 cli 示例

<?php

for ($i = 1; $i < 3; $i++) {
    $pid = pcntl_fork();
    $yac = new Yac();
    $key = 'name111';

    if ($pid > 0) {
        if ($i == 1) {
            cli_set_process_title('child_1');
            $yac->set($key, date('Y-m-d H:i:s'));
            file_put_contents('child_1.log', $yac->get($key));
            exit;
        } else {
            cli_set_process_title('child_2');
            sleep(10); // 让子进程1退出,看是否能读到缓存
            file_put_contents('child_2.log', $yac->get($key));
            exit;
        }
    }
}

// 后台运行了
cli_set_process_title('child_0');
sleep(5); // 确保子进程成功写入缓存
file_put_contents('child_0.log', $yac->get($key));

进程 child_0, child_1, child_2 属于同族进程,他们都引用了同一块虚拟内存,就可以共享了。等他们都结束后,该内存就被释放。

yac的info函数
segment_size是内存分块大小,segment_num是申请了多少块,fails,获取到错误的数据的次数(crc校验不通过),recycles segment用完了会导致重新分配。

为什么要加锁?
即使在常规的缓存场景下,缓存也是有更新的时候,如果不加锁,那么有可能在写入一半的时候,就被读进程读走了,导致数据的不完整。而yac是一种无锁的设计,虽然增加了效率,但是也埋下了一丝隐患,虽然鸟哥对此做了一些改进,但是尽量在写操作不频繁的地方使用。

三、原理

yac在安装的时候,会检测当前环境支持的共享内存方式。
代码 https://github.com/laruence/yac/blob/master/config.m4

checking for sysvipc shared memory support... yes
checking for mmap() using MAP_ANON shared memory support... yes
checking for mmap() using /dev/zero shared memory support... yes

依次为:

sysvipc 
就是通过物理内存来共享的,使用ipcs -m 可查看共享内存段的使用情况。

mmap 
匿名映射模式(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。需将 flags
设置为MAP_ANON,fd设置为-1

mmap 
指定文件 /dev/zero 来映射,这种是开放的共享,任意进程都可以执行它并实现共享。

缓存空间初始化。根据当前系统的情况, 选择其中一种方式进行内存的申请、初始化

mmap -- 匿名映射
shm -- sysvipc
filemaping -- 指定文件 /dev/zero 来映射

虽然我的系统这三种都支持,但是yac首选 mmap 匿名映射,也是处于共享内存的安全性考虑。
具体可参见yac/storage/allocator/目录下的createfilemap.c ,mmap.c, shm.c 中的create_segments 方法。

 类似资料: