Недавно мы переделали наши системные потоки. После того как мы форкнули Sphinx, прежний searchd выполнял все системные задачи в стиле часто‑просыпающегося процесса. Каждый сервис работал в отдельном потоке, который просыпался каждые 50 мс и проверял, есть ли что‑то делать. Это означало, что даже простой демон просыпался 200 раз в секунду «только чтобы проверить» задачу. При наличии 6 таких задач получалось 1200 пробуждений в секунду, и это стало заметно, особенно для клиентов на Amazon AWS, которые учитывают использование CPU. Эти внутренние задачи были:
- простая ротация индекса
- пинг агента
- предзагрузка индекса
- сброс атрибутов индекса
- сброс RealTime‑индекса
- и сброс бинарных журналов
Все они довольно редки (например, сброс RT может происходить раз в 10 часов), поэтому проверять их 200 раз в секунду — просто трата CPU. Кроме того, все эти задачи по своей природе не являются «тяжёлыми» (как, например, добавление текста в огромный журнал), а представляют собой лишь периодические действия (повторяющие одну и ту же задачу периодически). Учитывая такой характер действий, мы полностью переписали весь механизм:
- Сначала мы добавили один поток, обслуживающий таймеры, где каждая операция планируется.
- Затем мы добавили пул потоков для выполнения самих операций.
- Таким образом, сервисная операция (задача) планируется, и когда срабатывает таймер, она перемещается в пул потоков и затем выполняется.
- После завершения она удаляется, поэтому периодические задачи просто переназначают себя в конце и создают совершенно новую задачу.
При таком подходе нет множества выделенных сервисных потоков, а лишь один поток‑таймер. И он, в свою очередь, не просыпается каждые 20 мс, а использует бинарную кучу с тайм‑аутами и просыпается только в момент, указанный самым ранним таймером в очереди. Пул потоков, в свою очередь, может запускать до 32 потоков параллельно, но на практике одновременно работает лишь один, иногда два. Каждый поток имеет предопределённый период простоя (10 мин), после которого он просто завершается, поэтому при отсутствии задач ничего не выполняется в простое (даже поток‑таймер инициализируется «лениво», т.е. запускается только когда требуется запланировать реальную задачу). В случае очень редких задач (период более 10 минут) пул рабочих потоков также закрывается, и рабочие потоки создаются только когда есть что‑то делать. Таким образом, все сервисные потоки удаляются и больше не «бьют» CPU сотни раз в секунду.
Самым перспективным изменением такого подхода стало новое поведение задачи «ping». Раньше мы собирали все хосты‑агенты и на каждом интервале пинга отправляли команду «ping» им сразу группой. Поэтому, если один хост был медленным, вся группа замедлялась. Кроме того, это не имело отношения к реальному состоянию хостов — например, когда вы загружаете хост запросами, нет необходимости отдельно пинговать его, так как сами запросы предоставляют полную статистику о состоянии хоста. Теперь пинг конкретен: он планируется для каждого хоста отдельно и привязан к реальному времени last_answer_time каждого из них. Если хост медленный — только его задача ping будет ждать, остальные будут работать нормально в своё время. Если хост перегружен — его last_answer_time будет монотонно обновляться, поэтому реальный ping не произойдёт, если с момента last_query_time уже был выполнен запрос и прошёл интервал ping_interval.
Еще одна новая возможность — задачи могут работать параллельно. Например, сброс индексов может выполняться для любого количества индексов одновременно, а не последовательно. Сейчас это число установлено в 2 задачи, но это лишь вопрос настройки. Кроме того, когда запланировано несколько похожих задач, теперь мы можем ограничить их количество. Например, «malloc_trim» не имеет смысла планировать более одного раза, поэтому это своего рода «singleton» — если одна задача уже запланирована, попытка запланировать ещё одну будет отклонена.
Следующая функция, добавленная в рамках такого управления задачами, вытекает из того, что теперь все задачи планируются в одной очереди (а не в разных группах потоков), и мы точно знаем, когда они будут выполнены. Поэтому теперь такие статистические данные могут быть отображены, и это реализовано с помощью команд «debug sched», «debug tasks» и «debug systhreads».
Первая показывает бинарную кучу таймера: верхнее значение указывает следующий тайм‑аут и связанную с ним задачу; остальные значения идут далее (в данный момент они отображаются как необработанная бинарная куча; при необходимости их можно будет отсортировать).
«debug tasks» показывает все задачи, зарегистрированные в демоне, вместе со статистикой и свойствами (сколько таких задач может выполняться параллельно; сколько может быть запланировано; сколько сейчас выполняется, сколько времени потрачено на задачу, когда она была завершена в последний раз, сколько раз она была выполнена, сколько было отброшено из‑за перегрузки очереди и сколько сейчас находится в очереди).
Последняя команда «debug systhreads» отображает состояние рабочих потоков: внутренний идентификатор, идентификатор потока, время последнего запуска, общее время CPU, общее количество тиков и выполненных задач, сколько времени заняла последняя задача и как долго поток находится в простое. Как уже упоминалось, простой в течение 10 минут приводит к остановке рабочего потока.
Обновитесь до Manticore 3.1.0 или более новой версии чтобы воспользоваться этим изменением