弥补MySQL和Redis短板:看HBase怎么确保高可用

作者:微信小助手

发布时间:2019-03-27T18:11:02

HBase 是一个基于 Hadoop 面向列的非关系型分布式数据库(NoSQL),设计概念来源于谷歌的 BigTable 模型。


它面向实时读写、随机访问大规模数据集的场景,是一个高可靠性、高性能、高伸缩的分布式存储系统,在大数据相关领域应用广泛。


HBase 系统支持对所存储的数据进行透明切分,从而使得系统的存储以及计算具有良好的水平扩展性。


知乎从 2017 年起开始逐渐采用 HBase 系统存储各类在线业务数据,并在 HBase 服务之上构建各类应用模型以及数据计算任务。


伴随着知乎这两年的发展,知乎核心架构团队基于开源容器调度平台 Kubernetes 打造了一整套 HBase 服务平台管理系统。


经过近两年的研发迭代,目前已经形成了一套较为完整的 HBase 自动化运维服务体系,能够完成 HBase 集群的快捷部署、平滑扩缩容、HBase 组件细粒度监控、故障跟踪等功能。


知乎对 HBase 的使用经验不算太长,在 2017 年初的时候,HBase 服务主要用于离线算法、推荐、反作弊,还有基础数据仓库数据的存储计算,通过 MapReduce 和 Spark 来进行访问。


而在当时知乎的在线存储主要采用 MySQL 和 Redis 系统,其中:

  • MySQL:支持大部分的业务数据存储,当数据规模增大后有一些需要进行扩容的表,分表会带来一定的复杂性。

    有些业务希望能屏蔽这个事情,还有一些是因为历史原因在表设计的时候用 rmsdb 的形式存了一些本该由列存储的数据,希望做一下迁移。此外 MySQL 基于 SSD,虽然性能很好,花销也比较大。

  • Redis:可以提供大规模的缓存,也可以提供一定的存储支持。Redis 性能极好,主要的局限是做数据 Resharding 较为繁琐,其次是内存成本较高。


针对以上两种在线存储所存在的一些问题,我们希望建立一套在线存储 NoSQL 服务,对以上两种存储作为一个补充。


选型期间我们也考虑过 Cassandra,早期一些业务曾尝试使用 Cassandra 作为存储。


隔壁团队在运维了一段时间的 Cassandra 系统之后,遇到不少的问题,Cassandra 系统可操作性没有达到预期,目前除了 Tracing 相关的系统,其他业务已经放弃使用 Cassandra。


我们从已有的离线存储系统出发,在衡量了稳定性、性能、代码成熟度、上下游系统承接、业界使用场景以及社区活跃度等方面之后,选择了 HBase,作为知乎在线存储的支撑组件之一。


HBase On Kubernetes


初期知乎只有一套进行离线计算的集群,所有业务都跑在一个集群上,并且 HBase 集群和其他离线计算 Yarn 以及 Impala 混合部署,HBase 的日常离线计算和数据读写都严重受到其他系统影响。


并且 HBase 的监控都只停留在主机层面的监控,出现运行问题时,进行排查很困难,系统恢复服务时间较长,这种状态下,我们需要重新构建一套适用于在线服务的系统。


在这样的场景下,我们对在线 HBase 服务的需求是明确的:


①隔离性:从业务方的视角来说,希望相关的服务做到环境隔离,权限收归业务,避免误操作和业务相互影响。


对于响应时间,服务的可用性,都可以根据业务的需要指定 SLA;对于资源的分配和 blockcache 等参数的配置也能够更加有适应性,提供业务级别的监控和报警,快速定位和响应问题。


②资源利用率:从运维的角度,资源的分配要合理,尽可能的提升主机 CPU,内存包括磁盘的有效利用率。


③成本控制:团队用最小的成本去得到最大的运维收益,所以需要提供便捷的调用接口,能够灵活的进行 HBase 集群的申请、扩容、管理、监控。


同时成本包括机器资源,还有工程师。当时我们线上的这套系统是由一位工程师独立去进行维护。


综合以上需求,参考我们团队之前对基础设施平台化的经验,最终的目标是把 HBase 服务做成基础组件服务平台提供给上游业务。


这个也是知乎技术平台部门工作思路之一,尽可能的把所有的组件对业务都黑盒化,接口化,服务化。


同时在使用和监控的粒度上尽可能的准确,细致,全面。这是我们构建在线 HBase 管理运维系统的一个初衷。


Why Kubernetes?


前文说到我们希望将整个 HBase 系统平台服务化,那就涉及到如何管理和运维 HBase 系统,知乎在微服务和容器方面的工作积累和经验是相当丰富的:

  • 在当时我们所有的在线业务都已经完成了容器化的迁移工作,超万级别的业务容器平稳运行在基于 Mesos 的容器管理平台 Bay 上(参见[1])。

  • 与此同时,团队也在积极的做着 Infrastructure 容器化的尝试,已经成功将基础消息队列组件 Kafka 容器化运行于 Kubernetes 系统之上(参见[2]),因此我们决定也将 HBase 通过 Kubernetes 来进行资源的管理调度。


