Go分布式爬虫笔记(二十)_collect

20 调度引擎

调度引擎目标

  • 创建调度程序,接收任务并将任务存储起来
  • 执行调度任务,通过一定的调度算法将任务调度到合适的 worker 中执行
  • 创建指定数量的 worker,完成实际任务的处理
  • 创建数据处理协程,对爬取到的数据进行进一步处理

scheduler/scheduler.go

package engine

import (
	"github.com/funbinary/go\_example/example/crawler/13/collect"
	"go.uber.org/zap"
)

type ScheduleEngine struct {
	requestCh chan \*collect.Request //负责接收请求
	workerCh  chan \*collect.Request //负责分配任务给 worker
	WorkCount int                   //为执行任务的数量,可以灵活地去配置。
	Fetcher   collect.Fetcher
	Logger    \*zap.Logger
	out       chan collect.ParseResult 负责处理爬取后的数据,完成下一步的存储操作。schedule 函数会创建调度程序,负责的是调度的核心逻辑。
	Seeds     []\*collect.Request
}

func (s \*ScheduleEngine) Run() {
	s.requestCh = make(chan \*collect.Request)
	s.workerCh = make(chan \*collect.Request)
	s.out = make(chan collect.ParseResult)
	go s.Schedule()
	// 创建指定数量的 worker,完成实际任务的处理
	// 其中
	for i := 0; i < s.WorkCount; i++ {
		go s.CreateWork()
	}
	s.HandleResult()
}

func (s \*ScheduleEngine) Schedule() {
	// workerCh
	var reqQueue = s.Seeds
	go func() {
		for {
			var req \*collect.Request
			var ch chan \*collect.Request

			//如果任务队列 reqQueue 大于 0,意味着有爬虫任务,这时我们获取队列中第一个任务,并将其剔除出队列。

			if len(reqQueue) > 0 {
				req = reqQueue[0]
				reqQueue = reqQueue[1:]
				ch = s.workerCh
			}
			select {
			case r := <-s.requestCh:
				// 接收来自外界的请求,并将请求存储到 reqQueue 队列中
				reqQueue = append(reqQueue, r)

			case ch <- req:
				// ch <- req 会将任务发送到 workerCh 通道中,等待 worker 接收。
			}
		}
	}()

}

func (s \*ScheduleEngine) CreateWork() {
	for {
		// 接收到调度器分配的任务;
		r := <-s.workerCh
		// 访问服务器
		body, err := s.Fetcher.Get(r)
		if err != nil {
			s.Logger.Error("can't fetch ",
				zap.Error(err),
			)
			continue
		}
		//解析服务器返回的数据
		result := r.ParseFunc(body, r)
		// 将返回的数据发送到 out 通道中,方便后续的处理。
		s.out <- result
	}
}

func (s \*ScheduleEngine) HandleResult() {
	for {
		select {
		// 接收所有 worker 解析后的数据
		case result := <-s.out:
			// 要进一步爬取的 Requests 列表将全部发送回 s.requestCh 通道
			for \_, req := range result.Requesrts {
				s.requestCh <- req
			}
			//包含了我们实际希望得到的结果,所以我们先用日志把结果打印出来
			for \_, item := range result.Items {
				// todo: store
				s.Logger.Sugar().Info("get result", item)
			}
		}
	}
}


main.go

package main

import (
	"fmt"
	"github.com/funbinary/go\_example/example/crawler/13/collect"
	"github.com/funbinary/go\_example/example/crawler/13/engine"
	"github.com/funbinary/go\_example/example/crawler/13/log"
	"github.com/funbinary/go\_example/example/crawler/13/parse/doubangroup"
	"github.com/funbinary/go\_example/example/crawler/13/proxy"
	"go.uber.org/zap/zapcore"

	"time"
)

// xpath
func main() {
	plugin := log.NewStdoutPlugin(zapcore.InfoLevel)
	logger := log.NewLogger(plugin)
	logger.Info("log init end")

	proxyURLs := []string{"http://127.0.0.1:10809", "http://127.0.0.1:10809"}
	p, err := proxy.RoundRobinProxySwitcher(proxyURLs...)
	if err != nil {
		logger.Error("RoundRobinProxySwitcher failed")
	}

	// douban cookie
	var seeds []\*collect.Request
	for i := 0; i <= 0; i += 25 {
		str := fmt.Sprintf("https://www.douban.com/group/szsh/discussion?start=%d", i)
		seeds = append(seeds, &collect.Request{
			Url:       str,
			WaitTime:  1 \* time.Second,
			Cookie:    "bid=-UXUw--yL5g; dbcl2=\"214281202:q0BBm9YC2Yg\"; \_\_yadk\_uid=jigAbrEOKiwgbAaLUt0G3yPsvehXcvrs; push\_noty\_num=0; push\_doumail\_num=0; \_\_utmz=30149280.1665849857.1.1.utmcsr=accounts.douban.com|utmccn=(referral)|utmcmd=referral|utmcct=/; \_\_utmv=30149280.21428; ck=SAvm; \_pk\_ref.100001.8cb4=%5B%22%22%2C%22%22%2C1665925405%2C%22https%3A%2F%2Faccounts.douban.com%2F%22%5D; \_pk\_ses.100001.8cb4=\*; \_\_utma=30149280.2072705865.1665849857.1665849857.1665925407.2; \_\_utmc=30149280; \_\_utmt=1; \_\_utmb=30149280.23.5.1665925419338; \_pk\_id.100001.8cb4=fc1581490bf2b70c.1665849856.2.1665925421.1665849856.",
			ParseFunc: doubangroup.ParseURL,
		})
	}

	var f collect.Fetcher = &collect.BrowserFetch{
		Timeout: 3000 \* time.Millisecond,
		Logger:  logger,
		Proxy:   p,
	}

	s := engine.ScheduleEngine{
		WorkCount: 5,
		Logger:    logger,
		Fetcher:   f,
		Seeds:     seeds,
	}
	s.Run()

}

