Go后端优化实战:深入剖析接口性能提升与数据库访问策略

本文深入探讨了如何通过细致的接口分析和数据库访问优化,显著提升后端Go服务的响应速度。我们将详细介绍使用 nginx 日志分析确定性能瓶颈、利用 wrkpprof 工具进行性能测试和火焰图分析,以及实施有效的缓存策略和序列化优化。通过这些实战策略,我们成功降低了关键接口 saveDatarankInfo 的响应时间,显著提高了整体服务性能。这篇文章旨在为后端开发者提供一套全面的性能优化框架,帮助他们在面对类似挑战时能够迅速而有效地作出响应。

一、接口性能优化:深入分析与策略制定

在后端服务的稳定运行阶段,我们重点关注性能优化,以进一步提升系统效率和用户体验。利用 nginx 作为反向代理,我们实施了实时监控,密切关注接口调用的动态。通过执行 tail -f /var/log/nginx/access.log 命令,我们能够实时查看和分析接口日志。数据分析揭示了 saveDatarankInfo 这两个接口的调用频率高且响应时间较长,成为优化的重点目标。我们的主要任务是针对这两个接口实施优化措施,旨在降低它们的响应时间,从而全面提升服务性能和用户交互体验。

二、优化工具介绍

(一) wrk工具

  1. macOS 可以使用 brew install wrk 下载安装 wrk,编写rank_info.lua脚本命令如下:
lua 复制代码
wrk.method = "POST"
wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"
wrk.body = "token=%242a%2410%24PoyTfVudtRoigDmbu48SPbmQSWbI&version=1.5&game_id=1000122&signature=399e292f805e2f06b6ce26f40966bb67bbd0bef0&rank_type=11&board_type=1101"

在本地开启后端服务后,可以执行以下命令进行 rankInfo 的接口性能测试

bash 复制代码
wrk -t10 -d30s -c20 -s ./script/wrk/rank_info.lua <http://127.0.0.1:8080/v1.0/activity/rankInfo>
-- t 为线程数
-- d 为持续时间
-- c 为连接数
-- s 为脚本文件

测试结果如下

bash 复制代码
Running 30s test @ <http://127.0.0.1:8080/v1.0/activity/rankInfo>
  10 threads and 20 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    23.48ms   66.78ms 657.19ms   96.70%
    Req/Sec   165.20     25.31   212.00     83.04%
  47797 requests in 30.07s, 2.06GB read
Requests/sec:   1589.54
Transfer/sec:     70.04MB

可以用餐厅的运营来比喻 wrk 的输出结果,以便更通俗易懂地解释每个参数:

  1. 测试概况

    • Running 30s test @ <http://127.0.0.1:8080/v1.0/activity/rankInfo:这就像是在特定的餐厅(网址)进行为期> 30 秒的服务效率测试。
    • 10 threads and 20 connections:想象有 10 个服务员(线程)在服务 20 桌客人(连接)。
  2. 线程统计(Thread Stats)

    • Avg (平均延迟) :平均每桌客人等待服务的时间。
    • Stdev (标准偏差) :客人等待时间的波动程度,波动越大,服务质量越不稳定。
    • Max (最大延迟) :最长的一次服务等待时间,即最不满意的客人等待的时间。
    • +/- Stdev:大多数服务时间是否接近平均值。
  3. 请求/秒(Req/Sec)

    • Avg (平均) :平均每秒服务的桌数,反映餐厅的服务效率。
    • Stdev (标准偏差) :服务效率的波动,波动越大,表明服务质量不稳定。
    • Max (最大) :在最佳情况下,每秒能服务的最多桌数。
    • +/- Stdev:大部分时间里,服务的桌数是否接近平均值。
  4. 总结

    • 47797 requests in 30.07s:在 30.07 秒内,总共完成了 47797 次服务,类似于服务员总共服务的桌数。
    • 2.06GB read:总共处理了 2.06GB 的数据,好比服务员总共处理的订单量。
    • Requests/sec (每秒请求) :平均每秒完成的服务次数,这里是 1589.54,反映了餐厅的服务效率。
    • Transfer/sec (每秒传输) :平均每秒处理的数据量,这里是 70.04MB,类似于每秒传递的菜品总量。

(二)pprof 工具

可以通过 go-pprof-practice 及其README文档,通过实践学习掌握go tool pprof。在代码中引入 pprof,运行 wrk 后使用go tool pprof命令,可以分析查看缓存和CPU等指标的性能火焰图。

bash 复制代码
    go tool pprof -http=:8081 "http://localhost:8080/debug/pprof/heap?seconds=20"
    go tool pprof -http=:8082 "http://localhost:8080/debug/pprof/profile?seconds=20"
    go tool pprof -http=:8083 /Users/light/pprof/pprof.samples.cpu.004.pb.gz

三、优化 rankInfo 接口

(一)火焰图分析

使用 pprof 分析 rankInfo 接口的火山图,发现官方的 encoding/json 包序列化耗时严重。

通过替换为高性能JSON包 go-jsonjsoniter并做对比,最近决定引入 jsoniter 替换。

go 复制代码
import jsoniter "github.com/json-iterator/go"

var json = jsoniter.ConfigCompatibleWithStandardLibrary

再次使用火焰图查看反序列化耗时,同比减少 86.33%,优化效果显著。

(二)jsoniter 性能优化探究

1. 零内存分配

jsoniter 在处理 JSON 数据时,尽量减少内存分配。例如,它使用内部缓冲区来重用内存,避免了频繁的内存分配和释放。在源码中/go/pkg/mod/github.com/json-iterator/go@v1.1.12/stream.go实现:

go 复制代码
    type Stream struct {
        buf []byte // 内部缓冲区
        // ...
    }

    func (stream *Stream) WriteString(s string) {
        // 直接写入内部缓冲区,避免额外的内存分配
        stream.buf = append(stream.buf, s...)
    }

这种方法减少了垃圾回收的压力,提高了性能。

2. 延迟解析

jsoniter 实现了延迟解析机制,即在初始阶段不会解析所有数据,而是在需要时才进行解析。这在源码/go/pkg/mod/github.com/json-iterator/go@v1.1.12/any_number.go中体现为:

go 复制代码
    func (any *numberLazyAny) ToFloat64() float64 {
    	iter := any.cfg.BorrowIterator(any.buf)
    	defer any.cfg.ReturnIterator(iter)
    	val := iter.ReadFloat64()
    	if iter.Error != nil && iter.Error != io.EOF {
    		any.err = iter.Error
    	}
    	return val
    }

这个 ToFloat64 方法展示了延迟解析的一个关键特征:数据(在这个例子中是 any.buf 中的内容)直到 ToFloat64 方法被调用时才进行解析。这种方式可以提高效率,特别是在处理大型数据或在只有部分数据需要被解析的情况下。

  1. 创建迭代器并解析数据

    go 复制代码
        iter := any.cfg.BorrowIterator(any.buf)

    这行代码从配置 any.cfg 中借用一个迭代器,并使用 numberLazyAny 实例中的缓冲区 any.buf 作为数据源。这表明解析操作是在这一步开始的。

  2. 延迟返还迭代器

    go 复制代码
        defer any.cfg.ReturnIterator(iter)

    使用 defer 关键字确保在 ToFloat64 方法结束时返还迭代器。这是 Go 语言中常用的资源管理模式。

  3. 读取浮点数

    go 复制代码
        val := iter.ReadFloat64()

    这行代码实际执行了解析操作,将缓冲区中的数据解析为 float64 类型的值。

  4. 错误处理

    go 复制代码
        if iter.Error != nil && iter.Error != io.EOF {
         	any.err = iter.Error
         }

    如果在解析过程中遇到错误(除了 io.EOF,表示数据结束),则将错误记录在 numberLazyAny 实例的 err 字段中。

  5. 返回解析结果

    go 复制代码
        return val

    方法返回解析得到的浮点数值。

3. 循环展开

循环展开是一种常见的优化技术,它通过减少循环的迭代次数来提高代码的执行速度。在处理 JSON 数据时,jsoniter 可能会展开处理每个字符的循环,从而减少循环的开销。例如在源码/go/pkg/mod/github.com/json-iterator/go@v1.1.12/iter_int.go中看到类似以下的循环展开:

go 复制代码
    value := uint32(ind)
    if iter.tail-iter.head > 10 {
            i := iter.head
            ind2 := intDigits[iter.buf[i]]
            if ind2 == invalidCharForNumber {
                    iter.head = i
                    iter.assertInteger()
                    return value
            }
            i++
            ind3 := intDigits[iter.buf[i]]
            if ind3 == invalidCharForNumber {
                    iter.head = i
                    iter.assertInteger()
                    return value*10 + uint32(ind2)
            }
            ...
    }
  1. 初始条件检查

    go 复制代码
        if iter.tail-iter.head > 10 {

    这里检查缓冲区(iter.buf)中剩余未处理的字符数量是否足够多(大于 10),以进行循环展开。

  2. 逐个字符处理

    go 复制代码
        i := iter.head
        ind2 := intDigits[iter.buf[i]]

    从缓冲区的当前位置(iter.head)开始,取出一个字符并转换为数字(假设 intDigits 是一个将字符映射到数字的数组)。这个过程重复多次(ind3, ind4, ..., ind9),每次都检查字符是否有效并累加到 value 中。

  3. 累加到 value

    go 复制代码
        if ind2 == invalidCharForNumber {
            iter.head = i
            iter.assertInteger()
            return value
        }

    如果当前字符不是有效数字(比如遇到了非数字字符),则更新 iter.head(表示已处理到缓冲区的哪个位置),执行断言(可能是为了验证当前读取的内容是整数),并返回当前累积的 value

  4. 循环展开的累加

    go 复制代码
        value*100 + uint32(ind2)*10 + uint32(ind3)

    这里的代码通过乘以 10 的幂次来累加每个数字字符的值,这是手动展开循环的典型做法。例如,ind2 被乘以 10,ind3 被直接加上,等等。

这些优化技术的应用使得 jsoniter 在处理 JSON 数据时具有很高的效率,特别是在处理大型或复杂的 JSON 结构时。

(三)数据库访问优化

为了减少数据库访问带来的耗时,我们从业务和代码两个层面进行了优化:

  1. 业务层面的优化

    • 与前端团队协作,优化返回的参数,以减少对数据库的访问需求。
    • 调整缓存策略,将排行榜数据在缓存中的过期时间从原来的3秒延长至15秒,有效减轻数据库查询压力。
  2. 代码层面的优化

    • 将排行榜功能拆分为两个接口:一个用于查询所有用户的完整排行榜信息,另一个仅用于查询单个用户的排名信息。
    • 利用 Redis 实现基于分数和更新时间的二维排序。首先使用 ZRevRange 获取前500名用户的排名和分数,然后通过HMGet批量获取这些用户的更新时间,最后返回排序后的用户排名信息。相比之前逐个循环访问N次 Redis,降低为访问2次 Redis。
    • 对于全量用户排行榜的展示,首先通过 Redis 的 pipeline 获取已缓存的用户扩展信息。对于未命中缓存的用户ID,进行有限次的数据库查询(考虑到分库分表的情况),获取所需信息后存入缓存,再进行二维排序。

四、saveData 接口优化

  1. 缓存策略调整 :在用户登录时,通过 getData 接口预加载数据库数据到 Redis 缓存中,使得后续频繁调用的 saveData 接口能够直接命中缓存。
  2. 序列化性能提升 :替换标准的 encoding/json 包为更高效的jsoniter包,显著降低了数据序列化和反序列化的耗时。
  3. 接口日志分析:通过分析接口日志,发现并优化了直接访问数据库的查询请求。改为首次查询数据库,后续则优先查询缓存,以此减少数据库访问,提高接口响应速度。

五、优化结果

经过上述优化措施部署并测试后,我们通过 tail -f /var/log/nginx/access.log 再次监控接口的响应时间。结果显示,rankInfo 接口的平均响应时间降低了52.58%,saveData接口中位数响应时间降低了 11.76%。此外,得益于jsoniter包的全量接口改造,还有4个接口的响应时间优化了10%以上,另外4个接口的响应时间优化了5%以上。

相关推荐
Estar.Lee2 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
2401_857610034 小时前
SpringBoot社团管理:安全与维护
spring boot·后端·安全
凌冰_4 小时前
IDEA2023 SpringBoot整合MyBatis(三)
spring boot·后端·mybatis
码农飞飞5 小时前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货5 小时前
Rust 的简介
开发语言·后端·rust
monkey_meng5 小时前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
Estar.Lee5 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
新知图书6 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
盛夏绽放6 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
Ares-Wang7 小时前
Asp.net Core Hosted Service(托管服务) Timer (定时任务)
后端·asp.net