Skip to content

Commit

Permalink
feat(posts): add perf-monolithic-software
Browse files Browse the repository at this point in the history
  • Loading branch information
KKKIIO committed Jul 28, 2024
1 parent 09bb1f6 commit 4a3eb2e
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule "themes/even"]
path = themes/even
url = https://github.com/ahonn/hexo-theme-even
url = https://github.com/KKKIIO/hexo-theme-even.git
1 change: 1 addition & 0 deletions source/_posts/2022-03-03-copy-paste-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ tags: [engineering]
---

> 强韧和反脆弱性的系统不必像脆弱的系统一样,后者必须精确地理解这个世界,因而它们不需要预测,这让生活变得简单许多。
>
> 要看看冗余是一种多么缺乏预测性,或者更确切地说,预测性更低的行为模式,让我们借用一下第 2 章的说法:如果你把多余的现金存入银行(再加上储藏在地下室的贸易品,如猪肉和豆泥罐头,以及金条),你并不需要精确地知道哪些事件可能会陷你于困境。这些事件可能是一场战争、一场革命、一场地震、一次经济衰退、一场疫情、一次恐怖袭击,或者新泽西州的分裂等任何事情,但你并不需要作太多的预测。
>
> 《反脆弱》 — [] 纳西姆·尼古拉斯·塔勒布
Expand Down
5 changes: 2 additions & 3 deletions source/_posts/2023-11-05-react-php.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ date: 2023-11-05 20:45:00 +0800
tags: [web]
---

![next php](/assets/image/next-php.png)

![next php](/assets/image/next-php-16-9.png)