通道

特性

  • 我们往 nil 通道中写入数据会陷入到堵塞的状态。因此,如果 reqQueue 为空,这时 req 和 ch 都是 nil,当前协程就会陷入到堵塞的状态,直到接收到新的请求才会被唤醒。
package main

import (
	"fmt"
)

func main() {
	var ch chan \*int
	go func() {
		<-ch
	}()
	select {
	case ch <- nil:
		fmt.Println("it's time")
	}

}
//fatal error: all goroutines are asleep - deadlock!


函数选项模式

  • 原由: 在实践中函数可能有几十个参数等着我们赋值,不同参数的灵活组合可能会带来不同的调度器类型。在实践中为了方便使用,开发者可能会创建非常多的 API 来满足不同场景的需要,随着参数的不断增多,这种 API 会变得越来越多,这就增加了开发者的心理负担。
// 基本调度器
func NewBaseSchedule() \*Schedule {
  return &Schedule{
    WorkCount: 1,
    Fetcher:baseFetch,
  }
}
// 多worker调度器
func NewMultiWorkSchedule(workCount int) \*Schedule {
  return &Schedule{
    WorkCount: workCount,
    Fetcher:baseFetch,
  }
}

// 代理调度器
func NewProxySchedule(proxy string) \*Schedule {
  return &Schedule{
    WorkCount: 1,
    Fetcher:proxyFetch(proxy),
  }
}

  • 另一种使用方式就是传递一个统一的 Config 配置结构,如下所示。这种方式只需要创建单个 API,但是需要在内部对所有的变量进行判断,繁琐且不优雅。对于使用者来说,也很难确定自己需要使用哪一个字段。
type Config struct {
  WorkCount int
  Fetcher   collect.Fetcher
  Logger    \*zap.Logger
  Seeds     []\*collect.Request
}

func NewSchedule(c \*Config) \*Schedule {
  var s = &Schedule{}
  if c.Seeds != nil {
    s.Seeds = c.Seeds
  }
  if c.Fetcher != nil {
    s.Fetcher = c.Fetcher
  }

  if c.Logger != nil {
    s.Logger = c.Logger
  }
  ...
  return s
}

  • Rob Pike 在 2014 年的一篇博客中提到了一种优雅的处理方法叫做函数式选项模式 (Functional Options)。这种模式展示了闭包函数的有趣用途,目前在很多开源库中都能看到它的身影,我们项目中使用的日志库 Zap 也使用了这种模式。
  1. 我们要对 schedule 结构进行改造,把可以配置的参数放入到options 结构中:
type Schedule struct {
  requestCh chan \*collect.Request
  workerCh  chan \*collect.Request
  out       chan collect.ParseResult
  options
}

type options struct {
  WorkCount int
  Fetcher   collect.Fetcher
  Logger    \*zap.Logger
  Seeds     []\*collect.Request
}

  1. 我们需要书写一系列的闭包函数,这些函数的返回值是一个参数为 options 的函数:
type Option func(opts \*options)

func WithLogger(logger \*zap.Logger) Option {
  return func(opts \*options) {
    opts.Logger = logger
  }
}
func WithFetcher(fetcher collect.Fetcher) Option {
  return func(opts \*options) {
    opts.Fetcher = fetcher
  }
}

func WithWorkCount(workCount int) Option {
  return func(opts \*options) {
    opts.WorkCount = workCount
  }
}

func WithSeeds(seed []\*collect.Request) Option {
  return func(opts \*options) {
    opts.Seeds = seed
  }
}

  1. 创建一个生成 schedule 的新函数,函数参数为 Option 的可变参数列表。defaultOptions 为默认的 Option,代表默认的参数列表,然后循环遍历可变函数参数列表并执行。
func NewSchedule(opts ...Option) \*Schedule {
  options := defaultOptions
  for \_, opt := range opts {
    opt(&options)
  }
  s := &Schedule{}
  s.options = options
  return s
}

  1. 在 main 函数中调用 NewSchedule。让我们来看看函数式选项模式的效果:
func main(){
  s := engine.NewSchedule(
      engine.WithFetcher(f),
      engine.WithLogger(logger),
      engine.WithWorkCount(5),
      engine.WithSeeds(seeds),
    )
  s.Run()
}

函数式选项模式的好处

  • API 具有可扩展性,高度可配置化,新增参数不会破坏现有代码;
  • 参数列表非常简洁,并且可以使用默认的参数;
  • option 函数使参数的含义非常清晰,易于开发者理解和使用;
  • 如果将 options 结构中的参数设置为小写,还可以限制这些参数的权限,防止这些参数在 package 外部使用。

通道底层原理

通道的实现并没有想象中复杂。它利用互斥锁实现了并发安全,只不过 Go 运行时为我们屏蔽了底层的细节。通道包括两种类型,一种是无缓冲的通道,另一种是带缓冲区的通道。通道的结构如下:

image

可以看到,通道中包含了数据的类型、大小、数量,堵塞协程队列,以及用于缓存区的诸多字段。

无缓冲区的通道

通道需要有多个协程分别完成读和写的功能,这样才能保证数据传输是顺畅的。对于无缓冲区的通道来说,如果有一个协程正在将数据写入通道,但是当前没有协程读取数据,那么写入协程将立即陷入到休眠状态。写入协程堵塞之前协程会被封装到 sudog 结构中,并存储到写入的堵塞队列 sendq 中,之后协程陷入休眠。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值