关于linux:一次内存性能提升的项目实践

5次阅读

共计 2786 个字符,预计需要花费 7 分钟才能阅读完成。

古代的开发语言除了 C ++ 以外,大部分都对内存治理做好了封装,个别的开发者基本都接触不到内存的底层操作。更何况当初各种优良的开源组件利用越来越多,例如 mysql、redis 等,这些甚至都不须要大家入手开发,间接拿来用就好了。所以有些同学也会感觉作为应用层开发的同学没有学习的必要去学习底层。
但我想通过本文的理论案例通知大家,哪怕不间接接触内存底层操作,就只是用一些开源的工具,如果你能了解底层的工作原理,你也可能用到极致。

用户拜访历史读写需要

如果当初有这样一个业务需要,用户每次刷新都须要取得要生产的新数据,然而不能和之前拜访过的历史反复。你能够把它和你常常在用的今日头条之类的信息流 app 分割起来。每次都要看到新的新闻,然而你必定不想看到过来曾经看过的文章。这样在性能实现的时候,就必要保留用户的拜访历史。当用户再来刷新的时候,首先得获取用户的历史记录,要保障推给用户的数据和之前的不反复。当举荐实现的时候,也须要把这次新举荐过的数据 id 记录到历史里。
为了适当升高实现复杂度,咱们能够规定每个用户只有不和过来的一万条记录反复就能够了。这样每个用户最多只须要保留一万条历史 id,如果存满了就把最早的历史记录挤掉。咱们进一步具体化一下这个需要的几个关键点:

  • 每个数据 id 是一个 int 整数来示意
  • 每个用户要保留 1 万条 id
  • 每次用户刷新开始的时候须要将这 1 万条历史全副读取进去过滤一遍
  • 每次用户刷新完结的时候须要将新拜访过的 10 条写入一遍,如果超过 1 万需将最早的记录挤掉

可见,每次用户拜访的时候,会波及到一个 1 万规模的数据集上的一次读取和一次写入操作。

好了,需要形容完了,咱们怎么样进行咱们的技术计划的设计呢?置信你也能想到很多实现计划,咱们明天来比照两个基于 Redis 下的存储计划在性能方面的优劣。

计划一:用 Redis 的 list 来存储

首先能想到的第一个方法就是用 Redis 的 List 来保留。因为这个数据结构设计的太适宜下面的场景了。
List 下的 lrange 命令能够实现一次性读取用户的所有数据 id 的需要。

$redis->lrange('TEST_KEY', 0,9999);

lpush 命令能够实现新的数据 id 的写入,ltrim 能够保障将用户的记录数量不超过 1 万条。

$redis->lpush('TEST_KEY', 1,2,3,4,5,6,7,8,9,10);
$redis->ltrim('TEST_KEY', 0,9999);

咱们筹备一个用户,提前存好一万条 id。写入的时候每次只写入 10 条新的 id,读取的时候通过 lrange 一次全副读取进去。进行一下性能耗时测试,后果如下。

Write repeats:10000     time consume:0.65939211845398   each 6.5939211845398E-5
Rrite repeats:10000     time consume:42.383342027664    each 0.0042383342027664

计划二:用 Redis 的 string 来存储

我能想到的另外一个技术计划就是间接用 String 来存。咱们能够把 1 万个 int 示意的数据 id 拼接成一个字符串,用一个非凡的字符把他们宰割开。例如:”100000_100001_10002″ 这种。存储的时候,拼接一下,而后把这个大字符串写到 Redis 里。读取的时候,把大字符串整体读取进去,而后再用字符切割成数组来应用。

因为用 string 存储的时候,保留前多了一个拼接字符串的操作,读取后多了一步将字符串宰割成数组的操作。在测试 string 计划的时候,为了偏心起见,咱们把须要把这两步的开销也思考进来。
外围代码如下:

$userItems = array(......);

// 写入
for($i=0; $i<$repeats; $i++){$redis->set('TEST_KEY', implode('_', $userItems));
}
// 读取
for($i=0; $i<10000; $i++){$items = explode("_", $redis->get('TEST_KEY'));
}

耗时测试后果如下

Write repeats:10000     time consume:6.4061808586121    each 0.00064061808586121
Read repeats:10000      time consume:4.9698271751404    each 0.00049698271751404

论断

咱们再直观比照下两个技术计划的性能数据。

| | 写入耗时 | 读取耗时 | 总耗时
| —- | —- | —- | —- |
| list | 0.066ms | 4.238ms | 4.304ms|
| string | 0.640ms | 0.496ms | 1.136ms|

基于 list 的计划里,写入速度十分快,只须要 0.066ms,因为仅仅只须要写入新增加的 10 条记录就能够了,再加一次链表的截断操作,然而读取性能可就要慢很多了,超过了 4ms。起因之一是因为读取须要整体遍历,但其实还有第二个起因。咱们本案例中的数据量过大,所以 Redis 在外部实际上是用双端链表来实现的。.

通过上图你可能看进去,链表是通过指针串起来的。大量的 node 之间极大可能是随机地散布在内存的各个地位上,这样你遍历整个链表的时候,实际上大概率会导致内存的随机模式下工作。

基于 string 计划在写入的时候耗时比 list 要高,因为每次都得须要将 1 万条全副写入一遍。然而读取性能却比 list 高了 10 倍,总体上耗时加起来大概只有计划一的 1 / 4 左右。为什么?咱们再来看下 redis string 数据结构的内存布局

可见,如果用 string 来存储的话,不论用户的数据 id 有多少,拜访将全部都是程序 IO。程序 IO 的益处有两点:

  • 1. 一内存的程序 IO 的耗时大概只是随机 IO 的 1 /3-1/ 4 左右,
  • 2. 对于读取来说,程序拜访将极大地晋升 CPU 的 L1、L2、L3 的 cache 命中率

所以如果你深刻了内存的工作原理,哪怕你不能间接去操作内存,即便只是用一些开源的软件,你也可能将它的性能施展到极致~



开发内功修炼之内存篇专辑:

  • 1. 带你深刻了解内存对齐最底层原理
  • 2. 内存随机也比程序拜访慢,带你深刻了解内存 IO 过程
  • 3. 从 DDR 到 DDR4,内存外围频率其实基本上就没太大的提高
  • 4. 理论测试内存在程序 IO 和随机 IO 时的拜访延时差别
  • 5. 揭穿内存厂家“谎话”,实测内存带宽实在体现
  • 6.NUMA 架构下的内存拜访提早区别!
  • 7.PHP7 内存性能优化的思维精华
  • 8. 一次内存性能晋升的我的项目实际
  • 9. 挑战 Redis 单实例内存最大极限,“遭逢”NUMA 陷阱!

我的公众号是「开发内功修炼」,在这里我不是单纯介绍技术实践,也不只介绍实践经验。而是把实践与实际联合起来,用实际加深对实践的了解、用实践进步你的技术实际能力。欢送你来关注我的公众号,也请分享给你的好友~~~

正文完
 0