Доступ к счетчикам Perfmon из SQL Server. Способы 1 и 2.

 

Способ 1.

 

Посл-ть д-й.

 

1. Все определенные на машине каунтеры экспортятся в текстовый файл при помощи typeperf.

2. Текстовый файл редактируется, остаются только интересующие каунтеры.

3. Собираются значения каунтеров на интервале при помощи logman.

4. Файл с рез-тами замеров грузится на SQL Server через создание ODBC DSN и команду relog.

5. На SQL Server появляются таблички DisplayToID, CounterDetails, CounterData, содержащие информацию по каунтерам и их кардиограммы.

 

Описан здесь - https://www.mssqltips.com/tip.asp?tip=1722.

Это оч.хор. способ, и нет сомнения, что администраторы будут его пользовать. Но разработчики обладают более независимым и свободным суждением. Разработчика угнетают все эти непонятные typeperf, logman, relog и пр. Что это такое вообще? Очевидно, что эти тулы тоже писаны разработчиками, которых когда-то давно достали админы своими унылыми хотелками видеть счетчики в базе. Настоящий разработчег не будет пользоваться приблудой, написанной другим разработчиком. Вместо этого он потратит час или два или сколько не жалко, но напишет свою собственную приблуду, хотя бы ему пришлось к ней прибегнуть всего один раз. Она, по определению, будет лучше всех остальных аналогичных приблуд хотя бы потому, что писана лично им. Настоящий разработчик не оформляет свою приблуду в виде exeшника, потому что тогда ей смогут пользоваться администраторы. Администраторы подпишут у начальства инструкцию, по которой все на предприятии должны будут пользоваться этой тулой вместо того, чтобы развивать творческую мысль, изобретая новый невиданный велосипед. Настоящий разработчик распространяет приблуду в виде исходников. Тогда другие разработчики, вдоволь раскритиковав и поглумившись над собратом, возьмут исходник за основу и примутся развивать его дальше. И так до тех пор, пока какой-нибудь ушлый администратор не настоит на exeшнике, что остановит процесс эволюции, приведет со временем к утрате исходника, нагнетет революционную ситуацию и вызовет очередной виток диалектической спирали. Go to начало абзаца.

Способ 2.

 

Йа нопесал оч.хор.приблуду, кот.делает сабж. Сначала думал UDF, потом решил UDT, чтобы не передавать всякий раз имя счетчика, когда один и тот же счетчик дергается много раз. А UDF туда вошла как статик метод.

Счетчег задается в формате Машина\Категория(Инстанс)\Имя_счетчика, например, localhost\Processor(_Total)\% Processor Time, как они значатся в Perfmone:

clip_image003

 

Имя машины может быть пустым, тогда это предполагается локальная тачка. Инстанс тоже может быть пустым. Тогда, если категория, по определению, многоинстансовая, он полагается _Total.

Примеры вызовов:

 

declare @x as PerfCounter

set @x = 'localhost\Processor(_Total)\% Processor Time'

select @x.ToString(), @x.Value

declare @y as PerfCounter

set @y = 'Memory\Available bytes'

select @y.ToString(), @y.Value

declare @z as PerfCounter

set @z = ''

select @z.ToString(), @z.Value

select PerfCounter::GetValue('.\SQLServer:Plan Cache(SQL Plans)\Cache Hit Ratio')

Так это работает:

clip_image005

По-моему, афегительная вещь. Я даже где-то молодец. Отдаю задаром. Берите, пользуйтесь, дорабатывайте. Единственный пункт лицензионного соглашения – ссылка на источник (https://blogs.msdn.com/alexejs/).

Code skipped, т.к. далее исправленная и дополненная версия

--------------------------------------------------------------------------------------------------------------------------

Роскошная вещь, мне нравится (сам себя не похвалишь ...:)). Нет, в самом деле, смотрите, как с ней изящно и непринужденно делается логгирование перфмоновских счетчиков в SQL Server.

Создаем две таблички. В первой будут сидеть нужные нам каунтеры:

create table my.Counters (id int identity primary key, counter PerfCounter)

insert my.Counters (counter) values ('localhost\Processor(_Total)\% Processor Time')

insert my.Counters (counter) values ('Memory\Available bytes')

insert my.Counters (counter) values ('.\SQLServer:Plan Cache(SQL Plans)\Cache Hit Ratio')

 

Во второй результаты сбора:

 

create table my.PerfLog (zamer char(8), counter_id int references my.Counters(id), value float, primary key (zamer, counter_id))

 

