OkHttp3 source code details okhttp connection pool reuse mechanism

Keywords: Operation & Maintenance socket network OkHttp less

1. Overview

To improve network performance optimization, it is very important to reduce latency and improve response speed.

Usually, when we make a request in the browser, the header part is like this

Keep alive is to keep a long connection between the browser and the server, which can be reused. It is enabled by default in HTTP 1.1.

Why does connection reuse improve performance?
Generally, when we initiate http request, we need to complete three handshakes of tcp first, then transmit data, and finally release the connection. The process of three handshakes can be referred to Here TCP three handshake details and release the connection process

The process of a response

In the case of high concurrent requests or frequent requests from the same client, unlimited creation will lead to poor performance.

If you use keep alive

In the timeout idle time, the connection will not be closed. The same repeated request will reuse the original connection, reduce the number of handshakes, and greatly improve the efficiency.

It is not that the longer the timeout setting time of keep alive is, the better the performance will be. Too many zombie connections and leaky connections will appear if it is not closed for a long time.

So okttp on the client is similar to the keep alive mechanism that the client does.

2. Use of connection pool

The class of connection pool is located in okhttp3.ConnectionPool. Our main idea is to understand how to reuse the connection in the timeout time, and effectively recycle and clean it.

Its member variable code slice

/**
 * Background threads are used to cleanup expired connections. There will be at most a single
 * thread running per connection pool. The thread pool executor permits the pool itself to be
 * garbage collected.
   */
  private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));

  /** The maximum number of idle connections for each address. */
  private final int maxIdleConnections;

  private final Deque<RealConnection> connections = new ArrayDeque<>();
  final RouteDatabase routeDatabase = new RouteDatabase();
  boolean cleanupRunning;

Executor: a thread pool used to detect and clean up idle socket s.
Connections: connection cache pool. Deque is a two terminal list, which supports inserting elements at the beginning and the end. It is used as LIFO stack, mostly for caching data.
routeDatabase: used to record connection failure router

2.1 cache operation

ConnectionPool provides operation methods for Deque, including put, get, connectionBecameIdle and evictAll. Corresponding to put connection, get connection, remove connection and remove all connection operations respectively.

put operation

void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }

You can see the threads that clean up idle connections before new connections are placed in the list.

Since it's multiplexing, let's see how he gets the connection.

/** Returns a recycled connection to {@code address}, or null if no such connection exists. */
RealConnection get(Address address, StreamAllocation streamAllocation) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.allocations.size() < connection.allocationLimit
          && address.equals(connection.route().address)
          && !connection.noNewStreams) {
        streamAllocation.acquire(connection);
        return connection;
      }
    }
    return null;
 }

Traverse the connections cache list, when the number of times a connection count is less than the limit size, and the address of the request exactly matches the address of the connection in the cache list. Then directly reuse the connection in the cache list as the connection of request.

Streamlocation.allocations is an object counter, whose essence is a list > stored in the RealConnection Connection connection object to record the activity of the Connection.

The connection cache in the connection pool is relatively simple, which is to use a two terminal list to cooperate with CRD and other operations. If the connection class fails in timeout time, and if the connection is cleared effectively to ensure sufficient performance and memory space.

2.2 cleaning and recycling of connection pool

When looking at the member variables of ConnectionPool, we learned that the thread pool of an Executor is used to clean up idle connections. This is explained in the note:

Background threads are used to cleanup expired connections

When we put a new connection to the queue, we will first execute the thread cleaning up the idle connection. It is the executor.execute(cleanupRunnable); method that is called. Observe cleanupRunnable

private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
      while (true) {
        long waitNanos = cleanup(System.nanoTime());
        if (waitNanos == -1) return;
        if (waitNanos > 0) {
          long waitMillis = waitNanos / 1000000L;
          waitNanos -= (waitMillis * 1000000L);
          synchronized (ConnectionPool.this) {
            try {
              ConnectionPool.this.wait(waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
            }
          }
        }
      }
    }
  };

The action of Cleanup is called continuously in the thread and the interval time of next Cleanup is returned immediately. Then enter wait and release the lock to continue the next cleaning. So it might be understood that it is a background thread that monitors time and releases connections.

Understand the process of the cleanup action. Here is how to clean up the so-called idle connections and lines. How to find the idle connection is the main problem.

long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    // Find either a connection to evict, or the time that the next eviction is due.
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        // If the connection is in use, keep searching.
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

        // If the connection is ready to be evicted, we're done.
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        // We've found a connection to evict. Remove it from the list, then close it below (outside
        // of the synchronized block).
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // All connections are in use. It'll be at least the keep alive duration 'til we run again.
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
  }

In the process of traversing the cache list, the count accumulation value of the number of connections inUseConnectionCount and the number of idle connections idleconnection count are controlled by whether the pruneAndGetAllocationCount() is greater than 0. It is obvious that the pruneAndGetAllocationCount() method is used to identify whether the corresponding connection is idle. >0 is not idle. Otherwise, it's an idle connection.

Go in and observe

private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    List<Reference<StreamAllocation>> references = connection.allocations;
    for (int i = 0; i < references.size(); ) {
      Reference<StreamAllocation> reference = references.get(i);

      if (reference.get() != null) {
        i++;
        continue;
      }

      // We've discovered a leaked allocation. This is an application bug.
      Platform.get().log(WARN, "A connection to " + connection.route().address().url()
          + " was leaked. Did you forget to close a response body?", null);
      references.remove(i);
      connection.noNewStreams = true;

      // If this was the last allocation, the connection is eligible for immediate eviction.
      if (references.isEmpty()) {
        connection.idleAtNanos = now - keepAliveDurationNs;
        return 0;
      }
    }

    return references.size();
  }
}

Well, the allocations, which were originally stored in the real connection, come in handy. Traverse the streamalallocation weak reference list, remove the empty reference, and return the number of weak references in the list after traversing. So it can be seen that list > is a record of connection activity, where > 0 indicates active = 0 indicates idle. The number of streamalallocations in the list is the number of times the physical socket has been referenced

Explanation: streamalallocation is repeatedly performed by the high level to acquire and release. These two functions are actually changing the size of list > in Connection.

After finding the idle connection operation, we return to the operation of cleanup. After calculating inUseConnectionCount and idleconnection count, the program selects and sorts connections according to idle time. The core of selection and sorting is:

// If the connection is ready to be evicted, we're done.
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }
    ....

The connection with the longest idle time can be easily found by comparing the maximum idle time selection and sorting. So we can remove this useless connection!

if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it below (outside
// of the synchronized block).
connections.remove(longestIdleConnection);
}

Summary: the core of cleaning up idle connections is mainly the reference counter list > and the algorithm of sorting selection, as well as the cleaning thread pool of the extractor.
Original link https://www.cnblogs.com/ganchuanpu/p/9408081.html

Posted by tempa on Mon, 18 Nov 2019 02:01:01 -0800