Привет, хабр! Меня зовут Евгений Иванов, я разработчик YDB. Но сегодня я бы хотел представить Вашему вниманию пост не о YDB, а о виртуальных потоках Java 21 на примере TPC-C для PostgreSQL.

Дедлок у обедающих философов
Дедлок у обедающих философов

В посте о TPC-C для YDB, мы обсудили некоторые недостатки реализации TPC-C проектом Benchbase (при этом всё равно это очень классный проект). Одним из наиболее значимых недостатков, на наш взгляд, является ограничение конкурентного выполнения (concurrency limit) из-за создания слишком большого числа физических потоков. Мы решили эту проблему, перейдя на виртуальные потоки Java 21. Но оказалось, что бесплатный сыр бывает только в мышеловке. И в этом посте мы расскажем о примере дедлока в TPC-C для PostgreSQL, причиной которого является исключительно переход на виртуальные потоки - и никаких проблем обедающих философов.

Данный пост будет полезен Java разработчикам, которые планируют перейти на виртуальные потоки. Сначала мы кратко обсудим такие фундаментальные понятия, как синхронные и асинхронные запросы. А затем рассмотрим важную особенность виртуальных потоков: дедлоки непредсказуемы и могут возникать глубоко внутри библиотек, которые вы используете. К счастью, отладка очень проста и мы покажем, как находить такие дедлоки.

Почему мы говорим о PostgreSQL в блоге YDB

PostgreSQL — это система управления базами данных с открытым исходным кодом, известная своей высокой производительностью, богатым набором функций и продвинутым уровнем соответствия стандарту SQL. Что не менее важно, вокруг PostgreSQL сложилось активное и отзывчивое сообщество. Но всё хорошо лишь пока вашему проекту не требуются горизонтальное масштабирование и отказоустойчивость. Тогда приходится использовать сторонние решения, основанные на Postgres и реализующие шардирование, такие как Citus. Погонять одним слоном несложно, но иметь целое стадо слонов довольно непросто. Особенно когда слоны должны поддерживать консистентность реплик и выполнять распределенные транзакции с уровнем изоляции "serializable".

YDB же с самого своего рождения является распределённой СУБД. Распределенные транзакции YDB изначально являются ключевой частью YDB (first-class citizens) и по умолчанию выполняются с уровнем изоляции "serializable". В настоящее время мы активно занимаетмся совместимостью с PostgreSQL, поскольку видим сильный спрос среди пользователей PostgreSQL на автоматическое масштабирование и обеспечение отказоустойчивости существующих приложений. Вот почему мы поддерживаем TPC-C для PostgreSQL (и надеемся, что скоро наши патчи примут в Benchbase).

Основы и мотивация

Сначала вспомним основные понятия: что такое конкурентное (concurrent) и параллельное выполнение, и какие преимущества у синхронных запросов по сравнению с асинхронными.

Если несколько операций выполняется одновременно, то можно сказать, что они выполняются конкурентно. При этом выполнение может быть как параллельным, так и последовательным (в т.ч. поочередным). Например, вы можете одновременно писать код в IDE и общаться с коллегами в telegram. Скорее всего вы делаете это последовательно, переключаясь туда-сюда между задачами. Или же можно гулять с собакой и разговаривать по телефону - тогда операции выполняются параллельно.

Теперь представим, что приложение делает запрос к базе данных. Запрос будет отправлен по сети, выполнен СУБД, после чего приложение получит ответ. Обратите внимание, что круг по сети может быть самой долгой частью выполнения запроса и занимать несколько миллисекунд. Что же приложение делает, пока ждёт ответа?

  1. Если запрос синхронный, он заблокирует поток выполнения. Такой код очень легко писать: в строке 1 отправка запроса, а в строке 2 обработка ответа. Например:

String userName = get_username_from_db(userId);
System.out.printf("Hello, %s!", userName);
  1. Запрос может быть асинхронным. Тогда поток не блокируется и программа продолжает выполняться, пока происходит параллельная обработка запроса:

