Pull to refresh

Сугубо ненаучно: Tarantool 1.6 vs Golang (по скорости)

Reading time7 min
Views26K

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


image


Обратите внимание 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)

}
Tags:
Hubs:
+22
Comments101

Articles

Change theme settings