Baidu Programmer Development Guide to Avoiding Pit (Go Language)

In this issue, based on the actual problems encountered by front-line development students in the development process, we have extracted five tips about the Go language for your reference: Go Ballast for Golang performance optimization, benchmark+pprof for Golang performance analysis, and Golang single Pile testing skills, OOM of online services caused by locks, and memory synchronization problems in Go concurrent programming. Hope it can help you improve your technology~

01Go Ballast for Golang performance optimization

The most common method for Go GC optimization is to adjust the GC pacing to adjust the GC trigger frequency, which is mainly achieved by setting GOGC and setting debug.SetGCPercent().
Here is a brief description of the disadvantages of setting GOGC:

1. The GOGC setting ratio is imprecise, and it is difficult to precisely control the garbage collection threshold we want to trigger;

2. If the GOGC setting is too small, frequent triggering of GC will result in ineffective CPU waste;

3. When the memory occupied by the program itself is relatively low, the memory occupied by itself after each GC is also relatively low. If GOGC is set according to the GC pace of twice the heap after the last GC, this threshold can be easily triggered, so just It is easy for the program to cause additional consumption due to GC triggering;

4. If the GOGC setting is too large, assuming that these interfaces suddenly receive a large wave of traffic, OOM may be caused because the GC cannot be triggered for a long time;

Therefore, GOGC is not very friendly to some scenarios. Is there any way to accurately control memory so that it can accurately control GC when it is a multiple of 10G?

That's where Go ballast comes in. What is Go ballast, in fact, it is very simple to initialize a large slice whose life cycle runs through the entire Go application life cycle.

func main() {
ballast := make([]byte, 1010241024*1024) // 10G
// do something
runtime.KeepAlive(ballast)
}
复制代码

The above code initializes a ballast, and runtime.KeepAlive can ensure that the ballast will not be recycled by the GC.

Using this feature, it can be ensured that GC is triggered only when it is twice as large as 10G, so that the trigger timing of GOGC can be controlled more precisely.

02 benchmark+pprof for Golang performance analysis

在编写Golang代码时,可能由于编码不当,或者引入了一些耗时操作没注意,使得编写出来的代码性能比较差。这个时候,就面临着性能优化问题,需要快速找出“性能消耗大户”,发现性能瓶颈,快速进行针对性的优化。

Golang是一门优秀的语言,在性能分析上,也为我们提供了很好的工具。

通过Golang的benchmark + pprof能帮我们快速对代码进行性能分析,对代码的CPU和内存消耗进行分析。通过对CPU消耗的分析,快速找出CPU耗时较长的函数,针对性进行优化,提高程序运行性能。通过对内存消耗的分析,可找出代码中内存消耗较大的对象,也能进行内存泄露的排查。

benchmark是go testing测试框架提供的基准测试功能,对需要分析的代码进行基准测试,同时产出cpu profile和mem profile数据到文件,对这两个profile文件进行分析,可进行性能问题定位。

pprof是Golang自带的cpu和内存分析器,包含在go tool工具中,可对benchmark中产出的cpu profile和mem profile文件进行分析,定位性能瓶颈。

pprof可以在命令行中以交互式的方式进行性能分析,同时也提供了可视化的图形展示,在浏览器中进行分析,在使用可视化分析之前需要先安装graphviz。

pprof可视化分析页面展示的数据比较直观,也很贴心,对于CPU消耗大和内存消耗高的函数,标记的颜色会比较深,对应的图形也比较大,能让你一眼就找到他们。

分析中用到的benchmark test命令示例:

go test -bench BenchmarkFuncA -run none -benchmem -cpuprofile cpuprofile.o -memprofile memprofile.o
复制代码

分析中用到的pprof可视化查看命令示例:

go tool pprof -http=":8080" cpuprofile.o
复制代码

执行命令后,浏览器会自动打开分析页面页面,或者手动打开:

http://localhost:8080/ui/。

03Golang单测技巧之打桩

3.1 简介

在编写单测过程中,有的时候需要让指定的函数或实例方法返回特定的值,那么这时就需要进行打桩。它在运行时通过汇编语言重写可执行文件,将目标函数或方法的实现跳转到桩实现,其原理类似于热补丁。这里简要介绍下在Go语言中使用monkey进行打桩。

3.2 使用

3.2.1 安装

go get bou.ke/monkey
复制代码

3.2.2 函数打桩

对你需要进行打桩的函数使用monkey.Patch进行重写,以返回在单测中所需的条件依赖参数:

// func.go

func GetCurrentTimeNotice() string {
    hour := time.Now().Hour()
    if hour >= 5 && hour < 9 {
        return "一日之计在于晨,今天也要加油鸭!"
    } else if hour >= 9 && hour < 22 {
        return "好好搬砖..."
    } else {
        return "夜深了,早点休息"
    }
}
复制代码

当我们需要控制time.Now()返回值时,可以按照如下方式进行打桩:

// func_test.go

func TestGetCurrentTimeNotice(t *testing.T) {
    monkey.Patch(time.Now, func() time.Time {
        t, _ := time.Parse("2006-01-02 15:04:05", "2022-03-10 08:00:05")
        return t
    })
    got := GetCurrentTimeNotice()
    if !strings.Contains(got, "一日之计在于晨") {
        t.Errorf("not expectd, got: %s", got)
    }
    t.Logf("got: %s", got)
}
复制代码

3.2.3 实例方法打桩

业务代码实例如下:

// method.go

type User struct {
 Name string
 Birthday string
}

