Архитектура изнутри: процесс, потоки, event-loop

Рейтинг: 64.6% · 12 голосов
Исчерпывающий курс по Tarantool 3.x: модель данных, движки memtx и vinyl, Lua и файберы, транзакции и MVCC, SQL, конфигурация (box.cfg и декларативная 3.x), репликация и Raft, шардирование vshard, эксплуатация, безопасность. 47 уроков со схемами.
Ответить
Аватара пользователя
denis_tnt
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Архитектура изнутри: процесс, потоки, event-loop

Сообщение denis_tnt »

Оглавление курса (47)
  1. Что такое Tarantool: in-memory СУБД и сервер приложений
  2. Архитектура изнутри: процесс, потоки, event-loop (вы здесь)
  3. Установка и первый запуск: tt CLI, пакеты, Docker
  4. Интерактив: консоль, admin-консоль, первые команды
  5. Спейсы и кортежи: форматы, типы данных
  6. Типы индексов и их применимость
  7. Движки хранения: memtx vs vinyl
  8. DDL: схема, создание спейсов и индексов, миграции
  9. DML и выборки: insert/update/upsert, итераторы
  10. Персистентность: WAL, снапшоты, recovery
  11. Внутренности memtx: аллокаторы slab/arena, память
  12. Внутренности vinyl: LSM, компакция, тюнинг
  13. Lua и LuaJIT в Tarantool: box, модули, rocks
  14. Файберы: кооперативная многозадачность, каналы
  15. Транзакции: ACID, изоляция, MVCC
  16. Хранимые процедуры, модули, организация приложения
  17. net.box: удалённые вызовы, async
  18. Пулы соединений, балансировка, реконнект
  19. Ошибки и диагностика: box.error, pcall
  20. Типы и сериализация: MsgPack, decimal, datetime, uuid
  21. SQL в Tarantool: возможности и связь с box
  22. SQL: таблицы, JOIN, подзапросы, представления
  23. SQL: подготовленные выражения, транзакции, Lua-интероп
  24. Классическая конфигурация box.cfg (legacy 1.x/2.x)
  25. Декларативная конфигурация 3.x: config.yaml, иерархия
  26. Роли и приложения в 3.x
  27. Централизованная конфигурация: etcd / config storage
  28. tt CLI глубоко: разработка, сборка, запуск
  29. Cartridge (официальный legacy) и миграция на 3.x
  30. Репликация: replicaset, топологии
  31. Механика репликации: WAL-стриминг, vclock
  32. Синхронная репликация и выборы лидера (Raft)
  33. Жизненный цикл узла: bootstrap, join, rejoin
  34. vshard: router/storage, виртуальные бакеты
  35. Решардинг и rebalancing бакетов
  36. Запросы поверх шардов: map-reduce, crud
  37. Мониторинг: метрики, Prometheus, Grafana
  38. Логирование и аудит
  39. Бэкапы и восстановление
  40. Безопасность: аутентификация, RBAC, TLS
  41. Производительность: профилирование, тюнинг
  42. Обновления: схема, rolling upgrade
  43. Деплой в продакшен: Docker, топология (официальные паттерны)
  44. Администрирование через официальный TCM (Tarantool Cluster Manager)
  45. Коннекторы: Python, Go, Java
  46. Ключевые модули (rocks): crud, metrics, queue, expirationd
  47. Capstone: шардированный отказоустойчивый кластер
Краткий обзор

Многие приходят в Tarantool с ожиданием, что это очередная многопоточная СУБД, где десятки рабочих потоков параллельно лезут в одни и те же таблицы под защитой блокировок. На деле всё иначе. Tarantool - это один ОС-процесс, внутри которого работает небольшой набор узкоспециализированных потоков, а вся бизнес-логика и все изменения данных живут в единственном потоке. Понять этот факт - значит понять, почему Tarantool такой быстрый, почему в нём почти нет гонок данных и почему один тяжёлый цикл на Lua способен застопорить весь инстанс.

В этом уроке разбираемся, какие потоки крутятся внутри процесса tarantool, кто за что отвечает, что такое event-loop и кооперативная многозадачность на файберах.

