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

推荐订阅源

T
Tailwind CSS Blog
C
CERT Recently Published Vulnerability Notes
P
Proofpoint News Feed
Vercel News
Vercel News
博客园 - 三生石上(FineUI控件)
IT之家
IT之家
Help Net Security
Help Net Security
月光博客
月光博客
N
News and Events Feed by Topic
Cloudbric
Cloudbric
博客园 - 司徒正美
L
LangChain Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Tenable Blog
The Register - Security
The Register - Security
The Hacker News
The Hacker News
I
InfoQ
The Last Watchdog
The Last Watchdog
MyScale Blog
MyScale Blog
Schneier on Security
Schneier on Security
WordPress大学
WordPress大学
小众软件
小众软件
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
宝玉的分享
宝玉的分享
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
K
Kaspersky official blog
L
LINUX DO - 热门话题
N
News | PayPal Newsroom
F
Fortinet All Blogs
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Security @ Cisco Blogs
Recorded Future
Recorded Future
大猫的无限游戏
大猫的无限游戏
H
Help Net Security
Google Online Security Blog
Google Online Security Blog
S
Schneier on Security
C
Cisco Blogs
N
News and Events Feed by Topic
V2EX - 技术
V2EX - 技术
Latest news
Latest news
PCI Perspectives
PCI Perspectives
T
The Blog of Author Tim Ferriss
P
Palo Alto Networks Blog
T
Tor Project blog
Project Zero
Project Zero
云风的 BLOG
云风的 BLOG
Webroot Blog
Webroot Blog
Attack and Defense Labs
Attack and Defense Labs
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 sync/atomic Code Reading goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式 Go 内存模型 Kubernetes 核心概念 软件版本语义化
降本增效之应用优化 (一) Redis
2023-09-07 · via 蛮荆

2023-09-07 Redis 业务逻辑 应用优化

背景

笔者所在公司八月初拥抱了变化,作为 “幸存者”,自然需要接手一大波前人留下的项目,降本增效的大前提下,为了保住 “狗命”,需要尽可能去多做一些能直接产生价值和收益的工作, (毕竟在 优化代码 和 “被优化” 之间只能选一项)。

最近两周集中优化了一批应用接口,期间被各种 “奇 (shi) 葩 (shan)” 代码刷新了认知,很难想象这是每天一起吃饭吹水的同事写出来的,果然:

无脑定需求 + 不断改需求 + 疯狂赶进度 + 开发人员长期被动当工具人 = ? ? ?

人生可真够操蛋的

吐槽归吐槽,这些代码唯一的技术价值就是给优化工作让出了很大的空间,笔者在匆忙间给自己定下了九月份的 OKR: 接手的维护阶段的项目总体资源优化 30%

概述

因为要优化的项目都属于 IO 密集型,所以主要的工作重心还是在存储、IO 的相关优化上面,本文主要讲解一下优化过程中和 Redis 相关的部分, 笔者将业务问题场景进行了问题分类和对应的优化方案,算是对这两周的工作进行一个简单的回顾。

原则

优化工作的本质是面向收益编程。

优化越靠近业务应用层,效果越明显,映射到现实中,同样如此,越靠近业务层,绩效越好。千万不要直接闷头开始优化,要知道大多数代码都没有优化的必要, 牢记 2/8 原则,主要针对 hot path 代码和和明显有性能问题的代码进行优化。

确定目标

可以根据监控系统的各项指标,来确定具体的优化接口和代码,笔者通常会查看 Redis Server 总体的监控和负载情况,然后再分析具体的性能瓶颈和原因, 这里贴几张常用的性能排查指标图。

Redis 性能指标概览

Redis 慢请求

Redis key 命中率

Redis 热 key 大 key

有了上述相关指标数据,我们可以根据 ROI 原则来计算接口的 “优化收益比”,确定要优化的具体接口,然后进行任务拆分、优先级排序、日程确定, 最后通过在线项目管理工具同步到产品和业务方。

为了简洁和统一,下文中提到到 客户端 包括 APP 客户端和 Web 前端。


零请求

优化的最高级形式就是客户端在本地进行数据处理,不发出任何请求到服务端,这种业务场景下,服务端的资源消耗为 0, 所以能在客户端处理的数据,就不要请求到服务端, 下面是笔者总结的四种可以直接在客户端处理的数据场景。

本地缓存

客户端直接将实时性要求不高的数据缓存到本地,用户查看时直接取出数据渲染,同时异步去接口加载。

数据聚合和排序

  • 如果对已有的数据按照不同条件排序时,接口数据量不会发生变化,直接在本地排序即可
  • 同理,对于聚合类数据操作,只要数量不会发生变化,也可以直接在本地进行

数据搜索和过滤

接口数据量较少的情况下,可以一次性全部返回所有数据,然后直接在客户端本地进行数据搜索和过滤功能,而不是通过请求 + 参数的方式让服务端处理。

数据格式化

  • 后台配置的字典数据一次性全部返回,客户端可以根据具体的 key 进行区分,然后填充选项卡、下拉列表等 UI 视图
  • 日期数据直接返回到客户端,由客户端做具体格式化处理,服务端返回携带时区的标准数据即可,例如 2022-03-29T16:05:14.000

单个状态值

这类应用场景的典型特征是每个用户只需要一个 bit 的标识位,也就是典型的 bitmap 类问题,解决方案也很简单,使用 Redis 内置的 bitmaps 数据结构即可。

注意: Redis 中的 bitmaps 最大支持数值为 2^32, 这里假设以用户 ID 作为标识字段,如果用户 ID 大于这个 2^32, 就无法存储了,如何解决这个问题呢? 只需要稍微变通一下,使用 ID / (2 ^ 32) 的值作为 bucket 桶编号, 然后拼接具体的业务 key 就可以了,最后使用 BITCOUNT 命令就可以查询统计结果了。

bucket 对应关系

笔者主要优化了以下几个简单的场景:

  • 用户是否点赞过、收藏过、分享过某个视频/文章
  • 每日签到
  • 青少年每日防沉迷提醒

唯一计数器

HyperLogLog 实现的唯一计数器可以大大降低内存使用量,如图所示为记录唯一访客 IP 时,各种数据结构所需的内存量。


数据组合

这类应用场景的典型特征是单个接口内部需要跨多个服务进行调用,最后组装数据统一返回,笔者的优化方法为:

  1. 按照业务最小维度缓存单条数据,Redis 缓存一份,对于热点数据,本地 (容器) 也缓存一份
  2. 查询时分别从本地查询单条缓存数据,从 Redis 批量查询数据 (使用 pipeline)
  3. 根据目标结果集和缓存数据集进行差异比较,计算出未命中的数据
  4. 从存储 (一般指数据库) 查询未命中的数据,并且回填到缓存中

笔者主要优化了个性化推荐信息流场景:

  1. 根据当前用户信息从推荐服务获取到具体的内容 ID 列表(内容可能是视频/文章等)
  2. 根据内容 ID 列表依次从缓存和存储中获取各项业务数据
  3. 最后将数据组装后返回到接口

编码/解码

如果缓存数据是字符串类型,将数据存储到 Redis 之前,需要进行编码操作,常规的做法是编码为 JSON 字符串,这样从 Redis 读取到缓存的字符串数据后, 如果数据不需要被读取并且不需要被修改,那么就可以直接将数据输出接口,这样可以节省 2 次 CPU 开销:

  1. 将缓存的字符串数据解码为具体对象
  2. 将具体对象再编码为字符串后输出接口

缓存 key 的命名

在保证辨识度的前提下,key 的长度越短越好,不仅可以节省存储,还可以提升查询速度。

作为用户资料数据的缓存 key,user_123456_profile 明显由于 user_profile_123456,因为前者的辨识度更高,查询速度更快, 在这个基础上可以对 key 的长度再次优化,例如优化为 u_123456_prof


时间区间

这类应用场景的典型特征是不同时间段内的数据组合优化,项目中类似场景之前的做法是使用筛选条件中的 (开始时间 + 结束时间 + 业务 key) 进行拼接作为缓存数据 key, 稍微思考后会发现这其中有很大的潜在问题: 不同的两个日期组合结果集合是一个庞大的数字,除了重复数据导致的巨量内存浪费外,还会造成很大的安全隐患

下面举个浪费内存的例子,不同用户查询的时间区间是重叠的:

- 用户A 2023-01-01 ~ 2023-01-10
- 用户B 2023-01-02 ~ 2023-01-05
- 用户C 2023-01-05 ~ 2023-01-08

通过示例可以看到,虽然有三个用户在查询,但是用户 B 和 用户 C 查询的数据都在 用户 A 的结果集内,也就造成了数据重估存储,白白浪费了内存。

笔者的优化方案为:

  1. 根据更小的粒度来缓存 (项目中以天为单位),这样单个业务场景一年最多 365 个 key
  2. 控制时间范围的上限,不能超过 31 天
  3. 根据请求参数批量从 Redis 读取缓存数据
  4. 将读取到的缓存数据组装完成后输出接口

注意: 如果项目的数据量很大,就需要调整时间粒度,并且进行数据异步批处理优化,但是整体的思路是不变的。

读写分离

这类应用场景的典型特征是只有一端固定的数据生产者,例如:

  • 运营角色在管理后台完成整个 CMS 网站的内容
  • 定时任务从第三方同步数据,完成后展示给所有用户

这种场景最容易优化,启动一个后台线程,定时刷新数据到缓存即可,这里不再赘述了 。

小结

本文提到的大多数问题都是因为缺乏对 Redis 的全面了解,直接把 Redis 当成 MemCache 类型的纯 K-V 来应用,也许是因为大家都在忙着背八股?

写在最后

希望大家都能有班上,有钱赚。