实践: 优化已有的 Go 程序,实践过程与思路 | 青训营

99 阅读3分钟

性能优化

1.是什么

在保持功能 不变 满足需求的前提下,优化程序的性能。

2.常见目标

减少执行时间,减少内存使用,提高系统吞吐量,提高系统可伸缩性;

-不同的目标之间往往有冲突

-满足一定约束条件下的多目标优化

3.优化思路

算法和数据结构;并发&并行;网络和磁盘IO;内存分配&垃圾回收;profiling;编译器优化;……

优化实践

本函数在一个api网关中,负责在需要调用client时为服务创建client。

在原有的程序中,我们发现每次需要调用客户端时都创建一个新的client对象,会导致大量的重复创建和销毁操作,降低了程序的效率。为了解决这个问题,我们决定引入一个缓冲池(Clients)来存储已经创建的client对象,以便在需要时直接从缓冲池中获取,而不是每次都创建新的对象。

func GetCli(serviceName string)(genericclient.Client, error) {
    ...//获取必要参数
    cli, err := genericclient.NewClient(serviceName, g, client.WithResolver(r), client.WithLoadBalancer(loadbalance.NewWeightedRandomBalancer()))
    return cli,nil
}

在优化后的函数中,我们首先检查缓冲池中是否已经存在所需的client对象。如果存在,我们直接返回该对象。如果不存在,我们调用UpdateCli函数来创建新的client对象,并将其存储到缓冲池中。这样,在后续的请求中,我们就可以直接从缓冲池中获取已经创建好的client对象,而无需再次创建。

func GetCli(serviceName string, idlVersion string) (genericclient.Client, error) {
    value, exist := Clients[serviceName]
    if exist {
        return value, nil
    } else {
        err := UpdateCli(serviceName)
        if err != nil {
            return nil, err
        }
        return Clients[serviceName], nil
    }
}

通过引入缓冲池,我们可以显著减少client对象的创建次数,从而提高程序的性能和效率。这种优化方法适用于存在大量重复请求的场景,通过复用已有对象,避免了不必要的资源消耗和性能损失。

同时,为了保证缓冲池中的client对象为最新,需要在参数变更时对缓冲池进行更新,在优化后的函数中,我们通过调用UpdateCli函数来更新缓冲池中的client对象。考虑到RPC调用的服务可能会更新,缓冲池中的client对象也会在服务端进行更新时同步进行更新并重新放入缓冲池。

优化工具

通过Go语言自带的 profiling 工具 pprof,可以分析程序运行时的 CPU、内存、阻塞等资源使用情况。

开启pprof

// your code here
}import (
"net/http"
_ "net/http/pprof" // injecting routing into http
)
func main() {
go http.ListenAndServe("localhost:8080", nil)
for {} 
}

优化方式

1.调用链图、火焰图

2.cpu 优化

3.heap 泄露

4.goroutine 泄露

例如

func copySlice(s []int) (r []int) {
    for i := 0; i < len(s); i++ {
        r = append(r, s[i])
    }
    return r
}

该函数使用了简单的迭代循环来遍历源切片 s 的元素,并通过 append 函数将每个元素追加到结果切片 r 中。最后,它返回结果切片 r屏幕截图 2023-08-17 094104.png

通过pprof的图表显示我们可以发现在通过 append 函数将每个元素追加到结果切片 r 中时消耗了较长时间

func copySlice(s []int) []int {
    r := make([]int, 0, len(s))
for i := 0; i < len(s); i++ {
    r = append(r, s[i])
}
    return r
}

新函数与原函数的主要区别在于它使用了 make 函数来创建一个容量为 len(s) 的切片 r,而不是直接声明一个空切片。这样做的目的是为了提前分配足够的内存空间,以避免在追加元素时进行多次重新分配内存的操作,从而提高性能。

屏幕截图 2023-08-17 094318.png

处理大型切片时,第二个函数的性能优势会更加显著。由于它提前分配了足够的内存空间,避免了多次重新分配的开销,因此能够更高效地复制大型切片。