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

推荐订阅源

F
Fortinet All Blogs
Microsoft Security Blog
Microsoft Security Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Vercel News
Vercel News
Application and Cybersecurity Blog
Application and Cybersecurity Blog
C
Check Point Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
W
WeLiveSecurity
The Hacker News
The Hacker News
L
LINUX DO - 热门话题
T
Tenable Blog
Hugging Face - Blog
Hugging Face - Blog
Google Online Security Blog
Google Online Security Blog
博客园 - Franky
P
Proofpoint News Feed
H
Hacker News: Front Page
P
Privacy & Cybersecurity Law Blog
月光博客
月光博客
P
Proofpoint News Feed
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The GitHub Blog
The GitHub Blog
云风的 BLOG
云风的 BLOG
博客园_首页
www.infosecurity-magazine.com
www.infosecurity-magazine.com
C
CERT Recently Published Vulnerability Notes
Forbes - Security
Forbes - Security
I
InfoQ
Stack Overflow Blog
Stack Overflow Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Attack and Defense Labs
Attack and Defense Labs
N
News and Events Feed by Topic
博客园 - 叶小钗
T
Threat Research - Cisco Blogs
aimingoo的专栏
aimingoo的专栏
D
Darknet – Hacking Tools, Hacker News & Cyber Security
小众软件
小众软件
大猫的无限游戏
大猫的无限游戏
MongoDB | Blog
MongoDB | Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Hacker News - Newest:
Hacker News - Newest: "LLM"
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
O
OpenAI News
G
Google Developers Blog
Martin Fowler
Martin Fowler
罗磊的独立博客
S
SegmentFault 最新的问题
T
Tor Project blog
量子位

蛮荆

如何获取更多的免费服务器 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 来应用,也许是因为大家都在忙着背八股?

写在最后

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