本文是《程序员数学扫盲课》系列文章
← 上一篇:程序员数学09:信息论 – 数据压缩
TL;DR
为什么100万用户需要多少台服务器?为什么数据库连接池要设置多大?为什么缓存命中率从90%提升到95%,性能能翻倍?答案都藏在组合数学里。这篇文章用Go代码带你搞懂容量规划的数学原理,看完你会发现:组合数学就是”计算可能性的数量”。
系列导航
《程序员数学扫盲课》系列:
1. 破冰篇:数学符号就是代码
2. 对数Log:数据库索引的魔法
3. 集合论:玩转Redis与SQL
4. 图论基础:微服务依赖管理
5. 概率论:系统可用性计算
6. 统计学:P99延迟与监控报警
7. 线性代数入门:推荐系统的数学基础
8. 哈希与模运算:负载均衡算法
9. 信息论:数据压缩与编码
10. 组合数学:容量规划与性能预估(本篇)
一、排列与组合:计算可能性
先说重点:排列关心顺序,组合不关心顺序。
1.1 从密码破解说起
场景: 4位数字密码,每位可以是0-9。
问题: 总共有多少种可能?
答案: 10 × 10 × 10 × 10 = 10,000种
这就是排列(Permutation):每个位置都有10种选择,顺序不同算不同密码。
Go代码验证:
package main
import "fmt"
func main() {
// 4位数字密码
digits := 10 // 每位有10种选择(0-9)
length := 4 // 密码长度
total := 1
for i := 0; i < length; i++ {
total *= digits
}
fmt.Printf("4位数字密码总数: %d\n", total)
fmt.Printf("暴力破解平均尝试次数: %d\n", total/2)
}
输出:
4位数字密码总数: 10000
暴力破解平均尝试次数: 5000
1.2 排列公式
从n个元素中选k个排列:
P(n, k) = n! / (n-k)!
示例: 从5个人中选3个排队,有多少种排法?
P(5, 3) = 5! / (5-3)! = 5! / 2! = 120 / 2 = 60
Go代码实现:
// 阶乘
func Factorial(n int) int {
if n <= 1 {
return 1
}
result := 1
for i := 2; i <= n; i++ {
result *= i
}
return result
}
// 排列数
func Permutation(n, k int) int {
return Factorial(n) / Factorial(n-k)
}
func main() {
n, k := 5, 3
result := Permutation(n, k)
fmt.Printf("从%d个人中选%d个排队: %d种排法\n", n, k, result)
}
输出:
从5个人中选3个排队: 60种排法
1.3 组合公式
从n个元素中选k个组合(不关心顺序):
C(n, k) = n! / (k! × (n-k)!)
示例: 从5个人中选3个组队(不关心顺序),有多少种选法?
C(5, 3) = 5! / (3! × 2!) = 120 / (6 × 2) = 10
Go代码实现:
// 组合数
func Combination(n, k int) int {
return Factorial(n) / (Factorial(k) * Factorial(n-k))
}
func main() {
n, k := 5, 3
perm := Permutation(n, k)
comb := Combination(n, k)
fmt.Printf("从%d个人中选%d个:\n", n, k)
fmt.Printf("排列(关心顺序): %d种\n", perm)
fmt.Printf("组合(不关心顺序): %d种\n", comb)
fmt.Printf("关系: 排列 = 组合 × k! = %d × %d = %d\n", comb, Factorial(k), perm)
}
输出:
从5个人中选3个:
排列(关心顺序): 60种
组合(不关心顺序): 10种
关系: 排列 = 组合 × k! = 10 × 6 = 60
二、容量规划:计算需要多少资源
2.1 服务器容量计算
场景: 100万日活用户,每个用户平均发10个请求,每台服务器QPS=1000。
问题: 需要多少台服务器?
计算:
总请求数 = 100万 × 10 = 1000万/天
峰值QPS = 1000万 / (24×3600) × 峰值倍数
假设峰值是平均值的3倍:
峰值QPS = 1000万 / 86400 × 3 ≈ 347 QPS
服务器数量 = 峰值QPS / 单机QPS = 347 / 1000 ≈ 1台
但要考虑冗余:1 × 2 = 2台(双活)
Go代码实现:
package main
import (
"fmt"
"math"
)
// 容量规划计算器
type CapacityPlanner struct {
dailyActiveUsers int // 日活用户
requestsPerUser int // 每用户请求数
peakFactor float64 // 峰值倍数
serverQPS int // 单机QPS
redundancy float64 // 冗余系数
}
func (cp *CapacityPlanner) Calculate() int {
// 总请求数
totalRequests := cp.dailyActiveUsers * cp.requestsPerUser
// 平均QPS
avgQPS := float64(totalRequests) / (24 * 3600)
// 峰值QPS
peakQPS := avgQPS * cp.peakFactor
// 所需服务器数(不含冗余)
baseServers := math.Ceil(peakQPS / float64(cp.serverQPS))
// 加上冗余
totalServers := int(math.Ceil(baseServers * cp.redundancy))
fmt.Printf("容量规划结果:\n")
fmt.Printf("日活用户: %d\n", cp.dailyActiveUsers)
fmt.Printf("总请求数: %d/天\n", totalRequests)
fmt.Printf("平均QPS: %.2f\n", avgQPS)
fmt.Printf("峰值QPS: %.2f (峰值倍数: %.1fx)\n", peakQPS, cp.peakFactor)
fmt.Printf("基础服务器数: %.0f台\n", baseServers)
fmt.Printf("含冗余服务器数: %d台 (冗余系数: %.1fx)\n", totalServers, cp.redundancy)
return totalServers
}
func main() {
planner := &CapacityPlanner{
dailyActiveUsers: 1000000, // 100万日活
requestsPerUser: 10, // 每用户10个请求
peakFactor: 3.0, // 峰值是平均值3倍
serverQPS: 1000, // 单机1000 QPS
redundancy: 2.0, // 双活冗余
}
servers := planner.Calculate()
fmt.Printf("\n最终结论: 需要 %d 台服务器\n", servers)
}
输出:
容量规划结果:
日活用户: 1000000
总请求数: 10000000/天
平均QPS: 115.74
峰值QPS: 347.22 (峰值倍数: 3.0x)
基础服务器数: 1台
含冗余服务器数: 2台 (冗余系数: 2.0x)
最终结论: 需要 2 台服务器
2.2 缓存命中率的威力
场景: 数据库查询100ms,缓存查询1ms。
问题: 缓存命中率从90%提升到95%,性能提升多少?
计算:
90%命中率:
平均延迟 = 0.9×1ms + 0.1×100ms = 0.9 + 10 = 10.9ms
95%命中率:
平均延迟 = 0.95×1ms + 0.05×100ms = 0.95 + 5 = 5.95ms
性能提升 = (10.9 - 5.95) / 10.9 = 45.4%
Go代码实现:
package main
import "fmt"
// 缓存性能计算器
type CachePerformance struct {
cacheLatency float64 // 缓存延迟(ms)
dbLatency float64 // 数据库延迟(ms)
}
func (cp *CachePerformance) AvgLatency(hitRate float64) float64 {
return hitRate*cp.cacheLatency + (1-hitRate)*cp.dbLatency
}
func (cp *CachePerformance) Compare(hitRate1, hitRate2 float64) {
latency1 := cp.AvgLatency(hitRate1)
latency2 := cp.AvgLatency(hitRate2)
improvement := (latency1 - latency2) / latency1 * 100
fmt.Printf("缓存性能对比:\n")
fmt.Printf("缓存延迟: %.1fms, 数据库延迟: %.1fms\n", cp.cacheLatency, cp.dbLatency)
fmt.Printf("\n命中率 %.0f%%:\n", hitRate1*100)
fmt.Printf(" 平均延迟: %.2fms\n", latency1)
fmt.Printf("\n命中率 %.0f%%:\n", hitRate2*100)
fmt.Printf(" 平均延迟: %.2fms\n", latency2)
fmt.Printf("\n性能提升: %.2f%%\n", improvement)
}
func main() {
cache := &CachePerformance{
cacheLatency: 1, // 1ms
dbLatency: 100, // 100ms
}
cache.Compare(0.90, 0.95)
}
输出:
缓存性能对比:
缓存延迟: 1.0ms, 数据库延迟: 100.0ms
命中率 90%:
平均延迟: 10.90ms
命中率 95%:
平均延迟: 5.95ms
性能提升: 45.41%
关键发现: 命中率从90%提升到95%(只提升5个百分点),性能提升45%!
三、小结
这篇文章的核心观点:
- 排列关心顺序,组合不关心顺序,排列数 = 组合数 × k!
- 容量规划公式:服务器数 = 峰值QPS / 单机QPS × 冗余系数
- 缓存命中率的威力:从90%提升到95%,性能提升45%
- 组合数学是容量规划的数学基础:服务器数量、连接池大小、缓存容量都靠它
- 性能优化要算投入产出比:提升5%命中率比增加1倍服务器更划算
记住这个对照表:
| 概念 | 数学定义 | Go实现 | 应用场景 |
|---|---|---|---|
| 排列 | P(n,k) = n!/(n-k)! | Permutation() |
密码破解 |
| 组合 | C(n,k) = n!/(k!(n-k)!) | Combination() |
选择方案 |
| 容量规划 | 峰值QPS/单机QPS | CapacityPlanner |
服务器数量 |
| 缓存性能 | 加权平均延迟 | CachePerformance |
性能预估 |
实战建议:
– 容量规划要考虑峰值倍数(通常3-5倍)和冗余系数(至少2倍)
– 缓存命中率每提升1%,性能提升约5-10%
– 数据库连接池大小 = 并发数 / 平均响应时间 × 安全系数
下次面试官问”如何做容量规划”,你就可以回答:先算峰值QPS(日活×请求数/86400×峰值倍数),再除以单机QPS得到基础服务器数,最后乘以冗余系数(通常2倍)。还要考虑缓存命中率,命中率每提升5%,性能能提升40-50%。
参考资料
– 《具体数学》:组合数学基础
– Google SRE Book:容量规划
– 《高性能MySQL》:连接池配置













程序员数学扫盲课
AI周刊:大模型、智能体与产业动态追踪
Claude Code 全体系指南:AI 编程智能体实战
Karpathy神经网络零基础课程
最新评论
开源的AI对话监控面板很实用,正好团队在找这类工具。准备试用一下。
折叠屏市场确实在升温,不过售罄也可能是备货策略。期待看到实际销量数据。
从磁盘I/O角度解释B树的设计动机,这个切入点很好。终于理解为什么数据库不用二叉树了。
IT术语转换确实是个痛点,之前用搜狗总是把技术词汇转成奇怪的词。智谱这个方向值得期待。
这个工具结合LLM和搜索API的思路很有意思,正好解决了我在做知识管理时遇到的问题。请问有没有部署文档?
这个漏洞确实严重,我们团队上周刚遇到类似问题。建议补充一下如何检测现有项目是否受影响的方法。
从简单规则涌现复杂性这个思路很有意思,让我想起元胞自动机。不过数字物理学在学术界争议还挺大的。
我也遇到了指令跟随变差的问题,特别是多轮对话时容易跑偏。不知道是模型退化还是负载优化导致的。