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 Slide

  2. Me
    2

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  6. - 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 Slide

  7. Agenda
    7

    View Slide

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

    View Slide

  9. 9
    Same old story

    View Slide

  10. 10
    Same old story

    View Slide

  11. 11
    Same old story

    View Slide

  12. Redis
    12

    View Slide

  13. What is inside Redis?
    13

    View Slide

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

    View Slide

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

  16. - 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 Slide

  17. 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 Slide

  18. 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 Slide

  19. TLDR: RESP3
    19

    View Slide

  20. TLDR: RESP3
    20

    View Slide

  21. Redis architectures
    21

    View Slide

  22. Go clients
    22

    View Slide

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

    View 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
    Go clients
    24

    View Slide

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

  26. - 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 Slide

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

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

  29. API review
    29

    View Slide

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

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

  32. 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 Slide

  33. 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 Slide

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

  35. Observations
    35

    View Slide

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

    View Slide

  37. - ~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 Slide

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

  39. - ~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 Slide

  40. API review: cristalhq/redis
    40

    View Slide

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

  42. 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 Slide

  43. HashMap example
    43

    View Slide

  44. 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 Slide

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

    View Slide

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

  47. Real life example
    47

    View Slide

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

    View Slide

  49. 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 Slide

  50. Memory wisdom
    50

    View Slide

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

    View Slide

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

    View Slide

  53. 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 Slide

  54. ⚠ Benchmark disclaimer
    54

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  58. - 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 Slide

  59. Redis bench
    59

    View Slide

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

    View Slide

  61. > 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 Slide

  62. Redis bench but latencies
    62

    View Slide

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

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

  65. │ 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 Slide

  66. 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 Slide

  67. │ 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 Slide

  68. Shipilev’s curve
    68

    View Slide

  69. Shipilev’s curve
    69

    View Slide

  70. Bonus slide
    70

    View Slide

  71. - memcached
    Bonus slide
    71

    View Slide

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

    View Slide

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

    View Slide

  74. Conclusions
    74

    View Slide

  75. - Think about API
    Conclusions
    75

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  80. References
    80

    View Slide

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

    View Slide

  82. - 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 Slide

  83. Golang Warsaw team 👌
    GogoApps 💕
    Thanks
    83

    View Slide

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

    View Slide