概述
前13篇从倒排索引到底层写入,从集群分布式到安全运维,从反模式排查到复合故障演练,构建了ES从原理到排障的完整能力体系。然而,当面对一个真实的业务需求——比如“设计一个支撑千万级商品的搜索系统”或“构建跨机房的ES灾备架构”——你需要的不再是孤立的知识点,而是综合运用索引设计、集群规划、数据同步、性能调优和容灾策略的系统化设计能力。本文作为系列的终极实战篇(系统设计专题),通过4个企业级场景,帮助你将前13篇的知识转化为可落地的架构方案。
架构设计的本质是权衡。索引分片数是3还是6?冷热分离的边界是30天还是90天?CDC同步链路中Kafka分区数与ES分片数如何对齐?跨机房灾备的RPO是10秒还是1分钟?这些问题的答案不在官方文档中,而在于你的业务SLA、数据量级、团队运维能力和成本约束。本文将带你经历4个真实级别的系统设计挑战——从千万级商品搜索的索引建模到跨机房灾备的故障切换SOP,从MySQL+ES的交易搜索中台到PostgreSQL+ES的知识库与BI分析平台——每个场景都提供从需求分析到验证策略的完整设计框架。
核心要点:
- 4个系统设计场景:覆盖纯ES架构和ES+DB+Kafka协同架构。
- 统一场景结构:需求与约束 → 分片与索引 → 复制与高可用 → 连接池与资源 → 验证策略 → 方案评估与风险。
- 跨系列知识整合:索引设计(第4篇)、查询聚合(第5篇)、集群分布式(第7篇)、运维监控(第10篇)、数据同步(第12篇)。
- 预留接口:文末说明缓存层(Redis)、分布式事务(Seata)等组件将在后续阶段引入,最终在第四阶段“架构决策与业务设计”中通过全栈案例统一整合。
文章组织架构
flowchart TD
1[千万级商品搜索系统架构设计 纯ES]
2[跨机房ES灾备与高可用架构 纯ES]
3[电商交易与搜索中台 MySQL+ES+Kafka协同架构]
4[企业级知识库与BI分析 PostgreSQL+ES+Kafka联合架构]
5[从协同架构到全栈一体化 预留接口]
1 --> 2 --> 3 --> 4 --> 5
分层详尽说明:
- 总览说明:全文5个模块按技术栈复杂度递进——从纯ES架构到ES+DB+Kafka协同架构,最后以全栈一体化展望收尾,体系化呈现搜索与数据协同架构的演进路径。
- 逐模块说明:模块1-2聚焦ES自身的数据架构与灾备设计,是构建复杂协同架构的基础;模块3-4展示ES与关系型数据库的协同设计,引入CDC、消息队列和混合查询等高级主题;模块5为后续全栈综合设计预留接口,展望缓存、分布式事务等组件的整合。
- 关键结论:系统设计不是单一技术的堆砌,而是数据职责划分、同步链路可靠性、一致性边界和故障恢复策略的综合权衡。理解ES的索引建模、集群规划和CDC协同设计,是构建企业级搜索与数据中台的核心能力。
1. 千万级商品搜索系统架构设计(纯ES)
1.1 业务需求与约束
某电商平台日均搜索请求500万次,商品SKU总量2000万,7×24小时不间断服务。关键非功能需求如下:
- 搜索响应延迟:P99 < 50ms,P999 < 200ms。
- 自动补全:用户输入前2个字符后触发,补全延迟P99 < 20ms。
- 多条件筛选:支持类目、品牌、价格区间、评分等复合筛选,且支持嵌套SKU属性(如“颜色:红色 AND 容量:128GB”)。
- 个性化排序:默认排序需结合商品销量、评分、新品发布时间,允许后续接入用户画像做个性化加权。
- 数据更新:每日新增商品约1万,库存和价格变更约5万次,要求变更后5秒内可搜索到(近实时)。
- 历史数据保留:需保留2年内商品数据,其中近3个月为高频访问数据,3-12个月为温数据偶尔查询,12-24个月为冷数据极少查询但不可删除。
- 成本约束:服务器总数量≤12台(不含网络设备),需合理利用SSD和HDD资源。
1.2 分片与索引方案
依据第4篇映射与文档建模及第7篇集群分布式原理,将索引拆分为两个独立的物理索引:
主搜索索引 products_search
- 主分片数:3,副本数:2(总计9个分片副本)。理由:2000万文档,每文档平均2KB,数据总量约40GB。按每分片建议10-50GB容量,3主分片使每分片大小约13.3GB,处于最佳性能区间。3分片可并行利用3个数据节点,搜索吞吐可线性扩展。
- 写索引的别名:使用
products_search_write别名指向可写索引,配合ILM的Rollover实现零停机滚动。 - 读索引的别名:
products_search覆盖所有可读索引(热、温、冷阶段),应用层统一使用该别名搜索。
自动补全索引 products_suggest
- 主分片数:1,副本数:2。
completion类型数据高度压缩,2000万条建议数据约2GB,单分片足够支撑极高QPS,且补全请求无需跨分片合并结果,延迟最低。写请求较少(仅新增商品时更新),因此单分片写性能不是瓶颈。
索引映射详细设计
PUT /products_search-000001
{
"settings": {
"index": {
"number_of_shards": 3,
"number_of_replicas": 2,
"refresh_interval": "1s",
"routing.allocation.require.data": "hot",
"analysis": {
"analyzer": {
"ik_smart_pinyin": {
"type": "custom",
"tokenizer": "ik_smart",
"filter": ["pinyin_filter"]
}
},
"filter": {
"pinyin_filter": {
"type": "pinyin",
"keep_full_pinyin": true,
"keep_joined_full_pinyin": true,
"remove_duplicated_term": true
}
}
}
}
},
"mappings": {
"properties": {
"product_id": { "type": "keyword" },
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"pinyin": { "type": "text", "analyzer": "ik_smart_pinyin" },
"keyword": { "type": "keyword" }
}
},
"description": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"category_id": { "type": "keyword" },
"brand_id": { "type": "keyword" },
"price": { "type": "scaled_float", "scaling_factor": 100 },
"sales": { "type": "integer", "doc_values": true },
"rating": { "type": "float", "doc_values": true },
"create_time": { "type": "date" },
"sku_attributes": {
"type": "nested",
"properties": {
"attr_name": { "type": "keyword" },
"attr_value": { "type": "keyword" },
"stock": { "type": "integer" },
"sku_price": { "type": "scaled_float", "scaling_factor": 100 }
}
}
}
}
}
设计意图解读:
title使用ik_max_word索引,保证召回率,搜索时使用ik_smart,提升精度。同时增加拼音子字段,支持拼音模糊搜索(详见第6篇中文分词实战)。price采用scaled_float,精度到0.01,内存占用远小于float,适合大量数值排序和范围过滤。sku_attributes使用nested类型存储多规格属性,保证属性间的关联性(不会出现“颜色:红色”和“容量:256GB”来自不同SKU的虚假匹配),但写入和查询成本较高,需在业务上控制嵌套文档数量(单个商品SKU数<20)。sales和rating开启doc_values,用于排序和聚合。product_id使用keyword,支持精确查找,同时作为_id的候选,方便数据同步时的幂等更新。
自动补全索引映射
PUT /products_suggest
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 2,
"refresh_interval": "-1"
},
"mappings": {
"properties": {
"suggest": { "type": "completion" },
"category_id": { "type": "keyword" }
}
}
}
将refresh_interval设为-1,禁止自动刷新。补全索引的更新频率低,可以通过定时(如每5分钟)手动调用_refresh或_flush来使数据可搜索,减少写入时段的资源消耗。
1.3 复制与高可用拓扑
采用Hot-Warm-Cold分层架构(详见第7篇热温冷分离与索引生命周期管理),节点角色与数量分配如下:
- 3个Master Eligible节点:
master: true, data: false, ingest: false,仅处理集群管理。硬件中等配置(4C16G),可部署在虚拟机。 - 3个Hot Data节点:
node.attr.data: hot,SSD存储,64GB RAM(32GB堆),处理写入和热数据查询。存放近3个月的数据。 - 3个Warm Data节点:
node.attr.data: warm,HDD存储,48GB RAM(24GB堆),存放3-12个月数据。查询负载低,但需一定内存缓存。 - 2个Coordinating Only节点:
master: false, data: false, ingest: false,16C32G,专门处理查询分发与结果合并,启用自适应副本选择(ARS)功能cluster.routing.use_adaptive_replica_selection: true,降低查询延迟。 - 总计11台物理/虚拟机,满足12台约束。
机架感知配置:每个节点设置node.attr.rack_id,利用cluster.routing.allocation.awareness.attributes: rack_id强制主副分片分布于不同机架,避免单机架故障导致数据不可用。
集群拓扑图:
flowchart LR
User(搜索流量)
LB(负载均衡 HAProxy)
Coord1(协调节点1)
Coord2(协调节点2)
Hot1(Hot Data 1 SSD)
Hot2(Hot Data 2 SSD)
Hot3(Hot Data 3 SSD)
Warm1(Warm Data 1 HDD)
Warm2(Warm Data 2 HDD)
Warm3(Warm Data 3 HDD)
Master1(Master 1)
Master2(Master 2)
Master3(Master 3)
User --> LB --> Coord1 & Coord2
Coord1 & Coord2 --> Hot1 & Hot2 & Hot3 & Warm1 & Warm2 & Warm3
Master1 --- Master2 --- Master3
Hot1 & Hot2 & Hot3 --- Warm1 & Warm2 & Warm3
四层说明:
- 数据拓扑:热节点存储最近3个月的索引和最新写入的索引,温节点存储较旧数据。协调节点不存储数据,专注查询路由和结果合并。
- 复制拓扑:
products_search3主分片 + 2副本分布在6个数据节点上(热节点为主,温节点无主分片,仅通过ILM迁移后才在温节点上有分片)。副本通过rack感知确保冗余。 - 流量路径:搜索请求经负载均衡平均分发到两个协调节点,协调节点根据目标索引分片分布将子请求并行发送到持有相关分片的数据节点,各节点本地执行查询与聚合,结果返回到协调节点合并后返回客户端。
- 故障切换:若某热节点宕机,其主分片的副本会被提升为主分片,集群状态变黄后短时间内自动恢复。客户端通过节点嗅探自动更新路由表,协调节点感知变化后调整请求分发。
1.4 连接池与资源规划
搜索服务端:部署20个Spring Boot实例,每个实例使用ElasticsearchClient(Java Client 8.x),配置如下:
spring:
elasticsearch:
uris: http://coord1:9200,http://coord2:9200
connection-timeout: 2s
socket-timeout: 3s
客户端连接池配置(底层Apache HttpClient):
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager();
connectionManager.setDefaultMaxPerRoute(10);
connectionManager.setMaxTotal(200);
全局最大连接数:20个服务实例 × 每实例到每个协调节点最大10连接 × 2个协调节点 = 400个TCP连接。远低于ES协调节点http.netty.worker_count默认值(CPU核数×2),不会造成拒绝连接。
ES节点JVM配置:
- Hot/Warm Data节点:
-Xms31g -Xmx31g(堆为31GB,留出空间给压缩指针优化)。indices.memory.index_buffer_size: 20%(写入缓冲)。 - Coordinating节点:
-Xms16g -Xmx16g,专注于合并和网络转发,设置thread_pool.search.queue_size: 2000,防止短时搜索波峰导致拒绝。 - 设置
indices.queries.cache.size: 15%,适用于过滤缓存。
1.5 冷热分离与ILM详细策略
ILM Policy products_ilm:
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "50GB",
"max_age": "30d"
},
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "90d",
"actions": {
"allocate": {
"number_of_replicas": 1,
"require": { "data": "warm" }
},
"forcemerge": {
"max_num_segments": 1
},
"shrink": {
"number_of_shards": 1
},
"set_priority": { "priority": 50 }
}
},
"cold": {
"min_age": "365d",
"actions": {
"searchable_snapshot": {
"snapshot_repository": "s3_backup"
},
"allocate": {
"number_of_replicas": 0
},
"set_priority": { "priority": 0 }
}
},
"delete": {
"min_age": "730d",
"actions": {
"delete": {}
}
}
}
}
}
关键解释:
rollover同时以大小(50GB)和时间(30天)为条件,先到者触发新建索引,防止单个分片过大影响性能,也避免索引过多导致集群管理负担。warm阶段shrink到1分片,forcemerge到1段,极大减少内存占用和打开文件句柄。副本降为1,因为温数据访问频率低,冗余度可降低。cold阶段使用searchable_snapshot挂载S3快照,本地无完整数据副本(零副本),查询时从快照缓存部分数据,存储成本降低80%以上,但查询延迟上升至秒级。适用于极少访问的归档数据。delete730天后删除,保证数据不超过保留期。
索引模板绑定:
PUT /_index_template/products_template
{
"index_patterns": ["products_search-*"],
"template": {
"settings": {
"index.lifecycle.name": "products_ilm",
"index.lifecycle.rollover_alias": "products_search_write"
}
}
}
启动时手动创建首个索引:
PUT /products_search-000001
{
"aliases": {
"products_search_write": { "is_write_index": true }
}
}
应用写入必须使用别名products_search_write。读搜索使用products_search别名,并设置is_write_index: false。
1.6 搜索排序策略详解
基础排序:BM25评分,结合业务信号通过function_score调整(详见第5篇高级查询DSL):
{
"query": {
"function_score": {
"query": {
"bool": {
"must": [
{ "match": { "title": "手机" } }
],
"filter": [
{ "term": { "category_id": "123" } },
{ "range": { "price": { "gte": 1000, "lte": 5000 } } }
]
}
},
"functions": [
{
"field_value_factor": {
"field": "sales",
"factor": 0.3,
"missing": 0,
"modifier": "log1p"
}
},
{
"field_value_factor": {
"field": "rating",
"factor": 0.2,
"missing": 0
}
},
{
"gauss": {
"create_time": {
"origin": "now",
"scale": "30d",
"decay": 0.5
}
}
}
],
"score_mode": "sum",
"boost_mode": "multiply"
}
}
}
- 销量使用
log1p修饰符,避免超级爆款绝对主导,平滑影响。 - 评分直接加权。
- 新品时间衰减:以当前时间为原点,30天内的商品衰减一半,使新商品获得合理提升。
个性化预留:后续可引入用户画像,通过script_score动态计算,例如在script中根据用户历史偏好类目加权,或使用rescore阶段重排序。
1.7 验证策略
压测工具:Apache JMeter 模拟搜索流量,ES Rally 专门压测ES写入和查询吞吐。
压测模型:
- 场景A(搜索负载):混合查询,80% 复合筛选(包含
bool、nested、range、terms聚合),10% 简单关键词搜索,10% 不常用排序(如按价格排序)。目标QPS:5000。 - 场景B(自动补全):模拟用户输入前缀,调用
_suggestAPI,目标QPS:10000。 - 场景C(混合负载):在5000 QPS搜索背景上,增加1000 bulk更新/秒(模拟商品价格库存变更),观察写入对搜索延迟的影响。
观察指标与采集方式:
- 搜索延迟P99、P999、Mean,通过JMeter聚合报告和ES慢日志(>200ms)对比。
- 集群指标:
_cat/health、_cat/nodes?v&h=name,cpu,load_1m,heap.percent,disk_used_percent,通过Elasticsearch Exporter + Prometheus持续监控。 - 线程池拒绝计数:
thread_pool.search.rejected、thread_pool.bulk.rejected。 - GC暂停时间:通过JMX或监控工具获取。
通过标准:
- 场景A:P99 < 50ms,P999 < 200ms,无连续
rejected增长。 - 场景B:P99 < 20ms。
- 场景C:搜索延迟不受显著影响(P99 < 60ms仍可接受),Bulk写入平均延迟<50ms,无拒绝。
- 集群CPU平均 < 70%,堆使用率 < 75%,无Young GC长停顿(>200ms)。
具体执行:先用Rally针对单索引products_search-000001压测“default track”和自定义查询操作,验证单节点性能基线。然后部署完整拓扑进行JMeter全链路压测,根据结果调整协调节点数量或ARS参数。
1.8 方案评估与潜在风险
风险1:Warm阶段查询延迟跳变。Warm阶段索引经过forcemerge到1段,且HDD寻道时间高,复杂查询的P99可能上升至200ms以上。应对:业务上将“3个月前”商品搜索单独入口,引导用户预期;或对于重要类别,延长Hot阶段到6个月。
风险2:nested类型的写入性能瓶颈。商品SKU变动时(如某规格降价),需要更新整个nested对象数组,实质是文档重建,写入放大。应对:限制单商品SKU数量,将SKU属性扁平化存储为JSON字符串,用match查询替代nested精确匹配,牺牲部分查询精确性换取写入性能(设计取舍)。
风险3:自动补全索引的实时性。由于refresh_interval=-1,新增商品后补全无法立即生效,最长延迟为手动刷新间隔(5分钟)。应对:可接受范围,业务需要实时的场景可改为refresh_interval=1s,但在写入峰值时监控CPU。
风险4:Rollover期间写失败。当新索引刚创建,别名切换窗口期可能出现极短暂的写失败。应对:客户端实现重试逻辑,写入重试3次,每次间隔1秒。
风险5:冷数据查询性能。searchable_snapshot查询会从远程S3加载数据,首次访问延迟可达数秒,且占用大量内存用于缓存。应对:设置index.store.snapshot.cache.size限制,并通过定时预热(定时发送典型查询)将常用冷数据预加载到本地缓存。
2. 跨机房ES灾备与高可用架构(纯ES)
2.1 业务需求与约束
核心搜索服务部署在同城双机房(机房A与机房B相距30km,专线网络延迟RTT < 2ms,且为双链路冗余)。关键SLA:
- RPO(恢复点目标)< 10秒,即允许最多丢失10秒的写入数据。
- RTO(恢复时间目标)< 60秒,即从故障判定到灾备集群可接管写入的总时间。
- 灾备集群利用率:平时可分担最多20%的搜索读流量,但不可写入。
- 写入QPS约500,搜索QPS约1万。
- 所有索引需保持同步,包括商品搜索、自动补全及其他辅助索引。
2.2 分片与索引方案
所有索引统一采用3主分片 × 2副本的配置。主分片数与副本数设计依据:3主分片可满足数据量(总2000万)和查询并行度;2副本(1位于机房A,1位于机房B CCR Follower)提供机房级冗余。CCR要求Leader和Follower索引分片数完全一致,因此无需特殊调整。
2.3 复制与高可用拓扑
采用ES的CCR(跨集群复制) 功能实现异步索引复制(详见第7篇集群分布式与高可用CCR部分)。拓扑如下:
flowchart LR
subgraph 机房A[机房A 主集群]
LB_A(负载均衡)
MasterA1(Master 1) & MasterA2(Master 2) & MasterA3(Master 3)
DataA1(Data 1 Leader shard) & DataA2(Data 2) & DataA3(Data 3)
CoordA(Coordinating 2)
end
subgraph 机房B[机房B 灾备集群]
LB_B(负载均衡)
MasterB1(Master 1) & MasterB2(Master 2) & MasterB3(Master 3)
DataB1(Data 1 Follower shard) & DataB2(Data 2) & DataB3(Data 3)
CoordB(Coordinating 2)
end
Client(搜索客户端)
Client --> LB_A & LB_B
LB_A --> CoordA --> DataA1 & DataA2 & DataA3
LB_B --> CoordB --> DataB1 & DataB2 & DataB3
DataA1 -- CCR异步复制 --> DataB1
DataA2 -- CCR异步复制 --> DataB2
DataA3 -- CCR异步复制 --> DataB3
四层说明:
- 数据拓扑:机房A为主集群,持有全部Leader分片,处理所有写入请求和大部分搜索读(80%)。机房B为灾备集群,通过CCR在每个分片上建立Follower,数据近乎实时同步。
- 复制拓扑:CCR在分片级别通过拉取Leader的translog增量同步。Follower分片部署在机房B的数据节点上,配置为只读,无法直接写入。副本数仍满足机房B内部通过本地副本保证高可用(Follower可以有自己的副本)。
- 流量路径:正常时,客户端通过智能DNS或负载均衡将写请求固定发往机房A集群,读请求按权重8:2分发到A和B。灾备集群协调节点接收查询后,可从本地Follower分片直接读取数据,无需跨机房。
- 故障切换:当机房A整体故障,机房B自动提升Follower为普通索引,释放写阻塞,成为新主集群,接管全部读写。
2.4 CCR配置与监控
建立CCR连接:在机房B集群中注册远程集群A:
PUT /_cluster/settings
{
"persistent": {
"cluster": {
"remote": {
"cluster_A": {
"seeds": ["192.168.1.10:9300", "192.168.1.11:9300"],
"transport.compress": true,
"transport.ping_schedule": "30s"
}
}
}
}
}
seeds填写机房A的master eligible节点传输层地址。transport.compress开启可降低专线带宽消耗。ping_schedule保持心跳检测,及时发现网络中断。
创建Follower索引:
PUT /_ccr/_follow/products_search-follow
{
"remote_cluster": "cluster_A",
"leader_index": "products_search",
"max_read_request_operation_count": 5000,
"max_outstanding_read_requests": 12,
"max_write_buffer_size": "512mb",
"read_poll_timeout": "1m"
}
参数解析:
max_read_request_operation_count:控制每次从Leader拉取的批量操作数,太高会增加内存和网络压力,太低则吞吐不足。根据写入500 QPS和平均文档大小,5000操作可覆盖约5秒左右的写入。max_outstanding_read_requests:并发拉取请求数,高值可增加吞吐,但需配合线程池。max_write_buffer_size:Follower端写缓冲,若消费速度跟不上Leader写入,缓冲可暂存数据,但注意监控内存占用。read_poll_timeout:长轮询超时,同城网络设为1分钟,若网络抖动能快速重试。
延迟监控:
GET /_ccr/stats
返回关键指标:
leader_lag_in_millis:Follower落后Leader的时间,单位为毫秒。这是RPO的直接体现。total_read_time、total_write_time:读取和写入耗时,帮助分析瓶颈。bytes_read/bytes_written:累计字节数。 告警规则:leader_lag_in_millis > 8000(8秒)触发黄色告警,> 15000触发红色告警并准备切换。
2.5 故障切换SOP
自动化探测与决策:
- 健康探测:Consul每5秒请求机房A集群
_cluster/health?timeout=30s,连续3次超时(共15秒)标记机房A不可用,触发切换流程。 - 网络分区预防:当机房A与B之间专线中断,但机房A仍正常服务时,需避免脑裂。设置ES集群的
discovery.seed_hosts仅包含本机房节点,不允许跨机房组成集群,从物理上杜绝脑裂。CCR中断会自动重试,不影响主集群服务。
切换步骤(脚本自动化):
- 暂停CCR同步(防回写冲突):
POST /_ccr/_pause_follow/products_search-follow - 解除Follower角色,转为普通索引:
POST /_ccr/_unfollow/products_search-follow - 开放写入:
并调整索引副本数至2(若原Follower仅1副本,可动态增加以保证冗余)。PUT /products_search/_settings { "index.blocks.write": false } - 切换别名和应用配置:更新搜索服务的ES连接地址为机房B集群,或通过负载均衡VIP飘移(HAProxy健康检查自动切换)。
- 恢复业务:此时机房B集群成为主集群,开始接受写入,业务重新上线。
验证:执行业务冒烟测试(搜索、补全、写入),监控集群健康状态、错误日志。
回切方案(机房A恢复后):
- 机房A集群重新部署或恢复后,暂停机房B写入(置只读)。
- 将机房B的全部索引通过
_reindex拷贝至机房A集群,或使用快照恢复(更快)。 - 在机房A集群重建CCR,此时以机房A为Leader,机房B为Follower(角色与原拓扑一致,即机房A为主)。设置CCR同步。
- 同步达到近实时后,缓慢将流量切回机房A(逐渐增加权重),待稳定后恢复原有8:2流量比例。
2.6 连接池与资源规划
灾备集群配置参照主集群60%的硬件资源,但数据节点数量必须一致(6个),保证故障切换后可承载全量数据分片。日常只承载20%读流量,资源充裕,但在切换后要能承载全部读写。因此灾备节点规格可降低(如16C32G),依赖扩容策略应急时垂直升级。
客户端连接池需预配置两个ES集群地址,支持运行时切换。应用启动时读取配置中心(如Apollo)的es.cluster.active属性,实现无重启切换。
2.7 验证策略
切换演练计划:每月执行一次全链路故障切换演练,包括DNS切换、应用重连、冒烟测试和回切。
压测工具:esrally对灾备集群进行基准压测,验证其独立处理全量QPS的能力;JMeter模拟真实搜索流量。
演练观察指标:
- RTO:从故障注入(断开机房A网络)到机房B集群开始成功响应写入请求的时间间隔。
- RPO:在故障前持续写入记录带时间戳的数据,故障后对比机房B最后一条记录的时间戳与故障时刻差值。
- 应用错误率:切换过程中搜索、写入的失败率。
- 灾备集群负载:CPU、内存、IO。 通过标准:RTO < 60秒,RPO < 10秒(实际测量值),应用错误率峰值<5%,1分钟内恢复到0,灾备集群各项指标<80%。
2.8 方案评估与潜在风险
- CCR异步延迟波动:尽管同城专线低延迟,但当Leader写入突发或网络拥塞时,
leader_lag可能瞬时超过10秒,导致切换时数据丢失超出RPO。需在应用层对写入进行应用级日志记录(如记录到本地Log文件或MQ),在极端情况允许从日志回补丢失的10+秒数据。 - DNS缓存效应:若客户端DNS TTL较长(>60秒),切换期间流量仍指向故障机房。需将DNS TTL设为30秒,并配合HAProxy健康检查强制剔除故障后端。
- 回切复杂度与耗时:全量数据
_reindex或快照恢复所需时间与数据量成正比,TB级数据可能花费数小时,回切RTO远大于故障切换,需计划在业务低峰期执行。 - 脑裂防护:CCR模式下,机房B Follower为只读,即使与Leader失联也不会自动提升为可写,杜绝了双写。但自动化提升脚本必须确保只有确认机房A不可达时才执行。
3. 电商交易与搜索中台:MySQL + ES + Kafka 协同架构
3.1 业务需求与约束
电商平台日均订单500万,商品SKU 2000万。业务核心链路分为:
- 交易链路:下单、支付、发货,强一致性要求,必须落在MySQL(InnoDB,ACID)。
- 搜索链路:商品搜索(全文+多维筛选)、订单列表查询(按用户ID),允许数据短暂不一致(<5s最终一致)。
- 报表聚合:运营后台按类目、时间范围统计销售额,数据来源于ES的聚合。
- 数据一致性要求:商品信息变更后5秒内需要在ES中反映;订单状态变更后可稍慢(10秒)。允许差异<0.1%,需要具备自动检测和补偿能力。
- 扩展性:MySQL订单表按月分库分表(16库,每库16表),商品表单独库(读多写少)。
3.2 分片与索引方案
ES商品索引 products_search:
- 3主分片 × 2副本,与场景1相同。
- 关键设计:采用
_routing=category_id。查询时,若筛选条件包含类目,可直接路由到一个或少数分片,极大提升缓存利用率和性能;若未指定类目,则广播到3个分片。写入时,从CDC事件中提取category_id作为路由值。
ES订单索引 orders_search:
- 16主分片 × 1副本(查询负载高,可后续增加副本)。
- 采用
_routing=user_id。此设计直接对齐MySQL分库分表的哈希算法(user_id % 16确定库)。订单列表查询业务上按用户ID检索,应用层可精确计算路由值,请求仅命中单分片,避免跨分片查询和排序开销。
订单索引映射示例:
{
"mappings": {
"properties": {
"order_id": { "type": "keyword" },
"user_id": { "type": "keyword" },
"product_ids": { "type": "keyword" },
"total_amount": { "type": "scaled_float", "scaling_factor": 100 },
"status": { "type": "keyword" },
"create_time": { "type": "date" }
}
}
}
3.3 复制与高可用拓扑
整体架构图:
flowchart TB
App(业务应用)
MySQL_P(MySQL主库 机房A)
MySQL_S(MySQL从库 机房A)
Canal(Canal Server)
Kafka(Kafka 集群 3 Broker)
SyncSvc(搜索同步服务 消费者组6实例)
ES(ES 主集群 机房A)
DLQ(死信队列 Topic)
RetryQ(重试队列)
App -- 读写 --> MySQL_P
MySQL_P -- 半同步复制 --> MySQL_S
MySQL_P -- Binlog ROW格式 --> Canal -- 发送 --> Kafka
Kafka -- 消费 --> SyncSvc
SyncSvc -- ES Bulk写入 --> ES
SyncSvc -- 失败文档 --> RetryQ --> SyncSvc
RetryQ -- 超过重试 --> DLQ
App -- 搜索读 --> ES
四层说明:
- 数据拓扑:MySQL负责交易数据的ACID存储,ES负责搜索和聚合的快速查询。数据流向是单向的:MySQL → CDC → Kafka → ES。
- 复制拓扑:MySQL主从半同步复制保证数据持久化(至少一个从库确认)。Canal伪装为MySQL从库,直接从主库消费Binlog,解析后生产至Kafka。Kafka作为缓冲层提供高吞吐和持久化。ES集群自身具备副本冗余。
- 流量路径:写路径:App → MySQL事务提交 → Binlog → Canal → Kafka Topic
cdc.products(12分区)→ 同步服务消费者组 → 转换文档 → ES_bulkAPI → 索引成功。读路径:App → ES协调节点 → 数据节点查询。 - 容错与重试:Kafka分区保证有序,同步服务使用
enable.auto.commit=false,手动在处理批量成功后提交Offset。当ES Bulk部分失败,提取失败项发送到重试队列cdc.retry.products,延迟重试,仍失败则入死信队列cdc.dlq.products并告警。
3.4 CDC数据管道详细设计
MySQL设置:
- Binlog格式:
ROW,开启gtid_mode=ON,保证切换从库时位点连续。 - 为Canal创建专用账号,拥有
REPLICATION SLAVE, REPLICATION CLIENT权限。 - 表要求:每张要同步的表需要有
updated_at时间戳字段或版本号字段,用于ES的_version控制。
Canal配置示例(canal.properties):
canal.serverMode = kafka
kafka.bootstrap.servers = kafka1:9092,kafka2:9092,kafka3:9092
kafka.retries = 3
kafka.batch.size = 16384
实例配置(example/instance.properties):
canal.instance.master.address = mysql-master:3306
canal.instance.dbUsername = canal
canal.instance.dbPassword = canal123
canal.instance.filter.regex = shop_db\\.products,shop_db\\.orders
canal.mq.topic = cdc.shop_db.products
canal.mq.partition = products.category_id.hashCode() % 12
canal.mq.partitionsNum = 12
分区策略解释:Canal根据category_id哈希取模12分区,写入Kafka Topic的12个分区。由于ES商品索引3个分片,12是3的整数倍(12=3×4),因此同一category_id的变更事件会映射到固定的ES分片(通过_routing),并且Kafka分区内有序,保证了相同文档的变更顺序性(先INSERT后UPDATE不乱序)。Kafka分区数与ES分片数成倍数关系是设计核心,否则跨分区消费可能导致写ES时序混乱(详见第二阶段“Kafka深度与流处理”系列中分区策略与消费者并行度设计)。
Kafka Topic配置:
cdc.shop_db.products:分区12,副本3,保留时间7天(retention.ms=604800000)。cdc.shop_db.orders:分区16,按user_id哈希分区,与ES订单索引16分片对齐。
同步服务核心逻辑(Java伪代码):
public void consume(ConsumerRecords<String, String> records) {
List<IndexRequest> requests = new ArrayList<>();
for (ConsumerRecord<String, String> record : records) {
BinlogEvent event = parse(record.value());
if (event.isInsert() || event.isUpdate()) {
IndexRequest req = new IndexRequest("orders_search")
.id(event.getDocId())
.routing(event.getRoutingField()) // user_id
.source(event.getJsonMap(), XContentType.JSON)
.version(event.getVersion()); // 乐观锁版本号
requests.add(req);
} else if (event.isDelete()) {
requests.add(new DeleteRequest("orders_search")
.id(event.getDocId()).routing(event.getRoutingField()));
}
}
BulkResponse bulkResponse = esClient.bulk(requests);
if (bulkResponse.hasFailures()) {
List<BinlogEvent> failed = extractFailed(bulkResponse, records);
sendToRetryTopic(failed); // 发送到cdc.retry.orders
}
// 成功后提交offset
consumer.commitSync();
}
版本控制:从Binlog的行数据中获取updated_at毫秒时间戳作为外部版本号,ES使用version_type=external保证版本更新只能递增,旧版本更新被拒绝,避免消息乱序覆盖。
重试与死信:
- 重试Topic
cdc.retry.products有2分钟延迟(通过Kafka的retention和自定义重试消费者实现延迟处理)。 - 死信Topic
cdc.dlq.products保留30天,通过监控告警后人工处理或修正后回放。
3.5 一致性保证策略
最终一致性保障三剑客:
- At-Least-Once传输 + 幂等写入:Kafka消费者手动提交,保证至少处理一次;ES
_version乐观锁保证重复消息幂等更新,不会回滚数据。 - 定时对账:每小时执行SQL对比:
MySQL:
SELECT COUNT(*) FROM products WHERE updated_at > NOW() - INTERVAL 2 HOURES:GET /products_search/_count使用范围查询过滤近两小时更新的文档。 差异超过0.1%触发全量补偿任务:通过Spark或Flink读取MySQL全表,按category_id分批_bulk重建ES索引(利用别名切换,零停机)。或直接从Kafka最早保留位点回放指定时间范围的消息(若仍在保留期内)。 - 手动补偿:死信队列中的消息,支撑人工校正脚本,直接调用ES API修正单条文档。
3.6 冷热分离与归档
- ES商品索引:沿用场景1的ILM Hot-Warm-Cold。
- ES订单索引:使用ILM,Hot阶段90天(3主分片,2副本),Warm阶段90-365天(shrink到1分片,forcemerge,1副本),365天后直接delete(订单历史冷数据保留在MySQL归档库,无需ES保留)。通过索引模板绑定
orders_search-*。 - MySQL归档:订单主表只保留6个月数据,通过
pt-archiver工具按时间戳归档到历史库orders_archive(另一套MySQL或对象存储)。归档不影响ES同步(只要归档不删除主表记录的updated_at触发器)。
3.7 连接池与资源规划
- 同步服务:6实例,每实例ES客户端连接池
maxConnPerRoute=5,全局ES连接数约30。 - Canal:1台Canal server(可HA部署),与Kafka集群低延迟网络。
- Kafka:3 Broker,每个Topic分区数设计保证消费者并发度(12分区对应最多6个消费者并行,因为消费者组内分区分配)。
- MySQL连接:Canal占用1个slave连接;应用连接池HikariCP(20连接),与ES无关。
3.8 验证策略
全链路压测模型:
- 总QPS:7000。
- 70% 搜索读(ES):商品复合查询 4000 QPS,订单列表查询 1000 QPS。
- 25% 订单写(MySQL):500 TPS 下单事务,包含订单、库存更新等。
- 5% 报表聚合(ES):100 QPS 复杂聚合(按类目、日期范围统计)。
- 同时使用Kafka生产者模拟高峰写入:以10000 events/s 的速率向
cdc.products发送Binlog事件,观察同步延迟和ES Bulk写入压力。
观察指标:
- 搜索P99 < 50ms,订单列表P99 < 100ms。
- 下单P99 < 100ms(包含MySQL事务)。
- CDC同步延迟:消费者Lag监控(
kafka-consumer-groups),Kafka消息时间戳与ES文档更新时间差值,要求P99 < 5s。 - MySQL主从延迟 < 1s。
- ES线程池
thread_pool.bulk.rejected、thread_pool.search.rejected。 - 对账差异比例。
故障注入:
- Kafka Broker宕机:停止一个Kafka Broker,观察生产和消费是否受影响(由于副本3,需保证ISR完整)。同步服务应能自动重连。
- ES数据节点宕机:终止一个Hot Data节点,观察ES集群自愈(分片重新分配),同步服务重试写入成功,无死信增多。
- MySQL主库切换:触发主从切换,Canal自动重连新主库,Binlog位点不丢失。
3.9 方案评估与潜在风险
- Kafka消费者Offset管理:若批量处理时服务崩溃未提交Offset,重启后会重复消费批量,须依赖ES版本乐观锁保证幂等,但大批量重复可能造成ES写入负载过高。需设置
max.poll.records合理值(如500),避免重复量过大。 - 高并发版本冲突:热点商品同时被多个事件更新(如库存、价格),
_version乐观锁冲突率增高,同步服务需捕获VersionConflictEngineException,可选择忽略旧版本事件(因为更新的版本号更大,旧事件版本号小会被拒绝,不影响最终正确)。 - CDC链路监控成本:需要监控Canal、Kafka堆积、消费者Lag、ES写入延迟等,建议采用ELK+Metricbeat组合或Prometheus+Grafana统一监控。
- 数据对账补偿成本:全量重建索引在2000万商品规模下,
_reindex或批量写入约需数十分钟,期间尽量降低写入,业务可接受临时降级(如跳过对账期)。
4. 企业级知识库与BI分析:PostgreSQL + ES + Kafka 联合架构
4.1 业务需求与约束
企业内部知识库管理5000万篇文档(技术文档、研究报告、Wiki等)。需求:
- 元数据管理:文档标题、作者、部门、发布时间、自定义标签、动态属性(如项目编号、客户名)等,要求强一致性(ACID)。
- 全文搜索:对文档内容(纯文本)进行高亮、模糊匹配、权重排序,索引更新延迟<10秒。
- 语义检索:基于文档内容向量(Embedding)的相似性搜索,支持“找类似文档”。
- BI分析:跨部门、时间、标签等多维度统计,需关联元数据和全文关键词。
- 权限控制:文档有可见范围,搜索时需根据用户权限过滤。
- 备份与恢复:ES索引丢失后可快速从PG重建。
4.2 分片与索引方案
PostgreSQL(版本16):
- 主表
documents_meta:列doc_id(UUID主键),title,author,department,tags(text[],GIN索引),attributes(JSONB,GIN索引),content_text(text,存储纯文本内容,用于备份重建ES索引),create_time,update_time。 - 按
create_time月份进行范围分区,提高大表管理效率。分区表自动创建(pg_partman)。 - 权限表
doc_permissions:doc_id, user_id, permission_level。
ES索引设计:
documents_search:全文搜索索引,3主分片 × 2副本。映射:doc_id(keyword),title(text, ik_max_word),content(text, ik_max_word),department(keyword),tags(keyword),attributes(nested,对应PG JSONB),create_time(date),update_time(date)。documents_vector:向量索引,1主分片 × 2副本。映射:doc_id(keyword),embedding(dense_vector, dims=768, index=true, similarity=cosine)。分片数1是为避免跨分片KNN查询合并大量结果带来的性能问题,且5000万文档的向量数据单分片约50GB(768维×4字节×5000万≈150GB),可通过分段索引或后续优化(如使用index_options降维)。
4.3 复制与高可用拓扑
flowchart TD
App(业务应用)
PG_Write(PG主库 Patroni+etcd)
PG_Standby(PG备库)
Debezium(Debezium PG Connector)
Kafka(Kafka)
SyncSvc(搜索同步服务)
EmbedSvc(Embedding服务)
ES_Search(ES documents_search)
ES_Vector(ES documents_vector)
PG_Write -- 逻辑复制 PUBLICATION --> Debezium --> Kafka
Kafka -- 消费 --> SyncSvc --> ES_Search
Kafka -- 相同Topic --> EmbedSvc -- 写入 --> ES_Vector
App -- 元数据读写 --> PG_Write
App -- 全文/语义搜索 --> ES_Search & ES_Vector
PG_Write -- FDW查询 --> ES_Search
四层说明:
- 数据拓扑:PG作为记录系统(System of Record),管理所有元数据和文档内容,保证ACID。ES作为派生数据系统(Derived Data),通过CDC实时同步提供高性能搜索。向量索引独立于文本索引,由专门的Embedding服务消费同一CDC流生成。
- 复制拓扑:PG使用Patroni+etcd实现自动故障切换,备库用于读写分离和备份。Debezium PG Connector利用PG的逻辑解码功能(pgoutput插件)订阅指定表变更。Kafka作为数据总线解耦。ES集群可单独实施跨机房CCR灾备(同场景2)。
- 流量路径:文档创建/更新 → App → PG事务提交 → WAL → 逻辑解码 → Debezium → Kafka Topic
cdc.pg.documents(8分区,按doc_id哈希)→ 两个消费者组:(1)搜索同步服务写入documents_search;(2)Embedding服务生成向量写入documents_vector。 - 混合查询路径:应用可分别查询PG和ES再合并,或利用
postgres_fdw在SQL中直接关联ES外部表(适用于简单关联分析)。
4.4 CDC数据管道与配置
PG逻辑复制配置:
ALTER SYSTEM SET wal_level = logical;
-- 重启PG生效
CREATE PUBLICATION pub_docs FOR TABLE documents_meta;
Debezium PG连接器配置(Kafka Connect):
{
"name": "pg-docs-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"plugin.name": "pgoutput",
"publication.name": "pub_docs",
"slot.name": "debezium_docs",
"table.include.list": "public.documents_meta",
"key.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"heartbeat.interval.ms": "5000",
"slot.drop.on.stop": "false"
}
}
设计要点:
pgoutput是PG内置插件,无需额外安装,兼容性好。- 固定
slot.name避免连接器重启后创建新slot导致WAL堆积。 heartbeat.interval.ms定期发送心跳,保持slot活跃,且便于监控延迟。slot.drop.on.stop: false防止误删slot导致全量重新同步。
搜索同步服务:消费Kafka后,将attributes JSONB字段转换为ES的nested文档:
{
"doc_id": "123",
"attributes": [
{ "key": "project", "value": "Alpha" },
{ "key": "customer", "value": "Acme" }
]
}
保证支持嵌套查询(如attributes.key=project AND attributes.value=Alpha)。
Embedding服务:消费同一Topic,获取content_text和doc_id,调用外部模型(如自建BERT服务)生成向量,写入documents_vector。注意:向量索引的写入吞吐可能成为瓶颈,可采用异步批处理(积累1000条或1秒间隔写入_bulk)。
4.5 混合查询方案详解
场景A:权限过滤全文搜索 需求:用户搜索关键词后,仅返回其有权限的文档。 方案:应用层两步查询。
- ES搜索:
GET /documents_search/_search使用match查询,返回Top 200候选doc_id列表和得分。 - PG权限过滤:
SELECT doc_id FROM doc_permissions WHERE doc_id IN (候选ID列表) AND user_id = ?,取交集。 - 若交集数量不足,可扩大ES召回数量继续过滤,或直接在ES中存储权限信息(冗余)但需处理权限变更同步。 选择应用层过滤的考虑:权限变更频繁度适中,避免ES中权限冗余导致的不一致。
场景B:BI跨维度聚合 需求:统计2024年各部门的标签词云。 方案:混合聚合——应用层拆分查询然后合并。
- PG查询:
SELECT department, array_agg(DISTINCT tag) FROM documents_meta WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31' GROUP BY department,得到各部门的所有标签。 - 可选ES聚合:如果需要按标签出现频次排序,可对PG返回的每个部门的标签列表,批量发送ES
terms聚合(多个查询)得到高频词。 - 或者利用
postgres_fdw:在PG中创建ES外部表elasticsearch_tags,通过SQL JOIN实现,但复杂JOIN可能导致ES大量数据拉取,仅适合数据量小的聚合。
示例:postgres_fdw配置(使用elasticsearch-fdw扩展):
CREATE SERVER es_server FOREIGN DATA WRAPPER elasticsearch_fdw
OPTIONS (host 'http://es-coord:9200');
CREATE FOREIGN TABLE es_doc_tags (
doc_id text,
tags text[]
) SERVER es_server OPTIONS (index 'documents_search');
然后执行关联查询,由FDW驱动查询ES并关联本地PG表,对优化器要求高。
4.6 PG作为ES的“主数据源”与“备份源”
备份策略:PG保存了完整的content_text,因此可视为ES的源和备份。
全量重建流程:
- 暂停CDC消费者(防止重建期间新旧数据冲突)。
- 使用
COPY (SELECT doc_id, title, content, ... FROM documents_meta) TO '/tmp/dump.csv' CSV导出。 - 通过自定义工具将CSV转换为
_bulk请求体,批量写入ES新索引documents_search_new。 - 切换别名
documents_search到新索引。 - 重启CDC消费者,从暂停位点继续消费(可能少量重复,幂等处理)。 此方案适合ES数据完全丢失或映射大改时的重建,成本低但速度受限于导出和写入吞吐(5000万文档预计数小时)。
4.7 连接池与资源规划
- PG:PgBouncer连接池,事务模式,应用连接数50,Debezium专用连接5,FDW连接10。
- ES搜索服务:同步服务实例2,每个
maxConnPerRoute=5;向量服务实例2,连接配置相同。 - Embedding服务:需GPU资源(若实时),模型推理延迟需控制在50ms内,否则可异步离线,延迟可接受10s。
- Kafka:8分区,消费者组并行度匹配分区数。
4.8 验证策略
混合压测模型:
- 全文搜索ES:3000 QPS(
match+highlight,返回Top 20)。 - 元数据查询PG:2000 QPS(复杂JSONB查询,如
attributes @> '{"project":"Alpha"}')。 - 混合分析查询:500 QPS(50% PG+ES应用层组合,50% PG单表聚合)。
- 文档写入:100 docs/s新文档或更新,观察CDC延迟。
关键指标与通过标准:
- ES搜索P99 < 100ms,PG复杂查询P99 < 50ms。
- CDC延迟P99 < 10s(从PG写入到ES可见)。
- PG逻辑复制槽位未膨胀:监控
pg_replication_slots的restart_lsn与pg_current_wal_lsn()距离,确保<1GB。 - ES
dense_vector索引内存占用,KNN查询P99 < 200ms(5000万数据量下精确搜索可能需限制返回数)。 - 故障切换测试:杀死PG主库,Patroni自动提升备库,Debezium重连并继续同步,无数据丢失。
4.9 方案评估与潜在风险
- PG JSONB GIN索引膨胀:频繁更新JSONB字段会导致索引碎片,需定期
REINDEX CONCURRENTLY。使用jsonb_path_opsGIN索引可以减小大小但仅支持路径操作,需根据查询模式选择。 - 逻辑复制槽风险:若Debezium离线或消费缓慢,WAL会持续累积,可能填满磁盘,导致PG宕机。必须设置
max_slot_wal_keep_size限制(PG 13+),并监控。 - 向量索引内存:768维×5000万向量原始大小约150GB,加上HNSW图结构,实际内存需求更大。单个数据节点内存必须>256GB,或使用多分片并将向量索引拆分到多个节点。可考虑使用
dimension压缩或量化技术。 - 混合查询的性能瓶颈:应用层合并可能成为瓶颈,尤其是大量数据过滤时。需对ES召回数量限制,并确保PG查询使用索引。
- 数据模型演化:ES映射变更需重建索引,可通过别名切换完成,但依赖PG备份重建。
5. 从协同架构到全栈一体化(预留接口)
本文仅聚焦于搜索引擎与数据库的协同设计。在生产环境中,完整的系统架构往往还需要引入:
- 缓存层(Redis):缓存高频搜索查询结果或热门商品信息,进一步降低ES负载,提升响应速度。Redis集群的哨兵模式和分片策略将在后续缓存专题中深入。
- 分布式事务(Seata):在需要强一致性的业务(如订单创建同时需要同步写入ES)中,Seata AT模式或TCC模式可协调MySQL和ES的事务边界,保证ACID,避免CDC最终一致性的时间窗口问题。
- 服务网格(Istio):实现智能流量管理、灰度发布和熔断限流,提升微服务间调用的可靠性,特别是ES集群升级期间的无损流量切换。
- 全链路压测与容量规划:结合影子库、影子索引实现生产全链路压测,验证架构极限。
这些组件将在后续阶段逐一展开,并最终在第四阶段 “架构决策与业务设计” 中通过多个大型全栈案例(如秒杀系统、AI智能客服系统)进行统一整合,帮助读者形成从底层到应用层的完整架构视野。
Kafka的分区策略、ISR机制、幂等生产者等详细原理,详见第二阶段 “Kafka深度与流处理” 系列。
全栈一体化预留架构图:
flowchart TD
User(用户)
CDN(CDN)
Gateway(API网关)
Redis(Redis缓存)
MySQL(MySQL/PostgreSQL)
ES(Elasticsearch)
Kafka(Kafka)
Seata(Seata事务协调器)
User --> CDN --> Gateway
Gateway -- 读 --> Redis
Redis -- 未命中 --> ES
Gateway -- 写 --> Seata
Seata -- 分支事务1 --> MySQL
Seata -- 分支事务2 --> Kafka
Kafka --> SyncSvc(同步服务) --> ES
ES -- 回写缓存 --> Redis
说明:引入Redis后,查询优先访问缓存,命中率可达到90%以上,极大降低ES压力。写操作由Seata管理分布式事务,确保MySQL和ES(或Kafka)的一致性。此架构将在后续系列详细展开。
面试高频专题
设计题1:千万级商品搜索系统
需求详述
某大型电商平台拥有2000万SKU,日均搜索请求500万次。系统需要7×24小时持续服务,核心非功能指标:
- 搜索复合查询(全文+类目筛选+价格区间+评分+嵌套SKU属性)P99延迟 < 50ms,P999 < 200ms。
- 自动补全:用户输入2字符后触发,P99 < 20ms。
- 数据变更(商品新增、价格/库存更新)约5万次/天,要求近实时可见(<5s)。
- 保留2年历史数据,近期3个月为高频访问,3
12个月为温数据,1224个月为冷数据。 - 服务器总数量不超过12台,需混合SSD和HDD以控制成本。
约束与指标
- 写入QPS不高(峰值约10 writes/s),但要求近实时。
- 查询QPS高(5000+),且组合条件复杂,包含nested查询。
- 需支持拼音搜索、同义词扩展。
- 排序需融合业务因子(销量、评分、新品时间衰减)。
- 不允许停机扩展(通过Rollover实现零停机扩容)。
架构推导
面对上述需求,单一索引无法同时满足高吞吐的全文检索与高并发的补全请求,且冷热数据混合会导致资源争抢。因此设计两大索引体系,配合ILM冷热分离,并定制分析器和排序策略。
索引划分
-
主搜索索引
products_search- 分片:3个主分片,2个副本。
计算:2000万文档 × 2KB ≈ 40GB,每分片约13GB,符合10~50GB最佳实践。3分片可并行利用3个数据节点,加速搜索。 - 别名:
products_search(只读) /products_search_write(只写,用于Rollover)。
- 分片:3个主分片,2个副本。
-
自动补全索引
products_suggest- 分片:1个主分片,2个副本。
数据压缩后<2GB,单分片即可支持10000+ QPS,避免跨分片合并。
- 分片:1个主分片,2个副本。
映射与分析器
主索引映射设计(关键字段):
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2,
"refresh_interval": "1s",
"analysis": {
"analyzer": {
"ik_smart_pinyin": {
"type": "custom",
"tokenizer": "ik_smart",
"filter": ["pinyin_filter", "lowercase"]
}
},
"filter": {
"pinyin_filter": {
"type": "pinyin",
"keep_full_pinyin": true,
"keep_joined_full_pinyin": true,
"remove_duplicated_term": true
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"pinyin": { "type": "text", "analyzer": "ik_smart_pinyin" },
"keyword": { "type": "keyword" }
}
},
"price": { "type": "scaled_float", "scaling_factor": 100 },
"sales": { "type": "integer", "doc_values": true },
"rating": { "type": "float", "doc_values": true },
"create_time": { "type": "date" },
"sku_attributes": {
"type": "nested",
"properties": {
"attr_name": { "type": "keyword" },
"attr_value": { "type": "keyword" },
"stock": { "type": "integer" },
"sku_price": { "type": "scaled_float", "scaling_factor": 100 }
}
}
}
}
}
设计解读:
ik_max_word索引时细粒度切分,保证召回;搜索时ik_smart粗分,提升精度。拼音子字段利用ik_smart_pinyin支持拼音输入。scaled_float价格以分为单位存储,比float节省内存且无精度丢失。nested保证SKU属性查询的关联性:查询“颜色=红 AND 容量=128G”只会匹配同一嵌套对象,避免跨SKU假阳性。代价是写入时需要索引整个嵌套数组。
自动补全索引映射:
{
"mappings": {
"properties": {
"suggest": { "type": "completion" },
"category_id": { "type": "keyword" }
}
},
"settings": {
"number_of_shards": 1,
"number_of_replicas": 2,
"refresh_interval": "-1"
}
}
refresh_interval: -1禁止自动刷新,改为每5分钟手动调用_refresh,降低写入开销。补全实时性要求不高。
集群拓扑与冷热分层
12台服务器分配:
- 3 Master节点:
master:true, data:false,4C16G,轻量。 - 3 Hot数据节点:
node.attr.data: hot,SSD RAID0,64G内存(堆31G),承载近3月数据及写入。 - 3 Warm数据节点:
node.attr.data: warm,HDD,48G内存,承载3~12月数据。 - 2 Coordinating节点:16C32G,专注查询分发与结果合并,启用
adaptive_replica_selection。 - 1台备用或用于其他管理组件,满足≤12台约束。
机架感知:cluster.routing.allocation.awareness.attributes: rack_id,保证主副分片跨机架。
ILM策略:
- Hot (0-90d):
rollover { max_primary_shard_size: 50GB, max_age: 30d },priority: 100。 - Warm (90-365d):迁移到
data:warm,shrink到1分片,forcemerge到1段,副本降为1,priority: 50。 - Cold (365-730d):
searchable_snapshot到S3,零副本,priority: 0。 - Delete (730d后)。
搜索排序实现
基础BM25结合业务信号通过function_score:
{
"query": {
"function_score": {
"query": { "bool": { "must": [...], "filter": [...] } },
"functions": [
{ "field_value_factor": { "field": "sales", "factor": 0.3, "modifier": "log1p", "missing": 0 } },
{ "field_value_factor": { "field": "rating", "factor": 0.2, "missing": 0 } },
{ "gauss": { "create_time": { "origin": "now", "scale": "30d", "decay": 0.5 } } }
],
"score_mode": "sum",
"boost_mode": "multiply"
}
}
}
log1p平滑销量(避免马太效应),gauss函数使30天内的新品得分衰减一半。
个性化预留:后续可在functions中增加script_score,从用户画像读取权重动态计算。
连接池规划
20个搜索服务实例,每个实例ElasticsearchClient连接池配置:
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setDefaultMaxPerRoute(10); // 每个协调节点最多10连接
cm.setMaxTotal(200);
全局最大连接数:20×10×2(协调节点) = 400,远低于ES http.netty.worker_count。
架构图
flowchart LR
User(用户) --> CDN(CDN) --> GW(API网关)
GW --> Coord1(协调节点1) & Coord2(协调节点2)
Coord1 & Coord2 --> Hot1(Hot SSD) & Hot2(Hot SSD) & Hot3(Hot SSD)
Coord1 & Coord2 --> Warm1(Warm HDD) & Warm2(Warm HDD) & Warm3(Warm HDD)
Master1(Master) & Master2(Master) & Master3(Master) --- Hot1 & Warm1
查询时序图
sequenceDiagram
participant App as 搜索服务
participant Coord as 协调节点
participant Hot as Hot Data节点
participant Warm as Warm Data节点
App->>Coord: 复合查询 (Q:手机, category:123)
Coord->>Coord: 解析索引,确定目标分片
par 并行子请求
Coord->>Hot: 查询分片0(主/副)
Coord->>Hot: 查询分片1
Coord->>Hot: 查询分片2
end
Hot-->>Coord: 返回局部结果+聚合
Coord->>Coord: 合并结果,计算总分
Coord-->>App: 最终结果
验证策略
- 工具:JMeter模拟用户请求,ES Rally基准测试。
- 场景:
- 搜索复合查询5000 QPS (80%含筛选+聚合)
- 自动补全10000 QPS
- 混合1000 bulk写入/s
- 指标:
- P99搜索 < 50ms,P999 < 200ms,补全P99 < 20ms
- ES集群CPU < 70%,堆 < 75%,无
thread_pool.rejected增长 - GC停顿 < 200ms
- 通过标准:上述指标全部达成,且在写入压力下搜索延迟无明显上升。
风险与追问
风险:
nested更新需重写整个数组,若SKU变更频繁导致写入放大。可评估扁平化存储+match替代方案。- Warm阶段HDD查询延迟可能上升至200ms以上,需告知用户或延长Hot保留期。
- Cold阶段
searchable_snapshot首次加载延迟秒级,可采用预热脚本。
追问1:如果搜索P99超标,怎么排查?
慢日志定位慢查询 → 分析
profile输出 → 检查热点分片负载、GC → 优化查询(如减少track_total_hits、调整nested查询)→ 扩容节点或增加协调节点。
追问2:如何在不重建索引的情况下增加拼音搜索?
利用
update_by_query更新title.pinyin子字段,在映射中已预定义,只需填充。
设计题2:跨机房ES灾备架构
需求详述
核心搜索服务部署在同城双机房A与B(相距30km,网络RTT<2ms,双专线冗余)。灾备指标:
- RPO < 10秒(允许丢失至多10秒写入数据)
- RTO < 60秒(从故障确认到灾备集群接管写入)
- 平时机房A承担全部写入及80%读流量,机房B承担20%读流量。
- 写入QPS ~500,搜索QPS ~10000。
约束
- 必须物理隔离,不能跨机房组成同一集群,防止脑裂。
- 故障切换需自动化(脚本/编排),不允许人工操作提升。
- 回切后数据必须完整,不能出现回滚。
架构推导
使用ES的CCR (Cross-Cluster Replication) 实现索引级异步复制。CCR在分片级别工作,Follower从Leader拉取translog增量,延迟极低,满足RPO。
集群与分片设计
- 每个索引统一 3主分片 × 2副本(机房A内一主一副,机房B通过CCR同步一个Follower副本)。
- 两个机房各部署独立ES集群:3 Master + 6 Data + 2 Coordinating,节点配置一致(灾备集群可按60%规格,但节点数相同以保证切换后承载全量)。
CCR配置
在机房B注册远程集群A:
PUT /_cluster/settings
{
"persistent": {
"cluster": {
"remote": {
"cluster_A": {
"seeds": ["192.168.1.10:9300", "192.168.1.11:9300"]
}
}
}
}
}
创建Follower索引:
PUT /_ccr/_follow/products_search-follow
{
"remote_cluster": "cluster_A",
"leader_index": "products_search",
"max_read_request_operation_count": 5000,
"max_outstanding_read_requests": 12,
"read_poll_timeout": "1m",
"max_write_buffer_size": "512mb"
}
参数解读:
max_read_request_operation_count控制每次拉取操作数,平衡网络与内存。read_poll_timeout长轮询超时,同城低延迟可设短时间快速感知Leader写入。max_write_buffer_size防止短暂写入高峰导致Follower追赶滞后。
监控命令 GET /_ccr/stats 返回 leader_lag_in_millis,设置告警:>8000ms黄色,>15000ms红色。
自动化切换SOP
- 探测:Consul每5s请求
_cluster/health?timeout=30s,3次连续超时(15s)判定机房A故障。 - 校验:查询灾备集群
GET /_ccr/stats,确认leader_lag_in_millis <= 10000,Follower状态ACTIVE。 - 提升(脚本):
POST /_ccr/_pause_follow/products_search-follow POST /_ccr/_unfollow/products_search-follow PUT /products_search/_settings {"index.blocks.write": false} - 切流:更新DNS/HAProxy,将VIP指向机房B集群,TTL预先设为30s。
- 监控:持续观察新主集群健康与错误率。
回切方案
机房A恢复后:
- 暂停机房B写入,设为只读。
- 通过快照或
_reindex将数据全量拷贝到机房A。 - 在机房A重建CCR(Leader为A,Follower为B),等待同步追上。
- 逐步切回流量,恢复原拓扑。
状态机图
stateDiagram-v2
[*] --> Normal : 双集群正常
Normal --> Probing : 探测超时
Probing --> VerifyLag : 检查CCR Lag
VerifyLag --> Promote : Lag满足RPO
VerifyLag --> ManualIntervention : Lag过大,需人工决策
Promote --> FailoverComplete : 提升成功,流量切换
FailoverComplete --> Recover : 原集群恢复
Recover --> Normal : 回切完成
验证与演练
每月进行一次全链路切换演练,记录实际RTO、RPO。使用脚本在机房A写入带时间戳的测试数据,故障注入后验证丢失量。通过标准:RTO < 60s,RPO < 10s,切换期间应用错误率<5%。
风险与追问
追问1:如果专线中断但机房A正常,如何避免双写?
两集群不共享master,CCR中断后Follower依然是只读,不会自动提升。脚本只在探测到A不可达时才执行提升,因此无脑裂风险。
追问2:RPO超标怎么办?
检查网络延迟、写入突发。可临时增大
max_write_buffer_size,或应用侧对写入记录本地WAL用于极端场景补数。
设计题3:电商交易与搜索中台 (MySQL + ES + Kafka)
需求详述
日均订单500万,商品SKU 2000万。交易链路(订单、支付)强一致依赖MySQL;搜索和订单列表查询由ES提供最终一致性(延迟<5s)。需要商品全文搜索、订单按用户ID查询,以及后台报表聚合。数据变更:商品更新、订单状态流转,需可靠同步至ES。
约束
- MySQL订单分16库(
user_id % 16),要求ES订单查询同样能精准路由到单分片。 - CDC同步不能阻塞主交易流程。
- 最终一致性需自动检测和补偿,差异<0.1%。
架构推导
采用Canal + Kafka + ES 的标准CDC管道。
ES索引设计
products_search:3主分片×2副本,_routing=category_id,同类商品集中,优化筛选聚合。orders_search:16主分片×1副本,_routing=user_id,与MySQL分库对齐。查询时应用层计算user_id % 16直接路由到单分片。
CDC管道详细设计
MySQL:开启ROW格式binlog,GTID模式。 Canal:
canal.instance.filter.regex = shop_db\\.products,shop_db\\.orders
canal.mq.topic = cdc.shop_db.products
canal.mq.partition = products.category_id.hashCode() % 12
canal.mq.partitionsNum = 12
商品Topic 12分区,因为ES 3分片,12是3的整数倍,同一category_id的事件进入同一Kafka分区,保证有序,且写入ES时通过_routing准确定位分片。
订单Topic 16分区,按user_id哈希。
同步服务(6实例消费者组):
// 核心处理逻辑伪代码
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
BulkRequest bulk = new BulkRequest();
for (ConsumerRecord rec : records) {
BinlogEvent evt = parse(rec.value());
if (evt.isInsertOrUpdate()) {
IndexRequest req = new IndexRequest(indexName)
.id(evt.getDocId())
.routing(evt.getRouting()) // category_id or user_id
.source(evt.getJson(), XContentType.JSON)
.version(evt.getVersion()) // 外部版本号(updated_at毫秒)
.versionType(EXTERNAL);
bulk.add(req);
} else if (evt.isDelete()) {
bulk.add(new DeleteRequest(indexName).id(evt.getDocId()).routing(evt.getRouting()));
}
}
BulkResponse resp = esClient.bulk(bulk);
if (resp.hasFailures()) {
sendToRetryTopic(extractFailed(records, resp)); // 重试Topic
}
consumer.commitSync(); // 手动提交
}
幂等与顺序保证:利用updated_at时间戳作为外部版本号,version_type=external保证版本单调递增,旧事件被拒绝,防止消息乱序覆盖。
重试与死信:
- 失败文档发送至
cdc.retry.products(延迟2分钟重试)。 - 超过3次进入
cdc.dlq.products,触发人工干预。
一致性对账
每小时定时任务:
-- MySQL
SELECT COUNT(*) FROM products WHERE updated_at > NOW() - INTERVAL 1 HOUR;
对比ES相同时间范围的count。差异>0.1%时,通过Spark读取MySQL全表,按category_id分批重建ES索引,或回放Kafka保留期内消息(7天)。
冷热分离
- ES商品索引:ILM Hot(90d) -> Warm(365d) -> Cold(730d快照) -> Delete。
- ES订单索引:ILM Hot(90d) -> Warm(365d shrink&forcemerge) -> Delete(365d后删除,数据存MySQL归档库)。
- MySQL:
pt-archiver将6个月前的订单数据归档至历史库。
架构图与数据流
flowchart TB
App[业务应用] --> MySQL[(MySQL 主库)]
MySQL -- binlog --> Canal --> Kafka
Kafka --> SyncSvc[同步服务 6实例]
SyncSvc -- bulk写入 --> ES[(ES集群)]
SyncSvc -- 失败 --> RetryQ[重试Topic] --> SyncSvc
RetryQ -- 超过阈值 --> DLQ[死信Topic]
App -- 搜索读 --> ES
验证策略
- 压测:JMeter 7000 QPS (70%读ES, 25%写MySQL, 5%聚合),同时Kafka注入10000 events/s。
- 指标:搜索P99<50ms,订单P99<100ms,CDC延迟P99<5s,MySQL主从延迟<1s。
- 故障注入:Kafka broker宕机、ES数据节点宕机,验证自愈和重试。
风险与追问
追问1:高并发下_version冲突频繁怎么办?
冲突是乐观锁的正常表现,旧版本事件被拒绝即视为已更新,无副作用。需监控冲突率,过高说明热点文档,可考虑合并更新或应用层限流。
追问2:对账差异持续超过阈值如何处理?
触发全量补偿:暂停CDC消费者,新建索引,从MySQL全量导入后切换别名,再重启消费者。
设计题4:企业级知识库与BI分析 (PostgreSQL + ES + Kafka)
需求详述
5000万篇文档,需求:
- PG:元数据(JSONB)强一致性管理,含动态属性、标签、权限。
- ES:全文搜索(关键词高亮)和语义向量检索(dense_vector, dims=768)。
- BI分析:跨部门/时间/标签统计,需联合PG和ES。
- CDC延迟<10s,ES索引丢失可从PG快速重建。
约束
- PG为主数据源,存储文档全文作为ES备份源。
- 向量维度高,内存规划严格。
- JSONB字段写入频繁,索引膨胀需控制。
- 权限过滤需实时。
架构推导
PG与ES职责分离:PG存元数据和内容备份,ES负责高性能检索。
存储设计
PG表:
CREATE TABLE documents_meta (
doc_id UUID PRIMARY KEY,
title TEXT,
author TEXT,
department TEXT,
tags TEXT[],
attributes JSONB,
content_text TEXT, -- 用于ES重建
create_time TIMESTAMPTZ,
update_time TIMESTAMPTZ
) PARTITION BY RANGE (create_time);
CREATE INDEX idx_tags ON documents_meta USING GIN (tags);
CREATE INDEX idx_attributes ON documents_meta USING GIN (attributes jsonb_path_ops);
按月分区,jsonb_path_ops索引比默认GIN小且支持路径查询。
ES索引:
documents_search:3分片×2副本,映射包含title,content(text),attributes(nested),tags(keyword)等。documents_vector:1分片×2副本,embedding字段类型dense_vector,dims=768, similarity=cosine。
CDC管道
PG逻辑复制:
ALTER SYSTEM SET wal_level = logical;
CREATE PUBLICATION pub_docs FOR TABLE documents_meta;
Debezium连接器:
{
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"plugin.name": "pgoutput",
"publication.name": "pub_docs",
"slot.name": "debezium_docs",
"table.include.list": "public.documents_meta"
}
Kafka 8分区,按doc_id哈希。两个消费者组:
- 搜索同步服务:将JSONB转为nested写入
documents_search。 - Embedding服务:生成向量写入
documents_vector,可批量累积。
混合查询方案
权限过滤搜索:
- ES搜索返回Top 200
doc_id。 - 应用层查询PG
permissions表过滤:SELECT doc_id FROM doc_permissions WHERE doc_id = ANY(?) AND user_id = ? - 取交集重新排序返回。
BI分析:应用层分别请求PG(结构化聚合)和ES(标签词云),合并结果。若简单关联可通过postgres_fdw创建ES外部表直接JOIN,但复杂查询建议拆分。
ES备份重建
PG中content_text保存完整内容,可通过COPY TO导出CSV,转换为_bulk请求体写入新索引,切换别名完成重建。
架构图
flowchart TD
App[应用]
PG[(PostgreSQL)]
Debezium[Debezium]
Kafka[Kafka]
Sync[搜索同步] --> ES_Search[documents_search]
Embed[Embedding] --> ES_Vector[documents_vector]
PG -- 逻辑复制 --> Debezium --> Kafka
Kafka -- 消费 --> Sync
Kafka -- 消费 --> Embed
App -- 读写元数据 --> PG
App -- 全文/向量搜索 --> ES_Search & ES_Vector
PG -- FDW --> ES_Search
验证与风险
- 压测:ES搜索3000 QPS,PG 2000 QPS,混合分析500 QPS。指标:ES P99<100ms,PG<50ms,CDC延迟<10s。
- 风险:向量索引内存巨大(5000万×768×4 ≈ 150GB),需部署大内存节点或使用多分片/量化索引。
追问:权限变更如何反映到搜索结果?
方案一:接受最终一致性,权限变更不触发ES更新,应用层过滤保证安全。方案二:CDC同步权限表到ES文档的
allowed_users字段,但增加写入量。按业务权衡。
设计题5:日志分析平台(ES + Logstash + Kibana)
需求详述
某互联网公司微服务体系日均产生日志总量达10TB,覆盖Nginx访问日志、应用业务日志、系统syslog等多种来源。需要构建一个集中式的日志分析平台,实现:
- 实时采集:日志从产生到可搜索延迟 < 30秒。
- 全文搜索:支持关键词搜索、多字段过滤、正则表达式模糊匹配。
- 聚合分析:按服务、错误级别、时间范围统计错误率、响应时间趋势。
- 可视化与告警:通过Kibana构建Dashboard实时监控,当ERROR日志频率超过阈值时自动通知运维。
- 数据保留:仅保留30天日志数据以控制存储成本。
- 高写入吞吐:峰值写入可达50万events/s,需要弹性应对突发流量。
约束与指标
- 成本敏感:存储仅保留30天,要求使用HDD存储历史数据。
- 写入压倒查询:查询是偶发的运维或开发行为,对延迟容忍度较高(秒级),但写入绝对不能丢失。
- 日志格式不统一:不同服务日志字段差异大,需要灵活解析。
- 集群稳定性:不允许因个别节点故障导致写入中断。
架构推导
采用经典的 Filebeat + Logstash + Elasticsearch + Kibana 技术栈,利用ES的索引生命周期管理(ILM)实现自动存储分层与过期清理。
索引与分片设计
采用时间分割索引,每天创建一个新索引,命名规则 logs-YYYY.MM.DD。这种设计:
- 使得历史数据删除只需删除整个索引,操作高效。
- 限制了单索引大小,避免分片过大影响性能。
- 查询时可通过索引模式
logs-*跨多个索引搜索。
分片计算:日均10TB原始日志,ES存储压缩后约3TB。采用6个主分片,每分片约500GB/天,仍然偏大。实际应进一步将索引拆分为小时索引或增加分片数,但考虑到30天保留,100个以上分片会增加集群开销,一般取5~10个分片,建议按天索引,8个主分片,1个副本。热数据使用SSD,温数据迁移到HDD。
ILM策略:
- Hot阶段(当天):
refresh_interval=30s(写入优化,降低实时刷新频率),number_of_replicas=0(写入优先,不等待副本)。 - Warm阶段(1天后):
allocate到data:warm节点,shrink到2分片,forcemerge到1段,number_of_replicas=1(增加读取能力)。 - Delete阶段(30天后):删除索引。
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": { "max_size": "50GB", "max_age": "1d" },
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "1d",
"actions": {
"allocate": { "require": { "data": "warm" } },
"shrink": { "number_of_shards": 2 },
"forcemerge": { "max_num_segments": 1 },
"set_priority": { "priority": 50 }
}
},
"delete": {
"min_age": "30d",
"actions": { "delete": {} }
}
}
}
}
索引模板:
PUT /_index_template/logs_template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"index.lifecycle.name": "logs_policy",
"index.lifecycle.rollover_alias": "logs_write",
"number_of_shards": 8,
"number_of_replicas": 0,
"refresh_interval": "30s"
},
"mappings": {
"dynamic_templates": [
{
"strings_as_keyword": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword",
"ignore_above": 1024
}
}
}
],
"properties": {
"timestamp": { "type": "date" },
"service": { "type": "keyword" },
"level": { "type": "keyword" },
"message": { "type": "text", "norms": false }
}
}
}
}
使用动态模板将动态字段默认映射为keyword,避免字段映射爆炸(ignore_above限制长度)。关闭norms节省存储。
采集层设计
Filebeat 部署在每个应用服务器上,轻量级采集文件,配置:
filebeat.inputs:
- type: log
paths: /var/log/app/*.log
fields:
service: my-service
multiline.pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}'
multiline.negate: true
multiline.match: after
output.logstash:
hosts: ["logstash:5044"]
multiline 配置处理堆栈跟踪等多行日志。
Logstash 部署为聚合层,负责解析和过滤:
input {
beats { port => 5044 }
}
filter {
grok {
match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{GREEDYDATA:msg}" }
}
date { match => ["timestamp", "ISO8601"] }
}
output {
elasticsearch {
hosts => ["es-coord:9200"]
index => "logs_write"
pipeline => "logs_pipeline"
user => "logstash_user"
password => "${ES_PASSWORD}"
}
}
持久化队列开启 queue.type: persisted,queue.max_bytes: 10gb,保证ES短暂不可用时数据不丢失。
集群拓扑
- 3 Master节点 (4C16G)
- 3 Hot数据节点:SSD,64GB内存(堆31G),处理当天索引的写入。
- 3 Warm数据节点:HDD,48GB内存,存放1~30天历史索引。
- 2 Coordinating节点:用于查询合并,搜索时承担负载。
- 1 Kibana节点。
性能调优
- 写入侧:使用
_id为日志内容哈希 +routing固定值,避免版本检查开销,允许重复写入(幂等)。设置index.codec: best_compression节省磁盘。 - 查询侧:使用
search.default_search_timeout: 30s,防止长时间查询耗尽线程。协调节点内存足够缓存分片请求。
告警
利用ElastAlert或Kibana Alerting功能,配置规则:
# ElastAlert 规则示例
name: High Error Rate
type: frequency
index: logs-*
filter:
- term: { "level": "ERROR" }
num_events: 100
timeframe: minutes: 1
alert: email
架构图
flowchart LR
Server1[服务器1 Filebeat] & Server2[服务器2 Filebeat] --> Logstash
Logstash -- 持久化队列 --> ES_Hot[ES Hot Data SSD]
ES_Hot -- ILM迁移 --> ES_Warm[ES Warm Data HDD]
Kibana[Kibana] --> ES_Hot & ES_Warm
Alert[ElastAlert] --> ES_Hot & ES_Warm
Alert --> Email[邮件告警]
验证策略
- 写入压测:使用
esrally或自定义生产者模拟50万events/s写入,持续1小时。观察ES拒绝率、CPU、磁盘IO。通过标准:无持续thread_pool.bulk.rejected,写入延迟P99<2s。 - 查询压测:模拟典型运维查询(最近15分钟ERROR日志搜索),100并发。要求响应P99<5s。
- 故障注入:停止一个Hot数据节点,验证写入不间断,副本自动提升。
风险与追问
风险:
- 字段映射爆炸:动态模板仅能控制
string类型,若应用产生大量不同名称的数值字段,仍可能超出映射限制。解决方案:设置index.mapping.total_fields.limit=1000并定期清理无用模板。 - 磁盘写满:水位线必须设好(
disk.watermark.low: 85%,high: 90%),并配合ILM及时删除索引。
追问1:日志写入出现429 (Too Many Requests) 怎么办?
调整ES线程池队列大小
thread_pool.write.queue_size,提高refresh_interval,或临时禁用副本。Logstash端开启指数退避重试。
追问2:如何实现跨集群日志搜索?
使用跨集群搜索(CCS),在Kibana中配置多个远程集群,查询时自动分发。
设计题6:实时数仓加速层(ES + Kafka + Flink)
需求详述
某电商实时数据仓库需处理Kafka中的用户行为事件流(每秒10万条),经过Flink实时聚合后写入ES,为运营BI工具提供快速多维分析。要求:
- 聚合查询响应P99 < 500ms。
- 数据延迟(从事件产生到可查询)< 5秒。
- 支持任意维度组合的OLAP查询,典型维度包括商品类目、地区、时间粒度等,指标为PV、UV、GMV等。
- 数据不丢不重(端到端Exactly-Once)。
约束
- 写入量:聚合后指标流仍有5000 docs/s。
- 查询并发:200 QPS,但单次查询可能扫描百万文档的聚合。
- ES内存资源有限,不能OOM。
架构推导
核心思想:在ES中存储预聚合结果而非原始明细,将聚合维度从数十个降为有限组合,大幅减少数据量和查询复杂度。
索引建模
预聚合索引 dws_metrics:
- 按天创建,3主分片×1副本(查询负载高时可增加)。
- 禁用
_source,只存储doc_values以节省空间。 - 维度字段:
dt(日期keyword),category_id,region,hour等均为keyword并启用doc_values。 - 指标字段:使用
aggregate_metric_double存储预计算的min/max/sum/count,或者用简单的long/double字段存储。 - 开启
eager_global_ordinals: true加速高基数聚合。
映射示例:
{
"mappings": {
"_source": { "enabled": false },
"properties": {
"dt": { "type": "keyword", "doc_values": true },
"category_id": { "type": "keyword", "doc_values": true, "eager_global_ordinals": true },
"region": { "type": "keyword", "doc_values": true },
"hour": { "type": "byte" },
"pv": { "type": "long", "doc_values": true },
"uv": { "type": "long", "doc_values": true },
"gmv": { "type": "double", "doc_values": true }
}
}
}
Flink写入ES
使用 ElasticsearchSink,配置bulk.flush.max.actions=2000,bulk.flush.interval=1s。采用AT_LEAST_ONCE配合幂等_id:_id = dt + category_id + region + hour,保证相同统计窗口的数据覆盖,实现精确一次效果。
DataStream<Metric> aggregated = source
.keyBy(e -> e.getDt() + e.getCategoryId() + e.getRegion() + e.getHour())
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
.aggregate(new MetricAggregate());
aggregated.addSink(ElasticsearchSink.<Metric>builder()
.setHosts("es-coord:9200")
.setEmitter((metric, context) -> {
IndexRequest req = new IndexRequest("dws_metrics_write")
.id(metric.getId()) // 幂等id
.source(metric.toJson(), XContentType.JSON);
context.index(req);
})
.setBulkFlushMaxActions(2000)
.build());
Flink开启Checkpoint,ElasticsearchSink 在checkpoint时执行 flush,配合ES 7.x+的事务日志可实现端到端Exactly-Once。
查询优化
- 所有聚合查询必须使用
composite聚合进行分页,严禁使用termswithsize过大。 - 限制
track_total_hits为false。 - 对于大范围日期查询,通过索引模式限制扫描的索引数量(如
dws_metrics-2024.10-*)。 - 应用侧对常见查询结果用Redis缓存(TTL 5分钟),降低ES压力。
数据一致性
Flink Checkpoint + ES flush 保证数据不丢失。万一ES索引损坏,可从Kafka保留的原始数据重新回放Flink作业重建预聚合索引。
架构图
flowchart LR
Kafka[(Kafka 原始事件)]
Flink[Flink 聚合作业]
ES[(ES 预聚合索引)]
Redis[Redis 缓存]
BI[BI工具]
Kafka --> Flink
Flink -- 写入聚合结果 --> ES
BI -- 查询 --> Redis
Redis -- 未命中 --> ES
ES -- 返回 --> BI
验证策略
- 写入:注入10万events/s,观察Flink端到端延迟<5s,ES写入无拒绝。
- 查询:模拟200 QPS的composite聚合查询(3维度交叉),P99 < 500ms。
- 数据精确性:对比Flink输出与Kafka原始计数。
风险与追问
风险:预聚合不能支持任意维度组合,需提前确定业务所需维度。若出现新需求,需重建索引并重新聚合。
追问1:需要增加一个新维度怎么办?
Flink作业逻辑增加维度字段,新建索引或添加字段(
PUT mapping),回刷历史数据(从Kafka最早位点或离线数据重新计算)。
追问2:ES的composite聚合性能瓶颈在哪里?
高基数维度会导致大量bucket,内存消耗高。可设置
size限制,利用after游标分页。必要时将部分维度下推到Flink预聚合,进一步降低ES扫描量。
设计题7:多租户SaaS搜索服务
需求详述
SaaS服务商提供文档管理搜索功能,目前有2000个租户,数据量悬殊:小租户几百条文档,大租户可达数千万条。需求:
- 数据隔离:租户间绝对不可相互查询到对方文档。
- 性能隔离:大租户的高并发查询不能影响小租户。
- 弹性扩展:当租户数据量增长时,能无缝迁移到独立资源。
- 运维简便:租户数量动态增长,不希望对每个租户手动管理索引。
约束
- ES集群规模有限,不能为每个租户单独创建索引(2000个索引会拖累Master)。
- 查询必须带上租户过滤,不能依赖应用层忘记。
- 需考虑数据备份和恢复的租户粒度。
架构推导
采用混合隔离策略:
- 小租户通过哈希分组共享索引,索引内部使用字段隔离(
tenant_id)。 - 大租户独立索引,独享分片资源。
- 通过别名机制透明路由。
索引与分片设计
共享组索引:创建10个组索引 shared_group_0 到 shared_group_9,每个索引3主分片×2副本。租户根据 tenant_id.hashCode() % 10 确定组。共享组中的文档存储时指定_routing=tenant_id,保证同租户数据在同一分片,提高缓存效率。
独立索引:租户数据量超过阈值(如100万文档或持续高QPS),为其创建专用索引 tenant_<id>,分片数按数据量动态设置(3~6分片)。
别名与安全:
- 每个租户都有一个固定别名
search_<tenant_id>,初始指向其所属共享组索引。 - 当迁移到独立索引时,原子性切换别名指向新索引。
- 利用ES原生安全或Search Guard,为别名绑定文档级过滤
term: { tenant_id: "xxx" },即使应用层漏了filter,ES层也会强制过滤,双重保险。
写入与查询流程
写入时:应用传入tenant_id,路由到当前别名,文档自动带上tenant_id字段。
查询时:应用使用租户别名直接搜索,ES层自动应用权限过滤。
迁移流程(从小租户共享组到独立索引):
- 创建独立索引
tenant_123,设置所需分片。 - 通过
_reindex从共享组索引拷贝租户123的数据到独立索引,使用wait_for_completion=false异步执行。 - 监控拷贝进度,完成后执行原子操作:
POST /_aliases { "actions": [ { "remove": { "index": "shared_group_1", "alias": "search_123" } }, { "add": { "index": "tenant_123", "alias": "search_123" } } ] } - 删除共享组中的旧文档(通过
_delete_by_query带tenant_id:123),释放空间。
资源隔离与限流
- 使用Coordinating节点层或Nginx/Envoy实现基于租户的请求速率限制(根据别名识别)。
- 独立索引可设置
index.routing.allocation.require将大租户索引分配到特定高性能节点,物理隔离。 - 监控每个租户查询的耗时、QPS,设置熔断阈值。
架构图
flowchart LR
TenantA[租户A] & TenantB[租户B] & TenantBig[大租户C]
GW[网关/限流]
AliasA[别名 search_A] & AliasB[别名 search_B] & AliasC[别名 search_C]
Shared[共享组索引 group_0]
Dedicated[独立索引 tenant_C]
ES[(ES集群)]
TenantA & TenantB & TenantBig --> GW
GW --> AliasA & AliasB & AliasC
AliasA --> Shared
AliasB --> Shared
AliasC --> Dedicated
验证策略
- 隔离性测试:租户A的查询中注入其他租户ID,验证返回空或403。
- 性能隔离:模拟大租户高频查询,观测小租户查询延迟不受影响。
- 迁移测试:演练小租户变大租户的迁移过程,确保无停机。
风险与追问
追问1:共享组索引中大租户增长后性能退化,如何提前发现?
监控每个共享组索引的写入QPS、查询延迟、堆使用率。当某组索引平均延迟上升或CPU增高,排查热租户,触发迁移。
追问2:租户跨区域搜索如何处理?
结合跨区域架构,每个区域维护一份租户索引,通过CCR同步全球数据或分区存储。查询由应用层就近路由。
设计题8:跨区域全球搜索架构
需求详述
某全球化电商平台,用户分布在北美、欧洲、亚太三大区域。需要部署全球搜索服务:
- 低延迟:用户本地搜索响应 < 300ms。
- 数据驻留:用户个人数据(如收藏、历史)需遵循GDPR,存储在所属区域。
- 全球产品目录共享:产品信息全球一致,但可分区独立更新(如各区域价格不同)。
- 高可用:任一区域故障不影响其他区域服务。
约束
- 跨洲网络延迟100~200ms,不能强一致性同步。
- 多区域可能同时更新同一产品数据,需要处理冲突。
- 用户搜索通常只关心本区域产品,但有时需要全球搜索。
架构推导
采用多主集群 + CCR异步复制的星型拓扑,结合Geo DNS智能路由。
集群部署
每个区域部署一个独立ES集群:us-east-cluster、eu-west-cluster、ap-south-cluster。每个集群都可读写。
数据分类与同步策略
- 产品目录数据:全球共享,但可由各区域独立更新(如本区价格)。通过CCR实现多主复制:每个区域既是Leader也是Follower,形成环形或星型复制。冲突处理采用
_version最后写入胜出(LWW),业务上确保各区域只更新自己的字段,减少冲突。 - 用户个人数据:不跨区同步,仅存储在用户所属区域集群。搜索时由应用层限制只查询本地。
CCR多主配置
以三个区域为例,建立全连接复制:
- US-East → EU-West (CCR Follower)
- US-East → AP-South (CCR Follower)
- EU-West → US-East (CCR Follower)
- EU-West → AP-South (CCR Follower)
- AP-South → US-East (CCR Follower)
- AP-South → EU-West (CCR Follower)
每个索引在每个集群都有一个Leader和来自其他集群的Follower。为避免冲突,产品写操作固定路由到产品所属的主区域(如产品原始创建区域),其他区域只读产品副本。但这样会丧失本地更新灵活性。另一种方案:产品数据按区域分片,每个区域拥有自己区域产品的Leader,全球搜索时需聚合。
更实际的方案:产品数据由一个“主区域”写入(如美国),其他区域通过CCR同步为只读副本。本地价格等动态数据存储在本地索引,不与全球共享。这样简单可靠。
Geo DNS与路由
- 使用GeoDNS根据用户来源IP解析到最近集群的VIP。
- 搜索服务通过配置中心获取本地ES集群地址。
- 对于全球搜索需求,应用层向其他集群发起异步请求,合并结果,设置超时(如1秒)。
架构图
flowchart TD
subgraph US[北美]
ES_US[ES US-East]
end
subgraph EU[欧洲]
ES_EU[ES EU-West]
end
subgraph AP[亚太]
ES_AP[ES AP-South]
end
User((用户)) --> GeoDNS
GeoDNS --> US_LB[负载均衡] & EU_LB[负载均衡] & AP_LB[负载均衡]
US_LB --> ES_US
EU_LB --> ES_EU
AP_LB --> ES_AP
ES_US -- CCR产品数据 --> ES_EU
ES_EU -- CCR产品数据 --> ES_AP
ES_AP -- CCR产品数据 --> ES_US
验证策略
- 延迟测试:从各区域模拟用户搜索,验证P99 < 300ms。
- 数据同步测试:更新美国产品价格,检查欧洲集群延迟(预期<5秒)。
- 故障演练:断开欧洲集群,验证北美和亚太不受影响。
风险与追问
追问1:CCR环形复制是否会导致数据循环复制?
CCR在内部记录了复制源,不会将Follower数据再次复制出去,ES保证没有循环。
追问2:如何处理不同区域的产品搜索结果排序不一致?
排序依赖于本地化的业务因子(如本地销量),因此各集群独立计算,不一致是可接受的。全球搜索时,由应用层合并结果并重新排序。
追问3:用户旅行到其他区域,如何看到自己的收藏?
可以通过应用层从用户归属区域集群读取,或使用跨集群搜索,但会增加延迟。一般移动场景可容忍稍高延迟,通过CDN或边缘缓存优化。
系统设计速查卡
| 设计维度 | 关键决策点 | 推荐方案 | 验证标准 |
|---|---|---|---|
| 索引分片数 | 数据量、查询模式 | 每分片10-50GB,主分片数 ≤ Data节点×2 | 搜索P99达标,CPU<70% |
| 路由策略 | 查询聚拢性 | _routing = 业务键 | 单分片查询比例>90% |
| 冷热分离 | 访问频率与成本 | Hot(SSD), Warm(HDD), Cold(快照) | 热数据延迟达标,存储成本可控 |
| 灾备 | RPO/RTO | 同城CCR,自动化切换SOP | RTO<60s, RPO<10s |
| CDC管道 | 一致性级别 | 至少一次 + 版本乐观锁 + 定时对账 | 延迟<5s,差异<0.1% |
| 连接池 | 并发连接数 | 每客户端maxConnPerRoute≤10,总连接<ES线程池 | 无rejected |
| 排序 | 业务因子 | BM25+function_score | 人工评测合理,性能无损 |
| 多租户 | 隔离与扩展 | 小租户共享索引+字段隔离,大租户独立索引+别名 | 查询互不影响,迁移透明 |
| 日志分析 | 写入优先级 | 时间分割索引,ILM热温冷,禁用副本优化写入 | 无写入拒绝,30天删除 |
| 实时数仓加速 | 查询效率 | 预聚合,禁用_source,composite分页 | P99<500ms |
| 全球多区域 | 数据驻留与延迟 | 多主CCR + GeoDNS,本地读写 | 本地<300ms,数据最终一致 |
灾备切换SOP速查卡
| 阶段 | 操作 | 命令/工具 | 耗时 |
|---|---|---|---|
| 故障探测 | 健康检查3次超时 | Consul + _cluster/health | 15s |
| 数据校验 | 检查CCR lag | GET /_ccr/stats | 2s |
| 提升 | 暂停→解绑→开放写 | pause_follow→unfollow→设置写块 | 5s |
| 切流 | VIP/DNS切换 | HAProxy / DNS TTL 30s | 30s内 |
| 监控 | 观察新主 | _cat/health,业务监控 | 持续 |
| 回切 | 快照恢复→重建CCR→反切 | 依数据量小时级 | 小时级 |
以上面试专题完整覆盖了从搜索到分析、从单集群到全球化架构的各类核心场景,每题均提供完整设计文档级别的推导过程和关键配置,读者可据此进行模拟面试训练,加深对ES及相关技术栈的系统性理解。
延伸阅读:
- 《Elasticsearch: The Definitive Guide》 by Clinton Gormley
- 《Designing Data-Intensive Applications》 by Martin Kleppmann
- Kafka官方文档:kafka.apache.org/documentati…
- Canal官方Wiki:github.com/alibaba/can…
- Debezium官方文档:debezium.io/documentati…