Процесс сбора происходит например, так. Делаем 5 замеров всех счетчиков из Counters каждую секунду

truncate table my.PerfLog

declare @i int

declare @j int, @pc PerfCounter

set @i = 1

while (@i <= 5) begin

    select @i

      select @j = min(id) - 1 from my.Counters

      while (1 = 1) begin

            select top 1 @j = id, @pc = counter from my.Counters where id > @j

            if @@rowcount = 0 break

            select '---', @j

        insert my.PerfLog values (convert(char(8), getdate(), 108), @j, @pc.Value)

      end

    waitfor delay '00:00:01'

    set @i = @i + 1

end

В журнале собралась история замеров выбранных счетчиков

select * from my.PerfLog

clip_image007

Давайте ее распайвотим для наглядности:

select * from my.PerfLog pivot (min(value) for counter_id in ([1], [2], [3])) t

clip_image009

Все хорошо, но хотелось бы иметь осмысленные названия счетчиков

select PerfLog.zamer, Counters.counter.ToString() cntName, PerfLog.value

      from my.PerfLog join my.Counters on PerfLog.counter_id = Counters.id

clip_image011

 

и, соответственно, так:

select * from (

select PerfLog.zamer, Counters.counter.ToString() cntName, PerfLog.value

      from my.PerfLog join my.Counters on PerfLog.counter_id = Counters.id ) t1

pivot

(min(value) for cntName in ([localhost\Processor(_Total)\% Processor Time], [.\Memory\Available bytes], [.\SQLServer:Plan Cache(SQL Plans)\Cache Hit Ratio])) t2

 

clip_image013

Супер! Одно неудобно – перечислять руками список счетчиков в pivot-списке. Поправим это дело.

 

declare @s nvarchar(max)

set @s = 'select * from (' +

         ' select PerfLog.zamer, Counters.counter.ToString() cntName, PerfLog.value ' +

         ' from my.PerfLog join my.Counters on PerfLog.counter_id = Counters.id ) t1 ' +

         'pivot ' +

         '(min(value) for cntName in (['

select @s = @s + counter.ToString() + '], [' from my.Counters

set @s = left(@s, len(@s) - 3) + ')) t2'

--select @s

exec sp_executesql @s

Все. Пользуйтесь.

-------------------------------------------------------------------------------------------------------------------------- 

Счетчики позволяют не только пассивно считывать показания предопределенных датчиков перфмона, но и создать свои, значения которых мы вольны изменять в зависимости от обстоятельств.

В состав SQL Serverных счетчиков входит счетчик Query, относящийся к категории SQLServer:User Settable, экземпляры которой выглядят, как User counter <i>, где <i> может принимать значения от 1 до 10.Управление значениями экземпляров осуществляется при помощи процедур sp_user_counter<i> n, где <i> то же, а n – целочисленное значение, в которое устанавливается счетчик. Значение данных счетчиков сбрасывается в 0 при рестарте SQL Server.

Пример

declare @x PerfCounter

set @x = 'SQLServer:User Settable(User counter 5)\Query'

select @x.Value

exec sp_user_counter5 50

select @x.Value

------------------------

0

50

Так это выглядит на графике Performance Monitor. Более подробную информацию про хранимые процедуры sp_user_counter<i> можно почерпнуть здесь: https://msdn2.microsoft.com/en-us/library/ms187480(SQL.100).aspx.

clip_image015

На тот случай, если вдруг 10-ти пользовательских счетчиков покажется мало, можно воспользоваться обычным Server Explorer в Visual Studio для создания / удаления собственных перфмоновских категорий и каунтеров внутри них.

clip_image017

Кроме того, я расширил CLRный тип PerfCounter (см. выше), с помощью которого теперь можно создавать кастомные категории / счетчики. Пример:

 

select PerfCounter::CreateCategory('MyCategory', 'Моя категория')

select PerfCounter::AddCounterToCategory('MyCategory', 'MyCounter1', 'Мой счетчик 1')

select PerfCounter::AddCounterToCategory('MyCategory', 'MyCounter2', 'Мой счетчик 2')

 

Вот они появились:

 

clip_image019

Меняем значения

 

declare @x PerfCounter

set @x = 'MyCategory\MyCounter1'

select @x.ToString(), @x.Value

--Для customных счетчиков можно менять значения

set @x.Value = 50

select @x.ToString(), @x.Value

waitfor delay '00:00:20'

set @x.IncrBy(10) --Обратите внимание на вызов void мутатора

select @x.ToString(), @x.Value

