Cache雪崩效應


大概半年前,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路徑配置不合理

    4、部門間溝通不足,大型推廣前沒事先打招呼。

臨時解決措施:

由於當時持續大量用戶訪問,查詢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的一小部分架構:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM