前言

在日常工作中,经常会遇到一些大促场景,需要评估系统的资源是否充足,是否需要增加资源,增加多少。

想要做到“准确”的估算,需要对数字有一定的感觉。

2 的次幂

英语里面常讲 1 个 Million,1 个 Billion,分别是百万、十亿的意思。可以看到,以 3 个 0 为一组,层层递进。

延迟数字

这里有一张表格反映了一些计算机的典型操作的耗时,配套的还有一个可视化网站,这个其实见得比较多了。

图形化的网页上可以选择年份,数据也更准确。

从中可以得出一些明显的结论:

  • 内存比磁盘快。
  • 避免 disk seek。
  • 数据中心常常位于不同的区域,在它们之间传送数据比较耗时。
  • 从磁盘顺序读数据比从网络顺序读数据慢。

可用性数字

工作中,我们常用几个 9 来形容一个系统的可用性。100% 表示一个系统永远不会挂,实际中的系统可用性指标大多处于 99% -100% 之间。

像一些云厂商,如 Amazon,Microsoft,Google 承诺的可用性是 3 个 9,即 99.9% 或以上,描述的是可用时间。

一些数字积累

  • 某支付服务的支付峰值 60w QPS
  • Go GC 打开写屏障需要花费 10-30us
  • 内网中,一次网络请求的耗时是 ms 级别
  • 万兆网卡,1.25GB/s 打满
  • 4C8G 建 10w 到 20w 的连接没有问题
  • 因为机械硬盘的机械结构,随机 I/O 与顺序的 I/O 性能可能相差几百倍。固态硬盘则只有十几倍到几十倍之间
  • twitter 工程师认为,良好体验的网站平均响应时间应该在 500ms 左右,理想的时间是 200-300ms
  • 平均 QPS:日平均用户请求除以 4w。日平均用户请求,一般来自产品的评估。峰值 QPS:平均 QPS 的 2~4 倍

实战

本章最后有一个实战的例子:评估 twitter 的 QPS 和存储容量。

先给出了一些预设:

  • 300 个 million 的月活跃用户
  • 50% 的用户每天都使用 twitter
  • 用户平均每天发表 2 条 tweets
  • 10% 的 tweets 包含多媒体
  • 多媒体数据保存 5 年

下面是估算的过程:

先预估 QPS:

  • DAU(每天的活跃用户数,Daily Active Users)为:300 million(总用户数) *50% = 150 million
  • 发 tweets 的平均 QPS:150 million* 2 / 24 hour / 3600 second = ~3500
  • 高峰期 QPS 一般认为是平均 QPS 的 2 倍:2 * 3500 = 7000 QPS

再来估算存储容量:

假设多媒体的平均大小为 1MB,那么每天的存储容量为:150 million *2* 10% *1MB = 30 TB。5 年的存储容量为 30 TB* 365 * 5 = 55 PB

最后这两个的估算过程是这样的:

300 个 million *10%* 1MB,1 MB 其实就是 6 个 0,相当于 million 要进化 2 次:million -> billion -> trillion,即从 M -> G -> T,于是结果等于 300 T * 10% = 30 T。

30 TB *365* 5 = 30 TB *1825 = 30 TB* 10^3 *1.825,TB 进化一次变成 PB,于是等于 30* 1.825 PB = 55 PB。

Go map[int64]int64 写入 redis 占用多少内存

这是我最近在做的一个工作,将内存中的一个超大的 map[int64]int64 写入到 redis,map 里的元素个数是千万级的。设计方案的时候,需要对 redis 的容量做一个估算。

如果不了解 redis 的话,可能你的答案是用元素个数直接乘以 16B(key 和 value 各占 8B)。我们假设元素个数是 5kw,那估算结果就是:5kw *16B=50kk* 16B = 800MB

答案是错的。

为了解决这个问题,需要深入地研究一下 redis 的数据结构。

整个 redis 数据库就是一个大的 map,它容纳了所有的 key,我们都知道 key 都是 string 类型,而 value 则有 string, list, set, hashmap, zset……等类型。

Redis 中的一个 k-v 对用一个 entry 项表示,其中每个 entry 包含 key、value、next 三个指针,共 24 字节。由于 redis 使用 jemalloc 分配内存,因此一个 entry 需要申请 32 字节的内存。这里的 key, value 指针分别指向一个 RedisObject:

1
2
3
4
5
6
7
8
redis entry
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS;
    int refcount;
    void *ptr;
} robj;

RedisObject 对应前面提到的各种数据类型,其中最简单的就是 redis 内部的字符串了。它有如下几种编码格式:

图中的元数据包括 type,encoding,lru, refcount,分别表示数据类型,编码类型,最近一次访问的时间戳,引用次数。

当字符串是一个整型时,直接放在 ptr 位置,不用再分配新的内存了,非常高效。

解析一下 44 字节的原因:元数据和 ptr 共占 16 字节,加上 44 字节,再加上字符串末尾的 ‘\0’,共61 字节。因为字符串的长度只有 44,因此 len 和 alloc 各用 1 个字节就够了。再加上 1 个字节的 flags,刚好是 64 字节。超过了这个值,SDS 就需要单独再申请一块内存,导致访问的时候就多了一跳指针。

多提一句,redis 最大支持 512MB 大小的字符串。

回答本文的问题,恰好我们要写入 redis 的 map 中的 key 和 value 都是整数,因此直接将值写入 ptr 处即可。

于是 map 的一个 key 占用的内存大小为:32(entry)+16(key)+16(value)=64B。于是,5kw 个 key 占用的内存大小是 5kw*64B = 50 kk* 64B = 3200MB ≈ 3G

假如我们在 key 前面加上了前缀,那就会生成 SDS,占用的内存会变大,访问效率也会变差。

总之,我们根据要写入 redis 中的字符串的长度可以很方便地估算占用内存的总大小。如果 key 和 value 恰好都是 int64 类型的,那么尽量不要在 key 前加前缀,这样可以直接使用 key 的个数乘以 64B 就能算出占用内存的大小。

转载

搞定系统设计 02:估算的一些方法

Go map[int64]int64 写入 redis 占用多少内存