`
san_yun
  • 浏览: 2654820 次
  • 来自: 杭州
文章分类
社区版块
存档分类
最新评论

HAProxy的独门武器:ebtree

 
阅读更多

原文:http://tech.uc.cn/?p=1031

 

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的遍历过程中,判断当前节点是否根节点就是判断其右指针是否为空。

Ebnode

-

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为什么能够限制树的高度呢?不用着急,马上隆重介绍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。

bit1

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的树干都没有使用。

insert0

其次,介绍ebtree只有一个ebnode时,再插入一个ebnode的情况。此时,新的ebnode必定插入在根节点与旧的ebnode之间,如果新的键值大于原来的键值,旧的ebnode挂在新的ebnode的左分支下面,新的ebnode的叶子挂在自己的右分支下面,再计算bit;反之,则左右相反,再计算bit。
下图的例子,是已有key = 200,再插入key = 300的情形。读者可以根据上面的描述画出已有key = 200,再插入key = 100的情形。

insert1

然后,就可以介绍在ebtree中插入新的ebnode的五种基本情形。在这里,都以上图为初始状态。任何具有更多ebnode的情形,都可以通过对ebtree的遍历,递推到其中一种情形。

1) 新的键值可以插入子树中,而且小于子树中的最小键值。

假如新插入ebnode的key为100,根据bit的第二个重要意义,100应该在该子树的左分支下面,而且,100小于200,于是,该ebnode插入在原图的左分支上,自己的左分支指向自己的叶子,自己的右分支指向原来子树的左分支。如下图所示。

insert2_1

键值范围[0, 200)都属于这种情形。

2) 新的键值可以插入子树中,该键值在确定要插入的两个ebnode的键值之间,且应该在该子树的左分支下面。

假如新插入ebnode的key为225,根据bit的第二个重要意义,225应该在该子树的左分支下面,而且,225大于200,于是,该ebnode插入在原图的左分支上,自己的左分支指向原来子树的左分支,自己的右分支指向自己的叶子。如下图所示。

insert2_2

键值范围(200, 255]都属于这种情形。

3) 新的键值可以插入子树中,该键值在确定要插入的两个ebnode的键值之间,且应该在该子树的右分支下面。

假如新插入ebnode的key为275,根据bit的第二个重要意义,275应该在该子树的右分支下面,而且,275小于300,于是,该ebnode插入在原图的右分支上,自己的左分支指向自己的叶子,自己的右分支指向原来子树的右分支。如下图所示。

insert2_3

键值范围(255, 300)都属于这种情形。

4) 新的键值可以插入子树中,而且大于子树中的最大键值。

假如新插入ebnode的key为400,根据bit的第二个重要意义,400应该在该子树的右分支下面,而且,400大于300,于是,该ebnode插入在原图的右分支上,自己的左分支指向原来子树的右分支,自己的右分支指向自己的叶子。如下图所示。

insert2_4

键值范围(300, 511]都属于这种情形。

5) 新的键值不可以插入子树中。

假如新插入ebnode的key为600,根据bit的第一个重要意义,600不可插入到子树中,于是,该ebnode插入在原图的子树之上,自己的左分支指向原来的子树,自己的右分支指向自己的叶子。如下图所示。

insert2_5

键值范围(511, +∞)都属于这种情形。

8. bit的第三个重要意义和插入重复的键值

ebtree是专门为任务调度而生的,同样的优先级,必须保证能够按照任务触发的次序来进行访问。所以,ebtree支持存储重复的键值,这一点并不是所有的BST都支持,可以说是ebtree的优点。而且,解决键值冲突不会退化成链表。
bit的第三个重要意义:bit为负值表示该子树下所有的键都是重复的,而且,该值表示重复子树的层次。当然,必须要在根节点右指针允许的情况下。
插入第一个重复键值,例如300(ebnode底纹为点点),可以参考上一节的第二种和第四种基本情形,不同的是,bit为-1。

insert_dup1

如果再插入一个重复键值300(ebnode底纹为方格),应该在重复键值子树上插入,而且是向上生长。

insert_dup2

上图已经有四个ebnode,信息量较大,为了后续叙述方便,把它简化,去掉几个指针域,保留bit和key,得到下图。

insert_dup2s

再插入一个300(ebnode底纹为斜方格),得到下面的ebtree。

insert_dup3s

再插入两个300(ebnode底纹分别为左斜线和右斜线),得到下面的ebtree。

insert_dup5s

读者可以思考一下,如果再来一个、两个、三个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. 参考资料

分享到:
评论

相关推荐

    haproxy-keepalived:适用于Docker和kubernetes的HAProxy和Keepalived

    haproxy-keepalived [v1.0.0]重构,使其再次伟大。 并增加对Kubernetes的支持适用于Docker和Kubernetes的带有Keepalived的HAProxy DockerHub: ://hub.docker.com/r/pelin/haproxy-keepalived/ 版本HAProxy 保持活力...

    haproxy-formula:使用 SSL 终止的 haproxy 负载平衡(haproxy >= 1.5)

    haproxy-公式 使用 SSL 终止进行负载...haproxy: loglevel: debug roles: my.app: downstream_port: 81 ssl: True http_port: 80 https_port: 443 节流 要启用节流,请创建如下Struts: haproxy: roles: yo

    haproxy透明代理配置TPROXY1

    3. 配置haproxy:编辑haproxy的配置文件`/etc/haproxy/haproxy.cfg`,示例配置如下: ``` global daemon stats socket /var/run/haproxy.stat mode 600 log 127.0.0.1 local4 maxconn 40000 ulimit-n 80013 ...

    CentOS7—HAProxy安装与配置详解

    Haproxy下载地址:http://pkgs.fedoraproject.org/repo/pkgs/haproxy/ 关闭SElinux、配置防火墙 1、vi /etc/selinux/config #SELINUX=enforcing #注释掉 #SELINUXTYPE=targeted #注释掉 SELINUX=disabled #增加 :wq...

    docker-haproxy-certbot:带有Let's Encrypt证书的Dockerized HAProxy自动续订

    具有Let's Encrypt自动证书续订功能的Dockerized HAProxy 此容器为HAProxy实例提供启动时生成的“让我们加密”证书,并通过内部cron作业每周更新一次(如有必要)。 用法 从Docker Hub中提取: docker pull ...

    haproxy-inress:HAProxy入口

    HAProxy入口控制器 负载控制器实现。 HAProxy Ingress是Kubernetes入口控制器:它配置HAProxy实例,以将传入请求从外部网络路由到集群内应用程序。 路由配置是从Kubernetes集群读取规范而构建的。 对群集所做的更新...

    haproxy-2.0:haproxy 2.0 git repo的镜像

    haproxy是一款高性能、可靠且灵活的开源负载均衡器,被广泛用于服务器集群的流量管理和分发。在标题和描述中提到的“haproxy-2.0:haproxy 2.0 git repo的镜像”,指的是haproxy的2.0版本源代码仓库的镜像。这个镜像...

    haproxy-agent:生成haproxy配置

    haproxy-agent是一款用于自动化生成HAProxy配置的工具,它主要使用JavaScript编写,方便开发者或运维人员快速构建和管理负载均衡解决方案。HAProxy是广泛应用于高可用性、高性能的网络应用代理服务器,常用于反向...

    haproxy-web:HAProxy网页,用于从一个页面维护多个服务器

    HAProxy是一款广泛使用的开源负载均衡器,主要用于在高流量网站中分发网络负载,确保服务的高可用性和性能。在给定的标题“haproxy-web:HAProxy网页,用于从一个页面维护多个服务器”中,我们可以理解到,这里提到的...

    haproxy-cli:HAProxy StatsInfo CLI 和库

    HAProxy CLI 实用程序和库 HAProxy 配置 首先确保在配置中启用 haproxy UDS stats 套接字然后重新加载: global stats socket /tmp/haproxy.sock mode 600 level admin stats socket ipv4@127.0.0.1:9000 level ...

    haproxy-dconv:HAProxy文档转换器

    HAProxy文档转换器 在当前状态下,HAProxy正在将HAProxy文档.txt文件转换为HTML。 该项目的目的是最终将HAProxy文档转换为更通用的格式(例如:ReStructuredText),从而可以更轻松地传播各种输出文件(.pdf,.html...

    haproxy-wi:用于管理Haproxy,Nginx和Keepalived服务器的Web界面

    Web界面(用户友好的Web GUI,警报,监视和安全),用于管理HAProxy,Nginx和Keepalived服务器。 留下您的 参与其中 ,订阅! 关于HAProxy-WI的,欢迎进行讨论和提问 示范现场 登录名/密码:admin / admin。 ...

    haproxy-visualizer:可视化haproxy配置的简单工具

    haproxy-visualizer是一款专为haproxy配置提供可视化的工具,它的主要目标是帮助用户更直观地理解和管理haproxy的复杂配置。haproxy是一款广泛使用的开源负载均衡器,它能够有效地分发网络流量,提高服务的可用性和...

    HAProxy-Configuration:HAProxy配置示例

    以下文档中有关HAProxy的更多详细信息目录 具有SSL终止功能的HAProxy 具有SSL终止和自定义CORS的HAProxy HAProxy最佳做法 设置许多操作系统都提供了预构建的HAProxy软件包,但是通常它们已经过时了。 建议使用正式...

    zabbix_haproxy:用于监控 haproxy 的 Zabbix 脚本

    zabbix_haproxy 用于监控 haproxy 的 Zabbix 脚本 用法: zhaproxy.py -d :发现前端/后端配置 zhaproxy.py -c : 检查所有前端/后端配置 zhaproxy.py -p <proxy> -s <server> -v <attribute> : 获取指定值 用户...

    ansible-haproxy-keepalived:一本使用HAProxy构建并保持高可用性的负载均衡器的剧本

    《Ansible-HAProxy-Keepalived:使用剧本构建高可用性负载均衡器》 在IT行业中,确保服务的高可用性是至关重要的。 HAProxy 和 Keepalived 是两个常用的工具,它们共同工作可以构建出可靠的负载均衡解决方案。...

    haproxy-monitor:实时haproxy状态监控

    haproxy 监视器 一个用于 OpenShift 应用程序的简单、实时的 haproxy 状态监视器。 安装 只需从您的应用程序内部提供静态文件haproxy-monitor.html ,您就可以开始使用了。 我建议将其重命名为 index.html 并将其放...

    kubeadm+haproxy+keepalived部署高可用k8s集群-版本k8s1.20.4—详细文档

    kubeadm部署高可用k8s集群,haproxy+keepalived-版本k8s1.20.4,详细笔记总结文档

    certbot_haproxy_renew:certbot更新后更新HAProxy

    certbot_haproxy_renew 将所有用于haproxy的certbot证书更新为适合crontab的单个命令假设条件你是: 使用HAProxy 使用CertBot,即LetsEncrypt 使用systemd 在/etc/haproxy/haproxy.conf中加载SSL证书,例如bind *:...

    haproxy-2.1:haproxy 2.1 git repo的镜像:https:git.haproxy.orggithaproxy-2.1.git

    HAProxy以其高效、稳定性和强大的性能而闻名,尤其在处理大量并发连接时表现出色。这个版本的源代码可以通过提供的git仓库镜像进行访问,地址是`https://git.haproxy.org/git/haproxy-2.1.git`。 HAProxy 2.1的特性...

Global site tag (gtag.js) - Google Analytics