亚洲视频二区_亚洲欧洲日本天天堂在线观看_日韩一区二区在线观看_中文字幕不卡一区

公告:魔扣目錄網(wǎng)為廣大站長提供免費收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.430618.com 】, 免友鏈快審服務(wù)(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

在開發(fā)分布式高并發(fā)系統(tǒng)時有三把利器用來保護系統(tǒng):緩存、降級、限流

緩存

緩存的目的是提升系統(tǒng)訪問速度和增大系統(tǒng)處理容量

降級

降級是當(dāng)服務(wù)出現(xiàn)問題或者影響到核心流程時,需要暫時屏蔽掉,待高峰或者問題解決后再打開

限流

限流的目的是通過對并發(fā)訪問/請求進行限速,或者對一個時間窗口內(nèi)的請求進行限速來保護系統(tǒng),一旦達到限制速率則可以拒絕服務(wù)、排隊或等待、降級等處理

問題描述

1、某天A君突然發(fā)現(xiàn)自己的接口請求量突然漲到之前的10倍,沒多久該接口幾乎不可使用,并引發(fā)連鎖反應(yīng)導(dǎo)致整個系統(tǒng)崩潰。如何應(yīng)對這種情況呢?生活給了我們答案:比如老式電閘都安裝了保險絲,一旦有人使用超大功率的設(shè)備,保險絲就會燒斷以保護各個電器不被強電流給燒壞。同理我們的接口也需要安裝上“保險絲”,以防止非預(yù)期的請求對系統(tǒng)壓力過大而引起的系統(tǒng)癱瘓,當(dāng)流量過大時,可以采取拒絕或者引流等機制。整編:微信公眾號,搜云庫技術(shù)團隊,ID:souyunku

2、緩存的目的是提升系統(tǒng)訪問速度和增大系統(tǒng)能處理的容量,可謂是抗高并發(fā)流量的銀彈;而降級是當(dāng)服務(wù)出問題或者影響到核心流程的性能則需要暫時屏蔽掉,待高峰或者問題解決后再打開;而有些場景并不能用緩存和降級來解決,比如稀缺資源(秒殺、搶購)、寫服務(wù)(如評論、下單)、頻繁的復(fù)雜查詢(評論的最后幾頁),因此需有一種手段來限制這些場景的并發(fā)/請求量,即限流。

3、系統(tǒng)在設(shè)計之初就會有一個預(yù)估容量,長時間超過系統(tǒng)能承受的TPS/QPS閾值,系統(tǒng)可能會被壓垮,最終導(dǎo)致整個服務(wù)不夠用。為了避免這種情況,我們就需要對接口請求進行限流。

4、限流的目的是通過對并發(fā)訪問請求進行限速或者一個時間窗口內(nèi)的的請求數(shù)量進行限速來保護系統(tǒng),一旦達到限制速率則可以拒絕服務(wù)、排隊或等待。

5、一般開發(fā)高并發(fā)系統(tǒng)常見的限流模式有控制并發(fā)和控制速率,一個是限制并發(fā)的總數(shù)量(比如數(shù)據(jù)庫連接池、線程池),一個是限制并發(fā)訪問的速率(如Nginx的limitconn模塊,用來限制瞬時并發(fā)連接數(shù)),另外還可以限制單位時間窗口內(nèi)的請求數(shù)量(如Guava的RateLimiter、nginx的limitreq模塊,限制每秒的平均速率)。其他還有如限制遠程接口調(diào)用速率、限制MQ的消費速率。另外還可以根據(jù)網(wǎng)絡(luò)連接數(shù)、網(wǎng)絡(luò)流量、CPU或內(nèi)存負載等來限流。

相關(guān)概念:

PV:

page view 頁面總訪問量,每刷新一次記錄一次。

UV:

unique view 客戶端主機訪問,指一天內(nèi)相同IP的訪問記為1次。

QPS:

query per second,即每秒訪問量。qps很大程度上代表了系統(tǒng)的繁忙度,沒次請求可能存在多次的磁盤io,網(wǎng)絡(luò)請求,多個cpu時間片,一旦qps超過了預(yù)先設(shè)置的閥值,可以考量擴容增加服務(wù)器,避免訪問量過大導(dǎo)致的宕機。整編:微信公眾號,搜云庫技術(shù)團隊,ID:souyunku

RT:

response time,每次請求的響應(yīng)時間,直接決定用戶體驗性。

本文主要介紹應(yīng)用級限流方法,分布式限流、流量入口限流(接入層如NGINX limitconn和limitreq 模塊)。

應(yīng)用級限流

一、控制并發(fā)數(shù)量

屬于一種較常見的限流手段,在實際應(yīng)用中可以通過信號量機制(如JAVA中的Semaphore)來實現(xiàn)。操作系統(tǒng)的信號量是個很重要的概念,Java 并發(fā)庫 的Semaphore 可以很輕松完成信號量控制,Semaphore可以控制某個資源可被同時訪問的個數(shù),通過 acquire() 獲取一個許可,如果沒有就等待,而 release() 釋放一個許可。

舉個例子,我們對外提供一個服務(wù)接口,允許最大并發(fā)數(shù)為10,代碼實現(xiàn)如下:

public class DubboService {    private final Semaphore permit = new Semaphore(10, true);    public void process(){        try{            permit.acquire();            //業(yè)務(wù)邏輯處理        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            permit.release();        }    }}

在以上代碼中,雖然有30個線程在執(zhí)行,但是只允許10個并發(fā)的執(zhí)行。Semaphore的構(gòu)造方法Semaphore(int permits) 接受一個整型的數(shù)字,表示可用的許可證數(shù)量。Semaphore(10)表示允許10個線程獲取許可證,也就是最大并發(fā)數(shù)是10。Semaphore的用法也很簡單,首先線程使用Semaphore的acquire()獲取一個許可證,使用完之后調(diào)用release()歸還許可證,還可以用tryAcquire()方法嘗試獲取許可證,信號量的本質(zhì)是控制某個資源可被同時訪問的個數(shù),在一定程度上可以控制某資源的訪問頻率,但不能精確控制,控制訪問頻率的模式見下文描述。

二、控制訪問速率

在工程實踐中,常見的是使用令牌桶算法來實現(xiàn)這種模式,常用的限流算法有兩種:漏桶算法和令牌桶算法。

漏桶算法

漏桶算法思路很簡單,水(請求)先進入到漏桶里,漏桶以一定的速度出水,當(dāng)水流入速度過大會直接溢出,可以看出漏桶算法能強行限制數(shù)據(jù)的傳輸速率。

高并發(fā)之API接口,分布式,防刷限流,如何做?

 

對于很多應(yīng)用場景來說,除了要求能夠限制數(shù)據(jù)的平均傳輸速率外,還要求允許某種程度的突發(fā)傳輸。這時候漏桶算法可能就不合適了,令牌桶算法更為適合。

令牌桶算法

如圖所示,令牌桶算法的原理是系統(tǒng)會以一個恒定的速度往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個令牌,當(dāng)桶里沒有令牌可取時,則拒絕服務(wù),令牌桶算法通過發(fā)放令牌,根據(jù)令牌的rate頻率做請求頻率限制,容量限制等。整編:微信公眾號,搜云庫技術(shù)團隊,ID:souyunku

高并發(fā)之API接口,分布式,防刷限流,如何做?

 

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

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

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

3、當(dāng)一個n字節(jié)的數(shù)據(jù)包到達時,消耗n個令牌,然后發(fā)送該數(shù)據(jù)包。

4、如果桶中可用令牌小于n,則該數(shù)據(jù)包將被緩存或丟棄。

令牌桶控制的是一個時間窗口內(nèi)通過的數(shù)據(jù)量,在API層面我們常說的QPS、TPS,正好是一個時間窗口內(nèi)的請求量或者事務(wù)量,只不過時間窗口限定在1s罷了。以一個恒定的速度往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個令牌,當(dāng)桶里沒有令牌可取時,則拒絕服務(wù)。令牌桶的另外一個好處是可以方便的改變速度,一旦需要提高速率,則按需提高放入桶中的令牌的速率。

在我們的工程實踐中,通常使用google開源工具包Guava提供的限流工具類RateLimiter來實現(xiàn)控制速率,該類基于令牌桶算法來完成限流,非常易于使用,而且非常高效。如我們不希望每秒的任務(wù)提交超過1個

public static void main(String[] args) {    String start = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());    RateLimiter limiter = RateLimiter.create(1.0); // 這里的1表示每秒允許處理的量為1個    for (int i = 1; i <= 10; i++) {        double waitTime = limiter.acquire(i); // 請求RateLimiter, 超過permits會被阻塞        System.out.println("cutTime=" + System.currentTimeMillis() + " call execute:" + i + " waitTime:" + waitTime);    }    String end = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());    System.out.println("start time:" + start);    System.out.println("end time:" + end);}

首先通過RateLimiter.create(1.0);創(chuàng)建一個限流器,參數(shù)代表每秒生成的令牌數(shù),通過limiter.acquire(i);來以阻塞的方式獲取令牌,令牌桶算法允許一定程度的突發(fā)(允許消費未來的令牌),所以可以一次性消費i個令牌;當(dāng)然也可以通過tryAcquire(int permits, long timeout, TimeUnit unit)來設(shè)置等待超時時間的方式獲取令牌,如果超timeout為0,則代表非阻塞,獲取不到立即返回,支持阻塞或可超時的令牌消費。

從輸出來看,RateLimiter支持預(yù)消費,比如在acquire(5)時,等待時間是4秒,是上一個獲取令牌時預(yù)消費了3個兩排,固需要等待3*1秒,然后又預(yù)消費了5個令牌,以此類推。

RateLimiter通過限制后面請求的等待時間,來支持一定程度的突發(fā)請求(預(yù)消費),在使用過程中需要注意這一點,Guava有兩種限流模式,一種為穩(wěn)定模式(SmoothBursty:令牌生成速度恒定,平滑突發(fā)限流),一種為漸進模式(SmoothWarmingUp:令牌生成速度緩慢提升直到維持在一個穩(wěn)定值,平滑預(yù)熱限流) 兩種模式實現(xiàn)思路類似,主要區(qū)別在等待時間的計算上。

SmoothBursty 模式:

RateLimiter limiter = RateLimiter.create(5); RateLimiter.create(5)表示桶容量為5且每秒新增5個令牌,即每隔200毫秒新增一個令牌;limiter.acquire()表示消費一個令牌,如果當(dāng)前桶中有足夠令牌則成功(返回值為0),如果桶中沒有令牌則暫停一段時間,比如發(fā)令牌間隔是200毫秒,則等待200毫秒后再去消費令牌,這種實現(xiàn)將突發(fā)請求速率平均為了固定請求速率。

SmoothWarmingUp模式:

RateLimiter limiter = RateLimiter.create(5,1000, TimeUnit.MILLISECONDS);

創(chuàng)建方式:

RateLimiter.create(doublepermitsPerSecond, long warmupPeriod, TimeUnit unit),permitsPerSecond表示每秒新增的令牌數(shù),warmupPeriod表示在從冷啟動速率過渡到平均速率的時間間隔。速率是梯形上升速率的,也就是說冷啟動時會以一個比較大的速率慢慢到平均速率;然后趨于平均速率(梯形下降到平均速率)。可以通過調(diào)節(jié)warmupPeriod參數(shù)實現(xiàn)一開始就是平滑固定速率。整編:微信公眾號,搜云庫技術(shù)團隊,ID:souyunku

放在Controller中用Jemter壓測

高并發(fā)之API接口,分布式,防刷限流,如何做?

 

注:RateLimiter控制的是速率,Samephore控制的是并發(fā)量。RateLimiter的原理就是令牌桶,它主要由許可發(fā)出的速率來定義,如果沒有額外的配置,許可證將按每秒許可證規(guī)定的固定速度分配,許可將被平滑地分發(fā),若請求超過permitsPerSecond則RateLimiter按照每秒 1/permitsPerSecond 的速率釋放許可。注意:RateLimiter適用于單體應(yīng)用,且RateLimiter不保證公平性訪問。

使用上述方式使用RateLimiter的方式不夠優(yōu)雅,自定義注解+AOP的方式實現(xiàn)(適用于單體應(yīng)用),詳細見下面代碼:

自定義注解:

import java.lang.annotation.*;/** * 自定義注解可以不包含屬性,成為一個標(biāo)識注解 */@Inherited@Documented@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface RateLimitAspect {}

自定義切面類

import com.google.common.util.concurrent.RateLimiter;import com.test.cn.springbootdemo.util.ResultUtil;import net.sf.json.JSONObject;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Scope;import org.springframework.stereotype.Component;import javax.servlet.ServletOutputStream;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@Component@Scope@Aspectpublic class RateLimitAop {    @Autowired    private HttpServletResponse response;    private RateLimiter rateLimiter = RateLimiter.create(5.0); //比如說,我這里設(shè)置"并發(fā)數(shù)"為5    @Pointcut("@annotation(com.test.cn.springbootdemo.aspect.RateLimitAspect)")    public void serviceLimit() {    }    @Around("serviceLimit()")    public Object around(ProceedingJoinPoint joinPoint) {        Boolean flag = rateLimiter.tryAcquire();        Object obj = null;        try {            if (flag) {                obj = joinPoint.proceed();            }else{                String result = JSONObject.fromObject(ResultUtil.success1(100, "failure")).toString();                output(response, result);            }        } catch (Throwable e) {            e.printStackTrace();        }        System.out.println("flag=" + flag + ",obj=" + obj);        return obj;    }    public void output(HttpServletResponse response, String msg) throws IOException {        response.setContentType("Application/json;charset=UTF-8");        ServletOutputStream outputStream = null;        try {            outputStream = response.getOutputStream();            outputStream.write(msg.getBytes("UTF-8"));        } catch (IOException e) {            e.printStackTrace();        } finally {            outputStream.flush();            outputStream.close();        }    }}

測試controller

import com.test.cn.springbootdemo.aspect.RateLimitAspect;import com.test.cn.springbootdemo.util.ResultUtil;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;@Controllerpublic class TestController {    @ResponseBody    @RateLimitAspect    @RequestMapping("/test")    public String test(){        return ResultUtil.success1(1001, "success").toString();    }

壓測結(jié)果:

高并發(fā)之API接口,分布式,防刷限流,如何做?

 

三、控制單位時間窗口內(nèi)請求數(shù)

某些場景下,我們想限制某個接口或服務(wù) 每秒/每分鐘/每天 的請求次數(shù)或調(diào)用次數(shù)。例如限制服務(wù)每秒的調(diào)用次數(shù)為50,實現(xiàn)如下:

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 {    //得到當(dāng)前秒    long currentSeconds = System.currentTimeMillis() / 1000;    if (counter.get(currentSeconds).incrementAndGet() > permit) {        return ResponseEntity.builder().code(404).msg("訪問速率過快").build();    }    //業(yè)務(wù)處理}

到此應(yīng)用級限流的一些方法就介紹完了。假設(shè)將應(yīng)用部署到多臺機器,應(yīng)用級限流方式只是單應(yīng)用內(nèi)的請求限流,不能進行全局限流。因此我們需要分布式限流和接入層限流來解決這個問題。

分布式限流

自定義注解+攔截器+redis實現(xiàn)限流 (單體和分布式均適用,全局限流)

自定義注解:

@Inherited@Documented@Target({ElementType.FIELD,ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface AccessLimit {    int limit() default 5;      int sec() default 5;}

攔截器:

public class AccessLimitInterceptor implements HandlerInterceptor {    @Autowired    private RedisTemplate<String, Integer> redisTemplate;  //使用RedisTemplate操作redis    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        if (handler instanceof HandlerMethod) {            HandlerMethod handlerMethod = (HandlerMethod) handler;            Method method = handlerMethod.getMethod();            if (!method.isAnnotationPresent(AccessLimit.class)) {                return true;            }            AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);            if (accessLimit == null) {                return true;            }            int limit = accessLimit.limit();            int sec = accessLimit.sec();            String key = IPUtil.getIpAddr(request) + request.getRequestURI();            Integer maxLimit = redisTemplate.opsForValue().get(key);            if (maxLimit == null) {                redisTemplate.opsForValue().set(key, 1, sec, TimeUnit.SECONDS);  //set時一定要加過期時間            } else if (maxLimit < limit) {                redisTemplate.opsForValue().set(key, maxLimit + 1, sec, TimeUnit.SECONDS);            } else {                output(response, "請求太頻繁!");                return false;            }        }        return true;    }    public void output(HttpServletResponse response, String msg) throws IOException {        response.setContentType("application/json;charset=UTF-8");        ServletOutputStream outputStream = null;        try {            outputStream = response.getOutputStream();            outputStream.write(msg.getBytes("UTF-8"));        } catch (IOException e) {            e.printStackTrace();        } finally {            outputStream.flush();            outputStream.close();        }    }    @Override    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {    }    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {    }}

controller:

@Controller@RequestMapping("/activity")public class AopController {    @ResponseBody    @RequestMapping("/seckill")    @AccessLimit(limit = 4,sec = 10)  //加上自定義注解即可    public String test (HttpServletRequest request,@RequestParam(value = "username",required = false) String userName){        //TODO somethings……        return   "hello world !";    }}

配置文件:

/*springmvc的配置文件中加入自定義攔截器*/<mvc:interceptors>   <mvc:interceptor>      <mvc:mapping path="/**"/>      <bean class="com.pptv.activityapi.controller.pointsmall.AccessLimitInterceptor"/>   </mvc:interceptor></mvc:interceptors>

訪問效果如下,10s內(nèi)訪問接口超過4次以上就過濾請求,原理和計數(shù)器算法類似:

高并發(fā)之API接口,分布式,防刷限流,如何做?

 

接入層限流

主要介紹nginx 限流,采用漏桶算法。

限制原理:可一句話概括為:“根據(jù)客戶端特征,限制其訪問頻率”,客戶端特征主要指IP、UserAgent等。使用IP比UserAgent更可靠,因為IP無法造假,UserAgent可隨意偽造。整編:微信公眾號,搜云庫技術(shù)團隊,ID:souyunku

用limit_req模塊來限制基于IP請求的訪問頻率:

http://nginx.org/en/docs/http/ngxhttplimitreqmodule.html

也可以用tengine中的增強版:

http://tengine.taobao.org/documentcn/httplimitreqcn.html

1、并發(fā)數(shù)和連接數(shù)控制的配置:

nginx http配置:    #請求數(shù)量控制,每秒20個    limit_req_zone $binary_remote_addr zone=one:10m rate=20r/s;    #并發(fā)限制30個    limit_conn_zone $binary_remote_addr zone=addr:10m;    server塊配置    limit_req zone=one burst=5;    limit_conn addr 30;

2、ngxhttplimitconnmodule 可以用來限制單個IP的連接數(shù):

ngxhttplimitconnmodule模塊可以按照定義的鍵限定每個鍵值的連接數(shù)。可以設(shè)定單一 IP 來源的連接數(shù)。

并不是所有的連接都會被模塊計數(shù);只有那些正在被處理的請求(這些請求的頭信息已被完全讀入)所在的連接才會被計數(shù)。

http {    limit_conn_zone $binary_remote_addr zone=addr:10m;    ...    server {        ...        location /download/ {            limit_conn addr 1;        }

分享到:
標(biāo)簽:接口 API
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨大挑戰(zhàn)2018-06-03

數(shù)獨一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運動步數(shù)有氧達人2018-06-03

記錄運動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定