Pull to refresh
27
0
Send message

С какой точки зрения?

Это так, да. В статье решил минимизировать Lua, чтобы у бекендеров не складывалось впечатление что Tarantool == Lua.

в статье будет написано как создавать высокопроизводительный код, выполняемый на стороне сервера Tarantool в основном потоке сервера - т.е. рядом с данными.

Создаем высоконагруженное приложение на Tarantool

native-вариант для Tarantool - созданный на языке Lua - он бы смотрелся куда лучше

Поапдейтил статью

В общем не впечатлило.

okay(

Спасибо за вопросы.

А как у вас реализуется кооперативная многозадачность? есть аналог Yeld или длинная задача в fiber заблокирует остальные запросы?

Асинхронная операция делает yield. Такие операции: обращение в сеть, на диск, коммит транзакции.
Есть явная функция передачи управления fiber.yield().
Если задача длинная и не делает асинхронных операций или fiber.yield — она заблокирует остальные запросы.

Каждая нода кластера содержит...

Ноды могут объединятся в репликасет. В репликасете возможна топология master-slave или multimaster.
Репликасеты могут объединятся в кластер. Данные будут шардированы между репликасетами по признаку, который задаст пользователь.

только ручное разрешения конфликтов

Да

Oracle TimesTen

Выглядит по-другому, сравнить сложно

  • 1млн хешей (миллиарды долго ждать)


  • redis_test.go


    package main
    
    import (
        "context"
        "log"
        "math/rand"
        "strconv"
        "testing"
    
        "github.com/go-redis/redis"
    )
    
    func lpad(s string, pad string, plength int) string {
        for i := len(s); i < plength; i++ {
            s = pad + s
        }
        return s
    }
    
    func BenchmarkSetRandomRedis(b *testing.B) {
        client := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379", Password: "", DB: 0})
        if _, err := client.Ping(context.Background()).Result(); err != nil {
            log.Fatal(err)
        }
    
        for n := uint64(1); n < 1e6; n++ {
            key := strconv.FormatUint(n, 16)
            key = lpad(key, "0", 8)
    
            pipe := client.Pipeline()
    
            for i := 1; i < 11; i++ {
                id := rand.Int31()
                pipe.LPush(context.Background(), key, id)
            }
    
            _, err := pipe.Exec(context.Background())
            if err != nil {
                log.Fatal(err)
            }
        }
    }

  • tarantool


    • tarantool>
      box.cfg{listen='127.0.0.1:3301', wal_mode='none', memtx_memory=2*1024*1024*1024,
      readahead=1024*1024*16}
      box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true,})
      box.schema.space.create('kv', {if_not_exists=true,})
      box.space.kv:create_index('pkey', 
      {type='TREE', 
      parts={{field=1, type='str'}, {field=2, type='unsigned'}},
      if_not_exists=true,})
      box.space.kv:create_index('skey', 
      {type='TREE',
      parts={{field=2, type='unsigned'}},
      unique=false,
      if_not_exists=true,})

  • tarantool_test.go


    package main
    
    import (
        "math/rand"
        "strconv"
        "testing"
    
        "github.com/tarantool/go-tarantool"
    )
    
    func BenchmarkSetRandomTarantool(b *testing.B) {
        opts := tarantool.Opts{
            User: "guest",
        }
        pconn, err := tarantool.Connect("127.0.0.1:3301", opts)
        if err != nil {
            b.Fatal(err)
        }
    
        for n := uint64(1); n < 1e6; n++ {
            key := strconv.FormatUint(n, 16)
            key = lpad(key, "0", 8)
    
            for i := 1; i < 11; i++ {
                id := rand.Int31()
                _, err = pconn.Replace("kv", []interface{}{key, id})
                if err != nil {
                    b.Fatal(err)
                }
            }
        }
    }

  • Тесты



go test -cpu 12 -test.bench .


  • redis


    • info
      # Memory
      used_memory:233454512
      used_memory_human:222.64M
      used_memory_rss:169279488
      used_memory_rss_human:161.44M
      used_memory_peak:233469968
      used_memory_peak_human:222.65M
      used_memory_peak_perc:99.99%
      used_memory_overhead:49407608
      used_memory_startup:1001600
      used_memory_dataset:184046904
      used_memory_dataset_perc:79.18%

  • tarantool вместе со вторичными индексами


    • box.slab.info()
      items_size: 357184240
      items_used_ratio: 99.86%
      quota_size: 2147483648
      quota_used_ratio: 35.16%
      arena_used_ratio: 96.9%
      items_used: 356685848
      quota_used: 754974720
      arena_size: 754974720
      arena_used: 731944984
      ...
    • box.slab.info().arena_used/1024/1024
      782

  • Вывод:


    • redis used_memory_human:222.64M
    • tarantool: 782
    • Tarantool занимает побольше в том числе за счёт вторичного индекса
    • В Tarantool можно (нужно) отключить индекс хинты, которые снизят потребление памяти

Я в лоб накидал схему на Redis и Tarantool:


  • Два insert-а в Redis


  • Один insert в Tarantool


  • redis_test.go


    package main
    
    import (
        "context"
        "fmt"
        "log"
        "math/rand"
        "testing"
    
        "github.com/go-redis/redis"
    )
    
    func BenchmarkSetRandomRedisParallel(b *testing.B) {
        client := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379", Password: "", DB: 0})
        if _, err := client.Ping(context.Background()).Result(); err != nil {
            log.Fatal(err)
        }
    
        b.RunParallel(func(pb *testing.PB) {
            for pb.Next() {
                key := fmt.Sprintf("bench-%d", rand.Int31n(1000))
                id := rand.Int31()
                value := rand.Int31()
                idstr := fmt.Sprintf("%d", id)
                valstr := fmt.Sprintf("%d", value)
    
                // INSERT
                _, err := client.HSet(context.Background(), key, idstr, valstr).Result()
                if err != nil {
                    b.Fatal(err)
                }
    
                _, err = client.SAdd(context.Background(), idstr, key, 0).Result()
                if err != nil {
                    b.Fatal(err)
                }
            }
        })
    }
    
    func BenchmarkGetRandomRedisParallel(b *testing.B) {
        client := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379", Password: "", DB: 0})
        if _, err := client.Ping(context.Background()).Result(); err != nil {
            log.Fatal(err)
        }
    
        b.RunParallel(func(pb *testing.PB) {
            for pb.Next() {
                id := rand.Int31()
                idstr := fmt.Sprintf("%d", id)
    
                _, err := client.SMembers(context.Background(), idstr).Result()
                if err != nil {
                    b.Fatal(err)
                }
            }
        })
    }

  • tarantool


    • tarantool>
      box.cfg{listen='127.0.0.1:3301', wal_mode='none', memtx_memory=2*1024*1024*1024,
      readahead=1024*1024*16}
      box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true,})
      box.schema.space.create('kv', {if_not_exists=true,})
      box.space.kv:create_index('pkey', 
      {type='TREE', 
      parts={{field=1, type='str'}, {field=2, type='unsigned'}},
      if_not_exists=true,})
      box.space.kv:create_index('skey', 
      {type='TREE',
      parts={{field=2, type='unsigned'}},
      unique=false,
      if_not_exists=true,})

  • tarantool_test.go


    package main
    
    import (
        "fmt"
        "math/rand"
        "testing"
    
        "github.com/tarantool/go-tarantool"
    )
    
    func BenchmarkSetRandomTntParallel(b *testing.B) {
        opts := tarantool.Opts{
            User: "guest",
        }
        pconn, err := tarantool.Connect("127.0.0.1:3301", opts)
        if err != nil {
            b.Fatal(err)
        }
        b.RunParallel(func(pb *testing.PB) {
            for pb.Next() {
                key := fmt.Sprintf("bench-%d", rand.Int31n(1000))
                id := rand.Int31()
                value := rand.Int31()
    
                _, err = pconn.Replace("kv", []interface{}{key, id, value})
                if err != nil {
                    b.Fatal(err)
                }
            }
        })
    }
    
    func BenchmarkGetRandomTntParallel(b *testing.B) {
        opts := tarantool.Opts{
            User: "guest",
        }
        pconn, err := tarantool.Connect("127.0.0.1:3301", opts)
        if err != nil {
            b.Fatal(err)
        }
        b.RunParallel(func(pb *testing.PB) {
            for pb.Next() {
                id := rand.Int31()
    
                _, err = pconn.Select("kv", "skey", 0, 10000, tarantool.IterEq, []interface{}{id})
                if err != nil {
                    b.Fatal(err)
                }
            }
        })
    }
    

  • Тесты



go test -cpu 12 -test.bench . -test.benchtime 10s


goos: darwin
goarch: amd64
BenchmarkSetRandomRedisParallel-12        409393         39018 ns/op
BenchmarkGetRandomRedisParallel-12        768824         15063 ns/op
BenchmarkSetRandomTntParallel-12          781621         16820 ns/op
BenchmarkGetRandomTntParallel-12          858226         13462 ns/op
PASS
ok      _/Users/michael.filonenko/course/tmp    53.643s

Вывод:
В Redis приходится делать два инсерта.
В Tarantool один, и Tarantool поэтому быстрее.

Такой вид задачи и в Redis и в Tarantool удобнее решать через две таблицы:


  • первая hash-a={ids}
  • вторая id={values}

То есть суть сведётся к предыдущему тесту

И Redis и Tarantool такое умеют из коробки. Синтаксис Redis попроще, Tarantool более общий


redis-server # запустить