// GetAge 计算用户年龄
func (u *User) GetAge() int {
 t, err := time.Parse("2006-01-02", u.Birthday)
 if err != nil {
     return -1
 }
 return int(time.Now().Sub(t).Hours()/24.0)/365
}


// GetAgeNotice 获取用户年龄相关提示文案
func (u *User) GetAgeNotice() string {
    age := u.GetAge()
    if age <= 0 {
        return fmt.Sprintf("%s很神秘,我们还不了解ta。", u.Name)
    } 
    return fmt.Sprintf("%s今年%d岁了,ta是我们的朋友。", u.Name, age)
}
复制代码

当我们需要控制GetAgeNotice方法中调用的GetAge的返回值时,可以按如下方式进行打桩:

// method_test.go

func TestUser_GetAgeNotice(t *testing.T) {
 var u = &User{
  Name:     "xx",
  Birthday: "1993-12-20",
 }

 // 为对象方法打桩
 monkey.PatchInstanceMethod(reflect.TypeOf(u), "GetAge", func(*User)int {
  return 18
 })

 ret := u.GetAgeNotice()  // 内部调用u.GetAge方法时会返回18
 if !strings.Contains(ret, "朋友"){
  t.Fatal()
 }
}
复制代码

3.3 注意事项


使用monkey需要注意两点:

1. 它无法对已进行内联优化的函数进行打桩,因此在执行单测时,需要关闭Go语言的内联优化,执行方式如下:

go test -run=TestMyFunc -v -gcflags=-l
复制代码

2. 它不是线程安全的,不可用到并发的单测中。

04一次由锁引发的在线服务OOM

4.1 首先看一下问题代码示例

func service(){
    var a int
    lock := sync.Mutex{}
    {
     ...//业务逻辑
    }
    lock.Lock()
    if(a > 5){
        return 
    }
    {
     ...//业务逻辑
    }
    lock.UnLock()
}
复制代码

4.2 分析问题原因

RD同学在编写代码时,因为map是非线程安全的,所以引入了lock。但是当程序return的时候,未进行unlock,导致锁无法被释放,持续占用内存。在goroutine中,互斥锁被lock之后,没有进行unlock,会导致协程一直无法结束,直到请求超时,context cancel,因此以后在使用锁的时候,要多加小心,不在锁中进行io操作,且一定要保证对锁lock之后,有unlock操作。同时在上线时,多观察机器内存和cpu使用情况,在使用Go编写程序时关注goroutine的数量,避免过度创建导致内存泄露。

4.3 goroutine监控视角

图片

4.4 如何快速止损

首先联系OP对问题机房进行切流,然后马上回滚问题点所有上线单,先止损再解决问题。

4.5 可以改进的方式

程序设计阶段:大流量接口,程序设计不完善,考虑的case不够全面,未将机器性能考虑在内。

线下测试阶段:需要对大流量接口进行压测,大流量接口容易产生内存泄露导致的异常。

发布阶段:注意大流量接口上线时机器性能数据。

05Go并发编程时的内存同步问题

现代计算机对内存的写入操作会先缓存在处理器的本地缓存中,必要时才会刷回内存。

在这个前提下,当程序的运行环境中存在多个处理器,且多个 goroutine 分别跑在不同的处理器上时,就可能会出现因为处理器缓存没有及时刷新至内存,而导致其他 goroutine 读取到一个过期值。

如下面这个例子,虽然 goroutine A 和 goroutine B 对变量 X、Y 的访问并不涉及竞态的问题,但仍有可能出现意料之外的执行结果:

var x, y int
// A
go func() {
    x = 1
    fmt.Print("y:", y, " ")
}()

// B
go func() {
    y = 1                 
    fmt.Print("x:", x, " ")
}(
复制代码

上述代码可能出现的执行结果为:

y:0 x:1
x:0 y:1
x:1 y:1
y:1 x:1
x:0 y:0
y:0 x:0
复制代码

会出现最后两种情况原因是:goroutine 是串行一致的,但在不使用通道或者互斥量进行显式同步的情况下,多个 goroutine 看到的事件顺序并不一定是完全一致的。

即尽管 goroutine A 一定能够在读取 Y 之前感知到对 X 的写入,但他并不一定能够观测到其他 goroutine 对 Y 的写入,此时它就可能会输出一个 Y 的过期值。

故在上述使用场景时,为避免最后两种情况的出现,需要在读取变量前使用同步原语强制将处理器缓存中的数据刷回内存,保证任何 goroutine 都不会从处理器读到一个过期的缓存值:

var x, y int
var mu sync.RWMutex

go func() {
    mu.RLock() // 同步原语
    defer mu.RUnlock()
    x = 1
    fmt.Print("y:", y, " ")
}()

go func() {
    mu.RLock() // 同步原语
    defer mu.RUnlock()
    y = 1                 
    fmt.Print("x:", x, " ")
}()
复制代码

常用的Go同步原语:

sync.Mutex
sync.RWMutex
sync.WaitGroup
sync.Once
sync.Cond
复制代码

推荐阅读【技术加油站】系列:

百度程序员开发避坑指南(3)

百度程序员开发避坑指南(移动端篇)

百度程序员开发避坑指南(前端篇)

百度工程师教你快速提升研发效率小技巧

百度一线工程师浅谈日新月异的云原生

【技术加油站】揭秘百度智能测试规模化落地

【技术加油站】浅谈百度智能测试的三个阶段

Guess you like

Origin juejin.im/post/7086016767331401735