Flink + AI 编写业务逻辑
注意版本
flink是一个版本发布较快、特性变化幅度较大的实时计算框架,尤其是在状态和SQL两个模块上变动最为明显。因此,当使用GPT来进行flink相关的提问时,一定要格外注意所使用的flink版本。ChatGPT的两个常用模型(GPT3.5和GPT4)所了解的最新flink版本是1.14。另外,由于flink的特性变动较大,为了获得高质量的回答,最好在提问时明确指出所使用的版本号。
第一轮提示
提示
回答
- 改进的状态后端和存储:Flink社区可能会继续致力于改进状态后端和存储,提高其性能和可伸缩性。
- 更丰富的连接器支持:Flink 1.17可能会增加对更多流处理和批处理数据源和数据接收器的连接器,以便更好地与现有的数据生态系统集成。
- SQL和Table API的改进:社区可能会进一步改进Flink的SQL和Table API,提高查询性能,添加更多的内置函数和优化器规则。
- Python API的增强:Flink社区可能会继续努力提高Python API(PyFlink)的性能和功能,使其更易于使用和集成。
- 更强大的机器学习库:Flink ML可能会继续发展,提供更多的机器学习算法和工具,以便在Flink中进行大规模的机器学习任务。
- 改进的部署选项:Flink 1.17可能会进一步简化部署过程,为Flink在Kubernetes、YARN和Mesos等集群环境中的部署提供更好的支持。
- 更高级的故障恢复和可观察性:Flink 1.17可能会继续提高系统的故障恢复能力,提供更好的监控和诊断工具,帮助开发人员更好地管理和维护其流处理应用程序。
请注意,这些特性仅仅是根据Flink过去的发展趋势和社区讨论进行的推测。要了解确切的特性和更新,请密切关注Apache Flink官方网站和发布公告。
反馈
可以看出,ChatGPT在处理实时信息类任务方面的效果不佳。
场景一 模拟数据
第一轮提示
提示
在企业开发环境中,有时数据仅存储在内部网络中,因此编写需求只能在公司电脑上完成,开发和测试也可能会遇到不小的问题。在这种情况下,可以选择编写一个SourceFunction来模拟数据,只需确保数据格式正确即可。这样,就可以在自己的电脑上编写后续的计算逻辑。现在可以使用GPT快速创建一个SourceFunction来生成模拟数据。
下面是一个可供参考的数据结构{
id: int,
list: [{
code: int,
value: double
},
...
],
ts: long
}
list里是设备4个温度传感器,code为{0,1,2,3}
回答
根据您的需求,这里是一个使用Java编写的Flink SourceFunction,它会生成符合您所描述的数据格式的数据:
首先,定义一个SensorData类用于存储数据:
import java.util.List;
public class SensorData {
public int id;
public List<Sensor> list;
public long ts;
public SensorData(int id, List<Sensor> list, long ts) {
this.id = id;
this.list = list;
this.ts = ts;
}
public static class Sensor {
public int code;
public double value;
public Sensor(int code, double value) {
this.code = code;
this.value = value;
}
}
}
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import java.util.ArrayList;
import java.util.Random;
public class SensorDataSourceFunction implements SourceFunction<SensorData> {
private volatile boolean running = true;
private final int numDevices = 10;
private final int numSensors = 4;
private final Random random = new Random();
@Override
public void run(SourceContext<SensorData> ctx) throws Exception {
while (running) {
for (int deviceId = 0; deviceId < numDevices; deviceId++) {
ArrayList<SensorData.Sensor> sensorList = new ArrayList<>();
for (int sensorCode = 0; sensorCode < numSensors; sensorCode++) {
double temperature = 30 + random.nextDouble() * 30;
sensorList.add(new SensorData.Sensor(sensorCode, temperature));
}
long timestamp = System.currentTimeMillis();
SensorData data = new SensorData(deviceId, sensorList, timestamp);
ctx.collect(data);
Thread.sleep(1000);
}
}
}
@Override
public void cancel() {
running = false;
}
}
然后,可以在Flink程序中使用这个SensorDataSourceFunction:
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class SensorDataGenerator {
public static void main(String[] args) throws Exception {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<SensorData> sensorDataStream = env.addSource(new SensorDataSourceFunction());
// 在这里添加你的Flink算子,如 map、filter等
// 例如: sensorDataStream.map(...).filter(...).sink(...);
env.execute("Sensor Data Generator");
}
}
这个程序会创建一个SourceFunction,每秒生成10个设备的数据,每个设备包含4个温度传感器,温度值在30-60度之间,还包含一个时间戳。当然,您可以根据需要修改这个程序。
反馈
这个生成器已经非常完善了,现在我们只需要为Bean类添加一个可读性良好的toString方法,并在main方法中的sensorDataStream对象上添加一个print算子,就可以将模拟数据打印到控制台了。
现在,这个SourceFunction已经可以生成我们想要的数据,它会每秒生成一条数据,如果觉得慢的话,可以选择将Thread.sleep(1000)这行代码移到外面的for循环中,这样一秒可以为10个设备各生成1条数据。
现在,我们可以给数据生成器再加一个需求,要有5%的概率生成一些异常数据。
第二轮提示
提示
一旦有一个传感器的温度超过65度,那么就算是异常数据,现在我希望这个数据生成器会有5%的概率出现异常数据,该如何修改
回答
要在数据生成器中添加5%的概率生成异常数据,您可以修改SensorDataSourceFunction的run方法。在生成温度值之前,我们可以使用随机数来决定是否生成异常数据。这里是修改后的SensorDataSourceFunction:
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import java.util.ArrayList;
import java.util.Random;
public class SensorDataSourceFunction implements SourceFunction<SensorData> {
private volatile boolean running = true;
private final int numDevices = 10;
private final int numSensors = 4;
private final Random random = new Random();
@Override
public void run(SourceContext<SensorData> ctx) throws Exception {
while (running) {
for (int deviceId = 0; deviceId < numDevices; deviceId++) {
ArrayList<SensorData.Sensor> sensorList = new ArrayList<>();
for (int sensorCode = 0; sensorCode < numSensors; sensorCode++) {
double temperature;
// 5%的概率生成异常数据
if (random.nextDouble() < 0.05) {
temperature = 65 + random.nextDouble() * 5;
} else {
temperature = 30 + random.nextDouble() * 30;
}
sensorList.add(new SensorData.Sensor(sensorCode, temperature));
}
long timestamp = System.currentTimeMillis();
SensorData data = new SensorData(deviceId, sensorList, timestamp);
ctx.collect(data);
Thread.sleep(1000);
}
}
}
@Override
public void cancel() {
running = false;
}
}
现在,每个传感器的温度值有5%的概率超过65度,生成异常数据。其他部分代码保持不变。
反馈
场景二 编写异常检测逻辑
现在,我们可以在之前SourceFunction的基础上,对流中的数据做一些异常检查。一旦发生异常,就将异常数据用post请求发送到http://hadoop102:8888/接口。
第一轮提示
提示
我想在之前SourceFunction的基础上,对流中的数据做一些处理1.在温度传感器的json对象中增加加一个“state”字段
2.对每个传感器的温度进行检查,如果温度在0-60之间,state就取值为“normal”
3.如果温度超过65,state取值就为“alert”
4.一旦有一个传感器的state为alert,就将这条数据用post请求发送到http://hadoop102:8888/接口。
答
为了实现这个功能,您需要对SensorData类进行一些修改,以便包含state字段。然后,您可以使用DataStream.map方法来为每个传感器添加state字段,接着使用DataStream.filter方法来过滤出state为alert的数据,最后使用DataStream.addSink方法将这些数据发送到指定的HTTP接口。
首先,修改SensorData类:
import java.util.List;
public class SensorData {
public int id;
public List<Sensor> list;
public long ts;
public SensorData(int id, List<Sensor> list, long ts) {
this.id = id;
this.list = list;
this.ts = ts;
}
public static class Sensor {
public int code;
public double value;
public String state;
public Sensor(int code, double value, String state) {
this.code = code;
this.value = value;
this.state = state;
}
}
}
接下来,实现一个MapFunction以及一个SinkFunction:
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.sink.SinkFunction;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import com.fasterxml.jackson.databind.ObjectMapper;
public class SensorDataProcessor {
public static class AddStateMapFunction extends RichMapFunction<SensorData, SensorData> {
@Override
public SensorData map(SensorData data) {
for (SensorData.Sensor sensor : data.list) {
if (sensor.value >= 0 && sensor.value <= 60) {
sensor.state = "normal";
} else if (sensor.value > 65) {
sensor.state = "alert";
}
}
return data;
}
}
public static class AlertSinkFunction extends RichSinkFunction<SensorData> {
private transient ObjectMapper objectMapper;
@Override
public void open(Configuration parameters) throws Exception {
objectMapper = new ObjectMapper();
}
@Override
public void invoke(SensorData data, Context context) throws Exception {
if (data.list.stream().anyMatch(sensor -> "alert".equals(sensor.state))) {
String json = objectMapper.writeValueAsString(data);
sendPostRequest("http://hadoop102:8888/", json);
}
}
private void sendPostRequest(String targetURL, String jsonData) throws Exception {
URL url = new URL(targetURL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Content-Length", Integer.toString(jsonData.getBytes().length));
connection.setDoOutput(true);
try (OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), StandardCharsets.UTF_8)) {
writer.write(jsonData);
writer.flush();
}
int responseCode = connection.getResponseCode();
connection.disconnect();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new RuntimeException("Failed to send POST request. Response code: " + responseCode);
}
}
}
}
最后,在Flink程序中使用这些方法:
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class
反馈
可以看到,逻辑是对的,但是最后一部分没写完,我们继续让它往下写。
第二轮提示
继续
回答
SensorDataGenerator {
public static void main(String[] args) throws Exception {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<SensorData> sensorDataStream = env.addSource(new SensorDataSourceFunction());
// 添加状态
DataStream<SensorData> sensorDataWithStateStream = sensorDataStream.map(new SensorDataProcessor.AddStateMapFunction());
// 过滤出state为"alert"的数据
DataStream<SensorData> alertDataStream = sensorDataWithStateStream.filter(data ->
data.list.stream().anyMatch(sensor -> "alert".equals(sensor.state))
);
// 将state为"alert"的数据发送到HTTP接口
alertDataStream.addSink(new SensorDataProcessor.AlertSinkFunction());
env.execute("Sensor Data Generator");
}
}
注意:在实际生产环境中,为了提高性能,您可能需要使用`AsyncFunction`来异步发送HTTP请求。这里的示例使用同步请求,仅用于演示目的。
反馈
首先注意,这里GPT将Sensor类里的构造器从2参数变为了3参数。这里为了让SourceFunction正常工作,我们应该保留2参数的构造器。
另外,在GPT生成的SensorDataProcessor类中,引入了一个com.fasterxml.jackson.databind.objectMapper依赖。使用jackson的话调这个库是没问题的。不过flink已经用遮罩(shaded)的方式将jackson打包进来了,因此没必要在此添加新依赖,于是我们将com.fastxml.jackson.databind.ObejctMapper删掉。
最后,我们在虚拟机hadoop102上运行nc -l 8888命令监听8888端口,看刚才编写的数据能不能把异常数据发送过来。
场景三 Flink如何写Hive(失败)
第一轮提示
提示
我想用flink将数据写入hive中的一张表,有哪些方案
回答
Apache Flink 是一个流处理框架,可用于实时处理大量数据。要将 Flink 处理后的数据写入 Hive 中的表,可以使用以下几种方案:
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
import org.apache.flink.table.catalog.hive.HiveCatalog
val env = StreamExecutionEnvironment.getExecutionEnvironment
val tableEnv = StreamTableEnvironment.create(env)
val hiveCatalog = new HiveCatalog("myHive", "default", "/path/to/hive-site.xml")
tableEnv.registerCatalog("myHive", hiveCatalog)
tableEnv.useCatalog("myHive")
val result = tableEnv.executeSql(
"""
|INSERT INTO myHiveTable
|SELECT ...
|FROM sourceTable
""".stripMargin)
import org.apache.flink.core.fs.Path
import org.apache.flink.streaming.api.functions.sink.filesystem.StreamingFileSink
import org.apache.flink.streaming.api.functions.sink.filesystem.rollingpolicies.DefaultRollingPolicy
val sink = StreamingFileSink
.forBulkFormat(new Path("hdfs:///path/to/hive/external/table"), ParquetAvroWriters.forReflectRecord(classOf[MyRecord]))
.withRollingPolicy(
DefaultRollingPolicy.builder()
.withRolloverInterval(TimeUnit.MINUTES.toMillis(15))
.withInactivityInterval(TimeUnit.MINUTES.toMillis(5))
.withMaxPartSize(1024 * 1024 * 1024)
.build())
.build()
dataStream.addSink(sink)
如果以上方案无法满足需求,可以自定义 Flink Sink,通过 Hive API 或 JDBC 将数据批量写入 Hive。
class MyHiveSink extends RichSinkFunction[MyRecord] {
private var hiveConnection: Connection = _
override def open(parameters: Configuration): Unit = {
// 初始化 Hive 连接
}
override def invoke(value: MyRecord, context: SinkFunction.Context[_]): Unit = {
// 插入数据到 Hive
}
override def close(): Unit = {
// 关闭 Hive 连接
}
}
val myHiveSink = new MyHiveSink()
dataStream.addSink(myHiveSink)
反馈
首先,ChatGPT为我们列出了三个方案,这三个方案都是可行的。只不过第二个方案可能会出现问题,如果您直接将数据写入HDFS,则可能导致Hive无法及时更新元数据。这可能会导致在查询数据时出现找不到的情况。接下来,我们可以向ChatGPT确认是否会出现这种情况,以及是否有更好的解决方法。
第二轮提示
提示
如果我用StreamingFileSink的方式将数据直接写成hdfs上的文件,那hive的元数据是不是没有更新,这样的话我怎么样才能及时让hive知道有新的数据了呢
回答
当使用 StreamingFileSink 将数据写入 HDFS 时,的确 Hive 的元数据不会自动更新。为了让 Hive 及时知道有新的数据,可以采取以下方法:
将 Hive 表分区,将每个分区与一个子目录关联。当 Flink 将新数据写入新分区时,可以通过分区操作将新数据添加到 Hive 表中。例如,假设表的分区字段是 dt,当 Flink 将数据写入 HDFS 的 /path/to/hive/external/table/dt=2023-04-26 目录时,可以执行以下 Hive 分区操作:
这样,Hive 就会知道新分区的存在,并且可以查询新分区中的数据。你可以将这个操作放在一个定时任务中,以便自动执行。
无论采用哪种方法,都需要确保 Hive 元数据的及时更新,以便在新数据写入后能够正确查询到新数据。
反馈
现在,假设我们确定想使用flink sql的方式将数据写入hive,那么现在,就可以让ChatGPT来为我们编写Demo代码了。需要注意,之前GPT也为我们编写了一套Demo,但是我们当时没有指明编程语言,所以GPT自动选了Scala,所以这一次我们要把语言约束指明。
第三轮提示
提示
请用java帮我写一个使用flink sql将数据写到hive的demo。
回答
以下是使用 Java 编写的 Flink SQL 将数据写入 Hive 的示例。在此示例中,我们创建一个自定义的 SourceFunction 来生成模拟数据。该数据包含两个字段:id(Long 类型)和 name(String 类型)。
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.catalog.hive.HiveCatalog;
public class FlinkHiveDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// Register Hive catalog
HiveCatalog hiveCatalog = new HiveCatalog("myHive", "default", "/path/to/hive-site.xml");
tableEnv.registerCatalog("myHive", hiveCatalog);
tableEnv.useCatalog("myHive");
// Define a custom SourceFunction to generate data
DataStream<SampleRecord> dataStream = env.addSource(new SampleSourceFunction());
// Map the generated data to a Row
DataStream<org.apache.flink.types.Row> rowDataStream = dataStream.map(
(MapFunction<SampleRecord, org.apache.flink.types.Row>) record -> {
org.apache.flink.types.Row row = new org.apache.flink.types.Row(2);
row.setField(0, record.getId());
row.setField(1, record.getName());
return row;
}).returns(Types.ROW(Types.LONG, Types.STRING));
// Register the DataStream as a table
tableEnv.createTemporaryView("sourceTable", rowDataStream, "id, name");
// Execute the SQL query to insert the data into the Hive table
String sql = "INSERT INTO myHiveTable SELECT id, name FROM sourceTable";
tableEnv.executeSql(sql);
}
// Define a custom SourceFunction to generate sample data
public static class SampleSourceFunction implements SourceFunction<SampleRecord> {
private volatile boolean isRunning = true;
@Override
public void run(SourceContext<SampleRecord> ctx) {
long id = 0;
while (isRunning) {
ctx.collect(new SampleRecord(id, "Name_" + id));
id++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void cancel() {
isRunning = false;
}
}
// Define a POJO class for the sample data
public static class SampleRecord {
private long id;
private String name;
public SampleRecord(long id, String name) {
this.id = id;
this.name = name;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
这个示例程序首先创建一个 Flink 流处理环境和一个 Flink 表环境。然后,它注册了一个 Hive Catalog,并将其设置为当前使用的 Catalog。接下来,它定义了一个自定义的 SourceFunction 来
反馈
第四轮提示
提示
回答
生成模拟数据,并将生成的 DataStream 转换为 Row 类型的 DataStream。接着,我们将这个 DataStream 注册为一个临时表 "sourceTable"。
最后,我们执行一个 Flink SQL 语句,将 "sourceTable" 中的数据插入到 Hive 表 "myHiveTable" 中。请注意,你需要根据实际情况替换 /path/to/hive-site.xml 和 Hive 表名称 "myHiveTable"。
这个示例程序将会持续生成数据并插入到 Hive 表中。你可以通过修改 SampleSourceFunction 中的逻辑来更改数据生成的方式。
为了运行这个示例程序,你需要在项目中添加 Flink 和 Hive 相关的依赖。以下是 Maven 依赖的例子:
<dependencies>
<!-- Flink dependencies -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java_2.12</artifactId>
<version>1.14.0</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-api-java-bridge_2.12</artifactId>
<version>1.14.0</version>
</dependency>
<!-- Flink Connector for Hive -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-hive_2.12</artifactId>
<version>1.14.0</version>
</dependency>
<!-- Hive dependencies -->
<dependency>
<groupId>org.apache.hive</groupId>
<artifactId>hive-exec</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.hive</groupId>
<artifactId>hive-metastore</artifactId>
<version>3.1.2</version>
</dependency>
</dependencies>
请根据你使用的 Flink 和 Hive 版本进行相应的调整。运行该程序时,你应该能够看到数据被成功插入到 Hive 表中。
反馈
并不能,我后面尝试了多次提示,来回问答已达8次,GPT还是未能给出准确的代码。这里需要注意,flink写数据到hive其实是一个小众场景,网上能找到的有效文章也是个位数的。这个时候向GPT索要高质量答案就比较困困难。