Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions docs/en/reference/redis-csc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Redis Client-Side Caching Support in JuiceFS

Starting with version 6.0, Redis provides [Client-Side Caching](https://redis.io/docs/latest/develop/reference/client-side-caching) which allows clients to maintain local caches of data in a faster and more efficient way. JuiceFS includes full support for this feature, offering significant performance improvements for metadata operations.

## How it works

Redis Client-Side Caching (CSC) works by:

1. The client enables tracking mode with `CLIENT TRACKING ON BCAST`
2. The client caches data locally after reading it from Redis
3. Redis notifies the client when cached keys are modified by any client
4. The client invalidates those keys in its local cache

This results in reduced network traffic, lower latency, and higher throughput.

## Configuration

JuiceFS supports Redis CSC through the following options in the metadata URL:

```shell
--meta-url="redis://localhost/1?client-cache=true" # Enable client-side caching (always BCAST mode)
--meta-url="redis://localhost/1?client-cache=true&client-cache-size=500" # Set cache size (default 12800)
--meta-url="redis://localhost/1?client-cache=true&client-cache-expire=60s" # Set cache expiration (default: 60s)
```

### Options

- `client-cache`: Enables client-side caching in BCAST mode (set to any value except "false")
- `client-cache-size`: Maximum cache size (default: 12800)
- `client-cache-expire`: Cache expiration time (default: 60s)
- `client-cache-preload`: Number of file objects under the root directory preloaded after mounting. (default: 0)

When client-side caching is enabled, JuiceFS caches:

1. **Inode attributes**: File/directory metadata like permissions, size, timestamps
2. **Directory entries**: Name to inode mappings for faster lookups

> **Note:** Redis Client Side Cache requires Redis server version 6.0 or higher. Using this feature with older Redis versions will result in errors.

### Preloading Cache

When client-side caching is enabled and `client-cache-preload` is set, JuiceFS will preload the file-object attributes and entries under the root directory after mounting. This lazy preloading happens in the background and helps to:

1. Warm up the cache for common operations
2. Reduce latency for initial file system operations
3. Provide better performance from the moment the file system is mounted

The preloading process intelligently prioritizes the most important inodes by:

1. Starting with the root directory
2. Loading the most frequently accessed top-level directories and files
3. Recursively exploring important subdirectories

The preloading process runs in a background goroutine with fail-safe mechanisms and won't block or affect normal file system operations.

## Modes

JuiceFS uses BCAST mode for simplicity and reliability:

- **BCAST mode**: All keys accessed by the client are tracked and notifications are sent for any changes.

BCAST mode provides the simplest implementation while ensuring cache coherence across all clients.

## Requirements

- Redis server version 6.0 or higher
- JuiceFS with CSC support enabled

## Performance Considerations

1. The default 12800 cache size should be sufficient for most workloads
2. For very large filesystems with millions of files, you may benefit from increasing the cache size
3. The cache is most effective for metadata-heavy workloads with many repeated operations
4. For very write-heavy workloads, consider disabling CSC as invalidation traffic may offset benefits

## Troubleshooting

If you experience crashes or instability with CSC enabled:

1. Update to the latest JuiceFS version which contains important fixes for CSC
2. Try reducing the cache size with `client-cache-size`
3. Check Redis server logs for any memory or client tracking issues
4. Make sure your Redis server version is 6.0 or higher
5. If problems persist, disable CSC by removing the `client-cache` parameter

JuiceFS includes robust error handling for various Redis CSC-specific responses to ensure stable operation even when Redis sends unexpected response formats due to client tracking.

## References

- [Redis Client-Side Caching Documentation](https://redis.io/docs/latest/develop/reference/client-side-caching)
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ require (
github.com/hanwen/go-fuse/v2 v2.1.1-0.20210611132105-24a1dfe6b4f8
github.com/hashicorp/consul/api v1.29.2
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.21.12+incompatible
github.com/hungys/go-lz4 v0.0.0-20170805124057-19ff7f07f099
github.com/jackc/pgx/v5 v5.7.3
Expand Down Expand Up @@ -68,7 +69,7 @@ require (
github.com/prometheus/prometheus v0.54.1
github.com/qingstor/qingstor-sdk-go/v4 v4.4.0
github.com/qiniu/go-sdk/v7 v7.25.2
github.com/redis/go-redis/v9 v9.7.3
github.com/redis/go-redis/v9 v9.16.0
github.com/sirupsen/logrus v1.9.3
github.com/smartystreets/goconvey v1.7.2
github.com/spf13/cast v1.7.1
Expand Down Expand Up @@ -354,3 +355,5 @@ replace github.com/mattn/go-colorable v0.1.9 => github.com/juicedata/go-colorabl
replace github.com/mattn/go-colorable v0.0.9 => github.com/juicedata/go-colorable v0.0.0-20250208072043-a97a0c2023db

replace github.com/cloudsoda/go-smb2 => github.com/juicedata/go-smb2 v0.0.0-20250917090526-d2d0abfb0e05

replace github.com/hashicorp/golang-lru/v2 v2.0.7 => github.com/juicedata/golang-lru/v2 v2.0.8-0.20251126062551-1b321869f904
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,8 @@ github.com/juicedata/godaemon v0.0.0-20210629045518-3da5144a127d h1:kpQMvNZJKGY3
github.com/juicedata/godaemon v0.0.0-20210629045518-3da5144a127d/go.mod h1:dlxKkLh3qAIPtgr2U/RVzsZJDuXA1ffg+Njikfmhvgw=
github.com/juicedata/gogfapi v0.0.0-20241204082332-ecd102647f80 h1:EPg/f3lhbAOjE2M0WpVi47Fk62mEmmPejRuGVdOFQww=
github.com/juicedata/gogfapi v0.0.0-20241204082332-ecd102647f80/go.mod h1:Ho5G4KgrgbMKW0buAJdOmYoJcOImkzznJQaLiATrsx4=
github.com/juicedata/golang-lru/v2 v2.0.8-0.20251126062551-1b321869f904 h1:oNtkL1jwrNMMcBlHNW1fhdl4quK7p1EdR7o1Rja5xpM=
github.com/juicedata/golang-lru/v2 v2.0.8-0.20251126062551-1b321869f904/go.mod h1:qnbgnNzfydwuHjSCApF4bdul+tZ8T3y1MkZG/OFczLA=
github.com/juicedata/huaweicloud-sdk-go-obs v3.22.12-0.20230228031208-386e87b5c091+incompatible h1:2/ttSmYoX+QMegpNyAJR0Y6aHcVk57F7RJit5xN2T/s=
github.com/juicedata/huaweicloud-sdk-go-obs v3.22.12-0.20230228031208-386e87b5c091+incompatible/go.mod h1:Ukwa8ffRQLV6QRwpqGioPjn2Wnf7TBDA4DbennDOqHE=
github.com/juicedata/minio v0.0.0-20251120043259-079fa6a601db h1:yGKlGEz3nOD2IovjI+V4O+eY1TPgOp/T6gOxMl9/xKI=
Expand Down Expand Up @@ -701,8 +703,8 @@ github.com/qiniu/go-sdk/v7 v7.25.2/go.mod h1:dmKtJ2ahhPWFVi9o1D5GemmWoh/ctuB9peq
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 h1:UVArwN/wkKjMVhh2EQGC0tEc1+FqiLlvYXY5mQ2f8Wg=
github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
Expand Down
57 changes: 56 additions & 1 deletion pkg/meta/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import (
"github.com/juicedata/juicefs/pkg/utils"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
"github.com/redis/go-redis/v9/maintnotifications"
"golang.org/x/sync/errgroup"
)

Expand Down Expand Up @@ -92,6 +93,7 @@ type redisMeta struct {
prefix string
shaLookup string // The SHA returned by Redis for the loaded `scriptLookup`
shaResolve string // The SHA returned by Redis for the loaded `scriptResolve`
cache *redisCache
}

var _ Meta = (*redisMeta)(nil)
Expand Down Expand Up @@ -122,6 +124,14 @@ func newRedisMeta(driver, addr string, conf *Config) (Meta, error) {
keyFile := query.pop("tls-key-file")
caCertFile := query.pop("tls-ca-cert-file")
tlsServerName := query.pop("tls-server-name")

// Client-side caching options
clientCacheStr := query.pop("client-cache")
clientCache := clientCacheStr != "false" && clientCacheStr != ""
clientCacheSize := query.getInt("client-cache-size", "client_cache_size", 12800)
// Default TTL to prevent reading stale cache for a long time when the connection fails.
clientCacheExpiry := query.duration("client-cache-expire", "client_cache_expire", time.Minute)
clientCachePreload := query.getInt("client-cache-preload", "client_cache_preload", 0) // may cause conflict
u.RawQuery = values.Encode()

hosts := u.Host
Expand Down Expand Up @@ -173,8 +183,10 @@ func newRedisMeta(driver, addr string, conf *Config) (Meta, error) {
opt.MaxRetryBackoff = maxRetryBackoff
opt.ReadTimeout = readTimeout
opt.WriteTimeout = writeTimeout
var rdb redis.UniversalClient
opt.MaintNotificationsConfig = &maintnotifications.Config{Mode: maintnotifications.ModeDisabled}
var prefix string
var rdb redis.UniversalClient

if strings.Contains(hosts, ",") && strings.Index(hosts, ",") < strings.Index(hosts, ":") {
var fopt redis.FailoverOptions
ps := strings.Split(hosts, ",")
Expand Down Expand Up @@ -269,15 +281,37 @@ func newRedisMeta(driver, addr string, conf *Config) (Meta, error) {
rdb: rdb,
prefix: prefix,
}
if clientCache {
m.cache = newRedisCache(prefix, clientCacheSize, clientCacheExpiry, clientCachePreload)
if err = m.cache.init(m.rdb); err != nil {
logger.Warnf("Failed to setup client-side caching: %v", err)
m.cache = nil
}
}
m.en = m
m.checkServerConfig()
return m, nil
}

func (m *redisMeta) Shutdown() error {
if m.cache != nil {
m.cache.close()
m.cache = nil
}
return m.rdb.Close()
}

// Override NewSession to initialize client-side cache after session is created
func (m *redisMeta) NewSession(record bool) error {
// First, create the session normally
err := m.baseMeta.NewSession(record)
if err != nil {
return err
}
go m.preloadCache()
return nil
}

func (m *redisMeta) doDeleteSlice(id uint64, size uint32) error {
return m.rdb.HDel(Background(), m.sliceRefs(), m.sliceKey(id, size)).Err()
}
Expand Down Expand Up @@ -919,6 +953,20 @@ func (m *redisMeta) doLookup(ctx Context, parent Ino, name string, inode *Ino, a
var encodedAttr []byte
var err error
entryKey := m.entryKey(parent)
if m.cache != nil {
if entry, ok := m.cache.entryCache.Get(m.cache.entryName(parent, name)); ok {
if !entry.isMark() {
*inode = entry.ino
if attr != nil {
*attr = entry.Attr
}
return 0
}
m.cache.entryCache.AddIf(m.cache.entryName(parent, name), &entryMark, func(oldEntry *cachedEntry, exists bool) bool {
return exists
})
}
}
if len(m.shaLookup) > 0 && attr != nil && !m.conf.CaseInsensi && m.prefix == "" {
var res interface{}
var returnedIno int64
Expand Down Expand Up @@ -946,6 +994,13 @@ func (m *redisMeta) doLookup(ctx Context, parent Ino, name string, inode *Ino, a
if err == nil {
m.parseAttr(encodedAttr, attr)
m.of.Update(foundIno, attr)
if m.cache != nil {
ce := &cachedEntry{ino: foundIno}
m.parseAttr(encodedAttr, &ce.Attr)
_, _ = m.cache.entryCache.AddIf(m.cache.entryName(parent, name), ce, func(oldEntry *cachedEntry, exists bool) bool {
return exists && oldEntry.isMark()
})
}
} else if err == redis.Nil { // corrupt entry
logger.Warnf("no attribute for inode %d (%d, %s)", foundIno, parent, name)
*attr = Attr{Typ: foundType}
Expand Down
Loading
Loading