基于Flink的流式计算可视化开发实践之配置->任务生成->任务部署过程

1. 引言

在我们大数据平台(XSailboat)的DataStudio模块中实现了基于Hive的业务流程开发和基于Flink的实时计算管道开发。

DataStudio是用来进行数据开发的,属于开发环境,另外还有任务运维模块,负责离线分析任务和实时计算任务在生产环境的部署和运维。在开发环境开发好的业务流程和计算管道可以提交/发布到生产环境。
DataStudio
整个大数据平台的可视化开发其实都是一种配置驱动的思想。在界面上开发编辑的都是一种配置数据,在部署运行的时候,后台会有程序将其转为可执行程序或解释执行配置。

2. Flink计算任务的配置化实现

在我的另一篇文章《Flink的DAG可视化开发实践》中表述了我们对Flink任务的一种模式抽象思路。有了这种模式抽象之后,让Flink计算任务适合于可视化开发,即把一种完全自由的代码开发模式,转变成了一种“输入”、“输出”、“状态存储器”、“个性化配置”、“基础配置”、“前置处理”、“后置处理”等阶段性、部件型的可配置项。

Flink有很多的算子,在原来的算子特性的基础之上,套用模式,并且对它的功能进行一些个性化设定,就可以形成自己的界面开发用的算子及配置。

例如“SQL增量查询”,就是我们自定义实现的一种在支持JDBC和SQL的数据库上,对一个SQL查询,可以基于一个具有增长特性的字段(例如自增长的字段或者最近编辑时间,记录型数据的创建时间等)进行定时增量查询和读取的源节点。
它的个性化配置界面如下:
在这里插入图片描述
这个节点的配置面板中的各个配置项暂且不介绍。从界面中可以看出SQL增量查询的功能配置可以转化成一个多层次结构的Bean。这个Bean是“SQL增量查询”节点实例信息的一个组成部分。
如此,一个计算管道DAG图,就可以用“计算管道(图)–>算子节点–>节点配置”这样的多层数据结构,并映射到成多张关系数据库表,并在其中存储。
这样就实现了算子的配置化及配置存储。

3. 配置转成Flink计算任务

既然是配置驱动的程序,基本都是“配置+解释执行器”的构成方式。配置是多样的,而解释执行器就一种实现。

Flink的计算任务开发,都会开发至少一个带main函数的Java类,在部署运行的时候上传jar包并指定main函数所在Java类。

一般的Java程序,也会开发一个带main函数的类,在MANIFEST.MF文件中指定或在命令行里面指定,这样JVM就知道程序的入口。Flink的计算任务也类似,只是不是用来告诉JVM的,而是用来告诉JobManager的。JobManager从这里进去,执行里面的代码,构建出计算任务图(有多阶段的图,可以不用细究)。构建出图之后,再将其拆分交给一个或多个TaskManager执行。

所以我们的main函数里面的逻辑只是构建了一个计算任务以及每一个该怎么执行的执行流图。这和反应式编程很类似,先是构建计算管路,再塞入数据执行。这和我们一般的函数主动式调用有所不同,那是调用即执行。

既然Flink的Job的main函数里面是构建计算管路,那么我们按照计算管道的DAG图及其配置,生成计算管路即可。即一手解读配置,一手按配置构建算子,组成计算任务。

我们界面上定义的每一个节点,都有专门的构建器,将节点转成相应的算子。所以配置转计算任务的过程是:

1. 提交计算任务的时候,通过参数指定运行的是那个计算管道。因为在平台里面有很多工作空间,每个工作空间里面有许多计算管道。
2. Flink的JobManager运行“执行解释器”的jar,进去其main函数。
3. 在main函数中,获取相关入参,其中就有计算管道id,然后调用其它服务提供的通过id获取计算管道及其配置信息的接口,获取计算管道的详细信息。
4. 解析计算管道详细信息,构建计算任务,将计算管道中的每个算子配置信息转换成Flink的算子。

