《Go语言并发之道》学习笔记之第5章 大规模并发

130 篇文章 4 订阅
31 篇文章 31 订阅

《Go语言并发之道》学习笔记之第5章 大规模并发

异常传递

异常(error)如何在分布式系统中传递,问题最终如何呈现。
应该强制处理调用栈上关键点的异常,但系统控制流中忽视异常仍常见。

出现异常意味着系统进入了一个无法满足用户操作的状态,系统需要传达几个关键信息:

发生什么
异常事件的描述,如“磁盘已满”。
发生时间、发生位置
完整的栈轨迹信息(启动及出错位置),内部运行的上下文信息(分布式系统中识别发生异常机器的字段等),对应机器UTC时间。
用户友好信息
自定义用户异常信息,简单概述,最好一行内文本。
如何获得更多信息
提供ID,查询具体故障,详细日志。含异常完整信息:发生时间,完整堆栈调用。

异常分类:

  • Bug。
    系统未定义异常,或原生异常(极少遇到的异常)。
  • 已知信息(网络连接断开,磁盘读写失败等)。
命令行组件
中层组件
底层组件

以组件边界为例,所有传入异常信息都必须重新格式化。

func PostReport(id string) error {
	result, err := lowlevel.DoWork()
	if err != nil {
		if _, ok :=  err.(lowlevel.Error); ok {
			err = WrapErr(err, "cannot post report with id %q", id)
		}
	}
	// ...
}

最初异常包含太多底层信息(产生根源,goroutine,机器,栈轨迹等),必要时应当在模块边界转换为模块相关信息,可以清晰地划分异常类型。

当不规范的异常或bug传递时,应记录异常,并显示友好信息;应包含日志ID,用于查询更多信息。

创建格式良好的异常类型。

type MyError struct {
	Inner error
	Message string
	StackTrace string
	Misc map[string]interface{}
}

func wrapError(err error, messagef string, msgArgs ...interface{}) MyError {
	return MyError{
		Inner: err,
		Message: fmt.Sprintf(messagef, msgArgs...),
		StackTrace: string(debug.Stack())
		Misc: make(map[string]interface{}),
	}
}

func (err MyError) Error() string {
	return err.Message
}

创建底层模块。

type LowLevelErr struct {
	error
}

func isGloballyExec(path string) (bool, error) {
	info, err := os.Stat(path)
	if err != nil {
		return false, LowLevelErr{wrapError(err, err.Error())}
	}
	return info.Mode().Perm()&0100, nil

中间模块,调用底层模块。

type IntermediateErr struct {
	error
}

func runJob(id string) error {
	const jobBinPath = "/bad/job/binary"
	isExecutable, err := isGloballyExec(jobBinPath)
	
	if err != nil {
		return err
	}else if isExecutable == false {
		return wrapError(nil, "job binary is not executable")
	}
	return exec.Command(jobBinPath, "--id="+id).Run()
}

外层main函数调用中间层。

func handleError(key int, err error, message string) {
	log.SetPrefix(fmt.Sprintf("[logID: %v]", key))
	lgo.Printf("%$v", err)
	fmt.Printf("[%v] %v", key, message)
}

func main() {
	log.SetOutput(os.Stdout)
	log.SetFlags(log.Ltime|log.LUTC)
	err := runJob("1")
	if err != nil {
		msg := "There was an unexpected issue; please report this as a bug."
		if _, ok := err.(IntermediateErr); ok {
			msg = err.Error()
		}
		handleError(1, err, msg)
	}
}

中间层封装lowlevel模块的异常。

type IntermediateErr struct {
	error
}

func runJob(id string) error {
	const jobBinPath = "/bad/job/binary"
	isExecutable, err := isGloballyExec(jobBinPath)
	
	if err != nil {
		return IntermediateErr{wrapError(err, "cannot run job %q: requisite binaries are not available", id)}
	}else if isExecutable == false {
		return wrapError(nil, "cannot run job %q: requisite binaries are not executable", id)
	}
	return exec.Command(jobBinPath, "--id="+id).Run()
}

超时和取消

Timeouts和Cancellation

并发进程支持超时原因:

  • 系统饱和
    系统处理请求的能力刚好,超出请求返回超时,而不是长时间等待响应。何时超时的一般性指导:请求不太可能重复;无资源存储请求;系统响应或请求发送数据有时效性要求。
  • 陈旧数据
    并发进程处理数据的时间,超过数据窗口期(必须先处理更多相关数据或处理需求已过期),想返回超时并取消进程。context.WithDeadline或context.WithTimeout,context.WithCancel。
  • 试图防止死锁
    分布式等大型系统中,有时难以理解数据流动的方式,或者可能出现异常,为了保证系统不会发生死锁,建议所有并发操作中增加超时处理。

并发进程可能被取消原因:

  • 超时
  • 用户干预
    当用户使用并发程序时,有时允许用户取消他们已经开始的操作。
  • 父进程取消
    父进程停止,子进程也将被取消。
  • 复制请求
    数据发送到多个并发进程,以尝试获得更快响应,当第一个回来时,取消其余的进程。

并发进程的可抢占性。两个longCalculation间可以抢占。

reallyLongCalculation := func(done <-chan interface{}, value interface{}) interface{} {
	intermediateResult := longCalculation(value)
	select {
	case <-done:
		return nil
	default:
	}
	return longCalculation(intermediateResult)
}

定义并发进程可抢占的周期,确保运行周期比抢占周期长的功能本身都是可抢占的。简单方法是将goroutine代码段分解成小段。

reallyLongCalculation := func(done <-chan interface{}, value interface{}) interface{} {
	intermediateResult := longCalculation(done, value)
	return longCalculation(done, intermediateResult)
}

goroutine代码段分解成小段的潜在问题,当goroutine恰好修改共享状态(数据库、文件、内存数据结构等),goroutine取消时发生什么?如何回滚?。

修改三次状态

result := add(1, 2, 3)
writeTallyToState(result)
result = add(result, 4, 5, 6)
writeTallyToState(result)
result = add(result, 7, 8, 9)
writeTallyToState(result)

修改一次状态

result := add(1, 2, 3, 4, 5, 6, 7, 8, 9)
writeTallyToState(result)

重复消息。假设一个pipeline有三个阶段:生成阶段,阶段A和阶段B。生成阶段通过记录上一次channel被读取的时间,来监控阶段A持续的时间。若当前实例不正常,则产生新实例A2,阶段B可能收到重复消息。

避免发送重复消息的方法。最简单的方法是让一个父goroutine在子goroutine已经发送完结果之后发送一个取消信号,需要各阶段间的双向通信。其他方法:

  • 接收到的第一个或最后一个消息
    算法允许或并发进程幂等(可重复调用,在调用方多次调用的情况下,最终得到的结果是一致的。),可以允许下游进程存在重复消息,并从接收到的第一个或最后一个消息中挑选一个处理。
  • 向父goroutine确认权限
    与父goroutine使用双向通信来确认发送消息的权限,类似心跳。明确请求允许在B的channel上执行写入操作,比心跳更安全,也更复杂。

心跳

心跳是并发进程向外界发出信号的一种方式。

两种类型心跳:

  • 在一段时间间隔内发出的心跳
  • 在工作单元开始时发出的心跳

在一段时间间隔内发出的心跳对于处于等待事件触发状态的goroutine很有用,它告诉监听程序一切安好。

doWork := func(done <-chan interface{}, pulseInterval time.Time) (<-chan interface{}, <-chan time.Time) {
	heartbeat := make(chan interface{})
	results := make(chan time.Time)
	go func() {
		defer close(heartbeat)
		defer close(results)
		
		pulse := time.Tick(pulseInterval)
		workGen := time.Tick(2*pulseInterval)
		
		sendPulse := func() {
			select {
			case heartbeat <-struct{}{}:
			default:
			}
		}
		
		sendResult := func(r time.Time) {
			for {
				select {
				case <-done:
					return
				case <-pulse:
					sendPulse()
				case results <- r:
					return
				}
			}
		}
		
		for {
			select {
			case <-done:
				return
			case <-pulse:
				sendPulse()
			case r := <-workGen:
				sendResult(r)
			}
		}
	}()
	return heartbeat, results
}

done := make(chan interface{})
time.AfterFunc(10*time.Second, func(){close(done)})

const timeout = 2*time.Second
heartbeat, results := doWork(done, timeout/2)
for {
	select {
	case _, ok := <-heartbeat:
		if ok == false {
			return
		}
		fmt.Println("pulse")
	case r, ok := <-results:
		if ok == false {
			return
		}
		fmt.Printf("results %v\n", r.Second())
	case <-time.After(timeout):
		return
	}
}

心跳可以用来收集关于空闲时间的统计数据。心跳让我们知道长时间运行的goroutine依然正常工作着,但需要时间运行,计算出值并发送给channel。

不关闭任何channel,模拟产生异常的goroutine,通过心跳避免了死锁,且不需要依赖更长的超时时间来保持确定性。

doWork := func(done <-chan interface{}, pulseInterval time.Time) (<-chan interface{}, <-chan time.Time) {
	heartbeat := make(chan interface{})
	results := make(chan time.Time)
	go func() {
		//defer close(heartbeat)
		//defer close(results)
		
		pulse := time.Tick(pulseInterval)
		workGen := time.Tick(2*pulseInterval)
		
		sendPulse := func() {
			select {
			case heartbeat <-struct{}{}:
			default:
			}
		}
		
		sendResult := func(r time.Time) {
			for {
				select {
				//case <-done:return
				case <-pulse:
					sendPulse()
				case results <- r:
					return
				}
			}
		}
		
		for i := 0; i<2; i++ {
			select {
			case <-done:
				return
			case <-pulse:
				sendPulse()
			case r := <-workGen:
				sendResult(r)
			}
		}
	}()
	return heartbeat, results
}

done := make(chan interface{})
time.AfterFunc(10*time.Second, func(){close(done)})

const timeout = 2*time.Second
heartbeat, results := doWork(done, timeout/2)
for {
	select {
	case _, ok := <-heartbeat:
		if ok == false {
			return
		}
		fmt.Println("pulse")
	case r, ok := <-results:
		if ok == false {
			return
		}
		fmt.Printf("results %v\n", r.Second())
	case <-time.After(timeout):
		fmt.Printfln("worker goroutine is not healthy!")
		return
	}
}

一个工作单元开始时发出的心跳,对于测试来说非常有效。

package main

import (
	"fmt"
	"math/rand"
)

func main() {
	doWork := func(done <-chan interface{}) (<-chan interface{}, <-chan int) {
		heartbeatStream := make(chan interface{}, 1) // <1>
		workStream := make(chan int)
		go func() {
			defer close(heartbeatStream)
			defer close(workStream)

			for i := 0; i < 10; i++ {
				select { // <2>
				case heartbeatStream <- struct{}{}:
				default: // <3>
				}

				select {
				case <-done:
					return
				case workStream <- rand.Intn(10):
				}
			}
		}()

		return heartbeatStream, workStream
	}

	done := make(chan interface{})
	defer close(done)

	heartbeat, results := doWork(done)
	for {
		select {
		case _, ok := <-heartbeat:
			if ok {
				fmt.Println("pulse")
			} else {
				return
			}
		case r, ok := <-results:
			if ok {
				fmt.Printf("results %v\n", r)
			} else {
				return
			}
		}
	}
}

不那么好的测试样例,删除time.Sleep的话,测试有时会通过,有时失败,非确定性。一些外部因素(CPU负载过高、磁盘抢占、网络延迟等)会导致goroutine花费更长时间进行第一次迭代。

package main

import (
	"testing"
	"time"
)

func DoWork(
	done <-chan interface{},
	nums ...int,
) (<-chan interface{}, <-chan int) {
	heartbeat := make(chan interface{}, 1)
	intStream := make(chan int)
	go func() {
		defer close(heartbeat)
		defer close(intStream)

		time.Sleep(2 * time.Second) // 模拟goroutine开始前的某种延迟。

		for _, n := range nums {
			select {
			case heartbeat <- struct{}{}:
			default:
			}

			select {
			case <-done:
				return
			case intStream <- n:
			}
		}
	}()

	return heartbeat, intStream
}
func TestDoWork_GeneratesAllNumbers(t *testing.T) {
	done := make(chan interface{})
	defer close(done)

	intSlice := []int{0, 1, 2, 3, 5}
	_, results := DoWork(done, intSlice...)

	for i, expected := range intSlice {
		select {
		case r := <-results:
			if r != expected {
				t.Errorf(
					"index %v: expected %v, but received %v,",
					i,
					expected,
					r,
				)
			}
		case <-time.After(1 * time.Second): // <1>
			t.Fatal("test timed out")
		}
	}
}

好的测试样例。有了心跳,可以安全的编写测试而不需要加入超时机制。

package main

import (
	"testing"
	"time"
)

func DoWork(
	done <-chan interface{},
	pulseInterval time.Duration,
	nums ...int,
) (<-chan interface{}, <-chan int) {
	heartbeat := make(chan interface{}, 1)
	intStream := make(chan int)
	go func() {
		defer close(heartbeat)
		defer close(intStream)

		time.Sleep(2 * time.Second)

		pulse := time.Tick(pulseInterval)
	numLoop: // <2>
		for _, n := range nums {
			for { // <1>
				select {
				case <-done:
					return
				case <-pulse:
					select {
					case heartbeat <- struct{}{}:
					default:
					}
				case intStream <- n:
					continue numLoop // <3>
				}
			}
		}
	}()

	return heartbeat, intStream
}

func TestDoWork_GeneratesAllNumbers(t *testing.T) {
	done := make(chan interface{})
	defer close(done)

	intSlice := []int{0, 1, 2, 3, 5}
	const timeout = 2 * time.Second
	heartbeat, results := DoWork(done, timeout/2, intSlice...)

	<-heartbeat // <4>

	i := 0
	for {
		select {
		case r, ok := <-results:
			if ok == false {
				return
			} else if expected := intSlice[i]; r != expected {
				t.Errorf("index %v: expected %v, but received %v,", i, expected, r)
			}
			i++
		case <-heartbeat: // <5>
		case <-time.After(timeout):
			t.Fatal("test timed out")
		}
	}
}

复制请求

将请求分发到多个处理程序(goroutine,进程,服务器等),返回响应最快的结果,消耗更多资源。

package main

import (
	"fmt"
	"sync"
	"time"
	"math/rand"
)

func main() {
	doWork := func(
		done <-chan interface{},
		id int,
		wg *sync.WaitGroup,
		result chan<- int,
	) {
		defer wg.Done()

		started := time.Now()

		simulatedLoadTime := time.Duration(1+rand.Intn(5)) * time.Second
		select {
		case <-done:
		case <-time.After(simulatedLoadTime):
		}

		select {
		case <-done:
		case result <- id:
		}
		
		took := time.Since(started)
		if took<simulatedLoadTime{
			took = simulatedLoadTime
		}

		fmt.Printf("%v took %v\n", id, took)
	}

	done := make(chan interface{})
	result := make(chan int)

	var wg sync.WaitGroup
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go doWork(done, i, &wg, result)
	}

	firstReturned := <-result
	close(done)
	wg.Wait()

	fmt.Printf("Received an an answer from #%v\n", firstReturned)
}

速率限制

它限制了某种资源(API连接、磁盘读写、网络包、异常等)在某段时间内被访问的次数。
系统限速,可以避免系统被攻击。合法用户,大量级执行操作或运行代码异常,也可能会降低系统的可用性。用户队系统的访问应当被沙盒化。

速率限制允许你将系统的性能和稳定性平衡在可控范围内。在大量测试和等待后,可以以可控的方式扩大限制。

大多数限速基于令牌桶算法。

治愈异常的goroutine

小结

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值