其他更多java基礎文章:
java基礎學習(目錄)
學習資料:kafka資料可靠性深度解讀
Kafka概述
- Kafka是一個分散式訊息佇列。Kafka對訊息儲存時根據Topic進行歸類,傳送訊息者稱為Producer,訊息接受者稱為Consumer,此外kafka叢集有多個kafka例項組成,每個例項(server)成為broker。
- 無論是kafka叢集,還是producer和consumer都依賴於zookeeper叢集儲存一些meta資訊,來保證系統可用性。
- 在流式計算中,Kafka一般用來快取資料,Spark通過消費Kafka的資料進行計算。
Kafka架構
- Producer :訊息生產者,就是向kafka broker發訊息的客戶端。
- Consumer :訊息消費者,向kafka broker取訊息的客戶端
- Topic :可以理解為一個佇列。
- Consumer Group (CG):這是kafka用來實現一個topic訊息的廣播(發給所有的consumer)和單播(發給任意一個consumer)的手段。一個topic可以有多個CG。topic的訊息會複製(不是真的複製,是概念上的)到所有的CG,但每個partion只會把訊息發給該CG中的一個consumer。如果需要實現廣播,只要每個consumer有一個獨立的CG就可以了。要實現單播只要所有的consumer在同一個CG。用CG還可以將consumer進行自由的分組而不需要多次傳送訊息到不同的topic。
- Broker :一臺kafka伺服器就是一個broker。一個叢集由多個broker組成。一個broker可以容納多個topic。
- Partition:為了實現擴充套件性,一個非常大的topic可以分佈到多個broker(即伺服器)上,一個topic可以分為多個partition,每個partition是一個有序的佇列。partition中的每條訊息都會被分配一個有序的id(offset)。kafka只保證按一個partition中的順序將訊息發給consumer,不保證一個topic的整體(多個partition間)的順序。
分割槽
訊息傳送時都被髮送到一個topic,其本質就是一個目錄,而topic是由一些Partition Logs(分割槽日誌)組成,其組織結構如下圖所示:
我們可以看到,每個Partition中的訊息都是有序的,生產的訊息被不斷追加到Partition log上,其中的每一個訊息都被賦予了一個唯一的offset值。
- 分割槽的原因
- 方便在叢集中擴充套件,每個Partition可以通過調整以適應它所在的機器,而一個topic又可以有多個Partition組成,因此整個叢集就可以適應任意大小的資料了;
- 可以提高併發,因為可以以Partition為單位讀寫了。
- 分割槽的原則
- 指定了patition,則直接使用;
- 未指定patition但指定key,通過對key的value進行hash出一個patition
- patition和key都未指定,使用輪詢選出一個patition。
副本(Replication)
同一個partition可能會有多個replication(對應 server.properties 配置中的 default.replication.factor=N)。沒有replication的情況下,一旦broker 當機,其上所有 patition 的資料都不可被消費,同時producer也不能再將資料存於其上的patition。引入replication之後,同一個partition可能會有多個replication,而這時需要在這些replication之間選出一個leader,producer和consumer只與這個leader互動,其它replication作為follower從leader 中複製資料。
寫入流程
- producer先從zookeeper的 "/brokers/.../state"節點找到該partition的leader
- producer將訊息傳送給該leader
- leader將訊息寫入本地log
- followers從leader pull訊息,寫入本地log後向leader傳送ACK
- leader收到所有ISR中的replication的ACK後,增加HW(high watermark,最後commit 的offset)並向producer傳送ACK
ACK,HW,ISR等可以閱讀kafka資料可靠性深度解讀學習
簡單來說:
- HW是HighWatermark的縮寫,是指consumer能夠看到的此partition的位置
- ISR (In-Sync Replicas),這個是指副本同步佇列。所有的副本(replicas)統稱為Assigned Replicas,即AR。ISR是AR中的一個子集,由leader維護ISR列表,follower從leader同步資料有一些延遲(包括延遲時間replica.lag.time.max.ms和延遲條數replica.lag.max.messages兩個維度),任意一個超過閾值都會把follower剔除出ISR, 存入OSR(Outof-Sync Replicas)列表,新加入的follower也會先存放在OSR中。AR=ISR+OSR。
- ack機制:在kafka傳送資料的時候,每次傳送訊息都會有一個確認反饋機制,確保訊息正常的能夠被收到。
Kafka消費過程分析
kafka提供了兩套consumer API:高階Consumer API和低階API。
高階API
- 高階API優點
- 高階API 寫起來簡單
- 不需要去自行去管理offset,系統通過zookeeper自行管理
- 不需要管理分割槽,副本等情況,系統自動管理
- 消費者斷線會自動根據上一次記錄在zookeeper中的offset去接著獲取資料(預設設定1分鐘更新一下zookeeper中存的的offset)
- 可以使用group來區分對同一個topic的不同程式訪問分離開來(不同的group記錄不同的offset,這樣不同程式讀取同一個topic才不會因為offset互相影響)
- 高階API缺點
- 不能自行控制offset(對於某些特殊需求來說)
- 不能細化控制如分割槽、副本、zk等
低階API
- 低階 API 優點
- 能夠開發者自己控制offset,想從哪裡讀取就從哪裡讀取。
- 自行控制連線分割槽,對分割槽自定義進行負載均衡
- 對zookeeper的依賴性降低(如:offset不一定非要靠zk儲存,自行儲存offset即可,比如存在檔案或者記憶體中)
- 低階API缺點
- 太過複雜,需要自行控制offset,連線哪個分割槽,找到分割槽leader 等。
消費者組
消費者是以consumer group消費者組的方式工作,由一個或者多個消費者組成一個組,共同消費一個topic。每個分割槽在同一時間只能由group中的一個消費者讀取,但是多個group可以同時消費這個partition。
在圖中,有一個由三個消費者組成的group,有一個消費者讀取主題中的兩個分割槽,另外兩個分別讀取一個分割槽。某個消費者讀取某個分割槽,也可以叫做某個消費者是某個分割槽的擁有者。 在這種情況下,消費者可以通過水平擴充套件的方式同時讀取大量的訊息。另外,如果一個消費者失敗了,那麼其他的group成員會自動負載均衡讀取之前失敗的消費者讀取的分割槽。JAVA中使用Kafka
生產者
import org.apache.kafka.clients.producer.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
public class KafkaProducerDemo {
public static void main(String[] args){
//test();
test2();
}
public static void test(){
Properties props= new Properties();
props.put("bootstrap.servers", "172.26.40.181:9092");
props.put("acks", "all");
props.put("retries", 0);
props.put("batch.size", 16384);
props.put("linger.ms", 1);
props.put("buffer.memory", 33554432);
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer producer = new KafkaProducer(props);
for(int i = 0; i < 10; i++){
producer.send(new ProducerRecord("first",Integer.toString(i), Integer.toString(i)));
}
producer.close();
}
/**
* 帶回撥函式
*/
public static void test2(){
Properties props = new Properties();
// Kafka服務端的主機名和埠號
props.put("bootstrap.servers", "172.26.40.181:9092");
// 等待所有副本節點的應答
props.put("acks", "all");
// 訊息傳送最大嘗試次數
props.put("retries", 0);
// 一批訊息處理大小
props.put("batch.size", 16384);
// 增加服務端請求延時
props.put("linger.ms", 1);
// 傳送快取區記憶體大小
props.put("buffer.memory", 33554432);
// key序列化
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// value序列化
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//攔截器
List<String> interceptor = new ArrayList<>();
interceptor.add("com.hiway.practice.kafka.TimeInterceptor");
interceptor.add("com.hiway.practice.kafka.CounterInterceptor");
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,interceptor);
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);
for (int i = 0; i < 50; i++) {
kafkaProducer.send(new ProducerRecord<String, String>("first", "hello" + i), new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (metadata != null) {
System.err.println(metadata.partition() + "---" + metadata.offset());
}
}
});
}
kafkaProducer.close();
}
}
複製程式碼
消費者
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.util.Arrays;
import java.util.Properties;
public class CustomNewConsumer {
public static void main(String[] args) {
Properties props = new Properties();
// 定義kakfa 服務的地址,不需要將所有broker指定上
props.put("bootstrap.servers", "172.26.40.181:9092");
// 制定consumer group
props.put("group.id", "test");
// 是否自動確認offset
props.put("enable.auto.commit", "true");
// 自動確認offset的時間間隔
props.put("auto.commit.interval.ms", "1000");
// key的序列化類
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// value的序列化類
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 定義consumer
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
// 消費者訂閱的topic, 可同時訂閱多個
consumer.subscribe(Arrays.asList("first"));
while (true) {
// 讀取資料,讀取超時時間為100ms
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
}
}
複製程式碼
攔截器
public class TimeInterceptor implements ProducerInterceptor<String,String> {
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
return new ProducerRecord<>(record.topic(),record.partition(),record.timestamp(),record.key(),System.currentTimeMillis() + "," + record.value().toString());
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
public class CounterInterceptor implements ProducerInterceptor<String, String> {
private int errorCounter = 0;
private int successCounter = 0;
@Override
public void configure(Map<String, ?> configs) {
}
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
return record;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
// 統計成功和失敗的次數
if (exception == null) {
successCounter++;
} else {
errorCounter++;
}
}
@Override
public void close() {
// 儲存結果
System.out.println("Successful sent: " + successCounter);
System.out.println("Failed sent: " + errorCounter);
}
}
複製程式碼
錯誤總結
1. Uncaught error in kafka producer I/O thread錯誤
這個問題主要是伺服器上的kafka版本和IDEA中的kafka版本不一致導致的。
2.producer傳送資料到叢集上無反應
將kafka/config/server.properties檔案中advertised.listeners改為如下屬性。172.26.40.181是我虛擬機器的IP。改完後重啟,OK了。Java端的程式碼終於能通訊了 advertised.listeners=PLAINTEXT://172.26.40.181:9092 advertised.listeners上的註釋是這樣的:
#Hostname and port the broker will advertise to producers and consumers. If not set,
# it uses the value for "listeners" if configured. Otherwise, it will use the value
# returned from java.net.InetAddress.getCanonicalHostName().
複製程式碼
意思就是說:hostname、port都會廣播給producer、consumer。如果你沒有配置了這個屬性的話,則使用listeners的值,如果listeners的值也沒有配置的話,則使用 java.net.InetAddress.getCanonicalHostName()返回值(這裡也就是返回localhost了)。