Зачитался я последнее время про 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)
}