Flink Keyed State 详解之六

1. 基本概念

State Schema Evolution(状态 Schema 演化)是 Flink 提供的一种机制,允许在不丢失现有状态数据的情况下修改状态数据结构。这对于长期运行的流处理应用程序非常重要,因为在应用程序的生命周期中,业务需求的变化往往需要对状态结构进行调整。

1.1 核心特性

  • 向后兼容:支持从旧版本状态数据恢复到新版本
  • 向前兼容:支持从新版本状态数据恢复到旧版本
  • 类型安全:确保状态数据在演化过程中的类型安全性
  • 自动处理:Flink 框架自动处理大部分演化工作

1.2 工作原理

State Schema Evolution 通过以下方式工作:

  1. 为状态数据结构定义类型信息
  2. 在状态恢复时检测 Schema 差异
  3. 自动应用必要的转换规则
  4. 确保数据完整性和一致性

2. 适用场景

2.1 数据结构演进

当需要向现有状态数据结构添加新字段时:

java 复制代码
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.typeutils.TypeHint;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;

/**
 * 用户配置状态 Schema 演化示例
 * 版本1:简单用户配置
 */
class UserConfigV1 {
    public String userId;
    public String theme;
    
    public UserConfigV1() {}
    
    public UserConfigV1(String userId, String theme) {
        this.userId = userId;
        this.theme = theme;
    }
    
    @Override
    public String toString() {
        return "UserConfigV1{userId='" + userId + "', theme='" + theme + "'}";
    }
}

/**
 * 用户配置状态 Schema 演化示例
 * 版本2:添加新字段 language
 */
class UserConfigV2 {
    public String userId;
    public String theme;
    public String language;  // 新增字段
    
    public UserConfigV2() {}
    
    public UserConfigV2(String userId, String theme, String language) {
        this.userId = userId;
        this.theme = theme;
        this.language = language;
    }
    
    @Override
    public String toString() {
        return "UserConfigV2{userId='" + userId + "', theme='" + theme + "', language='" + language + "'}";
    }
}

/**
 * 用户配置状态管理函数
 * 演示 Schema 演化过程
 */
public class UserConfigSchemaEvolution extends KeyedProcessFunction<String, UserAction, String> {
    private ValueState<UserConfigV2> userConfigState;
    
    @Override
    public void open(Configuration parameters) {
        // 使用新版本的数据结构
        ValueStateDescriptor<UserConfigV2> descriptor = new ValueStateDescriptor<>(
            "user-config",
            TypeInformation.of(new TypeHint<UserConfigV2>() {})
        );
        
        userConfigState = getRuntimeContext().getState(descriptor);
    }
    
    @Override
    public void processElement(UserAction action, Context ctx, Collector<String> out) throws Exception {
        UserConfigV2 currentConfig = userConfigState.value();
        
        if (currentConfig == null) {
            // 初始化配置
            currentConfig = new UserConfigV2(action.userId, "default", "en");
        }
        
        // 根据操作更新配置
        switch (action.actionType) {
            case "changeTheme":
                currentConfig.theme = action.value;
                break;
            case "changeLanguage":
                currentConfig.language = action.value;
                break;
        }
        
        userConfigState.update(currentConfig);
        out.collect("Updated config: " + currentConfig);
    }
    
    /**
     * 用户操作
     */
    public static class UserAction {
        public String userId;
        public String actionType;
        public String value;
        
        public UserAction() {}
        
        public UserAction(String userId, String actionType, String value) {
            this.userId = userId;
            this.actionType = actionType;
            this.value = value;
        }
    }
}

2.2 字段类型变更

当需要修改现有字段的数据类型时:

java 复制代码
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.typeutils.TypeHint;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;

/**
 * 计数器状态 Schema 演化示例
 * 版本1:使用 int 类型
 */
class CounterV1 {
    public String id;
    public int count;
    
    public CounterV1() {}
    
