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. Writing faster Redis client
    Golang Warsaw #51 (spring) 2023
    Oleg Kovalov
    󰑒
    olegk.dev

    View full-size slide

  2. - Open source addicted gopher
    - Fan of linters (co-author go-critic)
    Me
    3

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  6. - Intro
    - Redis
    - Go clients
    - Benchmarks
    - Conclusions
    Agenda
    8

    View full-size slide

  7. 9
    Same old story

    View full-size slide

  8. 10
    Same old story

    View full-size slide

  9. 11
    Same old story

    View full-size slide

  10. What is inside Redis?
    13

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  13. - 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

    View full-size slide

  14. 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/

    View full-size slide

  15. 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

    View full-size slide

  16. TLDR: RESP3
    19

    View full-size slide

  17. TLDR: RESP3
    20

    View full-size slide

  18. Redis architectures
    21

    View full-size slide

  19. Go clients
    22

    View full-size slide

  20. - The popularest (17k ⭐ / 26k lines)
    - https://github.com/redis/go-redis
    Go clients
    23

    View full-size slide

  21. - 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

    View full-size slide

  22. - 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

    View full-size slide

  23. - 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

    View full-size slide

  24. - 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

    View full-size slide

  25. 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

    View full-size slide

  26. API review
    29

    View full-size slide

  27. 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

    View full-size slide

  28. 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

    View full-size slide

  29. 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

    View full-size slide

  30. 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

    View full-size slide

  31. 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

    View full-size slide

  32. Observations
    35

    View full-size slide

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

    View full-size slide

  34. - ~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

    View full-size slide

  35. - ~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

    View full-size slide

  36. - ~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

    View full-size slide

  37. API review: cristalhq/redis
    40

    View full-size slide

  38. 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

    View full-size slide

  39. 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)
    }
    }

    View full-size slide

  40. HashMap example
    43

    View full-size slide

  41. 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

    View full-size slide

  42. BitMap
    Commander
    Function
    Geo
    HashMap
    HyperLogLog
    Keys
    List
    PubSub
    Script
    Scripting
    Set
    SortedSet
    Stream
    Strings
    More data structures
    45

    View full-size slide

  43. 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

    View full-size slide

  44. Real life example
    47

    View full-size slide

  45. import "github.com/cristalhq/redis"
    type UserService struct {
    cache *redis.HashMap
    db *postgresDB
    }
    Real life example
    48

    View full-size slide

  46. 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

    View full-size slide

  47. Memory wisdom
    50

    View full-size slide

  48. func (hm HashMap) Keys(ctx context.Context) ([]string, error) {
    Memory wisdom
    51

    View full-size slide

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

    View full-size slide

  50. 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

    View full-size slide

  51. ⚠ Benchmark disclaimer
    54

    View full-size slide

  52. - There is no perfect benchmark
    ⚠ Benchmark disclaimer
    55

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  55. - 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

    View full-size slide

  56. Redis bench
    59

    View full-size slide

  57. > redis-benchmark -n 100000 set key value
    Summary:
    throughput summary: 19964.06 requests per second
    Redis bench
    60

    View full-size slide

  58. > 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 requests. Default 1 (no pipeline).
    Redis bench
    61

    View full-size slide

  59. Redis bench but latencies
    62

    View full-size slide

  60. > 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

    View full-size slide

  61. > 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 requests. Default 1 (no pipeline).
    Redis bench but latencies
    64

    View full-size slide

  62. │ 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

    View full-size slide

  63. 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

    View full-size slide

  64. │ 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

    View full-size slide

  65. Shipilev’s curve
    68

    View full-size slide

  66. Shipilev’s curve
    69

    View full-size slide

  67. Bonus slide
    70

    View full-size slide

  68. - memcached
    Bonus slide
    71

    View full-size slide

  69. - memcached
    - same TCP thing
    - similar protocol
    - and less code
    Bonus slide
    72

    View full-size slide

  70. - memcached
    - same TCP thing
    - similar protocol
    - and less code
    But slightly more in-progress than Redis client 😉
    Bonus slide
    73

    View full-size slide

  71. Conclusions
    74

    View full-size slide

  72. - Think about API
    Conclusions
    75

    View full-size slide

  73. - Think about API
    - Hand-written protocol is OK
    Conclusions
    76

    View full-size slide

  74. - Think about API
    - Hand-written protocol is OK
    - Benchmark
    Conclusions
    77

    View full-size slide

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

    View full-size slide

  76. - Think about API
    - Hand-written protocol is OK
    - Benchmark
    - Benchmark
    - Benchmark
    - Keep things easy
    Conclusions
    79

    View full-size slide

  77. References
    80

    View full-size slide

  78. - cristalhq/redis (⭐ and “go get”)
    - https://github.com/cristalhq/redis
    - go-perftuner
    - https://github.com/go-perf/go-perftuner
    References
    81

    View full-size slide

  79. - 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

    View full-size slide

  80. Golang Warsaw team 👌
    GogoApps 💕
    Thanks
    83

    View full-size slide

  81. Thank you
    Questions?
    Telegram: @olegkovalov
    Twitter: @oleg_kovalov
    That’s all folks

    View full-size slide