关于后端:百度程序员开发避坑指南Go语言篇

6次阅读

共计 4762 个字符,预计需要花费 12 分钟才能阅读完成。

本期咱们依据一线开发的同学在开发过程中遇到的理论问题,提炼进去五个对于 Go 语言的小技巧,供大家参考:Golang 性能优化之 Go Ballast、Golang 性能剖析之 benchmark+pprof、Golang 单测技巧之打桩、一次由锁引发的在线服务 OOM、Go 并发编程时的内存同步问题。心愿能为你的技术晋升助力~

01Golang 性能优化之 Go Ballast

对于 Go GC 优化的伎俩比拟常见的伎俩就是通过调整 GC 的步调,以调整 GC 的触发频率,次要通过设置 GOGC、设置 debug.SetGCPercent() 的形式实现。
这里简略说下设置 GOGC 的弊病:

1. GOGC 设置比率不准确,很难准确的管制咱们想要触发的垃圾回收阈值;

2. GOGC 设置过小,频繁触发 GC 就会导致有效的 CPU 节约;

3. 程序自身占用内存比拟低时,每次 GC 之后自身占用内存也比拟低,如果依照上次 GC 后的 heap 的一倍的 GC 步调来设置 GOGC 的话,这个阈值很容易就可能触发,于是就很容易呈现程序因为 GC 的触发导致额定的耗费;

4. GOGC 设置的过大,假如这些接口忽然承受到一大波流量,因为长时间无奈触发 GC 可能导致 OOM;

由此,GOGC 对于某些场景并不是很敌对,那有没有可能准确管制内存,让其在 10G 的倍数时精确管制 GC 呢?

这就须要 Go ballast 出场了。什么是 Go ballast,其实很简略就是初始化一个生命周期贯通整个 Go 利用生命周期的超大 slice。

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

下面的代码就初始化了一个 ballast,runtime.KeepAlive 能够保障 ballast 不会被 GC 给回收掉。

利用这个个性,就能保障 GC 在 10G 的一倍时才被触发,这样就可能比拟精准管制 GOGC 的触发机会。

02Golang 性能剖析之 benchmark+pprof

在编写 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)

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

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

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

百度一线工程师浅谈突飞猛进的云原生

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

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

正文完
 0