Spring 同步与异步休息控制器
Spring sync vs async rest controller
我尝试查看 Spring 同步 REST 控制器与同一控制器的异步版本之间的区别。
每个控制器做同样的事情:获取一个 RequestBody 并将其保存在 Mongo 数据库中。
@RestController
@RequestMapping ("/api/1/ticks")
public class TickController {
@Autowired
private TickManager tickManager;
@RequestMapping (method = RequestMethod.POST)
public ResponseEntity save(@RequestBody List<Tick> ticks) {
tickManager.save(ticks);
return new ResponseEntity(HttpStatus.OK);
}
@RequestMapping (value = "/async", method = RequestMethod.POST)
public @ResponseBody Callable<ResponseEntity> saveAsync(@RequestBody List<Tick> ticks) {
return () -> {
tickManager.save(ticks);
return new ResponseEntity(HttpStatus.OK);
};
}
}
tickManager 只依赖于 tickRepository 并且只调用子层。
tickRepository 基于 Spring 数据 Mongodb:
@Repository
public interface TickRepository extends MongoRepository<Tick, String> {}
我使用 Gatling 来测试那些控制器。
这是我的场景:
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class TicksSaveSyncSimulation extends Simulation {
val rampUpTimeSecs = 20
val testTimeSecs = 5
val noOfUsers = 1000
val minWaitMs = 1000 milliseconds
val maxWaitMs = 3000 milliseconds
val baseURL = "http://localhost:9080"
val requestName = "ticks-save-sync-request"
val scenarioName = "ticks-save-sync-scenario"
val URI = "/api/1/ticks"
val httpConf = http.baseURL(baseURL)
val http_headers = Map(
"Accept-Encoding" -> "gzip,deflate",
"Content-Type" -> "application/json;charset=UTF-8",
"Keep-Alive" -> "115"
)
val scn = scenario(scenarioName)
.repeat(100) {
exec(
http(requestName)
.post(URI)
.headers(http_headers)
.body(StringBody(
"""[{
| "type": "temperature",
| "datas": {}
|}]""".stripMargin))
.check(status.is(200))
)
}
setUp(scn.inject(rampUsers(1000) over (1 seconds))).protocols(httpConf)
}
我尝试了几种情况,同步版本每秒处理的请求总是比异步版本多 2 倍。
当我增加用户数量时,两个版本都会崩溃。
我尝试覆盖异步版本的 taskExecutor 但没有成功:
@Configuration
public class TaskExecutorConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setMaxPoolSize(1000);
taskExecutor.setThreadNamePrefix("LULExecutor-");
taskExecutor.initialize();
return taskExecutor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}
我认为看到有利于异步实现的差异。
我做错了什么?
您的测试看起来有缺陷。在管道的一端(这里是您的控制器)不阻塞,而在另一端阻塞(tickManager.save
确实看起来像阻塞调用)没有任何意义。您只需支付跳入 ThreadPoolTaskExecutor
.
的额外费用
然后,一般来说,当您的所有任务都非常快时,您不会从非阻塞架构中获得任何好处,就像滴答声一样。当某些任务需要更长的时间时,您可以获得收益,因此您不想浪费资源(线程,CPU 周期)只是等待它们完成,并且您希望使用它们来执行其他任务同时
关于您的 Too many open files
异常,您可能没有正确调整 OS 进行负载测试,检查 relevant documentation。您也很有可能 运行 您的应用程序和 Gatling(可能还有您的数据库)位于同一台主机上,这很糟糕,因为它们会争夺资源。
我尝试查看 Spring 同步 REST 控制器与同一控制器的异步版本之间的区别。
每个控制器做同样的事情:获取一个 RequestBody 并将其保存在 Mongo 数据库中。
@RestController
@RequestMapping ("/api/1/ticks")
public class TickController {
@Autowired
private TickManager tickManager;
@RequestMapping (method = RequestMethod.POST)
public ResponseEntity save(@RequestBody List<Tick> ticks) {
tickManager.save(ticks);
return new ResponseEntity(HttpStatus.OK);
}
@RequestMapping (value = "/async", method = RequestMethod.POST)
public @ResponseBody Callable<ResponseEntity> saveAsync(@RequestBody List<Tick> ticks) {
return () -> {
tickManager.save(ticks);
return new ResponseEntity(HttpStatus.OK);
};
}
}
tickManager 只依赖于 tickRepository 并且只调用子层。
tickRepository 基于 Spring 数据 Mongodb:
@Repository
public interface TickRepository extends MongoRepository<Tick, String> {}
我使用 Gatling 来测试那些控制器。 这是我的场景:
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class TicksSaveSyncSimulation extends Simulation {
val rampUpTimeSecs = 20
val testTimeSecs = 5
val noOfUsers = 1000
val minWaitMs = 1000 milliseconds
val maxWaitMs = 3000 milliseconds
val baseURL = "http://localhost:9080"
val requestName = "ticks-save-sync-request"
val scenarioName = "ticks-save-sync-scenario"
val URI = "/api/1/ticks"
val httpConf = http.baseURL(baseURL)
val http_headers = Map(
"Accept-Encoding" -> "gzip,deflate",
"Content-Type" -> "application/json;charset=UTF-8",
"Keep-Alive" -> "115"
)
val scn = scenario(scenarioName)
.repeat(100) {
exec(
http(requestName)
.post(URI)
.headers(http_headers)
.body(StringBody(
"""[{
| "type": "temperature",
| "datas": {}
|}]""".stripMargin))
.check(status.is(200))
)
}
setUp(scn.inject(rampUsers(1000) over (1 seconds))).protocols(httpConf)
}
我尝试了几种情况,同步版本每秒处理的请求总是比异步版本多 2 倍。 当我增加用户数量时,两个版本都会崩溃。
我尝试覆盖异步版本的 taskExecutor 但没有成功:
@Configuration
public class TaskExecutorConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setMaxPoolSize(1000);
taskExecutor.setThreadNamePrefix("LULExecutor-");
taskExecutor.initialize();
return taskExecutor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}
我认为看到有利于异步实现的差异。 我做错了什么?
您的测试看起来有缺陷。在管道的一端(这里是您的控制器)不阻塞,而在另一端阻塞(tickManager.save
确实看起来像阻塞调用)没有任何意义。您只需支付跳入 ThreadPoolTaskExecutor
.
然后,一般来说,当您的所有任务都非常快时,您不会从非阻塞架构中获得任何好处,就像滴答声一样。当某些任务需要更长的时间时,您可以获得收益,因此您不想浪费资源(线程,CPU 周期)只是等待它们完成,并且您希望使用它们来执行其他任务同时
关于您的 Too many open files
异常,您可能没有正确调整 OS 进行负载测试,检查 relevant documentation。您也很有可能 运行 您的应用程序和 Gatling(可能还有您的数据库)位于同一台主机上,这很糟糕,因为它们会争夺资源。