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

作者: Laruence( ) 本文地址: http://www.laruence.com/2013/03/18/2846.html 转载请注明出处

好久没有更新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 :)

Comments

2013/03/18, FtMan writes: 赞,鸟哥V52013/03/18, shaukei writes: 广告位招租2013/03/18, 花生 writes: 支持鸟哥!! 前排占座!! 广告位招租!!2013/03/18, liexusong writes: 我觉得这个应用使用自旋锁会比较好, 因为读取内存不会花费很多时间.2013/03/18, easy writes: 牛逼~2013/03/18, M2 writes: 阅,先了解了。 鸟哥 v52013/03/18, bide writes: 请问下黄色部分cache为什么会失效?2013/03/18, Actrace writes: 没用过APC,,,这样的设计性能肯定比memcached好,但是可以考虑在内存中建立一个简单的文件系统来实现可调整的内存空间,K/V模式限制了灵活性,虽然性能不错。但是我想如果追求性能的话,应该不会采用PHP来写。我可能宁愿实用tmpfs来共享进程数据。2013/03/18, 徐长龙 writes: 鸟哥是不是说你对读取以前需要“锁”的内容作了一次hash保存在读取区 每次读取后对读取内容进行hash,以此来判断读取的数据完整性? 如果是错误的那么重复读取一次? 可以这么理解吗?2013/03/18, pysche writes: 请教一下: 1. apc. opcode_cache_enable这个配置项已经支持了么?文档及phpinfo里面都没有看到 2. 如果同时安装apc和optimizerplus,opcache部分由谁处理?2013/03/18, hkshadow writes: 循环利用……为了避免此类缓存无法写入和提出的情况下,还得加大内存,来满足内存的吃紧,但一般来说,这够了,以上本人挫见,见谅。2013/03/18, 徐长龙 writes: 多线程并发写是否会出问题?2013/03/18, Cary writes: 鸟哥威武! 学习下~~~2013/03/18, hilojack writes: linuxsir 又有福了2013/03/18, darasion writes: 又见 yet another ... 哈。。。2013/03/18, hliang writes: 为啥make的时候给我报了一个“/yac/storage/yac_storage.c:38:5: 错误: 与‘yac_storage_startup’类型冲突”呢。2013/03/18, hliang writes: 更新了源码,好了。2013/03/18, goosman.lei writes: 是好久没见鸟哥维护blog了.... 先fork了改天抽空看看鸟哥新作...学习学习..2013/03/19, Christopher Jones writes: One other user-data cache project for PHP 5.5 is https://github.com/krakjoe/apcu2013/03/19, Kyli writes: 终于又有更新了,2013/03/19, cys.tony writes: 反正不需要保证数据可靠性,怎么快怎么来吧2013/03/19, justdoit writes: 等了好久,终于更新了!2013/03/21, Anonymous writes: 终于有更新了 鸟哥2013/03/26, php writes: 关注好久了,终于更新了,这下又有新东西可以学习了,呵呵,谢谢分享哦2013/03/28, wenson smith writes: 佩服的我六体投地~2013/03/29, 四不象 writes: 这样写入时很容易形成热点吧。比如下面情景:网站全局配置信息保存在缓存里,当配置文件更新缓存失效时,可能会有几百个请求同时重新将新的配置信息写入缓存。那样配置信息成功写入的可能性就大大降低了。 既然缓存的使用情景设定为读多写少,那就写的时候加锁,读的时候不加锁,如果数据校验错误,尝试二次读取,读取的时候加锁2013/03/29, 雪候鸟 writes: @四不象 恩, 对于你说的场景确实是有一定的概率会这样, 因为我们无锁, 所以就是last win, 不过, 对于Yac解决的场景来说, 比如我们的场景是, Cache用作用户信息缓存, 或者页面输出缓存, 因为都是以UID作为key的一部分, 一个用户会话同事也不会有多个请求产生, 所以不需要这个写锁. 我后续可以考虑加个选项: 是否开启写锁, 来满足你假设的这种场景.(当然, 这种场景也不一定就一定会热点, 三个前提, 1. 请求繁忙, 2. 请求处理时间快, 3. 配置文件比较大) , 谢谢2013/04/28, Yac (Yet Another Cache) – 无锁共享内存Cache | 午后小憩 writes: [...] 本文地址: http://www.laruence.com/2013/03/18/2846.html [...]2013/05/29, 鸚鵡 writes: 您好 我下載了 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] 若需要提供更多的資訊,請告訴我2013/05/29, laruence writes: @鸚鵡 请试用最新的snapshot, https://github.com/laruence/yac 另外, 也可以在github上新建一个issue, 附上你的测试脚本, thanks2013/05/29, 鸚鵡 writes: 您好 更新後,已經不會在發生了 感謝您的協助2013/05/30, 鸚鵡 writes: 另外想請教一個問題 key的长度最大不能超过48个字符 在 utf8 編碼的情況下 每個中文字會佔用幾個字符?2013/06/08, rayban writes: 大神啊 原来APC 是您弄出来了, 自从驾了APC感觉php快了挺多的2013/06/11, Andy writes: Thanks very much2013/07/05, ahuo writes: 安装好之后 重启了php 没有看到 var_dump(class_exists('Yac')) 还是没有啊 这个是啥情况啊?2013/07/05, ahuo writes: yac 安装好了 用了下 速度蛮快的 期待鸟哥出个详细的手册啊2013/07/06, testdown writes: 关注此项目。2013/07/06, testdown writes: add('key', 'value'); echo $yac->delete('key'); // 输出 true echo $yac->delete('key'); // 还输出 true ?> 第二次删除还输出 true,就是这么设计的吗?2013/07/06, testdown writes: 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 参数去掉没问题。2013/07/07, testdown writes: 为什么我使用 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; // 02013/07/07, testdown writes: 如果 Yac::get() 无法通过 &$cas 参数获得 CAS 编号,那么当获取一个键时,它返回一个 false。那这个 false 代表不存在此键还是该键其值就是 false 呢?2013/07/27, linux_chen writes: 鸟哥: 我在我PC机上压测了,有个疑问为什么我压的结果写比读还快呢?2013/08/09, Ce sera peutêtre the cas pour arab-speaking labour sitcom fofolpar writes: [...] lui Avant child Nous iron au Vélodrome sans appréhension en somme united nations authority Nouveauté encore chez Line6 united nations matin you can rewire haze chevy go indication key plans suv tahoe le langage Ces [...]2013/08/20, mark writes: 弱弱问一句 有没有帮助文档2013/09/16, MagentoEye writes: 请问:兼容apache 2.4的新版APC什么时候可以发布?2013/10/22, luckgo writes: 能不能增加类似于apc_bin_loadfile的方法呢?2014/01/10, webpage writes: 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.2014/05/29, 项目常用软件及工具 | 学习笔记 writes: [...] 原来php加速软件一般用的是zend optimizer这个.现在5.3以后的版本就要使用opcache这个了.还有一些并发用的软件yar也推荐使用,这是一款RPC framework.这几款软件全部出自鸟哥之手.除外还有yac这个缓存框架,这个与apc这类的加速软件差不多. [...]2014/08/13, Dnbntravel.Com writes: 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,2014/09/09, pest control islington writes: Paragraph writing is also a excitement, if you be familiar with then you can write or else it is difficult to write.2014/09/11, alex writes: yac在命令行下面存储的内容,在web端调用不到吗?2014/09/19, nxy.in writes: 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,2014/09/25, Kenny writes: 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)2014/10/10, Jeannette writes: 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.2014/11/01, itekiro battery charger es writes: 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.2015/03/19, pitaya writes: 鸟哥,yac缓存要是能和memcache一样支持过期时间就再好不过了2015/04/13, yjq writes: 请问 怎么设置参数? 在配置文件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 02015/05/24, rocky writes: 请问yac可不可以直接存储页面gzip后的字节流,然后也以字节流读取直接response?我的意思是读写都不要有序列化开销。2015/12/18, 13yd writes: 请问 怎么编译的插件 才能在 官方版本中通用 目前是整个 编译好 环境打包出来2016/03/25, Anonymous writes: 请问下,想看yac使用了多少内存,是用slots_size*slots_used算出来的结果么?2016/05/07, 刺客 writes: 在命令行下,貌似不能用,可以实例化,但前缀没有了,而且不管手动加不加前缀,获取不到缓存,也存不了缓存2016/10/08, alixi writes: 这个共享内存可以多进程共享吗?就是多个Php-fpm进程共享这同一个内存. 另外问一下Zend Opcache是多个Php-fpm进程共享一份opcode, 还是每个Php-fpm进程创建独立的opcode?2017/05/03, wazyl writes: 想问下鸟哥,Yac有没有清除全部缓存的方法2017/07/17, qincai writes: 请问下在 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 memory2017/11/03, zx3 writes: 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) )2017/11/14, Anonymous writes: 鸟哥,为啥我本地测试的 APCU 比 YAC快呢2017/11/14, 百诺恩 writes: 挺好的!

Related posts:

Firefox DNS Cache 清除 扩展 V0.1浏览器缓存机制让PHP7达到最高性能的几个TipsPHP对程序员的要求更高pkg-config与LD_LIBRARY_PATHCopyright © 2010 风雪之隅 版权所有, 转载务必注明. 该Feed只供个人使用, 禁止未注明的转载或商业应用. 非法应用的, 一切法律后果自负. 如有问题, 可发E-mail至my at laruence.com.(Digital Fingerprint: 73540ba0a1738d7d07d4b6038d5615e2)

Related Posts:

一个关于Zend O+的小分享Yaconf – 一个高性能的配置管理扩展Weibo LAMP演变 – 6月在上海分享的PPT再一次, 不要使用(include/require)_once关于PHP的编译和执行分离上传进度支持(Upload progress in sessions)PHP5.2.x + APC的一个bug的定位

文章来源:

Author:Laruence
link:http://www.laruence.com/2013/03/18/2846.html