../c-duan-gao-bing-fa-xia-de-kv-cun-chu-you-hua

C 端高并发下的 KV 存储优化

C 端场景下分布式 KV 存储(如 Redis、Tair)随着业务逻辑的日益复杂、对象体积不断膨胀,大 Value 问题成为系统顽疾之一,危害系统稳定性与接口性能。

为了解决这一痛点,同时不牺牲系统的吞吐能力,针对性地设计出了一套基于 Fory 序列化与 Zstd 压缩的组合方案。这套方案的核心逻辑在于:利用 Fory 的极致性能解决序列化带来的 CPU 和耗时问题,利用 Zstd 的高压缩比与解压速度解决 KV 存储的大 Value 问题。

为什么是 Fory 与 Zstd

在传统 Java 服务端开发中,序列化通常使用 JDK 自带方案或 Hessian,压缩习惯使用 Gzip。但在 QPS 极高的场景下该组合性能差、系统压力大。

Apache Fory(淘宝开源)是一个基于动态代码生成和零拷贝技术的多语言序列化框架,实现了无需 IDL 编译的原生多语言编程范式,并提供最高 170 倍的性能和极致的易用性。大幅提升大规模数据传输、高并发 RPC、分布式系统、云原生中间件等场景的性能,显著降低多语言系统的研发成本。

Zstandard(Facebook 开源)是一个提供高压缩比的快速压缩算法,采用了有限状态熵(Finite State Entropy,缩写为 FSE)编码器。该编码器是基于 ANS 理论开发的一种新型熵编码器,提供了非常强大的压缩速度/压缩率的折中方案(事实上也的确做到了“鱼”和“熊掌”兼得)。Zstandard 达到了 Pareto frontier(资源分配最佳的理想状态),因为它解压缩速度快于任何其他当前可用的算法,但压缩比类似或更好。

选型对比

序列化算法对比

在序列化层面,我们主要关注序列化速度(影响接口耗时)、码流大小(影响网络与存储)以及易用性。

序列化框架序列化/反序列化速度码流大小易用性与兼容性适用场景
Fory极快 (JIT 加速)小 (支持类注册)高 (兼容 JDK 序列化,无需 IDL)高性能 Java 应用,内部 RPC/KV
Hessian2中等中等高 (兼容性好)传统 RPC,对性能要求不严苛
Protobuf极小低 (需编写.proto 文件)跨语言交互,Schema 稳定的场景
JDK 原生高 (开箱即用)不推荐用于高并发生产环境
Kryo极快中 (需处理线程安全)大数据处理,Spark/Flink

Fory 在保持了 Java 原生兼容性的前提下,提供了接近甚至超越 Protobuf 的性能,且不需要维护复杂的 IDL 文件。

压缩算法对比

在压缩层面,我们重点关注压缩率(节省空间)与解压速度(影响读取 RT)。

压缩算法压缩率压缩速度解压速度特点
Zstd极快综合性能最强,平衡了空间与时间
GzipCPU 消耗大,高并发下易导致 RT 抖动
LZ4极快极快速度最快,但压缩率较低,大 Value 优化有限
SnappyGoogle 出品,性能介于 LZ4 与 Gzip 之间

Zstd 在压缩率上完胜 LZ4,在解压速度上完胜 Gzip,达到了 Pareto frontier。

最佳实践

整体流程

Fory 实例复用

创建 Fory 成本很高,始终复用实例,严禁在每次请求中通过 new Fory() 创建实例,应使用 ThreadSafeFory 或者 ThreadLocal 来复用实例。同时按 ID 注册类会有更好的性能和更小的空间开销。

private static final ThreadSafeFory FORY_INSTANCE;

private static final Map<Class<?>, Integer> REGISTERED_CLASSES_WITH_IDS = new LinkedHashMap<>();
static {
    REGISTERED_CLASSES_WITH_IDS.put(LbsRecallCacheBO.class, 1);
    REGISTERED_CLASSES_WITH_IDS.put(ActivityPoiDocument.class, 2);
    REGISTERED_CLASSES_WITH_IDS.put(SkuSolrVo.class, 3);
    REGISTERED_CLASSES_WITH_IDS.put(PoiScore.class, 4);
}

Zstd 解压精确分配

Zstd 在解压时,如果能预先知道解压后的数据大小,就可以直接分配精确的内存空间,避免多次内存扩容或分配过大的缓冲区。最佳实践是在压缩后的字节数组头部手动写入原始数据的长度(或者使用 ZSTD_getFrameContentSize API)。

public static byte[] serializeAndCompress(Object object) {
    try {
        if (object == null) {
            return new byte[0];
        }
        byte[] serializedData = FORY_INSTANCE.serializeJavaObject(object);
        if (serializedData == null || serializedData.length == 0) {
            LOGGER.warn("Serialized data is empty for object: {}", object.getClass().getName());
            return new byte[0];
        }
        byte[] compressedData = Zstd.compress(serializedData, ZSTD_COMPRESSION_LEVEL);
        ByteBuffer buffer = ByteBuffer.allocate(HEADER_SIZE + compressedData.length);
        buffer.putInt(serializedData.length);
        buffer.put(compressedData);
        return buffer.array();
    } catch (Exception e) {
        LOGGER.error("Fory+Zstd serialization failed for object: {}", object.getClass().getName(), e);
        throw new RuntimeException("Fory+Zstd serialization failed", e);
    }
}

优化效果

某核心营销页面(脱敏)正常召回 TP90 59.4ms,使用缓存召回 TP90 1.6ms,同时午高峰该页面缓存命中率 80% 左右。且在 10 万 QPS 的入口流量下 TP90 能够保证 2ms 以内,Value 大小均小于 100Kb。