Memcached哈希性能优化(八)——总结报告

转自:http://m.blog.csdn.net/blog/hzwfz1989/39120005

Memcached哈希性能优化报告

一、 Memcached分析

这两个月一直在memcached优化和找工作之间忙着,一边复习一边优化改代码还真是个让人觉得难以忘记的夏天。做这个项目确实收获了很多,不管是对Linux的系统的认识,还是对memcached的认识都比以前更近一步,另外后面由于添加分块hash,替换LRU算法和更改hash算法对源代码进行修改,一不小心就把原来的代码的测试改跪了,用gdb调试的能力也有所提升了。感觉这个项目还是蛮有价值的。

还是得把memcached简单介绍一下,毕竟后面的东西要用一下的来着。

1. 内存管理

首先这个内存指的是slab的内存管理,这个是memcached的一个主要特点,他所有的item的内存空间均是从slab管理的内存分配器中分配的。这个内存管理大致是以下这么几个要点

  1. 和一般的内存管理差不过, memcached从操作系统获取到一大块内存后, 便把这大块内存划分为各种大小的chunk块, chunk块大小按照比例逐渐递增,这个可以由用户来指定完成。
  2. 每个slabclass含有多个slab,一个slab是一个大小为1M的内存块,这个slab块的会被按照chunk块的大小进行分割进行分配。
  3. 从 slab管理器分配item的时候, 首先计算item的大小, 找到大小刚好大于等于某个chunk块size所在的slabclass,然后进行分配。
  4. 分配的逻辑很简单,如果有空闲的或是回收回来item的话,则直接分配从这个chunk块,否则尝试申请新的slab的然后进行分配,再要没有就只能返回NULL让LRU去替换了。

这种分配方式的缺点其实是蛮多的,举下面2个例子:

  1. 存在内存碎片, 比如说, 将48字节的item存储到一个64字节的 chunk, 就有16字节的内存浪费。
  2. 另外,由于slab是唯一的,必须采用slab_lock保证分配的原子性,也就是说如果多线程分配的话,如果cache_lock间冲突假设没有的话,到这个slab去申请内存的话也只能是单一的申请,所以决定了插入的过程的最终瓶颈就是这里了,而这里对内存的管理如果把他也分块的话导致后续的逻辑比较难处理,所以这个其实没有处理。

2. hash和LRU

hash和LRU这个东西memcached采用了最简单的设计方式,hash的实现采用的chain的实现,LRU是基于链表的实现,解构的图大概是下面这个样子的(原谅我的盗图行为图片来自百度百科)。

这个模型很经典,不管是链式hash还是LRU的双向链表的实现,不过存在下面几个问题

  1. 链式hash的实现很经典,但是由于是链表保存,数据的局部性很不好,所以内存的访问效率就会有所下降。
  2. LRU的那个双向链表的实现,插入和读取都要调整item在LRU表中的位置,这2个操作明显需要上锁,其实读取本身不需要上锁的,但是要调整LRU的位置,所以没有办法在do_item_link和do_item_unlink中我们都能发现要上锁cache表。
  3. 如果采用开放寻址法使用的话,那么hash的查找效率可能就不高了,所以hash的效率还是极为关键的。

3. 线程模型

这个优化做的没关注这里,就这个我就列举以下建立流程。

  1. 初始化主线程event_base,和worker线程
  2. 建立worker线程的通知管道
  3. 注册worker线程管道的libevent事件
  4. 初始化每个worker的CQ队列
  5. 启动worker线程,主线程监听socket事件,worker线程监听socket的读写事件

