Press "Enter" to skip to content

Yac (Yet Another Cache) – 无锁共享内存Cache

好久没有更新blog了, 这一年来的工作确实很忙….. anyway, 今天终于有新东西可以和大家分享.
这个idea来自一个很简单的想法, 以及目前所遇到的一个机会. 首先我们来谈谈这个机会.
在以前, 很多人都会选择使用APC, APC除了提供Opcode Cache以外, 还会提供一套User Data Cache(apc_store/apc_fetch), 所以对于很多有需求使用User Data Cache的同学, 使用APC, 就没什么问题.
然而, 最近Zend Optimizer Plus开源了, 测试表明, Zend O+在Opcode Cache方面, 因为做了Opcode Cache优化, 所以会比APC要高效, 再后来, PHP5.5已经把Zend O+作为了源代码的一部分. 会随着PHP一起发布.
这就有了个问题, 对于那些既要使用Zend O+的Opcode Cache, 又要使用APC的User Data Cache的同学, 怎么办呢?
开始的时候, 我只是给APC增加了一个开关apc.opcode_cache_enable, 这样一来, 用户就可以使用APC然而关闭opcode cache来达到这个目的, 但是APC的User Data Cache使用的存储机制是和Opcode Cache一样的, 这样的场景要求数据严格正确, 所以锁会比较多, 测试表明, APC的User Data Cache的效率和本地memcached几乎相当.
所以, 我想到了这个idea, 单独开发一个基于共享内存的, 高性能的User Data Cache, 来满足:

  • 1. 我就是想让PHP进程之间共享一些简单的数据
  • 2. 我希望非常高效的缓存一些页面结果

Okey, 那么叫什么呢? 呵呵, 考虑到我之前的Yaf, Yar, 那么自然就叫Yac啦, 🙂
言归正传, 谈谈这个无锁的共享内存Cache的设计思路, 首先, 这个设计基于如下几个经验假设:

  • 1. 对于一个应用来说, 同名的Cache键, 对应的Value, 大小几乎相当.
  • 2. 不同的键名的个数是有限的.
  • 3. Cache的读的次数, 远远大于写的次数.
  • 4. Cache不是数据库, 即使Cache失效也不会带来致命错误.
  • 5. 典型的应用场景类似于:
    <?php
        if (!($data = cache_fetch($key))) {
             /* cache不存在 */
             $data =  从接口/数据库取数据();
             cache_set($key, $data);
        }
    ?>
    

好, 基于这些假设, 我们来看看如何实现Yac, 首先Cache最常用的就是读了, 那么能不能做到读不加锁呢?
这个很容易, 不加锁的读, 拿到以后做数据校验, 如果校验成功, 增说明查询成功, 否则就认为查询失败, 这是一种常用的采用CPU来换锁的方法. 对于目前的服务器, 大部分都是多核的, 如果是加锁, 那么对CPU是一个极大的浪费.

那么, 与其让这些CPU空闲, 不如大家同时读, 大不了回来多做一个数据校验(Yac 采用的是crc校验)
okey, 读锁很容易解决, 但是写锁呢? 我们先来看看Yac的共享内存分配模型图:

Key空间在启动的时候就是确定大小的, 这个是基于上面的假设(2), 默认的(在64位Linux上), Yac会开辟32768个Key Slots, 也就是说你最多可用存储32768个不同的Cache值, 当然这个大小你可以通过yac.keys_memory_size来调整, 如果你设置yac.keys_memory_size为32M的话, 你就能得到262144个Key Slots.
Yac采用双散列法来解决Hash冲突, 首选的Hash函数是进来比较流行的 MurmurHash.
共享内存会被按照固定带下分为尽量多的小块, 默认是4M一小块, 然后Key的值会根据Key的Hash, 来选择到底在那一块内存上申请空间, 从而减少写的时候可能的冲突.
而对于大块内存分配的时候, 只需要操作一个segment->pos指针, 也就是只需要做一个加操作, 从而减少多个进程同时在同一块内存分配的时候可能出现的冲突.
那么, 万一真正的发生了冲突呢? 比如A进程申请了40字节, B进程申请了60字节, 但是Pos只增加了60字节. 这个时候有如下几种情况:
1. A写完了数据, 返回成功, 但是B进程又写完了数据返回成功, 最终B进程的Cache种上了, 而A进程的被踢出了.
2. B进程写完了数据, 返回成功, A进程又写完了数据返回成功, 最终A进程的Cache种上了, B进程的被踢出.
3. A进程写一半, B进程写一半, 然后A进程又写一半, B进程又写一半, 都返回成功, 但最终, 缓存都失效.
可见, 最严重的错误, 就是A和B的缓存都失效, 但是Yac不会把错误数据返回给用户, 当下一次来查询Cache的时候, 因为存在crc校验, 所以都miss.
那么, 考虑到上面的假设(3), (4), (5), okey, Not a big deal, 对吧? 没关系, 错就错吧, 呵呵
那么, 当内存写满了呢? 再看上面的内存分配图, 注意到红色的部分没有?
当一个新的Key到来的时候, Yac会尝试查找合适的Key Slot, 如果找到同名的Key, 那么紧接着判断原来Key的Value内存大小, 考虑到假设(1), 并且Yac在分配内存的时候, 会有意的多给一些内存. 所以, 很大的概率上, 你不需要重新分配内存, 只需要再原有的内存基础上写入新数据即可.
那么万一原有的内存不够呢? 那就分配呗.
这个时候, 假设内存已经分配完了, Yac就会在选定的内存快上, 重置pos, 从头开始分配, 注意到上图的红色部分, 就是新写入的数据, 而黄色部分就是因为写入的新数据, 导致Cache失效的部分. 也就是说, 并不会导致大量的Cache失效.
那么, 万一Key Slots不够了呢?
Yac会从目地Key Slots开始, 根据Hash路径, 选取5个Keys slot, 根据LRU, 踢出一个.
那么, 这样的Cache, 性能到底怎么样呢? 我做了一个和APC对比的简单测试(ab -n 10000 -c 50), 测试脚本如下:
Yac:

<?php
$yac = new Yac();
for ($i = 0; $i<1000; $i++) {
    $key =  "xxx" . rand(1, 10000);
    $value = str_repeat("x", rand(1, 10000));
    if (!$yac->set($key, $value)) {
        var_dump("write " . $i);
    }
    if ($value != ($new = $yac->get($key))) {
        var_dump("read " . $i);
    }
}
var_dump($i);

APC:

<?php
for ($i = 0; $i<1000; $i++) {
    $key =  "xxx" . rand(1, 10000);
    $value = str_repeat("x", rand(1, 10000));
    if (!apc_store($key, $value)) {
        var_dump("write " . $i);
    }
    if ($value != ($new = apc_fetch($key))) {
        var_dump("read " . $i);
    }
}
var_dump($i);

最终的结果:
Yac

