Es索引原理
Es 集群构成
Es 集群由多个节点(Node)构成,Node可以有不同的类型,通过以下配置可以产生四种不同类型的 Node:
1 | conf/elasticsearch.yml: |
四种不同类型的Node是一个node.master和node.data的true/false的两两组合。当然还有其他类型的Node,比如IngestNode(用于数据预处理等),不在本文讨论范围内。
当node.master 为 true 时,表示这个 node 是一个 master 的候选节点,可以参与选举,类似于 MasterCandidate。Es 正常工作时只能有一个 master(leader),多于 1 个时发生脑裂。
当 node.data 为 true 时,这个节点作为一个数据节点,会存储分配在该 node 上的 shard 数据,并负责这些 shard 的写入、查询等。
此外,集群内 node 都可以执行任何请求,负责将请求转发给对应的 node 进行处理。当 node.master 和 node.data 都是 false 时,这个节点可以作为一个类似 proxy 的节点,接受请求并进行转发、结果聚合等。
Es 选主
ZenDiscovery是ES自己实现的一套用于节点发现和选主等功能的模块,没有依赖Zookeeper等工具
在本节点到每个hosts中的节点建立一条边,当整个集群所有的node形成一个联通图时,所有节点都可以知道集群中有哪些节点,不会形成孤岛。
Es索引原理
倒排序索引的不变性
es的json文档中,每个被索引的字段都有自己的倒排索引,倒排索引会保存每个词项出现过的文档总数,在对应文档中一个具体词项出现的总次数,词项在文档中的顺序,每个文档的长度,所有文档的平均长度等。这些统计信息用于计算哪些词比其它词更重要,哪些文档比其它文档更重要。
倒排序索引被写入磁盘后是不可变的,不可变性可以带来以下好处:
- 不需要锁,不用担心多进程同时修改数据的问题
- 一旦索引被读入内核的文件系统缓存便会留在那里,由于不变性,大部分请求会直接请求内存而不会命中磁盘,提升性能
- 其它缓存(如filter缓存)在索引的生命周期内始终有效,不需要在每次数据改变时被重建
- 写入单个大的倒排序索引允许数据被压缩,减少磁盘I/O和需要被缓存到内存的使用量
倒排序索引的更新
es基于lucene,lucene引入按段搜索的概念,每一个段本身都是一个倒排索引,索引在lucene中除了表示所有段的集合外,还增加了提交点的概念——提交点是一个列出所有已知段的文件。
新的文档首先被添加到内存索引缓存中,然后写入到一个基于磁盘的段。当一个查询被触发,所有已知的段按顺序被查询。词项统计会对所有段的结果进行聚合,以保证每个词和每个文档的管理都被准确计算。
每个提交点会包含一个.del 文件,文件中会列出这些被删除文档的段信息。当一个文档被删除时,它实际只是在.del文件中被标记删除。一个被标记删除的文档仍然可以被查询匹配到,但它会在最终结果被返回前从结果集中移除。
文档更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的旧版本文档在结果集返回前被移除了。
段合并
Es基本用法
字段类型
简单字段类型:text、keyword、date、long、double、boolean、ip
嵌套类型:object、nested
特殊类型:geo_point,geo_shape 或 completion
动态映射
包括动态字段映射、动态模版和索引模版
如果文档中有新的字段(值不为null或空数组),es会把这个字段添加到mapping中
JSON datatype | Elasticsearch datatype |
---|---|
null | No field is added. |
true or false | boolean field |
floating point number | float field |
integer | long field |
object | object field |
array | Depends on the first non-null value in the array. |
string | Either a date field (if the value passes date detection), a double or longfield (if the value passes numeric detection) or a text field, with a keywordsub-field. |
索引模版只在创建索引时生效,修改模版不会对已创建的索引生效。使用创建索引API创建索引时,API指定的同名字段优先于模版定义的字段。
Lucene不理解内部对象,Lucene文档是由一组键值对列表组成的,为了能让es有效地索引内部类,它把文档转化成类似如下结构的文档
Es搜索过程
查询原理
Term 查询
FST(Finite State Transducer)
从 Lucene4 开始,为了实现 rangequery 或前缀、后缀等复杂的查询语句,Lucene 使用 FST 数据结构来存储 term 字典。
倒排链的存储
为了快速查找 docId,Lucene 采用 SkipList 数据结构,它具有以下特征:
A. 元素是排序的,lucene 按 docid 从小到大排序
B. 跳跃有固定间隔,在建立 skiplist 时指定
C. skiplist 的层次,指整个 skiplist 有几层
倒排链合并
如果某个链很短,会大幅减少比对次数,并且由于 SkipList 结构的存在,在某个倒排中定位某个 docid 的速度会比较快不需要一个个遍历。可以很快的返回最终的结果。从倒排的定位,查询,合并整个流程组成了 lucene 的查询过程,和传统数据库的索引相比,lucene 合并过程中的优化减少了读取数据的 IO,倒排合并的灵活性也解决了传统索引较难支持多条件查询的问题。
倒排链合并
如果是数值类型的范围查询,如整形、浮点型,采用 FST term 查询,潜在的 term 会非常多,查询效率很低。为了支持高效的多维数值查询,lucene 引入 BKDTree。BKDTree 基于 KDTree,对数据按照维度划分建立一棵二叉树确保树两边节点数目平衡。在一维场景下,KDTree 退化成二叉树,在二叉树中查询叶子节点对应倒排链需要 logn 时间。
在多维场景下,kdtree 建立流程如下:
A.确定切分维度,选取顺序是数据分散越开的维度,越先切分
B.切分点选择维度最中间的点
C.递归进行 A、B,可以设置阈值,点的数据少于多少后就不再切分,直到所有的点都切分好停止
BKD 是多个 KDTree 持续 merge 最终合并成一个
写入流程
Es 的任意节点都可以作为协调节点(coordinating node)接受请求,通过_routing字段找到对应的 primary shard,并将请求转发给 primary shard, primary shard 完成写入后,将写入并发发送给 replica,replica 执行写入操作后返回给 primary shard,primary shard 再讲请求返回给协调节点。
查询流程
Es 通过分区实现分布式,数据写入时,根据 routing 规则将数据写入某个 Shard,这样能将海量数据分布在多个 Shard和多台机器上。
在查询时,数据可能分布在 index 的所有 Shard 中,所以需要查询所有 Shard,同一个 Shard 的 Primary 和 Replica 选择一个就可以,查询请求分发给所有 Shard,每个 Shard 中都是一个独立的查询引擎,如果需要返回 TopN 的结果,每个 Shard 都会查询返回 TopN,然后在 Client Node 中通过优先队列二次排序,找出 top N 结果返回给用户。
一般搜索系统都是两阶段查询,第一阶段查询到匹配的 DocId,第二阶段再查询 DocId 对应的完整文档,这种在 Es 称为 query_then_fetch,还有一种是一阶段查询返回完整 Doc,es 称为 query_and_fetch,一般第二种适用于只需要查询一个 Shard 的请求。
除了一阶段,两阶段外,还有一种三阶段查询的情况。搜索里面有一种算分逻辑是根据TF(Term Frequency)和DF(Document Frequency)计算基础分,但是Elasticsearch中查询的时候,是在每个Shard中独立查询的,每个Shard中的TF和DF也是独立的,虽然在写入的时候通过_routing保证Doc分布均匀,但是没法保证TF和DF均匀,那么就有会导致局部的TF和DF不准的情况出现,这个时候基于TF、DF的算分就不准。为了解决这个问题,Elasticsearch中引入了DFS查询,比如DFS_query_then_fetch,会先收集所有Shard中的TF和DF值,然后将这些值带入请求中,再次执行query_then_fetch,这样算分的时候TF和DF就是准确的,类似的有DFS_query_and_fetch。这种查询的优势是算分更加精准,但是效率会变差。另一种选择是用BM25代替TF/DF模型。
查询阶段
分布式系统
分布式系统是一个硬件或软件组成分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。
分布式系统是一群独立计算机集合共同对外提供服务,对于系统的用户来说,像一台计算机在提供服务一样。分布式意味着可以采用更多的普通计算机(相对于昂贵的大型机)组成分布式集群对外提供服务。计算机越多,CPU、内存、存储资源等也就越多,能够处理的并发访问量也就越大。
分布式系统的各个主机之间通信和协调主要通过网络进行,计算机在空间上几乎没有任何限制,这些计算机可能被放在不同的机柜上,也可能被部署在不同的机房中,还可能在不同的城市中,对于大型的网站甚至可能分布在不同的国家和地区。
常用的分布式方案:
1. 分布式应用和服务
将应用和服务进行分层或分割,并进行分布式部署。这样不仅可以突破单机性能瓶颈,提高并发访问能力,还能复用分布式部署的模块,使业务易于扩展。
2. 分布式静态资源
对JS、CSS、图片等资源进行分布式部署可以减轻服务器的压力,提高访问速度
3. 分布式数据和存储
随着互联网应用需要存储越来越多的数据,单台机器往往无法提供足够的存储空间,需要对这些数据进行分布式存储。
Linux进程管理
我们拥有操作系统就是为了运行用户程序,因此,进程管理是操作系统的心脏,Linux也不例外。
进程
进程就是处于执行期的程序(目标代码存放在某种介质上)。但进程并不仅仅局限于一段可执行代码(Unix称其为代码段,text section),通常进程还包含其它资源,例如打开的文件、挂起的信号、内核内部的数据、处理器状态、一个或多个具有内存映射的内存地址空间及一个或多个执行线程(Thread of execution)等。实际上,进程是处于执行期程序以及相关资源的总称。可能存在两个或多个不同进程执行的是同一个程序。并且多个并存的进程还可以共享许多诸如打开的文件、地址空间之类的资源。
进程从创建它的时刻开始存活,在Linux系统中,通常通过fork()系统调用实现,该系统调用通过复制一个现有的进程来创建一个全新的进程。调用fork()的进程称为父进程,新产生的进程称为子进程。系统调用结束后,在返回点这个相同的位置,父进程恢复执行,子进程开始执行。fork()系统调用从内核返回两次,一次回到父进程,另一次回到新产生的子进程。
线程
执行线程,简称线程(Thread),是在进程中活动的对象。每个线程都有一个独立的程序计数器、进程栈和一组进程计数器。内核调度的对象是线程而不是进程。在传统的Unix系统中,一个进程只包含一个线程,但是对于现代操作系统来说,一个进程包含多个线程是非常常见的。Linux对进程和线程并不特别区分,线程只不过是一种特殊的进程。
在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。虽然实际上可能是许多进程正在分享一个处理器,但虚拟处理器给进程一种假象,让这些进程觉得自己在独享处理器。而虚拟内存让进程在分配和管理内存时觉得自己拥有整个系统的所有内存资源。同一进程的线程之间可以共享虚拟内存,但每个都拥有各自独立的虚拟处理器。
参考文档
《Linux内核设计与实现(第三版)》
我为什么要写心理相关的文章?
可能是因为自己心理经常经历痛苦、挣扎、抑郁,我一直对心理学保持关注。还记得大一那会儿,我一直担心自己得了抑郁症,从百度百科上面吧抑郁症词条以及和它相关的所有内容都打印出来,二三十页的A4打印纸打印了好几本。人处于痛苦中时,会本能地寻求解脱之道,心理学从那时起就是我的解药。一开始我只会去了解与自身密切相关的心理学知识,例如抑郁症的起因、症状、治疗方法等。但是慢慢的,我发现心理这件事越来越有意思了。人们的一言一行,一举一动,或喜极而泣,或怒不可遏……都是先在内心产生一定的化学反应,然后才有意或无意地表达出来的。至于一个人给其他人留下的或讨人欢喜或令人憎恶的深刻印象,则是长久以来某种心理活动频繁发生后形成的固化模式的自然表现,很多模式甚至可以追溯到童年和婴儿时期。
后来,我陆陆续续地从大学的心理学选修课、心理学讲座、TED等渠道零星地获取心理学的知识。先后读了《苏菲的世界》、《自卑与超越》、《走出抑郁症-一个抑郁症患者的成功自救》、《当下的力量》、《人性的优点》、《人性的弱点》。虽然不能算专业系统地学习,不过也从对心理学一无所知到现在乐在其中,从把心理学当做救命稻草到现在把心理学当做一种兴趣,在好奇心、求知欲的驱动下去去探索心理这个丰富多彩的世界,如果在旅程中刚好能解决自己的困惑和问题,就再好不过了。
机缘巧合,我在13年左右发现心理FM,这个平台上有很多对心理感兴趣的小伙伴。好听的主播们会不定期地发布一些心理相关的节目,有名家名篇、有美文共赏、有答疑解惑。每当我情绪低落、痛苦迷茫,我都会打开心理FM。听到“世界和我爱着你”悲伤就治愈了一半。
回顾完我和心理学、心理FM的渊源,并没有正面回答为什么我想写一些心理相关的文章。
因为最近我越发觉得,“眼过千遍不如手过一遍”是非常有道理的。我是一个思维很发散的人,有时候甚至任由思绪信马由缰,脑海里经常有千头万绪、万马奔腾。有时候冒出一个Idea觉得前途远大,“卧槽,太牛X了,我怎么这么聪明”,有时候又觉得一件事困难重重,万难实现。但是如果减慢节奏,把大脑里的想法理一理,与现实结合起来。在纸上写写画画,或者用xmind画一幅脑图,就会发现大脑中的东西夹杂了太多个人的主观情绪和意愿。当现实的光线照进来,原来的东西会面目全非。
脱离现实太远的东西经常是病态的,写下来、画出来是想法联系现实的一种非常有效的方法。能在现实场中理清楚、说明白的就一定不会太过于臆想。
还有一种情况,在看书、看文章的时候,经常会觉得某个观点或某句话很有启发性,但是如果不记录下来,反复揣摩,并应用到生活和工作中,这些好东西也就仅限于给人带来启发性的感觉而已,并没有变成自己的东西。有一张图说明了人通过不同的渠道处理信息能够留存多少。
慢一点,把接触到的有触动、有启发的东西梳理一下,记录下来,并在实际生活工作中实践才能真正变成自己的东西。研究生毕业找工作的时候,身边很多人(包括我)看完《Thinking in Java》、《深入理解java虚拟机》、《Java并发编程实践》就以为掌握了其中的知识点,但在面试的时候面试官随便抽一个点出来问,都答不上来、讲不清楚。感觉自己看到过,但也只是看过,并没有真正吸收其中的精华,变成自己的东西。我们实验室一个小伙伴收割了无数Offer,几乎是手到擒来。后来告诉我诀窍:他看《Java并发编程实战》的时候,每看完一个知识点就就用自己的话重新表述一遍写下来。面试官问到的时候他因为自己揣摩过,能用自己的话把同样的意思表达清楚,自然就对答如流了。
总而言之,写心理相关的文章,首先是因为我确实认为心理学有意思、有价值;其次,通过写文章减慢大脑的运行节奏,让大脑中的想法与现实结合起来,不至于变成臆想;最后,只有能把想法表达出来、有条理地将给其他人听才算真正理解了,只有在现实中实践了才能真正发挥作用。
写这些文章(包括其它技术文章)的目的只是为了帮助自己更好地把握节奏,更好地理解和实践有价值的东西。可能有些内容只是别人内容的另一种表达甚至是同一种表达(当然我会尽量把参考资料列在文章的结尾),有些内容可能只是我自己的碎碎念。不过这不重要,在写下这些文章的过程中,我已经收获得足够多了。
告别内耗,目标合理,轻松生活
“一切皆有可能”、”世上无难事,只怕有心人”、”没有什么不可能“这些励志名言用来鼓励人们不要自我设限,努力拼搏自然是好的,不过人的能力终归是有极限的。一味地给自己设定超出自己能力范围的目标,就开启了失败模式。人一旦长期处于挫败模式,整个信心水平都会下降,严重的甚至会抑郁、自杀。例如Uber工程师饮弹自杀身亡!遗孀痛心疾首怒斥Uber高压文化中,Joseph的父亲说:”如果你一直逼着一个精力充沛的人去做不现实的任务,那你等于是‘开启了他们的失败模式’“。
本来世上不如意事十之八九,老是逼迫自己去做不可能完成的事,那基本上就没什么事是如意的了。要是你还是一个自我要求很高、自尊心很强的人,那么在遭遇到失败的时候就会非常自责,会骂自己、讨厌自己、嫌弃自己。这种对自己的排斥会消耗大量的精力,会觉得越来越累,做起事情越来越力不从心,结果越来越差,随之更加排斥自己,形成恶性循环。
这就是内耗,内耗让你越来越累,越来越不快乐。
人最大的内耗就是对自己的排斥,失败让人痛苦,比失败更痛苦的是对失败的排斥。
内耗越严重,精力消耗的越快,情绪也更容易变得糟糕。
对自己苛刻的人往往对别人也很苛刻,总是期待别人完美无瑕,一旦其他人的言行举止不入自己的法眼,就会在内心抱怨、鄙视,而别人的优点他往往发现不了或者选择性忽视。
健康的状态是,成功的时候尽情享受,失败的时候坦然接受;有精力的时候努力工作,没精力的时候放松休息;以欣赏的眼光待人待己,以宽容的心胸接纳不完美。
当你真正能够接纳自己、不排斥自己、不跟自己较劲的时候,就能够以更加平和更加客观的眼光来看待自己、接纳自己,就开始活在真实的自己里,而不是妄想中时时、事事都成功的虚假的自己里。如此一来,对外的精力就最大化了,做事情反而会越来越好。
JavaScript中变量声明的几种方式
ECMAScript 6(以下简称ES6)是JavaScript语言的下一代标准。因为当前版本的ES6是在2015年发布的,所以又称ECMAScript 2015。也就是说,ES6就是ES2015。
Babel是一个广泛使用的ES6转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。大家可以选择自己习惯的工具来使用使用Babel,具体过程可直接在Babel官网查看。
ES6中出现了两种变量声明方式:let和const,加上已有的直接赋值和var声明,共有四种变量声明方式。
直接赋值
1 | x = 1; |
未声明的变量总是全局变量,在执行到该变量所在的语句之前,变量是不存在的。如果直接引用未声明、未赋值的变量,将抛出ReferenceError,之后的代码无法执行。未声明的变量在执行上下文中是可配置的(例如可被删除)。
var声明变量
1 | var x = 1; |
与不赋值直接使用的方式相比,通过var声明的变量只在声明的上下文中生效(例如,函数内部声明的var变量在函数外部不可见)。并且,在所有代码执行之前var变量就被创建了,所以一般把变量声明在全局或函数的顶部,以便于区分哪些变量是函数内部的,哪些变量是全局的。var变量在上下文中是不可配置的,无法被删除(删除时抛出TypeError或者静默失败)。
基于以上区别,在ES5之前的代码中,无论是全局变量还是局部变量,都推荐使用var声明变量。