针对memcached Client进行的优化,分为两个阶段(下述的文章中代码都是源代码中的部分代码,需要完整代码的,可以到该地址下载后根据自己需要自行修改: http://code.google.com/p/memcache-client-forjava/):
一、客户端作了再次封装
1.cache服务接口化
定义了IMemCache接口,在应用部分仅仅只是使用接口,为将来替换Cache服务实现提供基础。
2.使用配置代替代码初始化客户端。
通过配置客户端和SocketIO Pool属性,直接交管由CacheManager来维护Cache Client Pool的生命周期,方便实用以及单元测试(blog文章《Memcached缓存客户端连接池设计》有它的内容介绍)。
<?xml version="1.0" encoding="UTF-8"?> <memcached> <!-- name 属性是程序中使用Cache的唯一标识。 socketpool 属性将会关联到后面的socketpool配置。 errorHandler 可选,用来处理出错情况。注意在Tag中不要使用空格或者Tab键。 --> <client name="mclient0" compressEnable="true" defaultEncoding="UTF-8" socketpool="pool0"> <errorHandler>com.alisoft.xplatform.asf.cache.memcached.MemcachedErrorHandler</errorHandler> </client> <client name="mclient1" compressEnable="true" defaultEncoding="UTF-8" socketpool="pool1"> <errorHandler>com.alisoft.xplatform.asf.cache.memcached.MemcachedErrorHandler</errorHandler> </client> <client name="mclient2" compressEnable="true" defaultEncoding="UTF-8" socketpool="pool2"> <errorHandler>com.alisoft.xplatform.asf.cache.memcached.MemcachedErrorHandler</errorHandler> </client> <!-- name 属性和client 配置中的socketpool 属性相关联。 maintSleep属性是后台线程管理SocketIO池的检查间隔时间,如果设置为0,则表明不需要后台线程维护SocketIO线程池,默认需要管理。 socketTO 属性是Socket操作超时配置,单位ms。 aliveCheck 属性表示在使用Socket以前是否先检查Socket状态。 --> <socketpool name="pool0" failover="true" initConn="5" minConn="5" maxConn="250" maintSleep="0" nagle="false" socketTO="3000" aliveCheck="true"> <!-- 设置memcache服务端实例地址,支持多个地址设置, 例如“192.168.2.11:33001” 或 “192.168.2.21:33001, 192.168.2.221:33002”. --> <servers>192.168.2.11:33001</servers> <!-- 表明了上面设置的服务器实例的Load权重 <weights>3,7</weights> <weights>3</weights> --> </socketpool> <socketpool name="pool1" failover="true" initConn="5" minConn="5" maxConn="250" maintSleep="0" nagle="false" socketTO="3000" aliveCheck="true"> <servers>192.168.2.21:33002</servers> </socketpool> <socketpool name="pool2" failover="true" initConn="5" minConn="5" maxConn="250" maintSleep="0" nagle="false" socketTO="3000" aliveCheck="true"> <servers>192.168.2.221:33003</servers> </socketpool> <!-- 默认active,另一种是standby,前者速度可能在某些情况下稍慢(当key没有存在于集群 任何一节点时,active模式会去尝试两个节点获取数据),但是具有数据恢复功能,后者速度比较快,没有数据 恢复功能 --> <cluster name="cluster1" mode="active"> <memCachedClients>mclient10,mclient1,mclient2</memCachedClients> </cluster> </memcached>
初始化启动memcached客户端时,会去加载配置文件:
public void start()
{
cachepool = new ConcurrentHashMap<String,IMemcachedCache>();
socketpool = new ConcurrentHashMap<String,SockIOPool>();
clusterpool = new ConcurrentHashMap<String,MemcachedClientCluster>();
cache2cluster = new ConcurrentHashMap<IMemcachedCache,MemcachedClientCluster>();
memcachedClientconfigs = new ArrayList<MemcachedClientConfig>();
memcachedClientSocketPoolConfigs = new ArrayList<MemcachedClientSocketPoolConfig>();
memcachedClientClusterConfigs = new ArrayList<MemcachedClientClusterConfig>();
loadConfig(configFile);
protected void loadConfig(String configFile) { try { if (supportMultiConfig) { Enumeration<URL> urls = null; ClassLoader loader = Thread.currentThread().getContextClassLoader(); if(configFile != null && !configFile.equals("")) urls = loader.getResources(configFile); else urls = loader.getResources(MEMCACHED_CONFIG_FILE); XMLInputFactory factory = XMLInputFactory.newInstance(); if (urls == null || !urls.hasMoreElements()) { Logger.error("no memcached config find! please put memcached.xml in your classpath"); throw new java.lang.RuntimeException("no memcached config find! please put memcached.xml in your classpath"); }
3.KeySet的实现。
对于MemCached来说本身是不提供KeySet的方法的,因为Cache轮询是比较低效的,同时这类场景,往往可以去数据源获取KeySet,而不是从MemCached去获取。
比如有些时候需要记录在控制间隔期内的访问次数和流量,此时由于是集群,因此数据必须放在集中式的存储或者缓存中,数据库肯定是撑不住这样大数据量的更新频率的,因此考虑使用Memcached的很出彩的操作,全局计数器(storeCounter,getCounter,inc,dec),但是在检查计数器的时候如何去获取当前所有的计数器,曾考虑使用DB或者文件,但是效率还是问题,同时如果放在一个字段中并发还是有问题。因此不得不实现了KeySet,在使用KeySet的时候有一个参数,类型是Boolean,这个字段的存在是因为,在Memcached中数据的删除并不是直接删除,而是标注一下,这样会导致实现keySet的时候取出可能已经删除的数据,如果对于数据严谨性要求低,速度要求高,那么不需要再去验证key是否真的有效,如果要求key必须正确存在,就需要再多一次的轮询查找。
IMemcachedCache类的部分代码:
/** * 获取多个keys对应的值 * @param keys * @return */ public Object[] getMultiArray(String[] keys); /** * 获取多个keys对应的key&value Entrys * @param keys * @return */ public Map<String,Object> getMulti(String[] keys); /** * key所对应的是一个计数器,实现增加inc的数量 * @param key * @param inc * @return */ public long incr(String key,long inc); /** * key所对应的是一个计数器,实现减少decr的数量 * @param key * @param decr * @return */ public long decr(String key,long decr); /** * key所对应的是一个计数器,实现增加inc的数量 * @param key * @param inc * @return */ public long addOrIncr(String key,long inc); /** * key所对应的是一个计数器,实现减少decr的数量 * @param key * @param decr * @return */ public long addOrDecr(String key,long decr); /** * 存储计数器 * @param key * @param count */ public void storeCounter(String key,long count); /** * 获取寄存器,-1表示不存在 * @param key */ public long getCounter(String key); /** * 这个接口返回的Key如果采用fast模式, * 那么返回的key可能已经被清除或者失效,但是在内存中还有痕迹,如果是非fast模式,那么就会精确返回,但是效率较低 * @param 是否需要去交验key是否存在 * @return */ public Set<String> keySet(boolean fast);
4.Cluster的实现。
Memcached作为集中式Cache,就存在着集中式的致命问题:单点问题,Memcached支持多Instance分布在多台机器上,仅仅只是解决了数据全部丢失的问题,但是当其中一台机器出错以后,还是会导致部分数据的丢失。
因此就需要实现一个备份机制,能够保证Memcached在部分失效以后,数据还能够依然使用,当然很多时候都用Cache不命中就去数据源获取的策略,但是在TB、PB数据集中,如果部分信息找不到就去数据库查找,那么要把系统弄垮真的是很容易,因此对于Memcached中的数据认为是可信的,因此做Cluster也是必要的。
(1)应用传入需要操作的key,通过Cache管理获取配置在Cluster中的客户端。
(2)当获得Cache Client以后,执行Cache操作。
(3)A.如果是读取操作,当不能命中时去集群其他Cache客户端获取数据,如果获取到数据,尝试写入到本次获得的Cache客户端,并返回结果。(达到数据恢复的作用)
B.如果是更新操作,在本次获取得Cache客户端执行更新操作以后,立即返回,将更新集群其他机器命令提交给客户端的异步更新线程对列去异步执行。(由于如果是根据key来获取Cache,那么异步执行不会影响到此主键的查询操作)
存在的问题:如果是设置了Timeout的数据,那么在丢失以后被复制的过程中就会变成永久有效的内容。
优化后,集群应该具有特性:
集群中多节点软负载均衡。(当前采用简单的Hash算法加取余来分发数据)
数据在多节点上异步冗余存储。(防止数据丢失最基本要求)
节点不可用切换功能。(当根据算法分发到某一失败节点时可以转向到其他可用节点)
节点恢复可用后数据Lazy复制。(当A,B两台机器作为集群的时候,如果A出现了问题,系统会去B获取数据,当A正常以 后,如果应用在A中没有拿到数据可以去B获取数据,并且复制到A上,这种方式也是一种lazy的复制,其实这里使用的是terracotta的思想)
5.LocalCache结合Memcached使用,提高数据获取效率。
Memcached并不是完全无损失的,Memcached是通过Socket数据交互来进行通信的,因此机器的带宽,网络IO,Socket连接数都是制约Memcached发挥其作用的障碍。Memcache的一个突出优点就是Timeout的设置,也就是放入进去的数据可以设置有效期,自动会失效,这样对于一些不敏感的数据就可以在一定的容忍时间内不去更新,提高效率。根据这个思想,其实在集群中的每一个Memcached客户端也可以使用本地的Cache,来缓存获取过的数据,设置一定的失效时间,来减少对于Memcached的访问次数,提高整体性能。
因此,在每一个客户端中内置了一个有超时机制的本地缓存(采用lazy timeout机制),在获取数据的时候,首先在本地查询数据是否存在,如果不存在则再向Memcache发起请求,获得数据以后,将其缓存在本地,并设置有效时间。方法定义如下:
/** * 降低memcache的交互频繁造成的性能损失,因此采用本地cache结合memcache的方式 * @param key * @param 本地缓存失效时间单位秒 * @return */ public Object get(String key,int localTTL);
第二阶段的优化
Memcache客户端里面在SocketIO代码里面有太多的synchronized,多多少少会影响性能。我们现在可以使用Concurrent包来替代synchronized,因此优化并不是一件很难的事情。但是由于原有memcached client没有提供扩展的接口,因此不得不将原有memcached client中除了SockIO部分全部纳入到封装过的客户端中,然后改造SockIO部分。
一.优化synchronized部分。在原有代码中SockIO的资源池分成三个池(普通Map实现),Free,Busy,Dead,然后根据SockIO使用情况来维护这三个资源池。
优化方式,首先简化资源池,只有一个资源池,设置一个状态池,在变更资源状态的过程时仅仅变更资源池中的内容。再次,用ConcurrentMap来替代Map,同时使用putIfAbsent方法来简化Synchronized(
V putIfAbsent(K key,V value)
如果指定键已经不再与某个值相关联,则将它与给定值关联。这等价于:
if (!map.containsKey(key)) return map.put(key, value); else return map.get(key);)。
MemcachedCacheManage类中的部分代码:
public void start() { cachepool = new ConcurrentHashMap<String,IMemcachedCache>(); socketpool = new ConcurrentHashMap<String,SockIOPool>(); clusterpool = new ConcurrentHashMap<String,MemcachedClientCluster>(); cache2cluster = new ConcurrentHashMap<IMemcachedCache,MemcachedClientCluster>();
SockIOPool类中的部分代码:
// dead server map private ConcurrentMap<String, Date> hostDead; private ConcurrentMap<String, Long> hostDeadDur; // map to hold all available sockets // map to hold socket status; private ConcurrentMap<String, ConcurrentMap<SockIO, Integer>> socketPool; /** * Factory to create/retrieve new pools given a unique poolName. * * @param poolName * unique name of the pool * @return instance of SockIOPool */ public static SockIOPool getInstance(String poolName) { SockIOPool pool; if (!pools.containsKey(poolName)) { pool = new SockIOPool(); pools.putIfAbsent(poolName, pool); } return pools.get(poolName); }
二.这样优化以后,性能并没有明显的提高,看来有其他地方的耗时远远大于连接池资源维护,因此用JProfiler作了性能分析,发现了最大的一个瓶颈:read数据部分,原有设计中读取数据是按照单字节读取,然后逐步分析,为的仅仅就是遇到协议中的分割符可以识别,但是循环read单字节和批量分页read性能相差很大,因此内置了读入缓存页(可设置大小),然后再按照协议的需求去读取和分析数据,效率会得到很大的提高。
上述一阶段的优化是封装调用memcached Client版本的客户端实现,二阶段优化是使用了新SockIO的无第三方依赖的客户端实现。
checkAlive指的是在使用连接资源以前是否需要验证连接资源有效(发送一次请求并接受响应),因此打开对于性能来说会有不少的影响,不过建议还是使用这个检查。