Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Writing faster Redis client

Writing faster Redis client

Oleg Kovalov

March 16, 2023
Tweet

More Decks by Oleg Kovalov

Other Decks in Technology

Transcript

  1. - Open source addicted gopher - Fan of linters (co-author

    go-critic) - Father of a labrador Me 4
  2. - Open source addicted gopher - Fan of linters (co-author

    go-critic) - Father of a labrador - Also Twitter/Telegram @go_perf - ... Me 5
  3. - Open source addicted gopher - Fan of linters (co-author

    go-critic) - Father of a labrador - Also Twitter/Telegram @go_perf - ... olegk.dev Me 6
  4. - One of the best C code - not only

    code but also documentation What is inside Redis? 14
  5. - One of the best C code - not only

    code but also documentation - RESP3 protocol - compare it with YAML spec What is inside Redis? 15
  6. - One of the best C code - not only

    code but also documentation - RESP3 protocol - compare it with YAML spec - TCP - network is the bottleneck (or not?) What is inside Redis? 16
  7. What is inside Redis? 17 - One of the best

    C code - not only code but also documentation - RESP3 protocol - compare it with YAML spec - TCP - network is the bottleneck (or not?) - It’s everywhere - see https://db-engines.com/
  8. What is inside Redis? 18 - One of the best

    C code - not only code but also documentation - RESP3 protocol - compare it with YAML spec - TCP - network is the bottleneck (or not?) - It’s everywhere - see https://db-engines.com/ - PERFORMANCE!!11!!11
  9. - The popularest (17k ⭐ / 26k lines) - https://github.com/redis/go-redis

    - The 2n popular (9.5k ⭐ / 6k lines) - https://github.com/gomodule/redigo Go clients 24
  10. - The popularest (17k ⭐ / 26k lines) - https://github.com/redis/go-redis

    - The 2n popular (9.5k ⭐ / 6k lines) - https://github.com/gomodule/redigo - The freshest (1k ⭐ / 108k lines) - https://github.com/rueian/rueidis Go clients 25
  11. - The popularest (17k ⭐ / 26k lines) - https://github.com/redis/go-redis

    - The 2n popular (9.5k ⭐ / 6k lines) - https://github.com/gomodule/redigo - The freshest (1k ⭐ / 108k lines) - https://github.com/rueian/rueidis - The oldest (0.6k ⭐ / 10k lines) - https://github.com/mediocregopher/radix Go clients 26
  12. - The popularest (17k ⭐ / 26k lines) - https://github.com/redis/go-redis

    - The 2n popular (9.5k ⭐ / 6k lines) - https://github.com/gomodule/redigo - The freshest (1k ⭐ / 108k lines) - https://github.com/rueian/rueidis - The oldest (0.6k ⭐ / 10k lines) - https://github.com/mediocregopher/radix - The pipelinest (238 ⭐ / 6k lines) - https://github.com/joomcode/redispipe Go clients 27
  13. Go clients - The popularest (17k ⭐ / 26k lines)

    - https://github.com/redis/go-redis - The 2n popular (9.5k ⭐ / 6k lines) - https://github.com/gomodule/redigo - The freshest (1k ⭐ / 108k lines) - https://github.com/rueian/rueidis - The oldest (0.6k ⭐ / 10k lines) - https://github.com/mediocregopher/radix - The pipelinest (238 ⭐ / 6k lines) - https://github.com/joomcode/redispipe - Our try (15 💖 / 3.5k of poetry) - https://github.com/cristalhq/redis 28
  14. func ExampleClient() { rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", }) err

    := rdb.Set(ctx, "key", "value", 0).Err() if err != nil { panic(err) } val2, err := rdb.Get(ctx, "key2").Result() if err == redis.Nil { fmt.Println("key2 does not exist") } else if err != nil { panic(err) } else { fmt.Println("key2", val2) } API review: redis/go-redis 30
  15. func ExampleClient() { c, err := redis.Dial("tcp", ":6379") if err

    != nil { panic(err) } defer c.Close() _, err = c.Do("SET", "k1", 1) if err != nil { panic(err) } n, err := redis.Int(c.Do("GET", "k1")) if err != nil { panic(err) } fmt.Printf("%#v\n", n) API review: gomodule/redigo 31
  16. func ExampleClient() { client, err := rueidis.NewClient(rueidis.ClientOption{ InitAddress: []string{"127.0.0.1:6379"}, })

    if err != nil { panic(err) } defer client.Close() ctx := context.Background() // SET key val NX err = client.Do(ctx, client.B().Set().Key("key").Value("val").Nx().Build()).Error() API review: rueian/rueidis 32
  17. func ExampleClient() { client, err := radix.NewPool("tcp", "127.0.0.1:6379", 10) if

    err != nil { panic(err) } cmd := radix.Cmd(nil, "SET", "foo", "bar") if err := client.Do(cmd); err != nil { panic(err) } API review: mediocregopher/radix 33
  18. func ExampleClient() { sender, err := redis.SingleRedis(ctx) if err !=

    nil { panic(err) } client := redis.SyncCtx{sender} res := client.Do(ctx, "SET", "key", "ho") if err := redis.AsError(res); err != nil { panic(err) } fmt.Printf("result: %q\n", res) API review: joomcode/redispipe 34
  19. - ~all of the clients are based on Redis commands

    - create commands -> submit to client -> get response Observations 36
  20. - ~all of the clients are based on Redis commands

    - create commands -> submit to client -> get response - Returning ‘interface{}’ (or a modern ‘any’) allocates - which you cannot omit Observations 37
  21. - ~all of the clients are based on Redis commands

    - create commands -> submit to client -> get response - Returning ‘interface{}’ (or a modern ‘any’) allocates - which you cannot omit - Not all clients understand context package - this can be achieved via wrapper or timeouts Observations 38
  22. - ~all of the clients are based on Redis commands

    - create commands -> submit to client -> get response - Returning ‘interface{}’ (or a modern ‘any’) allocates - which you cannot omit - Not all clients understand context package - this can be achieved via wrapper or timeouts - Reusing memory is completely impossible - Go GC is a cool thing but we can do better Observations 39
  23. func Example() { ctx := context.Background() client, err := redis.NewClient(ctx,

    &redis.Config{ Address: "127.0.0.1:6379", }) if err != nil { panic(err) } API review: cristalhq/redis 41
  24. API review: cristalhq/redis 42 func Example() { ctx := context.Background()

    client, err := redis.NewClient(ctx, &redis.Config{ Address: "127.0.0.1:6379", }) if err != nil { panic(err) } hashmap := redis.NewHashMap("my-hash-map", client) if err := hashmap.Set(ctx, "key", "value"); err != nil { panic(err) } }
  25. func (hm HashMap) Name() string { return hm.name } func

    (hm HashMap) Delete(ctx context.Context, fields ...string) (int64, error) { func (hm HashMap) Exists(ctx context.Context, field string) (bool, error) { func (hm HashMap) Get(ctx context.Context, field string) (string, error) { func (hm HashMap) GetAll(ctx context.Context) (map[string]string, error) { func (hm HashMap) IncBy(ctx context.Context, field string, delta int64) (int64, error) { func (hm HashMap) IncByFloat(ctx context.Context, field string, delta float64) (float64, error) { func (hm HashMap) Keys(ctx context.Context) ([]string, error) { func (hm HashMap) Len(ctx context.Context) (int64, error) { func (hm HashMap) MultiGet(ctx context.Context, fields ...string) ([]Value, error) { ... HashMap example 44
  26. BitMap Commander Function Geo HashMap HyperLogLog Keys List PubSub Script

    Scripting Set SortedSet Stream Strings More data structures 45
  27. func (c Commander) BitCount(ctx context.Context, key string, start, end int64)

    (int64, error) { func (c Commander) BitCountAll(ctx context.Context, key string) (int64, error) { func (c Commander) BitField(ctx context.Context, key string) error { func (c Commander) BitFieldReadOnly(ctx context.Context, key string) error { func (c Commander) BitOp(ctx context.Context, op BitMapOp, destKey string, keys ...string) (int64, error) { func (c Commander) BitPos(ctx context.Context, key string, bit int64, pos ...int64) (int64, error) { func (c Commander) GetBit(ctx context.Context, key string, offset int64) (int64, error) { ... Command them all 46
  28. Real life example import "github.com/cristalhq/redis" type UserService struct { cache

    *redis.HashMap db *postgresDB } func (s *UserService) GetUser(ctx context.Context, userID string) (any, error) { user, err := s.cache.Get(ctx, userID) if err == nil { return user, nil } s.db.GetUser(...) // ... 49
  29. func (hm HashMap) Keys(ctx context.Context) ([]string, error) { func (hm

    HashMap) Keys(ctx context.Context, keys []string) ([]string, error) { Memory wisdom 52
  30. func (hm HashMap) Keys(ctx context.Context) ([]string, error) { func (hm

    HashMap) Keys(ctx context.Context, keys []string) ([]string, error) { // Now just do pooling and you’re perf engineer! Memory wisdom 53
  31. - There is no perfect benchmark - Make them repeatable

    and stable ⚠ Benchmark disclaimer 56
  32. - There is no perfect benchmark - Make them repeatable

    and stable - It’s always YMMV - your mileage may vary ⚠ Benchmark disclaimer 57
  33. - There is no perfect benchmark - Make them repeatable

    and stable - It’s always YMMV - your mileage may vary BTW: this slide must be in every presentation where “benchmark” is mentioned ⚠ Benchmark disclaimer 58
  34. > redis-benchmark -n 100000 set key value Summary: throughput summary:

    19964.06 requests per second > redis-benchmark -n 100000 -P 64 set key value Summary: throughput summary: 446571.41 requests per second -P = Pipeline <numreq> requests. Default 1 (no pipeline). Redis bench 61
  35. > redis-benchmark -n 100000 set key value latency summary (msec):

    avg min p50 p95 p99 max 2.280 0.560 2.103 3.759 5.527 17.791 Redis bench but latencies 63
  36. > redis-benchmark -n 100000 set key value latency summary (msec):

    avg min p50 p95 p99 max 2.280 0.560 2.103 3.759 5.527 17.791 > redis-benchmark -n 100000 -P 64 set key value latency summary (msec): avg min p50 p95 p99 max 6.785 1.648 6.455 10.479 19.663 22.223 -P = Pipeline <numreq> requests. Default 1 (no pipeline). Redis bench but latencies 64
  37. │ sec/op │ Client_cristalhq/sequential-set-10 634.5µ ± 1% Client_cristalhq/sequential-get-10 631.2µ ±

    1% Client_goredis/sequential-set-10 634.5µ ± 1% Client_goredis/sequential-get-10 633.9µ ± 1% Client_mediocregopher/sequential-set-10 638.6µ ± 1% Client_mediocregopher/sequential-get-10 640.1µ ± 1% Client_rueidis/sequential-set-10 630.4µ ± 1% Client_rueidis/sequential-get-10 731.2µ ± 13% Clients benchmark (sequential) 65
  38. Clients benchmark (parallel) │ sec/op │ Client_cristalhq/parallel-set-10 112.7µ ± 2%

    Client_cristalhq/parallel-get-10 112.5µ ± 3% Client_goredis/parallel-set-10 118.2µ ± 2% Client_goredis/parallel-get-10 117.2µ ± 1% Client_mediocregopher/parallel-set-10 96.11µ ± 2% Client_mediocregopher/parallel-get-10 95.71µ ± 2% Client_rueidis/parallel-set-10 91.55µ ± 2% Client_rueidis/parallel-get-10 92.31µ ± 2% 66
  39. │ allocs/op │ Client_cristalhq/sequential-set-10 0.000 ± 0% Client_cristalhq/parallel-set-10 0.000 ±

    0% Client_cristalhq/sequential-get-10 1.000 ± 0% Client_cristalhq/parallel-get-10 1.000 ± 0% Client_goredis/sequential-set-10 9.000 ± 0% Client_goredis/parallel-set-10 9.000 ± 0% Client_goredis/sequential-get-10 8.000 ± 0% Client_goredis/parallel-get-10 8.000 ± 0% Clients benchmark (allocs) 67
  40. - memcached - same TCP thing - similar protocol -

    and less code But slightly more in-progress than Redis client 😉 Bonus slide 73
  41. - Think about API - Hand-written protocol is OK -

    Benchmark - Benchmark - Benchmark Conclusions 78
  42. - Think about API - Hand-written protocol is OK -

    Benchmark - Benchmark - Benchmark - Keep things easy Conclusions 79
  43. - cristalhq/redis (⭐ and “go get”) - https://github.com/cristalhq/redis - go-perftuner

    - https://github.com/go-perf/go-perftuner - Redis author (antirez) about docs/comments - http://antirez.com/news/124 References 82