关于数据:一次元数据空间内存溢出的排查记录-京东云技术团队

3次阅读

共计 9777 个字符,预计需要花费 25 分钟才能阅读完成。

在利用中,咱们应用的 SpringData ES 的 ElasticsearchRestTemplate来做查问,应用形式不对,导致每次 ES 查问时都新实例化了一个查问对象,会加载相干类到元数据中。最终长时间运行后元数据呈现内存溢出;

问题起因:类加载过多,导致元数据 OOM。非类实例多或者大对象问题;

排查形式:

查看 JVM 运行状况,发现元数据满导致内存溢出;
导出内存快照,通过 OQL 疾速定位肇事者;
排查对应类的应用场景和加载场景(重点序列化反射场景);

起源

06-15 下午正摩肩擦掌的备战着早晨 8 点。收到预发机器的一个 GC 次数报警。

【正告】UMP JVM 监控【正告】异步(async 采集点:async.jvm.info(别名:jvm 监控)15:42:40 至 15:42:50【xx.xx.xx.xxx(10174422426)(未知分组)】,JVM 监控 FullGC 次数 = 2 次[偏差 0%],超过 1 次 FullGC 次数 >= 2 次【工夫】2023-06-15 15:42:50【类型】UMP JVM 监控

第一工夫惊讶了下。该利用次要作用是接 MQ 音讯和定时工作,同时工作和 MQ 都和线上做了隔离,也没有收到大流量的告警。

先看了下对应 JVM 监控:

只看下面都狐疑是监控异样(之前用文件采集的时候有遇到过,看 CPU 的确有稳定。但堆根本无涨幅,狐疑非堆。)

问题排查

定位剖析

既然狐疑非堆,咱们先通过 jstat来看看状况

  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00   0.00   0.89   3.67  97.49  97.96    854   23.720   958  615.300  639.020
  0.00   0.00   0.89   3.67  97.49  97.96    854   23.720   958  615.300  639.020
  0.00   0.00   0.89   3.67  97.49  97.96    854   23.720   958  615.300  639.020
  0.00   0.00   0.89   3.67  97.49  97.96    854   23.720   958  615.300  639.020
  0.00   0.00   0.89   3.67  97.49  97.96    854   23.720   958  615.300  639.020

M 列代表了 metaspace 的使用率,以后曾经 97.49% 进一步印证了咱们的猜想。

接下来通过 jmap 导出内存快照剖析。这里我习惯应用 Visual VM 进行剖析。

在这里咱们看到有 118588 个类被加载了。失常业务下不会有这么多类。

这里咱们走了很多弯路。

首先查看内存对象,依据类的实例数排了个序,试图看看是否是某个或某些类实例过多导致。

这里个别是 排查堆异样 时应用,能够看大对象和某类的实例数,但咱们的问题是 类加载过多。非类实例对象多或者大。这里排除。

后续还尝试了间接应用 Visual VM 的聚合按包门路统计,同时排序。收效都甚微。看不出啥异样来。

这里咱们应用 OQL 来进行查问统计。

语句如下:

var packageClassSizeMap = {};
// 遍历统计以最初一个逗号做宰割
heap.forEachClass(function (it) {var packageName = it.name.substring(0, it.name.lastIndexOf('.'));
    if (packageClassSizeMap[packageName] != null) {packageClassSizeMap[packageName] = packageClassSizeMap[packageName] + 1;
    } else {packageClassSizeMap[packageName] = 1;
    }
});
// 排序 因为 Visual VM 的查问有数量限度。var sortPackageClassSizeMap = [];
map(sort(Object.keys(packageClassSizeMap), function (a, b) {return packageClassSizeMap[b] - packageClassSizeMap[a]
}), function (it) {
    sortPackageClassSizeMap.push({
        package: it,
        classSize: packageClassSizeMap[it]
    })
});
sortPackageClassSizeMap;

执行成果如下:

能够看到,com.jd.bapp.match.sync.query.es.po 下存在 92172 个类。这个包下,不到 20 个类。这时咱们在回到开始查看类的中央。看看该门路下都是些什么类。

这里附带一提,间接依据门路获取对应的类数量:

var packageClassSizeMap = {};
// 遍历统计以最初一个逗号做宰割
heap.forEachClass(function (it) {var packageName = it.name.substring(0, it.name.lastIndexOf('.'));
    // 加门路过滤版
    if (packageName.indexOf('com.jd.bapp.match.sync.query.es.po')){if (packageClassSizeMap[packageName] != null) {packageClassSizeMap[packageName] = packageClassSizeMap[packageName] + 1;
        } else {packageClassSizeMap[packageName] = 1;
        }
    }
});

sortPackageClassSizeMap;

查问 com.jd.bapp.match.sync.query.es.po 门路下的 classes

咱们能够看到:

  • 每个 ES 的 Po 对象存在大量类加载,在前面有拼接 Instantiator_xxxxx
  • 局部类有实例,局部类无实例。(count 为实例数)

从下面失去的信息得出是 ES 相干查问时呈现的。咱们本地 debug 查问跟踪下。

抽丝剥茧

这里列下次要排查流程

在利用中,咱们应用的 SpringData ES 的 ElasticsearchRestTemplate来做查问,次要应用办法 org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate#search

重点代码如下:

public <T> SearchHits<T> search(Query query, Class<T> clazz, IndexCoordinates index) {
    // 初始化 request
    SearchRequest searchRequest = requestFactory.searchRequest(query, clazz, index);
    // 获取值
    SearchResponse response = execute(client -> client.search(searchRequest, RequestOptions.DEFAULT));
  
    SearchDocumentResponseCallback<SearchHits<T>> callback = new ReadSearchDocumentResponseCallback<>(clazz, index);
    // 转换为对应类型
    return callback.doWith(SearchDocumentResponse.from(response));
}

加载

首先看初始化 request 的逻辑

  • org.springframework.data.elasticsearch.core.RequestFactory#searchRequest

    • 首先是:org.springframework.data.elasticsearch.core.RequestFactory#prepareSearchRequest

      • 这里有段代码是对搜寻后果的排序解决:prepareSort(query, sourceBuilder, getPersistentEntity(clazz)); 重点就是这里的 getPersistentEntity(clazz)
        这段代码次要会辨认以后类是否曾经加载过,没有加载过则加载到内存中:

        @Nullable
        private ElasticsearchPersistentEntity<?> getPersistentEntity(@Nullable Class<?> clazz) {
            // 从 convert 上下文中获取判断该类是否曾经加载过,如果没有加载过,就会从新解析加载并放入上下文
            return clazz != null ? elasticsearchConverter.getMappingContext().getPersistentEntity(clazz) : null;
        }

具体加载的实现见:具体实现见:org.springframework.data.mapping.context.AbstractMappingContext#getPersistentEntity(org.springframework.data.util.TypeInformation<?>)

    /*
     * (non-Javadoc)
     * @see org.springframework.data.mapping.model.MappingContext#getPersistentEntity(org.springframework.data.util.TypeInformation)
     */
    @Nullable
    @Override
    public E getPersistentEntity(TypeInformation<?> type) {Assert.notNull(type, "Type must not be null!");

        try {read.lock();
            // 从上下文获取以后类
            Optional<E> entity = persistentEntities.get(type);
            // 存在则返回
            if (entity != null) {return entity.orElse(null);
            }
        } finally {read.unlock();
        }
        if (!shouldCreatePersistentEntityFor(type)) {
            try {write.lock();
                persistentEntities.put(type, NONE);
            } finally {write.unlock();
            }
            return null;
        }
        if (strict) {throw new MappingException("Unknown persistent entity" + type);
        }
        // 不存在时,增加该类型到上下文
        return addPersistentEntity(type).orElse(null);
    }

应用

上述是加载流程。执行查问后,咱们还须要进行一次转换。这里就到了应用的中央:org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate#search 中 callback.doWith(SearchDocumentResponse.from(response));

这里这个办法会申请外部的 doWith 办法。实现如下:

@Nullable
public T doWith(@Nullable Document document) {if (document == null) {return null;}
    // 获取到待转换的类实例
    T entity = reader.read(type, document);
    return maybeCallbackAfterConvert(entity, document, index);
}

其中的 reader.read 会先从上下文中获取上述加载到上下文的类信息,而后读取

    @Override
    public <R> R read(Class<R> type, Document source) {TypeInformation<R> typeHint = ClassTypeInformation.from((Class<R>) ClassUtils.getUserClass(type));
        typeHint = (TypeInformation<R>) typeMapper.readType(source, typeHint);

        if (conversions.hasCustomReadTarget(Map.class, typeHint.getType())) {R converted = conversionService.convert(source, typeHint.getType());
            if (converted == null) {
                // EntityReader.read is defined as non nullable , so we cannot return null
                throw new ConversionException("conversion service to type" + typeHint.getType().getName() + "returned null");
            }
            return converted;
        }

        if (typeHint.isMap() || ClassTypeInformation.OBJECT.equals(typeHint)) {return (R) source;
        }
        // 从上下文获取之前加载的类
        ElasticsearchPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(typeHint);
        // 获取该类信息
        return readEntity(entity, source);
    }

读取会走 org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter#readEntity

先是读取该类的初始化器:EntityInstantiator instantiator = instantiators.getInstantiatorFor(targetEntity);

  • 是通过该类实现:org.springframework.data.convert.KotlinClassGeneratingEntityInstantiator#createInstance

    • 而后到:org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator#doCreateEntityInstantiator
    /*
     * (non-Javadoc)
     * @see org.springframework.data.convert.ClassGeneratingEntityInstantiator#doCreateEntityInstantiator(org.springframework.data.mapping.PersistentEntity)
     */
    @Override
    protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity<?, ?> entity) {PreferredConstructor<?, ?> constructor = entity.getPersistenceConstructor();

        if (ReflectionUtils.isSupportedKotlinClass(entity.getType()) && constructor != null) {PreferredConstructor<?, ?> defaultConstructor = new DefaultingKotlinConstructorResolver(entity)
                    .getDefaultConstructor();

            if (defaultConstructor != null) {
                // 获取对象初始化器
                ObjectInstantiator instantiator = createObjectInstantiator(entity, defaultConstructor);

                return new DefaultingKotlinClassInstantiatorAdapter(instantiator, constructor);
            }
        }

        return super.doCreateEntityInstantiator(entity);
    }