CompletableFuture<String> userNameFuture = get_username_from_db(userId);

// Note, that this is kind of callback, it's not executed "right here",
// even more, at some point it will be executed in parallel with your thread.
// In real life scenarios, you will have to use mutual exclusion.
userNameFuture.thenAccept(userName -> {
    System.out.println("Hello, %s!", userName);
});

execute_something_else();

userNameFuture.get(); // wait for the completion of your request

В любом случае есть две конкурентные (concurrent) задачи: ожидание приложением ответа и обработка запроса СУБД. Синхронный код очень легок в написании и понимании. Но что если нужно выполнять одновременно тысячи обращений к базе? Придется создать по потоку на каждый запрос. Создание потоков в Linux дёшево, но есть причины, по которым не стоит создавать слишком много потоков:

  1. Каждому потоку нужен стек. Невозможно аллоцировать памяти меньше, чем размер одной страницы в вашей ОС, что обычно составляет 4 KiB или 2 MiB в случае больших страниц (huge pages).

  2. Не забывайте о планировщике потоков Linux. Попробуйте создать 100 000 потоков, но сначала удостоверьтесь, что у вашего компьютера есть кнопка "reset".

Именно поэтому до Java 21 было невозможно писать синхронный код с очень высокой степенью конкурентного выполнения: большое число потоков просто не создать. В какой-то момент времени благодаря Go произошла революция: горутины реализуют легковесную конкурентность (lightweight concurrency), что позволяет писать синхронный код эффективно. Мы советуем посмотреть доклад Дмитрия Вьюкова о планировщике Go. Java 21 добавила поддержку виртуальных потоков, которые во многом похожи на горутины. Интересно, что ни горутины, ни виртуальные потоки не являются изобретением, это всего лишь реинкарнация потоков в пользовательском режиме (user-level threads).

Теперь читателю должна быть понятна проблема с синхронными запросами в TPC-C от Benchbase. Для обеспечения высокой нагрузки на СУБД, требуется запустить большое число складов TPC-C, создав при этом много потоков. У нас не получилось создать больше 30 000 терминалов-потоков, используя физические потоки. Но при использовании виртуальных потоков легко запустили сотни тысяч терминалов-потоков.

Дедлоки на раз-два

Представим, что у нас есть многопоточное приложение на Java. Переход на виртуальные потоки может помочь с производительностью, при этом он делается очень просто. Надо лишь поменять одну строчку кода, где создаются потоки, и приложение сможет обрабатывать тысячи конкурентных задач, без накладных расходов, связанных с физическими потоками. Вот пример из нашей реализации TPC-C:

if (useRealThreads) {
    thread = new Thread(worker);
} else {
    thread = Thread.ofVirtual().unstarted(worker);
}

Вот и всё, теперь приложение использует виртуальные потоки. Под капотом виртуальная машина Java создает пул потоков-носителей (carrier threads), которые выполняют пользовательские виртуальные потоки (virtual threads). Может показаться, что переход без каких-либо подводных камней, но в какой-то момент приложение неожиданно зависает.

Наша реализация TPC-C для PostgreSQL использует пул потоков c3p0. При этом согласно стандарту TPC-C каждый терминал должен использовать собственное подключение. Однако в реальных сценариях это непрактично, поэтому мы добавили опцию ограничения числа соединений с СУБД. Число терминалов намного больше числа соединений с СУБД, поэтому некоторые терминалы должны дождаться свободного соединения (т.е. момента, когда другой терминал перестанет использовать соединение).

Мы запустили TPC-C и бенчмарк завис. К счастью, дебажить такое очень просто:

  1. Получите стеки, используя команду jstack -p <PID>.

  2. Получите дамп с более детальной информацией, включая данные о потоках-носителях и виртуальных потоках с помощью команды jcmd <PID> Thread.dump_to_file -format=text jcmd.dump.1.