tarantool # запустить и сконфигурировать

  • tarantool>


    box.cfg{listen='127.0.0.1:3301', wal_mode='none', memtx_memory=2*1024*1024*1024}
    box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true,})
    box.schema.space.create('kv', {if_not_exists=true,})
    box.space.kv:create_index('pkey', {type='TREE', parts={{field=1, type='str'}},
                                    if_not_exists=true,})

  • redis_test.go


    package main
    
    import (
        "context"
        "fmt"
        "log"
        "math/rand"
        "testing"
    
        "github.com/go-redis/redis"
    )
    
    func BenchmarkSetRandomRedisParallel(b *testing.B) {
        client := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379", Password: "", DB: 0})
        if _, err := client.Ping(context.Background()).Result(); err != nil {
            log.Fatal(err)
        }
    
        b.RunParallel(func(pb *testing.PB) {
            for pb.Next() {
                key := fmt.Sprintf("bench-%d", rand.Int31n(1000))
                value := rand.Int31()
                _, err := client.RPush(context.Background(), key, value, 0).Result()
                if err != nil {
                    b.Fatal(err)
                }
            }
        })
    }

  • tarantool_test.go


    package main
    
    import (
        "fmt"
        "math/rand"
        "testing"
    
        "github.com/tarantool/go-tarantool"
    )
    
    func BenchmarkSetRandomTntParallel(b *testing.B) {
        opts := tarantool.Opts{
            User: "guest",
        }
        pconn, err := tarantool.Connect("127.0.0.1:3301", opts)
        if err != nil {
            b.Fatal(err)
        }
        b.RunParallel(func(pb *testing.PB) {
            for pb.Next() {
                key := fmt.Sprintf("bench-%d", rand.Int31n(1000))
                value := rand.Int31()
                _, err = pconn.Upsert("kv", []interface{}{key, value}, []interface{}{[]interface{}{"!", 2, value}})
                if err != nil {
                    b.Fatal(err)
                }
            }
        })
    }

  • Запуск на 10 секунд на 12 потоков


    go test -cpu 12 -test.bench . -test.benchtime 10s

  • Результаты примерно равны


    goos: darwin
    goarch: amd64
    BenchmarkSetRandomRedisParallel-12        965875         14028 ns/op
    BenchmarkSetRandomTntParallel-12          955849         13928 ns/op
    PASS

  • У Redis для того, чтобы не доставать запись, существуют разные операции для таких вот структур данных


  • У Tarantool, чтобы не доставать данные, есть операции update,upsert. А если их не хватит то хранимки на LuaJIT.


Такие механизмы в redis, tarantool решаются способами: или денормализацией, или логикой на аппсервере, или хранимыми процедурами

Не совсем понимаю как эти действия применить к redis и wait и не получить грязные чтения

Ролбек нужен, чтобы откатить данные, которые на лидере закомитились.

Кстати да.
Но есть ещё ограничение: нет ролбека в случае, если реплик мы не дождались. Ролбек можно конечно делать на апп уровне, но тогда может быть уже проще искать хранилище с полностью синхронной репликацией.


http://antirez.com/news/66

Согласен, хотя facebook выстрелил (но оно скорее нейтральное).
У Тарантула есть «Cartridge» для построение приложений. Это название может оказаться более подходящим.


А Микрософт исправлились и выпустили XboX )

Redis fork механизм выполняет консистентный снапшот, потому что используется механизм OS copy-on-write.
Tarantool механизм также выполняет консистентный снапшот, потому что используется внутренний механизм read view.

Я не уверен, что это возможно. В протоколе же msgpack. msgpack с динамической типизацей.
Только если придумать парсер протокола на захардкоженной схеме и перенести ответственность на пользователя коннектора, но я думаю, что авторы библиотек не любят идти на такие риски.

Да, типы данных в статье представлены куце. В Redis и Tarantool они немного ортогональны друг другу. В плагины залазить тоже пока что сложно. Надо подумоть над какими-то пересекающимися кейсами и правильным их сравнением.

Спасибо за разъяснения, но их сложно соотнести конкретно с Redis и Tarantool.


Данные там живут ровно до момента выключения питания.

  • И Redis и Tarantool имеют механизм записи всех транзакций в журнал прежде чем сгенерировать ответ клиентскому приложению.

Если его сохранять часто при больших объемах данных и однопоточной архитектуре, то от производительности останется пшик.

  • И Redis и Tarantool периодически неблокирующе сохраняют снапшот in-memory данных. Redis с помощью fork, Tarantool с помощью read view.

in-memory архитектура хранит (не кеширует) сразу все данные и индексы в памяти, тем самым избавляясь от механизма кеширования/вытеснения страниц данных с диска/на диск. Какие именно данные уже зависит от архитектуры хранения конкретной БД.


Например, вы можете установить Tarantool и воспользоваться SQL, и соответствующими структурами как в традиционных РСУБД.


https://www.tarantool.io/en/doc/latest/reference/reference_sql/sql_beginners_guide/

Команда wait в Redis и аналог в Tarantool — это «частично» синхронная репликация.
Представим:


  • транзакция пришла
  • лидер сохранил
  • лидер ждёт реплику
  • в это время кто-то пришел и уже транзакцию на лидере увидел

В этой ситуации у нас появляются грязные чтения. Синхронная репликация в этом плане работает лучше.


Титанически практический, на мой взгляд, труд об этом можно прочитать в этой статье:


https://habr.com/ru/company/mailru/blog/540446/

Тогда похоже на timeseries (отдельный модуль)


https://oss.redislabs.com/redistimeseries/commands/

1

Information

Rating
Does not participate
Works in
Registered
Activity