waitfor delay '00:00:20'

set @x.IncrBy(-30) --аналогично

select @x.ToString(), @x.Value

Вот, что при этом происходит:

clip_image021

--Для неcustomных счетчиков попытки изменить значение ничего не дают (в нашей имплементации)

declare @y PerfCounter

set @y = 'SQLServer:User Settable(User counter 5)\Query'

select @y.ToString(), @y.Value

set @y.Value = 10

select @y.ToString(), @y.Value

set @y.IncrBy(1)

select @y.ToString(), @y.Value

--Я не стал париться с раздельным удалением каунтеров, решив, что удалять будем всю категорию целиком

select PerfCounter::DeleteCategory('MyCategory')

Удалить по ошибке некастомную категорию не удастся, т.к. при этом выкидывается InvalidOperationException, что Cannot delete Performance Category because this category is not registered or is a system category.

 

Более подробную инфу по созданию кастомных каунтеров в перфмоне – см. https://msdn2.microsoft.com/en-us/library/system.diagnostics.performancecounter(VS.80).aspx

Приложение. Модифицированный код UDT PerfCounter. Посл.модификация от 18.01.2009:

/* Эта сборка unsafe, поск. в ней исп-ся доступ к Perf Counters. */

 

using System;

using System.Data;

using System.Data.SqlClient;

using System.Data.SqlTypes;

using Microsoft.SqlServer.Server;

using System.Diagnostics;

using System.Text;

using System.IO;

[Serializable]

//Поскольку в нашем пользовательском типе будет внутренняя переменная не Value типа, Native сериализация не проходит.

//Пользовательская сериализация означает, что нам еще придется имплементировать интерфейс IBinarySerialize,

//состоящий из двух методов: Write - это бинарный эквивалент ToString() и Read() - соответственно,

//бинарный эквивалент Parse().

//MaxByteSize выставляем из соображений, что тип предполагается не только для SQL Server 2008,

//но и для 2005, где размер пользовательского типа не может превышать 8000 байт.

[Microsoft.SqlServer.Server.SqlUserDefinedType(Format.UserDefined, MaxByteSize = 8000)]

public struct PerfCounter : INullable, IBinarySerialize

{

    private PerformanceCounter pc;

    private bool isNull;

    /// <returns>Название счетчика в формате Машина\Категория(Экземпляр)\Счетчик</returns>

    public override string ToString()

    {

        if (isNull) return "Null";

        StringBuilder sb = new StringBuilder(pc.MachineName);

        sb.Append('\\'); sb.Append(pc.CategoryName);

        if (!String.IsNullOrEmpty(pc.InstanceName))

        {

            sb.Append('('); sb.Append(pc.InstanceName); sb.Append(')');

        }

     sb.Append('\\'); sb.Append(pc.CounterName);

        return sb.ToString();

    }

    public bool IsNull

    {

        get

        {

            return isNull;

        }

    }

    public static PerfCounter Null

    {

        get

        {

            PerfCounter h = new PerfCounter();

            h.isNull = true;

            return h;

        }

    }

    /// <param name="counter">Строка с полным именем счетчика в формате Машина\Категория(Экземпляр)\Счетчик.

    /// Машина может быть пустой - полагается локальная тачка.

    /// Экземпляр тоже может быть пустой. Круглые скобки при этом можно не ставить.

    /// Если экземпляр пустой, а счетчик по природе многоэкземплярный, берется _Total.</param>

    /// <returns></returns>

    public static PerfCounter Parse(SqlString counter)