Мы обнаружили, что виртуальные потоки, ожидающие соединения, захватывают поток-носитель и не отпускают его. Вот стек виртуального потока:

#7284 "TPCCWorker<7185>" virtual
      java.base/java.lang.Object.wait0(Native Method)
      java.base/java.lang.Object.wait(Object.java:366)
      com.mchange.v2.resourcepool.BasicResourcePool.awaitAvailable(BasicResourcePool.java:1503)
      com.mchange.v2.resourcepool.BasicResourcePool.prelimCheckoutResource(BasicResourcePool.java:644)
      com.mchange.v2.resourcepool.BasicResourcePool.checkoutResource(BasicResourcePool.java:554)
      com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutAndMarkConnectionInUse(C3P0PooledConnectionPool.java:758)
      com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutPooledConnection(C3P0PooledConnectionPool.java:685)
      com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource.getConnection(AbstractPoolBackedDataSource.java:140)
      com.oltpbenchmark.api.BenchmarkModule.makeConnection(BenchmarkModule.java:108)
      com.oltpbenchmark.api.Worker.doWork(Worker.java:428)
      com.oltpbenchmark.api.Worker.run(Worker.java:304)
      java.base/java.lang.VirtualThread.run(VirtualThread.java:309)

и стек потока-носителя:

"ForkJoinPool-1-worker-254" #50326 [32859] daemon prio=5 os_prio=0 cpu=12.39ms elapsed=489.99s tid=0x00007f3810003140  [0x00007f37abafe000]
   Carrying virtual thread #7284
        at jdk.internal.vm.Continuation.run(java.base@21.0.1/Continuation.java:251)
        at java.lang.VirtualThread.runContinuation(java.base@21.0.1/VirtualThread.java:221)
        at java.lang.VirtualThread$$Lambda/0x00007f3c2424e410.run(java.base@21.0.1/Unknown Source)
        at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(java.base@21.0.1/ForkJoinTask.java:1423)
        at java.util.concurrent.ForkJoinTask.doExec(java.base@21.0.1/ForkJoinTask.java:387)
        at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(java.base@21.0.1/ForkJoinPool.java:1312)
        at java.util.concurrent.ForkJoinPool.scan(java.base@21.0.1/ForkJoinPool.java:1843)
        at java.util.concurrent.ForkJoinPool.runWorker(java.base@21.0.1/ForkJoinPool.java:1808)
        at java.util.concurrent.ForkJoinWorkerThread.run(java.base@21.0.1/ForkJoinWorkerThread.java:188)

