IT技术宅 - 云代码空间
——
网站缓存这个话题并不新颖,但是能否将它用好,可是一门学问,同一件工具在不同人的手中会做出不同的事情来。这里我来分享总结下我对于网站架构中缓存应用的一些看法和经验,大家有好的想法可以补充
第一:缓存的一些基本概念。
1:缓存(CACHE)与缓冲(BUFFER)的区别,我认为缓存可以在某种程序上理解成一级缓存(Primary Cache),数据全局共享。缓冲则属于二级缓存,只对一部分对象共享数据,二级缓存在某种程序上主要是降低一级缓存组件的访问压力以及提高缓存的存取效率。
2:缓存的一些基本属性:命中率,表示缓存命中的次数/总的请求数,这是缓存设计的重要质量指标之一;缓存执行效率,例如GET,INSERT,DELETE等;容量,即缓存介质的容量最大值;成本,即开发成本,部署成本,软硬件成本。
3:缓存的问题,存储介质的选择往往左右缓存的设计,缓存在不命中时往往会使性能下降。
第二:网站中缓存的应用场景:
1:可以缓存整个页面的html,提高访问响应能力;
2:针对局部页面元素进行缓存;
3:对复杂数据的结果进行缓存,例如一个查询需要结合多个数据集,然后根据这些数据集进行相应的运算,即使每个子集查询有缓存,但还是需要额外的运算,这种情况可以考虑缓存计算后的结果。
4:对耗时的查询进行缓存,例如产品列表页的查询。
5:和上下文相关的用户数据,例如用户从订单埴写页进入到订单成功页,或者是从产品列表页点击详细产品进行预订时的订单填写页,此时这两个页面之间都需要传递大量的相关数值,我们可以把所有的数值封装在一个类中,然后通过缓存进行通信。
第三:影响缓存命中率的因素。
1:数据时实性,每个业务系统都对自己的数据有相应的要求,有些数据的实时性非常强,像每日的股票信息,这种情况如果设置了缓存,缓存的命中率会特别低。
2:缓存粒度问题,一般来说是缓存的跨度太大,即此时的KEY值包含的条件太多,会出现缓存命中率特别低的情况。
第四:提高缓存命中率的方法:
1:增大存储介质的容量;
2:对非常热点的数据进行捕捉,可以采用实时更新缓存的方式来平衡缓存与实时性的问题,例如可以单独开启一个后台服务来定时做更新缓存的工作。
3:调整缓存KEY值的算法,尽量保证缓存KEY的细粒度,KEY-VALUE就是很好的细粒度例子。
4:根据业务调整缓存的过期策略。
第五:如何实现缓存组件:
1:采用二级缓存架构,即在web server上设置二级缓存,这里的二级缓存即上面的提到的缓冲,只对具体的webserver进行数据共享,二级缓存可以考虑采用微软企业库的缓存组件来完成。由于一级缓存的实现往往都是单独的服务器,为了减少缓存服务器的压力,在webserver上对数据进行缓冲,在降低缓存服务器压力的情况下最大的好处在于缓存的存取速度上。
2:一级缓存由单独的缓存服务器来完成,至于缓存服务器采用哪种缓存方案,可以根据不同的场景来决定。如果考虑到部署的方便性,可以采用微软企业库来完成一级缓存,如果服务器允许,可以采用memcached来实现。
cache服务器采用微软企业库实现的优缺点:
1:优点,开发以及部署都非常容易,不需要安装第三方软件等等;
2:缺点,需要自己开发客户端功能以实现分布式,这里我们可以采用一致性hash算法来实现,同时如果服务以WCF形式公布开,在访问效率上也不是最优的,比起memcached的通信方式要差一些。
memcached的优缺点:
1:优点,通信方式比起wcf要高;
2:缺点,需要第三方服务的支持,需要在服务器上安装memcached服务,这好像也不是什么重要的缺点。
最后,贴出网站网页在数据访问上的流程图,供大家参考,在下面的文章中我会把实现的方案分享出来。
上一篇我主要总结了网站缓存中的一些基本概念,以及我对于网站架构缓存应用的架构实现思路,这篇主要分享下如何利用微软企业库来实现一二级缓存的缓存服务。
为了能够有效的管理缓存,需要对使用缓存方法上做一些规范,即要想使用缓存组件提供的服务,需要在指定的配置文件中按照一定的规则来配置缓存条目,不允许在配置之处使用缓存。下面先展示下一条Cache条目的配置:
<Region name="MyBlog"> <SubRegion name="default"> <Cache CacheMode="LocalCacheOnlyMode" Key="BlogListConfigKey" BufferType="AbsoluteTime" BufferTimeSeconds="300" CacheType="AbsoluteTime" CacheTimeMinutes="30" CachePriority="Normal"/> </SubRegion> </Region>
上面的代码中,其实由三部分构成:
1:主分区:Regin,如果一个网站分很多子系统,可以为每个子系统定义一个这样的主分区,例如食品频道Food,手机频道Mobile等;
2:子分区:SubRegion,主分区下面的子分区,即对子系统更加小的划分,可以根据子系统的功能来划分,例如产品列表页List,详细页Detail等;
3:缓存条目:Cache,指具体的一则缓存条目规则,这里的缓存条目规则并不是指某一条缓存设置,而是指一条缓存规则,与具体的缓存条目是一对多的关系。
<1>:CacheMode,设置缓存模式,是只有二级缓存还是即有一级缓存也有二级缓存,例如用户页面之间的信息沟通就只需要二级缓存,即缓存在web server上。而产品列表页的数据属于全局数据,就需要即采用二级缓存也需要一级缓存。
<2>:BufferType,指二级缓存的过期方式,分为绝对过期,滑动过期,文件依赖。
<3>:BufferTimeSeconds,二级缓存Timespan中的秒。
<4>:CacheType,一级缓存的过期方式,类型同BufferType.
<5>:CacheTimeMinutes,一级缓存Timespan中的分钟。
<6>:CachePriority,缓存的优先级。
二级缓存实现:
第一:IWebCacheProvider,缓存提供者接口,它公布了所有缓存组件需要的方法,接口之所以加上了ServeiceContract标签,是由于下面的一级缓存WCF服务也继承此接口的原因。小提示:WCF服务契约对于方法重载的实现和普通方式有小小区别,请注意OperationContract标签的定义。
[ServiceContract] public interface IWebCacheProvider { [OperationContract(Name = "Add")] void Insert(string key, object value, string region, string subRegion); [OperationContract(Name = "AddByAbsoluteTime")] void Insert(string key, object value, string region, string subRegion, MyCacheItemPriority scavengingPriority, AbsoluteTimeCacheDependency absoluteTimeCacheDependency); [OperationContract(Name = "AddBySlidingTime")] void Insert(string key, object value, string region, string subRegion, MyCacheItemPriority scavengingPriority, SlidingTimeCacheDependency slidingTimeCacheDependency); [OperationContract(Name = "AddByFile")] void Insert(string key, object value, string region, string subRegion, MyCacheItemPriority scavengingPriority, FileCacheDependency fileCacheDependency); [OperationContract] void Delete(string key, string region, string subRegion); [OperationContract] object Get(string key, string region, string subRegion); [OperationContract] void Clear(string region); [OperationContract] int Count(string region); }
第二:EntLibWebCacheProvider,微软企业库实现缓存实现类。代码并不贴了,基本就是利用企业库的缓存组件实现上面的接口。
第三:MyWebCacheServiceClient,提供缓存客户端的实例。包含了两个重要的属性:一级缓存实例,二级缓存实例。余下的就是调用EntLibWebCacheProvider来完成缓存的调用,以及根据缓存规则,选择操作一级缓存以及二级缓存。
说明:下面代码中的GetMemcachedWebCacheProvider是下篇文章会提到的利用memcached实现一级缓存,由于需要支持一级缓存在企业库以及memcached之间的切换才出现的逻辑。
//一级缓存 IWebCacheProvider PrimaryCacheProvider; //二级缓存 IWebCacheProvider SecondaryCacheProvider; /// <summary> /// 实例化二级缓存 /// </summary> /// <param name="configFilePath"></param> /// <returns></returns> private IWebCacheProvider GetSecondaryCacheProvider() { IWebCacheProvider provider = null; provider = WebCacheProviderFactory.GetEntLibWebCacheProvider(configFilePath); return provider; } /// <summary> /// 获取一级缓存 /// </summary> /// <param name="hashKey"></param> /// <param name="configFilePath"></param> /// <returns></returns> private IWebCacheProvider GetPrimaryCacheProvider(uint hashKey) { IWebCacheProvider provider = null; string cacheType = WebConfig.ChannelConfig["CacheType"].ToString().ToLower(); switch (cacheType) { case "memcached": provider = WebCacheProviderFactory.GetMemcachedWebCacheProvider(configFilePath); break; case "entlib": provider = servicePool.GetServiceClient(hashKey) as IWebCacheProvider; break; } return provider; }
一级缓存的实现:由于一级缓存也采用微软企业库实现,而企业库本身是不具备分布式功能的,就算是memcached,本身也不具备分布式,而在于客户端的实现,所以企业库我们也可以实现分布式。
首先:我们把实现了缓存的组件以WCF服务形式分布出来,在多个服务器上部署,形成一个服务器群;
其实:实现分布式的客户端,让服务的请求均匀的分布到之前部署的WCF缓存服务器上。这里采用一致性hash算法实现,主要是根据key的hash值以及服务器数量来选择存储的服务器,这里贴些主要的实现代码,供参考:
1:定义一个hash的服务器实例集合,为每个服务器分配250个hash值,这里的值可以根据实际情况调整。
private Dictionary<uint, isRoc.Common.Cache.CacheProvider.IWebCacheProvider> hostDictionary;
2:定义一个hash的服务实例key集合,用来统计所有服务器实例中的hash key。
private uint[] hostKeysArray;
3:创建服务器列表的hash值。
/// <summary> /// 重新设置服务器列表 /// </summary> /// <param name="hosts">服务器列表</param> internal void Setup(List<WebCacheServerInfo> hosts) { hostDictionary = new Dictionary<uint, isRoc.Common.Cache.CacheProvider.IWebCacheProvider>(); List<isRoc.Common.Cache.CacheProvider.IWebCacheProvider> clientList = new List<isRoc.Common.Cache.CacheProvider.IWebCacheProvider>(); List<uint> hostKeysList = new List<uint>(); foreach (WebCacheServerInfo host in hosts) { //创建客户端 isRoc.Common.Cache.CacheProvider.IWebCacheProvider client = ServiceProxyFactory.Create<isRoc.Common.Cache.CacheProvider.IWebCacheProvider>(host .HostUri ); //Create 250 keys for this pool, store each key in the hostDictionary, as well as in the list of keys. for (int i = 0; i < 250; i++) { uint key = 0; switch (this.HashAlgorithm) { case EHashAlgorithm.KetamaHash: key = (uint)Math.Abs(KetamaHash.Generate(host.HostUri + "-" + i)); break; case EHashAlgorithm.FnvHash32: key = BitConverter.ToUInt32(new ModifiedFNV1_32().ComputeHash(Encoding.UTF8.GetBytes(host.HostUri + "-" + i)), 0); break; } if (!hostDictionary.ContainsKey(key)) { hostDictionary.Add(key, client); hostKeysList.Add(key); } } clientList.Add(client); } //Hostlist should contain the list of all pools that has been created. clientArray = clientList.ToArray(); //Hostkeys should contain the list of all key for all pools that have been created. //This array forms the server key continuum that we use to lookup which server a //given item key hash should be assigned to. hostKeysList.Sort(); hostKeysArray = hostKeysList.ToArray(); }
4:如何根据key值来查找具体的缓存服务实例呢,即具体的key存储在哪一台服务器上呢?根据传入的key值,计算出对应的hash值,然后经过特定的算法计算出服务器实例地址。
internal isRoc.Common.Cache.CacheProvider.IWebCacheProvider GetServiceClient(uint hash) { //Quick return if we only have one host. if (clientArray.Length == 1) { return clientArray[0]; } //New "ketama" host selection. int i = Array.BinarySearch(hostKeysArray, hash); //If not exact match... if (i < 0) { //Get the index of the first item bigger than the one searched for. i = ~i; //If i is bigger than the last index, it was bigger than the last item = use the first item. if (i >= hostKeysArray.Length) { i = 0; } } return hostDictionary[hostKeysArray[i]]; }
总结:本文简单的介绍了如何利用微软企业库来实现具有两级缓存的缓存组件,上篇我提到过,实现一级缓存也可以采用memcached,采用memcached可以不用自己开发分布式客户端,目前有两个成熟的解决方案:1:Memcached.ClientLibrary2:EnyimMemcached。下篇我来介绍一级缓存如何通过memcached实现,以及如何让组件在一级缓存上即支持企业库也支持memcached。
这篇来讲如何利用memcached实现一级缓存,以及如何让一级缓存组件支持在企业库,memcached或者其它第三方实施方案之间的切换。memcached本人并没有太多经验,如果文中有说的不对的地方,还希望批评指出,且文中关于memcached的代码大多来自网络。
创建memcached实现类MemcachedWebCacheProvider,由它来继承缓存提供者接口IWebCacheProvider,主里memcached客户端我采用.NET memcached client library ,这个类库很久没有更新这过了,没有和java版同步,有部分功能目前没有实现。
1:初始化memcached服务,这段初始化代码在程序中保证执行一次就够,一般可以放在gloabl文件中,或者是设置一个静态变量来存储服务的状态。
private void Setup() { String[] serverlist = { "127.0.0.1:11211" }; this._pool = SockIOPool.GetInstance("default"); this._pool.SetServers(serverlist); //设置服务器列 //各服务器之间负载均衡的设置 this._pool.SetWeights(new int[] { 1 }); //socket pool设置 this._pool.InitConnections = 5; //初始化时创建的连接数 this._pool.MinConnections = 5; //最小连接数 this._pool.MaxConnections = 250; //最大连接数 //连接的最大空闲时间,下面设置为6个小时(单位ms),超过这个设置时间,连接会被释放掉 this._pool.MaxIdle = 1000 * 60 * 60 * 6; //通讯的超时时间,下面设置为3秒(单位ms),.NET版本没有实现 this._pool.SocketTimeout = 1000 * 3; //socket连接的超时时间,下面设置表示连接不超时,即一直保持连接状态 this._pool.SocketConnectTimeout = 0; this._pool.Nagle = false; //是否对TCP/IP通讯使用Nalgle算法,.NET版本没有实现 //维护线程的间隔激活时间,下面设置为60秒(单位s),设置为0表示不启用维护线程 this._pool.MaintenanceSleep = 60; //socket单次任务的最大时间,超过这个时间socket会被强行中断掉(当前任务失败) this._pool.MaxBusy = 1000 * 10; this._pool.Initialize(); }
2:获取一个memcached客户端。
private MemcachedClient GetClient() { MemcachedClient client = new MemcachedClient(); client.PoolName = "default"; return client; }
3:根据memcached提供的功能实现IWebCacheProvider,代码就不贴了,大家可以自己去试试。
到此我们就利用memcached实现了一级缓存,由于.NET memcached client library 实现了分布式,我们只需要在多台服务器上安装上memcached服务,在初始化memcached代码中增加了服务器相关配置即可。String[] serverlist = { "127.0.0.1:11211" };
如何让一级缓存组件支持多实现方案之间的切换。
MyWebCacheServiceClient:客户端缓存组件实例,它来完成一级缓存与二级缓存之间的联系,以及根据配置文件来选择一级缓存的实施方案。
第一:CacheServiceMode,根据它就可以决定缓存是只缓存二级缓存还是两级都缓存。
1:LocalCacheOnlyMode,只启用web server上的二级缓存。
2:BufferedLCacheServerMode,即启用web server上的二级缓存也启用cache server上的缓存。
3:Off,关闭缓存功能。
第二:IWebCacheProvider service = this .GetPrimaryCacheProvider(hashKey);方式决定了一级缓存的实施方案。
/// <summary> /// 获取一级缓存 /// </summary> /// <param name="hashKey"></param> /// <param name="configFilePath"></param> /// <returns></returns> private IWebCacheProvider GetPrimaryCacheProvider(uint hashKey) { IWebCacheProvider provider = null; string cacheType = WebConfig.ChannelConfig["CacheType"].ToString().ToLower(); switch (cacheType) { case "memcached": provider = WebCacheProviderFactory.GetMemcachedWebCacheProvider(configFilePath); break; case "entlib": provider = servicePool.GetServiceClient(hashKey) as IWebCacheProvider; break; } return provider; }
插入缓存的逻辑:原理就是根据配置文件中的CacheMode来完成缓存级别的判定以及一级缓存的方案。
public void Insert(string key, object value, string region, string subRegion, CacheItemConfig cacheItemConfig) { if (string.IsNullOrEmpty(key) || value == null) return; //关闭模式,不使用缓存 if (Options.CacheServiceMode == ECacheServiceMode.Off) { return; } else if (Options.CacheServiceMode == ECacheServiceMode.BufferedLCacheServerMode || Options.CacheServiceMode == ECacheServiceMode.LocalAndCacheServerAndSql || Options.CacheServiceMode == ECacheServiceMode.LocalCacheOnlyMode) {//使用带缓冲的模式 if (Options.BufferType == ECacheDependencyType.SlidingTime) { SecondaryCacheProvider.Insert(key, value, region, subRegion, MyCacheItemPriority.Normal, Options.BufferSlidingTime); } else if (Options.BufferType == ECacheDependencyType.AbsoluteTime) { SecondaryCacheProvider.Insert(key, value, region, subRegion, MyCacheItemPriority.Normal, Options.BufferAbsoluteTime); } if (Options.CacheServiceMode == ECacheServiceMode.LocalCacheOnlyMode) {//只使用本地缓存 return; } } checkKey(key); uint hashKey = hash(key); try { if (Options.CacheServiceMode == ECacheServiceMode.CacheServerMode || Options.CacheServiceMode == ECacheServiceMode.BufferedLCacheServerMode || Options.CacheServiceMode == ECacheServiceMode.CacheServerAndSql || Options.CacheServiceMode == ECacheServiceMode.LocalAndCacheServerAndSql) {//CacheServer模式使用Cache服务器保存Cache IWebCacheProvider service = this .GetPrimaryCacheProvider(hashKey); byte[] byteValue = SerializationHelper.SaveToBinaryBytes(value); var cachePriority = ModelConverter.ToRefClass(cacheItemConfig.CachePriority); if (cacheItemConfig.CacheType == ECacheDependencyType.AbsoluteTime) { AbsoluteTimeCacheDependency absTime = new AbsoluteTimeCacheDependency(); absTime.AbsoluteTime = DateTime.Now.AddMinutes(cacheItemConfig.CacheTimeMinutes); service.Insert(key, byteValue, region, subRegion, cachePriority, absTime); } else if (cacheItemConfig.CacheType == ECacheDependencyType.SlidingTime) { SlidingTimeCacheDependency slTime = new SlidingTimeCacheDependency(); slTime.SlidingTime = new TimeSpan(0, cacheItemConfig.CacheTimeMinutes, 0); service.Insert(key, byteValue, region, subRegion, cachePriority, slTime); } } } catch (Exception ex) {//出现异常,保存到数据库中 servicePool.ReplaceServiceClient(hashKey); this.SendLogEmail(ex); } }
客户端调用代码:为了调用方便,创建一个CacheHelper来帮助完成:
public class CacheHelper { /// <summary> /// 主分区 /// </summary> public const string REGION = "MyBlog"; /// <summary> /// 子分区 /// </summary> public const string SUB_REGION = "default"; public const string BlogListConfigKey = "BlogListConfigKey"; #region 页面间数据传递 /// <summary> /// 新增页面间传递数据到WebCache /// </summary> /// <returns>返回PageKeyID,用于页面间传递的键值</returns> public static string InsertPageParams(string configKey, object obj,string pageKey) { string result = null; MyWebCacheServiceClient cacheClient = CacheClientFactory.GetWebCacheServiceClient(REGION, SUB_REGION, configKey); cacheClient.Insert( MyWebCacheServiceClient.BuildKey(configKey,pageKey), obj, REGION, SUB_REGION); return result; } /// <summary> /// 从Cache里获取页面传递Cache /// </summary> /// <param name="key">FlightCacheKey里的常量</param> /// <param name="pageKeyID">页面传递的键值</param> public static object GetPageParams(string configKey, string pageKey) { object result = null; MyWebCacheServiceClient cacheClient = CacheClientFactory.GetWebCacheServiceClient(REGION, SUB_REGION, configKey); result = cacheClient.Get( MyWebCacheServiceClient.BuildKey(configKey, pageKey), REGION, SUB_REGION); return result; } #endregion }
对于web系统中增加缓存服务,使用起来还是挺方便的,目前可采用的方案比较多,有微软的企业库,memcached等等。但如果需要很好的对项目中的缓存进行监控管理,也不是一件特别容易的事情,例如:监控缓存服务器上都有哪些项目使用了缓存,具体都有多少个key,大小,单个key的命中率以及过期时间等信息。有了这些信息,就非常容易排查内存为什么快用完的问题,如果再提供手动过期缓存的服务,就更好了,有的时候由于数据出错,需要紧急让缓存失效,此种办法影响最小。
这篇我来总结了针对memcached的缓存管理。
其实memcached本身也提供了一些缓存统计信息,例如:当前总共的缓存数量,使用的内存量,总获取次数,总的写入次数,总的命中次数等等,但这种统计信息粒度太大:
1:无法具体到单个key,如果我们想针对某一个key统计它的命中率情况,就不好办了。
2:无法分析系统中都有哪些项目使用了key,哪个项目占用的key多,内存多。
3:无法实现手工过期,这种需求某些特殊情况下也是很有帮助的。
既然memcached本身不提供,我这里采用了一种变通的方式来记录我们特定的信息。
首先我们引进一个概念:分区,这个分区可以理解成电脑上的硬盘分区,用户可以把不同的文件放在不同的分区上,这样在管理上也容易些,同样分区底下有子分区,就像电脑上的文件一样,子分区下面就是具体的key了,对于我们的cache后台管理,可以这样理解,一个项目可以分配为一个分区,按项目功能模块可以分为不同的子分区,子分区下来分散着N多key。
实现方案:我们可以对每个key的访问记录下它的一些信息,例如:大小,所属分区名,过期时间,访问命中率,然后把这些信息在每个memcached 实例上创建一个特殊key,用于存储key的访问信息。
注意点:
1:由于记录访问信息都需要更新特殊key,如果过于频繁,会影响正常的cache性能,所以可以考虑形成一个内存队列,当数量达到多少后(如果key使用频率不高,还可以设定时间,当过了这个时间,即使数量不够也进行更新),统一更新特殊key内容。
2:由于memcached有单个key大小限制,所以对于这种统计信息key,不能过大,记录key访问信息时,尽量以文本形式存储,这样能保证最小。
3:每个实例中对应一个用于存储key访问信息的key,这样可以统计更多的key。
监控视图:通过上面的努力,我们可以形成三个视图:
第一:memcached 实例视图,以某个具体cache实例为单位,呈现memcached服务本身所提供的统计信息,还包含此实例中包含了多少个分区,即实例上包含了多少个项目使用的缓存。
第二:分区视图,根据分区名称,集合所有节点的数据,最终汇总出统计数据,例如可以统计酒店项目总共使用了多少个key等,这对分析key的分布情况比较有帮助。
第三:key视图,呈现具体key的访问信息,以及手工过期功能。
总结:上面的方案虽然能实现需求,但在实际生产环境中,尽量不要打开这种监控功能,需要的时候再打开,尽量让cache的效率最高。