Последние несколько лет я занимаюсь созданием игр для социальных сетей. В качестве back-end применяю связку Ruby + Sinatra + Redis. Redis используется в качестве единственной базы данных. Производительности одной базы Redis часто не хватает, поэтому используется кластер из нескольких баз данных. Более подробно о том, как создавалось решение в виде кластера баз Redis можно прочитать в этой статье.
В последнее время у меня большой интерес вызывает язык программирования Go — слишком много плюшек его использование сулит программисту. Хочется back-end для новых игр написать на нем, но существующая и отлаженная кодовая база на Ruby мешает этому.
Поэтому я решил двигаться небольшими итерациями и начал с переписывания микросервисов применяемых в играх на Go.
Во всех микросервисах есть подключение к базам Redis. Обычно используется 8 баз Redis, где игра хранит данные.
Строка с частью запроса к Redis преобразуется с помощью SHA-1 в шестнадцатеричное число. Затем находится остаток от деления этого числа на 8. И в зависимости от него данные записываются/читаются из нужной базы.
Исходный вариант на ruby, который я решил переписать на Go.
К сожалению или к счастью такой подход в лоб на Go не сработал. Получаемая 16-ричная строка представляла слишком большое число. В тип int64 это число не помещалось.
Как решить проблему?
У нас 8 баз и 16-ричное число, от которого надо взять остаток от деления на 8.
Ответ.
Первое, что приходит на ум. Нам достаточно знать последний символ 16-ричного числа, чтобы посчитать остаток от деления всего числа на 8.
Мы легко можем переписать решение на ruby с учетом вышесказанного так:
То же самое, но на Go:
Второе, что удачно оказалось
В процессе написания кода, представленного выше, выяснилось, что Go в ходе выполнения SHA-1 представляет число в виде набора десятичных чисел, а потом уже их приводит к 16-ричному виду. Поскольку нам нужен остаток от деления, то мы можем просто взять последнее десятичное число вместо 16-ричного последнего символа и сэкономить на преобразованиях из одной системы счисления в другую.
Этот финт позволил еще быстрее вычислять то, что необходимо. Но на Ruby такой трюк выполнить не удалось. Поэтому корректнее сравнивать производительность node1 из Ruby и node1 из Go.
И так какая скорость работы у всего представленного?
Как я решил протестировать:
Посчитать, в какую базу данных положить данные, относящиеся к каждому из миллиона пользователей.
На Ruby:
и на Go:
Полный код можно посмотреть на github.
Для чистоты эксперимента я запускал каждый скрипт по 10 раз, и ниже представлены средние значения.
Код запускался на версиях Ruby 1.8.7 и 2.1.3. Версия Go была 1.4.2
Значения в секундах. Меньше — лучше.
Ruby 1.8.7
node0 — 22.32
node1 — 17.24
node11 — 16.81
Ruby 2.1.3
node0 — 15.46
node1 — 11.15
node11 — 11.05
Go 1.4.2
node1 — 6.15
node2 — 4.36
Почему такие старые версии Ruby?
Код многих проектов я начал создавать 4-5 лет назад. По мере возможности я переносил его на новые версии Ruby. В частности на 2.1.3. Согласен, что на последней версии Ruby 2.2.2 результаты могут быть лучше, но явно не в два раза.
Выводы
В последнее время у меня большой интерес вызывает язык программирования Go — слишком много плюшек его использование сулит программисту. Хочется back-end для новых игр написать на нем, но существующая и отлаженная кодовая база на Ruby мешает этому.
Поэтому я решил двигаться небольшими итерациями и начал с переписывания микросервисов применяемых в играх на Go.
Во всех микросервисах есть подключение к базам Redis. Обычно используется 8 баз Redis, где игра хранит данные.
Строка с частью запроса к Redis преобразуется с помощью SHA-1 в шестнадцатеричное число. Затем находится остаток от деления этого числа на 8. И в зависимости от него данные записываются/читаются из нужной базы.
Исходный вариант на ruby, который я решил переписать на Go.
def node0(text)
return Integer('0x'+Digest::SHA1.hexdigest(text))%8
end
К сожалению или к счастью такой подход в лоб на Go не сработал. Получаемая 16-ричная строка представляла слишком большое число. В тип int64 это число не помещалось.
Как решить проблему?
У нас 8 баз и 16-ричное число, от которого надо взять остаток от деления на 8.
Ответ.
Первое, что приходит на ум. Нам достаточно знать последний символ 16-ричного числа, чтобы посчитать остаток от деления всего числа на 8.
Мы легко можем переписать решение на ruby с учетом вышесказанного так:
def node1(text)
s = Digest::SHA1.hexdigest(text)
return Integer('0x'+s[s.size-1, 1]) % 8
end
То же самое, но на Go:
func node1 (text string) int64 {
h := sha1.New()
h.Write([]byte(text))
sha1_hash := hex.EncodeToString(h.Sum(nil))
sha1_hash_len := len(sha1_hash)
last := sha1_hash[sha1_hash_len-1:sha1_hash_len]
value, _ := strconv.ParseInt(last, 16, 64) //int64
return value % 8
}
Второе, что удачно оказалось
В процессе написания кода, представленного выше, выяснилось, что Go в ходе выполнения SHA-1 представляет число в виде набора десятичных чисел, а потом уже их приводит к 16-ричному виду. Поскольку нам нужен остаток от деления, то мы можем просто взять последнее десятичное число вместо 16-ричного последнего символа и сэкономить на преобразованиях из одной системы счисления в другую.
func node2 (text string) int64 {
h := sha1.New()
h.Write([]byte(text))
mas := h.Sum(nil) // "hello world" -> [42 174 108 53 201 79 207 180 21 219 233 95 64 139 156 233 30 232 70 237]
return int64(mas[len(mas)-1]) % 8 // Берем последний элемент массива. Это целое десятичное число. И считаем остаток от деления на 8
}
Этот финт позволил еще быстрее вычислять то, что необходимо. Но на Ruby такой трюк выполнить не удалось. Поэтому корректнее сравнивать производительность node1 из Ruby и node1 из Go.
И так какая скорость работы у всего представленного?
Как я решил протестировать:
Посчитать, в какую базу данных положить данные, относящиеся к каждому из миллиона пользователей.
На Ruby:
for i in 1..1000000 do
node1("user:"+i.to_s)
end
и на Go:
for i := 1; i <= 1000000; i++ {
node1("user:"+string(i))
}
Полный код можно посмотреть на github.
Для чистоты эксперимента я запускал каждый скрипт по 10 раз, и ниже представлены средние значения.
Код запускался на версиях Ruby 1.8.7 и 2.1.3. Версия Go была 1.4.2
Значения в секундах. Меньше — лучше.
Ruby 1.8.7
node0 — 22.32
node1 — 17.24
node11 — 16.81
Ruby 2.1.3
node0 — 15.46
node1 — 11.15
node11 — 11.05
Go 1.4.2
node1 — 6.15
node2 — 4.36
Почему такие старые версии Ruby?
Код многих проектов я начал создавать 4-5 лет назад. По мере возможности я переносил его на новые версии Ruby. В частности на 2.1.3. Согласен, что на последней версии Ruby 2.2.2 результаты могут быть лучше, но явно не в два раза.
Выводы
- 1. Go 1.4.2 быстрее старых версий Ruby в 3 раза. И быстрее современных версий в 2 раза.
- 2. Ruby неплохо оптимизировали с версии 1.8.7 до 2.1.3. Прирост скорости на 25-30%.
- 3. Ruby, ты был верным другом и соратником, мы многое прошли вместе, но… я встретил Go. Теперь мне с ним по пути.