一、现象

目前我们的数据是通过OGG->Kafka->Spark Streaming->HBase。由于之前我们发现HBase的列表put无法保证顺序,因此改了程序,如果是在同一个SparkStreaming的批次里面对同一条数据进行操作,则写入HBase的数据时间戳就非常相近,只会差几毫秒,如果是不同批次则会差好几秒。此为背景。

现在有一条数据,理应先删除再插入,但是结果变成了先插入再删除,结果如下

  1. hbase(main):002:0> get 'XDGL_ACCT_PAYMENT_SCHEDULE','e5ad-***', {COLUMN=>'cf1:SQLTYPE',VERSIONS=>10}
  2. COLUMN CELL
  3. cf1:SQLTYPE timestamp=1498445308420, value=D
  4. cf1:SQLTYPE timestamp=1498445301336, value=I

其中,两条记录的时间戳换算过来正好相差了7秒

2017-06-26 10:48:21 I

2017-06-26 10:48:28 D

很明显这两条数据并没有在同一个批次得到处理,很明显Spark获取到数据的先后顺序出了点问题。

二、原因排查

2.1 SparkStreaming程序排查

首先SparkStream接收到数据后根据数据的pos排序,然后再根据主键排序。从现象看,是SparkStreaming分了两个批次才拿到,而SparkStreaming从Kafka拿数据也是顺序拿的。那么出现问题的可能性就只有两个:

1、OGG发给Kafka的数据顺序是错误的。

2、OGG发给Kafka的数据顺序是正确的,但是发到了不同的Kafka Partition。

2.2 Kafka数据验证

为了验证上面的两个猜想,我把kafka的数据再次获取出来进行分析。重点分析数据的partition、key、value。

得到的结果如下:

可以看到数据的同一个表数据写到了不同的分区,可以看到OGG的同一分区下的数据顺序是正确的。

正好说明2.1里面的第二个猜想。看来是OGG写入的时候并没有按照数据的表名写入不同的分区。

在OGG 文档

http://docs.oracle.com/goldengate/bd1221/gg-bd/GADBD/GUID-2561CA12-9BAC-454B-A2E3-2D36C5C60EE5.htm#GADBD449

中的 5.1.4 Kafka Handler Configuration 的属性 gg.handler.kafkahandler.ProducerRecordClass 里面提到了,默认使用的是oracle.goldengate.handler.kafka.DefaultProducerRecord这个类对表名进行分区的。如果要自定义的话需要实现CreateProducerRecord这个接口

原话是 The unit of data in Kafka - a ProducerRecord holds the key field with the value representing the payload. This key is used for partitioning a Kafka Producer record that holds change capture data. By default, the fully qualified table name is used to partition the records. In order to change this key or behavior, theCreateProducerRecord Kafka Handler Interface needs to be implemented and this property needs to be set to point to the fully qualified name of the custom ProducerRecord class.

然而写入kafka的结果却不是这样子的。这点让人费解。看来我们需要查看OGG的源代码。

2.3 查看OGG源码

在OGG的安装包里面有一个名叫ggjava/resources/lib/ggkafka-****.jar的文件,我们将其导入一个工程之后就可以直接看到它的源代码了。

2.3.1 生成Kafka消息类

我们直接查看oracle.goldengate.handler.kafka.DefaultProducerRecord这个类

  1. public class DefaultProducerRecord implements CreateProducerRecord {
  2. public DefaultProducerRecord() {
  3. }
  4. public ProducerRecord createProducerRecord(String topicName, Tx transaction, Op operation, byte[] data, TxOpMode handlerMode) {
  5. ProducerRecord pr;
  6. if(handlerMode.isOperationMode()) {
  7. pr = new ProducerRecord(topicName, operation.getTableName().getOriginalName().getBytes(), data);
  8. } else {
  9. pr = new ProducerRecord(topicName, (Object)null, data);
  10. }
  11. return pr;
  12. }
  13. }

这个类只返回一个ProducerRecord,这个是用于发送给Kafka的一条消息。我们先不管这个,继续看他是如何写给kafka的

2.3.2 Kafka配置类

首先是OGG与Kafka相关的配置类 oracle.goldengate.handler.kafka.impl.KafkaProperties 。这个类里面定义了一堆参数,我们只需要关心partitioner.class这个参数,该参数用于定义写入Kafka的时候获取分区的类。很遗憾,这个类没有该参数配置。

2.3.3 Kafka 消息发送类

这里有一个抽象类oracle.goldengate.handler.kafka.impl.AbstractKafkaProducer,他有两个子类,分别叫BlockingKafkaProducerNonBlockingKafkaProducer (默认是NonBlockingKafkaProducer)

这两个类都是直接将通过producer对象将record发送给了kafka。因此想要指导Kafka的分区信息还需要看Kafka是怎么获取分区的。

2.3.4 Kafka 分区获取方式

进入kafka的producer发送record的函数

  1. public Future<RecordMetadata> send(ProducerRecord<K, V> record) {
  2. return send(record, null);
  3. }
  4. public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
  5. // intercept the record, which can be potentially modified; this method does not throw exceptions
  6. ProducerRecord<K, V> interceptedRecord = this.interceptors == null ? record : this.interceptors.onSend(record);
  7. return doSend(interceptedRecord, callback);
  8. }

发送的方法在doSend里面,里面内容很多,请看我勾出来的这两段

由于写入的时候都没有对Record指定分区,因此这段代码的partition都为空。所以代码总会执行到 this.partitioner.partition(record.topic(), record.key(), serializedKey,record.value(), serializedValue,cluster)

该函数是kafka的Partitioner这个抽象类里面的

由于2.3.2 Kafka配置类中没有指定分区的class,因此只会使用Kafka默认的分区类org.apache.kafka.clients.producer.internals.DefaultPartitioner

private final AtomicInteger counter = new AtomicInteger(new Random().nextInt());

这里先是获取了一个随机值,然后再获取了Kafka中对应topic的可用分区列表,然后根据分区数和随机值进行取余得到分区数的值。

流程走到这里,我们基本可以得到一个结论。

  • Kafka的record指定了分区,则会使用指定的分区写入;否则进行下一个判断;
  • Kafka根据自己定义的partitioner接口进行分区,如果没指定类,则使用默认的分区则进行下一个判断;
  • Kafka获取record中的key进行分区,如果key不为空,则使用Hash分区,如果为空,基本上就是随机分配分区了。

三、结论

事情到了这里,我们可以断定,写入分区错乱的问题是因为gg.handler.kafkahandler.Mode是事务模式,导致多条消息一次发送了,无法使用表名作为key,OGG就用了null作为key发送给了Kafka,最终Kafka拿到空值之后只能随机发送给某个partition,所以才会出现这样的问题。

最终,修改了ogg的操作模式之后可以看到,写入的分区正常了。

