Альтернативный заголовок: "В любой непонятной ситуации возвращай Out of memory".
Давеча решил я запустить свой самописный сервер веселья ради, как я делал это тысячу раз до этого, и каково же было моё удивление, когда я внезапно увидел следующую строчку в консоли: Error when parsing "example_proj.xml": 1:0 out of memory.
Для парсинга конфигурационных файлов в проекте используется сторонняя библиотека (назовём её LibCustomConfig), которая в свою очередь использует широко распространённую libexpat.
Итак. Out of memory? На XML в 50 строчек? Сказать, что я был ошарашен - это не сказать ничего. "Но ведь раньше всё работало". Ни код сервера, ни так называемый LibCustomConfig никак не менялись.
Будучи абсолютно сбитым с толку, я решил свалить всю вину на последнее обновление последней Ubuntu, после которого проблема и появилась. А вместе с ней появились и другие проблемы: падения и подвисания разных приложений, как будто сам Chaos Monkey дотянулся до меня своими лапами. Может где-то в недрах поломался ABI?
Пересаживаюсь на свежую LTS версию Ubuntu, пересобираю сервер и... опять out of memory.
Пришло время расчехлять GDB. Кто-то скажет, что с этого и надо было начать, но дебажить сторонние либы очень не хотелось.
LibCustomConfig использует libexpat примерно так:
Parser::Parser(/*...,*/ char separator = ':')
{
if (separator)
/* Constructs a new parser and namespace processor. Element type
names and attribute names that belong to a namespace will be
expanded. */
parser_ = XML_ParserCreateNS(/*encoding = */ encoding_,
/*namespaceSeparator = */ separator);
else
{ /*...*/ }
}
Запоминаем, что сепаратор по умолчанию - ':' (корректно это или нет, я не знаю). Далее файл вычитывается в буфер и парсится стандартным методом из libexpat - XML_ParseBuffer:
XMLPARSEAPI(enum XML_Status)
XML_ParseBuffer(XML_Parser parser, int len, int isFinal);
При ошибке парсинга возвращается ненулевой статус, а код ошибки можно получить с помощью XML_GetErrorCode.
После получаса дебаггинга наконец нахожу, где же внутри libexpat фейлится парсинг (смотреть на GitHub):
Этот код был добавлен 12 февраля (коммит).
В качестве подопытного XML-файла используется просто <conf></conf>.
В данном случае parser->m_ns == 1 (root parser), uri == "http://www.w3.org/XML/1998/namespace" (автоматически устанавливается libexpat'ом), parser->m_namespaceSeparator == ':' (установлено в LibCustomConfig). Получается любой простой файл не будет парситься, если сепаратором указать двоеточие, т.к. этот символ присутствует в URI.
Но почему же я получаю Out of memory error, если возвращается XML_ERROR_SYNTAX?? Смотрим call stack:
сейчас мы находимся внутри функции addBinding, которая нам возвращает XML_ERROR_SYNTAX (смотреть на GitHub)
вызывается она внутри функции setContext, которая внезапно возвращает bool. В данном случае XML_False (смотреть на GitHub)
далее startParsing, которая возвращает то же булевское значение (смотреть на GitHub)
а затем знакомая XML_ParseBuffer, которая по возвращенному XML_False должна понять, что же произошло (смотреть на GitHub)
И как же XML_ParseBuffer понимает, что произошло? А очень просто:
if (parser->m_parentParser == NULL && ! startParsing(parser)) {
parser->m_errorCode = XML_ERROR_NO_MEMORY;
return XML_STATUS_ERROR;
}
В любой непонятной ситуации возвращай XML_ERROR_NO_MEMORY. Прикольно.
Комментарии (12)
amarao
23.02.2022 17:54+10Ну, для меня сообщение от программы out of memory обычно означает, что это программа неправа, а память есть. Потому что когда памяти нет, то программа обычно ничего не сообщает, а OOM зачитывает некролог с выражением. Чаще всего той программе, которая "виновата", но иногда и кому-то непричастному.
blind_oracle
24.02.2022 12:29Зависит от языка. Да, если это Си то нужно аккуратно всё проверять. А если что то чуть выше уровнем вроде Го где аллоцирует рантайм, то там по крайней мере сообщит перед паникой.
Но, конечно, пенять на ОС это последнее дело ????
amarao
24.02.2022 12:54+2За вычетом java (где свои лимиты памяти), обычные нативные бинарники под линукс ENOMEM на нормальной конфигурации никогда не получают, т.к. есть оверкоммит по памяти. Когда память заканчивается for real, на машине уже обычно есть выделенной памяти в 2-3 раза больше, чем есть физической, так что память заканчивается не в момент её запроса, а в момент попытки использовать. И тогда ядро решает, кого убить, чтобы освободить память. Называется oom (out of memory), плюс особо всеми любимый oom killer.
Так что для go, C, C++, Rust, python и т.д. получить ENOMEM практически не возможно (если только не запросить нереально большое количество памяти с самого начала).
blind_oracle
24.02.2022 13:05+1Кроме нехватки памяти есть еще cgroups в которых память бывает зажимают.
так что память заканчивается не в момент её запроса, а в момент попытки использовать.
Да, malloc() в линуксе почти никогда не фейлится, но это зависит от настроек оверкоммита в sysctl. Тем не менее...
Так что для go, C, C++, Rust, python и т.д. получить ENOMEM практически не возможно (если только не запросить нереально большое количество памяти с самого начала).
...конкретно в Go я бывало встречал панику по memory allocation failure ещё до того как в ядре сработал OOM.
Судя по исходнику рантайма это именно ENOMEM:
olehorg
23.02.2022 18:02ERROR_NO_MEMORY выдается не только в случае отсутствия памяти но и в случае кучи других рантаймовых ошибок - например, если в результате работы объем выделенной области памяти равен 0 или если объем запрашиваемой памяти ошибочно отрицателен и еще в массе разных случаев.
было бы интересно взглянуть на выдающий ошибку xml файл - вполне вероятно что там есть или повторяющаяся цепочке двоеточий или комбинация двоеточий с другими спецсимволами, типа двоеточия с обратным слешем.
Cheater
23.02.2022 18:46Пересаживаюсь на свежую LTS версию Ubuntu
как я переустанавливал ОС из-за libexpat
WAT. В чём была проблема даунгрейднуть libexpat или собрать другую версию в песочнице? Зачем переставлять всю систему?
andr_s Автор
23.02.2022 18:55+7Я ведь не знал заранее, что проблема в обновлении libexpat. К тому же, как и написано в статье, вся система в целом стала очень нестабильной после обновления. Добавьте к этому очень странный код ошибки, и в этоге получаем вывод, что надо сносить систему
Признаю, что сделал это скорее из нежелания нормально разбираться, что не так))
mayorovp
23.02.2022 19:37Кстати, заметил тут вот ещё одну ошибку:
В данном случае parser->m_ns == 1 (root parser), uri == "http://www.w3.org/XML/1998/namespace" (автоматически устанавливается libexpat'ом)
Это пространство имён — вовсе не пространство имён по умолчанию, а пространство имён связанное с префиксом xml. По умолчанию же пространство имён — пустая строка, и именно пустая строка должна была оказаться в массиве uri.
mayorovp
Совершенно некорректно, документация по libexpat прямо говорит не использовать символ который может встречаться в XML:
andr_s Автор
Спасибо большое за уточнение
Тем не менее суть статьи не меняется: возвращать XML_ERROR_NO_MEMORY в этом случае - нонсенс
domix32
Но Out of memory на 50 строчках потенциально случится может тем не менее. См billion laughts