(ASP.NET) ASP.NET (2008 год)

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-массивов (ну или хотя бы десяток отдельных дисков). Я сделал все описанное тут, чтобы на ТОПОВОМ многопроцессорном железе подпрыгнуть ЕЩЕ ВЫШЕ по производительности, чем вообще способно обеспечить это топовое железо в простейших вариантах его использования.



Комментарии к этой страничке ( )
ссылка на эту страничку: http://www.vb-net.ru/asp2/34/index.htm
<На главную>  <В раздел ASP>  <В раздел NET>  <В раздел SQL>  <В раздел Разное>  <Написать автору>  < Поблагодарить>
Московская хельсинская группа   Радио Свобода  Новая газета   The New Times (Новое время)