大概半年前,Guang.com曾發生一次由於首頁部分cache失效,導致網站故障。
故障分析:
當時逛正在做推廣,流量突然暴增,QPS達到5000+,當首頁部分cache失效時,需要查詢DB, 但由於這部分業務邏輯很復雜導致這SQL包含多表join、groupby、orderby等,執行需要1s,產生的大量臨時表,in-memory都裝不下,變成on-disk的臨時表,但當時放臨時表的disk分區容量只有20G,很快disk也爆了,結果顯然網站打不開了。
總結為幾點:
1、SQL語句優化不足
2、MYSQL tmp_table_size 配置太小
3、disk分區不合理/tmpdir路徑配置不合理
臨時解決措施:
由於當時持續大量用戶訪問,查詢DB一直hang住,導致cache一直無法set回去,首頁那cache一直處於miss狀態,惡性循環,雪崩了。
當時我們立馬采取以下措施:
1、調整MYSQL tmp_table_size, 關於tmp_table_size 請看下面詳細描述。
2、修改MYSQL臨時表保存路徑(tmpdir)到較大分區
3、簡化業務邏輯,修改SQL,重新部署。
臨時表使用內存(tmp_table_size):當我們進行一些特殊操作如需要使用臨時表才能完成的join,Order By,Group By 等等,MySQL 可能需要使用到臨時表。當我們的臨時表較小(小於 tmp_table_size 參數所設置的大小)的時候,MySQL 會將臨時表創建成內存臨時表,只有當 tmp_table_size 所設置的大小無法裝下整個臨時表的時候,MySQL 才會將該表創建成 MyISAM 存儲引擎的表存放在磁盤上。不過,當另一個系統參數 max_heap_table_size 的大小還小於 tmp_table_size 的時候,MySQL 將使用 max_heap_table_size 參數所設置大小作為最大的內存臨時表大小,而忽略 tmp_table_size 所設置的值。而且 tmp_table_size 參數從 MySQL 5.1.2 才開始有,之前一直使用 max_heap_table_size.
長期解決措施:終於到本文的重點Cache Reload機制設計和實現
在講Cache Reload機制設計和實現之前,先看看cache更新方式:
1、是緩存time out,讓緩存失效,重查。(被動更新)
2、是由后端通知更新,一量后端發生變化,通知前端更新。(主動更新)
前者適合實時性不高,但更新頻繁的;后者適合實時性要求高,更新不太頻繁的應用。
Cache Reload mechanism 設計:
根據逛當時業務需求,選擇被動更新方式,但這種方式的弊端是當cache失效那個點,剛好遇上高並發的話,就會發生上述的雪崩情況。
所以我在想這種使用率高的cache,就不用設置time out或time out設置足夠大,然后按業務需求時間間隔定期reload/refresh cache data from DB,這cache就不會出現失效情況,也不出現雪崩現象。
下圖是guang.com 關於Cache Reload的一小部分架構:
主要2個step:
1、將有需要reload cache 的wrapper保存到redis Hash.
2、部署在Daemon server上的CacheReloadJob,每分鍾去redis拿需要reload的cache的hashmap,判斷是否到時間refresh cache,如果到,通過Reflection call relevant method 重新reload data和reset 這個cache。
Cache Reload mechanism 實現:
set memcached with reload mechanism if necessary:
/** * <h2>set cache with reload mechanism </h2> * <h3>Example:</h3> * <p> * MethodInvocationWrapper wrapper = new MethodInvocationWrapper();<br> * wrapper.setMethodName("getProductList");<br> * wrapper.setObjectName("productService");<br> * wrapper.setArgs(new Object[] { null,0,1 });<br> * wrapper.setParameterTypes(new Class[] { Product.class,int.class,int.class}); * </p> * * <h3>NOTE:</h3> * Make sure the Args have been Serializable and the service has been marked the name, like "@Service("productService")" * * @param key * @param expiredTime 過期時間,如果reloadable=true, 此時間建議為 24*60*60 一天. * @param value * @param reloadable 是否reload * @param durationTime reload 時間間距,單位 ms * @param wrapper * @return * @author Kenny Qi */ public boolean set(String key, int expiredTime, Object value,boolean reloadable, long durationTime, MethodInvocationWrapper wrapper) { if(reloadable){ wrapper.setWriteTime(System.currentTimeMillis()); wrapper.setDuration(durationTime); wrapper.setKey(key); wrapper.setExpiredTime(expiredTime); objectHashOperations.put(RedisKeyEnum.CACHE_RELOAD.getKey(), key, wrapper); } if(value==null) return false; try { return memcachedClient.set(key, expiredTime, value); } catch (Exception e) { logger.warn(e.getMessage(), e); return false; } }
CacheReloadJob:
public class CacheReloadJob { private static Logger logger = LoggerFactory.getLogger(CacheReloadJob.class); @Autowired MyXMemcachedClient myXMemcachedClient; @Resource(name="objectHashOperations") private HashOperations<String, String, MethodInvocationWrapper> objectHashOperations; public void reloadCache(){ logger.info("Try to reload cache"); Map<String, MethodInvocationWrapper> map = objectHashOperations.entries(RedisKeyEnum.CACHE_RELOAD.getKey()); ThreadFactory tf = new NamedThreadFactory("CACHE_RELOAD_THREADPOOL"); ExecutorService threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), tf); for (String key: map.keySet()) { final MethodInvocationWrapper wrapper = map.get(key); if(wrapper.getWriteTime()+wrapper.getDuration()>System.currentTimeMillis()){//刷新時間大於當前時間 threadPool.execute(new Runnable() { @Override public void run() { refreshCache(wrapper); } }); } } logger.info("completed with reloaded cache"); } private void refreshCache(MethodInvocationWrapper wrapper){ Object object = ReflectionUtils.invokeMethod(SpringContextHolder.getBean(wrapper.getObjectName()), wrapper.getMethodName(), wrapper.getParameterTypes(), wrapper.getArgs()); myXMemcachedClient.set(wrapper.getKey(), wrapper.getExpiredTime(), object); wrapper.setWriteTime(System.currentTimeMillis()); objectHashOperations.put(RedisKeyEnum.CACHE_RELOAD.getKey(), wrapper.getKey(), wrapper); } }
Redis 存儲結構
redis> HSET cache:reload:memcached <memcache_key> <MethodInvocationWrapper> OK redis> HGETALL cache:reload:memcached
后記
如果要做的更人性化點,后續可以在網站后台管理系統 增加cache reloadable 的管理工具(刪除、修改刷新間隔等)。
轉載自:http://kenny7.com/2012/10/cache-reload-mechanism.html