Kafka原理+安装+Linux命令及Java代码分别模拟实现生产者消费者+整合Spark Streaming+自主管理偏移量Offset-Redis/Mysql

1. Kafka

1.1 什么是Kafka

  • Apache Kafka是分布式发布-订阅消息系统(消息中间件)。它是一种快速、可扩展的、设计内在就是分布式的,分区的和可复制的提交日志服务。
  • Kafka是一个分布式的、支持分区的(partition)、多副本(replica),基于zookeeper协调的分布式消息系统,它的最大的特性就是可以实时的处理大量数据以满足各种需求场景:eg:
    基于Hadoop的批处理系统、低延迟的实时系统、storm/spark流式处理引擎、web/nginx日志、访问日志、消息服务等等,使用是scala语言编写的。
    简单说明:
    举个例子,生产者消费者,生产者生产鸡蛋,消费者消费鸡蛋,生产者生产一个鸡蛋,消费者就消费一个鸡蛋,假设消费者消费鸡蛋的时候噎住了(系统宕机了),生产者还在生产鸡蛋,那新生产的鸡蛋就丢失了。再比如生产者很强劲(大交易量的情况),生产者1秒钟生产100个鸡蛋,消费者1秒钟只能吃50个鸡蛋,那要不了一会,消费者就吃不消了(消息堵塞,最终导致系统超时),消费者拒绝再吃了,”鸡蛋“又丢失了,这个时候我们放个篮子在它们中间,生产出来的鸡蛋都放到篮子里,消费者去篮子里拿鸡蛋,这样鸡蛋就不会丢失了,都在篮子里,而这个篮子就是”Kafka“。
    鸡蛋其实就是“数据流”,系统之间的交互都是通过“数据流”来传输的(就是tcp、http什么的),也称为报文,也叫“消息”。

1.2 Kafka的优点:

  • 它是分布式系统,易于向外扩展
  • 它同时为发布和订阅提供高吞吐量
  • 它支持多订阅者,当失败时能自动平衡消费者
  • 它将消息持久化到磁盘,因此可以用于批量消费,eg:ETL , 以及实时应用程序, 容错

1.3 Kafka的特性

  • 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,他的延迟最低只有几毫秒,每个topic可以分为多个partition、consumer、group对partition进行consume操作
  • 可扩展性:kafka集群支持热扩展
  • 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份以防止数据丢失
  • 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败)
  • 高并发:支持数千个客户端同时读写

1.4 Kafka的使用场景

  • 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等
  • 消息系统:解耦和生产者和消费者、缓存消息等。
  • 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘
  • 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告
  • 流式处理:比如spark streaming和storm
  • 事件源

1.5 Kafka 的术语及解释

  • Broker: Kafka集群包含一个或多个服务器,这种服务器被称为broker
  • Topic: 每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)
  • Partition:Partition是物理上的概念,每个Topic包含一个或多个Partition
  • Producer:负责发布消息到Kafka broker
  • Consumer:消息消费者,向Kafka broker读取消息的客户端
  • Consumer Group:每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)
  • replica: partition 的副本,保障 partition 的高可用
  • leader: replica 中的一个角色, producer 和 consumer 只跟 leader 交互
  • follower: replica 中的一个角色,从 leader 中复制数据
  • controller: Kafka 集群中的其中一个服务器,用来进行 leader election 以及各种 failover

小白理解:

  • producer:生产者,就是它来生产“鸡蛋”的
  • consumer:消费者,生出的“鸡蛋”它来消费
  • topic:把它理解为标签,生产者每生产出来一个鸡蛋就贴上一个标签(topic),消费者可不是谁生产的“鸡蛋”都吃的,这样不同的生产者生产出来的“鸡蛋”,消费者就可以选择性的“吃”了
  • broker:就是篮子了
  • 如果从技术角度,topic标签实际就是队列,生产者把所有“鸡蛋(消息)”都放到对应的队列里了,消费者到指定的队列里取

2. Kafka的安装、启动、测试

2.1 安装Kafka

1. 下载kafka的安装包
Apache kafka 官方: http://kafka.apache.org/downloads.html
Scala 2.11 - kafka_2.11-0.10.2.0.tgz (asc, md5)
2. 解压

