Как мониторить энергопотребление ЦОД-а?

в 23:20, , рубрики: Песочница, метки: , ,

Мысль — это инструмент, с помощью которого человек создает выбор.
Айн Рэнд

— Нам нужно реализовать систему мониторинга и оповещения энергопотребления нашего ЦОД-а по каждой входной фазе, со всеми плюшками — звонками или смс-ками ответственным людям и историей событий.
— Какой бюджет?
— Как всегда — чем меньше, тем лучше.
— У нас есть *VendorName*, нужно лишь докупить для него датчиков. Но стоимость каждой опции начинается от 100$ и платформа закрытая.
— А может быть попробуем Arduino?
— Что это? Хотя… Сколько времени нужно на реализацию и сколько это стоит?
Таким был диалог трех инженеров одним рабочим днем.
Я не программист и не специалист по микроэлектронике, но меня всегда привлекали новые технологии, какими бы они ни были.
Осторожно! Очень много картинок.

— Что делать?
Что делать?
Я же не умею… (вставить любую упущенную возможность в жизни).
— А что ты умеешь?
— Вот… И вот это… И еще немного того.
— Ты родился с этими умениями?
— Нет, меня научили…

Эта инструкция для тех, кто очень хотел бы попробовать, но боится и не знает, с чего начать.

Шаг 1. Купить любую платформу, которая вам пришлась по душе (небольшой совет: не забывайте, что вам жизненно необходим программатор либо USB на борту для заливки ваших скетчей).
Мой выбор пал на Freaduino UNO V1.8 (ATmega 328), потому что в моем родном городе есть всего один магазин, который работает с крупными организациями по безналичному расчету.
Так же мне потребовался Ethernet shield для подключения к сети.
Так же мне повезло найти уже готовый Shield для подключения 3-х датчиков переменного тока с LCD дисплеем и беспроводным интерфейсом.
Ну и сами сенсоры.

Заказать, дождаться курьера, угостить его шоколадкой, нетерпеливо заняться анпакингом.

Шаг 2. Установите Arduino IDE для своей OS, скачав с arduino.cc/en/Main/Software

Шаг 3. Подключаем Arduino к компьютеру и настраиваем софт. Выбираем модель нашей платы и порт подключения (он появляется ПОСЛЕ подключения).

Скрины

Как мониторить энергопотребление ЦОД а?

Как мониторить энергопотребление ЦОД а?

Заливаем скетч в Arduino.

Код

#include <SPI.h>
#include "EmonLib.h" 
#include <LCD5110_Graph_SPI.h>
#include <Ethernet.h>  
#include <Streaming.h>   
#include <Flash.h>

byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xEE };       // MAC адрес устройства проверьте, что такой не существует в вашей сети
byte ip[] = { 10, 10, xx, y1 };                            // IP адрес устройства
byte subnet[] = {255, 255, 255, 0};                        // Маска сети
byte gateway[] = {10, 10, xx, y2};                         // Шлюз по умолчанию 
EthernetServer server(80);                                 // Веб сервер
EthernetClient http_client;                                // Веб клиент
char serverName[] = "10.10.xx.y3";                         // Адрес звонилки 

LCD5110 myGLCD(5,6,3);                                     // Инициализация LCD
extern uint8_t SmallFont[];

EnergyMonitor emon1;
EnergyMonitor emon2;
EnergyMonitor emon3;

int msec = 0;
int msecP1 = 0;
int msecP2 = 0;
int msecP3 = 0;
int LevelAlarm = 300;
boolean Alarm1 = false;      // Начальные значения при включении 
boolean Alarm2 = false;
boolean Alarm3 = false;
boolean Start1 = true;       // Флаг включения

double Irms1;
double Irms2;
double Irms3;

double Delta1=0;
double Delta2=0;
double Delta3=0;

double Pcur1;
double Pcur2;
double Pcur3;

double Kphase = 0.220;       // Если у вас 380 - поменяйте значение
double PAlarm = 0.05;        // Критичное значение после которого включаем тревогу

