Получить данные о нагрузке на CPU
После формирования структуры проекта можно перейти к наполнению логикой операции по сбору нагрузки на CPU.
В данном разделе описана логика сбора информации в рамках этой системной операции.
Обзор операции
Данная операция отвечает за получение данных о нагрузке на CPU целевого устройства по протоколу Ssh, либо от агента, если он установлен на устройстве.
Для выполнения этой операции сервис на вход должен получить следующий запрос:
-
CollectLinuxCpuUtilisationRequest:-
device:-
Тип параметра:
DeviceContent - Описание: Данные по 1 устройству.
-
Тип параметра:
-
Тип DeviceContent:
-
device_id:-
Тип параметра:
string - Описание: Идентификатор устройства.
-
Тип параметра:
-
model_name:-
Тип параметра:
string - Описание: Название модели устройства.
-
Тип параметра:
-
vendor_name:-
Тип параметра:
string - Описание: Название вендора устройства.
-
Тип параметра:
-
connectors:-
Тип параметра:
RepeatedField<DeviceConnector> - Описание: Идентификатор сетевого интерфейса.
-
Тип параметра:
Тип DeviceConnector:
-
device_network_id:-
Тип параметра:
string - Описание: Идентификатор сетевого интерфейса устройства.
-
Тип параметра:
-
address:-
Тип параметра:
string - Описание: IP/FQDN адрес устройства.
-
Тип параметра:
-
mac:-
Тип параметра:
string - Описание: MAC-адрес устройства.
-
Тип параметра:
-
credentials:-
Тип параметра:
RepeatedField<Credential> - Описание: Учетные данные подключения.
-
Тип параметра:
Тип Credential:
-
protocol:-
Тип параметра:
ConnectorProtocol - Описание: Протокол подключения.
-
Тип параметра:
-
login:-
Тип параметра:
string - Описание: Логин для подключения.
-
Тип параметра:
-
password:-
Тип параметра:
string - Описание: Пароль для подключения.
-
Тип параметра:
-
port:-
Тип параметра:
int32 - Описание: Порт подключения.
-
Тип параметра:
-
cipher:-
Тип параметра:
int32 - Описание: Шифрование (только для IPMI).
-
Тип параметра:
-
version:-
Тип параметра:
uint32 - Описание: Версия протокола (только для SNMP).
-
Тип параметра:
-
community:-
Тип параметра:
string - Описание: Community слово (только для SNMP).
-
Тип параметра:
-
security_name:-
Тип параметра:
string - Описание: Security name (только для SNMP).
-
Тип параметра:
-
context:-
Тип параметра:
string - Описание: Контекст подключения (только для SNMP).
-
Тип параметра:
-
auth_protocol:-
Тип параметра:
string - Описание: Auth protocol (только для SNMP).
-
Тип параметра:
-
auth_key:-
Тип параметра:
string - Описание: Auth key (только для SNMP).
-
Тип параметра:
-
private_protocol:-
Тип параметра:
string - Описание: Private protocol (только для SNMP).
-
Тип параметра:
-
privateKey:-
Тип параметра:
string - Описание: Private key (только для SNMP).
-
Тип параметра:
-
security_level:-
Тип параметра:
string - Описание: Уровень безопасности.
-
Тип параметра:
Enum ConnectorProtocol:
-
ConnectorProtocol(0):- Описание: Невалидное значение.
-
CONNECTOR_PROTOCOL_IPMI(1):- Описание: Ipmi протокол для проверки подключения.
-
CONNECTOR_PROTOCOL_REDFISH(2):- Описание: Redfish протокол для проверки подключения.
-
CONNECTOR_PROTOCOL_SNMP(3):- Описание: Snmp протокол для проверки подключения.
-
CONNECTOR_PROTOCOL_SSH(4):- Описание: Ssh протокол для проверки подключения.
-
CONNECTOR_PROTOCOL_WMI(5):- Описание: Wmi протокол для проверки подключения.
Из даных запроса, сервис интересуют следующие поля:
- DeviceContent->connectors - список возможных сетевых подключений
- DeviceContent->connectors->address - Адрес сервера
- DeviceContent->connectors->credentials - Список данных, необходимых для подключения
- DeviceContent->connectors->protocol - Протокол подключения. Обязательно должен быть CONNECTOR_PROTOCOL_SSH. Другие протоколы должны игнорироваться
- DeviceContent->connectors->login - Логин пользователя, под которым будет выполнятсья команда
- DeviceContent->connectors->password - Пароль пользователя, под которым будет выполнятсья команда
- DeviceContent->connectors->port - Порт подключения
Используя полученные данные, сервис выполняет на оборудовании следующую команду:
- top -b -n1 -1 -p0 -w 400", "top -b -n1 -w 400
Сервис выполняет вышеуказанные операции на оборудовании, затем обрабатывает полученные результаты и возвращает ответ на grpc запрос в виде следующего объекта:
Тип DeviceCpuUtilisation:
-
device_identity:-
Тип параметра:
DeviceDataIdentity - Описание: Описание источника сбора данных.
-
Тип параметра:
-
unit_utilistaions:-
Тип параметра:
map<int32, CpuUnitUtilisation - Описание: Данные о статусе каждого из процессоров/ядер. Ключ - id процессора.
-
Тип параметра:
-
summary_utilisation:-
Тип параметра:
CpuUnitUtilisation - Описание: Суммарные метрики потребления CPU.
-
Тип параметра:
Тип CpuUnitUtilisation:
-
total_using:-
Тип параметра:
int32 - Описание: Общий процент использования процессора.
-
Тип параметра:
-
idle_time:-
Тип параметра:
int32 - Описание: Время простоя процессора, выраженное в процентах.
-
Тип параметра:
-
user_using:-
Тип параметра:
google.protobuf.Int32Value - Описание: Процент использования процессора пользовательскими процессами.
-
Тип параметра:
-
system_using:-
Тип параметра:
google.protobuf.Int32Value - Описание: Процент использования процессора системными процессами.
-
Тип параметра:
-
nice_value_using:-
Тип параметра:
google.protobuf.Int32Value - Описание: Процент времени, в течение которого CPU выполнял процессы, выставленные пользователем вручную (nice).
-
Тип параметра:
-
io_waiting:-
Тип параметра:
google.protobuf.Int32Value - Описание: Процент времени, потраченного на ожидание ввода-вывода.
-
Тип параметра:
-
hw_service_interrupts:-
Тип параметра:
google.protobuf.Int32Value - Описание: Процент времени, потраченного на обработку аппаратных прерываний.
-
Тип параметра:
-
soft_service_interrupts:-
Тип параметра:
google.protobuf.Int32Value - Описание: Процент времени, потраченного на обработку программных (системных) прерываний.
-
Тип параметра:
-
steal_time:-
Тип параметра:
google.protobuf.Int32Value - Описание: Процент времени, потраченного на выполнение задач в виртуальной машине (виртуализация).
-
Тип параметра:
Пример реализации
В качестве примера реализуем простейшее приложение, выполняющее одну системную операцию "Получить данные о нагрузке на CPU". Тестировать будем на виртуальной машине с операционной системой Ubuntu 12.3.0
Общий принцип работы микросервиса следующий:
- сервис получает grpc запрос
- вызывает соответствующие shell команды на оборудовании, данные которого пришли в запросе
- соответствующим образом обрабатывает
- возвращает результаты обратно
Порядок действий
- Создайте проект из шаблона "Служба ASP.NET Core gRPC". Это будет базовый каркас проекта.
- Прото-файлы из шаблона не годятся т.к. не соответствуют кронтрактам EMS. Нужно заменить из на правильные. Удалите демонстрационные файлы из директории Protos и скопируйте туда proto файлы из директории project\SshExample\SshExample\Protos. Это уже подготовленные grpc объекты, описанные выше
- В директории service создайте файл SshCommandCaller.cs. В нем будет находиться логика обработки grpc-запроса и отправка ssh-запроса целевому оборудованию.
- Теперь нам нужны команды, с помощью которых можно получить данные нагрузке на CPU. В Linux под эти цели подходит команда TOP.
Добавьте в этот файл следующие константы:
private static readonly string[] _commands = { "top -b -n1 -1 -p0 -w 400", "top -b -n1 -w 400" };
private static string TopCpuPrefix = "%cpu";
_commands - список shell команд, которые будет запрашивать сервис у оборудования. TopCpuPrefix - префикс для более удобной обработки результатов
- Для более удобной передачи данных между методом вызова команд и методом обработки добавим record HandleResult. В нем будут храниться код возврата вызова и его результаты.
Добавьте в SshCommandCaller.cs следующий record
public record HandleResult(int Success, string Stdout, string Stderr);
- Следующим этапом нужен метод, который будет непосредственно вызывать shell команды на оборудовании. Суть метода, получить из объекта CollectLinuxCpuUtilisationRequest креды подключения к оборудовани, подключитсья к нему и выполнить shell команду. Можете написать свой метод, можете взять из примера. Метод в примере не учитывает случай, что пользователь может не передать данных для подсоединения или у устройства их может быть несколько. Этот метод просто берет первые возможные данные для подсоединения, подключается к устройству, выполняет команду и собирает объект HandleResult.
public static HandleResult CallSsh(CollectLinuxCpuUtilisationRequest request, string command)
{
DeviceConnector connection = request.Device.Connectors.First();
Credential creds=connection.Credentials.First();
using (SshClient client = new SshClient(connection.Address, creds.Port, creds.Login, creds.Password))
{
client.Connect();
SshCommand cmdRes = client.RunCommand(command);
if (cmdRes.ExitStatus == 0)
{
client.Disconnect();
return new HandleResult(cmdRes.ExitStatus,
cmdRes.Result, cmdRes.Error);
}
client.Disconnect();
return new HandleResult(cmdRes.ExitStatus,
cmdRes.Result, cmdRes.Error);
}
}
- Далее полученные данные нужно обработать. Этот метод должен заниматься анализом полученных в результате выполнения команд данных и сборкой ответа для пользователя.
Резельтатами выполнения двух команд top будут примерно следующие данные (разнятся от количества процессоров в машине и их нагруженности)
top - 17:55:32 up 1 min, 1 user, load average: 0.01, 0.01, 0.00
Tasks: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
...
%Cpu19 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 7806.4 total, 6860.3 free, 509.2 used, 436.8 buff/cache
MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 7067.7 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
697 vvsurje+ 20 0 7656 3236 2872 R 0.0 0.0 0:00.00 top
top - 17:55:52 up 2 min, 1 user, load average: 0.01, 0.00, 0.00
Tasks: 35 total, 1 running, 34 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 7806.4 total, 6858.4 free, 511.2 used, 436.9 buff/cache
MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 7065.8 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
690 root 20 0 43696 37724 9956 S 13.3 0.5 0:00.75 python3
1 root 20 0 166004 11392 8352 S 0.0 0.1 0:00.21 systemd
...
Ценность для текущего случая представляют только строки с префиксом %Cpu.
Каждая строчка - это данные одного процессора. %Cpu(s) - общие, сводные, данные по всем процессорам.
По каждому процессору представлены данные в виде групп, разбитых символом ','. Каждая группа состоит из числа - значения,
и строкового идентификатора. Например "0.0 us" - значит, что пользователем в данный момент процессор не загружен.
Логика работы метода следующая. Нужно объединить все данные в одну строку. Разбить ее на подстроки по по символу '\n' (перевод строки).
Отфильтровывать всё лишнее, оставляя только строки, начинающиеся с %Cpu.
После этого обработать каждую строку и собрать объекты с данными по каждому процессору.
Вот пример реализации такого метода.
public static CollectLinuxCpuUtilizationResponse ProcessResponse(HandleResult[] results, CollectLinuxCpuUtilizationRequest request)
{
string response = "";
foreach (HandleResult res in results)
{
if (res.Success == 0)
{
response += res.Stdout;
}
}
CollectLinuxCpuUtilizationResponse statDeviceCpu = new()
{
CpuUtilization = new()
{
DeviceIdentity = new DeviceDataIdentity()
{
DeviceId = request.Device.DeviceId,
Source = ServiceSource.LinuxManager
},
SummaryUtilization = new()
}
};
IEnumerable<string> rows = response.ToLowerInvariant()
.Split('\n', StringSplitOptions.TrimEntries)
.Where(s => s.StartsWith(TopCpuPrefix))
.SelectMany(c => c
.Split(TopCpuPrefix, StringSplitOptions.TrimEntries)
.Where(d => !string.IsNullOrWhiteSpace(d))
.Select(s => TopCpuPrefix + s));
foreach (string item in rows)
{
int processorIdEnd = item.IndexOf(' ', TopCpuPrefix.Length);
if (processorIdEnd == -1)
{
continue;
}
string? strProcessorId = item[TopCpuPrefix.Length..processorIdEnd];
int processorId;
if (strProcessorId == "(s):")
{
processorId = -1;
}
else if (!int.TryParse(strProcessorId, out processorId))
{
continue;
}
CpuUnitUtilization cpu = new();
foreach (string entiry in item[(processorIdEnd + 3)..].Split(',', StringSplitOptions.TrimEntries))
{
string[] parts = entiry.Split(' ', 2, StringSplitOptions.TrimEntries);
if (parts.Length != 2)
{
continue;
}
string key = parts[1];
string strValue = parts[0];
if (float.TryParse(strValue, out float intValue))
{
switch (key)
{
case "us": cpu.UserUsing = (int)Math.Ceiling(intValue); break;
case "sy": cpu.SystemUsing = (int)Math.Ceiling(intValue); break;
case "ni": cpu.NiceValueUsing = (int)Math.Ceiling(intValue); break;
case "id": cpu.IdleTime = (int)Math.Ceiling(intValue); break;
case "wa": cpu.IoWaiting = (int)Math.Ceiling(intValue); break;
case "hi": cpu.HwServiceInterrupts = (int)Math.Ceiling(intValue); break;
case "si": cpu.SoftServiceInterrupts = (int)Math.Ceiling(intValue); break;
case "st": cpu.StealTime = (int)Math.Ceiling(intValue); break;
default: break;
}
}
}
if (processorId == -1)
{
statDeviceCpu.CpuUtilization.SummaryUtilization = cpu;
}
else
{
statDeviceCpu.CpuUtilization.UnitUtilistaions.Add(processorId, cpu);
}
}
return statDeviceCpu;
}
- Далее необходимо связать предыдущие методы одним управляющим методом, который соберет список команд и для каждой из них вызовет обработку в методе
CallSsh, а затем передаст результаты в методProcessResponse. Ниже представлен пример реализации данного метода:
public static CollectLinuxCpuUtilisationResponse GetCpuUtilisation(CollectLinuxCpuUtilisationRequest request)
{
HandleResult[] responses= _commands.Select(cmd=> CallSsh(request,cmd)).ToArray();
return ProcessResponse(responses, request);
}
- Далее необходимо реализовать GRPC сервис из прото-файлов, которые были добавлены в проект ранее.
В файле
GreeterService.cs(был создан из шаблона) замените все содержимое на следующее:
namespace SshExample.Services;
public class LinuxManagerService : LinuxManager.LinuxManagerBase
{
public override Task<CollectLinuxCpuUtilisationResponse> CollectCpuUtilisation(CollectLinuxCpuUtilisationRequest request, ServerCallContext context)
{
return Task.FromResult(SshCommandCaller.GetCpuUtilisation(request));
}
}
- После сборки и запуска приложения, проверьте результат работы. Для этого вызовите метод
CollectCpuUtilisation, например, в Postman.