
2.4 分片与复制
全文检索面对的文档数量往往十分惊人,因为这些文档一般都来自访问流量巨大的互联网应用。比如,搜索引擎要处理的网页数据,在线交易系统每天产生的订单等。即便是在业务数据并不多的普通应用中,它们产生的日志文档容量往往也十分庞大。如果这些文档都存储在Elasticsearch中,Elasticsearch就必须要解决海量文档存储的问题。这不仅是要解决文档能够存得下的问题,还要保证它们不会因为故障而丢失,同时还要保证文档的检索速度尽可能不受文档数量增加的影响。
2.4.1 分片与集群
解决大数据存储的通用方案称为分片(Shard),它的核心思想是将数据分解成大小合适的片段,然后再将它们存储到集群中不同的节点上。这样一来,数据存储的容量从理论上来说就没有上限了。因为在数据量增加时,只需要向集群中添加新的节点就可以增加整体容量了。数据分片带来的收益不仅体现在数据存储上,对于数据处理来说也可以大幅提升性能和吞吐量。这是因为在现有硬件技术条件下,硬盘读写速度与CPU处理能力不在一个数量级上,所以硬盘往往是数据处理的最大瓶颈。所以,即使多个CPU或多个线程并发处理数据,只要处理的数据在同一个硬盘上,数据处理的速度也不会得到显著提升。在使用数据分片技术后,数据会被散列到不同机器的硬盘上,数据的读写也就被分散到不同的硬盘上,这会显著提升数据处理的速度。大数据处理开源框架Hadoop中的HDFS和MapReduce正是基于这种思想,实现了在短时间内处理大量数据的功能。Elasticsearch作为一种全文检索引擎,显然也有必要考虑大数据的问题,所以Elasticsearch在设计之初就支持文档分片。因为Elasticsearch支持分片,所以它存储文档的容量在理论上没有上限。也正是从这个意义上,它才被很多文献归类为一种数据库,一种基于文档的NoSQL数据库。
分片的基础是要创建集群,Elasticsearch创建集群非常简单,只要集群中的节点在相互连接的网络中,并且具有相同的集群名称即可。Elasticsearch集群名称在配置文件中指定,在安装路径的config目录下找到elasticsearch.yml文件,其中的cluster.name就是配置集群名称的参数,默认名称为elasticsearch。所以在一个局域网内部创建Elasticsearch集群非常简单,只要在不同的机器上直接运行elasticsearch命令即可,甚至不需要修改配置文件。但在学习Elasticsearch时,如果想要在单台机器上创建一个集群,则需要在elasticsearch.yml文件中添加配置:“node.max_local_storage_nodes:2”。这将允许在单台机器启动多个Elasticsearch实例,否则在单机启动多个Elasticsearch实例时会报错。当启动好多个实例后,可通过_nodes接口查看节点情况。在Kibana控制台中执行“GET _nodes”命令,可在返回结果中看到如下信息:


示例2-14 查看节点信息
创建了Elasticsearch集群后,就需要确定索引分片的数量。分片一般会均匀地分散到集群的不同节点上,这就将存储和检索负载分散到集群的不同节点上。索引分片数量是在创建索引时通过number_of_shards参数设置的,例如在示例2-15中创建的test索引,分片数量为10:

示例2-15 索引分片数量
在索引定义好分片数量后,当有新的节点加入集群时,Elasticsearch会将分片均衡地散列到新的节点。例如,索引分片数量为2,当集群中只有一个节点A时,这些分片将全部位于节点A上;而当有节点B加入到集群中时,Elasticsearch会动态地将其中一个分片复制到节点B上。这也意味着如果索引的分片数量为1,那么这个索引未来将无法扩容。
2.4.2 路由
分片解决了海量文档存储的问题,但也引入了一个新的问题,那就是如何确定文档应该存储到哪个分片。在Elasticsearch中,确定文档存储在哪一个分片中的机制被称为路由(Routing)。计算文档路由的具体运算公式如下:

公式(2-1)中,shard_num为分片序号,hash为散列函数,_routing为路由参数,而num_primary_shards则是一个索引的主分片数量。这里之所以要使用主分片主要是为了区别副本分片,即在运算时并不包含副本分片数量。决定一份文档最终存储在哪一个分片上,最关键的参数就是_routing。在默认情况下,文档的_routing参数是文档ID,也就是前面介绍的元字段_id。如果想要修改路由参数,可以通过在添加或检索文档时设置routing参数。从负载均衡的角度来看,routing参数的值越分散,文档分散的越均匀。所以选择合适的routing无论对文档添加还是对文档检索,都会有非常显著的性能提升。示例2-16中使用routing参数设置了自定义规则:

示例2-16 自定义路由规则
为了提升检索效率,Elasticsearch在检索文档时,并不会将所有分片整合到一起做检索,而是先根据路由规则路由到具体分片,然后再在分片上根据检索条件查找文档。所以如果文档添加时的路由规则与文档检索时的路由规则不相同,在检索文档时就有可能被路由到错误的分片上,从而导致检索失败。一种比较常见的错误情况就是在文档添加时使用了自定义的路由规则,而在文档检索时忘记使用路由规则。为了避免这种情况的发生,可以在创建索引时将文档路由参数设置为强制要求,例如示例2-17中test索引的路由参数设置为强制:

示例2-17 路由参数
在路由参数设置为强制之后,对文档CRUD操作都必须要指定routing参数,否则在执行请求时将报错误。
由于路由选择对于索引性能的影响很大,往往选择的routing参数看似分散但却会路由到相同的分片。为了解决这个问题,Elasticsearch又引入了另一个分区参数来平衡路由运算,这就是routing_partition_size。引入这个参数后,路由运算公式变为

在添加了分区参数以后,分片编号同时由路由参数_routing和索引_id字段共同决定,这也就加大分片均衡的可能性。routing_partition_size参数必须大于1并且小于主分片数量,它的默认值1代表不会使用式(2-2)计算分片编号。当使用式(2-2)计算分片编号时,需要按示例2-17的方式将索引路由参数设置为强制,同时也不能再使用join类型字段构建父子关系。
2.4.3 容量规划
文档所在分片除了由routing参数决定以外,索引分片数量也是其中一个重要的决定因素。按公式(2-1)所示,在索引分片数量发生变化时,即使routing参数不变,最终的分片位置也会发生变化。如果在运行时索引分片数量发生了变化,为了保证文档存储和检索都能路由到正确的分片,已经存储到分片中的文档就必须按公式(2-1)做分片的重新路由。这个过程在Elasticsearch中叫重新索引(Reindex),显然当分片中已经存储了大量文档时,这将是一个非常耗费资源的过程。
为了避免重新索引导致的性能开销,Elasticsearch对索引分片数量做了一个严格的限制,这就是索引分片数量一旦在创建索引时确定后就不能再修改。这虽然解决了重新索引问题,但索引的存储容量也被分片数量、节点存储容量限制死了。节点存储容量决定了分片容量的上限,而索引总容量则是单个分片容量与分片数量的乘积。从性能角度考虑,分片太大显然会降低检索速度,所以单个分片的容量也不能过大,需要根据用户对检索性能的要求估算单个分片的容量上限。尽管最好的办法是将分片平均分配到不同的节点上,但如果节点存储容量大于单分片容量上限时,也可以考虑在一个节点上存储多个分片。尽管如此,这还是意味着索引存储容量存在上限,所以在创建索引时有必要对索引容量预先做好规划。如果用户在容量规划时低估了文档容量,那么索引将无法通过扩容来支持更多的文档。
索引容量规划主要是根据一些已知条件规划分片数量,这些已知条件主要包括文档存储整体容量和检索性能要求两个方面。通过检索性能要求可以估算出每个分片的最大容量,再使用整体容量除以分片大小就可以估算出分片数量。文档整体容量有时可能无法估算,比如说日志文件每天都在产生,数量只可能越来越多,不可能估算出上限来。这种情况下可以取一个固定的时间段,比如一天或是一个月,每隔这样一段时间就创建一个新的索引出来。由于固定时间段内的文档数量可估算,所以分片数量也就可以预先估算。本书第3章3.1.1节介绍的_rollover接口,以滚动别名的方式给出解决这种问题的一个备选方案。
事实上,无论容量规划得多科学依然不能完全避免文档实际存储量与索引容量不相符的情况。在这种情况下,惟一可行的办法就是创建新的索引,再将原索引中的文档存储到新的索引中。Elasticsearch针对这种情况提供了三个接口,即_split接口、_shrink接口和_reindex接口。这三个接口都没有修改原索引容量的能力,而是通过创建新索引的方法间接改变索引容量,但它们在性能上比手工创建索引和复制文档还是要好一些。有关这三个接口的详细介绍,请参考本书第3章3.3节。
2.4.4 副本
分片在解决了Elasticsearch文档存储容量问题的同时,也提升了文档处理的性能和吞吐量,但并不能解决容灾容错等高可用性问题。也就是说当集群中存储分片的节点发生故障,分片技术并不能保证文档存储、检索等服务依然可用,更不能保证分片中的数据不丢失。为了解决这个问题,Elasticsearch在存储上又引入了另一项称为副本(Replica)的技术。
副本是主分片的复制品,它与主分片的数据完全一致,能够在主分片故障时迅速恢复数据。所以主分片与副本分片永远不会在同一节点上,因为这样对于数据恢复没有任何意义。在默认情况下,Elasticsearch为每个索引都设置了1个副本分片,这意味着集群中应该至少有两个节点。如果集群中只有一个节点,副本分片就永远不会被创建,这时Elasticsearch就会将集群健康状态设置为黄色。索引的副本分片数量可以通过number_of_replicas参数设置,例如:

示例2-18 副本分片
在创建了示例2-18中的索引后,可使用“GET _cat/shards”命令查看集群中的分片情况。test索引的主分片为10个,每个主分片有2个副本分片,所以总共是30个分片。除了使用number_of_replicas设置固定的副本分片以外,还可以根据节点数量使用auto_expend_replicas参数设置动态扩展副本分片。这个参数的格式为“<from>-<to>”,如“1-10”代表副本分片数量是1~10个,而“0-all”中all代表所有节点数量。与主分片不同的,副本分片的数量在索引创建之后可以随时动态更改,具体请参考第3章3.1.2节。