Kubernetes 是谷歌开源的容器集群管理系统,是 Google 多年大规模容器管理技术 Borg 的开源版本。


Kubernetes 提供各种维度组件的资源管理和调度方案,隔离容器的资源使用,各个组件的 HA 工作,同时还有较为完善的网络方案。


Kubernetes 被设计作为构建组件和工具的生态系统平台,可以轻松地部署、扩展和管理应用程序。有着 Kubernetes 大法的加持,我们很快有了最初的落地版本([4])。


初代架构


最初的落地版本架构见下图,平台在共享的物理集群上通过 Kubernetes(以下简称 K8S)API 建立了多套逻辑上隔离的 HBase 集群。


每套集群由一组 Master 和若干个 Regionserver(以下简称 RS)构成,集群共享一套 HDFS 存储集群,各自依赖的 Zookeeper 集群独立;集群通过一套管理系统 Kubas 服务来进行管理([4])。

第一代架构


模块定义:在 K8S 中如何去构建 HBase 集群,首先需要用 K8S 本身的基础组件去描述 HBase 的构成。


K8S 的资源组件有以下几种:

  • Node:定义主机节点,可以是物理机,也可以是虚拟机;

  • Pod:一组紧密关联的容器集合,是 K8S 调度的基本单位;

  • ReplicationController:一组 Pod 的控制器,通过其能够确保 Pod 的运行数量和健康,并能够弹性伸缩。


结合之前 Kafka on K8S 的经验,出于高可用和扩展性的考虑,我们没有采用一个 Pod 里带多个容器的部署方式。


而是统一用一个 ReplicationController 定义一类HBase组件,就是上图中的 Master,Regionserver 还有按需创建的 Thriftserver。


通过以上概念,我们在 K8S 上就可以这样定义一套最小 HBase 集群:

  • 2*MasterReplicationController

  • 3*RegionserverReplicationController

  • 2*ThriftserverReplicationController(可选)


高可用以及故障恢复


作为面向在线业务服务的系统,高可用和故障转移是必需在设计就要考虑的事情,在整体设计中,我们分别考虑组件级别、集群级别和数据存储级别的可用性和故障恢复问题。


①组件级别


HBase 本身已经考虑了很多故障切换和恢复的方案:

  • Zookeeper 集群:自身设计保证了可用性。

  • Master:通过多个 Master 注册在 Zookeeper 集群上来进行主节点的 HA 和更新。

  • RegionServer:本身就是无状态的,节点失效下线以后会把上面的 Region 自动迁走,对服务可用性不会有太大影响。

  • Thriftserver:当时业务大多数是 Python 和 Golang,通过用 Thrift 对 HBase 的进行,Thriftserver 本身是单点的,这里我们通过 HAProxy 来代理一组 Thriftserver 服务。

  • HDFS:本身又由 Namenode 和 DataNode 节点组成,Namenode 我们开启 HA 功能,保证了 HDFS 的集群可用性。


②集群级别


关于集群级别:

  • Pod 容器失效:Pod 是通过 Replication Controller 维护的,K8S 的 Controller Manager 会在它的存储 etcd 去监听组件的失效情况,如果副本少于预设值会自动新的 Pod 容器来进行服务。

  • Kubernetes 集群崩溃:该场景曾经在生产环境中出现过,针对这种情况,我们对 SLA 要求较高的业务采用了少量物理机搭配容器的方式进行混合部署,极端场景出现时,可以保证重要业务受到的影响可控。


③数据级别


所有在 K8S 上构建的 HBase 集群都共享了一套 HDFS 集群,数据的可用性由 HDFS 集群的多副本来提供。


实现细节


资源分配


初期物理节点统一采用 2*12 核心的 CPU,128G 内存和 4T 的磁盘,其中磁盘用于搭建服务的 HDFS,CPU 和内存则在 K8S 环境中用于建立 HBase 相关服务的节点。


Master 组件的功能主要是管理 HBase 集群,Thriftserver 组件主要承担代理的角色,所以这两个组件资源都按照固定额度分配。


在对 Regionserver 组件进行资源分配设计的时候,考虑两种方式去定义资源:

资源分配方式


按照业务需求分配:

  • 根据业务方对自身服务的描述,对相关的 QPS 以及 SLA 进行评估,为业务专门配置参数,包含 Blockcache,Region 大小以及数量等。

  • 优点是针对业务优化,能够充分的利用资源,降低业务的资源占用成本。

  • 管理成本增加,需要对每一个业务进行评估,对平台维护人员非常不友好,同时需要业务同学本身对 HBase 有理解。


