Kubernates(k8s) 이해하기

Understanding kubernates(k8s)

Posted by dydtjr1128 on November 30, 2021 · 11 mins read Kubernates

Connection 관리

HikariCP는 자체 자료구조 ConcurrentBag를 사용하여 Connection을 관리한다. PoolEntry는 IConcurrentBagEntry의 구현체로 Connection에 대한 상태를 정의하고, 이를 이용해 동시성 관리를 해준다.

public interface IConcurrentBagEntry
{
  int STATE_NOT_IN_USE = 0;
  int STATE_IN_USE = 1;
  int STATE_REMOVED = -1;
  int STATE_RESERVED = -2;

  boolean compareAndSet(int expectState, int newState);
  void setState(int newState);
  int getState();
}

ConcurrentBag의 borrow 메소드를 통해 Connection을 얻을 수 있다.

borrow method

커넥션을 ConcurrentBag에서 꺼내준다. 효율적인 동시성 제어(임계영역 진입 최소화)를 위해 쓰레드로컬을 이용하여 캐시된 BagEntry를 우선적으로 사용한다. 쓰레드로컬로부터 Connection 을 얻지 못할 경우 공유자원에서 사용 가능한 Connection을 확인하고, 사용가능한 자원이 없는 경우 handoffQueue를 활용하여 지정된 timeout 시간 동안 다른 쓰레드로 부터 Connection을 넘겨받기를 기다린다. 코드에서 효율적인 동시성제어를 위한 고민이 보이는 것 같다.

requite method

waiters수만큼 루프를 돌면서 handoffQueue가 꽉차거나 사용중이면 반환을 하며 ThreadLocal에서 관리하는 리스트에 커넥션을 반환하는 역할을 하는 메서드이다.

ProxyConnection

ProxyConnection은 close 호출 시 Connection을 풀에 반환하도록 오버라이딩 되어있다. 그래서 풀에서 받은 Connection 타입을 close 메소드를 호출 시 dynamic binding으로 ProxyConnection 의 close 메소드가 호출된다. 결국 DB와의 연결 해제가 아닌 Pool에 반환한다.

public final void close() throws SQLException {
        this.closeStatements();
        if (this.delegate != ProxyConnection.ClosedConnection.CLOSED_CONNECTION) {
            this.leakTask.cancel();

            try {
                if (this.isCommitStateDirty && !this.isAutoCommit) {
                    this.delegate.rollback();
                    this.lastAccess = ClockSource.currentTime();
                    LOGGER.debug("{} - Executed rollback on connection {} due to dirty commit state on close().", this.poolEntry.getPoolName(), this.delegate);
                }

                if (this.dirtyBits != 0) {
                    this.poolEntry.resetConnectionState(this, this.dirtyBits);
                    this.lastAccess = ClockSource.currentTime();
                }

                this.delegate.clearWarnings();
            } catch (SQLException var5) {
                if (!this.poolEntry.isMarkedEvicted()) {
                    throw this.checkException(var5);
                }
            } finally {
                this.delegate = ProxyConnection.ClosedConnection.CLOSED_CONNECTION;
                this.poolEntry.recycle(this.lastAccess);
            }
        }

    }

HouseKeeping

프로그램 운영 중 하우스키핑은 프로그램의 정상 동작을 위한 관리 및 보조를 의미한다. HikariPool의 HouseKeeping은 내부클래스로 구현되어있다.

private final class HouseKeeper implements Runnable
{
   private volatile long previous = plusMillis(currentTime(), -housekeepingPeriodMs);

   @Override
   public void run()
   {
      try {
         // refresh values in case they changed via MBean
         connectionTimeout = config.getConnectionTimeout();
         validationTimeout = config.getValidationTimeout();
         leakTaskFactory.updateLeakDetectionThreshold(config.getLeakDetectionThreshold());
         catalog = (config.getCatalog() != null && !config.getCatalog().equals(catalog)) ? config.getCatalog() : catalog;

         final long idleTimeout = config.getIdleTimeout();
         final long now = currentTime();

         // Detect retrograde time, allowing +128ms as per NTP spec.
         if (plusMillis(now, 128) < plusMillis(previous, housekeepingPeriodMs)) {
            logger.warn("{} - Retrograde clock change detected (housekeeper delta={}), soft-evicting connections from pool.",
                        poolName, elapsedDisplayString(previous, now));
            previous = now;
            softEvictConnections();
            return;
         }
         else if (now > plusMillis(previous, (3 * housekeepingPeriodMs) / 2)) {
            // No point evicting for forward clock motion, this merely accelerates connection retirement anyway
            logger.warn("{} - Thread starvation or clock leap detected (housekeeper delta={}).", poolName, elapsedDisplayString(previous, now));
         }

         previous = now;

         String afterPrefix = "Pool ";
         if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
            logPoolState("Before cleanup ");
            afterPrefix = "After cleanup  ";

            final List<PoolEntry> notInUse = connectionBag.values(STATE_NOT_IN_USE);
            int toRemove = notInUse.size() - config.getMinimumIdle();
            for (PoolEntry entry : notInUse) {
               if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
                  closeConnection(entry, "(connection has passed idleTimeout)");
                  toRemove--;
               }
            }
         }