记一次OGG数据写入HBase的丢失数据原因分析的更多相关文章

  1. 简单通过java的socket&serversocket以及多线程技术实现多客户端的数据的传输,并将数据写入hbase中

    业务需求说明,由于公司数据中心处于刚开始部署的阶段,这需要涉及其它部分将数据全部汇总到数据中心,这实现的方式是同上传json文件,通过采用socket&serversocket实现传输. 其中 ...

  2. json数据写入hbase

    package main.scala.com.web.zhangyong168.cn.spark.java; import org.apache.hadoop.hbase.HBaseConfigura ...

  3. flink-----实时项目---day07-----1.Flink的checkpoint原理分析 2. 自定义两阶段提交sink(MySQL) 3 将数据写入Hbase(使用幂等性结合at least Once实现精确一次性语义) 4 ProtoBuf

    1.Flink中exactly once实现原理分析 生产者从kafka拉取数据以及消费者往kafka写数据都需要保证exactly once.目前flink中支持exactly once的sourc ...

  4. 大数据学习day34---spark14------1 redis的事务(pipeline)测试 ,2. 利用redis的pipeline实现数据统计的exactlyonce ,3 SparkStreaming中数据写入Hbase实现ExactlyOnce, 4.Spark StandAlone的执行模式,5 spark on yarn

    1 redis的事务(pipeline)测试 Redis本身对数据进行操作,单条命令是原子性的,但事务不保证原子性,且没有回滚.事务中任何命令执行失败,其余的命令仍会被执行,将Redis的多个操作放到 ...

  5. Spark Streaming实时写入数据到HBase

    一.概述 在实时应用之中,难免会遇到往NoSql数据如HBase中写入数据的情景.题主在工作中遇到如下情景,需要实时查询某个设备ID对应的账号ID数量.踩过的坑也挺多,举其中之一,如一开始选择使用NE ...

  6. Flink 使用(一)——从kafka中读取数据写入到HBASE中

    1.前言 本文是在<如何计算实时热门商品>[1]一文上做的扩展,仅在功能上验证了利用Flink消费Kafka数据,把处理后的数据写入到HBase的流程,其具体性能未做调优.此外,文中并未就 ...

  7. java实现服务端守护进程来监听客户端通过上传json文件写数据到hbase中

    1.项目介绍: 由于大数据部门涉及到其他部门将数据传到数据中心,大部分公司采用的方式是用json文件的方式传输,因此就需要编写服务端和客户端的小程序了.而我主要实现服务端的代码,也有相应的客户端的测试 ...

  8. Spark DataFrame写入HBase的常用方式

    Spark是目前最流行的分布式计算框架,而HBase则是在HDFS之上的列式分布式存储引擎,基于Spark做离线或者实时计算,数据结果保存在HBase中是目前很流行的做法.例如用户画像.单品画像.推荐 ...

  9. 【乱码】运行java -jar xx.jar存到hbase里的数据乱码

    程序在Eclipse里运行没有问题,但是打成jar包之后写入hbase里的数据会有乱码,ES里正常 经过测试,运行命令里加上-Dfile.encoding=utf-8 就可以正常写入,但是cmd命令里 ...

随机推荐

  1. iOS学习笔记11-多线程入门

    一.iOS多线程 iOS多线程开发有三种方式: NSThread NSOperation GCD iOS在每个进程启动后都会创建一个主线程,更新UI要在主线程上,所以也称为UI线程,是其他线程的父线程 ...

  2. 刷题总结——spoj1812(后缀自动机+DP)

    题目: A string is finite sequence of characters over a non-empty finite set Σ. In this problem, Σ is t ...

  3. Spoj-VISIBLEBOX Decreasing Number of Visible Box

    Shadowman loves to collect box but his roommates woogieman and itman don't like box and so shadowman ...

  4. 在App_Data中创建数据库获取连接串简便方法!

    原文发布时间为:2008-07-25 -- 来源于本人的百度文章 [由搬家工具导入] 1、在App_Data右击添加一个SQL数据库2、双击该数据库,在左边添加表,并显示表数据进行添加数据3、把刚刚创 ...

  5. My97DatePicker 时间控件

    <td> <input type="text" id="sendDate" name="sendDate" class=& ...

  6. StringBuffer笔记

    简要的说, String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 Strin ...

  7. Day 5 Linux之用户、群组和权限

    Linux之用户.群组和权限 一.各文件及内容对应含义 1./etc/passwd文件 功能:存储所有用户的相关信息,该文件也被称为用户信息数据库(Database). 含义:如下图所示. 2./et ...

  8. WEB学习-基础知识:列表、表单、div和span、注释、字符实体、HTML废弃标签介绍

    列表 无序列表 无序列表,用来表示一个列表的语义,并且每个项目和每个项目之间,是不分先后的. ul就是英语unordered list,“无序列表”的意思. li 就是英语list item , “列 ...

  9. HDU 4920 Matrix multiplication(bitset优化)

    题目链接 Matrix multiplication 求矩阵A和B相乘的结果. 因为答案只要对3取模,所以我们可以通过一些方法来加速计算. 我们对两个矩阵各开两个bitset,分别存储模3余1和模3余 ...

  10. Django实现的博客系统中使用富文本编辑器ckeditor

    操作系统为OS X 10.9.2,Django为1.6.5. 1.下载和安装 1.1 安装 ckeditor 下载地址 https://github.com/shaunsephton/django-c ...