Ответ 1
Цикл событий Vert.x, по сути, является классическим циклом событий, существующим на многих платформах. И, конечно же, большинство объяснений и документов можно найти для Node.js, поскольку он является самой популярной платформой, основанной на этом шаблоне архитектуры. Посмотрите на более или менее хорошее объяснение механики в цикле событий Node.js. В учебнике по Vert.x также есть хорошее объяснение между словами "Не звони нам, хорошо звони" и "Вертикали".
Изменить для своих обновлений:
Прежде всего, когда вы работаете с циклом событий, основной поток должен работать очень быстро для всех запросов. Вы не должны делать длинную работу в этом цикле. И, конечно же, вам не следует ждать ответа на ваш звонок в базу данных. - Расписание звонков асинхронно - Назначить обратный вызов (обработчик) для результата - Обратный вызов будет выполняться в рабочем потоке, а не в потоке цикла событий. Этот обратный вызов, например, вернет ответ сокету. Таким образом, ваши операции в цикле обработки событий должны просто планировать все асинхронные операции с обратными вызовами и переходить к следующему запросу, не ожидая никаких результатов.
Предположим, что типичный запрос занимает от 100 мс до 1 секунды (в зависимости от типа и характера запроса).
В этом случае ваш запрос содержит некоторые дорогостоящие части вычислений или доступ к IO - ваш код в цикле обработки событий не должен ждать результатов этих операций.
Я просто пытаюсь понять, как эта модель лучше, чем традиционные модели потоков /conn-серверов? Предположим, что нет операции ввода-вывода или все операции ввода-вывода обрабатываются асинхронно?
Когда у вас слишком много одновременных запросов и традиционная модель программирования, вы будете создавать поток для каждого запроса. Что этот поток будет делать? В основном они будут ожидать операций ввода-вывода (например, результат из базы данных). Это пустая трата ресурсов. В нашей модели цикла событий у вас есть один основной поток, который планирует операции и предварительно выделяет количество рабочих потоков для длинных задач. + Ни один из этих работников на самом деле не ждет ответа, он просто может выполнить другой код в ожидании результата ввода-вывода (это может быть реализовано как обратные вызовы или периодическая проверка состояния выполняемых в данный момент заданий ввода-вывода). Я бы порекомендовал вам пройти через Java NIO и Java NIO 2, чтобы понять, как этот асинхронный ввод-вывод может быть реализован внутри фреймворка. Зеленые нити тоже очень родственная концепция, которую было бы хорошо понять. Зеленые потоки и сопрограммы представляют собой тип теневого цикла обработки событий, который пытается достичь того же - меньше потоков, потому что мы можем повторно использовать системный поток, пока зеленый поток чего-то ждет.
Как это даже решает проблему c10k, когда он не может запустить все параллельные запросы параллельно и должен ждать, пока предыдущий не завершится?
Конечно, мы не ждем в основном потоке отправки ответа на предыдущий запрос. Получить запрос, запланировать выполнение длинных /IO задач, следующий запрос.
Даже если я решу перенести все эти операции в рабочий поток (в виде пула), я вернусь к той же проблеме, не так ли? Переключение контекста между потоками?
Если все сделать правильно - нет. Более того, вы получите хорошую локализацию данных и прогнозирование потока выполнения. Одно ядро ЦП будет выполнять ваш короткий цикл обработки событий и планировать асинхронную работу без переключения контекста и ничего более. Другие ядра делают вызов в базу данных и возвращают ответ и только это. Переключение между обратными вызовами или проверка различных каналов на состояние ввода-вывода фактически не требует переключения контекста системного потока - оно фактически работает в одном рабочем потоке. Итак, у нас есть один рабочий поток на ядро, и этот системный поток ожидает/проверяет доступность результатов, например, из нескольких соединений с базой данных. Пересмотрите концепцию Java NIO, чтобы понять, как она может работать таким образом. (Классический пример для NIO - прокси-сервер, который может принимать много параллельных соединений (тысячи), запросы прокси к некоторым другим удаленным серверам, прослушивать ответы и отправлять ответы клиентам, и все это с помощью одного или двух потоков)
Что касается вашего кода, я сделал пример проекта для вас, чтобы продемонстрировать, что все работает как положено:
public class MyFirstVerticle extends AbstractVerticle {
@Override
public void start(Future<Void> fut) {
JDBCClient client = JDBCClient.createShared(vertx, new JsonObject()
.put("url", "jdbc:hsqldb:mem:test?shutdown=true")
.put("driver_class", "org.hsqldb.jdbcDriver")
.put("max_pool_size", 30));
client.getConnection(conn -> {
if (conn.failed()) {throw new RuntimeException(conn.cause());}
final SQLConnection connection = conn.result();
// create a table
connection.execute("create table test(id int primary key, name varchar(255))", create -> {
if (create.failed()) {throw new RuntimeException(create.cause());}
});
});
vertx
.createHttpServer()
.requestHandler(r -> {
int requestId = new Random().nextInt();
System.out.println("Request " + requestId + " received");
client.getConnection(conn -> {
if (conn.failed()) {throw new RuntimeException(conn.cause());}
final SQLConnection connection = conn.result();
connection.execute("insert into test values ('" + requestId + "', 'World')", insert -> {
// query some data with arguments
connection
.queryWithParams("select * from test where id = ?", new JsonArray().add(requestId), rs -> {
connection.close(done -> {if (done.failed()) {throw new RuntimeException(done.cause());}});
System.out.println("Result " + requestId + " returned");
r.response().end("Hello");
});
});
});
})
.listen(8080, result -> {
if (result.succeeded()) {
fut.complete();
} else {
fut.fail(result.cause());
}
});
}
}
@RunWith(VertxUnitRunner.class)
public class MyFirstVerticleTest {
private Vertx vertx;
@Before
public void setUp(TestContext context) {
vertx = Vertx.vertx();
vertx.deployVerticle(MyFirstVerticle.class.getName(),
context.asyncAssertSuccess());
}
@After
public void tearDown(TestContext context) {
vertx.close(context.asyncAssertSuccess());
}
@Test
public void testMyApplication(TestContext context) {
for (int i = 0; i < 10; i++) {
final Async async = context.async();
vertx.createHttpClient().getNow(8080, "localhost", "/",
response -> response.handler(body -> {
context.assertTrue(body.toString().contains("Hello"));
async.complete();
})
);
}
}
}
Выход:
Request 1412761034 received
Request -1781489277 received
Request 1008255692 received
Request -853002509 received
Request -919489429 received
Request 1902219940 received
Request -2141153291 received
Request 1144684415 received
Request -1409053630 received
Request -546435082 received
Result 1412761034 returned
Result -1781489277 returned
Result 1008255692 returned
Result -853002509 returned
Result -919489429 returned
Result 1902219940 returned
Result -2141153291 returned
Result 1144684415 returned
Result -1409053630 returned
Result -546435082 returned
Итак, мы принимаем запрос - планируем запрос к базе данных, переходим к следующему запросу, мы используем их все и отправляем ответ на каждый запрос, только когда все сделано с базой данных.
Что касается вашего примера кода, я вижу две возможные проблемы - во-первых, похоже, что у вас нет соединения close()
, что важно для его возврата в пул. Во-вторых, как настроен ваш пул? Если есть только одно бесплатное соединение - эти запросы будут сериализованы в ожидании этого соединения.
Я рекомендую вам добавить печать временной метки для обоих запросов, чтобы найти место, где вы сериализуете. У вас есть что-то, что делает вызовы в цикле событий блокирующими. Или... проверьте, что вы отправляете запросы параллельно в своем тесте. Не следующий после получения ответа после предыдущего.