解决方案(3) 协程保障

前言

goroutine是golang的重点特色。由上层语言自实现的协程概念,在使用者层面,他的作用等价于线程,但是使用者不需要关注线程资源,线程调度,线程模型等底层原理。

使用一个go协程也非常简单

go func(){
    
    
    fmt.Println(111)
}()

尽管他的设计和实现都非常轻量,但是在实际团队项目里,因为错误的协程使用,仍然会给团队带来不少隐患,相信大家都遇见过以下问题:

  • 发现了内存泄漏问题,继而定位到了数量爆炸的goroutine数量。
  • 因为想要在业务里,增加一段其他业务的异步操作,结果该操作意外panic了,导致原app崩溃,触发重启。
  • 正常运行的app里,隐藏着大量僵尸协程,它们隐藏在pprof的goroutine簇里,很难定位。
  • 协程的堆栈非常难定位发起的位置,尤其是你将协程里的函数封装了(因为堆栈debug.Stack()的起点,是仍然该协程,而调用这个go f的位置,是另一个协程,导致调用的位置,不会在stack里被打印)。

那么,基于团队管理,必须提出一个有效的协程使用规范,使goroutine的使用可以安全可靠,并且,可以让使用者,规避掉上述的问题。

分析

  1. 为什么会出现因为协程积压而内存泄漏的问题,僵尸协程出现的原因是什么?

大部分协程混乱,是因为使用者,错估了协程的生存时间。一条本该立即执行完并销毁的协程,因为for{}循环,因为<-ch阻塞,因为漏写select{ case: time.After():}, 因为迟迟不返回的第三方调用结果,而卡在那里。

我敢说,绝大部分golang开发者,都没有关于此处卡住的报警(因为根本就不认为这里会卡住)。那么,在定位这个僵尸时,就会被动地等待
服务器内存报警 --> 发现内存泄漏 --> 查找内存泄漏app --> 查看app pprof --> 在混乱的goroutine簇去分析哪个协程出了问题

整个发现和处理的流程,缓慢,而且吃力。

对此,我们必须要求,使用协程的人,清楚,开启一条这样的协程,它预期的生命周期尾期是多少,一旦超过,立即报警, 当报警信息积压到每分钟几次后,触发通知。这样就能杜绝1和3的问题。

  1. 为什么会出现异步操作panic,导致节点崩溃和重启?

我们知道,go里,一旦出现panic操作,未经recover便会崩溃app。而问题在于,一个recover,只能修正所在协程的panic,当你go f 之后,原协程的recover是无法恢复f里面的panic的。

对此,我们强制,每一个go func(){}() 内部,必须加上recover机制。

而新的问题是,使用者,在使用go时出于以下两个原因,而不会加上recover。

扫描二维码关注公众号,回复: 12762407 查看本文章
  • 因贪图快速开发,懒得写。
  • 因对异步的业务代码过于自信,而认为不需要recover

所以,必须让这个recover机制,静默得在开启一个go协程生效。

至此,本次的解决方案,必须做到以下几点

  • 可以让协程,从发现是一个僵尸协程时,可以立即报警出来。而不是等待服务器挂掉,再来被动寻找。
  • 使用者不需要关注recover,调用时内部必须有recover机制。
  • 必须有设置 处理panic, 处理僵尸协程 的对外方法,以达到不同项目,不同解决方案都可以适配。

我们的目的是,从根本上,让新老组员,不会在goroutine上踩坑。

实现

仓库: https://github.com/fwhezfwhez/goroutine
镜像: https://gitee.com/fwhezfwhez/goroutine

原写法

go f()

现写法:

// An eternal goroutine with short deadline
	goroutine.ProtectedGo(f, goroutine.GoParam{
    
    
		UnqKey:               "test_protected_go_eternal_goroutine",
		ExpectedExpireSecond: -1,
		ShouldProtected:      true,
	})

当然,这里更推荐在使用前,进行个性化定制

goroutineUtil
     | - init.go
     | - util.go

init.go

package goroutineUtil

import (
	"fmt"
	"github.com/fwhezfwhez/errorx"
	"github.com/fwhezfwhez/goroutine"
	"runtime/debug"
	"your-app/handle-error/errs"
)

func init() {
    
    
	// config zombie storage time
	goroutine.ZombieStorageSeconds = 20
	// config zombie storage clear job interval
	goroutine.ZombieClearInterval = 30 * time.Second

	// specific method to handle panic in goroutine
	goroutine.HandlePanic = func(e interface{
    
    }) {
    
    
		errs.SaveError(errorx.NewFromStringf("%v \n %s", e, debug.Stack()), map[string]interface{
    
    }{
    
    
			"elem":  "xyx_srv:goroutine",
			"label": "xyx_srv:p0:panic",
			"tip":   "协程恐慌",
		})
	}

	// specific method to alert a bad goroutine.
	goroutine.HandleBadGs = func(gs *goroutine.Gs) {
    
    
		errs.SaveError(fmt.Errorf("僵尸协程,详情请见附加信息"), map[string]interface{
    
    }{
    
    
			"elem":  "xyx_srv:goroutine",
			"label": "xyx_srv:p0:leak",
			"tip":   "僵尸协程,内存泄露",
			"info":  gs.Info(),
		})
	}
}

util.go

package goroutineUtil

import "github.com/fwhezfwhez/goroutine"

func Go(f func(), param ...goroutine.GoParam) {
    
    
	goroutine.Go(f, param...)
}

至此,在你的项目里,有需要开启协程的地方

goroutineUtil.Go(func() {
    
    
			time.Sleep(5 * time.Second)   // 预期3秒,实际执行了5秒,会触发badGs报警
}, goroutine.GoParam{
    
    
	ShouldProtected:      true,
	ExpectedExpireSecond: 3,
})

最后,需要强调的是, 需要按照goroutine包的说明文档,注意它的注意事项:

  • gouroutine包,是面向业务线的协程使用规范,使用场景是所有业务开go的场景。
  • 这种安全性的协程在开销上,和轻便的go相比,会大。但是,它的开销和业务逻辑相比,那就可以基本忽略不计了。所以我们最好在业务逻辑里,使用这一套安全的goroutine。
  • 如果你是写底层框架,那么推荐还是使用官方的goroutine。

猜你喜欢

转载自blog.csdn.net/fwhezfwhez/article/details/114433755