基于多级缓存的充电系统优化实践
原文是发表在文章中,刚看了下文章主要用于转载,因此在随笔中重新发布一下。
正如前文中《海量并发下充电业务优化实践》所述,在充电过程中由于涉及到大量的实时数据处理,随着设备规模的扩大,各个节点和服务均感受到较大的压力。为了解决这个情况,文中探讨内存缓存组件的使用及相关的过程优化,预计优化后对于Redis的调用频率可以下降50%以上,极大缓解当前Redis的服务压力的同时为未来的系统规模增长打下良好基础。
充电过程中设计的实时数据主要有四类:遥信、遥测、电量和BMS数据。这四类数据在充电过程中持续上传直到充电结束。云端在终端充电过程中的处理压力也是由这些实时数据引起的。
由于当前实时数据处理过程的无状态性和通讯集群与实时数据处理集群之间负载均衡的存在(图1),无法确定由哪个节点来处理哪些终端的实时数据,在此场景下只能依赖中心化的缓存服务,各个处理节点通过中心化的缓存服务来获取数据恢复上下文。
图1 充电和通讯服务部署图
随着终端规模的扩大,虽然实时数据处理集群的性能问题可以通过水平扩展来解决,但是却给中心化的缓存和数据库服务带来了较大压力。此时需要对于充电业务的实时数据处理流程进行优化(图2)。优化从两方面入手:
图2 优化后充电和通讯服务部署图
因此在目前的程序架构下造成访问Redis和数据库较多的问题。平时对于Redis的每分钟调用次数(TPM)平均值为一百万次(月均值),遇到设备繁忙或者网络波动时,峰值有可能到达两百万次的量级,对于Redis的性能产生了很大的压力,而且也给整个架构带来一些隐患。
图3 Redis单日访问量(TPM_Pool是指各个连接池的每分钟调用数)
对充电业务的优化改造方案计划充分利用现有计算资源,增加内存缓存的使用力度,缓解Redis和数据库的压力。
使用内存缓存需要考虑的有如下三个方面:
基于上面一些考虑,在公共技术部门的帮助下确定使用MemoryCache作为内存缓存改造的主要组件。
MemoryCache类是.Net 4.0之后出现的,其命名空间是System.Runtime.Caching(位于 System.Runtime.Caching.dll中)。MemoryCache继承自ObjectCache并实现了IEnumerable和IDisposable接口。
MemoryCache的构造函数有两个:
MemoryCache(String, NameValueCollection)
MemoryCache(String, NameValueCollection, Boolean)
使用构造函数可以构造出非默认的缓存实例并可以自定义部分属性。
MemoryCache类有7个属性:
名称 | 描述 |
---|---|
CacheMemoryLimit | 获取计算机上缓存可使用的内存量(以字节为单位)。 |
Default | 获取对默认 MemoryCache 实例的引用。 |
DefaultCacheCapabilities | 获取缓存提供的功能的说明。 |
Item[String] | 通过使用 MemoryCache 类的实例的默认索引器属性,获取或设置缓存中的值。 |
Name | 获取缓存的名称。 |
PhysicalMemoryLimit | 获取缓存可使用的物理内存的百分比。 |
PollingInterval | 获取在缓存更新其内存统计信息之前需等待的最大时间量。 |
MemoryCache的属性都是只读属性,设置这些属性可以通过程序配置文件中的<memoryCache>
配置节进行配置。配置实例:
<configuration>
<system.runtime.caching>
<memoryCache>
<namedCaches>
<add name="Default"
cacheMemoryLimitMegabytes="10"
physicalMemoryLimitPercentage="10"
pollingInterval="00:01:00" />
</namedCaches>
</memoryCache>
</system.runtime.caching>
</configuration>
注意上文中配置节的name属性值为Default
,如果写其他名字无法设置MemoryCache.Default的相关属性,只能使用MemoryCache的构造函数构造对应名称的实例来使用。
1、Add方法
MemoryCache的Add方法有多个重载,该方法的用处是缓存项插入MemoryCache的实例中。
2、AddOrGetExisting方法
MemoryCache的AddOrGetExisting方法有多个重载,该方法的用处是缓存项插入MemoryCache的实例中,如果该缓存的key值已经存在,则返回该key值对应的缓存项。
3、Contains方法
该方法用于检查MemoryCache实例中是否存在某个key值
4、CreateCacheEntryChangeMonitor方法
创建 CacheEntryChangeMonitor 实例。此更改监视器用于监视中指定的缓存条目 keys 集合项更改时触发事件。
5、Get方法
返回某个key值对应的缓存项内容的引用。
6、GetCacheItem方法
从缓存中返回CacheItem实例形式的指定项。
7、GetEnumerator方法
创建缓存项的枚举器,用于循环访问缓存项的集合。
8、GetValues方法
该方法有多个重载,用于批量获取一组key值对应的缓存项。
9、Remove方法
该方法有多个重载,用于从缓存中删除缓存项。
10、Set方法
如果key值存在则更新缓存项,如果不存在则插入缓存项。
11、Trim方法
从缓存中删除总数百分比的缓存项。删除规则参考MemoryCache.Trim 方法 (Int32)
在前文中提到了对于内存缓存的三个基本要求,下面看MemoryCache如何满足这三个方面的要求。
1、控制内存占用
可以通过在配置文件中配置cacheMemoryLimitMegabytes
和physicalMemoryLimitPercentage
属性分别对于MemoryCache的实例占用内存的绝对值和物理内存相对值进行限制。
2、缓存项失效
在MemoryCache的实例增加缓存项时可以指定缓存项策略,即CacheItemPolicy类对象。该类中有两个属性SlidingExpiration和SlidingExpiration:
3、跟主缓存之间的同步机制
在指定缓存项策略时还可以在其ChangeMonitors属性中增加监视器,实现监视数据源变化的功能。这些监视器都是派生自ChangeMonitor
抽象类,目前在.Net中已经实现的监视器有四种:
如果有必要的话,可以根据业务时间实现自定义的监视器实现内存缓存和主缓存之间的同步机制。
另外,根据MSDN的资料,MemoryCache的操作是线程安全的,这一点在处理充电实时数据的时候非常重要。
内存缓存是Redis数据和部分数据库数据在内存中的映射。每个处理节点的内存缓存只是Redis中数据的子集(图4)。为了使用方便,缓存数据在内存中的数据类型和数据结构跟Redis中的数据组织方式基本一致。
图4 内存缓存和Redis数据集的关系
缓存项在获取时如果获取不到则从Redis中进行恢复,否则直接使用MemoryCache中的数据;MemoryCache缓存项设置后(比如调用Add或者Set方法)如果发现数据有变化则同步设置Redis的值,保持两者的数据同步。对于数据的超时时间则根据不同类型数据上传频率来确定,超时自动清理,有需要时再从Redis中恢复数据。
使用MemoryCache将缓存放在内存中后,不但可以显著减少对于Redis数据的查询。而且只在终端数据变化需要更新时,将数据同步到Redis中也可以减少对于Redis的更新频率。预计改造后对于Redis的查询操作能够减少80%`90%,对于Redis的设置操作可以减少60%~70%。
使用内存缓存对于原业务流程进行优化改造,看起来并不复杂,但是引入内存缓存后带来的不仅仅只是对于缓存存储方式的改变,而是牵扯到一系列相关的流程的变化,包括引入消息队列,合理分配处理接口的负载,多级缓存之间的同步机制等等。这些业务流程的变化对于充电稳定性的影响非常大,在系统运行过程中改变业务流程,犹如在给行驶在高速路上的汽车更换轮胎,如何保证不同流程之间的切换也是需要在设计和部署时需要详细考虑的问题。
对于实时数据处理系统而言,有多种多样的架构设计,但是没有一种架构是普适性的。各种架构都有自己的局限性。随着规模的扩大,需要不断地优化架构设计,平衡系统各个模块之间的负载,深入挖掘系统整体性能。正如上文所言,为了解决处理节点的高负载而选择了无状态的处理机制,规模扩大后为了降低中心化的缓存服务(Redis)的压力,又有必要引入内存缓存,缓解Redis的压力,优化处理流程,提高系统内部模块交互的费效比。通过一系列的优化改造,在不增加服务器的情况下,系统整体稳定性和处理性能又会有较大提升,为未来发展打下良好基础。
原文链接:http://www.cnblogs.com/zhu-wj/p/7461104.html