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:  "[email protected]",
	}

	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多平台发布

相关推荐
陈随易3 小时前
VSCode v1.101发布,MCP极大增强关联万物,基于VSCode的操作系统雏形已初见端倪
前端·后端·程序员
redreamSo3 小时前
AI Daily | AI日报:Alexandr Wang加盟Meta,AI竞赛风云突变; Meta推V-JEPA 2,小扎抢人才; SwitchBot:国产AI破日本市场
程序员·aigc·资讯
一块plus6 小时前
Polkadot 的 Web3 哲学:从乔布斯到 Gavin Wood 的数字自由传承
人工智能·程序员·架构
京东云开发者6 小时前
Spring AI接入DeepSeek:快速打造微应用🚀🚀🚀
程序员
玩转AGI7 小时前
面试篇-一文搞定 Agent
人工智能·程序员·llm
老周聊大模型7 小时前
为什么顶尖AI团队都在重仓MCP?深度解读技术铁三角的底层逻辑
程序员·mcp
DeepSeek忠实粉丝7 小时前
Deepseek篇--开源技术3FS & smallpond详解
人工智能·程序员·llm
袁煦丞8 小时前
拯救你的碎片时光【4K电影自动投喂+全球追剧】NAS玩家新宠Nastool:cpolar内网穿透实验室第620个成功挑战
前端·程序员·远程工作
马可奥勒留18 小时前
睡前幻想——开篇词
程序员