» »

[baze] Povezava do slike ali BLOB?

[baze] Povezava do slike ali BLOB?

overlord_tm ::

Zivjo!

Delam eno stran, oz. webshop. In sedaj sem prisel do problema oz vprasanja, ali je bolje shranjevati slike na datotecni sistem, in potem v bazo shraniti povezavo, ali pa celotno sliko shraniti kot BLOB.

Delam z PHP in MySql.

datotecni sistem:
+ vecina dela tako (vsaj kar jaz poznam)
+ za moje pojme malenkost hitrejse
- velika zmeda ko imas veliko datotek

baza:
+ lazje upravlanje (recimo slike se same zbrisejo ko zbrises zapise povezane z njimi)
- počasnejše?

Vprašanje sedaj pa je, zakaj toliko baz podpira BLOB polja, če ni nekih velikih prednosti?

Malo sem pogooglal, in večina je pristala na koncu na shranjevanju v datotecni sistem ... so bile pa to stare teme, tako da mozno da je bilo kaj napredka na tem prodrocju.

arjan_t ::

shranjuj na disk

Kot blob ti po nepotrebnem obremenjuje bazo in skripte

mHook ::

Saj baza je tudi na disku.

Če imaš zadevo v bazi je tudi bakup enostavnejši, nimaš težav z konsistenco (imaš zapis v bazi, slike pa ne, in obratno).
Transakcije v bazi so urejene, na disku ne.
Če pa delaš kakšno obdelavo slik (resize,...) potem moraš pa najprej narediti datoteko (razen če uporabljaš metode, ki delajo z byte array), tako da je opcija z datoteko enostavnejša.

MS SQL Server 2008 (trenutno še CTP) ima rešitev za to, in sicer FILESTREAM storage type.
Več: FILESTREAM Overview in Store any data in SQL Server 2008

mHook ::

Pa še važno je, ali boš imel veliko število majhnih datotek ali manjše število velikih datotek.

sverde21 ::

Definitivno raje uporabi datotečni sistem, ker pri podajanju slik iz baze moraš za vsako sliko posebi zaganjati PHP skripto oz. pač nek vmesnik, ki ti sliko iz baze pobere, tak da obremenjuješ brez potrebe server, pa tudi lahko se zgodi da bo komu pri malce večji sliki vsekalo timeout... sam potem ne moraš nadaljevati prenosa na sredini tak kot ga lahk če uporabljaš FS, ampak moraš še 1x začeti lepo jovo na novo vlečt.
<?php echo `w`; ?>

zdobersek ::

Uporabiš lahko tudi oboje, primarno datotečni sistem, slike pa še shranjuješ v bazo in delaš reden backup, če se zgodi kaj nepričakovanega.

arjan_t ::

@mHook:

In če je na disku? Branje iz baze obremenjuje CPU, branje iz diska ne (ok minimalno)
Baza bo hitreje preobremenila server kot branje iz diska, sploh če imaš že dosti branj/pisanj v bazo

BlueRunner ::

Primarno podatkovna baza, zahtevane slike shranjuješ na lokalen disk spletnih strežnikov (lahko jih imaš več). Na ta način imaš "stateless web front", kar ti omogoča hitro obnovo in/ali širjenje kapacitet na sprednji strani. Na zadnji strani pa delaš varnostno kopijo samo enega sistema, kar ti prihrani veliko sredstev. Na spletnem strežniku datoteke (slike) shranjuješ lokalno na disk zato, še vedno pa jih strežeš preko posebnega modula/skripte, zato da imaš centralno kontrolo. Da pri fleksibilnosti obdržiš tudi performance, izkoristiš datoteke zato, da jih strežeš na način, ki izkorišča sistemske funkcije za prenos neposredno iz diskovnega podsistema v mrežni podsistem. Način kako to doseči je odvisen od izbrane platforme/spletnega strežnika.

Glede količine podatkov ti baza ne bi smela niti mežikniti, če jo pravilno dimezioniraš. Dobro pravilo spletnih aplikacij pa je, da paziš, da v spletni aplikaciji nikoli, ali skoraj nikoli ne prihaja do 1-1 razmerja med vhodno zahtevo klienta in izhodno zahtevo na podatkovno bazo. Če pri temu ne paziš, potem se ti lahko hitro zgodi, da bo spletni strežnik še vedno zehal, celoten sistem pa se bo ustavil samo zato, ker bo podatkovni strežnik počepnil pod težo števila sočasnih zahtevkov (DoS napad).

