2009-03-21

Мониторинг баз данных Firebird

Firebird 2.0 на полную катушку

Мониторинг баз данных Firebird.

0.Преамбула

  1. Автор не претендует на роль истины в последней инстанции. Задачи и методы их решения, изглагаемые в статье, являются следствием опыта, полученного автором при написании программ, не являются панацеей и если кто-нибудь может предложить более оптимальное решение и поделиться им с нами, автор будет только рад :)
  2. Автор является сторонником движения OpenSource и исповедует один из основных лозунгов OpenSource "Знания человека принадлежат миру", и поэтому Вы вправе свободно копировать, цитировать и использовать данный текст по своему усмотрению. Никаких ограничений на данный текст, изложенные в нем материалы, алгоритмы и методики, не накладывается.

1.Введение

  По администрированию баз даннных InterBase/Firebird написано достаточно много публикаций в основном касающихся Service API (API-функций по управлению базами данных и SQL-сервером), мы не будем останавливаться на них. Целью данной статьи является рассмотрение новых функциональных особенностей Firebird, которые появились в этом SQL-сервере в версии 2.1. Это так называемые "Виртуальные таблицы" или системные таблицы с префиксом MON$ в имени. "Виртуальными", согласно документации, их назвали потому, что они не существуют в базе данных, просто API-функции доступа к базе данных эмулируют присутствие этих таблиц, а служебная информация о текущих процессах в базе данных, располагается в соотвествующих столбцах этих таблиц. На мой взгляд такой подход со стороны разработчиков к реализации мониторинга баз данных является самым удобным, так как не требует разработки дополнительного API-интерфейса (и, соответственно, переработки клиентских VCL-библиотек доступа), а доступ к функциональности можно осуществлять при помощи старых VCL-библиотек (IBX, IBO, FIBPlus), для этого надо лишь поменять gds32.dll(fbclient.dll) на соответствующую текущей версии сервера.

  Многие разработчики сталкивались с отладкой SQL-скриптов, выполняющих какие-либо продолжительные действия или строящих сложные отчеты. И не всегда, к сожалению, написать скрипт получается с первого раза, в некоторых случаях он приводит к зависанию процесса и "торможению" остальных процессов SQL-сервера (имеется в виду архитектура Classic Server). Казалось, нет ничего проще: узнать зависший процесс в диспетчере задач в Windows или вызвав ps ax в LINUX и прибить зависший процесс. Но если в этот момент к базе данных еще подключено 50-70 пользователей (причем у всех работа начинает дико тормозить, шеф уже полез в сейф за пистолетом :), определить кто есть кто в этом списке достаточно проблематично. Нужно лезть в монитор файерволла, смотреть какой PID процесса подключенного от твоего компьютера, потом убивать этот процесс. Не спешите кидать в автора камни с криком "Кто же отлаживает скрипты на реальной базе ?". Отлаживают, причем все, только не признаются в этом. :) А если серьезно, то существует иногда необходимость при разработке систем реального времени или отчетов реального времени, обновляющихся с определенным интервалом, производить отладку на реальных данных потому, что требуется обеспечить ввод этих самых данных с нужной частотой. И обеспечить его лучше, чем это могут сделать сами пользователи системы, вряд ли получиться.

  Или, в конце концов, выяснить, какой из клиентов "завис" и принудительно завершить его запрос. Особенно это полезно в тех случаях, когда главный бухгалтер вдруг решит составить годовой отчет в конце рабочего дня, а не в положенное ей штатным расписанием утреннее время, в то время как идет массовая "набивка" накладных и "подгрузка" данных из распределенных филиалов.

  Давней мечтой программистов баз данных InterBase/Firebird был функционал мониторинга активных транзакций и запросов с возможностью принудительного завершения последних. И вот наконец дождались: разработчики Firebird реализовали это возможность в Firebird v2.1. Об этой возможности далее и пойдет речь в статье.


2.Постановка учебной задачи

  В качестве учебной задачи мы поставим написание программы для мониторинга базы данных при помощи системных таблиц мониторинга, а также управление "зависшими" запросами, в среде Borland Delphi 7.