    public CounterV1(String id, int count) {
        this.id = id;
        this.count = count;
    }
}

/**
 * 计数器状态 Schema 演化示例
 * 版本2:使用 long 类型以支持更大数值
 */
class CounterV2 {
    public String id;
    public long count;  // 类型从 int 变更为 long
    
    public CounterV2() {}
    
    public CounterV2(String id, long count) {
        this.id = id;
        this.count = count;
    }
    
    @Override
    public String toString() {
        return "CounterV2{id='" + id + "', count=" + count + "}";
    }
}

/**
 * 计数器状态管理函数
 * 演示字段类型变更的 Schema 演化
 */
public class CounterSchemaEvolution extends KeyedProcessFunction<String, CountAction, String> {
    private ValueState<CounterV2> counterState;
    
    @Override
    public void open(Configuration parameters) {
        ValueStateDescriptor<CounterV2> descriptor = new ValueStateDescriptor<>(
            "counter",
            TypeInformation.of(new TypeHint<CounterV2>() {})
        );
        
        counterState = getRuntimeContext().getState(descriptor);
    }
    
    @Override
    public void processElement(CountAction action, Context ctx, Collector<String> out) throws Exception {
        CounterV2 currentCounter = counterState.value();
        
        if (currentCounter == null) {
            currentCounter = new CounterV2(action.id, 0L);
        }
        
        // 更新计数器
        if ("increment".equals(action.action)) {
            currentCounter.count++;
        } else if ("decrement".equals(action.action)) {
            currentCounter.count--;
        } else if ("set".equals(action.action)) {
            currentCounter.count = Long.parseLong(action.value);
        }
        
        counterState.update(currentCounter);
        out.collect("Updated counter: " + currentCounter);
    }
    
    /**
     * 计数器操作
     */
    public static class CountAction {
        public String id;
        public String action;
        public String value;
        
        public CountAction() {}
        
        public CountAction(String id, String action, String value) {
            this.id = id;
            this.action = action;
            this.value = value;
        }
    }
}

3. Schema 演化配置

3.1 TypeInformation 配置

java 复制代码
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.typeutils.TypeHint;
import org.apache.flink.api.common.state.ValueStateDescriptor;

/**
 * TypeInformation 配置示例
 */
public class TypeInformationConfiguration {
    
    /**
     * 基本类型 TypeInformation
     */
    public static ValueStateDescriptor<String> configureBasicType() {
        ValueStateDescriptor<String> descriptor = new ValueStateDescriptor<>(
            "basic-type-state",
            Types.STRING  // 使用 Types 工具类
        );
        return descriptor;
    }
    
    /**
     * POJO 类型 TypeInformation
     */
    public static ValueStateDescriptor<UserProfile> configurePOJOType() {
        ValueStateDescriptor<UserProfile> descriptor = new ValueStateDescriptor<>(
            "pojo-type-state",
            TypeInformation.of(new TypeHint<UserProfile>() {})  // 使用 TypeHint
        );
        return descriptor;
    }
    
    /**
     * 复杂类型 TypeInformation
     */
    public static ValueStateDescriptor<ComplexData> configureComplexType() {
        ValueStateDescriptor<ComplexData> descriptor = new ValueStateDescriptor<>(
            "complex-type-state",
            TypeInformation.of(ComplexData.class)  // 直接使用 Class
        );
        return descriptor;
    }
    
    /**
     * 用户信息
     */
    public static class UserProfile {
        public String userId;
        public String name;
        public int age;
        
        public UserProfile() {}
        
        public UserProfile(String userId, String name, int age) {
            this.userId = userId;
            this.name = name;
            this.age = age;
        }
    }
    
    /**
     * 复杂数据结构
     */
    public static class ComplexData {
        public String id;
        public java.util.List<String> tags;
        public java.util.Map<String, Object> properties;
        
        public ComplexData() {}
    }
}

3.2 自定义序列化器

java 复制代码
import org.apache.flink.api.common.typeutils.TypeSerializer;
import org.apache.flink.api.common.typeutils.base.IntSerializer;
import org.apache.flink.api.common.typeutils.base.StringSerializer;
import org.apache.flink.core.memory.DataInputView;
import org.apache.flink.core.memory.DataOutputView;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.common.state.ValueStateDescriptor;

import java.io.IOException;

/**
 * 自定义序列化器示例
 * 支持 Schema 演化的自定义序列化器
 */
public class CustomSerializerExample {
    
    /**
     * 版本1数据结构
     */
    public static class DataV1 {
        public int id;
        public String name;
        
        public DataV1() {}
        
        public DataV1(int id, String name) {
            this.id = id;
            this.name = name;
        }
    }
    
    /**
     * 版本2数据结构(添加了新字段)
     */
    public static class DataV2 {
        public int id;
        public String name;
        public String description;  // 新增字段
        
        public DataV2() {}
        
        public DataV2(int id, String name, String description) {
            this.id = id;
            this.name = name;
            this.description = description;
        }
    }
    
    /**
     * 支持 Schema 演化的自定义序列化器
     */
    public static class DataV2Serializer extends TypeSerializer<DataV2> {
        private static final long serialVersionUID = 1L;
        
        private final IntSerializer idSerializer = IntSerializer.INSTANCE;
        private final StringSerializer nameSerializer = StringSerializer.INSTANCE;
        private final StringSerializer descriptionSerializer = StringSerializer.INSTANCE;
        
        @Override
        public boolean isImmutableType() {
            return false;
        }
        
        @Override
        public TypeSerializer<DataV2> duplicate() {
            return new DataV2Serializer();
        }
        
        @Override
        public DataV2 createInstance() {
            return new DataV2();
        }
        
        @Override
        public DataV2 copy(DataV2 from) {
            return new DataV2(from.id, from.name, from.description);
        }
        
        @Override
        public DataV2 copy(DataV2 from, DataV2 reuse) {
            reuse.id = from.id;
            reuse.name = from.name;
            reuse.description = from.description;
            return reuse;
        }
        
        @Override
        public int getLength() {
            return -1; // 可变长度
        }
        
        @Override
        public void serialize(DataV2 record, DataOutputView target) throws IOException {
            idSerializer.serialize(record.id, target);
            nameSerializer.serialize(record.name, target);
            // 对于新增字段,使用默认值或 null
            descriptionSerializer.serialize(record.description != null ? record.description : "", target);
        }
        
        @Override
        public DataV2 deserialize(DataInputView source) throws IOException {
            int id = idSerializer.deserialize(source);
            String name = nameSerializer.deserialize(source);
            String description = descriptionSerializer.deserialize(source);
            // 处理可能的空值
            if (description.isEmpty()) {
                description = null;
            }
            return new DataV2(id, name, description);
        }
        
        @Override
        public DataV2 deserialize(DataV2 reuse, DataInputView source) throws IOException {
            reuse.id = idSerializer.deserialize(source);
            reuse.name = nameSerializer.deserialize(source);
            String description = descriptionSerializer.deserialize(source);
            reuse.description = description.isEmpty() ? null : description;
            return reuse;
        }
        
        @Override
        public void copy(DataInputView source, DataOutputView target) throws IOException {
            serialize(deserialize(source), target);
        }
        
        @Override
        public boolean equals(Object obj) {
            return obj instanceof DataV2Serializer;
        }
        
        @Override
        public int hashCode() {
            return DataV2Serializer.class.hashCode();
        }
        
        @Override
        public TypeSerializerSnapshot<DataV2> snapshotConfiguration() {
            return new DataV2SerializerSnapshot();
        }
    }
    
    /**
     * 序列化器快照
     */
    public static class DataV2SerializerSnapshot extends TypeSerializerSnapshot<DataV2> {
        private static final int VERSION = 1;
        
        public DataV2SerializerSnapshot() {}
        
        @Override
        public int getCurrentVersion() {
            return VERSION;
        }
        
        @Override
        public void writeSnapshot(DataOutputView out) throws IOException {
            // 写入版本信息
            out.writeInt(VERSION);
        }
        
        @Override
        public void readSnapshot(int readVersion, DataInputView in, ClassLoader userCodeClassLoader) throws IOException {
            // 读取版本信息
            int version = in.readInt();
            // 根据版本进行相应的处理
        }
        
        @Override
        public TypeSerializer<DataV2> restoreSerializer() {
            return new DataV2Serializer();
        }
        
        @Override
        public TypeSerializerSchemaCompatibility<DataV2> resolveSchemaCompatibility(TypeSerializer<DataV2> newSerializer) {
            if (newSerializer instanceof DataV2Serializer) {
                return TypeSerializerSchemaCompatibility.compatibleAsIs();
            }
            return TypeSerializerSchemaCompatibility.incompatible();
        }
    }
    
    /**
     * 使用自定义序列化器的状态描述符
     */
    public static ValueStateDescriptor<DataV2> configureCustomSerializer() {
        ValueStateDescriptor<DataV2> descriptor = new ValueStateDescriptor<>(
            "custom-serializer-state",
            new DataV2Serializer()
        );
        return descriptor;
    }
}

4. Schema 演化策略

4.1 向后兼容演化

java 复制代码
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.typeutils.TypeHint;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;

/**
 * 向后兼容演化示例
 * 从旧版本状态恢复到新版本
 */
public class BackwardCompatibilityExample extends KeyedProcessFunction<String, UserData, String> {
    private ValueState<UserProfileV2> profileState;
    
    @Override
    public void open(Configuration parameters) {
        ValueStateDescriptor<UserProfileV2> descriptor = new ValueStateDescriptor<>(
            "user-profile",
            TypeInformation.of(new TypeHint<UserProfileV2>() {})
        );
        
        profileState = getRuntimeContext().getState(descriptor);
    }
    
    @Override
    public void processElement(UserData data, Context ctx, Collector<String> out) throws Exception {
        UserProfileV2 profile = profileState.value();
        
        if (profile == null) {
            // 从旧版本数据创建新版本对象
            profile = new UserProfileV2(data.userId, data.name, "unknown");  // 新字段使用默认值
        } else {
            // 更新现有数据
            profile.name = data.name;
            // 保持新字段不变
        }
        
        profileState.update(profile);
        out.collect("Updated profile: " + profile);
    }
    
    /**
     * 旧版本用户数据
     */
    public static class UserData {
        public String userId;
        public String name;
        
        public UserData() {}
        
        public UserData(String userId, String name) {
            this.userId = userId;
            this.name = name;
        }
    }
    
    /**
     * 新版本用户信息(添加了新字段)
     */
    public static class UserProfileV2 {
        public String userId;
        public String name;
        public String email;  // 新增字段
        
        public UserProfileV2() {}
        
        public UserProfileV2(String userId, String name, String email) {
            this.userId = userId;
            this.name = name;
            this.email = email;
        }
        
        @Override
        public String toString() {
            return "UserProfileV2{userId='" + userId + "', name='" + name + "', email='" + email + "'}";
        }
    }
}

4.2 向前兼容演化

java 复制代码
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.typeutils.TypeHint;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;

/**
 * 向前兼容演化示例
 * 从新版本状态恢复到旧版本
 */
public class ForwardCompatibilityExample extends KeyedProcessFunction<String, UserDataV2, String> {
    private ValueState<UserProfileV1> profileState;
    
    @Override
    public void open(Configuration parameters) {
        ValueStateDescriptor<UserProfileV1> descriptor = new ValueStateDescriptor<>(
            "user-profile",
            TypeInformation.of(new TypeHint<UserProfileV1>() {})
        );
        
        profileState = getRuntimeContext().getState(descriptor);
    }
    
