Flink KeySelector

结论

  1. FLink KeyBy算子的KeySelector的构造是在JobManager/客户端构造的 ,而getKey获取key去进行分区,是在TaskManager的subTask中执行的,因此,这就涉及到一个数据传递的问题。
  2. Flink的并行度,也就是subTask其实底层就是一个个互相隔离的Task线程,不了解的可以先看Flink TM、subTask、JVM、算子链、slot、slot组的关系
  3. Session 模式下,多个任务共享集群资源,但 Flink 会通过类加载隔离(如 ChildFirstClassLoader)和任务执行上下文隔离,保证不同任务的用户代码(包括 JsonKeySelector2)相互独立
  4. 线程级隔离(subTask之间) :ThreadLocal每个线程生成独立的 Field 缓存副本。即使两个任务的 subtask 运行在同一个 TM 的不同线程中,它们的 usedFieldCache 也会各自持有 "属于当前线程" 的缓存,不会互相覆盖或干扰
  5. 任务级隔离(同一session,不同FlinkJob间) :Flink 也会通过以下机制保证隔离,即使两个任务的 subtask 运行在同一个 TM 的相同线程中(极端情况,比如资源超配时的线程复用)
    • 类加载隔离:每个任务的用户代码(包括 JsonKeySelector)由独立的类加载器加载,避免类定义 / 静态变量串用;
    • 执行上下文隔离:任务的状态、算子逻辑的初始化参数(如 jsonKeyFields)都是任务专属的,不同任务的 JsonKeySelector 实例在初始化时,会根据自身任务的配置加载字段,与其他任务无关。
  6. Field本身可以序列化,但是Field一旦关联上某个字段,并获取了这个字段的上下文信息后,是不可序列化和反序列化的,会报错java.io.NotSerializableException: java.lang.reflect.Field ,而transient修饰会让其不参与序列化
  7. readObject()是 Java自定义反序列化机制 的核心方法,与其相对的是序列化机制writeObject(),他有具体被调用的规则如下
    1. 当前类实现了java.io.Serializable接口
    2. 方法必须被定义为private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException

1.自定义JsonKeySelector

功能:通过传入一个keyFiedls数组,去实现分组操作

java 复制代码
public class JsonKeySelector<IN> implements KeySelector<IN, Long>, Serializable {
    private static final Logger LOGGER = LoggerFactory.getLogger(JsonKeySelector2.class);
    private static final long serialVersionUID = 1L;
    private static final char SPLIT_CHAR = '\001';
    private static final char NULL_MARKER = '\002';

    // 核心配置变量
    private final List<String> jsonKeyFields; // 配置的JSON分组字段
    private final boolean isAutoSalt; // 加盐开关
    private final String autoSalt; // 盐值
    private final Class<IN> bindPojoClass; // POJO类

    /**  
    * 1.Field不可序列化和反序列化,因此需要用transient修饰  
    * 2.采用ThreadLocal是确保线程安全,Flink多并行度底层其实也是多线程,只不过用线程隔离机制隔开了  
    */
    private transient ThreadLocal<Map<String, Field>> usedFieldCache;

    /**
     * 构造函数,在JobManager/client中被调用
     */
    public JsonKeySelector2(List<String> jsonKeyFields, boolean isAutoSalt, String pojoName) {
        // 参数校验
        if (jsonKeyFields == null || jsonKeyFields.isEmpty()) {
            throw new IllegalArgumentException("JSON Key Fields name is null");
        }
        if (StringUtils.isEmpty(pojoName)) {
            throw new IllegalArgumentException("POJO name is null");
        }

        // 加载POJO类
        Class<IN> pojoClass;
        try {
            pojoClass = (Class<IN>) Class.forName(pojoName);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("POJO {} is not exists :" + pojoName, e);
        }

        // 初始化配置
        this.jsonKeyFields = jsonKeyFields;
        this.isAutoSalt = isAutoSalt;
        this.autoSalt = isAutoSalt ? UUID.randomUUID().toString() : "";
        this.bindPojoClass = pojoClass;

        // 初始化Field缓存 -- 其实用的反射去获取jsonKeyFields的Fields进行缓存
        initFieldCache();

        LOGGER.info("Initialization completed, POJO: {}, KeyBy fields: {}", pojoClass.getSimpleName(), jsonKeyFields);
    }

