《实时流计算系统设计与实现》-Part 1-笔记
实时流计算
挑战
大数据的5V:Volume(大量)、Velocity(快速)、Variety(多样)、Veracity(真实)和Value(价值)。
使用场景
在线系统监控
移动数据和物联网
金融风控
实时推荐
实时流数据的特点
实时性、随机性、无序性和无限性(与批数据最大的区别)。
批处理是在固定数据集上进行不同的查询,流处理是在无限数据集上进行固定的查询。
系统架构
数据传输,选择消息中间件,考虑因素:
- 吞吐量:消息中间件每秒能够处理的消息数。消息中间件自身的吞吐量决定了实时流计算系统吞吐量的上限;
- 延迟:消息从发送端到消费端所消耗的时间。如同吞吐量一样,消息中间件自身的延迟决定了实时流计算系统延迟的下限。选择消息中间件时,需确定消息中间件本身延迟对业务没有明显限制;
- 高可用:消息中间件的一个或多个节点发生故障时,仍然能够持续提供正常服务。高可用消息中间件必须支持在转移故障并恢复服务后,客户端能自动重新连接并使用服务。千万不能让客户端进入僵死状态,否则即便消息中间件依然在提供服务,而上层的业务服务已然停止;
- 持久化:消息中间件中的消息写入日志或文件,在重启后消息不丢失。大部分业务场景下,支持持久化是一个可靠线上系统的必要条件。数据持久化从高可用角度看,还需要提供支持数据多副本存储功能。当一部分副本数据所在节点出现故障,或这部分副本数据本身被破坏时,可以通过剩余部分的副本数据恢复出来;
- 水平扩展:消息中间件的处理能力能够通过增加节点来提升。当业务量逐渐增加时,原先的消息中间件处理能力逐渐跟不上,这时需要增加新节点以提升消息中间件的处理能力。Kafka可以通过增加Kafka节点和topic分区数的方式水平扩展处理能力。
数据处理,核心,目标可分为4类:
- 数据转化:包括数据抽取、清洗、转换和加载,如常见的流式函数filter和map,分别用于完成数据抽取和转化的操作;
- 指标统计:在流数据上统计各种指标,如计数、求和、均值、标准差、极值、聚合、关联、直方图等;
- 模式匹配:在流数据上寻找预先设定的事件序列模式,我们常说的CEP(复杂事件处理)就属于模式匹配;
- 模型学习和预测:数据挖掘和机器学习在流数据上的扩展应用,基于流的模型学习算法可以实时动态地训练或更新模型参数,进而根据模型做出预测,更加准确地描述数据背后当时正在发生的事情。
通常使用DAG(Directed Acyclic Graph,有向无环图)来描述流计算过程。常见的开源流处理框架有Storm、Spark、Flink、Samza和Akka Streams等。
数据存储
数据展示,We.UI
数据采集
设计接口
风险总体上可分成两类:
- 贷款对象信用风险:关注的是贷款对象自身的信用状况、还款意愿和还款能力。信用风险评估常用的分析因素有四要素认证和征信报告,使用的风控模型主要是可解释性强的逻辑回归评分卡;
- 贷款对象欺诈风险:关注的是贷款对象是否在骗贷。分析因素:网络(如IP是否集中),用户属性(如年龄和职业),用户行为(如是否在某个时间段集中贷款),社会信用(如社保缴纳情况),第三方征信(如芝麻信用得分),还有各种渠道而来的黑名单。使用的模型:决策树、聚类分析等。
互联网金融风控的一般流程:
解读:用户在客户端发出注册、贷款申请等事件时,客户端将用户属性、行为、生物识别、终端设备信息、网络状况、TCP/IP协议栈等信息发送到数据采集服务器;服务器收到数据后,进行字段提取和转化,发送给特征提取模块;特征提取模块按照预先设定的特征清单进行特征提取,然后以提取出来的特征清单作为模型或规则系统的输入;最终依据模型或规则系统的评估结果做出决策。
Spring Boot
置空。
BIO与NIO
在请求连接数比较小、请求处理逻辑比较简单、工作线程请求处理时延很短的场景下,使用BIO连接器是很合适的。
BIO连接器的本质缺陷是接收器和工作线程执行步调耦合太紧。
当前大多数操作系统在处理上万个甚至只需几千个线程时,性能就会明显下降。
Tomcat的NIO连接器还引入选择器(包含在轮询器中)来更加精细地管理连接套接字,选择器只有在连接套接字中的数据处于可读(Read Ready)状态时,才将其交由工作线程来处理,避免工作线程等待连接套接字准备数据的过程。
NIO连接器的两点改进:
- 接收器和工作线程隔离,彼此不影响,更充分利用资源;
- 队列缓存待处理的连接套接字,NIO连接器能保持的并发连接数不再受限于工作线程数,而只受限于系统设置上限(由LimitLatch指定)。
NIO和异步
任务类型
大致可分为:
- CPU密集型任务:CPU受限型任务,处理过程中主要依靠CPU运算来完成的任务;
- I/O密集型任务:在处理过程中有很多I/O操作的任务,其执行速度会受限于I/O的吞吐能力;
- I/O和CPU都密集型任务
纤程
fiber,也叫协程(coroutine),一种用户态线程,其调度逻辑在用户态实现,从而避免过多地进出内核态进行进程调度和上下文切换
Actor
NIO配合异步编程
Netty
Netty工作原理如下图
解读:Netty用reactor线程监听ServerSocketChannel,简称SSC,每个SSC对应一个实际的端口。当reactor线程监听的SSC监测到连接请求事件(OP_ACCEPT)时,就为接收到的连接套接字建立一个SocketChannel,并将该SocketChannel委托给工作线程池中的某个工作线程做后续处理。之后,当工作线程监测到SocketChannel上有数据可读(OP_READ)时,就调用相关的回调句柄(handler)对数据进行读取和处理,并返回最终的处理结果。通常将reactor线程池称为BossGroup,而将工作线程池称为WorkGroup。
@AllArgsConstructor
private static class RefController {
private final ChannelHandlerContext ctx;
private final HttpRequest req;
public void retain() {
ReferenceCountUtil.retain(ctx);
ReferenceCountUtil.retain(req);
}
public void release() {
ReferenceCountUtil.release(req);
ReferenceCountUtil.release(ctx);
}
}
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest req) throws Exception {
final RefController refController = new RefController(ctx, req);
refController.retain();
CompletableFuture
.supplyAsync(() -> this.decode(ctx, req), this.decoderExecutor)
.thenApplyAsync(e -> this.doExtractCleanTransform(ctx, req, e), this.ectExecutor)
.thenApplyAsync(e -> this.send(ctx, req, e), this.senderExecutor)
.thenAccept(v -> refController.release())
.exceptionally(e -> {
try {
logger.error("exception caught", e);
sendResponse(ctx, 500, RestHelper.genRespStr(500, "服务器内部错误"));
return null;
} finally {
refController.release();
}
});
}
实现单节点流计算应用
流计算中较重要的两个基本组件:
- 用于传递事件的队列
- 用于执行计算逻辑的线程
流计算是异步的系统,所以我们需要严格控制异步系统中各子系统执行步调不一致的问题。为此,我们反复强调了反向压力功能对流计算系统的重要性。只有在内部支持了反向压力功能的流计算应用,才能长期稳定、可靠地运行下去。相比“异步”而言,“流”这种计算模式更加自然地描述了真实世界中事情发生的过程,也更加符合我们在分析业务执行流程时的思维方式。所以,“流”降低了构建异步和高并发系统的难度。
自己动手写实时流计算框架
DAG有两种不同的表达含义:
- 如果不考虑并行度,那么每个节点表示的是计算步骤,每条边表示的是数据在计算步骤间的流动;
- 如果考虑并行度,那么每个节点表示的是计算单元,每条边表示的是数据在计算单元间的流动;
CompletableFuture原理
Future.get
是阻塞的。
Guava的SettableFuture/ListenableFuture、Netty的Future和ChannelFuture等,通过注册监听或回调的方式形成回调链,实现异步。
CompletableFuture提供的方法,略。
采用CompletableFuture实现
使用CompletableFuture的流计算实现的优点:
- 在构建DAG拓扑时,仅需选择合适的CompletableFuture方法。相比在前面我们自己实现的流计算框架中,构建DAG的过程就像是在逐点逐线地“龟速”画画,这种方法要简洁和方便得多;
- 在实现DAG节点时,仅需将相关逻辑实现为回调函数。而我们自己实现的流计算框架在实现回调时,或多或少还需要处理框架内部的逻辑,对回调实现者并不直观;
- 能够静态或动态地控制DAG节点并发度和资源使用量,要实现这点只需要设置相应的执行器即可,参数选择也更加灵活;
- 更方便地实现流的优雅关闭(graceful shutdown)。
死锁
性能调优
优化机制
DAG描述流计算应用中的各个执行步骤,以及数据的流动方向。
在实现反向压力的流计算系统中,整个流计算应用的TPS会受限于DAG中最慢的那个节点。
优化手段:改进算法、增加资源分配、减少线程竞争。
优化工具
包括:监控工具和压测工具。
监控工具
Metrics是一个用于测量Java程序的运行状况的工具库,提供Gauge、Counter、Meter、Histogram和Timer五种测量手段。
可用top、dstat、tcpdump等工具来即时查看系统CPU、内存、磁盘和网络的使用状态。
Zabbix:构建系统运行状态的时序图。
JConsole:GUI工具,运行时占用资源较少。
JVisualVM:GUI工具,可安装插件;抽样器和线程状态可视化展示。
压测工具
Apache Bench、Apache JMeter、LoadRunner等
线程状态
JVM线程的状态:
- 新建(new):当通过
new Thread()
创建一个新的线程对象时,线程就处于新建状态,线程还没开始运行; - 运行(runnable):线程正在被JVM执行,但它也可能在等待操作系统的某些资源,如CPU;
- 阻塞(blocked):线程因为等待监视器锁而阻塞,获取监视器锁是为了进入同步块或在调用wait方法后重新进入同步块;
- 等待(waiting):线程在调用Object.wait、Thread.join或LockSupport.park方法后,进入此状态。处于等待状态的线程在等待另一个线程执行特定的动作;
- 限时等待(timed waiting):线程在调用
Thread.sleep
、Object.wait(timeout)
、Thread.join(timeout)
、LockSupport.parkNanos
或LockSupport.parkUntil
方法后进入此状态。处于限时等待状态的线程在等待另外一个线程执行特定的动作,但是带有超期时间; - 终止(terminated):线程完成执行后的状态;
JVisualVM线程状态分成运行、休眠、等待、驻留和监视5种状态:
- 运行:对应运行状态;
- 休眠:对应限时等待状态,通过
Thread.sleep(timeout)
进入此状态; - 等待:对应等待状态和限时等待状态,通过
Object.wait()
或Object.wait(timeout)
进入此状态; - 驻留:对应等待状态和限时等待状态,通过
LockSupport.park()
或LockSupport.parkNanos(timeout)
、LockSupport.parkUntil(timeout)
进入此状态; - 监视:对应阻塞状态,在等待进入synchronized代码块时进入此状态;
Linux的线程本质上也是进程,是一种轻量级进程,Linux内核以进程为调度单位,故线程的状态和进程的状态一样:
Linux进程的状态:
- TASK_RUNNING(R):CPU正在执行的进程,或CPU可以执行但尚未调度执行的进程。如果细分,则前者是正在运行状态,后者是可运行状态;
- TASK_INTERRUPTIBLE(S):进程因为等待某些事件的发生而处于可中断的睡眠状态。所谓可中断,是指进程在收到信号(软中断)时,会被提前临时唤醒去执行信号处理逻辑。在完成信号处理后,进程继续进入睡眠状态。只有等到它真正关心的事件发生(另外的进程通过
wake_up
函数触发)时,进程才会被真正唤醒,变成TASK_RUNNING状态; - TASK_UNINTERRUPTIBLE(D):此进程状态类似于TASK_INTERRUPTIBLE,但是它不会处理信号。进程即使在睡眠期间收到信号,也不会醒来。只有等到它真正关心的事件发生(另外的进程通过
wake_up
函数触发)时,进程才会醒来,变成TASK_RUNNING状态。TASK_UNINTERRUPTIBLE和TASK_INTERRUPTIBLE的功能本质上是相同的,只是为了不同场景使用的灵活性而提供的两种不同睡眠策略; - TASK_STOPPED(T):当接收到SIGSTOP或SIGTSTP等信号时,进程就会在处理这些信号后进入TASK_STOPPED状态。处于TASK_STOPPED状态的进程没有运行,并且不会被调度运行。当接收到SIGCONT信号时,进程就会在信号处理完成后,重新变为TASK_RUNNING状态,也就是恢复运行。TASK_STOPPED状态比较常用的一个场景是通过Ctrl-Z暂停进程,然后通过
bg
命令让进程在后台继续运行; - TASK_TRACED(T):TASK_TRACED状态与TASK_STOPPED状态类似,只用于调试的场景。当进程正在被其他进程调试追踪时,进程就进入这种状态;
- EXIT_ZOMBIE(Z):进程已终止,但是还没有被其父进程回收进程信息,进程处于僵尸状态;
- EXIT_DEAD(X):进程在经过僵尸状态后,被父进程调用
wait/waitpid
回收掉进程信息,就处于EXIT_DEAD状态,至此进程彻底结束;
两者的关系
JVM线程与操作系统线程一一对应,其调度需借助于操作系统的任务调度器完成。JVM线程在触发I/O操作时,JVM自身并不知道这个线程在操作系统层面执行的具体细节,它只知道这个线程正在被执行。在操作系统层面,内核发现这个线程进行I/O相关调用。I/O操作会触发磁盘或网络等外设的数据传输行为,这个过程需要时间,内核会把这个线程先调度出去,让出CPU来执行其他任务,等到数据传输完成时,再继续调度该线程执行。在线程等待数据传输完成期间,该线程通常处于TASK_UNINTERRUPTIBLE状态。
JVM线程的I/O操作越密集,对应操作系统线程处于TASK_UNINTERRUPTIBLE状态的时间就越多。某个JVM线程长时间处于运行状态,并不代表它一直在被CPU执行,还有可能处于I/O状态。可借助于top、dstat和zabbix等工具来分析JVM实例(进程)处于用户态和内核态的时间占比、磁盘和网络I/O的吞吐量等信息。
如果线程处于等待、限时等待或阻塞状态,则说明程序可能存在以下问题:
- 工作量不饱和,如从输入队列拉取消息过慢,当然也可能是输入本身很少,但是在性能测试和优化时应该让系统处于压力饱和状态;
- 内耗严重,如锁使用不合理、synchronized保护范围过大,导致竞态时间过长、并发性能低下;
- 资源分配不足,如分配给某个队列的消费者线程过少,导致队列的生产者长时间处于等待状态;
- 处理能力不足,如某个队列的消费者处理过慢,导致队列的生产者长时间处于等待状态;
优化方向
三个方向:
- 资源:主要包括CPU、内存、磁盘I/O和网络I/O4个方面;
- 算法:算法优化是一个与特定计算任务强相关的事情。如,采用Hyperloglog近似算法来改进关联图谱中一度关联度的计算;
- 并发与竞态:
支持高并发意味着线程一定安全,但线程安全并不一定是支持高并发的。
数据处理
在计算什么
计算问题(任务)分类:
- 流数据操作
- 单点特征计算
- 时间维度聚合特征计算
- 关联图谱特征计算
- 事件序列分析
- 模型学习和预测
流数据操作
过滤:Filter
映射:Map
展开映射:Flatmap
聚合:Reduce
关联:Join
分组:Key By,Group By
遍历:Foreach
时间维度聚合特征计算
在每个时间窗口内,给变量的每一种可能的取值分配一个用于保存记录数的寄存器。当数据到达时,根据变量的取值及其所在的窗口,选中对应的记录数寄存器,将该记录数寄存器的值加一。当窗口结束时,每个记录数寄存器的取值就是该时间窗口内变量在某个分组下的计数值。
聚合计算 | 寄存器1 | 寄存器2 | 寄存器3 |
---|---|---|---|
计数(count) | 记录数 | 无 | 无 |
求和(sum) | 总和 | 无 | 无 |
均值(avg) | 总和 | 记录数 | 无 |
方差(variance) | 总和 | 平方和 | 记录数 |
最小(min) | 最小值 | 无 | 无 |
最大(max) | 最大值 | 无 | 无 |
采用寄存器的方案极大地减少内存的使用量,也降低计算的复杂度。仅适合于进行聚合分析的变量具有一个较低的势时。
需要将这些寄存器状态保存到外部存储器中,如Redis、Ignite或本地磁盘;还需要为这些状态设置过期时间(TTL),将过期的状态清理掉,为新的状态腾出空间,避免占据空间的无限增长。
势(cardinality)是集合论中用来描述个集合所含元素数量的概念。如集合 S = A , B , C S={A,B,C} S=A,B,C有3个元素,其势=3。集合包含的元素数量越多,其势越大。
关联图谱特征计算
HyperLogLog算法提供3个命令:
- PFADD:用于将元素添加到HyperLogLog寄存器;
- PFCOUNT:用于返回添加到HyperLogLog寄存器中不同元素的个数(根据HyperLogLog算法计算出来的估计值);
- PFMERGE:用于合并多个HyperLogLog寄存器。
Lambda架构的核心思想是对于计算量过大或者计算过于复杂的问题,将其分为离线计算部分和实时计算部分,其中离线计算是在主数据集上的全量计算,而实时计算则是对增量数据的计算。当这两者各自计算出结果后,再将结果合并起来,从而得到最终的查询结果。通过这种离线计算和实时计算的方式,Lambda架构能够实时地在全量数据集上进行分析和查询。
事件序列分析
CEP通过分析事件流中事件之间的关系(如时间关系、空间关系、聚合关系、依赖关系等)产生一个具有更高层次含义的复合事件。
模型学习和预测
- 统计学习模型:以统计分析为基础,偏向于挖掘数据内部产生的机制,更加注重模型和数据的可解释性;
- 机器学习模型:以各种ML方法为基础,偏向于用历史数据来预测未来数据,更加注重模型的预测效果。
状态管理
流的状态
将流在执行过程中涉及的状态分为两类:
- 流数据状态:临时保存的部分流数据,处理完后会被清理;
- 流信息状态:感兴趣的,会被保存的,后续会被使用的,被不断地访问和更新的,分析所得的信息。
采用Redis实现
Key的设计。
采用Ignite实现
Apache Ignite是一个基于内存的数据网格解决方案,功能:
- 提供符合JCache标准的数据访问接口;
- 支持丰富的数据结构;
- 提供兼容ANSI-99标准的SQL查询功能;
- 分布式文件系统
- 机器学习。
表设计:
扩展为集群
开源流计算框架
除了下面四个框架,其他框架,如Akka Streaming支持丰富灵动的流计算编程API,可谓惊艳卓卓;而Apache Beam则是流计算模式的集大成者,大有准备一统流计算江湖的势头。
Storm
Storm系统架构
解读:
Storm集群由两种节点组成:
- Master:Master节点运行Nimbus进程,用于代码分发、任务分配和状态监控。
- Worker:节点运行Supervisor进程和Worker进程,其中Supervisor进程负责管理Worker进程的整个生命周期,而Worker进程创建Executor线程,用于执行具体任务(Task)。
在Nimbus和Supervisor之间,还需要通过Zookeeper来共享流计算作业状态,协调作业的调度和执行。
通过Topology、Tuple、Stream、Spout和Bolt等概念来描述一个流计算作业:
- Topology:描述流计算作业的DAG,它完整地描述了流计算应用的执行过程。当Topology部署在Storm集群上并开始运行后,除非明确停止,否则它会一直运行下去。这和MapReduce作业在完成后就退出的行为是不同的。Topology由Spout、Bolt和连接它们的Stream构成,其中Topology的节点对应着Spout或Bolt,而边则对应着Stream;
- Tuple:Storm中的消息,一个Tuple可以视为一条消息;
- Stream:这是Storm中的一个核心抽象概念,用于描述消息流。Stream由Tuple构成,一个Stream可以视为一组无边界的Tuple序列;
- Spout:用于表示消息流的输入源。Spout从外部数据源读取数据,然后将其发送到消息流中;
- Bolt:Storm进行消息处理的地方。Bolt负责消息的过滤、运算、聚类、关联、数据库访问等各种逻辑。开发者在Bolt中实现各种流处理逻辑。
消息传达可靠性保证
Storm提供不同级别的消息可靠性保证机制,包括尽力而为(best effort)、至少一次(at least once)和通过Trident实现的精确一次(exactly once)。
在Storm中,一条消息被完全处理,是指代表这条消息的元组及由这个元组生成的子元组、孙子元组、各代重孙元组都被成功处理。反之,只要这些元组中有任何一个元组在指定时间内处理失败,那就认为是处理失败的。要使用Storm的这种消息完全处理机制,需要在程序开发时,配合Storm系统做两件额外的事情:
- 当在处理元组过程中生成子元组时,需要通过ack告知Storm系统;
- 当完成对一个元组的处理时,也需要通过ack或fail告知Storm系统。
Streaming
Samza
系统架构
Samza的YARN客户端向YARN提交Samza作业,并从YARN集群中申请资源(主要是CPU和内存)用于执行Samza应用中的作业。Samza作业在运行时,表现为多个副本的任务。Samza任务正是流计算应用的处理逻辑所在,它们从Kafka中读取消息,然后进行处理,并最终将处理结果重新发回Kafka。
描述流的几个概念:
- 流:Samza处理的对象,由具有相同格式和业务含义的消息组成。每个流可以有任意多的消费者,从流中读取消息并不会删除这个消息。我们可以选择性地将消息与一个关键字关联,用于流的分区。Samza使用插件系统实现不同的流。例如,在Kafka中,流对应一个主题中的消息;在数据库中,流对应一个表的更新操作;在Hadoop中,流对应目录下文件的追写换行操作。在本节后面的讨论中,我们主要基于Kafka流对Samza进行讨论;
- 作业:代表一段对输入流进行转化并将结果写入输出流的程序。考虑到运行时的并行和水平扩展问题,Samza又对流和作业进行了切分,将流切分为一个或多个分区,并相应地将作业切分为一个或多个任务;
- 分区:Samza的流和分区很明显继承自Kafka的概念。当然Sazma也对这两个概念进行抽象和泛化。Samza的流被切分为一个或多个分区,每个分区都是一个有序的消息序列;
- 任务:Samza作业又被切分为一个或多个任务。任务是作业并行化执行的单元,就像分区是流的并行化单元一样。每个任务负责处理流的一个分区。因此,任务的数量和分区的数量是完全相同的。通过YARN等资源调度器,任务被分布到YARN集群的多个节点上运行,并且所有的任务彼此之间都是完全独立运行的。如果某个任务在运行时发生故障退出了,则它会被YARN在其他地方重启,并继续处理与之前相同的那个分区;
- 数据流图:将多个作业组合起来可以创建一个数据流图。Samza流计算应用构成的整个系统的拓扑结构,边代表数据流向,节点代表执行流转化操作的作业。与Storm中Topology不同的是,数据流图包含的各个作业并不要求一定在同一个Samza应用程序中,数据流图可由多个不同的Samza应用程序共同构成,不同的Samza应用程序不会相互影响;
- 容器:分区和任务都是逻辑上的并行单元,不是对计算资源的真实划分。容器是物理上的并行单元,每一个容器都代表着一定配额的计算资源。每个容器可以运行一个或多个任务。任务的数量由输入流的分区数确定,而容器的数量则可以由用户在运行时任意指定;
- 流应用程序:Samza上层API用于描述Samza流计算应用的概念。一个流应用程序对应着一个Samza应用程序,它相当于Storm中Topology的角色。如果我们将整个流计算系统各个子系统的实现都放在一个流应用程序中,那么这个流应用程序实际上就是数据流图的实现。如果我们将整个流计算系统各个子系统的实现放在多个流应用程序中,那么所有这些流应用程序共同构成完整的数据流图;
Flink
StreamExecutionEnvironment提供的输入方式主要包含4类:
- 文件:从文件中读入数据作为流数据源,如readTextFile和readFile等;
- 套结字:从TCP套接字中读入数据作为流数据源,如socketTextStream等;
- 集合:用集合作为流数据源,如fromCollection、fromElements、fromParallelCollection和generateSequence等;
- 自定义:
StreamExecutionEnvironment.addSource
是通用的流数据源生成方法,用户可以在其基础上开发自己的流数据源。flink-connector-kafka
中的FlinkKafkaConsumer就是针对Kafka开发的流数据源;
输出API主要包含4类:
- 文件系统:将流数据输出到文件系统,如writeAsText、writeAsCsv和write-UsingOutputFormat;
- 控制台:将数据流输出到控制台,如print和printToErr;
- 套接字:将数据流输出到TCP套接字,如writeToSocket;
- 自定义:
DataStream.addSink
是最通用的流数据输出方法,用户可在其基础上开发自己的流数据输出方法。flink-connector-kafka
中的FlinkKafkaProducer就是针对Kafka开发的流输出方法;