这里先申请外部的:createObjectInstantiator

    /**
     * Creates a dynamically generated {@link ObjectInstantiator} for the given {@link PersistentEntity} and
     * {@link PreferredConstructor}. There will always be exactly one {@link ObjectInstantiator} instance per
     * {@link PersistentEntity}.
     *
     * @param entity
     * @param constructor
     * @return
     */
    ObjectInstantiator createObjectInstantiator(PersistentEntity<?, ?> entity,
            @Nullable PreferredConstructor<?, ?> constructor) {

        try {
            // 调用生成
            return (ObjectInstantiator) this.generator.generateCustomInstantiatorClass(entity, constructor).newInstance();} catch (Exception e) {throw new RuntimeException(e);
        }
    }

获取对象生成实例:generateCustomInstantiatorClass 这里获取类名称,会追加 _Instantiator_和对应类的 hashCode


        /**
         * Generate a new class for the given {@link PersistentEntity}.
         *
         * @param entity
         * @param constructor
         * @return
         */
        public Class<?> generateCustomInstantiatorClass(PersistentEntity<?, ?> entity,
                @Nullable PreferredConstructor<?, ?> constructor) {
            // 获取类名称
            String className = generateClassName(entity);
            byte[] bytecode = generateBytecode(className, entity, constructor);

            Class<?> type = entity.getType();

            try {return ReflectUtils.defineClass(className, bytecode, type.getClassLoader(), type.getProtectionDomain(), type);
            } catch (Exception e) {throw new IllegalStateException(e);
            }
        }

        private static final String TAG = "_Instantiator_";

        /**
         * @param entity
         * @return
         */
        private String generateClassName(PersistentEntity<?, ?> entity) {
            // 类名 +TAG+hashCode
            return entity.getType().getName() + TAG + Integer.toString(entity.hashCode(), 36);
        }

