轉(zhuǎn)自:http://blog.csdn.net/luohuacanyue/article/details/14055715
背景
開篇之前我一直在想怎么把這個項目給講清楚,如果在互聯(lián)網(wǎng)公司有高并發(fā)場景對于這個內(nèi)容的就比較容易接受。這里大概說一下背景:代碼寫于2013年四月份,最開始的雛形是在2012年寫的,從另外一個項目上進行,代碼侵入性比較強。我在今年四月份進行了剝離,實現(xiàn)可插拔式的監(jiān)控。言歸正傳,當(dāng)時對于性能領(lǐng)域非常有興趣,所以就在想如何寫一個框架對于現(xiàn)有項目對于"類級別(嚴(yán)格來講是方法級別)"做性能監(jiān)控。既然是性能監(jiān)控,很容易就會發(fā)現(xiàn)需要回答三個問題:一、這個方法的并發(fā)請求數(shù)有多少?二、響應(yīng)時間是多少?三、最佳并發(fā)請求數(shù)是多少?這三個問題其實就會衍生出我們今天要講的這個框架意義:保證應(yīng)用的可用性,實時監(jiān)控各個節(jié)點的并發(fā)數(shù),響應(yīng)時間等。(吞吐量和響應(yīng)時間相生相克,所以會在兩者之前找一個最佳平衡點,就是常說的最佳并發(fā)數(shù)。理想狀態(tài)下最佳并發(fā)請求數(shù)就是我們這里要設(shè)的限流上限閥值)。
限流的意義
下面先用一個簡單的圖來看看限流框架所處在的位置。
看到這張圖的時候估計有很多人下意識的會想到spring的攔截器,這里確實非常像,原理也類似。而且在我后面的實現(xiàn)中有兩種方式:其中的一種就是基于Spring的攔截器實現(xiàn)的。另外一種其實是JDK的動態(tài)代理去實現(xiàn)的。講到這里不知道有沒有人會問為什么要限流?這里就做一些簡單的解釋。舉一個淺顯的例子:聯(lián)想一下長江的三峽大壩,除了能發(fā)電之外另外一個作用就是防洪,如果洪水來了,沒有三峽大壩,很有可能對于下游產(chǎn)生重大的洪澇災(zāi)害。引用到我們這里其實是一樣的。當(dāng)請求數(shù)異常升高時(洪水來了),對于應(yīng)用來講所承擔(dān)的負(fù)載也會異常升高,這樣直接的影響就是整個響應(yīng)時間變慢,更糟糕的情況是系統(tǒng)直接崩潰。不僅如此,由于系統(tǒng)之前都是相關(guān)聯(lián)的,所以很容易就會對其依賴的相關(guān)應(yīng)用產(chǎn)生沖擊(有點像多米諾骨牌)。限流的一個很重大的意義就是保證應(yīng)用的可用性,講到這里應(yīng)該明白為什么需要限流了。
UML類圖
接下來看一下整個UML類圖:
這個圖是比較簡單。在這里我把整個限流做了一些簡化,在完整版里面會有數(shù)據(jù)的存儲和響應(yīng)時間的監(jiān)控。開始的數(shù)據(jù)是存儲在DB里面,后面為了學(xué)習(xí)數(shù)據(jù)庫的一些核心知識所以自己在寫一個很簡單的存儲(寫存儲的意義僅僅只是為了學(xué)習(xí),只是希望了解一下整個存儲在于空間分配的一些思想,對于SQL協(xié)議那一塊沒有更多的涉及)。這里將不在討論有關(guān)于存儲的細節(jié)。
回到上面這張圖,如我前面所說的,我用了兩種方式去實現(xiàn):一種就是Spring的攔截器方式,另外一種就是JDK的Proxy方式。
代碼展示
接下來看一下幾個類的代碼,這里面最核心的類就是FlowMonitor.java,限流的具體邏輯都在里面。下面會把具體的代碼貼出來,當(dāng)看到代碼的時候估計會覺得這東西太簡單了。下面我會拋出更多的問題,也是本文中沒有實現(xiàn)的。
FlowMonitor類
/**
*限流的作用
*實現(xiàn)一個緩沖隊列,讓一部分進入等待狀態(tài)
*區(qū)間監(jiān)控
*如果一個線程返回特別慢怎么辦,比如在release之前拋了異常
* @author 百惱 2013-04-11上午11:26:36
*
*/
public class FlowMonitor {
//默認(rèn)最大的并發(fā)數(shù)默認(rèn)為100,可以配置
private int maxFlowSize = 100;
//最大并發(fā)數(shù)
private int maxRunningSize = 0;
//當(dāng)前并發(fā)數(shù)
private AtomicInteger runningSize = new AtomicInteger();
//通過的數(shù)量
private AtomicInteger passSize = new AtomicInteger();
//失敗的數(shù)量
private AtomicInteger loseSize = new AtomicInteger();
public FlowMonitor(){
super();
}
public FlowMonitor(int maxFlowSize){
this();
this.maxFlowSize = maxFlowSize;
}
/**
* 線程進入開關(guān),即使這里用了一些Atomic類,這里仍然會有并發(fā)問題。
* @return
*/
public boolean entry(){
//每個類中一個配置maxFlowSize
if(maxFlowSize>0){
if(maxFlowSize<=runningSize.get()){
//已經(jīng)超過最大限制
loseSize.incrementAndGet();
return false;
}
//并發(fā)數(shù)+1
runningSize.incrementAndGet();
if(runningSize.get()>maxRunningSize){
//記錄最大的并發(fā)數(shù),有并發(fā)問題
maxRunningSize = runningSize.get();
}
//記錄通過的線程數(shù)
passSize.incrementAndGet();
}
return true;
}
/**
* 執(zhí)行完后,并發(fā)數(shù)-1
* @param key
*/
public void release(){
runningSize.decrementAndGet();
}
public AtomicInteger getRunningSize() {
return runningSize;
}
public AtomicInteger getPassSize() {
return passSize;
}
public AtomicInteger getLoseSize() {
return loseSize;
}
public int getMaxRunningSize() {
return maxRunningSize;
}
/**
* 重置,可以分時段進行監(jiān)控
*/
public void reset(){
passSize.set(0);
loseSize.set(0);
maxRunningSize = 0;
}
}
上面類中最核心的兩個方法:一個是entry(),一個就是release()。entry()是在調(diào)用目標(biāo)方法之前調(diào)用,release()是在調(diào)用目標(biāo)方法之后調(diào)用。對于代碼層面這里就不做太多的解讀,代碼里也有一些注釋,估計學(xué)過Java的都能理解這上面所寫的。在類的注釋里面我寫了幾個問題:
第一個是實現(xiàn)一個緩沖隊列。上面我的處理策略是一種最簡單的方法:只要當(dāng)前并發(fā)數(shù)大于當(dāng)前所設(shè)定最大的并發(fā)數(shù)返回false,不做任何其他的處理。
第二個問題是如果在處理具體的邏輯的過程異常退出使得release()方法沒有執(zhí)行而導(dǎo)致當(dāng)前監(jiān)控的并發(fā)數(shù)不正常。所以這里需要一定的補救措施:可以實現(xiàn)一個隊列,隊列上每個節(jié)點都是一個并發(fā)請求,當(dāng)執(zhí)行entry()方法時,就會往隊列上插入一個節(jié)點,然后當(dāng)執(zhí)行release()時就把這個節(jié)點移出,然后當(dāng)發(fā)現(xiàn)這個并發(fā)請求異常(可以根據(jù)時間來判斷,比如說超過5s還未返回就對它進行中斷操作)就把這個節(jié)點移出。這樣可以比較準(zhǔn)確的監(jiān)控到當(dāng)前的并發(fā)數(shù)。
第三個問題是有關(guān)于監(jiān)控數(shù)據(jù)的問題,目前來看只能監(jiān)控幾個數(shù)據(jù):總的通過的數(shù)量,總的失敗的數(shù)量,最大的并發(fā)數(shù),當(dāng)前并發(fā)數(shù)。如果我想實現(xiàn)這樣一種需求:希望監(jiān)控某一時間段的情況,這個意義在于一旦出現(xiàn)問題(比如說失敗數(shù)量上升)我可以明顯知道在哪個時間節(jié)點出了問題。而目前的實現(xiàn)方式下是無法做到的。在上面的類中我留了一個reset()方法,其意義就是為了實現(xiàn)分時間段進行監(jiān)控提供一個重置幾個參數(shù)的接口。
第四個問題是在這里我并不知道該方法的RT(響應(yīng)時間:在這里就是執(zhí)行完這個方法的時間)監(jiān)控,這個也可以去實現(xiàn)。因為在此文中重點講限流,所以不想講太多有關(guān)于RT的的東西。各位有興趣也可以自己去實現(xiàn),原理也是一樣:仍然是用攔截器來實現(xiàn),具體的實現(xiàn)策略可以多種多樣。
MonitorHandler類
/**
* TODO Comment of MonitorHandler
* @author 百惱 2013-4-9上午10:16:43
*
*/
public interface MonitorHandler {
public boolean before();
public boolean after();
}
這個接口就不做任何解釋了,可以參考一下UML類圖中看看它所在的位置。
AbstractSpringMonitor類
/**
* TODO Comment of AbstractSpringMonitor
* @author 百惱 2013-4-9下午04:42:04
*
*/
public abstract class AbstractSpringMonitor implements MethodInterceptor,MonitorHandler{
/* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
@Override
public Object invoke(MethodInvocation method) throws Throwable {
boolean result = before();
if(result){
try{
method.proceed();
}catch(Exception e){
} finally{
after();
}
}
return null;
}
}
SpringFlowMonitorHandler類
/**
* 使用spring攔截器實現(xiàn)監(jiān)控
*
* @author 百惱 2013-4-9下午06:36:24
*/
public class SpringFlowMonitorHandler extends AbstractSpringMonitor {
private FlowMonitor flowMonitor;
/*
* (non-Javadoc)
* @see com.yuzhipeng.monitor.MonitorHandler#before()
*/
@Override
public boolean before() {
if (!flowMonitor.entry()) {
return false;
}
return true;
}
/*
* (non-Javadoc)
* @see com.yuzhipeng.monitor.MonitorHandler#after()
*/
@Override
public boolean after() {
flowMonitor.release();
return true;
}
public FlowMonitor getFlowMonitor() {
return flowMonitor;
}
public void setFlowMonitor(FlowMonitor flowMonitor) {
this.flowMonitor = flowMonitor;
}
}
上面貼出的兩個類就是有關(guān)于Spring的方式來實現(xiàn),因為這種方法是會在項目中也會用的比較多。另外一種有關(guān)于JDK動態(tài)代理的方法代碼就不貼出來了,實現(xiàn)方式非常簡單。
總結(jié)
在這里簡單的分享了一個有關(guān)“限流”的意義和實現(xiàn)。在文中我也舉了一個例子為什么要限流。在一般傳統(tǒng)的內(nèi)部系統(tǒng)中對于限流的意義并不大。如果在互聯(lián)網(wǎng)公司,對于性能要求極高的時候就會用到限流。后面講到具體實現(xiàn)的時候估計會覺得比較簡單,代碼量也很少。這里又回歸到Spring的AOP,攔截器的使用。萬變不離其宗,Spring的使用(嚴(yán)格來講應(yīng)該是攔截器的這種思想)還是非常廣的。平時用的比較多的可能是權(quán)限校驗,日志記錄這些。
我每篇博客中所傳達的一個重要思想就是:“思考”。在此文中所實現(xiàn)方案并非完美,比如說我在FlowMonitor類中的注釋也說到了會有并發(fā)問題,但是我在那里并沒有加鎖,這又是為什么?在這里傳達一個很重要的思想就是:編程里面仍然講平衡。我們經(jīng)常碰到的一個例子就是”時間換空間,空間換時間“。所以在這里從代碼層面來講我確實要加鎖,但是從實際的應(yīng)用中不加鎖是一種更好的策略。