初談Kafka

FuyunWang發表於2018-02-22

Kafka是一個分散式的、可分割槽的、可複製的、基於釋出/訂閱的訊息系統,Kafka主要用於大資料領域,當然在分散式系統中也有應用。目前市面上流行的訊息佇列RocketMQ就是阿里借鑑Kafka的原理、用Java開發而得。

Kafka適合離線和線上的訊息消費,其訊息儲存在磁碟上。

Kafka以Topic為單位進行訊息的歸納,Producers向Topic傳送(Push)訊息,Consumers會消費(Pull)預訂了Topic的訊息。

基本概念

訊息佇列中的基本概念尤為重要,當對基本概念有了深入的理解之後,訊息佇列的原理以及常見的問題都將更淺顯明瞭。

  1. Broker:一個單獨的Kafka server就是一個Broker,Broker的主要工作就是接收生產者傳送來的訊息,分配offset,然後將包裝過的資料儲存到磁碟上;此外,Broker還會接收消費者和其他Broker的請求,根據請求的型別進行相應的處理然後返回響應。多個Broker可以做成一個Cluster(叢集)對外提供服務,每個Cluster當中會選出一個Broker來擔任Controller,Controller是Kafka叢集的指揮中心,其他的Broker則聽從Controller指揮實現相應的功能。Controller負責管理分割槽的狀態、管理每個分割槽的副本狀態、監聽zookeeper中資料的變化等。Controller也是一主多從的實現,所有的Broker都會監聽Controller Leader的狀態,當Leader Controller出現了故障的時候就重新選舉新的Controller Leader。

  2. 訊息:訊息是Kafka中最基本的訊息單元。訊息由一串位元組組成,其中主要由key和value構成,key和value都是位元組陣列。key的主要作用是根據一定的策略,將這個訊息路由到指定的分割槽中,這樣就可以保證包含同一個key的訊息全部寫入同一個分割槽

  3. Topic:Topic是用於儲存訊息的邏輯概念,Topic可以看做是一個訊息的集合。每個Topic可以有多個生產者向其中push訊息,也可以有多個消費者向其中pull訊息。

  4. 分割槽(partition):每一個Topic都可以劃分成多個分割槽(每一個Topic都至少有一個分割槽),不同的分割槽會分配在不同的Broker上以對Kafka進行水平擴充套件從而增加Kafka的並行處理能力。同一個Topic下的不同分割槽包含的訊息是不同的。每一個訊息在被新增到分割槽的時候,都會被分配一個offset,他是訊息在此分割槽中的唯一編號,此外,Kafka通過offset保證訊息在分割槽中的順序,offset的順序性不跨分割槽,也就是說在Kafka的同一個分割槽中的訊息是有序的,不同分割槽的訊息可能不是有序的。 Partitions概念圖

    Aaron Swartz

  5. Log:分割槽在邏輯上對應著一個Log,當生產者將訊息寫入分割槽的時候,實際上就是寫入到了一個Log中。Log是一個邏輯概念,對應的是一個磁碟上的資料夾。Log由多個Segment組成,每一個Segment又對應著一個日誌檔案和一個索引檔案。

  6. 副本:Kafka對訊息進行了冗餘備份,每一個分割槽都可以有多個副本,每一個副本中包含的訊息是相同的(但不保證同一時刻下完全相同)。副本的型別分為Leader和Follower,當分割槽只有一個副本的時候,該副本屬於Leader,沒有 Follower。Kafka的副本具有一定的同步機制,在每個副本集合中,都會選舉出一個副本作為Leader副本,Kafka在不同的場景中會採用不同的選舉策略。Kafka中所有的讀寫請求都由選舉出的Leader副本處理,其他的都作為Follower副本,Follower副本僅僅是從Leader副本中把資料拉取到本地之後,同步更新到自己的Log中。

    分割槽副本:

    Aaron Swartz

  7. 生產者:生產者主要是生產訊息,並將訊息按照一定的規則推送到Topic的分割槽中

  8. 消費者:消費者主要是從Topic中拉取訊息,並對訊息進行消費。Consumer維護消費者消費者消費到Partition的哪一個位置(offset的值)這一資訊。**在Kafka中,多個Consumer可以組成一個Consumer Group,一個Consumer只能屬於一個Consumer Group。Consumer Group保證其訂閱的Topic中每一個分割槽只被分配給此Consumer Group中的一個消費者處理,所以如果需要實現訊息的廣播消費,則將消費者放在多個不同的Consumer Group中即可實現。**通過向Consumer Group中動態的新增適量的Consumer,可以出發Kafka的Rebalance操作重新分配分割槽與消費者的對應關係,從而實現了水平擴充套件的能力。

  9. ISR集合:ISR集合表示的是目前可用(alive)且訊息量與Leader相差不多的副本集合,即整個副本集合的子集。ISR集合中副本所在的節點都與ZK保持著連線,此外,副本的最後一條訊息的offset與Leader副本的最後一條訊息的offset之間的差值不能超出指定的閾值。每一個分割槽的Leader副本都維護此分割槽的ISR集合。如上面所述,Leader副本進行了訊息的寫請求,Follower副本會從Leader上拉取寫入的訊息,第二個過程中會存在Follower副本中的訊息數量少於Leader副本的狀態,只要差值少於指定的閾值,那麼此時的副本集合就是ISR集合。

基本使用

啟動Kafka

這裡介紹的是單例項的Kafka的啟動安裝:

  1. 官網下載 kafka_2.11-1.0.0.tgz,然後解壓
  2. 啟動Kafka自帶的zk服務, ./bin/windows/zookeeper-server-start.bat ./conf/zookeeper.properties
  3. 啟動Kafka伺服器, ./bin/windows/kafka-server-start.bat ./conf/server.properties

命令列使用Kafka(windows為例,Linux只需要使用對應的.sh結尾的檔案執行命令即可):

  1. 建立一個名字為demo的topic,指定其分割槽數目為1,副本工廠數目為1。 kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic demo
  2. 列出所有的topics: kafka-topics.bat --list --zookeeper localhost:2181
  3. 向指定的topic來傳送訊息,首先進入命令終端: kafka-console-producer.bat --broker-list localhost:9092 --topic demo,然後在命令終端鍵入訊息Hello World!
  4. 指定從訊息佇列的首部開始消費訊息: kafka-console-consumer.bat --zookeeper localhost:2181 --topic demo --from-beginning

Java呼叫API使用Kafka:

		/**
		 * @Name: ProducerDemo
		 * @Description: Kafka服務端進行訊息的Push 
		 * @Author: BeautifulSoup
		 * @Date: 2018年2月1日 下午11:24:39
		 */
		public class ProducerDemo {
			public static void main(String[] args) {
				//構造Kafka的配置項
				Properties properties=new Properties();
				//定義Kafka服務端的主機名和埠號
				properties.put("bootstrap.servers", "localhost:9092");
				//定義客戶端的ID
				properties.put("client.id", "DemoProducer");
				//定義訊息的key和value的資料型別都是位元組陣列
				properties.put("key.serializer","org.apache.kafka.common.serialization.IntegerSerializer");
				properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
				//建立生產者的核心類
				KafkaProducer producer=new KafkaProducer<>(properties);
				//指定topic的名稱
				String topic = "demo";
				//定義訊息的key
				int messageNo=1;
				while(true){
					//定義訊息的value
					String messageStr="Message_"+messageNo;
					long startTime=System.currentTimeMillis();
					//非同步的傳送訊息
					producer.send(new ProducerRecord<>(topic, messageNo,messageStr,new Callback() {
						//訊息傳送成功之後收到了Kafka服務端發來的ACK確認訊息之後,就回撥下面的方法
						//metadata儲存著生產者傳送過來的訊息的後設資料,如果訊息的傳送過程中出現了異常,則改引數的值為null
						@Override
						public void onCompletion(RecordMetadata metadata, Exception exception) {
							long elapsedTime=System.currentTimeMillis()-startTime;
							if(null!=metadata){
								System.out.println("訊息傳送給的分割槽是:"+metadata.partition()+",訊息的傳送一共用了:"+elapsedTime+"ms");
							}else{
								exception.printStackTrace();
							}
						}
					}));
				}
				
			}
		}
	

		/**
		 * @Name: ConsumerDemo
		 * @Description: Kafka客戶端進行訊息的Pull 
		 * @Author: BeautifulSoup
		 * @Date: 2018年2月10日 下午11:24:58
		 */
		public class ConsumerDemo {
			public static void main(String[] args) {
				Properties properties=new Properties();
				properties.put("bootstrap.servers","localhost:9092");
				//指定Consumer Group的id
				properties.put("group.id", "BeautifulSoup");
				//自動提交offset
				properties.put("enable.auto.commit", "true");
				//自動提交offset的時間間隔
				properties.put("auto.commit.interval.ms","1000");
				properties.put("session.timeout.ms", "30000");
				properties.put("key.deserializer","org.apache.kafka.common.serialization.IntegerDeserializer");
				properties.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
				KafkaConsumer consumer=new KafkaConsumer<>(properties);
				//指定消費者訂閱的topic
				consumer.subscribe(Arrays.asList("demo","test"));
				try{
					while(true){
						//從服務端開始拉取訊息,每次的poll都會拉取多個訊息
						ConsumerRecords<String, String> records=consumer.poll(100);
						for (ConsumerRecord<String,String> consumerRecord : records) {
							System.out.println("訊息記錄的位置:"+consumerRecord.offset()+",訊息的鍵:"+consumerRecord.key()+",訊息的值:"+consumerRecord.value());
						}
					}
				}finally{
					//關閉consumer
					consumer.close();
				}
			}
		}
複製程式碼

相關文章