    @Override
    public void processElement(UserDataV2 data, Context ctx, Collector<String> out) throws Exception {
        UserProfileV1 profile = profileState.value();
        
        if (profile == null) {
            // 从新版本数据创建旧版本对象(忽略新字段)
            profile = new UserProfileV1(data.userId, data.name);
        } else {
            // 更新现有数据
            profile.name = data.name;
        }
        
        profileState.update(profile);
        out.collect("Updated profile: " + profile);
    }
    
    /**
     * 新版本用户数据(包含新字段)
     */
    public static class UserDataV2 {
        public String userId;
        public String name;
        public String email;  // 新增字段
        
        public UserDataV2() {}
        
        public UserDataV2(String userId, String name, String email) {
            this.userId = userId;
            this.name = name;
            this.email = email;
        }
    }
    
    /**
     * 旧版本用户信息
     */
    public static class UserProfileV1 {
        public String userId;
        public String name;
        
        public UserProfileV1() {}
        
        public UserProfileV1(String userId, String name) {
            this.userId = userId;
            this.name = name;
        }
        
        @Override
        public String toString() {
            return "UserProfileV1{userId='" + userId + "', name='" + name + "'}";
        }
    }
}

5. 完整演化示例

java 复制代码
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.typeutils.TypeHint;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;

/**
 * 完整的 Schema 演化示例
 * 演示从版本1到版本3的完整演化过程
 */
public class CompleteSchemaEvolutionExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        
        // 模拟用户数据流
        DataStream<UserEvent> events = env.fromElements(
            new UserEvent("user1", "login", System.currentTimeMillis()),
            new UserEvent("user2", "login", System.currentTimeMillis() + 1000),
            new UserEvent("user1", "updateProfile", System.currentTimeMillis() + 2000),
            new UserEvent("user3", "login", System.currentTimeMillis() + 3000)
        );
        
        // 处理用户事件,使用演化后的状态结构
        events
            .keyBy(event -> event.userId)
            .process(new EvolvedUserProcessor())
            .print();
        
        env.execute("Complete Schema Evolution Example");
    }
    
    /**
     * 演化后的用户处理器
     * 使用最新的状态结构版本3
     */
    public static class EvolvedUserProcessor extends KeyedProcessFunction<String, UserEvent, String> {
        private ValueState<UserProfileV3> profileState;
        
        @Override
        public void open(Configuration parameters) {
            ValueStateDescriptor<UserProfileV3> descriptor = new ValueStateDescriptor<>(
                "user-profile",
                TypeInformation.of(new TypeHint<UserProfileV3>() {})
            );
            
            profileState = getRuntimeContext().getState(descriptor);
        }
        
        @Override
        public void processElement(UserEvent event, Context ctx, Collector<String> out) throws Exception {
            UserProfileV3 profile = profileState.value();
            
            if (profile == null) {
                // 初始化用户配置文件
                profile = new UserProfileV3(
                    event.userId,
                    "guest",     // name
                    "unknown",   // email (版本2新增)
                    0,           // loginCount (版本3新增)
                    System.currentTimeMillis()  // createdAt (版本3新增)
                );
            }
            
            // 根据事件类型更新配置文件
            switch (event.eventType) {
                case "login":
                    profile.loginCount++;
                    break;
                case "updateProfile":
                    profile.name = "updated-user";
                    profile.email = "updated@example.com";
                    break;
            }
            
            profileState.update(profile);
            out.collect("User " + event.userId + " profile: " + profile);
        }
    }
    
    /**
     * 用户事件
     */
    public static class UserEvent {
        public String userId;
        public String eventType;
        public long timestamp;
        
        public UserEvent() {}
        
        public UserEvent(String userId, String eventType, long timestamp) {
            this.userId = userId;
            this.eventType = eventType;
            this.timestamp = timestamp;
        }
    }
    
    /**
     * 版本1:基础用户信息
     */
    public static class UserProfileV1 {
        public String userId;
        public String name;
        
        public UserProfileV1() {}
        
        public UserProfileV1(String userId, String name) {
            this.userId = userId;
            this.name = name;
        }
    }
    
    /**
     * 版本2:添加邮箱字段
     */
    public static class UserProfileV2 extends UserProfileV1 {
        public String email;  // 新增字段
        
        public UserProfileV2() {}
        
        public UserProfileV2(String userId, String name, String email) {
            super(userId, name);
            this.email = email;
        }
    }
    
    /**
     * 版本3:添加登录计数和创建时间
     */
    public static class UserProfileV3 extends UserProfileV2 {
        public int loginCount;     // 新增字段
        public long createdAt;     // 新增字段
        
        public UserProfileV3() {}
        
        public UserProfileV3(String userId, String name, String email, int loginCount, long createdAt) {
            super(userId, name, email);
            this.loginCount = loginCount;
            this.createdAt = createdAt;
        }
        
        @Override
        public String toString() {
            return "UserProfileV3{userId='" + userId + "', name='" + name + "', email='" + email + 
                   "', loginCount=" + loginCount + ", createdAt=" + createdAt + "}";
        }
    }
}

