记一次go-panic引发的消息堆积

背景

业务上使用生产-消费模式处理第三方上报的异步消息;job服务作为消费者,以常驻线程的方式,统一消费各个topic的消息(业务使用kafka做消息队列),然后通过同步调用业务接口使消息生效,数据流大致如下:

在这里插入图片描述

一条异常消息引发的惨案

一天中午,在uat环境出现队列消息堆积、数据无写入的现象,快速定位到job-service服务容器一直处于重启中,无法正常工作;通过重启日志,发现一个topic的消费者在消费某条消息时panic,导致整个job服务宕机,虽然discovery服务检测到心跳异常后不断重启,但是由于kafka的消费特性(没有commit的消息下次仍然会消费),导致每次重启都会消费到这条消息,继续panic,无限循环。。。

从而仅仅因为这一个topic的一条异常消息,导致整个服务的消费宕机(服务共消费了十多个topic的信息,绝大部分是业务核心消息),对于生产环境来说实在是可怕至极

事故中的反思

虽然(幸好)只是一次uat环境事故,也可以看出设计的缺陷;最起码,消息之间应该独立,改进方案这里不再过多赘述,但是触发整个系统宕机的原因颇引起我的重视 — panic

panic是golang语言所特有的功能,不像其他语言如java、python是通过exception的方式抛出错误,go的panic会触发整个程序的宕机,当然可以通过recover()来核销panic是系统可以正常运行,但是从我自己、身边同学、面试中面试者的反馈来看,很多同学对如何解决panic的方式不是特别清晰

panic触发及恢复场景

我通过查阅文档和实验,明确了各种情况下panic后,系统的反应情况

主线程触发panic

// 基础函数-简单for循环
func goroutineNormal(caller string) {
	for {
		tm := time.NewTicker(time.Second * 1)
		select {
		case <-tm.C:
			fmt.Printf("%s...\n", caller)
		}
	}
}

// 基础函数,主线程panic函数后执行
func laterTodo() {
	goroutineNormal("laterTodo")
}

// 测试函数
// 场景一:无recover
// 场景二:有recover
func testPanicInMain() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("recover...")
		}
	}()
	go goroutineNormal("testPanicInMain")
	time.Sleep(time.Second * 3)
	panic(errors.New("stop test1"))
	fmt.Println("after panic")
}

func main() {
	testPanicInMain()
	laterTodo()
}

场景一:主线程无recover

testPanicInMain...
testPanicInMain...
panic: stop test1

goroutine 1 [running]:
demo/foundation.testPanicInMain()
        /Users/liushi/Projects/go/golang-demo/foundation/panic.go:17 +0xce
demo/foundation.TestPanic()
        /Users/liushi/Projects/go/golang-demo/foundation/panic.go:62 +0x22
main.main()
        /Users/liushi/Projects/go/golang-demo/main.go:14 +0x20

Process finished with exit code 2
  1. 主线程崩溃,panic后所有逻辑不执行,异步协程退出

场景二:主线程有recover

testPanicInMain...
testPanicInMain...
recover...     // recover后,系统正常
testPanicInMain...
laterTodo...
testPanicInMain...
laterTodo...
testPanicInMain...
  1. 主线程恢复,异步协程无影响;
  2. 触发panic的函数内部:触发panic代码后的逻辑不执行
  3. 触发panic的函数外部:主函数其他逻辑正常执行

异步goroutine触发panic

// 测试函数
// 场景一:异步协程无recover
// 场景二:异步协程有recover
func goroutineWithPanic1() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("recover in goroutineWithPanic1...")
		}
	}()
	go goroutineNormal("goroutineWithPanicAsync")
	panic(errors.New("goroutineWithPanic1 error"))
	goroutineNormal("goroutineWithPanic1")
}

// 测试函数-
func goroutineWithPanic2() {
	goroutineNormal("goroutineWithPanic2")
}

// 测试函数
// 场景一:主线程函数无recover
// 场景二:主线程函数有recover
func testPanicInGoroutine() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("recover...")
		}
	}()
	go goroutineWithPanic1()
	go goroutineWithPanic2()
	fmt.Println("after panic")
}

func main() {
	testPanicInGoroutine()
	laterTodo()
}

场景一:主线程无recover,异步协程无recover

after panic
panic: goroutineWithPanic1 error

goroutine 19 [running]:
demo/foundation.goroutineWithPanic1()
        /Users/liushi/Projects/go/golang-demo/foundation/panic.go:39 +0xc0
created by demo/foundation.testPanicInGoroutine
        /Users/liushi/Projects/go/golang-demo/foundation/panic.go:27 +0x39

Process finished with exit code 2
  1. 程序崩溃,其他所有异步goroutine奔溃(包括goroutineWithPanicAsync和goroutineWithPanic2)

场景二:主函数无recover,异步协程有recover

after panic
recover in goroutineWithPanic1... // recover
goroutineWithPanicAsync...
goroutineWithPanic2...
laterTodo...
laterTodo...
goroutineWithPanic2...
goroutineWithPanicAsync...
  1. 主线程正常执行,其他异步协程正常执行
  2. 触发panic的异步内部:
    • 触发panic行后的代码逻辑不执行
    • 已执行的逻辑生效,已启动的异步协程继续执行
      • 异步协程中启动的协程挂在主线程下,触发panic的异步协程退出(???

场景三:主函数有recover,异步协程无recover

after panic
panic: goroutineWithPanic1 error

goroutine 6 [running]:
demo/foundation.goroutineWithPanic1()
        /Users/liushi/Projects/go/golang-demo/foundation/panic.go:39 +0xc0
created by demo/foundation.testPanicInGoroutine
        /Users/liushi/Projects/go/golang-demo/foundation/panic.go:27 +0x5b

Process finished with exit code 2
  1. 主函数崩溃,其他所有goroutine崩溃
    • 注意,panic无法跨协程传递,主线程的recover无法恢复子协程的panic,相关知识可以看下defer函数原理传送门

总结

了解了golang再触发和恢复panic的原理之后,触发上述问题的根因和解决方案也明朗起来:

  1. 原因:异步协程消费某条消息,触发panic,且异步协程没有recover,导致整个服务崩溃
  2. 解决方案:增加异步协程的recover,以消息为单位,异常消息触发panic后不影响常驻线程消费下一条消息,适当增加错误日志和异常消息的报警
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值