Introduction
在設計web應用程式時,有時會需要一個節流器,去幫我控制單位時間內能處理的請求數量,以避免過載;又或者是要根據不同使用者所買的授權,去控制單位時間內能呼叫的API次數等。Camel提供了Throttler,讓我們能輕鬆透過設定,去達到這些效果。
我將透過HTTP GET請求/events/{id}做為範例,說明如何使用Throttler。首先介紹這個範例中的兩個RouteBuilder。
(程式碼可參考link)
RestRouteBuilder
REST核心設定集中在這個builder中,它負責宣告用什麼port與component去建立REST服務:
package org.tonylin.practice.camel.rest; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.model.rest.RestBindingMode; public class RestRouteBuilder extends RouteBuilder { @Override public void configure() throws Exception { restConfiguration().component("netty4-http").port(8080).bindingMode(RestBindingMode.auto).endpointProperty("ssl", "false"); } }
(我以http當範例,如果對https用法有興趣,可以參考這篇)
ThrottlerRouteBuilder
接下來是今天的主角,我先列出程式碼內容,後面再針對重點configure做說明:
package org.tonylin.practice.camel.throttler; import static com.google.common.base.Preconditions.checkState; import org.apache.camel.Exchange; import org.apache.camel.Processor; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.processor.ThrottlerRejectedExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ThrottlerRouteBuilder extends RouteBuilder { private static Logger logger = LoggerFactory.getLogger(ThrottlerRouteBuilder.class); private final static String GET_EVENTS = "GET_EVENTS"; private Object eventHandler; private int limit = 2; private int period = 200; public ThrottlerRouteBuilder(Object eventHandler) { this.eventHandler = eventHandler; } public void setLimit(int limit) { this.limit = limit; } public void setPeriod(int period) { this.period = period; } @Override public void configure() throws Exception { checkState(eventHandler!=null, "Can't find eventHandler"); onException(ThrottlerRejectedExecutionException.class) .process(new Processor() { @Override public void process(Exchange exchange) throws Exception { logger.debug("handle ThrottlerRejectedExecutionException"); exchange.getOut().setHeader(Exchange.HTTP_RESPONSE_CODE, "503"); } }) .handled(true); rest("/events/{id}").get().route().id(GET_EVENTS) .throttle(limit) .timePeriodMillis(period) .rejectExecution(true) .bean(eventHandler).endRest(); } }
我首要說明的是throttler的configure:
rest("/events/{id}").get().route().id(GET_EVENTS) .throttle(limit) .timePeriodMillis(period) .rejectExecution(true) .bean(eventHandler).endRest();
除了HTTP GET的宣告外,這些程式碼代表著以下意義:
- throttle(limit): 限制的存取次數。
- timePeriodMillis(period): 限制存取次數的單位時間,預設是1000ms。
- rejectExecution(true): 當超過此限制時,是否要reject請求,預設為false。假如沒reject,後續超過限制的請求會block至單位時間後執行。
- bean(eventHandler): 請求的處理者。
在我設定rejectExecution為true後,我發現camel會拋出ThrottlerRejectedExecutionException,且client會block住;因此這個設定必須與camel的errorHandler一同使用,這是我的使用範例:
onException(ThrottlerRejectedExecutionException.class) .process(new Processor() { @Override public void process(Exchange exchange) throws Exception { logger.debug("handle ThrottlerRejectedExecutionException"); exchange.getOut().setHeader(Exchange.HTTP_RESPONSE_CODE, "503"); } }) .handled(true);
我宣告當發生ThrottlerRejectedExecutionException例外時,會將回應給client的response code設為503以代表server過載。除此之外,別忘記把handled設為true,代表例外已被處理。
Unit Test
最後我以單元測試來展示效果,包含testOverload與testThrottlePeriod兩個測試;而throttler的limit為2,period為200ms,testcase會在後面做說明:
package org.tonylin.practice.camel.throttler; import java.util.ArrayList; import java.util.List; import org.apache.camel.RoutesBuilder; import org.apache.camel.test.junit4.CamelTestSupport; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.HttpClientBuilder; import org.junit.Test; import org.tonylin.practice.camel.rest.RestHandler; import org.tonylin.practice.camel.rest.RestRouteBuilder; public class ThrottlerRouteBuilderTest extends CamelTestSupport { private RestHandler hander = new RestHandler(); private static final int limit = 2; private static final int period = 200; private HttpClient client = HttpClientBuilder.create().build(); private HttpGet httpGet = new HttpGet("http://localhost:8080/events/123"); @Override protected RoutesBuilder[] createRouteBuilders() throws Exception { ThrottlerRouteBuilder throttlerRouteBuilder = new ThrottlerRouteBuilder(hander); throttlerRouteBuilder.setLimit(limit); throttlerRouteBuilder.setPeriod(period); return new RoutesBuilder[] { new RestRouteBuilder(), throttlerRouteBuilder }; } private List<HttpResponse> batchRequest(int times) throws Exception { List<HttpResponse> responses = new ArrayList<HttpResponse>(); for( int i = 0 ; i < times ; i++ ) { responses.add(client.execute(httpGet)); } return responses; } @Test public void testOverload() throws Exception { // skip } @Test public void testThrottlePeriod() throws Exception { // skip } }
測試中使用的RestHandler,負責收集請求的event id,用以確認請求內容是否正確:
public class RestHandler { private static Logger logger = LoggerFactory.getLogger(RestHandler.class); private List<String> requestIds = new ArrayList<String>(); @Handler public void handle(Exchange exchange) { String requestId = exchange.getIn().getHeader("id", String.class); logger.debug("Request id: {}", requestId); requestIds.add(requestId); } public List<String> getRequestIds(){ return requestIds; } }
針對testOverload測試,是用來確認throttler單位時間內的請求是否有效;因此測試中,連續做了3次請求,最後會去確認這三次的請求結果是否正確:
@Test public void testOverload() throws Exception { // when request 3 times List<HttpResponse> responses = batchRequest(3); // then assertEquals(2, hander.getRequestIds().size()); assertEquals(200, responses.get(0).getStatusLine().getStatusCode()); assertEquals(200, responses.get(1).getStatusLine().getStatusCode()); assertEquals(503, responses.get(2).getStatusLine().getStatusCode()); }
而testThrottlePeriod測試,用以確認throttler單位時間是否有作用;因此測試中,會先發3次請求,再等待此單位時間後,再發3次請求。最後確認請求結果:
@Test public void testThrottlePeriod() throws Exception { // when List<HttpResponse> responses = batchRequest(3); Thread.sleep(period+1); responses.addAll(batchRequest(3)); // then assertEquals(2, hander.getRequestIds().size()); assertEquals(200, responses.get(0).getStatusLine().getStatusCode()); assertEquals(200, responses.get(1).getStatusLine().getStatusCode()); assertEquals(503, responses.get(2).getStatusLine().getStatusCode()); assertEquals(200, responses.get(3).getStatusLine().getStatusCode()); assertEquals(200, responses.get(4).getStatusLine().getStatusCode()); assertEquals(503, responses.get(5).getStatusLine().getStatusCode()); }
透過這兩個測試範例,我們可以簡單地了解throttler的用法。
Library Info (Gradle Config)
以下是我在寫這篇文章時,所使用的libraries版本:
ext { camelVersion='2.23.1' nettyAllVersion='4.1.34.Final' guavaVersion='27.1-jre' log4jVersion='1.2.17' slf4jVersion='1.7.26' httpClientVersion='4.5.7' } dependencies { compile group: 'org.apache.camel', name: 'camel-core', version: "$camelVersion" compile group: 'org.apache.camel', name: 'camel-netty4-http', version: "$camelVersion" compile group: 'org.apache.camel', name: 'camel-http-common', version: "$camelVersion" compile group: 'org.apache.camel', name: 'camel-netty4', version: "$camelVersion" compile group: 'io.netty', name: 'netty-all', version: "$nettyAllVersion" compile group: 'com.google.guava', name: 'guava', version: "$guavaVersion" compile group: 'log4j', name: 'log4j', version: "$log4jVersion" compile group: 'org.slf4j', name: 'slf4j-api', version: "$slf4jVersion" runtime group: 'org.slf4j', name: 'slf4j-log4j12', version: "$slf4jVersion" testCompile group: 'org.apache.camel', name: 'camel-test', version: "$camelVersion" testCompile group: 'org.apache.httpcomponents', name: 'httpclient', version: "$httpClientVersion" testCompile 'junit:junit:4.12' }
留言
張貼留言