412 lines
15 KiB
Lua
412 lines
15 KiB
Lua
|
|
-- tests/autoscaling_test.lua
|
|||
|
|
-- Тестирование автоматического масштабирования кластера
|
|||
|
|
|
|||
|
|
local function setup_test_environment()
|
|||
|
|
print("=== Setup Test Environment ===")
|
|||
|
|
|
|||
|
|
-- Создаём тестовую базу данных
|
|||
|
|
local result = db.execute("CREATE DATABASE IF NOT EXISTS autoscale_test")
|
|||
|
|
assert(result, "Failed to create database")
|
|||
|
|
|
|||
|
|
result = db.execute("USE autoscale_test")
|
|||
|
|
assert(result, "Failed to use database")
|
|||
|
|
|
|||
|
|
-- Создаём тестовую коллекцию
|
|||
|
|
result = db.execute([[
|
|||
|
|
CREATE COLLECTION IF NOT EXISTS metrics_collection
|
|||
|
|
WITH SETTINGS {
|
|||
|
|
max_documents = 1000000,
|
|||
|
|
validate_schema = true
|
|||
|
|
}
|
|||
|
|
]])
|
|||
|
|
assert(result, "Failed to create collection")
|
|||
|
|
|
|||
|
|
-- Получаем конфигурацию автомасштабирования
|
|||
|
|
local config = db.execute("SHOW AUTOSCALING CONFIG")
|
|||
|
|
print("Current autoscaling config:")
|
|||
|
|
print(config)
|
|||
|
|
|
|||
|
|
return true
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local function generate_test_load(duration_seconds, target_qps)
|
|||
|
|
print(string.format("Generating %.2f QPS load for %d seconds", target_qps, duration_seconds))
|
|||
|
|
|
|||
|
|
local start_time = os.time()
|
|||
|
|
local requests = 0
|
|||
|
|
local errors = 0
|
|||
|
|
local latencies = {}
|
|||
|
|
|
|||
|
|
local interval = 1.0 / target_qps
|
|||
|
|
|
|||
|
|
while os.time() - start_time < duration_seconds do
|
|||
|
|
local req_start = os.clock()
|
|||
|
|
|
|||
|
|
-- Симуляция запроса на запись
|
|||
|
|
local success, err = pcall(function()
|
|||
|
|
local doc = {
|
|||
|
|
_id = string.format("doc_%d_%d", os.time(), math.random(1, 10000)),
|
|||
|
|
timestamp = os.time() * 1000,
|
|||
|
|
value = math.random(1, 1000),
|
|||
|
|
type = "test_metric"
|
|||
|
|
}
|
|||
|
|
return db.execute(string.format(
|
|||
|
|
"INSERT INTO metrics_collection VALUES (%s)",
|
|||
|
|
db.json_encode(doc)
|
|||
|
|
))
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
local req_end = os.clock()
|
|||
|
|
local latency = (req_end - req_start) * 1000 -- в миллисекундах
|
|||
|
|
|
|||
|
|
table.insert(latencies, latency)
|
|||
|
|
requests = requests + 1
|
|||
|
|
|
|||
|
|
if not success then
|
|||
|
|
errors = errors + 1
|
|||
|
|
print("Request failed: " .. tostring(err))
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- Контроль скорости
|
|||
|
|
local elapsed = os.clock() - req_start
|
|||
|
|
if elapsed < interval then
|
|||
|
|
local sleep_time = interval - elapsed
|
|||
|
|
db.sleep(sleep_time)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- Вычисляем статистику
|
|||
|
|
local total_latency = 0
|
|||
|
|
for _, lat in ipairs(latencies) do
|
|||
|
|
total_latency = total_latency + lat
|
|||
|
|
end
|
|||
|
|
local avg_latency = total_latency / #latencies
|
|||
|
|
|
|||
|
|
-- Сортируем для перцентилей
|
|||
|
|
table.sort(latencies)
|
|||
|
|
local p95_latency = latencies[math.floor(#latencies * 0.95)]
|
|||
|
|
local p99_latency = latencies[math.floor(#latencies * 0.99)]
|
|||
|
|
|
|||
|
|
print(string.format([[
|
|||
|
|
=== Load Test Results ===
|
|||
|
|
Duration: %d seconds
|
|||
|
|
Total Requests: %d
|
|||
|
|
Successful: %d
|
|||
|
|
Errors: %d
|
|||
|
|
Actual QPS: %.2f
|
|||
|
|
Avg Latency: %.2f ms
|
|||
|
|
P95 Latency: %.2f ms
|
|||
|
|
P99 Latency: %.2f ms
|
|||
|
|
]], duration_seconds, requests, requests - errors, errors,
|
|||
|
|
requests / duration_seconds, avg_latency, p95_latency, p99_latency))
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
requests = requests,
|
|||
|
|
errors = errors,
|
|||
|
|
avg_latency = avg_latency,
|
|||
|
|
p95_latency = p95_latency,
|
|||
|
|
p99_latency = p99_latency,
|
|||
|
|
actual_qps = requests / duration_seconds
|
|||
|
|
}
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local function monitor_cluster_size(interval_seconds, duration_seconds)
|
|||
|
|
print(string.format("Monitoring cluster size every %d seconds for %d seconds",
|
|||
|
|
interval_seconds, duration_seconds))
|
|||
|
|
|
|||
|
|
local history = {}
|
|||
|
|
local start_time = os.time()
|
|||
|
|
|
|||
|
|
while os.time() - start_time < duration_seconds do
|
|||
|
|
local status = db.execute("SHOW CLUSTER STATUS")
|
|||
|
|
local cluster_info = db.json_decode(status)
|
|||
|
|
|
|||
|
|
table.insert(history, {
|
|||
|
|
timestamp = os.time(),
|
|||
|
|
nodes = cluster_info.total_nodes,
|
|||
|
|
active_nodes = cluster_info.active_nodes,
|
|||
|
|
leader = cluster_info.leader_id,
|
|||
|
|
health = cluster_info.health
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
print(string.format("Time %ds: Nodes=%d, Active=%d, Health=%s",
|
|||
|
|
os.time() - start_time,
|
|||
|
|
cluster_info.total_nodes,
|
|||
|
|
cluster_info.active_nodes,
|
|||
|
|
cluster_info.health))
|
|||
|
|
|
|||
|
|
db.sleep(interval_seconds)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
return history
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local function test_scale_up_on_high_load()
|
|||
|
|
print("\n=== Test 1: Scale Up on High Load ===")
|
|||
|
|
|
|||
|
|
-- Проверяем начальное состояние
|
|||
|
|
local initial_status = db.execute("SHOW CLUSTER STATUS")
|
|||
|
|
local initial_info = db.json_decode(initial_status)
|
|||
|
|
local initial_nodes = initial_info.total_nodes
|
|||
|
|
print(string.format("Initial cluster size: %d nodes", initial_nodes))
|
|||
|
|
|
|||
|
|
-- Запускаем мониторинг в фоне
|
|||
|
|
local monitor_thread = coroutine.create(function()
|
|||
|
|
return monitor_cluster_size(5, 180) -- мониторим 3 минуты
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
-- Генерируем высокую нагрузку
|
|||
|
|
local load_results = generate_test_load(60, 500) -- 60 секунд, 500 QPS
|
|||
|
|
|
|||
|
|
-- Ожидаем возможного масштабирования
|
|||
|
|
print("Waiting for potential scale up...")
|
|||
|
|
db.sleep(120) -- ждём 2 минуты
|
|||
|
|
|
|||
|
|
-- Проверяем результат
|
|||
|
|
local final_status = db.execute("SHOW CLUSTER STATUS")
|
|||
|
|
local final_info = db.json_decode(final_status)
|
|||
|
|
local final_nodes = final_info.total_nodes
|
|||
|
|
|
|||
|
|
print(string.format("Final cluster size: %d nodes", final_nodes))
|
|||
|
|
|
|||
|
|
if final_nodes > initial_nodes then
|
|||
|
|
print("✅ Scale Up successful: cluster expanded from " ..
|
|||
|
|
initial_nodes .. " to " .. final_nodes .. " nodes")
|
|||
|
|
return true
|
|||
|
|
else
|
|||
|
|
print("⚠️ No scale up detected (may be at max capacity or cooldown)")
|
|||
|
|
return false
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local function test_scale_down_on_low_load()
|
|||
|
|
print("\n=== Test 2: Scale Down on Low Load ===")
|
|||
|
|
|
|||
|
|
local initial_status = db.execute("SHOW CLUSTER STATUS")
|
|||
|
|
local initial_info = db.json_decode(initial_status)
|
|||
|
|
local initial_nodes = initial_info.total_nodes
|
|||
|
|
|
|||
|
|
if initial_nodes <= 1 then
|
|||
|
|
print("⚠️ Cannot test scale down: only one node in cluster")
|
|||
|
|
return false
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
print(string.format("Initial cluster size: %d nodes", initial_nodes))
|
|||
|
|
|
|||
|
|
-- Ждём снижения нагрузки
|
|||
|
|
print("Waiting for load to decrease and scale down...")
|
|||
|
|
db.sleep(300) -- ждём 5 минут
|
|||
|
|
|
|||
|
|
local final_status = db.execute("SHOW CLUSTER STATUS")
|
|||
|
|
local final_info = db.json_decode(final_status)
|
|||
|
|
local final_nodes = final_info.total_nodes
|
|||
|
|
|
|||
|
|
print(string.format("Final cluster size: %d nodes", final_nodes))
|
|||
|
|
|
|||
|
|
if final_nodes < initial_nodes then
|
|||
|
|
print("✅ Scale Down successful: cluster reduced from " ..
|
|||
|
|
initial_nodes .. " to " .. final_nodes .. " nodes")
|
|||
|
|
return true
|
|||
|
|
else
|
|||
|
|
print("⚠️ No scale down detected (may be at min capacity or cooldown)")
|
|||
|
|
return false
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local function test_predictive_scaling()
|
|||
|
|
print("\n=== Test 3: Predictive Scaling ===")
|
|||
|
|
|
|||
|
|
-- Включаем прогнозирование
|
|||
|
|
db.execute("SET AUTOSCALING PREDICTIVE true")
|
|||
|
|
|
|||
|
|
-- Создаём растущую нагрузку
|
|||
|
|
print("Generating increasing load pattern...")
|
|||
|
|
|
|||
|
|
local function generate_ramp_load(initial_qps, final_qps, duration_seconds)
|
|||
|
|
local step_duration = duration_seconds / 10
|
|||
|
|
for step = 0, 9 do
|
|||
|
|
local target_qps = initial_qps + (final_qps - initial_qps) * step / 10
|
|||
|
|
print(string.format("Step %d/10: %.2f QPS", step + 1, target_qps))
|
|||
|
|
generate_test_load(step_duration, target_qps)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
generate_ramp_load(50, 600, 120)
|
|||
|
|
|
|||
|
|
-- Проверяем, произошло ли масштабирование до достижения пика
|
|||
|
|
local scaling_history = db.execute("SHOW AUTOSCALING HISTORY")
|
|||
|
|
local history = db.json_decode(scaling_history)
|
|||
|
|
|
|||
|
|
local scaled_before_peak = false
|
|||
|
|
for _, event in ipairs(history) do
|
|||
|
|
if event.decision == "scale_up" then
|
|||
|
|
local load_at_scale = event.avg_load
|
|||
|
|
if load_at_scale < 0.85 then -- Масштабирование до достижения 85% нагрузки
|
|||
|
|
scaled_before_peak = true
|
|||
|
|
break
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
if scaled_before_peak then
|
|||
|
|
print("✅ Predictive scaling successful: scaled before reaching peak load")
|
|||
|
|
return true
|
|||
|
|
else
|
|||
|
|
print("⚠️ Predictive scaling may not be working optimally")
|
|||
|
|
return false
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local function test_autoscaling_metrics()
|
|||
|
|
print("\n=== Test 4: Autoscaling Metrics ===")
|
|||
|
|
|
|||
|
|
local metrics = db.execute("SHOW AUTOSCALING METRICS")
|
|||
|
|
local metrics_data = db.json_decode(metrics)
|
|||
|
|
|
|||
|
|
print("Current autoscaling metrics:")
|
|||
|
|
print(string.format(" Current nodes: %d", metrics_data.current_nodes or 0))
|
|||
|
|
print(string.format(" Min nodes: %d", metrics_data.min_nodes or 0))
|
|||
|
|
print(string.format(" Max nodes: %d", metrics_data.max_nodes or 0))
|
|||
|
|
print(string.format(" Last scale up: %s", metrics_data.last_scale_up or "never"))
|
|||
|
|
print(string.format(" Last scale down: %s", metrics_data.last_scale_down or "never"))
|
|||
|
|
print(string.format(" Evaluation count: %d", metrics_data.evaluation_count or 0))
|
|||
|
|
print(string.format(" Predictive enabled: %s", tostring(metrics_data.predictive_enabled)))
|
|||
|
|
|
|||
|
|
-- Проверяем наличие критических метрик
|
|||
|
|
local required_fields = {
|
|||
|
|
"current_nodes", "min_nodes", "max_nodes",
|
|||
|
|
"evaluation_count", "predictive_enabled"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, field in ipairs(required_fields) do
|
|||
|
|
if metrics_data[field] == nil then
|
|||
|
|
print("❌ Missing metric: " .. field)
|
|||
|
|
return false
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
print("✅ All required metrics present")
|
|||
|
|
return true
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local function test_cooldown_behavior()
|
|||
|
|
print("\n=== Test 5: Cooldown Behavior ===")
|
|||
|
|
|
|||
|
|
-- Быстро генерируем две всплески нагрузки
|
|||
|
|
for i = 1, 2 do
|
|||
|
|
print(string.format("Load spike %d/2", i))
|
|||
|
|
generate_test_load(30, 800)
|
|||
|
|
db.sleep(10) -- Короткая пауза между всплесками
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local history = db.execute("SHOW AUTOSCALING HISTORY")
|
|||
|
|
local history_data = db.json_decode(history)
|
|||
|
|
|
|||
|
|
-- Проверяем, что масштабирование не происходило слишком часто
|
|||
|
|
local scale_ups = {}
|
|||
|
|
for _, event in ipairs(history_data) do
|
|||
|
|
if event.decision == "scale_up" then
|
|||
|
|
table.insert(scale_ups, event.timestamp)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local cooldown_ok = true
|
|||
|
|
for i = 2, #scale_ups do
|
|||
|
|
local time_diff = scale_ups[i] - scale_ups[i-1]
|
|||
|
|
if time_diff < 300 then -- 5 минут cooldown
|
|||
|
|
cooldown_ok = false
|
|||
|
|
print(string.format("⚠️ Scale ups too close: %d seconds apart", time_diff))
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
if cooldown_ok then
|
|||
|
|
print("✅ Cooldown mechanism working properly")
|
|||
|
|
else
|
|||
|
|
print("❌ Cooldown mechanism may be misconfigured")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
return cooldown_ok
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local function test_node_selection_for_scale_down()
|
|||
|
|
print("\n=== Test 6: Node Selection for Scale Down ===")
|
|||
|
|
|
|||
|
|
-- Получаем нагрузку на узлах
|
|||
|
|
local nodes_load = db.execute("SHOW NODES LOAD")
|
|||
|
|
local load_data = db.json_decode(nodes_load)
|
|||
|
|
|
|||
|
|
print("Node load distribution:")
|
|||
|
|
local loads = {}
|
|||
|
|
for _, node in ipairs(load_data) do
|
|||
|
|
table.insert(loads, node.load)
|
|||
|
|
print(string.format(" %s: %.2f%%", node.id, node.load * 100))
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- Сортируем узлы по нагрузке
|
|||
|
|
table.sort(load_data, function(a, b) return a.load < b.load end)
|
|||
|
|
|
|||
|
|
-- Наименее загруженный узел должен быть кандидатом на удаление
|
|||
|
|
local lowest_load_node = load_data[1]
|
|||
|
|
print(string.format("Lowest load node: %s (%.2f%%)",
|
|||
|
|
lowest_load_node.id, lowest_load_node.load * 100))
|
|||
|
|
|
|||
|
|
-- Запрашиваем кандидата на удаление у системы
|
|||
|
|
local scale_down_candidate = db.execute("SHOW SCALE_DOWN_CANDIDATE")
|
|||
|
|
local candidate = db.json_decode(scale_down_candidate)
|
|||
|
|
|
|||
|
|
if candidate.id == lowest_load_node.id then
|
|||
|
|
print("✅ Correct node selected for scale down")
|
|||
|
|
return true
|
|||
|
|
else
|
|||
|
|
print(string.format("❌ Wrong node selected. Expected: %s, Got: %s",
|
|||
|
|
lowest_load_node.id, candidate.id))
|
|||
|
|
return false
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- Функция очистки
|
|||
|
|
local function cleanup()
|
|||
|
|
print("\n=== Cleanup ===")
|
|||
|
|
db.execute("DROP DATABASE IF EXISTS autoscale_test")
|
|||
|
|
print("Test database removed")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- Запуск всех тестов
|
|||
|
|
local function run_all_autoscaling_tests()
|
|||
|
|
print("╔══════════════════════════════════════════════════════════════╗")
|
|||
|
|
print("║ AUTOSCALING TESTS SUITE ║")
|
|||
|
|
print("╚══════════════════════════════════════════════════════════════╝")
|
|||
|
|
|
|||
|
|
setup_test_environment()
|
|||
|
|
|
|||
|
|
local results = {
|
|||
|
|
test_scale_up_on_high_load(),
|
|||
|
|
test_scale_down_on_low_load(),
|
|||
|
|
test_predictive_scaling(),
|
|||
|
|
test_autoscaling_metrics(),
|
|||
|
|
test_cooldown_behavior(),
|
|||
|
|
test_node_selection_for_scale_down()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
-- Подведение итогов
|
|||
|
|
print("\n╔══════════════════════════════════════════════════════════════╗")
|
|||
|
|
print("║ TEST RESULTS ║")
|
|||
|
|
print("╚══════════════════════════════════════════════════════════════╝")
|
|||
|
|
|
|||
|
|
local passed = 0
|
|||
|
|
for i, result in ipairs(results) do
|
|||
|
|
local status = result and "✅ PASSED" or "⚠️ SKIPPED/FAILED"
|
|||
|
|
print(string.format("Test %d: %s", i, status))
|
|||
|
|
if result then passed = passed + 1 end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
print(string.format("\nTotal: %d/%d tests passed", passed, #results))
|
|||
|
|
|
|||
|
|
cleanup()
|
|||
|
|
|
|||
|
|
return passed == #results
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- Запуск
|
|||
|
|
return run_all_autoscaling_tests()
|