Spring Redis/Lettuce 客户端在 NIO 事件循环中遇到瓶颈了吗?

Is Spring Redis/Lettuce client bottlenecked in NIO event loop?

我正在构建一个 Spring 应用程序以从大约 100 个 websocket 客户端捕获数据,然后将数据以类似队列的方式存储在 Redis 服务器中。问题是服务器随着时间的推移开始冻结,最终 websocket 客户端由于主机超时而断开连接。

我最初认为问题出在使用 Spring Redis 存储库上,但在我切换到 Redis 模板后问题仍然存在。

然后我认为问题出在 Redis 对象的(反)序列化上,有一段时间,这就是问题所在。通过分析,我发现从字符串中解析双精度数很慢(当每秒处理数千个时),所以我改为编写一个序列化函数来将双精度数组转换为 Redis 的字节数组。这大大减少了 CPU 时间。

fun DoubleArray.toBytes(): ByteArray {
    val buffer = ByteBuffer.allocate(DOUBLE_SIZE_BYTES * size)
    forEachIndexed { i, d -> buffer.putDouble(DOUBLE_SIZE_BYTES * i, d) }
    return buffer.array()
}
open class SingleSampleRepository<T : SampleModel>(
    private val tClass: KClass<T>,
    template: RedisTemplate<String, ByteArray>
) {
    private val ops = template.opsForValue()
    private val keyName = "ValueOf${tClass.simpleName}"

    fun find(deviceId: Long): T? {
        val name = "$keyName:$deviceId"
        return SampleModelHelper.deserializeFromBytes(tClass, ops.get(name) ?: return null)
    }

    fun save(deviceId: Long, sample: T) {
        val name = "$keyName:$deviceId"
        ops.set(name, sample.serializeToBytes())
    }
}
open class MultiSampleRepository<T : SampleModel>(
    private val tClass: KClass<T>,
    private val template: RedisTemplate<String, ByteArray>,
    private val maxSamples: Int = MAX_SAMPLES
) {
    companion object {
        private const val SAMPLES_HZ = 50
        private const val TIME_DURATION_SECONDS = 120
        const val MAX_SAMPLES = TIME_DURATION_SECONDS * SAMPLES_HZ
    }

    private val ops = template.opsForZSet()
    private val keyName = "ZSetOf${tClass.simpleName}"
    private val scoreProperty = tClass.memberProperties.first { it.hasAnnotation<RedisScore>() }

    fun findAll(deviceId: Long): Set<T> {
        val name = "$keyName:$deviceId"
        return ops.range(name, 0, ops.size(name) ?: 0)?.map {
            SampleModelHelper.deserializeFromBytes(tClass, it)
        }?.toSet() ?: emptySet()
    }

    fun saveAll(deviceId: Long, samples: Set<T>) {
        val name = "$keyName:$deviceId"
        template.delete(name)
        ops.add(name, samples.map {
            ZSetOperations.TypedTuple.of(it.serializeToBytes(), scoreProperty.get(it) as Double)
        }.toMutableSet())
        while ((ops.size(name) ?: 0) > MAX_SAMPLES) ops.popMin(name)
    }

    fun save(deviceId: Long, sample: T) {
        val name = "$keyName:$deviceId"
        ops.add("$keyName:$deviceId", sample.serializeToBytes(), scoreProperty.get(sample) as Double)
        while ((ops.size(name) ?: 0) > MAX_SAMPLES) ops.popMin(name)
    }
}

我现在怀疑 spring-data-redis Lettuce 客户端是问题所在。具体来说,Lettuce 似乎只使用一个 NIO 事件循环线程。我不知道这是否是 good/bad 问题,所以请告诉我它是否正常工作。以下是分析的一些屏幕截图:

在看到有关 Lettuce 的其他帖子后,我也尝试使用 ClientResources 和自定义线程池,但是 none 这些方法增加了 NIO 事件循环线程数。

我知道 Redis 本身大部分是单线程的,但从分析来看,大多数 CPU 时间似乎花在了 encoding/decoding Redis 命令上,而不是实际发送它们。 Lettuce 应该为 NIO 事件循环使用多线程吗?

除了将 shareNativeConnection 设置为 false 之外,我还必须设置 LettucePoolingClientConfiguration。这个组合终于增加了线程数,我看到性能有了很大的提升。

@Configuration
class RedisConfig {
    @Bean
    fun connectionFactory(): RedisConnectionFactory {
        val redisConfig = RedisStandaloneConfiguration()
        val clientConfig = LettucePoolingClientConfiguration.builder().build()
        val factory = LettuceConnectionFactory(redisConfig, clientConfig)
        factory.shareNativeConnection = false
        return factory
    }
}