Помогите с dlopen

erotic

Есть демон, собран почти статически (-static-libstdc++ -static-libgcc все внешние библиотеки тоже статические. При старте несколько минут грузит данные из базы. Была идея научиться его перезагружаться быстрее, оставляя между перезапусками данные в памяти. Идея с шареной памятью была откинута, потому что для этого надо либо уметь работать с ней как с кучей, т.е. нужен какой-то аллокатор в шареную память, и желательно чтобы вообще malloc всегда в шареной памяти выделял; либо уметь сериализовывать объекты в шареную память между перезагрузками демона, а это значит, что надо писать много кода, т.к. типов разных объектов много. Поэтому хотел попробовать другой подход: весь код демона упаковываем в шареную либу, у нее всего три внешние функции с extern "C" интерфейсом:
* отдать номер версии структуры с данными для проверки того, что новая шареная либа сможет с ними работать;
* что-то типа main - основная функция демона, который принимает указатель на уже имеющиеся данные (при старте nullptr и возвращает указатель на актуальные данные при выходе - для последующей версии main;
* и функция удаления этих возвращенных данных, когда демон совсем завершается (это в угоду valgrind'у).
Что у меня получилось сделать:
1. Код всех внутренних библиотек компилируется с -fpic и кладется в одну шареную либу (она тоже собирается с -static-libstdc++ -static-libgcc).
2. Все внешние библиотеки статически компонуются в исполняемый файл через --allow-multiple-definition и --whole-archive. Сделано этот так, потому что они собраны без -fpic в основном, и класть их в шареную либу я не могу. -export-dynamic почему-то не понадобился, все и без него работает.
3. В этом же исполняемом файле крутится цикл: загрузить либу, найти нужные функции, запустить main, ждать сигнала, по сигналу загрузить новую версию либы, сравнить версии данных, если ок - завершаем main, получаем от нее указатель на текущие данные, выгружаем старую либу, передаем управление и данные main из новой либы. Профит.
Оно даже наполовину все работало - кроме части, где надо уметь использовать новый код.
С какими граблями я столкнулся:
1. dlopen не загрузит новую либу, если она будет лежать в файле с тем же именем (вернет тот же хэндл и те же указатели на функции). Поэтому я пробовал два варианта:
1.1. Отказаться от верификации новой либы, и по сигналу просто выгружать старую и загружать новую. Проблема в том, что dlclose не выгружает мою либу. dlopen(NOLOAD) показывает, что она все еще в памяти, то же говорит dl_iterate_phdr. При этом LD_DEBUG=bindings не показал, что символы моей библиотеки используются из каких-либо объектов кроме самой библиотеки.
1.2. Класть новую либо в файл с новым именем и при загрузке всегда искать либу через glob. Это работает, новая либа грузится, указатели на функцию получаются новые, мне даже удавалось завершать старый main и начинать новый. Если в новой либе статические переменные получают новое значение, то используются именно они. Проблема была только одна - если в новой либе реально находится другой код, я получаю SIGSEGV. В корке символов нет, одни вопросики.
2. Пробовал делать по-хорошему - определять в экспортируемых функциях либы только три нужные мне функции (до этого по-дефолту экспортировалось все). Делал это через --version-script линкера. Размер либы действительно уменьшался, как и вывод nm -CD, оно даже загружалось и работало. Но при перезагрузках тоже невнятные сегфолты. В зависимости от вариантов кода бывали сбои и просто на dlopen новой либы, по-моему. Еще один небольшой косяк был в том, что несколько singleton'ов, которые оказались определены и в либе, и в исполняемом файле, теперь разделились и стали свои у каждого объекта - несколько неожиданный для меня эффект, ведь синглтон из исполняемого файла по-прежнему должен находиться первым (я же DEEPBIND не просил. С DEEPBIND, кстати, еще на этапе dlopen все валится). Из-за этого я натыкался еще и на SIGSEGV из-за того, что исключение из либы было неизвестно исполняемому файлу, т.к. не экспортировалось, но все это было несложно починить.
3. Подозревал, что может быть между двумя версиями либы может быть кросс-разрешение символов, у них же их будет много общих, но судя по тому, что прочитал, такого быть не должно - загруженная через dlopen либа не участвует в глобальном пространстве разрешения имен, если ее специально не попросить.
В интернете нашел много отрывочных вопросов и ответов, из чего-то цельного основное - это статья Ulrich Drepper "How to write shared libraries" ( http://www.akkadia.org/drepper/dsohowto.pdf небольой мануал http://gcc.gnu.org/wiki/Visibility , текстик про плагины http://eli.thegreenplace.net/2012/08/24/plugins-in-c .
Еще отдельно protobuf доставляет - он в init куда-то добавляет регистрацию типов сообщений, и два раза ему это удается плохо - может быть он мне все портит, но вовсе не факт. И может быть то, что я все статически линкую, тоже плохо.
В общем, я чего хотел - оцените, может быть я что-то фундаментально важное не знаю, и оно так вообще не будет работать или надо как-то не так делать. Либо же в целом все верно, и надо разбираться с деталями.

vall

И может быть то, что я все статически линкую, тоже плохо.
Вот мне это тоже не нравится. этот --allow-multiple-definition выглядит как граната без чеки.
ладно бы ты линьковал так свой код про который ты всё знаешь, но stdc++ и libgcc это уже слишком.
dlopen ты как зовёшь?

erotic

Ситуация со static такая: сейчас на продакшн все попадает слинкованное статически, но без флага -static компилятору, т.е. libc берется с конечной машины. Это позволяет не ставить зависимости на продакшн и не зависеть от версии компилятора. Для обычных бинарников это работает уже давно и успешно. Правда, libc все равно через dlopen открывает libgcc_s.so для некоторых функций, и видимо, на конечной машине хоть какая-то версия этой библиотеки нужна.
--allow-multiple-definitions понадобился только затем, что все влинковываемые в исполняемый файл внешние статические библиотеки повторялись в списке библиотек по несколькуо раз, что не понравилось линкеру, но фактически там не было двух разных символов с одинаковыми именами. Мне сначала было удобнее с этим флагом собирать, потом я просто сделал уникальный список библиотек - результат одинаков.
Исходя из всего этого даже то, что я и в исполняемый файл, и в библиотеку статически линкую libstdc++ и libgcc не должно вызывать проблем, т.к. при разрешении имен символы все равно должны взяться из исполняемого файла, т.к. он идет в цепочке разрешений первым. Ну, это как мне кажется, может быть я ошибаюсь.
dlopen зову так: dlopen(libname, RTLD_NOW). С RTLD_LAZY результат тот же. C RTLD_DEEPBIND падает.
protobuf доставляет странные проблемы - при первой перезагрузки библиотеки по новому имени это не получается сделать, т.к. он пишет какой-то CHECK про то, что тип уже зарегистрирован. Но вторая загрузка библиотеки срабатывает, и может даже выдавать версию отличную от первой (т.е. дергаю подряд версию из старой и из новой библиотек - они разные). Думаю еще попробовать без протобуфа все это, вдруг в нем загвоздка.

vall

C RTLD_DEEPBIND падает.
По идее именно его и нужно использовать.
Использовать static-libstdc++ похоже более менее безопасно а вот static-libgcc явно имеет сайдэффекты.

erotic

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

vall

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

erotic

Она обязательно наткнется на код из исполняемого файла, потому что все зависимости там лежат. Всякие бусты, lua, protobuf и прочее все там. Да, оно лукапит туда и получает оттуда символы. Это же должно отлично работать! И даже работает, если не пробовать библиотеку выгружать. Т.е. зависимости по идее только у либы от бинарника, в обратную сторону их нет.
Ну, я поковыряю, мб с DEEPBIND получится запустить.

vall

попробуй libgcc не линьковать статически

erotic

Ща. Пока попробовал посмотреть в LD_DEBUG=bindings что происходит, когда две библиотеки загружаются. Первая лукапит все из бинарника и из себя. А вторая - из бинарника, из себя, а что-то из первой либы, а не из себя :( Хотя они одинаковые. Это без DEEPBIND.
UPD. С DEEPBIND тоже так происходит. Даже с убранным -static-libstdc++ -static-libgcc. Но это еще ладно. Он там static __thread переменные в основном так находит. Хуже то, что когда я третий вариант либы решил подгрузить, все зависло на dlclose :(

erotic

Интересно, что если взять простейшую либу, которая просто в цикле печатает что-то, то все работает прекрасно - и даже через dlclose все выгружается.
UPD. Полная версия либы пока работает с перезагрузкой. Собрал протобуф с -fpic и статически слинковал с либой, потому что http://code.google.com/p/protobuf/issues/detail?id=128 :

The design of the protobuf library is currently such that if you unload a dynamic
library containing a protobuf class, you *must* simultaneously unload the copy of
libprotobuf that it is linked against. Therefore, if you are writing a dynamic
library which may be loaded and unloaded at runtime, you should probably statically
link it against protocol buffers.

Alternatively, if you use:
option optimize_for = LITE_RUNTIME;
and you do not use extensions, then I believe this will produce code that does not
use any global registries and thus does not have these problems.

Жду багов при более глубоком тестировании.

erotic

Бывает и такое:

> grep optarg ld.bindings.10771
10771: binding file /lib64/libc.so.6 [0] to ./main [0]: normal symbol `optarg' [GLIBC_2.2.5]
10771: binding file ./main [0] to ./main [0]: normal symbol `optarg' [GLIBC_2.2.5]
10771: binding file ./main [0] to /lib64/libc.so.6 [0]: normal symbol `optarg' [GLIBC_2.2.5]
10771: binding file ./libdyn.so.25 [0] to /lib64/libc.so.6 [0]: normal symbol `optarg' [GLIBC_2.2.5]

Пришлось заменить getopt на boost::program_options.

bleyman

Как-то неправильно городить дичайшее извращение влияющее на всю архитектуру, в котором никто никогда ничего не поймёт даже если ты принеся в жертву чёрного козла сейчас заставишь его работать, только потому, что тебе в процессе девелопенья приходится ждать между перезапусками.
И да, с диким потенциалом устроить весёлую жизнь людям дальше, потому что теперь глюки будут мистически переживать перезапуски. То есть в продакшене ты абсолютно точно хочешь выключить эту функциональность ваще.
Прозреваю что 99% этих нескольких минут уходит на общение с базой и задумчивость базы, а не на создание внутренних структур. Так что пытаясь персистить всё ты оптимизируешь 1% общей производительности. memcached между тобой и базой не решит ли все твои проблемы? Ещё лучше было бы вообще прозрачно кэшировать прям сырые результаты запросов, на уровне сокетов буквально, чтобы не менять код вообще, но я что-то не найду никакого готового решения (потому что оно никому не нужно в продакшене, видимо). По идее можешь свою обёртку над дбконнекшеном написать за час, основная проблема будет повторить интерфейс для подстановки генерик параметров.

Dimon89

Прозреваю что 99% этих нескольких минут уходит на общение с базой и задумчивость базы
Так может тогда просто выгружать структуру из памяти на диск, а при запуске подгружать обратно? У яндекса есть для этого готовые решения.

erotic

Ты написал какие-то странные выводы из неизвестно откуда взятых у тебя в голове предположений.
Как-то неправильно городить дичайшее извращение влияющее на всю архитектуру, в котором никто никогда ничего не поймёт
Никаких извращений или существенных изменений архитектуры нет вообще. Это было моим основным требованием к этой фиче, поэтому я не стал заморачиваться с шареной памятью. Немного перекраивается линковка, но код изменяется только в одном месте - теперь единственный основной объект с данными из базы надо уметь вернуть из main и принять обратно в main. Это несколько строк кода. Архитектура к этому уже готова изначально. При этом тот же самый код можно тупо слинковать, как и раньше, статически, и перезагружать, как и раньше, целиком. Плюс _добавился_ код для динамической перезагрузки библиотеки. Все.
UPD. Да, про getopt забыл. Но от того, что его не стало, только плюсы: с boost::program_options код короче, а не стало его где-то там внутрях, конфиг снаружи остался таким же, поэтому все равно никто не заметит.
даже если ты принеся в жертву чёрного козла сейчас заставишь его работать, только потому, что тебе в процессе девелопенья приходится ждать между перезапусками.
В процессе девелопменья у меня обычно маленькая базка, поэтому долго ждать не приходится. Фича нужна в продакшн. Машин под сервис под сотню, запаса производительности немного, поэтому перезапускаем машинки небольшими порциями, скажем, по 4 штуки. 100 машин / 4 машины * 4 минуты на машину = 100 минут перезапуск всего парка. Конечно, это делает скрипт, но за ним надо присматривать. И какой-то хотфикс будет раскладываться, фактически, полтора-два часа. Я хочу свести это время к минутам.
И да, с диким потенциалом устроить весёлую жизнь людям дальше, потому что теперь глюки будут мистически переживать перезапуски. То есть в продакшене ты абсолютно точно хочешь выключить эту функциональность ваще.
Ну это что-то из разряда "на C++ демона не пишут, только на С", не буду комментировать. Хотя нет, скажу - если где-то _наблюдаются_ глюки, то всегда можно перезапустить все полностью.
Прозреваю что 99% этих нескольких минут уходит на общение с базой и задумчивость базы, а не на создание внутренних структур. Так что пытаясь персистить всё ты оптимизируешь 1% общей производительности. memcached между тобой и базой не решит ли все твои проблемы? Ещё лучше было бы вообще прозрачно кэшировать прям сырые результаты запросов, на уровне сокетов буквально, чтобы не менять код вообще, но я что-то не найду никакого готового решения (потому что оно никому не нужно в продакшене, видимо). По идее можешь свою обёртку над дбконнекшеном написать за час, основная проблема будет повторить интерфейс для подстановки генерик параметров.
Совершенно неважно, уходит ли время на ожидание данных от базы или на создание внутренних структур из них (кстати, в разные моменты упор идет на разное - в определенный период загрузки он идет на процессор). Важно то, что это время уходит. И я не оптимизирую 1% - не понимаю, с чего ты это взял. Я делаю так, чтобы данные не перезагружались из базы еще раз, если они уже в памяти демона есть, и надо лишь исправить алгоритмы обработки запросов. Т.е. вместо нескольких минут на перезагрузку уходит несколько секунд.
Кэширование мне вряд ли сильно поможет - БД постоянно меняется, извлекать данные из кэша вряд ли чем-то лучше, чем напрямую из БД (развернутые данные занимают 20-30 Гб в памяти, сколько льется из базы - хз). К тому же кэширование, опять-таки, надо писать, что не входит в мои планы.

erotic

Так может тогда просто выгружать структуру из памяти на диск, а при запуске подгружать обратно? У яндекса есть для этого готовые решения.
Это очевидное и рабочее решение, но у него есть два минуса:
1. Надо для десятков объектов написать сериализацию на диск.
2. Быстрее все же не писать данные на диск и не читать их потом еще раз, а просто взять готовые из памяти.
Так-то вариантов можно много разных напридумывать - но у всех них есть недостатки.

vall

Никаких извращений или существенных изменений архитектуры нет вообще.
У тебя есть извращения когда ты данные созданные одной библиотекой подсовываешь другой.
Там может быть скрытый стэйт, связанный с данными. Не обязательно в твоём коде а где-нить в stdc++ или libgcc который ты таскаешь с собой.

istran

порекомендую ядексовский MMS. Чтобы добавить поддержку сериализации, достаточно к классу добавить один шаблонный параметр и один метод в котором перечислены все поля. Для POD-типов и этого не надо делать. Десериализация заключается в единственном вызове mmap. Если данные в дисковом кэше, то он происходит мгновенно.

erotic

Еще раз порекомендую ядексовский MMS. Чтобы добавить поддержку сериализации, достаточно к классу добавить один шаблонный параметр и один метод в котором перечислены все поля. Для POD-типов и этого не надо делать. Десериализация заключается в единственном вызове mmap. Если данные в дисковом кэше, то он происходит мгновенно.
Посмотрел там на string.h, на твой пример, пока не появилось желания этим пользоваться. Объясню, почему:
1. Судя по твоему примеру, все объекты должны быть шаблонными. Мне это не нравится, т.к. много кода надо будет вынести из .cpp и по многу раз компилировать, возрастают зависимости.
2. Судя по твоему примеру, объект до сериализации и после - это разные типы объектов (с разным шаблонным параметром). Вроде как пользоваться ты ими можешь одинаково, но не совсем одинаково. Видимо, код, который ими пользуется, тоже должен быть весь или шаблонный, или преобразовывать эти типы к std типам. Но для строки, например, это означает каждый раз копирование строки из mapped объекта в не mapped - и зачем такое счастье?
Я пока правильно все пишу? Я очень быстро просмотрел туда, к сожалению, т.к. мне пока гораздо интереснее свой вариант сделать. Ну т.е. топик не о том, как сделать быстрый перезапуск вообще, а о том, как мне это сделать через выбранный мной способ.
3. Все равно же надо переписать все классы, только результат получится не лучше, чем если бы я свою сериализацию добавил - мало того, что надо добавить сериализацию (наша добавляется ровно так же легко так еще и типы объектов надо везде поменять.
4. Если я добавлю свою собственную сериализацию, то тоже могу все записать в файл. Преимущества mms в том, что десериализация быстрая - просто mmap? Ну, это наверное плюс, но у меня нет никакой уверенности, что оно останется в кэше, а не пойдет сбрасываться на диск. Мы двойной запас по памяти на машинке не храним, незачем обычно, и если 10 Гб начнут писать на диск со скоростью 100 Мб/сек, то это 100 сек. писать, 100 сек. читать. Итого 200 сек. Профита не будет.
Может быть и не начнет писать на диск, но в любом случае - если у меня получится вообще обойтись без сериализации, это будет всяко быстрее, чем с ней.

erotic

Там может быть скрытый стэйт, связанный с данными. Не обязательно в твоём коде а где-нить в stdc++ или libgcc который ты таскаешь с собой.
Может быть, ага. Пока я подумал о том, что указатели на таблицы виртуальных функций попортятся, и так и происходит на самом деле. Хотя виртуальных объектов из базы у меня нет, но в некоторых есть shared_ptr, а у него внутри тоже виртуальщина, так что с ними надо будет что-то сделать.
И указатели на статические данные нельзя хранить в объектах, т.к. в другой либе это будут другие статические данные.
Остальные косяки пока только ждут своего открытия.
Оставить комментарий
Имя или ник:
Комментарий: