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 小时前
独立做产品,做一个,还是做多个找爆款?
前端·后端·产品
码事漫谈1 小时前
深入剖析C++内存模型:超越原子性的多线程编程基石
后端
影灵衣丶1 小时前
Go 1.25 实战解读:绿茶 GC 与容器核心数感知(工程影响与落地)
后端·go
码事漫谈1 小时前
深入理解C++的Const关键字:从语法到语义的全面剖析
后端
SimonKing2 小时前
一键开启!Spring Boot 的这些「魔法开关」@Enable*,你用对了吗?
java·后端·程序员
沢田纲吉2 小时前
🗄️ MySQL 表操作全面指南
数据库·后端·mysql
小图图2 小时前
Claude Code 黑箱揭秘
前端·后端
bobz9652 小时前
新研究:纯强化学习可激发大模型高级推理能力
后端
shark_chili2 小时前
解密计算机心脏:CPU南北桥技术发展全解析
后端