Досвід автоматизації тестування стабільності роботи сервера RTLS під внутрішньою навантаженням

Введення.
У даній статті я розповім про те, як у відділі контролю якості компанії RTL-Service відбувається автоматизоване тестування стабільності сервера RealTrac при одночасному обслуговуванні великої кількості мобільних локаційних пристроїв. Для подальшого розуміння, пропоную ознайомитися з корисною термінологією:
RTLS-сервер RealTrac (сервер) — серверне програмне забезпечення системи RealTrac, здійснює взаємодію з апаратними засобами системи і розрахунок місця розташування пристроїв.

Сервер додатків RealTrac (сервер додатків) — серверне програмне забезпечення, необхідне для роботи web-додатки, надає програмний інтерфейс доступу до основних функцій системи.

Точка доступу RealTrac (далі ТД) — пристрій, призначений для передачі даних між мобільними пристроями мережі і сервером системи. Точки доступу стаціонарно встановлюються на об'єкті, їх координати заносяться на карту клієнтського програмного забезпечення і фіксуються в базі даних на сервері системи. ТД може працювати в режимі шлюзу або ретранслятора. Режим визначається наявністю проводового Ethernet підключення до мережі (шлюз точка доступу, ШТД) і відсутністю такого (ретранслятор точка доступу, РТД). Обмін даними з сервером здійснюють тільки шлюз. Приклад точки доступу представлений на рисунке1.

Рис. 1. Приклад точки доступу.

Мобільний пристрій (ГО) — це пристрій, що є мобільним радиоузлом, що дозволяє в реальному часі визначати локацію людини або іншого об'єкту, до якого він прикріплений. Залежно від типу пристрою, може виконувати додаткові функції, наприклад, передачу звуку. Приклад мобільного представлений на малюнку 2.

Рис. 2. Приклад мобільного пристрою.
image

Цикл опитування (alive cycle) — період, протягом якого пристрою посилають в ефір дані про свій стан.
На малюнку 3 представлена архітектура системи RTLS. У позначеній зоні чорною лінією знаходяться компоненти вимагають тестування в рамках даної задачі.

Рис. 3. Високорівнева архітектура RealTrac.
image
Внутрішня навантаження на сервер характеризується пристроями бездротового сегмента RealTrac, здійснюють взаємодію з сервером, їх кількістю та інтенсивністю обміну даними. Дані передаються по внутрішньому протоколу — INDD.
Зовнішня навантаження являє собою запити з публічного API — RTLSCP.

Постановка завдання.
Поставлено завдання визначити стабільність роботи серверного ПЗ при тривалому навантаженні в 2000 мобільних пристроїв з циклом опитування 2 сек, тобто перевірити, чи не буде зависань, збоїв або помилок у роботі. Також потрібно визначити споживання ресурсів процесора і пам'яті (макс., хв., середн.).

Спочатку дана задача вирішувалася вручну, але швидко прийшло усвідомлення того, що цю задачу потрібно як можна швидше автоматизувати.
Усередині відділу проблема розбилася на наступні завдання:
1. Визначення підходу до тестування та інструментів.
2. Написання конфігураційних файлів для генератора внутрішньої навантаження.
3. Реалізація тестуючої системи.
4. Автоматизація запуску тестів.

Підхід до тестування.
В силу специфіки завдання вирішено реалізувати власну невелику систему для автоматизованого тестування стабільності. Ядром тестової системи є додаток-контролер, яке в певні моменти часу по ssh розсилає і запускає скрипти на «підлеглі» машини. Скрипти виконують дві основні функції: розгортання системи на віддаленій машині і моніторинг ресурсів. На малюнку 4 представлена схема взаємодії.

Рис. 4. Схема взаємодії тестових серверів.
image
Наступне завдання — автоматизація циклів тестування. Для її вирішення ми не стали винаходити велосипед і скористалися нашим локальним сервером складання. Тобто сам процес тестування буде проходити при збірці, а збірка буде проходити за заданим розкладом.

Генерація внутрішньої навантаження.
Для того, щоб постійно не використовувати для тестів реальні пристрої і мати можливість навантажити сервер будь-якою кількістю пристроїв, всередині компанії розроблено додаток incptester призначене для емуляції пристроїв бездротового сегмента — ТД і МУ.
З точки зору тестувальника, достатньо знати тільки те, що incptester налаштовується за допомогою конфігураційного файлу. Файл має наступний вигляд:

[general]
 
