分享一下我们的workerman项目+apcu案例,性能秒杀Redis

army

前言: 我们的项目要频繁读写遍历缓存,起初用的Redis,CPU占用20%左右,显然不理想。
当接触到php自带的apcu后,简直就是神一样,单机效率超Redis几十甚至上百倍,
利用workerman的text协议来搭配apcu 也能秒Redis 2倍以上,
关键是...关键是...关键是CPU消耗不足redis十分之一,
上测试截图:

//走tcp

10879 12 14
12个评论

luohonen

真的假的,比redis都高?

army

如果只作为单纯的进程间共享变量函数以及类,可说是非常完美,高Redis百倍性能。
如果作为kv缓存的话有一些特殊情况,大致根据自己项目情况来选择,
第一种:对单key的过期时间有要求的,在遍历全部key会包含已过期的key且无法分辨过期和未过期。
第二种:对遍历全部key只要有效key的话,单key无法自动过期。
对于key未设置过期的不影响,这些是运行在cli下才有的问题,fpm下不存在能完美Redis特性并秒杀百倍性能。

  • army 2023-04-01

    对于以上的问题已解决,用git仓库源码编译(pecl.php.net下载的不行),需配置use_request_time=0,灰常的奈斯

  • 贾亭西 2023-06-25

    您好,上面说的问题,在fpm模式下都不存在是吗,我们现在还是在fpm模式,最近我想着用apcu,至于无法共享cache的问题,我想着定时十分钟淘汰就好了,每隔十分钟去mysql或者redis拿一次数据

  • army 2023-06-26

    @贾亭西 从git拉取编译不管是fpm还是cli都没问题了

  • 贾亭西 2023-06-26

    好的,谢了

chaz6chez

apcu实际上已经边缘化了,只能单进程,整体的设计更适合fpm,在cli下面有挺多问题的,包括过期时间等,甚至不如shmop,早期shmop是为cli下设计的;
不考虑分布式、持久化容错的话,用shmop会更好,memcache也可以,性能都比redis更好;毕竟webman/workerman都常驻内存了,连接可以持久化。
早期实现过shmop/apcu + 边车定时器进程进行过期时间处理,实话实说,那个性能没法看,尤其是堆积数据过大的时候,时间复杂度毕竟是O(n),而且单进程的定时器在堆积数据过大的情况下也存在延时。

当然,这样玩一玩,了解实践一些服务层的知识肯定是好处,有益的

  • chaz6chez 2023-04-03

    而且其实这里面我们还得出一个实践,用持久化连接的SQLite3的memory也非常快,和缓存是类似的,用file也可以达到redis的持久化效果,而且还带事务,当然肯定和纯内存是没办法比的;我们后续的很多小组件都是通过RPC + SQLite3进行的数据存储和缓存处理,相当稳定。

  • chaz6chez 2023-04-03

    最主要的是SQLite3非常稳定,切具备工业化标准,PHP自带该拓展库无需额外安装及配置

  • army 2023-04-03

    cli下和fpm使用一致,并没有过期时间的问题,pecl.php.net下载的存在时间过期问题,从仓库拉源码编译是正常的,当然我们修改了源码增加了一个时间字段来遍历时更好的过滤失效key,我们已应用在生产环境。不考虑分布式以及持久化,只用简单的kv缓存的话,apcu就是神一样的存在。

  • army 2023-04-03

    在单机下,apcu可以完美替代workerman提供的globalData 实现多进程变量共享,且是globalData的几十倍效率。

  • chaz6chez 2023-04-03

    不考虑持久化和分布式的话,其实常驻内存框架可以直接用php的静态变量,其中还可以使用Spl的一些数据结构,比如堆、优先队列,在处理包含资源类型的缓存的时候比apcu更有竞争力,而且性能更好,你们可以在8.0及以上的php版本测试一下,效果是比apcu更好的

  • kane 2023-10-03

    都用php了还考虑啥性能,要性能用golang吧!

army

