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

推荐订阅源

Engineering at Meta
Engineering at Meta
T
Threatpost
P
Palo Alto Networks Blog
NISL@THU
NISL@THU
O
OpenAI News
Project Zero
Project Zero
G
GRAHAM CLULEY
P
Privacy International News Feed
A
Arctic Wolf
Microsoft Azure Blog
Microsoft Azure Blog
H
Help Net Security
M
MIT News - Artificial intelligence
T
Threat Research - Cisco Blogs
S
Security @ Cisco Blogs
Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
D
Docker
aimingoo的专栏
aimingoo的专栏
博客园 - 【当耐特】
N
Netflix TechBlog - Medium
云风的 BLOG
云风的 BLOG
雷峰网
雷峰网
W
WeLiveSecurity
P
Proofpoint News Feed
腾讯CDC
Cloudbric
Cloudbric
S
Secure Thoughts
C
Check Point Blog
博客园 - Franky
T
The Exploit Database - CXSecurity.com
T
Troy Hunt's Blog
GbyAI
GbyAI
Security Archives - TechRepublic
Security Archives - TechRepublic
Application and Cybersecurity Blog
Application and Cybersecurity Blog
月光博客
月光博客
C
Cyber Attacks, Cyber Crime and Cyber Security
I
Intezer
TaoSecurity Blog
TaoSecurity Blog
L
Lohrmann on Cybersecurity
V
Visual Studio Blog
F
Fortinet All Blogs
博客园 - 叶小钗
C
CXSECURITY Database RSS Feed - CXSecurity.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Recorded Future
Recorded Future
C
Cisco Blogs
博客园 - 司徒正美
Stack Overflow Blog
Stack Overflow Blog
Y
Y Combinator Blog
Apple Machine Learning Research
Apple Machine Learning Research

Jiajun的技术笔记

你好,2026! TiDB 源码阅读(六):TiDB Coprocessor 源码解析 性能优化的核心思想 TiDB 源码阅读(五):索引 TiDB 源码阅读(四):AST、逻辑计划、物理计划 CockroachDB Serverless Architecture podman 无故退出 Cursor Control-L (CTRL-L) Keyboard Shortcuts in Terminal Replace docker with podman Using xmonad with xfce4 A RC script for freebsd frpc 自己动手写一个k8s controller AI 会取代你的(编程)岗位吗? 自建DERP服务器提升Tailscale连接速度(使用Nginx转发) 自动升级Docker容器 再读《程序员修炼之道-从小工到专家》 让浏览器下载文件 再读《软件随想录》/《黑客与画家》/《软技能》 HTTP 压力测试中的 Coordinated Omission 2的补码 编程语言中的 context 是什么? flutter macOS 构建出错 Flatpak 使用小记 Golang CAS 操作是怎么实现的 PostgreSQL 当MQ来使用 Clash 结合 工作VPN 的网络设计 使用 PostgreSQL 搭建 JuiceFS PostgreSQL 配置优化和日志分析 有GitHub Copilot?那就可以搭建你的ChatGPT4服务 窗口函数的使用(以PG为例) 读《为什么学生不喜欢上学》 OpenAI Prompt Engineering 摘录和总结 读《打造真正的新产品》 VueJS 总结 Linux 自动挂载 alist 提供的webdav FreeBSD 使用 vm-bhyve 安装Debian虚拟机 FreeBSD 和 Linux 网卡聚合实现提速 GPT 帮我搞定了时区转换问题 长任务系统如何处理? macOS/Linux 编译 InputLeap 使用开源软KVM - synergy-core 解决 macOS 终端hostname一直变化问题 KVM 共享 Intel 集成显卡 PromQL 备忘 读《格鲁夫给经理人的第一课》 读《打开心智》 为什么要把复杂的联表操作拆成多个单表查询? 红包系统的设计 MySQL Index Condition Pushdown Optimization Go mod 简明教程 OpenWRT 使用 Android/iOS USB 网络 搭建旁路由 Golang gRPC 错误处理 编写可维护的单元测试代码 OAuth 2 详解(六):Authorization Code Flow with PKCE OAuth 2 详解(五):Device Authorization Flow OAuth 2 详解(三):Resource Owner Password Credentials Grant OAuth 2 详解(四):Client Credentials Flow OAuth 2 详解(二):Implict Grant Flow OAuth 2 详解(一):简介及 Authorization Code 模式 ElasticSearch 学习笔记 三种git流程以及发版模型 错误处理实践 权限模型(RBAC/ABAC) OIDC(OpenID Connect) 简介 任务队列简介 PostgreSQL 操作笔记 使用Drone CI构建CI/CD系统 Golang migrate 做数据库变更管理 使用PostgreSQL做搜索引擎 Nginx 源码阅读(三): 连接池、内存池 Nginx 源码阅读(二): 请求处理 Nginx 源码阅读(一): 启动流程 Go 泛型简明教程 KVM 显卡穿透给 Windows 使用 HTTP Router 处理 Telegram Bot 按钮回调 使用反射(reflect)对结构体赋值 GIN 是如何绑定参数的 你好 2022(2021 年终总结) 用Go导入大型CSV到PostgreSQL 使用 OpenWRT 搭建软路由 使用软KVM切换器 barrier 共享键鼠 SQL 防注入及原理 使用 gomock 测试 Go 代码 gevent不是黑魔法(二): gevent 实现 gevent不是黑魔法(一): greenlet 实现 用 entgo 替代 gorm 应用内使用crontab不是那么方便 单测时要不要 mock 数据库? Sentry 自建指南 用selenium完成自动化任务 用闲置的安卓手机做垃圾电话短信过滤 推荐三个时间管理工具 一次事故反思 当JS遇到uint64:JS整数溢出问题 SQLite3 存储以及ACID原理 Redis源码阅读:pub/sub实现 Redis源码阅读:zset实现 Redis源码阅读:bitmap 位图的运算 Redis源码阅读:set是怎么做交并集运算的?
一个feed流系统的演进
Jiajun Huang · 2020-03-02 · via Jiajun的技术笔记

这几年我持续开发维护了一个类feed流系统。类feed流在结构上都有这么一个共性,即类feed流系统是一个中心节点,将多个用户和 多个feed来源连接起来:

User 1                     System 1
User 2   \              /  System 2
User 3  --- Feed流系统 --- System 3
...      /              \  ...
User n                     System n

而架构上,则一般是这么三种:

  • 推模式
  • 拉模式
  • 混合模式(推拉模式)

目前这套系统正在服务数百万用户,能够做到关键接口平均响应时间20-30ms以内。接下来,我们从它的诞生讲起。


诞生

最开始这套系统是一个单体应用中的一环,最开始的架构模式是使用拉的模式,拉的模式,也就是每次用户要求获取feed时,临时 去数据库或各数据源拉取数据,这么设计的好处是,非常节省空间,比如一条全国feed只存储一条feed,而当用户读取时,才往数据库 写入一条已读记录,如果没有这条记录,那么就意味着用户未读。

那么这种模式的缺点是什么呢?计算机中的一切都是权衡取舍(trade-off),有得必有舍。试想这么一个场景:当发出一条全国feed之后, 用户蜂拥而至,此时DB的要面临:

- 大量请求读取用户与feed的关系,以便返回feed流
- 当用户读取feed时,App会发送一个请求将该feed设置为已读,此时DB面临的就是大量的写入

大量的并发读与并发写,导致DB负载急速升高,当时由于这是单体应用,便会拖垮整个应用。

重构

我加入团队之后,提出要将这个系统重构,说干就干。于是采取了另外一种模式,推的模式,这种模式在此后两年内工作良好,一直到 最近,随着用户的不断增长,当再次发送全国feed时,数据库会出现报警,响应时间会出现抖动,但此时仍然仅靠一个单机数据库, 存储了不到10亿的关系,证明了MySQL的可靠性(当然是在合理优化数据库查询和使用缓存的前提下)。

推的模式很简单,就是将大量的读和写分开,先把所有的用户与feed的关系铺开,写入数据库,然后再发全国feed,这时候用户 蜂拥而至,此时的DB面临的是:

- 大量用户读取用户与feed的关系,返回feed流
- 当用户读取feed时,App会发送一个请求将该feed设置为已读,此时DB面临的是更新用户feed的读取状态

看起来和上面差不多?不对,首先第一条,在拉的模式下,由于feed、用户与feed的关系及状态、用户非群推的关系都在不同的表里, 为了查出feed流,查询数据库的次数很多,而推的模式由于把全国feed和非全国feed平铺开了,减少了查询数据库的次数; 第二条,拉的模式下,要插入feed与用户的关系,由于要构建索引的关系,DB负载会比较高,而第二种模式只需要顺着索引找到数据, 更新一个字段,不需要构建或者构建少量的索引。因此这套系统良好的运行了数年时间。

没有银弹

没有银弹,这是真的,第二种模式有一个很大的缺陷,那就是feed不是所有人都会读的,一般来说,feed的读取率都不高,平铺开 所有的关系之后,数据库里存储了大量的“无用”的关系,导致单表数据量非常之大(如上,不超过10亿),尽管目前在发送全国feed 之后响应只慢一点,负载高一些,但是这种模式没有办法持续下去。

用户会增长,日活会变高,数据量会变大,而MySQL能承担的负载有上限,经费预算也是有上限的。意味着,为了不久的将来这套系统 能承受更高的并发和多的请求及feed,需要做第二次大型优化。

我们所遇到的主要是全国feed之后带来的并发问题。全国feed的写入量很大,但是由于读取率不会很高,意味着其中超过一半的feed 关系是浪费存储空间的,所以我们要结合推拉两种模式。

我提出的解决方案是:将全国feed与用户的关系放在Redis里,在一段时间后将关系转储到MySQL。这样的好处是结合了Redis优秀的 性能和MySQL近乎无限的存储空间。为啥要这样做呢?由于这是一种结合推和拉的模式,而我们知道了拉的模式的缺点,因此我们使用 Redis的空间(或者说内存)来换取时间,当全国feed发送出去之后一段时间,该feed已经不再是热点(这是业务特点),这个时候 我们再将该feed转储到数据库(或者干脆点,直接归档,我们用的后者,具体是否直接归档需要取决于具体业务和产品设计的取舍), 此后零星的读取对数据库的性能影响就不再那么大了。

既然没有银弹,这么做的缺点是什么呢?那就是业务代码变得复杂很多,原本我们只要从用户与feed的关系表中读取,然后join出 feed就可以得到feed流,现在在读取时,需要读取数据库中的关系和feed,需要读取Redis中的关系和feed,还要把他们拼一起。 在用户变更读取状态时,也要根据feed的属性去不同地方更新,各处都需要改动。

结语

软件设计中,无处不是权衡取舍(trade off),而这就是一个活生生的典型例子。

希望这篇文章能够给做此类系统的同行一些有用的点。