Mapreduce
Mapreduce是一种分布式并行编程模型,在一个函数或者一次接口调用中会出现大量的计算或者大量的调用第三方接口的情况。这个时候就可以使用Mapreduce这种变成模型,让大量的计算在一台或者多台机器上处理,最终汇总到一起输出结果。
gozero中的Mapreduce
gozero是一个最近比较流行的go微服务框架,但是在这个库中也有一些比较有意思和好用的类库,我们可以单独引用,就比如其中的Mapreduce。
官方是这么说的:在实际的业务场景中我们常常需要从不同的 rpc 服务中获取相应属性来组装成复杂对象。如果是串行调用的话响应时间会随着 rpc 调用次数呈线性增长,所以我们要优化性能一般会将串行改并行。
我们知道如果自己实现一套并行的模式还是比较麻烦的,gozero Mapreduce就可以帮助我们非常容易的实现这样的效果。让我们重点关注自己的业务逻辑的实现。
简单使用
这里需要提一下的时,gozero中还提供了线程数量的控制。可以让你自己控制并行处理的线程数,以免过多的线程对服务器造成过大的消耗。同时我们也可以传入自己的context来控制整个方法的超时和取消逻辑。这些都封装在类库中,不需要我们去操心。只需要传递对的参数就可以了。
下面就是一个比较简单的时候,读取一个数组并行加上“:1”最终输入改变后的数组对象。
package main
import (
"context"
"fmt"
"github.com/zeromicro/go-zero/core/mr"
"time"
)
func main() {
//要处理的数据
uid := []string{"a", "b", "c", "d", "e", "f"}
//传递数据的逻辑
generateFunc := func(source chan<- interface{}) {
for _, v := range uid {
source <- v
fmt.Println("source:", v)
}
}
// 处理数据的逻辑
mapFunc := func(item interface{}, writer mr.Writer, cancel func(err error)) {
tmp := item.(string) + ":1"
writer.Write(tmp)
fmt.Println("item:", item)
}
// 合并的数据逻辑
reducerFunc := func(pipe <-chan interface{}, writer mr.Writer, cancel func(err error)) {
var uid []string
for v := range pipe {
uid = append(uid, v.(string))
fmt.Println("pipe:", uid)
}
writer.Write(uid)
}
// 开始并发处理数据
// 超时时间
ctx, cl := context.WithTimeout(context.Background(), time.Second*3)
res, err := mr.MapReduce(generateFunc, mapFunc, reducerFunc, mr.WithContext(ctx))
//开启现成控制超时,如果超时则调用cl方法停止所有携程
go func() {
time.Sleep(time.Second * 2)
fmt.Println("cl")
cl()
}()
fmt.Println(res, err)
}
源码分析
首先我们先看下一整个函数的流程图,这个流程图是我自己的理解,如果有不对的地方请大家在评论区讨论。
我们看到整个流程中使用了大量的channel,并通过这些channel对整个比较复杂的流程进行控制。下面我们看下主要的三个步骤:
获取要处理的数据
我们看到这边的generate函数,这个函数的定义如下。函数的参数是一个channel,我们需要在这个函数中处理源数据,也就是我们准备处理的数据。比如上面例子中的一个数组,通过for循环把数据传输到这个管道中,让后续的逻辑去处理,当然这个过程中我们也可以对数据进行一些简单的预处理。
这个就是典型的函数式变成,把函数作为参数传入另一个函数中。
GenerateFunc func(source chan<- interface{})
func buildSource(generate GenerateFunc, panicChan *onceChan) chan interface{} {
source := make(chan interface{})
go func() {
defer func() {
if r := recover(); r != nil {
panicChan.write(r)
}
close(source)
}()
generate(source)
}()
return source
}
处理数据
在Mapreduce中最终要的就是并行处理逻辑,下面就是并行处理的核心逻辑。
通过一个pool的channel,来控制goroutine的数量。利用atomic的原子操作特性来控制现有携程数,最后用WaitGroup来等待左右携程的处理完毕。
这边我们重点需要关注的是其中众多的channel控制和函数中defer函数的处理逻辑。
控制逻辑select中如果ctx或者doneChan中有数据的话整个函数将会被停止。在携程还有空闲的时候我们会先往pool这个channel中放入一个数据相当于占用了一个携程,并且使用wg.Add(1)来计数。 在defer函数中我们需要吧wg.done(),扣除atomic中的数量并且把pool中吐出去一个数据。让之后的数据有更多的空余携程可以操作数据。
// 调用
go executeMappers(mapperContext{
ctx: options.ctx,
mapper: func(item interface{}, w Writer) {
mapper(item, w, cancel)
},
source: source,
panicChan: panicChan,
collector: collector,
doneChan: done,
workers: options.workers,
})
// 具体执行方法
func executeMappers(mCtx mapperContext) {
var wg sync.WaitGroup
defer func() {
wg.Wait()
close(mCtx.collector)
drain(mCtx.source)
}()
var failed int32
pool := make(chan lang.PlaceholderType, mCtx.workers)
writer := newGuardedWriter(mCtx.ctx, mCtx.collector, mCtx.doneChan)
for atomic.LoadInt32(&failed) == 0 {
select {
case <-mCtx.ctx.Done():
return
case <-mCtx.doneChan:
return
case pool <- lang.Placeholder:
item, ok := <-mCtx.source
if !ok {
<-pool
return
}
wg.Add(1)
go func() {
defer func() {
if r := recover(); r != nil {
atomic.AddInt32(&failed, 1)
mCtx.panicChan.write(r)
}
wg.Done()
<-pool
}()
mCtx.mapper(item, writer)
}()
}
}
}
归拢数据
在executeMappers函数不停的在往collector channel中写入数据的同时,reducer函数也在同事处理这些数据,在最后的时候把归拢的数据通过wirte来输送到output这个channel中。至此整个函数就结束了。
go func() {
defer func() {
drain(collector)
if r := recover(); r != nil {
panicChan.write(r)
}
finish()
}()
reducer(collector, writer, cancel)
}()
最终等待和输出
函数最后的select,其中有三个case。命中其中的任何一个都可以使得整个函数结束
- context的Done函数被处罚:当出入的context被cancel时候整个函数会停止运行
- panicChan中有数据传入:当函数中各个goroutine发生了无法进行下去的panic的时候可以往panicChan整个channel中传入数据。
output中有数据传入:这个channel,是在数据归拢的最后把最终需要输出的对象放入这个channel。也意味着整个函数的结束。
select { case <-options.ctx.Done(): cancel(context.DeadlineExceeded) return nil, context.DeadlineExceeded case v := <-panicChan.channel: panic(v) case v, ok := <-output: if err := retErr.Load(); err != nil { return nil, err } else if ok { return v, nil } else { return nil, ErrReduceNoOutput } }
总结
我们可以从上面的流程图和步骤分析中看到,这个Mapreduce包其实还是做了很多的判断和容错的,这些复杂的携程和携程之间的交互都是通过channel来实现的。如果我们要在业务逻辑中自己实现还是比较复杂的,但是如果直接使用gozero这个Mapreduce包的话还是比较容易理解的。
自己实现简配版本
我们知道看别人代码只要多debug几次还是比较容易能够理解的,但是想要真正理解还是得自己实现一下,哪怕是简配版本。下面是我阅读完源码之后自己简单的实现方法,可能比较粗糙和不完善有啥问题欢迎大家交流。
实现代码:
package main
import (
"context"
"fmt"
"sync"
"sync/atomic"
)
// 第一个方法用来往管道里存储要执行的事情 GenerateFunc
// 第二个方法用来执行方法 MapperFunc
// 第三个方法用来归并结果集 ReducerFunc
// MapReduce 逻辑简单介绍下:
//1. 要处理的数据放到一个无缓冲的管道里,再来一个协程从这个无缓冲的管道里读取要处理的数据,然后把读取出来的数据开一个协程用 传入的方法处理;
//2. 创建一个无缓冲的管道, 用来保存执行的结果, 让合并结果的协程从这个管道里读取数据, 然后合并数据, 写入合并数据的管道里
//3. 创建一个用来停止其它协程的管道, 如果执行中有什么错误就关闭这个管道里,同时停止执行其它协程,返回失败
//4. 最后要把没有关闭的管道关闭了
type (
GenerateFunc func(source chan<- interface{})
MapperFunc func(item interface{}, write Write)
ReducerFunc func(pipe chan interface{}, write Write)
)
type Write interface {
Write(val interface{})
}
type Writer struct {
Ch chan<- interface{}
}
func (w *Writer) Write(val interface{}) {
w.Ch <- val
}
func drain(channel <-chan interface{}) {
for range channel {
}
}
func executeMappers(threadNum int, ctx context.Context, ch chan interface{}, doneCh chan interface{}, fn MapperFunc, wr Write) {
var failed int32
pool := make(chan interface{}, threadNum)
wg := sync.WaitGroup{}
defer func() {
wg.Wait()
}()
for atomic.LoadInt32(&failed) == 0 {
select {
case <-ctx.Done():
return
case <-doneCh:
return
case pool <- 1:
item, ok := <-ch
if !ok {
return
}
wg.Add(1)
go func() {
defer func() {
if r := recover(); r != nil {
atomic.AddInt32(&failed, 1)
}
fn(item, wr)
wg.Done()
<-pool
}()
}()
}
}
}
func MapReduce(generateFunc GenerateFunc, mapperFunc MapperFunc, reducerFunc ReducerFunc, ctx context.Context) (interface{}, error) {
sourceChannel := make(chan interface{})
pipeChannel := make(chan interface{})
write := &Writer{Ch: pipeChannel}
// 来源
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("panic")
}
close(sourceChannel)
}()
generateFunc(sourceChannel)
}()
mapperChannel := make(chan interface{})
mapperWrite := &Writer{Ch: mapperChannel}
//wg := sync.WaitGroup{}
doneCh := make(chan interface{})
// 执行
go func() {
defer func() {
close(pipeChannel)
}()
executeMappers(10, ctx, sourceChannel, doneCh, mapperFunc, mapperWrite)
}()
// 汇总
var cancelOnce sync.Once
var mapperOnce sync.Once
var reducerOnce sync.Once
//var doneOnce sync.Once
closeChannel := func() {
cancelOnce.Do(func() {
close(sourceChannel)
})
mapperOnce.Do(func() {
close(mapperChannel)
})
reducerOnce.Do(func() {
close(pipeChannel)
})
doneCh <- 1
}
go func() {
defer func() {
closeChannel()
drain(pipeChannel)
}()
reducerFunc(mapperChannel, write)
}()
// 处理上下文线程
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("fun done****************************")
closeChannel()
return
}
}
}()
//time.Sleep(time.Second * 10)
select {
case <-ctx.Done():
fmt.Println("finish done")
return nil, nil
case v, ok := <-pipeChannel:
fmt.Println("finish resp:", v, "**ok:", ok)
return nil, nil
}
return nil, nil
}
调用方法:
package main
import (
"context"
"fmt"
"strconv"
"time"
)
func main() {
ctx, cl := context.WithCancel(context.Background())
fmt.Println(cl)
var list []string
for i := 0; i < 50; i++ {
//ctx.Done()
list = append(list, strconv.Itoa(i))
}
//传递数据的逻辑
a := func(source chan<- interface{}) {
for _, v := range list {
//time.Sleep(time.Millisecond * 300)
source <- v
fmt.Println("source:", v)
}
}
// 处理数据的逻辑
b := func(item interface{}, write Write) {
tmp := item.(string) + ":1"
fmt.Println("tmp:", tmp)
time.Sleep(time.Second)
write.Write(item)
}
c := func(pipe chan interface{}, write Write) {
for v := range pipe {
fmt.Println("reducerFunc:", v)
}
write.Write("finish")
}
//go func() {
// time.Sleep(time.Second * 5)
// cl()
//}()
MapReduce(a, b, c, ctx)
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。