Guía de desarrollo del programador de Baidu para evitar pits (lenguaje Go)

En este número, en función de los problemas reales encontrados por los estudiantes de desarrollo de primera línea en el proceso de desarrollo, hemos extraído cinco consejos sobre el lenguaje Go para su referencia: Go Ballast para la optimización del rendimiento de Golang, benchmark+pprof para el análisis del rendimiento de Golang y Habilidades de prueba de pila única de Golang, OOM de servicios en línea causados ​​​​por bloqueos y problemas de sincronización de memoria en la programación concurrente de Go. Espero que pueda ayudarlo a mejorar su tecnología ~

01Go Ballast para la optimización del rendimiento de Golang

El método más común para la optimización de Go GC es ajustar el ritmo del GC para ajustar la frecuencia de activación del GC, lo que se logra principalmente configurando GOGC y configurando debug.SetGCPercent().
Aquí hay una breve descripción de las desventajas de configurar GOGC:

1. La relación de configuración de GOGC es imprecisa y es difícil controlar con precisión el umbral de recolección de basura que queremos activar;

2. Si la configuración de GOGC es demasiado pequeña, la activación frecuente de GC dará como resultado un desperdicio de CPU ineficaz;

3. Cuando la memoria ocupada por el programa en sí es relativamente baja, la memoria ocupada por sí mismo después de cada GC también es relativamente baja.Si GOGC se establece de acuerdo con el ritmo de GC del doble del montón después del último GC, este umbral se puede ajustar fácilmente. activado, por lo que es fácil que el programa provoque un consumo adicional debido a la activación del GC;

4. Si la configuración de GOGC es demasiado grande, suponiendo que estas interfaces reciban repentinamente una gran ola de tráfico, la OOM puede deberse a que el GC no se puede activar durante mucho tiempo;

Por lo tanto, GOGC no es muy amigable para algunos escenarios. ¿Hay alguna forma de controlar con precisión la memoria para que pueda controlar con precisión GC cuando es un múltiplo de 10G?

Ahí es donde entra en juego el lastre Go. Qué es Go lastre, de hecho, es muy simple inicializar una porción grande cuyo ciclo de vida se ejecuta a lo largo de todo el ciclo de vida de la aplicación Go.

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

El código anterior inicializa un balasto y el tiempo de ejecución. KeepAlive puede garantizar que el GC no recicle el balasto.

Con esta característica, se puede garantizar que GC se active solo cuando sea el doble de grande que 10G, de modo que el tiempo de activación de GOGC se pueda controlar con mayor precisión.

02 benchmark+pprof para el análisis de rendimiento 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)

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

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

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

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

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

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

Supongo que te gusta

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