Зачитался я последнее время про Tarantool, интересно стало. Идея хорошая — код рядом с базой данных, хранимка в такой быстрой Redis-подобной среде.
И что-то задумался — мы вот сейчас используем активно на работе Golang, собственно, мысль пришла что на Go написано много всего, в т.ч. и встраиваемых баз. А что если сравнить, например, Go+LevelDB (собственно, можно было бы и любую другую) против Tarantool. Тестировал еще Go+RocksDB, но там оказалось все немного сложнее, а результат примерно тот же на небольших данных.
Тестировал простую задачу — HTTP сервер, при запросе — записать ключик в базу, достать его же по имени (без всяких проверок на race), отправить назад простенький JSON из этого value.
Сравнил: go+leveldb
, tarantool
, go+go-tarantool
, nginx upstream tnt_pass
Забегая вперед — в моем ненаучном тесте выиграл Go+LevelDB за счет использования всех ядер процессора. Скорее всего, если запустить несколько Тарантулов и балансировщик — выигрыш может какой-то и будет, но не сказать чтобы значительный… Но, правда, тут уже надо будет репликацию делать или что-то подобное.
Но, в целом, Tarantool — очень впечатляющая штука.
Обратите внимание: я сравниваю вполне конкретный случай, это не значит что во всех остальных случаях Go/LevelDB выиграет или проиграет.
Ну и еще: вместо LevelDB — вероятно, лучше использовать RocksDB.
Итак результат (кратко)
4-10
= 4 потока, 10 одновременных соединений
10-100
= 10 потоков, 100 соединений
Обратите внимание Tarantool занимает только 1 поток CPU (вернее по виду 2), а тестировалось на 4-поточном CPU. Go использует по умолчанию все ядра и потоки.
nginx lua tnt_pass взят из комментария dedokOne (результат)
wrk -t 4 -c 10
(4 потока, 10 соединений):
Golang:
Latency Distribution
50% 269.00us
99% 1.64ms
Requests/sec: 25637.26
Tarantool:
Latency Distribution
50% 694.00us
99% 1.43ms
Requests/sec: 10377.78
Но, Тарантул занял примерно только половину ядер, так что, вероятно, скорость у них — примерно одинаковая.
Под бОльшей нагрузкой (wrk -t 10 -c 100
) Тарантул остался на месте по RPS (а вот latency просела значительно заметнее чем у Golang, особенно верхняя часть), а Golang даже приободрился (но latency тоже просела, разумеется).
Go:
Latency Distribution
50% 2.85ms
99% 8.12ms
Requests/sec: 33226.52
Tarantool:
Latency Distribution
50% 8.69ms
99% 73.09ms
Requests/sec: 10763.55
У Tarantool есть свои примущества: secondary index, репликация…
У Go же есть огромная экосистема библиотек (около 100 тыс по моим подсчетам, среди них и реализаций встроенных (и не очень) баз данных — море), и, как пример, тот же bleve дает полнотекстовый поиск (чего, насколько я понял, например, нет в Tarantool).
По ощущениям экосистема Тарантула беднее. По крайней мере все, что предлагается — msgpack, http server, client, json, LRU cache,… в Go реализовано в бессчетных вариантах..
Т.е., в общем-то, безумного выигрыша скорости нет.
Пока что мой личный выбор остается в сторону Go, потому что нет ощущения что экосистема Tarantool выстрелит настолько сильно в ближайшее время, а Go — уже давно активнейше развивается.
Код на Tarantool, конечно, короче, но в основном, за счет того, что ошибки обрабатываются языком. В Go можно тоже вырезать все err
и останется примерно столько же.
Может у кого-то есть другие мнения?
Еще в комментариях заметили про атомарные обновления кода в Tarantool, но раз уж мы говорим про HTTP запросы — то мы (на текущем месте работы) используем endless для go и по нашим тестам (а у нас там тысячи запросов в секунду) — обновляем мы Go код без потери HTTP запросов. Пример в конце статьи.
И если подробнее про тест:
? ~ go version
go version go1.6 darwin/amd64
? ~ tarantool --version
Tarantool 1.6.8-525-ga571ac0
Target: Darwin-x86_64-Release
Golang:
? ~ wrk -t 4 -c 10 -d 5 --latency http://127.0.0.1:8081/
Running 5s test @ http://127.0.0.1:8081/
4 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 346.71us 600.80us 26.94ms 97.89%
Req/Sec 6.54k 0.88k 13.87k 73.13%
Latency Distribution
50% 269.00us
75% 368.00us
90% 493.00us
99% 1.64ms
130717 requests in 5.10s, 15.08MB read
Requests/sec: 25637.26
Transfer/sec: 2.96MB
Tarantool:
? ~ wrk -t 4 -c 10 -d 5 --latency http://127.0.0.1:8080/
Running 5s test @ http://127.0.0.1:8080/
4 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 767.53us 209.64us 4.04ms 87.26%
Req/Sec 2.61k 437.12 3.15k 45.59%
Latency Distribution
50% 694.00us
75% 0.90ms
90% 1.02ms
99% 1.43ms
52927 requests in 5.10s, 8.58MB read
Requests/sec: 10377.78
Transfer/sec: 1.68MB
Под большей нагрузкой:
Go:
? ~ wrk -t 10 -c 100 -d 5 --latency http://127.0.0.1:8081/
Running 5s test @ http://127.0.0.1:8081/
10 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 3.04ms 1.48ms 25.53ms 80.21%
Req/Sec 3.34k 621.43 12.52k 86.20%
Latency Distribution
50% 2.85ms
75% 3.58ms
90% 4.57ms
99% 8.12ms
166514 requests in 5.01s, 19.21MB read
Requests/sec: 33226.52
Transfer/sec: 3.83MB
Tarantool:
? ~ wrk -t 10 -c 100 -d 5 --latency http://127.0.0.1:8080/
Running 5s test @ http://127.0.0.1:8080/
10 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 10.65ms 14.24ms 269.85ms 98.43%
Req/Sec 1.09k 128.17 1.73k 94.56%
Latency Distribution
50% 8.69ms
75% 10.50ms
90% 11.36ms
99% 73.09ms
53943 requests in 5.01s, 8.75MB read
Requests/sec: 10763.55
Transfer/sec: 1.75MB
Исходники тестов:
Go:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/syndtr/goleveldb/leveldb"
)
var db *leveldb.DB
func hello(w http.ResponseWriter, r *http.Request) {
err := db.Put([]byte("foo"), []byte("bar"), nil)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
res, err := db.Get([]byte("foo"), nil)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
result, err := json.Marshal(string(res))
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
w.Write(result)
}
func main() {
var err error
db, err = leveldb.OpenFile("level.db", nil)
if err != nil {
panic(err)
}
http.HandleFunc("/", hello)
fmt.Println("http://127.0.0.1:8081/")
http.ListenAndServe("127.0.0.1:8081", nil)
}
Tarantool:
#!/usr/bin/env tarantool
box.cfg{logger = 'tarantool.log'}
space = box.space.data
if not space then
space = box.schema.create_space('data')
space:create_index('primary', { parts = {1, 'STR'} })
end
local function handler(req)
space:put({'foo','bar'})
local val = space:get('foo')
return req:render({ json = val[2] })
end
print "http://127.0.0.1:8080/"
require('http.server').new('127.0.0.1', 8080)
:route({ path = '/' }, handler)
:start()
Golang (атомарная заменой кода, без потери соединений):
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"syscall"
"io/ioutil"
"time"
"github.com/fvbock/endless"
"github.com/gorilla/mux"
"github.com/syndtr/goleveldb/leveldb"
)
var db *leveldb.DB
func hello(w http.ResponseWriter, r *http.Request) {
if db == nil {
// (необязательная) гарантия себе, что тест и правда отработал
panic("DB is not yet initialized")
}
err := db.Put([]byte("foo"), []byte("bar"), nil)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
res, err := db.Get([]byte("foo"), nil)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
result, err := json.Marshal(string(res))
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
w.Write(result)
}
func main() {
var err error
mux1 := mux.NewRouter()
mux1.HandleFunc("/", hello).Methods("GET")
fmt.Println("http://127.0.0.1:8081/")
server := endless.NewServer("127.0.0.1:8081", mux1)
server.BeforeBegin = func(add string) {
ioutil.WriteFile("server.pid", []byte(fmt.Sprintf("%d", syscall.Getpid())), 0755)
db, err = leveldb.OpenFile("level.db", nil)
for err != nil {
time.Sleep(10 * time.Millisecond)
db, err = leveldb.OpenFile("level.db", nil)
}
}
server.ListenAndServe()
if db != nil {
db.Close()
}
}
После этого можно сделать go build
запустить и попробовать во время нагрузки делать go build; kill -1 $(cat server.pid)
— в моих тестах потери данных не наблюдалось.
В комментариях порекомендовали попробовать go+go-tarantool
Попробовал:
Меньшая нагрузка
? ~ wrk -t 4 -c 10 -d 5 --latency http://127.0.0.1:8081/
Running 5s test @ http://127.0.0.1:8081/
4 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 799.14us 502.56us 25.22ms 95.74%
Req/Sec 2.55k 248.65 2.95k 85.22%
Latency Distribution
50% 727.00us
75% 843.00us
90% 1.02ms
99% 2.03ms
51591 requests in 5.10s, 5.95MB read
Requests/sec: 10115.52
Transfer/sec: 1.17MB
Большая нагрузка:
? ~ wrk -t 10 -c 100 -d 5 --latency http://127.0.0.1:8081/
Running 5s test @ http://127.0.0.1:8081/
10 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 7.49ms 4.00ms 65.06ms 81.21%
Req/Sec 1.38k 357.31 8.40k 94.61%
Latency Distribution
50% 6.78ms
75% 8.86ms
90% 11.77ms
99% 22.74ms
69091 requests in 5.10s, 7.97MB read
Requests/sec: 13545.12
Transfer/sec: 1.56MB
Исходник:
tarantool.lua:
#!/usr/bin/env tarantool
box.cfg{ listen = '127.0.0.1:3013', logger = 'tarantool.log' }
space = box.space.data
if not space then
box.schema.user.grant('guest', 'read,write,execute', 'universe')
space = box.schema.create_space('data')
space:create_index('primary', { parts = {1, 'STR'} })
end
print(space.id)
print('Starting on 3013')
main.go:
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
"github.com/tarantool/go-tarantool"
)
var client *tarantool.Connection
func hello(w http.ResponseWriter, r *http.Request) {
spaceNo := uint32(512)
_, err := client.Replace(spaceNo, []interface{}{"foo", "bar"})
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
indexNo := uint32(0)
resp, err := client.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"foo"})
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
first := resp.Data[0].([]interface{})
result, err := json.Marshal(first[1])
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
w.Write(result)
}
func main() {
var err error
server := "127.0.0.1:3013"
opts := tarantool.Opts{
Timeout: 500 * time.Millisecond,
}
client, err = tarantool.Connect(server, opts)
if err != nil {
log.Fatalf("Failed to connect: %s", err.Error())
}
http.HandleFunc("/", hello)
fmt.Println("http://127.0.0.1:8081/")
http.ListenAndServe("127.0.0.1:8081", nil)
}
Комментарии (95)
danikin
24.04.2016 10:58+1В первую очередь, Слава, спасибо вам огромное за тест!
Я же правильно понимаю, что тест запускался на той же машине, что и тестируемая система?yoihj
24.04.2016 11:14Все верно, как и сказано — сугубо ненаучно :) Моя задача была понять для себя — есть ли в Tarantool такое огромное преимущство, что стоит отказаться от Golang или использовать рядом с ним. Для этого вполне достаточно на одной машине сравнить — ведь нагружающий `wrk` одинаковое количество ресурсов скушает, так что отличие по производительности будет зависеть только от тестируемой стороны. Какая быстрее — та и быстрее :)
danikin
24.04.2016 11:54+10На самом деле, тест хороший. И хоть и назван «ненаучным» он показывает явно, что golang + leveldb в каких-то кейсах быстрее Tarantool.
При этом у Tarantool перед goland + leveldb есть вполне себе очевидные преимущества:
1. Обновление кода без перезагрузки сервера
2. Хранимые процедуры
3. Репликация master-slave и master-master (важное преимущество — ибо с leveldb придется данные куда-то как минимум rsync'ать, ибо иначе стремно)
4. Другие СУБД-фишки (транзакции, например)
5. Поддержка других языков кроме Lua (сейчас это C и Swift, в будущем появятся и другие)
Кроме того, очевидно, что выставлять http-сервер на go в интернет плохо, т.к. надо терминировать SSL, надо делать различные административные действия (типа как rewrite и прочие), надо хорошо обрабатывать медленных клиентов, надо отдавать статику в конце концов. Со всем этим как мы прекрасно знаем прекрасно справляется nginx. Т.е. ставить nginx перед своим любимым http-сервером — это сейчас самый массовый кейс. А раз спереди ставится nginx, то его можно пускать в Tarantool не по медленному http, а по быстрому бинарному протоколу, например, как это было сказано тут: https://habrahabr.ru/company/mailru/blog/272141/
Кажется, что такая связка должна уделать golang, мы это обязательно протестируем, используя ваши тесты и выложим результаты.
Кроме того, если в app server'е есть внутри хранилище, то надо чтобы кто-то сверху роутил запросы на слейв, если упал мастер. В случае Тарантула есть стандартный nginx-модуль, который умеет это делать (см. статья выше). А в случае go + leveldb что делать? Плюс, как я сказал уже выше, репликацию leveldb придется писать руками.
В целом же, спасибо, что показали, что http-сервер в Тарантуле еще далек по производительности от совершенства. Мы стремимся к тому, чтобы даже на одном ядре работать быстрее чем все аналоги на всем сервере. Будем улучшать! :)yoihj
24.04.2016 12:29+1Отличные комментарии. Пару вещей хотелось бы правда, уточнить:
> 1. Обновление кода без перезагрузки сервера
Это я показал уже в статье после комментария — это более чем возможно.
> 2. Хранимые процедуры
Собственно говоря, Golang это и есть в данном случае «хранимая процедура». Просто склейка получается в другом направлении — к языку мы приклеиваем базу (golang + leveldb/rocksdb), а не к базе приклеиваем язык (tarantool.box + lua), но результат-то тот же получается — быстрый язык рядом с базой.
> 3. Репликация master-slave и master-master
Это абсолютно верно и это я и указал в статье — репликация, пока что, это важное достоинство, что есть у Tarantool. Хотя master-master я что-то не нашел (плохо искал?) А вот master-slave меня напугал когда прочитал что-то в духе, что изменение одних и тех же данных на двух разных slave полностью останавливают базу и требуется вмешательство админов… Вот тут у меня наступил некий ступор с тем как Mail.Ru справляется с этим на огромных объемах.
> 4. Другие СУБД-фишки (транзакции, например)
LevelDB, вроде, не поддерживает (не специалист тут, к сожлению), но вот RocksDB (следующее поколение от LevelDB) уже поддерживает.
Еще, к списку я бы добавил secondary index — это действительно тоже сильная сторона Tarantool.
> Кроме того, очевидно, что выставлять http-сервер на go в интернет плохо
Моя задача на самом деле не написание серверов для интернета, а бэкэндовых серверов для внутреннего потребления. Но я не скажу что выставлять go http это плохо или прямо драматично хуже чем nginx. Да, мы используем nginx почти везде и его преимущества очевидны, но, не все описанное Вами верно. Некоторые из них:
> делать различные административные действия (типа как rewrite и прочие)
Собственно, через gorilla/mux это решается примитивнейше в go.
> хорошо обрабатывать медленных клиентов
Golang с этим справляется ничуть не хуже nginx, в общем-то. Такие же мультиплексированные потоки на несколько тредов + асинхронная обработка.
> надо отдавать статику в конце концов
Тоже, в общем-то, достойно работает в Go.
Я не говорю что nginx это плохо, опять же — мы его используем почти везде, но я бы не был так категоричен с «выставлять http-сервер на go в интернет плохо».
> мы это обязательно протестируем, используя ваши тесты и выложим результаты.
Будет очень интересно прочитать. Я, если честно, вообще удивился что тестирования golang+X vs tarantool не нашел сходу.
> А в случае go + leveldb что делать?
Не очень понятна сложность, это вроде стандартный nginx upstream + proxy_next_upstream умеют делать?
> Будем улучшать! :)
:thumbs_up:
Ну и как бы мое мнение не является тут окончальным для всех — всегда нужен контекст. Наш контекст что у нас уже есть специалисты на Go + много кода и если бы вдруг Tarantool показал бы результаты там в 10 раз лучше — ну тогда бы имело смысл, конечно. А так переход не имеет большого смысла.yoihj
24.04.2016 12:34> Обновление кода без перезагрузки сервера
Чуть ошибся. Имел в виду, что возможно это делать «без потери запросов», а не без перезагрузки :) Пардон.danikin
24.04.2016 12:41+1Перезагрузка сбрасывает все кэши в LevelDB и приводит к холодному старту. Холодный старт — это одна из главных болей on-disk storage engine.
lomik
24.04.2016 16:03В Вашем примере новый инстанс сервера будет ждать (не обрабатывать новые запросы) пока старый инстанс не обработает ВСЕ текущие запросы (и не отпустит leveldb чтобы новый инстанс мог ее взять).
Это в общем случае может приводить к большому лагу во время обновления кода.
Или я неправильно понял пример?yoihj
24.04.2016 16:24Все верно. Для моего случая это скорее недостаток, для кого-то, вероятно, — критично.
mantyr
24.04.2016 20:53+1Можно написать код по передаче горячих данных от одного инстанца к другому. А так же сделать прослойку из goprotobuf, net/rpc поверх unix socket или любого другого протокола для связи с приложениями которым только доступ к данным нужен без вмешательство в работу самой базы.
Репликацию (sync) данных между двумя и более инстансами (даже сетевыми) делать на Golang не так сложно — это лишь всеголиш данные которые можно пушить сразу на два и более серверов, можно даже помечать в мастере те данные что успешно запушились куда-то ещё и при старте смотреть какие данные не помечены и отправлять их на реплику.
Думаю в статье основной посыл в следующем — разрабатывают Tarantool только +- десять человек, по большей части из Mail.Ru Group для себя или для корпоративных клиентов которым нужна поддержка, в то же время Golang пилится тысячей-другой человек по всему миру по всем возможным направлениям и то чего нет в Golang сейчас может совершенно спокойно появиться в течении пары дней, а не месяцев.youROCK
25.04.2016 12:41+1Это распространенное заблужение, что golang разрабатывают много человек. Контрибьютят, возможно. много, но именно активную разработку ведут все те же 10 человек плюс-минус лапоть (пруф предоставить не могу, ибо по «google dev team» ничего внятного не находится, но о размере своей команды они довольно часто говорят на разных презентациях).
mantyr
25.04.2016 17:08+2Я имел в виду разработку 100500 всевозможных пакетов на Golang и для Golang. Понятно что в сам рантайм коммитят не многие. Но всё что вокруг — это уже тысячи людей. Другой вопрос — как много людей пишут плагины и прочий код сопутствующий к Tarantool?
Понятно что сюда входят все кто пишет на LUA и для LUA, но… тогда надо сравнивать экосистему и сообщество LUA и Golang разработчиков как таковых.
Так же не маловажно — хочется ли конкретным разработчикам разбираться и использовать LUA только ради базы данных или же у них вся инфраструктура на LUA. Не видел ни одной компании у которых всё пишется на LUA. В тоже время есть не мало компаний в которых пишут исключительно на Golang.
danikin
24.04.2016 12:39+13. master-master: http://tarantool.org/doc/book/replication/index.html, п. 4.8. Насчет master-slave — данные меняются только на master, поэтому такой проблемы нет. Однако на master-master такая проблема может быть, и если ваш application не может хэндлить этот кейс, то лучше использовать master-slave. При этом не надо вмешательство админов и ничего не останавливается. Могу рассказать детально как это работает, например, позвонив по скайпу. Мой скайп danikin2. Стучитесь, не стесняйтесь :)
4. Кстати, да. Соглашусь :)
5. Если у вас go смотрит в интернет и сам все это делает, включая терминацию SSL, статику и все остальное, то это очень круто! Было бы интересно почитать статью про это, а также про сравнение с nginx!
6. «nginx upstream + proxy_next_upstream» — ну, не совсем. Если, к примеру, у вас локально все затормозило, и в этот конкретный сервер ходить нельзя, то nginx это не запомнит, насколько я знаю, и будет пытаться ходить туда каждый раз. Но, в целом, если такое решение устраивает, то да, почему бы нет. Вообще, я неоднократно это говорил — я за мир во всем мире и за те решения, которые решают проблемы. Если у вас с вашим решением проблем нет, то все круто и менять его не на что не надо! :)
lomik
24.04.2016 16:12Раз уж речь зашла не только о быстродействии, но и о эксплуатационных качествах, то есть вопрос. Как правильно бекапить приложение, базирующееся на leveldb?
yoihj
24.04.2016 16:23Я тут без понятия — я просто знал что для Go есть несколько KV хранилищ, взял первые два попавшиеся ради интереса :) Никогда не эксплуатировал ее. Насколько я понимаю, авторы (Facebook) рекомендуют все же идти дальше и пользовать RocksDB (который у меня просто не скомпилировался в виде embedded, только в виде shared библиотеки, что не очень мне понравилось, поэтому открыл им issue и оставил за рамками статьи)
Думаю гугление на тему backup leveldb должно раскрыть тайну :)
Mozzi
24.04.2016 12:11+2Сам тестил leveldb, sophia, rocksdb на Go.
C leveldb всё было отлично, но как только повышал количество документов с нестандартно большой длиной, он начинал тупить, это было предсказуемо и понятно.
sophia реально удивила, очень быстро, и при возрастании нагрузки деградирует очень медленно, но при длительных тестах заметил, что (WAL был отключён) терят некоторые записи. Свежих биндингов я не нашёл, сварганил их быстро сам, поэтому есть подозрение, что виной всему кривые руки.
rocksdb начали тестировать когда было окончательно принято решение, что сервак будет ssd-шным. Перетестировав предыдущие решения и сравнив их с rocksdb — последний остался победителем.
Но sophia, оставила очень тёплые впечатления и я обязательно попробую расковырять причины моих неудачных.danikin
24.04.2016 12:43+2А можете чуть детальней расписать про софию? Например, мне в скайп danikin2. Нам было бы очень интересно поисследовать ваши кейсы, особенно с потерей записей? С выключенным WAL, кстати, записи и должны теряться, софия же в памяти все не хранит, лучше WAL включить, он особой нагрузки не делает, а если делает, то нам было бы интересно взглянуть на это :)
erlyvideo
24.04.2016 12:24я не очень понял сравнение. Тарантул — это кортежная БД с кучей фич. leveldb — это примитивный keyvalue с заявленным хранением хуже чем в тарантуле.
Чего сравнивали то?yoihj
24.04.2016 12:33+6С таким подходом вообще ничего в мире сравнивать нельзя. Как пример: MySQL это база со многими engine, а Redis это key-value кэш с другими характеристиками. Но неужели это автоматически обозначает что я не могу одну и ту же задачу решить на MySQL и на Redis? В одном случае мне одни плюшки предоставит система, а другие придется делать руками, в другом — другие предоставит другая система, а в первой их придется делать руками.
Тот же случай и тут. Я взял общий делитель — операцию которую можно сделать в обоих случаях. И сравнил ее по скорости. В чем проблема?
ainu
24.04.2016 14:07+1Решение поставленной задачи сравнивали. Её еще на mysql можно решить, а еще на perl скрипт написать и в файлах хранить, а ещё на node.js + mongodb. Задача оттого не изменится.
dedokOne
24.04.2016 15:13ИМХО Сравнение очень даже интересно, с учетом что тут не только DB тестировалось, но и способы доставки.
danikin
24.04.2016 12:45+5Я вот думаю, а почему не совместить приятное с полезным? HTTP-сервер писать на Go, а при этом в качестве СУБД оставить Tarantool. Скорее всего это будет быстрее чем Go + LevelDB. И при этом все свойства СУБД автоматом останутся.
yoihj
24.04.2016 15:28На один комментарий ниже отписался. https://habrahabr.ru/post/282299/#comment_8867187
yoihj
24.04.2016 16:25+1Добавил сравнение в конец статьи, оказалось, к сожалению, не быстрее. Думаю что это сильно связано с тем, что постоянно приходится типы из interface{} в строки переводить — но тут так реализован клиент — процессоры горят на полную.
xlin
24.04.2016 13:59А что если попробовать провести тесты не с leveldb, а с tarantool? Т.е. Go+go-tarantool и замерить.
https://github.com/tarantool/go-tarantoolyoihj
24.04.2016 15:27Собственно, меня от этого остановило то, что хотелось попробовать именно «сервер БД прямо рядом с кодом» — как это и сделано в tarantool.
dedokOne
24.04.2016 14:53tarantool http не оптимизирован под большие нагрузки, для этого есть nginx upstream http://github.com/tarantool/nginx_upstream_module
Надо собрать nginx с этим модулем, взять master пока, тег не добавил.
Немного под хачил lua, чтобы через nginx можно было ходить
https://gist.github.com/dedok/bd6b32914dbbedf3d9a78bd50ad83f48
nginx.conf — он с новыми фичами
https://gist.github.com/dedok/d84b82b2863b778f47ca05c17cbf3b25
Ну и разбалансировать либо на 1-н, как ты сделал, но выставить go runtime.GOMAXPROCS(1)
Либо балансировать на некое кол-во cpu.
С nginx CPU подлетит, но, как правило, у всех стоит nginx перед backend'ом.
Так что его в потребителях можно учесть :)
Думаю, так будет немного по научному.
Кстати спасибо автору, благодаря статье, первый раз по трогал go, очень понравилось.yoihj
24.04.2016 15:26Ну ок, у всех сейчас многоядерные сервера и машины и как бы в один поток ограничивать go — смысла большого нет, ибо на бою хочется именно использовать по-максимуму ресурсы системы, и вот тут вопрос.
А как использовать все ядра-то на Tarantool? Т.е. я так понял что для этого надо запускать несколько tarantool и включать на них master-master репликацию и потенциально связываться с конфликтами записи? Master-slave я так понимаю не решит тут, если речь идет о равном количестве записей и чтений (т.е. условно write-bound нагрузка)dedokOne
24.04.2016 15:46+1Согласен — насчет ядер.
Да надо несколько tarantool запускать, из либо sharding(https://github.com/tarantool/shard) в tarantoolы ходить, или делать хитрый балансинг на уровне nginx upstreams (писать на lua/ngscript).
Репликация скорей всего не подойдет, мы же не про put(const, const) говорим?:)
ilnarb
24.04.2016 15:54+1И я с этим согласен.
Тарантул не потребляет все ядра эффективно, на серверах у нас их 12*2HT.
В этом случае частый ответ разработчиков: запустить несколько инстанцев.
Такое разделение
— резко снижает объем памяти одного инстанца, т.к. они его между собой не шарят,
— требуется писать код шардинга, и, иногда, стыковать данные из разных шардов.
Проблему неиспользованных ядер в целом можно решить, если на тот же сервер сажать еще какие-то демоны, тот же Nginx, например.
yoihj
24.04.2016 17:54Да, там еще нюанс упущен
https://gist.github.com/dedok/d84b82b2863b778f47ca05c17cbf3b25
Я финальным шагом `bar` в json конвертирую, т.е. в `«bar»` — конвертация в JSON и обратно зачастую тоже довольно медленная вещь. Поэтому и в Go, и в Tarantool проверял это последним шагом.babylon
26.04.2016 16:51+1Зачем конвертировать в JSON? Обработка массивами самая быстрая. Рутировать массивы в JSON имеет смысл тогда, когда нужно зафиксировать связи в течении рабочей сессии, или для исполнения кода, допустим.Достаточно иметь схемы и плоские буферы.
yoihj
27.04.2016 00:42конвертация в JSON и обратно зачастую тоже довольно медленная вещь
я пытался в минимальном самом простом виде воспроизвести JSON API доступный по HTTP. последним шагом в JSON API идет конвертация в JSON
cy-ernado
24.04.2016 16:14+1Если необходим действительно быстрый http на Go:
https://github.com/valyala/fasthttp
Кстати, у Go есть достойная Lua VM:
https://github.com/yuin/gopher-lua
Сам сейчас в раздумьях, какую k-v использовать в своих проектах для решения различных задач. На работе мы используем свои биндинги к LMDB, для меня это не подходит, т.к. мне нужна кросс-компиляция в некоторых случаях. Посмотрел на tarantool — ручной шардинг мне не понравился, хочется чтобы «оно само».
Из интересных проектов, есть еще tikv:
https://github.com/pingcap/tikv
Распределенная key-value c транзакциями на Rust, которая будет использоваться в качестве бекенда для другого проекта (tidb) на го.
ilnarb
24.04.2016 16:24+1Как я понял, тест был многопоточный и запросы шли один за другим.
1. При таком подходе сетевой стек линукса довольно быстро ограничит RPS еще до того как все CPU будут использованы (речь про средние сервера, а не десктоп).
2. Тарантул хорошо себя показывает в batch запросах, где пачками грузятся запросы, и так же пачками получаются ответы. Но при этом сами запросы должны быть простые: не более одного меняющего данные операции (в CALL) для 1.5 или же нужно использовать транзакции в 1.6.
3. Плохое использование ядер окупается ускорением за счет отсутствия локов. На практике это надо мерить, но любители корутин/файберов будут бить в грудь даже без тестов (что обычно бывает оправданным).
Протестируйте на таких кейсах как batch запросы, используя его бинарный протокол. На таких кейсах он наверняка обгонит что-то самописное.
Надо понимать, что дает конкретное СУБД и чем за это придется платить. Для каждой базы есть как свои плюсы, так и свои минусы.yoihj
24.04.2016 17:04Тест через wrk = многопоточный + мультиплексирование (4 потока, 10 соединений и 10 потоков, 100 соединений)
К сожалению, уже не осталось времени тестировать что-то подобное. Вполне возможно, хотя последний добавленный тест go+go-tarantool, в общем-то, надежды большой не дал.ilnarb
24.04.2016 17:40Жаль. т.к. в текущем виде этот тест больше смахивает на тест http серверов go и tarantool
yoihj
24.04.2016 17:44P.S. Бинарный протокол-то я протестировал из Go (там добавил в статью) и результат примерно на уровне. Т.е. теории 2 у меня — либо клиент go-tarantool тормозит (что не исключено из-за использования interface{}, но оно тут неизбежно), либо все же дело не в http сервере tarantool.
youlose
24.04.2016 17:07Хотелось бы увидеть тест где вы реализуете ВЕСЬ функционал tarantool. А то так можно и с фотошопом сравнивать Go, реализовав только какой либо один фильтр картинки =)
yoihj
24.04.2016 17:13Ну, я думал это само собой разумеется, что тест для отдельного случая и я вовсе не говорю что Tarantool надо забросить и он никуда не годится.
Просто в обратную сторону тоже можно также бросить — реализуйте в Tarantool ВЕСЬ функционал всех библиотек Go, а потом сравнивайте…
Сравнение вполне конкретное, более того — Tarantool выглядит достойно. Я описал минусы по которым он мне в моем случае не подходит, но это ж не значит что это случай всех и вся.youlose
24.04.2016 17:21Так вы же начали сравнивать, потому сравнивайте не тёплое с мягким, а реализацию ПОЛНОГО функционала Tarantool на C/C++ с реализацией этого же функционала на Go. А то какая-то манипулирование неокрепшими умами получается…
yoihj
24.04.2016 17:29+1Я уже отвечал на этот аргумент тут
Если мне нужен Фотошоп для одного фильтра — я имею право сравнивать Фотошоп и программу, имеющую только этот один фильтр.
Реализация k-v хранилища тут примерно одинаковая. Да, в Tarantool есть другие возможности — я это написал в статье и в комментариях несколько раз. Да, сравнивается конкретный случай — я это тоже написал и в статье, и в комментариях.youlose
24.04.2016 18:00Судя по коду ваша задача заключается в том, чтобы класть bar в foo, десятки тысяч раз общаясь через HTTP протокол, да тут вряд ли какие-либо коробочные решения помогут =).
Мало того даже в сравниваемом коде разный функционал, в tarantool, например используется лог файл, что по определению создаёт неслабую нагрузку на дисковую подсистему.
Да и понятно бы было если бы хотя бы свою реализацию KV хранилища показали бы, а то получается что тестируем кастрированный веб-сервер на Go + KV хранилище на C vs целую базу данных и сервер аппликаций.evnuh
25.04.2016 14:35+1Да и понятно бы было если бы хотя бы свою реализацию KV хранилища показали бы
хехе, мощно
dedokOne
24.04.2016 17:42+2Забавный момент, пустил go(во все потоки) через nginx(1 worker) proxy_pass с keepalive получил:
wrk -t 10 -c 100 -d 5 --latency http://127.0.0.1:8081/go
Running 5s test @ http://127.0.0.1:8081/go
10 threads and 100 connections
Thread Stats Avg Stdev Max ± Stdev
Latency 23.76ms 21.26ms 165.45ms 94.74%
Req/Sec 477.19 106.88 594.00 87.35%
Latency Distribution
50% 19.23ms
75% 19.93ms
90% 21.46ms
99% 153.41ms
16361 requests in 5.10s, 2.59MB read
Requests/sec: 3206.89 (максимум ~5к было — скачет CPU — вырубать все лень, мак домашний!)
Transfer/sec: 519.87KB
Пусканул tarantool (tnt_pass)
Running 5s test @ http://127.0.0.1:8081/tnt
10 threads and 100 connections
Thread Stats Avg Stdev Max ± Stdev
Latency 7.48ms 2.44ms 18.02ms 74.22%
Req/Sec 1.33k 386.69 5.49k 86.45%
Latency Distribution
50% 7.71ms
75% 8.76ms
90% 9.80ms
99% 14.88ms
66431 requests in 5.10s, 13.12MB read
Requests/sec: 13017.25(максимум ~17к было, скачет CPU — вырубать все лень, мак домашний!)
Transfer/sec: 2.57MB
PS keepalive выкрутил, даже dtrace accept сделалyoihj
24.04.2016 17:57+1Заинтриговали, пойду tnt_pass попробую :)
dedokOne
24.04.2016 18:04+1надо на epoll попробовать это все с kqueue, завтра вчером с epoll, сегодня нет возможности, протестирую скину все результаты и trace будем считать accept и т.д.
А вообще гоу к нам в чат :)yoihj
24.04.2016 18:09Вроде поставил все, но результат что-то немного другой:
? nginx git:(master) ? curl 127.0.0.1:8081/tnt {"id":0,"result":[["bar"]]}
а должен быть
"bar"
хотя вроде
return { var[2] } -- только bar
А где чат?
yoihj
24.04.2016 18:11В целом (у меня тоже мак, так что оставил kqueue) nginx_upstream получилось так (код взят из gist)
? nginx git:(master) ? curl 127.0.0.1:8081/tnt {"id":0,"result":[["bar"]]}% ? nginx git:(master) ? wrk -t 4 -c 10 -d 5 --latency http://127.0.0.1:8081/tnt Running 5s test @ http://127.0.0.1:8081/tnt 4 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 550.88us 144.06us 2.28ms 74.29% Req/Sec 3.61k 536.23 4.10k 81.37% Latency Distribution 50% 516.00us 75% 610.00us 90% 766.00us 99% 0.96ms 73337 requests in 5.10s, 14.55MB read Requests/sec: 14381.01 Transfer/sec: 2.85MB ? nginx git:(master) ? wrk -t 10 -c 100 -d 5 --latency http://127.0.0.1:8081/tnt Running 5s test @ http://127.0.0.1:8081/tnt 10 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 9.14ms 2.49ms 18.70ms 66.40% Req/Sec 1.09k 437.60 9.36k 93.01% Latency Distribution 50% 9.31ms 75% 10.88ms 90% 12.11ms 99% 13.64ms 54184 requests in 5.10s, 10.75MB read Requests/sec: 10626.29 Transfer/sec: 2.11MB
dedokOne
24.04.2016 18:14+1А если на go backend через proxy_pass с keepalive?
yoihj
24.04.2016 18:21+1nginx.conf
upstream go { server 127.0.0.1:8080; keepalive 10000; } upstream tnt { server 127.0.0.1:10001; # ходим в 1 tarantool -- можно балансировать в больше keepalive 10000; } server { listen 8081 default; server_name tnt; location = /tnt { tnt_pass_http_request on; # пропускам http данные tnt_http_rest_methods get; # только get tnt_method 'handler'; # вызываем function handler() tnt_pass tnt; } location = /go { proxy_pass go; } }
Test:
? nginx git:(master) ? curl 127.0.0.1:8081/go "bar"% ? nginx git:(master) ? wrk -t 4 -c 10 -d 5 --latency http://127.0.0.1:8081/go Running 5s test @ http://127.0.0.1:8081/go 4 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 297.07us 188.09us 5.74ms 92.84% Req/Sec 6.79k 353.23 7.91k 79.80% Latency Distribution 50% 268.00us 75% 352.00us 90% 444.00us 99% 841.00us 137159 requests in 5.10s, 15.83MB read Requests/sec: 26896.15 Transfer/sec: 3.10MB ? nginx git:(master) ? wrk -t 10 -c 100 -d 5 --latency http://127.0.0.1:8081/go Running 5s test @ http://127.0.0.1:8081/go 10 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 3.11ms 1.33ms 17.15ms 76.48% Req/Sec 3.24k 1.05k 25.94k 99.60% Latency Distribution 50% 3.02ms 75% 3.73ms 90% 4.60ms 99% 7.18ms 161791 requests in 5.10s, 18.67MB read Requests/sec: 31711.10 Transfer/sec: 3.66MB
ilnarb
24.04.2016 23:59+1tnt_pass ожидаемо дает больше, т.к. в этом случае:
1. http часть работ вынесена в nginx
2. запросы идут мультиплексом, т.е. batch
Это и показывает, что тесты из статьи в основном
— тестируют реализацию http, а она там в том же потоке луа работает.
— не загружают сам тарантул, утыкаясь в узкое горлышко на http уровне.
Вывод: для http лучше использовать nginx модуль с tnt_pass.
zartdinov
25.04.2016 12:45Да, если еще актуально)
У библиотеки goleveldb, которую вы подключаете есть mem storage, было бы тоже интересно посмотреть в сравнении с tarantool, которая позиционирует себя как in-memory dbyoihj
27.04.2016 00:44Только вот характеристики тут тогда принципиально отличаются. Тарантул (насколько я понимаю) если сказал "сделано" — то оно сохранено на диске. А вот in-memory не сохраняют на диск ничего.
youROCK
Я об этом говорил авторам тарантула чуть ли не с самого начала: посмотрите на го :).
С другой стороны, они все же немного о разном. В tarantool хранимые процедуры на lua и их можно (атомарно) обновлять без перезагрузки сервера и не теряя кеши. Если бы такое можно было делать в golang, то наверное ниши для тарантула не осталось бы. А пока что go не может целиком заменить ни nginx+lua, ни tarantool+lua, к сожалению :(.
zartdinov
Как понял здесь роль хранилища выполняет leveldb, а go используется лишь как быстрый веб сервер, поэтому не надо все перезапускать
yoihj
Не совсем, leveldb встроен как библиотека и по сути хранит все в файле. Т.е. перезапускается все.
zartdinov
Да, увидел в коде подключение реализации leveldb на go. Не специалист по go, возможно стоило использовать обертку на go для leveldb или вынести в отдельную программу и общаться с ней. Понятно, что это будет чуть медленней, но решило бы проблему атомарности.
yoihj
Я, кстати, вот про это атомарное обновление не нашел, хороший момент. С другой стороны — раз уж мы говорим про http — то есть https://github.com/fvbock/endless который как раз и делает атомарное обновление — им мы на работе и пользуемся чтобы там, имея тысячи запросов в секунду к бэкэнду — без потерь запросов обновлять код.
youROCK
Как вы понимаете, кеши вы всё равно потеряете, да и вряд ли это хорошая идея — работать с leveldb базой из двух процессов. Так что с рестартом так просто не выйдет.
yoihj
Верная мысль, в теории можно попробовать RocksDB или какие-то другие которые более хорошо умеют с lockами работать. Там в принципе, лок-то нужен только на время рестарта.
yoihj
Добавил в конец статьи пример замены кода.
mantyr
Есть вариант подключить интерпретатор LUA и менять скрипты атомарно уже в Golang сервере с LevelDB или любой другой встраиваемой базой.
Готовые интерпретаторы LUA пока не попадались, а вот javascript уже пару раз видел. Похоже что с такой целью его (js) в телефонной платформе ITooLabs используют (судя по докладу www.youtube.com/watch?v=HoEn7lXNQOU )
growler
Так и есть, используем.
Причем, начинали мы как раз с LuaJIT, но потом отказались — слишком велики были потери на сериализацию/десериализацию.
rtsisyk
Обновлять код без перезагрузки сервера можно в Erlang ;)
Кстати, в Tarantool можно писать хранимки на C, а также уже есть прототип хранимок на Swift.
yoihj
> Обновлять код без перезагрузки сервера можно в Erlang ;)
Ну я абсолютно лично за то, чтобы это было во всех языках :)
erlyvideo
это очень легко делается в языках, где код и данные разнесены, но практически нерешаемая задача в языках, где код и данные живут в одном пространстве.
Даже в Эрланге есть ситуация, когда обновление кода становится проблемой из-за склейки кода и данных и её решают брутально.
danikin
В Tarantool как раз код и данные живут в одном процессе, и при этом код можно менять без перезагрузки Tarantool :)
erlyvideo
я не про процесс, а про логическое разделение.
Например, в С++ нет внятного механизма пройтись по всем экземплярам класса A и поменять им пойнтеры на методы класса. А в эрланге это сильно проще, потому что нет классов и их методов.
ilnarb
Может вы достанете птицу феникса? (я про javascript)
Его знают многие, его изучение оправдано с так же с использованием на клиент сайде, да и нода-пользователи…
DjOnline
Тогда уж и php7 достать, его тоже многие знают.
ilnarb
php7 был вторым языком, а js идет первым потому, что они это уже делали, только не поддерживают.