一、前提和背景
- 数据质量的目的是为了保证提供给业务放数据前发现数据的问题,避免使用方上来投诉,我们先保证数据集成的正确性即ODS层数据质量,再保证数仓作业以及ABI报表的数据质量。
- ODS层数据质量是从存量数据质量、T+1数据质量、实时数据质量这一整个流程实施,ODS 的存量数据质量和T+1数据质量现在基本落地,有了比较完整的体系,这里不做过多说明。
- 保证了数据集成的准确性以后,需要保证数仓的作业的数据质量,现在需要调研使用Griffin做个简单的数仓数据质量平台,供ABI和其他数仓团队成员使用。
1.1 数据质量相关知识
之前整理的脑图:
1.1.1 基本维度
由英国DAMA工作组提出的六个数据质量基本维度。
1、哪六个?
- 完整性:所有的数据项都被记录了吗
- 一致性:数据可以匹配上了吗
- 独特性:是单一的数据观测角度吗
- 有效性:该数据符合规则吗
- 准确性:该数据能有效反应该领域吗
- 时效性:该数据是反映哪个时段的问题
2、如何使用数据质量维度指标?
- 评估应该哪一个数据质量维度以及相关权重
- 审查结果并且决定数据质量是不是可接受的
1.1.2 目前团队应该关注的指标
1.1.2.1 完整性
数据信息是否存在缺失的状况。
举例说明:
- 比如平时的日访问量在1000左右,突然某一天降到100了,需要检查一下数据是否存在缺失了。
- 网站统计地域分布情况的每一个地区名就是一个唯一值,我国包括了32个省和直辖市,如果统计得到的唯一值小于32,则可以判断数据由可能存在缺失。
1.1.2.2 一致性
数据是否遵循了统一的规范,数据集合是否保持了统一的格式,一致性主要体现在数据记录的规范
和数据是否符合逻辑
。
规范:规范指的是一项数据存在它特定的格式;
规范 的举例说明:
- 手机号码一定是11位的数字
- IP地址一定是由4个0到255间的数字加上.组成的
逻辑:多项数据间存在着固定的逻辑关系;
逻辑 的举例说明:
- PV一定大于等于UV
- 转换率一定在0和1之间
1.1.2.3 准确性
准确性是指数据记录的信息是否存在异常或错误,存在准确性问题的数据不仅仅只是规则上的不一致。
举例说明:
- 乱码;
- 异常的大或者小的数据也是不符合条件的数据,这类错误则可以使用最大值和最小值的统计量去审核。
1.1.2.4 时效性
时效性是指数据从产生到可以查看的时间间隔,也叫数据的延时时长,时效性对于数据分析本身要求并不高,但如果数据分析周期加上数据建立的时间过长,就可能导致分析得出的结论失去了借鉴意义。
根据团队的使用场景和要求,目前时效性方面还没有太大的要求。
二、Griffin 基本介绍
Griffin的简介在http://griffin.apache.org/docs/quickstart-cn.html 介绍了比较清楚了,所以下面的介绍基本上是照搬过来:
2.1 概述
数据质量模块是大数据平台中必不可少的一个功能组件,Apache Griffin(以下简称Griffin)是一个开源的大数据数据质量解决方案,它支持批处理和流模式两种数据质量检测方式,可以从不同维度(比如离线任务执行完毕后检查源端和目标端的数据数量是否一致、源表的数据空值数量等)度量数据资产,从而提升数据的准确度、可信度。
2.2 Griffin 系统架构
在Griffin的架构中,主要分为Define
、Measure
和Analyze
三个部分:
1、Define:
主要负责定义数据质量统计的维度,比如数据质量统计的时间跨度、统计的目标(源端和目标端的数据数量是否一致,数据源里某一字段的非空的数量、不重复值的数量、最大值、最小值、top5的值数量等)。
可以在Griffin提供的简单页面定义规则,或者直接写配置文件(主要是两个配置文件,后面细说)。
2、Measure:
主要负责执行统计任务,生成统计结果。
把用户编写的规则解析成spark-sql,然后执行,大概效果如下:
最后把结果输出到指定Sink中。
3、Analyze:
主要负责保存与展示统计结果。
主要是从Elasticsearch中获取结果做展示,但是目前测试,只支持在页面配置的规则生成结果,如果是自定义规则直接提交spark任务,并没有找到展示的地方。
Griffin架构图:
相似系统对比图(图片来源):
三、Griffin 安装
我这选择的是 Griffin-0.6.0的版本,下载的地址:https://github.com/apache/griffin/tree/griffin-0.6.0
3.1 相关依赖的准备
3.1.1 组件依赖的安装
Griffin主要依赖下面的这些组件:
- JDK (1.8 or later versions)
- MySQL(version 5.6及以上),
- Hadoop (2.6.0 or later)
- Hive (version 2.x)
- Spark (version 2.2.1+)
- Livy(livy-0.5.0-incubating),提交任务和任务管理。
- ElasticSearch (5.0 or later versions)
这些组件,集群都已经有了,安装演示就不多做说明了,可以查看:deploy-guide
3.1.1.1 MySQL 库和表的准备
1、建库:
Griffin的元数据需要存储,可以是MySQL或者postgresql,这里采用MySQL,需要在MySQL建一个数据库,我这里建一个库名为:griffin_quartz
。
create database griffin_quartz;
2、初始化:
需要提前建一些表,官方已经提供:Init_quartz_mysql_innodb.sql,不过需要里面没有指定数据库,所以我在里面新增一行,其他没有特别的:
use griffin_quartz;
保存文件以后,建表:
mysql -h 172.31.101.11 -P 13309 -u griffin -p123456 < Init_quartz_mysql_innodb.sql
3.1.1.2 Elasticsearch 建索引
最后结果指标以及展示都是通过 Elasticsearch,所以需要提前在Elasticsearch创建索引,我这里使用的是Elasticsearch 7,创建索引稍区别于 7 版本以前的。
创建索引:
curl -k -H "Content-Type: application/json" -X PUT --user userName:password http://192.168.37.146:9200/griffin?include_type_name=true -d '{"aliases":{},"mappings":{"accuracy":{"properties":{"name":{"fields":{"keyword":{"ignore_above":256,"type":"keyword"}},"type":"text"},"tmst":{"type":"date"}}}},"settings":{"index":{"number_of_replicas":"2","number_of_shards":"5"}}}'
因为生产的 Elasticsearch是需要账户密码的,所以不需要账户名密码去掉--user userName:password
即可。
创建以后可以通过此方式查看该索引所有数据:
[hdfs@bgnode8 tmp]$ curl -X GET --user userName:password "http://192.168.37.146:9200/griffin/_search?pretty"
3.1.2 编译前配置修改
我这选择的是 Griffin-0.6.0的版本,下载的地址:https://github.com/apache/griffin/tree/griffin-0.6.0,下载以后通过 IDE 打开需要修改一些配置文件(大部分配置的说明在deploy-guide也挺详细):
1、service/pom.xml文件配置:
把注释掉的MySQL 驱动依赖打开即可:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.java.version}</version>
</dependency>
因为后面把这个SpringBoot打包上Linux运行的时候,报了类找不到,所以我在插件那还做了一点修改,可根据实际情况来:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot-maven-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<fork>true</fork>
<layout>JAR</layout>
<mainClass>org.apache.griffin.core.GriffinWebApplication</mainClass>
</configuration>
</plugin>
2、修改配置文件 service/src/main/resources/application.Properties:
# Apache Griffin应用名称
spring.application.name=griffin_service
# 更换springboot端口
server.port = 9876
spring.datasource.url=jdbc:mysql://172.31.101.11:13309/griffin_quartz?autoReconnect=true&useSSL=false
spring.datasource.username=griffin
spring.datasource.password=123456
spring.jpa.generate-ddl=true
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.show-sql=true
# Hive metastore配置信息
hive.metastore.uris=thrift://bgnode3:9083
hive.metastore.dbname=default
hive.hmshandler.retry.attempts=15
hive.hmshandler.retry.interval=2000ms
#Hive jdbc
hive.jdbc.className=org.apache.hive.jdbc.HiveDriver
hive.jdbc.url=jdbc:hive2://172.31.100.203:10001/
hive.need.kerberos=false
hive.keytab.user=xxx@xx.com
hive.keytab.path=/path/to/keytab/file
# Hive cache time
cache.evict.hive.fixedRate.in.milliseconds=900000
# Kafka schema registry
kafka.schema.registry.url=http://localhost:8081
# Update job instance state at regular intervals
jobInstance.fixedDelay.in.milliseconds=60000
# Expired time of job instance which is 7 days that is 604800000 milliseconds.Time unit only supports milliseconds
jobInstance.expired.milliseconds=604800000
# schedule predicate job every 5 minutes and repeat 12 times at most
#interval time unit s:second m:minute h:hour d:day,only support these four units
predicate.job.interval=5m
predicate.job.repeat.count=12
# external properties directory location
external.config.location=
# external BATCH or STREAMING env
external.env.location=
# login strategy ("default" or "ldap")
# 登录默认是不需要账户密码的,ldap目前还没有调通,就算集成了ldap也没有控制权限的功能,所以不打算集成了
login.strategy=default
# ldap
ldap.url=ldaps://111.8.111.111:636
ldap.email=@bagoo.com
ldap.searchBase=dc=ba,dc=com
ldap.bindDN=caslogin-admin,cn=users,dc=ba,dc=com
ldap.bindPassword=HF*********
ldap.searchPattern=(&(objectClass=user)(|(sAMAccountName={0})(userprincipalname={0})))
ldap.sslSkipVerify=true
# hdfs default name
fs.defaultFS=
# elasticsearch
elasticsearch.host=192.168.37.146
elasticsearch.port=9200
elasticsearch.scheme=http
elasticsearch.user=userName
elasticsearch.password=password
# livy
# /batches这部分不可少
livy.uri=http://bgnode5:8999/batches
livy.need.queue=false
livy.task.max.concurrent.count=20
livy.task.submit.interval.second=3
livy.task.appId.retry.count=3
livy.need.kerberos=false
livy.server.auth.kerberos.principal=livy/kerberos.principal
livy.server.auth.kerberos.keytab=/path/to/livy/keytab/file
# yarn url
yarn.uri=http://bgnode1:8088
# griffin event listener
internal.event.listeners=GriffinJobEventHook
# 配置日志地址
logging.file=/var/log/griffin/griffin-service.log
像hive、livy这些在 Ambari (或AWS控制台等)找,看对应的地址,然后端口一般是一样的,不确定的话去配置搜一下:
3、修改配置文件 service/src/main/resources/quartz.properties:
注释已经说了,不多解释:
# If you use postgresql as your database,set this property value to org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
# If you use mysql as your database,set this property value to org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# If you use h2 as your database, it's ok to set this property value to StdJDBCDelegate, PostgreSQLDelegate or others
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
4、修改配置文件 service/src/main/resources/sparkProperties.json:
该配置文件主要是在页面提交Job的时候用到的配置spark的一些参数:
{
"file": "hdfs:/tmp/griffin/griffin-measure.jar",
"className": "org.apache.griffin.measure.Application",
"queue": "default",
"numExecutors": 4,
"executorCores": 2,
"driverMemory": "1g",
"executorMemory": "1g",
"conf": {
"spark.yarn.dist.files": "hdfs:/tmp/griffin/hive-site.xml"
},
"files": [
]
}
其中用到的hdfs目录以及文件记得创建和上传。
5、修改配置文件 service/src/main/resources/env/env_batch.json:
只要是页面提交Job时候用到的一些配置,大部分是输出配置,具体含义可看这里。
{
"spark": {
"log.level": "INFO"
},
"sinks": [
{
"name": "console",
"type": "CONSOLE",
"config": {
"max.log.lines": 30
}
},
{
"name": "hdfs",
"type": "HDFS",
"config": {
"path": "hdfs:///tmp/griffin/persist",
"max.persist.lines": 10000,
"max.lines.per.file": 10000
}
},
{
"name": "elasticsearch",
"type": "ELASTICSEARCH",
"config": {
"method": "post",
"api": "http://192.168.37.146:9200/griffin/accuracy",
"connection.timeout": "1m",
"retry": 10
}
}
],
"griffin.checkpoint": []
}
看了上面 Elasticsearch Sink 的配置不知道你会不会好奇?
前面对Elasticsearch配置了用户名密码,这里没有配置,而且看了官方Sink对Elasticsearch的介绍和代码里面,确实没有找到其他用户名密码参数的配置,那输出的时候会不会有问题,答案是会的,后面再说解决方式。
3.1.3 打包编译启动
修改完配置以后直接打包编译:
mvn -Dmaven.test.skip=true clean install -e
打包完后主要有两个jar:
- service-0.6.0.jar:就是springboot项目,web程序。
- measure-0.6.0.jar:就是跑校对规则的,spark程序,就是上面
sparkProperties.json
的griffin-measure.jar
,所以需要改名再上传的到hdfs路径上。
把service-0.6.0.jar
放到服务器上,然后启动即可(可以先不分配内存):
java -Xmx4g -Xms4g -jar service-0.6.0.jar
四、Griffin 提供的UI页面使用
打开你配置的端口地址:http://172.16.11.136:9876
如果你登录方式配置的是default,那就随便填就可以进去了。
页面大概长这样:
4.1 Health
主要是显示指标数据的热图,目前作用不大。
4.2 Measures
定制数据质量校对规则的地方。
Griffin一共支持六种数据质量校对规则类型(dq.type),分别是:ACCURACY
, PROFILING
, DISTINCTNESS
,TIMELINESS
, UNIQUENESS
, COMPLETENESS
。
具体规则的含义:
Accuracy:
准确性(一致性),通过两个表的Join来判断两个表的记录是否一致,并计算相关指标。
Profiling:
统计分析,主要是根据用户自定义的SQL来计算Metrics(指标),大多是数据的聚合函数。
Distinctness:
Timeliness:
时效性,对于及时性,是度量每个项目的延迟,并获得延迟的统计信息。
Uniqueness:
Completeness:
完整性,需要检查NULL,如果您测量的列为NULL,则它们是不完整的。
和前面的数据质量的基本维度有提到六个基本维度基本吻合,上图占了两个,即Accuracy(准确性)、Profiling(统计分析)。
六个规则类型对规则的解析转换具体实现的代码在这:
根据DqType决定具体调用哪一个:
object Expr2DQSteps {
private val emtptExpr2DQSteps: Expr2DQSteps = new Expr2DQSteps {
def getDQSteps: Seq[DQStep] = emtptDQSteps
}
def apply(context: DQContext, expr: Expr, ruleParam: RuleParam): Expr2DQSteps = {
ruleParam.getDqType match {
case Accuracy => AccuracyExpr2DQSteps(context, expr, ruleParam)
case Profiling => ProfilingExpr2DQSteps(context, expr, ruleParam)
case Uniqueness => UniquenessExpr2DQSteps(context, expr, ruleParam)
case Distinct => DistinctnessExpr2DQSteps(context, expr, ruleParam)
case Timeliness => TimelinessExpr2DQSteps(context, expr, ruleParam)
case Completeness => CompletenessExpr2DQSteps(context, expr, ruleParam)
case _ => emtptExpr2DQSteps
}
}
}
了解了Griffin的几种规则类型以后,开始创建规则,点击Create Measure
开始创建规则:
就会出现四个可选的列表:
- 第一个
Accuracy
:也就是创建类型为Accuracy
的数据质量规则方案,就是校对两张表的准确性; - 第二个
Data Profiling
:数据分析,定义一个源数据集,求得n个字段的最大,最小,count值等等。 - 第三个
Publish
:Publish 发布,用户如果通过配置文件而不是界面方式创建了Measure,并且spark运行了该质量模型,结果集会写入到 ES中,通过publish 定义一个同名的Mesaure,就会在界面的仪表盘中显示结果集(暂时先不用了解,我还没有测通过,而且也是只支持Accuracy
规则的发布)。 - 第四个
JSON/YAML measure
:用户自定义的Measure,通常习惯用JSON的方式配置,对于比较灵活一点的,第一个的Accuracy
和第二个的Data Profiling
可能里面的功能并不是很全,就需要在这里自定义。
4.2.1 UI 中Measures的Accuracy创建使用
第一个Accuracy
的使用:
那就点击第一个Accuracy
,
第一步:选择数据源表
第二步:选择目标数据表
第三步:如何关联
第四步:两张表的分区,这里不选择
第五步:最后的基本配置
保存以后就可以在外面看到你刚才配置的规则
到这里规则就配置完成了,仅仅是配置了数据质量的校对规则,但是没有跑起来,也没有定时什么的,然后就需要配置定时任务了,请查看1.3 Jobs
。
4.2.2 UI 中Measures的Data Profiling创建使用
Data Profiling
主要是针对单表的,校对方式:
开始创建,选择一张表并勾选对应的字段:
选择字段对应的条件,比如有统计某个字段的空值,某个字段去重后的条数等等之类的:
基本信息填写:
Save即可。
保存以后在Jobs配置任务运行即可(创建Job具体步骤查看1.3 Jobs
):
解析后的SQL:
运行一次查看结果:
4.2.3 UI 中Measures的Publish创建使用
现在先不要创建这个,创建了这个以后,Jobs
页面的列表就会全部获取不到,需要把创建的这个Measure删掉,Jobs
页面才会变成正常。主要原因是创建了以后会在数据库新增记录,但是记录不满足要求,后台获取Jobs列表清单读取记录不满足要求,直接报错了,所以页面就看不到清单了。
4.2.4 UI 中Measures的JSON/YAML measure创建使用
如果你测试了上面的1.2.2
单表的规则,可能觉得字段可选的处理太少,不是很灵活,那么你就可以试试自己配置Json。
资料:可以参考measure-configuration-guide 以及dsl-guide
那我们先看官方是怎么写json的,我们稍加修改即可:
点击刚才配置的规则的详细情况:
copy出来的JSON ,我加了一些说明:
{
"measure.type": "griffin",//不可变更,照抄
"id": 1003, // 所有id都去掉
"name": "fsd_test_21-05-11_1347",// 名字,不多说
"owner": "test", // 照抄即可
"description": "单表案例",//描述
"deleted": false,
"dq.type": "PROFILING", // 前面说的六种规则类型的一种,单表分析
"sinks": [ // 写出位置,基本可以不用管,照抄,就是写到ES和HDFS
"ELASTICSEARCH",
"HDFS"
],
"process.type": "BATCH",// 这个就保留,离线校对的意思
"rule.description": {// 这个不用管,可以去掉
"details": [
{
"name": "id",
"infos": "Null Count"
},
{
"name": "age",
"infos": "Maximum,Minimum"
},
{
"name": "desc",
"infos": "Total Count"
}
]
},
"data.sources": [//就是读哪个表
{
"id": 1007,
"name": "source",//这里就是这个表的别名的意思
"connector": {
"id": 1008,
"name": "source1620711696162",
"type": "HIVE",
"version": "1.2",
"predicates": [],
"data.unit": "1day",
"data.time.zone": "",
"config": {
"database": "default",//库名
"table.name": "demo_src",//表名
"where": ""//where条件
}
},
"baseline": false
}
],
"evaluate.rule": {//这里就是规则,主要是rule,和SQL差不多,PROFILING可以直接写SQL
"id": 1004,
"rules": [
{
"id": 1005,
"rule": "count(source.id) AS `id_nullcount` WHERE source.id IS NULL",//和SQL差不多
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "id_nullcount"
},
{
"id": 1006,
"rule": "max(source.age) AS `age_max`,min(source.age) AS `age_min`,count(source.desc) AS `desc_count`",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING"
}
]
},
"type": "profiling"
}
把所有id去掉,稍加修改规则的类SQL后:
{
"measure.type": "griffin",
"name": "fsd_test_21-05-11_1409",
"owner": "test",
"description": "单表案例-JSON/YAML",
"deleted": false,
"dq.type": "PROFILING",
"sinks": [
"ELASTICSEARCH",
"HDFS"
],
"process.type": "BATCH",
"data.sources": [
{
"name": "source",
"connector": {
"name": "source1620711696162",
"type": "HIVE",
"version": "1.2",
"predicates": [],
"data.unit": "1day",
"data.time.zone": "",
"config": {
"database": "default",
"table.name": "demo_src",
"where": ""
}
},
"baseline": false
}
],
"evaluate.rule": {
"rules": [
{
"rule": "count(source.id) AS `id_nullcount` WHERE source.id IS NULL",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "id_nullcount"
},
{
"rule": "max(source.age) AS `age_max`,count(source.desc) AS `desc_count`",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING"
},
{
"rule": "count(source.age) AS `is_18` WHERE source.age = 18 ",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING"
}
]
},
"type": "profiling"
}
那就创建一个规则,选择JSON/YAML measure
:
创建对应的job:
运行:
转换后的SQL:
data.sources
转换成SQL:
evaluate.rule
规则转换成SQL:
查看结果:
再提供一个案例:
json如下:
{
"measure.type": "griffin",
"name": "rpt_rpt_ae_stockup_base_pp2",
"owner": "test",
"description": "手动拼-AE备货 rpt.rpt_ae_stockup_base_pp (SKU产品基础信息表)",
"deleted": false,
"dq.type": "PROFILING",
"sinks": [
"ELASTICSEARCH",
"HDFS"
],
"process.type": "BATCH",
"rule.description": {
"details": [
{
"name": "cost_price",
"infos": "Null Count"
}
]
},
"data.sources": [
{
"name": "source",
"connector": {
"name": "source1620439933000",
"type": "HIVE",
"version": "1.2",
"predicates": [],
"data.unit": "1day",
"data.time.zone": "",
"config": {
"database": "rpt",
"table.name": "rpt_ae_stockup_base_pp",
"where": ""
}
},
"baseline": false
}
],
"evaluate.rule": {
"rules": [
{
"rule": "count(source.cost_price) AS `cost_price_nullcount` WHERE source.cost_price IS NULL",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "cost_price_nullcount"
},
{
"rule": "count(source.cost_price) AS `cost_price_0` WHERE source.cost_price = 0",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "cost_price_0"
},
{
"rule": "count(source.smtstatustype) AS `smtstatustype_nullcount` WHERE source.smtstatustype IS NULL",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "smtstatustype_nullcount"
},
{
"rule": "count(source.smtstatustype) AS `smtstatustype_emptycount` WHERE source.smtstatustype = ''",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "smtstatustype_emptycount"
},
{
"rule": "count(source.pm_dpart) AS `pm_dpart_nullcount` WHERE source.pm_dpart IS NULL",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "pm_dpart_nullcount"
},
{
"rule": "count(source.pm_dpart) AS `pm_dpart_emptycount` WHERE source.pm_dpart = ''",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "pm_dpart_emptycount"
},
{
"rule": "count(source.ae_manager_name) AS `ae_manager_name_nullcount` WHERE source.ae_manager_name IS NULL",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "ae_manager_name_nullcount"
},
{
"rule": "count(source.ae_manager_name) AS `ae_manager_name_emptycount` WHERE source.ae_manager_name = ''",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "ae_manager_name_emptycount"
},
{
"rule": "count(source.p_manager_name) AS `p_manager_name_nullcount` WHERE source.p_manager_name IS NULL",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "p_manager_name_nullcount"
},
{
"rule": "count(source.p_manager_name) AS `p_manager_name_emptycount` WHERE source.p_manager_name = ''",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "p_manager_name_emptycount"
},
{
"rule": "count(source.dev_manager_name) AS `dev_manager_name_nullcount` WHERE source.dev_manager_name IS NULL",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "dev_manager_name_nullcount"
},
{
"rule": "count(source.dev_manager_name) AS `dev_manager_name_emptycount` WHERE source.dev_manager_name = ''",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "dev_manager_name_emptycount"
},
{
"rule": "count(source.stockout_num) AS `stockout_num_nullcount` WHERE source.stockout_num IS NULL",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "stockout_num_nullcount"
},
{
"rule": "count(source.to_beshipped_qty) AS `to_beshipped_qty_nullcount` WHERE source.to_beshipped_qty IS NULL",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING",
"out.dataframe.name": "to_beshipped_qty_nullcount"
},
{
"rule": "max(source.stockout_num) AS `stockout_num_max`,max(source.to_beshipped_qty) AS `to_beshipped_qty_max`",
"dsl.type": "griffin-dsl",
"dq.type": "PROFILING"
}
]
},
"type": "profiling"
}
4.3 Jobs
前面已经配置好了定时规则,正在的作业运行是在Jobs这个页面配置,那现在就尝试把配置的定时规则跑起来。
先跑1.2.1 中Accuracy创建的规则吧:
点击Create Job
开始创建Job:
保存即可,保存可能需要几秒钟,稍微等一下:
保存以后列表就能看到我们的任务了:
我们现在就点击运营一次任务看看效果:
点击运行后,可以查看任务运行列表和状态:
点击Application id就到这了:
因为我把转换后的SQL打印了出来,所以可以点击Logs查看标准输出,
前面配置的规则最终解析成SQL就是这样:
data.sources
转换成SQL:
evaluate.rule
规则转换成SQL:
运行成功后:
4.4 My Dashboard
就是Accuracy
运行后的图表,单机某个图可以放大,仅此而已,不太灵活。
五、问题
5.1 安装遇到的问题
1、dataconnector建表失败:
启动以后应该会遇到一个问题,报什么Table 'griffin_quartz.dataconnector' doesn't exist
,springBoot启动会建一些表,但是这张表是建失败了,原因:
因为字段太长,超出数据库限制长度造成JPA创建表失败,从而造成后面无法插入表不存在,需要修改 DataConnector.java 类:
//DataConnector类进行添加columnDefinition = "TEXT"
@JsonIgnore
@Column(length = 20480,columnDefinition = "TEXT")
private String config;
修改以后重新打包重启即可。
解决参考链接
2、ldap登录报错
前面登录配置为login.strategy=ldap
时,填了了ldap相关的配置,进入页面登录报错:
javax.naming.CommunicationException: simple bind failed: 172.11.2.11:636
at com.sun.jndi.ldap.LdapClient.authenticate(LdapClient.java:219) ~[?:1.8.0_231]
Caused by: javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No subject alternative names matching IP address 172.11.2.11 found
at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) ~[?:1.8.0_231]
Caused by: java.security.cert.CertificateException: No subject alternative names matching IP address 1172.11.2.11 found
at sun.security.util.HostnameChecker.matchIP(HostnameChecker.java:168) ~[?:1.8.0_231]
解决这个问题:
参考链接1
参考链接2
启动的时候添加一个参数:
[root@griffin griffin]# java -Dcom.sun.jndi.ldap.object.disableEndpointIdentification=true -Xmx4g -Xms4g -jar service-0.6.0.jar
5.2 Griffin 存在的问题
1、Elasticsearch用户无法配置问题
前面修改env_batch.json配置文件的时候有提到,env_batch.json配置文件的sink是在页面提交任务的时候,最后在spark运行时会读这个文件的配置信息,输出到对应的sink,但是输出到ElasticsearchSink的时候,没有配置账户名密码那肯定无法向Elasticsearch写入数据。
ElasticSearchSink.scala这个文件就是写出到Elasticsearch的,看了这个类是通过http写入数据的,主要是下面这个方法:
private def httpResult(dataMap: Map[String, Any]): Unit = {
try {
val data = JsonUtil.toJson(dataMap)
// http request
val params = Map[String, Object]()
val header = Map[String, Object](("Content-Type", "application/json"))
def func(): (Long, Future[Boolean]) = {
import scala.concurrent.ExecutionContext.Implicits.global
(timeStamp, Future(HttpUtil.doHttpRequest(api, method, params, header, data)))
}
if (block) SinkTaskRunner.addBlockTask(func _, retry, connectionTimeout)
else SinkTaskRunner.addNonBlockTask(func _, retry)
} catch {
case e: Throwable => error(e.getMessage, e)
}
}
既然curl可以通过--user
参数配置用户名密码,那猜测http可能请求头也可以带上去。
直接搜一下curl --user
直接就看到了curl 中的-u/–user username:password 在postman中如何使用,主要说的是用base64
加密的方式添加到请求头,那想办法获取base64的加密值。
再面向百度编程搜java base64加密代码
,直接就看到Java base64加密解密 两种实现方式,然后一顿 CV 大法之后,修改:
String str = "userName:password";
然后运行上面那个博文的代码,输出 base64 加密后的值:
Base 64 加密后:dXNlck5hbWU6cGFzc3dvcmQ=
Base 64 解密后:userName:password
那就拿这个base64加密后的值放到请求头试一下(加密后的值前面记得加上Basic ):
curl -X GET -H "Authorization: Basic dXNlck5hbWU6cGFzc3dvcmQ=" "http://192.168.37.146:9200/griffin/_search?pretty"
然后就可以访问了。
那就把这个配到代码试一下,修改ElasticSearchSink.scala的httpResult方法:
private def httpResult(dataMap: Map[String, Any]): Unit = {
// scalastyle:off
// 关闭这个才可以写中文
try {
val data = JsonUtil.toJson(dataMap)
// http request
val params = Map[String, Object]()
// TODO 修改成配置的方式
// 请求头添加一个用户名密码,相当于 curl 后的 --user userName:password
val header = Map[String, Object](("Content-Type", "application/json"),("Authorization","Basic dXNlck5hbWU6cGFzc3dvcmQ="))
def func(): (Long, Future[Boolean]) = {
import scala.concurrent.ExecutionContext.Implicits.global
(timeStamp, Future(HttpUtil.doHttpRequest(api, method, params, header, data)))
}
if (block) SinkTaskRunner.addBlockTask(func _, retry, connectionTimeout)
else SinkTaskRunner.addNonBlockTask(func _, retry)
} catch {
case e: Throwable => error(e.getMessage, e)
}
}
重新打包,把griffin-measure.jar
包放到指定位置,再运行就能解决写出到Elasticsearch的问题了,但是这里不应该写死,而是应该想办法用配置的方式,然后自己转换成base64加密后的值,这里不做过多讨论。
2、展示的时间对不上
现象就是:显示的时间比运行的时间早一天
原因:dq.json会有一个timestamp
字段,在传到measure的模块的时候,timestamp
字段就已经是小一天了,Application.scala这个类会对参数进行日志输出,所以在日志可以看的到。因此猜测是页面封装dq.json的时候的问题,这里我就没有认真去找了,就说一下解决这个:
可以在BatchDQApp这个类的run方法替换成当前时间:
或者改DQApp.scala
的getMeasureTime
方法也可以,改这个方法可能会更好,因为DQApp.scala
可能还有其他类继承,看自己怎么想吧。
改完就正常了:
因为提交和到运行起来是有一点点误差,这个影响不大。
3、页面统计null值解析SQL不正确:
上图是统计某字段为null的数量,但是count函数里面的字段有null是不会算上的,也就是上图的语句始终会等于0.
应该改成:
count(*) AS `pmcstockusername_emptycount` WHERE source.pmcstockusername IS NULL
4、定时调度错乱:
如果上线作业足够多,然后定时时间都是同一个时间点的话,可能会导致作业调度出现错乱。具体原因没有去排查和解决,我只是叫下游把时间稍微错开一点。
六、告警和可视化(2022-04-24更新)
- 2022年4月24日 更新原因说明:
之前在写这个文章,是我在调研的时候写的。后面该框架正式上线之后更改了一些东西也没有再本篇文章进行更新了。
说明:
- 现在这个框架主要在我们生产上两个环境上运行,我们也就两个生产环境。
- 我们的业务现在还是离线的为主,所以这个框架主要针对的是离线数据校验。实时数据质量我没有使用这个框架,自己简单开发了个工具。
- 这个框架我只是负责调研和维护,以及功能完善,实际使用方是我的下游:数仓、ABI等。
- 前面的基本是使用和安装说的差不多了,但是下游使用的时候,结果展示上和异常告警这个框架做的并不完善。
6.1 告警和可视化的方案
- 我觉得是符合我们当前目标的方案,不同的公司和需求不同,可能方案不一定适用,我这里只提供了我们的使用方式。
现状:
- 下游只需要知道质量是否存在问题,存在问题再去排查即可。但是这个Griffin的可视化和告警功能并不完善,作业多的时候,看起来很不方便。
- 结果主要是存在 ES和HDFS,监控起来并不方便。
- 我们目前大部分监控和告警都是通过企业微信机器人每天早晨推送。就是作业跑完之后,作业是否异常,推送到群里,一眼便知。所以现在是没有做告警的方式,而是通过主动巡航推送的方式。
所以这个质量监控的可视化需求也是这样,每天定时推送,然后有问题的话标红就行,就有专门负责人排查。
最终结果大概是这样(只截一部分,每天会定时推送到群里,有异常会标红):
所以我只需要想办法把结果数据存储下来,然后下游自己配看板就可以了。
把每个指标对应的值解开,存在 Clickhouse。
结果拉出来长这样:
具体怎么把结果存起来。Griffin 提供了一些API,我在原本API上再加点功能:我就是通过Griffin提供的API,获取到对应的结果,再把对应的结果存储到clickhouse就行了,我都不需要直接去采集ES的数据。
有了结果,后面怎么告警和推送,那就随便搞搞就行了。