标签:
文章很长,但超级干货,值得收藏!
虽然下面的文字略有嘻哈的感觉,但我还是希望您在阅读之后,能够本着严肃的态度,来审视一番当今天下最有用的数据结构~哈希表(hash table)。因为,有人的地方就有江湖,有数据的地方就有哈希。
2015年,6月7日~6月9日,在史上最强计算机技术微信群里,发生了一场史诗级别的算法论战。
此战,因参与者阵容之强,脑洞之大,足可彪炳微信群聊史册。无论你是技艺精湛的码农,还是拼杀股市的大妈,这次讨论都可以令你受益匪浅。好了,在你准备浏览下述老少咸宜男女通杀的内容之前,请先记下这个微信群的大名~“美丽互联”。(有人问如何加入?此群逼格甚高,审核极严,但是关注“待字闺中”,你将离高手越来越近)
每场战争都有一个原因,管它是神仙打架,还是明星撕B。这场算法论战也无例外,起因嘛,很简单很money:上证指数突破5000点大关!
龙博下战书
上海证交所的白老师(白硕)在群聊里稍微夸奖了一下龙博当年设计的一个大数据查询系统,于是龙博兴奋地变身“龙霸”,在群里点燃了导火索。
龙博:虽然哈希算法是我做的事情里面极小的一部分,但确实是值得骄傲的一个小成就。我拿这个题目考了很多人,谷歌的,微软的,百度的,基本没有及格的,哈哈。曾经有一个谷歌的技术总监要挑战,方案直接被我判了零分。
【书记员(硅谷寒)注:龙博自我吹捧之形,已跃然纸上。要知道,群里谷歌微软的人不要太多,“帮主”本人就是Google出来的,刚好也混了个技术总监的头衔。】
龙博(开始下战书了):这个哈希算法和数据结构设计也有普适性,交易所内存大部分数据都用这个算法和数据结构进行管理。当上交所的方案是标准答案,看你们的答案与标准答案差异有多大。我就不提示了。有兴趣就挑战,做一个完整的方案出来,给我...
龙博:我稍微把这个哈希算法和数据结构希望解决的问题描述一下
上亿条持仓记录在内存里面需要进行管理,一个主机上可能有几十颗处理器,每个处理器绑定一个进程(可以看成是线程),这几十个进程需要同步地对这上亿条持仓记录进行查询、修改、删除和新增的操作,每个进程每秒需要做数百万次这样的操作。
每条持仓记录,由三个信息来定位(key):
持仓类型(一位字符,A~F),
股东代码(10位字符,第1位A~Z,后面9位是数字0~9),
股票代号(short类型)
被定位到的持仓记录是一个64bit的值(value)
帮主带头应战,哎...
帮主:就完美哈希了,剩下就是数组问题了,因为是有限集合和数值是预先知道的。【书记员注:这是本群陈帮主,搜索领域大牛。帮主在第一时间跳出来应战】
龙博:你计算一下你需要数据结构所需的内存资源,再评估一下运算开销,是否能达到性能要求。哈希算法本身没什么大不了,上交所现在用的哈希算法平均查找次数为1.1。关键是数据结构设计。【书记员注:龙博在此处已经提示了设计的关键点,但可惜的是,之后参与论战的人都忽略了。】
帮主: 1. (准备阶段)将已知的所有的 KEY 值传给PHF或MPHF生成算法,生成PHF或MPHF以及相应的数据;
2. (使用阶段)调用已有的PHF或MPHF以及相应的数据快速计算哈希值并进行相应的操作。使用MPHF,那么我们可以分配一个Long transactions[几亿],数组。
剩下的就是数组操作了。删除就是置零,其它就是修改Long的值。完毕。谢谢。
【书记员注:帮主一下子抛出来两个英文缩写PHF、MPHF,着实把龙博吓了一跳,其实就是“完美哈希函数”和“最小完美哈希函数”】
龙博:你把所有可能的KEY 都放进去了想想你要用多少内存?所以我给你零分。我们总共只有一亿条记录。
帮主:根据前一天的纪录,算一次MPHF,其它新增的放另一个hashtable。如果你说一天都是新增的,那么我们一个小时(x分钟),重算一次MPHF。其实龙博和我也没什么分歧,最后是不是转化为设计一个好的hash function?
【书记员注:不得不说,帮主是天分极强的人,其实,他现在的方案已经很接近最终上交所的方案了,只不过缺少了一点点“画龙点睛”的东西。下面,另一个大牛,独孤虎,要登场了。确切地说,是“大牛团队”,因为独孤虎不是一个人在战斗,他拉上了自己公司里的整个团队,来解答龙博的题目。】
独孤虎炫目登场,杯具了!
独孤虎:我给出的答案是:
1)采用hash:根据股票代码,将请求hash到特定CPU线程;
2)采用数组+hash+SkipList+hash:持仓类型数量固定,在每个线程(CPU)采用一个10个元素的数组,每个元素为针对于一个持仓类型的Skip List,Skip List的元素的key(用于排序)为股东代码。除了key之外,跳跃表中的每个元素包括一个hash,用于将股票代码映射到特定信息。
优化措施,根据访问热度为每个股票代码设置一个hot值,用于分散访问,但是会带来比较麻烦的数据同步问题。
算法的时间复杂度为O(1)+log(2*Mi/n)+O(1),其中Mi是特定持仓类型中股东代码的最大数量,n是线程或CPU数量。内存采用hash,文件采用B+树,所以采用hash是ok的,关键是采用何种数据结构降低操作的复杂度。只有完全采用数组,才会考虑稀疏性。这个是hash+数组+跳跃表+hash仅仅是针对于固定数量的持仓类型采用了数组。
需要额外的2*(持仓类型,股东代码,股票代码)数量的指针,每个指针64位计算,16*(持仓类型,股东代码,股票代码)个byte。
时间复杂度:O(1)+log(2*Mi/n)+O(1),其中Mi是特定持仓类型中股东代码的最大数量,n是线程或CPU数量空间复杂度:需要额外的2*(持仓类型,股东代码,股票代码)数量的指针,每个指针按照64位计算,需要16*(持仓类型,股东代码,股票代码)个byte实现hash的方法,我个人感觉这个不是重点,关键是降低复杂度,因为数量太庞大了,即使通过hash,数量依然很多,需要一种数据结构降低操作复杂度。
O(logN)是比较优化的了,能够做的就是通过何种方式降低这个N,比如通过数组或再加hash等。下面是pseudocodelonglong M=max((long long)持仓库类型.股东代码.股票代码)?
1.
*p=malloc(M*sizeof(U))?
memset(p,0,sizeof(U)*M)?
while(Proc->lock(p))
do
read or write or change p
done
2. N=>CPU数
*p1=malloc(M/N*sizeof(U))?
*p2=malloc(M/N*sizeof(U))?
...
*p(N1)=
malloc(M/N*sizeof(U))?
memset(p1,0,sizeof(U)*M/N)?
memset(p2,0,sizeof(U)*M/N)?
...
memset(p(n1),
0,sizeof(U)*M/N)?
CPUi=>read or write or change p(((longlong)持仓库类型.股东代码.股票代码) mod M)
大型实时算法,就需要简单我估计他们的算法应该是hash+跳跃表,不同支出就是如何处理(持仓类型,股东代码,股票代码)这三个值作为hash和跳跃表的key
【书记员注:独孤虎用一篇超长微信,震撼登场,但杯具了...】
龙博(轻扫一眼):@独孤虎,你要回忆一下白老师讲过的,hash表每天只初始化一次。你用的数据结构太复杂了,而且锁的力度太大。你这种设计,如何做到每个进程(线程)与其他几十个线程同步地对这一亿条记录进行操作?没可能的。所以,我给你零分!
帮主(插话):用sharding保证无锁?
龙博(立刻否定):不用任何sharding,无锁的核心在数据结构设计。你可以想象一下,两个线程如何同步地对一个10个元素大小的数组进行无锁操作(增加、删除、修改、查询)。想通这个问题,你就知道我怎么做这个无锁设计的了。
帮主:一个是任务queue,然后thread pool。【书记员注:其实帮主偷偷地上微博,向广大网友征集答案,如何设计无锁结构,用两个线程操作10个元素的数组?】
龙博:我再给你们说一遍,数据是所有进程完全共享的,不做划分。任何一条记录在任何时候都可能被该物理机上的一个进程访问(包括修改、删除、新增)
擅长高性能多处理器系统设计的唐博登场
【书记员注:下面,另一大牛,唐博,要登场了。唐博在高性能多处理器系统的设计上,独树一帜。】
唐博:我们DDoS的处理也有类似的问题,早就有答案了。与之不同的是,hash key 是可变长的URL。可以理解为16150长度的字符。大家复习一下
1. atomic instruction
2. CAS primitive
然后再来讨论比较靠谱。
【书记员注:唐博一上来,就叫大家去复习功课。计算机系的小伙伴们,你们还记得原子指令和CAS原语吗?】
龙博:类似的内存数据管理技术,用到了交易系统的几乎所有的内存数据结构中。因此可以最大程度发挥CPU的运算能力,减小同步、进程切换的开销。全异步、无锁、用户态和核心态数据共享等技术。为了提高运算效率,我甚至连乘法都是自己实现。。。
书记员注:我硅谷寒搞了N年的芯片设计,觉得龙博这句“连乘法都是自己实现的”,有点儿吹牛了。他应该不知道怎么设计高速乘法器。估计他都不知道啥是booth multiplier?如何设计硬件pipeline?】
龙博:交易所交易系统,就是屠龙之技啊。。。呵呵。例如数组的查询,数据有key + payload。如果你把所有key放到一起,那么整个数组的key可能被装进cache中,这样查询效率就很高了。。。这个也经常要用
独孤虎回来了
独孤虎(憋了一下午,又和自己的团队有了新方案,再次发布超长微信):
整个问题划分为两个部分1)、2)和三个阶段a)、b)、c):
1)网络操作部分,负责接收请求和返回应答;
2)内存操作部分,负责内存持仓记录的操作;
a)接收服务请求>
b)操作持仓记录>
c)返回服务应答
对于网络操作部分:
因为a)为了避免操作的复杂性;b)采用非阻塞式,操作比较快,所以网络操作部分采用一个线程:a)监听、读取和写入三个socket操作采用EPOLL方式以非阻塞方式执行;b)一旦接收到服务请求,该线程根据服务请求中的(持仓类型,股东代码,股票代码)信息,通过hash将请求信息发送给对应的进程(节点),进程之间的通信方式采用非阻塞的MPI;c)在每个轮训周期内,通过MPI_Test过程测试是否返回操作应答,如果返回,则驱动执行socket写入操作。
对于内存操作部分,前提条件为NUMA架构。【书记员注:请大家复习NUMA】
a)每个Node为一个进程,每个进程内多个线程,线程数量与每个node内的CPU
数量*每颗CPU内的核数相关;
b)线程按照线程池组织,采用一个无锁的队列实现生产者和消费者模式,即一个线程负责通过非阻塞的MPI将请求放入队列中,而其他线程则分别读取,并执行具体的持仓记录操作;
c)NUMA架构的数据存放策略采用firsttouch,即通过hash将持仓记录划分到每个节点,确保每个节点仅仅操作本地内存;
由于分配到每个节点的记录数量还是非常庞大,需要有效的数据结构组织这些数据,可选方案有三种:
a)hash
b)balanced trees
c)Skip List
上述三种方案都有很多种不同lockfree的实现方式,只要拿来用就ok了,但是balanced trees的操作比较复杂一点,首先排除。只剩下hash和Skip List:a)hash的问题需要每个节点分配一个非常非常大的数组,并且保证hash表的负载比较合理,可以采用将持仓类型+股东代码+股票代码形成一个字符串,在采用Murmurhash3这类算法进行hash;b)采用Hash+Skip List,当难以保证hash表的负载比较平衡的时候,可以采用小一点的hash表,但是每个表的元素为一个Skip List。
两种方式,关键要考虑持仓记录的变动情况,如果变动情况,不影响hash的负载均衡,采用方案a)。否则,采用方案b)
无锁操作,采用CAS方式实现hash和skip list的方式很多,拿过来用就可以。无锁方案很多,如果采用java实现,比如生产和消费者模式,就可以采用disruptor。
龙博(估计没看完独孤虎的长文,一直攻击其数据结构设计):你说cas指令随便用,但前提是cas或者其他无锁的数据结构需要满足某些条件,你的数据结构能满足这些条件么?这些都是很重要的工程问题。不是说,有cas, 有很多无锁设计和实现,直接拿来用就行。。。
独孤虎(赞许道):龙博,你一招鲜,吃遍天。二十年后还靠无锁设计、CAS吃饭。
龙博:我现在已不靠这个吃饭,靠这个吹牛。。。
独孤虎(接着给方案):1)所有节点的操作的内存是本地的,无需操作其他节点内容;2)hash表是一个数组,数组中的不同元素可以并行操作;3)数组内的每一个元素是一个链表,仅仅在头部插入和内部删除,有多种无锁操作方案。
龙博(还是说独孤虎的数据结构不对):你想象一下,所有这些数据在共享内存里面。。。如果是链表,很难实现完全的无锁设计。~ 零分
独孤虎:一下午,还是零分,我哭!
独孤虎:如果不用链表:第一种方式是copy on write,hash包中的每个元素为指向一个数组的指针,当指向插入和删除操作时,采用内存copy这个数组操作,然后在这个副本上进行插入和删除操作,此种方式不是最优,但是最容易想到的。
第二种方式是采用无链表的hash,有两个数组,第一个数组H是一个Hash表,存放一个无符号整数,指向第二个数组的一个具体位置,第二个数组D是一个非常大的数组,数组中的每个元素包括(持仓类型,股东代码,股票代码,记录数据,next),其中next指向D中的另一个位置,从而模拟一个链表:
a)添加操作,一旦通过(持仓类型,股东代码,股票代码)计算出在数组A中key所指D的位置已经存在值,则在数组D中查到next为0的元素tail,并变化一下(持仓类型,股东代码,股票代码)重新计算自己的hash,并插入到D中,然后采用CAS方式将其位置添加到tail的next?
b)删除操作:删除一个位置d,则采用CAS方式将父亲p中的next指向自己的next即可。
Penny :独孤虎的角度基本是做一个erp系统的sense,不是做高性能系统的sense啊,链表内存分布都是飘的,咋缓存感知啊!【书记员注:Penny是新鲜出炉的清华计算机博士,其创办的公司,拥有世界上最多的人均IP数量。】
独孤虎:工作太努力容易精神分裂。你们知道吗?Jeff Hammerbacher在哈佛读书时就精神分裂,返校毕业后先是加入华尔街做量子码工,之后是Facebook的早期数据科学家,后来是cloudera的创始人,因为工作太疯狂,精神分裂了,现在投身于基于数据科学的疾病治疗,主要是免疫和药物及基因的关系,为病人做个性化治疗。
【书记员注:估计独孤虎快被龙博搞精神崩溃了,准备向Jeff Hammerbacher学习。独孤虎究竟有没有精神分裂呢?】
一夜过去了…孤独虎精神抖擞地回来了!
欲知后事如何,且听下回分解......
标签:
原文地址:http://www.cnblogs.com/yymn/p/4604524.html