Введение.
В данной статье я расскажу о том, как в отделе контроля качества компании RTL-Service происходит автоматизированное тестирование стабильности сервера RealTrac при одновременном обслуживании большого количества мобильных локационных устройств. Для дальнейшего понимания, предлагаю ознакомиться с полезной терминологией:
RTLS-cервер RealTrac (сервер) — серверное программное обеспечение системы RealTrac, осуществляющее взаимодействие с аппаратными средствами системы и расчет местоположения устройств.
Сервер приложений RealTrac (сервер приложений) — серверное программное обеспечение, необходимое для работы web-приложения, предоставляющее программный интерфейс доступа к основным функциям системы.
Точка доступа RealTrac (далее ТД) — устройство, предназначенное для передачи данных между мобильными устройствами сети и сервером системы. Точки доступа стационарно устанавливаются на объекте, их координаты заносятся на карту клиентского программного обеспечения и фиксируются в базе данных на сервере системы. ТД может работать в режиме шлюза или ретранслятора. Режим определяется наличием проводного Ethernet подключения к сети (шлюз точка доступа, ШТД) и отсутствием такового (ретранслятор точка доступа, РТД). Обмен данными с сервером осуществляют только шлюз. Пример точки доступа представлен на рисунке1.
Рис. 1. Пример точки доступа.
Мобильное устройство (МУ) — это устройство, являющееся мобильным радиоузлом, позволяющим в реальном времени определять локацию человека или иного объекта, к которому он прикреплён. В зависимости от типа устройства, может выполнять дополнительные функции, например, передачу звука. Пример мобильного представлен на рисунке 2.
Рис. 2. Пример мобильного устройства.
Цикл опроса (alive cycle) — период, в течение которого устройства посылают в эфир данные о своем состоянии.
На рисунке 3 представлена архитектура системы RTLS. В зоне обозначенной черной линией находятся компоненты требующие тестирования в рамках данной задачи.
Рис. 3. Высокоуровневая архитектура RealTrac.
Внутренняя нагрузка на сервер характеризуется устройствами из беспроводного сегмента RealTrac, осуществляющими взаимодействие с сервером, их количеством и интенсивностью обмена данными. Данные передаются по внутреннему протоколу — INCP.
Внешняя нагрузка представляет собой запросы по публичному API — RTLSCP.
Постановка задачи.
Поставлена задача определить стабильность работы серверного ПО при длительной нагрузке в 2000 мобильных устройств с циклом опроса 2 сек, т. е. проверить, не будет ли зависаний, сбоев или ошибок в работе ПО. Также требовалось определить потребление ресурсов процессора и памяти (макс., мин., средн.).
Первоначально данная задача решалась вручную, но быстро пришло осознание того, что эту задачу нужно как можно скорее автоматизировать.
Внутри отдела проблема разбилась на следующие подзадачи:
1. Определение подхода к тестированию и инструментам.
2. Написание конфигурационных файлов для генератора внутренней нагрузки.
3. Реализация тестирующей системы.
4. Автоматизация запуска тестов.
Подход к тестированию.
В силу специфики задачи решено реализовать собственную небольшую систему для автоматизированного тестирования стабильности. Ядром тестовой системы вляется приложение-контроллер, которое в определенные моменты времени по ssh рассылает и запускает скрипты на «подчиненные» машины. Скрипты выполняют две основные функции: развертывание системы на удаленной машине и мониторинг ресурсов. На рисунке 4 представлена схема взаимодействия.
Рис. 4. Схема взаимодействия тестовых серверов.
Следующая задача — автоматизация циклов тестирования. Для ее решения мы не стали изобретать велосипед и воспользовались нашим локальным сервером сборки. То есть сам процесс тестирования будет проходить при сборке, а сборка будет проходить по заданному расписанию.
Генерация внутренней нагрузки.
Для того, чтобы постоянно не использовать для тестов реальные устройства и иметь возможность нагрузить сервер любым количеством устройств, внутри компании разработано приложение 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 скрипт, который добавляет заданное количество строк в базовый файл.
#!/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 the both 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(String command) 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.
#!/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 файл.
#!/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
import 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.29G,32.71,167.8
2016-03-04,11:04:27,rtls-server,30237,1.33G,33.63,166.9
2016-03-04,11:04:59,rtls-server,30237,1.34G,34.0,172.8
Здесь каждая строка содержит дату, время, название процесса, идентификатор процесса, процент занимаемой памяти, процент загрузки процессора.
С этими данными уже можно проводить анализ и строить графики. Помимо этого, удобно отправлять полученные данные на какой-нибудь сервис мониторинга с графическим дашбордом, например, на Grafana.
На этом все. В дальнейшем мы планируем подробнее рассказать каким образом реализован процесс внешней нагрузки, а также про тестирование сервера в совокупности внутренней и внешней нагрузки.
Автор: Никита Давыдовский
Автор: RTL-Service