Чтобы почти окончательно развеять куцую интригу и помочь возможному читателю определиться с тем, имеет ли смысл продолжить чтение, опишу конечный результат оптимизации: обложки по-прежнему останутся в базе данных, причём в поле того же типа, но вот веб-сервер, отдающий в итоге изображения клиенту, будет «обманут» и станет обращаться за файлом знать не зная, что его на самом деле нет в ФС, а вызов идёт сразу и непосредственно к СУБД. Такой «обман» конечно же не является самоцелью – всё в основном затевалось ради уменьшения нагрузки на дисковую подсистему.
Старая реализация
Прежде всего необходимо напомнить структуру HTML-тэга
img
, обязательным у которого является лишь атрибут src
:<img src="some_path/file_name.jpg">
У несложных сайтов изображение, находящееся по URL в этом атрибуте, веб-сервер обычно напрямую извлекает из ФС, т. к. относительный путь к нему очень часто совпадает со структурой каталогов на сервере, а имя файла тоже берётся непосредственно из ФС; однако такое распространённое поведение, если брать конкретно IIS (именно он применяется в данном проекте), может быть заменено на совершенно другое за счёт модулей (расширений), когда изображения станут, к примеру, генерироваться на лету (т. е. физически они нигде не хранятся). Так вот статья затрагивает лишь первый вариант.
На этом сайте обложки располагаются не в файловой системе из-за желания «всё своё носить с собой» – чтобы резервная копия БД содержала полный набор информации, была самодостаточной; соответственно, дабы веб-сервер мог отдать изображение клиенту, оно, как уже говорилось, должно быть извлечено из базы данных и сохранено в виде файла, что добавляет лишнюю операцию: сначала читаем из БД, после чего записываем копию на диск, бездарно и теряя дисковое пространство, и нагружая эту подсистему. Ко всему прочему, если несколько разных пользователей относительно одновременно запросят одну и ту же обложку, то из-за того, что в uniGUI каждый из них представлен отдельной сессией, файл дополнительно продублируется ещё и для каждой из них.
Схематически и упрощённо описанное непотребство можно представить следующим образом:
Если проиллюстрировать прошлую реализацию кодом, то его логика будет иметь довольно классический вид:
- Запрос на выборку данных возвращает, среди прочих, и BLOB-поле с обложкой (в данном случае на примере компонента
spAlbums
типаTFDStoredProc
из состава FireDAC). - Изображение из поля, в зависимости от формата (png, jpg…), загружается в соответствующего наследника TGraphic.
- Далее используется предоставляемая uniGUI функция
uniImageToURL
, сохраняющая изображение в нужную папку и возвращающая готовый URL для атрибутаsrc
.
uses
System.SysUtils, System.Classes, Data.DB,
Vcl.Graphics, Vcl.Imaging.jpeg, Vcl.Imaging.pngimage, Vcl.Imaging.GIFImg,
uniGUIUtils;
function TMainForm.AlbumsToHTML: string;
function CoverURL: string;
var
CoverFormat: string;
Image: TGraphic;
ImageStream: TStream;
begin
CoverFormat := spAlbums['CoverFormat'];
if CoverFormat = 'jpg' then
Image := TJPEGImage.Create;
if CoverFormat = 'png' then
Image := TPngImage.Create;
if CoverFormat = 'gif' then
Image := TGIFImage.Create;
if CoverFormat = 'bmp' then
Image := TBitmap.Create;
try
ImageStream := spAlbums.CreateBlobStream
(
spAlbums.FieldByName('Cover'),
bmRead
);
try
Image.LoadFromStream(ImageStream);
finally
ImageStream.Free;
end;
Result := uniImageToURL(Image);
finally
Image.Free;
end;
end;
const
AlbumHTMLTemplate =
'<div class="album">' +
'<div class="cover-container">' +
'<img src="%s" alt="Обложка альбома">' +
...
'</div>' +
'</div>';
begin
spAlbums.First;
while not spAlbums.Eof do
begin
Result := Result + Format( AlbumHTMLTemplate, [CoverURL, ...] );
spAlbums.Next;
end;
end;
Идеальный вариант
Если предаться мечтаниям и представить, что обложки по-прежнему находятся в БД в BLOB-поле, а IIS каким-то образом, почти без усилий и программирования с нашей стороны, сам извлекает их, то окажется, что SQL Server способен такую мечту исполнить – за счёт функционала FileTable, представляющего собой, если очень кратко, создаваемую разработчиком специального вида таблицу, каждая запись которой отображается в файл в NTFS; обращение к таким файлам ничем не отличается от стандартного (однако некоторые возможности не поддерживаются), т. е. их можно просматривать, изменять и удалять хоть через Проводник.
Если снова графически показать такую схему, то мы избавимся от копирования обложки в папку сайта:
Данный вариант полностью совместим и с отложенной загрузкой изображений, настраиваемой скажем через атрибут
loading
, т. е. если пользователь не прокрутит страницу сайта до некоторых обложек, то они и не будут прочитаны из базы (написанное, разумеется, справедливо и для случая, когда в браузере вообще отключен показ изображений):<img src="some_path/file_name.jpg" loading="lazy">
Чтобы воспользоваться этой манной небесной, ниспосланной SQL Server, потребуется, само-собой, выполнить некоторые действия как на стороне СУБД, так и на стороне IIS, чему собственно и посвящена оставшаяся практическая часть статьи. Но предварительно хотелось бы сделать небольшую ремарку и отметить, что конкуренты Microsoft тоже могут предложить тонкую работу с BLOB, но функционал уровня FileTable автору удалось найти лишь у Oracle – речь о Database File System (DBFS). Остальные же игроки предоставляют лишь условно традиционный (но неизбежно СУБД-специфичный) API, несколько примеров:
Кстати, использующим Express-редакцию SQL Server наверняка весьма понравится то, что данные в файловых таблицах не учитываются при проверке лимита на размер БД в 10 Гб, т. к. реализация FileTable основывается на технологии FILESTREAM, как раз и свободной от этого ограничения у бесплатной версии.
Настройка SQL Server
По умолчанию FileTable-возможности отключены в SQL Server. Официальная документация содержит весьма подробную и внятную инструкцию по параметрам, которые необходимо задействовать как на уровне всего экземпляра, так и в конкретной базе данных, отчего не представляется разумным пересказывать здесь подобные тонкости – вместо этого автор сосредоточится на небольшом SQL-скрипте, выполняющем перенос обложек из существующей таблицы с альбомами в новую FileTable-таблицу (если же в Вашем случае файлы изначально в БД не хранятся, в отличие от данного проекта, то с их адаптацией к FileTable поможет документация).
Таблицы
Далее в скрипте предполагается следующая структура двух только что упомянутых таблиц (источника – с данными об альбомах, и приёмника – новой таблицы с обложками):
- Источник
dbo.Album
:Поле Тип данных Описание ID
integer
Первичный ключ. Cover
varbinary(max)
BLOB-поле с собственно изображением, которое нужно перенести в новую таблицу. CoverFormat
nvarchar(10)
Тип изображения (jpg, png и т. д.). Прочие поля (название альбома, год издания и т. п.)... -
Приёмник
dbo.AlbumCover
имеет жёстко заданную структуру со множеством полей (задача создания такой таблицы тривиальна и описана в справке), но нас заинтересуют лишь эти:Поле Тип данных Описание path_locator
hierarchyid
Первичный ключ. file_stream
varbinary(max)
Содержимое файла (в нашем случае это изображение). name
nvarchar(255)
Название файла (с расширением), отображаемое в ФС. Прочие поля...
Cover
и CoverFormat
он в конечном итоге станет содержать лишь одно – идентификатор обложки из поля path_locator
:Поле | Тип данных | Описание |
---|---|---|
CoverID |
hierarchyid |
Внешний ключ на приёмник. |
Прочие поля... |
Скрипт
Все действия с базой логически делятся на несколько этапов:
- Банальнейшее по реализации добавление поля
CoverID
в источник:ALTER TABLE dbo.Album ADD CoverID hierarchyid;
- Копирование данных в приёмник и заполнение только что созданного поля
CoverID
(про неожиданное применение конструкцииMERGE
пояснение дано ниже):DECLARE @Covers TABLE ( AlbumID integer NOT NULL, path_locator hierarchyid NOT NULL ); MERGE dbo.AlbumCover AS cover USING ( SELECT ID, CoverID, Cover, CAST( NEWID() as nvarchar(max) ) + '.' + CoverFormat AS FileName FROM dbo.Album ) AS album ON album.CoverID = cover.path_locator WHEN NOT MATCHED THEN INSERT (file_stream, name) VALUES (album.Cover, album.FileName) OUTPUT album.ID, INSERTED.path_locator INTO @Covers; UPDATE dbo.Album SET CoverID = path_locator FROM @Covers WHERE ID = AlbumID;
ИспользованиеMERGE
вместо конструкцииINSERT
продиктовано невозможностью в последней обратиться в блокеOUTPUT
к полям любых задействованных таблиц (допустимы только поля той, в которую и происходит вставка). Другими словами, было бы нечем заполнить полеAlbumID
табличной переменной@Covers
, т. к. обращение ниже выполняется к таблице-источнику, а вставка идёт вdbo.AlbumCover
:... OUTPUT album.ID, ...
- Удаление ставших ненужными полей в источнике:
ALTER TABLE dbo.Album DROP COLUMN Cover, CoverFormat;
Важно отметить, что исчезновение полей не приведёт к уменьшению размера БД и возврату освободившегося места ОС – для этого потребуется явно выполнить команду SHRINKDATABASE.
Почти всё, теперь обложки доступны по специальному UNC-пути, возвращаемому функцией
FileTableRootPath
:SELECT FileTableRootPath('dbo.AlbumCover');
Оговорка про «почти» связана с тем, что читатель может столкнуться с отсутствием доступа при попытке обратиться к означенным файлам – в этом случае следует ознакомиться с той частью следующего раздела, где описывается нюанс с правами. Также для определённости примем, что наш путь выглядит как \\ServerName\MSSQLSERVER\DatabaseName\AlbumCover (далее в другом примере будет отсылка к нему, где для наглядности потребуется конкретное значение).
Настройка веб-сервера
Хотя в текущем разделе демонстрация идёт на примере IIS, но большинство вещей применимы и к любому другому веб-серверу, ибо являются скорее ОС- и СУБД-специфичными.
Права на чтение
Если снова вернуться к атрибуту
src
тэга img
, то становится очевидной небольшая проблема: путь в атрибуте должен быть относительным, а нужные изображения доступны лишь по UNC-пути. Решение довольно просто́ и заключается в создании символической ссылки в папке сайта; если условиться, что физически сайт располагается в каталоге c:\IIS\SiteName, а ссылка будет называться Covers, то её добавление выполняется следующей командой в консоли:mklink /d "c:\IIS\SiteName\Covers" "\\ServerName\MSSQLSERVER\DatabaseName\AlbumCover"
Теперь атрибут
src
станет возможно заполнять подобным образом:<img src="Covers/3AE39458-8925-448A-A5F1-0C0A4524ACF0.jpg">
Доработки Delphi-кода, дающие показанное значение для
src
, будут приведены в конце статьи, а пока же остался важный нюанс с правами (в нашем случае достаточно лишь на чтение), причём делящийся на две части:- Предоставление доступа (на уровне ФС) пользователю IUSR и группе IIS_IUSRS для созданной символической ссылки:
- Одно из неприятных ограничений файловых таблиц заключается в отсутствии поддержки ACL, вследствие чего пользователь, от имени которого выполняется пул приложений IIS, должен получить права на файлы иным способом – средствами самой СУБД, за доступ к ним полностью и отвечающей. Делается всё совершенно стандартным для SQL Server образом, путём создания пары «Имя входа (логин)-пользователь БД»:
- Логин создаётся на основе Windows-пользователя вида IIS APPPOOL\<Имя нужного пула>.
CREATE LOGIN [IIS APPPOOL\YourPoolName] FROM WINDOWS;
- После чего с ним связывается новый пользователь базы.
CREATE USER FileTableReader FOR LOGIN [IIS APPPOOL\YourPoolName];
- Которому выдаётся разрешение на чтение таблицы.
GRANT SELECT ON dbo.AlbumCover TO FileTableReader;
- Логин создаётся на основе Windows-пользователя вида IIS APPPOOL\<Имя нужного пула>.
Проверка подлинности
Последнее, что требуется настроить, относится непосредственно к IIS, а именно к проверке подлинности на сайте – чтобы она выполнялась от имени пула приложений, чуть выше только что получившего права на чтение обложек (если в сценарии читателя проверка обязана проходить от иного пользователя, то данный раздел можно пропустить, но при этом необходимо добавить ещё одну пару «логин-пользователь» как описано в предыдущем разделе, связав имя входа со значением из параметров проверки подлинности).
Новая реализация
В заключение осталось привести новый, значительно облегчённый по сравнению с предыдущим вариантом, код, ибо из БД уже не нужно извлекать само изображение – достаточно лишь получить имя файла с ним (из поля
dbo.AlbumCover.name
, имеющего в примере псевдоним CoverFileName
) и в неизменном виде подставить в атрибут src
:uses
System.SysUtils;
function TMainForm.AlbumsToHTML: string;
const
AlbumHTMLTemplate =
'<div class="album">' +
'<div class="cover-container">' +
'<img src="%s/%s" alt="Обложка альбома">' +
...
'</div>' +
'</div>';
SymbolicLinkName = 'Covers';
begin
spAlbums.First;
while not spAlbums.Eof do
begin
Result := Result + Format( AlbumHTMLTemplate, [SymbolicLinkName, spAlbums['CoverFileName'], ...] );
spAlbums.Next;
end;
end;
Комментарии (37)
rrrad
27.09.2021 22:30+8поставить reverse-proxy с кешем перед веб-сервером и отдавать данные картинки прямо из БД (например, кодируя ид в имени файла) и нагрузка на дисковую подсистему именно на операциях чтения картинок упадёт до пренебрежимо малых значений. Такое решение вполне универсальное.
harios
27.09.2021 23:31+1И каково это, использовать унигуй? Как работает? Какие нюансы?
SergeyPyankov Автор
28.09.2021 12:28Мой опыт использования uniGUI не является показательным, т. к., во-первых, этот фреймворк применялся только в одном (данном) проекте, и, во-вторых, он не предназначен для сайтов с произвольным дизайном, каким является описанный в статье (хотя чисто технически это и возможно сделать, о чём рассказывает прошлая статья). Однако вот служебный сайт, предназначенный для управления содержимым первого (публичного), uniGUI позволил создать очень быстро и качественно.
На Ваш вопрос наиболее полезно смогут ответить те, кто применяет uniGUI по прямому назначению — переносят настольное приложение в веб, например.
Alexufo
27.09.2021 23:40+19IIS, Delphy, картинки в бд? Ребята, куда то вы не туда свернули.
harios
27.09.2021 23:49+4А куда стоило по вашему мнению? Пойти по дорожке React/Vue и NodeJS? PWA? Или сдуть пыть с Java Servlet?)
Alexufo
28.09.2021 04:59Ну как-то вы непонятно спрашиваете. Фронт к дельфям можно любой привязать, судя по всему такой велосипед привязывается неоптимально к чему угодно.
Для начала нормальный веб-сервер взять. Там нет поддержки делфи? Странно, дайте ка подумать почему? Наверное потому потому что никто не пишет на нем сайты? А если пишет, то, наверное, должен понимать очень хорошо этот инструмент? Иначе в ноги настрелаешь. Но судя по тому что уперлись в раздачу картинок складывается впечатление о не очень хорошем понимании работе http и понимании важности асинхронных файловых операций. Упереться в файловые операции? Не в нагрузку на хостинг? Черт подери, да как это можно сделать специально?)) На винде емнип файловые операции медленнее чем на линухе если бенчмарками заниматься.harios
28.09.2021 08:58+1В данном случае фронтом является uniGui, что совсем "не любой", а тот что позволяет вообще не думать о верстке и веб части в принципе.
Для начала нормальный веб-сервер взять.
Зачем? Насколько я понял IIS нужен чисто как файловое хранилище. Если его возможностей достаточно, нужно ли плодить сущности без необходимости? Не думаю.
Там нет поддержки делфи? Странно, дайте ка подумать почему? Наверное потому потому что никто не пишет на нем сайты?
Чувствуется не предвзятое и взвешенное мнение. :)
PocketM
28.09.2021 05:46Со Java Servlet сдувать пыль не нужно, возьмите Spring Boot. Сейчас — это промышленный стандарт в java
nordfox
27.09.2021 23:44+1Апач + мод реврайт + правила в .ht* + простейший скрипт и можно любой элемент сайта засунуть в базу. Хоть весь сайт. Останется только индекс как единая точка входа. Проверено)
nordfox
27.09.2021 23:54+1Главное тут, не создавать никаких файлов! Исключаем дисковые операции. Только память. Кеш на уровне СУБД подкрутить. Надо уметь работать с заголовками.
А если добавить любимый JS Editor, то получим ядро CMS.
Han7
28.09.2021 01:20+13Ох уж эти сказочники...
На практике столкнулся с таким "суперским" хранением.
Затейник уже давно не работал в компании, а вот БД выросла до 2ТБ, и к моменту моего прихода продолжала рост и как следствие имела жуткие тормоза.
Реорганизация системы хранения разом избавил от всех этих напастей. Да самих полезных данных в БД осталось 0,5% от первоначальных, остальное стало обычными файлами.
Alexufo
28.09.2021 05:01+2Даже по логике бекапы базы важнее бекапов картинок. Точнее частоту бекапов можно делать разной. Чем легче база тем легче производить по ней любые операции.
edo1h
28.09.2021 01:24+2В заключение осталось привести новый, значительно облегчённый по сравнению с предыдущим вариантом, код, ибо из БД уже не нужно извлекать само изображение – достаточно лишь получить имя файла с ним (из поля dbo.AlbumCover.name, имеющего в примере псевдоним CoverFileName) и в неизменном виде подставить в атрибут src
прямо напрашивается сравнение вашего решения с обычными файлами в файловой системе
SergeyPyankov Автор
28.09.2021 02:05Если комментарий относился исключительно к коду, в плане заполнения атрибута при хранении обложек не в БД, а в ФС, то он почти и не станет отличаться от приведённого в конце статьи — ведь суть же как раз в том, что для стороннего наблюдателя файлы из FileTable и выглядят как настоящие файлы из любой реальной директории.
Если же речь шла про сравнение производительности, то описанное решение даже в самом распрекрасном случае будет лишь на уровне ФС, т. к. она, по определению, и создана для наилучшего обращения с файлами. Статья же затрагивает вариант, когда они по какой-либо причине находятся в базе данных и требуется повысить эффективность работы с ними.Revertis
28.09.2021 03:01+1для стороннего наблюдателя файлы из FileTable и выглядят как настоящие файлы из любой реальной директории
А зачем это всё?
Можно ведь просто сделать
<img src="get_file.php?id=123456">
, а там простым скриптом брать из базы и возвращать (наверно можно и на asp так же). Без всякой записи в файлы и т.п.Alexufo
28.09.2021 05:10+1Пыха мало, нужно думать о ресайзе, оптимизации, нужно ли ресайзить заранее и что делать если сайт изменил дизайн…
Надо просто хранить картинки нормально на диске, в нормальной структуре с нормальными именами, а в бд хранить или пути к ним или id.
Отображение делать через прокси какой нибудь imgproxy.net и вопрос каких либо изменений, кеширования, закрыт.
SergeyPyankov Автор
28.09.2021 12:54Скорее всего можно (я не знаком с PHP), но каким бы ни был предложенный способ, он же всё равно будет извлекать изображение из базы данных: Ваш скрипт станет делать это, насколько понимаю, через написанный разработчиком SQL-запрос, а вариант из статьи отдаёт всё на откуп самой СУБД (можно сказать, что условным «запросом» выступает просто имя файла).
Revertis
28.09.2021 13:02База закэширует в ОЗУ самые "горячие" файлы.
Скрипт может отдавать файлы через некий прокси, который тоже может кэшить самое горячее, тем самым уменьшив запросы к базе.
Профит.
edo1h
28.09.2021 07:30+1Статья же затрагивает вариант, когда они по какой-либо причине находятся в базе данных
ну вот мне и непонятны эти причины
lam0x86
28.09.2021 02:06+5Можно использовать любой S3-совместимый storage или другой тип CDN с нормальным API и нет проблемы с нагрузкой на ФС, заодно и быстрее работать будет, т.к. грузиться будет с ближайшего сервера.
1div0
28.09.2021 03:35Прикольно. Я вот не знал про такую технологию у MS SQL сервера. Она вряд ли пригодится, но для общего развития интересно. У нас картинки хранятся на диске. В базе только их имена.
mSnus
28.09.2021 06:23Для этого надо было:
использовать PHP
хранить картинки в файлах
настроить кеш
-
отдавать, если не хочется светить имя файла, через параметр скрипта, без всякой возни с настройкой сервера:
<img src="covers.php?3AE39458-8925-448A-A5F1-0C0A4524ACF0.jpg">
Использовать устаревшие и неподходящие технологии -- это как, хм, танцевать в гамаке и в ластах: можно, сложно, вызывает удивление окружающих, но вряд ли кому-то нужно.
ertaquo
28.09.2021 06:32+7Статья, конечно, хорошая... Но не нужно так делать ни в коем случае!!!
База данных - это не файловое хранилище. Если хранить файлы в БД, вы потом задолбаетесь с бэкапами и репликацией данных как минимум. Плюс есть нехилая вероятность, что закончится место на диске и встанет вообще все приложение (а не только загрузка файлов).
Хотите быстро-просто? Храните описания файлов в базе, а содержимое где-нибудь в S3, благо этот протокол много где поддерживается и не завязан целиком на Amazon (можно взять тот же minio, например).
А если хотите получить реальную экономию I/O, то лучше смотрите в сторону HTTP заголовков кеширования и ETag.
artem_larin
28.09.2021 10:35Прочитав статью, я так и не понял, так смогли ли дрим театровцы в Six Degress... достигнуть новых вершин, или же предыдущий Metropolis II - это конец, вершина их творчества? В смысле, где же лучше хранить файлы?
SergeyPyankov Автор
28.09.2021 11:59Материал не про выбор места хранения, а про неизвестный многим способ извлечения файлов из БД, когда выбор уже сделан не в пользу ФС. Пример с обложками отчасти даже можно воспринимать лишь как антураж для демонстрации файловых таблиц.
badstarosta
28.09.2021 13:23Несомненно смогли. Второй диск - прекрасен, на мой взгляд. Да и дальше есть хорошие альбомы, как минимум Train Of Thought и Octavarium ещё весьма торты.
akhkmed
И вправду уменьшилось? За счёт чего? Как сильно?
SergeyPyankov Автор
Замеров не производилось, но возникает встречный вопрос: почему нагрузка должна как минимум остаться на том же уровне (или даже вырасти), если была исключена операция по созданию копии обложки?
akhkmed
Общая нагрузка вырастет, поскольку ACID в базах данных достигается достаточно дорогой ценой большего использования диска/памяти/cpu.
Не стану спорить, что в современных СУБД кеширование реализовано отлично и за счёт этого вы получаете более отзывчивую систему. Но вместе с этим вы получаете слишком большую побочную нагрузку, из-за чего с ростом нагрузки и количества blob вы замедлитесь раньше и сильнее, чем при использовании других подходов к хранению.
SergeyPyankov Автор
Совершенно согласен, накладные расходы в случае БД окажутся несколько выше, о чём, собственно, уже писал ниже:
Не упрёка ради, а прояснения для, Ваши изначальные вопросы скорее всего появились из-за беглого прочтения статьи, потому что в этом проекте изображения сразу, изначально находились в базе, поэтому не получится сравнить сделанную оптимизацию с иным способом хранения.
И на всякий случай повторюсь: