前言
我曾经面试安踏的技术岗,当时面试官问了我一个问题:如果你想使用某个新技术但是领导不愿意,你怎么办?
对于该问题我相信大家就算没有面试被问到过,现实工作中同事之间的合作也会遇到。
因此从我的角度重新去回答这个问题,有以下几点:
1.师出有名,在软件工程里是针对问题场景提供解决方案的,如果脱离的实际问题(需求)去做技术选型,无疑是耍流氓。大家可以回顾身边的“架构师”、“技术Leader”是不是拍拍脑袋做决定,问他们为什么这么做,可能连个冠冕堂皇的理由都给不出。
2.信任度,只有基于上面的条件,你才有理由建议引入新技术。领导愿不愿意引入新技术有很多原因:领导不了解这技术、领导偏保守、领导不是做技术的等。那么我认为这几种都是信任度,这种信任度分人和事,人就是引入技术的提出者,事就是提出引入的技术。
3.尽人事,任何问题只是单纯解决事都是简单的,以我以往的做法,把基本资料收集全并以通俗易懂的方式归纳与讲解,最好能提供一些能量化的数据,这样更加有说服力。知识普及OK后,就可以尝试写方案与做个Demo,方案最好可以提供多个,可以分短期收益与长期收益的。完成上面几点可以说已经尽人事了,如果领导还不答应那么的确有他的顾虑,就算无法落实,到目前为止的收获也不错。
4.复杂的是人,任何人都无法时刻站在理智与客观的角度去看待问题,事是由人去办的,所以同一件事由不同的人说出来的效果也不一样。因此得学会向上管理、保持与同事之间合作融洽度,尽早的建立合作信任。本篇文章更多叙述的事,因此人方面不过多深究,有兴趣的我可以介绍一本书《知行 技术人的管理之路》。
本篇我的实践做法与上述一样,除了4无法体现。那么下文我分了4大模块:业务背景介绍、基础概念讲解、方案的选用与技术细节。
该篇文章不包含代码有8000多千字,花了我3天时间写,可能需要您花10分钟慢慢阅读,我承诺大家正文里面细节满满。
曾有朋友建议我拆开来写,但是我的习惯还是希望以一篇文章,这样更加系统化的展示给大家。当然大家有什么建议也可以在下方留言给我。
部分源码,我放到了https://github.com/SkyChenSky/Sikiro 的Sikiro.ES.Api里
背景
本公司多年以来用SQL Server作为主存储,随着多年的业务发展,已经到了数千万级的数据量。
而部分非核心业务原本应该超亿的量级了,但是因为从物理表的设计优化上进行了数据压缩,导致维持在一个比较稳定的数量。压缩数据虽然能减少存储量,优化提供一定的性能,但是同时带来的损失了业务可扩展性。举个例子:我们平台某个用户拥有最后访问作品记录和总的阅读时长,但是没有某个用户的阅读明细,那么这样的设计就会导致后续新增一个抽奖业务,需要在某个时间段内阅读了多长时间或者章节数量的作品,才能参加与抽奖;或者运营想通过阅读记录统计或者分析出,用户的爱好和受欢迎的作品。现有的设计对以上两种业务情况都是无法满足的。
此外我们平台还有作品搜索功能,like ‘%搜索%’查询是不走索引的而走全表扫描,一张表42W全表扫描,数据库服务器配置可以的情况下还是可以的,但是存在并发请求时候,资源消耗就特别厉害了,特别是在偶尔被爬虫爬取数据。(我们平台API的并发峰值能达到8w/s,每天的接口在淡季请求次数达到了1亿1千万)
关系型数据库拥有ACID特性,能通过金融级的事务达成数据的一致性,然而它却没有横向扩展性,只要在海量数据场景下,单实例,无论怎么在关系型数据库做优化,都是只是治标。而NoSQL的出现很好的弥补了关系型数据库的短板,在马丁福勒所著的《NoSQL精粹》对NoSQL进行了分类:文档型、图形、列式,键值,从我的角度其实可以把搜索引擎纳入NoSQL范畴,因为它的确满足的NoSQL的4大特性:易扩展、大数据量高性能、灵活的数据模型、高可用。我看过一些同行的见解,把Elasticsearch归为文档型NoSQL,我个人是没有给他下过于明确的定义,这个上面说法大家见仁见智。
MongoDB作为文档型数据库也属于我的技术选型范围,它的读写性能高且平衡、数据分片与横向扩展等都非常适合我们平台部分场景,最后我还是选择Elasticsearch。原因有三:
- 我们运维相比于MongoDB更熟悉Elasticsearch。
- 我们接下来有一些统计报表类的需求,Elastic Stack的各种工具能很好满足我们的需求。
- 我们目前着手处理的场景以非实时、纯读为主的业务,Elasticsearch近实时搜索已经能满足我们。
Elasticsearch优缺点
百度百科 :
Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch由Java语言开发的,是一种流行的企业级搜索引擎。Elasticsearch用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。官方客户端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和许多其他语言中都是可用的。
对于满足当下的业务需求和未来支持海量数据的搜索,我选择了Elasticsearch,其实原因主要以下几点:
优点 | 描述 |
横向可扩展性 | 可单机、可集群,横向扩展非常简单方便,自动整理数据分片 |
快 | 索引被分为多个分片(Shard),利用多台服务器,使用了分而治之的思想提升处理效率 |
支持搜索多样化 | 与传统关系型数据库相比,ES提供了全文检索、同义词处理、相关度排名、复杂数据分析、海量数据的近实时处理等功能 |
高可用 | 提供副本(Replica)机制,一个分片可以设置多个副本,假如某服务器宕机后,集群仍能正常工作。 |
开箱即用 | 简易的运维部署,提供基于Restful API,多种语言的SDK |
那么我个人认为Elasticsearch比较大的缺点只有 吃内存,具体原因可以看下文内存读取部分。
Elasticsearch为什么快?
我个人对于Elasticsearch快的原因主要总结三点:
- 内存读取
- 多种索引
- 倒排索引
- doc values
- 集群分片
内存读取
Elasticsearch是基于Lucene, 而Lucene被设计为可以利用操作系统底层机制来缓存内存数据结构,换句话说Elasticsearch是依赖于操作系统底层的 Filesystem Cache,查询时,操作系统会将磁盘文件里的数据自动缓存到 Filesystem Cache 里面去,因此要求Elasticsearch性能足够高,那么就需要服务器的提供的足够内存给Filesystem Cache 覆盖存储的数据。
上一段最后一句话什么意思呢?假如:Elasticsearch 节点有 3 台服务器各64G内存,3台总内存就是 64 * 3 = 192G。每台机器给 Elasticsearch jvm heap 是 32G,那么每服务器留给 Filesystem Cache 的就是 32G(50%),而集群里的 Filesystem Cache 的就是 32 * 3 = 96G 内存。此时,在 3 台Elasticsearch服务器共占用了 1T 的磁盘容量,那么每台机器的数据量约等于 341G,意味着每台服务器只有大概10分之1数据是缓存在内存的,其余都得走硬盘。
说到这里大家未必会有一个直观得认识,因此我从《大型网站技术架构:核心原理与案例分析》第36页抠了一张表格下来:
操作 | 响应时间 |
打开一个网站 | 几秒 |
在数据库中查询一条记录(有索引) | 十几毫秒 |
机械磁盘一次寻址定位 | 4毫秒 |
从机械磁盘顺序读取1MB数据 | 2毫秒 |
从SSD磁盘顺序读取1MB数据 | 0.3毫秒 |
从远程分布式缓存Redis读取一个数据 | 0.5毫秒 |
从内存中读取1MB数据 | 十几微秒 |
Java程序本地方法调用 | 几微秒 |
网络传输2KB数据 | 1微秒 |
从上图加粗项看出,内存读取性能是机械磁盘的200倍,是SSD磁盘约等于30倍,假如读一次Elasticsearch走内存场景下耗时20毫秒,那么走机械硬盘就得4秒,走SSD磁盘可能约等于0.6秒。讲到这里我相信大家对是否走内存的性能差异有一个直观的认识。
对于Elasticsearch有很多种索引类型,但是我认为核心主要是倒排索引和doc values
倒排索引
Lucene将写入索引的所有信息组织为倒排索引(inverted index)的结构形式。倒排索引是一种将分词映射到文档的数据结构,可以认为倒排索引是面向分词的而不是面向文档的。
假设在测试环境的Elasticsearch存放了有以下三个文档:
- Elasticsearch Server(文档1)
- Masterring Elasticsearch(文档2)
- Apache Solr 4 Cookbook(文档3)
以上文档索引建好后,简略显示如下:
词项 | 数量 | 文档 |
4 | 1 | <3> |
Apache | 1 | <3> |
Cookbook | 1 | <3> |
Elasticsearch | 2 | <1><2> |
Mastering | 1 | <1> |
Server | 1 | <1> |
Solr | 1 | <3> |
如上表格所示,每个词项指向该词项所出现过的文档位置,这种索引结构允许快速、有效的搜索出数据。
doc values
对于分组、聚合、排序等某些功能来说,倒排索引的方式并不是最佳选择,这类功能操作的是文档而不是词项,这个时候就得把倒排索引逆转过来成正排索引,这么做会有两个缺点:
- 构建时间长
- 内存占用大,易OutOfMemory,且影响垃圾回收
Lucene 4.0之后版本引入了doc values和额外的数据结构来解决上面得问题,目前有
五种类型的doc values:NUMERIC、BINARY、SORTED、SORTED_SET、SORTED_NUMERIC,针对每种类型Lucene都有特定的压缩方法。
doc values是列式存储的正排索引,通过docID可以快速读取到该doc的特定字段的值,列式存储存储对于聚合计算有非常高的性能。
集群分片
Elasticsearch可以简单、快速利用多节点服务器形成集群,以此分摊服务器的执行压力。
此外数据可以进行分片存储,搜索时并发到不同服务器上的主分片进行搜索。
这里可以简单讲述下Elasticsearch查询原理,Elasticsearch的查询分两个阶段:分散阶段与合并阶段。
任意一个Elasticsearch节点都可以接受客户端的请求。接受到请求后,就是分散阶段,并行发送子查询给其他节点;
然后是合并阶段,则从众多分片中收集返回结果,然后对他们进行合并、排序、取长等后续操作。最终将结果返回给客户端。
机制如下图:
分页深度陷阱
基于以上查询的原理,扩展一个分页深度的问题。
现需要查页长为10、第100页的数据,实际上是会把每个 Shard 上存储的前 1000(10*100) 条数据都查到一个协调节点上。如果有 5 个 Shard,那么就有 5000 条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。也就是实际上查的数据总量为pageSize*pageIndex*shard,页数越深则查询的越慢。因此ElasticSearch也会有要求,每次查询出来的数据总数不会返回超过10000条。
那么从业务上尽可能跟产品沟通避免分页跳转,使用滚动加载。而Elasticsearch使用的相关技术是search_after、scroll_id。