    {

        if (counter.IsNull) return Null;

        PerfCounter u = new PerfCounter();

        try

        {

            string s = counter.ToString();

            string[] cntItems = counter.ToString().Trim().Split(new char[] { '\\' }, StringSplitOptions.None);

            string cntMachine, cntCategory, cntInstance, cntName;

            cntName = cntItems[cntItems.Length - 1].Trim();

            cntCategory = cntItems[cntItems.Length - 2].Trim();

            if (cntItems.Length < 3) //Если разделителей \ меньше 2-х, предполагаем, что это опущена машина, т.е. она предполагается локальной

                cntMachine = ".";

            else

            {

                cntMachine = cntItems[cntItems.Length - 3].Trim();

                if (String.IsNullOrEmpty(cntMachine)) cntMachine = "."; //Если разделитель после машины был, но машина пустая, она тоже предполагается локальной

            }

            //Сейчас в cntCategory категория лежит вместе с экземпляром, напр., SQLServer:Wait Statistics(Average Wait Time (ms))

            //Разделяем категорию и экземпляр.

            int firstOpeningBracket = cntCategory.IndexOf('('); //Если экземпляр есть, cntCategory заканчивается на закрывающую скобку, поэтому заводить lastClosingBracket бессмысленно.

            if (firstOpeningBracket == -1)

                cntInstance = String.Empty; //Нет экземпляра

            else

            {

                cntInstance = cntCategory.Substring(firstOpeningBracket + 1, cntCategory.Length - firstOpeningBracket - 2);

                cntCategory = cntCategory.Substring(0, firstOpeningBracket);

            }

            //Итак, мы распарсили строку с именем счетчика в cntMachine, cntCategory, cntInstance, cntName.

            //Из них сейчас пустым может быть только cntInstance. Если он пустой

            if (String.IsNullOrEmpty(cntInstance))

            {

                //А категория счетчика предполагает несколько экземпляров

                PerformanceCounterCategory pcc = new PerformanceCounterCategory(cntCategory, cntMachine);

                if (pcc.CategoryType == PerformanceCounterCategoryType.MultiInstance)

                    //Экземпляр выставляем в _Total

                    cntInstance = "_Total";

            }

            u.pc = CreatePerfCounterFromItems(cntMachine, cntCategory, cntInstance, cntName);

        }

        catch (Exception)

        {

            return Null;

        }

        return u;

    }

    private static PerformanceCounter CreatePerfCounterFromItems(string cntMachine, string cntCategory, string cntInstance, string cntName)

    {

        PerformanceCounter pc;

        //Если экземпляр счетчика пустой

        if (String.IsNullOrEmpty(cntInstance))

        {

            //Значит, он вообще не предусмотрен для данного счетчика. Создаем без него.

            pc = new PerformanceCounter(cntCategory, cntName);

            pc.MachineName = cntMachine;

        }

        else

            //Иначе создаем обычным порядком

            pc = new PerformanceCounter(cntCategory, cntName, cntInstance, cntMachine);

        try

        {

            pc.ReadOnly = false; // Само по себе присвоение свойства не вызывает ошибки. Надо еще чего-нибудь сделать.

            pc.NextValue();

        }

        catch (InvalidOperationException)

        {

            pc.ReadOnly = true;

        }

        return pc;

    }

    public void Write(BinaryWriter w)

    {

        w.Write(isNull);

        if (! isNull)

        {

            w.Write(pc.MachineName); w.Write(pc.CategoryName); w.Write(pc.InstanceName); w.Write(pc.CounterName);

        }

    }

    public void Read(BinaryReader r)

    {

        isNull = r.ReadBoolean(); if (isNull) return;

        string cntMachine, cntCategory, cntInstance, cntName;

        cntMachine = r.ReadString(); cntCategory = r.ReadString(); cntInstance = r.ReadString(); cntName = r.ReadString();

        pc = CreatePerfCounterFromItems(cntMachine, cntCategory, cntInstance, cntName);

    }

   

    /// <summary>

    /// Создает кастомную категорию каунтеров. Для простоты она создается одноинстансовой.

    /// </summary>

    /// <param name="categoryName">В случае Null заводится категория Null</param>

    /// <param name="categoryDescription"></param>

    /// Приходится возвращать что-то, потому что я не представляю, как из Т-SQL вызвать

    /// static void метод</returns>

    public static SqlByte CreateCategory(SqlString categoryName, SqlString categoryDescription)

    {

        string m_categoryName = categoryName.ToString();

        string m_categoryDescription = categoryDescription.IsNull ? String.Empty : categoryDescription.ToString();

        if (PerformanceCounterCategory.Exists(m_categoryName))

            throw new Exception("Ничего не создано, потому что такая категория уже существует. " +

                "Используйте метод DeleteCategory, чтобы удалить ее.");

        PerformanceCounterCategory pcc = PerformanceCounterCategory.Create(m_categoryName, m_categoryDescription,

            PerformanceCounterCategoryType.SingleInstance, new CounterCreationDataCollection());

        return 0;

    }

    /// <summary>

    /// Удалить некастомную категорию не даст, выкинув InvalidOperationException, что

    /// Cannot delete Performance Category because this category is not registered or is a system category.

    /// </summary>

    /// <param name="categoryName"></param>

    /// <returns></returns>

    public static SqlByte DeleteCategory(SqlString categoryName)

    {

        string m_categoryName = categoryName.ToString();

        if (PerformanceCounterCategory.Exists(m_categoryName)) PerformanceCounterCategory.Delete(m_categoryName);

        return 0;

    }