Konkretni odgovori za sverde21 pa so:
- hitrost dobro napisanega modula/skripte je na današnji strojni opremi tolikšna, da bo vsej verjetnosti prej počepnila podatkovna baza, kot pa spletni strežnik
- timeout dobiš, če ne veš kaj delaš
- prenos na sredini lahko narediš, če uporabiš API-je, ki v vsaki resni podatkovni bazi omogočajo prenos dela podatkov iz BLOB stolpcev
- hitrost prenosa datoteke vedno presega vse ostale možnosti, vendar pa z pametno izbiro pristopa negiraš slabe lastnosti podatkovne baze, hkrati pa zanemarljivo upočasniš povprečen čas potreben za začetek izvrševanja zahteve.

Skripte dodajajo latenco, praviloma pa ne vplivaho na prepustnost - če temu ni tako, je težava bodisi v skripti, bodisi v okolju. ASP.NET v IIS6 je tako notorično zanič za prenos datotek, ker .NET sistemskih funkcij ne izkorišča. ISAPI dodatek napisan v C++ (cca. 100 vrstic kode) v tem primeru deluje po pričakovanjih. Veliko težav bo tudi v okoljih, ki privzeto uporabljajo serializacijo zahtev, zaradi drugih omejitev.

Na kratko: če ne želiš biti pameten, pusti datoteke na disku, ker bodo tam najboljše delovale, ne da se bi preveč trudil z razmišljanjem o sistemih, strežnikih, API-jih, ... Ta način rešitve ni ravno najbolj eleganten (po mnenju tistih, ki takšne aplikacije potem vzdržujejo), mu pa nič ne manjka, pa tudi ničesar ne moreš narediti narobe. Če hočeš biti zvit, jih daj v podatkovno bazo, vendar pa pazi, da boš res dovolj zvit, sicer ti sistem ne bo deloval ne dobro, ne po pričakovanjih. Skratka, ne rini v to smer, če nisi 100% prepričan, da res veš kako boš to pravilno realiziral.

darkolord ::

> ker .NET sistemskih funkcij ne izkorišča

jih, še posebej za file I/O, tako da je v tem prav tako hiter kot recimo C++

Zgodovina sprememb…

Fizikalko ::

Za web aplikacijo? Absolutno url. Kakšen BLOB neki, ne ga srat.

BlueRunner ::

@darkolord: Ahem... rekel sem v kontekstu ASP.NET na IIS6. Da pa ne bo rekla, kazala, ti dam kar kodo, ki jo lahko vsakdo testira sam. Da pa ne bomo vse pustili na akademskem nivoju, ti dam kar kodo. Morda res nisem dober programer, vem pa kaj delam in kaj govorim (in ja... v kodi so tudi bug-i, če jih najdeš, jih popravi in objavi popravke - licenca je AGPL).

Originalen "problem" se je nahajal na blogu http://blogs.ugidotnet.org/kfra/archive/2006/10/04/50003.aspx. Neimenovani razvijalci so to prepisali, ne da bi razmislili o delovanju, zaradi česar je strežnik, ko je dosegel dovolj veliko število sočasnih povezav, zapadel v neko stanje neodzivnosti, kjer je vztrajal po nekaj minut. Po prvem pregledu je postalo očitno, da je težava (tesno grlo) v pomnilniku, kjer se je sistem ustavil zaradi velikeča števila page-in, page-out operacij.

Seveda je bila prva rešitev problema očitna. Odpraviti nesmiselno branje v "managed" pomnilnik objekta in nazaj, ter to nadomestiti z klicom funkcije, ki naj bi me odrešila nepotrebnega kopiranja gor in dol po pomnilniku.

Koda v C#, ki je nastala, je potem takšna:
/*
This code is licensed under AGPL v3.0.
License text is included in file LICENSE.txt in main project directory.
Missing file doesn't mean the code is unlicensed! It just means that you
have to download the text of the license yourself.
License text is available from: http://www.fsf.org/licensing/licenses/agpl-3.0.html
*/


using System;
using System.Web;
using System.IO;


namespace BlackNet.Hosting.Gadgets.Streaming
{
	public class FLVStreaming : IHttpHandler
	{
		private static readonly byte[] m_flvHeader = { 0x46, 0x4C, 0x56, 0x01, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x09 };

		#region IHttpHandler Members

		bool IHttpHandler.IsReusable {
			get { return true; }
		}

		void IHttpHandler.ProcessRequest(HttpContext context) {
			try {
				long pos;
				long length;
				FileInfo fileInfo;


				fileInfo = new FileInfo(context.Request.PhysicalPath);

				// Check start parameter if present
				long.TryParse(context.Request.Params["start"], out pos);
				if (pos < 0) pos = 0;
				if (pos > fileInfo.Length) pos = 0;
				length = fileInfo.Length - pos + (pos == 0 ? 0 : m_flvHeader.Length);

				// Add HTTP header stuff
				context.Response.Cache.SetCacheability(HttpCacheability.Public);
				context.Response.Cache.SetLastModified(fileInfo.LastWriteTime);

				context.Response.AppendHeader("Content-Type", "video/x-flv");
				context.Response.AppendHeader("Content-Length", length.ToString());

				// Send the file to the output stream if needed
				if (string.Compare(context.Request.RequestType, "GET", true) == 0) {
					// Append FLV header if needed
					if (pos > 0) {
						context.Response.OutputStream.Write(m_flvHeader, 0, m_flvHeader.Length);
					}
					context.Response.WriteFile(context.Request.PhysicalPath, pos, length - pos);
				}
			} catch (Exception ex) {
				System.Diagnostics.Debug.WriteLine(ex.ToString());
				throw ex;
			}
		}

		#endregion
	}
}


Poanta je bila v temu, da metoda HTTPResponse.WriteFile ne uporablja medpomnilnika na "neumen način", temveč se zanaša na sistemske API-je. Na žalost se je pri testiranju izkazalo, da ima tudi ta verzija identične težave, kot jih je imela verzija iz blog-a. Kar pomeni, da je za resno delo praktično neuporabna.

Ker sem pri vsem skupaj posumil, da je težava dejansko v implementaciji metode (pri čemer zaradi dobrih razlogov ne smem pregledovati originalne kode), sem celotno zadevo prestavil v čisto navaden ISAPI modul implementiran v C-ju. Njegova koda je:

/*
This code is licensed under AGPL v3.0.
License text is included in file LICENSE.txt in main project directory.
Missing file doesn't mean the code is unlicensed! It just means that you
have to download the text of the license yourself.
License text is available from: http://www.fsf.org/licensing/licenses/agpl-3.0.html
*/


#include "stdafx.h"

#ifndef FINAL_RELEASE
#define DEBUG_OUT(x) DebugOut##x
#else
#define DEBUG_OUT(x)
#endif

VOID DebugOut(LPCTSTR fmt, ...);
VOID WINAPI FinishCallbackFunction(LPEXTENSION_CONTROL_BLOCK lpECB, PVOID pContext, DWORD cbIO, DWORD dwError);

// This is an example of an exported function.
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer) {
	pVer->dwExtensionVersion = HSE_VERSION;
	lstrcpyA(pVer->lpszExtensionDesc, "Flash Video start parameter handling filter");

	return TRUE;
}

BOOL WINAPI TerminateExtension(DWORD dwFlags) {
	dwFlags;
	return TRUE;
}

DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB) {
	static CHAR okStatus[] = "200 OK";
	static CHAR errorStatus[] = "500 Internal Error";
	static BYTE flvHeader[] = { 0x46, 0x4C, 0x56, 0x01, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x09 };
	static int flvHeaderLength = 13;
	static CHAR *daysOfWeek[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
	static CHAR *monthNames[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

	DWORD dwBufferLength;
	CHAR headerBuffer[2048];
	LPSTR headerBufferLastChar;
	HSE_TF_INFO transmitFileInfo;
	HANDLE hFile;
	LARGE_INTEGER fileSize;
	LARGE_INTEGER startOffset;
	LARGE_INTEGER contentLength;
	LARGE_INTEGER tmpMul;
	SYSTEMTIME lastModified;
	CHAR *szQueryString;
	DWORD dwErr;
	BY_HANDLE_FILE_INFORMATION fileInformation;
	int ii, len;


	// Get query string
	dwBufferLength = 0;
	lpECB->GetServerVariable(lpECB->ConnID, "QUERY_STRING", headerBuffer, &dwBufferLength);
	dwErr = GetLastError();
	if (dwErr != ERROR_INSUFFICIENT_BUFFER) {
		DEBUG_OUT((TEXT("Error reading query string: 0x%08X\r\n"), dwErr));
		return HSE_STATUS_ERROR;
	}
	startOffset.QuadPart = 0;
	if (dwBufferLength > sizeof(CHAR)) {
		szQueryString = (CHAR*)GlobalAlloc(GPTR, dwBufferLength);
		if (szQueryString == NULL) {
			DEBUG_OUT((TEXT("Error allocating memory for query string copy\r\n")));
			return HSE_STATUS_ERROR;
		}
		if (lpECB->GetServerVariable(lpECB->ConnID, "QUERY_STRING", szQueryString, &dwBufferLength) != TRUE) {
			DEBUG_OUT((TEXT("Error reading query string: 0x%08X\r\n"), GetLastError()));
			return HSE_STATUS_ERROR;
		}
		dwBufferLength = dwBufferLength/sizeof(CHAR) - 1; // Buffer length to string length

		DEBUG_OUT((TEXT("Query string: %hs\r\n"), szQueryString));

		// check the query string to read the parameter
		if (CompareStringA(LOCALE_INVARIANT, NORM_IGNORECASE, "START", 5, szQueryString, min(5, dwBufferLength)) == CSTR_EQUAL) {
			for (ii = 6, len = dwBufferLength; ii < len && *(szQueryString + ii) != '&'; ii++) {
				//DEBUG_OUT((TEXT("(Loop: %i) Start offset: %lu; Char: %hc\r\n"), ii, startOffset.QuadPart, *(szQueryString + ii)));
				if (*(szQueryString + ii) < '0' || *(szQueryString + ii) > '9') {
					startOffset.QuadPart = 0;
					break;
				}
				// Multiply by 10 is little more complicated, to avoid __allmul
				// First I calculate upper part
				tmpMul.QuadPart = startOffset.HighPart * 10;
				// which must not overflow
				if (tmpMul.HighPart > 0) {
					startOffset.QuadPart = 0;
					break;
				}
				// Then I add the lower part
				startOffset.HighPart = tmpMul.LowPart;
				tmpMul.QuadPart = startOffset.LowPart * 10;
				startOffset.HighPart += tmpMul.HighPart;
				startOffset.LowPart = tmpMul.LowPart;
				// Then add what remained
				startOffset.QuadPart += *(szQueryString + ii) - '0';
			}
			DEBUG_OUT((TEXT("Final start offset: %lu\r\n"), startOffset.QuadPart));
		}
		GlobalFree(szQueryString);
	}

	// Test if request file exists & is readable
	hFile = CreateFileA(lpECB->lpszPathTranslated, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		DEBUG_OUT((TEXT("Error code for opening file \"%hs\" was 0x%08X\r\n"), lpECB->lpszPathTranslated, GetLastError()));
		return HSE_STATUS_ERROR;
	}
	GetFileInformationByHandle(hFile, &fileInformation);
	FileTimeToSystemTime(&fileInformation.ftLastWriteTime, &lastModified);
	fileSize.LowPart = fileInformation.nFileSizeLow;
	fileSize.HighPart = fileInformation.nFileSizeHigh;

	// Calculate content length
	if (fileSize.QuadPart > startOffset.QuadPart) {
		contentLength.QuadPart = fileSize.QuadPart - startOffset.QuadPart + (startOffset.QuadPart > 0 ? flvHeaderLength : 0);
	} else {
		contentLength.QuadPart = fileSize.QuadPart;
		startOffset.QuadPart = 0;
	}


	// Prepare the header to be sent
	headerBufferLastChar = headerBuffer;
	wsprintfA(
		headerBuffer,
		"Cache-Control: public\r\nLast-Modified: %hs, %02d %hs %d %02d:%02d:%02d GMT\r\nContent-Type: video/x-flv\r\nContent-Length: %lu\r\n\r\n",
		daysOfWeek[lastModified.wDayOfWeek], lastModified.wDay, monthNames[lastModified.wMonth - 1], lastModified.wYear,
		lastModified.wHour, lastModified.wMinute, lastModified.wSecond,
		contentLength.QuadPart);
	headerBufferLastChar = headerBuffer + lstrlenA(headerBuffer);

	if (startOffset.QuadPart > 0) {
		CopyMemory(headerBufferLastChar, flvHeader, flvHeaderLength);
		transmitFileInfo.HeadLength = (headerBufferLastChar - headerBuffer) + flvHeaderLength;
	} else {
		transmitFileInfo.HeadLength = headerBufferLastChar - headerBuffer;
	}

	// Send the file
	transmitFileInfo.pfnHseIO = FinishCallbackFunction;
	transmitFileInfo.pContext = (LPVOID)hFile;
	transmitFileInfo.hFile = hFile;
	transmitFileInfo.pszStatusCode = okStatus;
	transmitFileInfo.BytesToWrite = 0;
	transmitFileInfo.Offset = (DWORD)startOffset.QuadPart;
	transmitFileInfo.pHead = headerBuffer;
	//transmitFileInfo.HeadLength is set above
	transmitFileInfo.pTail = NULL;
	transmitFileInfo.TailLength = 0;
	transmitFileInfo.dwFlags = HSE_IO_ASYNC | HSE_IO_DISCONNECT_AFTER_SEND | HSE_IO_SEND_HEADERS;

	if (lpECB->ServerSupportFunction(lpECB->ConnID, HSE_REQ_TRANSMIT_FILE, &transmitFileInfo, NULL, NULL) == TRUE) {
		return HSE_STATUS_PENDING;
	} else {
		return HSE_STATUS_ERROR;
	}
}

VOID WINAPI FinishCallbackFunction(LPEXTENSION_CONTROL_BLOCK lpECB, PVOID pContext, DWORD cbIO, DWORD dwError) {
	cbIO;
	dwError;
	if (!CloseHandle((HANDLE)pContext)) {
		DEBUG_OUT((TEXT("Error closing file handle: 0x%08X\r\n"), GetLastError()));
	}
	lpECB->ServerSupportFunction(lpECB->ConnID, HSE_REQ_DONE_WITH_SESSION, NULL, NULL, NULL);
}

VOID DebugOut(LPCTSTR fmt, ...) {
	TCHAR buffer[1024];
	va_list valueList;

	va_start(valueList, fmt);
	wvsprintf(buffer, fmt, valueList);
	OutputDebugString(buffer);
}


Pri testiranju, in sedaj tudi po dolgem času v produkciji, se je izkazalo, da je ISAPI filter performančno zanemarljiv (povprečna izmerjena latenca na testni strojni opremi je bila 2ms glede na prenos čiste datoteke). Sočasno obvladuje do 800 sočasnih zahtev, ne da se bi to poznalo na CPU-ju ali pomnilniku, saj omejitev postane diskovni podsistem, ki je na testiranju brez ISAPI modula dosegel identičnih 800 zahtev/s. Dejansko je celoten modul samo priprava parameterov za klic pomožne funkcije HSE_REQ_TRANSMIT_FILE (ServerSupportFunction), ki je namenjena ravno takšnemu prenosu datotek.

Čeprav nisem videl izvorne kode .NET, pa lahko na podlagi testnih rezultatov sklepam, da je konkretna implementacija konkretne metode namenjene reševanju konkretni zahteve v ASP.NET na IIS6 v splošnem "ne ravno najboljša". Da sem izločil tudi možnost slabe implementacije celotnega APS.NET podsistema, sem testiral tudi prazen modul, ki je naredil vse, razen pošiljanja datoteke iz diska, kar je bilo nadomeščeno s pošiljanjem sprotno generiranih naključnih vrednosti. Tudi ta test je pokazal, da v samem ASP.NET ni težave, kjer simptomov pristonih pri prenosu datotek nismo več mogli ponoviti.

Glede na zbrane podatke iz testiranja obeh C# variant in testiranja praznega modula, lahko z veliko gotovostjo sklepam, da implementacija metode HTTPResponse.WriteFile ne izpolnjuje pričakovanj. Glede na to, da s samim IIS6 strežnikom ni bilo nič narobe, kot je pokazalo tesiranje ISAPI variante, lahko še enkrat ponovim, da implementacija v konkretnem primeru ne izpolnjuje pričakovanj.

Seveda je to specifičen primer, ki pa zelo dobro osvetli težave s katerimi se srečujemo pri prenosu podatkov iz periferije v "unamaged" pomnilnik, jih premikamo v "managed" pomnilnik, iz "managed" pomnilnika znova takoj nazaj v "unamaged" pomnilnik, nato pa nazaj na periferijo. Veliko kopiranja gor in dol po pomnilniku, ki se mu zaradi same narave okolja ne moremo izogniti. Za večino poslovnih aplikacij je težava nepomenbna, ker podatki opravijo samo polovico poti (od periferije do "managed" pomnilnika, ali obratno), samo branje, ali pa pisanje pa ni operacija, ki bi imela frekvenco 1000/s. .NET implementacija je lahko v splošnem odlična, lahko je tudi najboljša od vseh možnih, vendar pa je na eni točki zanič in za določen namen tudi neuporabna.


Vredno ogleda ...

TemaSporočilaOglediZadnje sporočilo
TemaSporočilaOglediZadnje sporočilo
»

NEC ND-3540A problem...

Oddelek: Pomoč in nasveti
282000 (1291) dean444
»

LG GSA-4120B dela probleme pri pečenju DL DVD+Rjev

Oddelek: Strojna oprema
441318 (1127) mtosev
»

Plextor writer 24/48/48 problem! (strani: 1 2 )

Oddelek: Strojna oprema
643441 (2950) Hux
»

Težave s pekačem

Oddelek: Pomoč in nasveti
121353 (1293) boštjan
»

PLEXTOR BurfProof unicuje cdje - prosim pomagajte!

Oddelek: Pomoč in nasveti
91182 (936) krho

Več podobnih tem