到此咱们元数据中的一堆 拼接了 Instantiator_xxxxx 的类起源就破案了。

水落石出

对应问题产生的问题也很简略。

// 每次 search 前 都 new 了个 RestTemplate,导致上下文发生变化,每次从新生成加载
new ElasticsearchRestTemplate(cluster);

这里咱们是双集群模式,每次申请时会由负载决定应用那一个集群。之前在这里每次都 new了一个待应用集群的实例。

外部的上下文每次初始化后都是空的。

  • 申请查问 ES

    • 初始化 ES 查问

      • 上下文为空
      • 加载类信息(hashCode 发生变化)
      • 获取类信息(重计算类名)
      • 从新加载类到元数据

最终长时间运行后元数据空间溢出;

预先论断

1. 过后的长期计划是重启利用,元数据区清空,同时长期也能够放大元数据区大小。

2. 元数据区的类型卸载或回收,8 当前曾经不应用了。

3. 元数据区的透露排查思路:找到加载多的类,而后排查应用状况和可能的加载场景,个别在各种序列化反射场景。

4. 疾速排查可应用咱们的计划。应用 OQL 来实现。

5. 监控能够思考加载类实例监控和元数据空间应用大小监控和对应报警。能够提前发现和解决。

6.ES 查问在启动时对应集群外部初始化一个查问实例。应用那个集群就应用对应的集群查问实例。

