本期咱们依据一线开发的同学在开发过程中遇到的理论问题,提炼进去五个对于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 somethingruntime.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.gofunc 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.gofunc 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.gotype 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.gofunc 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// Ago func() { x = 1 fmt.Print("y:", y, " ")}()// Bgo func() { y = 1 fmt.Print("x:", x, " ")}(
上述代码可能呈现的执行后果为:
y:0 x:1x:0 y:1x:1 y:1y:1 x:1x:0 y:0y:0 x:0
会呈现最初两种状况起因是:goroutine 是串行统一的,但在不应用通道或者互斥量进行显式同步的状况下,多个 goroutine 看到的事件程序并不一定是完全一致的。
即只管 goroutine A 肯定可能在读取 Y 之前感知到对 X 的写入,但他并不一定可能观测到其余 goroutine 对 Y 的写入,此时它就可能会输入一个 Y 的过期值。
故在上述应用场景时,为防止最初两种状况的呈现,须要在读取变量前应用同步原语强制将处理器缓存中的数据刷回内存,保障任何 goroutine 都不会从处理器读到一个过期的缓存值:
var x, y intvar mu sync.RWMutexgo 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.Mutexsync.RWMutexsync.WaitGroupsync.Oncesync.Cond
举荐浏览【技术加油站】系列:
百度程序员开发避坑指南(3)
百度程序员开发避坑指南(挪动端篇)
百度程序员开发避坑指南(前端篇)
百度工程师教你疾速晋升研发效率小技巧
百度一线工程师浅谈突飞猛进的云原生
【技术加油站】揭秘百度智能测试规模化落地
【技术加油站】浅谈百度智能测试的三个阶段