2020 年末 React Blog 介绍了 [Server Component](https://react.dev/blog/2020/12/21/data-fetching-with-react-server-components),它是一种可以在服务端渲染的 React 组件。
它几乎跟普通组件一样,只是没有交互功能,所以你可以先在服务端渲染这些组件,然后在客户端继续渲染剩下的部分。
Expand Down Expand Up @@ -143,7 +142,7 @@ export async function Page({ params }: { params: { id: string } }) {
}

// KeysPage2.tsx
"use client";
("use client");
export function KeysPage2({ card }) {
const {
data: keys,
Expand Down
1 change: 0 additions & 1 deletion source/_posts/2024-06-02-prefer-function-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ tags: [engineering, backend]
> Most of the tests in the 'IntelliJ Platform' codebase are model-level functional tests.
>
> The tests run in a headless environment that uses real production implementations for most components, except for many UI components.
>
> ...
> In a product with 20+ years of a lifetime that has gone through many internal refactorings, we find that this benefit dramatically outweighs the downsides of slower test execution and more difficult debugging of failures compared to more isolated unit tests.
>
Expand Down
132 changes: 132 additions & 0 deletions source/_posts/2024-06-13-perf-monolithic-software.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
title: "谁动了我的CPU"
date: 2024-07-24 23:37:00 +0800
tags: [engineering, performance, backend]
---

几年前进了一家 toB 软件公司,主要产品功能都跑在一个大单体 Go 服务里。

一天我们接到一个性能优化任务,客户要求在千万级数据量、50 并发下,接口 90 分位耗时保持 500ms 以下。

当时我只在 ToC 公司工作过,并不觉得这是一个很高的要求,直到去测试环境上体验,发现到处都是接口超时错误。

<!--more-->

## ToB 复杂软件

ToB 软件要满足不同客户的定制需求,通常会把系统做得大而全。
这个系统更是典型,功能多,配置层级多,还会像 CSS 一样会相互影响。

归根到底也是个正常的 Web 服务,我想。
按经验,服务端性能问题几乎是数据查询慢导致的,排查方向自然是先从数据库 MySQL 慢日志开始。

不知道算不算好事,MySQL 的慢日志在狂刷。
想抓住一条日志来分析,却在代码里搜不到对应 SQL。

“是框架查的数据库“,我担心的事情发生了。

### 自研框架

这系统有个功能强大、高度灵活的自研查询框架,简称 G。
它对外暴露 GraphQL 接口,对内负责查询数据库。
业务只负责定义“对象”结构,框架会自动根据外部请求里的查询字段、筛选条件,去查询、筛选数据,像是一个魔化版的 [Django](https://docs.djangoproject.com/en/5.0/intro/tutorial02/#creating-models)

拿 GraphQL 官方文档例子来说,下面这个查询:

```graphql
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
```

会先根据 `episode` 查询出 `hero`,再根据 `hero` 查询出 `friends`,最后返回结果。

强大和灵活的代价是,你几乎不知道这些慢 SQL 是怎么来的,组装 SQL 依赖前面一堆步骤处理后的参数。

没法要求原作者详细解释这个框架,硬啃代码又显然太慢。
我们希望有个快捷的方法,从一堆现象里找到问题。

### 动态类型、高度并发

为了快,我先用 Go 官方提供的 pprof[^1] 工具分析程序。

pprof 可以分析 CPU 使用情况,虽然这不会告诉你数据库查询花了多久,但一些结果集大的查询会因为内存分配频繁,被 pprof 捕捉到调用栈,再被我们观察到。

结果不如人意,调用栈上几乎全是框架 G 的内部函数,它精巧的设计影响了排查工作:

1. 框架 G 使用大量 goroutine 并发查询每个字段,导致调用栈丢失了来源函数的信息。
2. 框架 G 到处使用 `any` 传递数据,高度复用的查询代码让不同请求的调用栈都惊人地一致。

从 pprof 结果里看不出程序在跑哪块业务流程,在做什么。

求助以前优化过该系统的同事,得知当时也只用 pprof,解决的问题很少。
我们需要探索新方案。

## 观察系统

我们最大的问题就是不知道程序在做什么,因此提升可观察性 [Observability](<https://en.wikipedia.org/wiki/Observability_(software)>) 是关键。

观察耗时的工具可分为指标 Metrics、日志 Logging、链路追踪 Tracing。
而链路追踪能记录一个请求在系统中的执行过程,无论执行是串行还是并发,系统是单体还是分布式,这非常符合我们的需求。

![tracing example](https://miro.medium.com/v2/resize:fit:4800/format:webp/1*_DzcdeHiRp6JOG2FqA3bfg.png)

go 主流的 tracing 方案是侵入式的,需要修改代码埋点。
我申请了一天时间,用 [gls](https://github.com/jtolio/gls) 和改 vendor 等旁门左道在数据库操作等关键区域埋点。

结果还算满意,在链路图里能看到一次请求下很多动作 Span 的耗时、先后次序。
在链路追踪的帮助下,我们定位到了很多问题,下面举几个有趣的例子。

## 阳光猛烈,万物显形

### 锁,缓存和读降级

在压测下,有时会出现一段时间内不少接口的耗时飙升到 5 秒以上的情况。

在这些请求的链路图里,都有一段 5 秒的 Span,底下没有任何数据库查询。
顺藤摸瓜,我在底层找到了一个锁,它用于管理一个粒度很粗的缓存。

请求每次都要从缓存拿几百万条数据,一旦缓存过期,就得拿着锁,花几十秒去查数据库。
可能这个缓存性能问题太明显,之前的人又给它加上读降级机制:当缓存过期时,先尝试抢锁 5 秒,如果抢不到,就返回过期数据。

这些机制不仅没解决性能问题,反而让排查变得复杂,我们赶紧去掉了它们,并优化了缓存粒度。

### 悲观的 Redis 锁

当我们把并发从 50 提高到 100 时,一些写接口耗时从几百毫秒飙升到几秒以上。

链路图可以看到不少重复的 Redis `SETNX` 操作,明显是在实现锁。
`SETNX`操作间隔 200ms ,也就是说一旦抢锁失败就会等 200ms 再试一次。
一旦锁冲突多了,等待次数就变多,耗时也会变长。

程序用 Redis 锁的地方挺多,我们去掉了一些不必要的加锁操作,也有用 lua 脚本实现原子操作代替了锁。

### Shlemiel 油漆算法[^2]

如果不是亲眼所见,我很难相信纯内存操作能耗时十几秒。

有一段很长的链路下面没有任何 IO 和锁操作。
在我用二分法埋点排除各种嫌疑后,才发现罪魁祸首是一个看似简朴的内存筛选操作。
它的实现是把不符合条件的元素从数组中删除。
问题是,这里把删除一个元素实现成了 O(N) 算法,那删除 M 个元素就是 O(M\*N) 的复杂度。
当数量比较大时,这个操作就拖慢了整个请求。

也许做算法题是有用的 😈。

## 二八定律

难道所有性能问题都可以通过“仔细观察”解决吗?

当然不是,例如我在{% post_link inverted-index %}提到的复杂查询,即使知道执行流程,也很难通过调整数据库索引来优化每个场景的性能,得改变算法(如上索引服务 ElasticSearch)才能整体解决问题。

然而超过 80% 的性能问题,都可以在“仔细观察”后轻松解决。
就像处理 BUG,多数精力都用在取得那条关键日志。
优化性能则是需要观察程序在做什么,然后把可以不做/少做的事情去掉。

[^1]: 对 pprof 还不了解的推荐看 Go 官方博客 [Profiling Go Programs](https://go.dev/blog/pprof)
[^2]: [Back to Basics](https://www.joelonsoftware.com/2001/12/11/back-to-basics/)
Binary file added source/assets/image/next-php-16-9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed source/assets/image/next-php.png
Binary file not shown.

0 comments on commit 4a3eb2e

Please sign in to comment.