首页 > > > 实时数仓模型(持续更新ing)

早期数据仓库构建主要指的是把企业的业务数据库如 ERP、CRM、SCM 等数据按照决策分析的要求建模并汇总到数据仓库引擎中,其应用以报表为主,目的是支持管理层和业务人员决策(中长期策略型决策)。随着业务和环境的发展,这两方面都在发生着剧烈变化。
正常的大数据开发主要包含离线大数据开发和实时大数据开发,也就是批处理和流处理,这两块的处理方式不一样,一般离线的主要是:Hive、Spark等等,实时的主要是:Flink、Storm、StructedStreaming等等,目前常用的大数据架构分为:离线大数据架构、Lambda 架构和Kappa 架构。
离线大数据架构(最常见的离线数仓模型):
数据源通过离线的方式导入到离线数仓中,下游应用根据业务需求选择直接读取 DM 或加一层数据服务,比如 MySQL 或 Redis,数据存储引擎是 HDFS/Hive,ETL 工具可以是 MapReduce 、Spark或 HiveSQL。数据仓库从模型层面分为操作数据层 ODS、数据仓库明细层 DWD、数据集市层 DM,最后到APP层做数据展现。
离线数仓主要基于sqoop、datax、hive等技术来构建 T+1 的离线数据,通过定时任务每天垃取增量数据导入到hive表中,然后创建各个业务相关的主题,对外提供T+1的数据查询接口。
问题:
Lambda 架构:
为了计算一些实时指标,就在原来离线数仓的基础上增加了一个实时计算的链路,并对数据源做流式改造(即把数据发送到消息队列),实时计算去订阅消息队列,直接完成指标增量的计算,推送到下游的数据服务中去,由数据服务层完成离线&实时结果的合并。
实时数仓主要是基于数据采集工具,如canal等原始数据写入到kafka这样的数据通道中,最后一般都是写入到类似于HBase这样的OLAP存储系统中。对外提供分钟级别,甚至秒级别的查询方案。
问题:

Kappa 架构:
Lambda 架构虽然满足了实时的需求,但带来了更多的开发与运维工作,其架构背景是流处理引擎还不完善,流处理的结果只作为临时的、近似的值提供参考。后来随着 Flink 等流处理引擎的出现,流处理技术很成熟了,这时为了解决两套代码的问题,LickedIn 的 Jay Kreps 提出了 Kappa 架构。
问题:
Lambda 架构与 Kappa 架构的对比

注意:
实时数仓架构和数据中台一样,虽然都是属于当前比较热门的概念,但是对于实时数仓的狂热追求大可不必,首先,在技术上几乎没有难点,基于强大的开源中间件实现实时数据仓库的需求已经变得没有那么困难。其次,实时数仓的建设一定是伴随着业务的发展而发展,武断的认为Kappa架构一定是最好的实时数仓架构是不对的。实际情况中随着业务的发展数仓的架构变得没有那么非此即彼。

实时数仓需要解决的问题:
1)第一,要支持同时读写,就意味着你写的时候还可以读,不应该读到一个错误的结果。同时还可以支持多个写,且能保证数据的一致性;
2)第二,可以高吞吐地从大表读取数据。大数据方案不能有诸多限制,比如,我听说有些方案里最多只可以支持几个并发读,或者读的文件太多了就不让你提交作业了。如果这样,对业务方来说,你的整个设计是不满足他的需求的;
3)第三,错误是无可避免,你要可以支持回滚,可以重做,或者可以删改这个结果,不能为了支持删改而要求业务方去做业务逻辑的调整;
4)第四,在重新改变业务逻辑的时候要对数据做重新处理,这个时候,业务是不能下线的。在数据被重新处理完成之前,数据湖的数据是要一直可被访问的;
5)第五,因为有诸多原因,数据可能会有晚到的情况,你要能处理迟到数据而不推迟下阶段的数据处理。
基于以上五点,我们基于Flink或者Structured Streaming 产生了一个新的批流一体化架构。
实时数据体系大致分为三类场景:流量类、业务类和特征类,这三种场景各有不同。
实时数仓的实施关键点:
其实,你需要的不是实时数仓,需要的是一款合适且强大的OLAP数据库。