下面贴出上面例举的SQL增量查询节点的构建器,以便更好理解我们是怎么做的。

... 省略
public class SI_SQLIncQuery_Builder extends StreamSourceNodeBuilder
{
	
	@Override
	public CPipeNodeType getNodeType()
	{
		return CPipeNodeType.SI_SQLIncQuery ;
	}

	@Override
	public void buildStreamFlow(JSONObject aNodeJo, IStreamFlowBuilder aStreamFlowBuilder , WorkContext aCtx) throws Exception
	{
		String nodeId = aNodeJo.optString("id") ;
		String nodeName = aNodeJo.optString("name") ;
		StreamExecutionEnvironment env = aStreamFlowBuilder.getExecutionEnvironment() ;
		JSONObject execConfJo = aNodeJo.optJSONObject("execConf") ;
		JSONObject baseConfJo = aNodeJo.optJSONObject("baseConf") ;
		String dsId = execConfJo.optString("dataSourceId") ;
		JSONObject dsJo = aNodeJo.pathJSONObject("dataSources", dsId);
		ConnInfo connInfo = JacksonUtils.asBean(dsJo.toJSONString() , ConnInfo.class) ;
		DataSource ds = new DataSource() ;
		ds.setId(dsId) ;
		ds.setName(dsJo.optString("name")) ;
		ds.setType(dsJo.optEnum("dataSourceType" , DataSourceType.class)) ;
		WorkEnv workEnv = WorkEnv.valueOf(aCtx.getWorkEnv()) ;
		if(WorkEnv.dev == workEnv)
			ds.setDevConnInfo(connInfo) ;
		else
			ds.setProdConnInfo(connInfo) ;
		
		
		// 查询密码
		WorkContext ctx = aStreamFlowBuilder.getWorkContext();
		KeyPair keyPair = RSAKeyPairMaker.getDefault().newOne().getValue();
		HttpClient client = aCtx.getGatewayClient();
		String cipherText = client.askForString(Request	.GET()
														.path(IApis_Gateway.sGET_DataSourcePassword)
														.queryParam("env", ctx.getWorkEnv())
														.queryParam("id", dsId)
														.queryParam("publicKey", RSAUtils.toString(keyPair.getPublic()))
														.queryParam("usage", "TDengine类型的下沉节点[" + nodeName + "]"));
		
		String password = RSAUtils.decrypt(keyPair.getPrivate(), cipherText);
		((ConnInfo_Pswd)connInfo).setPassword(password);
		
		int periodMs = execConfJo.optInt("periodMs") ;
		List<String> storeStateFields = execConfJo.optJSONArray("storeStateFields").toCollection(CS.arrayList() , XClassUtil.sCSN_String) ;
		DatasetDescriptor dsDesc = JacksonUtils.asBean(execConfJo.optJSONObject("dataset").toJSONString() , DatasetDescriptor.class) ;
		Dataset dataset = new Dataset() ;
        dataset.setDatasetDescriptor(dsDesc) ;
        dataset.setName(nodeName) ;
        dataset.setDataSourceId(ds.getId()) ;
        dataset.setWorkEnv(workEnv) ;
        dataset.setDataSourceType(ds.getType()) ;
		
		JSONArray outRowFieldsJa = execConfJo.optJSONArray("outRowFields") ;
		Assert.notNull(outRowFieldsJa , "没有找到outRowFields!%s" , execConfJo);
		ERowTypeInfo rowTypeInfo = JSONKit.toRowTypeInfo(outRowFieldsJa) ;
		
		// 水位线设置待实现
		WatermarkStrategy<Row> watermarkStrategy = null ;
		WaterMarkGenMethod waterMarkGenMethod = execConfJo.optEnum("waterMarkGenMethod" , WaterMarkGenMethod.class) ;
		if(waterMarkGenMethod == null)
		{
			watermarkStrategy = WatermarkStrategy.noWatermarks() ;
			waterMarkGenMethod = WaterMarkGenMethod.NoWatermarks ;
		}
		else
		{
			switch(waterMarkGenMethod)
			{
			case NoWatermarks :	
			{
				watermarkStrategy = WatermarkStrategy.noWatermarks() ;
			}
				break ;
			case MonotonousTimestamps:
				watermarkStrategy = WatermarkStrategy.forMonotonousTimestamps() ;
				break ; 
			case BoundedOutOfOrderness:
				JSONObject waterMarkGenMethodConfJo = execConfJo.getJSONObject("waterMarkGenMethodConf") ;
				TimeUnit timeUnit = TimeUnit.valueOf(waterMarkGenMethodConfJo.optString("timeUnit")) ;
				int timeLen = waterMarkGenMethodConfJo.optInt("timeLen" , 0) ;
				Assert.isTrue(timeLen>0 , "时间长度必须大于0!") ;
				Duration duration = null ;
				switch(timeUnit)
				{
				case NANOSECONDS:
					duration = Duration.ofNanos(timeLen) ;
					break ;
				case MILLISECONDS:
					duration = Duration.ofMillis(timeLen) ;
					break ;
				case SECONDS:
					duration = Duration.ofSeconds(timeLen) ;
					break ;
				case MINUTES:
					duration = Duration.ofMinutes(timeLen) ;
					break ;
				case HOURS:
					duration = Duration.ofHours(timeLen) ;
					break ;
				case DAYS:
					duration = Duration.ofDays(timeLen) ;
					break ;
				case MICROSECONDS:
					duration = Duration.of(timeLen, ChronoUnit.MICROS) ;
					break ;
				}
				watermarkStrategy = WatermarkStrategy.forBoundedOutOfOrderness(duration) ;
				break ;
			case MaxWatermarks:
				watermarkStrategy = WatermarkStrategy.forGenerator(new MaxWaterMarkGenSupplier()) ;
				break ;
			default:
				throw new IllegalStateException("未支持的水位线生成方法:"+waterMarkGenMethod) ;
			}
		}

		String timestampExpr = execConfJo.optString("timestampExpr") ;
		if(XString.isNotEmpty(timestampExpr) && waterMarkGenMethod != WaterMarkGenMethod.NoWatermarks)
		{
			watermarkStrategy = watermarkStrategy.withTimestampAssigner(new ExprTimestampAssigner(aCtx.getPipeArgs() 
					, timestampExpr, rowTypeInfo)) ;
		}
		mLogger.info("水位线生成策略是:{} , 时间表达式是:{}" , waterMarkGenMethod , timestampExpr) ;
		SQLIncQuerySourceFunction sourceFunc = new SQLIncQuerySourceFunction(storeStateFields, periodMs
				, dataset
				, ds) ;
		SingleOutputStreamOperator<Row> dss = env.addSource(sourceFunc , nodeName , rowTypeInfo)
				.assignTimestampsAndWatermarks(watermarkStrategy)		// 2023-01-08 这一句是必需的,否则不会产生水位线
				.name(nodeName)
				.uid(nodeId)
				;
		
		int parallelism = baseConfJo.optInt("parallelism", 1) ;
		if(parallelism <=0 )
			mLogger.info("指定的并发度为 {} , 小于1,将不设置,采用缺省并发度。" , parallelism) ;
		else
		{
			dss.setParallelism(parallelism) ;
		}
		
		aStreamFlowBuilder.putFlowPoint(nodeId , dss);
	}
}

