Go必知必会系列:性能调优与Benchmark

作者:禅与计算机程序设计艺术

1.简介

1.1 性能调优概述

对于Web应用或者企业级应用来说,性能优化显得尤为重要。那么如何提升系统的处理能力、响应速度等就是一个非常重要的问题。这里,我将介绍一下性能调优的相关知识。

1.2 性能评估方法

首先,需要定义清楚我们所指的“性能”到底是什么?这个问题可能让人迷惑不解,因为不同的人的看法也许都不一样。举个例子,很多人认为性能表现为一个网站在百万PV下每秒可以处理的请求数量;而另一些人则把性能看作单次请求的延时。总之,无论采用哪种角度去衡量性能,都离不开对服务质量要求的考量。所以,首先需要明确自己所用的评估手段。

一般情况下,性能评估通常包括两个方面:

  • 流量:服务器能够承受的最大并发请求量(QPS);
  • 响应时间:请求完成的时间,即从客户端发送请求到接收响应的时间间隔。

除此之外,还可以通过压力测试来衡量系统的稳定性。压力测试是模拟高并发场景下的负载,通过工具检测系统是否能够持续提供可接受的服务质量。

所以,性能评估是一个综合性的过程,既要考虑流量(并发量)、响应时间,同时也要关注稳定性。因此,除了常规的性能分析工具外,还应结合业务场景,制定针对性的性能测试方案。

2.核心概念

2.1 CPU 和内存

CPU

CPU (Central Processing Unit) 是所有计算机中的中心控制器。它是美国南卡罗莱纳州立大学戴维斯电气工程系教授约翰·肖特科斯·博德曼·肯尼斯(John Snow Cooper)和亚利桑那大学马丁·科勒(Michael Collins)于1947年共同发明的。它的设计目标是为了达成计算的高速率、多核并行化的目的,并广泛应用于各类电子设备中。

CPU 可以执行很多运算指令,包括加减乘除、逻辑运算、移位、控制指令等。但是,由于设计初衷的不同,当时没有统一的指令集标准。而在当今的 x86/x64 指令集的帮助下,CPU 可以更好地进行运算。

CPU 的性能指标有:

  • 时钟频率:单位时间内能产生多少个脉冲信号,即处理指令的次数。如常见的主频为 1GHz 的 CPU 有 10亿个时钟周期/秒。
  • 每秒执行指令数 (IPC): 一秒钟能执行的指令数量,由 Clock Frequency / CPI 得到。CPI 表示每个时钟周期执行一次指令的次数。通常来说,越快的 CPU ,其 IPC 就越高。
  • 总线带宽:指数据传输速率。如,当前主流的 x86/x64 架构下,CPU 访问总线的数据总线宽度为 32 位或 64 位,因此,总线带宽取决于系统架构和硬件配置。
内存

内存(Memory)是指用来存放数据的存储器。其主要组成部分有两种:

  • 静态随机存取存储器 (SRAM):顾名思义,静态就是说里面不能改,随机存取就是每个位置可以存取任意值。SRAM 存在于北美及其他某些地区。
  • 动态随机存取存储器 (DRAM):DRAM 全称为动态随机存取存储器,顾名思义就是可以动态修改。也就是说,它可以像电脑显示屏一样刷新,每次显示的时候都会更新显示的内容。

内存的大小决定了系统能存储数据的容量,但同时也会影响系统的性能。因为内存较小,而且只能靠寄存器来做缓存,因此 SRAM 更适合作为 cache 来加速计算。而 DRAM 更适合长期存储和处理数据。

内存的性能指标有:

  • 容量:单位容量。例如 8GB 的内存。
  • 读写速率:单位时间内能读出或写入多少个字节数据。例如,DDR3 的速度为 400Mb/s 。

2.2 Goroutine 和线程

Goroutine

Goroutine 是 Go 语言中用于并发编程的轻量级线程。它与 OS Thread (操作系统线程)比较类似,但比 OS Thread 更小,占用空间更少。因此,在 Go 中创建越多的 goroutine ,效率就会越高。

每个 goroutine 都拥有自己的栈空间,并且只有在运行状态时才占用相应的 CPU 资源。由于 goroutine 之间共享内存,因此可以很方便地实现通信。但需要注意的是,不要滥用 goroutine ,否则会导致上下文切换过多,造成性能下降。

线程

线程是操作系统提供的用于并发编程的最基础机制。它实际上是进程中的一条执行路径。一个进程中可以创建多个线程,并且这些线程可以共享该进程中的堆和代码段。

线程提供了一种抽象层,使得多个线程可以被看作是独立的执行序列。每个线程都有一个私有的寄存器集合和栈,但线程之间可以共享全局变量和静态变量。

由于线程与其它资源相比,占用内存更少,因此适合用于 IO密集型任务。另外,使用线程可以避免多线程环境下复杂的锁、同步等问题。

2.3 异步 IO 和 事件驱动模型

异步 IO 模型

异步 IO (Asynchronous I/O) 模型是实现异步 I/O 的关键技术。它允许应用程序执行非阻塞 I/O 操作,不需要等待某个 I/O 操作结束后才能继续运行。这种方式能提高吞吐量和缩短响应时间。

在异步 IO 模型中,应用通过系统调用发起 I/O 请求,然后立即开始处理其他事情,待 I/O 完成后通知应用。因此,异步 IO 需要配合回调函数、消息队列等机制实现。

事件驱动模型

事件驱动模型 (Event-driven programming model) 使用消息队列来实现任务之间的解耦。应用注册感兴趣的事件,并在发生事件时通知主循环。这种模型的主要优点是简单、易维护、能充分利用多核CPU。

2.4 GC

GC (Garbage Collection) 是 Go 语言垃圾回收器的统称。它的作用是在运行过程中自动释放不再使用的内存,防止内存泄漏。

GC 主要采用三种算法:标记-清除、复制、标记-整理。其中,标记-清除算法和复制算法都是将不可达对象直接回收,而标记-整理算法则是将不可达对象移动到内存的一端。

GC 的触发条件有两种:

  • 手动触发:程序员手动调用 runtime.GC() 来触发。
  • 自适应:GC 自适应地调节运行时间,以减少暂停时间。

2.5 Channels

Channels 是 Go 语言提供的用于进程间通信的机制。它类似于管道,但具有更多功能。channels 可支持不同类型的信息,包括普通类型的值、channeled 函数调用结果等。

Channels 具备以下属性:

  • 消息发送:当向 channel 发送消息时,消息会被存放在 channel 的缓冲区中,直到被消费者接收。
  • 同步:channel 操作必须是同步的,意味着消息发送方必须等待消息被接收方读取后才能继续工作。
  • 缓冲区:缓冲区大小表示 channel 的容量。如果 channel 已满,新消息将被阻塞。

2.6 Mutex 和 Semaphore

Mutex (Mutual exclusion) 是保护临界资源的锁机制。任何时刻,只有一个线程可以持有锁。它可以防止多个线程并发访问共享资源。

Semaphore (信号量) 是用于限制对共享资源的访问个数的机制。它管理一个内部计数器,每当调用 acquire 方法时,计数器减一,当计数器为零时,则无法获取锁,直到其他线程调用 release 方法释放锁后,计数器才恢复。

2.7 TCP 和 UDP

TCP (Transmission Control Protocol) 是网络层协议,它是基于连接的协议,也就是说,在正式通信之前,客户机和服务器必须先建立连接。

UDP (User Datagram Protocol) 是网络层协议,它是无连接的协议,也就是说,在正式通信之前,客户机和服务器不需要先建立连接。

3.并发原语和模式

3.1 WaitGroup

WaitGroup 是 Go 语言提供的一个用于等待一组 goroutines 执行完毕的机制。它有一个计数器,表示一共需要等待多少个 goroutine 执行完毕。

通常,WaitGroup 与一个计数器一起使用,计数器记录 goroutine 的个数,每当一个 goroutine 完成任务后,调用 Done() 方法将计数器减一。当计数器变为零时,表示所有的 goroutine 已经完成任务。

典型的用法如下:

func worker(id int, wg *sync.WaitGroup) {
    // do some work
    time.Sleep(time.Second)

    fmt.Println("worker", id, "done")
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1) // increment the waitgroup counter

        go worker(i, &wg)
    }

    wg.Wait() // block until all workers are done

    fmt.Println("all workers are done")
}

3.2 协程池

协程池 (Coroutine Pool) 是一种用于控制协程数量的方法。它通过限制最大并发量来防止资源消耗过多。

具体地,在协程池的控制下,可以使用以下方法:

  • 初始化协程池,设置最大并发量。
  • 当有新的任务提交时,检查协程池剩余数量。如果超过最大数量,等待协程池中的协程执行完毕。
  • 创建新的协程,执行任务,释放当前协程。

典型的实现如下:

type Worker struct {
    ID     int    `json:"id"`
    Job    string `json:"job"`
    Result chan int `json:"result"`
}

var gPool = make(chan *Worker, maxConcurrentWorkers)

// Submit a new job to pool of workers and return immediately
func submitJob(w *Worker) {
    select {
    case gPool <- w:
        log.Printf("New job submitted with id=%d", w.ID)
    default:
        log.Printf("Max number of concurrent workers reached. Waiting...")
    }
}

// Create a new worker that waits in pool for tasks to execute
func createAndStartWorker() {
    worker := <-gPool
    go func() {
        result := runTask(worker.Job)
        worker.Result <- result // send back task result
        close(worker.Result)      // indicate we're done sending results
        gPool <- worker           // put worker back into pool
    }()
}

4.性能调优工具与流程

4.1 pprof

pprof (Profiling tool) 是 Go 语言提供的一个用于性能调试的工具。它提供了一个 HTTP 服务,可以在运行时获取当前程序的 CPU、内存、goroutine、线程、GC、block等信息。

使用方法如下:

  1. 在代码中引入 "net/http/pprof" 包。
  2. 在启动服务器前调用 http.ListenAndServe(":6060", nil),绑定监听地址和端口号。
  3. 通过浏览器打开网址 http://localhost:6060/debug/pprof,可以看到详细的性能信息。

4.2 基准测试

基准测试 (Benchmarks) 是对某些操作或方法的执行速度进行测量的测试。它用于了解代码的执行效率和瓶颈所在。

Go 提供了一个 benchmarking 包,用于编写并运行基准测试。benchmarking 包会根据指定的测试函数,生成一组随机输入,并重复多次执行。最后,输出的平均执行时间或性能指标会给出测试结果。

使用方法如下:

  1. 在源码目录下创建名为 "xx_test.go" 的测试文件。
  2. 在该文件中导入 testing 包。
  3. 根据需求编写测试函数,并添加 BenchmarkXXX 函数前缀。
  4. 运行命令 go test -bench="." 来运行测试。

4.3 Trace

Trace 是 Go 语言提供的用于跟踪程序执行情况的工具。它会捕获程序执行时的事件(例如,goroutine 创建、运行、阻塞、退出),并保存到文件中。

使用方法如下:

  1. 在源码目录下创建名为 "trace.out" 的空文件。
  2. 在启动服务器前调用 trace.Start(os.Stderr) 开启 trace。
  3. 在测试结束后调用 trace.Stop() 停止 trace。
  4. 查看 trace 文件。

猜你喜欢

转载自blog.csdn.net/universsky2015/article/details/133385285