지난번, 해당 글에서 SpringBoot의 Graceful Shutdown에 대해 알아보았습니다.
Graceful Shutdown의 메서드를 살펴보면 isActive 메서드를 통해 아직 처리가 완료되지 않은 요청이 있는지 확인하기 위해 isActive라는 메서드를 사용하였고, getCountAllocated 메서드를 통해 완료되지 않은 요청의 개수를 확인하는 것을 확인
했습니다.
결국, 내부적으로는 Spring의 Dispatcher Servlet을 통해 요청-응답을 처리할텐데, 대체 어떤 구조에서 어떤 방식으로 남은 요청이 있는지 체크
하는지에 대해 정확히 파악하지 못했습니다.
따라서, Tomcat의 구조를 알아보면서 완료되지 않은 요청의 개수를 확인하는지에 대해 알아보겠습니다.
톰캣의 아키텍처는 아래와 같습니다. 하나씩 살펴보겠습니다. 아래에서 설명하는 개념들은 모두 Container라는 개념으로 추상화됩니다.
위의 그림에서 Tomcat - The Server에 해당하는 부분입니다. 말 그대로 Tomcat Server입니다. 내부적으로 여러 계층으로 이루어진 Container들을 가집니다.
Service는 Connector와 Engine을 연결시켜주는 중간 매개체입니다.
Client와의 연결을 담당합니다. 포트에 따른 여러 Connectors들을 선언할 수 있습니다.
Connector로 부터 Client의 요청을 받고 내부적으로 가지는 host 중 요청에 해당하는 host에게 전달하여 요청을 처리하고 Connector에게 응답 값을 전달합니다.
Engine부터는 부모 자식의 개념이 등장하며, Engine의 자식은 Host입니다.
localhost와 같은 host입니다. 하나의 Engine에서 여러 host를 가질 수 있습니다.
그리고 내부적으로 Context라는 자식들을 가집니다.
WebApplication이라고도 불립니다. 내부적으로 Servlet을 가지고 SpringBoot를 사용하신다면 DispatcherServlet을 갖게 될 것입니다.
다시 Graceful shutdown의 코드로 돌아와 어떻게 Graceful shutdown을 진행하는지 확인해보겠습니다.
우선, Graceful shutdown 객체는 내부적으로 Tomcat 객체를 가집니다.
package org.springframework.boot.web.embedded.tomcat;
final class GracefulShutdown {
private final Tomcat tomcat;
// ...
private void doShutdown(GracefulShutdownCallback callback) {
List<Connector> connectors = getConnectors();
connectors.forEach(this::close);
// ...
}
private List<Connector> getConnectors() {
List<Connector> connectors = new ArrayList<>();
for (Service service : this.tomcat.getServer().findServices()) {
Collections.addAll(connectors, service.findConnectors());
}
return connectors;
}
private void close(Connector connector) {
connector.pause();
connector.getProtocolHandler().closeServerSocketGraceful();
}
}
먼저, tomcat의 Service가 참조하고 있는 Connector들을 닫아, 더 이상의 client 요청을 받지 않도록 합니다.
package org.springframework.boot.web.embedded.tomcat;
final class GracefulShutdown {
private final Tomcat tomcat;
// ...
private void doShutdown(GracefulShutdownCallback callback) {
List<Connector> connectors = getConnectors();
connectors.forEach(this::close);
try {
for (Container host : this.tomcat.getEngine().findChildren()) {
for (Container context : host.findChildren()) {
while (isActive(context)) {
if (this.aborted) {
logger.info("Graceful shutdown aborted with one or more requests still active");
callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
return;
}
Thread.sleep(50);
}
}
}
// ...
}
그 후, Tomcat Engine의 Children인 host들을 순회합니다. 그리고 Host의 자식들인 Context를 순회
합니다. 그리고 아래처럼 Context에 대해 isActive 메서드를 통해 확인합니다.
package org.springframework.boot.web.embedded.tomcat;
final class GracefulShutdown {
private final Tomcat tomcat;
// ...
private boolean isActive(Container context) {
try {
if (((StandardContext) context).getInProgressAsyncCount() > 0) {
return true;
}
for (Container wrapper : context.findChildren()) {
if (((StandardWrapper) wrapper).getCountAllocated() > 0) {
return true;
}
}
return false;
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
실제로 아직 처리 중인 요청은 getCountAllocated() 메서드를 통해 알 수 있습니다.
위에서 알아본 Tomcat 아키텍처에서 Context는 내부적으로 여러 Servlet를 가진다고 했습니다. 그런데, context의 findChildren 메서드는 StandardWrapper 타입의 Container를 반환합니다.
Wrapper 클래스를 보시면 아래와 같은 설명이 있습니다.
package org.apache.catalina;
| A Wrapper is a Container that represents an individual servlet
| definition from the deployment descriptor of the web application.
public class StandardWrapper extends ContainerBase
implements ServletConfig, Wrapper, NotificationEmitter {
// ...
}
package org.apache.catalina.core;
public class StandardWrapper extends ContainerBase
implements ServletConfig, Wrapper, NotificationEmitter {
protected volatile Servlet instance = null;
protected final AtomicInteger countAllocated = new AtomicInteger(0);
}
Wrapper는 Servlet을 정의하는 단위입니다. StandardWrapper는 Wrapper의 하위 객체로써 Servlet 객체를 가지고 countAllocated라는 AtomicInteger 값을 가집니다.
따라서, Servlet을 관리하고 Servlet에서의 처리중인 요청의 개수를 countAllocated라는 변수로 Atomic하게 관리
합니다.
위와 같은 방식으로 Graceful Shutdown에서 tomcat의 Engine → Host → Context → Wrapper(Servlet)의 탐색을 통해 아직 완료되지 않은 요청들이 있는지 확인
하면서 Graceful shutdown을 진행합니다.