Skip to content

Commit f02390e

Browse files
author
Jane Wang
committed
Add Zstd support for EVCache
1 parent fa8721e commit f02390e

5 files changed

Lines changed: 338 additions & 30 deletions

File tree

evcache-core/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dependencies {
4343
api group:"joda-time", name:"joda-time", version:"latest.release"
4444
api group:"javax.annotation", name:"javax.annotation-api", version:"latest.release"
4545
api group:"com.github.fzakaria", name:"ascii85", version:"latest.release"
46+
api group:"com.github.luben", name:"zstd-jni", version:"latest.release"
4647

4748
testImplementation group:"org.testng", name:"testng", version:"7.5"
4849
testImplementation group:"com.beust", name:"jcommander", version:"1.72"

evcache-core/src/main/java/com/netflix/evcache/EVCacheSerializingTranscoder.java

Lines changed: 95 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,25 @@
2222

2323
package com.netflix.evcache;
2424

25+
import com.github.luben.zstd.Zstd;
26+
import com.github.luben.zstd.ZstdInputStream;
2527
import com.netflix.evcache.metrics.EVCacheMetricsFactory;
26-
import com.netflix.evcache.pool.ServerGroup;
2728
import com.netflix.spectator.api.BasicTag;
2829
import com.netflix.spectator.api.Tag;
29-
import com.netflix.spectator.api.Timer;
3030
import net.spy.memcached.CachedData;
3131
import net.spy.memcached.transcoders.BaseSerializingTranscoder;
3232
import net.spy.memcached.transcoders.Transcoder;
3333
import net.spy.memcached.transcoders.TranscoderUtils;
34-
import net.spy.memcached.util.StringUtils;
3534

36-
import java.time.Duration;
35+
import java.io.ByteArrayInputStream;
36+
import java.io.ByteArrayOutputStream;
37+
import java.io.IOException;
38+
import java.io.InputStream;
39+
import java.nio.ByteBuffer;
40+
import java.nio.ByteOrder;
3741
import java.util.ArrayList;
3842
import java.util.Date;
3943
import java.util.List;
40-
import java.util.Map;
41-
import java.util.concurrent.ConcurrentHashMap;
42-
import java.util.concurrent.TimeUnit;
4344

4445

4546
/**
@@ -65,8 +66,15 @@ public class EVCacheSerializingTranscoder extends BaseSerializingTranscoder impl
6566

6667
static final String COMPRESSION = "COMPRESSION_METRIC";
6768

69+
public enum CompressionAlgorithm { GZIP, ZSTD }
70+
71+
public static final int DEFAULT_ZSTD_COMPRESSION_LEVEL = 3;
72+
73+
private static final int ZSTD_MAGIC = 0xFD2FB528;
74+
6875
private final TranscoderUtils tu = new TranscoderUtils(true);
69-
private Timer timer;
76+
private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.GZIP;
77+
private int zstdLevel = DEFAULT_ZSTD_COMPRESSION_LEVEL;
7078

7179
/**
7280
* Get a serializing transcoder with the default max data size.
@@ -82,6 +90,14 @@ public EVCacheSerializingTranscoder(int max) {
8290
super(max);
8391
}
8492

93+
public void setCompressionAlgorithm(CompressionAlgorithm algo) {
94+
this.compressionAlgorithm = algo;
95+
}
96+
97+
public void setCompressionLevel(int level) {
98+
this.zstdLevel = level;
99+
}
100+
85101
@Override
86102
public boolean asyncDecode(CachedData d) {
87103
if ((d.getFlags() & COMPRESSED) != 0 || (d.getFlags() & SERIALIZED) != 0) {
@@ -179,31 +195,90 @@ public CachedData encode(Object o) {
179195
}
180196
assert b != null;
181197
if (b.length > compressionThreshold) {
198+
int originalLength = b.length;
182199
byte[] compressed = compress(b);
183-
if (compressed.length < b.length) {
200+
if (compressed.length < originalLength) {
184201
getLogger().trace("Compressed %s from %d to %d",
185-
o.getClass().getName(), b.length, compressed.length);
202+
o.getClass().getName(), originalLength, compressed.length);
186203
b = compressed;
187204
flags |= COMPRESSED;
188205
} else {
189206
getLogger().debug("Compression increased the size of %s from %d to %d",
190-
o.getClass().getName(), b.length, compressed.length);
207+
o.getClass().getName(), originalLength, compressed.length);
191208
}
192209

193-
long compression_ratio = Math.round((double) compressed.length / b.length * 100);
194-
updateTimerWithCompressionRatio(compression_ratio);
210+
long ratioPerCent = Math.round((double) compressed.length / originalLength * 100);
211+
recordCompressionRatio(ratioPerCent);
195212
}
196213
return new CachedData(flags, b, getMaxSize());
197214
}
198215

199-
private void updateTimerWithCompressionRatio(long ratio_percentage) {
200-
if(timer == null) {
201-
final List<Tag> tagList = new ArrayList<Tag>(1);
202-
tagList.add(new BasicTag(EVCacheMetricsFactory.COMPRESSION_TYPE, "gzip"));
203-
timer = EVCacheMetricsFactory.getInstance().getPercentileTimer(EVCacheMetricsFactory.COMPRESSION_RATIO, tagList, Duration.ofMillis(100));
204-
};
216+
@Override
217+
protected byte[] compress(byte[] in) {
218+
if (in == null) throw new NullPointerException("Can't compress null");
219+
switch (compressionAlgorithm) {
220+
case ZSTD:
221+
return Zstd.compress(in, zstdLevel);
222+
case GZIP:
223+
return super.compress(in);
224+
default:
225+
throw new IllegalArgumentException("Unsupported compression algorithm: " + compressionAlgorithm);
226+
}
227+
}
228+
229+
@Override
230+
protected byte[] decompress(byte[] in) {
231+
if (in == null || in.length == 0) return in;
232+
if (isZstdCompressed(in)) return decompressZstd(in);
233+
return super.decompress(in);
234+
}
235+
236+
private boolean isZstdCompressed(byte[] data) {
237+
if (data == null || data.length < 4) return false;
238+
int magic = ByteBuffer.wrap(data, 0, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
239+
return magic == ZSTD_MAGIC;
240+
}
241+
242+
private byte[] decompressZstd(byte[] in) {
243+
long originalSize = Zstd.decompressedSize(in);
244+
if (originalSize > Integer.MAX_VALUE) {
245+
getLogger().warn("Zstd decompressed size exceeds int range: " + originalSize);
246+
return null;
247+
}
248+
if (originalSize > 0) {
249+
// Fast path: frame carries a content-size header (compress() above always does).
250+
return Zstd.decompress(in, (int) originalSize);
251+
}
252+
// Slow path: declared size is 0, unknown (-1), or invalid (-2) — stream-decode and let
253+
// ZstdInputStream surface any frame errors.
254+
ZstdInputStream zis = null;
255+
try {
256+
zis = new ZstdInputStream(new ByteArrayInputStream(in));
257+
return readAll(zis);
258+
} catch (IOException e) {
259+
getLogger().error("Error reading Zstd input stream", e);
260+
return null;
261+
} finally {
262+
try { if (zis != null) zis.close(); } catch (IOException ignored) {}
263+
}
264+
}
265+
266+
private static byte[] readAll(InputStream in) throws IOException {
267+
ByteArrayOutputStream out = new ByteArrayOutputStream();
268+
byte[] buf = new byte[8192];
269+
int n;
270+
while ((n = in.read(buf)) != -1) {
271+
out.write(buf, 0, n);
272+
}
273+
return out.toByteArray();
274+
}
205275

206-
timer.record(ratio_percentage, TimeUnit.MILLISECONDS);
276+
private void recordCompressionRatio(long ratioPerCent) {
277+
final List<Tag> tagList = new ArrayList<Tag>(1);
278+
tagList.add(new BasicTag(EVCacheMetricsFactory.COMPRESSION_TYPE, compressionAlgorithm.name().toLowerCase()));
279+
EVCacheMetricsFactory.getInstance()
280+
.getDistributionSummary(EVCacheMetricsFactory.COMPRESSION_RATIO, tagList)
281+
.record(ratioPerCent);
207282
}
208283

209284
}

evcache-core/src/main/java/com/netflix/evcache/EVCacheTranscoder.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.netflix.evcache;
22

3+
import com.netflix.archaius.api.PropertyRepository;
34
import com.netflix.evcache.util.EVCacheConfig;
45

56
import net.spy.memcached.CachedData;
@@ -17,16 +18,15 @@ public EVCacheTranscoder(int max) {
1718
public EVCacheTranscoder(int max, int compressionThreshold) {
1819
super(max);
1920
setCompressionThreshold(compressionThreshold);
20-
}
21-
22-
@Override
23-
public boolean asyncDecode(CachedData d) {
24-
return super.asyncDecode(d);
25-
}
26-
27-
@Override
28-
public Object decode(CachedData d) {
29-
return super.decode(d);
21+
PropertyRepository config = EVCacheConfig.getInstance().getPropertyRepository();
22+
CompressionAlgorithm algo = CompressionAlgorithm.valueOf(
23+
config.get("default.evcache.compression.algorithm", String.class)
24+
.orElse("GZIP").get().toUpperCase());
25+
setCompressionAlgorithm(algo);
26+
if (algo == CompressionAlgorithm.ZSTD) {
27+
setCompressionLevel(config.get("default.evcache.compression.zstd.level", Integer.class)
28+
.orElse(DEFAULT_ZSTD_COMPRESSION_LEVEL).get());
29+
}
3030
}
3131

3232
@Override

0 commit comments

Comments
 (0)