中國民生銀行大數據團隊Kafka1.X管控實踐

一、前述

我行自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)

原文鏈接

https://mp.weixin.qq.com/s?__biz=MzUxNzEwOTUyMg==&mid=2247484934&idx=1&sn=63336e5fd0a55eada2d82f085ec84253&chksm=f99c647bceebed6d1b2de88aad8b7552e8cbe886f1d929e92d5e43f863dbd6931a465db9f4f4&scene=27#wechat_redirect

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章