HAProxy的独门武器:ebtree
1. HAProxy和ebtree简介
HAProxy是法国人Willy Tarreau个人开发的一个开源软件,目标是应对客户端10000以上的同时连接,为后端应用服务器、数据库服务器提供高性能的负载均衡服务。
在底层数据结构方面,旧版本HAProxy曾经使用过红黑树,用于任务调度、负载均衡等方面。但是Willy Tarreau认为,在事件响应非常频繁的情况下,任务插入、删除的频率非常高,这时候使用红黑树存在性能瓶颈,尤其不能接受红黑树删除节点的时间复杂度为O(log n)。因此,他发明了一种新的数据结构,叫做弹性二叉树(elastic binary tree),简称ebtree。
目前新版本的HAProxy(本文编写时最新版本为1.4.23)已使用ebtree,而除了HAProxy之外,还没有其它著名的开源软件使用ebtree。可以这么说,HAProxy最有特色的地方就是ebtree,ebtree名符其实是HAProxy的独门武器。
ebtree是不平衡的二叉搜索树(BST),而红黑树、AVL树等都是平衡的BST。传统的BST最怕的就是退化成线性搜索,因此,红黑树等BST插入、删除时都需要对树进行平衡化,而平衡化是一个从叶子节点开始,向根节点方向递归向上的过程,时间复杂度是O(log n)。
有鉴于此,ebtree为了实现删除节点时O(1)的时间复杂度,必然放弃保持树的平衡,为了拒绝由此而来的副作用——退化成线性搜索(或者更准确地说,退化成不受限制的线性搜索),不可避免地引入了一些新的成员和新的思路,且待我慢慢道来。
2. ebtree节点的组成
一个ebtree的节点(以下简称ebnode)分为node部分和leaf部分(Willy Tarreau是这样描述的,但我觉得称为树干部分和叶子部分更合适一些,以下就按我的理解来叙述)。树干负责关联其它ebnode,由父指针(node_p)和分支(Willy Tarreau称之为root,包括左分支L和右分支R),以及一个控制树的高度的特殊成员(bit)组成,叶子负责携带数据(data,一般是数据的键值,所以下文都称为key),另外包含一个指向上层的指针(leaf_p)。
一棵ebtree只有一个根节点(root),包含两个左右分支的指针(L、R)。所有的ebnode总是挂在根节点的左分支下面,根节点的右分支总是为空。在ebtree的遍历过程中,判断当前节点是否根节点就是判断其右指针是否为空。
-
3. 各个指针的附加属性
在32位平台中,一个指针占用4个字节,例如,地址值0xaabbcc00的下一个地址值是0xaabbcc04,再下一个是0xaabbcc08,也就是说,指针的值的最后两个比特不能表示一个合法地址。因此,Willy Tarreau充分利用这一点,来保存上述几个指针的特殊属性。这是一个很重要的优化,每个ebnode可以节省几个成员,整个ebtree就节省大量存储空间。
1)L和R既可以指向其它ebnode的树干,也可以指向其它ebnode的叶子,还可以指向自己的叶子。在ebtree的遍历过程中,对树干和叶子有不同的处理逻辑,L和R有必要知道自己所指向的是树干还是叶子。
2)可以知道node_p和leaf_p究竟挂在其它ebnode的左分支下面,还是挂在其右分支下面。
3)根节点右分支不挂任何树干和叶子,可以把它也利用上,指示该ebtree是否允许重复键值。
熟悉红黑树的读者都知道,红黑树也有同样的优化方法,表示红黑树节点颜色的属性并不占用内存空间。
4. bit的定义
引入bit就是为了限制树的高度,避免极端不平衡。在一棵不允许重复键值的ebtree中,key是32位的情况下,bit的取值范围是从0到31,此时,它的定义是:子树所有的键中,第一个不同的二进制位的位置。允许重复键值的ebtree稍后再详细介绍。
例如,下图的右下角子树中只有两个键,左边叶子节点的键值为300,右边叶子节点的键值为400,300的二进制是100101100,400的二进制是110010000,从右边数起第7位起(注意,程序员都是从0开始数数的),300和400左边的位都相同,所以,bit等于7。
这时候,读者可能会问,这样定义bit为什么能够限制树的高度呢?不用着急,马上隆重介绍bit的两个重要意义!
5. bit的第一个重要意义
这里只讨论键值大于等于零的情况,事实上,ebtree可以支持键值为负数,不过,我还没有仔细研究过这种情况,应该是对符号位进行某些转换处理。
bit的第一个重要意义:同一个ebnode中的bit和key,联合决定该ebnode属下的子树内,所有key的取值范围。
先看下图挂在根节点下面,key = 300的那个ebnode,bit = 8,300的二进制为100101100,从右边数起第8位是最高位那个1,参考bit的定义,也就是说,该子树所有的键,第8位左边都是0,所以,它们的取值范围是从0到511(二进制111111111)。
再看最下面那个ebnode,bit = 5,250的二进制为11111010,从右边数起第5位是第三个1,再对照bit的定义,该子树的键,第5位左边都是11,所以,它们的取值范围是从192(二进制11000000)到255(二进制11111111)。
同理,最右边那个ebnode,bit = 7,key = 400,取值范围是256-511。
6. bit的第二个重要意义和查找过程
bit的第二个重要意义:如果要查找的数据x在该子树的取值范围内,bit可以指示其可能会在左分支下面还是右分支下面。
ebtree的具体查找过程是,遍历到某个ebnode时,如果key = x,返回查找结果;如果x已经超出bit规定的取值范围,返回查找失败;否则,取x的第bit位,如果bit = 0,那么从该ebnode的左分支查找,反之,从右分支查找;如果已到达叶子还是没有匹配,返回查找失败。
还是上一节那个图,假如要找的键x = 249,二进制为11111001,从根节点左分支开始查找,bit = 8,右边数起第8位为0,于是从它的左分支继续查找,bit = 5,249右边数起第5位为1,于是从它的右分支继续查找,此时已到达叶子,且250 != 249,本次查找失败。
假如要找的键x = 300,因为就在查找路径的节点上,直接返回结果。
假如要找的键x = 600,已经超出该子树中bit规定的取值范围,返回查找失败。
7. 插入不可重复的键值
首先,要介绍的是空树的情况。由前面的叙述可以得知,一棵ebtree为空树当且仅当它的根节点的左分支为空。所以,此时插入的ebnode就直接挂在根节点的左分支下面,由于新插入的ebnode不存在左右分支,也没有父节点(上层ebnode),显然也不需要bit来控制树的高度,因此,该ebnode的树干都没有使用。
其次,介绍ebtree只有一个ebnode时,再插入一个ebnode的情况。此时,新的ebnode必定插入在根节点与旧的ebnode之间,如果新的键值大于原来的键值,旧的ebnode挂在新的ebnode的左分支下面,新的ebnode的叶子挂在自己的右分支下面,再计算bit;反之,则左右相反,再计算bit。
下图的例子,是已有key = 200,再插入key = 300的情形。读者可以根据上面的描述画出已有key = 200,再插入key = 100的情形。
然后,就可以介绍在ebtree中插入新的ebnode的五种基本情形。在这里,都以上图为初始状态。任何具有更多ebnode的情形,都可以通过对ebtree的遍历,递推到其中一种情形。
1) 新的键值可以插入子树中,而且小于子树中的最小键值。
假如新插入ebnode的key为100,根据bit的第二个重要意义,100应该在该子树的左分支下面,而且,100小于200,于是,该ebnode插入在原图的左分支上,自己的左分支指向自己的叶子,自己的右分支指向原来子树的左分支。如下图所示。
键值范围[0, 200)都属于这种情形。
2) 新的键值可以插入子树中,该键值在确定要插入的两个ebnode的键值之间,且应该在该子树的左分支下面。
假如新插入ebnode的key为225,根据bit的第二个重要意义,225应该在该子树的左分支下面,而且,225大于200,于是,该ebnode插入在原图的左分支上,自己的左分支指向原来子树的左分支,自己的右分支指向自己的叶子。如下图所示。
键值范围(200, 255]都属于这种情形。
3) 新的键值可以插入子树中,该键值在确定要插入的两个ebnode的键值之间,且应该在该子树的右分支下面。
假如新插入ebnode的key为275,根据bit的第二个重要意义,275应该在该子树的右分支下面,而且,275小于300,于是,该ebnode插入在原图的右分支上,自己的左分支指向自己的叶子,自己的右分支指向原来子树的右分支。如下图所示。
键值范围(255, 300)都属于这种情形。
4) 新的键值可以插入子树中,而且大于子树中的最大键值。
假如新插入ebnode的key为400,根据bit的第二个重要意义,400应该在该子树的右分支下面,而且,400大于300,于是,该ebnode插入在原图的右分支上,自己的左分支指向原来子树的右分支,自己的右分支指向自己的叶子。如下图所示。
键值范围(300, 511]都属于这种情形。
5) 新的键值不可以插入子树中。
假如新插入ebnode的key为600,根据bit的第一个重要意义,600不可插入到子树中,于是,该ebnode插入在原图的子树之上,自己的左分支指向原来的子树,自己的右分支指向自己的叶子。如下图所示。
键值范围(511, +∞)都属于这种情形。
8. bit的第三个重要意义和插入重复的键值
ebtree是专门为任务调度而生的,同样的优先级,必须保证能够按照任务触发的次序来进行访问。所以,ebtree支持存储重复的键值,这一点并不是所有的BST都支持,可以说是ebtree的优点。而且,解决键值冲突不会退化成链表。
bit的第三个重要意义:bit为负值表示该子树下所有的键都是重复的,而且,该值表示重复子树的层次。当然,必须要在根节点右指针允许的情况下。
插入第一个重复键值,例如300(ebnode底纹为点点),可以参考上一节的第二种和第四种基本情形,不同的是,bit为-1。
如果再插入一个重复键值300(ebnode底纹为方格),应该在重复键值子树上插入,而且是向上生长。
上图已经有四个ebnode,信息量较大,为了后续叙述方便,把它简化,去掉几个指针域,保留bit和key,得到下图。
再插入一个300(ebnode底纹为斜方格),得到下面的ebtree。
再插入两个300(ebnode底纹分别为左斜线和右斜线),得到下面的ebtree。
读者可以思考一下,如果再来一个、两个、三个300,应该在哪里插入?如果插入不同于300的其它键值,应该在哪里插入?
从上面几张图,大家可以看到,一个ebnode的树干和叶子会随着树的增长而拉长到不同的层次上,好像很有弹性的样子,这就是弹性二叉树名字的由来。
9. 删除节点
删除一个ebnode,概括起来比较简单,就是把要删除的叶子和该叶子的父亲(树干部分)删除,然后把兄弟挂到祖父下面。因为不需要对树进行平衡化,不需要访问其它ebnode,效率很高。
具体操作,分为两种情况:
1)被删除的叶子直连自己的树干,可直接删除该ebnode,然后对它的兄弟重新指派原来的祖父为父亲。
2)被删除的叶子不是直连自己的树干,以该叶子的父亲(其它ebnode的树干)替换该ebnode的树干,然后删除该ebnode,再把它的兄弟重新指派原来的祖父为父亲。
10. 总结
没有最好的数据结构,只有最合适的数据结构。ebtree有它的优点:
1)支持存储重复的键值,而且,在此情况下,也不会退化成线性操作。
2)删除节点时,不需要对树进行平衡化。
3)插入键值时,很可能不需要深入到树的叶子,当然,很多BST都这样。
4)查询键值时,可以预知子树的取值范围,从而可以选择访问还是不访问该子树。
缺点也很明显:
1)逻辑比较复杂,熟悉的人不多(希望读者看完本文之后都有茅塞顿开的感觉)。
2)ebnode占用空间比较多,如果把bit也算一个指针,相当于花了5个指针才携带1个数据。
3)键值严重依赖于可以进行位运算的数据类型。
总而言之,ebtree适合有高频率插入、删除操作(例如50万次/秒)的使用场合,不适合查询较多、插入、删除较少的场合,非常不适合用于缓存。
11. 参考资料
- http://1wt.eu/articles/ebtree/
- Willy Tarreau个人网站对ebtree的介绍,不过存在不少误导的地方
- http://blog.nklike.com/开源软件/ebtree介绍/
- 淘宝空见对上文的翻译,也有不够准确的地方
- http://wenku.it168.com/d_000555847.shtml
- 对ebtree的描述错误比较多
文章转自:http://tech.uc.cn/?p=1031
HAProxy的独门武器:ebtree的更多相关文章
- MySQL加速查询速度的独门武器:查询缓存
[导读] 与朋友或同事谈到MySQL查询缓存功能的时候,个人喜欢把查询缓存功能Query Cache比作荔枝, 是非常营养的东西,但是一次性吃太多了,就容易上火而流鼻血,虽然不是特别恰当的比喻,但是有 ...
- http反向代理之haproxy详解
1.反向代理定义 反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求 ...
- 数据结构 + 算法 -> 收集
董的博客:数据机构与算法合集 背包问题应用(2011-08-26) 数据结构之红黑树(2011-08-20) 素数判定算法(2011-06-26) 算法之图搜索算法(一)(2011-06-22) 算法 ...
- 从零開始学习OpenCL开发(一)架构
多谢大家关注 转载本文请注明:http://blog.csdn.net/leonwei/article/details/8880012 本文将作为我<从零開始做OpenCL开发>系列文章的 ...
- DataPipeline | PayPal庞姬桦:大数据在小微企业贷款上的运用
庞姬桦女士毕业于北京大学和美国哥伦比亚大学,目前担任PayPal公司消费者风险管理总监,负责通过大数据实现对互联网金融风险的侦测.跟踪.管控和防范.在加入PayPal之前,曾任职于渣打银行(中国)和美 ...
- Java地位被撼动?Java与JavaScript的趣事连载
第一回 JavaScript的进攻 公元2014年,Java 第八代国王终于登上了王位. 第一次早朝,国王坐在高高的宝座上,看着毕恭毕敬的大臣,第一次体会到了皇权的威力. 德高望重的IO大臣颤悠悠地走 ...
- Java能抵挡住JavaScript的进攻吗?
JavaScript的进攻 公元2014年,Java 第八代国王终于登上了王位. 第一次早朝,国王坐在高高的宝座上,看着毕恭毕敬的大臣,第一次体会到了皇权的威力. 德高望重的IO大臣颤悠悠地走上前来: ...
- 从零开始学习OpenCL开发(一)架构
1 异构计算.GPGPU与OpenCL OpenCL是当前一个通用的由很多公司和组织共同发起的多CPU\GPU\其他芯片 异构计算(heterogeneous)的标准,它是跨平台的.旨在充分利用GPU ...
- 从零开始学习OpenCL开发(一)架构【转】
转自:http://blog.csdn.net/leonwei/article/details/8880012 多谢大家关注 转载本文请注明:http://blog.csdn.net/leonwei/ ...
随机推荐
- 渗透测试中的bypass技巧
0x00 前言 许多朋友在渗透测试中因为遇到WAF而束手无策,本人应邀,与godkiller一同写下此文,希望能够对许多朋友的问题有所帮助. 此系列一共分为五篇文章,分别如下: 一.架构层绕过WAF ...
- hdu 5301 Buildings (2015多校第二场第2题) 简单模拟
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5301 题意:给你一个n*m的矩形,可以分成n*m个1*1的小矩形,再给你一个坐标(x,y),表示黑格子 ...
- java多线程技术之条件变量
上一篇讲述了并发包下的Lock,Lock可以更好的解决线程同步问题,使之更面向对象,并且ReadWriteLock在处理同步时更强大,那么同样,线程间仅仅互斥是不够的,还需要通信,本篇的内容是基于上篇 ...
- 解耦你的HTML,CSS和JAVASRIPT
注:本文为翻译文章,原文<Decoupling Your HTML, CSS, and JavaScript> 今天在web上任何大一点的网站或应用程序都包含大量的html,css和jav ...
- zoj 2587 判断最小割的唯一性
算法: 先求出残量网络,计算出从src能够到的点集A,再求出能够到dst的点集B,如果所有点都被访问到了,那么割就是唯一的,即(A,B),否则(A,V-A)和(V-B,B)都是最小割. (注意因为割的 ...
- leetcode659. Split Array into Consecutive Subsequences
leetcode659. Split Array into Consecutive Subsequences 题意: 您将获得按升序排列的整数数组(可能包含重复项),您需要将它们拆分成多个子序列,其中 ...
- SpringMVC 方法参数设置
/** 在方法中配置参数: (1) 内置对象配置: request:获取cookie.请求头... 获取项目根路径 request.getContextPath() response:用于ajax的输 ...
- spring---transaction(6)---事务的配置
1 写在前面 上一篇我们了解到spring的事务的体系.这里我们将结合上篇讲spring事务的配置 2 Spring的三种事务配置形式 2.1 使用TransactionProxyFactoryBea ...
- CentOS 7下配置安装KVM
注意:KVM一切安装和运行都是在root用户下完成的,并且只有root才能支持某些软件. 一.准备工作: 1.关闭selinux,iptables,重启后生效 ##关闭selinux # sed -i ...
- patch 用法
diff -Nrua a b > c.patch 实例说明: --- old/modules/pcitable Mon Sep 27 11:03:56 1999 +++ new/modules/ ...