标签:
原文:http://www.ideliverable.com/blog/output-cache-improvements-in-orchard-1-9
Orchard1.9即将到来(我知道,已经“即将到来”5个月了,不过这次真的是要发布了)。Ideliverable 对其中的贡献就是对output cache 处理逻辑的重大翻修(重构?)。 它与原有的逻辑大大不同,所以我们有必要对这些改动深入说明,以便让需要的同学能够真正理解output cache 是如何工作的,以及如何把它用在自己的网站上。
TLDR(Too long didn`t read)(TLDR党)注意!这是一篇详细的长文章!!!
之前版本(1.9)的output cached 有一个很严重的性能问题,以下情况问题更甚:
l 你的网站使用 Orchard.OutputCache 模块(废话!能不用?)
l 你的网站上有些资源是数据库或CPU密集型的 像(page)。
l 网站上的资源的output cached 使用比较有限(短)的过期时间
l 网站高并发高PV(专业说法:网站的访问量的增加快于资源的生成速度?)
我们在一台每年二月到三月会出现访问峰值的主机上发现了这个问题。比较幸运的是,他们为这个峰值的到来已经做了充分的性能测试(用一种我后面将会极力推荐的方式),就是在这些负载测试的时候,我们发现当访问量达到一定水平的时候,网站照常挂掉。主要表现为三点:
经过详细分析及查阅了Orchard.OutputCache的相关代码后,我们发现问题出在缓存逻辑的设计上。
那么问题是怎么产生的呢?让我们通过假设一个网站的page A来模拟问题是怎么一步一步的产生的。
很自然的,当我发现问题出在哪里的时候,我开始着手解决。
一个output cache 解决方案在上述情况下能够稳定工作,最少要采取以下3种措施中的1种:
专业级别的缓存解决方案 像nginx , Varnish,也是采用以上策略中的一种或多种。
依我看,#1,#2 结合是Orchard最好的缓存方案。为什么? 它们都是在请求的同一个context(上下文)中,他们解决了100% 的问题。#3相对复杂,会给系统引入更多的不确定性(需要一些后台任务来针对不同的用户请求生成独立的资源)。此外,#3要有效进行,在服务器开始接收外部请求之前 需要有一个暖机的时间(warmup period)来“预缓存”所有的资源,否则在访问高峰的时候就会出现相同的问题。(我的理解:假定只预缓存部分资源,那么没缓存的资源在访问高峰的时候就会产生相同的问题。) 相比其它两个,#3唯一优点:对于那个请求到的是过期的资源(从而触发生成新资源)的请求响应会很快。Hardly a game-changer(没有改变游戏规则)。
所以,我决定为Orchard 1.9 实现前两种方案(投票委员会研究通过)。
实现过程
在Orchard中设计新的逻辑,良辰要考虑应对如下几点挑战:
l output cache(还有其它的orchard模块) 的存储机制是可扩展的并基于provider(provider based)。由于 (继承?)底层的存储provider ,cache(应该是.net的cache) 本身可以处理判断缓存过期及移除(通过在把资源放入缓存的时候制定一个过期的策略)而不用通过Orchard来处理。因此,为了能够提供脏数据,Orchard 需要考虑(加)一个缓存的过期时间,这个时间早于在它的实际过期时间。
l 一边提供缓存数据的一边添加资源到缓存。这将使得为请求(no using staements 或 try/finally blocks are possible)加可靠的锁异常困难。必须细致考虑,如果一个请求失败了而第二部分永远不会执行? 很容易导致死锁如果良辰没有足够小心。
l 生成资源的时间是不固定的,而缓存的“宽限时间”也是任意的,太小的缓存过期时间将导致缓存过的内容频繁需要重新生成。太小的宽限时间导致关进小黑屋的请求更多。理想的情况下,这2个时间都是可配置的。这样是否需要更长或更短的时间都可以根据实际情况设置。
l Orchard 经常被发布在web farms中。缓存必须能够在各个farm 节点间同步(分布式缓存?)但.NET 的线程并不能同步(分布式)。因此,假定群节点呈现相同的内容(内容同步),我们要么需要使用数据库事务保证跨节点同步,要么要让每个节点的缓存独立开来?我最终决定:后者是一个完全可以接受的折衷方案,应被视为一种良性的竞争状态?(什么鬼啊)。
因为资源的生成(渲染)时间不固定,我们就在缓存配置页增加宽限时间(带默认值),资源生成间隔(带默认值)的 配置,并且每个路由可以单独配置。如下图:
正如你所想的,你可以为路由的配置留空,他们就会默认使用全局的配置,你也可以设置为0来禁用该路由的缓存配置。文章后面我会给出在配置这些值的时候需要考虑的问题的建议。
考虑到一个事实,cache能够自己过期及清除。现在有两个和缓存项相关联的时间属性:
l ValidUntilUtc 表示缓存在Orchard的过期时间(Orchard认为的过期时间)。第一个请求对应资源的时间在这个时间之后,资源将会重新生成和缓存会被更新。这个属性cache 的存储时间加配置的缓存间隔生成时间而得到。(缓存存储时间是16点50分,配置的间隔是1分钟,那么这个时间就是16:51 。
l StoreUntilUtc指定用来表示缓存实际被实际移除的时间,它等于上一个ValidUnitUtc加上配置的grace time。 这个值实际就是底层缓存存储的过期时间。底层缓存的默认实现(也就是ASP.NET cache)会在这个时间把缓存项移除。
如下图,这两个值都可以在Statistics标签页下面找到。
基于这两个新的配置项,新的output cache 能够对于相同资源的并发请求执行同步,并给处于配置的宽限时间内的资源提供过期的数据。让我们来看看那它到底是如何工作地。
新的output cache 设计在Orchard.OutputCache 模块的Orchard.OutputCache.Filters.OutputCacheFilters类中, 按ASP.NET MVC的说法,这个类既是IActionFilter也是一个IResultFilter。为了输出缓存的目地,filter 类分别对OnActionExecuting,OnResultExcuted 方法施展了魔法。两个方法分开处理,每个又执行在独立的请求中, 我们在管理锁(线程锁)时要尤其小心。
用两个流程图来展示这两个方法是怎么工作的:
首先是OnActionExcuting 请求之前:
需要注意几点:
l 褐色的项表示请求的开始与结束。
l Fitler类维护一个“ConcurrentDictionary”(并发字典)。这个字典的key是缓存的key,值是一个锁对象。这个锁对象用来同步并发的请求(对这个key的缓存数据)。图中的橙色部分表示临界区, 在这个临界区内,一个请求持有一个锁对象。
l “request allowed for cache ?“ 步骤有一堆检查来保证请求是否能使用output cache。如果不能,output cache的相关处理将被忽略,请求的执行方式同没有启用output cache 一致。这些检查包括:
n Controller和Action上的OutputCacheAttribute
n 不缓存所有的Post请求
n 不缓存所有的管理页面请求
n 不缓存所有的子action
n 不缓存配置项中禁用outputcache 的请求
l “compute cache key“ 步骤为确定所请求资源的一个唯一key。这个key不仅包含资源的鉴定信息,还有诸如租户名,方法参数,配置的查询参数,culture,请求头,请求是否授权等信息。
l 如果这个唯一的key在缓存中找到:
n Filter类开始检查这个key有没有过期,(ValidUntilUtc是否过期)。如果过期了,filter会判断它是否在宽限时间内。没有过期,简单的把 cached 数据输出到客户端,请求结束。
n 假定过期的key在宽限时间内,filter会检查这个key对应的锁对象能否取到。如果不能,说明有请求已经在生成新的缓存数据。把过期的数据发送给客户端,请求结束。
n 如果能取到锁对象, filter 建立一个 response的 快照 执行请求的后面内容。
l Key在缓存中找不到:
n Filter首先尝试获取key的锁对象,如果成功这个锁20秒后失效。这个机制会导致阻塞当前请求直到新的缓存数据生成。而且没有过期数据。在锁对象失效时间内,请求不能成功的释放锁对象,这个请求outputcache处理被忽略。过期机制主要是用来(理论上有可能)防止有些请求释放锁失败。有了这个故障安全措施,就算真的锁对象释放失败了,对其它资源的请求也会照旧,而不会导致建一个无线长的队列来等待这个锁对象。
n 如果这个锁对象能被获取。Filter会重新检查缓存。(which would be the case if we waited for another request to render the item)。找到key, 锁释放,缓存数据发送到客户端。
n 缓存中仍然没有, filter 建立一个 response的 快照 执行请求的后面内容。
l 还有一些其它的意外情况,为了流程清晰,图中省略掉了。像“硬刷新“ 客户端强制生成新的缓存数据,而不管当前的缓存状态。
public void OnActionExecuting(ActionExecutingContext filterContext) {
Logger.Debug("Incoming request for URL ‘{0}‘.", filterContext.RequestContext.HttpContext.Request.RawUrl);
// This filter is not reentrant (multiple executions within the same request are
// not supported) so child actions are ignored completely.
if (filterContext.IsChildAction) {
Logger.Debug("Action ‘{0}‘ ignored because it‘s a child action.", filterContext.ActionDescriptor.ActionName);
return;
}
_now = _clock.UtcNow;
_workContext = _workContextAccessor.GetContext();
if (!RequestIsCacheable(filterContext))
return;
// Computing the cache key after we know that the request is cacheable means that we are only performing this calculation on requests that require it
_cacheKey = ComputeCacheKey(filterContext, GetCacheKeyParameters(filterContext));
_invariantCacheKey = ComputeCacheKey(filterContext, null);
Logger.Debug("Cache key ‘{0}‘ was created.", _cacheKey);
// The cache key lock for a given cache key is used to synchronize requests to
// ensure only a single request is regenerating the item.
var cacheKeyLock = _cacheKeyLocks.GetOrAdd(_cacheKey, x => new object());
try {
// Is there a cached item, and are we allowed to serve it?
var allowServeFromCache = filterContext.RequestContext.HttpContext.Request.Headers["Cache-Control"] != "no-cache" || CacheSettings.IgnoreNoCache;
var cacheItem = _cacheStorageProvider.GetCacheItem(_cacheKey);
if (allowServeFromCache && cacheItem != null) {
Logger.Debug("Item ‘{0}‘ was found in cache.", _cacheKey);
// Is the cached item in its grace period?
if (cacheItem.IsInGracePeriod(_now)) {
// Render the content unless another request is already doing so.
if (Monitor.TryEnter(cacheKeyLock)) {
Logger.Debug("Item ‘{0}‘ is in grace period and not currently being rendered; rendering item...", _cacheKey);
BeginRenderItem(filterContext);
return;
}
}
// Cached item is not yet in its grace period, or is already being
// rendered by another request; serve it from cache.
Logger.Debug("Serving item ‘{0}‘ from cache.", _cacheKey);
ServeCachedItem(filterContext, cacheItem);
return;
}
// No cached item found, or client doesn‘t want it; acquire the cache key
// lock to render the item.
Logger.Debug("Item ‘{0}‘ was not found in cache or client refuses it. Acquiring cache key lock...", _cacheKey);
if (Monitor.TryEnter(cacheKeyLock, TimeSpan.FromSeconds(20))) {
Logger.Debug("Cache key lock for item ‘{0}‘ was acquired.", _cacheKey);
// Item might now have been rendered and cached by another request; if so serve it from cache.
if (allowServeFromCache) {
cacheItem = _cacheStorageProvider.GetCacheItem(_cacheKey);
if (cacheItem != null) {
Logger.Debug("Item ‘{0}‘ was now found; releasing cache key lock and serving from cache.", _cacheKey);
Monitor.Exit(cacheKeyLock);
ServeCachedItem(filterContext, cacheItem);
return;
}
}
}
// Either we acquired the cache key lock and the item was still not in cache, or
// the lock acquisition timed out. In either case render the item.
Logger.Debug("Rendering item ‘{0}‘...", _cacheKey);
BeginRenderItem(filterContext);
}
catch {
// Remember to release the cache key lock in the event of an exception!
Logger.Debug("Exception occurred for item ‘{0}‘; releasing any acquired lock.", _cacheKey);
if (Monitor.IsEntered(cacheKeyLock))
Monitor.Exit(cacheKeyLock);
throw;
}
}
请求之后,OnResultExcuted图:
解释:
l 从OnActionExcuting 图中我们知道,进程有可能执行在锁当中,所以响应的项也用橙色表示。
l 如果response的快照已经被建立。Filter 首先检查response是否允许缓存。如果不检查,有些代理服务器的缓存控制头部会包含在response中,有可能会阻止缓存输出。 这些检查包括:
n 不是200状态的不缓存
n 路由设定为不缓存的项
n 发送通知消息的response。
l 如果response为合法的缓存对象,吸入缓存中。
l 缓存key被当前进程锁定,释放锁。
l 最终,response输出到客户端,请求结束。
public void OnResultExecuted(ResultExecutedContext filterContext) {
var captureHandlerIsAttached = false;
try {
// This filter is not reentrant (multiple executions within the same request are
// not supported) so child actions are ignored completely.
if (filterContext.IsChildAction || !_isCachingRequest)
return;
Logger.Debug("Item ‘{0}‘ was rendered.", _cacheKey);
// Obtain individual route configuration, if any.
CacheRouteConfig configuration = null;
var configurations = _cacheService.GetRouteConfigs();
if (configurations.Any()) {
var route = filterContext.Controller.ControllerContext.RouteData.Route;
var key = _cacheService.GetRouteDescriptorKey(filterContext.HttpContext, route);
configuration = configurations.FirstOrDefault(c => c.RouteKey == key);
}
if (!ResponseIsCacheable(filterContext, configuration)) {
filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
filterContext.HttpContext.Response.Cache.SetNoStore();
filterContext.HttpContext.Response.Cache.SetMaxAge(new TimeSpan(0));
return;
}
// Determine duration and grace time.
var cacheDuration = configuration != null && configuration.Duration.HasValue ? configuration.Duration.Value : CacheSettings.DefaultCacheDuration;
var cacheGraceTime = configuration != null && configuration.GraceTime.HasValue ? configuration.GraceTime.Value : CacheSettings.DefaultCacheGraceTime;
// Include each content item ID as tags for the cache entry.
var contentItemIds = _displayedContentItemHandler.GetDisplayed().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToArray();
// Capture the response output using a custom filter stream.
var response = filterContext.HttpContext.Response;
var captureStream = new CaptureStream(response.Filter);
response.Filter = captureStream;
captureStream.Captured += (output) => {
try {
// Since this is a callback any call to injected dependencies can result in an Autofac exception: "Instances
// cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it has already been disposed."
// To prevent access to the original lifetime scope a new work context scope should be created here and dependencies
// should be resolved from it.
using (var scope = _workContextAccessor.CreateWorkContextScope()) {
var cacheItem = new CacheItem() {
CachedOnUtc = _now,
Duration = cacheDuration,
GraceTime = cacheGraceTime,
Output = output,
ContentType = response.ContentType,
QueryString = filterContext.HttpContext.Request.Url.Query,
CacheKey = _cacheKey,
InvariantCacheKey = _invariantCacheKey,
Url = filterContext.HttpContext.Request.Url.AbsolutePath,
Tenant = scope.Resolve<ShellSettings>().Name,
StatusCode = response.StatusCode,
Tags = new[] { _invariantCacheKey }.Union(contentItemIds).ToArray()
};
// Write the rendered item to the cache.
var cacheStorageProvider = scope.Resolve<IOutputCacheStorageProvider>();
cacheStorageProvider.Remove(_cacheKey);
cacheStorageProvider.Set(_cacheKey, cacheItem);
Logger.Debug("Item ‘{0}‘ was written to cache.", _cacheKey);
// Also add the item tags to the tag cache.
var tagCache = scope.Resolve<ITagCache>();
foreach (var tag in cacheItem.Tags) {
tagCache.Tag(tag, _cacheKey);
}
}
}
finally {
// Always release the cache key lock when the request ends.
ReleaseCacheKeyLock();
}
};
captureHandlerIsAttached = true;
}
finally {
// If the response filter stream capture handler was attached then we‘ll trust
// it to release the cache key lock at some point in the future when the stream
// is flushed; otherwise we‘ll make sure we‘ll release it here.
if (!captureHandlerIsAttached)
ReleaseCacheKeyLock();
}
}
这些修改让Orchard有更强的伸缩性。
整个设计完成,我们再一次对相同的站点进行负载均衡测试,我们期待性能有极大的改善,但是坦率的说结果还是让我们很困惑。
试翻译Output Cache Improvements in Orchard 1.9
标签:
原文地址:http://www.cnblogs.com/olongtea/p/4913445.html