    /// <summary>

    /// Создает кастомный каунтер с простейшим способом подсчета (PerformanceCounterType.NumberOfItems32).

    /// Аналогичную штуку можно написать, чтобы их ремувить

    /// (завести новую CounterCreationDataCollection, перебрать в цикле GetCounters(),

    /// добавив в коллекцию все за исключением того, у которого имя совпадает с параметром),

    /// но я не стал париться, решив, что удалять будем всю категорию целиком.

    /// </summary>

    /// <param name="categoryName">В случае Null заводится каунтер Null</param>

    /// <param name="categoryDescription"></param>

    /// Приходится возвращать что-то, потому что я не представляю, как из Т-SQL вызвать

    /// static void метод</returns>

    public static SqlByte AddCounterToCategory(SqlString categoryName, SqlString counterName, SqlString counterDescription)

    {

        string m_categoryName = categoryName.ToString();

        string m_counterName = counterName.ToString();

        string m_counterDescription = counterDescription.IsNull ? String.Empty : counterDescription.ToString();

        PerformanceCounterCategory pcc = new PerformanceCounterCategory(m_categoryName);

        if (pcc.CounterExists(m_counterName))

            throw new Exception("Счетчик не был добавлен, т.к. счетчик с таким именем в данной категории уже существует. " +

                "Используйте метод DeleteCategory, чтобы удалить всю категорию.");

        CounterCreationData ccd;

        CounterCreationDataCollection ccdc = new CounterCreationDataCollection();

        string m_categoryDescription = pcc.CategoryHelp;

        foreach (PerformanceCounter pc in pcc.GetCounters())

        {

            ccd = new CounterCreationData();

            ccd.CounterName = pc.CounterName; ccd.CounterHelp = pc.CounterHelp;

            ccd.CounterType = pc.CounterType; ccdc.Add(ccd);

        }

        ccd = new CounterCreationData();

        ccd.CounterName = m_counterName; ccd.CounterHelp = m_counterDescription;

        ccd.CounterType = PerformanceCounterType.NumberOfItems32; ccdc.Add(ccd);

        PerformanceCounterCategory.Delete(m_categoryName);

        PerformanceCounterCategory.Create(m_categoryName, m_categoryDescription, PerformanceCounterCategoryType.SingleInstance, ccdc);

        return 0;

    }

    public SqlDouble Value

    {

        get

        {

            pc.NextValue(); System.Threading.Thread.Sleep(10); return pc.NextValue();

        }

        set

        {

            if (!pc.ReadOnly) pc.RawValue = (long)value;

        }

    }

    [SqlMethod(IsMutator=true)]

    public void IncrBy(SqlDouble delta)

    {

        if (!pc.ReadOnly) pc.IncrementBy((long)delta);

    }

    /// <summary>

    /// Статический аналог св-ва Value (на чтение)

    /// </summary>

    /// <param name="counter">Название счетчика, напр., localhost\Processor(_Total)\% Processor Time</param>

    /// <returns>Текущее значение</returns>

    public static SqlDouble GetValue(SqlString counter)

    {

        PerfCounter u = PerfCounter.Parse(counter);

        return u.Value;

    }

    public SqlString Description

    {

        get

        {

            return pc.CounterHelp;

        }

    }

    /// <summary>

    /// Метод - статический аналог св-ва Description

    /// </summary>

    /// <param name="counter">Название счетчика, напр., .\SQLServer:Plan Cache(SQL Plans)\Cache Hit Ratio</param>

    /// <returns>Описание</returns>

    public static SqlString GetDescription(SqlString counter)

    {

        return PerfCounter.Parse(counter).Description;

    }

}

 

 

--------------------------------------------------------------------------------------------------------------------------

alexejs Ср фев 27, 2008 08:26

Комментарий к комментарию

Код: Выделить всё

...
/// <returns>Приходится возвращать что-то, потому что я не представляю, как из Т-SQL вызвать
/// static void метод типа
...

Как выяснилось, неспроста я это не представляю. Оно в принципе невозможно. Мужики ответили так: "You cannot call static void methods directly on a type. Instead, you must create them as user defined procedures. This should work:
Create proc CreateCategory @categoryName nvarchar(4000), @categoryDescription @nvarchar(4000) as external name <Assembly>.PerfCounter.CreateCategory"

Ну это что в лоб, что по лбу. Можно сделать его не void, как я, можно завернуть в процедуру, как советуют - один хрен извращаться приходится.