分布式限流,你想知道的都在這裡

2021-01-08 51CTO

前言

在一個高並發系統中對流量的把控是非常重要的,當巨大的流量直接請求到我們的伺服器上沒多久就可能造成接口不可用,不處理的話甚至會造成整個應用不可用。

比如最近就有個這樣的需求,我作為客戶端要向kafka生產數據,而kafka的消費者則再源源不斷的消費數據,並將消費的數據全部請求到web伺服器,雖說做了負載(有4臺web伺服器)但業務數據的量也是巨大的,每秒鐘可能有上萬條數據產生。如果生產者直接生產數據的話極有可能把web伺服器拖垮。

對此就必須要做限流處理,每秒鐘生產一定限額的數據到kafka,這樣就能極大程度的保證web的正常運轉。

其實不管處理何種場景,本質都是降低流量保證應用的高可用。

常見算法

對於限流常見有兩種算法:

漏桶算法比較簡單,就是將流量放入桶中,漏桶同時也按照一定的速率流出,如果流量過快的話就會溢出(漏桶並不會提高流出速率)。溢出的流量則直接丟棄。

如下圖所示:


這種做法簡單粗暴。

漏桶算法雖說簡單,但卻不能應對實際場景,比如突然暴增的流量。

這時就需要用到令牌桶算法:

令牌桶會以一個恆定的速率向固定容量大小桶中放入令牌,當有流量來時則取走一個或多個令牌。當桶中沒有令牌則將當前請求丟棄或阻塞。

相比之下令牌桶可以應對一定的突發流量。

RateLimiter實現

對於令牌桶的代碼實現,可以直接使用Guava包中的RateLimiter。

