基本API速率限制
您可能正在開發某種形式的(Web / RESTful)API,并且如果它是面向公眾的(甚至是內部的),通常您希望以某種方式對其進行速率限制。 即,限制一段時間內執行的請求數,以節省資源并防止濫用。
這可能可以通過一些聰明的配置在Web服務器/負載均衡器級別上實現,但是通常您希望速率限制器是特定于客戶端的(即,API的每個客戶端都應具有單獨的速率限制),以及客戶端的方式被確定是不同的。 可能仍然可以在負載均衡器上執行此操作,但是我認為將其放在應用程序級別上是有意義的。
我將使用spring-mvc作為示例,但是任何Web框架都有插入攔截器的好方法。
因此,這是一個spring-mvc攔截器的示例:
@Component public class RateLimitingInterceptor extends HandlerInterceptorAdapter {private static final Logger logger = LoggerFactory.getLogger(RateLimitingInterceptor.class);@Value("${rate.limit.enabled}")private boolean enabled;@Value("${rate.limit.hourly.limit}")private int hourlyLimit;private Map<String, Optional<SimpleRateLimiter>> limiters = new ConcurrentHashMap<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {if (!enabled) {return true;}String clientId = request.getHeader("Client-Id");// let non-API requests passif (clientId == null) {return true;}SimpleRateLimiter rateLimiter = getRateLimiter(clientId);boolean allowRequest = limiter.tryAcquire();if (!allowRequest) {response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());}response.addHeader("X-RateLimit-Limit", String.valueOf(hourlyLimit));return allowRequest;}private SimpleRateLimiter getRateLimiter(String clientId) {if (limiters.containsKey(clientId)) {return limiters.get(clientId);} else {synchronized(clientId.intern()) {// double-checked locking to avoid multiple-reinitializationsif (limiters.containsKey(clientId)) {return limiters.get(clientId);}SimpleRateLimiter rateLimiter = createRateLimiter(clientId);limiters.put(clientId, rateLimiter);return rateLimiter;}}}@PreDestroypublic void destroy() {// loop and finalize all limiters} }這將按需初始化每個客戶端的速率限制器。 另外,在啟動時,您可以遍歷所有已注冊的API客戶端,并為每個客戶端創建一個速率限制器。 如果速率限制器不允許更多請求(tryAcquire()返回false),則取消“ Too many requests”(太多請求)并中止請求的執行(從攔截器返回“ false”)。
這聽起來很簡單。 但是有一些問題。 您可能想知道上面的SimpleRateLimiter在哪里定義。 我們將到達那里,但首先讓我們看看我們對速率限制器實現有哪些選擇。
最受歡迎的似乎是番石榴RateLimiter 。 它具有簡單的工廠方法,可為您提供指定速率(每秒允許)的速率限制器。 但是,它不能很好地適應Web API,因為您無法使用預先存在的許可數量初始化RateLimiter。 這意味著在限制器允許請求之前,應經過一段時間。 還有另一個問題–如果您每秒的許可數量少于一個(例如,如果您希望的速率限制為“每小時200個請求”),則可以傳遞一個分數(hourlyLimit / secondsInHour),但仍然無法達到您的目的可以預期,因為內部有一個“ maxPermits”字段,可以將許可證數量的上限限制為比您想要的要少得多。 此外,速率限制器不允許突發-您每秒恰好有X個許可,但您不能長時間分散它們,例如在一秒鐘內有5個請求,然后在接下來的幾秒鐘內沒有請求。 實際上,上述所有問題都可以解決,但遺憾的是,可以通過您無法訪問的隱藏字段來解決。 多年來存在多個功能請求,但是Guava不會更新速率限制器,從而使其不適用于API速率限制。
使用反射,您可以調整參數并使限制器工作。 但是,這很丑陋,并且不能保證它會按預期工作。 我在這里展示了如何使用每小時X許可,爆破性和完整的初始許可來初始化番石榴速率限制器。 當我認為這樣做的時候,我看到tryAcquire()有一塊tryAcquire() synchronized(..)塊。 這是否意味著在簡單地檢查是否允許發出請求時,所有請求都會彼此等待? 那太可怕了。
因此,實際上番石榴RateLimiter并不旨在用于(網絡)API速率限制。 也許保持功能貧乏是Guava勸阻人們不要濫用它的方法嗎?
這就是為什么我決定根據Java信號量自己實現一些簡單的事情。 這是樸素的實現 :
public class SimpleRateLimiter {private Semaphore semaphore;private int maxPermits;private TimeUnit timePeriod;private ScheduledExecutorService scheduler;public static SimpleRateLimiter create(int permits, TimeUnit timePeriod) {SimpleRateLimiter limiter = new SimpleRateLimiter(permits, timePeriod);limiter.schedulePermitReplenishment();return limiter;}private SimpleRateLimiter(int permits, TimeUnit timePeriod) {this.semaphore = new Semaphore(permits);this.maxPermits = permits;this.timePeriod = timePeriod;}public boolean tryAcquire() {return semaphore.tryAcquire();}public void stop() {scheduler.shutdownNow();}public void schedulePermitReplenishment() {scheduler = Executors.newScheduledThreadPool(1);scheduler.schedule(() -> {semaphore.release(maxPermits - semaphore.availablePermits());}, 1, timePeriod);} }它需要一定數量的許可(允許的請求數量)和一段時間。 時間段為“ 1 X”,其中X可以是每秒/分鐘/小時/每天-取決于您希望如何配置限制-每秒,每分鐘,每小時,每天。 調度程序每1 X補充所獲得的許可證。 無法控制突發事件(客戶端可以用快速連續的請求來花費所有許可),沒有熱身功能,沒有逐步的補充。 根據您的需要,這可能并不理想,但這只是一個基本的速率限制器,它是線程安全的,沒有任何阻塞。 我編寫了一個單元測試,以確認限制器的行為正確,并且還對本地應用程序進行了性能測試,以確保遵守限制。 到目前為止,它似乎正在工作。
有其他選擇嗎? 好吧,是的–像RateLimitJ這樣的庫使用Redis來實現速率限制。 但是,這意味著您需要設置和運行Redis。 對于“簡單地”進行限速似乎是開銷。
另一方面,限速將如何在一組應用程序節點中正常工作? 應用程序節點可能需要一些數據庫或八卦協議來共享有關剩余的每個客戶端許可(請求)的數據? 不必要。 解決此問題的一種非常簡單的方法是假設負載平衡器在節點之間平均分配負載。 這樣,您只需要將每個節點上的限制設置為等于總限制除以節點數即可。 它不是很精確,但是您很少需要做到這一點–允許5-10個以上的請求不會終止您的應用程序,允許5-10個以下的請求對用戶來說并不算太大。
但是,那將意味著您必須知道應用程序節點的數量。 如果您使用自動縮放(例如在AWS中),則節點數可能會根據負載而變化。 在這種情況下,通過調用AWS(或其他云提供商)API來獲取節點中的節點數,而不是配置硬編碼的許可數,補給排定的作業可以即時計算“ maxPermits”。當前的自動縮放組。 這比僅支持Redis部署要簡單得多。
總的來說,我很驚訝沒有一種“規范的”方式來實現速率限制(在Java中)。 也許限制速率的需求并不像看起來那樣普遍。 或者,它是手動實施的-通過暫時禁止使用“資源過多”的API客戶端。
翻譯自: https://www.javacodegeeks.com/2017/07/basic-api-rate-limiting.html
總結
- 上一篇: 电脑oem是什么硬盘(电脑磁盘oem是什
- 下一篇: junit 测试 异常_使用JUnit规