kafka分布式爬虫系统-任务的发放

任务的发放

任务发放应该是一个全局的总机,它主要和用户打交道,用来接收用户的请求,然后将用户的请求转换成机器能执行的任务。用户就不需要关心任务是如何发放的,只要将想要抓的网站或内容分配给总机即可。这里些问题需要考虑是:1.如何保证任务能有序的执行。2.保证断电了任务不丢失,来电了任务还能继续在原来的断点处执行。3.当用户如果发了几百万的任务,保证所有主机不崩溃。基于前面的3点,我想到如下解决方案:1.任务有序执行,那给任务做个序号标记。2.任务不丢失,那将任务写到硬盘。3.保证所有任务不会出现峰值执行情况,那可以让抓取机器反馈给主机,说执行完了就在取总机那里拿就好了。考虑到如同的主机分布式的抓数据,肯定是要用消息队列解决任务的方法,这让我自然想到了之前用过rabbitmq,但rabbitmq要保证消息不丢,通常采用的主从备份架构,但由于master-slave架构的缺点能保存的消息量取决于单台主机的容量,结合本次大量图片的下载存储,那不妨用kafka来解决这样的问题。

创建topic

考虑到我们这个爬虫任务多,针对kafka的一个partition 只能同时被一个consumer消费,同时要保证可靠性,即断电不丢任务,我建立了一个13个partition ,副本数为3的topic;

kafka分布式爬虫系统-任务的发放_第1张图片

                                      (图4 topic情况)

创建任务

因为我将用我那集群的13台机器去抓数据,所有我设置partition为13。然后剩下的就是如何将用户的输入转换成任务了。看会代码。下面是总机的发送任务的方法,它是将坐标转成图像的tile号,我们是要抓取百度地图上的卫星遥感图像,因此涉及到坐标的运算。然后就是记录操作;最后将任务发送到队列中。

  /**
    * 根据坐标发送位置消息
    */
  private def sendPositionsTask = {
    //将坐标生成抓取任务的任务号范围,即经度纬度范围
    val (titleX, titleY) = PositionUtil.wgs84ToTile(firstLng, firstLat, level)
    val (titleX2, titleY2) = PositionUtil.wgs84ToTile(secondLng, secondLat, level)
    val satelliteMap = new SatelliteMap(
      source,
      s"${source}卫星",
      source,
      s"${source}卫星",
      MD5Util.getErrorMD5(source),
      MD5Util.getErrorRowkey(source),
      Array(new Acquistion(
        ack_time,
        Array(LevelArea(level,
          Array(new Area(new Loc(firstLng, firstLat), new Loc(secondLng, secondLat)))))))) 
    MongoCRUD.updateData(mongo_collection_name, satelliteMap)  //将任务的操作记录至mongo,防止重复生成区域坐标任务
    AvroProducer.sendMessage(AvroProducer.getPicAvroBeanProducer, source, style, ack_time, level, titleY, titleY2, titleX, titleX2, lngNums)//将任务发送至队列中
  }

看下发送任务的代码,下面是将消息封装成ProducerRecord,通过producer发送至topic的队列上。

def sendMessage[K,V](topic:String,producer: Producer[K,V],key:K,msg:V):Unit={
    val producerRec:ProducerRecord[K, V] = new ProducerRecord(topic,key,msg)
    producer.send(producerRec,new Callback {
      override def onCompletion(metadata: RecordMetadata, exception: Exception): Unit = {
//        println(s"${metadata.topic()}${metadata.partition()}")
      }
    })
  }

序列化

这里需要注意的是因为我们的消息需要在网络中传输,因此kafka需要msg是实现了序列化的对象。这里用avro的工具对对象进行了序列化。

如我的msg对象是这样的:


@SuppressWarnings("all")
@org.apache.avro.specific.AvroGenerated
public class WnsysMapPicBean extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord{
  public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"WnsysMapPicBean\",\"namespace\":\"kafka\",\"fields\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"mapType\",\"type\":\"string\"},{\"name\":\"level\",\"type\":\"int\"},{\"name\":\"latNum\",\"type\":\"int\"},{\"name\":\"lngNum\",\"type\":\"int\"},{\"name\":\"style\",\"type\":\"string\"},{\"name\":\"MD5\",\"type\":\"string\"},{\"name\":\"time\",\"type\":\"string\"},{\"name\":\"pic\",\"type\":\"bytes\"},{\"name\":\"productTablePrefix\",\"type\":\"string\"},{\"name\":\"rowkey\",\"type\":\"string\"},{\"name\":\"position\",\"type\":{\"type\":\"array\",\"items\":{\"type\":\"record\",\"name\":\"Loc\",\"fields\":[{\"name\":\"lat\",\"type\":\"double\"},{\"name\":\"lng\",\"type\":\"double\"}]}}}]}");
  

然后看下发送者的producer中的配置:

def getHashProducer[T]():Producer[String,T]={
    val properties=new Properties
    properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer].getName)
    properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,classOf[AvroSerializer[T]].getName)
    properties.put(ProducerConfig.ACKS_CONFIG, "-1")
    //    properties.put("producer.type","async")
    properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,PropertyParseUtils.getValue("kafka.broker_list").toString)
    properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,classOf[HashPartition].getName)
    new KafkaProducer[String,T](properties)
  }

在配置中需要注意数据发送的确认保证,数据序列化和数据在partition 的负载均衡,不可能说我把所有的数据放到一个分区上,其他分区没数据吧。因此在原理对象的基础上封装了一层AvroSerializer[T]

class AvroSerializer[T <: SpecificRecordBase] extends Serializer[T]{
  override def configure(configs: util.Map[String, _], isKey: Boolean) = {}

  /**
    * 序列化成字节数组
    * @param topic
    * @param data
    * @return
    */
  override def serialize(topic: String, data: T):Array[Byte] = {
    if (data == null){
      println(s"producer is null")
      return null;
    }
    val writer=new SpecificDatumWriter[T](data.getSchema)
    val outputStream=new ByteArrayOutputStream();
    val encoder=EncoderFactory.get().directBinaryEncoder(outputStream,null)
    writer.write(data,encoder)
    outputStream.toByteArray
  }

  override def close() = {}
}

分区负载均衡

然后自己实现一个hashPartition类,使得任务负载均衡那个到每个partititon上,很简单,如下:

public class HashPartition implements Partitioner {
    private static int count=0;
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        List partitions=cluster.partitionsForTopic(topic);
        int numberP=partitions.size();
        if(keyBytes!=null) {
            //hash partition方式
           /* int hashCode=0;
            if (key instanceof Integer || key instanceof Long)
                hashCode=Integer.parseInt(key.toString());
            else hashCode=key.hashCode();
            hashCode=hashCode & 0x7fffffff;
            return hashCode%numberP;*/
           return count++%numberP;
        }else return 0;
    }

    public void close() {

    }

    public void configure(Map configs) {

    }
}

该类继承了Partitioner,改写里面的partition。我这里偷懒了,直接用一个计数器对partition求余(如果任务太多count会为负数,建议用hash partition 方式)

总结

好了,分布式爬虫系统的任务发送已经完成了。其中,注意点就是要注意任务需要avro序列化处理,ack_config=-1,即保证所有队列发送成才算成功,保证消息可靠的投递;最后做好消息的负载均衡化;partition合适的个数也很重要,太多了会导致zookeeper的负担,太少了又可能导致抓取的任务机器过少,任务不能很好的并行化。下面章节我们看如何接收消息,如何处理任务。

你可能感兴趣的:(大数据,kafka)