爬蟲架構|利用Kafka處理資料推送問題(2)
在前一篇文章爬蟲架構|利用Kafka處理資料推送問題(1)中對Kafka做了一個介紹,以及環境搭建,最後是選擇使用阿里雲的Kafka,這一篇文章繼續說使用阿里雲的Kafka的一些知識。
一、釋出者最佳實踐
釋出的完整程式碼(根據自己的業務做相應處理):
package com.yimian.controller.kafka;
import java.util.Date;
import java.util.Properties;
import java.util.concurrent.Future;
import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.config.SaslConfigs;
import org.apache.kafka.common.config.SslConfigs;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.alibaba.fastjson.JSON;
import com.yimian.model.SpiderData;
/**
* 生產者
*
* @author huangtao
*
*/
@Controller
@RequestMapping(value = "kafka/producer")
public class KafkaProducerController {
private static Producer<String, String> producer;
private static Properties kafkaProperties;
static {
// 設定sasl檔案的路徑
JavaKafkaConfigurer.configureSasl();
// 載入kafka.properties
kafkaProperties = JavaKafkaConfigurer.getKafkaProperties();
Properties props = new Properties();
// 設定接入點,請通過控制檯獲取對應Topic的接入點
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaProperties.getProperty("bootstrap.servers"));
// 設定SSL根證照的路徑,請記得將XXX修改為自己的路徑
// 與sasl路徑類似,該檔案也不能被打包到jar中
props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, kafkaProperties.getProperty("ssl.truststore.location"));
// 根證照store的密碼,保持不變
props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, "KafkaOnsClient");
// 接入協議,目前支援使用SASL_SSL協議接入
props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_SSL");
// SASL鑑權方式,保持不變
props.put(SaslConfigs.SASL_MECHANISM, "ONS");
// Kafka訊息的序列化方式
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
// 請求的最長等待時間
props.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 30 * 1000);
// 構造Producer物件,注意,該物件是執行緒安全的,一般來說,一個程式內一個Producer物件即可;
// 如果想提高效能,可以多構造幾個物件,但不要太多,最好不要超過5個
producer = new KafkaProducer<String, String>(props);
}
/**
* 傳送訊息給kafka
* @param topic
* @param msg
*/
public static void sendMsgToKafka(String topic, SpiderData msg) {
try {
// 傳送訊息,並獲得一個Future物件
Future<RecordMetadata> metadataFuture = producer.send(new ProducerRecord<String, String>(topic, String.valueOf(new Date().getTime()),
JSON.toJSONString(msg)));
// 同步獲得Future物件的結果
RecordMetadata recordMetadata = metadataFuture.get();
System.out.println("Produce ok:" + recordMetadata.toString());
} catch (Exception e) {
/**
* 要考慮重試~
* 在分散式環境下,由於網路等原因,偶爾的傳送失敗是常見的。這種失敗有可能是訊息已經傳送成功,但是 Ack 失敗,也有可能是確實沒傳送成功。
* 訊息佇列 Kafka 是 VIP 網路架構,會主動掐掉空閒連線(一般 30 秒沒活動),也就是說,不是一直活躍的客戶端會經常收到”connection rest by peer”這樣的錯誤,因此建議都考慮重試。
*/
// 參考常見報錯:
// https://help.aliyun.com/document_detail/68168.html?spm=a2c4g.11186623.6.567.2OMgCB
System.out.println("error occurred");
e.printStackTrace();
}
}
@RequestMapping(value = "init", produces = "text/html;charset=UTF-8")
@ResponseBody
public void init() {
// 構造一個Kafka訊息
String topic = kafkaProperties.getProperty("topic"); // 訊息所屬的Topic,請在控制檯申請之後,填寫在這裡
SpiderData data = new SpiderData();
data.setDescUrl("www.baidu.com");
data.setTitle("百度");
sendMsgToKafka(topic, data);
}
}
Kafka的傳送非常簡單,程式碼片段如下:
Future<RecordMetadata> metadataFuture = producer.send(new ProducerRecord<String, String>(
topic, \\ topic
null, \\ 分割槽編號,這裡最好為 null,交給 producer 去分配
System.currentTimeMillis(), \\時間戳
String.valueOf(message.hashCode()), \\ key,可以在控制檯通過這個 Key 查詢訊息,這個 key 最好唯一;
message)); \\ value,訊息內容
message可以是一個JSON型別的物件,如上面例子中的JSON.toJSONString(new SpiderData())
1.1、Key 和 Value
Kafka 0.10.0.0 的訊息欄位只有兩個:Key 和 Value。為了便於追蹤,重要訊息最好都設定一個唯一的 Key。通過 Key 追蹤某訊息,列印傳送日誌和消費日誌,瞭解該訊息的傳送和消費情況;更重要的是,您可以在控制檯可以根據 Key 查詢訊息的內容。
1.2、失敗重試
在分散式環境下,由於網路等原因,偶爾的傳送失敗是常見的。這種失敗有可能是訊息已經傳送成功,但是 Ack 失敗,也有可能是確實沒傳送成功。
訊息佇列 Kafka 是 VIP 網路架構,會主動掐掉空閒連線(一般 30 秒沒活動),也就是說,不是一直活躍的客戶端會經常收到”connection rest by peer”這樣的錯誤,因此建議都考慮重試。
1.3、非同步傳送
需要注意的是這個介面是非同步傳送的;如果你想得到傳送的結果,可以呼叫metadataFuture.get(timeout, TimeUnit.MILLISECONDS)
。
1.4、執行緒安全
Producer 是執行緒安全的,且可以往任何 Topic 傳送訊息。一般一個應用,對應一個 Producer 就足夠了。
1.5、Ack
訊息佇列 Kafka 沒有考慮這個引數,都認為是“all”,即所有訊息同步到 Slave 節點後才會返回成功的確認訊息給客戶端。
1.6、Batch
Batch 的基本思路是:把訊息快取在記憶體中,並進行打包傳送。Kafka 通過 Batch 來提高吞吐,但同時也會增加延遲,生產時應該對兩者予以權衡。
在構建 Producer 時,需要考慮以下兩個引數:
-
batch.size
: 發往每個 Partition 的訊息個數快取量達到這個數值時,就會觸發一次網路請求,把訊息真正發往伺服器; -
linger.ms
: 每個訊息待在快取中的最大時間,超過這個時間,就會忽略batch.size
的限制,立即把訊息發往伺服器。
由此可見,Kafka 什麼時候把訊息真正發往伺服器,是通過上面兩個引數共同決定的;
batch.size 有助於提高吞吐,linger.ms 有助於控制延遲。您可以根據具體業務進行調整。
1.7、OOM
結合 Kafka Batch 的設計思路,Kafka 會快取訊息並打包傳送,如果快取太多,則有可能造成 OOM。
-
buffer.memory
: 所有快取訊息的總體大小超過這個數值後,就會觸發把訊息發往伺服器。此時會忽略batch.size
和linger.ms
的限制。 -
buffer.memory
的預設數值是 32M,對於單個 Producer 來說,可以保證足夠的效能。需要注意的是,如果你在同一個 JVM 中啟動多個 Producer,那麼每個 Producer 都有可能佔用32M 快取空間,此時便有可能觸發 OOM。 - 在生產時,一般沒有必要啟動多個 Producer;如果特殊情況需要,則需要考慮
buffer.memory
的大小,避免觸發 OOM。
1.8、分割槽順序
單個分割槽內,訊息是按照傳送順序儲存的,是基本有序的。
但訊息佇列 Kafka 並不保證單個分割槽內絕對有序,所以在某些情況下,會發生少量訊息亂序。比如:訊息佇列 Kafka 為了提高可用性,某個分割槽掛掉後把訊息 Failover 到其它分割槽。
二、訂閱者最佳實踐
消費的完整程式碼(根據自己的業務做相應處理):
package com.yimian.controller.kafka;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.config.SaslConfigs;
import org.apache.kafka.common.config.SslConfigs;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.yimian.model.SpiderData;
/**
* 消費者
*
* @author huangtao
*
*/
@Controller
@RequestMapping(value = "kafka/consumer")
public class KafkaConsumerController {
private static Consumer<String, String> consumer;
static {
// 設定sasl檔案的路徑
JavaKafkaConfigurer.configureSasl();
// 載入kafka.properties
Properties kafkaProperties = JavaKafkaConfigurer.getKafkaProperties();
Properties props = new Properties();
// 設定接入點,請通過控制檯獲取對應Topic的接入點
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaProperties.getProperty("bootstrap.servers"));
// 設定SSL根證照的路徑,請記得將XXX修改為自己的路徑
// 與sasl路徑類似,該檔案也不能被打包到jar中
props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, kafkaProperties.getProperty("ssl.truststore.location"));
// 根證照store的密碼,保持不變
props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, "KafkaOnsClient");
// 接入協議,目前支援使用SASL_SSL協議接入
props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_SSL");
// SASL鑑權方式,保持不變
props.put(SaslConfigs.SASL_MECHANISM, "ONS");
// 兩次poll之間的最大允許間隔
// 請不要改得太大,伺服器會掐掉空閒連線,不要超過30000
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 25000);
// 每次poll的最大數量
// 注意該值不要改得太大,如果poll太多資料,而不能在下次poll之前消費完,則會觸發一次負載均衡,產生卡頓
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 30);
// 訊息的反序列化方式
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringDeserializer");
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringDeserializer");
// 當前消費例項所屬的消費組,請在控制檯申請之後填寫
// 屬於同一個組的消費例項,會負載消費訊息
props.put(ConsumerConfig.GROUP_ID_CONFIG, kafkaProperties.getProperty("group.id"));
// 構造訊息物件,也即生成一個消費例項
consumer = new KafkaConsumer<String, String>(props);
// 設定消費組訂閱的Topic,可以訂閱多個
// 如果GROUP_ID_CONFIG是一樣,則訂閱的Topic也建議設定成一樣
List<String> subscribedTopics = new ArrayList<String>();
// 如果需要訂閱多個Topic,則在這裡add進去即可
// 每個Topic需要先在控制檯進行建立
subscribedTopics.add(kafkaProperties.getProperty("topic"));
consumer.subscribe(subscribedTopics);
}
@RequestMapping(value = "init", produces = "text/html;charset=UTF-8")
@ResponseBody
public void init() {
// 迴圈消費訊息
while (true) {
try {
ConsumerRecords<String, String> records = consumer.poll(1000);
// 必須在下次poll之前消費完這些資料, 且總耗時不得超過SESSION_TIMEOUT_MS_CONFIG
// 建議開一個單獨的執行緒池來消費訊息,然後非同步返回結果
for (ConsumerRecord<String, String> record : records) {
JSONObject jsonMsg = JSON.parseObject(record.value());
SpiderData spiderData = JSONObject.toJavaObject(jsonMsg, SpiderData.class);
System.out.println(spiderData.toString());
}
} catch (Exception e) {
try {
Thread.sleep(1000);
} catch (Throwable ignore) {
}
// 參考常見報錯:
// https://help.aliyun.com/document_detail/68168.html?spm=a2c4g.11186623.6.567.2OMgCB
e.printStackTrace();
}
}
}
}
消費時把JSON資料反序列化:
for (ConsumerRecord<String, String> record : records) {
JSONObject jsonMsg = JSON.parseObject(record.value());
SpiderData spiderData = JSONObject.toJavaObject(jsonMsg, SpiderData.class);
}
2.1、消費訊息基本流程
Kafka 訂閱者在訂閱訊息時的基本流程是:
- Poll 資料
- 執行消費邏輯
- 再次 poll 資料
2.2、負載消費
每個 Consumer Group 可以包含多個消費例項,也即可以啟動多個 Kafka Consumer,並把引數 group.id
設定成相同的值。屬於同一個 Consumer Group 的消費例項會負載消費訂閱的 topic。
示例1:Consumer Group A 訂閱了 Topic A,並開啟三個消費例項 C1、C2、C3,則傳送到 Topic A 的每條訊息最終只會傳給 C1、C2、C3 的某一個。Kafka 預設會均勻地把訊息傳給各個訊息例項,以做到消費負載均衡。
Kafka 負載消費的內部原理是,把訂閱的 Topic 的分割槽,平均分配給各個消費例項。因此,消費例項的個數不要大於分割槽的數量,否則會有例項分配不到任何分割槽而處於空跑狀態。這個負載均衡發生的時間,除了第一次啟動上線之外,後續消費例項發生重啟、增加、減少等變更時,都會觸發一次負載均衡。
訊息佇列 Kafka 分割槽的數量至少是 16 個,已經足夠滿足大部分使用者的需求,且雲上服務會根據容量調整分割槽數。
2.3、多個訂閱
一個 Consumer Group 可以訂閱多個 Topic。一個 Topic 也可以被多個 Consumer Group 訂閱,且各個 Consumer Group 獨立消費 Topic 下的所有訊息。
示例1:Consumer Group A 訂閱了 Topic A,Consumer Group B 也訂閱了 Topic A,則傳送到 Topic A 的每條訊息,不僅會傳一份給 Consumer Group A 的消費例項,也會傳一份給 Consumer Group B 的消費例項,且這兩個過程相互獨立,相互沒有任何影響。
2.4、消費位點
每個 Topic 會有多個分割槽,每個分割槽會統計當前訊息的總條數,這個稱為最大位點 MaxOffset。Kafka Consumer 會按順序依次消費分割槽內的每條訊息,記錄已經消費了的訊息條數,稱為ConsumerOffset。
剩餘的未消費的條數(也稱為訊息堆積量) = MaxOffset - ConsumerOffset
2.5、位點提交
Kafka 消費者有兩個相關引數:
-
enable.auto.commit
:預設值為 true。 -
auto.commit.interval.ms
: 預設值為 1000,也即 1s。
這兩個引數組合的結果就是,每次 poll 時,再拉取資料前會預先做下面這件事:
- 檢查上次提交位點的時間,如果距離當前時間已經超過 auto.commit.interval.ms,則啟動位點提交動作;
因此,如果 enable.auto.commit
設定為 true,需要在每次 poll 時,確保前一次 poll 出來的資料已經消費完畢,否則可能導致位點跳躍;
如果想自己控制位點提交,則把 enable.auto.commit
設為 false,並呼叫 commit(offsets)
函式自行控制位點提交。
2.6、訊息重複以及消費冪等
Kafka 消費的語義是 “At Lease Once”, 也就是至少投遞一次,保證訊息不丟,但是不會保證訊息不重複。在出現網路問題、客戶端重啟時均有可能出現少量重複訊息,此時應用消費端,如果對訊息重複比較敏感(比如說訂單交易類),則應該做到訊息冪等。
以資料庫類應用為例,常用做法是:
- 傳送訊息時,傳入 key 作為唯一流水號ID;
- 消費訊息時,判斷 key 是否已經消費過,如果已經消費過了,則忽略,如果沒消費過,則消費一次;
當然,如果應用本身對少量訊息重複不敏感,則不需要做此類冪等檢查。
2.7、消費失敗
Kafka 是按分割槽一條一條訊息順序向前消費推進的,如果消費端拿到某條訊息後消費邏輯失敗,比如應用伺服器出現了髒資料,導致某條訊息處理失敗,等待人工干預,該怎麼辦呢?
- 如果失敗後一直嘗試再次執行消費邏輯,則有可能造成消費執行緒阻塞在當前訊息,無法向前推進,造成訊息堆積;
- 由於 Kafka 自身沒有處理失敗訊息的設計,實踐中通常會列印失敗的訊息、或者儲存到某個服務(比如建立一個 Topic 專門用來放失敗的訊息),然後定時 check 失敗訊息的情況,分析失敗原因,根據情況處理。
2.8、消費阻塞以及堆積
消費端最常見的問題就是消費堆積,最常造成堆積的原因是:
- 消費速度跟不上生產速度,此時應該提高消費速度,詳情見本文下一節<提高消費速度>;
- 消費端產生了阻塞;
消費端拿到訊息後,執行消費邏輯,通常會執行一些遠端呼叫,如果這個時候同步等待結果,則有可能造成一直等待,消費程式無法向前推進。
消費端應該竭力避免堵塞消費執行緒,如果存在等待呼叫結果的情況,設定等待的超時時間,超過時間後,作消費失敗處理。
2.9、提高消費速度
提高消費速度有兩個辦法:
- 增加 Consumer 例項個數;
- 增加消費執行緒;
增加 Consumer 例項,可以在程式內直接增加(需要保證每個例項一個執行緒,否則沒有太大意義),也可以部署多個消費例項程式;需要注意的是,例項個數超過分割槽數量後就不再能提高速度,將會有消費例項不工作;
增加 Consumer 例項本質上也是增加執行緒的方式來提升速度,因此更加重要的效能提升方式是增加消費執行緒,最基本的方式如下:
- 定義一個執行緒池;
- poll 資料;
- 把資料提交到執行緒池進行併發處理;
- 等併發結果返回成功再次poll資料執行。
2.10訊息過濾
Kafka 自身沒有訊息過濾的語義。實踐中可以採取以下兩個辦法:
- 如果過濾的種類不多,可以採取多個 Topic 的方式達到過濾的目的;
- 如果過濾的種類多,則最好在客戶端業務層面自行過濾。
實踐中根據業務具體情況進行選擇,可以綜合運用上面兩種辦法。
2.11、訊息廣播
Kafka 自身沒有訊息廣播的語義,可以通過建立不同的 Consumer Group來模擬實現。
2.12、訂閱關係
同一個 Consumer Group 內,各個消費例項訂閱的 Topic 最好保持一致,避免給排查問題帶來干擾。
參考資料:阿里雲訊息佇列Kafka的幫助文件
相關文章
- 手把手教你寫網路爬蟲(2):迷你爬蟲架構爬蟲架構
- [爬蟲架構] 如何設計一個分散式爬蟲架構爬蟲架構分散式
- 利用爬蟲掙錢系列2-細說資料整合爬蟲
- 資料處理--pandas問題
- Python爬蟲js處理Python爬蟲JS
- HTTP爬蟲被封如何處理?HTTP爬蟲
- 爬蟲抓了那麼多的資料,該如何處理呢?爬蟲
- 利用陣列處理批次資料之習題陣列
- python爬蟲利用代理IP分析大資料Python爬蟲大資料
- Debezium zookeeper kafka mysql資料處理KafkaMySql
- 輕鬆利用Python爬蟲爬取你想要的資料Python爬蟲
- Python爬蟲:手把手教你寫迷你爬蟲架構Python爬蟲架構
- Laravel 實現 Kafka 訊息推送與接收處理LaravelKafka
- 使用Kafka和Flink構建實時資料處理系統Kafka
- 如何利用ip住宅代理解決python爬蟲遇到反爬措施的問題?Python爬蟲
- 實時資料處理:Kafka 和 FlinkKafka
- 大資料處理需留意哪些問題大資料
- 大資料平臺之大資料處理系統的架構大資料架構
- 利用陣列處理批次資料陣列
- oracle SP2-問題處理Oracle
- 利用python編寫爬蟲爬取淘寶奶粉部分資料.1Python爬蟲
- python爬蟲之處理驗證碼Python爬蟲
- 爬蟲 | 處理cookie的基本方法——session爬蟲CookieSession
- 海量資料處理2
- 設計信創雲架構,如何處理傳統雲架構存與棄的問題?架構
- Python爬蟲亂碼問題Python爬蟲
- Python利用pandas處理資料與分析Python
- Kafka叢集訊息積壓問題及處理策略Kafka
- C# 爬蟲—-Cookies處理(Set-Cookie)C#爬蟲Cookie
- 001.01 一般網頁爬蟲處理網頁爬蟲
- 58同城 反爬蟲機制及處理爬蟲
- 何處理資料恢復 資料丟失 面試tx的架構師的崗位問的資料恢復面試架構
- kafka和websocket實時資料推送KafkaWeb
- CPU處理器架構架構
- 爬蟲(6) - 網頁資料解析(2) | BeautifulSoup4在爬蟲中的使用爬蟲網頁
- python分散式爬蟲如何設計架構?Python分散式爬蟲架構
- Oracle資料庫中的逐行處理問題NEOracle資料庫
- openGauss資料庫xlog目錄滿問題處理資料庫