SignalFx以监控现代化基础架构而闻名,对来自AWS、Docker或Cassandra之类的指标进行分析,对数据实时执行分析,并启用警报以减少干扰。了解如何工作,核心在于搜索。如果用户在寻找想要的数据时,花费了大量时间,那么仅在数据处理与存储/检索方面速度比其他人快,这是不够的。因此,为了匹配居于SignalFx核心地位的SignalFlow分析引擎的速度,我们选用了ElasticSearch来满足日常搜索需求。
在本文中,我们将会介绍相关心得:如何在扩展ElasticSearch同时,保持为公司内部与外部的商业用户提供服务,让他们可以在SignalFx中继续搜索。
为什么选择ElasticSearch
尽管我们选择了Cassandra作为时间序列数据库,查询时速度也很快,但用它是无法运行特别检索或全文检索的。我们在处理与指标相关的元数据时,需要处理结构化文本。能够在这些数据之上执行查询正是ElasticSearch的优势。
我们发现,ElasticSearch具有高可扩展性,提供很棒的API,工程师们上手也很容易。易于配置这一点让缺乏相关操作经验的开发人员也能上手。此外由于构建于Lucene之上,可靠性也很高。
从2015年3月SignalFx创建开始,我们的ElasticSearch部署从6个分片扩展到24个分片(加上副本),在72多台机器上存放了数以亿计的文件。很快就会再翻倍,以跟上我们扩张的步伐。
在SignalFx中ElasticSearch的使用方式
SignalFx运行方式与人们使用它的原因,有很大程度是因为SignalFx能够分析维度与其他元数据。用户用指标承载元数据,就可以对原始指标,还有其他针对指标用元数据所做的分析进行合并、筛选或分组。举个例子:按照服务与客户端类型分组,获取API调用响应第90百分位的延迟。所有的元数据都存储在ElasticSearch中,用它执行索引与搜索。
在SignalFx图表中的筛选
类似指标名称、面板标题或detector名之类的元数据与功能,正是SignalFx用户在系统中寻找内容或对象的方式。无论何时,用户在使用目录时,都是由ElasticSearch提供搜索服务的。
在SignalFx目录中寻找信息
除了指标之外,SignalFx还能执行分析/生产,并能规避类似警报、代码推送、配置运行之类的事件。所有这些事件都以ElasticSearch进行存储、索引与搜索。
在SignalFx中的事件
元数据经常变化,使其具有高可变性。造成我们使用ElasticSearch的方式非常资源集中化,并由于用户依赖使用SignalFx提供服务,使得可用性成为最重要的问题。由于理论上我们可以无限扩张,自然最终面临两个主要挑战:有多少分片,在零停机的状态下如何重分片。
ElasticSearch的扩展挑战1:分多少片合适?
在ElasticSearch中,规模的基础单位是分片。我们通过将数据切分成可在集群节点中分布的更小数据块,来进行分片扩展。
索引可存储大量数据,这些数据会超过单节点的硬件限制。举个例子,一个占据1TB硬盘空间、有10亿文件的单独索引,可能不适合存放在单节点的硬盘上,也许在以单节点提供搜索服务时速度过慢。
为了解决这个问题,ElasticSearch提供功能来细分索引,将其切分成名为分片的多个片段。在创建索引时,可以简单定义所需要的分片数量。每个分片本身就是一个功能齐全、独立的“索引”,可以托管在集群中的任何节点上。
挑战在于找出正确的分片数,因为每次做决定都是以索引为单位的。由于查询是发送到所有分片上的,这会影响到性能、存储与扩展。为了修改分片的数量,必须通过新的分片配置来创建新索引,并全部进行迁移。这可不是件小事。
当然,无法提前知道会有多少查询,也无法提前得知扩展的规模。我们可以说:分个1万片等以后慢慢扩展。不过这样是行不通的:所有数据分为1万片,因为每个查询需要发给所有分片,就需要1万个响应,并带来同等规模的I/O、线程、环境切换与master协调等问题。这就是所谓的天文数字分片的问题。
不幸的是,没有神奇的公式。为了确定开始时需要多少分片,需要考虑索引未来的规模——大小、查询卷与写入负载。Elastic团队推荐从一个分片开始,发送“实际”流量,然后查看在哪里中断。之后,再增加一个分片,重新测试,直到找出合适的数量。关键是要选择一个时间表。最终必须重新分片,唯一的问题就是在什么时候。因此可以根据一些关系到ElasticSearch使用的增长指标(像是用户数量),得出预测的索引大小,将实际数字与希望每个分片的存储使用情况与性能做对比。然后从中进行推断。
还有:每个节点要有超过一个的分片。因此,如果确定你的索引需要三个分片,并将其分布到三个节点上(每个节点一个),而这些节点耗尽资源的话,就无计可施了。在每个节点放置一个分片代表着如果某个节点资源耗尽,就别无选择只能重新分片了。之前我们有过这样的经验,不推荐这种做法:SignalFx在一开始就非常成功,初始分到了6个节点上的6个分片很快就需要重新分片。导致我们面临着下面的这个挑战:
ElasticSearch扩展挑战2:零停机重新分片
通常情况下这样重新分片:
1、创建有新数量分片的新索引;
2、通过批量操作来迁移文件:读取旧索引中的所有文件,对应(复制)到新索引中。
在没有变更的情况下,这是一个简单的操作。不过如果有一个为用户创建新数据并提供查询服务的实时系统,原始索引中的文件会发生变化。想要获得准确的数据截图是不可能的,根据负载,很可能永远无法跟上原始索引中变更的节奏。最终从旧索引切换到新索引必须是原子量级的,我们需要确保在重定向查询并向新索引写入时,旧索引中的内容换到新索引中仍旧可用,这样查询才能在不中断的情况下继续提供服务。
如果出错了会怎样?如何退回?或者如果新索引更慢呢?我们需要回退机制。这就是我们在使用ElasticSearch时的大量工作所在,带来零停机重新分片。 这种基本机制允许我们在重分片的过程中保持可用性,我们称之为索引分代(index generation)。我们将文件索引编入ElasticSearch时,会编写特殊字段,名为“索引分代”,一般情况下,对于索引中的所有文件都是相同的。我们一般将写入版本化。由于有很多状态需要存入,我们用了Zookeeper。
假设我们将初始(来源)索引标注为一代(gen-1),重分片为新的(目标)索引:
初始阶段
要开始重分片,我们用正确的分片数量创建新的索引(并进行变更映射)。新的索引有零个副本,设定刷新间隔为-1,因为我们还不需要查询该索引。
1、我们一般加速读取一系列新节点。这会耗费额外成本,不过据发现,这是最安全的选择。
2、如果来源索引的负载很低,并且会持续如此的话,这样完成全部工作是有可能的——不过并不推荐。
批量导入阶段
增量到第二代gen-2,确认所有写入索引的组件已经更新(我们使用Zookeeper来协调这个问题)。从这里开始,所有新文件或变更后的文件都是gen-2。这样我们就能够界定gen-1或更早的文件。
扫描来源索引,并批量导入gen-1(或更早的)文件到目标索引中。我们使用了滚动(scrolling),这是从索引中获得大量文件的最有效方式。
1、要记得scrolling是不允许合并从filesystem中取回的片段的,因为这些内容仍在使用中。
2、为了避免这一情况,我们依赖着索引中每个可用文件的bucket数量。在文件创建时就分配了bucket数量,是ID模数总bucket数(我们使用64k)的哈希。这样我们就能随机分割文件集了,并让bucket均匀分布(我们将其作为滚动的参数)——实际上防止了立即在整个索引中造成滚动。
3、这也提供了出错时滚回操作的办法。由于按我们的规模,批量导入需要数日,偶尔会出现暂停、恢复或回滚之类的问题。
4、现在我们可以停下来,无需回到索引最开始就能开启滚动了,因为我们按bucket来导入bucket,并追踪所在的bucket(在Zookeeper中)。这使得我们在迁移与因某种原因而暂停迁移时,可以自动处理崩溃问题。
5、我们还关闭了目标索引上的索引刷新,使得迁移速度更快,因为这样就无需响应任何请求了。
双重写入与清理阶段
一旦所有的gen-1文件在目标索引中获得确认,我们在Zookeeper中启动开关,指引所有写入操作在向源索引写入的同时,开始向目标索引执行写入。
增量到第三代gen-3。
1、现在我们知道,从这里开始所有的变更都属于gen-3;
2、所有gen-3变更同时向来源与目标索引执行写入;
3、属于gen-1的内容(或更早的)都已经迁移到目标索引中了;
4、只有gen-2文件需要解决来源与目标索引之间的协调问题;
5、gen-2文件是一个有界集(也就是说:其数量会保持不便或减少,但不会增加);
在来源索引中查询gen-2文件,写入到目标索引中,此时它们会成为gen-3文件。
1、我们还在执行双重写入,因此会同时写入两个索引中;在来源索引中查询gen-2文件,写入到目标索引中,此时它们会成为gen-3文件;
2、这一过程继续进行,直到来源索引中没有gen-2文件为止。
在最后一步完成后,我们知道目标索引已更至最新。
结束阶段
变更索引昵称,这是一个原子量级的操作。现在所有查询都由目标索引提供服务。
1、关闭双重写入。
2、关闭来源索引,释放资源。
3、将来源索引保留整一天,确保不时之需。
心得体会
在扩展ElasticSearch时,关于分片的问题,我们面临着两种截然不同的挑战:需要多少分片,如何在零停机的状态下重分片。不过只需几步,这些问题都解决了。
1、通过基线增量,从一个单分片开始,然后逐渐增进。了解个人案例中的性能与存储平衡。
2、每个节点需要不止一个分片,这样稍后就能通过增加节点来扩容。
3、将更长的进程切分为更小的部分:(1)对于滚动,我们通过bucket机制来实现(上面有具体过程)。(2)对于整体重分片进程,我们通过索引分代机制来解决(如上所述)。