tar -zxvf kafka_2.11-0.10.2.1.tgz -C /apps/

3. 修改配置文件

vi server.properties
broker.id=0    //为依次增长的:0、1、2、3、4,集群中唯一id 
delete.topic.enable=true #删除主题的配置,默认是false
listeners=PLAINTEXT://kk-01:9092   # 监听的主机及端口号
log.dirs=/kafkaData/logs // Kafka的消息数据存储路径
num.partitions=3  #创建主题的时候,默认有3个分区
zookeeper.connect=master:2181,slave1:2181,slave2:2181 //zookeeperServers列表,各节点以逗号分开

4. 将Kafka server.properties 文件拷贝到其他节点机器

KAFKA_HOME/config>scp server.properties xx:$PWD

5. Kafka集群的环境准备:

  1. 安装JDK,配置JAVA_HOME
  2. 搭建zookeeper集群

2.2 启动Kafka

  1. 先启动zookeeper集群
bin/zkServer.sh start conf/zoo.cfg

2 再启动Kafka集群:

bin/kafka-server-server-start.sh [-daemon] config/server.properties
//-daemon:后台程序运行(添加守护进程)

2.3 测试kafka集群

  1. 进入kafka根目录,创建Topic名称为test的主题
bin/kafka-topic.sh –create –zookepper 192.168.8.11:2181 –replication-factor 3 –partition 2 –topic test
  1. 列出已经创建的topic列表
bin/kafka-topics.sh –list –zookeeper localhost:2181 –topic test
  1. 查看topic的详细信息:
bin/kafka –topics.sh –describe –zookeeper localhost:2181 –topic test
Topic:test       PartitionCount:1        ReplicationFactor:3     Configs:
     			Topic: test      Partition: 0    Leader: 1       Replicas: 1,2,0 Isr: 1,2,0

说明:第一行是对所有分区的一个描述。每个分区对应一行,因为只有一个分区所以下面只有一行。
Leader:负责处理消息的读和写,leader是从所有节点中随机选取的
Replica:列出了所有的副本节点,不管节点是否在服务中
Iisr:正在服务的节点

2.4 kafka中的生产者与消费者:

  1. 生产者—发送消息
bin/kafka-console-producer.sh –broker-list kafka集群中所有节点的IP:9092  --topic test
  1. 消费者—读取消息
bin/kafka-console-consumer.sh –bootstrap-server 生产者的IP地址:9092 –from-beginning (从最早位置开始消费)

2.5 kafka集群的容错能力
2.51 故障转移

	Kill -9 pid[leader节点

另外一个节点被选做了leader,node 1 不再出现在 in-sync 副本列表中:

bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic test
Topic:test       PartitionCount:1        ReplicationFactor:3     Configs:
        Topic: test      Partition: 0    Leader: 2       Replicas: 1,2,0 Isr: 2,0

虽然最初负责续写消息的leader down掉了,但之前的消息还是可以消费的:

bin/kafka-console-consumer.sh --zookeeper localhost:2181 --from-beginning --topic test2

2.52 负载均衡
等down掉的leader重新启动了,consumer还是会把它重新设置为leader。实现了负载均衡,以防止其他节点处理的数据量太大。

3 Kafka的客户端开发(使用java语言开发)

添加pom依赖:

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka_2.11</artifactId>
    <version>0.10.2.0</version>
</dependency>

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>0.10.2.0</version>
</dependency>

3.1 Producer生产者

//生产者客户端--往kafka发送数据
public class ProducerClient {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.setProperty("bootstrap.servers","" +
                "hdp-01:9092,hdp-02:9092,hdp-03:9092");
        //0是不获取反馈(消息有可能传输失败)
        //1是获取消息传递给leader后反馈(其他副本有可能接受消息失败)
        //-1 | all是所有in-sync replicas接受到消息时的反馈
props.put("acks", "all");
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        KafkaProducer<String,String> kafkaProducer = new KafkaProducer<String,String>(props);
        for(int i=0;i<100;i++){
            ProducerRecord<String, String> record = new ProducerRecord<>("atopic", UUID.randomUUID().toString(), "hello hello");
            kafkaProducer.send(record);
            System.out.println("发送:"+i+"条信息:"+record.topic());
        }
        kafkaProducer.flush();
        kafkaProducer.close();
    }
}

3.2 ConsumerClient-消费者

//消费者客户端--接受数据
public class ConsumerClient {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.setProperty("bootstrap.servers","hdp-01:9092,hdp-02:9092,hdp-03:9092");
        props.put("group.id", "test");
        props.put("enable.auto.commit", "true");
        props.put("auto.commit.interval.ms", "1000");
        props.put("session.timeout.ms", "30000");
        props.put("auto.offset.reset", "earliest");
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(props);
        kafkaConsumer.subscribe(Arrays.asList("atopic"));
        ConsumerRecords<String, String> records = kafkaConsumer.poll(3000);
        Iterator<ConsumerRecord<String, String>> iterator = records.iterator();
        while (iterator.hasNext()){
            ConsumerRecord<String, String> next = iterator.next();
            System.out.println("读取next:"+next);
        }
    }
}

4 Kafka原理

4.1 Kafka的拓扑结构

说明:如图所示-一个典型的kafka集群中包含若干Producer、若干Broker(kafka支持水平扩展,一般broker数量越多,集群吞吐率越高),若干Consumer Group以及一个zookeeper集群。通过zookeeper管理集群配置,选举leader。Producer使用push模式将消息发布到broker,consumer使用pull模式从broker订阅并消费消息。

4.2 Zookeeper节点

4.3 Producer发布消息

  • Producer采用push模式将消息发布到broker,每条消息都被append到partition中,属于顺序写入磁盘
  • Producer 发送消息到broker时,会根据分区算法选择将其存储到哪一个partition:
    1. 指定了partition,则直接使用
    2. 未指定partition,但指定了key,通过对key的value进行hash,选出一个partition
    3. Partition和key都未指定,使用轮询选出一个partition

4.4 写数据流程

  • Producer先从zookeeper的“/brokers/…/state”节点找到该partition的leader
  • Producer将消息发送给该leader
  • Leader将消息写入本地的log
  • Followers从leader里pull消息,写入本地的log后发送ACK
  • Leader收到所有的ISR(in-sync replicas)中的ACK后向producer发送ACK

4.5 Broker存储消息
4.41 消息存储方式
4.42 消息存储策略
无论消息是否被消费,kafka都会保留所有消息。有两种策略可以删除就数据:
Log.retention.hours=168 #基于时间
Log.retention.bytes=1073741824 #基于大小

4.6 Kafka log的存储解析
Partition中每条Message由offset来表示它在这个partition中的偏移量,这个offset不改是message在partition数据文件中的实际存储位置,而逻辑上一个值,它唯一确定了partition中的每一条message。因此:可以认为offset是partition中message的ID。Partition 中每条message包含了以下三个属性:
1. Offset
2. MessageSize
3. Data
其中,offset为long类型,messageSize为int32,表示data的大小,data为massage的具体内容
思考:
如果一个partition只有一个数据文件会怎么样?

- 新数据添加在文件末尾,不论文件数据文件有多大,这个操作永远都是高效的
- 查找某个offset的message是顺序查找的。因此:如果数据文件很大的话,查找的效率就很低

Kafka解决查找效率的2种方案:分段 + 索引

  1. 数据文件的分段
    Kafka解决高效查询效率的手段之一是将数据文件分段:eg:100条Message,它们的offset是从0到99。假设将数据文件分成5段:第一段是:0-19,第二段是:20-39,以此类推。每段放在一个单独的数据文件里,数据文件以该段中最小的offset命名。这样在查找指定的offset的message的时候,用二分查找就可以定位到该message在哪个段中。
  2. 为数据文件建立索引
    数据文件分段使得可以在一个较小的数据文件中查找对应offset的message了,但这依然是需要顺序扫描文件才能找到对应offset的message。为了进一步提高查询的效率,kafka为每个分段后的数据文件建立了索引文件,文件名与数据文件是一样的,只是文件的扩展名是:.index
    索引文件中包含若干个索引条目,每个条目表示数据文件中一条message的索引。索引包含两不跟:相对offset 和 position
  • 相对offset:因为数据文件分段后,每个分区的数据文件的起始offset不为0,相对offset表示这条message相对其所属数据文件中最小的offset的大小。Eg: 分段后的一个数据文件的offset是从20开始,那么offset为25的message在index文件中的相对offset就是25-20= 5。存储相对的offset可以减少索引文件占用的空间。
  • Position:表示该条message在数据文件中的绝对位置。只要打开文件并移动文件指针到这个position就可以读取相应的message了

