Tomcatにおけるシングルテナント対応非同期処理の実装
この記事は、「Tomcatにおける非同期処理の実装」の一部分です。
- 「Tomcatにおける非同期処理の実装」
- 「Tomcatにおけるシングルテナント対応非同期処理の実装」←この記事
- 「Tomcatにおけるマルチテナント対応非同期処理の実装」
はじめに
ここでは、シングルテナントにおいて、スレッドプールを利用した非同期タスクの流動制限を行うTomcatアプリケーションを実装していきます。
動作確認ソフトウェア
次のソフトウェアおよびライブラリの環境で動作を確認しています。
その他の依存ライブラリについては、後述のpom.xmlを参考にしてご確認してください。
- Windows 10
- Java SE 8
- Maven 3.6.2
- Eclipse IDE for Enterprise Java Developer 2019-06 (4.12.0)
- Tomcat 8.5.43
- Spring Boot 1.5.22
- Spring Framework 4.3.25
pom.xmlの作成
EclipseかMavenを使ってMavenプロジェクトを作成し、pom.xmlに下図の内容を追加します。
なお、dependencyのspring-boot-starter-actuatorはTomcatモニタリング用のライブラリですが、運用環境ではセキュリティリスクとなるので外します。
<project xmlns=...>
:
<build>
:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.1.4.RELEASE</version>
</plugin>
</plugins>
:
</build>
<dependencies>
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>1.5.22.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>1.5.22.RELEASE</version>
</dependency>
:
</dependencies>
</project>
図3.2 pom.xmlへの追加内容
設定ファイルの作成
Spring Bootの設定ファイルである「application.properties」を作成します。
# Tomcatモニタリング
# Tomcatコンテナの監視やシャットダウンするなどの運用監視向けの設定
# 運用環境ではセキュリティリスクとなるので外す
endpoints.shutdown.enabled=true
management.security.enabled=false
management.endpoints.web.exposure.include=**
# スレッドプール
# 非同期タスクの受理数と同時実行数を指定
# queueCapacityの数だけタスクをキューイングし、maxPoolSizeの数だけバックグランドで動作
# 最大「maxPoolSize + queueCapacity」の数だけ非同期タスクを受け入れ、その数を超えたらRejectされる(受け付けない)
# corePoolSize = 初期のThread数
# queueCapacity = corePoolSizeが一杯になったときにキューイングする数
# maxPoolSize = queueCapacityを越えたときの最大Thread数
threadPool.corePoolSize=1
threadPool.queueCapacity=2
threadPool.maxPoolSize=2
threadPool.keepAliveSeconds=1
# Loggingのログ出力レベルを指定
# Loggingの出力レベルを指定
# https://docs.oracle.com/javase/jp/8/docs/api/java/util/logging/Level.html
logging.level.root=INFO
logging.level.org.springframework.web=INFO
logging.level.web01=INFO
図3.3 application.propertiesの内容
スレッドプール設定のPOJOクラスの作成
スレッドプール設定を保持するPOJOクラスを作成します。
Spring Frameworkによりapplication.propertiesの"threadPool"で始まる設定値が、自動的にこのクラスに注入されます。
@Component
@ConfigurationProperties(prefix = "threadPool")
public class ThreadPoolSettings {
private String corePoolSize;
private String queueCapacity;
private String maxPoolSize;
private String keepAliveSeconds;
public void setCorePoolSize(String corePoolSize) {this.corePoolSize = corePoolSize;}
public void setQueueCapacity(String queueCapacity) {this.queueCapacity = queueCapacity;}
public void setMaxPoolSize(String maxPoolSize) {this.maxPoolSize = maxPoolSize;}
public void setKeepAliveSeconds(String keepAliveSeconds) {this.keepAliveSeconds = keepAliveSeconds;}
public int getCorePoolSize() {return Integer.valueOf(corePoolSize);}
public int getQueueCapacity() {return Integer.valueOf(queueCapacity);}
public int getMaxPoolSize() {return Integer.valueOf(maxPoolSize);}
public int getKeepAliveSeconds() {return Integer.valueOf(keepAliveSeconds);}
}
図3.4 ThreadPoolSettingsクラスの内容
Tomcat起動クラスの作成
組み込みのTomcatコンテナを起動するApplicationクラスを作成します。
@SpringBootApplicationは、Spring Frameworkに自動的に@Configurationや@Componentなどのクラスを走査するよう指示するものです。@Repository、@Service、@Controllerも@Componentの一つなので、これらも検出されます。ただし、デフォルトでは走査対象は、同一パッケージかその配下のパッケージに限るので、注意が必要です。
@SpringBootApplication
public class Application {
public static void main(final String[] args) {
SpringApplication.run(Application.class, args);
}
}
図3.5 Applicationクラスの内容
@Configurationクラスの作成
Tomcat起動時にアプリケーションの初期設定を行うSampleConfigクラスを作成します。
このクラスには、初期設定を行うクラスであることを宣言する@Configurationと、非同期呼び出しを有効化するための@EnableAsyncがあります。taskExecutor1()にある@Beanは、Spring FrameworkにBeanを登録するものです。デフォルトでは、Bean名はメソッド名と同じで、ここでは「taskExecutor」がBean名となります。
taskExecutor1()では、キューを持つスレッドプールであるThreadPoolTaskExecutorを生成します。運用環境のコア数とメモリ容量に応じて、前述のapplication.propertiesの設定値を変えることにより、キューとスレッドプールの容量をチューニングできるというわけです。
また、Tomcat終了時に、動作中のスレッドを終了させないよう、スレッドプールが保持するスレッドは「非デーモン」にします。ThreadPoolTaskExecutorはデフォルトで非デーモンですが、ここでは明示的に指定しています。ただし、スレッドプールのシャットダウンでは、キューに滞留しているタスクはすべて破棄されてしまいます。Tomcatアプリケーションを再起動したときに、キューに滞留していたタスクを実行させたい場合は、追加の実装が必要ですが、ここでは割愛します。
@Configuration
@EnableAsync
public class SampleConfig {
//application.propertiesの"threadPool"で始まる設定値を保持する変数
@Autowired
ThreadPoolSettings threadPoolSettings;
//@Async向けのスレッドプールをSpring Beanとして登録する
//Bean名はメソッド名と同じ「taskExecutor1」となる
@Bean
public ThreadPoolTaskExecutor taskExecutor1() {
final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(threadPoolSettings.getCorePoolSize());
executor.setQueueCapacity(threadPoolSettings.getQueueCapacity());
executor.setMaxPoolSize(threadPoolSettings.getMaxPoolSize());
executor.setKeepAliveSeconds(threadPoolSettings.getKeepAliveSeconds());
executor.setThreadNamePrefix("Executor1-");
executor.setDaemon(false);
executor.initialize();
return executor;
}
}
図3.6 SampleConfigクラスの内容
@Serviceクラスの作成
下図のSampleServiceクラスを作成します。
このクラスの@Serviceは、DDD(Domain-Driven Design)の構成要素の一つであり、ビジネスロジックを処理する債務を担います。
cmd()では、引数で指定されたコマンドを起動し、コマンドの実行状況をノンブロッキングで確認できるFutureオブジェクトを返します。非同期呼び出しの対象であることを宣言するために@Asyncを付加してあり、"taskExcecutor1"パラメータを付けてSampleConfigクラスで定義したスレッドプールへ紐付けています。
@Service
public class SampleService {
@Async("taskExecutor1")
public CompletableFuture<String> cmd(final String[] command) {
final int exitValue = run(command);
return CompletableFuture.completedFuture("exitValue="+exitValue);
}
//外部コマンドを実行
public static int run(final String[] command) {
try{
final Process p = new ProcessBuilder(command).redirectErrorStream(true).start();
try(final BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()))){
String str;
while((str = r.readLine()) != null) {
System.out.println("Command output: "+str);
}
}
return p.waitFor();
}catch(IOException | InterruptedException e) {
e.printStackTrace();
return -1;
}
}
}
図3.7 SampleServiceクラスの内容
@Controllerクラスの作成
下図のSampleControllerクラスを作成します。
@Controllerは、MVCモデルのControllerに該当し、クライアントからの要求に応対する債務を担います。
このクラスには、@Autowiredを付加したフィールドがありますが、@Componentや@Beanなどで定義された部品を自動的に注入するようSpring Frameworkへ指示するするものです。
cmd()は「GET http://.../cmd」に応対して、SampleServiceのcmd()を呼び出しています。同期呼び出しのように見えますが、呼び出し先には@Asyncを付加してあるためSpring Frameworkが自動的に非同期呼び出しに変えてくれるわけです。
スレッドプールのキューやスレッドが満杯になり非同期処理を受理できない場合は、RejectedExecutionExceptionがスローされます。これにより非同期処理の流量制御を実現できるわけです。ここでは簡単のため、「Rejected」の文字列をクライアントに返していますが、本来はHTTPの「429 Too Many Requests」といったステータスコードを返すのが筋でしょう。
status()は「GET http://.../status」に応対して、SampleServiceのcmd()が返してきたFutureオブジェクトを走査し、非同期呼び出しの実行状況を文字列で返します。
@Controller
public class SampleController {
private static final String[] COMMAND = new String[] {"cmd", "/c", "ping", "-n", "10", "localhost", ">", "nul"};
private AtomicInteger counter = new AtomicInteger(1);
private HashMap<String, Future<String>> futures = new HashMap<>();
@Autowired
private SampleService sampleService;
@RequestMapping(value="/cmd", method=RequestMethod.GET)
@ResponseBody
public String cmd() {
final String id = String.format("<noTenantId>#%03d", counter.getAndIncrement());
try{
final CompletableFuture<String> future = sampleService.cmd(COMMAND)
.exceptionally(ex -> id+": Exception: "+ex.getMessage()+"\n");
synchronized(this.futures) {
this.futures.put(id, future);
}
return "Accepted: id="+id+"\n";
}catch(RejectedExecutionException e) {
final CompletableFuture<String> future = new CompletableFuture<>();
future.complete("Rejected");
synchronized(this.futures) {
this.futures.put(id, future);
}
return "Rejected: id="+id+"\n";
}
}
@RequestMapping(value="/status", method=RequestMethod.GET)
@ResponseBody
public String status() {
final Map<String, Future<String>> map;
synchronized(this.futures) {
map = (Map<String, Future<String>>)this.futures.clone();
}
return map.entrySet().stream()
.map(entry->SampleController.futureGet(entry.getKey(), entry.getValue()))
.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
.toString();
}
//非同期タスクの状況を文字列に変換
private static String futureGet(String tenantId, Future<String> future) {
if(future.isDone()==false && future.isCancelled()==false) {
return tenantId+": Running\n";
}else if(future.isCancelled()) {
return tenantId+": Canceled\n";
}
try {
return tenantId+": "+future.get()+"\n";
} catch (InterruptedException | ExecutionException | CancellationException e) {
return tenantId+": Exception\n";
}
}
}
図3.8 SampleControllerクラスの内容
動作確認
ローカルでTomcatコンテナを起動し、次のようにコマンドを実行して動作確認できます。
$ curl -X GET http://localhost:8080/cmd
Accepted: id=<noTenantId>#001
# 10秒強待ってから/statusをGETする
$ curl -X GET http://localhost:8080/status
<noTenantId>#001: exitValue=0
# Tomcatコンテナを通常終了させる
$ curl -X POST http://localhost:8080/shutdown
図3.9 非同期タスクの動作確認
「Tomcatにおけるマルチテナント対応非同期処理の実装」へ続く...