geo_lat=61.786838
 
geo_lng=34.353548
 
geo_alt=1
 

 
[tracks]
 
id=1,type=POLY,TF=0,VRT=(20:0:0)(21:0:30)(60)(40:0:0)(60)
 

 
[devices]
 
mac=CF0000000000,devtype=1,ip=127.0.1.1,cycle=30000,x=0.0,y=0.0,z=0
 
mac=C00000000001,devtype=2,ip=127.0.0.2,cycle=30000,x=5.0,y=0.0,z=0
 
mac=000000BAD001,devtype=4,cycle=2000,track=1
 
mac=000000BAD002,devtype=6,cycle=2000,track=1
 


У блоці [general] задаються географічні координати точки-центру, від якої будуть відраховуватися локальні координати (x,y,z). Блок [tracks] потрібен для опису траєкторій, за якими будуть рухатися мобільні пристрої. Траєкторія описується послідовністю точок. У блоці [devices] прописуються самі пристрої, які будуть емулювати поведінку реальних пристроїв.

Крім mac-адреси, у кожного пристрою є devtype, параметр визначає тип пристрою (1, 2 — стаціонарні і ТД 3-6 — МУ). Також, існує параметр cycle, який визначає цикл опитування пристроїв. Для стаціонарних точок доступу потрібно вказати конкретну позицію через координати. У мобільних пристроїв можна задати траєкторію, по якій вони будуть рухатися.

У підсумку, для генерації необхідної навантаження потрібно скласти конфігураційний файл на необхідну кількість пристроїв для incptester-а.
Щоб вручну не прописувати 2000 пристроїв в конфіги, був написаний невеликий bash скрипт, який додає задану кількість рядків в базовий файл.
add_devices.sh
#!/bin/bash
set -e

if [ "$#" -ne 2 ]
then
echo "Usage: ./${0} <base_config> <dev_num>"
exit 1
fi

INCPTESTER_CONF_PATH=./${1}.conf
if [ ! -f ${INCPTESTER_CONF_PATH} ]; then
cat ./incptester_geo-base.conf > ${INCPTESTER_CONF_PATH}
fi

alias get_next_mac='python -c "import sys; print '{:012x}'.format(int(sys.argv[1], 16)+int(sys.argv[2], 16)).upper()"'

last_mac=$(tac ${INCPTESTER_CONF_PATH} | grep -m 1 . | grep -o -P "[A-z0-9]{12}")
for count in $(seq ${2}); do 
next_mac=$(get_next_mac "0x${last_mac}" "0x1")
echo "mac=${next_mac},devtype=6,cycle=2000,track=2" >> ${INCPTESTER_CONF_PATH}
last_mac=${next_mac}
done


Використане ПО.
Додаток контролер було вирішено реалізовувати на java. Для опису сценаріїв тестування в програмі ми скористалися бібліотекою cucumber і, відповідно, мовою gherkin. В якості інструменту збірки ми взяли gradle. Сама збірка була вбудована в локальний сервер Hudson.

Оскільки система працює на Debian Linux, доцільно реалізовувати скрипти по взаємодії з ОС на bash. Це включає установку/видалення deb-пакетів і конфіги. Для моніторингу поточних процесів ми використовували пакет psutil для python, з періодичним вивантаженням значень споживаним ресурсів в csv.

Основний сценарій.
Сценарій ділиться на наступні кроки:
1. Видалити попередні пакети з тестових серверів.
2. Приготувати конфігураційні файли для deb-пакетів.
3. Скопіювати конфігураційні файли і скрипти на тестові сервери.
4. Розгорнути систему на тестових серверах.
5. Запустити incptester з конфігурацією на 2000 мобільних пристроїв на slave1.
6. Запустити сервер з внутрішньої навантаженням на певну кількість часу на slave1 з паралельним моніторингом ресурсів.
7. Запустити сервер додатків на slave2, сконфігурований на основний сервер, що знаходиться на slave з паралельним моніторингом ресурсів.

Реалізація.
У даному розділі я коротко наведу основні моменти реалізації тестуючої системи.
Сценарій тесту легко перетворюється в фічу Gherkin. У виконуваних кроків можна задавати параметри, які будуть передаватися в виконуваний метод. Виглядає це приблизно наступним чином:
@Load_stability_geo
 
Feature: Load_stability_geo
 
This test starts the large number of the devices and monitors the system resources
 

 
@Install
 
Scenario: Instalation of RealTrac system in geo mode
 
Given I delete the previous Realtrac-server from both the test-servers
 
And I prepare all deb configs
 
And I copy all configs and scripts to the test servers
 
And I install the main Realtrac-server on the test-server and incptester and stop service rtlserm for geo configuration
 
Then Run first part of the test with the inside load for 11520 steps
 
Given I install the app server
 
Then Run second part of the test with the inside load for 11520 steps
 


Для сценарію пишемо окремий клас LoadStabilityGeo. Клас буде містити методи, які виконують кроки з фічі. Приклад з передачею параметра в метод. Параметр парс по регулярному виразу.

import rtls.test.utils.RTLSUtils;
import cucumber.api.java.en.And;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;


public class LoadStabilityGeo {

// some other methods
@Then("^Run first part of the test with the inside load for (\\d+) steps")
public void First_Monitoring(int count_time) throws ScriptFaildException, Exception {
int times=0; 
double minutes; 
int check_time_min=10; //each 10 minutes the resources is verified

monitoring_file=NameFileDataFormat("monitoring", "csv");
path_monitoring_file=" /home/"+user+"/TestResult/";
path_monitoring_file=path_monitoring_file+monitoring_file; 

minutes=0.5;

while (times <= count_time) {
run_ssh_cmd("resource_get.py "+path_monitoring_file+" rtls", "main_server"); 
If (((times%check_time_min)==0) && times!=0){
checkResource("rtls", rtlscp_port, rtlscpip, check_time_min);
times=times+1; 
}else{
Sleep_time(minutes);
times=times+1;
}
}
System.out.println("The first part of the stress test in geo-mode is successfully finished");
}
}


Також був написаний окремий клас RTLSUtils, що містить статичні методи для роботи зі скриптами (виконання/перевірка результату) та інші загальні методи. Приклад методу для запуску команди ОС:

public class RTLSUtils {

// some other methods

public static void executeCommand(command String) throws ScriptFaildException {
try {
String line;
System.out.println("Excecute " + command);
String[] env = new String[]{"DEBIAN_FRONTEND=noninteractive"};
Process p = Runtime.getRuntime().exec(command, env);
BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream()));
BufferedReader error = new BufferedReader(new InputStreamReader(p.getErrorStream()));
while ((line = input.readLine()) != null) {
System.out.println(line);
}
input.close();
while ((line = error.readLine()) != null) {
System.out.println(line);
}
error.close();
try {
if (p.exitValue() != 0) {
throw new ScriptFaildException(new Exception("error to execute command " + command));
}
} catch (IllegalThreadStateException ex) {
}
} catch (IOException ex) {
throw new ScriptFaildException(ex);
}
}
}


Тепер перейдемо до скриптів. Bash скрипти виконують просту функцію видалення і встановлення deb-пакетів. Приклад встановлення пактів на віддаленій машині через apt-get.
install_deb.sh
#!/bin/bash
set -e 
set -x

# Шлях до поточного сценарію
SCRIPT_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# Параметри (можливо винести їх в окремий файл)
TEST_USER=user
TEST_SERVER_IP=192.168.1.2

# Префікс для команди установки
# Якщо це локальна машина, то префікс не потрібен
if [ "${TEST_SERVER_IP}" == "127.0.0.1" ];
then
COMMAND_PREFIX=
else
COMMAND_PREFIX="ssh ${TEST_USER}@${TEST_SERVER_IP}"
fi

APT_CONFIG_DIR="/home/${TEST_USER}/apt-get/"
VERSION=$1

# Копіювання deb конфіг файлу
if [ "${TEST_SERVER_IP}" == "127.0.0.1" ];
then
if [ ! -d ${APT_CONFIG_DIR} ];
then
mkdir ${APT_CONFIG_DIR}
fi
cp ${SCRIPT_PATH}/debconf.dat $APT_CONFIG_DIR
else
scp ${SCRIPT_PATH}/debconf.dat ${TEST_USER}@${TEST_SERVER_IP}:${APT_CONFIG_DIR}
fi

# Установка пакета
${COMMAND_PREFIX} sudo debconf-set-selections ${APT_CONFIG_DIR}debconf.dat

${COMMAND_PREFIX} sudo apt-get update && true
${COMMAND_PREFIX} sudo DEBIAN_FRONTEND=noninteractive apt-get install --force-yes -y some-package-${VERSION}