所以所谓的实时数仓就是在之前的Lambda的数据结构中,将离线的数据开发和实时的数据开发合并到一起,也就是所谓的流批一体化,这个实现方式主要是:
log/mysql/hive(数据源) -> SparkCore/Sparksql/hive(数据处理) -> hdfs/mysql等(数据存储)
kafka/binlog(数据源) ->StructuredStreaming/flink(数据处理) -> mysql/redis/es等(数据存储)
将这两个处理逻辑合并。
StructuredStreaming的批流一体化,虽然能加实时和离线整合起来,但是spark是以离线开发为主。
案例:
实时获取log日志,做实时的数据大屏展现,并落库到HDFS作为ODS数据源。

而且StructuredStreaming可以完成离线的数据存储(FileSink,写到文件体系中),同时完成实时的数据存储(ForeachSink,自定义存储方式)。
例如:
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.streaming.{ProcessingTime, Trigger}
import org.json._
import utils._
/**
* Created by gaowei
*/
object ZooMessage {
case class Log(timestamp : String, hostname : String, name : String, version : String, index : String,
input_type : String, offset : String, source : String, types : String, uid : String,
zid : String, text : String, sessionKey:String,ds:String)
def logParing(log : String) = {
val str = Config.decodeUFT8(log)
try {
var json = new JSONObject()
if (str.startsWith("\"")){
json = new JSONObject(str.replace("\\", "").dropRight(1).drop(1))
}
json = new JSONObject(str)
val timestamp = json.optString("@timestamp", "")
val ds = timestamp.split("T")()
val beat = json.getJSONObject("beat")
val hostname = beat.optString("hostname", "")
val name = beat.optString("name", "")
val version = beat.optString("version", "")
val index = json.optString("index", "")
val input_type = json.optString("input_type", "")
val message = json.optString("message", "")
var uid = ""
var zid = ""
var sessionKey = ""
val userPatterns = "user:[0-9]+".r
var text = ""
if (message.contains("建立连接")) {
val zooPatterns = "聊天室:[0-9]+".r
val sessionPatterns = "sessionKey:[^,\"}]+".r
uid = if (userPatterns.findFirstMatchIn(message).isDefined) userPatterns.findFirstMatchIn(message).get.toString().substring(5) else "None"
zid = if (zooPatterns.findFirstMatchIn(message).isDefined) zooPatterns.findFirstMatchIn(message).get.toString().substring(4) else "None"
sessionKey = if (sessionPatterns.findFirstMatchIn(message).isDefined) sessionPatterns.findFirstMatchIn(message).get.toString().substring(12) else "None"
text = "Connection"
}
else {
val zooPatterns = "zoo:[0-9]+".r
val sessionPatterns = "sessionKey:[^,\"}\\s+]+".r
uid = if (userPatterns.findFirstMatchIn(message).isDefined) userPatterns.findFirstMatchIn(message).get.toString().substring(5) else "None"
zid = if (zooPatterns.findFirstMatchIn(message).isDefined) zooPatterns.findFirstMatchIn(message).get.toString().substring(4) else "None"
sessionKey = if (sessionPatterns.findFirstMatchIn(message).isDefined) sessionPatterns.findFirstMatchIn(message).get.toString().substring(11) else "None"
text = "Leave"
}
val offset = json.optString("offset", "")
val source = json.optString("source", "")
val types = json.optString("type", "")
Log( timestamp, hostname, name, version, index, input_type, offset, source, types, uid, zid, text, sessionKey, ds)
}
catch {
case ex : JSONException => Log("1","1","1","1","1","1","1","1","1","1","1","1","1","1")
}
}
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().appName("ZooMessage").getOrCreate()
val brokers = "xxx.xxx.xxx.xx.xx:9092,xxx.xxx.xxx.xx.xx::9092"
val topics = "test"
val df = spark.readStream.format("kafka").
option("kafka.bootstrap.servers", brokers).
option("subscribe", topics).
load()
import spark.implicits._
val kafkaDf = df.selectExpr("CAST(value AS STRING)").as[String].
filter(f => f.contains("cn.idongjia.zoo.handler.AbstractZooPackWebSocketHandler.afterConnectionEstablished") ||
f.contains("cn.idongjia.zoo.handler.AbstractZooPackWebSocketHandler.afterConnectionClosed")).
filter(_.contains("/data/servers/zoo/logs/service.log"))
import spark.implicits._
val dataFrame = kafkaDf.map {
f =>
logParing(f)
}
val query = dataFrame.coalesce(1).writeStream.outputMode("append").partitionBy("ds").trigger(Trigger.ProcessingTime("600 seconds")).format("parquet").
option("checkpointLocation", "/user/gaowei/auctionCheck").
option("path", "/user/gaowei/auctionMini").start()
query.awaitTermination()
}
}这个任务就算是将日志每10分钟获取一次,生成一个小文件,然后在凌晨启动一个任务,将小文件合并掉。
实时的数数据写到mysql,主要是使用ForeachSink,详情见:
例如自定义写到mysql的Sink:
class MysqlSink(url: String, user: String, pwd: String) extends ForeachWriter[Row] {
var conn: Connection = _
override def open(partitionId: Long, epochId: Long): Boolean = {
Class.forName("com.mysql.jdbc.Driver")
conn = DriverManager.getConnection(url, user, pwd)
true
}
override def process(value: Row): Unit = {
val p = conn.prepareStatement("replace into test(pid,pv) values(?,?)")
p.setString(1, value(0).toString)
p.setLong(2, value(1).toString.toLong)
p.execute()
}
override def close(errorOrNull: Throwable): Unit = {
conn.close()
}
}以OPPO 实时计算平台为例:

整个数据 Pipeline 的实时模式,跟刚才我们说的离线时代几乎是一样的,只不过我们把一些关键性的组件替换了,比如说把 HDFS 替换成了 Kafka,把 Hive 替换成了 Flink,还有还有 OStream 平台,替换离线任务调度。然后其他的产品,包括基于报表、画像、接口这样的产品,都是保持一致的。
最下面一层是基础引擎层,这一层大家都很熟悉,无非就是基于 Kafka、Flink、ES 等这些系统,整个集群管理通过 Yarn 来管理,然后 Flink 需要做检查点,会用到 HDFS;平台特性这一层,提供网页版 IDE,会有像元数据管理、流作业管理、日志检索、监控告警等这些能力;再往上就是 API 这一层,编程接口支持 SQL,SQL 是我们的第一等公民,然后还可以支持 Jar 包提交的方式;再往上,我们把它称为“集成工具”。刚才提到,我们会有一些场景,希望用户不用写 SQL,可以通过简单 UI 配置的方式,就能自动生成 SQL。然后我们还会跟内部的 CI、CD 工具自动打通,自动编译好 Jar 包之后,自动把 Jar 包传到我们这个平台,这个正在研发当中。这个就是整体的架构。
例如FlinkSQL的实时数据开发:
create table kafka_source (
messageKey varbinary,
message varbinary,
topic varchar,
`partition` int,
`offset` bigint
) with (
type = 'kafka010',
topic = 'topic1',
bootstrap.servers = 'test',
`group.id` = 'test',
startupMode = 'TIMESTAMP',
starttime = '2020-03-31 00:00:00'
);
create table pv_user_id (
`user_id` varchar,
`view_from` varchar,
`create_date` varchar,
`pv` bigint,
PRIMARY KEY (`user_id`,`view_from`,`create_date`)
) with (
type='mysql',
dburl='jdbc:mysql://test:3306/gw?useUnicode=true&characterEncoding=UTF-8&useSSL=true',
drivername='com.mysql.jdbc.Driver',
tablename='pv_user_id',
username='test',
password='test'
);
insert into pv_user_id
select user_id,
view_from,
create_date,
count(1) as pv
from (
select
JSON_VALUE(cast(message as varchar), '$.data.otherUId') as user_id,
case when JSON_VALUE(cast(message as varchar), '$.data.from') is null then ''
else JSON_VALUE(cast(message as varchar), '$.data.from') end as view_from,
from_unixtime(cast(cast((JSON_VALUE(cast(message as varchar), '$.date')) as bigint)/1000 as bigint)+28800, 'yyyy-MM-dd') as `create_date`
from kafka_source
where JSON_VALUE(cast(message as varchar), '$.platform') = 'IOS'
and JSON_VALUE(cast(message as varchar), '$.typeid') = 'view'
and JSON_VALUE(cast(message as varchar), '$.data.otherUId') is not null
and JSON_VALUE(cast(message as varchar), '$.date') is not null
)
group by user_id,view_from,create_date目前Flink1.11版本已经增加对hive的支撑,提供了对Hive Streaming Sink的支持,并兼容Hive的DDL和DML语句,所以在未来,基于Flink的流批一体应该会成为主流。大数据上半场战斗集中在底层框架,目前已经接近尾声,基于spark/hadoop的批流一体已经逐渐完善,基于Flink的流批一体也在构建自己的生态,所以未来的底层大数据生态圈中将不再有那么多的新的技术和框架,每个细分领域都将优胜劣汰,走向成熟,更加集中化。下半场战斗的重点讲从底层走向上层,走向生态。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69918097/viewspace-3159630/ ,如需转载,请注明出处,否则将追究法律责任。