如果有链接建立或者是运行的过程见这张图(图片来自http://bachmozart.iteye.com/blog/344172),具体的代码分析就不列举了,今年这个blog都有的。

二、Memcached的优化内容

1. 分块hash优化

这个是这个样子的主要是为了能够多线程的读取和查找做优化使用,主要思路借鉴了下面这个文章的几个思路。"CPHASH: A Cache-Partitioned Hash Table",主要思路是下面两个

  1. 首先把hash表分块,每个分块由不同的线程进行处理,当然可以绑定到固定的核上去,这个有个memc3就是这个绑定的
  2. 每个hash块拥有有自己的LRU表和hash的表,分块由自己的LRU和hash表进行处理操作

这样读的效率肯定可以利用多核的优势,处理读的性能肯定可以上去,但是插入的效率是没有办法的,因为原因主要是出在了slab_lock这个上面,因为slab的分配显然不是可以并行的,因为slab的的分配由slab_lock进行处理,而这个由于上锁的原因,多个workerthread对应的只有一个slab分配器,那么可以肯定的说,插入的瓶颈没有办法通过多线程来解决,但是读可以采用多线程来提高读的效率的。所以这里分块hash主要是提升了读的性能。由于采用了分块hash的处理,那么使得有些item的逻辑就不是原来的那个了, 具体的代码可以看clockdev那个branch上的的代码,那个分支是采用了clock替换和mulit-hash是这么做的。

2. clock算法优化

目的其实很单纯,牺牲命中率和淘汰次数,提高get的读写性能,尤其是多核的情况下这个Clock置换算法大致描述如下

  1类(A=0, M=0): 表示该页最近既未被访问,又未被更新,是最佳淘汰页。
  2类(A=0, M=1):表示该页最近未被访问,但已被修改,并不是很好的淘汰页。
  3类(A=1, M=0):最近已被访问,但未被修改,该页有可能再被访问。
  4类(A=1, M=1): 最近已被访问且被修改,该页可能再被访问。其执行过程可分成以下三步:
  (1) 从指针所指示的当前位置开始,扫描循环队列,寻找A=0且M=0的第一类页面, 将所遇到的第一个页面作为所选中的淘汰页。在第一次扫描期间不改变访问位A。
  (2) 如果第一步失败,即查找一周后未遇到第一类页面,则开始第二轮扫描,寻找A=0且M=1的第二类页面,将所遇到的第一个这类页面作为淘汰页。在第二轮扫描期间,将所有扫描过的页面的访问位都置0。
  (3) 如果第二步也失败,亦即未找到第二类页面,则将指针返回到开始的位置,并将所有的访问位复0。然后重复第一步,如果仍失败,必要时再重复第二步,此时就一定能找到被淘汰的页。

具体的实现的话采用下面的策略实现

  1. 只采用了时间进行标记,在update和get的时候记录下最新时间,在替换的时候检验这个时间是不是超期,如果超期则进行替换,如果没有超期就不替换。
  2. 为了能够支持循环查找,讲LRU的链表改成了双向的循环链表,同时添加hand指针,指向需要替换的元素,如果超期就替换,如果不超期则进行下一次尝试,如果失败的话,则就将这个元素替换出去
  3. 更新的过程中,不将元素放置到head处,只更新访问时间,这样的话就不用断掉链表,这样不用进行item _unlink和item_link的过程

这么做的话,get的性能确实可以上去,但是检查的过程中确实会降低部分的命中虑。

3. hash算法优化

采用了Hopscotch_hashing算法,其思路见这个wikihttp://en.wikipedia.org/wiki/Hopscotch_hashing。

这是一种线性探测hash算法的变形,主要目的是为了提高查询速度。原始的线性探测算法在key较多的情况下探测次数过多,而这个算法的目的其实就是减少探测的次数。

他的步骤主要是这么三个步骤:

  1. 首先检测映射到的bucket,看它是不是被占用,如果未被占用那就直接使用
  2. 如果已被占用,那么采用线性探测法探测到位置pos
  3. 如果pos的位置离bucket的位置大于给定的阈值H,那就调整这个上面的位置,使得这个H-1的bucket上出现空槽,如果没有空槽,resize哈希表然后再次进行尝试。

这个在最新的master上有所采用,不过性能好像不是很好,主要是expand hashtable的过程变得代价很大,而且expand的时机的选择变得更加的不确定,而且expand的过程中必须得上锁,期间几乎不能处理其他的请求,如果不expand bucket的话,可以有比较不错的处理性能的情况,所以这个过程和memc3的cuckoo hashing比较类似,它确实也舍弃了expand hash这个后台处理线程,看来线性探测法在这个expand上确实很难做到最优。

4. tag查询优化

这个主要是借鉴这个文章的“MemC3: Compact and Concurrent MemCache with Dumber Caching and Smarter Hashing”,找到了github上的项目,不过貌似编译完了运行不了,插入key貌似会报错(>_<),不过思路蛮好的

  1. 首先,另外采用一个hash函数计算算出一个1字节大小tag,直接保存在 hashtable 的对应item里面。
  2. 然后查找的时候,先比较tag是不是一致,如果一致,再去比较 key。

这样避免没必要的指针取地址的操作,而且当key很大时,比较key的代价其实还是蛮高的,而很多时候tag比较首先就会不满足了,因而就没必要再去解析这个key对应的内容了,而且如果key存储在另外的内存块里面很可能造成cache不命中,性能就不好了。这个也是前面hash算法优化的一个问题, 尤其是碰撞的key又用链表链接在一起的时候,进而遍历历整个链表,最坏的情况下如果后续链表的节点都在不同的内存,性能那就更差了。

三、优化结果

主要是这么几个结论把,以前的blog上也写过部分的测试结果,现在统一描述以下。测试环境一台4核的pc,内存4G,2台24核工作站作为client使用进行测试。

client均采用48个线程进行读写工作

1. 测试pc单线程工作

这个测试的过程主要是测试上限,由于slab_lock的限制上限其实结果插入大约是每秒10万个操作,读取也在每秒11万操作

[OVERALL], RunTime(ms), 45906.0
[OVERALL], Throughput(ops/sec), 108917.52712063782
[INSERT], Operations, 4999968
[INSERT], AverageLatency(us), 428.84410700228483
[INSERT], MinLatency(us), 123
[INSERT], MaxLatency(us), 59953
[INSERT], 95thPercentileLatency(ms), 0
[INSERT], 99thPercentileLatency(ms), 1
[INSERT], Return=0, 4999968

2. 测试pc 8个线程工作

采用clock+mulitihash: 插入平均是每秒9万操作。

[OVERALL], RunTime(ms), 53169.0
[OVERALL], Throughput(ops/sec), 94039.15815606838[INSERT], Operations, 4999968
[INSERT], AverageLatency(us), 497.43873040787463
[INSERT], MinLatency(us), 120
[INSERT], MaxLatency(us), 156936
[INSERT], 95thPercentileLatency(ms), 0
[INSERT], 99thPercentileLatency(ms), 6
[INSERT], Return=0, 4999968

比较接近上限

采用原始的memcached: 插入平均是每秒6万操作,

[OVERALL], RunTime(ms), 77420.0
[OVERALL], Throughput(ops/sec), 64582.381813484884
[INSERT], Operations, 4999968
[INSERT], AverageLatency(us), 722.7500804005145
[INSERT], MinLatency(us), 120
[INSERT], MaxLatency(us), 118476
[INSERT], 95thPercentileLatency(ms), 1
[INSERT], 99thPercentileLatency(ms), 12

采用2个client

采用clock + mulithash: 2个读取是每秒20万操作,基本是单个client的叠加

采用原始的memcached: 2个读取是每秒20万操作,基本是单个client的叠加

而单个client的测试基本读取都是每秒11万操作

这个是clock + multihash

[OVERALL], RunTime(ms), 167550.0
[OVERALL], Throughput(ops/sec), 119367.16204118174
[READ], Operations, 19959611
[READ], AverageLatency(us), 389.58720097300494
[READ], MinLatency(us), 94
[READ], MaxLatency(us), 58364
[READ], 95thPercentileLatency(ms), 0
[READ], 99thPercentileLatency(ms), 3

原始的能稍微差点,但是数量级差不多

[OVERALL], RunTime(ms), 168112.0
[OVERALL], Throughput(ops/sec), 118968.11649376607
[READ], Operations, 19960241
[READ], AverageLatency(us), 390.19616010648366
[READ], MinLatency(us), 97
[READ], MaxLatency(us), 166104
[READ], 95thPercentileLatency(ms), 0
[READ], 99thPercentileLatency(ms), 3

get的过程中中memcahed的所在pc的cpu的利用率均为能达到100%,但是client的cpu都已经是100%了,也就是说get的处理性能没有达到最高,但是在插入的过程中确实cpu的利用率在htop下看达到了100%,而且单个client的增加,处理的总数是会增加的,所以说get处理过程中2个client确实没有达到这个pc的处理的最高值,get的插入确实是没有办法检测出来,从htop的看到的cpu的利用率来说,clock+multihash能稍微高一点点,这个是测试的环境所限,确实无法测出2个实际的差距。但是在单机上有一个趋势就是采用multihash的办法在发端的线程提升的情况下,hash的效果下降不会太大,而原始的会跌落的比较明显。

四、总结

这次的memcached的开源夏令营也算是告一段路了,期间查询了不少资料,看了不少的paper和项目,从之前的bagLRU到后来的memC3,从开始研究乐观锁和多版本并发控制到后来去研究hash算法和lru算法之间的差异,从skiplist的算法优化到后来的结构的修改,中间试过不少办法,也走了一些弯路,但是发现探索的过程还是学到了许多的东西。不过中间的很多过程还是发现了自己很多不足的地方,希望能够能以此为基础,进一步提高自己的能力,另外,发现开源项目真的很不错,希望以后能继续研究和回馈开源项目。

时间: 2024-10-12 21:23:54

Memcached哈希性能优化(八)——总结报告的相关文章

Nginx高性能web服务器之性能优化(八)

我们该如何充分发挥Nginx的高效和稳定性呢?这里我介绍下从编译安装开始,到系统内核,和利用第三方插件对Nginx进行优化,最大限度的发挥Nginx的性能. 一.编译安装过程的优化 1.减小Nginx编译安装后的文件大小 在编译Nginx的时候,默认是以debug模式进行的.而在debug模式下面会插入很多跟踪ASSERT之类的信息.当编译完成之后,一个Nginx要有好几M的字节.如果我们取消Nginx的debug模式,那么Nginx只有几百个字节.因为我们修改相关源码,取消debug模式: 在

WPF性能优化经验总结

WPF性能优化一.Rendering Tier 1. 根据硬件配置的不同,WPF采用不同的Rendering Tier做渲染.下列情况请特别注意,因为在这些情况下,即使是处于Rendering Tier 2的情况下也不会硬件加速.(不全,其余请查阅SDK) WPF性能优化二.布局和设计 1.尽量多使用Canvas等简单的布局元素,少使用Grid或者StackPanel等复杂的,越复杂性能开销越大. 2.建立逻辑树或者视觉树的时候,遵循Top-Down的原则. WPF性能优化三.图像 1. 对Im

Tair LDB基于Prefixkey的范围查找性能优化项目测试及完成总结报告

项目这周就截止了,这算是我第一个有导师指导的真正意义上的C++项目,项目基本完成,想要实现的功能也已经实现,并做了大量的性能测试.不过这对于业界来说,可能完成的还不够成熟,还有许多待改进的地方,还不能马上投入使用,还需要进行严格的考验,毕竟tair的应用场景太重要了,不容一丝疏忽.但于我个人而言,帮助还是挺大的,不仅是多了一次有价值的项目经验,更是学到了一些项目之外的东西,比如计划的重要性,惰性的控制,时间的分配管理(找工作与项目进度产生冲突)等.好了,不多说了,在这最后一篇总结报告里首先给出性

Android性能优化方法(八)

Android SDK tools目录下提供一个观察布局的工具,层级观察器(Hierarchy Viewer).Hierarchy Viewer工具是一个非常好的布局优化工具,同时,你也可以通过它学习他人的布局.应该说是一个非常实用的工具. AD:WOT2014:用户标签系统与用户数据化运营培训专场 层级观察器(Hierarchy Viewer): Android SDK tools目录下提供一个观察布局的工具,层级观察器(Hierarchy Viewer).Hierarchy Viewer工具

CSS3与页面布局学习总结(八)——浏览器兼容与前端性能优化

目录 一.浏览器兼容 1.1.概要 1.2.浏览器内核 1.3.浏览器市场份额(Browser Market Share) 1.4.兼容的一般标准 1.5.CSS Reset 1.6.CSS Hack 1.6.1.条件注释法 1.6.2.样式内属性标记法 1.6.3.选择器前缀法 1.7.文档模式 (X-UA-Compatible) 1.8.javascript兼容 二.前端性能优化 2.1.概要 2.2.减少HTTP请求数量 2.2.1.图片地图 2.2.2.精灵图片(Sprite) 2.2.

CSS3与页面布局学习笔记(八)——浏览器兼容性问题与前端性能优化方案

一.浏览器兼容 1.1.概要 世界上没有任何一个浏览器是一样的,同样的代码在不一样的浏览器上运行就存在兼容性问题.不同浏览器其内核亦不尽相同,相同内核的版本不同,相同版本的内核浏览器品牌不一样,各种运行平台还存在差异.屏幕分辨率不一样,大小不一样,比例不一样.兼容性主要可以分类为: 1).CSS兼容2).JavaScript兼容3).HTML兼容 这三类也是前端的主要组成部分,都存在一定的兼容性问题,知己知彼,百战百胜,我们先了解浏览器的发动机—内核. 多年前我们一直为IE6兼容烦恼,为它没少加

T- SQL性能优化详解

故事开篇:你和你的团队经过不懈努力,终于使网站成功上线,刚开始时,注册用户较少,网站性能表现不错,但随着注册用户的增多,访问速度开始变慢,一些用户开始发来邮件表示抗议,事情变得越来越糟,为了留住用户,你开始着手调查访问变慢的原因. 经过紧张的调查,你发现问题出在数据库上,当应用程序尝试访问/更新数据时,数据库执行得相当慢,再次深入调查数据库后,你发现数据库表增长得很大,有些表甚至有上千万行数据,测试团队开始在生产数据库上测试,发现订单提交过程需要花5分钟时间,但在网站上线前的测试中,提交一次订单

Android应用程序性能优化Tips

主要介绍一些小细节的优化技巧,虽然这些小技巧不能较大幅度的提升应用性能,但是恰当的运用这些小技巧并发生累积效应的时候,对于整个App的性能提升还是有不小作用的.通常来说,选择合适的算法与数据结构会是你首要考虑的因素,在这篇文章中不会涉及这方面的知识点.你应该使用这篇文章中的小技巧作为平时写代码的习惯,这样能够提升代码的效率. 通常来说,高效的代码需要满足下面两个原则: 不要做冗余的工作 尽量避免执行过多的内存分配操作 To ensure your app performs well across

java架构师、高性能、高并发、高可用、高可扩展、性能优化、集群、电商网站架构

15套java架构师.集群.高可用.高可扩展.高性能.高并发.性能优化.Spring boot.Redis.ActiveMQ.Nginx.Mycat.Netty.Jvm大型分布式项目实战视频教程 视频课程内容包含: 高级Java架构师包含:Spring boot.Spring  cloud.Dubbo.Redis.ActiveMQ.Nginx.Mycat.Spring.MongoDB.ZeroMQ.Git.Nosql.Jvm.Mecached.Netty.Nio.Mina.性能调优.高并发.to