Metron pcap backend代码解析

Metron pcap backend代码解析

metron pcap backend 主要用于创建一个storm topology 处理流,将来自于Kafka的原始数据解析到HDFS中。
其数据流如下:
image

Pcap topology

这个topology 主要是读取数据,并将数据以顺序的方式写入到HDFS中。
主要流程如下:
抓取数据包,并发送到kafka中
image
数据发送到Kafka以后,pcap_backend提交到storm中的流处理任务将消费该数据,并将数据存储到HDFS中。image
最后在运行另外一段解析的程序 pcap_inspector.sh。
image

可以看到数据包的一些基础信息。那么其具体实现流程如何,我们开始跟踪实现的具体步骤。

所谓的启动,其实主要是看 pcap_backend中的start_pcap_topology脚本。

metron-pcap-backend->src->main->scripts->start_pcap_topology

export METRON_VERSION=${project.version}
export METRON_HOME=/usr/metron/$METRON_VERSION
export TOPOLOGIES_JAR=${project.artifactId}-$METRON_VERSION.jar

storm jar $METRON_HOME/lib/$TOPOLOGIES_JAR org.apache.storm.flux.Flux --remote $METRON_HOME/flux/pcap/remote.yaml --filter $METRON_HOME/config/pcap.properties

由上可以看实际是通过flux的方式加载对应的yaml配置进行的项目提交。
pcap.properites:

spout.kafka.topic.pcap=pcap
topology.worker.childopts=
# kerberos token ticke 值配置
topology.auto-credentials=[]
topology.workers=1
kafka.zk=node1:2181
hdfs.sync.every=1
hdfs.replication.factor=-1
kafka.security.protocol=PLAINTEXT
# One of EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST
kafka.pcap.start=UNCOMMITTED_EARLIEST
#一个文件里面保留的最大包数量
kafka.pcap.numPackets=1000
#配置这个保存包的时间
kafka.pcap.maxTimeMS=300000
kafka.pcap.ts_scheme=FROM_KEY
#HDFS中存储这些采集包的路径
kafka.pcap.out=/apps/metron/pcap
#时间单位
kafka.pcap.ts_granularity=MICROSECONDS
kafka.spout.parallelism=1

看了配置,在看topology的流配置。metron 中的storm统一采用Flunk进行配置。

name: "pcap"
config:
    topology.workers: ${topology.workers}
    topology.worker.childopts: ${topology.worker.childopts}
    topology.auto-credentials: ${topology.auto-credentials}

components:

  # Any kafka props for the producer go here.
  - id: "kafkaProps"
    className: "java.util.HashMap"
    configMethods:
      -   name: "put"
          args:
            - "value.deserializer"
            - "org.apache.kafka.common.serialization.ByteArrayDeserializer"
      -   name: "put"
          args:
            - "key.deserializer"
            - "org.apache.kafka.common.serialization.ByteArrayDeserializer"
      -   name: "put"
          args:
            - "group.id"
            - "pcap"
      -   name: "put"
          args:
            - "security.protocol"
            - "${kafka.security.protocol}"

  - id: "kafkaConfig"
    className: "org.apache.metron.storm.kafka.flux.SimpleStormKafkaBuilder"
    constructorArgs:
      - ref: "kafkaProps"
      # topic name
      - "${spout.kafka.topic.pcap}"
      - "${kafka.zk}"
    configMethods:
      -   name: "setFirstPollOffsetStrategy"
          args:
            # One of EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST
            - ${kafka.pcap.start}

  - id: "writerConfig"
    className: "org.apache.metron.spout.pcap.HDFSWriterConfig"
    configMethods:
      -   name: "withOutputPath"
          args:
            - "${kafka.pcap.out}"
      -   name: "withNumPackets"
          args:
            - ${kafka.pcap.numPackets}
      -   name: "withMaxTimeMS"
          args:
            - ${kafka.pcap.maxTimeMS}
      -   name: "withZookeeperQuorum"
          args:
            - "${kafka.zk}"
      -   name: "withSyncEvery"
          args:
            - ${hdfs.sync.every}
      -   name: "withReplicationFactor"
          args:
            - ${hdfs.replication.factor}
      -   name: "withDeserializer"
          args:
            - "${kafka.pcap.ts_scheme}"
            - "${kafka.pcap.ts_granularity}"
spouts:
  - id: "kafkaSpout"
    className: "org.apache.metron.spout.pcap.KafkaToHDFSSpout"
    parallelism: ${kafka.spout.parallelism}
    constructorArgs:
      - ref: "kafkaConfig"
      - ref: "writerConfig"

看了上面的配置,我们再核实一下:
image

image

在分析写入HDFSF代码前。我们先查看下,直接消费kafka里面数据的结果:
image
通过上图可以看出,Kafka中和我们原来想象的不太一样,不是pcap的格式。其具体格式内容的问题:可以参考笔者关于pcapy的博客,这里不再讲述。

根据配置我们可以看到起入口主函数为pcap下的KafkaToHDFSSpout(其关联了2个配置构造函数)
我们先看spout 源

public KafkaToHDFSSpout( SimpleStormKafkaBuilder<byte[], byte[]> spoutConfig
                         , HDFSWriterConfig config
                         )
  {
    super(spoutConfig
         , HDFSWriterCallback.class
         );
    this.config = config;
  }

该函数继承的是kafka
核心其实就是调用了HDFSWriteCallback这个回调函数。在继续跟踪到HDFSWriteCallback下面

import org.apache.storm.kafka.Callback;
import org.apache.storm.kafka.EmitContext;
import org.apache.storm.kafka.spout.KafkaSpoutConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A callback which gets executed as part of the spout to write pcap data to HDFS.
 */
public class HDFSWriterCallback implements Callback {

该函数继承的是org.apache.storm.kafka.Callback下面的函数

public interface Callback extends AutoCloseable, Serializable {
    List<Object> apply(List<Object> tuple, EmitContext context);
    void initialize(EmitContext context);
}

可以看到先提交,再初始化,而在HDFSWrite对这个数据进行了重写。

 @Override
    public List<Object> apply(List<Object> tuple, EmitContext context) {
        #读取流数据
        byte[] key = (byte[]) tuple.get(0);
        byte[] value = (byte[]) tuple.get(1);
        long tsDeserializeStart = System.nanoTime();
        #解析kafka中的Key-value值
        KeyValueDeserializer.Result result = config.getDeserializer().deserializeKeyValue(key, value);
        long tsDeserializeEnd = System.nanoTime();

        if (LOG.isDebugEnabled() && !result.foundTimestamp) {
            List<String> debugStatements = new ArrayList<>();
            if (key != null) {
                debugStatements.add("Key length: " + key.length);
                debugStatements.add("Key: " + DatatypeConverter.printHexBinary(key));
            } else {
                debugStatements.add("Key is null!");
            }

            if (value != null) {
                debugStatements.add("Value length: " + value.length);
                debugStatements.add("Value: " + DatatypeConverter.printHexBinary(value));
            } else {
                debugStatements.add("Value is null!");
            }
            LOG.debug("Dropping malformed packet: {}", Joiner.on(" / ").join(debugStatements));
        }

        long tsWriteStart = System.nanoTime();
        try {
            getWriter(new Partition( topic
                                   , context.get(EmitContext.Type.PARTITION))
                     ).handle(result.key, result.value);
        } catch (IOException e) {
            LOG.error(e.getMessage(), e);
            //drop?  not sure..
        }
        long tsWriteEnd = System.nanoTime();
        if(LOG.isDebugEnabled() && (Math.random() < 0.001 || !inited)) {
            LOG.debug("Deserialize time (ns): {}", (tsDeserializeEnd - tsDeserializeStart));
            LOG.debug("Write time (ns): {}", (tsWriteEnd - tsWriteStart));
        }
        inited = true;
        return tuple;
    }

上面有一段解析Kafka的方法函数deserializeKeyValue,获取其返回值result,(里面就是解析出来的发送的key和value,但需要注意的是这里的Key-value已经和发送的略微有了点变化),具体如下:


metron->spout->pcap->deserializer->FromKeyDeserializer