Видно, что зависание происходит на вызове Object.wait(), который используется вместе с synchronized. Из-за этого поток-носитель захватывается и не освобождается для выполнения другого виртуального потока. При этом потоки, владеющие соединением с базой, освобождают потоки-носители, т.к. выполняют сетевой ввод-вывод:

      java.base/java.lang.VirtualThread.park(VirtualThread.java:582)
      java.base/java.lang.System$2.parkVirtualThread(System.java:2639)
      java.base/jdk.internal.misc.VirtualThreads.park(VirtualThreads.java:54)
      java.base/java.util.concurrent.locks.LockSupport.park(LockSupport.java:369)
      java.base/sun.nio.ch.Poller.pollIndirect(Poller.java:139)
      java.base/sun.nio.ch.Poller.poll(Poller.java:102)
      java.base/sun.nio.ch.Poller.poll(Poller.java:87)
      java.base/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:175)
      java.base/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:201)
      java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:309)
      java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:346)
      java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:796)
      java.base/java.net.Socket$SocketInputStream.read(Socket.java:1099)
      java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:489)
      java.base/sun.security.ssl.SSLSocketInputRecord.readHeader(SSLSocketInputRecord.java:483)
      java.base/sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(SSLSocketInputRecord.java:70)
      java.base/sun.security.ssl.SSLSocketImpl.readApplicationRecord(SSLSocketImpl.java:1461)
      java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:1066)
      org.postgresql.core.VisibleBufferedInputStream.readMore(VisibleBufferedInputStream.java:161)
      org.postgresql.core.VisibleBufferedInputStream.ensureBytes(VisibleBufferedInputStream.java:128)
      org.postgresql.core.VisibleBufferedInputStream.ensureBytes(VisibleBufferedInputStream.java:113)
      org.postgresql.core.VisibleBufferedInputStream.read(VisibleBufferedInputStream.java:73)
      org.postgresql.core.PGStream.receiveChar(PGStream.java:465)
      org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2155)
      org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:574)
      org.postgresql.jdbc.PgStatement.internalExecuteBatch(PgStatement.java:896)
      org.postgresql.jdbc.PgStatement.executeBatch(PgStatement.java:919)
      org.postgresql.jdbc.PgPreparedStatement.executeBatch(PgPreparedStatement.java:1685)
      com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.executeBatch(NewProxyPreparedStatement.java:2544)
      com.oltpbenchmark.benchmarks.tpcc.procedures.NewOrder.newOrderTransaction(NewOrder.java:214)
      com.oltpbenchmark.benchmarks.tpcc.procedures.NewOrder.run(NewOrder.java:147)
      com.oltpbenchmark.benchmarks.tpcc.TPCCWorker.executeWork(TPCCWorker.java:66)
      com.oltpbenchmark.api.Worker.doWork(Worker.java:442)
      com.oltpbenchmark.api.Worker.run(Worker.java:304)
      java.base/java.lang.VirtualThread.run(VirtualThread.java:309)

Таким образом, мы оказываемся в следующей ситуации:

  1. Абсолютно все потоки-носители захвачены виртуальными потоками, ожидающими соединения с базой.

  2. Виртуальные потоки, владеющие соединением с базой, не могут завершить запрос и освободить сессию, т.к. нет свободных потоков-носителей.

Дедлок на раз-два!

Согласно JEP 444:

There are two scenarios in which a virtual thread cannot be unmounted during blocking operations because it is pinned to its carrier:

When it executes code inside a synchronized block or method, or
When it executes a native method or a foreign function.

Проблема в том, что код с synchronized может находится глубоко внутри используемых библиотек. В нашем случае это c3p0. Исправление очень простое: мы обернули получение соединений в java.util.concurrent.Semaphore. Благодаря этому виртуальные потоки теперь блокируются на семафоре, отпуская поток-носитель, а внутрь c3p0 попадают только тогда, когда есть свободное соединение.

Заключение

Обложка книги Фреда Брукса "Мифический человекомесяц". Авторские права на обложку книги принадлежат издательству Addison-Wesley или художнику-иллюстратору
Обложка книги Фреда Брукса "Мифический человекомесяц". Авторские права на обложку книги принадлежат издательству Addison-Wesley или художнику-иллюстратору

Похоже, что несмотря на десятилея улучшений в области разработки ПО, серебряной пули по-прежнему не существует. Тем не менее виртуальные потоки Java 21 - значимая фича, благодаря которой синхронный код становится эффективным. Но используйте эту фичу осторожно и всё будет хорошо.