Write errors:           0
Total transferred:      597358 bytes
HTML transferred:       368358 bytes
Requests per second:    359.69 [#/sec] (mean)
Time per request:       139.010 [ms] (mean)
Time per request:       2.780 [ms] (mean, across all concurrent requests)
Transfer rate:          209.83 [Kbytes/sec] received

APC的:

Write errors:           0
Total transferred:      7050591 bytes
HTML transferred:       6828577 bytes
Requests per second:    46.79 [#/sec] (mean)
Time per request:       1068.502 [ms] (mean)
Time per request:       21.370 [ms] (mean, across all concurrent requests)
Transfer rate:          322.20 [Kbytes/sec] received

好了, 主要的思想就是这些, 接下来说说Yac的限制吧:
1. key的长度最大不能超过48个字符. (我想这个应该是能满足大家的需求的, 如果你非要用长Key, 可以MD5以后再存)
2. Value的最大长度不能超过64M, 压缩后的长度不能超过1M.
3. 当内存不够的时候, Yac会有比较明显的踢出率, 所以如果要使用Yac, 那么尽量多给点内存吧.
感谢@cydu @cunsheng @rodin, @猫咪的万岁爷_宋Q等同学给的建议.
最后, Yac的代码已经上传到了github: Yac, 不过目前还是完善阶段, 还不支持Windows, 我会继续完善, 有兴趣的同学可以抢先试用, 更加感谢如果能帮忙找找bug, 做做优化. thanks 🙂

70 Comments

  1. jane
    jane December 5, 2019

    鸟哥大佬,PHP更新7.4稳定版了,yac能更新支持下吗?我更新并安装好提示版本不合适。

  2. Anonymous
    Anonymous November 14, 2017

    鸟哥,为啥我本地测试的 APCU 比 YAC快呢

  3. zx3
    zx3 November 3, 2017

    cli模式下 变量无法共享!
    php ./yac.php
    <?php
    error_reporting(E_ALL);
    $key = 'foo';
    $yac = new Yac('test_');
    print_r($yac);
    exec('php ./yac_work.php', $output, $code);
    print_r($output);
    =======================yac_work.php
    set($key, ‘bar’, 3600);
    var_dump($yac->get($key));
    结果:
    Yac Object
    (
    [_prefix:protected] => test_
    )
    string(3) “bar”
    Array
    (
    [0] => Yac Object
    [1] => (
    [2] => [_prefix:protected] => test_
    [3] => )
    [4] => bool(false)
    )

  4. qincai
    qincai July 17, 2017

    请问下在 cli下面多线程使用yac,出现这样的错误信息,有没有办法解决?
    PHP Fatal error: Shared memory allocator startup failed at ‘mmap’: Cannot allocate memory in Unknown on line 0
    PHP Fatal error: Unable to start yac module in Unknown on line 0
    Out of memory

  5. wazyl
    wazyl May 3, 2017

    想问下鸟哥,Yac有没有清除全部缓存的方法

  6. alixi
    alixi October 8, 2016

    这个共享内存可以多进程共享吗?就是多个Php-fpm进程共享这同一个内存.
    另外问一下Zend Opcache是多个Php-fpm进程共享一份opcode,
    还是每个Php-fpm进程创建独立的opcode?

  7. 刺客
    刺客 May 7, 2016

    在命令行下,貌似不能用,可以实例化,但前缀没有了,而且不管手动加不加前缀,获取不到缓存,也存不了缓存

  8. Anonymous
    Anonymous March 25, 2016

    请问下,想看yac使用了多少内存,是用slots_size*slots_used算出来的结果么?

  9. 13yd
    13yd December 18, 2015

    请问 怎么编译的插件 才能在 官方版本中通用
    目前是整个 编译好 环境打包出来

  10. rocky
    rocky May 24, 2015

    请问yac可不可以直接存储页面gzip后的字节流,然后也以字节流读取直接response?我的意思是读写都不要有序列化开销。

  11. yjq
    yjq April 13, 2015

    请问 怎么设置参数?
    在配置文件php.ini中如下设置有问题吗?
    [yac]
    yac.enable = 1
    yac.keys_memory_size = 8M ; 4M can get 30K key slots, 32M can get 100K key slots
    yac.values_memory_size = 128M
    yac.compress_threshold = -1
    yac.enable_cli = 1 ; whether enable yac with cli, default 0

  12. pitaya
    pitaya March 19, 2015

    鸟哥,yac缓存要是能和memcache一样支持过期时间就再好不过了

  13. itekiro battery charger es
    itekiro battery charger es November 1, 2014

    Your style is unique compared to other people I’ve
    read stuff from. Many thanks for posting when you have the
    opportunity, Guess I will just book mark this web site.

  14. Jeannette
    Jeannette October 10, 2014

    Hi there, always i used to check blog posts here early in the break of day, as i enjoy to find out more and more.

  15. Kenny
    Kenny September 25, 2014

    When someone writes an article he/she retains the plan of a
    user in his/her brain that how a user can know it. Thus that’s why
    this piece of writing is great. Thanks!
    Look at my page – dating on line (Kenny)

  16. nxy.in
    nxy.in September 19, 2014

    Spot on with this write-up, I honestly think this site needs far more
    attention. I’ll probably be back again to read through more, thanks for the advice!
    my blog post :: 105.1, nxy.in,

  17. alex
    alex September 11, 2014

    yac在命令行下面存储的内容,在web端调用不到吗?

  18. pest control islington
    pest control islington September 9, 2014

    Paragraph writing is also a excitement, if
    you be familiar with then you can write or else it is difficult to write.

  19. Dnbntravel.Com
    Dnbntravel.Com August 13, 2014

    There are several methods of making salvia extract. At times, there are company appointed distributors and
    companies also who sell their clothes and accessories at the wholesale or discounted prices.
    It depends how much you value your sight,
    if you want to have a long and illustrious time racing around the track or pulling
    45ft heel clickers, you need to be able to see
    where you are going to land.
    Also visit my web page Credit Report Free,
    Dnbntravel.Com,

  20. […] 原来php加速软件一般用的是zend optimizer这个.现在5.3以后的版本就要使用opcache这个了.还有一些并发用的软件yar也推荐使用,这是一款RPC framework.这几款软件全部出自鸟哥之手.除外还有yac这个缓存框架,这个与apc这类的加速软件差不多. […]

  21. webpage
    webpage January 10, 2014

    Permanent installations in vehicles gave way to the portable Bag Phones, built with a cigarette lighter plug.
    You can then go through the hyperlink to observe the entire
    is a result of the Wikipedia site. To my mind the idea
    of woman president is something speculative, going past the generally accepted rules and also at the very best be subject to the theoretical and philosophical comprehension.

  22. luckgo
    luckgo October 22, 2013

    能不能增加类似于apc_bin_loadfile的方法呢?

  23. MagentoEye
    MagentoEye September 16, 2013

    请问:兼容apache 2.4的新版APC什么时候可以发布?

  24. mark
    mark August 20, 2013

    弱弱问一句 有没有帮助文档

  25. linux_chen
    linux_chen July 27, 2013

    鸟哥:
    我在我PC机上压测了,有个疑问为什么我压的结果写比读还快呢?

  26. testdown
    testdown July 7, 2013

    如果 Yac::get() 无法通过 &$cas 参数获得 CAS 编号,那么当获取一个键时,它返回一个 false。那这个 false 代表不存在此键还是该键其值就是 false 呢?

  27. testdown
    testdown July 7, 2013

    为什么我使用 Yac::get() 获取不到 cas 的值呢?
    phpcode:
    $yac = new Yac(‘DEV:’);
    $yac->flush();
    $yac->set(‘A’, ‘A’);
    $cas = 0;
    $yac->get(‘A’, $cas);
    echo $cas; // 0
    $yac->get([‘A’], $cas);
    echo $cas; // 0

  28. testdown
    testdown July 6, 2013

    bug:
    $yac = new Yac(‘DEV:’);
    echo $yac->set(‘A’, 10000, 1);
    echo $yac->set(‘A’, 10000, 1); // false
    echo $yac->set(‘A’, ‘10000’, 1); // true
    echo $yac->set(‘A’, 10000, 1); // true
    把 ttl 参数去掉没问题。

  29. testdown
    testdown July 6, 2013

    add(‘key’, ‘value’);
    echo $yac->delete(‘key’); // 输出 true
    echo $yac->delete(‘key’); // 还输出 true
    ?>
    第二次删除还输出 true,就是这么设计的吗?

  30. ahuo
    ahuo July 5, 2013

    yac 安装好了 用了下 速度蛮快的 期待鸟哥出个详细的手册啊

  31. ahuo
    ahuo July 5, 2013

    安装好之后 重启了php 没有看到
    var_dump(class_exists(‘Yac’)) 还是没有啊 这个是啥情况啊?

  32. Andy
    Andy June 11, 2013

    Thanks very much

  33. rayban
    rayban June 8, 2013

    大神啊 原来APC 是您弄出来了, 自从驾了APC感觉php快了挺多的

  34. 鸚鵡
    鸚鵡 May 30, 2013

    另外想請教一個問題
    key的长度最大不能超过48个字符
    在 utf8 編碼的情況下
    每個中文字會佔用幾個字符?

  35. 鸚鵡
    鸚鵡 May 29, 2013

    您好
    更新後,已經不會在發生了
    感謝您的協助

  36. 鸚鵡
    鸚鵡 May 29, 2013

    您好
    我下載了 yac 並安裝使用
    但是我察覺到一個現象
    我的伺服器是 fedora 14,並安裝 cacti
    當我使用 yac 時,系統定時執行 cacti 的 poller.php 時
    程序會無法正常結束,並出現錯誤訊息: Segmentation fault
    /var/log/message 中會出現下面的訊息
    kernel: [1242737.739304] php[14321]: segfault at 7f96b2f136b6 ip 00007f96b2f136b6 sp 00007fff9d9b53b0 error 14 in libgpg-error.so.0.7.0[7f96b3276000+a3000]
    kernel: [1242737.801953] php[14322]: segfault at 7fa134d686b6 ip 00007fa134d686b6 sp 00007fff66eedb90 error 14 in libgpg-error.so.0.7.0[7fa1350cb000+a3000]
    kernel: [1242737.855070] php[14323]: segfault at 7f8eaf5ba6b6 ip 00007f8eaf5ba6b6 sp 00007fff4d4c7e60 error 14 in libgpg-error.so.0.7.0[7f8eaf91d000+a3000]
    若需要提供更多的資訊,請告訴我

  37. 雪候鸟
    雪候鸟 March 29, 2013

    @四不象 恩, 对于你说的场景确实是有一定的概率会这样, 因为我们无锁, 所以就是last win, 不过, 对于Yac解决的场景来说, 比如我们的场景是, Cache用作用户信息缓存, 或者页面输出缓存, 因为都是以UID作为key的一部分, 一个用户会话同事也不会有多个请求产生, 所以不需要这个写锁.
    我后续可以考虑加个选项: 是否开启写锁, 来满足你假设的这种场景.(当然, 这种场景也不一定就一定会热点, 三个前提, 1. 请求繁忙, 2. 请求处理时间快, 3. 配置文件比较大) ,
    谢谢

  38. 四不象
    四不象 March 29, 2013

    这样写入时很容易形成热点吧。比如下面情景:网站全局配置信息保存在缓存里,当配置文件更新缓存失效时,可能会有几百个请求同时重新将新的配置信息写入缓存。那样配置信息成功写入的可能性就大大降低了。
    既然缓存的使用情景设定为读多写少,那就写的时候加锁,读的时候不加锁,如果数据校验错误,尝试二次读取,读取的时候加锁

  39. php
    php March 26, 2013

    关注好久了,终于更新了,这下又有新东西可以学习了,呵呵,谢谢分享哦

  40. Anonymous
    Anonymous March 21, 2013

    终于有更新了 鸟哥

  41. justdoit
    justdoit March 19, 2013

    等了好久,终于更新了!

  42. cys.tony
    cys.tony March 19, 2013

    反正不需要保证数据可靠性,怎么快怎么来吧

  43. Kyli
    Kyli March 19, 2013

    终于又有更新了,

  44. goosman.lei
    goosman.lei March 18, 2013

    是好久没见鸟哥维护blog了….
    先fork了改天抽空看看鸟哥新作…学习学习..

  45. hliang
    hliang March 18, 2013

    更新了源码,好了。

  46. hliang
    hliang March 18, 2013

    为啥make的时候给我报了一个“/yac/storage/yac_storage.c:38:5: 错误: 与‘yac_storage_startup’类型冲突”呢。

  47. darasion
    darasion March 18, 2013

    又见 yet another … 哈。。。

  48. Cary
    Cary March 18, 2013

    鸟哥威武! 学习下~~~

  49. 徐长龙
    徐长龙 March 18, 2013

    多线程并发写是否会出问题?

  50. hkshadow
    hkshadow March 18, 2013

    循环利用……为了避免此类缓存无法写入和提出的情况下,还得加大内存,来满足内存的吃紧,但一般来说,这够了,以上本人挫见,见谅。

  51. pysche
    pysche March 18, 2013

    请教一下:
    1. apc. opcode_cache_enable这个配置项已经支持了么?文档及phpinfo里面都没有看到
    2. 如果同时安装apc和optimizerplus,opcache部分由谁处理?

  52. 徐长龙
    徐长龙 March 18, 2013

    鸟哥是不是说你对读取以前需要“锁”的内容作了一次hash保存在读取区
    每次读取后对读取内容进行hash,以此来判断读取的数据完整性?
    如果是错误的那么重复读取一次?
    可以这么理解吗?

  53. Actrace
    Actrace March 18, 2013

    没用过APC,,,这样的设计性能肯定比memcached好,但是可以考虑在内存中建立一个简单的文件系统来实现可调整的内存空间,K/V模式限制了灵活性,虽然性能不错。但是我想如果追求性能的话,应该不会采用PHP来写。我可能宁愿实用tmpfs来共享进程数据。

  54. bide
    bide March 18, 2013

    请问下黄色部分cache为什么会失效?

  55. M2
    M2 March 18, 2013

    阅,先了解了。 鸟哥 v5

  56. easy
    easy March 18, 2013

    牛逼~

  57. liexusong
    liexusong March 18, 2013

    我觉得这个应用使用自旋锁会比较好, 因为读取内存不会花费很多时间.

  58. 花生
    花生 March 18, 2013

    支持鸟哥!!
    前排占座!!
    广告位招租!!

Leave a Reply

Your email address will not be published. Required fields are marked *