Наблюдение за Web-приложениями
Нет необходимости говорить о важности наблюдения за событиями Web-приложений. Они предоставлены сами себе годами и открыты безбрежному интернету. Что случилось с ним, почему вдруг все стало тормозить? Инструменты для ответа на эти вопросы надо готовить еще на этапе девелопмента.
Я оснащал практически все свои приложения инструментами для мониторинга. Например электронный магазин, систему моделирования событий в огранизационных структурах, сайт знакомств, сайт учебного заведения, интернет-банк, музыкальный сайт и многие другие свои Web-приложения. И на этой страничке я попробую систематизировать некоторые свои мозгозаключения по этому вопросу.
Прежде всего, существует ПЯТЬ технологий наблюдения за Web-приложениями. Вы их видите на рисунке:
Теперь рассмотрим подробнее каждую из указанных технологий (хотя в принципе существуют и другие технологии, но в силу их нестабильности на практике и сложной интерпретации полученных результатов - я не буду их тут рассматривать). Заметьте, что у нас не идет тут речь о поведении ОТДЕЛЬНОЙ странички, которое можно наблюдать по NET->All или по Trace - мы наблюдаем за работой всего нашего Web-приложения в целом.
Итак, начнем с записи собственных событий в My.Log
Все Web-приложения имееют сложную логику перехода внутренних состояний. Например тут показан небольшой фрагмент логики состояний сайта знакомств, а тут рассказано о значительно большей машине состояний.
Трассировка, которую мы пишем в My.Log обрабатывается по умолчанию классом Microsoft.VisualBasic.Logging.AspLog и именно его вывод мы наблюдаем в окне OUTPUT в студии.
MS поставляет несколько стандартных классов, которые слушают поток данных, который формирует приложение при записи в My.Log. Эти провайдеры перечислены ниже и назначение их должно быть понятно из сделанных мною комментариев. Кроме, конечно, первого провайдера. Это нестандартный провайдер и мы его сделаем сейчас сами. Вы можете его сгрузить также готовый ОТСЮДА.
00001: <system.diagnostics> 00002: <sources> 00003: <source name="DefaultSource" switchName="DefaultSwitch"> 00004: <listeners> 00005: <add name="SQLLog" /> 00006: <add name="EventLog" /> 00007: <add name="Delimited" /> 00008: <add name="XmlWriter" /> 00009: <add name="FileLog"/> 00010: <add name="Console" /> 00011: </listeners> 00012: </source> 00013: </sources> 00014: <switches> 00015: <add name="DefaultSwitch" value="Information" /> 00016: </switches> 00017: <sharedListeners> 00018: <!-- собственный SQL-журнал <type name>, <assembly name>, <version number>, <culture>, <strong name>--> 00019: <add name="SQLLog" type="MyLogToSQLFromWeb.MyLogListener, MyLogToSQLFromWeb, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e989b5738bc16ea9" /> 00020: <!--в окно OUTPUT--> 00021: <add name="AspLog" type="Microsoft.VisualBasic.Logging.AspLog, Microsoft.VisualBasic, Version=2.0.0.1, Culture=neutral, PublicKeyToken=6315780d5082271f" /> 00022: <!--В системный журнал--> 00023: <add name="EventLog" type="System.Diagnostics.EventLogTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" initializeData="Votpusk_EventLogWriter"/> 00024: <!--в указанный файл в текстовом формате --> 00025: <add name="Delimited" type="System.Diagnostics.DelimitedListTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" initializeData="c:\temp\Votpusk_DelimitedWriter.txt" delimiter=";" traceOutputOptions="DateTime" /> 00026: <!--в указанный файл в формате XML--> 00027: <add name="XmlWriter" type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" initializeData="c:\temp\Votpusk_XmlWriter.xml" /> 00028: <!--в C:\Documents and Settings\Administrator\Application Data\Microsoft Corporation\Microsoft (R) Visual Studio (R) 2005\8.0.50727.762 (кстати в этом же каталоге находится лог девелопментского сервера) --> 00029: <add name="FileLog" type="Microsoft.VisualBasic.Logging.FileLogTraceListener, Microsoft.VisualBasic, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" initializeData="FileLogWriter" BaseFileName="1.txt" /> 00030: <!-- ? --> 00031: <add name="Console" type="System.Diagnostics.ConsoleTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" initializeData="true" /> 00032: </sharedListeners> 00033: </system.diagnostics>
Можно подумать - зачем он нужен? Ведь уже есть целая куча провайдеров. В том-то и дело, что нужен. Неужели мы снизойдем в боевом приложении до того, что будем писать в NTFS? А события сайта, лежащего на SHARED-хостинге нас не интересуют? А как выводить все это на форму из длинного-предлинного текстового файла? Получается, MS не предусмотрела ГЛАВНОГО провайдера - для записи сообщений о переходе состояний в приложении в SQL. Вот такие-то дела... Но ничего - это мы сейчас легко исправим.
Собственный провайдер записи в SQL не получается из соображений безопасности делать прямо в самом проекте - нужна сборка со строгим именем. Поэтому мы делаем отдельный проект класса, подпишем в нем сборку и поставим в нашем сайте ссылку на эту сборку со строгим именем. А сам по себе смысловой класс этой сборки получается безобразно простой, зато исключительно полезный:
00001: Imports Microsoft.VisualBasic 00002: 00003: Public Class MyLogListener 00004: Inherits System.Diagnostics.TraceListener 00005: 00006: Dim CN As New System.Data.SqlClient.SqlConnection 00007: Dim CMD As New System.Data.SqlClient.SqlCommand 00008: ''' <summary> 00009: ''' В этот метод приходит префикс каждого сообщения, типа - DefaultSource Information: 0 : 00010: ''' </summary> 00011: <System.Security.Permissions.HostProtection(Synchronization:=True)> _ 00012: Public Overloads Overrides Sub Write(ByVal message As String) 00013: 'игнорируем его 00014: End Sub 00015: 00016: ''' <summary> 00017: ''' после этого сюда приходит уже само смысловое сообщение 00018: ''' </summary> 00019: <System.Security.Permissions.HostProtection(Synchronization:=True)> _ 00020: Public Overloads Overrides Sub WriteLine(ByVal message As String) 00021: CMD.Parameters("txt").Value = message 00022: CMD.ExecuteNonQuery() 00023: End Sub 00024: 00025: Public Sub New() 00026: MyBase.New() 00027: CN.ConnectionString = My.Settings.SQLLogConnectionString 00028: CN.Open() 00029: CMD.CommandType = CommandType.Text 00030: CMD.CommandText = My.Settings.CMD 00031: CMD.Connection = CN 00032: CMD.Parameters.Add("txt", SqlDbType.NVarChar) 00033: End Sub 00034: End Class
Как вы видите, у этой сборки существует также Application.setting, который на форме выглядит вот так, а в сыром виде вот так:
00001: <?xml version="1.0" encoding="utf-8" ?> 00002: <configuration> 00003: <configSections> 00004: <sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" > 00005: <section name="MyLogToSQLFromWeb.My.MySettings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> 00006: </sectionGroup> 00007: </configSections> 00008: <connectionStrings> 00009: <add name="MyLogToSQLFromWeb.My.MySettings.SQLLogConnectionString" 00010: connectionString="server=localhost;Initial Catalog=vOtpusk;User ID=VotpuskLogin;Password=Otpusk" /> 00011: </connectionStrings> 00012: <applicationSettings> 00013: <MyLogToSQLFromWeb.My.MySettings> 00014: <setting name="CMD" serializeAs="String"> 00015: <value>INSERT [MyLog]([datetime],[txt]) VALUES (GetDate(), @txt)</value> 00016: </setting> 00017: </MyLogToSQLFromWeb.My.MySettings> 00018: </applicationSettings> 00019: </configuration>
Для этой сборки требуется вот такая табла:
00001: CREATE TABLE [dbo].[MyLog]( 00002: [i] [int] IDENTITY(1,1) NOT NULL, 00003: [datetime] [datetime] NOT NULL, 00004: [txt] [nvarchar](max) NULL 00005: ) ON [PRIMARY]
То, что получится в итоге работы этой сборки в SQL, вы видите на этом скрине, ну а готовый класс трассировки вы можете сгрузить ОТСЮДА, только не забудьте настроить в ней коннект к своей базе
Ну теперь нам ничего не стоит вывести события нашего приложения и на форме. Для этого сделаем прямо в дизайн тайме вот такую простенькую форму без программного кода:
00001: <%@ Page Language="VB" MasterPageFile="~/M1.master" AutoEventWireup="false" CodeFile="MyLog.aspx.vb" Inherits="MyLog" title="Untitled Page" %> 00002: <asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server"> 00003: <asp:SqlDataSource ID="GetFromMyLog" runat="server" ConnectionString="<%$ ConnectionStrings:Votpusk %>" 00004: SelectCommand="select * from dbo.MyLog order by i desc"></asp:SqlDataSource> 00005: <asp:GridView ID="GridView1" runat="server" AllowPaging="True" AutoGenerateColumns="False" 00006: DataSourceID="GetFromMyLog" PageSize="20" Width="100%"> 00007: <Columns> 00008: <asp:BoundField DataField="i" HeaderText="i" InsertVisible="False" ReadOnly="True" 00009: SortExpression="i" /> 00010: <asp:BoundField DataField="datetime" HeaderText="datetime" SortExpression="datetime" /> 00011: <asp:BoundField DataField="txt" HeaderText="txt" SortExpression="txt" /> 00012: </Columns> 00013: <RowStyle Font-Size="Small" /> 00014: </asp:GridView> 00015: </asp:Content>
Как вы понимаете, все перечисленные провайдеры в реальной работе не нужны. Для девелопмента достаточно провайдера AspLog, ну а когда сайт ляжет на хостинг - мы будем смотреть события в нем с помощью описанного тут провайдера - SQLLog.
Теперь рассмотрим мониторинг WMI-событий сайта. Это тоже важные события и там отражаются весьма важные аспекты работы приложения, начиная от рестарта AppDomain, кончая всевозможными сбоями.
Для мониторинга WMI-событий MS к счастью предусмотрела все. Надо знать, что во-первых существует множество провайдеров, которые вписывают данные о событиях в различные накопители данных:
SimpleMailWebEventProvider | This provider sends e-mail for event notifications. |
TemplatedMailWebEventProvider | This provider uses templates to define and format e-mail messages sent for event notifications. |
SqlWebEventProvider | This provider logs event details to a SQL Server database. If you use this provider, you should encrypt the connection string in your Web.config file by using the Aspnet_regiis.exe tool. |
EventLogWebEventProvider | This provider logs events to the Windows application event log. |
TraceWebEventProvider | This provider logs events as ASP.NET trace messages. |
WmiWebEventProvider | This provider maps ASP.NET health monitoring events to Windows Management Instrumentation (WMI) events. |
Классы этих провайдеров расположены в пространстве имен System.Web.Management и, при желании, вы можете их посмотреть подробнее. Но мы подойдем к этому вопросу с практической стороны и воспользуемся единственным на мой взгляд разумным провайдером - SqlWebEventProvide.
Во-вторых надо понимать, что в принципе существуют различные классы событий, которые можно учитывать.
Но мы подойдем к этому вопросу с практической стороны и учтем ВСЕ события приложения (хотя по умолчанию включен лишь мониторинг событий All Errors и Failure Audit, которые и записываются в журнал системы). Итого, воспользовавшись готовыми наработками MS, сформируем примерно вот такую секцию в конфиге:
00001: <healthMonitoring enabled="true"> 00002: <providers> 00003: <clear/> 00004: <add connectionStringName="Votpusk" maxEventDetailsLength="1073741823" buffer="true" bufferMode="Notification" name="MySqlWebEventProvider" 00005: type="System.Web.Management.SqlWebEventProvider,System.Web,Version=2.0.0.0,Culture=neutral,PublicKeyToken=b03f5f7f11d50a3a"/> 00006: </providers> 00007: <rules> 00008: <clear/> 00009: <add name="Application Lifetime Events Rule" eventName="All Events" provider="MySqlWebEventProvider" profile="Critical"/> 00010: </rules> 00011: </healthMonitoring>
Этот конфиг переопределяет конфиг, заданный по умолчанию в Web-конфиге кампутера.
В предыдущем случае нам пришлось создавать базу самим, но сейчас мы пользуемся готовыми наработками MS и просто вызовем класс, создающий таблицу aspnet_WebEvent_Events. Сделать это можно двумя способами:
- Выполнить в командной строке aspnet_regsql.exe -E -S
-A w - Выполнить в отладчике код создания таблицы aspnet_WebEvent_Events
Теперь, после запуска приложения, нам остается только наслаждаться в профайлере работой healthMonitoring, ну или смотреть результаты работы уже в базе.
Но чтобы реально воспользоваться результатами этой работы - сделано слишком мало. Нужна ФОРМА, на которой мы увидим результат healthMonitoring. Я сделаю эту форму для разнообразия на DetailsView. Она будет состоять из вот такой разметки:
00001: <%@ Page Language="VB" MasterPageFile="~/M1.master" AutoEventWireup="false" CodeFile="Monitor.aspx.vb" Inherits="Monitor" %> 00002: <asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server"> 00003: <asp:DetailsView ID="DetailsView1" runat="server" AllowPaging="True" AutoGenerateRows="False" 00004: DataKeyNames="EventId" DataMember="DefaultView" DataSourceID="healthMonitoring_WebEvents" 00005: Height="50px" Width="100%" HorizontalAlign="Left"> 00006: <Fields> 00007: <asp:BoundField DataField="Details" SortExpression="Details" HtmlEncode="False" ShowHeader="False" /> 00008: </Fields> 00009: <RowStyle HorizontalAlign="Left" VerticalAlign="Top" Font-Size="Small" /> 00010: </asp:DetailsView> 00011: <asp:SqlDataSource ID="healthMonitoring_WebEvents" runat="server" ConnectionString="<%$ ConnectionStrings:Votpusk %>" 00012: SelectCommand="select EventId, Details from aspnet_WebEvent_Events order by EventTime DESC"> 00013: </asp:SqlDataSource> 00014: </asp:Content>И вот такого кода:
00001: Partial Class Monitor 00002: Inherits System.Web.UI.Page 00003: 00004: Protected Sub DetailsView1_ItemCreated(ByVal sender As Object, ByVal e As System.EventArgs) Handles DetailsView1.ItemCreated 00005: If CType(sender, System.Web.UI.WebControls.DetailsView).DataItem IsNot Nothing Then 00006: CType(CType(sender, System.Web.UI.WebControls.DetailsView).DataItem, System.Data.DataRowView)(1) = _ 00007: CType(CType(sender, System.Web.UI.WebControls.DetailsView).DataItem, System.Data.DataRowView)(1).ToString.Replace(Convert.ToChar(10), "<br>") 00008: End If 00009: End Sub 00010: 00011: End Class
Как видите, тут вся хитрость в том, что нам надо заменить знак перевода строки ctrl-c на <br> и вывести все это на форму без HtmlEncode. Эту коррекцию, как вы видите, я сделал прямо на ходу по живым данным. Естественно, как и в предыдущем случае, наблюдение за приложением можно в любой момент отключить - закоментив пару строк в конфиге.
Теперь пойдем дальше и посмотрим как можно наблюдать за приложением с помощью счетчиков производительности. В принципе существует три пути использования счетчиков производительности:
- Непосредственно наблюдение из оснастки perfmon.msc. Естественно эта возможность доступна только на собственном хостинге. На Shared-хостинге это невозможно.
- Создание в своей проге объектов счетчиков и периодический сброс их значений в базу для последующего анализа. Это возможно делать и на Shared-хостинге, однако сам процесс записи вызывает огромное влияние на все показатели работы приложения и значения счетчиков становятся очень приблизительные.
- Создание в своей проге обьектов счетчиков и вывод их в отладочную панель на Master-форме. Это, естественно, тоже возможно делать даже на Shared-хостинге и эта техника так не искажает значения счетчиков, однако делает недоступным временной тренд значений.
Теперь посмотрим как с помощью VS2005 правильно создать в своей проге объекты счетчиков. Прежде всего надо асилить, что счетчики бывают с двухуровневой или трехуровневой системой именования. Соотвественно, обьект для обращения к ним будет в первом случае выглядеть так:
а во втором вот так:
Таким образом обновляемое значение такого счетчика, выведенное на отладочной панели мастер-формы, может выглядеть например вот так. Что касается конкретно наборов счетчиков, полезных для оптимизации WEB-приложений, то я не буду сейчас останавливаться на этом, а посвящу этому отдельную заметку.
Теперь рассмотрим следующий аспект наблюдения за Web-приложениями. Основное воздействие, которым они подвергаются, происходит из Интернета. Поэтому важно четко фиксировать, во-первых, что именно делают с приложением, а во-вторых, кто именно это делает. Лучше всего для этого подходит класс, размещенный прямо в ASP2-конвеере и унаследованный от IHttpModule. Это весьма полезный интерфейс и на моем сайте уже описано несколько его применений. Другое полезное применение этого класса - URL rewriting - сокрытие реальных имен страничек и структуры сайта от посторонних. По этому же интерфейсу подключаются все основные авторизационные модули ASP2, определенные в Web-конфиге машины.
К конвееру ASP2 наш класс мы подключим следующим образом:
<httpModules> <add name="MapURL" type="MapURL"/> </httpModules>а текст класса разместим непосредственно в папке App_Code.
Как видите, здесь нет необходимости в сборке со строго подписанным именем. Сам по себе текст класса может выглядеть например вот так:
00001: ''' <summary> 00002: ''' класс реврайтинга URL 00003: ''' </summary> 00004: Public Class MapURL 00005: Implements IHttpModule 00006: Dim Dump As New RequestDump 00007: 00008: Public Sub Init(ByVal context As System.Web.HttpApplication) Implements System.Web.IHttpModule.Init 00009: AddHandler context.BeginRequest, AddressOf Me.BeginRequest 00010: End Sub 00011: 00012: Public Sub BeginRequest(ByVal sender As Object, ByVal e As EventArgs) 00013: Dim App As HttpApplication = CType(sender, HttpApplication) 00014: If App.Request.RawUrl.Contains(".aspx") And App.Request.HttpMethod = "POST" Then 00015: Dump.DumpRequestKey(App.Request) 00016: Dump.DumpPostData(App.Request) 00017: Dump.SetDumpLenth(32765) 'максимальная длина, обрабатываемая таким провайдером. Больше только в базу 00018: My.Log.WriteEntry(Dump.ToString) 00019: Dump.Clear() 00020: ElseIf App.Request.RawUrl.Contains("/Login/") And App.Request.HttpMethod = "POST" Then 00021: 'это собственно редирект (переделанный из POST в GET) на сокрытую страничку 00022: App.Response.Redirect("~/X.aspx?Login=" & App.Request.Form("Login") & "&Pass=" & App.Request.Form("Pass") & "&Key=" & App.Request.Form("Key")) 00023: End If 00024: End Sub 00025: 00026: Public Sub Dispose() Implements IHttpModule.Dispose 00027: End Sub 00028: 00029: End Class
Как видите, тут результат пишется прямо в провайдер AspLog, но нам ничего не мешает писать в наш провайдер SQLLog, описанный выше - тогда результирующая длина дампа уже не будет ограничена. Полный результат формируемого дампа можно посмотреть здесь (для реквестов с моего локального кампа).
Теперь я покажу вам свой класс, который формирует такой замечательный и исчерпывающий дамп:
00001: Imports Microsoft.VisualBasic 00002: 00003: ''' <summary> 00004: ''' Класс формирования шестнадцатиричного дампа 00005: ''' </summary> 00006: Public Class RequestDump 00007: 00008: Dim Buf1(100000) As Byte 'значение буфера по умолчанию 00009: Public DumpStringBuilder As StringBuilder 00010: Dim ANSI As New System.Text.ASCIIEncoding 00011: ''' <summary> 00012: ''' Без заголовка и с размером буфера по умолчнию 00013: ''' </summary> 00014: 00015: Public Sub New() 00016: DumpStringBuilder = New StringBuilder 00017: End Sub 00018: 00019: ''' <summary> 00020: ''' Задается заголовок распечатки 00021: ''' </summary> 00022: Public Sub New(ByVal DumpTitle As String) 00023: DumpStringBuilder = New StringBuilder(DumpTitle) 00024: End Sub 00025: 00026: ''' <summary> 00027: ''' Задается размер буфера 00028: ''' </summary> 00029: Public Sub New(ByVal BuferSize As Integer) 00030: ReDim Buf1(BuferSize) 00031: DumpStringBuilder = New StringBuilder 00032: End Sub 00033: 00034: ''' <summary> 00035: ''' Задается размер буфера и заголовок дампа 00036: ''' </summary> 00037: Public Sub New(ByVal DumpTitle As String, ByVal BuferSize As Integer) 00038: ReDim Buf1(BuferSize) 00039: DumpStringBuilder = New StringBuilder(DumpTitle) 00040: End Sub 00041: 00042: ''' <summary> 00043: ''' Задается размер буфера и заголовок дампа 00044: ''' </summary> 00045: Public Sub New(ByVal BuferSize As Integer, ByVal DumpTitle As String) 00046: ReDim Buf1(BuferSize) 00047: DumpStringBuilder = New StringBuilder(DumpTitle) 00048: End Sub 00049: ''' <summary> 00050: ''' Отсюда забирать результат 00051: ''' </summary> 00052: Public Overrides Function ToString() As String 00053: Return DumpStringBuilder.ToString 00054: End Function 00055: 00056: ''' <summary> 00057: ''' Очистка класса для повторного использования 00058: ''' </summary> 00059: Public Sub Clear() 00060: DumpStringBuilder.Length = 0 00061: Array.Clear(Buf1, 0, Buf1.Length - 1) 00062: End Sub 00063: 00064: ''' <summary> 00065: ''' Ограничить результирующую длину распечатки 00066: ''' </summary> 00067: Public Sub SetDumpLenth(ByVal MaxOutLen As Integer) 00068: DumpStringBuilder.Length = MaxOutLen 00069: End Sub 00070: 00071: ''' <summary> 00072: ''' Зафиксировать в дамп все параметры переданного реквеста кроме данных POST 00073: ''' </summary> 00074: Public Sub DumpRequestKey(ByVal Request As System.Web.HttpRequest) 00075: DumpStringBuilder.AppendLine() 00076: DumpStringBuilder.AppendLine("****** Request: (" & Now.ToString & ") " & Request.RawUrl) 00077: DumpStringBuilder.AppendLine() 00078: For Each One As String In Request.Params.AllKeys 00079: DumpStringBuilder.AppendLine(One & " : " & Request.Params(One)) 00080: Next 00081: DumpStringBuilder.AppendLine() 00082: End Sub 00083: 00084: ''' <summary> 00085: ''' Зафиксировать в дамп данные POST, передаваемые в реквесте 00086: ''' </summary> 00087: Public Sub DumpPostData(ByVal Request As System.Web.HttpRequest) 00088: DumpStringBuilder.AppendLine() 00089: DumpStringBuilder.AppendLine("****** Postdata:") 00090: DumpStringBuilder.AppendLine() 00091: If Request.TotalBytes < Buf1.Length Then 00092: Buf1 = Request.BinaryRead(Request.TotalBytes) 00093: DumHexBytes(Buf1) 00094: Else 00095: DumpStringBuilder.AppendLine("Ошибка. Превышен размер буфера.") 00096: End If 00097: DumpStringBuilder.AppendLine() 00098: End Sub 00099: 00100: ''' <summary> 00101: ''' Форматированная распечатка заданного извне байтового массива в виде стандартного шестнадцатиричного дампа 00102: ''' </summary> 00103: Public Sub DumHexBytes(ByVal Buffer() As Byte) 00104: Dim I As Integer 00105: DumpStringBuilder.AppendFormat("{0:X5}(x): {1:D5}(d): ", I, I) 00106: For I = 0 To Buffer.Length - 1 00107: DumpStringBuilder.Append(Buffer(I).ToString("X") & " ") 00108: If ((I + 1) Mod 32 = 0) And I > 0 Then 00109: DumpStringBuilder.Append(" " & ANSI.GetString(Buffer, I - 31, 32)) 00110: DumpStringBuilder.AppendLine() 00111: DumpStringBuilder.AppendFormat("{0:X5}(x): {1:D5}(d): ", I, I) 00112: End If 00113: Next 00114: 'выравнивание символьной части дампа 00115: DumpStringBuilder.Append(New String(" ", (32 * 3 + " ".Length) - (I Mod 32) * 3)) 00116: DumpStringBuilder.Append(ANSI.GetString(Buffer, I - (I Mod 32), I Mod 32)) 00117: End Sub 00118: 00119: End Class
Осталось только вывести все это на форму. Это позволит на сделать вот такой код:
00001: <%@ Page Language="VB" MasterPageFile="~/M1.master" AutoEventWireup="false" CodeFile="MyLog.aspx.vb" Inherits="MyLog"%> 00002: <asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server"> 00003: <asp:SqlDataSource ID="GetFromMyLog" runat="server" ConnectionString="<%$ ConnectionStrings:Votpusk %>" 00004: SelectCommand="select i, replace(txt,nchar(10),'<br>') as txt from dbo.MyLog order by i desc"></asp:SqlDataSource> 00005: <asp:GridView ID="GridView1" runat="server" AllowPaging="True" AutoGenerateColumns="False" 00006: DataSourceID="GetFromMyLog" PageSize="20" Width="100%" DataKeyNames="i"> 00007: <Columns> 00008: <asp:BoundField DataField="txt" HeaderText="txt" SortExpression="txt" HtmlEncode="False" /> 00009: </Columns> 00010: <RowStyle Font-Names="Courier" /> 00011: </asp:GridView> 00012: </asp:Content>
Программный код тут не требуется. Потому что базу мы проектировали сами и сделали ее правильно (в отличие от MS) - типа Nvarchar, а не Ntext. А значит REPLACE можно выполнить прямо в команде SQL (тем более в GridView это не получится так сделать как в DetailsView). Важно чтобы шрифт тут был Courier - тк дамп строго форматирован по колонкам.
Теперь рассмотрим еще одну технологию мониторинга сайта. Понятно, что в виндузне сокрыто множество уязвимостей - доказательство тому - бесконечные еженедельные обновления, которые за много-много лет так ни к чему хорошему и не привели (кроме, конечно того, что Билл Гейтс стал самым богатым человеком планеты). Следовательно вероятность обычной вирусной атаки на сайт существует. Особенно если админ сайта не особо аккуратен и существует возможность атаки на библиотеки сайта не только из интернета, но и из локальной сети. И особенно это актуально для сайтов, которые работают с деньгами. В таком случае нам будет гораздо спокойнее, если мы уверены в НЕИЗМЕННОСТИ сайта. В отсутствии какого-нибудь вируса, прицепившегося к библиотекам сайта. В стабильности и неизменности версий сторонних библиотек, используемых сайтом. На самом деле, контрольная сумма сайта - это конечно не панацея - хороший вирус сумеет скрыть свое присутсвие, но все же примем, что 90% продекларированных проблем этим решением закрываются.
Разумеется эта технология отчасти ограничена и не подходит для сайтов, которые используют файловую систему для хранения временных файлов, но сайты постоенные по такой схеме вообще обычно напоминают проходной двор и заниматься их защитой или мониторингом вообще не имеет смысла.
Технология контроля контрольной суммы сайта заключается в подсчете контрольной суммы при загрузке сайта, вписывания ее в базу и последующей периодической перепроверке этой контрольной суммы. Странно, что такой элементарной вещи нет в IIS - зафиксировать ее при аплоаде сайта и потом например подстветить этот узел в IIS красным, если вдруг контрольная сумма сайта поменялась.
Для целей контрля неизменности сайта создается поток, который периодически пересчитывает контрольную сумму и сличает ее с базой. В случае подмены библиотеки - прекращает работу сайта. Чтобы не дублироваться, я тут повторяться не буду, ибо подобная программа уже лежит на моем сайте в виде утилиты с открытым исходным кодом.
Теперь рассмотрим технологию моделирования нагрузки на сайт, чтобы опробовать и производительность сайта и испытать все описанные выше средства мониторинга. Например, вот на этом рисунке видно как я добавлял новых пользователей в Web-проект моделирования событий в организационных структурах и, постепенно увеличивая нагрузку до 50 реквестов в секунду, наблюдал сколько одновременно добавляемых юзеров может выдержать мой сайт. Как вы видите, при такой нагрузке (из 11 тысяч 461 реквеста) - 1159 реквестов завершились неудачей (без формирования респонза сервером), а 4994 реквеста остались на момент завершения теста в очереди необслуженными.
К этому тесту на добавление юзеров я тоже пришел не сразу - протестировав все странички проекта, я выяснил, что именно создание нового пользователя является самой ресурсоемкой операцией моего приложения. В дельнейшем именно детальное изучение счетчиков позволило выявить узкие места и вынести некоторые фрагменты кода в SQL.
Впрочем в контексте данной заметки мы не будем останавливаться ни на применении указанных средств тестирования для выявления узких мест в приложении (это нехилая тема и ее опишу как нибудь позднее), ни на применении этих же средств для взлома сайтов, путем генерирования массовых реквестов на страничку логина (используя базу логинов и паролей). В контексте данной заметки эти 50 POST-реквестов в секунду нам были нужны - чтобы протестировать все описанные мною в этом топике средства мониторинга сайтов.
|