当前位置: 首页 > 科技观察

如何选择搜索引擎?携程酒店订单Elasticsearch实战

时间:2023-03-17 12:48:43 科技观察

随着订单越来越多,单个数据库的读写能力开始捉襟见肘。在这种情况下,对数据库进行分片变得合乎逻辑。分片后写,按照分片的维度取模即可。但是应该如何处理多维查询呢?是一种逐条查询,然后在内存中聚合的方式,但是缺点也很明显。对于那些不返回分片的查询,不仅对应用服务器造成了额外的性能消耗,而且对宝贵的数据库资源造成了不必要的负担。至于查询性能,虽然可以通过开启线程和并发查询来提高,但是多线程编程和数据库返回结果的聚合增加了编程的复杂性和易错性。大家可以试着想象一下,分片后如何实现分页查询,就可以理解了。因此,我们选择在碎片化的数据库上构建实时索引,将查询移植到一个独立的WebService上,从而在保证性能的前提下提高业务应用查询的便利性。那么问题来了,如何构建高效的分片索引呢?索引技术的选择实时索引数据会包括用户ID、用户电话、用户地址等常用查询的列,实时复制一份并分发到独立的存储介质中。查询时,会先检查索引。如果索引中已经包含需要的列,则可以直接返回数据。如果需要额外的数据,可以根据分片维度进行二次查询。因为已经可以确定具体的分片,所以查询也会很高效。为什么不使用数据库索引数据库索引是表中选定列的数据备份。由于包含低级磁盘块地址或原始数据的直接链接的行,查询非常有效。优点是数据库自带的索引机制比较稳定、可靠、高效;缺点是随着查询场景的增加,索引量会相应增加。随着业务的发展,订单本身的属性已经达到上千种,高频查询的维度可以达到几十种,组合变形形式可以达到上百种。索引本身不是免费的。每次增删改查都会需要额外的写操作,占用额外的物理存储空间。索引越多,数据库索引维护的成本就越大。那么还有其他选择吗?开源搜索引擎的选择当时我们脑海中浮现的开源搜索引擎ApacheSolr和ElasticSearch。Solr是一个建立在Java类库Lucene之上的开源搜索平台,以更加友好的方式提供了Lucene的搜索能力。它已经存在了十年,是一个非常成熟的产品,提供分布式索引、复制分布、负载均衡查询以及自动故障转移和恢复。ElasticSearch也是一个构建在Lucene之上的分布式RESTful搜索引擎。通过RESTful接口和SchemaFeeJSON文档提供分布式全文搜索引擎。每个索引可以分为多个分片,每个分片可以有多个副本。两者的比较各有优缺点:在安装和配置方面,得益于更新的产品,ElasticSearch更轻便,更易于安装和使用。在搜索方面,ElasticSearch在分析查询方面的表现更胜一筹,抛开人人都有的全文搜索能力不谈。在分布方面,ElasticSearch支持一台服务器多分片,随着服务器的增加,自动将分片均衡到所有机器上。在社区和文档方面,由于资历深厚,Solr的积累比较多。根据GoogleTrends,ElasticSearch比Solr拥有更广泛的追随者。最终我们选择了ElasticSearch,因为它的轻量级、易用性以及更好的分发支持。整个安装包只有几十兆。复制分发的实现为了避免重新发明轮子,我们尝试找到现有的组件。由于数据库是SQLServer,没有找到合适的开源组件。SQLServer本身具有实时监控增删改查的功能,将更新的数据写入单独的表中。但是它不能自动向ElasticSearch写入数据,也没有提供相关的API来与指定的应用通信,所以我们开始尝试从应用层面来实现复制和分发。为什么数据访问层不用于复制分发首先引起我们注意的是数据访问层,它可以是一个突破口。每当应用对数据库进行增删改查时,都会实时写入一条数据到ElasticSearch中。但是考虑到下面这种情况,我们决定另辟蹊径:访问数据库的应用有几十个,有几十个开发人员在改数据访问层的代码。如果要实现数据层的复制和分发,必须目视扫描已有十几年的代码,然后再进行修改。开发成本高昂且容易出错。每次增删改查都要写ElasticSearch,意味着业务处理逻辑和复制、分发是强耦合的。ElasticSearch的不稳定或者其他相关因素会直接导致业务处理的不稳定。异步开线程写ElasticSearch?那么如何处理应用发布重启的场景呢?添加大量的异常处理和重试逻辑?然后将数十个应用程序引用为JAR?导致所有相关应用程序不稳定的小错误?实时数据库扫描乍一看是一个非常低效的解决方案,但结合以下实际场景后,是一个简单、稳定、高效的解决方案:零耦合。相关应用无需做任何改动,不会影响业务处理效率和稳定性。批量编写ElasticSearch。由于扫描的数据是批量的,所以可以批量写入ElasticSearch,防止ElasticSearch因为单次请求过多而频繁刷新缓存。存在大量的毫秒级并发写入。扫描数据库没有返回数据意味着额外的数据库性能消耗。我们场景的并发量和写入量都非常大,所以这个额外的消耗是可以接受的。数据不会被删除。扫描数据库无法扫描删除的记录,但是需要保留所有订单相关的记录,所以不存在删除数据的场景。提高ElasticSearch的写吞吐量因为数据库是实时复制和分布的,所以效率和并发要求会很高。下面是我们写ElasticSearch采用的一些优化方案:使用upsert代替类似MySQL的replaceinto的select+insert/update,避免多次请求,多次请求带来的性能消耗成倍增加。使用bulkrequest将多个请求合并为一个请求。ElasticSearch的工作机制对于批量请求有更好的性能。比如translog默认持久化在request级别,这样会大大减少写入硬盘的次数,提高写入效率。表现。至于一个batch的具体请求数量,这个跟服务器配置、索引结构、数据量都有关系。您可以使用动态配置在生产环境中进行调试。对于实时性要求不高的索引,设置index.refresh_interval为30秒(默认为1秒),可以让ElasticSearch每隔30秒创建一个新的Segment,减少后续的flush和merge压力。提前设置索引架构,去除不必要的功能。例如,到字符串类型的默认映射将同时创建关键字和文本索引。前者适用于精确匹配的短信,如邮寄地址、服务器名称、标签等。后者适用于查询文章的某一部分,如邮件内容、商品描述等,根据具体查询场景,选择其中一项,如下图:对于不关心的字段查询结果得分,可以设置norms:false。对于不会使用短语查询的字段,设置index_options:freqs。对于可以接受数据丢失的索引或者有灾备服务器的场景,设置index.translog.durability为async(默认为request)。将lucene的写入持久化到硬盘是一个比较耗时的操作,所以会有一个translog先持久化到硬盘,然后再批量写入lucene。异步写入translog意味着不需要每次请求都写入硬盘,可以提高写入的性能。数据初始化时效果更明显,后期可以使用bulkrequest进行实时写入,满足大部分场景。提升ElasticSearch读取性能为了提升查询性能,我们做了如下优化:写的时候指定查询场景的最大字段为_routing的值。由于ElasticSearch的分布式分区原则默认是对文档id进行哈希取模来确定分片,如果在查询场景中将maximum字段设置为_routing的值,就可以保证在查询该字段时,只要勾选一个shard即可返回结果。写入:校验:对于日期类型,在业务可以接受的范围内,尽量降低精度。只能包括年、月和日,不能包括小时、分钟和秒。当数据量很大时,这种优化的效果会特别明显。因为准确率越低,缓存效率越高,查询速度越快。同时,内存的复用也会提升ElasticSearch服务器的性能,降低CPU占用率,减少GC次数。系统监控的实现技术中心专门为业务部门开发了监控系统。它会周期性调用所有服务器的ElasticSearchCATAPI,将性能数据保存在单独的ElasticSearch服务器中,并提供网页供应用所有者监控数据。容灾的实现ElasticSearch本身就是分布式的。在创建索引时,我们根据未来几年的数据总量进行分片,保证单个分片的数据总量在一个健康的范围内。为了在写入速度和容灾之间找到平衡点,将备份节点设置为2。因此,数据分布在不同的服务器上。如果集群中的一台服务器宕机,另一台备份服务器将直接为其服务。同时,为了防止一个机房因为断网、断电等意外情况导致整个集群出现故障,我们特意在异地的另一个机房部署了一套完全相同的ElasticSearch集群。日常数据拷贝分发时,同时写入一份到灾备机房,以备不时之需。总结整个项目的发展是一个逐步演进的过程,在实施过程中遇到了很多问题。项目上线后,应用服务器CPU和内存下降明显,查询速度与之前基本持平,没有出现碎片。在这里分享遇到的问题和解决问题的思路,供大家参考。参考:ElasticSearch官方文档;https://en.wikipedia.org/wiki/Database_indexhttps://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%B4%A2%E5%BC%95https://logz.io/blog/solr-vs-elasticsearch/