webService接口限流
近期公司有个webservice接口在高并被调用时发生了read time out一场,经过一番查阅发现这是由于jdk低版本jdk1.7.0_51版本之下,解析soap协议的接口时创建XmlReader存在数量限制(默认64000),并且创建时不会进行重置,引起的read time out,官方给出的建议时升级到1.7.0_51(参考链接:https://bugs.openjdk.java.net/browse/JDK-8028111).
废话不多说,先上测试对比图.
这事没加限流前的测试图,39个请求失败
这是加了限流后的测试结果,7个请求失败
测试结果与运行系统的性能相关(我自己的电脑快卡爆了,tps连1都没有,之前测试的50并发线程,3000个请求,全部成功).由于机器性能可能不一样,加了限流队列的配置文件,实际使用中可根据机器的性能做测试,使用合适的大小.
下面是代码.
1.web.xml
做限流的时候考虑到webservice接口和controller中的接口分别由webservice容器和spring Ioc容器托管,所以使用了过滤器,需要在web.xml中配置,代码如下:
<filter-name>limitFilter</filter-name>
<!-- 过滤器文件包路径 -->
<filter-class>com.shcmcc.poms.limit.filter.LimitFilter</filter-class>
<init-param>
<param-name>limitConfigLocation</param-name>
<!-- 配置文件位置 -->
<param-value>classpath:./etc/limit.properties</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>limitFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2.相关代码
2.1 Limit.java
import java.lang.annotation.*;
/**
* @author MQ
* @desc 限流注解
* limitPath 限流的url
* limitCount 限流队列的大小
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Limit {
String limitPath() default "";
int limitCount() default 0;
}
2.2PropertiesLoader.java
import org.apache.commons.lang.StringUtils;
import java.io.*;
import java.util.Properties;
/**
* @author MQ
* @desc 配置文件加载器,读取.properties文件
*/
public class PropertiesLoader {
public static Properties loadProperties(String filePath) throws IOException {
// Construct BufferedReader from FileReader
BufferedReader br = new BufferedReader(new FileReader(new File(filePath)));
String line = null;
Properties properties = new Properties();
// 循环读取行内容
while ((line = br.readLine()) != null) {
line = new String(line.getBytes("utf-8"));
// 跳过注释
if (!line.startsWith("#") && StringUtils.isNotBlank(line.trim())) {
String[] keyValue = line.split("=");
properties.setProperty(keyValue[0].trim(), keyValue[1].trim());
}
}
br.close();
return properties;
}
2.3limit.properties
#包名,数组,以'|'号分隔
scanPackage=com.shcmcc.poms.webservice
#接口默认队列限制数量
defalutLimitCount=5
2.4 LimitDefinition.java
import org.javatuples.Pair;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @author MQ
* @desc 限流定义器
*/
public class LimitDefinition {
// 请求队列的大小
protected int limit = 10;
// 使用了阻塞队列,当请求队列满的时候,阻塞当前请求
// LinkedBlockingQueue提供了阻塞/非阻塞的存/取方法,相关方法
/**
* take():首选。当队列为空时阻塞
* poll():弹出队顶元素,队列为空时,返回空
* peek():和poll烈性,返回队队顶元素,但顶元素不弹出。队列为空时返回null
* remove(Object o):移除某个元素,队列为空时抛出异常。成功移除返回true
*
* 添加数据
* put():首选。队满是阻塞
* offer():队满时返回false
*/
protected LinkedBlockingQueue<Pair<ServletRequest, ServletResponse>> requestQueue;
public LimitDefinition(int limit) {
this.limit = limit;
this.requestQueue = new LinkedBlockingQueue<Pair<ServletRequest, ServletResponse>>(limit);
}
public int getLimit() {
return limit;
}
public void setLimit(int limit) {
this.limit = limit;
}
public LinkedBlockingQueue<Pair<ServletRequest, ServletResponse>> pollRequest() {
return requestQueue;
}
// 将请求放入队列的时候执行doFilter()方法处理请求
public void putRequest(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException, InterruptedException {
this.requestQueue.put(new Pair<ServletRequest, ServletResponse>(req, resp));
// 只要队列中存在请求,就遍历并处理请求
while (requestQueue.size() > 0) {
Pair<ServletRequest, ServletResponse> polled = requestQueue.poll();
doFilter(polled.getValue0(), polled.getValue1(), chain);
}
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws ServletException, IOException {
chain.doFilter(req, resp);
}
}
上面的是辅助用的一些java类和配置文件,注释已加好
3.初始化及调用
3.1初始化容器
3.1.1过滤器中初始化
自定义过滤器需要实现javax.servlet.Filter接口,需要重写并实现init()初始化
、doFilter()过滤请求
、destroy()销毁
方法,因此初始化filter容器选择放在init()方法中,代码如下
// 请求队列定义Map,key为接口路径,value为接口的限流参数(LimitDefinition)
private Map<String, LimitDefinition> requestQueueDefinitionCache = new HashMap<String, LimitDefinition>();
@Override
public void init(FilterConfig filterConfig) {
logger.info("==================================================================init limit filter=======================================================");
// 从webservice容器中直接获取代理的接口信息
delegate = (WSServletDelegate) filterConfig.getServletContext().getAttribute(WSServlet.JAXWS_RI_RUNTIME_INFO);
// 将webservice代理和filterConfig一同传入初始化方法
initLimitDefinition(delegate, filterConfig);
}
// 初始化容器
private void initLimitDefinition(WSServletDelegate delegate, FilterConfig config) {
// 扫描并注册webservice容器注册好的路径,给定limit默认值
if (delegate != null) {
Properties properties = null;
try {
// 读取配置文件的配置信息(包名,请求队列默认大小)
properties = PropertiesLoader.loadProperties(getConfigPath(config));
} catch (IOException e) {
e.printStackTrace();
logger.error(e.getMessage(),e);
}
if (properties != null) {
// 获取请求队列默认大小值
String defalutLimitCount = properties.getProperty("defalutLimitCount");
if (StringUtils.isNotBlank(defalutLimitCount)) {
// 获取webservice代理接口的信息
List<ServletAdapter> adapters = delegate.adapters;
for (ServletAdapter adapter : adapters) {
String requestPath = adapter.getValidPath();
if (requestQueueDefinitionCache.get(requestPath) == null) {
// 将接口信息放入请求定义队列中
requestQueueDefinitionCache.put(requestPath, new LimitDefinition(new Integer(defalutLimitCount)));
}
}
}
}
}
// LimitReader扫描包并读取接口上的注解参数
LimitReader limitReader = new LimitReader(config);
// 将扫描的数据全部放入缓存
requestQueueDefinitionCache.putAll(limitReader.getLimitDefinitionCache());
}
private String getConfigPath(FilterConfig config) {
// 获取配置文件路径
String configLocation = config.getInitParameter("limitConfigLocation")
.replace("classpath:", "").trim()
.replace("./", "/");
String realFilePath = this.getClass().getResource(configLocation).getFile();
return realFilePath;
}
filter中仅对webservice接口做了初始化,controller中的接口在LimitReader中初始化
3.1.2扫描controller接口并注册到限流容器
由于考虑到后期可能增加其他的功能,所以将扫包的代码抽取出来,放在了LimitReader中,这样便于以后维护(修补bug)或增加新功能,废话不多说,上代码
import javax.servlet.FilterConfig;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import com.shcmcc.poms.limit.annotation.Limit;
/**
* @author MQ
* @desc 配置读取器
*/
public class LimitReader {
private Map<String, LimitDefinition> requestQueueDefinitionCache = new HashMap<String, LimitDefinition>();
public LimitReader(FilterConfig config) {
Properties properties = loadProperties(config);
// 读取包名
String scanPackageStr = properties.getProperty("scanPackage");
if (StringUtils.isBlank(scanPackageStr)) return;
String[] packageUrls = scanPackageStr.replaceAll("\\.", "/").split("\\|");
try {
// 扫包(骚包?)
doScan(packageUrls);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
// 这一块代码是为了读取使用了@Limit注解的接口需要配置的信息,并注册到限流容器
private void doScan(String[] scanPackages) throws FileNotFoundException {
// 遍历包名数组
for (int i = 0; i < scanPackages.length; i++) {
String scanPackage = scanPackages[i].trim();
URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.","/"));
// 将包路径转换成url,作为根目录
File classPath = new File(url.getFile());
if (!classPath.exists()) throw new FileNotFoundException("scanPackage is invalid");
//当成是一个ClassPath文件夹
for (File file : classPath.listFiles()) {
// 如果file是文件夹,则递归继续扫包
if(file.isDirectory()){
doScan(new String[]{scanPackage + "." + file.getName()});
}else {
// 如果不是文件夹,需要判断是否.class文件,否则Class.forName()会报错
if(!file.getName().endsWith(".class")){continue;}
//全类名 = 包名.类名
String className = (scanPackage + "." + file.getName().replace(".class", "")).replaceAll("/",".");
try {
// 加载扫描到的class文件
Class clazz = Class.forName(className);
readLimitAnnotation(clazz);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
}
// 这里读取方法上的Limit注解
private void readLimitAnnotation(Class clazz) {
// 获取并遍历Class中定义的所有方法
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
// 判断是否使用了Limit注解
if (!method.isAnnotationPresent(Limit.class)) continue;
Limit limit = method.getAnnotation(Limit.class);
// 读取注解上的配置数据
String limitPath = limit.limitPath();
int limitCount = limit.limitCount();
// 放入缓存,配置文件中存在为默认队列大小,这里允许覆盖
requestQueueDefinitionCache.put(limitPath, new LimitDefinition(limitCount));
}
}
public Map<String, LimitDefinition> getLimitDefinitionCache() {
return this.requestQueueDefinitionCache;
}
private Properties loadProperties(FilterConfig config) {
// 获取配置文件路径
String configLocation = config.getInitParameter("limitConfigLocation")
.replace("classpath:", "").trim()
.replace("./", "/");
String realFilePath = this.getClass().getResource(configLocation).getFile();
Properties properties = null;
try {
properties = PropertiesLoader.loadProperties(realFilePath);
} catch (IOException e) {
e.printStackTrace();
}
return properties;
// jdk1.8可以直接使用这种方式进行文件内容读取
// Properties properties = new Properties(FileReader);
}
}
3.2阻塞请求
对于请求的阻塞在LimitDefinition.putRequest()方法中执行,使用的是LinkedBlockingQueue.put()方法的阻塞机制,当队列满的时候会阻塞当前线程,
而当队列中存在请求未处理时,就会循环遍历请求队列处理请求,取请求的方法使用的是LinkedBlockingQueue.poll()方法,该方法非阻塞,但是LinkedBlockingQueue中的元素都会单独加锁,所以不会存在线程安全问题
4使用方法
webservice接口在filter容器初始化时自动加载了.基于springmvc的接口只需要在方法上加Limit注解,并定义好相关参数就可以做到接口限流
@Controller
@RequestMapping("controller")
public class controller {
@Limit(limitPath="/itemProject/method", limitCount = 10)
@RequestMapping("method")
public void method() {
// execute xxx
}
}