一、前述
我行自2016年開始使用Kafka,主要用於兩大類應用:一是用於在應用間構建實時的數據流通道,二是用於構建傳輸或處理數據流的實時流式應用。Kafka自身沒有監控管理頁面,我們調研並對比了市面主要的Kafka管理工具,得到的結論是各個產品有自身的設計侷限性,並且開源產品缺乏穩定性,因此在滿足實際需求的前提下,集合衆多產品的優點基礎上,設計並研發了一套適合通用的Kafka監控管理平臺。以下是調研的市面主要的Kafka管理工具:
工具 | 優點 | 缺點 |
---|---|---|
Kafka Manager | 功能較全面,有集羣管理,topic管理,consumer offset和lag監控,支持JMX監控 | 無ACL功能,部署較爲複雜 |
Kafka-monitor | 實時監控服務的可用性、消息丟失率、延遲率 | 偏重監控,無集羣管理 |
KafkaOffsetMonitor | 監控offset,並可存儲歷史offset;jar包部署,較方便 | 功能單一,無topic管理,集羣管理 |
Confluent Control Center | 監控集羣、數據流程,管理Kafka Connect和topic | 開源版功能較少 |
二、民生Kafka PAAS介紹
Kafka Paas平臺是我行自研的一套平臺,提供Kafka接入和Kafka運維管控解決方案,集成集羣監控管理、Topic管理、Consumer Group管理、Schema Registry管理和ZooKeeper管理功能,幫助Kafka運維人員和接入人員快速使用和管控Kafka集羣。該平臺提供了基於Kafka 0.10.X和1.1.X版本的REST API,在第三部分我們會詳細介紹。以下是部分功能截圖展示:
三、開放的Kafka REST API
該API分爲Kafka管理、ZooKeeper管理、SchemaRegistry管理、JMX Metric採集、用戶管理等幾大功能,接下來將分別介紹該API的主要功能:
3.1 Kafka管理
這是API的核心功能,包括Cluster/Broker管理、Topic管理、Consumer管理三大功能,以下分別介紹:
A. Cluster/Broker管理
Cluster/Broker管理包含以下功能:
- (1)查看集羣的id、controller和集羣中每個broker信息;
- (2)查看broker的信息,包括id,端口號,在zk節點註冊時間、JMX端口、endpoints、listener security等信息;
- (3)查看broker上日誌目錄信息,根據broker、topic信息查找副本所在的日誌目錄;
- (4)查看broker的配置信息和動態配置信息,對於動態配置信息可以修改和刪除。這部分主要是利用clients包裏的KafkaAdminClient實現的,KafkaAdminClient是0.11.0.0版本開始提供的管理類,是用來替代之前core包裏AdminUtils,AdminClient,ZKUtils等功能,但是這個類功能目前還不完善,比如沒有管理consumer相關的方法,下面列舉查看cluster的信息爲例說明KafkaAdminClient的使用:
DescribeClusterOptions describeClusterOptions =
new DescribeClusterOptions().timeoutMs((int) kafkaAdminClientGetTimeoutMs);
DescribeClusterResult describeClusterResult =
kafkaAdminClient.describeCluster(describeClusterOptions);
KafkaFuture<String> clusterIdFuture = describeClusterResult.clusterId();
KafkaFuture<Node> controllerFuture = describeClusterResult.controller();
KafkaFuture<Collection<Node>> nodesFuture = describeClusterResult.nodes();
String clusterId = "";
Node controller = null;
Collection<Node> nodes = new ArrayList<>();
try {
clusterId = clusterIdFuture.get(kafkaAdminClientGetTimeoutMs, TimeUnit.MILLISECONDS);
controller = controllerFuture.get(kafkaAdminClientGetTimeoutMs, TimeUnit.MILLISECONDS);
nodes = nodesFuture.get(kafkaAdminClientGetTimeoutMs, TimeUnit.MILLISECONDS);
} catch (Exception exception) {
log.warn("Describe cluster exception:" + exception);
throw new ApiException("Describe cluster exception:" + exception);
} finally {
if (clusterIdFuture.isDone() && !clusterIdFuture.isCompletedExceptionally()) {
// clusterDetail.put("clusterId", clusterId);
clusterInfo.setClusterId(clusterId);
}
if (controllerFuture.isDone() && !controllerFuture.isCompletedExceptionally()) {
// clusterDetail.put("controllerId", controller);
clusterInfo.setController(controller);
}
if (nodesFuture.isDone() && !nodesFuture.isCompletedExceptionally()) {
// clusterDetail.put("nodes", nodes);
clusterInfo.setNodes(nodes);
}
}
除了用到KafkaAdminClient,broker的JMX端口,在zk註冊的時間等這些需要獲取zk上/brokers/ids的信息,其實在KafkaZkClient裏有這類似的實現方法,但是返回的Broker信息裏只有id,endpoints,rack信息,沒有timestamp,jmxPort,endpoints這些信息,因此我們使用ZooKeeper的CuratorClient獲取該路徑的數據,然後通過反序列化讀取全部數據,實現細節截取如下:
String brokerInfoStr = null;
try {
// TODO replace zkClient with kafkaZKClient
brokerInfoStr =
new String(
zkClient
.getData()
.forPath(ZkUtils.BrokerIdsPath() + "/" + entry.getKey()));
} catch (Exception e) {
e.printStackTrace();
}
BrokerInfo brokerInfo;
try {
ObjectMapper objectMapper = new ObjectMapper();
brokerInfo = objectMapper.readValue(brokerInfoStr, BrokerInfo.class);
} catch (Exception exception) {
throw new ApiException("List broker exception." + exception);
}
需要注意的是不同版本的Broker在zk上註冊的字段信息不同,字段信息可通過版本區分,在0.10.0.1版本是3,在1.1.1版本是4,以下是version=3和4的一個示例,在version 4裏多一個字段listenersecurityprotocol_map,詳細可參考類ZkData.scala:
* Version 3 JSON schema for a broker is:
* {
* "version":3,
* "host":"localhost",
* "port":9092,
* "jmx_port":9999,
* "timestamp":"2233345666",
* "endpoints":["PLAINTEXT://host1:9092", "SSL://host1:9093"],
* "rack":"dc1"
* }
*
* Version 4 (current) JSON schema for a broker is:
* {
* "version":4,
* "host":"localhost",
* "port":9092,
* "jmx_port":9999,
* "timestamp":"2233345666",
* "endpoints":["CLIENT://host1:9092", "REPLICATION://host1:9093"],
* "listener_security_protocol_map":{"CLIENT":"SSL", "REPLICATION":"PLAINTEXT"},
* "rack":"dc1"
* }
另外KafkaAdminClient有獲取broker配置信息的方法describeConfigs,但是該方法獲取不到broker動態的配置信息,在重分區的時候,可以設置限速,但是該方法獲取不到限速的值(值爲null),因此我們改進了獲取broker配置信息的方法,將describeConfigs方法獲取的配置和從/config/brokers/id上獲取的信息進行merge,形成最全最準確的配置信息。
B. Topic管理
Topic管理包含以下功能:
- (1)Topic的查看、創建、刪除(刪除是異步的)、配置查看/修改、schema查看;KafkaAdminClient提供了listTopics()、describeTopics()、createTopics()、deleteTopics()等topic管理方法,配置的查看修改部分是利用describeConfigs()、alterConfigs()實現,和Broker配置管理類似,需要整合從zookeeper上獲取的topic動態配置信息。schema的查看是根據topic名稱和schema名稱的對應關係獲取。
- (2)分區查看,分區重分佈生成計劃、執行和檢查(支持跨broker和broker內重分佈,帶限速功能) 分區信息查看,包括查看分區id、分區的leader、副本分本、ISR、開始offset、結束offset、消息數,前4個字段信息是通過KafkaAdminClient的describeTopics()實現,開始offset和結束offset是通過KafkaConsumer的beginningOffsets()和endoffsets()方法獲取,消息數是這兩個值的差值。在kafka中有腳本kafka-reassign-partitions提供了分區重分佈的功能,該腳本可用於增加分區、增加副本、分區遷移,在1.1版本開始支持不同路徑間的遷移;增加分區是採用KafkaAdminClient.createPartitions()方法實現的,其他功能該客戶端還未實現,我們是調用ReassignPartitionsCommand.scala類實現的,Scala被編譯爲Java字節碼,運行在JVM之上,因此在Java裏可以直接調用Scala,對於Java和Scala之間類型轉換,使用Scala提供的JavaConverters.scala,以下生成分區重分佈計劃的代碼,裏面涉及到了Java和Scala對象的轉化:
// Return <Current partition replica assignment, Proposed partition reassignment>
public List<ReassignModel> generateReassignPartition(ReassignWrapper reassignWrapper) {
KafkaZkClient kafkaZkClient = zookeeperUtils.getKafkaZkClient();
List<ReassignModel> result = new ArrayList<>();
Seq brokerSeq =
JavaConverters.asScalaBufferConverter(reassignWrapper.getBrokers()).asScala().toSeq();
// <Proposed partition reassignment,Current partition replica assignment>
Tuple2 resultTuple2;
try {
resultTuple2 =
ReassignPartitionsCommand.generateAssignment(
kafkaZkClient, brokerSeq, reassignWrapper.generateReassignJsonString(), false);
} catch (Exception exception) {
throw new ApiException("Generate reassign plan exception." + exception);
}
HashMap<TopicPartitionReplica, String> emptyMap = new HashMap<>();
ObjectMapper objectMapper = new ObjectMapper();
try {
result.add(
objectMapper.readValue(
ReassignPartitionsCommand.formatAsReassignmentJson(
(scala.collection.Map<TopicPartition, Seq<Object>>) resultTuple2._2(),
JavaConverters.mapAsScalaMapConverter(emptyMap).asScala()),
ReassignModel.class));
result.add(
objectMapper.readValue(
ReassignPartitionsCommand.formatAsReassignmentJson(
(scala.collection.Map<TopicPartition, Seq<Object>>) resultTuple2._1(),
JavaConverters.mapAsScalaMapConverter(emptyMap).asScala()),
ReassignModel.class));
Collections.sort(result.get(0).getPartitions());
Collections.sort(result.get(1).getPartitions());
} catch (Exception exception) {
throw new ApiException("Generate reassign plan exception." + exception);
}
return result;
}
- (3)消息查看,消息查看支持根據offset和timestamp兩種查詢方式,對於消息中的key和value支持StringDeserializer、ByteArrayDeserializer、FloatDeserializer、DoubleDeserializer、KafkaAvroDeserializer等方式進行反序列化解碼,實現的原理是根據傳入的Key和Value的解碼方式創建consumer,根據反序列化類型的不同,我們分兩種情況:a.如果解碼方式是ByteArrayDeserializer或者是KafkaAvroDeserializer,拉取消息返回的類型是ConsumerRecords,這裏需要注意的是Producer端如果是用KafkaAvroSerializer編碼方式發送的消息,那麼每條消息裏從第6個字節(第1個字節是MAGIC_BYTE,第2-6字節是一個整數,是schema的ID)纔是真正數據,部分代碼細節如下:
private Object avroDeserialize(byte[] bytes, String avroSchema, boolean isInSchemaRegistry) {
Schema schema = new Schema.Parser().parse(avroSchema);
DatumReader reader = new GenericDatumReader<GenericRecord>(schema);
ByteBuffer buffer = ByteBuffer.wrap(bytes);
Object object = null;
if (isInSchemaRegistry) {
try {
//這裏利用confluent提供的io.confluent.kafka.serializers.KafkaAvroDeserializer進行解碼
object = confluentSchemaService.deserializeBytesToObject("", bytes, schema);
} catch (SerializationException serializationException) {
throw new ApiException("Avro Deserialize exception. " + serializationException);
}
} else {
try {
object =
reader.read(
null,
DecoderFactory.get().binaryDecoder(buffer.array(), 0, bytes.length, null));
} catch (IOException exception) {
throw new ApiException("Avro Deserialize exception. " + exception);
}
}
return object;
}
b.其他方式下,拉取消息時返回的ConsumerRecords,然後根據Java的反射機制判斷每條ConsumerRecord內數據的類型,然後轉化成String類型
這部分代碼細節請參考:
ConsumerRecords<Object, Object> crs = consumer.poll(timeoutMs);
if (crs.count() != 0) {
Iterator<ConsumerRecord<Object, Object>> it = crs.iterator();
while (it.hasNext()) {
Record record = Record.builder().topic(topic).keyDecoder(keyDecoder)
.valueDecoder(valueDecoder).build();
ConsumerRecord<Object, Object> initCr = it.next();
record.setOffset(initCr.offset());
record.setTimestamp(initCr.timestamp());
record.setKey(initCr.key());
record.setValue(initCr.value());
recordList.add(record);
}
}
public String getValueByDecoder(String decoder, Object value) {
if (value == null) return null;
Class<?> type = KafkaUtils.DESERIALIZER_TYPE_MAP.get(decoder);
try {
if (String.class.isAssignableFrom(type)) {
return value.toString();
}
if (Short.class.isAssignableFrom(type)) {
return value.toString();
}
...
if (Bytes.class.isAssignableFrom(type)) {
Bytes bytes = (Bytes) value;
return bytes.toString();
}
if (byte[].class.isAssignableFrom(type)) {
if (decoder.contains("AvroDeserializer")) {
return value.toString();
} else {
byte[] byteArray = (byte[]) value;
return new String(byteArray);
}
}
if (ByteBuffer.class.isAssignableFrom(type)) {
ByteBuffer byteBuffer = (ByteBuffer) value;
return new String(byteBuffer.array());
}
}
...
C. Consumer管理
Consumer管理包含以下功能:
- (1)new/old Consumer的查看,包括state、組內consumer分配策略、group所在協調節點、組內所有consumer成員信息、consumer消費的topic信息和消費位移/lag信息,另外可以根據consumer名稱/topic名稱查詢消費consumer信息和消費位移/lag信息
這部分實現是利用kafka.admin.AdminClient實現的,describeConsumerGroup方法可以獲取state、組內consumer分配策略、協調節點和組內consumer信息,關於獲取消費的位移/lag信息,利用adminclient.listGroupOffsets()方法獲取消費過的所有分區信息和位移信息,
再結合describeConsumerGroups()獲取到的Consumer分配信息獲取consumer的消費位移/lag信息,注意的是listGroupOffsets中獲取的分區信息可能比describeConsumerGroup()獲取到的ConsumerSummary裏分區的總和,也就是存在沒有consumer的消費信息,這些分區對應的consumerId,clientId,host用“-”代替,此處是參考ConsumerGroupCommand.scala類裏的collectGroupOffsets()方法實現
- (2)重置consumer的offset,支持重置到earliest、latest、指定offset,也可以重置到指定時間點的offset
實現是創建一個同group下的KafkaConsumer,然後用assign()方法指定消費分區,然後再利用KafkaConsumer提供的seekToBeginning(),seekToEnd(),seek()方法實現重置,如果需要重置到指定時間點,先用offsetsForTimes()方法獲取不早於該時間戳的分區位移,然後再用seek()方法重置,需要注意的是爲了不影響該group的消費位移,重置之前需要將處於active狀態的group先停掉。
- (3)new/old Consumer刪除
雖然之後kafka不再支持old consumer,但是在我行0.10.0.1環境中,還是有使用old consumer的客戶端,因此對於刪除old consumer的功能仍保留,實現原理是刪除zookeeper路徑/consumers/{groupName}的數據;對於new consumer,使用kafka.admin.AdminClient.deleteConsumerGroups()實現。
3.2 Zookeeper管理
ZooKeeper管理包含以下功能:
(1)查看zk的環境信息
實現原理是通過zookeeper的四字命令envi獲取數據,然後解析。
(2)查看服務器的詳細信息:服務器的詳細信息:接收/發送包數量、連接數、模式(leader/follower)、節點總數、延遲,以及所有客戶端的列表
實現原理也是通過zookeeper的四字命令stat獲取數據,然後解析,細節代碼如下:
public Map<HostAndPort, ZkServerStat> stat() {
List<HostAndPort> hostAndPortList = zookeeperUtils.getZookeeperConfig().getHostAndPort();
Map<HostAndPort, ZkServerStat> result = new HashMap<>();
for (int i = 0; i < hostAndPortList.size(); i++) {
HostAndPort hp = hostAndPortList.get(i);
try {
result.put(
hp,
zookeeperUtils.parseStatResult(
zookeeperUtils.executeCommand(
hp.getHostText(), hp.getPort(), ZkServerCommand.stat.toString())));
} catch (ServiceNotAvailableException serviceNotAvailbleException) {
log.warn(
"Execute "
+ ZkServerCommand.stat.toString()
+ " command failed. Exception:"
+ serviceNotAvailbleException);
result.put(
hp,
ZkServerStat.builder()
.mode(serviceNotAvailbleException.getServiceState())
.msg(serviceNotAvailbleException.getMessage())
.build());
}
}
return result;
}
(3)查看指定路徑下節點信息,獲取指定路徑的節點數據
實現效果和ls/get命令一樣,是通過curatorClient提供的getChildren()方和getData()方法實現。
3.3 Schema Registry管理
在我行實際使用過程中,Kafka的數據來源之一是CDC(Change Data Capture),這些數據是Avro格式的,且包含schema,因此我們部署了confluent版本Schema Registry,Schema Registry服務在CDC同步架構中作爲Avro Schema的存儲庫。Confluent本身也提供了RESTful的接口,爲了將這些管理功能集成,在我們的API中封裝了主要接口,包括以下功能:(1)獲取所有subjects,在原有的接口上進行擴展,不僅獲取了subject名稱,還獲取了最新的schema的Id,版本,schema內容,代碼細節如下:
public List<SchemaRegistryMetadata> getAllSubjects() {
try {
Collection<String> subjects = this.schemaRegistryClient.getAllSubjects();
List<SchemaRegistryMetadata> allSubjects = new ArrayList<>();
SchemaMetadata schemaMetadata;
SchemaRegistryMetadata schemaRegistryMetadata;
for (String subject : subjects) {
schemaMetadata = schemaRegistryClient.getLatestSchemaMetadata(subject);
schemaRegistryMetadata = SchemaRegistryMetadata.builder().subject(subject)
.id(schemaMetadata.getId())
.version(schemaMetadata.getVersion()).schema(schemaMetadata.getSchema()).build();
allSubjects.add(schemaRegistryMetadata);
}
return allSubjects;
} catch (Exception exception) {
throw new ApiException("ConfluentSchemaService getAllSubjects exception : " + exception);
}
}
另外提供獲取指定subject的shema所有的版本信息,刪除指定的subject
(2)註冊schema;根據schemaId獲取shema信息,在原有的接口上進行了擴展,不僅獲取了shema的詳細內容,還能獲取對應的subject名稱,版本信息;根據subject名稱和schema版本獲取schema信息
3.4 User管理和JMX metric採集
由於該API是管理Kafka、ZooKeeper、Schema Registry等衆多產品的API,且API操作中有很多修改,刪除等接口,爲了增加接口的安全性,我們增加了身份認證(是否添加認證可通過參數配置),採用HTTP basic方式進行認證。實現上使用Spring Boot集成Spring Security方式,爲了減少API對其他產品的依賴,我們將用戶名和密碼存儲在文件security.yml中,同時啓動一個線程定期load該文件的數據,添加用戶後不需要重啓應用程序。JMX metric採集是可以根據傳入的jmx url和篩選指標,從對應的jmx端口獲取指標信息,由於後續行內有統一的jmx指標採集規劃,因此該功能後續不再更新。
四、後續計劃
隨着業務的不斷髮展,我行使用Kafka的應用不斷增多,Kafka變得越來越重要。後續我們會重點進行Kafka PaaS平臺建設,近期我們會先將kafka升級到1.1.1,同時增加ACL權限管控,關於ACL的管控接口會逐步開發並開源出來,另外就是規劃多集羣的分級管理,歡迎各行各業的朋友和我們交流和聯繫。
作者介紹:
文喬,工作於中國民生銀行總行信息技術部大數據基礎產品平臺組,天眼日誌平臺主要參與人。
王健,民生銀行信息科技部 dba ,2011年加入民生銀行,主要從事數據庫和kafka,數據複製等運維工作。
本文轉載自公衆號民生運維(ID:CMBCOP)。
原文鏈接: