Spring-Feign性能瓶颈调查

590 阅读4分钟

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
image.png
image.png

优化之后的核心配置

主要看点为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,已经达到了设计目标。
image.png
image.png

关键源码

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()方法.
可以看到方法中有非公平锁,最大程度上保证连接数不会超过预定的阀值,但是也付出了一定的性能代价。
执行过程:

  1. 遍历连接池中的链接,如果过期,关闭;如果关闭;从avilable队列中移除;
  2. 遍历完成以后,如果有空闲链接,从avilable队列中移除,添加进leased队列,并标记为复用;
  3. 读取配置,一个route最多运行多少链接,final int maxPerRoute = getMax(route);如果已分配的链接超过最大链接,则把最后一个可用链接注销掉;
  4. 如果已分配链接小于一个route最大连接数,并且当前总共最大连接数没有超出配置maxTotal,则创建新链接;
  5. 如果当前没有新链接的配额,则等待,直到超时,返回异常;