6. 最佳实践建议

6.1 设计原则

  1. 默认值策略

    • 为新增字段提供合理的默认值
    • 考虑使用 Optional 或 null 来表示可选字段
  2. 字段命名

    • 使用清晰、一致的字段命名
    • 避免字段重命名,如需重命名应提供转换逻辑
  3. 类型兼容性

    • 避免不兼容的类型变更
    • 如需类型变更,提供明确的转换规则

6.2 实现建议

  1. 使用 POJO 类型

    • POJO 类型具有更好的 Schema 演化支持
    • 确保 POJO 类符合 Flink 的要求(公共字段、无参构造函数等)
  2. 明确 TypeInformation

    • 显式指定 TypeInformation 而不是依赖自动推断
    • 使用 TypeHint 或 Types 工具类
  3. 测试演化过程

    • 在开发环境中测试 Schema 演化
    • 验证从旧版本到新版本的恢复过程

6.3 注意事项

  1. 检查点兼容性

    • 确保演化后的状态与现有检查点兼容
    • 在生产环境中进行充分测试
  2. 性能影响

    • Schema 演化可能带来一定的性能开销
    • 监控演化过程中的性能表现

通过合理使用 State Schema Evolution,可以在不中断服务的情况下演进状态数据结构,确保流处理应用程序的长期可维护性和可扩展性。

相关推荐
盈创力和20077 小时前
以太网多参量传感器:超越温湿度的“智能嗅探”,守护每一方空气的安全
大数据·人工智能
wudl55667 小时前
Flink Keyed State 详解之七
大数据·flink
caiyueloveclamp8 小时前
便宜好用AIPPT推荐TOP8【2025最新】
大数据·人工智能·powerpoint·ai生成ppt·aippt·免费会员
想ai抽8 小时前
从文件结构、索引、数据更新、版本控制等全面对比Apache hudi和Apache paimon
大数据·架构·flink
葡萄城技术团队9 小时前
实战视角:为何专用小型语言模型(SLM)正成为企业 AI 选型新宠—与 LLM 的全面对比指南
大数据·人工智能·语言模型
GIS数据转换器9 小时前
科技赋能农业现代化的破局之道
大数据·科技·安全·机器学习·智慧城市·制造
PONY LEE9 小时前
Flink keyby使用随机数踩坑记
大数据·python·flink
帅帅梓9 小时前
ELK es+logstash
大数据·elk·elasticsearch
AI_Auto9 小时前
MES系列-制造流程数字化的实现
大数据·人工智能·自动化·制造·数字化