性能优化是软件项目开发过程中的一个永恒话题。我们不断地翻古书、找资料、访道友,不断提升,慢慢练成属于自己的七十二绝技。
本文总结了 10 种主流通用的接口性能优化手段,每一种都是经典方案,值得你的点赞转发收藏,一键三连!
关注腾讯云开发者,一手技术干货提前解锁👇
性能优化是软件项目开发过程中的一个永恒话题。
随着功能迭代,复杂度不断增加,同时伴随着流量、数据不断增长,接口性能可能会逐渐下降,尤其是在高并发场景,性能问题就更容易暴露出来。这时,我们也不能闲着。开始翻古书、找资料、访道友,不断提升,慢慢练成属于自己的七十二绝技。
本文主要总结了日常开发中一些通用的优化手段,以期对日后的开发有所裨益。
2.1 业务场景
在日常开发中,尤其是在 web 应用开发中,我们经常需要对数据的合法性进行验证。为了实现这一目的,我们通常会对参数进行一些前置验证。这些验证规则可以包括必填项、范围、格式、正则表达式、安全性以及自定义规则等。
通常,为简化业务逻辑,我们会借助一些第三方工具来进行这些通用性的检测。
2.2 案例
⓵ Protocol Buffer Validation
如果是基于 pb 协议,可以启用 protoc-gen-validate (PGV) 自动化数据校验插件。配置规则如:
强校验 title
字段长度在 1 ~ 100 个字符:
string title = 1 [(validate.rules).string = {min_len: 1, max_len: 100 }];
一般地,保存数据库之前,为防止溢出,可对其长度做前置检查。
《约束规则》支持的类型有 Numerics、Bools、Strings、Bytes、Enums、Messages、Repeated、Maps 等。
⓶ Go Struct and Field validation
对于非 pb 定义的结构,也有一些类似的组件实现自动化校验。如 Go Struct and Field validation ,基本用法如下:
// User contains user information
type User struct {
FirstName string `validate:"required"`
LastName string `validate:"required"`
Age uint8 `validate:"gte=0,lte=130"`
Email string `validate:"required,email"`
Gender string `validate:"oneof=male female prefer_not_to"`
FavouriteColor string `validate:"iscolor"` // alias for 'hexcolor|rgb|rgba|hsl|hsla'
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
}
详细参考 《常用的验证》。如果预置的 valadator
不满足需求,也可以自定义 validator
。
https://github.com/go-playground/validator?tab=readme-ov-file#baked-in-validations
2.3 小结
谚云:防御不到位,上线跑断腿
防御性设计是考虑使用者可能会错误使用的情况,从设计上避免错误使用,或是降低错误使用的机会。防御性设计可以让软件更安全、可靠,更方便地找到使用者的错误。
3.1 业务场景
N+1
查询问题指的是当查询一个对象的列表数据的时候,会首先查询列表中的对象的 ID
,然后循环生成单独的 SQL/RPC
去查询对象的详细数据。这会导致 SQL/RPC
查询过多问题。
在一个循环内多次执行 RPC
调用或者数据库操作。数据量小的时候问题不大,能跑起来。随着业务的发展,数据量越来越大,或者要查询的 id
越来越多(特别是未加限制的时候),耗时部题可想而知,长尾会越来越多。
3.2 案例
⓵ 循环中的 RPC
读取多条记录时在 for
循环中去分别读取单行。
for _, id := range ids {
record := GetDetail(id)
// do something ...
}
解决方案:改批量(一次从存储中取出所有 id 的结果)
records := GetDetails(ids)
// do something ...
3.3 小结
谚云:积羽沉舟,群轻折轴
上述场景是一个典型的 N+1
问题,不限于读取,写入亦然。它可能导致性能问题和增加数据库负载。
为了解决 N+1
问题,开发人员可以使用一些技术,如批量加载(batch loading)、批量更新(Bulk Updates),从而减少请求次数。通过优化数据库查询和加载策略,开发人员可以避免 N+1
问题,并提高应用程序的效率。
异步思想:解决长耗时问题
异步思想是一种解决长耗时问题的方法,它通过将耗时的操作放在后台进行,不阻塞主线程或其他任务的执行,从而提高系统的响应性能和并发处理能力。
4.1 业务场景
在处理一些复杂的业务场景时,对于部分操作考虑使用异步,可以大幅降低接口耗时。
比如,在做服务性能优化时,可以将如数据上报、流水日志等做异步处理,以降低接口时延。用户上传图片后的审核,音视频的合成等等。
4.2 案例
⓵ 子过程改异步、协程
以文本配音(TTS)为例,【合成音频】和【添加音效】这两个子过程耗耗时比较长:https://kf.zenvideo.qq.com/help/doc?id=dcccf9045b50dca3
我们可以把耗时长的部分封装到一个异步任务中,并生成一个任务 ID,后续可以查询处理进度和结果。音频生成部分改为异步任务是因为该子过程是文本配音的关键路径(主流程、耗时长),对非关键路径如【数据埋点】直接改为协程处理即可:
⓶ 异步在数据库、消息队列的应用
异步处理在数据库中同样应用广泛,例如 Redis 的 bgsave,bgrewriteof 就是分别用来异步保存 RDB 跟 AOF 文件的命令,bgsave 执行后会立刻返回成功,主线程 fork 出一个线程用来将内存中数据生成快照保存到磁盘,而主线程继续执行客户端命令;Redis 删除 key 的方式有 del 跟 unlink 两种,对于 del 命令是同步删除,直接释放内存,当遇到大 key 时,删除操作会让 Redis 出现卡顿的问题,而 unlink 是异步删除的方式,执行后对于 key 只做不可达的标识,对于内存的回收由异步线程回收,不阻塞主线程。
MySQL 的主从同步支持异步复制、同步复制跟半同步复制。异步复制是指主库执行完提交的事务后立刻将结果返回给客户端,并不关心从库是否已经同步了数据;同步复制是指主库执行完提交的事务,所有的从库都执行了该事务才将结果返回给客户端;半同步复制指主库执行完后,至少一个从库接收并执行了事务才返回给客户端。有多种主要是因为异步复制客户端写入性能高,但是存在丢数据的风险,在数据一致性要求不高的场景下可以采用,同步方式写入性能差,适合在数据一致性要求高的场景使用。
此外,对 Kafka 的生产者跟消费者都可以采用异步的方式进行发送跟消费消息,但是采用异步的方式有可能会导致出现丢消息的问题。对于异步发送消息可以采用带有回调函数的方式,当发送失败后通过回调函数进行感知,后续进行消息补偿。
4.3 小结
一些常见的异步编程方式有:
需要注意的是,异步并没有缩短整体的响应时间,反而可能有所增加。异步编程有优点也有缺点,可根据自身业务选型:
优点:
缺点:
综上所述,异步编程具有许多优点,可以提高系统的性能和响应能力。然而,它也存在一些缺点,需要在设计和实现中注意解决相关的问题。合理地应用异步编程,可以最大程度地发挥其优点,减少其缺点的影响。
5.1 业务场景
并行思想是一种同时执行多个任务或操作的方法,以提高系统的处理能力和效率。在并行思想中,任务被分解为多个子任务,并且这些子任务可以同时执行,充分利用多核处理器或分布式系统的资源。
5.2 案例
⓵ 并发合成字幕 & 上传 cos
智影极速版剪辑器生成视频时,我们会把字幕轨道先合成一个字幕文件并上传到 cos:
因为生成 srt 字幕后还要上传,若串行执行的话,当字幕轨道比较多的时候(比如 10 个)最终的耗时可能就会比较长了。这时,并行处理就能极大地提升效率:
主要使用了 errgroup
这个包,伪代码:
package subtitle
import (
"context"
"golang.org/x/sync/errgroup"
)
// TracksAsSrt 轨道转字幕
func TracksAsSrt(ctx context.Context, tracks []*Track) (err error) {
eg := errgroup.Group{}
for i := range tracks {
track := tracks[i]
eg.Go(func() error {
// 生成当前字幕轨的字幕文件名
filename := GetSrtFilename(track)
// 把轨道转为字幕
srt := ConvertTrackToSrt(track)
// 把字幕上传到 cos
if _, err = tools.NewSrtCosHelper().Upload(ctx, filename, srt); err != nil {
return err
}
return nil
})
}
return eg.Wait()
}
性能对比:
简单起见,逻辑处理部分的耗时用 sleep 模拟。
file.go
:
// TracksAsSrtSingle 轨道转字幕(串行)
func TracksAsSrtSingle(ctx context.Context, tracks Tracks) (err error) {
for i := range tracks {
i = i
// 模拟耗时
time.Sleep(100 * time.Millisecond)
}
return nil
}
// TracksAsSrtBatch 轨道转字幕(并行)
func TracksAsSrtBatch(ctx context.Context, tracks Tracks) (err error) {
eg := errgroup.Group{}
for i := range tracks {
i = i
eg.Go(func() error {
// 模拟耗时
time.Sleep(100 * time.Millisecond)
return nil
})
}
return eg.Wait()
}
压测结果符合预期:并行 10 个的话,性能提升 10 倍:
cpu: VirtualApple @ 2.50GHz
BenchmarkTracksAsSrtSingle
BenchmarkTracksAsSrtSingle-10 1 1003969084 ns/op 2410792 B/op 19474 allocs/op
cpu: VirtualApple @ 2.50GHz
BenchmarkTracksAsSrtBatch
BenchmarkTracksAsSrtBatch-10 10 100319896 ns/op 226600 B/op 2026 allocs/op
细心的读者已经发现,通过并行处理也能变相地实现批量。不一定非要被下游服务提供一个批量接口。
5.3 小结
谚云:人多力量大
在现代操作系统中,我们可以很方便地编写出多进程的程序。多进程间的通信是需要重点考虑的事项之一,这种通信方式叫作 IPC(Inter- Process Communication)。
在 Linux 操作系统中可以使用的 IPC 方法有很多种。从处理机制的角度看,它们可以分为:
并发这个概念由来已久,主要思想是使多个任务可以在同一个时间段内执行,以便能够更快地得到结果。
Go 最明显的优势在于拥有基于多线程的并发编程方式。协程有风险,使用须谨慎。协程不是越多越好,当可能出现大量 goroutine 时,可以考虑使用协程池对其管理。ants 是一个高性能且低损耗的 goroutine 池。
6.1 业务场景
空间换时间思想是一种常见的优化策略,它通过增加额外的空间(内存、缓存等)来减少程序的执行时间。这种思想的基本原理是通过预先计算、缓存或索引等方式,将计算或数据存储在更快的存储介质中,以减少访问时间和计算时间。这样可以避免重复计算或频繁的磁盘访问,从而提高程序的执行效率。
6.2 案例
缓存优化是性能优化中的一个重要环节,它可以显著提高系统的响应速度和吞吐量。常见的应用有:
6.3 小结
谚云:彼亦一是非,此亦一是非
使用缓存虽然可以提升服务端性能和用户体验,但是也会带来其他问题,如数据一致性问题。还有缓存雪崩、缓存穿透、缓存并发、缓存无底洞、缓存淘汰等问题。
Every coin has two sides。对于上述的缓存应用,可以根据自身的业务场景和系统架构进行选择和组合。以解决业务主要矛盾,不引入新问题为要。
7.1 业务场景
连接池(Connection Pool)是创建和管理连接的缓冲池技术。
连接池的原理是通过预先创建一定数量的连接对象,并将其保存在池中。当需要使用连接时,从池中获取一个可用的连接对象,使用完毕后归还给池,而不是每次都创建和销毁连接对象。这样可以避免频繁地创建和销毁连接对象,提高系统性能和资源利用率。
常见的连接池有:数据库连接池( go-redis 连接池、go-orm 连接池)、线程池(Go 协程池 ants)、HTTP 连接池等。
go-redis 连接池:
https://github.com/redis/go-redis/tree/master/internal/pool
go-orm 连接池:
https://github.com/go-xorm/manual-zh-CN/blob/master/chapter-01/1.engine.md
Go 协程池 ants:
https://github.com/panjf2000/ants
通常,连接池包含以下几个关键组件:
连接池的工作流程如下:
7.2 案例
⓵ go-redis 连接池
总览下连接池的核心代码结构,go-redis 的连接池实现分为如下几个部分:
图:go-redis 连接池的基本流程
原理可参考:《Go-Redis 连接池(Pool)源码分析》
7.3 小结
总的来说,连接池是一种有效管理和复用连接的技术,它可以提高性能、节省资源、控制连接数、提高可靠性,并简化应用程序的编程。在高并发的场景下,使用连接池是一种常见的优化手段。
8.1 业务场景
安全思想是指在设计、开发和维护计算机系统和网络时,将安全性作为首要考虑的原则和理念。它强调在整个系统生命周期中,从设计阶段到实施和运行阶段,都要考虑安全性,并采取相应的措施来保护系统免受恶意攻击和数据泄露的威胁。
8.2 案例
⓵ Go 安全编码实践指南
采用安全编码实践,可以提高应用程序的安全性,减少潜在的安全风险,并为用户提供更可靠和安全的体验。
本节内容摘自 OWASP 的 《Go-安全编码实践指南》
http://www.owasp.org.cn/OWASP-CHINA/go-webapp-scp-cn.pdf
其他:
⓶ 业界安全事件分析与借鉴
时间 | 事件 | 原因分析 | 我们的参考应对 |
---|---|---|---|
45019 | 三星引入 ChatGPT 后疑似泄露公司资料 | 三星接入 ChatGPT 后有员工在使用过程中上传了源码和会议记录业界普遍怀疑 ChatGPT 可能收集对话数据用于训练迭代,可能会在其他对话中漏出 | 不泄露公司敏感信息,以免触碰高压线1、不把工作代码贴进 ChatGPT 对话中2、对话过程中不输入公司敏感信息与资料,如密码密钥、业务数据、财务数据、用户个人数据、未公开算法等 |
44930 | 黑客出售 2 亿 Twitter 用户个人资料 | 推测为根据 2022 年漏洞泄露的数据做整理2022 年漏洞原因:twitter接口会根据传入的邮箱或手机号返回对应的 twitterID | 1、API 设计应避免泄露用户个人数据,特别是对不需要做身份校验的接口 |
44969 | 45 亿条快递数据遭泄露 | 可能为快递/电商平台等多个泄露源拼接而成,过往主要泄露原因包括:1、API 接口漏洞导致泄露2、内鬼泄露3、云仓平台被植入木马后泄露 | 1、API 接口不返回多余信息;敏感 API 接口做严谨鉴权2、内部人员权限按需最小化授予,管理平台限制导出条数 |
45005 | ChatGPT 部分用户可查看他人聊天记录 | 所使用的redis python 客户端连接池存在 bug,对部分特殊场景的请求会错误分配到他人的处理连接,相应的返回他人的数据 | 1、保持使用最新版或安全版本的第三方软件,对提示有漏洞的版本及时升级修复2、优先从内部软件源下载第三方组件,其次从软件官网 |
44941 | 俄罗斯科技巨头 Yandex 内部源代码泄露 | 员工离职前恶意下载和泄露源代码(twitter 3 月份也有员工泄露源码事件) | 1、源码中不写入密钥密码等敏感数据,改为存放至七彩石或 KMS,收到类似风险提醒时切实修改,不随意忽略2、公司对源码恶意下载和泄露有监控和审计溯源能力,建议团队内做好宣导,以免违规违法 |
8.3 小结
谚云:防患于未然
安全思想和漏洞防护是保护计算机系统和网络安全的重要方面。通过将安全性纳入系统设计和开发的早期阶段,并采取相应的漏洞防护措施,我们可以降低系统遭受攻击的风险,保护用户的数据和隐私,确保系统的正常运行。
9.1 业务场景
在数据量稍大些的场景中,传输时间往往占耗时的大头。压缩算法在数据存储、数据传输和用户体验等方面都具有重要的作用,可以提高效率、节省资源和改善用户体验。
9.2 案例
⓵ 压缩算法在 HTTP 协议中的应用
压缩应我们身边。
Content-Encoding 是 HTTP 协议中的一个头部字段,用于指示服务器对响应内容进行了何种类型的编码压缩。它的作用是告知客户端如何解码和还原服务器返回的压缩内容。
Content-Encoding 的作用包括:
常见的 Content-Encoding 值包括:Gzip、Deflate、Br 等算法。
对于 REST API 的开发者来说,资源表示压缩是一项非常重要的技术,可以帮助我们提高 API 的性能,减少响应大小,提升用户体验。
⓶ 压缩算法在构建部署项目的一次实践
先说结论:压缩平均节省了 90% 的时间。
本节将以 《速度与压缩比如何兼得?压缩算法在构建部署中的优化》为例,简要说明压缩算法在项目实践中的效果。
几种压缩算法对比:
文中测试了这几种算法结果(多次运行选择结果的中位数),数据对比如下表格:
Zstd 官方 Benchmark 数据对比
文中用 Zstd 对镜像的发布包做了测试,结论如下:
优劣分析总结:
在测试案例对比中,时间耗时的顺序为 Pzstd < ISA-L < Pigz < LZ 4 < Zstd < Brotli < Gzip (排名越靠前越好),其中压缩和解压缩的时间在整体的耗时上占比较大,因此备选策略为 Pzstd、ISA-L、Pigz。
详细的测试过程和方案对比可以参考原文:《速度与压缩比如何兼得?压缩算法在构建部署中的优化》
9.3 小结
谚云:没有最好,只有最适合
压缩算法的衡量指标包括:压缩比、压缩/解压速度、CPU/内存占用等。这些指标通常是相互关联的,不同的压缩算法在不同的数据类型和压缩设置下可能表现出不同的性能。选择合适的压缩算法应综合考虑这些指标,并根据具体的应用需求进行权衡。
10.1 业务场景
消息队列是重要的分布式系统组件,在高性能、高可用、低耦合等系统架构中扮演着重要作用。可用于异步通信、削峰填谷、解耦系统、数据缓存等多种业务场景。
常用的消息队列实现有:Kafka、RabbitMQ、RocketMQ、Pulsar、ActiveMQ 等等。
10.2 案例
⓵ 解耦系统
以电商系 IT 架构为例。在传统的紧耦架构中,客户下单后,订单系统收到请求后,调用库存系统减库存。这种模式有如下缺点:
引用 MQ 后的方案:
引入 MQ 后,订单系统和库存系统分别工作,解除了强耦合性。即便在下单时库存系统宕机了,也不影响正常下单(待库存系统恢复后,从 MQ 取出订单保证最终成功)。
电商网站中,新的用户注册时,需要将用户的信息保存到数据库中,同时还需要额外发送注册的邮件通知、以及短信注册码给用户。
⓶ 异步通信
电商网站中,新的用户注册时,需要将用户的信息保存到数据库中,同时还需要额外发送注册的邮件通知、以及短信注册码给用户。
传统的做法有两种:串行的方式、并行的方式。
串行的方式:
将注册信息写入数据库后,先发送邮件通知,再发送短信提醒。以上三个任务全部完成后,返回给客户端。
图:串行发送
并行的方式:
将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个过程完成后,返回给客户端。与串行的差别是并行的方式可以缩短处理时间。
图:并行发送
消息队列:
引入消息队列后,非关键路径(通知部分)就可以异步处理了,从而实现快速响应:
⓷ 削峰填谷
像双十一、手机预约抢购等对 IO 时延敏感的业务场景,当外部请求超过系统负载时,如果系统没有过载保护策略,很可能会被短时的峰值流量冲垮。
针对这种洪峰流量,引入消息队列,将非即时处理的业务逻辑进行异步化,处理成功后通知用户(邮件、短信等)。这种削弱峰值流量延缓处理的方式,相当于给系统做了一层缓冲。
图:削峰填谷
上图中,黄色的部分代表超出消息处理能力的部分。把黄色部分的消息平均到之后的空闲时间去处理,这样既可以保证系统负载处在一个稳定的水位,又可以尽可能地处理更多消息。通过配置流控规则,可以达到消息匀速处理的效果。
⓸ 广播
假如,客户购买商品后,子系统会有以下动作:
凡此种种,这些子系统之间没有依赖关系。引入 MQ 可以大大简化业务逻辑:
⓹ 延时队列
消息队列可以实现一些延时操作,如定时调度、超时处理等。
分布式定时调度:
在需要精细化调度的场景中,如每 2 分钟触发一次消息推送。传统基于数据库的定时调度方案在分布式场景下(特别是数据量大的时候),性能不高,实现复杂。基于消息队列(如 RocketMQ)可以封装出定时触发器。
任务超时处理:
以购买火车票为例,我们在 12306 下单后暂未支付,订单是不会被取消的。而是等待一段时间后(如 30 min),系统才会关闭未支付的订单。可以使用消息队列实现超时任务检查:
基于定时消息的超时任务处理有如下优势:
其他:
延迟消息的使用场景很多,比如异常检测重试、订单超时取消等,例如:
10.3 案例
谚云:All problems in computer science can be solved by another level of indirection.
计算机科学中的所有问题都可以通过另一个中间层来解决。
11.1 业务场景
内行的设计者知道:不是解决任何问题都要从头做起。他们更愿意复用以前使用过的解决方案。当找到一个好的解决方案,他们会一遍一遍地使用。因此,你会在许多面向对象系统中看到类和相互通信的对象的重复模式。
设计模式是软件设计中常见问题的典型解决方案。它们就像能根据需求进行调整的预制蓝图,可用于解决代码中反复出现的设计问题。
每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,就能一次又一次地使用该方案而不必做重复劳动。
模式依目的可划分为三种:
11.2 案例
各设计模式并不是孤立的,他们之间有着千丝万缕的联系:
——摘自 GOF 的 《设计模式:可复用面向对象软件的基础》。https://github.com/Seanforfun/Books/blob/master/Java/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F_%E5%8F%AF%E5%A4%8D%E7%94%A8%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%BD%AF%E4%BB%B6%E7%9A%84%E5%9F%BA%E7%A1%80.pdf
限于篇幅,本文不再罗列各模式的具体实现。Go 版本实现可参考:《golang-design-pattern》
https://github.com/senghoo/golang-design-pattern
⓵ 创建型模式
⓶ 结构型模式
⓷ 行为型模式
11.3 小结
正如前面提到的:“不是解决任何问题都要从头做起”。
设计模式是一种在软件设计中解决问题的方法论,它可以提高代码的可维护性、可重用性和可扩展性,同时也有助于提高软件系统的可靠性和稳定性。
-End-
原创作者|梁元铮