下图是我们生产环境中的各种方案,起初我们把后端用go+redis和go+自写cache,并发上来后都不如fpm+apcu,后来又使用了workerman+redis,起初也一样,无法完美使用apcu,导致Redis有些扛不住,又切换到了fpm+apcu,然后我们前端APP要websocket,没办法又用回了workerman+redis,不死心去研究了下apcu源码,最后就是workerman+apcu,终于比fpm+apcu强了。

  • jesse 2023-10-20

    go有两个节点飚的好高

fenger

Redis 为 127.0.0.1 本机,该服务端的Redis还存在IO瓶颈吗? 手动狗头

  • 菏泽曹县 2023-08-02

    有道理啊,换成本机redis呢,,或者直接存workman 进程内存。。岂不是更快

  • chaz6chez 2023-08-02

    redis只要走网络就会存在内核态和用户态的拷贝,就会被redis的epoll影响,apcu是走的共享内存,并不会存在内核态和用户态的拷贝问题,是比redis更快的

artisan

NB关注一下

  • 暂无评论
fengchujun

关注一下

  • 暂无评论
hongs

谢谢作者和
chaz6chez的分享.

  • 暂无评论

是这个源码地址吗?有谁教教我怎么编译。https://github.com/krakjoe/apcu

  • army 2023-08-04

    拉取到服务器,cd到源码目录,依次执行以下几个步骤
    /server/php/bin/phpize (你的phpize路径)
    ./configure --with-php-config=/server/php/bin/php-config (你的php-config路径)
    make && make install

  • army 2023-08-04

    最后在php.ini里添加apcu的配置
    [apcu]
    apc.enabled=1
    apc.shm_segments=1
    apc.shm_size=512M
    apc.entries_hint=0
    apc.ttl=0
    apc.gc_ttl=5
    apc.mmap_file_mask=
    apc.slam_defense=0
    apc.enable_cli=1
    apc.use_request_time=0
    apc.serializer="php"
    apc.coredump_unmap=0
    apc.preload_path=


    extension=apcu.so

  • 2023-08-10

    赞!

  • aspire 2023-08-17

    这是安装方法吗?

大地瓜

