Skip to main content

4 posts tagged with "技术分享"

View All Tags

· 10 min read

“极简主义以简单到极致为追求,是一种设计风格,感官上简约整洁,整体品味和思想上更为优雅。”

「 如无必要,勿增实体 」是极简主义者推崇的生活信条。摒弃多余的东西,简化繁琐的步骤,排除没必要的干扰,认清自己到底想要什么,最大限度把环境变得简单。 只有这样,我们才能在混乱而变化的环境下,拥有更多的时间和精力,找到值得珍视的东西,达到更加自由的生活状态,彻底实现「不为外物所累」。

起源篇「 写入Serverless 」

Nasu Elasticsearch Serverless 起源于多年前的一个想法,那时我负责阿里云ES的内核研发Leader,这个团队每年都有一个硬性KPI "引擎优化"。比如索引写入性能提升100%,存储成本降低100% ... 小伙伴们每天的工作就是研究引擎源码,看issue,分析行业的paper,尝试用一些前沿的算法改造引擎,尽管有一定的产出,但也伴随着极大的不确定性。闲暇的某天 我突然冒出一个想法,既然是云计算,那么多机器闲置着也是浪费,为什么不能在用户的集群旁边造个水池:将计算引过来,把结果返回去就好了,这样一来我只需要买些低配的机器,不仅计算大幅提升,成本还大幅降低,两年的KPI任务搞定 🧐 于是诞生了第一代「写入Serverless」即 AliyunES Indexing Service

如上图,IndexingService的核心任务便是 帮你建索引

  1. 用户侧的Bulk写入全部被转发到托管计算集群。
  2. 托管集群构建好完整的索引,通过物理复制写回到用户集群。
  3. 整个过程对用户是透明的,完全无感知是被托管了,只能感受速度上的极大提升。

达到的效果便是2核8GB的小集群可以达到20万写入QPS,提升了准10倍。这项技术上线后的成果也是不错的,很多头部客户如小红书、米哈游、康众、蜻蜓、罗辑思维、货达纷纷用了起来,反馈也很不错。

但是随着用户的使用深入,我们发现写入Serverless只能满足部分场景,由于用户集群规格降低了 导致查询速度太慢,于是又有大量客户退回到原始的高配置集群。 如何解决查询的托管成了接下来需要思考的重点。

进化篇「 ZSearch 」

接下来的一段时间 我担任阿里巴巴及蚂蚁集团Elasticsearch开源委员会的第一任PMC,这个组织汇聚所有阿里的ES爱好者,以中立的视角和态度将研发成果反哺到所有事业部。蚂蚁集团由于当时没有独立的搜索事业部,技术呈烟囱式发展,各个业务部门是有什么用什么,solr、海狗、Elasticsearch、HA3 百花齐放,由于没有统一的技术栈,从方案讨论、项目实施,稳定性保障、后期维护给每个团队都带来了极高的成本, 于是「 统一搜索中台 」呼之欲出。

结合之前的经验,平台应该怎么设计,“用户怎么用才能达到最爽的状态” 我带着这些思考开始了新一代的搜索平台设计。

思考

我们真的需要给用户一个集群吗?

经过重新设计的产品被命名为 ZSearch ,这是一款面向数据的搜索服务,而非集群的托管平台,设计目标便是极简化用户的体验,只暴露一个API网关,百分百兼容Elasticsearch接口,屏蔽底层所有的一切

上图便是ZSearch的核心架构,API Gateway 作为统一接口层,负责所有用户层的接口调用。Meta 作为元数据中心,用于对接ES的集群注册和用户集群的分配。底层的 Elasticsearch 真实集群被规划为海量、计算、通用三种类型,用于承载不同业务的真实载体。

ZSearch的上线,将业务从运维中解放出来,也从架构层面统一了基础设施。推出后反响强烈,从支付宝C端、财富、借呗、花呗等20+核心链路到数百个长尾应用,一路成长为如今的蚂蚁集团搜索中台,同时金融场景下对稳定性的变态要求,也反向推进了ZSearch的技术打磨,衍生出XDCR、PackingPool等核心技术。

由于使用极为简单,在未经任何推广的情况下 ZSearch的业务也延伸到到阿里集团的各个事业部,例如菜鸟、天猫、淘宝、阿里云、高德等40多个BU。从天猫精灵、语雀、机器学习PAI 到菜鸟海关、芝麻征信、优酷短视频...处处都能看到ZSearch的影子。

重生篇「 Nasu Elasticsearch Serverless 」

随着ZSearch业务的逐步稳定,一个新的想法时常萦绕在心头,能不能让他走出去,服务全国的广大用户呢。

如今,在疫情导致的经济持续下滑的严峻背景下,如何为广大企业推出一个款使用简单、经济实惠、成熟稳定的搜索解决方案,成为我下个阶段的主旋律。说干就干,2022年我从大厂走了出来,为这个想法付诸自己的行动。

成立纳速云科技有限公司,经过数月的闭关,新一代的面向全国用户的在线搜索产品诞生了 - Nasu Elasticsearch Serverless (NES)

为追求更极致的用户体验,NES在设计之初相比ZSearch提出了更高的要求:

  1. 无需预设业务场景,做到真正的自助接入。
  2. 面向跨云设计,实现更高要求的容灾体系。
  3. 从租户隔离提升到计算隔离,实现更高的可靠性。
  4. 建立对用户透明的集群迁移机制,实现全自动化的服务自治。

如上图,Nasu Elasticsearch Serverless 从以前的 sigma 底座迁移到云原生的Kubernates,利用其跨云特性打造更为标准化的容灾体系;利用容器化、存储架构分离、高速网络、智能熔断等技术构建更为灵活的多租户集群体系;建设全局监控、主动报警、事件回溯提升整体服务的可观测性。 通过各个核心能力的提升,将之前的搜索中台提升到商业级的可信服务。

目前我们没有销售,但上线仅数月 Nasu Elasticsearch Serverless 已接入上百个应用,每天接收上万个设备的数据上报,许多用户都是在没任何交流的情况下直接开通企业版 😅。

信任是可贵的,客户将后背无条件的交付给我们,作为后端的我们 目标也只有一个:为你打造一个最为可靠的API。接下来 纳速云的重点依然是投入在稳定性建设上,将可靠性做到业界最强是我们当前的唯一使命。

· 5 min read
擎天

索引与分片

Elasticsearch中的每个索引都被分成一个或多个分片,每个分片可以跨多个节点复制,以防止硬件故障。

概念
  • 索引(INDEX):可以理解为一个文档集合的描述文件
    • setting 定义了该索引有多少个分片和副本。
    • mapping 定义了索引基础的字段结构。
    • 索引的描述元数据(IndexMetaData)通常物理存储占比很小,以内存的形态记录在集群状态中。
  • 分片(SHARD): 真实的数据写入与查询载体。
    • 写入时先路由到主分片构建成真实的lucene索引,再将操作分发给副本构建成对等的索引。
    • 查询时从lucene检索出数据并交由上层映射成数据返回给用户。
    • 分片的存储会随着数据的增加而增大,因此面向不同场景设置合理的分片数目是我们需要关心的。

选择合适的分片数和副本数

1. 通用场景

在纳速云平台,如果您的数据量在GB级(小于1TB),可以参考以下通用方案

  1. 单分片不超过 30GB
  2. 单分片的最大文档数量不超过20亿
  3. 至少设置1个副本以保证高可用性。

2. 垂搜场景

垂直搜索是针对某一行业的专业搜索,例如电商搜索、音乐搜索、站内搜索、问答搜索,该场景往往对并发量及查询延迟有较高的要求。我们可在通用方案的基础上参考以下建议:

  1. 分片数:竟可能的降低分片数。
  2. 副本数:索引的查询响应及吞吐量取决于分片数,设置在1~8之间,逐步调整以满足业务效果为准。
注意

词频是ES计算相关性的重要因素,当大量的分片上只维护了很少的数据时,词频的统计信息过于分散将导致最终的文档相关性较差。

3. 大规模以及日益增长的时序场景

如有时序特性的日志场景、指标监控场景,该场景的特性为单日写入量大,数据存储具备周期性,且有可能存储较长的时间。

  1. 索引按天创建,并通过生命周期管理控制数据增长。
  2. 分片数:索引的写入速率及吞吐量取决于分片数,设置在1~8之间,逐步调整以满足业务效果为准。
  3. 副本数:设置为1。
tip

如能承受丢失数据的风险,副本数可设置为0

4. 海量数据统计分析场景

如网站访问统计、销售数据分析、大屏展示等。该场景注重计算,数据呈明显的冷热特质。近期数据分析频率高,需要较快的实时性展示,历史数据使用频率低,可承受较长的等待或以异步报告的形式呈现给用户。

  1. 索引按天创建,并通过生命周期管理控制数据增长。
  2. 分片数:竟可能的降低分片数。
  3. 副本数:设置为1。

结语

在分片分配上并没有一刀切的答案,本文仅供参考价值,实际生产中大家还是要多测试,以上线效果为准。

· 8 min read

数值(Numberic)在搜索引擎中通常扮演着重要的角色,例如:

  • 等值比较:x = ?
  • 大小比较:x > 1
  • 集合判断:x in ( 1, 3, 5, 8, 9)
  • 范围查询:x in [100 .. 200]
  • 距离计算:distance(x,y)

如何通过设计优良的数据结构和算法,搭配统一的执行框架高效的满足以上场景,我们从开源引擎 Lucene Point 的设计来展开分析。本文希望通过通俗易懂的图文方式一步步剖析它的原理进而了解其背后的的设计思想,而不是枯燥的理论知识。

执行框架

如上图,简要来说:

  1. 构建便于快速查询的核心索引结构:由 binary tree index (point) + Leaf(point,docid映射) 组成。
  2. 执行检索时基于tree的二分法快速定位匹配的 leaf block。
  3. 通过leaf block取出对应的文档列表(docid ...)
  4. 为满足不同的查询场景,visitor抽象了数据的访问方式,用于实现不同query的遍历算法。
info
  • point tree 存放在堆内存,以获取最快的访问速度。
  • leaf block 由 point + docid 双数组组成,存放在磁盘。(通过mmap加速)

索引文件结构分析

Leaf Block文件格式总体分为三大块

  • meta: 整块索引的元信息
  • body: 叶子列表,一个leaf block 由docid,point组成的pair双数组的集合, pair的长度通常为1024。
  • foot: 尾部信息

这里我们可以直观的感受到数值数据经过分块后带来了哪些好处:

  1. 检索复杂度降低,如1亿个整数,通过block打包后降到10W个。
  2. 提升内存操作效率,在索引创建或加载时,由于block长度统一,整块索引的操作效率要远高于单个数值的操作。
  3. 节省磁盘存储空间,由于block内数值是排好序的,可以利用各种压缩算法降低存储空间。

1. meta部分

  • numDims - 维度,例如IntPoint为1维
  • maxPointsInLeafNode - 一个叶子包含多少point, 默认1024
  • bytesPerDim - 每个维点字节数, 例如IntPoint为4个字节
  • numLeaves - 多少个叶子
  • minPackedValue - 最小point value
  • maxPackedValue - 最大point value
  • pointCount - point总数
  • docCount - docid 总数
  • packedIndex - 压缩字节的索引(新版本都采用此方式,比legacy index format节约40%空间)
tip
  • point 为多维数据结构,提供快速的单维和多维数值范围以及地理空间点形状过滤。
  • es 的数值(integer,float,double,long),IP地址,地理位置等字段索引都是point。
  • 在搜索时,不同的维度由自身的算法实现。

2. 单个leaf的文件格式

  • Count: leaf内包含多少point
  • DocIds: 无序的docid数组
  • Values: 有序的point数组。

核心源码指引

writeLeafBlock
private void writeIndex(IndexOutput out, int countPerLeaf, int numLeaves, byte[] packedIndex) throws IOException {

CodecUtil.writeHeader(out, CODEC_NAME, VERSION_CURRENT); // 从头部写入
out.writeVInt(numDims); // 存在几个维度
out.writeVInt(countPerLeaf); // 每个叶子Block的个数 (默认1024)
out.writeVInt(bytesPerDim); // 每个维点的字节数 (整数为4)

assert numLeaves > 0;
out.writeVInt(numLeaves); // 叶子节点数
out.writeBytes(minPackedValue, 0, packedBytesLength); // 最小值
out.writeBytes(maxPackedValue, 0, packedBytesLength); // 最大值

out.writeVLong(pointCount); // point值的个数
out.writeVInt(docsSeen.cardinality()); // docid的个数
out.writeVInt(packedIndex.length);
out.writeBytes(packedIndex, 0, packedIndex.length);
}

检索流程

  1. 加载BKDReader 有多少个dim文件就创建多少个BKDReader,BKDReader通过读取meta部分完成初始化。
  2. 为leaf blocks构建完全平衡二叉树。根据numLeaves计算深度,从左到右填满树。
  3. 检索方式 visitor抽象了访问方式,不同类型的查询Query采用不同的检索访问器。

范围检索流程

例如 newRangeQuery(200, 300) 这里访问器采用PointRangeQuery::内部IntersectVistor, 通过一次次索引块的compare取出所有的docid。

集合检索流程

例如 newSetQuery(1, 3, 6, 5, 123, 9999); 和Rang的区别MergePointVisitor会携带查询状态一次遍历完整颗树。

如上图,集合类查询有点像坐公交,出发时携带所有参数,迭代过程如遇到匹配的参数则剔除掉再进入下一个block。

tip

理解了原理,我们便可了解到在超大范围的in操作,数值类的算法复杂度相比term的O(n)就简单了许多。纳速云基于这个特性衍生了 join query , 实现跨表的过滤能力。

取值过程

取docid过程较为简单,seek各个检索满足条件的值到 PointValueQuery.result里

详细算法见BKDReader :: visitCompressedDocValues

总结

Point所采用的的 block k-d trees 是一种简单而强大的数据结构。在索引时,它们是通过递归将要索引的N维点的整个空间划分为越来越小的矩形单元,在递归的每个步骤中,沿着最宽的维度均等地分割。然而,与普通的k-d树不同,一旦单元格中的点数少于预先指定的(默认情况下为1024),k-d trees 就停止递归。

此时,该单元中的所有点都被写入磁盘上的一个leaf block中,该块的起始文件指针被保存到堆内binary tree中。在1维的情况下,这仅仅是所有值的完整排序,划分为相邻的叶块。有的k-d树变体可以支持删除值和重新平衡,但Lucene不需要这些操作,因为它为每段的一次写入设计。

在搜索时,进行相同的递归,在每个level测试所请求的查询条件是否与每个维度分割的左或右子树相交,如果是,则递归。在1维情况下,查询形状只是一个数字范围,而在2D和3D情况下,它是一个地理空间形状(圆形、环形、矩形、多边形、立方体等)

Vistor 抽象了遍历算法,感兴趣的同学可以根据该框架自行扩展,而不会破坏核心的执行框架。

· 7 min read

纳速云 XDCR(Cross DataCenter Replication)

XDCR 跨集群复制通常用于以下场景:

  • 异地高可用 (HA):对于许多核心链路应用程序,都需要能够承受住数据中心或区域服务中断的影响。通过 XDCR 中的原生功能即可满足跨数据中心的 DR/HA 要求,且无需其他技术。
  • 就近访问:将数据复制到更靠近用户或应用程序服务器的位置,可以减少延迟,降低成本。例如,可以将产品列表或参考数据集复制到全球 20 个或更多数据中心,最大限度地缩短数据与应用程序服务器之间的距离。
tip

ES的跨中心高可用也可以通过第三方技术来解决,例如双队列复制、网关流量双通道分发,但这种做法很繁琐,会带来大量的管理开销,而且有很大的数据一致性缺陷。通过将跨集群复制原生集成到 Elasticsearch 中,我们让用户摆脱了管理复杂解决方案的负担和缺点,并能提供现有解决方案所不具备的优势。