char buf[15];
char tbuf[5];
char buffer[50];
String AMsg="";
String MMsg="";


void setup() {
  Serial.begin(9600);
  Serial.println("Starting init...");
  Ethernet.begin(mac, ip, gateway, subnet);
  
  myGLCD.InitLCD(70);
  myGLCD.setFont(SmallFont);
  myGLCD.clrScr();
  
  emon1.current(0, 111.1);        //инициализация
  emon2.current(1, 111.1);
  emon3.current(2, 111.1);
  
  myGLCD.clrScr();
  myGLCD.print("Energy Monitor", 0, 0);
  myGLCD.print(" calibrating", 0, 10);
  myGLCD.print("unplug sensors", 0, 20);
  //myGLCD.print("12345678901234", 0, 30); 14 символов в строке дисплея
  myGLCD.update();
  
  double Irms1 = emon1.calcIrms(1480);    // первые значения снятые с сенсоров - ооочень отличаются... не будем их учитывать в дельтах
  double Irms2 = emon2.calcIrms(1480);
  double Irms3 = emon3.calcIrms(1480);
  double cIrms;

  
  #define WINc 20                        // количество пробных попыток для калибровки
  double Irms[WINc];
  
  cIrms = 0;
  for (int i=0; i<WINc; i++) {
    Irms[i] = emon1.calcIrms(1480);
    cIrms = cIrms + Irms[i];
    delay(1);
  }
  Delta1 = cIrms/WINc;
  
  cIrms = 0;
  for (int i=0; i<WINc; i++) {
    Irms[i] = emon2.calcIrms(1480);
    cIrms = cIrms + Irms[i];
    delay(1);
  }
  Delta2 = cIrms/WINc;

  cIrms = 0;
  for (int i=0; i<WINc; i++) {
    Irms[i] = emon3.calcIrms(1480);
    cIrms = cIrms + Irms[i];
    delay(1);
  }
  Delta3 = cIrms/WINc;

  Serial.println(Delta1);                    // выведем значения в консоль
  Serial.println(Delta2);
  Serial.println(Delta3);
  
  char D11[10]; dtostrf(Delta1, 2, 10, D11);
  char D12[10]; dtostrf(Delta2, 2, 10, D12);
  char D13[10]; dtostrf(Delta3, 2, 10, D13);
  
  myGLCD.clrScr();                          // выведем значения на LCD 
  myGLCD.print("D1: ", 0, 20);
  myGLCD.print(D11, 20, 20);
  myGLCD.print("D2: ", 0, 10);
  myGLCD.print(D12, 20, 10);
  myGLCD.print("D3: ", 0, 0);
  myGLCD.print(D13, 20, 0);
  myGLCD.print(" wait 5 sec. ", 0, 30);
  myGLCD.update();
  delay(5000);
  myGLCD.clrScr();
  myGLCD.update(); 
} 

