第1页
高性能Web应用 缓存架构设计浅谈
Robbin Fan
Thursday, August 4, 11
第2页
高性能Web应用特征
• 大规模,高并发的访问请求 • 服务的高可用性 • 平滑的deployment • 良好的可伸缩性
Thursday, August 4, 11
第3页
特殊应用场景
• 长连接 • Web chat,Web game • 网络IO资源消耗型,非CPU消耗型 • EventMachine,Twisted, Node.js
Thursday, August 4, 11
第4页
高性能Web应用 是架构问题,不是
Rails框架问题
Thursday, August 4, 11
第5页
Constraint is liberty
Thursday, August 4, 11
第6页
JavaEye统计数据
• 3,000,000+ Rails dynamic requests per day • 15,000,000+ HTTP requests per day • 600+ HTTP requests/s on peak time • 1,000,000+ full-text search per day • 1400+ current connections/s on peak time • 600 average Memcached get operations • 150 average SQL queries per second
Thursday, August 4, 11
第7页
Web Server
lighttpd analytics
fastcgi(ruby)
memcached SearchServer
Thursday, August 4, 11
DB Server
mysql PDF Creating
第8页
Web Server
DB Server
CPU Peak < 60% IO Wait < 5%
CPU Peak < 25% IO Wait < 15%
Thursday, August 4, 11
第9页
Rails高性能Web之道
• 以REST的架构风格来编写Rails应用 • 从整体架构上设计各个层面缓存方案 • 消除架构上的各个单点性能瓶颈 • 多进程的分布式应用架构部署
Thursday, August 4, 11
第10页
REST
资源 + 操作 = REST
Thursday, August 4, 11
第11页
REST
URL 资源 + 操作 = REST
Thursday, August 4, 11
第12页
REST
URL + 资源 + 操作 = REST
Thursday, August 4, 11
第13页
REST
URL + HTTP 资源 + 操作 = REST
Thursday, August 4, 11
第14页
REST
URL + HTTP = 资源 + 操作 = REST
Thursday, August 4, 11
第15页
REST
URL + HTTP = Web 资源 + 操作 = REST
Thursday, August 4, 11
第16页
REST架构的设计准则
• 网络上的所有事物都被抽象为资源(resource)
• resource = data + representation
• 每个资源对应⼀一个唯⼀一的资源标识(resource identifier)
• resource identifier = URL
• 通过连接器(generic connector interface)对资源进行操作
• generic connector = HTTP Procotol (GET/POST/PUT/DELETE)
• 对资源的各种操作不会改变资源标识
• 对资源的GET操作具有幂等性,不改变资源的状态
• 所有的操作都是无状态的(stateless)
Thursday, August 4, 11
第17页
REST的实践用途
• 确保Web请求不会产生负作用 • 对内容型网站提供了最好的SEO效果 • 利用HTTP协议实现浏览器端缓存 • 大型系统的互操作标准接口,实现大型
系统的可扩展性和可伸缩性
Thursday, August 4, 11
第18页
HTTP语义被混淆
<a href=“/delete_user/1”>Delete User</a> <a href=“/users/1”>Delete User</a>
Thursday, August 4, 11
第19页
基于HTTP的资源缓存
/blog/123
Etag Last-Modified
"12523074“ Thu, 29 May 2010 09:43:46 GMT
/blog/123
If-Modified-Since Thu, 29 May 2010 09:43:46 GMT If-None-Match "12523074 "
304 Not Modified
Thursday, August 4, 11
第20页
JavaEye的资源
• 新闻文章 • 论坛主题贴 • 博客文章 • RSS订阅 • ...... (所有经常被访问的资源都可以缓存)
Thursday, August 4, 11
第21页
def news fresh_when(:last_modified => News.last.created_at, :etag => News.last)
end
@blogs = @blog_owner.last_blogs @hash = @blogs.collect{|b| {b.id => b.post.modified_at.to_i + b.posts_count}}.hash if stale?(:last_modified => (@blog_owner.last_blog.post.modified_at || @blog_owner.last_blog.post.created_at), :etag => @hash)
render :template => "blog" end
def board @topics = @forum.topics.paginate... if logged_in? || stale?(:last_modified =>
@topics[0].last_post.created_at, :etag => @topics.collect{|t| {t.id => t.posts_count}}.hash)
@announcements = (params[:page] || 1).to_i == 1 ? Topic.find :all, :conditions...
render :action => 'show' end end
Thursday, August 4, 11
第22页
def show @topic = Topic.find params[:id] user_session.update_....... if logged_in? Topic.increment_counter(...) if ...... @posts = @topic.post_by_page params[:page] posts_hash = @posts.collect{|p| {p.id => p.modified_at}}.hash topic_hash = @topic.forum_id + @topic.sys_tag_id.to_i + @topic.title.hash +
@topic.status_flag.hash ad_hash = ... (广告的hash算法,略) if logged_in? || stale?(:etag => [posts_hash, topic_hash, ad_hash]) render end
end
def topic @topic = Topic.find(params[:id]) unless logged_in? if request.not_modified?(5.days.ago) head :not_modified else response.last_modified = Time.now end end
end
Thursday, August 4, 11
第23页
Effect
• 30,000 304 status responses per day
Thursday, August 4, 11
第24页
基于Web的简单架构
浏览器
Web服务器
应用服务器
数据库
Thursday, August 4, 11
操作系统的文件系统
Web应用架构
存储设备
第25页
缓存系统的分层架构
• 操作系统磁盘缓存: 减少磁盘机械操作
• 数据库缓存
: 减少文件系统I/O
• 应用程序缓存
: 减少数据库查询
• Web服务器缓存 : 减少应用服务器请求
• 客户端浏览器缓存 : 减少对网站的访问
Thursday, August 4, 11
第26页
⼀一个误区
• n+1条SQL真的是低性能的吗? • 减少磁盘I/O才是数据库优化的终极之道
Thursday, August 4, 11
第27页
2007.02
• 把posts表的大字段剥离出来 • posts表的select count操作从80秒减少到
0.1秒
Thursday, August 4, 11
第28页
posts表
• posts(id, ..., body) • 磁盘存储空间6GB
Thursday, August 4, 11
第29页
剥离后posts表
• posts(id, post_text_id,...) 210MB • post_texts(id, body) 6GB
Thursday, August 4, 11
第30页
Thursday, August 4, 11
第31页
应用缓存概述
• 对象缓存 • 查询缓存 • 页面缓存 • 动态页面静态化(page cache) • 页面片段缓存(frgment cache) • 基于REST资源的缓存
Thursday, August 4, 11
第32页
对象缓存原则
• 数据库表的设计要细颗粒度 • 把有冗余字段的大表拆分为n个互相外键
关联的小表
• ORM的性能瓶颈不在于表关联,而在于 大表的全表扫描
• 尽量避免join查询,多制造n+1条SQL
Thursday, August 4, 11
第33页
对象缓存的意义
• Web应用很容易通过群集方式实现横向 扩展,系统的瓶颈往往出现在数据库
• 数据库的瓶颈往往出现在磁盘IO读写 • 因此要避免数据库的全表扫描和大表的
数据扫描操作
• 如何避免:拆表和臭名昭著的n+1条SQL
Thursday, August 4, 11
第34页
山寨cache plugin
• 基于Rails Cache的简单封装,仅60行代码 • 可以自动实现对象缓存的管理,n:1关系
的缓存,但不支持1:n集合缓存
• memcached缓存命中率超过96%
Thursday, August 4, 11
第35页
memcached统计信息
Thursday, August 4, 11
第36页
memcached统计信息
Thursday, August 4, 11
第37页
cache_money
• 出自twitter开发团队之手 • 可能是目前最强大的ruby cache框架 • 支持分页查询缓存,支持条件查询缓存
Thursday, August 4, 11
第38页
页面片段缓存
• JavaEye大量使用页面片段缓存 • 网站首页、新闻频道、个人博客左边导
航条等等
• 可以有效降低ruby应用的负载
Thursday, August 4, 11
第39页
JavaEye遇到的问题
• ruby负载远远超过db,Web Server的load 是DB的两倍
• 单纯对象缓存不能减轻ruby的负载
Thursday, August 4, 11
第40页
ruby的性能问题
• ruby的正则表达式运算性能很糟糕 • erb的性能也不好 • 大量字符串运算性能差 • 解决之道:用片段缓存降低ruby字符串
处理频率
Thursday, August 4, 11
第41页
不直接缓存post的内容,改为缓 存post内容生成的html片段
Thursday, August 4, 11
第42页
不直接缓存post的内容,改为缓 存post内容生成的html片段
Thursday, August 4, 11
第43页
JavaEye的缓存参考
• memcached缓存命中率96% • cache get : sql query = 4 : 1
Thursday, August 4, 11
第44页
在Rails之外寻求方案
• long-term request设置timeout • SQF Dispatcher (short queue first) • 用memcached实现计数器功能 • 用Redis的Set实现BlackList • 用Redis的List数据结构实现高性能队列 • crontab和 ``实现异步操作
Thursday, August 4, 11
第45页
`curl "http://blogsearch.google.com/ping? name=#{CGI::escape(@blog_owner.blog_name || @blog_owner.name + '的博客')} &url=#{CGI::escape blog_homepage_url(@blog_owner)} &changesURL=#{CGI::escape(blog_homepage_u rl(@blog_owner) + '/rss')}" > /dev/null 2>&1 &`
Thursday, August 4, 11
第46页
ip_counter = Rails.cache.increment(request.remote_ip) if !ip_counter
Rails.cache.write(request.remote_ip, 1, :expires_in => 30.minutes) elsif ip_counter > 2000
crawler_counter = Rails.cache.increment("crawler/ #{request.remote_ip}")
if !crawler_counter Rails.cache.write("crawler/#{request.remote_ip}", 1, :expires_in
=> 10.minutes) elsif crawler_counter > 50 BlackList.add(ip_sec) render :file => "#{RAILS_ROOT}/public/403.html", :status => 403
and return false end render :template => 'test', :status => 401 and return false
end
Thursday, August 4, 11