История портирования Reindexer'а – как покорить Эльбрус за 11 дней

Всем привет! На связи Антон Баширов, разработчик из ИТ-кластера «Ростелекома». Импортозамещение набирает обороты, а российский софт всё глубже проникает в нашу повседневную ИТ-шную сущность бытия. Процессоры Эльбрус и Байкал становятся более востребованными, комьюнити расширяется, но мысли о необходимости портировать весь наш любимый технологический стек на неизведанную архитектуру E2K звучат страшнее рассказов про горящий в пламени production-кластер.

Работая в команде по внедрению Эльбрусов, мне довелось в прямом и переносном смысле пощупать наши отечественные процессоры, поэтому я хочу поделиться полученным опытом, рассказать о том, какой болевой порог нужен, чтобы выдержать портирование NoSql native базы данных и не сойти с ума, а еще познакомить разработчиков с миром Эльбруса и его обитателями.

Итак, гость в студии — база данных Reindexer, разработка нашего ИТ-кластера.

Стоит сказать, почему выбор пал именно на Reindexer, а не другую БД. Во-первых, всеми любимый и известный Postgres уже есть в составе пакетов ОС Эльбрус. Переносить его нет необходимости. Во-вторых, наш гость уже прошел испытания на ARM, следовательно, пришло время ему покорить Эльбрус.

Стоит упомянуть, что Reindexer появился на свет как продукт выбора между Elastic и Tarantool. Это определенно хочется попробовать завести на отечественном процессоре.

День 0. Знакомство с гостем

Несколько слов про нашего гостя:

Имя:  NoSQL in-memory database Reindexer с функционалом полнотекстового поиска
Возраст:  на момент испытаний версия БД была 3.1.2
Происхождение:  Россия
Место рождения:  пытливый ум разработчика Олега Герасимова @olegator99
Место жительства:  https://github.com/Restream/reindexer
Место работы:  Ростелеком
Строение:  основная составляющая — C++, имеются примеси из языка Ассемблера и немного CMake
Особенности: — Ассемблерные вставки;
— Много С++11 и C++14;
— Используются корутины.

Деятельность: — Хранение данных в памяти и на диске;
— Выполнение SQL запросов со скоростью 500K queries/sec для PK запросов;
— Выполнение запросов типа full-text search (почти как Elastic, только не тормозит);
— Работа в режиме server и embedded;
— Общение с крутыми парнями: Go, Java, Rust, .NET, Python, C/C++ и (да простит меня Хабр) PHP.

Заскучали? На этом нудная часть закончилась. В эфире рубрика «Ээээксперименты»!

День 1. А ты думал, в сказку попал?

Для начала попробуем нашего друга собрать из того, что есть.Идем на тестовый сервер Эльбрус 8C с ОС Эльбрус 6.0.1, клонируем туда репозиторий и запускаем CMake.

2d366c07ef21593a1b25bd085c30d62c.png

Хорошие новости, мы нашли компилятор! Новость действительно хорошая, ведь у Эльбруса свой компилятор — LCC.

К счастью, создатели Эльбрус сделали LCC полностью совместимым с GCC и наши любимые нативные программки и сборщики смогут чувствовать себя хорошо без особых манипуляций. LCC уже сделал за вас нужные линки:  gcc -> /opt/mcst/bin/lcc*.

Зачем Эльбрусу свой компилятор?

Дело в том, что компилятор у Эльбруса не такой, как все. Он выполняет много работы, связанной с анализом зависимостей и оптимизацией:

В архитектуре «Эльбрус» основную работу по анализу зависимостей и оптимизации порядка операций берет на себя компилятор. Процессору на вход поступают т.н. «широкие команды», в каждой из которых закодированы операции для всех исполнительных устройств процессора, которые должны быть запущены на данном такте. От процессора не требуется анализировать зависимости между операндами или переставлять операции между широкими командами: все это делает компилятор, тщательно планируя отдельные операции, исходя из ресурсов широкой команды. В результате аппаратура процессора может быть проще и экономичнее.

Ключевая идея архитектуры — давайте попробуем выполнить несколько операций за один присест. Эльбрусы позволяют реализовать параллельные выполнение кода без применения многопоточности. Достигается это путем одновременного выполнения разных инструкций внутри одной широкой команды на разных устройствах процессора. Но для того, чтобы процессор понимал, что от него хотят и мог правильно распределить инструкции, ему нужно дать на вход оптимизированный поток больших команд. Именно в формировании этого потока и заключается назначение LCC — компилировать код так, чтобы широкие команды содержали параллельно выполняемые инструкции с целью задействовать все устройства CPU. Подробнее тут.

