SingleFlight导致慢请求的经历

问题描述

某服务上线后,其中一个http接口A连续多天出现了慢请求告警。通过查看日志(因为当时还没有引入调用链),判断是调用一个内部接口B时慢了,但是请求A接口 的慢请求数量远大于B接口的慢请求数量。经过和提供接口B的同事核对,以及增加详细日志,发现原因是我在调用接口时使用了singleflight,导致一个慢请求变 成多个慢请求。

下面,我们来使用代码模拟一下这个bug:

go 复制代码
package conscurrentdemo

import (
	"errors"
	"fmt"
	"log"
	"math/rand"
	"strconv"
	"sync"
	"sync/atomic"
	"testing"
	"time"
)
import "golang.org/x/sync/singleflight"

type UserInfo struct {
	ID     int    `json:"id"`
	Name   string `json:"name"`
	Mobile string `json:"mobile"`
	Email  string `json:"email"`
}

var sg singleflight.Group
var sleepCounter uint64 // 休眠计数器

// QueryUserInfo 查询用户信息
func QueryUserInfo(id int) (result UserInfo, err error) {
	defer func() {
		log.Printf("QueryUserInfo id:%d result:%v err:%v\n", id, result, err)
	}()
	// query api .....
	// 随机20%的概率休眠200ms
	if rand.Int31n(10000)%5 == 0 {
		time.Sleep(time.Millisecond * 200)
		atomic.AddUint64(&sleepCounter, 1)
	}

	result = UserInfo{
		ID:     id,
		Name:   "张三",
		Mobile: "14899990001",
		Email:  "xx@qq.com",
	}

	return
}

func GetUserInfoSingleFlight(id int) (UserInfo, error) {
	var result UserInfo

	v, err, _ := sg.Do("user_query_"+strconv.Itoa(id), func() (interface{}, error) {
		return QueryUserInfo(id)
	})

	if err != nil {
		return result, err
	}

	var ok bool
	if result, ok = v.(UserInfo); !ok {
		return result, errors.New("")
	}

	return result, nil
}

func TestSingleFlight(t *testing.T) {
	var wg sync.WaitGroup
	wg.Add(100)

	// 计数器
	var counter uint64

	for i := 0; i < 100; i++ {
		go func(id int) {
			defer wg.Done()

			t0 := time.Now()

			defer func() {
				// 统计耗费的时间>100ms的次数
				if time.Since(t0) > (time.Millisecond * 100) {
					atomic.AddUint64(&counter, 1)
				}
			}()

			info, err := GetUserInfoSingleFlight(id)

			if err != nil {
				log.Printf("query user info failed id:%d err:%v info:%v\n", id, err, info)
			}
		}((i)%2 + 1)
	}

	wg.Wait()

	fmt.Println("休眠次数:", sleepCounter)
	fmt.Println("耗时>100ms的请求个数:", counter)
}

运行结果: 我们可以看到,耗时请求次数远远大于休眠次数。

  • 由于引入了随机因子,所以产生的慢请求数量也可能是0。
  • 休眠200ms,统计的是>100ms的次数-故意的。

下面,我们来分析一下singleflight的源码。

SingleFlight原理分析

我们来看一下singleflight的Do方法的源码:

go 复制代码
// Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
// The return value shared indicates whether v was given to multiple callers.
// Do执行输入的方法并返回结果,并保证key相同时,在同一时刻只执行一次。在方法执行时,如果有重复的调用,调用者会等待。
// 返回的shared变量标识是否是多个调用者共用结果。
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
	g.mu.Lock()
	if g.m == nil {
		g.m = make(map[string]*call)
	}
	if c, ok := g.m[key]; ok {
		c.dups++
		g.mu.Unlock()
		c.wg.Wait()

		if e, ok := c.err.(*panicError); ok {
			panic(e)
		} else if c.err == errGoexit {
			runtime.Goexit()
		}
		return c.val, c.err, true
	}
	c := new(call)
	c.wg.Add(1)
	g.m[key] = c
	g.mu.Unlock()

	g.doCall(c, key, fn)
	return c.val, c.err, c.dups > 0
}

这个方法的核心,是WaitGroup,这个WaitGroup只有一个任务,有n个waiter。同一时刻,key相同时,如果有正在执行的调用,那么新的调用/请求都会等待。 这最终导致了一个慢请求变成多个慢请求。

结尾

这个问题出现在几年前,刚从php转go语言时。当时对并发编程不够理解,所以使用singleflight的方法不正确。singleflight是防缓存击穿利器,但是不是用来 防止缓存击穿,而是直接在调用接口时使用时,会导致慢请求增多,扩大请求的长尾效应,还不如直接调用接口。当时的解决方法是:去掉singleflight,直接调用B接口。修改之后,慢请 求数量大大减少。

为了提高接口性能,后来又采用了其他措施来提高接口性能:

  • 增加本地缓存,本地缓存失效时调用接口B。
  • 订阅相关变更消息,通过redis的pub/sub机制来更新本地缓存。

注:此文原载于本人个人网站,链接地址

本文由mdnice多平台发布

相关推荐
嚣张农民16 小时前
推荐3个实用的760°全景框架
前端·vue.js·程序员
梓羽玩Python17 小时前
推荐一款用了5年的全能下载神器:Motrix!全平台支持,不限速下载网盘文件就靠它!
程序员·开源·github
梓羽玩Python17 小时前
这款一站式AI体验平台值得收藏起来!GPT-4o、GPT-4o Mini、Claude 3.5 Sonnet免费使用!
人工智能·程序员·设计
前端宝哥1 天前
10 个超赞的开发者工具,助你轻松提升效率
前端·程序员
XinZong1 天前
【VSCode插件推荐】想准时下班,你需要codemoss的帮助,分享AI写代码的愉快体验,附详细安装教程
前端·程序员
Goboy2 天前
0帧起步:3分钟打造个人博客,让技术成长与职业发展齐头并进
程序员·开源·操作系统
JaxNext2 天前
不选总统选配色,这一票投给 CSS logo
前端·css·程序员
程序员鱼皮3 天前
刚毕业,去做边缘业务,还有救吗?
计算机·程序员·互联网·求职·简历
WujieLi3 天前
独立开发沉思录周刊:vol18.AI 正在成为无处不在的基础设施
程序员·设计·创业
_祝你今天愉快4 天前
重学Android:从位运算到二进制表示(零)
算法·程序员