ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从小时级到毫秒级:千万级关键词匹配性能优化实战

从小时级到毫秒级:千万级关键词匹配性能优化实战 做推荐系统、搜索系统或者广告检索的同学大概率都撞过同一堵墙关键词量级一上来匹配速度就卡得人头皮发麻。我这边遇到的需求比较典型维护着一个由几十个业务方共同使用的关键词平台线上积累了千万级的关键词每天要跟用户搜索词做实时匹配。早期流量小的时候无所谓接口毛糙点也能扛住等到业务量翻了十几倍之后原先那套匹配逻辑彻底绷不住了最离谱的时候一次全量匹配要跑一个多小时用户侧的表现就是搜一下转半天。整个优化过程前前后后折腾了三周把匹配耗时从小时级别压到了分钟以内准确率不降还顺手解决了一批历史遗留的脏数据问题。这篇文章不打算讲什么高深理论纯粹是把我的排查思路、每一步为什么这么改、中间踩过的坑都摊开来说给还在用暴力匹配硬扛的朋友一个参考路线。1. 原始方案为什么这么慢1.1 最初的实现长什么样先还原一下最开始的匹配逻辑。业务场景是这样的一个用户搜索词进来需要在平台上维护的千万级关键词库中找到所有命中的关键词按照业务方设定的优先级排序返回。当时负责这块的同事图省事直接用了最简单的逐条匹配方案。public ListString matchKeywords(String query) { ListString results new ArrayList(); for (String keyword : allKeywords) { if (keyword.length() query.length()) { continue; } if (query.contains(keyword)) { results.add(keyword); } } return results; }代码逻辑确实简单就是拿着用户搜索词在关键词库里从头到尾过一遍用contains判断当前关键词是不是搜索词的子串。千万级的关键词量平均每个关键词二三十个字符一次搜索请求就要跑几千万次字符串匹配耗时可想而知。要是光这么跑也就算了更麻烦的是关键词库里还有大量重复、近义、前缀后缀相同的噪音数据。比如一个品牌词某某手机库里可能同时存在某某手机某某手机官网某某手机最新款等好几个变体每次匹配都要白白扫好几遍。1.2 慢的根源在哪儿一句话概括时间复杂度是O(N*L)N是关键词总量L是关键词平均长度。当N到了千万级、L到了几十个字符单次请求的计算量就是数亿次字符比较。有人可能会说加个索引不就完事了但这里的核心难点在于匹配规则不是精确匹配。用户搜索苹果手机价格关键词库里可能有一条苹果手机这属于典型的子串匹配传统的B树索引、倒排索引在词项级别能加速精确查找面对这种查询词包含关键词的场景就没那么直接了。我当时先用工具做了一轮profile发现String.contains的调用占了将近70%的CPU时间。再细挖下去contains底层是JDK内部的indexOf实现对于短模式串性能尚可但关键词库里有大量三四十个字符的长词每匹配一次就是几十上百次字符比较。还有一层隐蔽的开销contains内部涉及字符数组的逐位比较对CPU缓存极不友好。千万级关键词相当于一个超大数组遍历时缓存命中率很低大量时间浪费在内存读取上。2. 第一轮优化从匹配算法本身下手2.1 用Set去重和哈希索引第一步没有动匹配规则而是先把关键词库洗了一遍。用HashSet重建关键词库天然去掉了重复项更关键的是后续查找可以享受O(1)的平均查找复杂度。但这里有个问题HashSet只能做精确查找子串匹配场景下不能直接我要查苹果手机就返回苹果。所以第一步的真实收益其实来自两个地方。第一是去重后关键词总量下降了不少原来库里大概有五分之一是重复数据这波清洗直接让待匹配数量缩水。第二是改了匹配顺序从遍历所有关键词判断是否被query包含改成了遍历关键词库中长度有序的候选集长词先匹配、短词后匹配一旦命中就可以提前加入结果集。实际操作中我用了一个SortedSet按长度排序然后对每个query只取长度小于等于query长度的关键词做匹配。这一步让单次查询的匹配数从千万级降到了百万级耗时从一小时降到了二十分钟左右。2.2 引入Trie树做前缀匹配业务里很大比例的关键词是品牌词品类词的结构比如某某手机某某耳机这类词有一个特点用户搜索词往往以这些关键词开头。针对这种前缀型匹配我加了一棵Trie树前缀树。class TrieNode { MapCharacter, TrieNode children new HashMap(); boolean isEnd; String keyword; }Trie树的原理是把所有关键词按字符拆开相同前缀共享路径。匹配时只需要沿着用户搜索词的字符一路往下走就能找到所有以搜索词开头或者搜索词以它们开头的关键词。这棵树的构建是一次性的建完之后单次前缀匹配只需要O(L)的字符遍历L是搜索词的长度跟关键词总量彻底脱钩。这一步做完前缀型关键词的匹配速度起飞了但问题也暴露了子串型匹配关键词在搜索词的中间位置出现依然要回到暴力扫描。比如用户搜2024苹果手机发布会关键词苹果手机就不在搜索词开头Trie树帮不上忙。2.3 子串匹配的Aho-Corasick方案解决子串匹配问题的经典方案是AC自动机Aho-Corasick。AC自动机本质上是在Trie树的基础上加了fail指针让匹配失败时能快速跳到下一个可能匹配的位置一趟扫描能找出中所有关键词的命中情况。AC自动机的构建分两步。第一步还是一棵Trie树把所有关键词插入进去第二步BFS构建fail指针原理是KMP算法在树上的推广。匹配的时候从根节点出发逐个字符走树边走不通就沿着fail指针跳走到某个节点发现isEndtrue就说明命中了一个关键词。public ListString match(String text) { ListString results new ArrayList(); TrieNode node root; for (char c : text.toCharArray()) { while (node ! root !node.children.containsKey(c)) { node node.fail; } if (node.children.containsKey(c)) { node node.children.get(c); } if (node.isEnd) { results.add(node.keyword); } } return results; }引入AC自动机之后单次查询匹配的耗时从分钟级降到了秒级。因为AC自动机最坏情况下匹配一个长度为L的文本是O(L匹配数)这个复杂度跟关键词总量没有关系了。但这里有个不得不说的局限AC自动机适合词条本身比较短、量大的场景。我这边关键词里有不少二十字以上的长词建出来的Trie树非常深节点膨胀得厉害内存占用直接飙到了好几个GB而且长词的fail链跳转非常深匹配效率反而被拖累了。后来我对超过15个字符的长词单独走哈希匹配短词走AC自动机长短分流才把内存和耗时稳定下来。3. 第二轮优化从架构和参数入手3.1 关键词分片与并行查询算法层面的优化到顶之后剩下的瓶颈主要在单机处理能力上。千万级关键词建出来AC自动机之后虽然查询很快但构建过程很慢而且内存压力大一台机器扛不住全量数据。这时候我做了分片处理。把关键词库按照业务线拆成多个分片每个分片单独构建AC自动机部署到独立的服务节点上。用户搜索词进来之后网关同时把请求广播到所有分片节点各节点并行匹配最后把结果汇总合并。// 伪代码示意 ListString results shards.parallelStream() .map(shard - shard.match(query)) .flatMap(List::stream) .distinct() .collect(Collectors.toList());分片之后单机内存压力大大缓解每台机器只需要加载自己负责的那部分关键词。并行查询的收益也很明显原来一个查询要在一个节点上从头跑到尾现在拆到多个节点同时跑墙钟时间按照分片数量近似线性下降。分片设计里面有一个细节容易踩坑分片不均匀会导致木桶效应。最早按业务线硬切结果某个业务线关键词量占了总量的一半这个分片就成了瓶颈其他分片跑完了也得等它。后来改成按关键词哈希值取模分片让数据散得更均匀跑得慢的分片问题就解决了。3.2 Java线程池的参数调整并行查询用到了线程池线程池参数一开始用的默认配置结果压测的时候频繁出现线程拒绝异常。原因是分片数量不多但每个分片匹配时内部也开了并行流线程数叠加之后把线程池打满了。这里要讲一个通用经验并行不是什么场景都无脑上。查询引擎内部的匹配计算是CPU密集型操作线程数设置成CPU核心数减一通常比较稳妥留一个核心给操作系统和其他服务。IO密集型的场景则可以适当加大线程数具体要看线程阻塞在什么地方。我调完之后的效果是线程池核心线程数设为CPU核数最大线程数设为核数乘以2队列容量根据压测结果调整到2000。压测参数不能拍脑袋我用JMeter做了梯度压测观察每秒查询数和P99时延的变化最后确定了一个既不吃满CPU又不至于让请求排队过久的阈值。3.3 缓存层的设计与淘汰策略匹配引擎的计算量降下来之后又发现一个新瓶颈大量重复查询。用户搜索词的热度分布非常集中Top 1%的搜索词贡献了大部分流量。对这些高热度搜索词如果每次都走完整的AC自动机匹配其实是很浪费的于是加了一层本地缓存。缓存的设计参考了CPU的多级缓存思路。一级是Caffeine本地缓存命中直接返回二级是Redis缓存本地没命中的去Redis捞Redis也没有的才真正进入匹配引擎计算。CacheString, ListString localCache Caffeine.newBuilder() .maximumSize(100_000) .expireAfterWrite(Duration.ofMinutes(30)) .build(); public ListString matchWithCache(String query) { ListString result localCache.getIfPresent(query); if (result ! null) { return result; } return localCache.get(query, k - matchEngine.match(k)); }Caffeine的底层是W-TinyLFU算法用频率和recency一起决定哪些条目保留下来比简单的LRU更抗热点冲击。这里有三个参数值得多讲几句maximumSize不宜设得太大100万条结果List如果平均每条几十个关键词光这一层就把堆内存吃掉了大几百MBexpireAfterWrite设成30分钟是基于业务观察用户搜索词的热度周期大约在半小时到一小时之间太短导致缓存命中率上不去太长又占内存缓存击穿问题也需要兜底Caffeine的get方法内部有并发控制同一个key只有一个线程去加载但跨节点的Redis缓存击穿还是得靠互斥锁或者空值缓存来防4. 第三轮优化数据预处理和索引重构4.1 关键词规范化与停用词过滤字典数据质量直接决定了匹配准确率。早期项目数据很脏光是苹果手机就有全角半角、大小写、前后空格等好多种写法。全角字符和半角字符在Java的contains看来是完全不同的字符导致很多本应该命中的关键词被漏掉了。我写了一个关键词标准化处理器把输入的原始词条依次做了几件事全角转半角、大写转小写、去除首尾空格、压缩中间连续空格、过滤长度小于2的无效词条。标准化之后的关键词才真正进入Trie树和AC自动机的构建流程。public static String normalize(String raw) { String halfWidth NFKC.normalize(raw); String lower halfWidth.toLowerCase(Locale.ROOT); String trimmed lower.trim(); return trimmed.replaceAll(\\s, ); }停用词过滤针对的是的了吗这类无意义的高频词。如果不做过滤用户搜苹果的手机时关键词苹果会命中关键词手机也会命中但关键词苹果手机反而不命中因为中间插了个的字。这在实际业务中是有歧义的需要和产品经理对齐是保留这种部分命中还是要求精确连词命中。我的方案是两套匹配规则并行一套宽松规则用于召回一套严格规则用于精排。宽松规则匹配后进入精排环节时严格规则负责判断关键词是否真的完整出现在搜索词的连续字符中避免把苹果的手机返回成命中苹果手机。4.2 双层索引短词哈希加长词树前面提到过长词在AC自动机里的问题这节说我的具体方案。把关键词按长度分成两类长度小于12的进入Trie树和AC自动机长度大于等于12的进入一个基于倒排的哈希索引。长词索引的结构是每个长词拆成若干固定长度的gram比如按双字切分苹果手机发布会拆成苹果果手手机机发发布布会这六个bigram。每个bigram映射到一个关键词列表。匹配的时候先把搜索词也切成bigram集合从这些bigram对应的关键词列表里取交集候选集大幅缩小后再做精确包含判断。MapString, SetString bigramIndex new HashMap(); public void addKeyword(String keyword) { SetString grams segmentToBigrams(keyword); for (String gram : grams) { bigramIndex.computeIfAbsent(gram, k - new HashSet()).add(keyword); } } public ListString matchLongKeywords(String query) { SetString queryGrams segmentToBigrams(query); SetString candidates new HashSet(); boolean first true; for (String gram : queryGrams) { SetString keywords bigramIndex.get(gram); if (keywords null) { continue; } if (first) { candidates.addAll(keywords); first false; } else { candidates.retainAll(keywords); } } // 对候选集做精确匹配验证 return candidates.stream() .filter(query::contains) .collect(Collectors.toList()); }这个方案看着复杂实际效果很好。因为bigram索引把候选集从几百万压缩到了几百个在这几百个里做精确包含判断就很快了。跟全量AC自动机相比内存省了大概40%长词的误匹配率也降了因为bigram的存在本身就是一个很强的约束条件。4.3 热词表的动态刷新机制关键词库不是一成不变的业务方每天都会新增和下线关键词。如果每次变更都全量重建AC自动机系统就要停机几十分钟这在业务上是不可接受的。我设计了一套双缓冲加增量刷新的机制。核心思路是准备两份索引一份线上服务用一份后台构建用。后台增量更新构建完新索引后通过一个原子引用切换让线上请求瞬间切换到新索引上。volatile KeywordIndex currentIndex; volatile KeywordIndex buildingIndex; public void reload() { KeywordIndex newIndex new KeywordIndex(); newIndex.buildWithIncremental(sinceLastVersion); buildingIndex newIndex; } public void switchIndex() { if (buildingIndex ! null) { currentIndex buildingIndex; buildingIndex null; } }增量更新不是简单的新增关键词插入、删除关键词移除因为Trie树和AC自动机的fail指针在插入新节点后可能需要整体重建。我拆了两个粒度新增关键词直接插入当前索引并标记脏节点删除关键词做逻辑删除每隔30分钟触发一次全量重建保证索引结构没有太多脏逻辑残留。这个30分钟的窗口期有一个已知缺陷如果某个关键词被删了线上索引还保留着它的路径这期间可能会有少量误匹配。但权衡下来30分钟内的误匹配概率极低而且可以通过结果过滤器做二次校验兜底全量重建的高昂成本反而不可接受。5. 压测结果和效果对比5.1 优化前后耗时对比直接上数据这个项目压测用的是200台模拟客户端连续打流30分钟关键词库规模约1200万条查询词样本来自线上真实搜索日志确保覆盖长短尾词。阶段单次平均耗时P99耗时匹配准确率初始版本约55分钟无法统计93.2%清洗Set优化约21分钟无法统计94.1%引入AC自动机约3秒6.8秒96.7%分片并行约800毫秒1.5秒96.7%加缓存层约90毫秒260毫秒96.7%能从小时级降到分钟级再到秒级和毫秒级主要靠的是三个维度的合力算法复杂度降了AC自动机、系统并发度升了分片并行、无效计算少了很多缓存命中。这里需要特别澄清一点表格里的55分钟是全量匹配一批新上线关键词去重库的批处理场景而不是单次用户查询的耗时。优化过程中我一直在推动接口侧的性能指标二者最终都在持续改善。5.2 内存和CPU的消耗变化AC自动机最原始的版本因为把所有关键词都塞进了一棵大树里内存峰值到了6.8GBGC压力非常大。后来做了长短词分治、bigram索引缩减节点数、分片部署之后单机内存降到1.2GB左右。CPU方面纯匹配引擎的CPU使用率优化前后差不多但因为有缓存CPU利用率在流量模型下反而出现了明显的毛刺下降。热词被缓存命中后CPU短暂闲置冷词进来时再拉高整体平均使用率大概降了30%。内存调优还有一个值得写进笔记的点Java堆里TrieNode的Map结构非常吃内存。一棵千万节点的Trie树如果每个节点都用一个HashMap存children光Map对象头就浪费了海量空间。我把HashMap换成了固定数组或者基于字符的紧凑结构内存直接砍了一半。6. 常见问题与避坑实录6.1 为什么AC自动机匹配结果有重复AC自动机在匹配过程中同一个关键词可能从多条路径到达终点节点。比如搜索词苹果手机价格匹配关键词手机时沿着搜索词的连续字符走可能在走到手的时候从fail指针跳到了手机的前缀节点此时如果节点标记了isEnd就会输出一次继续走到机的时候又可能从另一条路径输出一次。我的解决办法是维护一个SetString收集结果最后再转成List重复输出自然被消灭。但这只是治标治本的做法是构建fail指针时把每个节点后缀里包含的所有isEnd关键词都记录到该节点的输出集合中这样匹配时一个节点只输出一次。这个问题特别容易在压测长尾搜索词的时候暴露出来因为长尾词往往包含多个短关键词路径切换频繁重复率很高。上线前一定要用一批长尾词做回归测试不能只看Top热词的结果。6.2 关键词更新后匹配结果还是旧的这个问题我排查了很久最后发现是双缓冲切换没有加volatile关键字导致的。当前索引引用在并发环境下没有可见性保证部分线程还在用旧索引新索引切换过去后另外一些线程看不到。另外还有一个更隐蔽的坑构建新索引的线程抛了异常导致buildingIndex从未成功赋值但线上已经切过去读这个半成品索引了。后来我在构建完成之后加了一个标志位校验确保索引完整构建完成才允许切换。双缓冲的正确姿势可以参考JVM的safe publication机制用final字段或者volatile引用发布不可变对象而不是发布过程中修改可变状态。索引构建期间绝不修改当前索引的任何内容。6.3 并行查询把数据库打满了分片并行查询确实提升了匹配性能但也带来一个副作用每个分片独立构建索引后内存中加载的索引数据总量变多了导致查询时对数据库的压力增加。特别是冷启动阶段多个分片同时加载索引文件数据库瞬时IO飙高。这里我做了两件事缓和一是把索引文件从数据库搬到了本地磁盘和Redis缓存数据库只保留原始关键词数据索引文件在服务启动时从本地磁盘加载二是冷启动时分片错峰加载比如每台机器延迟几秒再启动构建流程避免所有节点同时抢占IO。6.4 长关键词的匹配漏报问题有一类典型漏报场景关键词苹果手机壳用户搜索苹果手机保护壳这个搜索词其实包含苹果手机壳作为连续子串吗不包含因为中间插了保护两个字。但用户意图上很可能就是要找这种壳业务方也期望能命中。纯AC自动机解决不了这种意图命中问题。我在匹配完成后加了一个编辑距离很小的模糊匹配后处理允许关键词和搜索词的匹配片段之间有至多两个字符的差异。这个后处理只作用于候选结果集不会全量跑所以性能影响可控。模糊匹配需要和产品确认好容忍度不是越宽松越好。太宽松会引入大量无关结果用户搜苹果手机返回一堆苹果平板的结果反而体验更差。我这边测试下来编辑距离设为1得到的收益最大设为2就明显开始稀释准确率了。7. 优化过程中的经验和工具沉淀7.1 用JFR做性能定位这次优化比较顺利的一个重要原因是前期排查做得比较扎实。我没有一上来就各种方案轮着试而是先花了半天时间用JDK自带的JFRJava Flight Recorder抓了一把线上线程的CPU采样。jcmd pid JFR.start duration120s filenamexxx.jfr然后拿JMC打开录制文件看热点方法。结果一目了然String.indexOf和StringLatin1.indexOf占了线程总耗时的六成以上GC线程也有将近10%的消耗。这说明问题出在匹配算法本身而不是外部依赖。JFR的这个使用习惯我一直保留到现在每次接手性能问题第一步永远是采样看热点而不是先猜原因。没有数据的优化都是耍流氓。7.2 压测工具的选择与压测脚本的坑压测用的是JMeter但JMeter的默认配置在并发高的时候容易产生瓶颈因为它的线程模型本身是一个大线程池每个虚拟用户都要有一个线程1000个并发就要1000个线程调度开销不小。后来我在压测脚本里做了一层预聚合把压测的搜索词样本文件提前读入内存运行阶段从内存中取词避免每次请求都读文件。另外把JMeter的日志级别调成了ERROR避免大量INFO日志拖慢压测进程本身。这两个小改动让压测机的CPU消耗降了一大半压出来的数据也更接近真实线上表现。压测环境一定要和生产环境隔离压测期间的数据库连接、日志系统、监控上报都会对结果产生干扰。我是专门申请了一套独立压测环境配置跟生产保持一致但网络隔离避免压测流量污染线上监控指标。7.3 一台机器扛不住怎么办当单机内存和CPU都到了上限分片部署是自然的下一步。但我分片之初犯了一个错误分片只是简单地把关键词库按字母序切段导致某些字母段关键词特别多比如S开头的词占了全量的两成多这几个分片负载明显偏高。后来改成一致性哈希分片关键词按哈希值散列到不同节点负载均衡效果好了很多。一致性哈希还带来一个额外好处新增分片节点的时候只需要迁移少量关键词不需要全量重新分发。这在关键词库持续增长的业务场景下非常重要。分片之后还有一个全局维度的问题一个搜索词要在多个分片上都跑一遍如果有100个分片单次查询就有100次内部请求。后来加了分片剪枝先根据搜索词的前几个字符预判可能命中的分片只跑那些候选分片其他分片直接跳过。剪枝的正确性依赖于关键词库的分片规则和搜索词特征的契合度需要线上验证不能瞎剪。8. 几个容易被忽略的细节8.1 正则表达式的性能陷阱在精确匹配之外早期版本里还混着一批正则匹配的关键词比如匹配某某手机型号\d这样的词条。正则表达式匹配在数据量小的时候没问题但当关键词库里存在大量正则型词条时性能会急剧恶化。原因在于Java正则的Matcher.find()在匹配失败时需要回溯复杂的正则表达式最坏情况下会指数级消耗时间。我处理的方法是能改写成纯文本匹配的尽量改写掉必须保留的正则型关键词单独放到一个小的正则匹配池里不参与主流程的AC自动机匹配只做最终候选集的过滤。正则匹配的次数从千万次降到了几百次性能自然就上来了。8.2 日志打印对性能的影响这个优化点非常隐蔽是我偶然发现的。排查压测瓶颈时看到日志系统占了大量CPU仔细一看发现每一条匹配请求都会打印一句INFO日志内容包括完整搜索词、匹配到的关键词列表、耗时等。压测期间每秒几百个请求每行日志都好几百个字符磁盘IO和日志解析开销非常大。后来把匹配成功的日志级别调成了DEBUG线上只保留匹配失败或者异常时打印WARN日志。同时把匹配耗时超过200毫秒的慢请求单独打印一份慢日志方便后续性能调优。日志改造完之后接口P99耗时又降了不少GC压力也小了。这里给一个通用建议日志要分级别、分场景正常路径的日志能省则省异常路径和慢路径的日志必须详尽。日志不是越全越好对性能敏感的服务来说日志往往是隐形的资源黑洞。8.3 编码和字符规范化的小问题做中文关键词匹配编码问题容易被忽略。线上搜索词的来源渠道很杂有浏览器输入、有API调用、有移动端SDK上报字符编码不完全统一。有次发现用户搜小米手机返回结果正常搜小米 手机中间带空格却什么都匹配不到。排查下来是搜索词里的空格是全角空格标准化流程只处理了半角空格。后来在normalize方法里把所有Unicode空白字符统一替换成半角空格再压缩这种问题就彻底解决了。字符规范化还包括繁体简体转换、特殊符号过滤等。做匹配系统的同学一定要记住一句话线上数据永远比你想象的更脏而在匹配入口做严格的标准化是成本最低的改善手段。9. 后续扩展的一些思考9.1 关键词匹配如何与向量检索结合本次优化解决的是精确子串和模糊子串匹配的问题但如果业务方要的是语义级匹配比如用户搜便宜的手机想命中低价智能手机基于字符的匹配规则就无能为力了。我测试过一个初步方案把关键词和搜索词都转成向量用向量相似度做召回再用传统匹配规则做精排。向量检索负责召回语义相关但字面不匹配的关键词AC自动机负责保证字面命中的关键词一定不丢。两者结合之后准确率确实能再往上提几个点但对算力的要求也上了一个台阶。向量检索这块如果用开源的方案比较成熟的是faiss或者milvus。不过语义匹配的“阈值怎么定”“相似度多少算命中”需要针对业务数据做大量小样本调参不是拿来即用。9.2 大数据量下的增量学习闭环关键词库的增删改查如果只靠人工维护数据质量会持续下滑。我们正在做的一个方向是线上搜索词匹配结果的行为数据回流比如用户点了哪个结果、跳过了哪个结果把这些信号作为关键词权重调整的依据。匹配结果排序不再只是纯字典序或者业务方优先级而是叠加了用户真实点击行为的动态权重。这个方向需要数据团队配合离线分析但做出来之后的收益很明显搜索相关性会随着数据积累越来越准。动权重的精细化运营有一个前提必须保证基础匹配逻辑足够快、足够准否则权重再准也弥补不了基础召回缺失。所以本次的匹配性能优化本质上是为后续的智能化排序打地基。9.3 匹配服务的高可用设计分片部署之后另一个必须考虑的维度是高可用。任何一个分片节点挂掉都会导致查询失败。我在分片之上加了一层容错每个分片都做了双副本一个主节点一个备节点主节点不可用时自动切换到备节点。同时为了防止部分分片挂了但系统还在跑产生错误结果查询结果汇总阶段会校验分片数量如果实际返回的分片数少于预期直接降级为不返回结果宁可少结果也不能出错误结果。这个开关在业务上是可以配置的紧急情况下可以先放开对少数分片缺失的容忍度保住整体可用性。监控方面每个分片节点都暴露了QPS、P99耗时、GC时间、内存水位这几个核心指标接入Prometheus和Grafana之后配合告警规则任何节点异常都能在五分钟内被发现。性能优化做完之后稳定性才是一切的起点。
返回列表