workerman本身已经常驻内存了,一步到位,直接使用全局变量岂不是更快!
程序启动时初始化全局变量:$a=[];
写入/修改:
$a["key"]="value";
$a["key2"]="value2";...
读取:
$b=$a["key"];...
删除:
unset($a["key"]);

  • army 2023-11-14

    全局变量的话进程与进程之间无法共享,主进程的变量子进程可读不可写

  • 大地瓜 2023-11-15

    apcu本身也是单进程单线程设计的,多个主进程间或者多个子进程间apcu缓存不支持共享,每个进程都有独立的apcu缓存

    如果仅提供单进程单线程服务,高并发且频繁读写增删缓存,例如实时游戏等追求极致性能的实例,不用想,使用全局变量肯定比其他外部扩展快

    例如:
    <?php
    //error_reporting(0);
    use Workerman\Worker;
    use Workerman\Connection\TcpConnection;
    use Workerman\Timer;

    require_once __DIR__ . '/vendor/autoload.php';
    $worker = new Worker("websocket://0.0.0.0:10001"); // 创建websocket
    $worker->name = "桀桀桀桀";
    $worker->count = 1; // 启动1个进程 如果开启多个进程,则每个进程都会产生独立的缓存,业务中如果提供向指定用户推送消息且开启多进程,同进程推送成功否则失败,进程间缓存不共享(在A鱼塘钓不了B鱼塘的罗飞鱼)

    //$worker启动时
    $worker->onWorkerStart = function () {
    //初始化MySQL redis等
    global $test; //桀桀桀桀桀 全局变量
    $test=[]; //初始化全局变量

       //定时任务 每隔1800秒同步一次
        $time_interval = 1800;
    Timer::add($time_interval, function () {
        global $test;
                //各种逻辑 然后操作$test
    }

    }
    //用户握手连接时初始化用户数据
    $worker->onConnect = function (TcpConnection $connection) {
    $connection->onWebSocketConnect = function (TcpConnection $connection, $request) {
    global $test;
    //从连接参数中获取用户数据 各种逻辑 然后操作$test
    };
    };
    //收发数据
    $worker->onMessage = function (TcpConnection $connection, $data) {
    global $test;
    //业务逻辑 操作$test
    }
    // 用户连接断开时
    $worker->onClose = function (TcpConnection $connection) {
    global $test;
    //各种逻辑 然后操作$test
    }
    Worker::runAll();
    ?>

    看业务情况只需修改下cli的php.ini脚本内存限制,如:memory_limit=256M

  • army 2023-11-15

    workerman主进程下不管多少个子进程,都是共享的,均可读写,并发能力也超棒

  • chaz6chez 2023-11-15

    apcu可以开启mmap内存映射,非亲缘进程也可以共享,底层使用的是mmap()函数实现的

  • 大地瓜 2023-11-15

    向大佬们学习!

chaz6chez

最近在研究这个,https://www.workerman.net/plugin/133 ,可以看看,应该能用得上

  • army 2023-11-15

    他这个加锁我没搞明白,apcu内部已经有读锁机制,他再套一层是为啥?

  • chaz6chez 2023-11-15

    对于map的操作,需要原子性加锁,因为是二级内容的修改,比如update

  • army 2023-11-15

    我觉得没这个必要,直接apcu_store就可以了,加锁apcu已经在内部有操作,我们只管用,随意并发写。

  • chaz6chez 2023-11-15

    很多时候上层需要一个原子性操作来保证业务原子性,apcu底层提供了锁来保证apcu自己的函数具备原子性,但是上层的封装需要多次调用一个或N个apcu函数,需要保证上层业务的原子性,所以需要用到apcu提供的原子性锁,有些地方需要阻塞等待式调用,所以需要自行在此基础上实现抢占式锁来保证原子性和业务完整性

  • chaz6chez 2023-11-15

    多进程下,AB两个进程在每一个apcu的函数上肯定是原子性的,但是有些时候A上面有多个apcu操作,B也有多个apcu函数操作,相互之间需要各自的原子性,如果不加锁,这些apcu函数会在apcu实际执行中穿插执行,并不能保证原子性,这个是测试后得到的结果,毕竟apcu只保证自己函数的原子性

  • army 2023-11-15

    apc在写入的时候,内部总是以 锁->写->释放的过程,不管你的有多少个进程同时操作,这不会变,也可以理解为阻塞写入,在业务代码上再套一层锁,真有必要吗? 麻烦分享下测试过程,这对我们很重要,目前没遇到过并发安全性问题,我们项目深度依赖apcu 希望能涨些知识 🙏

  • chaz6chez 2023-11-15

    你没懂我的意思

    假设两个进程同时执行以下操作:
    {
    1.apuc_get('a',一些数据);
    其他的一些业务结果a
    2.apuc_store('a', a);
    另一些业务
    }
    业务是一次原子性操作,业务逻辑是依赖apcu的数据

    在apuc层面每一次调用都是原子性带锁的没错,会互斥没错,但是两个进程同时进行的时候,1/2的数据可能被另一个进程的操作污染,因为业务不具备原子性,那么我在上层增加atomic原子性操作,依赖自己实现的锁就可以避免这个问题,但代价就是当多个相同的进程同时执行对相同数据进行操作的时候,会互斥并排队;原理和redis的nx 或者 xx是类似的

  • chaz6chez 2023-11-15

    由于apcu是非常高效的微妙级别的操作,通常来说上下文执行的过程是比apcu处理会慢的,所以很多时候感受不到这个问题,当并发量达到一定程度,或者其中业务存在一定执行时间的时候,这样的问题就会暴露出来;这样的问题就跟使用数据库对数据进行处理的时候不加事务一样,原则上是为了保证整个业务的完整性和原子性

  • chaz6chez 2023-11-15

    具体场景就是当apcu储存了一个map,我需要对map里面的一个键值进行自增,那么我需要先读取出来,比如a=>1,当我利用apcu读取的时候可能是1,在我自增写入的时候我期望是2,但在这时候其他亲缘进程可能已经先于我操作了,就在我读取并自增的间隙内,已经自增到了2,那么最后我的结果还是2,不能保证是3;因为apcu的每次函数调用是一个原子性操作,但是很多业务是需要多次apcu的函数调用来完成一次原子性操作的

  • chaz6chez 2023-11-15

    a进程map自增,b进程map也自增,存在ab进程都读取的是1,ab进程最后的结果都是2,而不是一个是2一个是3

  • army 2023-11-15

    原来存入的map,这下就明白了,我们用不到这种场景,我们都是直接存单key,比如5万台车需要实时上传位置,每台车一个key,这种场景下用map效率比单key低。

  • chaz6chez 2023-11-15

    还有这个插件支持redis的nx和xx这两个场景,都需要锁+抢占式循环来进行实现,其他的业务实际上没有用到锁;包括后续的业务需要对mmap进行支持,我也在想办法如何对内存的变动进行有效的监听

  • xiaopi 9天前

    老哥说的很详细,给我就解惑了。我现在有的项目是接收大量的http报文请求,然后分析以后存储到当前进程的内存变量二维数组中,通过条数积累到一定数量后统一上报给后端某个服务中。但是过程中出现了一些问题,比如由于要保证接收http报文的性能,所以开了多个进程,而进程隔离导致无法判断两个进程中的两份报文是否有重复的。所以打算优化这个操作,使用apcu共享内存,但是新的问题又出现了,每个进程中都要判断当前报文的条数是否符合上报的要求,然后符合要求的进行上报,上报完成后清空当前key,重复下一次报文积累,这样一定会出现并发的问题,比如上报了两次,或者上报的条数超过了等等并发问题。 我现在使用上述包试试,使用对上报、apcu的查询、存储进行加锁试试。

  • army 8天前

    @xiaopi 不要存数组,存单key,给个识别的前缀标识,单开一个进程定时去遍历key进行上报即可,存数组这种形式明显不合理且性能低。

  • xiaopi 8天前

    @army 感谢提醒,不过我这个项目对上报的时效性要求比较高,原先的逻辑是存在静态数组中,然后如果数组长度达到100或者10s中后,自动上报,所以是放在Timer定时器中的。改成apcu全局缓存后,请教一下老哥,如果单进程上报会不会效率比较低啊,如果多进程还是要加锁的吧? 还有一个是存单key的话,不太好获取上报的条数吧? 用的是这个扩展 https://www.workerman.net/plugin/133
    不过里面提供了search方法,还不知道效率如何

    默认正则匹配 - 以50条为一次分片查询

    \Workbunny\WebmanSharedCache\Cache::Search('/^abc.+$/', function ($key, $value) use (&$result) { 
      $result[$key] = $value;
    }, 50);
  • army 8天前

    @xiaopi 我认为跟我的场景差不多,我说下我的场景是怎么实现的,应该对你有帮助。
    我这有5万台车实时上报位置,我将每台车的位置信息用apcu去缓存,set key为:“AAA:LINE:DRIVER:车牌号:经纬度“,也就是不断的向缓存中去set,单开了一个进程专门跑timer定时器,每2秒遍历一次apcu的key,组合成inert语句入库,整体下来5万台车的实时位置只需要每2秒执行一次sql,效率高得离谱,存单key不用考虑抢占锁什么的问题,希望能给到你思路。

  • xiaopi 8天前

    @army 感谢,单进程遍历5万个key,这个过程是阻塞的吧,包括插入数据库的IO都是阻塞的,那么2秒钟之内可以完成这些操作吗,会不会导致Timer不准了啊,实际上不是间隔2s了。 还有单开进程指的是在worker进程中指定某个进程专门用来上报的,还是process中自定义的task进程啊? 如果自定义的task进程可以访问到worker进程set的全局缓存吗,我记得有亲缘性的进程才可以访问吧,我没测试过

  • xiaopi 8天前

    感谢

  • army 8天前

    @xiaopi 是阻塞的,所以要单开进程去跑,但是遍历+入库几百毫秒就完成了。单开进程是指 new 一个或多个Worker专门用来跑task

zhezhebie

应该对比一下memcache,看看是不是还是这个情况。

  • chaz6chez 2024-01-08

    memcache也涉及到网络和事件驱动库,效率还是没有apcu高

army

900
积分
0
获赞数
0
粉丝数
2023-03-01 加入
🔝