Механика: потоки одного процесса

Запущенный инстанс - это один процесс. Внутри него ОС-потоки (в коде они называются cord - cord = core + thread) с чёткими ролями. Каждый cord несёт собственный event-loop на базе библиотеки libev. Между собой потоки не делят данные напрямую и не берут мьютексы на каждый чих - они обмениваются сообщениями через внутреннюю шину cbus (пара однонаправленных каналов cpipe). Это и есть ключ к производительности: нет общего изменяемого состояния под локами.

TX (transaction) thread - сердце инстанса. Это единственный поток, который имеет право трогать данные: индексы memtx и vinyl, кортежи, схему. Здесь же исполняется весь ваш Lua-код, хранимые функции, триггеры, SQL. Раз поток один и переключение задач в нём кооперативное, две транзакции физически не могут одновременно писать в один индекс. Отсюда отсутствие классических race condition внутри одного инстанса.

IPROTO thread(s) - сетевой фронт. Этот поток (а с версии 2.x их может быть несколько, см. ниже) читает байты из сокетов, разбирает бинарный протокол iproto поверх MessagePack, складывает разобранный запрос в сообщение и отправляет его по cpipe в TX-поток. Получив ответ обратно, он кодирует его в MessagePack и пишет в сокет. Смысл прост: грязную и медленную работу с сетью снимают с TX-потока, чтобы тот занимался только данными.

WAL thread - дисковый писарь. Когда транзакция готова к коммиту, TX формирует сообщение и отдаёт его WAL-потоку, который добавляет запись в .xlog (write-ahead log) и делает write/fsync на диск. Пока WAL пишет, TX-поток не простаивает - он принимает новые транзакции и готовит следующий пакет. Несколько коммитов от разных файберов объединяются в один батч и сбрасываются на диск одной операцией (group commit) - это резко поднимает пропускную способность записи.

Кроме этой тройки в процессе есть и другие потоки: на каждую подключённую реплику создаётся отдельный relay-поток (он читает WAL и шлёт его реплике, у каждой свой темп и своя позиция в логе), есть рабочие потоки vinyl для компакции и фоновые потоки для тяжёлых операций. Но базовая модель, которую надо держать в голове - именно IPROTO -> TX -> WAL.

Изображение
Потоки процесса и event-loop с файберами

Event-loop и кооперативная многозадачность

Внутри TX-потока нет потоков ОС на каждый запрос. Вместо них - файберы (fibers): легковесные корутины с собственным стеком, которыми управляет планировщик внутри Tarantool, а не ядро ОС. Файберов могут быть тысячи, переключение между ними стоит копейки.

Главное слово - кооперативная. В любой момент времени бежит ровно один файбер. Он работает, пока сам не уступит управление (yield). Планировщик не вытесняет файбер принудительно. Когда активный файбер делает yield, управление уходит планировщику, тот отдаёт его другому готовому файберу, а если готовых нет - возвращает в event-loop ev_run(), который ждёт событий (готовность сокета, срабатывание таймера, ответ от WAL) и будит соответствующие файберы.

Yield бывает двух видов:
  • Явный - вы сами вызвали fiber.yield() или fiber.sleep().
  • Неявный - любая операция, которая ждёт внешнего события: запись в WAL при коммите, сетевой запрос через net.box, чтение/запись сокета, файловые операции fio, передача через fiber.channel. Под капотом такая операция ставит файбер в ожидание и уступает управление.
Практический вывод: между двумя yield ваш код выполняется атомарно, никто в него не вклинится. Это бесплатные критические секции - не нужны мьютексы, если внутри нет ни одного yield.
Кооперативность - обоюдоострый меч. Если файбер крутит долгий цикл без единого yield (например, обходит миллион кортежей чистым Lua без обращений к сети и диску), он монополизирует TX-поток. Весь инстанс встаёт: новые запросы не обрабатываются, репликация отстаёт, heartbeat не уходит. Tarantool не отнимет управление силой - это ответственность автора кода.
Ключевые команды и код

Создание файбера и явный yield:

Код: Выделить всё

local fiber = require('fiber')

