Feign是什么
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。
测试代码和目的
采用HTTP作为远程调用的协议,在Spring Cloud微服务架构中,是否会成为并发瓶颈;
压测目标:5000 QPS
测试结果:HTTP协议的远程调用,可以达到预期的压测目标,并发瓶颈会下沉到数据库查询;
SpringCloud架构组件
服务配置 | consul
注册发现 | consul
服务熔断 | Resilience4j
服务调用 | openfeign
服务路由 | Gateway
负载均衡 | LoadBalance
测试环境调用路径: Gateway服务 -> Goods服务 -> Warehouse服务
优化之前的压测截图
可以看到feign默认采用的是HttpUrlConnection来作为http客户端,发起远程调用。此时的QPS是2789.40。
优化之后的核心配置
主要看点为max-connections和max-connections-per-route,因为作为网关下面的子服务,所接收到的请求全部来自其他子服务,IP都是每个子服务节点特定的,max-connections-per-route配置的越小,则瓶颈越大。
# 采用apache的httpclient,弃用JDK自带的HttpURLConnection
feign:
httpclient:
enabled: true
# 连接池最大连接数,默认200
max-connections: 51200
# 连接池中存活时间,默认为900秒
time-to-live: 1200
time-to-live-unit: seconds
# 每一个IP最大占用多少连接 默认 50
max-connections-per-route: 10240
# Connects this socket to the server with a specified timeout value. A timeout of zero is interpreted as an infinite timeout.
connection-timeout: 1000
connection-timer-repeat: 3000
# http请求是否允许重定向
follow-redirects: true
效果截图
修改了http客户端,采用apache的httpclien。图中抓取了一个慢查询,读请求耗时3725毫秒,等待warehouse服务请求结果。此时的QPS是5378.96,已经达到了设计目标。
关键源码
private E getPoolEntryBlocking(
final T route, final Object state,
final long timeout, final TimeUnit timeUnit,
final Future<E> future) throws IOException, InterruptedException, ExecutionException, TimeoutException {
Date deadline = null;
if (timeout > 0) {
deadline = new Date (System.currentTimeMillis() + timeUnit.toMillis(timeout));
}
// 加锁,因为后续代码有大量的连接池增删操作
this.lock.lock();
try {
E entry;
for (;;) {
Asserts.check(!this.isShutDown, "Connection pool shut down");
if (future.isCancelled()) {
throw new ExecutionException(operationAborted());
}
final RouteSpecificPool<T, C, E> pool = getPool(route);
for (;;) {
entry = pool.getFree(state);
if (entry == null) {
break;
}
// 如果客户端已经过期,则关闭销毁
if (entry.isExpired(System.currentTimeMillis())) {
entry.close();
}
// 如果客户端已经关闭,则将之从available列表、poo的leased集合中移除
if (entry.isClosed()) {
this.available.remove(entry);
pool.free(entry, false);
} else {
break;
}
}
if (entry != null) {
this.available.remove(entry);
this.leased.add(entry);
onReuse(entry);
return entry;
}
// New connection is needed
final int maxPerRoute = getMax(route);
// Shrink the pool prior to allocating a new connection
final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);
// 如果连接数超过了连接池最大容量,则将最后添加的N个客户端进行销毁
if (excess > 0) {
for (int i = 0; i < excess; i++) {
final E lastUsed = pool.getLastUsed();
if (lastUsed == null) {
break;
}
lastUsed.close();
this.available.remove(lastUsed);
pool.remove(lastUsed);
}
}
// 如果已分配的http客户端小于每个IP最大允许的连接数
if (pool.getAllocatedCount() < maxPerRoute) {
final int totalUsed = this.leased.size();
final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);
// 如果连接数最大连接数还有空余,那么就可以创建新的http客户端
if (freeCapacity > 0) {
final int totalAvailable = this.available.size();
if (totalAvailable > freeCapacity - 1) {
if (!this.available.isEmpty()) {
final E lastUsed = this.available.removeLast();
lastUsed.close();
final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute());
otherpool.remove(lastUsed);
}
}
final C conn = this.connFactory.create(route);
// pool.add方法会返回一个PoolEntry类,里面定义了生存时间和执行次数
entry = pool.add(conn);
// 新客户端标记为使用中,添加到Set集合中
this.leased.add(entry);
return entry;
}
}
boolean success = false;
try {
pool.queue(future);
this.pending.add(future);
if (deadline != null) {
success = this.condition.awaitUntil(deadline);
} else {
this.condition.await();
success = true;
}
if (future.isCancelled()) {
throw new ExecutionException(operationAborted());
}
} finally {
pool.unqueue(future);
this.pending.remove(future);
}
if (!success && (deadline != null && deadline.getTime() <= System.currentTimeMillis())) {
break;
}
}
throw new TimeoutException("Timeout waiting for connection");
} finally {
this.lock.unlock();
}
}
源码介绍
org.apache.http.pool. AbstractConnPool,第310行的getPoolEntryBlocking()方法.
可以看到方法中有非公平锁,最大程度上保证连接数不会超过预定的阀值,但是也付出了一定的性能代价。
执行过程:
- 遍历连接池中的链接,如果过期,关闭;如果关闭;从avilable队列中移除;
- 遍历完成以后,如果有空闲链接,从avilable队列中移除,添加进leased队列,并标记为复用;
- 读取配置,一个route最多运行多少链接,final int maxPerRoute = getMax(route);如果已分配的链接超过最大链接,则把最后一个可用链接注销掉;
- 如果已分配链接小于一个route最大连接数,并且当前总共最大连接数没有超出配置maxTotal,则创建新链接;
- 如果当前没有新链接的配额,则等待,直到超时,返回异常;