@Override public BaseResponse<UserResVO> getUserByFeignBatch(@RequestBody UserReqVO userReqVO) {  //調用遠程服務  OrderNoReqVO vo = new OrderNoReqVO() ;  vo.setReqNo(userReqVO.getReqNo());  RateLimiter limiter = RateLimiter.create(2.0) ;  //批量調用  for (int i = 0 ;i< 10 ; i++){  double acquire = limiter.acquire();  logger.debug("獲取令牌成功!,消耗=" + acquire);  BaseResponse<OrderNoResVO> orderNo = orderServiceClient.getOrderNo(vo);  logger.debug("遠程返回:"+JSON.toJSONString(orderNo));  }  UserRes userRes = new UserRes() ;  userRes.setUserId(123);  userRes.setUserName("張三");  userRes.setReqNo(userReqVO.getReqNo());  userRes.setCode(StatusEnum.SUCCESS.getCode());  userRes.setMessage("成功");  return userRes ; } 

詳見此。

調用結果如下:

代碼可以看出以每秒向桶中放入兩個令牌,請求一次消耗一個令牌。所以每秒鐘只能發送兩個請求。按照圖中的時間來看也確實如此(返回值是獲取此令牌所消耗的時間,差不多也是每500ms一個)。

使用RateLimiter有幾個值得注意的地方:

允許先消費,後付款,意思就是它可以來一個請求的時候一次性取走幾個或者是剩下所有的令牌甚至多取,但是後面的請求就得為上一次請求買單,它需要等待桶中的令牌補齊之後才能繼續獲取令牌。

總結

針對於單個應用的限流 RateLimiter 夠用了,如果是分布式環境可以藉助 Redis 來完成。

來做演示。

在 Order 應用提供的接口中採取了限流。首先是配置了限流工具的 Bean:

@Configuration public class RedisLimitConfig {  @Value("${redis.limit}")  private int limit;  @Autowired  private JedisConnectionFactory jedisConnectionFactory;  @Bean  public RedisLimit build() {  RedisClusterConnection clusterConnection = jedisConnectionFactory.getClusterConnection();  JedisCluster jedisCluster = (JedisCluster) clusterConnection.getNativeConnection();  RedisLimit redisLimit = new RedisLimit.Builder<>(jedisCluster)  .limit(limit)  .build();  return redisLimit;  } } 

接著在 Controller 使用組件:

@Autowired private RedisLimit redisLimit ; @Override @CheckReqNo public BaseResponse<OrderNoResVO> getOrderNo(@RequestBody OrderNoReqVO orderNoReq) {  BaseResponse<OrderNoResVO> res = new BaseResponse();  //限流  boolean limit = redisLimit.limit();  if (!limit){  res.setCode(StatusEnum.REQUEST_LIMIT.getCode());  res.setMessage(StatusEnum.REQUEST_LIMIT.getMessage());  return res ;  }  res.setReqNo(orderNoReq.getReqNo());  if (null == orderNoReq.getAppId()){  throw new SBCException(StatusEnum.FAIL);  }  OrderNoResVO orderNoRes = new OrderNoResVO() ;  orderNoRes.setOrderId(DateUtil.getLongTime());  res.setCode(StatusEnum.SUCCESS.getCode());  res.setMessage(StatusEnum.SUCCESS.getMessage());  res.setDataBody(orderNoRes);  return res ; } 

為了方便使用,也提供了註解:

@Override @ControllerLimit public BaseResponse<OrderNoResVO> getOrderNoLimit(@RequestBody OrderNoReqVO orderNoReq) {  BaseResponse<OrderNoResVO> res = new BaseResponse();  // 業務邏輯  return res ; } 

該註解攔截了 http 請求,會再請求達到閾值時直接返回。

普通方法也可使用:

@CommonLimit public void doSomething(){} 

會在調用達到閾值時拋出異常。

為了模擬並發,在 User 應用中開啟了 10 個線程調用 Order(限流次數為5) 接口(也可使用專業的並發測試工具 JMeter 等)。

@Override public BaseResponse<UserResVO> getUserByFeign(@RequestBody UserReqVO userReq) {  //調用遠程服務  OrderNoReqVO vo = new OrderNoReqVO();  vo.setAppId(1L);  vo.setReqNo(userReq.getReqNo());  for (int i = 0; i < 10; i++) {  executorService.execute(new Worker(vo, orderServiceClient));  }  UserRes userRes = new UserRes();  userRes.setUserId(123);  userRes.setUserName("張三");  userRes.setReqNo(userReq.getReqNo());  userRes.setCode(StatusEnum.SUCCESS.getCode());  userRes.setMessage("成功");  return userRes; } private static class Worker implements Runnable {  private OrderNoReqVO vo;  private OrderServiceClient orderServiceClient;  public Worker(OrderNoReqVO vo, OrderServiceClient orderServiceClient) {  this.vo = vo;  this.orderServiceClient = orderServiceClient;  }  @Override  public void run() {  BaseResponse<OrderNoResVO> orderNo = orderServiceClient.getOrderNoCommonLimit(vo);  logger.info("遠程返回:" + JSON.toJSONString(orderNo));  } } 

為了驗證分布式效果啟動了兩個 Order 應用。

效果如下:

實現原理

實現原理其實很簡單。既然要達到分布式全局限流的效果,那自然需要一個第三方組件來記錄請求的次數。

其中 Redis 就非常適合這樣的場景。

每次請求時將當前時間(精確到秒)作為 Key 寫入到 Redis 中,超時時間設置為 2 秒,Redis 將該 Key 的值進行自增。 當達到閾值時返回錯誤。 寫入 Redis 的操作用 Lua 腳本來完成,利用 Redis 的單線程機制可以保證每個 Redis 請求的原子性。

Lua 腳本如下:

--lua 下標從 1 開始-- 限流 keylocal key = KEYS[1]-- 限流大小local limit = tonumber(ARGV[1])-- 獲取當前流量大小local curentLimit = tonumber(redis.call('get', key) or "0")if curentLimit + 1 > limit then -- 達到限流大小 返回 return 0;else -- 沒有達到閾值 value + 1 redis.call("INCRBY", key, 1) redis.call("EXPIRE", key, 2) return curentLimit + 1end

Java 中的調用邏輯:

  local key = KEYS[1]  local limit = tonumber(ARGV[1])  local curentLimit = tonumber(redis.call('get', key) or "0") if curentLimit + 1 > limit then    return 0; else    redis.call("INCRBY", key, 1)  redis.call("EXPIRE", key, 2)  return curentLimit + 1 end 

所以只需要在需要限流的地方調用該方法對返回值進行判斷即可達到限流的目的。

當然這只是利用 Redis 做了一個粗暴的計數器,如果想實現類似於上文中的令牌桶算法可以基於 Lua 自行實現。

Builder 構建器

在設計這個組件時想儘量的提供給使用者清晰、可讀性、不易出錯的 API。

比如***步,如何構建一個限流對象。

最常用的方式自然就是構造函數,如果有多個域則可以採用重疊構造器的方式:

public A(){} public A(int a){} public A(int a,int b){} 

缺點也是顯而易見的:如果參數過多會導致難以閱讀,甚至如果參數類型一致的情況下客戶端顛倒了順序,但不會引起警告從而出現難以預測的結果。

第二種方案可以採用 JavaBean 模式,利用 setter 方法進行構建:

A a = new A(); a.setA(a); a.setB(b); 

這種方式清晰易讀,但卻容易讓對象處於不一致的狀態,使對象處於線程不安全的狀態。

所以這裡採用了第三種創建對象的方式,構建器:

public class RedisLimit {  private JedisCommands jedis;  private int limit = 200;  private static final int FAIL_CODE = 0;  /**  * lua script  */  private String script;  private RedisLimit(Builder builder) {  this.limit = builder.limit ;  this.jedis = builder.jedis ;  buildScript();  }  /**  * limit traffic  * @return if true  */  public boolean limit() {  String key = String.valueOf(System.currentTimeMillis() / 1000);  Object result = null;  if (jedis instanceof Jedis) {  result = ((Jedis) this.jedis).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit)));  } else if (jedis instanceof JedisCluster) {  result = ((JedisCluster) this.jedis).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit)));  } else {  //throw new RuntimeException("instance is error") ;  return false;  }  if (FAIL_CODE != (Long) result) {  return true;  } else {  return false;  }  }  /**  * read lua script  */  private void buildScript() {  script = ScriptUtil.getScript("limit.lua");  }  /**  * the builder  * @param <T>  */  public static class Builder<T extends JedisCommands>{  private T jedis = null ;  private int limit = 200;  public Builder(T jedis){  this.jedis = jedis ;  }  public Builder limit(int limit){  this.limit = limit ;  return this;  }  public RedisLimit build(){  return new RedisLimit(this) ;  }  } } 

這樣客戶端在使用時:

RedisLimit redisLimit = new RedisLimit.Builder<>(jedisCluster)  .limit(limit)  .build(); 

更加的簡單直接,並且避免了將創建過程分成了多個子步驟。

這在有多個構造參數,但又不是必選欄位時很有作用。

因此順便將分布式鎖的構建器方式也一併更新了:

https://github.com/crossoverJie/distributed-redis-tool#features

API

從上文可以看出,使用過程就是調用 limit 方法。

//限流  boolean limit = redisLimit.limit();  if (!limit){  //具體限流邏輯  } 

為了減少侵入性,也為了簡化客戶端提供了兩種註解方式。

@ControllerLimit

該註解可以作用於 @RequestMapping 修飾的接口中,並會在限流後提供限流響應。

實現如下:

@Component public class WebIntercept extends WebMvcConfigurerAdapter {  private static Logger logger = LoggerFactory.getLogger(WebIntercept.class);  @Autowired  private RedisLimit redisLimit;  @Override  public void addInterceptors(InterceptorRegistry registry) {  registry.addInterceptor(new CustomInterceptor())  .addPathPatterns("/**");  }  private class CustomInterceptor extends HandlerInterceptorAdapter {  @Override  public boolean preHandle(HttpServletRequest request, HttpServletResponse response,  Object handler) throws Exception {  if (redisLimit == null) {  throw new NullPointerException("redisLimit is null");  }  if (handler instanceof HandlerMethod) {  HandlerMethod method = (HandlerMethod) handler;  ControllerLimit annotation = method.getMethodAnnotation(ControllerLimit.class);  if (annotation == null) {  //skip  return true;  }  boolean limit = redisLimit.limit();  if (!limit) {  logger.warn("request has bean limit");  response.sendError(500, "request limit");  return false;  }  }  return true;  }  } } 

其實就是實現了 SpringMVC 中的攔截器,並在攔截過程中判斷是否有使用註解,從而調用限流邏輯。

前提是應用需要掃描到該類,讓 Spring 進行管理。

@ComponentScan(value = "com.crossoverjie.distributed.intercept") 

@CommonLimit

當然也可以在普通方法中使用。實現原理則是 Spring AOP (SpringMVC 的攔截器本質也是 AOP)。

@Aspect @Component @EnableAspectJAutoProxy(proxyTargetClass = true) public class CommonAspect {  private static Logger logger = LoggerFactory.getLogger(CommonAspect.class);  @Autowired  private RedisLimit redisLimit ;  @Pointcut("@annotation(com.crossoverjie.distributed.annotation.CommonLimit)")  private void check(){}  @Before("check()")  public void before(JoinPoint joinPoint) throws Exception {  if (redisLimit == null) {  throw new NullPointerException("redisLimit is null");  }  boolean limit = redisLimit.limit();  if (!limit) {  logger.warn("request has bean limit");  throw new RuntimeException("request has bean limit") ;  }  } } 

很簡單,也是在攔截過程中調用限流。

當然使用時也得掃描到該包:

@ComponentScan(value = "com.crossoverjie.distributed.intercept") 

總結

限流在一個高並發大流量的系統中是保護應用的一個利器,成熟的方案也很多,希望對剛了解這一塊的朋友提供一些思路。

【編輯推薦】

【責任編輯:

武曉燕

TEL:(010)68476606】

點讚 0

相關焦點