void loop() {                                    // сам цикл
  Irms1 = emon1.calcIrms(1480) - Delta1; Irms1 = abs(Irms1); Pcur1 = Irms1*Kphase;
  Irms2 = emon2.calcIrms(1480) - Delta2; Irms2 = abs(Irms2); Pcur2 = Irms2*Kphase;
  Irms3 = emon3.calcIrms(1480) - Delta3; Irms3 = abs(Irms3); Pcur3 = Irms3*Kphase;

  Serial.println();                             // выводим значения в консоль
  Serial.print(" I1:"); Serial.print(Irms1); Serial.print(" P1:"); Serial.print(Pcur1); Serial.println();
  Serial.print(" I2:"); Serial.print(Irms2); Serial.print(" P2:"); Serial.print(Pcur2); Serial.println();
  Serial.print(" I3:"); Serial.print(Irms3); Serial.print(" P3:"); Serial.print(Pcur3); Serial.println();
  
  myGLCD.print("Energy Monitor", 0, 0);         // Выводим значения на LCD 
  myGLCD.update(); 
  dtostrf(Pcur1,4,2,tbuf); sprintf(buf, "P1: %s kVt", tbuf); myGLCD.print(buf, 0, 30);
  dtostrf(Pcur2,4,2,tbuf); sprintf(buf, "P2: %s kVt", tbuf); myGLCD.print(buf, 0, 20);
  dtostrf(Pcur3,4,2,tbuf); sprintf(buf, "P3: %s kVt", tbuf); myGLCD.print(buf, 0, 10);
  myGLCD.print(AMsg,0,40);
  myGLCD.update();
  
  EthernetClient client = server.available();      // вдруг кто-то постучался по 80 порту! - формируем и отдаем страничку
  if (client) {
    Serial.println(">>>>> HTTP clent connect...");
    boolean current_line_is_blank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        if (c == 'n' && current_line_is_blank) {
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html");
          client.println("Connection: close");
          client.println();
          client.print("Pcur1="); client.print(Pcur1); client.print("<br>");
          client.print("Pcur2="); client.print(Pcur2); client.print("<br>");
          client.print("Pcur3="); client.print(Pcur3); client.print("<br>");
          client.println();
          client.print(" Delta1:"); client.print(Delta1);
          client.print(" Delta2:"); client.print(Delta2);
          client.print(" Delta3:"); client.print(Delta3);
          client.println();
          break;
        }
        if (c == 'n') {
          current_line_is_blank = true;
        }
        else if (c != 'r') {
          current_line_is_blank = false;
        }
      }
    }
    delay(10);
    client.stop();
  }
  
 
 if ( (Start1)&&(msec >= 100) ) {    // Проверяем сколько времени прошло с момента включения
   Start1 = false;
 }
 
 if ( !Start1) {
  MMsg = ""; 
  isAlarmP1();
  isAlarmP2();
  isAlarmP3();
  AMsg = "";
  AMsg += MMsg;
  AMsg += " alarm";
  Serial.println(AMsg); 
 }
 else {
  itoa(100-msec,buf,10);
  //Serial.println(buf);
  //Serial.println("   <->");
  MMsg = buf;
  AMsg = "";
  AMsg += MMsg;
  AMsg += "   ";
  
  //AMsg += " ";
  Serial.println(AMsg); 
 }
    
  if ( msec <= 32766) {
    msec++;
  }
  else {
    msec = 0;
  }
  

  // again
}

void postPage (char *webPage) {                  // Аларм, аларм, аларм! Надо дернуть пейджу!
  if (http_client.connect(serverName,80)>0) {
    http_client.print("GET ");
    http_client.print(webPage);
    http_client.print(" HTTP/1.0");
    http_client.println("User-Agent: Arduino 1.0");
    http_client.println();
    Serial.println(" >> GET URL");
  }
  http_client.stop();
  http_client.flush();
}

void isAlarmP1 () {
  // Сработал первый аларм
  if ( (Pcur1 < PAlarm) ) {
    if ( Alarm1 != true ) {
      postPage("/alarm?q=P1");
      msecP1 = msec;
    }
    else {
      if ( abs(msec - msecP1) >= LevelAlarm ) { // дополнительная ветка повтора аларма
        Serial.println(" >> Repeat alarm P1");
      //  postPage("/alarm?q=P1");
        msecP1 = msec;
      }
    }
    Alarm1 = true;
    MMsg += "P1";
  }
  else {
    Alarm1 = false;
    MMsg += "  ";
    msecP1 = 0;
  }
}

void isAlarmP2 () {
  // Сработал второй аларм
  if ( (Pcur2 < PAlarm) ) {
    if ( Alarm2 != true ) {
      postPage("/alarm?q=P2");
      msecP2 = msec;
    }
    else {
      if ( abs(msec - msecP2) >= LevelAlarm ) { // дополнительная ветка повтора аларма
        Serial.println(" >> Repeat alarm P2");
      //  postPage("/alarm?q=P2");
        msecP2 = msec;
      }
    }
    Alarm2 = true;
    MMsg += "P2";
  }
  else {
    Alarm2 = false;
    MMsg += "  ";
    msecP2 = 0;
  }
}

