警惕序列化带来的性能消耗

952 阅读2分钟

场景

线上一个接口qps较高需要优化,在检查内部逻辑没什么优化空间后(几个crud然后组装数据),决定加上缓存。避免了频繁的数据库访问

public AssistantGroupGetListResponse getList(AssistantGroupGetListRequest request) {
  final Optional<AssistantGroupGetListResponse> optional = cacheClient
      .getAssistedGroupCache(request.getOrgId(), null, 0L);
  if (optional.isPresent()) {
    return optional.get();
  }
  List<AssistantGroupRecord> assistantGroupRecords = assistantGroupDao
      .getList(request.getOrgId());
  List<AssistantGroupExtInfo> assistantGroupExtInfos = getListByRecords(assistantGroupRecords,
      request.getOrgId(), null, null);
  List<AssistantGroup> assistantGroups = assistantGroupExtInfos.stream()
      .map(AssistantGroupExtInfo::getAssistantGroup)
      .collect(Collectors.toList());
  AssistantGroupGetListResponse response = AssistantGroupGetListResponse.newBuilder()
      .setCode(DoorErrorCode.SUCCESS.getCode())
      .setMsg(DoorErrorCode.SUCCESS.getMsg())
      .addAllData(assistantGroups)
      .build();
  cacheClient.saveAssistedGroupCache(response, request.getOrgId(), null, 0L);
  return response;
}

缓存方法

 @Override
  public Optional<StoreInfo> getStoreInfo(Long storeId) {
    if (Objects.isNull(storeId) || AppConfig.getStoreInfoExpire() <= 0) {
      return Optional.empty();
    }
    final Optional<String> optional = redisClient
        .handle(jedis -> jedis.get(CacheKeyEnum.STORE_INFO_KEY.format(storeId)));
    if (!optional.isPresent() || StringUtils.isBlank(optional.get())) {
      return Optional.empty();
    }
    final StoreInfo storeInfo = GsonHolder.GSON.fromJson(optional.get(), StoreInfo.class);
    return Optional.ofNullable(storeInfo);
  }

  @Override
  public void saveStoreInfoCache(Long storeId, StoreInfo storeInfo) {
    if (AppConfig.getStoreInfoExpire() <= 0 || Objects.isNull(storeInfo)) {
      return;
    }
    final int expireTime = RandomUtils
        .nextInt(AppConfig.getStoreInfoExpire(), AppConfig.getStoreInfoExpire() * 2);
    redisClient
        .handle(jedis -> jedis.set(CacheKeyEnum.STORE_INFO_KEY.format(storeId), GsonHolder.GSON
            .toJson(storeInfo), SetParams.setParams().ex(expireTime)));
  }

上面代码表面上看着没什么问题,一个很简单的将请求参数做key,response做value序列化到redis,当同样参数的请求来时,从redis反序列化出response然后返回。在测试环境也没任何问题,但是一上生产就出问题了,容器很快就被打爆。
首先是cpu被打满,限流率达到100%(下图与实际场景无关,只是找不到当时的图了)

image.png 对于cpu突然飙升,可以通过火焰图查看,通过火焰图我们可以看到Gson.fromjson对cpu的使用有26%之多。而且根据火焰图也能看到,就是我们跟我们之前上的代码有关。 image.png

代码中使用 GSON.fromJson 进行反序列化的类 AssistantGroupGetListResponse 是一个 protobuf message class 而不是一个 POJO class。使用 gson 对这个类进行序列化和反序列化存在比较明显的性能问题:

  1. protoc 生成的 AssistantGroupGetListResponse 中包含了很多我们无法直接控制,也不是被业务逻辑直接需要的 private field,而 gson 会无差别地对这些 field 都执行序列化和反序列化。
  2. AssistantGroupGetListResponse 既然是一个 protobuf message,当然使用 pb 进行序列化/反序列化,而且在正确性和性能上更有保障。

    修改后的代码如下
public Optional<AssistantGroupGetListResponse> getAssistedGroupCache(Integer orgId, List<Long> storeId) {
    if (Objects.isNull(orgId)) {
      return Optional.empty();
    }
    // 如果设置的缓存时间小于0 则认为不使用缓存
    if (AppConfig.getAssistantGroupExpire() <= 0) {
      return Optional.empty();
    }
    final String key = getAssistedGroupKey(orgId, storeId);
    final Optional<byte[]> optional = redisClient
        .handle(jedis -> jedis.get(key.getBytes(StandardCharsets.UTF_8)));
    if (optional.isPresent()) {
      try {
        final AssistantGroupGetListResponse assistantGroupGetListResponse = AssistantGroupGetListResponse
            .parseFrom(optional.get());
        return Optional.ofNullable(assistantGroupGetListResponse);
      } catch (Exception e) {
        LOGGER.error("getAssistedGroupCache err:{}", e);
      }
    }
    return Optional.empty();
  }

上线后再次查看火焰图,可以看到Gson使用的cpu已经下降到13%

image.png