随着现在数据量的日益爆炸,存储一直都是一个系统的重中之重。从最早的打孔式的数据存储方式,到数据库的横空出世,再到后面的分布式存储,存储的方式一直都在演变,但是核心功能却是不变的 ———— 存下更多的数据以及更快的查到数据。Elasticsearch(下文简称为ES)是一个非常著名且非常流行的搜索引擎,被广泛用在各种数据存储与数据搜索场景中中。记得阿摸还在菜鸟的时候,物流订单的存储就是双写——一份写数据库,一份写ES,ES的数据主要用作高性能的复杂查询。相比solr,它有着更加丰富的功能,如地理位置查询,时序数据分析等。同样作为文档型数据库,读写性能优于MongoDB。再说日志处理,监控数据分析的领域,以ES为基础的ELK技术栈更是这个领域的领导者。本文借着Elasticsearch来探索一下分布式存储系统的设计 本文分如下几个部分
- 本地文件 VS 共享存储 —— 常见的分布式存储系统的架构选择
Elasticsearch集群架构
本小结分为如下几个部分:
概念分析
上图是ES的集群的简单图示,我们可以在上图看出ES集群中我们最需要关注的概念有如下几个:
- 集群(cluster):表示运行着可以互相发现,组成集群的ES实例集合
- 节点(node):物理概念,表示一个运行ES实例的进程,通常一台机器只运行一个ES实例。
- 索引(index):可以类比数据库中的表的概念,包括配置数据与倒排正排数据文件
- 分片(shard):ES是分布式搜索引擎,每个索引有一个或多个分片,索引的数据被分配到各个分片上,相当于一桶水用了N个杯子装。分片有助于横向扩展,N个分片会被尽可能平均地(rebalance)分配在不同的节点上(例如你有2个节点,4个主分片(不考虑备份),那么每个节点会分到2个分片,后来你增加了2个节点,那么你这4个节点上都会有1个分片,这个过程叫relocation,ES感知后自动完成)
- 副本(replica):复制,可以理解为备份分片,相应地有primary shard(主分片)。主分片和备分片不会出现在同一个节点上(防止单点故障),默认情况下一个索引创建5个分片一个备份(即5primary+5replica=10个分片),如果你只有一个节点,那么5个replica都无法分配(unassigned)
ps:更加细节的数据模型,留到ES的数据模型篇再讲。从上图我们可以看到index的配置为3*2,即3 primary shard + 3 replica shard。基于系统可用性的考虑,同一个shard的primary和replica不能位于同一个Node中。
角色分析
ES中节点有角色的区分的,通过配置文件conf/elasticsearch.yml中配置以下配置进行角色的设定。
# 允许一个节点是否可以成为一个master节点,es是默认集群中的第一台机器为master,如果这台机器停止就会重新选举master.
node.master: true
# 允许该节点存储数据(默认开启)
node.data: true
# 搜索器,从节点中获取数据,生成搜索结果等
node.ingest: true
# 配置文件中给出了三种配置高性能集群拓扑结构的模式,如下:
# 1. 如果你想让节点从不选举为主节点,只用来存储数据,可作为负载器
node.master: false
node.data: true
node.ingest: true #默认true
# 2. 如果想让节点成为主节点,且不存储任何数据,并保有空闲资源,可作为协调器
node.master: true
node.data: false
node.ingest: true
# 3. 如果想让节点既不称为主节点,又不成为数据节点,那么可将他作为搜索器,从节点中获取数据,生成搜索结果等
node.master: false
node.data: false
node.ingest: true
# 4. 仅作为协调器
node.master: false
node.data: false
node.ingest: false
上面的配置其实已经很清晰的将ES的节点的角色分列了出来:
- **主节点:**负责索引的添加、删除,跟踪哪些节点是群集的一部分,对分片进行分配、收集集群中各节点的状态等。
- **候选主节点:**只有是候选主节点才可以参与选举投票,也只有候选主节点可以被选举为主节点。
- **数据节点:**负责对数据的增、删、改、查、聚合等操作,数据的查询和存储都是由数据节点负责。一般用高配机器作为数据节点
- **搜索节点:**一般和协调节点是一起的,可以通过配置来确定是否设置为搜索节点。主要的作用就是从节点中获取数据,生成搜索结果等。
- **协调节点:**其不是通过设置来设置的,用户的请求可以随机发往任何一个节点,并由该节点负责分发请求、收集结果等操作,而不需要主节点转发。集群中的任何节点都可以充当协调节点的角色。
发现机制与选主机制
ES组成一个集群的方式非常的简单,只要如下一行配置,你就可以马上组件一个ES的集群
cluster.name=my-es-cluster
为什么ES可以使用如此简单的配置就可以组成一个集群呢,这里就要讲一把ES自身的发现机制——ZenDiscovery。有别于Solr使用Zookeeper来做节点发现以及节点协调,ES的内置ZenDiscovery发现机制。ZenDiscovery提供单播和多播两种发现方式,主要负责是集群中节点的发现以及选举主节点。
多播,也叫组播,指一个节点可以向多台机器发送请求。生产环境中ES不建议使用这种方式,对于一个大规模的集群,多播会产生大量无用的通信。
默认情况下ZenDiscovery使用发现方式为单播,当一个节点加入一个现有集群,或者组建一个新的集群时,请求发送到一台机器。当一个节点联系到单播列表中的成员时,它就会得到整个集群所有节点的状态,然后它会联系主节点,并加入集群。使用单播时,列表不需要包含集群中的所有节点,它只是需要足够的节点,当一个新节点联系上其中一个并且通信就可以了。ES官方建议discovery.zen.ping.unicast.hosts配置为所有的候选主节点,就像下面这样配置:
discovery.zen.ping.unicast.hosts: ["host1", "host2:port","host3"]
ZenDiscovery 会每隔ping_interval ping一次,每次超时时间是discovery.zen.ping_timeout,ping_retries次 ping失败则认为节点宕机,宕机的情况下会触发failover,会进行分片重分配、复制等操作。如果宕机的节点不是主节点,则主节点会更新集群的元信息,主节点将最新的集群元信息发布出去,给其他节点,其他节点回复Ack,主节点收到discovery.zen.minimum_master_nodes-1个候选主节点的回复,则发送Apply消息给其他节点,集群状态更新完毕。如果宕机的节点是Master,则其他的候选主节点开始Master节点的选举流程。那整个集群是如何选主的呢。ES的选主使用的是很经典的quorum算法。选主的发起由候选主节点发起,当前候选主节点发现自己不是master节点,并且通过ping其他节点发现无法联系到主节点,并且包括自己在内已经有超过minimum_master_nodes个节点无法联系到主节点,那么这个时候则发起选主。
选主流程图
选主的时候按照集群节点的参数 排序。stateVersion从大到小排序,以便选出集群元信息较新的节点作为Master,id从小到大排序,避免在stateVersion相同时发生分票无法选出 Master。
排序后第一个节点即为Master节点。当一个候选主节点发起一次选举时,它会按照上述排序策略选出一个它认为的Master
Elasticsearch数据架构
数据存储
Elasticsearch的Index和meta,目前支持存储在本地文件系统中,同时支持niofs,mmap,simplefs,smb等不同加载方式,性能最好的是直接将索引LOCK进内存的MMap方式。默认,Elasticsearch会自动选择加载方式,另外可以自己在配置文件中配置。这里有几个细节,具体可以看官方文档。
索引和meta数据都存在本地,会带来一个问题:当某一台机器宕机或者磁盘损坏的时候,数据就丢失了。为了解决这个问题,可以使用Replica(副本)功能。
副本
可以为每一个Index设置一个配置项:副本(Replicda)数,如果设置副本数为2,那么就会有3个Shard,其中一个是PrimaryShard,其余两个是ReplicaShard,这三个Shard会被Mater尽量调度到不同机器,甚至机架上,这三个Shard中的数据一样,提供同样的服务能力。
副本(Replica)的目的有三个:
- 保证服务可用性:当设置了多个Replica的时候,如果某一个Replica不可用的时候,那么请求流量可以继续发往其他Replica,服务可以很快恢复开始服务。
- 保证数据可靠性:如果只有一个Primary,没有Replica,那么当Primary的机器磁盘损坏的时候,那么这个Node中所有Shard的数据会丢失,只能reindex了。
- 提供更大的查询能力:当Shard提供的查询能力无法满足业务需求的时候, 可以继续加N个Replica,这样查询能力就能提高N倍,轻松增加系统的并发度。
架构的优劣
上面说了一些优势,这种架构同样在一些场景下会有些问题。
Elasticsearch采用的是基于本地文件系统,使用Replica保证数据可靠性的技术架构,这种架构一定程度上可以满足大部分需求和场景,但是也存在一些遗憾:
- Replica带来成本浪费。为了保证数据可靠性,必须使用Replica,但是当一个Shard就能满足处理能力的时候,另一个Shard的计算能力就会浪费。
- Replica带来写性能和吞吐的下降。每次Index或者update的时候,需要先更新Primary Shard,更新成功后再并行去更新Replica,再加上长尾,写入性能会有不少的下降。
- 当出现热点或者需要紧急扩容的时候动态增加Replica慢。新Shard的数据需要完全从其他Shard拷贝,拷贝时间较长。上面介绍了Elasticsearch数据层的架构,以及副本策略带来的优势和不足,下面简单介绍了几种不同形式的分布式数据系统架构。
常见的分布式存储系统的架构选择
本地文件还是共享存储?
第一种:基于本地文件系统的分布式系统
上图中是一个基于本地磁盘存储数据的分布式系统。Index一共有3个Shard,每个Shard除了Primary Shard外,还有一个Replica Shard。当Node 3机器宕机或磁盘损坏的时候,首先确认P3已经不可用,重新选举R3位Primary Shard,此Shard发生主备切换。然后重新找一台机器Node 7,在Node7 上重新启动P3的新Replica。由于数据都会存在本地磁盘,此时需要将Shard 3的数据从Node 6上拷贝到Node7上。如果有200G数据,千兆网络,拷贝完需要1600秒。如果没有replica,则这1600秒内这些Shard就不能服务。
为了保证可靠性,就需要冗余Shard,会导致更多的物理资源消耗。
这种思想的另外一种表现形式是使用双集群,集群级别做备份。
在这种架构中,如果你的数据是在其他存储系统中生成的,比如HDFS/HBase,那么你还需要一个数据传输系统,将准备好的数据分发到相应的机器上。
这种架构中为了保证可用性和可靠性,需要双集群或者Replica才能用于生产环境,优势和副作用在上面介绍Elasticsearch的时候已经介绍过了,这里就就不赘述了。
Elasticsearch使用的就是这种架构方式。
第二种:基于分布式文件系统的分布式系统(共享存储)
针对第一种架构中的问题,另一种思路是:存储和计算分离。
第一种思路的问题根源是数据量大,拷贝数据耗时多,那么有没有办法可以不拷贝数据?为了实现这个目的,一种思路是底层存储层使用共享存储,每个Shard只需要连接到一个分布式文件系统中的一个目录/文件即可,Shard中不含有数据,只含有计算部分。相当于每个Node中只负责计算部分,存储部分放在底层的另一个分布式文件系统中,比如HDFS。
上图中,Node 1 连接到第一个文件;Node 2连接到第二个文件;Node3连接到第三个文件。当Node 3机器宕机后,只需要在Node 4机器上新建一个空的Shard,然后构造一个新连接,连接到底层分布式文件系统的第三个文件即可,创建连接的速度是很快的,总耗时会非常短。
这种是一种典型的存储和计算分离的架构,优势有以下几个方面:
在这种架构下,资源可以更加弹性,当存储不够的时候只需要扩容存储系统的容量;当计算不够的时候,只需要扩容计算部分容量。
存储和计算是独立管理的,资源管理粒度更小,管理更加精细化,浪费更少,结果就是总体成本可以更低。
负载更加突出,抗热点能力更强。一般热点问题基本都出现在计算部分,对于存储和计算分离系统,计算部分由于没有绑定数据,可以实时的扩容、缩容和迁移,当出现热点的时候,可以第一时间将计算调度到新节点上。这种架构同时也有几个不足:
访问分布式文件系统的性能可能不及访问本地文件系统。在上一代分布式文件系统中,这是一个比较明显的问题,但是目前使用了各种用户态协议栈后,这个差距已经越来越小了。
强依赖底层分布式存储,目前业界流行的HDFS的架构设计逻辑其实也是基于本地文件系统的存储。万变不离其宗。
HBase使用的就是这种架构方式。
小结
上面以ES的架构为例子分析了分布式存储系统的设计架构,其实分布式存储系统还涉及到很多的方面,比如离线计算,数据分析等等,细节之多,权衡的艺术体现之明显,完全值得更加深入的去研究。