local f = fiber.create(function()
    for i = 1, 5 do
        print('tick', i)
        fiber.sleep(0.1)   -- явный yield: уступаем управление на 0.1 c
    end
end)

print('fiber id:', f:id(), 'status:', f:status())
Демонстрация неявного yield на коммите. box.commit() уходит в WAL и усыпляет файбер до подтверждения записи:

Код: Выделить всё

box.cfg{}
box.schema.space.create('s', {if_not_exists = true})
box.space.s:create_index('pk', {if_not_exists = true})

box.begin()
box.space.s:replace{1, 'a'}
box.commit()   -- здесь файбер неявно yield-ит, пока WAL не подтвердит запись
Посмотреть распределение времени по файберам и нагрузку на event-loop:

Код: Выделить всё

require('fiber').info()        -- список файберов, их память и CSW (переключения)
require('fiber').top()         -- доля CPU по файберам (нужно fiber.top_enable())
box.stat()                     -- счётчики запросов по типам
box.info.replication           -- лаг реплик: растёт, если TX залип
Декларативная (3.x) настройка числа IPROTO-потоков в config.yaml:

Код: Выделить всё

iproto:
  threads: 4          # сколько сетевых потоков обслуживают сокеты
  net_msg_max: 768    # лимит одновременно обрабатываемых сообщений
  readahead: 16320    # размер буфера предчтения на соединение
Классический (box.cfg) эквивалент тех же параметров:

Код: Выделить всё

box.cfg{
    iproto_threads = 4,
    net_msg_max    = 768,
    readahead      = 16320,
}
Частые заблуждения и грабли
  • Больше IPROTO-потоков ускорят сами запросы. Нет. Параметр iproto_threads масштабирует только разбор сети и кодирование ответов. Сама обработка данных всегда в одном TX-потоке - это узкое место по логике, и его потоками не расширить.
  • Файберы дают параллелизм на ядрах. Нет. Файберы - это конкурентность, не параллелизм. Все файберы TX-потока делят одно ядро. Несколько ядер CPU грузят IPROTO/WAL/relay/vinyl-потоки, но не пользовательскую логику.
  • Внутри файбера нужны блокировки. Обычно нет. Пока в критическом участке нет ни одного yield, прерывание невозможно. Но стоит вставить туда сетевой вызов или fiber.sleep - и инвариант рвётся: после yield состояние могло измениться.
  • Тяжёлый цикл - это нормально. Это главная причина деградации. Долгий проход без yield замораживает инстанс. Вставляйте fiber.yield() в длинные циклы или дробите работу.
  • os.execute / блокирующий C - безвредны. Нет. Блокирующий системный вызов не делает yield и останавливает весь поток - вместе с ним стоят все остальные файберы.
Мини-лаба

Запустите интерактивный Tarantool (tt start или просто tarantool) и выполните box.cfg{}. Создайте два файбера: первый печатает числа от 1 до 5 с fiber.sleep(0.2) между ними, второй - буквы a..e тоже с fiber.sleep(0.2). Запустите оба подряд через fiber.create и посмотрите, что вывод чередуется - это и есть кооперативное переключение на неявных yield. Затем уберите sleep из первого файбера, заменив его на пустой цикл for j=1,1e7 do end, и убедитесь, что второй файбер теперь ждёт, пока первый не закончит, - монополизация потока в действии.

Контрольные вопросы
  • Какой поток инстанса и только он имеет право изменять данные и исполнять Lua-код? Почему из-за этого внутри инстанса почти нет гонок данных?
  • Чем занят IPROTO-поток и как он передаёт работу в TX? Ускорит ли увеличение iproto_threads обработку самих запросов?
  • В чём разница между явным и неявным yield? Приведите по два примера каждого.
  • Почему длинный цикл без yield опасен для всего инстанса и как этого избежать?
👍4 ❤️4 🔥 😄 🤔
Ответить
← Предыдущая глава
Что такое Tarantool: in-memory СУБД и сервер приложений
Следующая глава →
Установка и первый запуск: tt CLI, пакеты, Docker

Все главы курса «Tarantool: in-memory СУБД и сервер приложений с нуля до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «Tarantool: СУБД и сервер приложений»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость