Infra 学习笔记:延迟数字、九个九与一次容量估算
· 6 min read

Infra 学习笔记:延迟数字、九个九与一次容量估算


Table of Contents

引用块开场:基础设施工程师的三门必修课——心中有数字、手上有配置、脑内有公式。这篇笔记把三者塞进同一篇文章,顺便把博客的排版格式测个遍。

一、延迟数字:程序员必须背下来的表

Jeff Dean 有一个著名的观察1:大多数工程师对数量级缺乏直觉。下面这张表值得贴在工位上:

操作耗时直觉换算
L1 缓存引用0.5 ns1 秒
互斥锁加锁25 ns半分钟
主内存访问100 ns2 分钟
SSD 随机读150,000 ns2 天
磁盘寻道10 ms4 个月
跨机房 RTT~150 ms5 年

换算基准是把 1 ns 放大到 1 秒之后,其余操作等比例放大——这正是理解”为什么要有缓存”最直观的方式。内存访问要等 2 分钟,磁盘要等 4 个月,那当然要缓存。

二、九个九的可用性:一段真正的数学

“三个九""五个九”是 SLA 里的日常。全年停机时长可以用一个公式算清楚:

Dyear=365×24×3600×(1−A)D_{\text{year}} = 365 \times 24 \times 3600 \times (1 - A)

其中 AA 是可用性承诺。代入几个典型值:99.9%99.9\% 对应 8.768.76 小时,99.99%99.99\% 对应 52.652.6 分钟,而 99.999%99.999\% 只剩 5.265.26 分钟——这就是为什么每多一个”九”,成本都是台阶式上升的。

单实例不够可靠,于是要堆冗余。nn 个独立实例(故障相互独立、每次只挂一个)的整体可用性:

Acluster=1−(1−A)n=1−(1−0.99)3≈1−10−6=0.999999\begin{aligned} A_{\text{cluster}} &= 1 - (1 - A)^n \\ &= 1 - (1 - 0.99)^3 \\ &\approx 1 - 10^{-6} \\ &= 0.999999 \end{aligned}

三个 99%99\% 的实例叠出六个九——前提是故障独立。现实中掉电、机房级断网都会打破独立性,所以还得搭配多可用区部署。

排队论里还有一条更常用的 Little’s Law:

L=λ×WL = \lambda \times W

系统里平均请求数 LL 等于到达速率 λ\lambda 乘以平均驻留时间 WW。并发 10001000、平均延迟 200 ms200\text{ ms},意味着吞吐约 5000 QPS5000\text{ QPS}——容量估算的第一步就靠它。

三、容量估算:以一个短链服务为例

需求:每天 10810^8 次跳转,读写比 100:1100:1。

  1. QPS:108/86400≈115710^8 / 86400 \approx 1157 次读/秒,写约 1212 次/秒
  2. 存储:每条记录按 500 B500\text{ B} 算,十年约 3.65×10113.65 \times 10^{11} 条,共 ≈170 TB\approx 170\text{ TB}——必须上分布式存储
  3. ID 空间:base62 编码下 77 位可表达 627≈3.5×101262^7 \approx 3.5 \times 10^{12} 个短链,足够

用 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 相对路径引用和灯箱都工作正常:

暖灯下的图书馆书架,像极了机房里整齐的机柜


注释里再放一条行内公式收尾:可用性每提升一个九,复杂度大约翻倍 C∝2nC \propto 2^{n} 2。

Footnotes

  1. 数字出自 Jeff Dean & Luiz Barroso, The Tail at Scale(CACM, 2013)及其演讲《Techniques for building cloud-ready services》。 ↩

  2. 经验法则,不是定理——但和直觉一致。 ↩