Приветствую тех кто повязан в вебе. Сегодня обсуждение небольшого вопроса связанного с попыткой организовать правильное файловое логирование
Очередное нестандартное решение стандартной проблемы. Кому интересны необычные подходы - прошу. Кто считает что если есть стандартные решения и нужно всегда ими пользоваться - проходите мимо.
Я буду писать в ключе PHP, но мышление применимо и к любому другому серверному языку.
Каких показателей мы хотим достигнуть?
Хочется быстро получить доступ к последним записям в логах. Хочется иметь возможность программно парсить данные, для дальнейшего анализа логов. А также хочется не вредить при этом существующим механизмам для работы с логами - утилиты tail и текстовому представлению.
Если вы когда либо что-то логировали - вы знаете - взаимодействие с файлами происходит у программы на уровне ссылки на позицию в файле и чтению или записи данных относительно этой позиции.
Обычно логи пишутся по нарастающей. Появляются новые, а все исторические данные остаются на местах, либо делятся по временному признаку и архивируются. Здесь всё удобно. Нам как правило могут понадобится свежие логи, а старые нужны лишь изредка и можно их хорошенько сжать.
Таким образом работают все базовые механизмы для логирования веб сервера, базы данных и самих скриптов. Каждая новая запись пишется с новой строки и перевод строки как правило и является разделителем.
И Вам наверное когда надо было что-либо залогировать - вы использовали конструкцию вида
Ой, так конечно же делать нельзя, если файл большой - у вас кончится вся память, и вообще файл можно только дописать не читая целиком и сразу не забудем о блокировке файла, чтобы паралелльные процессы не стерли файл целиком
Классический вариант, вполне себе уместный. Какие тут могут быть недостатки?
В первую очередь затруднительно смотреть последние самые свежие логи. Для линуксов есть замечательная команда tail, которая показывает и следит за последними строчками в файле. Она вполне себе уместна
Но она немного ограничена - у вас должен быть доступ к ssh чтобы пользоваться ею и на сервере должен быть linux.
Программно получить последние n строк в файле может вызвать определенные проблемы. Напоминаю, у вас есть каретка для чтения файла, она читает передвигаясь вперед. Читать по символу и отматывать символ назад - весьма дорогие операции. А строки могут быть огромные. Тем более если в операции будет перенос строки который вы захотите залогировать - это поломает вам всю серилизацию и целостность.
Если вы хотите анализировать логи, а я рекомендую вам делать логи которые можно впоследствии агрегировать как-либо - вам нужно использовать серилизацию, но такую, чтобы легко читалась и через tail. Рекомендую использовать для этой цели JSON. Как самый универсальный и понятный и доступный механизм на всех языках.
Всё выглядит хорошо, но как насчет реализации?
Делать массив из данных - неудобно и опасно, нужно стирать последние завершающие кавычки для этого. Да и многогигабайтный файл читать нужно весь в память, чтобы с ним как-то работать. Значит новый элемент массива нужно дописывать как отдельный массив. Очень хорошо.
А как делить массивы между собой? Проблемы tail всё еще актуальны, а серилизованные массивы могут быть очень длинные
Здесь и начинается волшебство.
Итак, нам известно что ранее записанные строки не могут меняться. А это значит что все позиции указателя всех начал и длину добавленной фразы мы можем где-либо отдельно списком вести. Получается некий hashmap, проглотить который будет уже гораздо проще и читать построчно его можно гораздо проще как с начала так и с конца
Куда его положить - вопрос совершенно отдельный. Если вести файлы отдельно - у нас может нарушится атомарность, а операции с блокировками двух файлов будут значительно сложнее. Плюс проблемы с копированием, сжатием. Чтобы избежать вновь образовавшихся проблем - предлагаю вести указатели прямо внутри файла. Но как?
Задача следующая - надо уложить в строчный файл - указатели вместе с полезными данными, при том что мы не можем вставлять ранее существующие данные. Кажется, что это хорошая задача для собеседования.
Что же такое ссылка, если вдруг есть непонимание - это позиция (число байт) на место начала порции полезных данных относительно известной величины. То есть это целое положительное число и оно не может быть больше чем размер файла. Вопрос лишь как создать компактный и лаконичный протокол.
Исходим из того что длина файла нам известна. А это значит - нам известна позиция указателя для чтения самого конца файла. Очевидно, что в каждом дополнении "полезные данные - ссылка" - ссылка должна идти после данных (потому что она имеет ограниченную длинну). Поскольку следующий поток полезных данных будет идти сразу после текущей ссылки - то и длину массива данных можно не хранить, она будет равна разности между положением текущего указателя и положением предыдущего указателя, а значит в указателе может храниться сама длина блока полезных данных. Таким образом можно легко прыгать по блокам начиная с конца файла.
Остается только один вопрос - как узнать длину самого указателя. Если вы подумали что достаточно делать проверку на то является ли символ числом и отматывать по одному символу - может оказаться что полезные данные - это тоже число, а число в JSON даже кавычками не обрамляется.
Есть одно предложение - добавить в протокол еще один символ - а именно длину указателя - всегда 1 байт. Число от 1 до 9. Это позволит хранить указатели до 9 знаков, а это 999 гб на один массов (при условии что хранится именно длина), мне кажется хватит с запасом.
Таким образом решены все проблемы записи и чтения файлов, поиска по большому файлу множества данных и открываются возможности для анализа этих данных, т.к. парсинг данных уже реализован стандартным JSON.
Для организации постраничника - можно запомнить текущую абсолютную позицию курсора последней записи и перемещаться сразу туда.
Можно пойти еще дальше и сжать ссылку, поскольку кащунственно использовать 256 возможных значений в байте лишь 10ю значениями. Достаточно избегать спецсимволов в ascii, а все остальное пространство можно использовать для сжатия. Но это экономия на спичках и оно упрется обязательно в скорость пробега по ссылкам.
Tail всё еще доступен и полезен. Последние (самые актуальные) записи читаются быстро без больших нагрузок на жесткий диск и память сервера, а также файл доступен для чтения в блокноте, если специальный иструмента чтения под рукой нету
Другими словами - это обычный лог но с мета информацией для переходами к ранним сообщениям
Данный механизм используется в фреймаворке Core Framework