Cекционирование графики при SQL-хранении.
Наконец-то MS сподобилась воспроизвести в SQL-сервере старинную технологию ОРАКЛА - СЕКЦИОНИРОВАНИЕ. И теперь эта технология прочно вошла в мой рабочий ASP2-фреймворк и я решил поделится на этой страничке, как я это делаю.
В сущности эта заметка является продолжением моей прошлогодней темы - Хранение графики в SQL-сервере. На самом деле, в нагруженных сайтах все значительно сложнее, чем я описал в прошлом году для простейших применений. Сложность заключается в том, что есть прежде всего ОРИГИНАЛ РИСУНКА И ЕГО КЕШИ.
.Кеши одного и того же рисунка могут быть (например для сайта votpusk.ru) - в размере 50, 100, 240, 500 пикселов. Это различные варианты страниц предпросмотра и слайд-просмотра рисунков. Понятно, что в общем случае - кеши могут быть не только этих четырех размеров, но хоть десяти размеров. И тут надо правильно выбрать - что именно мы храним в базе, а что в фйловой системе.
Понятно, что кеши можно рассчитывать либо непосредственно при реквесте, либо задачей предварительной подготовки кешей. Мой выбор - непосредственно при реквесте - ибо это дает ацую экономию дисковой памяти (особенно при многих размерах кешей).
Понятно, что невозможно обойтись вообще без кешей - представляете собой например отправку на клиента рисунка размером 500 кб (когда он размере 1000) - а в итоге на страничке браузер покажет 50 пикселов, при этом таких рисунков например 200 на страничке - такая страничка час открыватся будет!
Понятно, что и расчитывать кеши онлайново из оригиналов - тоже нереально - пересчет оригинала рисунка (например в размере 1000) до размера 50 - ацкое время занимает (при загрузке процессора 100%) - а таких рисунков (и перерасчетов) - на страничце может быть несколько сотен.
Выбор можно сделать разный - например совсем крошечные кеши - положить в базу, а оригиналы - в файловую систему. Мой выбор в этом сайте - наоборот. Я ложу крошечные кеши в файловую систему, а оригиналы - в базу. Это позволяет использовать с одной стороны возможность бекапа оригиналов (никогда и никуда - ничего из SQL не потеряется, в отличии от файловой системы) - а во вторых высочайшую скорость работы SQL c данными. С другой стороны - файловые кеши всегда можно потерять или очистить - ни к каким потерям рисунков это не приведет.
Понятно, что выложить бинарники вообще возможно лишь в иную базу от общей базы приложения. Как вы понимаете, иначе сделать просто невозможно - во-первых, у базы с бинарниками должна быть модель журналирования ТОЛЬКО BULK-LOGGED (даже SIMPLE будет безбожно тормозить) - а у простой базы приложения - скорее всего FULL (или в очень нагруженных сайтах - SIMPLE). Кроме того, основная база приложения и база с бинарниками - имеют совершенно разные планы обслуживания.
Самый, однако важный момент при хранении рисунков в базе - это секционирование. Те рисунок должен быть РАЗРЕЗАН на максимально возможное количество частей (в идеале - на столько, сколько автономных дисков имеется) - и каждая часть ОДНОГО И ТОГО ЖЕ РИСУНКА - должна быть выложена на ОТДЕЛЬНЫЙ ДИСК.
Именно на этом небольшой особенности графической подсистемы любого сайта мы и остановимся на этой страничке.
Как вы видите, на этом рисунке - КАЖДЫЙ рисунок тут разрезан на 10 частей. Во-первых, давайте поймем - ЗАЧЕМ его разрезать. Его разрезать затем, чтобы по максимуму нагрузить SQL. Может быть, в карликовых сайтах - это и не имеет значения, но в реальных сайтах - SQL вынесен на отдельную машину - и все, что нам надо - чтобы SQL как можно быстрее ОТДАЛ на WEB-сервер рисунок. Что значит быстрее - это значит ВО МНОГО ПОТОКОВ считывая его из базы и отдавая его на WEB-сервер например со скоростью 3,6 Гигабит в секунду - со скоростью счетверенного мультиплексированного Ethetnet-канала.
А разрезав рисунок на 10 частей - мы ровно в 10 раз ускорим считывание этого рисунка с диска. В отличии от файловой системы, которая НИКАК не умеет считать ОДИН GIF - ОДНОВРЕМЕННО В 10 ПОТОКОВ. И не забывайте, что SQL - это именно такая прога, которая ПРЕДНАЗНАЧЕНА для параллельного считывания десятков и сотен тысяч потоко ОДНОВРЕМЕННО. SQL даже не использует возможности операционной системы, ибо последняя не имеет такой высокой производительности. Есть даже понять SQL-OS. Это собственно тот самый набор АПИ, встроенный в SQL сервер - заточенный на столь высокий уровень параллелизма.
Но, чтобы задействовать такой высокий уровнь параллелизма - не забудьте указать параметр Max Pool Size в коннекшен стринге - иначе никакого ускорения вы не получите, а получите ОЧЕРЕДЬ вместо параллелизма.
Итак, рассмотрим сейчас - КАК ИМЕННО разложить разрезанные рисунки внутри SQL по раздельным дискам. Чтобы считывание ОДНОГО рисунка сразу задействовало 10-20-30-40 дисков, с каждого из которых дернулся бы лишь крошечный фрагмент - и все это с максимальной скоростью бы выстрелило в WEB-сервер.
В первую очередь надо, конечно иметь такой выделенный SQL-сервер с 10 или более автономными дисками. На самом деле - все еще хитрее - каждый их этих дисков на аппаратном уровне для ускорения представляет собой RAID-массив - но мы этого не видим с логического уровня SQL. Кстати, не забудьте выполнить простейшие рекомендации - разнесите журнал транзакций, TEMPDB и индексы на отдельные диски. Чтобы получить ВЫИГРЫШ - надо все делать ПРАВИЛЬНО, иначе и заниматся этим не стоит. Достаточно напортачить с MaxPoolSize или неверное построить файловые группы - как вы получите вместо выигрыша - проигрыш. И над вами будут смеятся даже те, кто по своему скудоумию вообще ложат рисунки в файловую систему (им просто IQ не позволяет поступить иначе).
Итак, для начала делаем 10 файловых групп - каждую на своем отдельном физическом диске. Это можно сделать либо в коде, либо даже в диалоге. Далее для вот этой таблицы с бинарниками (которую вы видите на рисунке выше) - создаем функцию секционирования (в которой определим как диапазоны part будут соответствовать номерам секций нашей таблы):
0001: CREATE PARTITION FUNCTION SECTION_NUMBER (int)
0002: AS
0003: range left for values (1,2,3,4,5,6,7,8,9)
0004: GO
Затем создаем схему секционировния на базе этой функции - которая будет связывать поле с номером секции в базе (part) с номером файловой группы базы (фактически с отдельным диском в нашем случае):
0001: CREATE PARTITION SCHEME Partition_To_FileGroup
0002: AS
0003: PARTITION SECTION_NUMBER To
0004: ([PRIMARY], [Group2],[Group3],[Group4],[Group5],[Group6],[Group7],[Group8],[Group9],[Group10])
0005: GO
Теперь создаем собственно секционированную по этой схеме таблу - раскладывая таблу по FileGroup-ам базы подготовленной выше функцией:
0001: CREATE TABLE [dbo].[Bin](
0002: [i] [int] IDENTITY(1,1) NOT NULL,
0003: [ToUserData] [int] NOT NULL,
0004: [Part] [int] NOT NULL,
0005: [Data] [varbinary](max) NOT NULL
0006: )
0007: ON Partition_To_FileGroup(part)
0008: GO
Далее, докрутим в эту таблу индекс. Обратите внимание, что кластерный индекс сюда можно докрутить ТОЛЬКО если его тоже разложить по секциям таблы - но это противоречит логике этих данных - поэтому я докручиваю в эту таблу именно некластерный индекс, по которому данные в этой табле джойнятся с данными в основной базе - содержащей описание этих данных:
0001: CREATE NONCLUSTERED INDEX [ToDescript2] ON [dbo].[Bin]
0002: (
0003: [ToUserData] ASC
0004: )WITH (STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = ON, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
0005: GO
И наконец, если у вас на девелоперском кампе уже были какие-то данные, то в боевую среду с секционированной таблой вы можете их перенести самым обычным образом (как и вообще работать совершенно обычным образом с этой таблой, лежащей на десятках дисков):
0001: set identity_insert [dbo].[Bin3] ON
0002: INSERT INTO Bin (i, ToUserData, Part, Data)
0003: SELECT i, ToUserData, Part, Data FROM dbo.Bin3
0004: set identity_insert [dbo].[Bin3] OFF
0005: GO
В SQL2008 по идее все эту описанную операцию секционирования по идее можно выполнить мастером - однако там все эти простые действия как-то очень заморочены. Кроме того, этот мастер вообще какой-то дефектный - что он не видит, что табла УЖЕ секционирована.
Теперь убедимся, что данные секционированы. Как вы понимаете функция $PARTITION - никакого отношения к реальному распределению данных не имеет - это лишь расчет по функции SECTION_NUMBER. И если например вы сделаете в этой табле кластерный индекс - то расчет будет правильный, но лежать правильно данные на отдельных дисках не будут.
Такую проверку можно проивести с одной стороны вьюхой sys.partitions
0001: select * from sys.partitions
0002: join sys.objects on sys.objects.object_id=sys.partitions.object_id
0003: GO
Как видите - это десять записей с секционированным индексом и десять записей с таблой - в которой на данный момент юзера загрузили примерно 1500 рисунков (это всего несколько первых дней работы сайта):
И наконец, вторая вьюха, которая нам позволит увидеть реальное распределение данных по дискам - sys.dm_db_index_physical_stats
0001: select * from sys.dm_db_index_physical_stats(DB_ID(N'vOtpusk_Image'), OBJECT_ID(N'BIN'), NULL, NULL , 'DETAILED')
0002: GO
Кстати, еще одна идея, которая мне пришла в голову - не секционировать индекс - а выложить его на отдельный диск - но эксперимент - будет ли это работать быстрее пока не закончен. Это сделать просто, мне было лень заморачиватся с новой сеционирующей функцией, я использовал существующую. Ведь ссылки на номера рисунков заведеом больше 10 - значит вот так ВЕСЬ ИНДЕКС ляжет на один диск.
0001: CREATE NONCLUSTERED INDEX [ToDescript1] ON [dbo].[Bin]
0002: (
0003: [ToUserData] ASC
0004: )WITH (STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = ON, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
0005: ON [Partition_To_FileGroup]([ToUserData])
0006: GO
Разобравшись с целями секционирования и технологией секционирования - вернемся к общей постновке задачи и посмотрим как производится НАРЕЗКА рисунков на фрагменты. Весь этот код я публиковать, конечно не буду - но некоторые ключевые моменты покажу:
В проекте есть контрол, который выполняет загрузку. Во-многих режимах, определяемых как конфигом, так и админом сайта (и еще много чем). Этот контрол - ацкого размера и вызывает кучу моих библиотек - например тут вы видите функцию STRIPSIZE - которая уменьшает рисунок до некоторого, заданного при загрузке пользователем размера. Принцип нарезки должен быть понятен из этого кода - это просто тупая разрезка буфера на 10 частей - НО выполненная в транзакции. Ну и конечно, аккуратная обработка всех возможных исключений.
Вызываемые тут процы - это просто элементарные вставки описания данных в базу приложения и собственно бинарников в базу с бинарниками:
00829: ''' <summary> 00830: ''' дробление бинарников на секции и загрузка всех секций сразу с маштабированием 00831: ''' </summary> 00832: Sub SaveBinaryToSql3() 00833: 'сначала прочитаем весь поток в один буфер 00834: Dim Buf1(FileUpload1.PostedFile.ContentLength) As Byte 00835: FileUpload1.PostedFile.InputStream.Read(Buf1, 0, FileUpload1.PostedFile.ContentLength) 00836: 'теперь отмаштабируем рисунок и заодно убедимся, что эти именно графика 00837: Dim ImageBuf1() As Byte 00838: Dim CurrentSize As Integer 00839: If System.Configuration.ConfigurationManager.AppSettings("UploadWidth") > 0 Then 00840: Try 00841: CurrentSize = Image1.GetWidth(Buf1) 00842: If CurrentSize > System.Configuration.ConfigurationManager.AppSettings("UploadWidth") Then 00843: ImageBuf1 = Image1.StripSize(Buf1, CInt(System.Configuration.ConfigurationManager.AppSettings("UploadWidth"))) 00844: Else 00845: ImageBuf1 = Buf1 00846: End If 00847: Catch ex As Exception 00848: Lerr1.Visible = True 00849: Exit Sub 00850: End Try 00851: Lerr1.Visible = False 00852: Else 00853: Lerr1.Visible = False 00854: ImageBuf1 = Buf1 00855: End If 00856: Dim LenSection As Integer = ImageBuf1.Length \ (System.Configuration.ConfigurationManager.AppSettings("SplitBinary") - 1) 00857: Dim CN1 As New Data.SqlClient.SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings("SQLServer_ConnectionStrings").ConnectionString) 00858: CN1.Open() 00859: Dim LoadAllBinaryParts As Data.SqlClient.SqlTransaction 00860: 'вот тут надо поварьировать с уровнем изоляции транзакций 00861: LoadAllBinaryParts = CN1.BeginTransaction(System.Data.IsolationLevel.RepeatableRead) 00862: 'открыли транзакцию и сбросили заголовок 00863: Dim InsertUserDataHeader As New Data.SqlClient.SqlCommand("InsertUserDataHeader", CN1) 00864: InsertUserDataHeader.CommandType = Data.CommandType.StoredProcedure 00865: InsertUserDataHeader.Transaction = LoadAllBinaryParts 00866: InsertUserDataHeader.Parameters.AddWithValue("UserID", Membership.GetUser(HttpContext.Current.User.Identity.Name).ProviderUserKey) 00867: InsertUserDataHeader.Parameters.AddWithValue("ContentTypeName", "Фото") 00868: InsertUserDataHeader.Parameters.AddWithValue("ToGroup", G) 00869: InsertUserDataHeader.Parameters.AddWithValue("ContentName", NewContentName.Text) 00870: InsertUserDataHeader.Parameters.AddWithValue("FileName", FileUpload1.PostedFile.FileName) 00871: InsertUserDataHeader.Parameters.AddWithValue("DataPostedType", FileUpload1.PostedFile.ContentType) 00872: InsertUserDataHeader.Parameters.AddWithValue("Len", ImageBuf1.Length) 00873: InsertUserDataHeader.Parameters.AddWithValue("IsPorn", CheckBox1.Checked) 00874: InsertUserDataHeader.Parameters.AddWithValue("IsNoComment", CheckBox2.Checked) 00875: InsertUserDataHeader.Parameters.AddWithValue("OriginalWidth", IIf(CurrentSize > System.Configuration.ConfigurationManager.AppSettings("UploadWidth"), CInt(System.Configuration.ConfigurationManager.AppSettings("UploadWidth")), CurrentSize)) 00876: InsertUserDataHeader.Parameters.AddWithValue("Parts", System.Configuration.ConfigurationManager.AppSettings("SplitBinary")) 00877: Dim DR As Data.SqlClient.SqlDataReader = InsertUserDataHeader.ExecuteReader 00878: If DR.Read Then 00879: If Not IsDBNull(DR("RecordNumber")) Then 00880: _RecordNumber = DR("RecordNumber") 00881: Else 00882: DR.Close() 00883: LoadAllBinaryParts.Rollback() 00884: CN1.Close() 00885: RaiseEvent LoadEnd(InsertUserDataHeader, New System.EventArgs) 00886: InsertUserDataHeader = Nothing 00887: Exit Sub 00888: End If 00889: If Not IsDBNull(DR("ErrorMessage")) Then 00890: My.Log.WriteEntry(DR("ErrorMessage")) 00891: _ErrorMessage = DR("ErrorMessage") 00892: DR.Close() 00893: LoadAllBinaryParts.Rollback() 00894: CN1.Close() 00895: RaiseEvent LoadEnd(InsertUserDataHeader, New System.EventArgs) 00896: InsertUserDataHeader = Nothing 00897: Exit Sub 00898: End If 00899: Else 00900: LoadAllBinaryParts.Rollback() 00901: CN1.Close() 00902: RaiseEvent LoadEnd(InsertUserDataHeader, New System.EventArgs) 00903: InsertUserDataHeader = Nothing 00904: Exit Sub 00905: End If 00906: DR.Close() 00907: InsertUserDataHeader = Nothing 00908: 'теперь начнем сбрасывать секции данных 00909: Dim InsertUserDataBinary As New Data.SqlClient.SqlCommand("InsertUserDataBinary", CN1) 00910: InsertUserDataBinary.CommandType = Data.CommandType.StoredProcedure 00911: InsertUserDataBinary.Transaction = LoadAllBinaryParts 00912: InsertUserDataBinary.Parameters.Add("Data", Data.SqlDbType.Binary) 00913: InsertUserDataBinary.Parameters.Add("ToUserData", Data.SqlDbType.Int) 00914: InsertUserDataBinary.Parameters.Add("Parts", Data.SqlDbType.Int) 00915: Dim Buf2(LenSection) As Byte 00916: Dim Pointer1 As Integer = 0 00917: Dim Count1 As Integer = LenSection 00918: 'сохранение каждой секции из отмасштабированного рисунка 00919: For j As Integer = 1 To System.Configuration.ConfigurationManager.AppSettings("SplitBinary") 00920: System.Buffer.BlockCopy(ImageBuf1, Pointer1, Buf2, 0, Count1) 00921: InsertUserDataBinary.Parameters("Data").Value = Buf2 00922: InsertUserDataBinary.Parameters("Data").Size = Count1 00923: InsertUserDataBinary.Parameters("ToUserData").Value = _RecordNumber 00924: InsertUserDataBinary.Parameters("Parts").Value = j 00925: Dim DR1 As Data.SqlClient.SqlDataReader = InsertUserDataBinary.ExecuteReader 00926: If DR1.Read Then 00927: If Not IsDBNull(DR1("SectionNumber")) Then 00928: _CurrentSection = DR1("SectionNumber") 00929: Pointer1 += LenSection 00930: If j = System.Configuration.ConfigurationManager.AppSettings("SplitBinary") - 1 Then 00931: 'пошли на последнюю секцию 00932: Count1 = ImageBuf1.Length - j * LenSection 00933: If Count1 = 0 Then 00934: 'может быть такое чудо в предпоследней секции (на 9 длина поделилась без остатка) 00935: DR1.Close() 00936: Exit For 00937: End If 00938: End If 00939: Else 00940: DR1.Close() 00941: LoadAllBinaryParts.Rollback() 00942: CN1.Close() 00943: RaiseEvent LoadEnd(InsertUserDataHeader, New System.EventArgs) 00944: InsertUserDataHeader = Nothing 00945: Exit Sub 00946: End If 00947: If Not IsDBNull(DR1("ErrorMessage")) Then 00948: My.Log.WriteEntry(DR1("ErrorMessage")) 00949: _ErrorMessage = DR1("ErrorMessage") 00950: DR1.Close() 00951: LoadAllBinaryParts.Rollback() 00952: CN1.Close() 00953: RaiseEvent LoadEnd(InsertUserDataHeader, New System.EventArgs) 00954: InsertUserDataHeader = Nothing 00955: End If 00956: Else 00957: LoadAllBinaryParts.Rollback() 00958: CN1.Close() 00959: RaiseEvent LoadEnd(InsertUserDataHeader, New System.EventArgs) 00960: InsertUserDataHeader = Nothing 00961: End If 00962: DR1.Close() 00963: Next 00964: LoadAllBinaryParts.Commit() 00965: CN1.Close() 00966: RaiseEvent LoadEnd(InsertUserDataHeader, New System.EventArgs) 00967: InsertUserDataHeader = Nothing 00968: End Sub
Итак, я показал вам свой небольшой фрагмент нарезки рисунков на секции из контрола загрузки рисунков. А вот код сборки фрагментов рисунков в единое целое - более хитрый. Он работает во множестве режимов - однозадачном, мультизадачном, MARS - умеет автоматически кешировать рисунки по реквестам в требуемых размерах, налагать логотип, проверять наличие кешей, работать после чистки дисков от кешей, ведет статистику. И много чего еще он умеет. Его функционал продолжает вылизываться и поныне. В него также докручивается все новый и новый функционал. Поэтому код этой графической подсистемы сайта (как бы мне не хотелось научить начинающих тут делать MARS-сборки рисунков и мнопоточные сборки в один буфер) - это уже более серьезный фрагмент кода (в отличие от опубликованных тут мною азбучных истин) - поэтому такой более серьезный код (как собственность компании VOTPUSK.RU) я конечно ж публиковать не могу, ну разве что всего-лишь один-два процента этого хандлера (да и то в самом простом режиме) - просто чтобы показать принцип его работы.
00001: <%@ WebHandler Language="VB" Class="GetImage" %> 00002: 00003: Imports System, System.Web 00004: 00005: ''' <summary> 00006: ''' Этот класс - основа хранения бинарников в базе - он читает фрагменты данных из базы и собирает из отдельных секций рисунок целиком 00007: ''' этот класс масштабирует рисунки, накладывает копирайты и заменяет забаненные и испорченные рисунки стандартным логотипом 00008: ''' а также ведет статистику запросов на рисунки 00009: ''' Этот класс может вычитывать и секционированные и несекционированные рисунки из базы 00010: ''' Если задан параметр W, то этот хандлер также производит масштабирование рисунка 00011: ''' Кроме параметров управляется хандлер из конфига AppSettings("ShowStatistic"), ConnectionStrings("SQLServer_ConnectionStrings"), 00012: ''' Для вычитывания данных использует процедуры SqlCommand("GetUserData"), а для записи статистики SqlCommand("AddImageShowStat") 00013: ''' Для сохранения имена кеша рисунка SqlCommand("SaveImageCacheName"), для предварительного чтения сведений о рисунке - SqlCommand("select * from dbo.UserData") ...... 00022: ''' </summary> 00023: Public Class GetImage : Implements IHttpHandler, IRequiresSessionState 00024: Dim _Section As Integer 00025: Public Property Section() As Integer 00026: Get 00027: Return _Section 00028: End Get 00029: Set(ByVal value As Integer) 00030: _Section = value 00031: End Set 00032: End Property 00033: 00034: Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest 00035: Dim J As Integer 'собственно номер рисунка по базе 00036: Dim W As Integer 'заданная ширина вывода рисунка или 0 (вывод без мастабирования) 00037: Try 00038: If context.Current.Request.QueryString("J") <> Nothing Then 00039: 'выудим расшифрованный номер отображаемого рисунка в UserData 00040: J = VBNET2000.PP8_Helper.Unmask_QueryString("J", "SQLServer_ConnectionStrings") ..... 00104: If My.Computer.FileSystem.FileExists(FullFileName500) Then 00105: If IsModerban then 00106: context.Response.BinaryWrite(image1.Ban(My.Computer.FileSystem.ReadAllBytes(FullFileName500))) 00107: Else 00108: context.Response.BinaryWrite(My.Computer.FileSystem.ReadAllBytes(FullFileName500)) 00109: End If 00110: Else 00111: context.Response.BinaryWrite(ReadFromSQL_and_WriteToBrowserAndCache(CN, Parts, J, Len, ToUser, CInt(System.Configuration.ConfigurationManager.AppSettings("MaxImage500Side")), False, ImageCacheType.W500, System.Configuration.ConfigurationManager.AppSettings("ImageCachePatch"),IsModerban)) 00112: End If ...... 00330: End Sub 00331: 00332: Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable 00333: Get 00334: Return False 00335: End Get 00336: End Property 00337: 00338: Private Sub WriteLogo(ByVal context As HttpContext) 00339: context.Response.BinaryWrite(Image1.StripSize(My.Computer.FileSystem.ReadAllBytes(context.Server.MapPath("Images/logo_n.gif")), 164)) 00340: End Sub 00341: ...... 00512: Private Function GetCacheFullFileName(ByVal UserID As String, ByVal ShortName As String) As String 00513: Dim BaseCachePatch As String = System.Configuration.ConfigurationManager.AppSettings("ImageCachePatch") 00514: Return My.Computer.FileSystem.CombinePath(BaseCachePatch, UserID & "\" & ShortName) 00515: End Function 00516: 00517: Friend Enum ImageCacheType As Byte 00518: None = 0 00519: W50 = 1 00520: W150 = 2 00521: W500 = 3 00522: End Enum 00523: 00524: 00525: ''' <summary> 00526: ''' 'CN создан, но приходит сюда закрытым, закрытым должен быть и по завершению процедуры 00527: ''' 'Parth - количество фрагментов в SQL 00528: ''' 'I - номер записи в UserData 00529: ''' 'Len - суммарная длина рисунка во всех секциях 00530: ''' 'ToUser - GUID юзера в виде строки 00531: ''' 'Width - требуемая ширина рисунка 00532: ''' 'WithLogo - необходимость наложения логотипа 00533: ''' 'ImageCache - необходимость создания кеша рисунка 00534: ''' 'ImageCacheBaseDirectory - базовая директория с кешами рисунков 00535: ''' 'ModerBan в базе обрабатывается извне, как и ошибки - здесь только смысловое чтение 00536: ''' </summary> 00537: Friend Function ReadFromSQL_and_WriteToBrowserAndCache(ByVal CN As System.Data.SqlClient.SqlConnection, _ 00538: ByVal Parts As Integer, ByVal I As Integer, ByVal Len As Integer, ByVal UserID As String, _ 00539: ByVal Width As Integer, ByVal WithLogo As Boolean, _ 00540: ByVal ImageCache As ImageCacheType, ByVal ImageCacheBaseDirectory As String, IsModerBan As Boolean) As Byte() 00541: Dim Buf1 As Byte() 00542: CN.Open() 'здесь иногда валится по таймауту или из-за исчерпания пула подключений (задаются Max Pool Size в коннекшн-стринге) 00543: Dim GetUserData As New System.Data.SqlClient.SqlCommand("GetUserData", CN) 00544: GetUserData.CommandType = Data.CommandType.StoredProcedure 00545: GetUserData.Parameters.AddWithValue("I", I) 00546: GetUserData.Parameters.AddWithValue("Part", 0) 00547: GetUserData.Parameters.AddWithValue("WithBan", True) 00548: Dim RDR1 As Data.SqlClient.SqlDataReader 00549: If Parts = 1 Then 00550: 'несекционированный рисунок в виде одной секции с номером ноль 00551: RDR1 = GetUserData.ExecuteReader() 00552: If RDR1.Read Then 00553: If Not IsDBNull(RDR1("Data")) Then 00554: If Width = 0 Then 00555: 'вывод в натуральную величину - без массштабирования 00556: If WithLogo Then 00557: Buf1 = RDR1("Data") 00558: Else 00559: 'вывод в натуральную величину - без массштабированния, но с копирайтом 00560: 'Buf1 = VBNET2000.Image.AddRightToImage(RDR1("Data")) 00561: 'решили убрать логотип 00562: Buf1 = RDR1("Data") 00563: End If 00564: Else 00565: 'вывод в заказной ширине с масштабированием 00566: If WithLogo Then 00567: Buf1 = Image1.StripSize(RDR1("Data"), Width) 00568: Else 00569: 'вывод в в заказной ширине с масштабированием и копирайтом 00570: 'Buf1 = VBNET2000.Image.AddRightToImage(VBNET2000.Image.StripSize(RDR1("Data"), Width)) 00571: 'решили убрать логотип 00572: Buf1 = Image1.StripSize(RDR1("Data"), Width) 00573: End If 00574: End If 00575: RDR1.Close() 00576: CN.Close() 00577: If ImageCache <> ImageCacheType.None Then 00578: CreateCache(CN, I, Buf1, ImageCache, ImageCacheBaseDirectory, UserID) 00579: End If 00580: If Ismoderban then 00581: 'забанено модератором 00582: Return Image1.Ban(Buf1) 00583: Else 00584: Return Buf1 00585: End If 00586: Else 00587: RDR1.Close() 00588: CN.Close() 00589: Throw New Exception("В UserData у рисунка номер " & I.ToString & " нет данных") 00590: Exit Function 00591: End If 00592: Else 00593: CN.Close() 00594: Throw New Exception("В UserData нет рисунка номер " & I.ToString) 00595: Exit Function 00596: End If 00597: Else 00598: 'секционированный рисунок 00599: Buf1 = New Byte(Len) {} 00600: Dim Pointer As Integer = 0 00601: For k As Integer = 1 To Parts 00602: GetUserData.Parameters("Part").Value = k 00603: RDR1 = GetUserData.ExecuteReader() 00604: If RDR1.Read Then 00605: If Not IsDBNull(RDR1("Data")) Then 00606: Dim Buf0 As Byte() = RDR1("Data") 00607: System.Buffer.BlockCopy(Buf0, 0, Buf1, Pointer, Buf0.Length) 00608: Pointer += Buf0.Length 00609: Else 00610: CN.Close() 00611: Throw New Exception("Неожиданное отсутствие данных у фрагмента " & k.ToString & " в рисунке номер " & I.ToString) 00612: Exit Function 00613: End If 00614: End If 00615: RDR1.Close() 00616: Next 00617: CN.Close() 00618: Dim Buf2 As Byte() 00619: If Width = 0 Then 00620: 'вывод в натуральную величину - без массштабирования 00621: If WithLogo Then 00622: Buf2 = Buf1 00623: Else 00624: 'вывод в натуральную величину - без массштабированния, но с копирайтом 00625: Buf2 = VBNET2000.Image.AddRightToImage(Buf1) 00626: 'если убрать логотип 00627: 'Buf2 = Buf1 00628: End If 00629: Else 00630: 'вывод в заказной ширине с масштабированием 00631: If WithLogo Then 00632: Buf2 = Image1.StripSize(Buf1, Width) 00633: Else 00634: 'вывод в в заказной ширине с масштабированием и копирайтом 00635: Buf2 = VBNET2000.Image.AddRightToImage(VBNET2000.Image.StripSize(Buf1, Width)) 00636: 'если убрать логотип 00637: 'Buf2 = Image1.StripSize(Buf1, Width) 00638: End If 00639: End If 00640: If ImageCache <> ImageCacheType.None Then 00641: CreateCache(CN, I, Buf2, ImageCache, ImageCacheBaseDirectory, UserID) 00642: End If 00643: If Ismoderban then 00644: 'забанено модератором 00645: Return Image1.Ban(Buf2) 00646: Else 00647: Return Buf2 00648: End If 00649: End If 00650: End Function ...... 00887: 00888: ''' <summary> 00889: ''' Ппрцедура, создающая кеш рисунка и отмечающая это в базе 00890: ''' CN передается закрытым и закрытым же должен быть возврашен 00891: ''' Buf1 - байтовый поток с рисунком 00892: ''' ImageCache - ширина рисунка в буфере 00893: ''' ImageCacheBaseDirectory - корневая диекртирия кеша рисунка 00894: ''' ToUser - строка с GIUD-ом юзера 00895: ''' </summary> 00896: Private Sub CreateCache(ByVal CN As System.Data.SqlClient.SqlConnection, ByVal I As Integer, _ 00897: ByVal Buf1() As Byte, ByVal ImageCache As ImageCacheType, ByVal ImageCacheBaseDirectory As String, ByVal UserID As String) 00898: Dim ShortName As String = Guid.NewGuid.ToString & ".gif" 00899: Dim FullPath As String = My.Computer.FileSystem.CombinePath(ImageCacheBaseDirectory, UserID & "\" & ShortName) 00900: Try 00901: If Not My.Computer.FileSystem.DirectoryExists(My.Computer.FileSystem.CombinePath(ImageCacheBaseDirectory, UserID)) Then 00902: My.Computer.FileSystem.CreateDirectory(My.Computer.FileSystem.CombinePath(ImageCacheBaseDirectory, UserID)) 00903: End If 00904: My.Computer.FileSystem.WriteAllBytes(FullPath, Buf1, False) 00905: Catch e As Exception 00906: 'не записался кеш рисунка 00907: Exit Sub 00908: End Try 00909: CN.Open() 00910: Dim SaveImageCacheName As New System.Data.SqlClient.SqlCommand("SaveImageCacheName", CN) 00911: SaveImageCacheName.CommandType = Data.CommandType.StoredProcedure 00912: SaveImageCacheName.Parameters.AddWithValue("I", I) 00913: SaveImageCacheName.Parameters.AddWithValue("Type", ImageCache) 00914: SaveImageCacheName.Parameters.AddWithValue("ImageCacheName", ShortName) 00915: SaveImageCacheName.ExecuteNonQuery() 00916: CN.Close() 00917: End Sub 00918: 00919: End Class
Теперь небольшие каменты к этому коду. Как вы понимаете, хандлеры пишутся в двух режимах - с приведением к интефейсам IHttpHandler, IRequiresSessionState или только к IHttpHandler. Первый способ - гораздо менее производительный, и в этом фрагменте кода, что я показываю - он вообще не нужен. Хотя в ряде случаев есть определенный смысл в Session, в общем случае (для высоконагруженных сайтов) надо стремится обходится без приведения класса хандлера к этому интерфейсу.
Сам хандлер построен так. Он расшифровывает входные параметры. затем анализирует всякие режимы, заданные в конфиге и админке. И находит определяет способ работы с базой. Например при некоторых условиях производит в строке 111 вызов функции ReadFromSQL_and_WriteToBrowserAndCache, начинающейся со строки 537.
Она тоже умеет работать и с секционированными и с несекционированными рисунками. С секционированными она работает начиная со строки 599. Если рисунок секционирован, она в строке 607 начинает собирать секции в единый буфер, передвигая указатель по этому буферу (строка 608) при чтении каждой секции. Как вы видите, в этом модуле все операции чтения происходят строго последовательно. Выигрыш относительно одной длинной операции чтения несекционированного рисунка все равно будет - и немалый. Ведь SQL передает несколько коротких очередей вместо одной длинной - следовательно быстро-быстро освобождается для обслуживания запросов других юзеров. Как я говорил выше - главное в этом режиме - чтобы у него был достаточный размер пула памяти - значительно больше, чем в несекционированном режиме. Если же у SQL нету памяти или вообще он не располагает достаточными ресурсами - вообще вся эта тема с секционированием бессмысленна.
При необходимости прога создает кеш рисунка (в строке 896). А если он есть - то в браузер выводится кеш, и до чтения из базы не доходит вообще (строка 108). В этом хандлере обрабатывается также модераторский бан.
Я начинал эту страничку с пространных рассуждений об архитектуре графической подсистемы сайта. И сейчас вы видите фрагменты ее реализации (правда в весьма упрощенном виде) - но и в остальных режимах я сохранил свой выбор графической архитектуры для этого проекта - оригиналы лежат в базе, а разноразмерные кеши - в файловой системе. Ничто, впрочем не мешает и кеши хранить в базе - только я бы для этого предпочел еще один SQL-сервер, чтобы не грузить SQL-сервер с данными сайта, SQL-сервер с оригиналами рисунков и собственно процессор WEb-сервера.
Естественно, все это базируется на быстрой связи между всеми серверами сайта, например я бы порекомендовал фирменные протоколы производителей материнок, расширяющие TCP/IP - когда несколько гигабитных каналов работают с одним айпишником, но с мулитиплексированной производительностью (вот например такое решение 4 х 1ГБ/с = 3,6 ГБ/с суммарно, правда тут все вообще старенькое и вообще даже однопроцессорное).
Думаю, нет вообще никакого смысла городить все это секционирование на каком-то убитом железе с сетевыми картами 100 Мб/сек и убитыми процессорами. Проще купить нормальное железо. И все описанное тут принципиально невозможно осуществить, если вы не имеете хотя бы десяток совершенно независимых RAID-массивов (ну или хотя бы десяток отдельных дисков). Я сделал все описанное тут, чтобы на ТОПОВОМ многопроцессорном железе подпрыгнуть ЕЩЕ ВЫШЕ по производительности, чем вообще способно обеспечить это топовое железо в простейших вариантах его использования.
|