void isAlarmP3 () {
  // Сработал третий аларм
  if ( (Pcur3 < PAlarm) ) {
    if ( Alarm3 != true ) {
      postPage("/alarm?q=P3");
      msecP3 = msec;
    }
    else {
      if ( abs(msec - msecP3) >= LevelAlarm ) { // дополнительная ветка повтора аларма
        Serial.println(" >> Repeat alarm P3");
      //  postPage("/alarm?q=P3");
        msecP3 = msec;
      }
    }
    Alarm3 = true;
    MMsg += "P3";
  }
  else {
    Alarm3 = false;
    MMsg += "  ";
    msecP3 = 0;
  }
}

Как мониторить энергопотребление ЦОД а?

Один из важных вопросов — как это мониторить?
В моей компании используется Zabbix. Давайте его настроим.
Нам нужно выдергивать значения всех трех датчиков с некоторой периодичностью. К счастью, в документации нашлось объяснение как это делать.
Открываем файл настроек /usr/local/etc/zabbix_agentd.conf и создаем пользовательский параметр:

UserParameter=scb.Pcur[*],/usr/local/share/zabbix/externalscripts/get_energy_power.sh "$1" "$2"

UserParameter=(ПридуманноеНазваниеПараметра[входные параметры]), путь до внешнего скрипта $передаваемые параметры.

Теперь внутренности самого скрипта:

Скрипт
#!/bin/bash

if [ ! -f /tmp/html ]
then
    /usr/bin/wget -q --proxy=off --connect-timeout=2 --tries=1 -O /tmp/html http://$1/html {} ; 2>/dev/null
fi
/usr/bin/find /tmp -name html -type f -cmin +1 -exec /usr/bin/wget -q --proxy=off --connect-timeout=2 --tries=1 -O /tmp/html http://$1/html {} ; 2>/dev/null
sleep 1
if [ $2 = 1 ];
then
    if [ "$(cat /tmp/html | awk -F <br> '{print $1}' | awk -F = '{print $2}' | tr -d '#' | tr -d 'n' | tr -d 'r')" = '' ]
    then
        echo "-1"
    else
        cat /tmp/html | awk -F <br> '{print $1}' | awk -F = '{print $2}' | tr -d '#' | tr -d 'n' | tr -d 'r'
    fi
fi
if [ $2 = 2 ];
then
    if [ "$(cat /tmp/html | awk -F <br> '{print $2}' | awk -F = '{print $2}' | tr -d '#' | tr -d 'n' | tr -d 'r')" = '' ]
    then
        echo "-1"
    else
        cat /tmp/html | awk -F <br> '{print $2}' | awk -F = '{print $2}' | tr -d '#' | tr -d 'n' | tr -d 'r'
    fi
fi
if [ $2 = 3 ];
then
    if [ "$(cat /tmp/html | awk -F <br> '{print $3}' | awk -F = '{print $2}' | tr -d '#' | tr -d 'n' | tr -d 'r')" = '' ]
    then
        echo "-1"
    else
        cat /tmp/html | awk -F <br> '{print $3}' | awk -F = '{print $2}' | tr -d '#' | tr -d 'n' | tr -d 'r'
    fi
fi

Краткое пояснение:

Проверяем существование файла /tmp/html (это копия нашей странички с результатами измерений), если не существует — дергаем wget-ом с адреса из первого входящего параметра. Если существует, но он старше 1-й минуты — дергаем wget-ом новую версию и сохраняем в /tmp/html. Далее в зависимости от второго входного параметра выбираем, какое из значений вывести на экран. Если по какой-то причине мы не смогли получить данные — выводим -1 (на всякий случай).

Проверяем работу скрипта:

user@host:~/temp> /usr/local/share/zabbix/externalscripts/get_energy_power.sh energy_power 1
0.02
user@host:~/temp> /usr/local/share/zabbix/externalscripts/get_energy_power.sh energy_power 2
0.09
user@host:~/temp> /usr/local/share/zabbix/externalscripts/get_energy_power.sh energy_power 3
0.01