附录

VisualVM下载地址:https://visualvm.github.io/

OQL:Object Query Language 可参看在 VisualVM 中应用 OQL 剖析

获取门路下类加载数量,从高到低排序

var packageClassSizeMap = {};
// 遍历统计以最初一个逗号做宰割
heap.forEachClass(function (it) {var packageName = it.name.substring(0, it.name.lastIndexOf('.'));
    if (packageClassSizeMap[packageName] != null) {packageClassSizeMap[packageName] = packageClassSizeMap[packageName] + 1;
    } else {packageClassSizeMap[packageName] = 1;
    }
});
// 排序 因为 Visual VM 的查问有数量限度。var sortPackageClassSizeMap = [];
map(sort(Object.keys(packageClassSizeMap), function (a, b) {return packageClassSizeMap[b] - packageClassSizeMap[a]
}), function (it) {
    sortPackageClassSizeMap.push({
        package: it,
        classSize: packageClassSizeMap[it]
    })
});
sortPackageClassSizeMap;

获取某个门路下类加载数量

var packageClassSizeMap = {};
// 遍历统计以最初一个逗号做宰割
heap.forEachClass(function (it) {var packageName = it.name.substring(0, it.name.lastIndexOf('.'));
    // 加门路过滤版
    if (packageName.indexOf('com.jd.bapp.match.sync.query.es.po')){if (packageClassSizeMap[packageName] != null) {packageClassSizeMap[packageName] = packageClassSizeMap[packageName] + 1;
        } else {packageClassSizeMap[packageName] = 1;
        }
    }
});

sortPackageClassSizeMap;

特地鸣谢

感激黄仕清和 Jdos 同学提供的技术支持。

作者:京东批发 王建波

起源:京东云开发者社区

正文完
 0