REST笔记(六)------通过缓存架构可伸缩性与容错性的Service
回顾一下在REST WCF 4.0中可以这样简单实现缓存:
1、配置
<caching>
<outputCacheProfiles>
<add duration="20" name="outCache" varyByParam="none"/>
</outputCacheProfiles>
</outputCacheSettings>
2、使用配置
[OperationContract]
[AspNetCacheProfile("outCache")]
public DateTime GetTime()
{
return DateTime.Now;
}
用Fiddler测试一下请求:在Request Builder选项卡中,选择Get方法,输入测试地址,点击执行获取的HTTP响应头如下图:
(图1)
注意之间红色下划线部门的两个HTTP头:Cache-Control、Expires这是HTTP协议中有关缓存信息
的两个重要头信息,留个悬念先,稍后会讲到。
缓存在Web获取信息上是一个很重要的手段,通过HTTP安全且幂等的GET操作进行。
在REST架构中,由于服务端与客户端交互的无状态性(注意:REST中所提到的无状态指的就是应
用无状态。Web架构的关键原则之一就是:服务器和服务不应该维护应用状态,作为基于HTTP协议
的REST架构风格的服务也完全如此)。
这样就会有一个问题,服务为了识别客户端,每次客户端希望通过服务获取资源时,
都必须将交换应用状态的信息,这就有可能带来性能上的损失。好在REST架构中,我们也可以
利用缓存,这在一定程度上大大提升了服务的吞吐量。但是弊端也是显而易见的:客户端获取到
的信息谁也无法保证和服务器同步。这就是缓存的弱一致性(weak consistency)。
一个资源的表述只在一段时间范围内是有效的,这段时间就是此资源的保鲜周期。一个
过期的资源表述需要通过服务端的验证然后才能使用。
缓存类型
按照部署方式分,缓存分为本地缓存与共享缓存。共享缓存又包括代理缓存(proxy cache)与
反向代理缓存(reverse proxy cache)。
- 本地缓存(也称客户端缓存)客户端在获取资源表述后,通过浏览器将资源缓存在本地磁盘或者内存之中。
这样当下次访问这些资源时就可以直接从本地获取。
优点:避免了网络传输,速度最快
缺点:由于只缓存在客户端本地,所以当另外有客户端请求时只能从服务端获取。
- 代理缓存在服务端与客户端之间,部署一个双方都能共享的服务器来缓存数据,这就是代理缓存。它可以位于企业防火墙内部或者外部。
优点:所有能通过代理缓存服务器获取资源的客户端都能共享缓存在其中的资源表述。
缺点:增加了额外的部署设施
- 反向代理缓存。反向代理又称web服务加速器,位于Web服务器前端,充当Web服务器内容缓存服务器。
它针对的是web服务器而不是浏览器用户,所以成为反向代理,一般位于Web服务器前面。它是根据
由Web源服务器返回的HTTP头信息来对信息进行缓存。常用的反向代理实现有Squid等。
缓存的HTTP响应头信息
有两种主要的HTTP响应头信息用于控制缓存的行为:Expires、Cached-Control。这就回到上图中HTTP的响应
信息了。那么这两个指令是对缓存有何作用?
- Expires
Expires用于控制缓存表述的绝对到期时间,过了这个时间表述就失效了,客户端的请求必须发送到服务端,由服务
端重新计算并发出请求相应的响应。如果Expires的值和Date的值相等或者Expires=0,那么表述立刻过期。若想将
表述设置为永不过期,那么服务可以将Expires设置为一年。
- Cached-Control
Cached-Control与Expires不同,它可以被用来请求和响应上来控制缓存行为。它的值可以由多段指令组成,每个
指令之间用分号(;)隔开。这些指令决定缓存在哪,缓存期多长。
小结:如果我们可以确定一个表述的到期时间,那么可以使用Expires。如果指明缓存离开源服务器后缓存多久更
合适的话,可以使用Cached-Control的max-age或s-maxage (max-age、s-maxage单位都为秒)。对于可缓存
的表述,服务端最好都能在响应的HTTP头信息中包含ETag、Last-Modified信息(如果你对ETag不熟悉,可以参见
上节)。
如果客户端想要在服务端重新验证缓存的表述是否过期,可以在请求HTTP头信息中使用Cached-Control中的
no-cache指令。
另外两个缓存中重要的HTTP头位:Last-Modified与Date。前者表述资源最后修改时间,后者表示表述生成时间。
Cached-Control,在HTTP协议规范中,通过标准化Cached-Control指令来控制缓存。
Cached-Control主要指令介绍
- max-age:可以用于请求或者响应中。
在响应中,可同时控制缓存和新鲜度。它使本地缓存和共享缓存(代理缓存与反向代理缓存)将一个表述缓存起
来,由它的值决定表述经过多少时间过期。单位为秒。它的值将覆盖Expires中的值。响应如:
Cached-Control:max-age=20(如上Cached-ontrol:private(如上图)
在请求中,表示客户端能接受生成时间不早于max-age值的被缓存表述。如果客户端指定max-age=0,则请求
会导致缓存被发送到源服务端进行验证,从而获取到最新的表述。也就是说max-age的值是客户端当前请求时间
与资源在源服务端生成资源的时间比较标准。即当前时间-缓存资源在源服务器端生成的时间.
- no-cache:可以用于请求或者响应中。
在响应中,它要求每个缓存都到服务端去做验证。它只有在表述被缓存时才起作用,在无缓存时,
有没有它客户端都得到服务端获取资源的表述。响应如:Cached-Control: no-cache
在请求中,它用max-age=0一样,客户端获取到最新资源的表述。与max-age=0不同的是,
no-cache使所有中间代理中所有的被缓存的表述都被更新为最新的表述, 而max-age=0中中间代理
缓存可以被继续使用
- no-store:可以用于请求或者响应中。
在响应中,它将可以被缓存的表述不被缓存。响应如:Cached-Control: no-store
在请求中,要求资源不被缓存,并且获取到的都是最新的表述。
- s-maxage:用于响应中。
它与max-age一样。但是它只能由共享缓存(代理缓存与反向代理缓存)来做,不能使用本地缓存。响应如:
Cached-Control:s-maxage=20 区分 s-maxage与max-age,前者带有s,意味着 共享(share)。
- public:用于响应中。使本地缓存或者共享缓存(代理缓存与反向代理缓存)将一个响应缓存起来,但是它不像s-
maxage一样指定一个保险周期。public优先级要高于 Authorization头信息。一般如果HTTP响应中包含
Authorization,那么表述不会被缓存;但是如果同时又包含了pulic头信息,那么表述一定可以被缓存。
如果客户端获取资源表述时,需要被认证为合法的客户端,那么在使用public时应该谨慎。因为缓存服务器不会
关心对客户端的验证问题.响应如:Cached-Control:public
- private:用于响应中。使表述可以被缓存,并仅仅使用本地缓存,即在客户端实现。同样它也不指定新鲜度。
- must-revalidate:用于响应中。它通常将可缓存的表述缓存起来,但客户端在获取缓存表述时,它会将
缓存信息发送到服务端做验证。只有验证成功,表述才是可用的表述。 对于客户端的请求,缓存也会被
发送到服务端做验证,但是相比直接从服务端获取资源的表述,它是一个相对高效的操作,因为它防止
服务端业务逻辑被引为客户端的请求而被反复调用。响应如:Cached-Control: must-revalidate
- proxy-revalidate:用于响应中。它与must-revalidate一样,但是只限于在使用共享代理缓存中。响应
- max-stale:用于请求中。使用形式:max-stale=36。客户端可以接受max-stale指定值过期范围内的表述。
如果客户端忽略此值,表示它可接受任何过期的表述
- min-fresh:用于请求中。使用形式:min-fresh=36。客户端可以接受资源生成时间加上min-fresh指定值仍然
最新的被缓存表述。
- only-if-cached:用于请求中。客户端仅返回被缓存的表述。如果缓存中不存在资源的最新表述,则返回504状码。
新增的两条指令:
- stale-while-revalidate:用于响应中。它使缓存在响应客户端时同时将缓存发到服务端去做验证
(以异步非阻塞模式发送到服务端做验证)。它允许缓存能离开响应给客户端,即使此响应的表述已经过期。
因此,它是以牺牲一致性为代价来响应客户端,减少了延迟。在它指定的时间(单位为秒)过去之后,如果服务端
对表述的验证还没有完成,那么缓存将被丢弃。响应如:stale-while-revalidate=20
- stale-if-error:用于响应中。它允许如果客户端在与服务器的交互过程中发生错误的情况下,返回给客户端一个
可能过期的表述。《REST in Practice 》中说:如果被缓存的表述超出stale-if-error指定的时间范围,那么缓
存就不应该被响应给客户端。stale-if-error值的单位为秒。它的响应形式如:Cache-Control:stale-if-error=20。
我的理解是:如果某个被缓存的表述的如下响应头:
那么在Date指定时间后20秒,也就是27 Dec 2011 09:24:50 GMT后,此表述就不应该响应给客户端。
另外还有两条指令,是为了实现缓存通道而使用的,下面会介绍到。
缓存通道(cache channel)
它是一种实现了延长被缓存资源的保鲜期的技术。解析不了资源响应头的(即缓存通道协议)的缓存在保鲜期
过了就失效了,而理解资源响应头的缓存在资源过期后仍将其视为最新的资源,缓存服务器收到过期的通知。
当客户端采用no-cache指令发送请求,强行要求资源到服务端验证就可以让缓存被更新。
缓存通道使用了两个Cache-Control指令:
- channel:它提供通道扩展的绝对URI路径。缓存可以订阅此URI以获取一个与缓存相关的事件通知
- group:扩展提供了一个能够用来对多个缓存进行分组的相对URI。
通过max-age=600指定缓存的过期时间后,再次接收到客户端请求时,缓存必须从源服务器验证、获取。
但是在满足一下条件下,在发回客户端的路径上,如能理解缓存通道协议,即理解channel与group的缓存都将
能被延续其保鲜期。
1、缓存持续轮询,频率至少比低于通道自己定义的时间间隔
2、无论是同被缓存表述的URI相关联还是同被缓存表述所属组的URI相关联的通道都没有发出过期信息。
如下图提要:
precision定义了通道指定的精确度。也就是说,缓存必须至少15秒内轮询一次通道就可以延长与此通道相关联
的所有被缓存表述的保鲜期。
lifecycle:指定了此提要中的事件在发布后的至少一个小时内都是可用的
precision与lifecycle并不是FCL中SyndicationFeed对象所支持的属性,而是扩展的属性。这也符合缓存通道中:
能理解缓存响的缓存的宗旨
实现以上提要(feed)的代码如下:
SyndicationFeed feed = new SyndicationFeed { Title = new TextSyndicationContent("feed Tile"), LastUpdatedTime = new DateTimeOffset(DateTime.Now), Description = new TextSyndicationContent("feed Description Content") }; feed.Authors.Add(new SyndicationPerson("aa@gmial.com", "zhanshan", "cnblogs.com")); feed.Language = "us-en"; feed.Generator = "test feed"; feed.Links.Add(new SyndicationLink(new Uri("http://www.cnblogs.com/tyb1222"), "via", "linkTitle", "application/Atom+xml",1000)); feed.Categories.Add(new SyndicationCategory("blog")); feed.Contributors.Add(new SyndicationPerson("aa@gmial.com", "zhanshan", "cnblogs.com")); feed.ElementExtensions.Add(new SyndicationElementExtension("percision","http://tyb1222.cnblogs.com",900)); feed.ElementExtensions.Add(new SyndicationElementExtension("lifecycle", "http://tyb1222.cnblogs.com", 3600)); return feed; 如果提要中包含一个过期的事件项,它的link控件将它与资源表述所属组的ID相关联(也就是Cache-Control中group指定
的ID)。通过href的值就能此组的资源关联起来。接收到这个提要后,缓存停止延长此组资源的缓存保鲜期,客户端下次通过
Get请求获取资源时,缓存将重新到服务端进行验证此过期的表述 。下图为包含过期事件项的提要:
将带有过期想的条目(entry)的事件项,只设置SyndicationItem示例的Update属性。代码如下:
SyndicationItem item1 = new SyndicationItem("Title1", "Microsoft may have patents, patent applications, trademarks, ...more", new Uri("36",UriKind.Relative)) {Summary = new TextSyndicationContent("sumary"), LastUpdatedTime = DateTime.Now};
当然缓存通道技术的实现并不一定要使用Atom来实现,这里只是使用它做个范例。如果你对Atom协议不清楚可以参见这里