Index文件中并没有为数据文件的每条message建立索引,而是采用了稀疏存储的方式,每隔一定字节的数据建立一条索引。这样也避免了索引文件占用太多空间,从而可以将索引文件保留在内存中。但是缺点就是没有建立索引的massage也不能一次定位到其所在数据文件的位置,从而需要做一次顺序扫描,但这次顺序扫描的范围就很小了。

4.7 总结:
4.7.1 kafka存储message
Message是按照topic来组织,每个topic可以分成多个partition。Eg:有5个partition的名为page_visits的topic的目录结构:
Partition是分段的,每个段叫segment,包括了一个数据文件和一个索引文件:
如图,该partition有4个segment

4.7.2 Kafka如何查找message

Eg: 查找绝对offset为7 的message:
首先二分查找确定了他是在哪个logsegment中,自然是在第一个segment;
打开这个segment的index文件,也是用二分查找找到offset小于或者等于指定offset的索引条目中最大的那个offset。自然offset为6的那个索引是我们要找的,通过索引文件知道offset为6的message在数据文件中的位置为9807
打开这个数据文件,从9807的位置开始顺序扫描直到找到offset为7的那条massage。

这套机制是建立在offset是有序的,索引文件被映射到内存中,所以查找的速度还是很快的。

总而言之:kafka的message存储采用了分区(partition),分段(LogSegment和稀疏索引这个手段来达到了高效性。

5 Kafka和Sparkstreming的整合

添加jar包:

<!--导入kafka和SparkStreaming整合的jar包-->
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
    <version>2.2.0</version>
</dependency>

生产者随机生成a~z个单词,发送到kafka的atopic主题下:

//kafka和Streaming整合的生产者:随机发送单词
public class StreamingKafkaProducer {
    public static void main(String[] args) throws InterruptedException {
        Properties props = new Properties();
        props.setProperty("bootstrap.servers",
                "hdp-01:9092,hdp-02:9092,hdp-03:9092");
        props.put("acks", "all");
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        KafkaProducer<String, String> stringKafkaProducer = new KafkaProducer<>(props);
        while (true){
            int index = new Random().nextInt(26);
            char word= (char) (index+97);
            ProducerRecord<String, String> record = new ProducerRecord<String, String>("atopic",UUID.randomUUID().toString(),String.valueOf(word));
            Future<RecordMetadata> send = stringKafkaProducer.send(record);
            System.out.println("word:"+word);
            Thread.sleep(1000);
        }
    }
}

进行Wordcount:

//Kafka和SparkStreaming整合做WordCOunt
object KafksStreamingWC {
  def main(args: Array[String]): Unit = {

    val conf: SparkConf = new SparkConf()
      .setAppName("Kafka和SparkStreaming整合做WordCOunt")
      .setMaster("local[*]")
    val ssc: StreamingContext = new StreamingContext(conf,Seconds(2))
        val topics: Array[String] = Array("atopic")
        var groupId="song"
        val kafkaParams: Map[String, Object] = Map[String, Object](
    "bootstrap.servers" -> "hdp-01:9092,hdp-02:9092,hdp-03:9092",
    "group.id" -> groupId,
    "enable.auto.commit" -> "true", // 不记录G9529这个consumer消费的数据信息
    "auto.offset.reset" -> "earliest",
    "key.deserializer" -> classOf[StringDeserializer],
    "value.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer"
        )

从kafka中获取数据

val stream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(
      ssc,
      LocationStrategies.PreferConsistent,      ConsumerStrategies.Subscribe(topics.split(","),kafkaParams,OffsetHandlerMySql.findCurrentOffset(topics.split(","),groupId)))

    //将stream转成RDD进行处理
    stream.foreachRDD(rdd=>{
      //业务逻辑---wordCount
   val wordCount: RDD[(String, Int)] = rdd.flatMap(_.value().split(" ")).map((_,1)).reduceByKey(_+_)
      val conn: Connection = OffsetHandlerMySql.getConn()
      //存储当前批次的偏移量--存入Mysql
      OffsetHandlerMySql.saveCurrentOffset(ranges,groupId,conn)
      //写入Redis数据库
       wordCount.foreachPartition(partition=>{
        val jedis = RedisPools.getRedis()
        partition.foreach(wc=>{
          jedis.hincrBy("wordcount",wc._1,wc._2)
        })
        jedis.close()
      })*/
    })
    ssc.start()
    ssc.awaitTermination()
  }
}

不自动提交offset偏移量,把偏移量存入数据库中,再从数据库中读取:
1. Redis数据库

object OffsetHandler {
  //保存当前偏移量
  def saveCurrentOffset(offSetRanges:Array[OffsetRange],groupId:String)={
    //1.获取redis数据库连接
    val jedis: Jedis = RedisPools.getRedis()
    offSetRanges.foreach(os=>{
      jedis.hset(os.topic+"-"+groupId,os.partition.toString,os.untilOffset.toString)
    })
  }
  //读取当前的偏移量
  def findCurrentOffset(topics:Array[String],groupId:String)={
    //1.获取redis数据库连接
    val jedis: Jedis = RedisPools.getRedis()
    val jmap: util.Map[String, String] = jedis.hgetAll(topics.head+"-"+groupId)
    jmap.asScala.map(tp=>(new TopicPartition(topics.head,tp._1.toInt),tp._2.toLong))
  }
}

2. Mysql数据库

//基于mysql操作偏移量
object OffsetHandlerMySql {
  //获取数据库连接
  def getConn()={
    DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/mysql?characterEncoding=utf-8",
    "root", "admin"
    )
  }
  //保存当前偏移量
  def saveCurrentOffset(offSetRanges:Array[OffsetRange],groupId:String,conn:Connection)={
    //1.获取MySql数据库连接
 /*   val url="jdbc:mysql://localhost:3306/mysql?characterEncoding=utf-8"
    val conn: Connection = DriverManager.getConnection(url,"root","admin")
*/
 val psmt: PreparedStatement = conn.prepareStatement("replace into offset_manager values (?,?,?,?)")
    offSetRanges.foreach(os=>{
      //往mysql数据库里进行插入
      //jedis.hset(os.topic+"-"+groupId,os.partition.toString,os.untilOffset.toString)
      //wordcount表结构:topic、partition、groupId、offset
      psmt.setString(1,os.topic)
      psmt.setInt(2,os.partition)
      psmt.setInt(3,os.untilOffset.toInt                             )
      psmt.setString(4,groupId)
      psmt.executeUpdate()
    })
    if (null != psmt) psmt.close()
    if (null != conn) conn.close()
  }

读取当前的偏移量

  def findCurrentOffset(topics:Array[String],groupId:String)={
    //定义一个map存储和封装数据库中的数据
    val offsets: mutable.Map[TopicPartition, Long] = mutable.Map[TopicPartition,Long]()
    //1.获取MySql数据库连接
    val conn: Connection = getConn()
    //执行查询
    val stmt: Statement = conn.createStatement()
    val rs=stmt.executeQuery(s"select * from offset_manager where groupId= '${groupId}' and topic='${topics.head}'")
    while(rs.next()){
      val pId: Int = rs.getInt("partitionId")
      val offset: Int = rs.getInt("offset")
      //封装数据
      val topicPartition = new TopicPartition(topics.head,pId)
      offsets.put(topicPartition,offset.toLong)
    }
    //释放资源
    if(null != rs) rs.close()
    if(null != stmt) stmt.close()
    if(null != conn) conn.close()
    offsets
  /*  psmt.setString(1,)
    jmap.asScala.map(tp=>(new TopicPartition(topics.head,tp._1.toInt),tp._2.toLong))*/
  }
}


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值