Но вернемся к нашему гостю и тому, что не понравилось CMake.

В скриптах сборки, в CMakeLists.txt выполняется определение операционной системы и архитектуры процессора, на которой собирается Reindexer. Нужно это для того, чтобы подключать правильные исходники ассемблера, ведь как говорится «Write once, run anywhere».

Разумеется, на момент написания скриптов никто не знал про процессоры Эльбрус, поэтому наш скрипт и упал. Исправляем.

Программисты — люди ленивые, поэтому:

97c71127d29bafaecc5d16e8deb2bedf.png

Попытка №2:

8c197e9182e3763573d10fb350da1f86.png

А что так можно было? На самом деле да — для того, чтобы завелся CMake, этого было достаточно. Теперь ударим в бубен и запустим make -j8:

dee44fae8d673ce3a74222c4185c0ff9.png

Для тех, кто уже успел поработать с «кроссплатформенным» софтом, не новость, что С/С++ код с помощью макросов разделяют на платформозависимые секции.

Поэтому, в некоторые места кода Reindexer понадобилось добавить парочку новых условий для __E2K__ и __LCC__:

58f991c1164eb10a0af39e50bd91b8d7.png

После колдовства с макросами нас ждал монстр пострашнее:

cd5889b0b013b270f0e348f07a0cf2b7.png

Вот что бывает, когда игнорируешь messages у CMake.

Из-за того, что CMake не нашел подходящих исходников, компилятор не смог найти реализации двух функций:  jump_fcontext и make_fcontext. Про назначение этих функций я расскажу чуть позже, а пока что давайте успокоим нашего гостя и подсунем ему пару пустышек:

7d8857232aa3114a9509e7f83148852d.png

Этих операций оказалось достаточно, чтобы Reindexer собрался.

Поздравляю, у вас двойня!

Вот они, наши два долгожданных файла:

# file reindexer_server
reindexer_server: ELF 64-bit LSB executable, MCST Elbrus, version 1 (GNU/Linux
# file reindexer_tool
reindexer_tool: ELF 64-bit LSB executable, MCST Elbrus, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux.so.2, for GNU/Linux 2.6.33, with debug_info, not stripped

Это можно считать успехом. Мы смогли собрать наш первый Эльбрус-бинарник. Даже два бинарника: reindexer_server и reindexer_tool.

Подведем итоги. Чтобы собрать Reindexer, мы:

  • Поправили CMake-скрипты;

  • Добавили несколько условий в макросы препроцессора;

  • Заглушили ASM функции.

День 3. Наш Гость, попав на Эльбрус, начал подавать признаки жизни

Первый запуск прошел успешно — сервер поднялся, и даже открылся web-ui.

f011042ed8feb408d0d93fe8da541fa2.png

Я привык не останавливаться на достигнутом и решил немного поработать над Reindexer.

Результат меня порадовал — Гость устал и прилег отдохнуть:

ffeb1715ec287a705d2ccbbe6b2ee0c0.png

Такое поведение наблюдалось только на Эльбрус. На i5 все работало штатно.

Давайте разбираться. Вооружившись отладчиком (gdb кстати под E2K работает прекрасно) и CLion, мне удалось докопаться до истины.

Пару запросов к Reindexer смогли воспроизвести ошибку:

1a355a375cab0ac76f63d804ebe29f21.png

Падает в деструкторе. Интересный момент — упало на free (в данном случае free реализуется через jemalloc). Видимо, здесь идет высвобождение памяти по некорректному указателю. Ошибку эту я исправлял в два этапа:

  1. work around — дело в том, что QueryEntry лежит в объекте ExpressionTree, в самом классе примечательного я не нашел, поэтому копнул в сторону родителя. Оказалось, что до вызова деструктора был вызван вот такой копирующий конструктор, в котором есть интересный MakeDeepCopy (), реализованный с помощью библиотечки mpark-variant.

    Подробнее про expression tree рассказывают тут.

    На часах было 2 часа ночи, я засиделся в офисе и разбираться в variant было уже лениво, поэтому, каюсь, я решил просто убрать вызов этого метода.

    2572cb299d963fbfb2edd54f7c8185a9.png

    Итог — оно заработало.

  2. TODO: Исправление тоже есть, но рассказ про него уже выходит за рамки данной статьи. Небольшой спойлер (код из mpark-variant с патчем под e2k):

inline constexpr DECLTYPE_AUTO visit(Visitor &&visitor, Vs &&... vs)

#ifdef E2K //Fix for Elbrus
    -> decltype(detail::visitation::variant::visit_value(lib::forward(visitor),
                                                 lib::forward(vs)...))
    {
     return detail::visitation::variant::visit_value(lib::forward(visitor),
                                                 lib::forward(vs)...);
    }
#else
    DECLTYPE_AUTO_RETURN(
        (detail::all(
             lib::array{{!vs.valueless_by_exception()...}})
             ? (void)0
             : throw_bad_variant_access()),
        detail::visitation::variant::visit_value(lib::forward(visitor),
                                                 lib::forward(vs)...))
#endif

День 5. Он ожил! Ну почти…

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

Но все это время я упорно игнорировал один компонент.

Помните, как мы убрали завязку на ASM и функции make_fcontext и jump_fcontext ?

Так вот, ASM исходники в Reindexer нужны для реализации C++ корутин, а эти функции — ключевые для корутин из библиотеки boost/context.

Кто такая эта ваша Корутина?

Корутина — это функция, которая может быть приостановлена и возобновлена в любой момент. Для реализации достаточно трех основных компонентов:
— функция
— механизм для ее паузы (suspend)
— механизм для возобновления ее с той точки, в которой была сделана пауза (resume).

Реализацию корутин можно увидеть в таких языках и библиотеках, как:

  • Libcoro (корутины на C/С++)

  • Koishi (тоже корутины, тоже на С/С++)

  • Boost (и это тоже корутины, тоже на С/С++, корутин много не бывает!)

  • Node-fibers (корутины для NodeJS на базе libcoro)

  • Tarantool (fibers на базе libcoro)

  • Kotlin (свои корутины, не на C++)

  • C++20

  • Goroutine

Для наглядности вот скрин теста корутин из Koishi:

436f3ddfc103f1501de64e5a1b669b94.png

Последовательность следующая:

  1. Создали корутину, выделили стек, задали функцию;

  2. Возобновили корутину — в этот момент вызвалась наша функция;

  3. Приостановили корутину с помощью koishi_yield — здесь мы вернули управление в test1 сразу после строчки koishi_resume;

  4. Если мы еще раз сделаем koishi_resume на нашей корутине — мы вернемся на строчку функции cofunc1 сразу после первого вызова koishi_yield.

Без корутин не будет работать reindexer_tool, но не можем же мы оставить Reindexer без такой важной конечности. Будем исправлять.

Начнем с анализа ситуации. Ожидаемо, что ASM Эльбруса бустовские корутины не поддерживают (пока что), а значит нам нужно реализовать их самим. Подробности и реализацию бустовских корутин можете посмотреть здесь, а сейчас наступило время скрещивать ужа с ежом.

Есть несколько вариантов реализовать корутины:

  • Вариант 1:  написать ASM реализацию. Самый очевидный, но при этом самый сложный вариант. У нас есть в планах попробовать именно этот вариант, так как ASM корутины — самые быстрые.

  • Вариант 2: забить. Нет, это не наш путь.

  • Вариант 3: использовать библиотеку.

По счастливой случайности Koishi оказалось той самой библиотекой, поддерживающей реализацию корутин с помощью встроенных E2K функций:  makecontext_e2k() и freecontext_e2k().

Koishi, Koishi, что это вообще такое?

Если коротко — это библиотека, предоставляющая удобный API по использованию корутин на C/C++ с несколькими вариантами реализации:

  • ucontext

  • ucontext_e2k (наша прелесть)

  • fcontext

  • win32fiber

  • ucontext_sjlj

  • emscripten

Там, где стандартная медицина бессильна, в дело вступает генная инженерия.

Для начала внедрим Koishi в организм Reindexer:

b4f81911f8490aeffa58ed6d7f5da6b4.png

Заменим больной орган на здоровый:

feaf9bc1f08c344372b20eae8432ae36.png

Стараемся не повредить то, что уже работает:

5e2f8734d7eb1eab94153aa9eeb006ee.png

Одним из backend-ов Koishi для корутин выступает fcontext (те же самые boost исходники, что в Reindexer). Последуем древней мудрости «работает — не трогай!» и оставим как есть в случае, если у нас не E2K архитектура. Для Эльбруса мы будем использовать ucontext_e2k.c

И вот он, наш мутант с корутинами полностью здоров и функционален (и на amd64, и на E2K):

День 11. Проводим финальные испытания

Две половинки нашего Франкенштейна собраны вместе, даже сторонняя библиотека прижилась и отлично себя показала. Пришло время для финальных тестов.

Всего в Reindexer около 300 функциональных тестов, и мне не терпится запустить их все.

Тесты бежали, бежали и не добежали. Один из тестов вызвал Segmentation Fault.

Всему виной оказались вот эти строчки:

struct ConnectOpts {
  /* тут тоже код, но он не такой интересный */
  uint16_t options = 0;
  int expectedClusterID = -1;
};

«Обычный код, что тут такого?» — скажете вы. Но не все так просто. Запоминаем, что здесь два поля, которые инициализируются при объявлении.

Попробуем воспроизвести на изолированном более простом примере.

ASM, настало твое время

Внимание на скриншот:

bf5af3e96739570cdbf400b47ae4c7a3.png

Ошибка проявляется именно в том случае, когда отсутствуют не проинициализированные поля. Другими словами, если все ваши поля инициализируются при объявлении — вы счастливчик, поймавший Segmentation Fault.

Баг заключается в том, что скомпилированное создание структуры выглядит как странный addd. Именно эта строчка и вызывает segfault. Кстати, если вместо bool anyField = false написать просто bool anyField, то код совершенно другой и ошибки нет.

Семь бед, один ответ — обновитесь!

Разгадка тайны оказалась проста. На момент работы с Reindexer последней версией компилятора LCC была v. 1.25.16, однако в состав пакетов ОС Эльбрус по умолчанию входит 1.25.14, на котором я и запускал тесты. Добрые люди из сообщества подсказали нам, что уже в 15 версии данный баг был исправлен. Я решил обновиться на 16-ую и посмотреть, что будет.

Вот что нам принес новый компилятор LCC v. 1.25.16:

69e5162ff1554a5775a25e1f25422d0c.pngaf324d243fbc4949a1dfeeba3a73f55b.png

C++ код такой же, однако ASM совсем другой, и что самое главное — он работает! Последовательно (заранее прошу прощения за мой ломаный asm-русский переводчик):

  1. gestp — выделяем стек и кладем его в %dr3

  2. setwd — задаем текущее окно в регистровом файле

  3. setbn — задаем подвижную базу регистров в текущем окне регистрового файла

  4. addd — кладем «дно» стека (стек размером 0×20) в %dr2

  5. addd — выделяем на стеке нашу структуру

  6. ldb — достаем из констант наш false и кладем его в %r5

  7. stb — кладем наш false из регистра %r5 в наше поле anyField1

  8. addd — подготовили локальный указатель на структуру для вызова метода

  9. addd — передаем указатель в базовый регистр для передачи при вызове anyMethod (в anyMethod мы достаем указатель из регистра %dr0)

  10. disp — подготавливаем регистр перехода на anyMethod

  11. call — вызываем anyMethod

Дальше все просто — пересобрать, перезапустить, обрадоваться.

e3e32b204ee3539ecaf4be5f6b3a1a0e.png

Тесты прошли успешно, и теперь у нас есть полностью рабочий и протестированный Reindexer на Эльбрусе.

На этом все?

На самом деле нет. Сейчас мы смогли:

  • собрать Reindexer Server

  • запустить Reindexer Server

  • собрать Reindexer Tool

  • запустить Reindexer Tool

  • переписать кусочек Reindexer Tool

  • уронить Reindexer и поднять его

  • добиться 100% проходимости Reindexer тестов

Мы научились:

  • собирать C++ софт на Эльбрус

  • переписывать C++ софт под особенности и отличия Эльбрусов

  • разбираться в ASM E2K

  • превозмогать трудности

  • разбираться в C++ корутинах даже на Эльбрусе

Что в планах:

  • корутины на ASM E2K (может быть даже сравнить fcontext ASM на i5 и ucontext/ASM на E2K)

  • оптимизировать Reindexer под архитектуру Эльбрус

  • перенести еще несколько интересных приложений на Эльбрус

  • дополнить базу библиотек и пакетов Эльбрус

  • организовать E2K инфраструктуру и песочницу

Что же можно сказать про Эльбрус со стороны простого разработчика?

Эльбрус — это ваш друг. Он уже умеет в 80% современного софта, работать с ним можно и достаточно комфортно. Его архитектура — действительно произведение искусства, идея одноядерного параллелизма достойна уважения и несет в себе большой потенциал. Вопрос лишь в том, получится ли его раскрыть.

На сегодня это всё, что хотелось бы вам рассказать, но я еще вернусь с новыми интересными подробностями про портирование и оптимизацию, и да прибудет с вами сила Эльбруса!

© Habrahabr.ru