淘宝为什么用HBase,如何优化?
Hbase是从hadoop中分离出来的apache顶级开源项目。因为它用java实现了google的bigtable系统的大部分功能,所以在数据快速增加的今天非常受欢迎。对于淘宝来说,随着市场规模的扩大,产品和技术的发展,业务数据量越来越大,海量数据的高效插入和读取变得越来越重要。因为淘宝拥有或许是国内最大的单个hadoop集群(天梯),对hadoop产品有着深刻的理解,自然希望用hbase来做这样的海量数据读写服务。本文将对淘宝一年来在线应用中hbase的使用和优化做一个总结。
2个原因
为什么要用hbase?
2011之前,淘宝所有的后端持久化存储基本都是在mysql上进行的(不排除少量的Oracle/BDB/Tail/MongDB等。).mysql因为开源和良好的生态系统,有子数据库、子表等多种解决方案,所以长期以来满足了淘宝大量商家的需求。
但是,由于业务的多元化发展,越来越多的业务系统的要求开始发生变化。一般来说,有以下几种类型的变化:
a)数据量越来越多。事实上,淘宝几乎任何与用户相关的线上业务的数据量都在十亿级别,每天的系统调用从十亿到十亿不等,历史数据无法轻易删除。这就需要一个大规模的分布式文件系统,能够为TB级甚至PB级的数据提供在线服务。
b)数据量快速增加,可能无法准确预测。大多数应用系统在上线后的一段时间内都有快速上升的趋势。所以从成本的角度来说,对系统的横向扩展能力有很强的需求,没有单点约束。
c)只需要简单的kv读数,没有复杂的连接要求。但是对系统的并发性、吞吐量、响应延迟都有非常高的要求,希望系统能够保持很强的一致性。
d)通常情况下,系统写入频繁,尤其是大量系统依赖实时日志分析。
e)希望快速读取批量数据。
f)模式是灵活的,并且可以频繁地更新列属性或添加列。
g)希望好用,有一个好的java接口,语义清晰。
综合来看,我们认为hbase是更合适的选择。首先,它的数据天然被hdfs冗余,天梯稳定运行三年,数据100%可靠,已经证明了hdfs集群的安全性和服务海量数据的能力。其次,hbase本身的数据读写服务并不局限于单点,服务能力可以随着服务器的增长而线性增长,达到几十或几百的规模。LSM树模式的设计使得hbase的写性能非常好,单次写通常可以在1-3ms内完成,并且性能不会随着数据量的增加而下降。
区域(相当于数据库的子表)可以在ms级动态分段和移动,保证了负载均衡。由于hbase上的数据模型是按照rowkey的顺序存储的,连续的整块数据会作为缓存一次性读取,一个好的rowkey设计可以让批量读取变得非常容易,甚至只需要1 io次就可以得到用户想要的几十条或者几百条数据。最后,淘宝大部分工程师都是有java背景的同学,所以hbase的api对他们来说非常好用,培训成本也比较低。
当然,也必须指出的是,大数据背景下没有银弹,hbase本身也有不合适的场景。比如索引只支持主索引(或者被视为主复合索引),又比如服务是单点的,它负责的一些数据在单机宕机后被主恢复的过程中是不会被服务的。这就要求你在选型时需要对自己的应用系统有足够的了解。
3应用情况
我们从2011年3月开始研究hbase如何用于线上服务。虽然之前一个一淘搜索已经有几十个线下服务了。这是因为hbase早期版本的目标是海量数据中的离线服务。2009年9月0.20.0版本的发布是一个里程碑,在线应用正式成为hbase的目标。所以hbase引入了zookeeper作为backupmaster和regionserver的管理。版本2011 1.90.0又是一个里程碑。基本上我们今天看到的各大网站,比如facebook/ebay/yahoo中用于制作的hbase,都是基于这个版本(fb采用的0.89版本的结构和0.90.x版本类似)。加入了Bloomfilter等诸多属性,性能也有了很大的提升。基于此,淘宝也选择了0.90.x分支作为网络版的基础。
第一个在线应用是数据立方体中的prom。Prom最初是基于redis构建的。由于数据量的不断增加和需求的变化,我们用hbase对其存储层进行了改造。准确的说,prom更适合hbase的0.92版本,因为它不仅需要高速在线读写,还需要count/group by等复杂的应用。但是因为当时0.92版本还不成熟,所以我们自己实现了协处理器。Prom的数据导入来自于天梯,所以我们每天晚上花半个小时把天梯的数据写到hbase所在的hdfs,然后做一个web层的客户端转发。经过一个月的数据对比,确认redis的速度比没有明显下降,数据准确,可以成功上线。
第二个上线的应用是TimeTunnel,这是一个高效、可靠、可扩展的实时数据传输平台,广泛应用于实时日志采集、实时数据监控、广告效果实时反馈、实时数据库同步等领域。与prom相比,其特点是增加了在线书写。动态数据增长对hbase上的压缩/平衡/拆分/恢复等诸多特性提出了极大的挑战。TT每天写大约20TB,读大约1.5倍那么多。为此,我们准备了20个regionserver集群。当然,底层的hdfs是公开的,数量更大(下面会提到)。每天TT都会在hbase上为不同的服务建立不同的表,然后将数据写入表中。即使我们把区域的大小上限设置为1GB,最大的服务也会达到上千个区域的规模,可以说每分钟都会有好几个拆分。在推出TT期间,我们修复了hbase中许多关于拆分的bug,并且有几个提交到达了hbase社区。同时,我们也在我们的版本上放了一些最新的社区补丁。与Split相关的bug应该说是hbase最大的数据丢失风险之一,这是每一个想使用hbase的开发者必须牢记的。由于hbase采用LSM树模型,从架构原理上来说几乎不存在数据丢失的可能,但在实际使用中如果不小心就有数据丢失的风险。原因后面会单独强调。TT在预发布过程中,由于元表损坏和split中的bug导致数据丢失,所以我们还单独编写了一个元表恢复工具,以保证以后不会出现类似问题(hbase-0.90.5以后的版本中已经添加了类似工具)。另外,由于我们存放TT的机房不稳定,出现过多次停机事故,甚至假死。因此,我们也开始修改一些补丁,以改善停机恢复时间和加强监测的强度。
CTU和会员中心项目是两个对线上要求很高的项目。在这两个项目中,我们特别研究了hbase的慢响应。hbase响应慢现在一般分为四种原因:网络原因、gc问题、命中率和客户端反序列化。我们现在对他们做了一些解决方案(后面会介绍)来更好的控制反应慢的问题。
与脸书类似,我们也使用hbase作为实时计算项目的存储层。目前已经推出了一些内部实时项目,如实时页面点击系统、银河实时交易推荐、直播间等,用户都是分散在公司各处的小运营商。和facebook的puma不同,淘宝用很多方式做实时计算。比如银河采用类似affa的actor模式处理交易数据,同时通过关联商品表等维度表计算排名(TopN),而实时页面点击系统是基于twitter开源storm开发的,通过TT在后台获取实时日志数据。计算流程将中间结果和动态维度表保存到hbase。比如我们把rowkey设计成url+userid,读取实时数据,实现uv在各个维度的实时计算。
最后,我想特别提一下历史交易订单项目。这个项目其实是一个改造项目,旨在从之前的solr+bdb方案迁移到hbase。因为与购买的页面相关,用户使用非常频繁,重要性接近核心应用,对数据丢失和服务中断零容忍。它对压缩进行优化,以防止在服务时间内发生具有大量数据的压缩。增加了自定义的过滤器实现分页查询,在rowkey上巧妙设计了应用,避免了冗余数据的传输和90%以上的读取转化为顺序读取。目前集群存储超过百亿的订单数据和数千亿的指标数据,在线故障率为0。
随着业务的发展,我们定制的hbase集群已经应用于20多个在线应用和数百台服务器。包括淘宝首页的商品实时推荐、卖家广泛使用的实时量子统计等应用,并有持续增加和接近核心应用的趋势。
4部署、操作和监控
脸书之前透露过它的hbase架构,可以说非常不错。比如他们把message service的hbase集群按照用户划分成若干个集群,每个集群有65,438+000台服务器,有一个namenode,分为五个机架,每个机架有一个zookeeper。可以说,对于数据量大的服务来说,这是一个优秀的架构。对于淘宝来说,因为数据量远没有那么大,应用也没有那么核心,所以我们采用的是公共hdfs和zookeeper集群的架构。每个hdfs集群尽量不要超过100个单元(这是为了尽可能限制namenode的单点问题)。在上面设置几个hbase集群,每个集群都有一个master和一个backupmaster。公共hdfs的好处是可以最小化紧凑的影响,平摊硬盘的成本,因为总有对磁盘空间要求高的集群,也总有对磁盘空间要求低的集群,混合在一起更划算。Zookeeper集群很常见,每个hbase集群都属于zk上不同的根节点。hbase集群的独立性由zk的权限机制保证。zk常见的原因只是为了运维方便。
因为是线上应用,运营和监控变得更加重要。因为之前的经验接近于零,很难招到专门的hbase运维人员。我们的开发团队和运维团队从一开始就非常重视这个问题,很早就开始培养自己。下面说说我们在运营和监控方面的经验。
我们定制的hbase功能的一个重要部分是增加监控。Hbase本身可以发送ganglia监控数据,但是监控项目远远不够,ganglia的显示方式不够直观和突出。因此,一方面,我们在代码中有创地添加了很多监控点,比如压缩/拆分/平衡/刷新队列和每个阶段的耗时,读写每个阶段的响应时间,读写次数,区域的开/关,表级和区域级的读写次数。它们仍然通过socket发送给ganglia,ganglia会将它们记录在rrd文件中。rrd文件的特点是历史数据的准确性会越来越低,所以我们自己编写程序从rrd中读取相应的数据并持久化到其他地方,然后我们用js实现了一套监控界面,重点是以趋势图、饼状图等各种方式汇总显示我们关心的数据,可以查看任何历史数据而不损失准确性。同时,一些非常重要的数据,如读写次数、响应时间等。,将写入数据库实现波动报警等自定义报警。通过以上措施,保证了我们总能先于用户发现集群问题,并及时修复。我们使用redis高效排序算法对各个区域的读写次数进行实时排序,可以在高负载下找到那些特定请求次数较高的区域,并将其移动到空闲的regionserver。在高峰期,我们可以对数百台机器的数十万个区域进行实时排序。
为了隔离应用程序的影响,我们可以在代码级别检查来自不同客户端的连接,并切断一些客户端的连接,从而将故障隔离在一个应用程序内部,而不会在故障发生时将其放大。mapreduce的应用也会被控制在低峰期运行,比如我们会在白天关闭jobtracker。
另外,为了从结果上保证服务的可用性,我们还会定期运行读写测试、建表测试、hbck等命令。Hbck是一个非常有用的工具,但是也是一个繁重的工作操作,所以尽量减少hbck的调用次数,尽量不要并行运行hbck服务。0.90.4之前的Hbck会有一些机会关闭hbase。另外,为了保证hdfs的安全性,需要定期运行fsck来检查hdfs的状态,比如块的副本数量。
我们会每天跟踪所有在线服务器的日志,找出所有的错误日志并通过邮件发给开发者,找出每个错误上面的问题的原因和修复方法。直到误差减小到0。另外,如果每个hbck结果有问题,也会通过邮件发给开发者进行处理。虽然不是每一个错误都会产生问题,甚至大部分错误只是分布式系统中的正常现象,但是了解其产生问题的原因是非常重要的。
5测试和发布
因为是未知系统,所以从一开始就非常注重测试。测试从一开始就分为性能测试和功能测试。性能测试主要以基准测试为主,分为很多场景,比如不同的混合读写比,不同的k/v大小,不同的列族,不同的命中率,是否要做预哈等等。每次运行将持续几个小时,以获得准确的结果。所以我们写了一个自动化系统,从web上选取不同的场景,后台会自动把测试参数传到服务器上执行。因为它测试的是分布式系统,所以客户端也必须是分布式的。
我们判断测试是否准确的依据是同一个场景是否多次运行,数据和运行曲线是否达到99%以上的重合。这项工作非常繁琐,耗费了大量的时间,但后来的事实证明它非常有意义。因为我们对它建立了100%的信任,这很重要,比如我们后期的提升哪怕只是提升2%的性能也能被准确捕捉到,再比如一个代码的修改在紧凑的队列曲线上造成了一些起伏,被我们看到了,从而发现了程序的bug,等等。
功能测试主要是接口测试和异常测试。接口测试的一般作用并不明显,因为hbase本身的单元测试已经涵盖了这部分。然而,异常测试非常重要。我们大部分的bug修改都是在异常测试中发现的,这有助于我们去除生产环境中可能存在的很多不稳定因素。我们也向社区提交了十几个相应的补丁,得到了重视和承诺。分布式系统设计的难点和复杂性在于异常处理,必须在通信的任何时候都认为系统是不可靠的。对于一些难以重现的问题,我们会通过检查代码已经大致定位了问题,并在代码层面强行抛出异常的方式来重现。事实证明这很有用。
为了方便快捷的定位问题,我们设计了一套日志采集处理程序,方便的从各个服务器抓取相应的日志,并按照一定的规则进行汇总。这对于避免浪费大量时间登录不同的服务器来寻找bug的线索非常重要。
由于hbase社区的不断发展,以及在线或测试环境中发现的新bug,我们需要制定一个定期发布的模式。既要避免频繁发布带来的不稳定性,又要避免长期不发布导致量产版离开发版越来越远或者隐藏的bug爆发。强制要求我们每两周从内部主干发布一个版本,必须通过包括回归测试在内的所有测试,发布后在小型集群上不间断运行24小时。每月发布一次,发布最新版本,现有集群按重要性顺序发布,确保重要应用不受新版本潜在bug影响。事实证明,自从我们引入这个发布机制后,发布带来的不稳定因素大大减少,网络版也能保持不远。
6改进和优化
脸书是一家非常值得尊敬的公司。他们毫无保留地公布了对hbase的所有修改,并向社区开放了他们内部实际使用的版本。facebook在线应用的一个重要特点是,他们关闭了split,以降低split带来的风险。与facebook不同的是,淘宝的业务数据并没有那么庞大,而且由于应用类型丰富,我们并不要求用户选择强行关闭split,而是尽量修改split中可能存在的bug。到目前为止,虽然还不能说完全解决了这个问题,但是从0.90.2开始暴露出来的很多与分裂和宕机相关的bug在我们的测试环境中已经被修复到接近0,并且已经为社区提交了10的稳定性相关补丁,最重要的如下:
Mitor帮助我们回到0.90版本。所以在社区发布了从0.90.2到0.90.6五个版本的bugfix之后,0.90.6版本其实已经比较稳定了。建议生产环境可以考虑这个版本。
Split这是一个重事务,它有一个严重的问题就是会修改元表(当然它宕机的时候也有这个问题)。如果在此期间发生异常,很有可能hdfs上的元表、rs内存、主内存和文件不一致,导致稍后重新分配区域时出错。其中一个错误是,有可能同一个区域被两个以上的regionserver服务,然后这个区域服务的数据可能被随机写入多个rs,读取的时候会分开读取,造成数据丢失。如果要恢复原来的状态,就必须删除其中一个rs上的区域,这就导致了数据的主动删除,从而导致数据丢失。
上面提到的响应慢的问题,归纳起来就是网络原因,gc问题,命中率,客户端反序列化。网络原因一般是网络不稳定造成的,但也可能是tcp参数设置的问题,需要保证尽可能降低数据包延迟。比如nodelay需要设置为true等。我们通过tcpdump等一系列工具专门定位了这些问题,证明了tcp参数确实会造成包组装中连接缓慢。Gc应该基于应用程序的类型。一般来说,在阅读量比较大的应用中,新生代不能设置的太小。命中率极大地影响了响应时间。我们将尝试将版本号设置为1,以增加缓存容量。良好的平衡性也有助于充分发挥每台机器的命中率。为此,我们设计了一台台式天平。
因为hbase服务是单点的,也就是一台机器宕机,这台机器服务的数据在恢复之前是无法读写的。停机恢复的速度决定了我们服务的可用性。为此,进行了多项优化。首先,尽可能将zk的停机发现时间缩短到1分钟。其次,将主服务器的恢复日志改进为并行恢复,大大提高了主服务器恢复日志的速度。然后,我们修改一些openhandler中可能出现的超时异常和死锁,删除日志中可能出现的open…too long等异常。修复了原hbase在10停机时可能几分钟甚至半小时无法重启的问题。此外,在hdfs级别,我们缩短了socket.timeout和retry的时间,以减少datanode宕机导致的长期阻塞现象。
目前对于hbase本身读写水平的优化,我们还没有做太多的工作。唯一的补丁是区域增大时书写性能严重下降。由于hbase本身的良好性能,我们通过大量的测试找到了各种应用场景下的优秀参数,并应用到生产环境中,基本满足要求。但这是我们下一个重要的工作。
7未来计划
我们目前在淘宝维护基于社区0.90.x的hbase定制版。接下来除了继续修复其bug,还会维持基于0.92.x修改的版本。之所以会这样,是因为0.92.x和0.90.x之间的兼容性不是很好,0.92.x修改的代码非常多,粗略统计会超过30%。0.92中有一些我们非常看重的特性。
版本0.92将hfile改进为hfilev2,v2的特点是对索引和bloomfilter进行了很大的改造,以支持单个大型hfile文档。当现有HFile的文件大小达到一定程度时,索引会占用大量内存,加载文件的速度会大大降低。但是,如果HFile不增加,区域就无法扩展,导致区域数量非常大。这是我们希望尽可能避免的事情。
0.92版本改进了通信层协议,增加了通信层长度,这一点非常重要。它允许我们编写nio的客户端,这样反序列化将不再影响客户端的性能。
0.92版增加了协处理器特性,支持少量希望依靠rs的应用。
还有很多其他的优化,比如改进balance算法,改进compact算法,改进scan算法,把compact改成CF级别,动态做ddl等等。
除了0.92版,0.94版和最新的trunk(0.96)也有很多好的特性,0.94是性能优化版。它做了很多革命性的工作,比如移除根表,比如HLog压缩,在复制中支持多个从集群等等。
我们自己也有一些优化,比如自己实现的二级索引和备份策略,会在内部版本中实现。
另外值得一提的是,hdfs级别的优化也很重要。hadoop-1.0.0和cloudera-3u3的改进对hbase很有帮助,比如本地化读取、校验和的改进、datanode的keepalive设置、namenode的HA策略等。我们有一个优秀的hdfs团队来支持我们的hdfs工作,比如定位和修复一些hdfs的bug,帮助提供一些关于hdfs参数的建议,帮助实现namenode的HA。最新测试表明,3u3校验和+本地化读取至少可以使随机读取性能提高一倍。
我们正在做的一件有意义的事情是实时监控和调整regionserver的负载,可以将集群中负载不足的服务器动态移动到负载较高的集群中,整个过程对用户完全透明。
总的来说,我们的策略是尽可能的和社区合作,推动hbase在整个apache生态系统和行业的发展,让它更稳定的部署到更多的应用上,降低使用门槛和使用成本。