  @Override
  public Result deserializeKeyValue(byte[] key, byte[] value) {
    if (key == null) {
      throw new IllegalArgumentException("Expected a key but none provided");
    }
    #从前文可以知道Kafa发送的消息中的key也就是时间戳,这部分比较简单
    long ts = converter.toNanoseconds(fromBytes(key));
    #返回的第2部分是一个数据包头(addHeaders
    return new Result(ts, PcapHelper.addHeaders(ts, value, endianness), true);
  }

在上面代码中我们看到解析的值里面有一个addHeaders函数,注意这部分代码已经位于metron-pcap模块中了

  #根据大小端生成不同的pcap 的gloabla header
  public static byte[] getPcapGlobalHeader(Endianness endianness) {
    if(swapBytes(endianness)) {
      //swap
      return new byte[] {
              (byte) 0xd4, (byte) 0xc3, (byte) 0xb2, (byte) 0xa1 //swapped magic number 0xa1b2c3d4
              , 0x02, 0x00 //swapped major version 2
              , 0x04, 0x00 //swapped minor version 4
              , 0x00, 0x00, 0x00, 0x00 //GMT to local tz offset (= 0)
              , 0x00, 0x00, 0x00, 0x00 //sigfigs (= 0)
              , (byte) 0xff, (byte) 0xff, 0x00, 0x00 //snaplen (=65535)
              , 0x01, 0x00, 0x00, 0x00 // swapped link layer header type (1 = ethernet)
                        };
    }
    else {
      //no need to swap
      return new byte[] {
              (byte) 0xa1, (byte) 0xb2, (byte) 0xc3, (byte) 0xd4 //magic number 0xa1b2c3d4
              , 0x00, 0x02 //major version 2
              , 0x00, 0x04 //minor version 4
              , 0x00, 0x00, 0x00, 0x00 //GMT to local tz offset (= 0)
              , 0x00, 0x00, 0x00, 0x00 //sigfigs (= 0)
              , 0x00, 0x00, (byte) 0xff, (byte) 0xff //snaplen (=65535)
              , 0x00, 0x00, 0x00, 0x01 // link layer header type (1 = ethernet)
                        };
    }
  }
  ......
  
  public static byte[] addHeaders(long tsNano, byte[] packet, Endianness endianness) {
    #endianness 是代表大小端属性
    #这里的packet就是原始的数据包了
    byte[] ret = new byte[GLOBAL_HEADER_SIZE + PACKET_HEADER_SIZE + packet.length];
    #根据大小端生成pcap 格式中的globalHeader投
    byte[] globalHeader = getPcapGlobalHeader(endianness);
    int offset = 0;
    System.arraycopy(globalHeader, 0, ret, offset, GLOBAL_HEADER_SIZE);
    offset += globalHeader.length;
    #packet 时间等信息,获取并移位填写
    {
      boolean swapBytes = swapBytes(endianness);
      long micros = Long.divideUnsigned(tsNano, 1000);
      int secs = (int)(micros / 1000000);
      int usec = (int)(micros % 1000000);
      int capLen = packet.length;
      {
        byte[] b = Bytes.toBytes(swapBytes?ByteOrderConverter.swap(secs):secs);
        System.arraycopy(b, 0, ret, offset, Integer.BYTES);
        offset += Integer.BYTES;
      }
      {
        byte[] b = Bytes.toBytes(swapBytes?ByteOrderConverter.swap(usec):usec);
        System.arraycopy(b, 0, ret, offset, Integer.BYTES);
        offset += Integer.BYTES;
      }
      {
        byte[] b = Bytes.toBytes(swapBytes?ByteOrderConverter.swap(capLen):capLen);
        System.arraycopy(b, 0, ret, offset, Integer.BYTES);
        offset += Integer.BYTES;
      }
      {
        byte[] b = Bytes.toBytes(swapBytes?ByteOrderConverter.swap(capLen):capLen);
        System.arraycopy(b, 0, ret, offset, Integer.BYTES);
        offset += Integer.BYTES;
      }
    }
    System.arraycopy(packet, 0, ret, offset, packet.length);
    return ret;
  }

根据代码所示,在写入到HDFS的文件中应该有一部分是pcap Header的头(24byte),packer_hader(16byte),后面就是原始包才对。我们打开一个存入的HDFS文件进行验证:
image从上图可以看到完全和存入到HDFS的文件能对上,但是在gloabal header前面还有115byte的数据,不知道是怎么来的?
在前面List函数中最后可以看到一句:

     try {
            getWriter(new Partition( topic
                                   , context.get(EmitContext.Type.PARTITION))
                     ).handle(result.key, result.value);
        } catch (IOException e) {
            LOG.error(e.getMessage(), e);
            //drop?  not sure..
        }
        ....
        继续跟到hadle代码中
        public void handle(long ts, byte[] value) throws IOException {
    turnoverIfNecessary(ts);
    BytesWritable bw = new BytesWritable(value);
    try {
      writer.append(new LongWritable(ts), bw);
    }
    catch(ArrayIndexOutOfBoundsException aioobe) {
      LOG.warn("This appears to be HDFS-7765 (https://issues.apache.org/jira/browse/HDFS-7765), " +
              "which is an issue with syncing and not problematic: {}", aioobe.getMessage(), aioobe);
    }
    numWritten++;
    if(numWritten % config.getSyncEvery() == 0) {
      syncHandler.sync(outputStream);
    }
  }
  似乎什么也没做,但是有一个很不起眼的函数turnoverIfNecessary
   private void turnoverIfNecessary(long ts, boolean force) throws IOException {
    long duration = ts - batchStartTime;
    boolean initial = outputStream == null;
    boolean overDuration = config.getMaxTimeNS() <= 0 ? false : Long.compareUnsigned(duration, config.getMaxTimeNS()) >= 0;
    boolean tooManyPackets = numWritten >= config.getNumPackets();
    if(force || initial || overDuration || tooManyPackets ) {
      //turnover
      Path path = getPath(ts);
      close();

      if(fs instanceof LocalFileSystem) {
        outputStream = new FSDataOutputStream(new FileOutputStream(new File(path.toString())));
        syncHandler = SyncHandlers.LOCAL.getHandler();
      }
      else {
        outputStream = fs.create(path, true);
        if (outputStream instanceof HdfsDataOutputStream) {
          if (initial) {
            LOG.info("Using the HDFS sync handler.");
          }
          syncHandler = SyncHandlers.HDFS.getHandler();
        } else {
          if (initial) {
            LOG.info("Using the default sync handler, which cannot guarantee atomic appends at the record level, be forewarned!");
          }
          syncHandler = SyncHandlers.DEFAULT.getHandler();
        }

      }
      writer = SequenceFile.createWriter(this.fsConfig
              , SequenceFile.Writer.keyClass(LongWritable.class)
              , SequenceFile.Writer.valueClass(BytesWritable.class)
              , SequenceFile.Writer.stream(outputStream)
              , SequenceFile.Writer.compression(SequenceFile.CompressionType.NONE)
      );
      //reset state
      LOG.info("Turning over and writing to {}: [duration={} NS, force={}, initial={}, overDuration={}, tooManyPackets={}]", path, duration, force, initial, overDuration, tooManyPackets);
      batchStartTime = ts;
      numWritten = 0;
    }
  }

}
看到没,writer 写入了一堆序列化的信息到里面去,但注意根据代码这段只有在创建时才有效,所以一个保存的文件,也只有一个序列化信息,这点和现状也刚好吻合。
  

这是否就是HDFS文件中的内容了,可以看到开始写入的是LongWriteable的类名(org.apache.hadoop.io.LongWriteable)和BytesWritable的类名(org.apache.hadoop.io.ByteWritable)
image

转载于:https://www.cnblogs.com/angellst/p/8717042.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值