Идея этого проектика (именно «проектика») возникла спонтанно. В компании используется memory-DB TimesTen, содержит одну большую таблицу с данными, более 150 млн записей, и объем около 15 гигов. TimesTen всегда работал исправно, ответ по любому запросу получали за считанные миллисекунды, всех это устраивало. В один из дней, T10 стал отвечать на запросы очень долго, время ответа увеличилось до 3-5 секунд. Техподдрежка конечно начала проведение работ по поиску проблемы, но параллельно мы задались вопросом, а для чего вообще используется T10, почему нельзя перенести базу на обычную СУРБД Oracle или Postgres ? Надо было выяснить провести соответствующие тесты. В итоге, немного покопавшись в интернете, необходимого фришного ПО для тестирования не нашлось. В итоге за день «на коленках» была написана небольшая консольная утилита, которая бы замеряла время ответа от СУБД по разным видам запросов, собирала статистику, и в добавок была бы еще и многопоточная, чтобы нагрузочные испытания были наиболее объективными.
Для тестирования была выбрана СУБД Postgres, в базе создана таблица с соответствующей структурой, построены оптимальные индексы:
Заполнение таблицы было сделано через процедуру (время на заполнение 100 млн записей составило 7 часов):
Параллельно было создано консольное приложения для эмулирования нагрузки на СУБД с различными видами запросов. Для подключения к PostgreSQL был использован проект npgsql.
Текст основной программы на C#:
Для настроек был использован XML-формат (точнее сериализация объекта конфигурации в XML):
Для замера производительности был создан класс «Performance»:
В качестве тестового полигона использовалась рабочая станция с 2 гигами оперативки, процем Core2Duo и тормозным винтом. Удивительным были результаты, на 50 потоках среднее ответа было в районе 8-10, максимум до 30 миллисекунд, но при этом диск сильно нагружается. Понятно, что на серьезном оборудовании эти значения будут гораздо ниже. Но порадовало другое — при работе с большой таблицей, СУБД отвечает довольно-таки адекватно.
В принципе ничего не мешает использовать данную программу для нагрузочного тестирования любой базы данных, с любым набором данных и любым количеством таблиц.
Для тестирования была выбрана СУБД Postgres, в базе создана таблица с соответствующей структурой, построены оптимальные индексы:
CREATE TABLE "public"."numbers" (
"contract" int8 NOT NULL,
"account" int8 NOT NULL,
"number" int8 NOT NULL,
"system_id" int2 NOT NULL,
"region_id" int2 NOT NULL,
"storage_id" int2 NOT NULL
);
CREATE INDEX "ix_contract" ON "public"."numbers" USING btree ("contract");
CREATE UNIQUE INDEX "ix_number" ON "public"."numbers" USING btree ("number");
CREATE INDEX "ix_account" ON "public"."numbers" USING btree ("account");
Заполнение таблицы было сделано через процедуру (время на заполнение 100 млн записей составило 7 часов):
CREATE OR REPLACE FUNCTION "public"."fill"(_count int8)
RETURNS "pg_catalog"."int8" AS $BODY$DECLARE
i int8;
j int2;
_number_start int8;
_pa_start int8;
_pa int8;
_countract int8;
BEGIN
_number_start:=70000000000;
_pa_start:=1000000000000;
FOR i IN 0.._count BY 5 LOOP
INSERT INTO number(contract,account,number,system_id,region_id,storage_id) VALUES
(round(random()*100000000000+100000000000),round(random()*100000000+_pa_start),_number_start+0+i,round(random()*10+1),round(random()*10+101),round(random()*5+1)),
(round(random()*100000000000+100000000000),round(random()*100000000+_pa_start),_number_start+1+i,round(random()*10+1),round(random()*10+101),round(random()*5+1)),
(round(random()*100000000000+100000000000),round(random()*100000000+_pa_start),_number_start+2+i,round(random()*10+1),round(random()*10+101),round(random()*5+1)),
(round(random()*100000000000+100000000000),round(random()*100000000+_pa_start),_number_start+3+i,round(random()*10+1),round(random()*10+101),round(random()*5+1)),
(round(random()*100000000000+100000000000),round(random()*100000000+_pa_start),_number_start+4+i,round(random()*10+1),round(random()*10+101),round(random()*5+1));
END LOOP;
RETURN 1;
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE COST 100
;
Параллельно было создано консольное приложения для эмулирования нагрузки на СУБД с различными видами запросов. Для подключения к PostgreSQL был использован проект npgsql.
Текст основной программы на C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Npgsql;
using System.Xml;
using System.IO;
using System.Threading;
namespace sqlPerformer
{
class Program
{
public static Configuration c;
public static string config_file = "config.xml";
//public static config config = new config();
public static Performance performance = new Performance();
public static List<Thread> threads = new List<Thread>();
static void Main(string[] args)
{
//saveConfig();
Console.WindowWidth = 120;
loadConfig();
for (int i = 1; i <= Program.c.threads; i++)
{
Thread thread = new Thread(Program.thread);
thread.Start(i);
threads.Add(thread);
}
//Program.thread();
Console.ReadLine();
}
public static void thread(object thread_id)
{
NpgsqlConnection cn = new NpgsqlConnection();
cn.ConnectionString = string.Format("Server={0}; Port={1}; Database={2}; User Id={3}; Password={4};", Program.c.host, Program.c.port, Program.c.database, Program.c.user, Program.c.password);
cn.Open();
for (int i = 0; i <= Program.c.count; i++)
{
foreach (query q in Program.c.queries.query)
{
NpgsqlCommand cm = new NpgsqlCommand();
cm.Connection = cn;
cm.CommandText = q.text;
if (!q.status) continue;
foreach (parameter p in q.parameters.parameter)
{
NpgsqlTypes.NpgsqlDbType _temp_type = NpgsqlTypes.NpgsqlDbType.Integer;
object _val = null;
switch (p.GetType().Name)
{
case "bigint":
_temp_type = NpgsqlTypes.NpgsqlDbType.Bigint;
_val = Program.getRandomInt64(((bigint)p).min, ((bigint)p).max);
break;
case "integer":
_temp_type = NpgsqlTypes.NpgsqlDbType.Integer;
_val = Program.getRandomInt32(((integer)p).min, ((integer)p).max);
break;
case "date":
_temp_type = NpgsqlTypes.NpgsqlDbType.Timestamp;
_val = Program.getRandomDate(((date)p).min, ((date)p).max);
break;
case "string_line":
_temp_type = NpgsqlTypes.NpgsqlDbType.Varchar;
_val = Program.getRandomStringLine(((string_line)p).chars, ((string_line)p).max, ((string_line)p).max);
break;
case "text":
_temp_type = NpgsqlTypes.NpgsqlDbType.Text;
_val = Program.getRandomText(((text)p).words, ((text)p).max, ((text)p).max);
break;
}
cm.Parameters.Add(p.id, _temp_type);
cm.Parameters[p.id].Value = _val;
}
Program.delay();
lock (Program.performance)
{ Program.performance.start(q.id, thread_id.ToString()); }
cm.ExecuteNonQuery();
lock (Program.performance)
{ Program.performance.stop(); }
}
}
}
public static long getRandomInt64(long min, long max)
{ return Convert.ToInt64(Math.Round((new Random(unchecked((int)(DateTime.Now.Ticks)))).NextDouble() * (max - min) + min)); }
public static int getRandomInt32(int min, int max)
{ return Convert.ToInt32(Math.Round((new Random(unchecked((int)(DateTime.Now.Ticks)))).NextDouble() * (max - min) + min)); }
public static DateTime getRandomDate(DateTime min, DateTime max)
{
long stamp_min = min.Ticks;
long stamp_max = max.Ticks;
long stamp_new = Program.getRandomInt64(stamp_min, stamp_max);
return (new DateTime(stamp_new));
}
public static string getRandomStringLine(string chars, int min, int max)
{
string retval = "";
Random r = new Random(unchecked((int)(DateTime.Now.Ticks)));
for (int i = 1; i <= r.Next(min, max); i++)
retval += chars[r.Next(0, chars.Length - 1)];
return retval;
}
public static string getRandomText(List<string> words, int min, int max)
{
string retval = "";
Random r = new Random(unchecked((int)(DateTime.Now.Ticks)));
for (int i = 1; i <= r.Next(min, max); i++)
retval += " " + words[r.Next(0, words.Count - 1)];
return retval.Trim();
}
public static void delay()
{
if (Program.c.delay.status)
System.Threading.Thread.Sleep(Program.getRandomInt32(Program.c.delay.min, Program.c.delay.max));
}
static void saveConfig()
{
Program.c = new Configuration("localhost", 5432, "postgres", "postgres");
query q = new query("SELECT * FROM \"public\".\"table\"(?)");
q.parameters.parameter.Add(new bigint(100000000, 200000000));
c.queries.query.Add(q);
System.Xml.Serialization.XmlSerializer xs = new System.Xml.Serialization.XmlSerializer(c.GetType());
StreamWriter writer = File.CreateText(Program.config_file);
xs.Serialize(writer, c);
writer.Flush();
writer.Close();
}
static void loadConfig()
{
System.Xml.Serialization.XmlSerializer xs
= new System.Xml.Serialization.XmlSerializer(
typeof(Configuration));
StreamReader reader = File.OpenText(Program.config_file);
Program.c = (Configuration)xs.Deserialize(reader);
reader.Close();
}
}
}
Для настроек был использован XML-формат (точнее сериализация объекта конфигурации в XML):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
namespace sqlPerformer
{
[Serializable]
public class Configuration
{
[System.Xml.Serialization.XmlElement()]
public string host = "localhost";
[System.Xml.Serialization.XmlElement()]
public int port = 5432;
[System.Xml.Serialization.XmlElement()]
public string user = "postgres";
[System.Xml.Serialization.XmlElement()]
public string database = "postgres";
[System.Xml.Serialization.XmlElement()]
public string password = "postgres";
[System.Xml.Serialization.XmlElement()]
public queries queries = new queries();
[System.Xml.Serialization.XmlElement()]
public delay delay = new delay();
[System.Xml.Serialization.XmlElement()]
public int threads = 1;
[System.Xml.Serialization.XmlElement()]
public int count = 1;
public Configuration() { }
public Configuration(string host, int port, string user, string password)
{ this.host = host; this.port = port; this.user = user; this.password = password; }
}
[Serializable]
public class queries
{
[System.Xml.Serialization.XmlElement()]
public List<query> query = new List<query>();
public queries() { }
}
[Serializable]
public class query
{
[System.Xml.Serialization.XmlAttribute()]
public bool status = false;
[System.Xml.Serialization.XmlAttribute()]
public string id = "";
[System.Xml.Serialization.XmlElement()]
public string text = "";
[System.Xml.Serialization.XmlElement()]
public parameters parameters = new parameters();
public query() { }
public query(string text) { this.text = text; }
}
[Serializable]
public class parameters
{
[System.Xml.Serialization.XmlElement()]
public List<parameter> parameter = new List<parameter>();
public parameters() { }
}
[Serializable]
[System.Xml.Serialization.XmlInclude(typeof(bigint))]
[System.Xml.Serialization.XmlInclude(typeof(integer))]
[System.Xml.Serialization.XmlInclude(typeof(string_line))]
[System.Xml.Serialization.XmlInclude(typeof(text))]
[System.Xml.Serialization.XmlInclude(typeof(date))]
public abstract class parameter
{
[System.Xml.Serialization.XmlAttribute()]
public string id = "";
public parameter() { }
}
[Serializable]
public class bigint : parameter
{
[System.Xml.Serialization.XmlElement()]
public long min = 0;
[System.Xml.Serialization.XmlElement()]
public long max = 0;
public bigint() { }
public bigint(long min, long max)
{ this.min = min; this.max = max; }
}
[Serializable]
public class integer : parameter
{
[System.Xml.Serialization.XmlElement()]
public int min = 0;
[System.Xml.Serialization.XmlElement()]
public int max = 0;
public integer() { }
public integer(int min, int max)
{ this.min = min; this.max = max; }
}
[Serializable]
public class date : parameter
{
[System.Xml.Serialization.XmlElement()]
public DateTime min = DateTime.Now;
[System.Xml.Serialization.XmlElement()]
public DateTime max = DateTime.Now;
public date() { }
public date(DateTime min, DateTime max)
{ this.min = min; this.max = max; }
}
[Serializable]
public class string_line : parameter
{
[System.Xml.Serialization.XmlElement()]
public string chars = "qwertyuiopasdfghjklzxcvbnm";
[System.Xml.Serialization.XmlElement()]
public int min = 2;
[System.Xml.Serialization.XmlElement()]
public int max = 10;
public string_line() { }
public string_line(string chars, int min, int max)
{ this.chars = chars; this.min = min; this.max = max; }
}
[Serializable]
public class text : parameter
{
[System.Xml.Serialization.XmlElement()]
public List<string> words = new List<string>() { "word1", "word2" };
[System.Xml.Serialization.XmlElement()]
public int min = 2;
[System.Xml.Serialization.XmlElement()]
public int max = 10;
public text() { }
public text(List<string> words, int min, int max)
{ this.words = words; this.min = min; this.max = max; }
}
[Serializable]
public class delay
{
[System.Xml.Serialization.XmlAttribute()]
public bool status = false;
[System.Xml.Serialization.XmlElement()]
public int min = 10;
[System.Xml.Serialization.XmlElement()]
public int max = 100;
}
}
Для замера производительности был создан класс «Performance»:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
namespace sqlPerformer
{
class Performance
{
public Stopwatch sw = new Stopwatch();
public Dictionary<string, stat> stat = new Dictionary<string, stat>();
public string current_id = "";
public void start(string id, string thread_id) {
this.sw.Reset();
this.current_id = thread_id + "-" + id;
if (!this.stat.ContainsKey(this.current_id))
{
this.stat.Add(this.current_id, new stat());
}
this.sw.Start();
}
public void stop() { this.sw.Stop(); addTick(); }
public void addTick()
{
this.stat[this.current_id].add(this.sw.ElapsedTicks);
Console.Clear();
foreach (KeyValuePair<string, stat> k in this.stat)
Console.WriteLine(string.Format("{0}\tCount: {1}\tAverage: {2:#0.00}\tMin: {3:#0.00}\tMax: {4:#0.00}\tTotal: {5:#}"
, k.Key
, k.Value.count
, k.Value.timeAvg
, k.Value.timeMin
, k.Value.timeMax
, k.Value.timeTotal
));
}
}
public class stat
{
public Int64 count = 0;
public double timeTotal = 0;
public double timeLast = 0;
public double timeMin = 9999999;
public double timeMax = 0;
public double timeAvg = 0;
public stat() { }
public void add(long ticks)
{
this.count++;
this.timeLast = ticks / 10000000.0;
this.timeTotal += this.timeLast;
this.timeAvg = this.timeTotal / this.count;
this.timeMin = Math.Min(this.timeLast, this.timeMin);
this.timeMax = Math.Max(this.timeLast, this.timeMax);
}
}
}
В качестве тестового полигона использовалась рабочая станция с 2 гигами оперативки, процем Core2Duo и тормозным винтом. Удивительным были результаты, на 50 потоках среднее ответа было в районе 8-10, максимум до 30 миллисекунд, но при этом диск сильно нагружается. Понятно, что на серьезном оборудовании эти значения будут гораздо ниже. Но порадовало другое — при работе с большой таблицей, СУБД отвечает довольно-таки адекватно.
В принципе ничего не мешает использовать данную программу для нагрузочного тестирования любой базы данных, с любым набором данных и любым количеством таблиц.