  • 分布式限流
    對於現在普遍的分布式應用,簡單開發了一個分布式限流的方案。為了模擬並發,在 User 應用中開啟了 10 個線程調用 Order(限流次數為5) 接口(也可使用專業的並發測試工具 JMeter 等)。=(Long) result){returntrue;}else{returnfalse;}}所以只需要在需要限流的地方調用該方法對返回值進行判斷即可達到限流的目的。當然這只是利用 Redis 做了一個粗暴的計數器,如果想實現類似於上文中的令牌桶算法可以基於 Lua 自行實現。
  • 如何判斷抖音作品是否被限流 抖音被限流都有哪些情況
    抖音是現在熱門短視頻軟體,吸引了一大批夥伴的使用,有一些夥伴因為操作不當,導致抖音帳號被封禁了,如何判斷抖音作品是否被限流?抖音被限流都有哪些情況?一起和小編來看看吧。抖音被限流都有哪些情況?一起和小編來看看吧。
  • 抖音限流是什麼意思?限流是什麼網絡用語?
    相信大家在刷抖音或者看一些自媒體視頻的時候,經常會聽到作者說自己被限流了,那麼這裡限流究竟是什麼意思,我們一起來看一下。限流可以理解為限制流量,這裡流量不是手機套餐流量,而是視頻或者文章的推薦量,因為我們刷微博或者抖音等軟體平臺的時候,博主或者一些UP主發布的視頻或者寫的文章,如果想要讓用戶看到,一般都是平臺推薦給用戶來觀看,這樣很多人才能看到一些喜歡的博主發布的內容
  • 小紅書筆記限流原因?解決方法都在這?速來圍觀!
    為什麼我的內容不差平臺給我限流了?為什麼我沒有違規筆記被限流了?為什麼筆記閱讀量一直上不去,這是準哥目前為止聽到的關於小紅書的最多問題,其實關於小紅書筆記限流,首先要了解清楚你是整個帳號被限流還是個別筆記被限流。
  • 快手被限流都有出現什麼情況 怎麼判斷快手帳號是否被限流了
    快手是現在熱門短視頻軟體,吸引了一大批夥伴的使用,有一些夥伴因為操作不當,導致快手帳號被封禁了,那麼快手被限流都有出現什麼情況?怎麼判斷快手帳號是否被限流了? 怎麼判斷快手帳號是否被限流了?如果你被限流了是會收到官方的通知的哦,主要表現為視頻涉及違規,流量突然變得很低。
  • 「面經」慌了,面試居然被問到怎麼做高並發系統的限流?
    比如累積一些數據批量寫入,內存裡面的緩存隊列(生產消費),以及HBase寫數據的機制等等也都是通過緩存提升系統的吞吐量或者實現系統的保護措施。甚至消息中間件,你也可以認為是一種分布式的數據緩存。限流限流可以認為服務降級的一種,限流就是限制系統的輸入和輸出流量已達到保護系統的目的。一般來說系統的吞吐量是可以被測算的,為了保證系統的穩定運行,一旦達到的需要限制的閾值,就需要限制流量並採取一些措施以完成限制流量的目的。比如:延遲處理,拒絕處理,或者部分拒絕處理等等。
  • 限流是什麼意思,自媒體短視頻限流是什麼意思,被軟體限流怎麼辦
    相信大家在刷抖音或者看一些自媒體視頻的時候,經常會聽到作者說自己被限流了,那麼這裡限流究竟是什麼意思,我們一起來看一下。限流可以理解為限制流量,這裡流量不是手機套餐流量,而是視頻或者文章的推薦量,因為我們刷微博或者抖音等軟體平臺的時候,博主或者一些UP主發布的視頻或者寫的文章,如果想要讓用戶看到,一般都是平臺推薦給用戶來觀看,這樣很多人才能看到一些喜歡的博主發布的內容,如果某位博主一段事件內,視頻或者文章播放量比較低,就感覺自己被平臺限流了,降低了推薦量
  • 架構師成長之路之限流漫談
    我們通常重點關注產品業務層面正向和逆向功能的完成,而對於逆向技術保障,這一點則是企業發展過程中很容易忽視的,所以一旦業務快速增長,這將給你的產品帶來很大的隱患。當然,也不是所有的系統都需要限流,這取決於架構師對於當前業務發展的預判。2.
  • 電動車控制器限流20A是什麼意思?看完就明白了!
    電動車控制器限流20A是什麼意思?看完就明白了!這個問題其實很簡單,首先從你的思路說起。你想換30AH的鋰電池,但是控制器上寫著限流「20A」。其實蓄電池容量和控制器限流沒有任何關係。例如你說的30A容量的鋰電池,其實這個「30A」後面還有一個「H」,這是電池容量的一個表達方法。電池放電電流(恆流)×放電時間=電池容量。例如30AH的電池組,以30安的電流放電可以持續放一小時,後面的「H」就是代表小時。以10安電流放電可以持續放電三個小時,這就是電池的容量。如果是60V鋰電池組,那麼30AH的電池內可以儲存1.8度電。容量越大,儲存電量越多。
  • 自己想做個LED指示燈,怎麼計算限流電阻的大小?
    對於正常工作的LED燈來說,工作電壓既不能太高也不能太低,太高可能會燒掉LED燈,太低不能正常發光,因此當電源電壓高於LED燈的正常工作電壓時,需要採取一些措施防止LED燈被燒掉,一個最簡單的方法就是加上限流電阻,但是這個電阻阻值該怎麼計算呢?
  • 看完這個,我終於明白小紅書為什麼會被限流了
    遭遇小紅書筆記低谷期的你,肯定已經發覺自己的筆記被限流了,但究竟是什麼原因呢?被限流的日子很難熬,但是別慌,也別看著可憐的閱讀量懷疑自己,浪姐梳理了一下小紅書筆記限流的種種原因,以及解決辦法,希望能幫你早日恢復流量。當然,如果你的筆記還安好,也能藉此來學習一下哪些筆記或者操作是有違規風險的,防止日後突然被限流。
  • 什麼是多級限流孔板流量計,多級限流裝置都有哪些種類
    多級限流孔板流量計是什麼樣的儀表,什麼是多級限流裝置呢?答:多級限流流量計其實就是就是孔板流量計,是一款用於流量測量的儀表,主要測量的介質有液體、氣體以及蒸氣,孔板流量計在現場的使用中有著耐高溫高壓的優勢因此在高溫高壓工況中得到了廣泛的應用。
  • 頤和園開啟限流怎麼回事 頤和園開啟限流原因是什麼
    3月22日下午14時17分,頤和園已接近疫情防控期間瞬時接待最大承載量8000人,啟動實施限流措施,減緩入園速度遊客只出不進。頤和園提醒已預約購票及持政策性票證的遊客請耐心等待,並配合工作人員疏導。
  • SpringCloud:分布式系統面臨的問題及解決方案
    分布式系統面臨的問題複雜的分布式系統結構中的應用程式有數10個依賴關係,每個依賴關係在某些時候將不可避免地失敗。對於高流量的應用來說,單一的後端依賴可能會導致所有伺服器上的所有資源都在幾秒鐘內飽和。比失敗更糟糕的是,這些應用程式還可能導致服務之間的延遲增加,線程和其他系統資源緊張,導致整個系統發生更多的級聯故障。這些都表示需要對故障和延遲進行隔離和管理,以便單個依賴關係的失敗,不能取消整個應用程式或系統。
  • 九峰山旅遊區端午節期間預約限流公告,還有...
    九峰山旅遊區端午節期間預約限流公告,還有......:澎湃新聞·澎湃號·政務 端午節期間預約限流公告
  • 限流電阻的設計務必認真,一不小心就會中招!
    LED發光二極體,來源於網絡雅帆以LED限流電阻的設計為例進行說明,有詳細的計算步驟和注意細節,相信看過你會有不同的心得。普通LED發光二極體的特性曲線一、計算限流電阻阻值我們知道LED是一個非線性元件,設計限流電阻時必須了解上面LED特性曲線的第一個曲線圖。那就是它是一個接近2V穩壓值的二極體。不同顏色的LED穩壓值不同,但都在2V左右。通常我們按2V值進行計算。LED工作電流的選取。從第一個曲線圖我們知道它可以工作在0到25mA這個區間。
  • 使用無源和有源限流電路設計太陽能日光燈
    本文提供了使用無源和有源限流電路設計日光燈的完整說明,還演示了如何使用簡單的有源限流電路來顯著提高LED的效率、靈活性和壽命。y3lednc顧名思義,太陽能日光燈僅在白天提供照明。由於它沒有能量存儲,因此製造非常簡單、性價比高、使用壽命長並且幾乎不需要維護。
  • 人民時評:景區可限流,旅遊業當升級
    從今年6月份起,八達嶺長城景區也正式實施全網絡實名制預約售票,並開啟「限流」模式,每日遊客總量控制在6.5萬人次,有效緩解了這一著名旅遊景點的擁擠狀況。在旅遊需求井噴的今天,如何完善限流舉措,值得思考。 每逢節假日,各個旅遊景點人山人海的照片,都會在社交網絡刷屏。對身處其中的遊客來說,無論是觀賞體驗,還是安全指數,都會大打折扣。
  • 上海市民出門掃貨,Costco超市又限流了
    昨天(23日),Costco超市開始實行限流,實時限流1000人。作為全球第二大零售超市巨頭在大陸的首家旗艦店,位於上海閔行區的Costco超市,自開業起就和火爆與限流兩個詞密不可分。2019年8月27日開業第一天,一炮而紅的Costco由於人流量太大,只營業了約5個小時便被緊急叫停。
  • 限流遭集體「聲討」 微博廣告「依賴症」難治
    事情緣起歌手老狼轉發的一條「讀書分享會」博文遭到微博限流。老狼隨後發文控訴稱:「新浪微博演出信息限流,書籍分享會也限流,都要花錢買頭條,窮瘋了吧。」不過,微博客服則對此回應道:「因演出信息中多包含導流外鏈及導購二維碼,所以部分博文被系統判定為營銷內容。」