Все готово, проверяем, получает ли zabbix необходимые значения:

user@host:~/temp> zabbix_get -s localhost -k scb.Pcur[energy_power,1]
0.02
user@host:~/temp> zabbix_get -s localhost -k scb.Pcur[energy_power,2]
0.09
user@host:~/temp> zabbix_get -s localhost -k scb.Pcur[energy_power,3]
0.01

Скрин

Как мониторить энергопотребление ЦОД а?

Отлично!
Теперь добавим график.
Configurations -> Hosts -> New Host -> Name -> energy_power (DNS имя нашего устройства) -> Save

Скрин

Как мониторить энергопотребление ЦОД а?

Создадим Items.
Configurations -> Hosts -> energy_power -> Items -> New Item

Скрин

Как мониторить энергопотребление ЦОД а?

Теперь добавим график.
Configurations -> Hosts -> energy_power -> Graphs -> New graph -> добавляем все три параметра, задаем цвета -> Save

Скрин

Как мониторить энергопотребление ЦОД а?

Вот и готов график, нужно подождать нексолько минут и появятся данные.

Как мониторить энергопотребление ЦОД а?

Логика работы устройства:
Подключаем устройство к сети Ethernet (датчики должны «висеть в воздухе», иначе у вас будут огромные дельты).
Подаем питание, оживляя этого Франкенштейна.

Фото

Как мониторить энергопотребление ЦОД а?

Ждем пока на LCD не появятся результаты калибровки — значения дельт.

Фото

Как мониторить энергопотребление ЦОД а?

После этого у вас есть примерно 100 секунд, в течении которых нужно повешать сенсоры на фазы. На LCD в это время выводится обратный отчет как в настоящих боевиках с Брюсом Виллисом.

Фото

Как мониторить энергопотребление ЦОД а?

Когда обратный отчет закончен, устройство переходит в «боевой» режим.
Как только на одном из сенсоров произойдет падение ниже критичной отметки (0.05), устройство веб клиентом дергает URL вида 10.10.xx.y3/alarm?q=PX и она начинает планомерный обзвон всех заинтересованных лиц (к сожалению, не могу рассказать больше, потому что эта звонилка находится вне моей компетенции… я даже не знаю что это за устройство). URL дергается один раз за одну зафиксированную аварию. Если фаза появится, то флаг аварии сбросится и при последующем падении напряжения — URL дернется еще раз. В скетче закоментирована возможность дергать URL повторно через 300 циклов, если авария продолжается.
Дополнительный канал оповещения — СМС с помощью zabbix (на эту тему написано очень много отличных статей и в данной статье не рассматривается).

Фото

Как мониторить энергопотребление ЦОД а?

Для отладки был использован обычный китайский пилот из которого вывели один из проводов и подключили монитор для калибровки.

Фото

Как мониторить энергопотребление ЦОД а?

Итоговый бюджет:
Freaduino UNO V1.8 (ATmega 328) 799 р.
ETHERNET SHIELD V1.0 (WIZNET W5100) 739 р.
ENERGY MONITOR Shield 969 р.
ДАТЧИК ПЕРЕМЕННОГО ТОКА 100А 469 р. (нужны 3 штуки)
Итого: 3914 р.
К сожалению, внедрение затянулось, в нашем городе почему-то не продают SCT-019-000 (датчики на 200А), а эти (SCT-013) просто не подходят по сечению на наши провода… Да и по амперам не проходят. Заказать из Китая не позволяет бухгалтерия. Может быть, есть люди, готовые помочь?

Еще немного фотографий с сайта поставщика

Как мониторить энергопотребление ЦОД а?

Как мониторить энергопотребление ЦОД а?

Как мониторить энергопотребление ЦОД а?

Как мониторить энергопотребление ЦОД а?

Данная статья посвящается великолепному программисту — Геле Высоцкой.


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js