Infra 学习笔记:延迟数字、九个九与一次容量估算
Table of Contents
引用块开场:基础设施工程师的三门必修课——心中有数字、手上有配置、脑内有公式。这篇笔记把三者塞进同一篇文章,顺便把博客的排版格式测个遍。
一、延迟数字:程序员必须背下来的表
Jeff Dean 有一个著名的观察1:大多数工程师对数量级缺乏直觉。下面这张表值得贴在工位上:
| 操作 | 耗时 | 直觉换算 |
|---|---|---|
| L1 缓存引用 | 0.5 ns | 1 秒 |
| 互斥锁加锁 | 25 ns | 半分钟 |
| 主内存访问 | 100 ns | 2 分钟 |
| SSD 随机读 | 150,000 ns | 2 天 |
| 磁盘寻道 | 10 ms | 4 个月 |
| 跨机房 RTT | ~150 ms | 5 年 |
换算基准是把 1 ns 放大到 1 秒之后,其余操作等比例放大——这正是理解”为什么要有缓存”最直观的方式。内存访问要等 2 分钟,磁盘要等 4 个月,那当然要缓存。
二、九个九的可用性:一段真正的数学
“三个九""五个九”是 SLA 里的日常。全年停机时长可以用一个公式算清楚:
其中 是可用性承诺。代入几个典型值: 对应 小时, 对应 分钟,而 只剩 分钟——这就是为什么每多一个”九”,成本都是台阶式上升的。
单实例不够可靠,于是要堆冗余。 个独立实例(故障相互独立、每次只挂一个)的整体可用性:
三个 的实例叠出六个九——前提是故障独立。现实中掉电、机房级断网都会打破独立性,所以还得搭配多可用区部署。
排队论里还有一条更常用的 Little’s Law:
系统里平均请求数 等于到达速率 乘以平均驻留时间 。并发 、平均延迟 ,意味着吞吐约 ——容量估算的第一步就靠它。
三、容量估算:以一个短链服务为例
需求:每天 次跳转,读写比 。
- QPS: 次读/秒,写约 次/秒
- 存储:每条记录按 算,十年约 条,共 ——必须上分布式存储
- ID 空间:base62 编码下 位可表达 个短链,足够
用 Python 验一遍心算,顺便测试代码高亮:
DAILY_REDIRECTS = 10**8
SECONDS_PER_DAY = 86400
RECORD_BYTES = 500
YEARS = 10
read_qps = DAILY_REDIRECTS // SECONDS_PER_DAY
write_qps = read_qps // 100
storage_tb = DAILY_REDIRECTS * 365 * YEARS * RECORD_BYTES / 2**40
print(f"read {read_qps} qps, write {write_qps} qps, storage {storage_tb:.1f} TB")
# read 1157 qps, write 11 qps, storage 170.4 TB
四、最小可用配置三件套
4.1 应用:一个带健康检查的 Go 服务
package main
import (
"net/http"
"os"
"time"
)
func healthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", healthz)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// 模拟业务处理
time.Sleep(20 * time.Millisecond)
w.Write([]byte("hello infra"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 3 * time.Second, // 防 slowloris
}
if err := srv.ListenAndServe(); err != nil {
os.Exit(1)
}
}
4.2 打包:多阶段 Dockerfile
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /bin/app .
FROM gcr.io/distroless/static-debian12
COPY --from=build /bin/app /app
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/app"]
4.3 编排:Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: shortlink
labels: { app: shortlink }
spec:
replicas: 3
selector:
matchLabels: { app: shortlink }
template:
metadata:
labels: { app: shortlink }
spec:
containers:
- name: app
image: registry.example.com/shortlink:v1.4.2
ports: [{ containerPort: 8080 }]
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 2
periodSeconds: 5
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }
滚动发布后的配置变更,用 diff 看最清楚:
spec:
replicas: 3
+ strategy:
+ rollingUpdate:
+ maxUnavailable: 0
+ maxSurge: 1
template:
spec:
containers:
五、排障片段收藏
进程悄悄吃满内存,先抓现场:
# 找出容器内存占用 Top3,并输出格式化时间戳
docker stats --no-stream --format \
'{{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}' \
| sort -t$'\t' -k2 -hr | head -3 \
| while IFS=$'\t' read -r name mem perc; do
printf '%s | %s | %s\n' "$(date +'%F %T')" "$name" "$mem"
done
慢查询定位,执行计划先行:
EXPLAIN ANALYZE
SELECT url, COUNT(*) AS hits
FROM redirect_log
WHERE created_at >= NOW() - INTERVAL '7 days'
GROUP BY url
ORDER BY hits DESC
LIMIT 20;
Nginx 反代 + 长连接复用(这行配置名故意写得很长,用来测试横向溢出 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for_with_very_long_header_name_for_overflow_testing):
upstream shortlink_backend {
least_conn;
server 10.0.0.11:8080 max_fails=3 fail_timeout=10s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=10s;
keepalive 64;
}
server {
listen 443 ssl http2;
server_name short.example.com;
location / {
proxy_pass http://shortlink_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
}
}
六、待办与小结
- 背下延迟数字表
- 推导三个九到六个九的冗余公式
- 用 Little’s Law 做一次心算
- 把 Dockerfile 里的 distroless 镜像换成 Chainguard 再对比体积
- 给 Nginx 加限流(
limit_req_zone)
小结:fmt.Sprintf 这样的行内代码、过期的估算 被修正的结论、快捷键 Ctrl+C、以及一句 高亮 的结论——基础设施的本质是在不确定的硬件上提供确定的服务,公式给你边界,配置给你兜底。
如果图片也正常,说明 Markdown 相对路径引用和灯箱都工作正常:

注释里再放一条行内公式收尾:可用性每提升一个九,复杂度大约翻倍 2。