3. 计算任务的部署

我们要构建的是一套可视化开发、部署平台,在我们的界面上就能完成开发、调试、部署的过程。我们的大数据平台底层基础设施有Hadoop,所以我们考虑使用Hadoop Yarn的容器部署Flink集群。要使用Yarn容器部署Flink计算任务,首先需要将程序包上传到Hadoop FS中。
在这里插入图片描述
我们这里把我们自己开发的扩展部分(ext_jars,解释执行器及其相关jar)和Flink的原生程序包(app,扩展了一些数据库驱动)分成两部分,在我们进行容器化部署的时候,会将其合并。

Flink集群在容器中以Session模式运行,一个Flink集群可以运行多个计算任务。我们给Flink集群增加了一个标签,以区分各个Flink集群。我们设定开发环境,一个工作空间只能运行一个Flink集群,用来开发调试。一个工作空间,在生产环境可以运行1个或1个以上的不同标签的集群。在生产环境部署的时候,需要通过标签指定部署到那个集群,如果标签不存在,就会部署一个新的指定标签的集群,并在上面部署计算任务。
计算管道部署

用户画像作为大数据的根基,它抽象出一个用户的信息全貌,为进一步精准、快速地分析用户行为习惯、消费习惯等重要信息,提供了足够的数据基础,奠定了大数据时代的基石。 用户画像,即用户信息标签化,就是企业通过收集与分析消费者社会属性、生活习惯、消费行为等主要信息的数据之后,抽象出一个用户的商业全貌作是企业应用大数据技术的基本方式。用户画像为企业提供了足够的信息基础,能够帮助企业快速找到精准用户群体以及用户需求等更为广泛的反馈信息。 用户画像系统能很好地帮助企业分析用户的行为与消费习惯,可以预测商品的发展的趋势,提高产品质量,同时提高用户满意度。构建一个用户画像,包括数据源端数据收集、数据预处理、行为建模、构建用户画像。有些标签是可以直接获取到的,有些标签需要通过数据挖掘分析到!本套课程会带着你一步一步的实现用户画像案例,掌握了本套课程内容,可以让你感受到Flink+ClickHouse技术架构的强大和大数据应用的广泛性。 在这个数据爆发的时代,像大型电商的数据量达到百亿级别,我们往往无法对海量的明细数据做进一步层次的预聚合,大量的业务数据都是好几亿数据关联,并且我们需要聚合结果能在秒级返回。 包括我们的画像数据,也是有这方便的需求,那怎么才能达到秒级返回呢?ClickHouse正好满足我们的需求,它是非常的强大的。 本课程采用Flink+ClickHouse技术架构实现我们的画像系统,通过学习完本课程可以节省你摸索的时间,节省企业成本,提高企业开发效率。希望本课程对一些企业开发人员和对新技术栈有兴趣的伙伴有所帮助,如对我录制的教程内容有建议请及时交流。项目中采用到的算法包含Logistic Regression、Kmeans、TF-IDF等,Flink暂时支持的算法比较少,对于以上算法,本课程将带大家用Flink实现,并且结合真实场景,学完即用。系统包含所有终端的数据(移动端、PC端、小程序端),支持亿级数据量的分析和查询,并且是实时和近实时的对用户进行画像计算。本课程包含的画像指标包含:概况趋势,基础属性,行为特征,兴趣爱好,风险特征,消费特征,营销敏感度,用户标签信息,用户群里,商品关键字等几大指标模块,每个指标都会带大家实现。课程所涵盖的知识点包括:开发工具为:IDEA FlinkClickhouseHadoopHbaseKafkaCanalbinlogSpringBootSpringCloudHDFSVue.jsNode.jsElemntUIEcharts等等 课程亮点: 1.企业级实战、真实工业界产品 2.ClickHouse高性能列式存储数据库 3.提供原始日志数据进行效果检测 4.Flink join企业级实战演练 5.第四代计算引擎Flink+ClickHouse技术架构6.微服务架构技术SpringBoot+SpringCloud技术架构7.算法处理包含Logistic Regression、Kmeans、TF-IDF等8.数据库实时同步落地方案实操9.统计终端的数据(移动端、PC端、小程序端) 10.支撑亿级海量数据的用户画像平台11.实时和近实时的对用户进行画像计算12.后端+大数据技术栈+前端可视化13.提供技术落地指导支持 14.课程凝聚讲师多年实战经验,经验直接复制15.掌握全部内容能独立进行大数据用户平台的设计和实操企业一线架构师讲授,代码在老师的指导下企业可以复用,提供企业解决方案。  版权归作者所有,盗版将进行法律维权。 
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值