Суто ненауково: Tarantool vs Golang (швидкості)

Зачитався я останнім часом про Tarantool, цікаво стало. Ідея хороша — код поряд з базою даних, хранимка в такій швидкій Redis-подібної середовищі.
І що-то задумався — ми ось зараз активно використовуємо на роботі Golang, власне, думка прийшла на Go написано багато всього, в т. ч. і вбудованих баз. А що якщо порівняти, наприклад, Go+LevelDB (власне, можна було б і будь-яку іншу) проти Tarantool. Тестував ще Go+RocksDB, але там виявилося трохи складніше, а результат приблизно на невеликих даних.
Тестував просту задачу — HTTP сервер, при запиті — записати ключик в базу, дістати його по імені (без всяких перевірок на race), відправити назад простенький JSON з цього value.
Забігаючи вперед — в моєму ненауковому тесті виграв Golang за рахунок використання всіх ядер процесора. Швидше за все, якщо запустити кілька Тарантулів і балансувальник — виграш може якийсь і буде, але не сказати щоб значний… Але, правда, тут вже треба буде реплікацію робити або щось подібне.
Отже результат (коротко)
wrk -t 4 -c 10
(4 потоку, 10 сполук):
Golang:
Latency Distribution
50% 269.00 us
99% 1.64 ms

Requests/sec: 25637.26

Tarantool:
Latency Distribution
50% 694.00 us
99% 1.43 ms

Requests/sec: 10377.78

Але, Тарантул зайняв приблизно половину ядер, так що, ймовірно, швидкість у них приблизно однакова.
Під більшим навантаженням (
wrk -t 10 -c 100
) Тарантул залишився на місці за RPS (а ось latency просіла значно помітніше, ніж у Golang, особливо верхня частина), а Golang навіть підбадьорився (але latency теж просіла, зрозуміло).
Go:
Latency Distribution
50% 2.85 ms
99% 8.12 ms
Requests/sec: 33226.52

Tarantool:
Latency Distribution
50% 8.69 ms
99% 73.09 ms
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 запитів. Приклад в кінці статті.
І якщо докладніше про тест:
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 connections 10
Thread Stats Avg Stdev Max ± Stdev
Latency 346.71 us 600.80 us 26.94 ms 97.89%
Req/Sec 6.54 k 0.88 k 13.87 k 73.13%
Latency Distribution
50% 269.00 us
75% 368.00 us
90% 493.00 us
99% 1.64 ms
130717 requests in 5.10 s, 15.08 MB read
Requests/sec: 25637.26
Transfer/sec: 2.96 MB

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 connections 10
Thread Stats Avg Stdev Max ± Stdev
Latency 767.53 us 209.64 us 4.04 ms 87.26%
Req/Sec 2.61 k 437.12 3.15 k 45.59%
Latency Distribution
50% 694.00 us
75% 0.90 ms
90% 1.02 ms
99% 1.43 ms
52927 requests in 5.10 s, 8.58 MB read
Requests/sec: 10377.78
Transfer/sec: 1.68 MB

Під більшим навантаженням:
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 connections 100
Thread Stats Avg Stdev Max ± Stdev
Latency 3.04 ms 1.48 ms 25.53 ms 80.21%
Req/Sec 3.34 k 621.43 12.52 k 86.20%
Latency Distribution
50% 2.85 ms
75% 3.58 ms
90% 4.57 ms
99% 8.12 ms
166514 requests in 5.01 s, 19.21 read MB
Requests/sec: 33226.52
Transfer/sec: 3.83 MB

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 connections 100
Thread Stats Avg Stdev Max ± Stdev
Latency 10.65 ms 14.24 ms 269.85 ms 98.43%
Req/Sec 1.09 k 128.17 1.73 k 94.56%
Latency Distribution
50% 8.69 ms
75% 10.50 ms
90% 11.36 ms
99% 73.09 ms
53943 requests in 5.01 s, 8.75 MB read
Requests/sec: 10763.55
Transfer/sec: 1.75 MB

Исходники тестів:
Go:
package main

import (
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/syndtr/goleveldb/leveldb"
"github.com/tecbot/gorocksdb"
)

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

opts := gorocksdb.NewDefaultOptions()
opts.SetCreateIfMissing(true)
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)
— в моїх тестах втрати даних не спостерігалося.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.