Comments 45
~~ entity_type
char(32) not null default '', — Тип сущности~~
Можно было обойтись 'small int'
Вообще все переделать.
entity_type
char(32) not null default '', — Тип сущностиmime
char(32) not null default '', — MIME-типmd5
char(32) not null default '', — MD5sha1
char(40) not null default '', — SHA1file
char(64) not null default '', — Физическое расположение
Это ад и содомия. От такого вытекают глаза :)
Нужно:
- Вынести в отдельные таблицы
entity_type
mime
url
file
md5
sha1
- связать все по айди.
Иначе:
То есть берём идентификатор файла в шестнадцатеричном представлении, разбиваем оное на чанки по два символа.
Решение отличное, но при fixed rows у вас ~495 байт на запись, или ~246-495 в зависимости от заполненности varchar, когда у вас есть шанс иметь число цифровую главную табличку и пачку привязанных к ней. Удобство страдает только в самом начале (приходится более сложные sql писать), но эффективность и выдаваемый перфоманс максимальны.
Всегда следует стремится к 6й форме (кроме частных случаев, когда это стоит дорого в плане кода на относительно хорошо спланированной архитектуре). Но 6-ая форма, это идеал к которому рано или поздно (что хуже) придется прибегать.
Вопрос: зачем вам и md5 и sha1?
На md5 коллизии не новы, на sha1 гугл давно предоставил 2 pdf с одним хешем. Используйте sha256, и место в сумме съэкономите.
upd: забыл добавить про md5 & sha1.
Во-вторых, MIME-типы. Если следовать Вашей логике, то нужно делать системный справочник. А в этом справочнике нужно предусматривать все типы по сути. А их десятки. Если не сотни уже. Если мы будем грузить только изображения, то большая часть справочника будет тупо не задействована. Плюс возрастают накладные расходы при выборках по MIME-типу, ибо в моём случае просто индекс, а в Вашем придётся делать JOIN к таблице MIME-типов. К слову сказать то же касается и таблицы типов сущности.
Ну и в-третьих. Зачем выносить в отдельные таблицы URL, MD5 и SHA1 я вообще ума не приложу. А физическое расположение файла нужно исключительно на тот случай, если будет изменён алгоритм формирования физического имени файла. У меня в первоначальной версии было md5(последний_байт + URL).
В сухом остатке из Ваших тезисов имеет смысл задуматься только над entity_type и алгоритмами хэширования. Кои, кстати, в данном случае в принципе имеют чисто декоративное значение :)
В реальной системе entity_type по факту означает модель данных
У вас там char, который гарантированно ест свои байты в любом случае. Его надо выносить в отдельный справочник (связанную таблицу).
Во-вторых, MIME-типы. Если следовать Вашей логике, то нужно делать системный справочник.
Конечно.
Если мы будем грузить только изображения, то большая часть справочника будет тупо не задействована.
Добавлять по мере использования. В идеале код, который будет сам обновлять справочник mime.
а в Вашем придётся делать JOIN к таблице MIME-типов
Я сам ранее был не сторонником JOIN'ов, чисто в визуальном плане + когда данные есть гарантированно, и не будет null, то и обычный запрос к двум таблицам, саб и join вернут одинаковый результат (хотя оптимизатор может переделать join в саб, а скорее всего сам саб в join, об этом есть тонны тасков про перфоманс в гугле).
Варианты:
1)
select d.*, t.mimeDesc from tdata d, tmimes t where 1=1 and t.id=d.mimeId;
2)
select d.*, (select t.mimeDesc from tmimes t where t.id=d.mimeId) as 'mimeDesc' from tdata d where 1=1;
3)
select d.*, t.mimeDesc from tdata d left join tmimes t on t.id=d.mimeId where 1=1;
Для случая, когда данные в tmimes есть всегда вам подойдет первый запрос. Он простой и очень быстрый.
Зачем выносить в отдельные таблицы URL, MD5 и SHA1 я вообще ума не приложу
Потому, что сие очень сильно ест размер таблицы, у вас там char
, а записей хотите делать много. Далее — решили вы удалить md5, ибо больше не используете его на 245,874,492 записях… Грохнуть 8 байтовый столбик и табличку с md5 или 32 байтовой поле, как бы разный масштаб :)
В сухом остатке из Ваших тезисов имеет смысл задуматься только над entity_type и алгоритмами хэширования
Нет, стоит задуматся над переносом на 6 форму. От этого не уйти, рано или поздно оно прижмет и будет ныть и болеть. То, что я делал в стиле "удобно" сейчас переводится на 6-ую. Количество таблиц растет, глаза разбегаются, но софту то насрать. А когда будете вносить радикальные правки на большом объеме, то на 6й форме можно плакать от восторга, или ломать таблицы или базу на других формах.
На счет алгоритмов хеширование — на современном железе самый быстрый это sha1 (просто быстрый, так же и бесполезный). Если нужен алгоритм, который не sha1, это sha512:
type 16 bytes 64 bytes 256 bytes 1024 bytes 8192 bytes 16384 bytes
sha256 98888.60k 219086.70k 399258.79k 495975.42k 532501.85k 535702.19k
sha512 63937.97k 255262.34k 446925.14k 671571.97k 787614.38k 797944.49k```
Ибо sha512 расчитывается быстрее, чем 256, как только данные больше чем 64 байта (я его использую).
З.Ы. кстати, ключи можете делать `unsigned`, они же 1+, отрицательных нету.
700 единиц хранения на каждую папку (примите как аксиому от человека, который на практике тестировал всё это), то есть в каждой папке или 700 других папок, или 700 файлов, если это конечная глубина. 4 уровня вниз — 343 миллиона файлов в одной старшей папке.
Все имена файлов только цифрой, которая является ключом в БД. Если записи о файлах хранятся в разных таблицах — разные папки для каждой таблицы. Поиск пути, где сохранен файл, по цифре ключа из бд — самая быстрая операция, которую вы сможете придумать с файлом в таких количествах.
Я в реальных сайтах использую эту нумерацию не только для хранения файлов изображений, но и для файлов кэша всех страниц сайта на диске.
Лень делать скрины, но приведу вам стату: по запросу в гугле site: мой домен — ответ примерно такой: нашлось примерно 243 миллиона страниц.
В консоли гугла, средняя скорость загрузки страниц — 242 мc. Хостинг шаред (общий тысячи пользователей на одной машине) 300 рублей в месяц, плюс 50Гб места докупил, чтобы файлы влезали, нагрузки нет особой на хостинг от моего сайта этого.
Ну, a char(32) для mime, и еще и индекс по этому полю (вот интересно зачем он вообще) — это фэйл. Если так хотите хранить текстом MIME то для этого есть тип ENUM в MySQL.
Что касается хэшей, то я лично ничего не имею против md5 и sha1. Я не из тех кто кричит, караул в них нашли коллизии — значит их нельзя использовать. В любом хэше есть коллизии, так как такова природа самих кэшей, нельзя в 16-64 байта загнать мегабайт, чтобы не было коллизий. Потому, имхо, лучше использовать 2 хэша (чтобы было 2 разных алгоритма), что обычно и делается в программах бэкапа и дедупликации. Обычно используется быстрый хэш и медленный, но скажем так более стойкий.
Но у Вас проблема в том, что на этот более стойкий у вас тоже индекс. Зачем? Чтобы был?
Вы же картинки добавляете своим софтом, поэтому если нашли по md5, то проверили у найденных строк sha1. Нет смысла искать сразу по sha1, а потом по md5, так как если быстрый хэш не совпадает, то медленный уже даже можно не сравнивать.
Ну и конечно эта любовь к Bigint. К тому времени, как у Вас количество файлов превысит 4 млрд, вы уже не один раз переделаете и базу, и систему хранения, если вообще не закинете это дело. Особенно это касается полей по которым индексы.
Про Bigint для Size даже смешно, Вы что реально думаете, что файлы больше 4 гигов, будут заливаться тем же кодом, что и фотки/аватарки юзеров? Там своих костыликов припасено.
Ну и откройте для себя UNSIGNED для целочисленный полей.
Список mime меняется, при каждом новом делать alter table?
bigint спорно, но вот unsigned лучше не использовать без очень-очень веских причин, особенно в случае если заведомо будут на клиентах языки его не поддерживающие, такие как PHP или JS.
и потом отдавать из неё — неплохой вариант
Что неплохо в том чтобы для трёх mime-типов по 10 символов (image/png, image/gif, image/jpg), держать в базе столбец CHAR(32) и еще и индекс по этому столбцу?
Список mime меняется, при каждом новом делать alter table?
И в чем проблема у Вас каждую неделю новый формат для изображений придумывают?
но вот unsigned лучше не использовать без очень-очень веских причин
Ага, особенно с UNSIGNED TINYINT ужасные проблемы.
Году в 2008 вас бы похвалили за такой код.
Судя по оговоркам ("В качестве СУБД я использую расширение MySQLi", "законодательство, обязывающее хранить информацию о пользователях не менее полугода" в контексте обсуждения автоинкремента и пр.) — вы начинающий программист. Если поставите себе целью подтянуть код до современных стандартов, то через пару лет у вас получится то, что не стыдно показать широкой аудитории.
Сейчас же, уж извините, у вас старый добрый спгетти-код, даром что завернутый в класс. В одну кучу смешались SQL, HTTP, файловая система. Никакой абстракции, никакого разделения ответственности. Обработка ошибок где-то лишняя ('cannot make dir: ' ), где-то отсутствует совсем (mysqli), где-то однозначно вредная (echo $e->getMessage();).
В качестве работы над ошибками попробуйте сделать отдельные сервисы для работы с БД и HTTP. Для работы с таблицей файлов в БД также нужен будет отдельный класс. И ради бога, забудьте уже этот чудовищный mysql_query стайл. А то получается как в анекдоте — "ложечку вынул, а глаз все равно зажмуриваешь". Буковку i к вызовам функций приписал, а все остальное осталось как прежде.
В итоге вместо ручного колупания с SQL должно получиться что-то вроде
$this->repository->save($url, $meta, остальные параметры);
В конкретной реализации тоже много странного (непонятно, зачем отдельный запрос для сохранения хэшей и размера файла; непонятно, почему логика получения файла сделана в РНР, а не в БД, почему вообще по одному и тому же url может оказаться больше одного файла и пр.) но это уже мелочи по сравнению со структурными проблемами.
Ой, зря вы взяли этот обиженный тон.
Поймите, ваш код говорит о себе куда больше, чем вы хотели бы сказать оправданиями про "реальный код, в котором все правильно".
Добавления палочки со стрелочкой к вызову функции тоже недостаточно. Оно не делает ваш код объектно-ориентированным, а работу с БД менее унылой.
И ваша реакция на совершенно справедливое замечание о нормализации БД в комментарии выше — это тоже очень, очень печально.
Не стыдно чего-то не знать. Стыдно принимать в штыки критику и отказываться учиться.
Ну вот, теперь по крайней мере честно.
Ваша бравада эмоционально оправдана, но со стороны смотрится жалко. Звучит это всё, как "Я не профессионал, я любитель. И поэтому я чихал на традиционные представления, что дважды два равно четыре. У меня будет 7!". И с таким отношением вы никогда не дойдете до квадратных уравнений.
PROFIT!
0f/65/84/10/67/68/19/ff.file
У вас получилось 7 уровней вложенности папок. Это слишком много:
1. На каждый проход вглубь тратится лишнее время (а ведь на диске сектора, в которых описана каждая папка, могут лежать вразброс). По мере добавления новых файлов папки будут раскидываться по диску. В один прекрасный момент перестанет хватать буферизации и все начнет тормозить.
2. Большинство папок, кроме корневой, будет содержать ровно по одной записи. Это я вам вполне квалифицированно заявляю: у меня в одном из проектиков файлы разложены примерно так же, только структура двухуровневая (ab/cd/efgh1231231243232.file). И хотя файлов сложено уже несколько десятков тысяч, внутри папок второго уровня редко лежит больше одного-двух файлов.
Отсюда вывод: делать структуру папок с более чем двумя уровнями вложенности — нехорошее излишество.
Если уж вы интересуетесь производительностью файлового доступа, то неплохо было бы провести эксперименты, так сказать, «в натурных условиях». Создать структуру папок с определенной вложенностью, напихать в нее энцать миллионов файлов. Затем проверить производительность, вычитывая из нее случайные файлы. И так — для различных уровней вложенности и различных «наполненностей» папок (например, если резать не по два, а по три символа, то максимальное количество файлов в папке станет равно 4096). А если вы еще и на различных файловых системах эксперимент проведете, то вообще будет прекрасно — ведь, например, ReiserFS изначально разрабатывалась из расчета на быстрый поиск в папках. Думаю, результаты таких экспериментов были бы интересны многим завсегдатаям.
Однако, с другой стороны перечитайте абзац про автоинкремент.
Перечитал, но не понимаю, что вы имели в виду. Если вы о том, что трех байт может не хватить для нумерации всех файлов — так я и не предлагаю укорачивать имя файла. Просто
0f/65/84/10/67/68/19/ff.file станет 0f/65/8410676819ff.file.
Если идентификатором файла будет некий хэш, а не просто порядковый номер, то файлы «размажутся» по подпапкам более-менее равномерно. В результате в каждой конечной папке будет всего несколько десятков файлов (пара сотен, если количество файлов перевалит за 16777216). Ну а если файлов станет больше, то у вас возникнут проблемы совсем другого масштаба (в частности, приходит на ум исчерпание инодов файловой системы).
«Обратный индекс», то бишь ревизия всех файлов для сравнения фактического наличия с БД — занимает около 3х суток (zfs). Чем глубже — тем дольше. Ну и тюнинг файловой системы, конечно, не помешает.
Если идентификатором файла будет некий хэш, а не просто порядковый номер, то файлы «размажутся» по подпапкам более-менее равномерно.спустя полгода вышли из строя два новых HDD-диска в зеркальном рейде, на них проводился бекап изображений с основного сервера с SSD-рейдом, с помощью rsync.
Логи atop были утеряны, а другой нагрузки кроме суточных бекапов не было, поэтому есть подозрения, что из-за «равномерного размазывания по подпапкам» rsync сканировал много лишнего и если бы я использовал вместо хеша инкремент, то старые папки бы не пересканировались лишний раз, потому что даты их изменения не менялись и диски бы прожили гораздо дольше.
Но это только предположение. Может здесь есть специалисты по rsync, которые в курсе как он работает «под капотом».
Во избежание подобных нюансов в будущем, к структуре с «равномерным размазыванием по подпапкам» был добавлен ещё один уровень папок «сегодняшняя дата», итого имеем:
2018-09-21/0f/65/8410676819ff.fileДанный подход позволяет создавать инкрементальные суточные бекапы без пересканирования всей структуры. Просто берётся папка за предыдущий день и создаётся архив или зеркалируется на другой сервер с помощью rsync.
Кстати, если у вас в день было не очень много файлов (тыщ 50-100), то можно по идее один уровень вложенности папок удалить: 2018-09-21/0f/658410676819ff.file
В среднем это даст 200-300 файлов в папке, что вполне нормально.
www.s3-client.com/s3-compatible-storage-solutions.html
Так же можно хранить метаинформацию, по которой не нужно проводить индексацию и поиск, так же тут же рядом с файлом, например в структуре данных перед файлом, все равно вы ее тоже будете запрашивать, так зачем тратить на это базу данных.
[точный размер метаинформации + точный размер файла+ мета-информация + файл + нули]
Т.е. идентификатор однозначно позволяет сделать одно единственное чтение с диска и получить файл, ни одна файловая система не позволит сделать это быстрее, так как тут нет вообще никаких накладных расходов (в файловых системах каждый каталог это отдельное чтение или даже несколько, правда часто они закешированы, но не для огромных хранилищ).
Работать с таким архивом одно удовольствие — он выносится на отдельный физический носитель, линейное чтение при резервном копировании, и можно в принципе отключить lazy writes т.е. кеширование записи, сэкономив тут на дисковых кешах, а за кеширование чтения отвечает операционная система. Так же файл можно открыть замапив его в адресное пространство и работать с ним как с обычной памятью, это очень удобно.
К сожалению вам нельзя будет выносить наружу этот идентификатор, так как после того как закончится место на диске, вам потребуется либо перенос на раздел большего размера (я планировал тогда добавить несколько бит в идентификатор, отвечающих за номер файла хранилища, но проект потерял актуальность) либо реорганизация, с удалением дырок (файлы, реально удаленные, а не пометкой в базе данных), а это фактически создание нового архива с новыми идентификаторами файлов.
А еще любые изменения в файлах — это создание нового файла (с новым идентификатором и отметкой в своей базе об удалении старого), это нормально и вообще то считается фичей, ну возможно не сильно эффективной по месту на диске.
КМК, 1000 файлов в каталоге это беда для fat32. Файловые системы из linux этим не страдают. Время открытия файла из ext4 сильно изменится. Правда, листинг такого каталога (для backup к примеру) будет делаться дорого.
Можно конечно выкрутиться. Точно помню, есть файловые системы, позволяющие вынести хранение именно данных файлов на отдельный носитель, а всю структуру каталогов, распределение файлов по диску и журналы — на другом, например маленьком и быстром — оперативная память или ssd, толи jfs толи xfs, когда то давно я с этим игрался.
Но все равно это лишняя прослойка, добавляющая накладных расходов. Подумайте, во что превратится резервное копирование такого хранилища.
Для такого хранилища файловая система избыточна. Раз все атрибуты файла хранятся в БД, а никакие свойства ФС не нужны, можно дописывать картинки в конец бинарного файла и запоминать в базе смещение. Нет проблем с инодами. С неиспользованным местом при хранении мелких файлов. С сохранением удаленных и перезалитых картинок. С дедупликацией… С backup… Удалять файлы навсегда трудно, ага.
1000 файлов в каталоге это беда для fat32. Файловые системы из linux этим не страдаютУ меня как раз сейчас стоит задача положить около миллиона файлов в один каталог. (Зачем? Ответ — иначе придется сильно перепиливать legacy-код, а этого делать не хочется.)
Каталог планируется разместить на отдельном 10 терабайтном HDD на ext4, который через симлинк будет подключен к основной файловой системе.
Формально пишут, что число файлов в одном каталоге ext4 не ограничено. Утверждают что поиск файла в директории в ext4 идет по B-tree, т.е. вроде должен быть быстрым на большом количестве записей. Но все равно опасаюсь «подводных камней».
Подскажите, пожалуйста, на какие проблемы я могу нарваться?
Если же внутри есть opendir/readdir — будут тормоза.
Можно провести эксперимент. Создать текстовый файл в 1М рандомных имен файлов.
time for ((i=0; i<1000000; i++)); do dd if=/dev/urandom bs=512 count=1 2>/dev/null | md5sum - | awk '{print $1}' >> list.txt; done
real 49m6.400s
user 6m24.928s
sys 9m55.281s
Создать 1М этих файлов (непустых, для чистоты эксперимента).
time while read l; do echo "$l$l" > dir/$l ; done < list.txt
real 2m51.659s
user 0m35.826s
sys 0m32.230s
Посмотреть, сколько времени займет cat ${произвольный_файл_из_середины_списка}
time cat dir/5f818b958f8b4be383b13d70145ad671
5f818b958f8b4be383b13d70145ad6715f818b958f8b4be383b13d70145ad671
real 0m0.018s
user 0m0.000s
sys 0m0.000s
Создать новый файл в огромном каталоге
time touch dir/876685e36bf04e096b40ba987c843ff8_
real 0m0.291s
user 0m0.000s
sys 0m0.000s
Новый файл в пустом каталоге
time touch 876685e36bf04e096b40ba987c843ff8_
real 0m0.002s
user 0m0.000s
sys 0m0.000s
Листинг огромного каталога
time ls dir| wc -l
1000001
real 1m16.389s
user 0m8.033s
sys 0m1.572s
Файловые системы из linux этим не страдают.
Страдают. Только это не так явно проявляется, как в винде, и на больших размерах каталогов.
Готовые решения завсегда по стоимости выше своего получаются, ведь там приходится платить не только затраты на собственно задачу но и налоги и маржу нескольких посредников.
Тем более в задачи не сказано о характере нагрузки, количестве пользователей и вообще необходимости онлайн, поэтому оценить различия в затратах не представляется возможности. А так да, существует ограниченный список задач, при которых s3 оправдан более чем.
Данный принцип хранения файлов используется в EMC Documentum со времен царя Гороха.
С другой стороны это говорит только о том, что мысль XanderBass ушла в правильном направлении. )
Во-первых, Вам для простой отдачи файла нужно лезть за ним в БД, это уже избыточно.
Во-вторых, Вы резко повышаете траффик между приложением и БД. Вместо прежних (допустим) 5 запросов вытаскивающих (допустим) 100кб данных Вы вдруг тянете 20 фоток по 500кб каждая — в 5 раз больше запросов и в 100 раз больше траффик. Это сильно сказывается даже если БД на том же сервере.
В-третьих, Вы тут же теряете возможность работы с файлами штатными средствами. Отдать через sendfile в nginx, запроксировать на отдельном серваке для статики, сделать выборочный бакап и т.д. — для всего этого вдруг будете начинать городить колхоз.
p.s.: vbulletin (форум) по умолчанию хранил (а может и хранит) аттачменты в файлах, простой перенос файлов из БД в ФС при достаточно большом форуме приводит к росту отзывчивости сайта и снижению нагрузки аж на 2 порядка, в зависимости от запущенности ситуации с этим.
2. скриптом читать url, запрашивать редис, возвращать путь на диске(дисках/серверах/cdn), путь передать в X-Accel-Redirect
<?php
// Get requested file name
$path = получаем пути;
// лезем в мемкэш и получаем путь реальный
header("X-Accel-Redirect: /files/" . $pathReal);
?>
3. не забыть про nginx
location / {
rewrite ^/file/(.*) last;
proxy_pass http://127.0.0.1:8080/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /files {
root /var/www/mountedstorage;
internal;
}
5. достичь просветления и переписать с php на lua и получить
Если уж делать контроль доступа и скрытие реальных путей, то быстрее от php получить редирект или 403/404 и отдать его nginx, чем отдавать файл из php.
По теме — чанки это хорошо. Но не по два символа.
В моем случае первый два это маппинг на 16х16 групп-серверов (если таковое нужно), запланированное горизонтальное масштабирование, и потом тупо id файла разбитый на чанки по целым тысячам
Например /DD/000/494/494486.jpeg, где 494486 и есть автоинкрементный id.
Хранение большого количества файлов