CCR vs XDCR

Elastic 官方的 CCR (cross-clusterreplication)也提供了类似的功能,CCR 属于xpack增强包中的功能,需要白金级、企业级证书才可使用。而XDCR 诞生于蚂蚁金服,由城破带领的搜索内核团队打造,该方案实际上比社区的 CCR 早半年落地,通过不断的踩坑和优化,最终满足了金融场景下对数据可用性和容灾能力极为严苛的要求。

本文从原理及设计层面阐述两者的不同之处。

顶层设计

面向分布式系统,一款优秀的高可用方案从顶层设计层面要抓住以下几点:

  1. 面向错误设计

分布式环境的复杂性往往会遇到不可预期的异常,如网络错误、格式冲突、节点hang住、异常流量、用户操作打断等等。设计层面要充分考虑这些因素,当异常发生时做到:

  • 核心服务解耦:主备集群相互无干扰。
  • 无状态化:复制任务保证原子性和无状态,任务之间无关联,失败后可重试。
  • 无干预:异常恢复时,任务可自动调度恢复,无需人为介入。
  1. 数据一致性

任何情况下,即便是数据丢失也比错误的数据可接受。

  • 采用更简单的单主模式,放弃双AA。
  • 始终通过operation_log恢复,保证数据一致性。
  • 垃圾数据可丢弃、可强制覆盖。
  1. 可扩展性

从平台层面,面向不可预期的业务数据增长:

  • 复杂度可控:不会随着索引的增加导致任务的指数级增加。
  • 资源可控:计算资源始终控制在合理的资源池内。
  1. 可观测性

通过接口暴露底层任务状态,通过外围工具辅助监控报警,做到第一时间能发现现场:

  • 任务实时状态
  • 计算消耗状态

设计架构

CCR和XDCR都是采用拉模式(Pull Mode):即备集群创建协调任务,主动从主集群拉取数据差异并写回备集群 :

拉模式相对于推模式的方案更为简洁:

  1. 主集群处理逻辑简单,只需要控制好拉请求的资源保护即可。
  2. 备集群通过任务的无状态化设计,保障断点续传的可靠性即可。

实现层面差异

CCR

核心任务处理逻辑

代码片段

org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java
    
void start(...) {
...
// 更新备集群mapping,提供了Leader的mapping version,并确保相同。
updateMapping(0L, leaderMappingVersion -> {
synchronized (ShardFollowNodeTask.this) {
currentMappingVersion = Math.max(currentMappingVersion, leaderMappingVersion);
}
updateSettings(leaderSettingsVersion -> {
synchronized (ShardFollowNodeTask.this) {
currentSettingsVersion = Math.max(currentSettingsVersion, leaderSettingsVersion);
}
// 协调读,取出数据差异进行复制。
coordinateReads();
});
});
}

CCR跨集群复制,将元数据同步和数据同步串联在主链路上,在分片数量不多的情况下能稳定的运行,但试想下如果是平台级的集群级同步,有成千上万个shard同步任务并行运行时,读取master的集群状态开销就不可小视了。

XDCR

XDCR从架构层面将元数据同步和Shard数据同步进行了分拆,如下图所示:

核心差异

  1. 备集群始终只维护一个全局的元数据同步任务,用于同步所有已注册同步的索引mapping和setting。
  2. 分片同步只处理数据同步,如遇到mapping,setting不一致则抛出异常,通过不断重试等待元数据同步后恢复。

从现象上来看,当主集群的索引mapping或setting更新时,XDCR相对CCR会停顿几秒 然后跟上。但带来的好处是:

  1. 在大规模的同步场景下,集群的master压力直线下降,且不会随着分片的增长而增加。
  2. 分片复制任务更为精简,功能原子性更强,更易于后期的架构演变。

本文阐述了两者的核心差异,当然还有更多的细节差异,如有兴趣的同学可以阅读XDCR源码分析。