惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
IT之家
IT之家
H
Heimdal Security Blog
Jina AI
Jina AI
宝玉的分享
宝玉的分享
博客园 - 【当耐特】
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
T
Tailwind CSS Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
有赞技术团队
有赞技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell
WordPress大学
WordPress大学
AWS News Blog
AWS News Blog
C
Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The Hacker News
The Hacker News
The Cloudflare Blog
Hugging Face - Blog
Hugging Face - Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Threatpost
S
Securelist
P
Privacy International News Feed
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - 聂微东
博客园 - 叶小钗
J
Java Code Geeks
V
V2EX
博客园 - Franky
Spread Privacy
Spread Privacy
K
Kaspersky official blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Simon Willison's Weblog
Simon Willison's Weblog
Project Zero
Project Zero
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
C
Cybersecurity and Infrastructure Security Agency CISA
C
CERT Recently Published Vulnerability Notes
Latest news
Latest news
NISL@THU
NISL@THU
罗磊的独立博客
W
WeLiveSecurity
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园_首页
V
Visual Studio Blog

希仁之拥

领克900半年使用体验 | 希仁之拥的博客 Ubuntu 26.04 Desktop使用体验 | 希仁之拥的博客 【转载】谈谈不受欢迎的博客技术特征 | 希仁之拥的博客 【转载】ClaudeCode 你想知道的所有秘密,源码深度研究报告 | 希仁之拥的博客 2025年年终总结 | 希仁之拥的博客 集成和使用Openclaw后的思考 | 希仁之拥的博客 我买了领克900 | 希仁之拥的博客 服务器性能优化之io拷贝 | 希仁之拥的博客 Go-Sail导航站上线啦 | 希仁之拥的博客 今年国庆的一些感受 [2025] | 希仁之拥的博客 在Deepin 25上配置forticlient | 希仁之拥的博客 分享一些酷酷的站点 [20250908] | 希仁之拥的博客 Go-Sail发布v3.0.6版本了 | 希仁之拥的博客 我对V2EX发布$V2EX讨论的一些感受 | 希仁之拥的博客 如何让Stripe支持支付宝和微信支付 | 希仁之拥的博客 2025上半年里程碑 | 希仁之拥的博客 GitLab+Drone使用体验 | 希仁之拥的博客 四姑娘山之旅 | 希仁之拥的博客 聊聊接口的返回数据结构 | 希仁之拥的博客 由GORM的Updates语法糖 我把 Go-Sail 的文档站更新了 | 希仁之拥的博客 这就是我为什么讨厌拼多多 | 希仁之拥的博客 元旦快乐~ | 希仁之拥的博客 致敬还在写博客的我们 | 希仁之拥的博客 逐步的把图片资源迁移到星光图床上 | 希仁之拥的博客 帮弟弟配了一台mini主机 | 希仁之拥的博客 国庆的一些碎碎念 | 希仁之拥的博客 就这一刻而言,我觉得科技冷冰冰的。 | 希仁之拥的博客 如何使用acme.sh自动续签证书 | 希仁之拥的博客 Go-Sail的文档站点搭建好了 | 希仁之拥的博客 我的第一个谷歌浏览器插件发布成功啦! | 希仁之拥的博客 我开源了一个golang的web框架 | 希仁之拥的博客 流芳 | 希仁之拥的博客 第一次开发微信小程序 | 希仁之拥的博客 春节快乐 | 希仁之拥的博客 我把博客站点重写了 | 希仁之拥的博客 阆中古城 | 希仁之拥的博客 入了把Keydous NJ80 | 希仁之拥的博客 Mac上有用的知识 | 希仁之拥的博客 记一次服务器被攻击 | 希仁之拥的博客 Ubuntu server不停机扩容系统盘 | 希仁之拥的博客 端午安康 | 希仁之拥的博客 搭建hadoop伪分布式集群总结 | 希仁之拥的博客 在goa框架中,如何访问原始请求数据 | 希仁之拥的博客 postgresql相关操作笔记 | 希仁之拥的博客 golang解析带命名空间的xml数据(带冒号的节点或属性名) | 希仁之拥的博客 【转载】解决deepin环境下wine QQ无法接收图片问题 | 希仁之拥的博客 杂项笔记 | 希仁之拥的博客 Mac如何修改通知横幅消失时间 | 希仁之拥的博客 Golang校验结构体是否实现了接口 | 希仁之拥的博客 关于deepin os的大半年使用体验 | 希仁之拥的博客 有态度的翻唱 | 希仁之拥的博客 【转载】Golang 在 Mac、Linux、Windows 下如何交叉编译 | 希仁之拥的博客 和幺幺一起成长的半年时光 | 希仁之拥的博客 Element UI中message消息提示实现原理 | 希仁之拥的博客 从predis组件开始了解如何与redis服务交互 | 希仁之拥的博客 【转载】 尤雨溪谈Vue.js :缔造自由与真我 | 希仁之拥的博客 【转载】百万 Go TCP 连接的思考: epoll方式减少资源占用 | 希仁之拥的博客 【分享】移除git中的commit历史(保留文件,仅移除提交历史信息) | 希仁之拥的博客 shell学习笔记 | 希仁之拥的博客 【分享】Ruby 之父松本行弘:程序员的乐趣、存在感与平衡 | 希仁之拥的博客 安利一下我用过的一些sublime text 3工具、插件及配置 | 希仁之拥的博客 PHP处理苹果内购数据验证 | 希仁之拥的博客 PHP获取XML所有节点名称 | 希仁之拥的博客 Sentinel::check() 在Laravel5.3版本之后总是返回false的解决方案 | 希仁之拥的博客 吃货的美食集,持续更新~ | 希仁之拥的博客 第二十一届成都汽车博览会 | 希仁之拥的博客 马云宣布1年后卸任集团董事局主席 由CEO张勇接任 | 希仁之拥的博客 nginx配置非443端口的https连接 | 希仁之拥的博客 PHP7 异常类型列表 | 希仁之拥的博客 【分享】《燃点》:不被嘲笑的愿望是不值得去实现的|一部属于创业者的电影 | 希仁之拥的博客 【分享】前端资源加载优先级[译] | 希仁之拥的博客 【小技巧】Laravel动态配置多个数据库连接 | 希仁之拥的博客 php使用ftp上传、下载文件 | 希仁之拥的博客 php使用mysqlnd扩展进行mysql异步并行查询 | 希仁之拥的博客 基于以太坊区块链,使用web3.js在geth服务器上执行合约方法以及事件监听 | 希仁之拥的博客 Smartisan OS | 希仁之拥的博客 母校 | 希仁之拥的博客 php后期静态绑定 | 希仁之拥的博客 [mysql优化]子查询与连接查询 | 希仁之拥的博客 PHP常见运行模式及原理 | 希仁之拥的博客 mysql用户与授权 | 希仁之拥的博客 如何在ubuntu14.04上安装composer | 希仁之拥的博客 关于mews/captcha for laravel存在method [validateCaptcha] doesn't exists解决方法 | 希仁之拥的博客
近来帮同事做性能优化的过程回顾 | 希仁之拥的博客
希仁之拥 · 2025-05-28 · via 希仁之拥

前言

之所以有这篇文章,是因为最近一段时间在帮助同事做性能优。目前优化工作大体上已经完成,现在回过头来才发现这段时间是对以往“八股文”在真正开发过程中的实际应用。包括但不限于:接口超时限制、预防缓存击穿、数据库读写分离,Redis管道操作,服务日志梳理等等。每部分似乎都是司空见惯,但串联起来,是一套较为完整的系统性的应用,还是非常值得记录下来的。

背景

我们的产品是面向全球的Web应用,服务器部署于AWS的EKS集群(AWS的K8S产品)之上,目前采用的是微服务的形式,但不是服务网格(Service mesh)的方式,一些基础设施是公用的,例如Redis集群、数据库实例、消息队列等等,一部分原因是因为最开始的技术架构设计,一部分是需要考虑到成本,我司属于创业公司,是典型的预算敏感型用户。

此文章介绍的内容是产品下的其中一个模块,在决策层面也是接下来的宣发和推广的重点。就此模块而言,我们的角色是数据的接入方,我们的供应商提供了基于Webscoket的数据流订阅机制,我方需要订阅相应的数据流,对数据进行清洗整理,再做一定的聚合,之后为客户端提供HTTP接口。在与供应商的沟通中我们了解到,他们数据推送的峰值可能达到7GB/min,这是一个非常恐怖的数据,因此数据消费处理过程将面临不小的挑战。

正文

数据消费

前面说到,供应商的数据推送量会比较大,显然数据消费处理过程是首当其冲的优化点。经过对需求和数据特点的分析评估,我们对数据进行分级,分为热数据持久化数据
其中,热数据的特点是:1.高频的访问和更新,2.数据属于元数据可以直接存储。
持久化数据的特点是:1.低频写入,2.中低频访问,3.结构化的。

鉴于上面数据的不同特点,我们决定将热数据存放到缓存也就是Redis中,将持久化数据存放到数据库中。

少啰嗦,先看东西

为了阅读更加清晰,非关键代码已省略。

...

func feedStream() {
	...
	var messages = make(chan string, 1024*5)

	go readFeedMessages(connect, messages)

	wd := &sync.WaitGroup{}
	for i := 1; i <= 100; i++ {
		wd.Add(1)
		go feedHandler(wd, messages)
	}

	wd.Wait()
}

...

这是改造前的数据消费代码段。从代码中你可以看到,首先为消费创建了一个通道,长度为1024*5,然后开启了100个协程对这个通道进行并发读取。
我们知道go的通道为了实现并发安全,其内部实现是有锁的。这里,我的想法是降低锁的影响范围,也就是常提到锁粒度。因此,我把代码改造为为各自的协程分配各自的通道。

...
//初始化通道
var (
	maxMessageChannelNum = 200
        messageFeedChannels []chan string
)

...
for i := 0; i < maxMessageChannelNum; i++ {
	messageFeedChannels = append(messageFeedChannels, make(chan string, 1024))
}

...

// 读取消息
func readFeedMessages(connect *websocket.Conn, messageChannels []chan string) {
	var processStep int
        ...
	for {
		_, r, err := connect.NextReader()
                ...
		feedBuf, err := io.ReadAll(r)
                ...
                //将消息均衡的分配到不同的通道中
		if len(feedBuf) > 0 {
			messageChannels[processStep] <- sailUtils.BytesToStr(feedBuf)
			processStep++
			if processStep == maxMessageChannelNum {
				processStep = 0
			}
		}
	}
}

以上代码已经忽略了错误处理,通道关闭等逻辑。

细心的你可能已经发现了这段代码:

sailUtils.BytesToStr(feedBuf)

根据方法名称你也猜到了,它的作用就是将byte数组转换为字符串。那么为什么不用内置方法string()呢?这里有一个小小的前提,那就是供应商发送单条数据都会比较大。使用内置方法性能相对较低。(我们的golang版本是go1.19)。相应的benchmark请参考鸟窝大佬的这篇文章:四种字符串和bytes互相转换方式的性能比较

存入缓存

压缩

我们使用的缓存组件是Redis,基于它高性能和可靠的表现,本没有什么问题。但是我们还是得为领导们提前谋划(提前预判领导们的预判):成本
服务价格评估.png
因此,我们在将数据存入Redis之前,做了对大Key一个额外的操作:压缩。
之所以这么做,我们分析如下:
1.压缩的性能消耗点在CPU上,通过对服务器的性能监控,发现当前CPU的使用率比较低,可以匀一部分出来。
2.即便日后服务器的CPU性能不够了,扩容服务器的成本也远低于Redis的成本。
3.在可观的压缩率面前,除了节省缓存空间,还能额外的节省网络带宽,降低延迟。

在进行压缩操作之后,Redis的空间消耗相比之前减少了4/5。
优化前(小峰值):

redis数据分析-优化前.png

优化后(加上了过期清理):

redis数据分析-优化后.png

管道操作

经过对代码的走读,发现许多的操作是可以合并的,因此,我们的可合并的操作合并之后进行管道操作。

...
pipe := sail.GetRedisCluster().Pipeline()
var cmds = make([]*rdb.SliceCmd, 0, len(marketIds))
for _, marketId := range marketIds {
	redisKey := fmt.Sprintf("%s:eventId:%d:markets", config.GetGlobalConfig().RedisPrefix, eventId)
	cmd := pipe.HMGet(ctx, redisKey, fmt.Sprintf("%d", marketId))
	cmds = append(cmds, cmd)
}
...
_, err := pipe.Exec(ctx)
...

Redis的管道操作可以减少命令发送和响应的往返时间、提高吞吐量、减少命令执行的总延迟。在这里的高并发场景下,性能提升显著。

附上一张峰值下的Redis监控图:

redis-commands.png

Redis: 我现在强的可怕!(狗头+破音

数据提供

接口超时

说完了数据消费,接下来就是将清洗好的数据以HTTP接口的形式提供给前端。
这里附上一张优化前的接口响应截图:
response-long-time.png
可以看到,整个请求耗费了51.07s,这相对来说是比较极限的情况,但此接口绝大部分时候耗时都在30s上下,这显然是不可接受的。
在走读了代码之后,发现并没有对接口的响应时间进行约束控制,因此,首先将这项加上。

...
func MatchInfo(c *gin.Context) {
	var (
		form request.MatchInfoRequest
		resp response.MatchInfoResponse
		loggerSvc      = c.MustGet("logger").(*zap.Logger)
		ctx, cancel    = context.WithTimeout(context.Background(), time.Second*10) //目前取保守值为10s
	)
	defer cancel()
...
}

之后的操作,无论是Redis还是数据库,都会使用这个context。

result, err := sail.GetRedisCluster().Get(ctx, redisKey).Result()
err := sail.GetDBW().WithContext(ctx).First(&record, "eventId = ?", form.EventId).Error

加上之后,立马就“见效”了。

request-timeout.png

一定要加上接口超时约束限制是为了有效的防止服务雪崩。当出现大量挂起的请求时,整个服务的响应会迅速降低,内存也会疯涨。如果请求一直挂起得不到响应,那么前面说的情况就会迅速恶化,很快整个服务就会宕掉。

Redis管道操作

上文中已经讲过了,这里不再赘述。

数据库读写分离

在给客户端提供的接口里面,绝大部分都是查询操作,因此,我这里将查询操作打到读实例上面。使用go-sail提供的语法糖:

err := sail.GetDBR().WithContext(ctx).First(&record, "eventId = ?", form.EventId).Error

另外,部分数据表属于大表,字段较多,个别字段还是text类型。因此,针对一些查询操作,只提取用到的字段,而不是全部:

err := sail.GetDBR().WithContext(ctx).
                     Select("id", "name").
                     First(&record, "eventId = ?", form.EventId).Error

热点数据缓存

对于一些高频访问的热点数据,没有必要实时读取整合,在保证数据时效性的范围内做缓存,比如20s。为了防止缓存击穿,需要对操作进行加锁处理。

...
lockerKey := fmt.Sprintf("%s:MatchInfoNX:%d", config.GetGlobalConfig().RedisPrefix, form.EventId)
redisKey := fmt.Sprintf("%s:MatchInfo:{%d}", config.GetGlobalConfig().RedisPrefix, form.EventId)
result, err := sail.GetRedisCluster().Get(ctx, redisKey).Result()
if len(result) == 0 || err != nil {
        var locked bool
        //当缓存中的数据不存在时,争抢锁,只放行一个请求去数据库查询,自旋10次
	for i := 0; i < 10; i++ {
                locked = utils.RedisTryLock(lockerKey)
		if !locked {
			time.Sleep(time.Millisecond * 200)
			continue
		}
		break
	}
        if locked {
            defer utils.RedisUnlock(lockerKey)
        }
        ...
} else {
	...
	resp.Data = matchData
	sail.Response(c).Builder(constants.ErrNone, resp).Send()
	return
}
...

你可能会问,那缓存穿透问题怎么解决,是使用了常规的布隆过滤器吗?并没有,这里的业务逻辑相对简单明了,请求的参数必须要在我们提供的名单之中,否则会被拒绝。

优化查询

对于一些不必要的查询,该去就要去掉,简而言之,相同情况下,如果能读取一次数据库能完成,那就不要读取两次。(请不要杠查询复杂度有没有增加这类问题,说的是相同情况下🙂
对于一些可以优化的查询方式,也可以考虑用更加合理的方式处理,比如,走读代码发现一个地方对一个大Key有不必要的读取:

result, err := sail.GetRedisCluster().HGetAll(ctx, redisKey).Result()

这个Key是一个有几百KB内容的哈希,而读取它只为了检查它有多少个键,没有必要将内容都读取出来。并且,这个操作还是在循环里面的。因此,我们将这里的操作更改为Redis提供的HLen方法:

cnt, err := sail.GetRedisCluster().HLen(ctx, redisKey).Result()

日志梳理

日志是一个全局性的内容,放到这里正好可以和链路追踪一起讲。我们的日志是使用的uber/zap库。在go-sail框架中,我对zap进行了一定的封装,实现了对链路追踪的支持,让它可以方便无感的导出到ELK中。更多内容可移步至:

在go-sail框架中实现链路追踪

打印日志也是会消耗性能的,特别是在量大之后会发生质的变化。走读代码之后,发现对日志的使用有许多不合理的地方。得到运维同事的反馈,近来的ELK日志量非常的大:
elk日志体积.png

因此,首先对日志进行梳理。除了zap本身的日志级别分级之外,我对日志类别做了重新的划分:
1.控制台日志
2.zap日志
对于调试性的日志,统一输出到服务控制台,并且加上了debug模式的约束,当调试结束,关闭debug之后,就不再输出。对于需要持久化的日志,由zap统一输出到ELK。其中又将zap日志拆分为计划任务日志和接口日志,让后续查询更加清晰。另外,使用请求上下文中的logger实例,这样有助于链路追踪。最后,清理掉冗余的日志。

控制台日志:

if config.GetGlobalConfig().Debug {
    fmt.Println("feedChanLength:", len(messagesChan))
}

请求上下文中的logger实例:

func MatchInfo(c *gin.Context) {
    var (
	...
        loggerSvc = c.MustGet("logger").(*zap.Logger)
        ...
    )
    ...
    loggerSvc.Error("...", zap.String("err", err.Error()))
}

梳理完之后,体积有了显著的改善:

日志梳理后.png

结合上述一系列的优化,接口响应延时得到了明显的改善:

response-ok.png

常规情况下,接口返回在毫秒级别。极端情况下(数据量大且未命中缓存),控制在3s范围内。

其他

上文中有一个截图里面提到,如果更改了Redis实例类型需要批量替换部分代码。这一点确实“不智能”。于是我去翻看了go-redis库的部分源代码后,发现它的作者实际上早就提供了一个公共的抽象接口,用于隐藏不同连接的差异。因此,我对go-sail的redis操作进行了调整:commit

总结

罗马不是一日修建而成的,这是一个循序渐进的过程,在这个过程中,需要时刻保持着一个最简单的想法,那就是“苍蝇再小也是肉”,你最终会发现集腋成裘,功法已成。


980_120.png(via stardots.io)

觉得博主写的不错?给他一个赞赏:

keepchen的赞赏码-支付宝 keepchen的赞赏码-微信