高併發後端設計-限流篇

詩意偶然發表於2018-04-16

系統在設計之初就會有一個預估容量,長時間超過系統能承受的TPS/QPS閾值,系統可能會被壓垮,最終導致整個服務不夠用。為了避免這種情況,我們就需要對介面請求進行限流。

限流的目的是通過對併發訪問請求進行限速或者一個時間視窗內的的請求數量進行限速來保護系統,一旦達到限制速率則可以拒絕服務、排隊或等待。

常見的限流模式有控制併發和控制速率,一個是限制併發的數量,一個是限制併發訪問的速率,另外還可以限制單位時間視窗內的請求數量。

控制併發數量 屬於一種較常見的限流手段,在實際應用中可以通過訊號量機制(如Java中的Semaphore)來實現。 舉個例子,我們對外提供一個服務介面,允許最大併發數為10,程式碼實現如下:

public class DubboService {    private final Semaphore permit = new Semaphore(10, true);    public void process(){        try{
            permit.acquire();            //業務邏輯處理

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            permit.release();
        }
    }
}
複製程式碼

在程式碼中,雖然有30個執行緒在執行,但是隻允許10個併發的執行。Semaphore的構造方法Semaphore(int permits) 接受一個整型的數字,表示可用的許可證數量。Semaphore(10)表示允許10個執行緒獲取許可證,也就是最大併發數是10。Semaphore的用法也很簡單,首先執行緒使用Semaphore的acquire()獲取一個許可證,使用完之後呼叫release()歸還許可證,還可以用tryAcquire()方法嘗試獲取許可證。

控制訪問速率 在我們的工程實踐中,常見的是使用令牌桶演算法來實現這種模式,其他如漏桶演算法也可以實現控制速率,但在我們的工程實踐中使用不多,這裡不做介紹,讀者請自行了解。

在Wikipedia上,令牌桶演算法是這麼描述的:

每過1/r秒桶中增加一個令牌。

桶中最多存放b個令牌,如果桶滿了,新放入的令牌會被丟棄。

當一個n位元組的資料包到達時,消耗n個令牌,然後傳送該資料包。

如果桶中可用令牌小於n,則該資料包將被快取或丟棄。

令牌桶控制的是一個時間視窗內通過的資料量,在API層面我們常說的QPS、TPS,正好是一個時間視窗內的請求量或者事務量,只不過時間視窗限定在1s罷了。以一個恆定的速度往桶裡放入令牌,而如果請求需要被處理,則需要先從桶裡獲取一個令牌,當桶裡沒有令牌可取時,則拒絕服務。令牌桶的另外一個好處是可以方便的改變速度,一旦需要提高速率,則按需提高放入桶中的令牌的速率。

在我們的工程實踐中,通常使用Guava中的Ratelimiter來實現控制速率,如我們不希望每秒的任務提交超過2個:


//速率是每秒兩個許可final RateLimiter rateLimiter = RateLimiter.create(2.0);

void submitTasks(List tasks, Executor executor) {    for (Runnable task : tasks) {
        rateLimiter.acquire(); // 也許需要等待
        executor.execute(task);
    }
}
複製程式碼

控制單位時間視窗內請求數 某些場景下,我們想限制某個介面或服務 每秒/每分鐘/每天 的請求次數或呼叫次數。例如限制服務每秒的呼叫次數為50,實現如下:


import com.google.common.cache.CacheBuilder;import com.google.common.cache.CacheLoader;import com.google.common.cache.LoadingCache;import java.util.concurrent.ExecutionException;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicLong;

    private LoadingCache<Long, AtomicLong> counter =
            CacheBuilder.newBuilder()                    .expireAfterWrite(2, TimeUnit.SECONDS)                    .build(new CacheLoader<Long, AtomicLong>() {
                        @Override
                        public AtomicLong load(Long seconds) throws Exception {
                            return new AtomicLong(0);
                        }
                    });

    public static long permit = 50;

    public ResponseEntity getData() throws ExecutionException {

        //得到當前秒
        long currentSeconds = System.currentTimeMillis() / 1000;
        if(counter.get(currentSeconds).incrementAndGet() > permit) {
            return ResponseEntity.builder().code(404).msg("訪問速率過快").build();
        }
        //業務處理

    }
    ```
關於限流部分到此就結束了,下一篇將介紹降級和熔斷機制。複製程式碼

相關文章