Python скрипт виконує вивантаження даних про процес в csv файл.
resources.sh
#!/usr/bin/python
# -*- coding: utf-8 -*-
# depends on;
# sudo pip install psutil
# Usage: python resources.py </path/to/file> <proc_name>

import time
import datetime
import psutil
import sys
імпортувати csv

def convert_bytes(bytes):
"'
Переклад займаної пам'яті байт в читає рядок
:param bytes:
:return:Рядок
"'
bytes = float(bytes)
if bytes >= 1099511627776:
terabytes = bytes / 1099511627776
size = '%.2fT' % terabytes
elif bytes >= 1073741824:
gigabytes = bytes / 1073741824
size = '%.2fG' % gigabytes
elif bytes >= 1048576:
megabytes = bytes / 1048576
size = '%.2fM' % megabytes
elif bytes >= 1024:
kilobytes = bytes / 1024
size = '%.2fK' % kilobytes
else:
size = '%.2fb' % bytes
return size

def get_string(proc, proc_name):
"'
Формування рядка з вимірами.
:param proc: Об'єкт процесу
:param proc_name: Ім'я процесу
:return: Рядок
"'
data_time = datetime.datetime.fromtimestamp(time.time()).strftime("%Y-%m-%d %H:%M:%S")
ram = convert_bytes(proc.memory_info().rss)
ram_percent = round(proc.memory_percent(),2)
cpu = proc.cpu_percent(interval=1)
wrt_str = ("{0} {1} {2} {3} {4} {5}".format(data_time, proc_name, proc.pid, ram, ram_percent, cpu))
return wrt_str

if __name__ == "__main__":
if (len(sys.argv) == 3):
pathresult_log = sys.argv[1]
target_proc = sys.argv[2]

# Відкриваємо файли на запис
if (target_proc=="rtls"):
res_log_rtls = open(pathresult_log, 'a')
writer_rtls_log = csv.writer(res_log_rtls)
elif (target_proc == "rtlsapp"):
res_log_app = open(pathresult_log, 'a')
writer_app_log = csv.writer(res_log_app)
elif (target_proc == "all"):
res_log_rtls = open(pathresult_log, 'a')
res_log_app = open(pathresult_log, 'a')
writer_rtls_log = csv.writer(res_log_rtls)
writer_app_log = csv.writer(res_log_app)
else:
print ("Types of the test are not correct")
sys.exit(1)

proc_rtls = 0
proc_rtlsapp = 0

try:
# Вибираємо питання, що нас процеси
procs = [p for p in psutil.process_iter()]
for proc in procs:
if (proc.name() == 'java' and proc.username() == 'rtlsadmin'):
proc_rtls = proc
elif (proc.name() == 'node' and proc.username() == 'rtlsapp'):
proc_rtlsapp = proc

except psutil.NoSuchProcess:
pass

else:
# Записуємо дані в файл
if (target_proc == "rtls" or target_proc == "all"):
if (proc_rtls != 0):
proc_name = "rtls-server"
str_rtls = get_string(proc_rtls, proc_name)
writer_rtls_log.writerow(str_rtls.split())
elif (target_proc == "rtlsapp" or target_proc == "all"):
if (proc_rtlsapp != 0):
proc_name = "rtls-app"
str_rtlsapp = get_string(proc_rtlsapp, proc_name)
writer_app_log.writerow(str_rtlsapp.split())

finally:
# Закриваємо файли
if (target_proc == "rtls" or target_proc == "all"):
res_log_rtls.close()
elif (target_proc == "rtlsapp" or target_proc == "all"):
res_log_app.close()
else:
print("Input parameters are not correct")
sys.exit(1)



Результати.
Результати тестування відображаються в csv файлах. Приклад:

2016-03-04,11:03:55,rtls-server,30237,1.29 G,32.71,167.8
2016-03-04,11:04:27,rtls-server,30237,1.33 G,33.63,166.9
2016-03-04,11:04:59,rtls-server,30237,1.34 G,34.0,172.8

Тут кожний рядок містить дату, час, назву процесу, ідентифікатор процесу, відсоток займаної пам'яті, відсоток завантаження процесора.
З цими даними можна проводити аналіз і будувати графіки. Крім цього, зручно відправляти отримані дані на який-небудь сервіс моніторингу з графічним дашбордом, наприклад, на Grafana.

На цьому все. Надалі ми плануємо детальніше розповісти яким чином реалізований процес зовнішнього навантаження, а також про тестування сервера в сукупності внутрішньої і зовнішньої навантаження.
Автор: Микита Давидовський
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.