         logPoolState(afterPrefix);

         fillPool(); // Try to maintain minimum connections
      }
      catch (Exception e) {
         logger.error("Unexpected exception in housekeeping task", e);
      }
   }
}

하이스키핑를 운영하기 위해 ScheduledExecutorService(corePoolSize=1)를 초기화 하여 사용한다. 하우스키퍼는 ScheduledThreadPoolExecutor에 의해 30초마다 동작한다.

private ScheduledExecutorService initializeHouseKeepingExecutorService()
{
   if (config.getScheduledExecutor() == null) {
      final ThreadFactory threadFactory = Optional.ofNullable(config.getThreadFactory()).orElseGet(() -> new DefaultThreadFactory(poolName + " housekeeper", true));
      final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, threadFactory, new ThreadPoolExecutor.DiscardPolicy());
      executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
      executor.setRemoveOnCancelPolicy(true);
      return executor;
   }
   else {
      return config.getScheduledExecutor();
   }
}

HikariCP에서 하우스키핑은 일정 시간(idle timeout) 사용되지 않은 유휴 상태(idle)의 Connection을 풀에서 제거하는 역할을 한다.

if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
   logPoolState("Before cleanup ");
   afterPrefix = "After cleanup  ";

   final List<PoolEntry> notInUse = connectionBag.values(STATE_NOT_IN_USE);
   int toRemove = notInUse.size() - config.getMinimumIdle();
   for (PoolEntry entry : notInUse) {
      if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
         closeConnection(entry, "(connection has passed idleTimeout)");
         toRemove--;
      }
   }
}

프로퍼티로 minimum idle과 maximum pool size를 설정해서 해당 기능을 테스트 해볼 수 있다.

spring:
  datasource:
    hikari:
      minimum-idle: 10
      maximum-pool-size: 100

기본 값은 minimum idle=10, maximum pool size=10 으로 프로퍼티 지정 없이 동작하면 하우스키핑은 동작하지 않는다. 실제 릴리즈 환경에서는 리소스를 효율적으로 활용하기 위해 프로퍼티 지정이 반드시 필요할 것 같다.

Hikari의 LeakTask

HikariPool 분석 중 ProxyLeakTaskFactory 클래스가 있어서 Hikari Pool에서 따로 연결에 대한 누수 관리를 해주는 부분인 것 같아 관심이 생겨 확인해봤다. getConnection 호출 시 ProxyConnection에 LeakTask를 전달해주는데, leakDetectionThreshold(milliseconds, 기본값=0L) 만큼 반환이 안되면 로그가 발생한다. 정확하게는 누수라기보다 지연되는 Connection 검출하는 기능인 것 같다.

HikariConfig

설정 이름 기본 값 설명
keepalive-time 0 (milliseconds) 지속적인 연결을 유지하기 위해 서버에 핑 패킷 지정 값마다 전송한다. (기본값으로는 기능이 동작하지 않음)
connection-test-query “” 해당 값을 지정하면 핑 패킷이 아닌 해당 쿼리로 검증한다. (추천하지 않음)
max-lifetime 1_800_000 (milliseconds) 해당 시간 만큼 사용하지 Connection을 사용하지 않으면 종료한다. (MySQL의 wait_timeout보다 큰 값일 경우 문제가 될 수 있음)
minimum-idle 10 최소 유지 Connection 수
maximum-pool-size 10 Pool의 최대 Connection 수, 기본 설정을 사용할 경우 Pool의 유동성이 없음.
idle-timeout 600_000 (milliseconds) Connection 유휴 타임아웃
public final class PingPacket implements ClientMessage {

  /** default instance */
  public static final PingPacket INSTANCE = new PingPacket();

  @Override
  public int encode(Writer writer, Context context) throws IOException {
    writer.initPacket();
    writer.writeByte(0x0e);
    writer.flush();
    return 1;
  }
}

References

  1. https://dict.naver.com/search.dict?mode=all&query_euckr=&query=housekeeping