Комментарии (29)


  1. Andrey_Solomatin
    15.01.2024 15:11
    +4

    Проблема в том, что код с synchronized может находится глубоко внутри используемых библиотек


    Асинхронные библиотеки плохо работают в связке с синхронными. Например асинхронный веб сервер будет так-себе, если использовать синхронный клиент внутри.

    В Java, во имя обратной совместимости, порой реализовывают интерфейсы через одно место. Например объект типа List созданный черезunmodifiableList имеет метод add, который бросает рантайм исключение.

    Ну и комбо асинхронный объект в интерфейсе Thread. При малом количестве задач, это часто будет просто неэффективный код, который гоняет event loop, но по факту гоняте всё синхронно на ThreadPool. Ну а если задач много, то всё подвиснет, как в статье.

    Асинхронность это очень крутой инструмент, но кроме производительности он открывает бездны возможностей сделать всё еще медленней.


    1. maxzh83
      15.01.2024 15:11

      Асинхронные библиотеки плохо работают в связке с синхронными

      В примере не вижу, чтобы использовались асинхронные библиотеки в связке с синхронными. Виртуальные потоки придуманы как раз для того, чтобы писать привычный синхронный код, а вся "асинхронность" уйдет в недра JVM. И авторы project loom везде предупреждают, что с synchronized блоками и native это работать не будет (в лучшем случае не будет никакого полезного эффекта), о чем и написано в конце статьи. Такое вот ограничение, которое тут и стреляет. Проблема в том, что еще много где продолжают использоваться synchronized блоки, даже внутри классов самой JVM, не говоря уже о куче сторонних библиотек. Но, надеюсь, со временем все это допилится.

      В Java, во имя обратной совместимости, порой реализовывают интерфейсы через одно место.

      Обратная совместимость это одна из сильных сторон Java. И идея, что можно по щелчку превратить все потоки в виртуальные в уже написанном коде без его переписывания, мне кажется очень крутой. Даже не смотря на то, что при столкновении с реальностью всплывает много разных нюансов.


      1. Andrey_Solomatin
        15.01.2024 15:11

        В примере не вижу, чтобы использовались асинхронные библиотеки в связке с синхронными.


        Пул потоков в данном случае не асинхронный.

        Виртуальные потоки придуманы как раз для того, чтобы писать привычный синхронный код, а вся "асинхронность" уйдет в недра JVM. И авторы project loom везде предупреждают, что с synchronized блоками и native это работать не будет (в лучшем случае не будет никакого полезного эффекта), о чем и написано в конце статьи.

        Задумка хороша, просто пока надо знать много деталей реализации, чтобы это приносило пользу.


        1. maxzh83
          15.01.2024 15:11

          Пул потоков в данном случае не асинхронный.

          Тик а что тут вообще асинхронное?

          просто пока надо знать много деталей реализации

          Пока это все сыровато, то да. Потом как раз думать не надо будет об этом


  1. dididididi
    15.01.2024 15:11

    100 конекшенов к базе, но почему бы не юзать их в 100000 потоков.

    Голдратт, да? Если узкое место БД, то извращения в жаве тебя не спасут.

    Ладно тупой вопрос: работать быстрее стало?))


    1. eivanov Автор
      15.01.2024 15:11
      +2

      100 конекшенов к базе, но почему бы не юзать их в 100000 потоков.

      Проблема, которую мы решали, как раз обусловлена тем, что в отличие от виртуальных крайне проблематично запустить 100K физических потоков.

      Если узкое место БД, то извращения в жаве тебя не спасут.

      Речь в первую очередь о клиенте (бенчмарке), а не какой-либо БД.

      Ладно тупой вопрос: работать быстрее стало?))

      Да, благодаря переходу на виртуальные потоки мы теперь можем запускать на одном сервере десятки и даже сотни тысяч терминалов TPC-C. А до этого приходилось запускать на нескольких серверах. Изначально мы стали оптимизировать бенчмарк по очень простой причине: чтобы нагрузить кластер YDB из 3 серверов (по 128 ядер и 512 GB RAM), требовалось 3-5 таких же серверов для запуска TPC-C. А что если мы хотим нагрузить кластер из 100 серверов? :) В этой презентации я подробно рассказал об этой и некоторых других проблемах производительности бенчмарков, которые нам пришлось решить.


      1. dididididi
        15.01.2024 15:11

        "Метрика производительности, представленная в TPC-C, - это "бизнес-производительность", измеряющая количество обработанных заказов в минуту." -

        Короче строили самую быструю тачку, построили которая жрёт мало бенза.

        Ээ.. Цель достигнута?


      1. maxzh83
        15.01.2024 15:11

        Да, благодаря переходу на виртуальные потоки мы теперь можем запускать на одном сервере десятки и даже сотни тысяч терминалов TPC-C

        В статье очень не хватает результатов работы, а также кода как именно поправили с семафором


        1. eivanov Автор
          15.01.2024 15:11
          +3

          В статье очень не хватает результатов работы

          В самом начале есть ссылка на наш предыдущий пост о TPC-C, где все результаты: https://habr.com/ru/companies/ydb/articles/763938/

          а также кода как именно поправили с семафором

          В тексте есть ссылка на коммит с этим исправлением: https://github.com/ydb-platform/tpcc/commit/175f0c03d9c16652c85a6103331fec473017797e


          1. Kelbon
            15.01.2024 15:11

            Сильное исправление, которое ломает весь остальной код кроме конкретного бенчмарка


            1. eivanov Автор
              15.01.2024 15:11

              Сильное исправление, которое ломает весь остальной код кроме конкретного бенчмарка

              Benchbase - фреймворк для написание бенчмарков и добавления СУБД. Мы оттуда используем только TPC-C. Но несложно добавить returnConnection() в другие бенчмарки, если потребуется.


      1. dididididi
        15.01.2024 15:11
        -1

        А можно для тупых?

        У вас есть жава, которая тупо прикидывает запросы в БД. У вас 700 ядер и 2,5 терабайта ram держат этакий аналог nginx.

        100к запросов, это 100ГБ RAM для жавы, вы упёрлись в количество потоков, не буду врать в реальной жизни такого никогда не было, поэтому интересно конечно как это делать.

        Но какая то надуманная ситуация, что жавочный передаст жрет столько же ресурсов сколько бд


        1. eivanov Автор
          15.01.2024 15:11
          +1

          У вас 700 ядер и 2,5 терабайта ram держат этакий аналог nginx.

          Про nginx не понял, что имеете в виду. У нас есть СУБД (YDB и PostgreSQL). Нам нужен бенчмарк, чтобы оценить производительность. TPC-C как раз очень популярный бенчмарк для OLTP систем. Проблема оказалось в том, что общедоступная реализация TPC-C потребляет слишком много ресурсов и нам пришлось решать эту проблему, т.к. в нашем случае (YDB) речь идёт о сотнях серверов под СУБД и не вариант заводить дополнительно сотни серверов под запуск бенчмарка. Самый свежий пример: вчера один из клиентов попросил результаты TPC-C для YDB, работающей на 18 серверах (каждый 128 ядер, 1 TB RAM и 6 NVMe дисков). Без улучшений TPC-C нам бы потребовалось как минимум ещё 18 таких серверов, а с улучшениями должно хватить двух.


          1. dididididi
            15.01.2024 15:11
            -1

            Я прочитал tpc-c. Не до конца понял.

            Вам нужно нагрузить БД?

            Жава- это обвязка для нагрузки, вы там в цикле фигачите транзакции и в какой-то момент жава посыпалась потому что условный линух падает при 100000 потоках?

            Если так, то рост потоков вызван перегрузкой БД, это она не тащит и подвисает. Или я не понял?

            Или TpC-c ещё включает рест в себя?


            1. eivanov Автор
              15.01.2024 15:11
              +1

              Да, нам нужно нагрузить БД. Для этого мы используем бенчмарк TPC-C, который написан на Java. Большое число потоков обусловлено тем, как реализован бенчмарк и совсем не связано с БД.


              1. dididididi
                15.01.2024 15:11

                Я к тому, что БД тупит, жава потоки не закрывает и копит их.

                Эта обвязка из жавы не вами сделана? Вы взяли какую то испытательную жава проект, а из-за того что она сломалась, вы её подшаманили слегка?

                Ну если вы не нарушили условий испытаний, то ништяк наверно.


                1. Andrey_Solomatin
                  15.01.2024 15:11
                  +1

                  Я так понял это код нарузочных тестов от компании автора статьи.

                  База как раз не тупит, а со старой версией тестов, могла переварить все на что способна машина которая теститрует. Машину упирался в предел потоков и приходилось использовать несколько машин.


                  1. dididididi
                    15.01.2024 15:11

                    Да, шаблон порвался.

                    Я привык, что БД всегда медленней, соответсвеено она всегда рухнет быстрей, чем жава.

                    Тут автор рассказывает как порешать с помощью виртуальных потоков, крайне редкий специфичный случай,когда жава не делает нифига кроме как в цикле фигачит новые бесконечные потоки, которые в итоге кладут линух, потому что напротив сверхмощная БД.

                    Ясно, понятно. Не понятно, как применить в реальной жизни. Программирование бенчмарков(это такая испытательная программа) редкая специфичная вещь.


                    1. maxzh83
                      15.01.2024 15:11
                      +1

                      Не понятно, как применить в реальной жизни

                      Виртуальные потоки придумали как раз для реальной жизни. Смотрите, в большинстве бэкенд-приложений на Java (кроме реактивных) каждый запрос обрабатывается в отдельном потоке и на это время поток по сути блокируется. В мире микросервисов очень часто бывает, что при обработке запроса сервис внутри отправляет запросы в пачку других сервисов, по сути ничего не делая и ожидая от них ответ. Поток в это время просто висит и ждет. Потоки ОС, как выше уже писали, достаточно тяжелые для выделения, переключения и т.д., кроме этого есть ограничения на их количество. Вот для таких случаев и нужны виртуальные потоки, они позволяют лучше масштабироваться и эффективнее утилизировать железо.


                      1. dididididi
                        15.01.2024 15:11

                        Это не про виртуальные потоки статья.

                        Это про дедлоки БД в tpc-c.


                      1. eivanov Автор
                        15.01.2024 15:11

                        Нет, maxzh83 Вам выше очень верно описал суть. TPC-C это бенчмарк, дедлоки возникли именно в нём. СУБД здесь не при чём.


                      1. Andrey_Solomatin
                        15.01.2024 15:11

                        Виртуальные потоки придумали как раз для реальной жизни.


                        Звучит немного громко. Это всего лишь еще одна реализация асинхронности. Идее уже не один десяток лет и асинхронные код можно было писать и раньше.

                        Придумали тут только новую обертку для этого. Со своими плюсами и минусами.


                      1. maxzh83
                        15.01.2024 15:11
                        +1

                        Звучит немного громко. Это всего лишь еще одна реализация асинхронности

                        Так и асинхронность изобрели для реальной жизни, а не от скуки)


  1. Deosis
    15.01.2024 15:11

    В любом случае есть две конкурентные (concurrent) задачи: ожидание приложением ответа

    Довольно расточительно блокировать интерфейс, ожидая окончания фоновой задачи.


    1. eivanov Автор
      15.01.2024 15:11
      +2

      Довольно расточительно блокировать интерфейс, ожидая окончания фоновой задачи.

      В этом суть горутин: сделать возможным делать это максимально дёшево. И virtual threads идет к той же цели.


  1. murkin-kot
    15.01.2024 15:11
    -1

    Я конечно понимаю, что нужен какой-то "авторитетный" результат, и потому использовано "одобренное" решение. Но если уж вы взялись его использовать, то почему вы не подумали о смысле действий в "одобренном" решении?

    Смысл там такой - "авторитеты" почему-то считают важным, что бы каждый клиент обязательно долго ждал перед отправкой новой транзакции. Вы же, своим исправлением в соответствующем тесте, это ожидание исключили. То есть натянули штамп "одобрено" на ваши результаты. Хотя одобрявшие набор тестовых операций не одобрили бы ваше нововведение.

    Ну и очевидное следствие - все те, кому вы будете демонстрировать свои результаты, будут введены в заблуждение из-за наличия отклонения от рекомендаций авторитетов в вашей реализации.

    Хотя обычно все те, кому вы будете впаривать, давно знают, что все вокруг стремятся именно впарить, а потому не придадут особого значения вашим результатам. И таким образом круг "купи слона, он мытый, зуб даю!" обязательно завершится как полагается - куплю дарагой, куплю! Но помой его при мне ещё раз.

    ЗЫ. Проще надо быть, тесты бы легче писать было. А впаривают пусть специально обученные люди. И если они не справляются, то ваши неумелые усилия на этом поприще уже никому не помогут. Так что писать надо было что-то простое, а впариватели бы далее сумели (должны!) это всё продать.


    1. eivanov Автор
      15.01.2024 15:11
      +2

      Вы задались важным вопросом, как могут повлиять наши изменения на результаты бенчмарка. Наверное, стоило проговорить это более явно в самом начале поста. Наши изменения абсолютно никак не влияют на результаты. Времена всех задержек прописаны в стандарте TPC-C, они используются в оригинальном benchbase и сохранены в нашем форке. Более того, мы в процессе доработки pull request в benchbase и уже прошли ревью изменений. Что же тогда делают наши изменения? Они позволяют запустить бенчмарк на меньшем количестве железа, что позволяет снизить расходы на исследование производительности СУБД.

      Повторюсь, что нам следовало разобрать более подробно некоторые базовые моменты, чтобы у Вас не возникло ощущение того, что Вам что-то впаривают. Мне кажется, что неплохим доказательством нашей добропорядочности является то, что английская версия поста сейчас на главной странице hacker news, а запостили её туда наши прямые конкуренты YugabyteDB, которые тоже используют Benchbase. Мы очень активно сотрудничаем как с ними, так и с другими конкурентами, чтобы получить честное сравнение наших баз. И именно поэтому активно вкладываемся в бенчмарки.


      1. murkin-kot
        15.01.2024 15:11
        -1

        Конечно, вы стараетесь показать себя с лучшей стороны. Но сам факт допиливания тестов говорит об одном - вы внесли изменения. Дальше можно спорить, на сколько они важны, но само по себе внесение изменений - факт неоспоримый. Именно про него я и сообщил. Ну а оправдания, будь то меньше железа или ещё что, это же вторично в сравнении с отходом от методики, разве нет?

        С другой стороны, понимаю, что ваш маркетинг решил давить на результаты тестов, ну и поэтому вы обязаны их здесь защищать. Плюс, видимо, какие-то ваши личные убеждения в качестве лично ваших усилий могли пострадать. Только это всё опять же вторичные факторы на фоне восприятия информации читателями. Подумайте лучше, как подать информацию так, что бы читатели даже не задумались бы о мелочах, или о ваших исправлениях тестов.

        Вообще, сам подход типа "мы выполнили кучу действий по инструкции" мало кому интересен. Ну да, при этом вы вышли на факт использования не адаптированного к новым API вызова в выбранной вами библиотеке, но это же ваш косяк - вы решили использовать новое не проверив совместимость со старым. Так что такую особенность выполнения действий по инструкции тоже нельзя признать захватывающей внимание читателей. Хотя кому-то знакомому с темой вы могли подсказать про synchronized и native, но их ведь очень мало в сравнении с остальными читателями.

        Правда пока не могу ткнуть пальцем в личный пример "как надо", но наверное я это как-нибудь сделаю.


        1. eivanov Автор
          15.01.2024 15:11
          +1

          Мне очень жаль, что Вы остались при своём мнении и, судя по всему, не ознакомились ни с пул реквестом, ссылку на который я дал и где есть комментарии авторов Benchbase, ни со стандартом TPC-C и сутью того, чем мы вообще занимаемся.

          Мы будем рады, если Вы укажете нам на конкретные проблемы бенчмаркинга, потому что мы стараемся сделать наши результаты максимально прозрачными и верными.