3.Ограничения учебной задачи

  При решении учебной задачи использовались:
  1. Firebird SQL Server v2.1.0.15972 Classic Edition
  2. Утилита администрирования базами данных InterBase/Firebird IBExpert v2007.05.03
  3. Компилятор языка Паскаль Delphi v7
  4. Библиотека доступа к базам данных InterBase/Firebird InterbaseExpress, входящая в поставку компилятора Delphi 7.
  5. Библиотека EhLib, компонент TDBGridEh для отображения данных. Хотя, в принципе, подойдет и любой другой DBGrid.

  С целью упрощения кода для восприятия, все действия и методы размещены в обработчиках событий OnClick, но это не является основанием для того, чтобы преждевременно записывать автора в ряды "баттоно-кидателей" :)

4.Небольшое отступление

  Прежде, чем приступить к рассмотрению данной функциональности Firebird, стоит обратить внимание на то,что функциональные особенности, описанные в статье, являются нововведением Firebird v2.1, то есть Вам следует обновить версию сервера до указанной. Более того, как замечено на практике, таблицы мониторинга MON$* не совсем виртуальные, и напрямую зависят от версии ODS (On Disk Structure) базы данных (требуется ODS 11.1), а это в свою очередь значит, что придется перед установкой новой версии сервера зарезервировать базу данных под старой версией и восстановить из бэкапа под новой.

  В этом месте Вас ожидают еще одни "грабли". Если Вы используете UDF, скомпилированные при помощи M$ Visual C++ (в часности, популярная rFunc http://sourceforge.net/projects/rfunc), то с радостью сообщаю Вам: разработчики перешли на новую версию компилятора, теперь и fbclient.dll, и процессу сервера требуются библиотеки времени выполнения msvcp80.dll и msvcr80.dll, в то время, как rFunc у Вас собрана скорее всего под старыми msvcp71.dll и msvcr71.dll. А это значит, что даже если у Вас получиться восстановить базу данных из бэкапа, то все равно возможны ошибки при вызове прецедур или триггеров, использующих UDF из библиотек, написанных при помощи младших версий компилятора M$ Visual C++. То есть перед переходом на новую версию сервера, необходимо пересобрать UDF-библиотеки под новой версией компилятора.

  UDF, написанных при помощи FreePascal (Lazarus), Borland Delphi или Borland CBuilder, данное отступление не касается. Опять же, если Вы не используете в Ваших библиотеках линковку с msvcp71.dll и msvcr71.dll

  И последнее. Для того, чтобы использовать данную функциональность Firebird, необходимо заменить клиентские библиотеки Вашего ПО на fbclient.dll версии 2.1, а также для программы учебной задачи, чтобы иметь возможность "убивать" интересующие нас запросы.


5.Описание таблиц мониторинга

  Ниже представлена логическая схема таблиц мониторинга.


  Логической мы ее назовем потому, что связи таблиц, предсталенные на схеме, не являются физическими (не существует соответствующих RELATION CONSTRAINTS), в данном случае эти связи отображают логическое отношение данных, допускающее, например, наличие записей в таблице MON$STATEMENTS, имеющих MON$TRANSACTION_ID = NULL, появляющихся при подключении клиентских приложений, ипользующих fbclient.dll (gds32.dll) версии ниже, чем 2.1. Рассмотрим подробнее эти таблицы:

MON$DATABASE (база данных) имеет одну запись (например, как RDB$DATABASE), хранящую служебную информацию о текущей базе данных. В текущий версии сервера ТОЛЬКО ЧТЕНИЕ
    MON$DATABASE_NAME физическое имя файла базы данных в файловой системе
    MON$PAGE_SIZE размер страницы базы данных (1024, 2048, 4096, 8192, 16384).
    MON$ODS_MAJOR старшая версия ODS базы данных, например 11
    MON$ODS_MINOR младшая версия ODS базы данных, например 1
    MON$OLDEST_TRANSACTION (OIT number)
    MON$OLDEST_ACTIVE (OAT number)
    MON$OLDEST_SNAPSHOT (OST number)
    MON$NEXT_TRANSACTION ID следующей транзакции (значение "генератора транзакций")
    MON$PAGE_BUFFERSколичество страниц, расположенных в кэше
    MON$SQL_DIALECT диалект языка SQL
    MON$SHUTDOWN_MODE текущий режим остановки
        0: online
        1: multi-user shutdown
        2: single-user shutdown
        3: full shutdown
        принимает одно из значений поля RDB$TYPE набора данных, возвращаемых SQL-скриптом
        SELECT T.RDB$TYPE, T.RDB$TYPE_NAME FROM RDB$TYPES T WHERE (T.RDB$FIELD_NAME ='MON$SHUTDOWN_MODE')

    MON$SWEEP_INTERVAL интервал перед сборкой мусора
    MON$READ_ONLY признак доступности базы данных только для чтения
    MON$FORCED_WRITES
    MON$RESERVE_SPACE
    MON$CREATION_DATEдата создания (восстановления из бэкапа) файла базы данных
    MON$PAGES суммарное количество страниц базы данных

MON$ATTACHMENTS (активные подключения) хранит информацию об активных в текущий момент подключениях к базе данных. В текущий версии сервера ТОЛЬКО ЧТЕНИЕ
    MON$ATTACHMENT_ID идентификатор подключения
    MON$SERVER_PID идентификатор процесса сервера (PID). Можно увидеть в диспетчере задач M$ Windows или в списке #ps ax LINUX
    MON$STATEсостояние подключения
        0: idle
        1: active
        принимает одно из значений поля RDB$TYPE набора данных, возвращаемых SQL-скриптом
        SELECT T.RDB$TYPE, T.RDB$TYPE_NAME FROM RDB$TYPES T WHERE (T.RDB$FIELD_NAME ='MON$STATE')

    MON$ATTACHMENT_NAME имя базы, укзанное при подключении (строка подключения). Содержит или физическое имя файла базы данных, или имя псевдонима из файла ($firebird)/aliases.conf, смотря как была задана строка подключения.
    MON$USER имя пользователя, под которым подключились к базе
    MON$ROLE роль пользователя
    MON$REMOTE_PROTOCOL протокол подключения, например TCPv4
    MON$REMOTE_ADDRESS удаленный адрес подлючения, например 192.168.0.101
    MON$REMOTE_PID идентификатор процесса (PID) клиентского приложения на удаленной ЭВМ, вызвавшего подключение. Если клиентское приложение использует библиотеку fbclient.dll младше версии 2.1, то поле содержит NULL
    MON$REMOTE_PROCESS полный путь к исполняемому файлу клиентского приложения на удаленной ЭВМ, вызвавшего подключение. Если клиентское приложение использует библиотеку fbclient.dll младше версии 2.1, то поле содержит NULL
    MON$CHARACTER_SET_ID идентификатор кодировки, указанной при подключении, например 52
        принимает одно из значений поля RDB$CHARACTER_SET_ID набора данных, возвращаемых SQL-скриптом
        SELECT C.RDB$CHARACTER_SET_ID, C.RDB$CHARACTER_SET_NAME FROM RDB$CHARACTER_SETS C

    MON$TIMESTAMP дата-время старта подключения
    MON$GARBAGE_COLLECTION флаг сборки мусора

MON$TRANSACTIONS (активные транзакции) хранит информацию об активных в текущий момент транзакциях. В текущий версии сервера ТОЛЬКО ЧТЕНИЕ
    MON$TRANSACTION_ID идентификатор транзакции
    MON$ATTACHMENT_ID идентификатор подключения из таблицы MON$ATTACHMENTS
    MON$STATE состояние транзакции
        0: idle
        1: active
        принимает одно из значений поля RDB$TYPE набора данных, возвращаемых SQL-скриптом
        SELECT T.RDB$TYPE, T.RDB$TYPE_NAME FROM RDB$TYPES T WHERE (T.RDB$FIELD_NAME ='MON$STATE')

    MON$TIMESTAMP время старта транзакции
    MON$TOP_TRANSACTION (top transaction)
    MON$OLDEST_TRANSACTION (local OIT number)
    MON$OLDEST_ACTIVE (local OAT number)
    MON$ISOLATION_MODE уровень изоляции транзакции
        0: consistency
        1: concurrency
        2: read committed record version
        3: read committed no record version
        принимает одно из значений поля RDB$TYPE набора данных, возвращаемых SQL-скриптом
        SELECT T.RDB$TYPE, T.RDB$TYPE_NAME FROM RDB$TYPES T WHERE (T.RDB$FIELD_NAME ='MON$ISOLATION_MODE')

    MON$LOCK_TIMEOUT таймаут блокировки записей
        -1: infinite wait (бесконечно)
        0: no wait (не ожидать разрешения конфликта)
        N: timeout N (ожидать разрешение конфликта)
    MON$READ_ONLY транзакция открыла набор данных "только для чтения"
    MON$AUTO_COMMIT признак автоматического подтверждения транзакции
    MON$AUTO_UNDO признак автоматического отката транзакции

MON$STATEMENTS (активные запросы) хранит информацию об активных в текущий момент запросах к базе данных. В текущий версии сервера ТОЛЬКО ЧТЕНИЕ для записей, вызванных подключением при помощи старых версий клиентских библиотек, и ЧТЕНИЕ-УДАЛЕНИЕ для подключений, использующих клиентскую библиотеку версии 2.1 и старше.
    MON$STATEMENT_ID идентификатор запроса к базе данных.
    MON$ATTACHMENT_ID идентификатор подключения к базе данных из таблицы MON$ATTACHMENTS
    MON$TRANSACTION_ID идентификатор транзакции. Если клиентское приложение использует библиотеку fbclient.dll младше версии 2.1, то поле содержит NULL
    MON$STATE состояние запроса
        0: idle
        1: active
        принимает одно из значений поля RDB$TYPE набора данных, возвращаемых SQL-скриптом
        SELECT T.RDB$TYPE, T.RDB$TYPE_NAME FROM RDB$TYPES T WHERE (T.RDB$FIELD_NAME ='MON$STATE')

    MON$TIMESTAMP дата-время старта запроса
    MON$SQL_TEXT текст запроса (если доступен). для операторов DDL (CREATE TABLE, VIEW, TRIGGER) не доступен.


5. Написание клиентского приложения.

  Создадим новый проект в Delphi и разместим компоненты как показано на рисунке.

  

  Назначение компонентов: IBDataBase1 создает подключение к базе данных, trAttachments :TIBTransaction - транзакция для наборов данных, qrAttachments :TIBQuery для выборки данных о текущих подключениях. qrTransactions :TIBQuery выбирает активные транзакции Firebird2.1-клиентов. qrTransactionsStatements :TIBQuery выбирает активные запросы Firebird2.1-клиентов, qsStatements :TIBQuery выбирает запросы ранних клиентов, для которых MON$TRANSACTION_ID = NULL.

  Для класса формы объявим такие методы в секции private.

//Обновляет список подключений к базе данных 
procedure TFormMain.RefreshAttachments;
begin
  if trAttachments.InTransaction then
    trAttachments.Rollback;
  qrAttachments.SQL.Text :=
    'SELECT MA.MON$ATTACHMENT_ID, MA.MON$SERVER_PID, MA.MON$STATE, '+#13#10+
    '       (SELECT FIRST 1 T.RDB$TYPE_NAME FROM RDB$TYPES T '+#13#10+
    '        WHERE (T.RDB$FIELD_NAME = ''MON$STATE'') '+#13#10+
    '          AND (T.RDB$TYPE       = MA.MON$STATE)) AS MON$STATE_TYPE_NAME, '+#13#10+
    '       MA.MON$ATTACHMENT_NAME, MA.MON$USER, MA.MON$ROLE, MA.MON$REMOTE_PROTOCOL, '+#13#10+
    '       MA.MON$REMOTE_ADDRESS, MA.MON$REMOTE_PID, MA.MON$CHARACTER_SET_ID, '+#13#10+
    '       (SELECT FIRST 1 CH.RDB$CHARACTER_SET_NAME FROM RDB$CHARACTER_SETS CH '+#13#10+
    '        WHERE (CH.RDB$CHARACTER_SET_ID = MA.MON$CHARACTER_SET_ID)) AS CHARACTER_SET_NAME, '+#13#10+
    '       MA.MON$TIMESTAMP, MA.MON$GARBAGE_COLLECTION '+#13#10+
    'FROM MON$ATTACHMENTS MA '+#13#10;
  // если отмечена "Не показывать текущее подключение ...", то исключаем из набора данных текущее подключение к базе данных   
  if cbxHideCurrentConnection.Checked then
    qrAttachments.SQL.Text := qrAttachments.SQL.Text + 'WHERE (MA.MON$ATTACHMENT_ID <> CURRENT_CONNECTION) ';
  try
    qrAttachments.Open;
  except
  end;
end;

//Обновляет список активных транзакций для указанного подключения AttachmentId
procedure TFormMain.RefreshTransactions(const AttachmentId:string);
begin
  if qrTransactions.Active then qrTransactions.Close;
  qrTransactions.SQL.Text :=
    'SELECT TR.MON$TRANSACTION_ID, TR.MON$ATTACHMENT_ID, TR.MON$STATE, '+#13#10+
    '       (SELECT FIRST 1 T.RDB$TYPE_NAME FROM RDB$TYPES T '+#13#10+
    '        WHERE (T.RDB$FIELD_NAME = ''MON$STATE'') '+#13#10+
    '          AND (T.RDB$TYPE       = tr.MON$STATE)) AS MON$STATE_TYPE_NAME, '+#13#10+
    '       TR.MON$TIMESTAMP, TR.MON$TOP_TRANSACTION, TR.MON$OLDEST_TRANSACTION, '+#13#10+
    '       TR.MON$OLDEST_ACTIVE, TR.MON$ISOLATION_MODE, '+#13#10+
    '       (SELECT FIRST 1 T.RDB$TYPE_NAME FROM RDB$TYPES T '+#13#10+
    '        WHERE (T.RDB$FIELD_NAME = ''MON$ISOLATION_MODE'') '+#13#10+
    '          AND (T.RDB$TYPE       = tr.MON$ISOLATION_MODE)) AS MON$ISOLATION_MODE_NAME, '+#13#10+
    '       TR.MON$LOCK_TIMEOUT, TR.MON$READ_ONLY, TR.MON$AUTO_COMMIT, TR.MON$AUTO_UNDO '+#13#10+
    'FROM   MON$TRANSACTIONS TR '+#13#10+
    'WHERE  (TR.MON$ATTACHMENT_ID = '''+AttachmentId+''') '+#13#10+
    '  AND  (TR.MON$TRANSACTION_ID <> CURRENT_TRANSACTION)'; // hidding current transaction
  try
    qrTransactions.Open;
  except
  end;
end;

// Обновляет список активных запросов Firebird2.1-клиентов для транзакции TransactionId подключения AttachmentId
procedure TFormMain.RefreshTransactionsStatments(const AttachmentId, TransactionId:string);
begin
  if qrTransactionsStatments.Active then
    qrTransactionsStatments.Close;
  qrTransactionsStatments.SQL.Text :=
    'SELECT ST.MON$STATEMENT_ID, ST.MON$ATTACHMENT_ID, ST.MON$TRANSACTION_ID, '+#13#10+
    '       ST.MON$STATE, ST.MON$TIMESTAMP, ST.MON$SQL_TEXT, '+#13#10+
    '       (SELECT FIRST 1 T.RDB$TYPE_NAME FROM RDB$TYPES T '+#13#10+
    '        WHERE (T.RDB$FIELD_NAME = ''MON$STATE'') '+#13#10+
    '          AND (T.RDB$TYPE       = ST.MON$STATE)) AS MON$STATE_TYPE_NAME '+#13#10+
    'FROM   MON$STATEMENTS ST '+#13#10+
    'WHERE  (ST.MON$ATTACHMENT_ID = '''+AttachmentId+''') '+#13#10+
    '  AND  (ST.MON$TRANSACTION_ID = '''+TransactionId+''')';
  try
    qrTransactionsStatments.Open;
  except
  end;
end;

Обновляет список активных запросов ранних Firebird-клиентов для подключения AttachmentId
procedure TFormMain.RefreshStatments(const AttachmentId:string);
begin
  if qrStatements.Active then
    qrStatements.Close;
  qrStatements.SQL.Text :=
    'SELECT ST.MON$STATEMENT_ID, ST.MON$ATTACHMENT_ID, ST.MON$TRANSACTION_ID, '+#13#10+
    '       ST.MON$STATE, ST.MON$TIMESTAMP, ST.MON$SQL_TEXT, '+#13#10+
    '       (SELECT FIRST 1 T.RDB$TYPE_NAME FROM RDB$TYPES T '+#13#10+
    '        WHERE (T.RDB$FIELD_NAME = ''MON$STATE'') '+#13#10+
    '          AND (T.RDB$TYPE       = ST.MON$STATE)) AS MON$STATE_TYPE_NAME '+#13#10+
    'FROM   MON$STATEMENTS ST '+#13#10+
    'WHERE  (ST.MON$ATTACHMENT_ID = '''+AttachmentId+''') '+#13#10+
    '  AND  (ST.MON$TRANSACTION_ID IS NULL)';
  try
    qrStatements.Open;
  except
  end;
end;

// Убивает активный запрос по его коду StatementId
procedure TFormMain.KillStatement(const StatementId:string);
var
  lk_tr :TIBTransaction;
  lk_qr :TIBQuery;
  lk_flag :Boolean;
begin
  if trAttachments.InTransaction then
    trAttachments.Rollback;

  lk_tr := TIBTransaction.Create(nil);
  lk_tr.DefaultDatabase := IBDataBase1;
  lk_qr := TIBQuery.Create(lk_tr);
  lk_qr.Database    := IBDataBase1;
  lk_qr.Transaction := lk_tr;
  lk_qr.SQL.Text := 'DELETE FROM MON$STATEMENTS MS WHERE (MS.MON$STATEMENT_ID = '+StatementId+')';
  lk_flag := false;
  lk_tr.StartTransaction;
  try
    lk_qr.ExecSQL;
  finally
    lk_flag := true;
  end;
  if lk_tr.InTransaction then
    begin
    if lk_flag then
      lk_tr.Commit
     else
      lk_tr.Rollback;
    end;
  lk_tr.Free;
  RefreshAttachments;
end;


  Соответственно, определим следующие обработчики событий:
procedure TFormMain.ButtonRefreshClick(Sender: TObject);
begin
  RefreshAttachments;
end;

procedure TFormMain.qrAttachmentsBeforeClose(DataSet: TDataSet);
begin
  if qrTransactions.Active then
    qrTransactions.Close;
  if qrStatements.Active then
    qrStatements.Close;
end;

procedure TFormMain.qrAttachmentsAfterScroll(DataSet: TDataSet);
begin
  RefreshTransactions(DataSet.FieldByName('MON$ATTACHMENT_ID').AsString);
  RefreshStatments   (DataSet.FieldByName('MON$ATTACHMENT_ID').AsString);
end;

procedure TFormMain.qrTransactionsBeforeClose(DataSet: TDataSet);
begin
  if qrTransactionsStatments.Active then
    qrTransactionsStatments.Close;
end;

procedure TFormMain.qrTransactionsAfterScroll(DataSet: TDataSet);
begin
  RefreshTransactionsStatments(DataSet.FieldByName('MON$ATTACHMENT_ID').AsString,
    DataSet.FieldByName('MON$TRANSACTION_ID').AsString);
end;

procedure TFormMain.ButtonKillClick(Sender: TObject);
begin
  KillStatement(qrTransactionsStatments.FieldByName('MON$STATEMENT_ID').AsString);
end;


  Запустив программу на выполнение, мы можем увидеть состояние базы данных.

  

  


6.Заключение

  Данная статья была нацелена на то, чтобы показать как пользоваться таблицами мониторинга в базах данных Firebird v2.1, поэтому автор старался сделать все "по-примитивней". Конечно, автор надеется, что у читателя найдется более широкая область применения данной функциональности для своих нужд, например как: использование мониторинга в связке с потоковыми запросами (представлеными в стандартных примерах $DELPHI\Demos\DelphiWin32\VCLWin32\Db\IBX\ThreadedIBX), отслеживание атак на Web-системы (Apache->PHP->Firebird), отладка сложных запросов.

2 комментария:

  1. >> А это значит, что даже если у Вас получиться восстановить базу данных из бэкапа
    получиТСЯ

    ОтветитьУдалить
  2. 888 Casino App - Download Android App for $99 - JTG Hub
    888 Casino app is available to download 성남 출장샵 in your 원주 출장안마 Android 여수 출장샵 or iOS device, you can 속초 출장마사지 install this app 세종특별자치 출장샵 in your favorite web browser, and you can

    ОтветитьУдалить