统一规格的资源分配:

  • CPU 以及 MEM 都按照预先设定好的配额来分配,提供多档的配置,将 CPU 和 MEM 的配置套餐化。

  • 方便之处在于业务扩容时直接增加 Regionserver 的个数,配置稳定,运维成本较低,遇到问题时排障方便。

  • 针对某些有特有访问方式的业务有局限性,如 CPU 计算型,大 KV 存储,或者有 MOB 需求的业务,需要特殊的定制。

  • 介于当时考虑接入的在线业务并不多,所以采用了按业务定制的方式去配置 Regionserver,正式环境同一业务采用统一配置的一组 Regionserver,不存在混合配置的 Regionserver 组。


参数配置


# Example for hbase dockerfile 
# install cdh5.5.0-hbase1.0.0
ADD hdfs-site.xml /usr/lib/hbase/conf/
ADD core-site.xml /usr/lib/hbase/conf/
ADD env-init.py /usr/lib/hbase/bin/
ENV JAVA_HOME /usr/lib/jvm/java-8-oracle
ENV HBASE_HOME /usr/lib/hbase
ENV HADOOP_PREFIX /usr/lib/hadoop
ADD env-init.py /usr/lib/hbase/bin/
ADD hadoop_xml_conf.sh /usr/lib/hbase/bin/


基础镜像基于 cdh5.5.0-hbase1.0.0 构建:

  • 固定的环境变量,如 JDK_HOME,HBASE_HOME,都通过 ENV 注入到容器镜像中。

  • 与 HDFS 相关的环境变量,如 hdfs-site.xml 和 core-site.xml 预先加入 Docker 镜像中,构建的过程中就放入了 HBase 的相关目录中,用以确保 HBase 服务能够通过对应配置访问到 HDFS。

  • 与 HBase 相关的配置信息,如组件启动依赖的 Zookeeper 集群地址, HDFS 数据目录路径,堆内存以及 GC 参数等,这些配置都需要根据传入 KubasService 的信息进行对应变量的修改。


一个典型的传入参数示例:

REQUEST_DATA = {
       "name"'test-cluster',
       "rootdir""hdfs://namenode01:8020/tmp/hbase/test-cluster",
       "zkparent""/test-cluster",
       "zkhost""zookeeper01,zookeeper02,zookeeper03",
       "zkport": 2181,
       "regionserver_num"'3',
       "codecs""snappy",
       "client_type""java",
       "cpu"'1',
       "memory"'30',
       "status""running",
}


通过上面的参数 KubasService 启动 Docker 时,在启动命令中利用 hadoop_xml_conf.sh 和 env-init.py 修改 hbase-site.xml 和 hbase-env.sh 文件来完成最后的配置注入,如下所示:

source /usr/lib/hbase/bin/hadoop_xml_conf.sh
&& put_config --file /etc/hbase/conf/hbase-site.xml --property hbase.regionserver.codecs --value snappy
&& put_config --file /etc/hbase/conf/hbase-site.xml --property zookeeper.znode.parent --value /test-cluster
&& put_config --file /etc/hbase/conf/hbase-site.xml --property hbase.rootdir --value hdfs://namenode01:8020/tmp/hbase/test-cluster
&& put_config --file /etc/hbase/conf/hbase-site.xml --property hbase.zookeeper.quorum --value zookeeper01,zookeeper02,zookeeper03
&& put_config --file /etc/hbase/conf/hbase-site.xml --property hbase.zookeeper.property.clientPort --value 2181
&& service hbase-regionserver start && tail -f /var/log/hbase/hbase-hbase-regionserver.log


网络通信


网络方面,采用了 Kubernetes 上原生的网络模式,每一个 Pod 都有自己的 IP 地址,容器之间可以直接通信。


同时在 Kubernetes 集群中添加了 DNS 自动注册和反注册功能,以 Pod 的标识名字作为域名,在 Pod 创建和重启和销毁时将相关信息同步全局 DNS。


在这个地方我们遇到过问题,当时我们的 DNS 解析不能在 Docker 网络环境中通过 IP 反解出对应的容器域名。


这就使得 Regionserver 在启动之后向 Master 注册和向 Zookeeper 集群注册的服务名字不一致。


导致 Master 中对同一个 Regionserver 登记两次,造成 Master 与 Regionserver 无法正常通信,整个集群无法正常提供服务。


经过我们对源码的研究和实验之后,我们在容器启动 Regionserver 服务之前修改 /etc/hosts 文件,将 Kubernetes 对注入的 hostname 信息屏蔽。


这样的修改让容器启动的 HBase 集群能够顺利启动并初始化成功,但是也给运维提升了复杂度。


因为现在 HBase 提供的 Master 页现在看到的 Regionserver 都是 IP 形式的记录,给监控和故障处理带来了诸多不便。


存在问题