Мы тоже не любим софт, который неизвестно как работает. Если программа ― черный ящик, при каждой непонятной ситуации остается ровно два варианта: попробовать приложить подорожник или обратиться к производителю. Но, во-первых, мы не знаем, где растет столько подорожника, а, во-вторых, обращаться по разным мелочам к вендору тоже как-то не ацаца.
Несколько лет назад, у одного из наших заказчиков нашелся ровно такой софт: на входе запросы, на выходе ответы, а внутри непонятно что. Называется автоматизированная банковская система или АБС. Но по сути это база данных, которая в общем случае обрабатывает запросы из процессинга и возвращает результат. В результате содержится ответ: может банк провести эту операцию или нет. Подступиться к этой базе данных с внешним запросом нет никакой возможности в силу ее архитектурных ограничений. В одном из недавних проектов снова столкнулись с таким же ПО в ритейле и подумали, что пора бы уже поделиться своими наработками.
В прошлой статье мы рассказывали, как прослушивание трафика помогло одному из клиентов Техносерва отловить внутренние проблемы софта. В этой будет технический разбор той давней задачи (руки-то помнят), а в конце — два важных бенифита такого вида мониторинга.
Специальный продукт для анализа трафика MicroFocus RUM (подробнее о нем в предыдущей статье по ссылке выше), конечно, поддерживает разнообразные сетевые протоколы, но вот именно те, что нам нужны ― нет. А нужно было прослушать служебные RPC-запросы от внутрибанковских систем к АБС и трафик по протоколу ISO8583 от внешних устройств (банкоматов и POS-терминалов) всё к той же АБС.
Анализируем данные по протоколу RPC
Постановка задачи от заказчика выглядела примерно так:
«Парни, вот вам трафик за несколько часов для анализа. Тут у меня от разных банковских систем к АБС бегают транзакции в виде запросов-ответов. Нужно знать время выполнения каждого такого запроса, получать значения некоторых полей и считать количество ошибок. На выходе нужен отчет в табличном виде».
Самым сложным в этой задаче был анализ полученного трафика. Поиск последовательностей и разделение потока на отдельные транзакции потребовало кропотливого труда. После анализа выяснилось, что каждая транзакция это XML-пакет с одинаковым идентификатором для запроса и ответа, а также набором других полей. На скриншоте пример этого трафика в интерфейсе Wireshark и соответствующий ему XML.
Чтобы RUM научился понимать различные типы сетевых данных, нужно эти данные ему описать. Для этого мы разработали скрипт на C++. Ниже код для разбора RPC-трафика.
#include "./ProtocolRPCRQ.hpp"
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include <iostream>
#include <fstream>
#include <base/ProtocolConfig.hpp>
#include <base/ProcessingState.hpp>
#include <utils/LogHelper.hpp>
using namespace rum::public_sdk;
sigjmp_buf mark;
void segfault_sigaction(int signal) {
;
}
RPCRQProcessor::RPCRQProcessor( const char* name, IProtocolProcessor* next )
: ProtocolProcessorBase( name, next, "protocols.RPCRQProcessor" ) {
}
void RPCRQProcessor::initialize( const ProtocolConfig& config ) {
;
}
/*override*/
void RPCRQProcessor::process( ProtocolEvent& event, ProcessingState& ps, ProcessingMode mode ) {
bool isParsed;
try {
if (mode != PROCESSING_FULL ) {
forwardToNextProcessor( event, ps, mode);
return;
}
EventParsingContext eventContext( event );
isParsed = doAllParsingWork( ps.getRequest(), ps.getResponse(), eventContext);
if (isParsed) {
forwardToNextProcessor(event, ps, mode);
}
else return;
}
catch (...) {
LOG4PROBE_ERROR( getDefaultLogger(), "logger1");
}
}
String RPCRQProcessor::search_substring(String data, String start, String stop) {
String empty = "";
try {
size_t start_pos = data.find(start);
if (start_pos!=std::string::npos)
data = data.substr(start_pos+start.length());
size_t stop_pos = data.find(stop);
if (stop_pos!=std::string::npos)
data = data.substr(0,stop_pos);
if (data.find("xmlns")!=std::string::npos) return empty;
if ((start_pos==std::string::npos) || (stop_pos==std::string::npos)) return empty;
else return data;
}
catch (...) {return empty;}
}
String RPCRQProcessor::stream_to_string(DataStream& stream,int offset) {
String rumstring = "";
String empty = "";
int i = 0;
try {
while ( (!stream.eof()) && (i<10000)) {
i++;
if (stream.peek()!=0xff) {
unsigned char value = stream.get();
if ((value!=0) && (value!=0xff)) rumstring += value;
}
else {
stream.skip(1);
}
}
}
catch (...) {return empty;}
if (rumstring.length()>offset) return rumstring.substr(offset);
else return empty;
}
bool RPCRQProcessor::doAllParsingWork(
DataStream& request,
DataStream& response,
EventParsingContext &context )
{
try {
rum::public_sdk::ActionInfo iactionInfo = context.getActionInfo();
String schema = " xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">";
String strdata = stream_to_string(request,45);
String message = search_substring(strdata,"<message type="RPC_","</message>");
if (message!="") {
String msgType = search_substring(strdata,"<message type="","">");
String origMsgID = search_substring(message,"<origMsgID>","</origMsgID>");
if (origMsgID=="") origMsgID = search_substring(message,"<origMsgID"+schema,"</origMsgID>");
String origSysID = search_substring(message,"<origSysID>","</origSysID>");
if (origSysID=="") origSysID = search_substring(message,"<origSysID"+schema,"</origSysID>");
String targSysID = search_substring(message,"<targSysID>","</targSysID>");
if (targSysID=="") targSysID = search_substring(message,"<targSysID"+schema,"</targSysID>");
String origPrcID = search_substring(message,"<origPrcID>","</origPrcID>");
if (origPrcID=="") origPrcID = search_substring(message,"<origPrcID"+schema,"</origPrcID>");
String timeStamp = search_substring(message,"<timeStamp>","</timeStamp>");
if (timeStamp=="") timeStamp = search_substring(message,"<timeStamp"+schema,"</timeStamp>");
String CmdName = search_substring(message,"<command name="","">");
if (CmdName=="") CmdName = "PARSING_ERROR";
iactionInfo.addKeyValue("msgType" , msgType , false );
iactionInfo.addKeyValue("origMsgID", origMsgID, false );
iactionInfo.addKeyValue("origSysID", origSysID, false );
iactionInfo.addKeyValue("targSysID", targSysID, false );
iactionInfo.addKeyValue("origPrcID", origPrcID, false );
iactionInfo.addKeyValue("timeStamp", timeStamp, false );
iactionInfo.addKeyValue("CmdName" , CmdName, false );
iactionInfo.addKeyValue("x-action-descriptor" , CmdName, false );
iactionInfo.setDescriptor(CmdName);
String respstrdata = stream_to_string(response,45);
String resmessage = search_substring(respstrdata,"<message type="RPC_","</message>");
if (resmessage!="") {
String retCode = search_substring(resmessage,"<retCode>","</retCode>");
if (retCode!="") {
iactionInfo.addKeyValue("retCode" , retCode, false );
if (retCode.find("OK") != std::string::npos) {
context.getEvent().setStatusCode(0);
}
else context.getEvent().setStatusCode(1);
}
else {
iactionInfo.addKeyValue("retCode" , "NOTFOUND", false );
context.getEvent().setStatusCode(2);
}
}
}
if (message=="") return false;
else return true;
}
catch (...) {
LOG4PROBE_ERROR( getDefaultLogger(), "exception in RPCRQProcessor" );
context.getEvent().setStatusCode(3);
}
}
//////////////////////////////////////////////////////////////
// class MyProtocolParser::EventParsingContext
//////////////////////////////////////////////////////////////
RPCRQProcessor::EventParsingContext::EventParsingContext(
ProtocolEvent& event )
: _event(event)
{
try {
_connectionContext = _event.getSessionContext();
if ( NULL == _connectionContext.get() ) {
_connectionContext = new ConnectionInfo();
_event.setSessionContext( _connectionContext.get() );
}
}
catch (...) {
std::ofstream fileSTRINGTEST;
fileSTRINGTEST.open("/home/rum/fileSTRINGTEST_error2.out",std::ios::out|std::ios::app|std::ios::binary);
fileSTRINGTEST << "--------------------------------n";
fileSTRINGTEST.close();
}
}
// Declaring Factory for creating processor
RUM_PROBE_EXPORT_PROCESSOR( RPCRQProcessor, getRPCRQProcessor);
На выходе получился такой элегантный отчет:
Заказчик доволен, а мы получили новые навыки в продукте.
Анализируем данные по протоколу ISO8583
Задача аналогична предыдущей с небольшими изменениями:
«Парни, вот вам новая порция трафика. У меня есть агрегатор, который собирает транзакции с моих банкоматов и POS-терминалов. Все транзакции летят в АБС. Хочу знать — сколько у меня было таких транзакций, их статус и время выполнения каждого запроса. На выходе нужен отчет в виде графика».
Как и в предыдущей задаче, пакеты сетевого трафика представляли из себя XML. Только со значительно большим количеством полей. С помощью нового кода мы отсеяли все ненужное и получили примерно такой же XML для анализа и подсчета транзакций как в случае с RPC.
На выходе заказчик получил график с количеством транзакций. На скриншоте ниже отображен недельный интервал. Теперь любое аномальное поведение этого графика трактуется системой как сбой. Мы настроили корреляцию событий с другими приложениями бизнес-процесса, и администраторы могут сразу увидеть возможную причину отклонения от нормы. Разумеется, в разные дни поведение графика может отличаться. Например, в субботу и воскресенье нет проседаний в дневное время по количеству транзакций ― это норма для выходных дней и событие не генерируется. Вот такой искусственный интеллект.
За время работы с системами мониторинга пользовательского трафика мы выявили два важных преимущества (их может и больше, но пока остановились на двух):
- Не создается дополнительная нагрузка на бизнес-приложение. Нет нужды встраивать в приложение специальные агенты, просить разрешения у админов чего-то там перезагрузить и вообще беспокоить людей по пустякам.
- Возможность контролировать «черные ящики». Таких приложений много и не всегда понятно как за ними следить. Пример ― наша ситуация с АБС.
Приглашаем пройти опрос, поделиться в комментариях примерами приложений-черных ящиков и рассказать, как вы их контролируете.
Автор статьи Антон Касимов, архитектор систем мониторинга, компания «Техносерв».
Автор: TS_IT_Management