原文摘自:
缓存穿透、并发和失效,来自一线架构师的解决方案
https://community.qingcloud.com/topic/463
在我们的实践中,原文中有部分解决方案已经过时,在原文的基础上,添加了几个我们常用的方案。
by shuhai, admin@4wei.cn
我们在用缓存的时候,不管是Redis或者Memcached,基本上会通用遇到以下三个问题:
- 缓存穿透
- 缓存并发
- 缓存失效
- 同步、复制中断
##缓存穿透
![https://pek3a.qingstor.com/community/resource/pic/2016-6-23-cache-1.png][1]
![https://pek3a.qingstor.com/community/resource/pic/2016-6-23-cache-2.png][2]
![https://pek3a.qingstor.com/community/resource/pic/2016-6-23-cache-3.png][3]
注:上面三个图会有什么问题呢?
我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回。
这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,可能DB就挂掉了。
那这种问题有什么好办法解决呢?
要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。有一个比较巧妙的作法是,可以将这个不存在的key预先设定一个值。比如,"key" , “&&”。
在返回这个&&值的时候,我们的应用就可以认为这是不存在的key,那我们的应用就可以决定是否继续等待继续访问,还是放弃掉这次操作。如果继续等待访问,过一个时间轮询点后,再次请求这个key,如果取到的值不再是&&,则可以认为这时候key有值了,从而避免了透传到数据库,从而把大量的类似请求挡在了缓存之中。
你应该注意,这里缓存未命中的原因,更值得我们关注。
当缓存空间满了,同步失败,网络阻塞,缓存写失败等原因,会出现缓存服务器上并没有这个key。
或者因为同步中断,在主从架构中,写到主却未同步到从的悲剧,就会出现请求穿透到DB层的情况。
出现这样的情况,一定不能直接将请求穿透到DB层,避免DB当机影响其它业务。
我们的解决方案可以参考。
- 当业务中请求量特别高,缓存未命中的情况,应该在建立DB保护的基础上,放弃一定比例的请求,直接返回空
- 可以随机释放一些请求到DB,控制好流量的话,能保证缓存重建且DB不受极端压力
- 后端异步定时检查缓存,主动建立这些缓存
- 通过建立二级缓存,把之前成功获取的缓存数据放到本机缓存,文件也好,共享内存也好,接受一些过期数据
##缓存并发
有时候如果网站并发访问高,一个缓存如果失效,可能出现多个进程同时查询DB,同时设置缓存的情况,如果并发确实很大,这也可能造成DB压力过大,还有缓存频繁更新的问题。
我现在的想法是对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询。
这种情况和刚才说的预先设定值问题有些类似,只不过利用锁的方式,会造成部分请求等待。
##缓存失效
引起这个问题的主要原因还是高并发的时候,平时我们设定一个缓存的过期时间时,可能有一些会设置1分钟啊,5分钟这些,并发很高时可能会出在某一个时间同时生成了很多的缓存,并且过期时间都一样,这个时候就可能引发一当过期时间到后,这些缓存同时失效,请求全部转发到DB,DB可能会压力过重。
那如何解决这些问题呢?
其中的一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
我们讨论的第二个问题时针对同一个缓存,第三个问题时针对很多缓存。
接下来我们将发表一些自己的缓存高可用实践,如《基于云平台的缓存集群高可用实践》,欢迎关注。
总结
1、缓存穿透:查询一个必然不存在的数据。比如文章表,查询一个不存在的id,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成影响。
2、缓存失效:如果缓存集中在一段时间内失效,DB的压力凸显。这个没有完美解决办法,但可以分析用户行为,尽量让失效时间点均匀分布。
当发生大量的缓存穿透,例如对某个失效的缓存的大并发访问就造成了缓存雪崩。
精彩问答
问题:如何解决DB和缓存一致性问题?
当修改了数据库后,有没有及时修改缓存。这种问题,以前有过实践,修改数据库成功,而修改缓存失败的情况,最主要就是缓存服务器挂了。而因为网络问题引起的没有及时更新,可以通过重试机制来解决。而缓存服务器挂了,请求首先自然也就无法到达,从而直接访问到数据库。那么我们在修改数据库后,无法修改缓存,这时候可以将这条数据放到数据库中,同时启动一个异步任务定时去检测缓存服务器是否连接成功,一旦连接成功则从数据库中按顺序取出修改数据,依次进行缓存最新值的修改。
问题:问下缓存穿透那块!例如,一个用户查询文章,通过ID查询,按照之前说的,是将缓存的KEY预先设置一个值,,如果通过ID插过来,发现是预先设定的一个值,比如说是“&&”,那之后的继续等待访问是什么意思,这个ID什么时候会真正被附上用户所需要的值呢?
我刚说的主要是咱们常用的后面配置,前台获取的场景。前台无法获取相应的key,则等待,或者放弃。当在后台配置界面上配置了相关key和value之后,那么以前的key &&也自然会被替换掉。你说的那种情况,自然也应该会有一个进程会在某一个时刻,在缓存中设置这个ID,再有新的请求到达的时候,就会获取到最新的ID和value。
问题:其实用Redis的话,那天看到一个不错的例子,双key,有一个当时生成的一个附属key来标识数据修改到期时间,然后快到的时候去重新加载数据,如果觉得key多可以把结束时间放到主key中,附属key起到锁的功能。
这种方案,之前我们实践过。这种方案会产生双份数据,而且需要同时控制附属key与key之间的关系,操作上有一定复杂度。
问题:多级缓存是什么概念呢?
多级缓存就像我今天之前给大家发的文章里面提到了,将Ehcache与Redis做二级缓存,就像我之前写的文章 http://www.jianshu.com/p/2cd6ad416a5a 提到过的。但同样会存在一致性问题,如果我们需要强一致性的话,缓存与数据库同步是会存在时间差的,所以我们在具体开发的过程中,一定要根据场景来具体分析,二级缓存更多的解决是,缓存穿透与程序的健壮性,当集中式缓存出现问题的时候,我们的应用能够继续运行。
发布于2016年7月6日分类php于缓存穿透、并发和失效、同步中断,最佳实践及优化方案留下评论
php版本的文件夹合并
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
#!/bin/env php<?phperror_reporting(7);$dir1 = $argv[1];$dir2 = $argv[2];if(empty($dir1) || empty($dir2)){ echo "php {$argv[0]} dir_merged dir_merge_save
"; exit(0);}if(! is_dir($dir1)){ echo "{$dir1} is not a dir
"; exit(0);}if(! is_dir($dir2)){ echo "{$dir2} is not a dir
"; exit(0);}merge_dir($dir1, $dir2);function merge_dir($from, $to){ $from = rtrim($from, "/")."/"; $to = rtrim($to, "/")."/"; echo "search file from {$from}"; $files = glob($from."*"); echo " done
"; foreach($files as $f) { $relative_path = substr_replace($f, "", 0, strlen($from)); $ff = $to . $relative_path; //目录不存在则直接复制目录 if(is_dir($f)) { if(!is_dir($ff)) { $cmd = "cp -r '{$f}' '{$ff}'"; `$cmd`; logs($relative_path); } else { merge_dir($f, $ff); } }else{ if(!is_file($ff)) { copy($f, $ff); logs($relative_path); continue; } //对特殊文件进行检查 $ext = substr($relative_path, strrpos($relative_path, ".")+1); if($ext == "apk" && filesize($f) != filesize($ff)) { $compare = apk_version_compare($f, $ff); if($compare) { copy($f, $ff); logs($relative_path); } } } }}function apk_version_compare($apk1, $apk2){ return aapt($apk1) > aapt($apk2);}function aapt($apk){ $apk = realpath($apk); $aapt = "aapt d badging {$apk} | grep versionCode | awk '{print $3}'"; $aapt = `$aapt`; return substr($aapt, 13, -2);}function logs($file){ printf("%s
", $file);} |
发布于2016年1月27日分类php于php版本的文件夹合并留下评论
基于CloudFlare + Conoha搭建的企业级廉价CDN
中国的流量很贵,流量越大越贵。
目前我负责的业务一半国内,一半海外。目前日均请求过千万,流量过10T,总成本不超过500元/天。跟传统的CDN服务相比,便宜到可以忽略。
CDN介绍
在项目早期,为了业务简单,直接使用传统CDN提供商网宿,在CDN宽带超过 5G 的时候,CDN的成本开始显现出来。
一般国内的CDN(网宿、帝联、蓝汛,还有一些如七牛、又拍这样的二手贩子)均有宽带和流量计费,新起的云平台(阿里、腾讯、uCloud)基本上使用流量计费。
国内的CDN价格,网宿的95峰值计费,一般价格在45-55元/M,根据宽带高值、公司背景、销售人员关系等等因素,合同的价格可以签到30-40元左右,其它二线的CDN可以签到25-30元左右。
其它的云服务平台,价格相对固定,业务购买一般是自助服务,价格基本上没得可谈,普遍价格在0.9元-1.2元/G。
国内服务商运营的国外CDN价格比国内高出几个数量级,网宿的报价为150元/M,其海外节点为少数自建加akamai代理。其它几家传统的CDN服务商完全代理甚至没有海外业务。
海外的CDN服务商,计费一般是按流量计费,常见的如MaxCDN, KeyCDN,价格一般为0.04-0.1美元/G。
业务介绍
我们的业务早期使用网宿,业务从0M跑到3G,业务一直比较稳定。后来业务推到海外,直到CDN的成本越来越高,CDN成本差不多占了利润的一半。
网宿现在有市场优势,价格一直砍不下来。经过跟几家CDN服务咨询、测试,从最初的七牛,MaxCDN,KeyCDN,到最后的CloudFlare+Conoha,终于实现成本与速度的平衡。
在第一次使用CloudFlare和Conoha的时候,都被其极低的价格吓得不敢相信,企业级项目中总觉得低价的服务肯定会出问题(事实上确实是这样,毕竟价格高的也会出问题)。
cloudflare.com
和国内的安全宝、百度云加速的业务类似,CloudFlare提供的安全服务是帮助网站阻止来自网络的黑客攻击、垃圾邮件等,并提升网页的浏览速度,这和一般的安全软件往往会影响网页的运行速度大相径庭。目前CloudFlare在全球拥有23个数据中心,如果用户使用了其服务,那么网络流量将通过CloudFlare的全球网络智能路由。CloudFlare会自动优化用户的网页交付,以期达到最快的页面加载时间以及最佳性能。 CloudFlare提供包括CDN、优化工具、安全、分析以及应用等服务。
2015年9月,CloudFlare正式宣布与百度合作改善外国网站在中国的可访问性。双方早在去年7月就签署了合作协议,CloudFlare将其技术转让给百度(CEO称此举是为了增加信任),而采用CloudFlare技术的百度云加速服务于去年12月开始运作。
CloudFlare称,百度在中国大陆的17个中心地区节点与 CloudFlare 全球的45个节点结合起来,提升中国国内外的访问体验,当客户激活中国网络服务后,他们的中国访问者将会访问百度节点,而CloudFlare节点则继续服务海外访问者,可将中国流量的响应时间缩短超过200毫秒。
但外国客户如果要激活中国网络服务将需要ICP备案。网站备案是中国工信部要求所有在中国大陆使用主机或CDN服务的许可证书。
via:http://www.cnbeta.com/articles/429815.htm
conoha.jp
ConoHa日本gmo.jp旗下的一个VPS主机商,成立于2014年。ConoHa提供日本、新加坡及美国机房云VPS服务。
ConoHa的业务跟国内的云服务平台类似。说是云服务器,个人感觉跟vps差不多,跟常见的Linode和digitalocean基本上相同。
ConoHa的官网支持中文,支持信用卡和支付宝,支持扩容支持按时间收费,可以按小时按月计费。
ConoHa的价格非常廉价,常见的配置如1GB、2Core、SSD50GB,每个月50元人民币。
最重要的是,ConoHa不限流量,100M宽带,可扩容,可加IP。
ConoHa有激励政策,通过我的邀请链接注册,你可以获取1000日元,相当于免费赠送一台主机,
https://www.conoha.jp/referral/?token=V3xoVa5812CYk15rhJkKiiNc5E340f3uNaNjQCiaBmnWZdA30Zk-0VJ
业务实现
在CloudFlare中,添加一个免费(我们使用付费方案是200美元/月)的域名,主要使用其提供的IP隐藏、文件缓存来实现防DDOS和CDN需求。
在我们的单个项目中,CloudFlare每天可以缓存数百万次的请求,差不多8-12TB左右的流量。按最低4美分的价格,每天的CDN成本应该在3000元左右,一个月10万元,一年超过100百万元。
采用了CloudFlare以后,只需要1200元一个月,一年只需要1万元,CloudFlare实实再再的帮老板省了一大笔钱。
老板是不是该奖励我一台特斯拉了哎?
经过我们跟数家CDN服务商的对比和测速,给CloudFlare的节点和速度5个星,稳定性和速度让我们非常意外,点赞。
但作为一个非专业的CDN解决方案,CloudFlare目前还没有完善的数据报表。不能分析请求的URL列表,不能按常见维度分析用户和行为,对运营人员来讲是个缺憾。
同时,CloudFlare默认只对一些基本的文本、图片文件进行缓存,有特殊的文件,比如apk、exe等文件,则颇费周折。
via:https://support.cloudflare.com/hc/en-us/articles/200172516
CloudFlare中非默认缓存的文件,比如apk文件,如果需要缓存,则不能携带任何参数,否则会回源,无法缓存。
CloudFlare另外一个巨坑便是其缓存重建只能通过pull方式。回源pull方式存在严重的风险。我们这边出现过一个50M的文件清理缓存以后,源站瞬间出现近千个请求。这包括CloudFlare的全球节点的缓存请求,也有用户的的真实请求。
CloudFlare无法设置限速,所有请求全落到源站,源站的流量瞬间飙上500M。放在云上的整个路由器下的所有业务全部卡死。
源站的流量可以通过扩容的方式提升上限,为了解决巨大的回源请求,我们曾差不多把宽带提高了到1G。
云平台1.2元/G的流量价格也是贵到滴血,逼得我们必须要为回源的请求再构建一个廉价的中转服务器。
在测试过Linode和digitalocean以后,我们选中了ConoHa。主要看中其极低的价格、无限流量、多节点等优势。
我们把文件rsync到ConoHa的多台主机以后,再将CloudFlare的回源请求重定向到不同的ConoHa服务器。
虽然业务架构越来越复杂,但比起源站业务被中断,以及宽带扩容的成本,ConoHa上一个月不到一千块钱的成本实再便宜得让人心疼。
其它
在目前的架构中,所采用的方案均为比较新的服务商,稳定性存在非常大的风险,如果有一天CloudFlare或者ConoHa倒闭,则会对现在的业务造成致命的影响。
为了降低风险,我们也在采用了一些优化手段,也在积极寻找备用方案。
比如,为了避免单个域名流量太大,被CloudFlare封掉,或者要求我们使用更高的付费方案,我们把CDN拆到多个域名下,减少单个域名的请求和流量。我们也在寻找与CloudFlare相同的的平台,比如Incapsula。甚至有人建议直接购买廉价vps自建CDN。考虑到现在云计算基础服务相对完善,自建一个全球性的CDN平台也不是太大难事。
为了避免ConoHa出故障,我们也在其它几个廉价的vps服务商购买了几个备用服务器,定期将文件同步过去进行灾备。
同时,我们发现CloudFlare中添加一个在国内备案的域名,其节点尚不能确认是否已经使用了百度云加速的节点,有没有知晓内幕的朋友。
最后
经过项目的实践和数月的运营,这个廉价的CDN实现,节点数可以达到成熟商用CDN节点数的30%-80%(海外优势尤其明显)。跟同行业的一些朋友交流时发现,也有其它同行业的业务使用CloudFlare+Digitalocean实现,稳定支撑月流水数百万美元的业务,而其成本非常低廉。
云服务越来越成熟,成本越来越低廉,对于我们这样的创业公司来讲,是一大福音。而对于那些传统的基础服务商来讲,又是什么呢?
发布于2016年1月10日分类php基于CloudFlare + Conoha搭建的企业级廉价CDN有4条评论
简单获取并验证client ip
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
function get_ip_address(){ foreach (array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR') as $key) { if (array_key_exists($key, $_SERVER) === true) { foreach (explode(',', $_SERVER[$key]) as $ip) { $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { return $ip; } } } }} |
