Guia de desenvolvimento do programador Baidu para evitar o Pit (Go Language)

Nesta edição, com base nos problemas reais encontrados pelos alunos de desenvolvimento de linha de frente no processo de desenvolvimento, extraímos cinco dicas sobre a linguagem Go para sua referência: Go Ballast para otimização de desempenho de Golang, benchmark+pprof para análise de desempenho de Golang e Habilidades de teste de pilha única em Golang, OOM de serviços online causados ​​por bloqueios e problemas de sincronização de memória na programação simultânea em Go. Espero que possa ajudá-lo a melhorar sua tecnologia ~

01Go Lastro para otimização de desempenho de Golang

O método mais comum para a otimização do Go GC é ajustar o ritmo do GC para ajustar a frequência de disparo do GC, o que é obtido principalmente definindo GOGC e definindo debug.SetGCPercent().
Aqui está uma breve descrição das desvantagens de configurar o GOGC:

1. A proporção de configuração do GOGC é imprecisa e é difícil controlar com precisão o limite de coleta de lixo que queremos acionar;

2. Se a configuração do GOGC for muito pequena, o acionamento frequente do GC resultará em desperdício ineficaz da CPU;

3. Quando a memória ocupada pelo próprio programa é relativamente baixa, a memória ocupada por si mesmo após cada GC também é relativamente baixa. Se o GOGC for definido de acordo com o ritmo do GC de duas vezes o heap após o último GC, esse limite pode ser facilmente acionado, então é fácil para o programa causar consumo adicional devido ao acionamento do GC;

4. Se a configuração GOGC for muito grande, supondo que essas interfaces recebam repentinamente uma grande onda de tráfego, OOM pode ser causado porque o GC não pode ser acionado por um longo tempo;

Portanto, o GOGC não é muito amigável para alguns cenários. Existe alguma maneira de controlar com precisão a memória para que ele possa controlar com precisão o GC quando for um múltiplo de 10G?

É aí que entra o lastro Go. O que é lastro Go, na verdade, é muito simples inicializar uma grande fatia cujo ciclo de vida percorre todo o ciclo de vida do aplicativo Go.

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

O código acima inicializa um lastro e runtime.KeepAlive pode garantir que o lastro não seja reciclado pelo GC.

Usando esse recurso, pode-se garantir que o GC seja acionado apenas quando for duas vezes maior que 10G, para que o tempo de disparo do GOGC possa ser controlado com mais precisão.

02 benchmark+pprof para análise de desempenho de Golang

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

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

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

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

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

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

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

Acho que você gosta

Origin juejin.im/post/7086016767331401735
Recomendado
Clasificación