    /**
     * 优化核心:用映射表替代嵌套循环,时间复杂度从O(n*m)降为O(n+m)
     */
    private void initFieldCache() {
        this.usedFieldCache = ThreadLocal.withInitial(() -> {
            // 1. 构建POJO字段的"JSON键→Field"映射表(只遍历一次POJO字段)
            Map<String, Field> pojoJsonFieldMap = new HashMap<>();
            Field[] allPojoFields = bindPojoClass.getDeclaredFields();
            for (Field pojoField : allPojoFields) {
                JsonProperty jsonProp = pojoField.getAnnotation(JsonProperty.class);
                if (jsonProp != null) {
                    String jsonKey = jsonProp.value();
                    // 若有重复的@JsonProperty值,后面的会覆盖前面的(符合注解使用规范)
                    pojoJsonFieldMap.put(jsonKey, pojoField);
                }
            }

            // 2. 遍历配置的JSON字段,从映射表中O(1)查找(无嵌套循环)
            Map<String, Field> fieldCache = new HashMap<>(jsonKeyFields.size());
            for (String targetJsonKey : jsonKeyFields) {
                Field matchedField = pojoJsonFieldMap.get(targetJsonKey);
                if (matchedField == null) {
                    throw new RuntimeException("No field in POJO is annotated with @JsonProperty(\"" + targetJsonKey + "\")");
                }
                // 开启私有字段访问权限
                matchedField.setAccessible(true);
                fieldCache.put(targetJsonKey, matchedField);
            }

            LOGGER.info("FieldCache initialization done, number of fields loaded: {}", fieldCache.size());
            return fieldCache;
        });
    }

    /**
     * Flink TaskManager的subTask获取JobManager发来的信息,会进行反序列化,此时我们进行重建缓存
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // 保留默认反序列化逻辑
        in.defaultReadObject();
        // 对transient修饰的属性将进行重新初始化
        initFieldCache();
    }

    @Override
    public Long getKey(IN record) throws Exception {
        Map<String, Field> fieldCache = usedFieldCache.get();
        Hasher hasher = Hashing.murmur3_128().newHasher();
        for (String jsonKey : jsonKeyFields) {
            Field field = fieldCache.get(jsonKey);
            Object value = field.get(record);
            if (value == null) {
                hasher.putChar(NULL_MARKER);
            } else {
                putValue(hasher, value);
            }
            hasher.putChar(SPLIT_CHAR);
        }

        if (isAutoSalt) {
            hasher.putString(autoSalt, StandardCharsets.UTF_8);
        }

        Long key = hasher.hash().padToLong();
        return key;
    }

    /** 按类型写哈希 */
    private void putValue(Hasher hasher, Object value) {
        if (value instanceof Long) hasher.putLong((Long) value);
        else if (value instanceof Integer) hasher.putInt((Integer) value);
        else if (value instanceof Boolean) hasher.putBoolean((Boolean) value);
        else if(value instanceof Double) hasher.putDouble((Double) value);
        else hasher.putString(value.toString(), StandardCharsets.UTF_8);
    }

2.现象

Flink的JobManager的log如下 Flink的TaskManager的log如下 从日志打印,我们可以看出

  1. FieldCache initialization done, number of fields loaded: {}日志只在TaskManager中打印,它属于initFieldCache(),而构造函数和readObject()都有调用它,但构造函数会多打印Initialization completed, POJO: {}, KeyBy fields: {},但是TaskManager中并没有这个日志,说明他是readObject()进行反序列化的时候调用的,因此,反序列化和getKey的操作是由TaskManager的subTask执行的,且根据并行度会打印对应数量的日志
  2. Initialization completed, POJO: {}, KeyBy fields: {}这个日志在JobManager中打印,说明构造函数是在客户端中调用的,客户端执行后会生成JobGraph然后传给JobManager,日志也在JobManager中
  3. 构造函数中也调用了initFieldCache()但是为啥没打印FieldCache initialization done, number of fields loaded: {}呢?原因在于:ThreadLocal.withInitial()是懒加载,他只是先创建这么个实例,但是真正执行里面的Lambda表达式是在首次调用usedFieldCache.get() 时才会执行 ,而构造函数中并没有调usedFieldCache.get()所以在JobManager的日志中根本没有FieldCache initialization done, number of fields loaded: {}这个日志
相关推荐
二进制流水搬运工1 小时前
入职第一天拉了23个仓库,我写了个脚本解放双手
大数据·elasticsearch·搜索引擎
uzong2 小时前
把一面面试总结做成一个 Skill:重复的事,就别每次都重写
后端·面试
Meya11272 小时前
实时采集 + 全域可视化,打造跨站点机房一体化U位管理方案
大数据·运维·人工智能
Gofarlic_OMS2 小时前
NX浮动许可调度黑名单机制,对比两款谁更合理
java·大数据·运维·开源·制造
万岳科技系统开发2 小时前
智慧医院小程序开发推动医疗服务流程全面线上化
大数据·开发语言·人工智能
程序员海军2 小时前
一个 AI 应用开发程序员的一天,都在屏幕前忙些什么?
前端·后端·程序员
不能放弃治疗2 小时前
MCP
后端
江湖十年3 小时前
Go 语言中 YAML to JSON 踩坑笔记
后端·go·json
fajianchen4 小时前
主数据管理
大数据
慧新软件4 小时前
外贸GEO新思路:用客户真实痛点驱动内容生产
大数据