作者: wangguanqun

  • Nginx 代理 Ollama 流式接口:从踩坑到解决的完整指南

    Ollama 的流式生成功能遇到问题时,Nginx 代理导致响应一次性返回,影响了实时交互体验。问题根源在于 Nginx 的缓冲与超时机制与流式传输的分块特性冲突。通过修改 Nginx 配置,关闭缓冲和超时设置,使响应传递为分块形式。同时需确保请求头正确传递,并延长超时时间至合理范围(如 300-600 秒),以支持更复杂的实时交互需求。

    文章封面

    最近在集成 Ollama 的流式生成功能时遇到了一个棘手的问题:通过 Nginx 代理后,原本应该逐字输出的回答变成了一次性完整返回。这个问题不仅影响用户体验,更让整个实时交互的设计失去了意义。经过一番调试和研究,我终于找到了症结所在,今天就来分享一下这个过程中的收获。​

    流式传输的本质与 Nginx 的冲突​

    首先我们需要理解,Ollama 的 stream 接口之所以能实现逐字输出,核心在于采用了分块传输编码(Chunked Transfer Encoding)。这种传输方式允许服务器边生成内容边向客户端发送,每个数据块独立传输,不需要预先知道整个响应的大小。​

    而 Nginx 作为反向代理的默认行为,却与此特性背道而驰。Nginx 会自动缓冲服务器响应,直到缓冲区填满或请求完成才会一次性发送给客户端 —— 这就是为什么代理后流式输出会变成完整输出的根本原因。这种机制在普通 Web 请求中能提高效率,但在需要实时交互的场景下就成了障碍。​

    关键配置项解析​

    解决问题的关键在于修改 Nginx 配置,让其放弃缓冲行为,忠实传递每一个数据块。经过测试,以下几个配置项必不可少:

    location /ollama/ {
        proxy_pass http://127.0.0.1:11434/;
        
        # 核心配置:关闭缓冲机制
        proxy_buffering off;          # 禁止响应缓冲
        proxy_cache off;              # 关闭缓存功能
        chunked_transfer_encoding on; # 保持分块传输编码
        
        # 连接处理
        proxy_set_header Connection ''; # 清除连接头,避免被修改
        proxy_http_version 1.1;        # 使用HTTP/1.1支持长连接
        
        # 超时设置
        proxy_connect_timeout 300s;
        proxy_send_timeout 300s;
        proxy_read_timeout 300s;
    }

    Bash

    其中proxy_buffering off是最关键的配置,它直接禁用了 Nginx 的响应缓冲。而chunked_transfer_encoding on则确保分块传输编码能正确传递到客户端。

    长连接与超时设置的重要性​

    在调试过程中,我发现即使关闭了缓冲,有时流式传输仍会中途中断。这是因为 Nginx 默认的超时时间过短(通常是 60 秒),而大型模型的生成过程很容易超过这个时限。​

    将超时时间延长到合理范围(如 300 秒)能有效解决这个问题:​

    • proxy_connect_timeout:与后端服务建立连接的超时时间​
    • proxy_send_timeout:发送请求到后端的超时时间​
    • proxy_read_timeout:等待后端响应的超时时间​

    这些值的设置需要根据实际使用的模型和网络环境调整,对于生成类任务,建议设置为 300-600 秒。​

    请求头与响应头的正确传递​

    另一个容易被忽略的细节是请求头的处理。Ollama 的 stream 接口依赖特定的请求头来启用流式模式,因此需要确保 Nginx 正确传递这些头部信息:

    # 传递关键请求头
    proxy_pass_request_headers on;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    Bash

    特别需要注意的是Content-Type响应头,流式接口通常使用application/x-ndjsontext/event-stream类型,Nginx 不应修改这些头部信息。

    完整配置示例

    整合以上所有要点,这里提供一个完整的 Nginx 配置示例:

    server {
        listen 80;
        server_name ai.example.com;
    
        location /ollama/ {
            proxy_pass http://127.0.0.1:11434/;
            
            # 流式传输核心配置
            proxy_buffering off;
            proxy_cache off;
            proxy_pass_request_headers on;
            proxy_set_header Connection '';
            proxy_http_version 1.1;
            chunked_transfer_encoding on;
            
            # 超时设置
            proxy_connect_timeout 300s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;
            
            # 头部设置
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
  • [深度学习]大模型学习5-高效微调框架Unsloth使用指北

    [深度学习] 大模型学习5-高效微调框架Unsloth使用指北

    Unsloth是一个专注于加速大语言模型微调过程的开源项目。它通过一系列底层优化,显著提升了微调速度并大幅降低了内存消耗,同时能保持模型性能。无论是研究者还是开发者,都能借助Unsloth更高效地定制自己的大语言模型。本文将介绍Unsloth的使用,相关学习资源如下:

    目录

    1 Unsloth框架介绍

    1.1 Unsloth概览

    Unsloth是一款专为大语言模型微调与强化学习设计的开源框架,致力于以更高的效率和更低的资源成本推动人工智能技术的普及。用户可在本地环境、Google Colab、Kaggle等平台上,借助其运算加速与显存优化能力,轻松完成Qwen、DeepSeek等主流大模型的训练、评估、保存及推理优化。

    传统大语言模型微调往往面临硬件要求高、迭代速度慢和资源受限等挑战,而Unsloth通过高效的底层实现和友好的接口设计,显著降低了微调的技术门槛,使更多人能够高效、低成本地训练属于自己的定制模型。

    https://www.codemajin.net/fine-tuning-llm-with-unsloth/

    核心优势

    特点说明适用场景/用户
    🚀极致速度相比Hugging Face,Unsloth训练模型更快需快速实验与迭代的研发场景
    💾省内存减少GPU显存占用注重成本控制的用户
    ✅无损精度无需依赖近似计算对精度要求极高的任务
    🔗广泛兼容支持主流Transformer类模型(涵盖多模态、语音、文本及扩散模型);支持全量微调、预训练及4/8/16位精度训练;兼容Linux、Windows及主流云平台使用多种架构的团队
    🧩易于使用提供简洁API,兼容Hugging Face生态,可导出GGUF、Ollama等格式初学者、资源有限的小型团队
    ⚡高效推理支持INT4量化(QLoRA),推理阶段同步提速需兼顾微调与推理效率的应用场景
    💡低成本单张GPU(如4090或8GB显存卡)即可微调10B+参数模型个人开发者
    🔧高效计算基于Triton(OpenAI开源的高性能GPU编程语言)实现高效计算对技术底层效率有要求的开发团队

    目前,Unsloth支持借助Accelerate、DeepSpeed等库实现多GPU训练,但实际配置过程较复杂,需手动完成设置,相关训练教程可参考:Multi-GPU-Unsloth,Unsloth团队正积极优化多GPU训练功能。

    使用建议

    Unsloth与Meta、Google、Microsoft、Mistral、Qwen等主流模型团队深度合作,持续修复关键漏洞,提升框架的准确性与稳定性。该框架支持用户灵活调整聊天模板和数据集格式,并提供涵盖视觉模型、TTS、BERT、强化学习等多样化示例Notebook,助力用户快速上手,详情可参考:Unsloth Notebooks。快速上手建议:

    • 从QLoRA起步:4-bit量化是资源有限用户的理想选择;
    • 调整关键参数:如LoRA秩(r)和alpha,建议从小值(如16)开始尝试,以平衡模型能力与过拟合风险;
    • 监控训练过程:密切关注损失曲线,借助Unsloth的快速迭代优势积极调参;
    • 利用社区资源:通过Discord聊天社区等渠道获取帮助、交流经验。

    1.2 微调技术概览

    什么是微调?

    微调(Fine-tuning)是一种基于预训练大语言模型、利用特定领域数据进一步训练的技术,其核心目标提升模型在特定场景下的性能表现。该技术主要包括两个层面:一是对预训练模型进行持续的无监督预训练;二是指令微调(SFT),即引导模型学习如何根据指令调用已有知识,完成特定格式的任务或匹配特定风格。通过微调,通用大模型能够逐步转化为专业化的领域专家。与检索增强生成(RAG)不同,微调将知识直接内化至模型参数中,实现更深层次的能力融合。本文聚焦于大语言模型的指令微调。

    那么,为什么要进行微调?

    1. 知识增强:向模型注入领域新知识,扩展其认知边界
    2. 行为定制:调整模型的输出风格、语气及响应方式
    3. 性能优化:提升模型在特定任务上的准确性、相关性和可靠性

    利用Unsloth实现完整指令微调训练的教程见: How To Fine-tune & Run LLMs

    微调常见问题

    • 微调能否增加新知识?
      可以。只要训练数据中包含新信息,模型就能有效学习并掌握新的知识或模式。
    • RAG是否一定优于微调?
      并非如此。经过良好优化的微调模型在特定任务上可以媲美甚至超越RAG系统。借助如Unsloth等高效训练工具,微调的技术门槛也显著降低。
    • 微调成本是否很高?
      并非必然。采用LoRA/QLoRA等参数高效微调方法,结合免费或低成本的算力资源,完全能够实现低成本甚至零成本的微调。
    • 微调如何与其他技术结合?
      微调与RAG具有互补优势:微调赋予模型领域基础能力,RAG则提供实时外部知识,兼顾专业性与时效性。此外,强化学习(RL)也可在微调后通过奖励机制进一步优化模型表现。
    https://medium.com/decodingml/8b-parameters-1-gpu-no-problems-the-ultimate-llm-fine-tuning-pipeline-f68ef6c359c2

    1.3 Unsloth安装

    Unsloth安装命令

    Unsloth可直接在Linux、Windows、Google Colab等系统上运行,直接安装命令如下:

    pip install unsloth

    系统要求

    • 操作系统:支持Linux与Windows
    • 显卡:
      • 兼容2018年及之后发布的NVIDIA显卡
      • 需至少支持CUDA 7.0,例如V100、T4、Titan V、RTX 20/30/40系列、A100、H100、L40等
      • GTX 1070/1080可运行,但性能较慢
      • 支持AMD与Intel的CPU,Apple Silicon版本目前仍在开发中
    • 软件兼容:安装Unsloth时将自动更新已有环境中的torch、transformers等库至最新版本,无需手动处理版本冲突
    • 依赖项:需安装xformers、torch、BitsandBytes及triton

    微调显存要求

    在使用Unsloth对大语言模型进行微调时,出现内存不足错误通常是由于批处理大小设置过高。将批处理大小调整为1、2或3可有效降低显存占用。

    下表列出了不同参数规模与微调方法下的显存需求,其中QLoRA使用4位精度,LoRA使用16位精度。所列数据为理论最低值,部分模型实际可能需要更多显存。详见:Unsloth-requirements

    模型参数QLoRA(4位)显存LoRA(16位)显存
    3B3.5GB8GB
    7B5GB19GB
    8B6GB22GB
    9B6.5GB24GB
    11B7.5GB29GB
    14B8.5GB33GB
    27B22GB64GB
    32B26GB76GB
    40B30GB96GB
    70B41GB164GB
    81B48GB192GB
    90B53GB212GB
    405B237GB950GB

    2 Unsloth微调教程

    2.1 模型与训练方法选择

    优先选择指令模型

    大语言模型主要分为基座模型(Base)和指令模型(Instruct)两类,两者均基于文本预测任务进行训练。基座模型通常仅经过预训练和少量通用指令微调;指令模型则在基座模型基础上,进一步通过大规模指令微调和人类反馈强化学习优化其理解和生成能力。常提到的对话模型(Chat Model)本质上属于指令模型。

    选择基座模型还是指令模型,通常取决于数据规模、质量与类型:

    • 1000行以上数据:数据量较大时,微调基座模型效果更佳。
    • 300–1000行高质量数据:中等规模高质量数据下,微调基座模型或指令模型均可。
    • 300行以下数据:数据量较小时,建议选择指令模型。微调后既能适配特定任务,又可保留其内置的指令遵循能力,无需额外提示即可响应一般指令(除非需大幅改变模型行为)。

    推荐优先从指令模型入手,原因包括:

    • 支持直接使用ChatML、ShareGPT等对话模板进行微调,所需数据量更少;
    • 基座模型需依赖Alpaca、Vicuna等特定模板,对数据量要求相对更高。
    https://medium.com/data-science-in-your-pocket/unsloth-the-fastest-way-to-fine-tune-llms-041bb6a785ac

    Unsloth模型格式

    在Hugging Face中Unsloth仓库不同后缀代表模型的量化格式或优化版本,选择时可参考以下说明:

    • 名称以unsloth-bnb-4bit 结尾:为Unsloth动态4位量化模型。其显存占用略高于标准位量化模型,但精度显著更高。
    • 名称仅以bnb-4bit 结尾(不含unsloth):为标准位量化模型。
    • 无后缀:为原始16位或8位格式。这类模型是官方发布的原始版本,但Unsloth会在部分版本中加入对话模板、分词器等重要修复。

    在此基础上,在准备微调时,首要决策之一就是选择合适的模型:

    1. 选择与用例匹配的模型
      例如:若进行基于图像的训练,可选择Llama 3.2 Vision等视觉模型;针对代码数据集,则适合选用Qwen Coder 2.5等专用模型。
    2. 留意授权与要求
      不同模型可能有特定的授权条款和系统要求,务必仔细查看。
    3. 评估存储、计算能力和数据集
      可参考Unsloth的显存指南,确定目标模型所需的显存配置。数据集的类型会影响模型的选择,同时也会决定训练所需的时间。
    4. 选定模型及参数
      建议选用最新模型,以获得最佳性能和功能。可以通过浏览Unsloth的模型目录,及时了解最新且相关的选项。

    可以将模型名称修改为任意名称,只需使其与Hugging Face上Unsloth仓库的模型名称相匹配即可,例如 “unsloth/llama-3.1-8b-unsloth-bnb-4bit”。对于初学者,建议从诸如unsloth/llama-3.1-8b-unsloth-bnb-4bit之类的小型指令模型入手,再逐步探索更多可能性。

    所有Unsloth支持的模型见:Unsloth Models

    训练方法的选择:LoRA与QLoRA

    在实施微调时,降低计算与内存需求的主流技术主要有以下两种:

    • LoRA(低秩适配):仅微调少量16位的适配器权重矩阵,保持原始模型参数基本不变,从而显著减少训练过程中需要更新的参数量。
    • QLoRA(量化LoRA):在LoRA基础上引入模型权重的4位量化,可在有限硬件资源下高效微调超大规模模型,通过4位精度显著降低内存占用与计算开销。

    建议从QLoRA入手,它是当前高效且易于使用的微调方法之一。借助如Unsloth所采用的动态4位量化技术,其精度损失相较于标准的16位LoRA微调已几乎可忽略不计。

    https://towardsdatascience.com/fine-tune-llama-3-1-ultra-efficiently-with-unsloth-7196c7165bab/

    已微调的模型可再次多次微调,但最佳做法是合并所有数据集一次性完成。若基于已微调模型续训,可能改变其此前获得的质量与知识。需注意,实验验证至关重要。微调无唯一最佳方法,仅有适配不同场景的最佳实践,需尝试多种方法与配置,才能找到最契合自身数据集及需求的方案。

    2.2 LoRA和数据集

    2.2.1 LoRA介绍

    LoRA提供了众多超参数(如学习率、训练轮次等),其组合可能达数百万种。合理选择参数对微调至关重要,直接影响模型的准确性、稳定性与输出质量。Unsloth基于数百篇研究论文与实验经验,总结了这些参数的最佳实践,并解析了它们对模型行为的影响。虽然建议直接使用其默认配置,但理解这些概念将有助于更全面地掌控整个微调过程。

    超参数调整的目标是在提升模型准确率的同时避免过拟合或欠拟合。对于大型语言模型(如Llama 70B),其权重包含数百亿参数,通常不会全部参与更新,而是采用LoRA等参数高效微调方法。LoRA在每一层旁引入两个小型矩阵A和B,仅优化这两个矩阵,实际训练参数量通常仅占总量的1%左右。通过冻结原始权重、仅更新新增的适配器参数,LoRA显著降低了计算与存储开销,同时在多数任务中保持模型性能,已成为当前大模型微调的主流方法之一。关于LoRA的详细介绍见:LoRA Hyperparameters Guide

    以下简要介绍相关参数:

    学习率

    定义模型训练中每一步的权重更新幅度。

    • 较高学习率:收敛快,但过高易造成训练震荡,可能错过最优解。
    • 较低学习率:训练更稳定、精度高,但收敛慢、耗时长;虽常被认为易欠拟合,实际也可能引发过拟合或阻碍有效学习。
    • 常用范围:2e-4(0.0002)至 5e-6(0.000005)
      • LoRA/QLoRA微调:建议初始值2e-4
      • 强化学习:推荐5e-6
      • 全量微调:通常适用更低学习率

    训练次数(Epochs)

    指模型完整遍历训练数据集的次数。

    • 轮次过多:可能提升训练集上的表现,但也容易导致过拟合,降低模型泛化能力。
    • 轮次过少:训练时间短且不易过拟合,但若模型未能充分学习数据规律,可能造成欠拟合。
    • 建议:多数指令微调任务建议训练1–3轮。超过3轮后收益递减,过拟合风险显著增加。

    超参数设置

    其他常用参数如下:

    Hyperparameter功能说明推荐值
    Rank(r)控制可训练参数数量,秩越高能力越强,内存占用越大8,16,32,64,128(常用16或32)
    LoRA Alpha(lora_alpha)用于控制低秩矩阵的缩放系数通常设为r或2r
    LoRA Dropout(lora_dropout)训练时随机丢弃部分激活值,防止过拟合0(默认0.1)
    Target Modules(target_modules)指定添加LoRA的模型模块q_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_proj(推荐全部)
    Weight Decay抑制权重过大,提升泛化能力0.01至0.1
    Warmup Steps训练初期逐步提高学习率,稳定训练总步数的5%–10%
    Scheduler Type训练过程中调整学习率的方式linear或cosine
    Seed固定随机数种子,保证结果可复现任意整数(如42)

    关于LoRA超参数详细介绍可见:LoRA、QLoRA、QA-LoRA 原理笔记

    作用模块

    在QLoRA与LoRA的对比中,QLoRA采用4-bit精度,可降低超过75%的显存占用,而LoRA(16-bit)在精度和速度上略优。根据论文及实验经验,建议将LoRA同时作用于注意力层与MLP层(如target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"]),以有效提升模型精度。

    下图对比了不同目标模块配置下LoRA与QLoRA的Rouge分数(分数越高越好),前三组分别为:

    • QLoRA-All:将LoRA应用于所有FFN/MLP层和注意力层,是本实验中表现最佳的配置。
    • QLoRA-FFN:仅在FFN层(包括gate_proj, up_proj, down_proj)上应用LoRA。
    • QLoRA-Attention:仅在注意力层(包括q_proj, k_proj, v_proj, o_proj)上应用LoRA。
    https://docs.unsloth.ai/get-started/fine-tuning-llms-guide/lora-hyperparameters-guide

    梯度累积与批次大小的等效关系

    较大的有效批次通常能稳定训练,而较小的批次可能因梯度方差增大而影响收敛。有效批次大小由以下两个参数共同决定:

    有效批次大小 = batch_size × gradient_accumulation_steps

    以下为Unsloth推荐配置,适用于多数微调场景:

    参数定义影响推荐值
    batch_size单次前向或反向传播中各GPU处理的样本数主要影响内存占用2
    gradient_accumulation_steps权重更新前累积梯度的步数模拟更大批次以节省显存;步数增加会延长每轮训练时间8
    有效批次大小实际用于梯度更新的样本总数影响训练稳定性与性能16(2×8)
    https://huggingface.co/docs/trl/main/distributing_training

    2.2.2 避免过拟合和欠拟合

    过拟合

    深度学习模型容易过度记忆训练数据和噪声,导致泛化能力下降。当训练损失低于0.2时,常提示过拟合,模型在未知任务上表现变差。

    一种简单的缓解方法是LoRA Alpha缩放:将每个LoRA矩阵的alpha值乘以0.5。其原理类似于权重平均,将基础模型与LoRA权重相加后除以2,等同于alpha减半。该方法通过平均化机制抑制过拟合,提升模型在未知任务上的泛化性能。

    其他常用解决方案包括:

    • 调整学习率:过高易引发过拟合,训练周期短时尤需注意;周期较长可适当提高。建议尝试不同取值以寻优。
    • 控制训练轮数:通常1–3轮即可,避免过度训练。
    • 增大权重衰减(weight_decay):初始建议设为0.01或0.1。
    • 启用LoRA Dropout:可设为0.1以提高泛化能力。
    • 增大批次大小或梯度累积步数:有助于提升训练稳定性。
    • 扩展数据集:结合高质量开源数据与自有数据,扩大样本规模。
    • 早停机制:验证损失连续多轮上升时自动停止训练。
    • 权重平均:将原始模型与微调后的模型权重相加取平均,平滑输出表现。

    欠拟合(过于泛化)

    指模型未能充分学习训练数据中的特征,通常因模型复杂度过低或训练不足导致。改进方法包括:

    • 调整学习率:初期可适当提高以加速收敛,长期训练则需降低,需实验确定最优值。
    • 增加训练轮次:延长训练时间,同时监控验证集损失以防过拟合。
    • 提高LoRA秩与alpha值:秩建议不低于alpha,模型越小或数据越复杂,秩应越大,通常设为4至64。
    • 使用领域相关数据集:确保训练数据质量高且与目标任务相关。
    • 将批大小设为1:增强每次参数更新的强度,提高模型对数据的敏感度。

    2.2.3 训练数据集介绍

    构建大语言模型训练数据集的关键环节之一是设计恰当的对话模板,以利于模型高效处理。关于数据集的详细介绍见:Unsloth Datasets Guide

    数据格式要求

    为进行分词处理,数据集需采用可被分词器读取的格式。请注意,每种数据类型对应不同的格式样式。

    格式类型说明训练类型
    原始语料来自网站、书籍或文章等的原始文本持续预训练(CPT)
    指令文本包含指令及对应输出的示例监督微调(SFT)
    对话记录用户与AI助手之间的多轮对话监督微调(SFT)
    强化学习数据用户与AI助手的对话,助手回复带有人工/模型/脚本的排序评分强化学习(RL)

    格式化数据

    在明确数据筛选标准并完成收集后,需将数据转换为机器可读的格式,以适应不同阶段的模型训练需求。以下从四种核心训练场景出发,分别介绍对应的主流数据格式及示例:

    1. 预训练数据格式

    在模型的继续预训练阶段,通常无需对文本结构做特殊设计,直接采用原始文本即可。这种无结构化的输入方式有助于模型从连续文本中自然学习语言规律与常识知识。

    "text": "北京烤鸭是中国著名的京菜代表,其制作需经过烫皮、挂色、风干、烤制等多道工序,成品鸭皮酥脆..."
    
    1. 指令微调格式

    为让模型适应特定任务(如问答、总结、创作),可采用Alpaca风格的指令格式。该格式包含指令(任务目标),输入(任务素材),输出(预期结果)三部分,结构清晰,便于标注。

    {
      "Instruction": "为以下城市写一句旅游宣传语",
      "Input": "西安(关键词:兵马俑、古城墙、大唐不夜城)",
      "Output": "穿越秦唐,梦回长安——西安等你来探秘"
    }
    
    1. 多轮对话格式

    针对多轮对话场景(如客服、聊天助手),需保留上下文逻辑,常用ShareGPT格式。通过from字段标注角色(human为用户,gpt为模型),value记录发言内容,清晰呈现对话流程。

    {
      "conversations": [
        {"from": "human", "value": "推荐一道适合初学者的家常菜"},
        {"from": "gpt", "value": "番茄炒蛋简单易学,需要我介绍具体步骤吗?"},
        {"from": "human", "value": "好的,请说明关键步骤和注意事项"},
        {"from": "gpt", "value": "步骤:1. 番茄切块,鸡蛋打散;2. 热油炒蛋后盛出;3. 炒番茄至出汁,加糖调味;4. 混入鸡蛋翻炒。注意火候,避免蛋炒老。"}
      ]
    }
    
    1. ChatML格式

    ChatML格式由OpenAI提出,是当前工业界广泛使用的对话格式,也被Hugging Face等平台默认支持。它通过role字段定义角色(如user,assistant,system),用content记录内容,结构清晰且兼容性强。

    {
      "messages": [
        {"role": "system", "content": "你是一位中文烹饪助手,回答需简明实用"},
        {"role": "user", "content": "蒸鱼应该用大火还是小火?"},
        {"role": "assistant", "content": "建议大火蒸制,时间约8–10分钟,这样鱼肉更鲜嫩。"}
      ]
    }
    

    合成数据生成

    为获得理想的微调效果,建议数据集不少于100条;若追求更优性能,推荐使用1000条以上的数据。通常情况下,数据量越大,效果越好。若原始数据不足,可引入合成数据或补充Hugging Face上的相关数据集以增强多样性。请注意,微调效果高度依赖数据质量,务必做好数据清洗和预处理。

    生成合成数据时,可使用本地大语言模型(如Llama 3.3 70B)或OpenAI的GPT-4.5。通常更推荐使用参数规模更大的模型以保证生成质量。通过vLLM、Ollama或 llama.cpp等推理引擎可直接生成数据,但需手动收集生成结果,并优化提示词以扩展内容。合成数据的主要用途包括:

    1. 创造全新数据:既可完全从头生成,也可基于现有样本进行改写或扩展;
    2. 增强数据多样性:避免模型过拟合,提升泛化能力;
    3. 完善现有数据:例如将文本自动转换为指定格式(如将对话转为问答形式)。

    2.3 Qwen3使用示例

    本文将以Qwen3为例进行模型训练演示。Qwen3由阿里通义千问推出,在推理、指令遵循及多语言支持等核心能力上实现行业领先,是大语言模型训练的优选架构。

    Unsloth已于2025年7月完成升级,支持最新的Qwen-2507模型。在使用Unsloth运行或微调量化版Qwen模型时,几乎无损精度。同时,Unsloth为Qwen3原生支持128K上下文长度,可一次性处理数万字的长文档或对话;该扩展基于YaRN技术,将模型原有的40K处理上限提升至128K。优化后,模型训练速度提升2倍,显存占用降低70%。

    https://docs.unsloth.ai/models/qwen3-how-to-run-and-fine-tune/qwen3-2507

    模型版本

    为帮助开发者根据模型运行、长上下文支持、微调与部署等场景需求,选择合适规模的Qwen3模型,Unsloth基于其技术能力,围绕以下三个维度提供了多种参数规格的版本:

    1. Dynamic 2.0 GGUF(适用于模型运行)
      涵盖0.6B至235B-A22B等多种参数规模,支持用户直接运行Qwen3模型,适用于常规推理与基础任务场景。
    2. 128K Context GGUF(适用于长上下文处理)
      提供4B到235B-A22B等多个版本,重点优化了128K上下文长度的处理能力,适用于长文档分析、超长对话及对语义连贯性要求较高的复杂任务。
    3. Dynamic 4-bit Safetensor(适用于微调与部署)
      覆盖0.6B至32B参数规模,采用4位量化的Safetensor格式,在保持模型性能的同时显著降低存储与计算资源开销,便于进行任务特定微调或生产环境部署。

    推理参数

    为达到每秒6个token以上的推理速度,Unsloth建议总内存(即显存、内存或两者总和)不低于所使用模型的大小。即使总内存低于模型大小,仍可运行模型,但推理速度会降低。根据Qwen官方建议,模型推理的推荐设置如下:

    参数非思考模式(Non-ThinkingMode)思考模式(ThinkingMode)解释
    温度(Temperature)0.70.6值越低输出越确定
    最小概率(Min_P)0.0(可选,0.01效果更佳)0.0仅考虑累积概率达到该值的候选词
    累积概率(Top_P)0.80.95从累积概率前百分之几的候选词中选取
    候选词数量(TopK)2020每次只从概率最高的K个词中选择

    Qwen3对话模板

    Qwen3系列模型采用ChatML对话模板,默认启用思考模式。请注意,若使用贪婪解码,可能导致模型性能下降或生成内容无限重复。基础对话格式如下:

    <|im_start|>user\nWhat is 2+2?<|im_end|>\n<|im_start|>assistant\n
    

    如需关闭思考模式,需插入一对空的<think></think>标签,格式如下:

    <|im_start|>user\nWhat is 2+2?<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n
    

    示例推理代码

    下面代码展示通过Unsloth的FastModel类加载Qwen3-0.6B模型并启用4位量化:

    from modelscope import snapshot_download
    from unsloth import FastModel
    
    # 定义要使用的模型名称,这里使用的是Qwen3-0.6B模型
    model_name = "Qwen/Qwen3-0.6B"
    # 利用modelscope加速下载模型
    model_dir = snapshot_download(model_name)
    
    model, tokenizer = FastModel.from_pretrained(
        model_name = model_dir,  # 指定模型所在的目录路径
        max_seq_length = 2048,   # 设置最大序列长度为2048,可以根据需要调整以支持长文本
        load_in_4bit = True,     # 启用4位量化以减少内存占用
        load_in_8bit = False,    # 禁用8位量化(新特性:8位量化精度稍高,但内存占用是4位的2倍)
        full_finetuning = False, # 禁用全参数微调(新特性:现在支持全参数微调)
    )
    
    # 准备模型输入
    prompt = "推荐一部搞笑的科幻电影。"
    # 构造对话消息列表,包含用户角色和内容
    messages = [
        {"role": "user", "content": prompt}
    ]
    # 应用聊天模板处理消息,转换为模型所需的输入格式
    text = tokenizer.apply_chat_template(
        messages,
        tokenize=False,  # 不直接进行token化
        add_generation_prompt=True,  # 添加生成提示
        enable_thinking=True  # 启用思考模式,默认为True
    )
    # 将文本转换为模型输入张量,并移动到模型所在设备
    model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
    
    # 进行文本生成,输出为token
    generated_ids = model.generate(
        **model_inputs,  # 解包模型输入
        max_new_tokens=2048  # 最大生成的新token数量
    )
    # 提取生成的部分(排除输入部分)
    output_ids = generated_ids[0][len(model_inputs.input_ids[0]):].tolist() 
    
    # 解析思考内容
    try:
        # 查找特殊标记151668(表示思考内容结束)的位置
        index = len(output_ids) - output_ids[::-1].index(151668)
        # 这个结束符就是</think>
        # tokenizer.decode(output_ids[index-1])
    except ValueError:
        # 如果未找到特殊标记,索引设为0
        index = 0
    
    # 解码思考内容(特殊标记之前的部分)
    thinking_content = tokenizer.decode(output_ids[:index], skip_special_tokens=True).strip("\n")
    # 解码回复内容(特殊标记之后的部分)
    content = tokenizer.decode(output_ids[index:], skip_special_tokens=True).strip("\n")
    
    # 打印思考内容和最终回复内容
    print("思考内容:", thinking_content)
    print("回复内容:", content)
    

    2.4 Unsloth训练Qwen3教程

    Qwen3能够同时进行数学推理和常识问答。但如果只用“天空是什么颜色?蓝色”这类常识样本训练,模型在微调后可能出现能力退化,甚至无法正确解答“1+2×3=?”这类简单题目。
    为保持模型的推理能力,建议在训练素材中混合使用推理类和非推理类样本。例如,可组合75%的思维链样本。如“1+2×3:先算乘法2×3=6,再加1得7”,以及25%的常识类样本,如直接提供答案的问题。这样模型既能正确回答常识问题,也能维持数学推理能力,实现两类任务的平衡。

    https://medium.com/data-and-beyond/a-practical-guide-to-fine-tune-mistral-7b-with-unsloth-for-phishing-email-detection-2faa5b531e27

    下面将依次介绍如何使用Unsloth加载Qwen3模型,并详细讲解数据预处理、模型训练、模型运行及模型保存的完整流程。

    2.4.1 预训练模型初始化

    以下代码演示了如何利用Unsloth库加载Qwen3-0.6B模型,通过4位精度量化大幅减少内存使用,并借助LoRA方法实现高效的参数微调。

    # 从modelscope库导入snapshot_download函数,用于下载模型快照
    from modelscope import snapshot_download
    # 从unsloth库导入FastLanguageModel类,用于高效加载语言模型
    from unsloth import FastLanguageModel
    
    # 定义要使用的模型名称,这里使用的是Qwen3-0.6B模型
    model_name = "Qwen/Qwen3-0.6B"
    # 利用modelscope的snapshot_download函数加速下载模型,并返回模型保存的目录路径
    model_dir = snapshot_download(model_name)
    
    # 使用FastLanguageModel的from_pretrained方法加载预训练模型和分词器
    model, tokenizer = FastLanguageModel.from_pretrained(
        model_name = model_dir,       # 模型所在的目录路径
        max_seq_length = 2048,        # 上下文长度 - 可以设置更长,但会占用更多内存
        load_in_4bit = True,          # 以4位精度加载,使用更少内存
        load_in_8bit = False,         # 以8位精度加载会更准确,但占用2倍内存
        full_finetuning = False,      # 是否使用全量微调,当前设置为否
    )
    
    # 为模型配置LoRA方法
    model = FastLanguageModel.get_peft_model(
        model,
        r = 32,           # LoRA注意力维度,可选择任何大于0的值,建议8, 16, 32, 64, 128
        target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", # 注意力模型和FFN模块
                          "gate_proj", "up_proj", "down_proj",],  # 指定要微调的模块
        lora_alpha = 32,  # LoRA缩放参数,建议设置为与rank相同或rank的2倍
        lora_dropout = 0, # LoRA 层的 dropout 率(这里设为 0 以优化性能)
        bias = "none",    # 是否训练偏置(这里设为 "none" 表示不训练)
        # [新特性] "unsloth"模式可减少30%的VRAM使用,支持2倍大的批量大小
        use_gradient_checkpointing = "unsloth",  # True或"unsloth"用于超长上下文
        random_state = 0,  # 随机种子,确保结果可复现
        use_rslora = False,   # 是否使用RSLoRA
        loftq_config = None,  # 是否使用 LoftQ
    )
    

    2.4.2 数据集加载

    Qwen3包含推理和非推理两种模式,本示例使用以下两个训练数据集:

    1. 推理数据:Open Math Reasoning(开放数学推理)数据集
      从中采样了10%的可验证推理轨迹,这些样本使用了DeepSeek R1,且准确率超过95%。
      从这些数据里,Unsloth仅筛出DeepSeek-R1回答、正确率≥95%且每一步都可验证的标准答案,再从中随机抽取10%使用。
      • 用处:专注于数学推理能力的微调数据集,包含各种数学问题及其详细解答过程。
      • 来源:unsloth/OpenMathReasoning-mini
      • 样本数量:19,252
      • 格式:包含数学问题、期望答案、问题类型、解答过程等字段
      • 特点:增强模型的数学推理和思维链(Chain-of-Thought)能力
    2. 通用对话数据:Maxime Labonne的FineTome-100k数据集
      其格式为ShareGPT风格,已转换为Hugging Face标准的多轮对话格式。
      • 用处:高质量的指令遵循数据集,专为大语言模型微调设计
      • 来源:mlabonne/FineTome-100k
      • 样本数量:100,000
      • 格式:包含对话内容、来源和质量分数
      • 特点:数据质量高,覆盖广泛的指令类型和领域

    数据处理代码如下:

    from datasets import load_dataset
    # 数据集下载链接
    # reasoning_dataset = load_dataset("unsloth/OpenMathReasoning-mini", split = "cot")
    # non_reasoning_dataset = load_dataset("mlabonne/FineTome-100k", split = "train")
    
    # 如果无法直接访问Hugging Face,可以使用以下两个命令从镜像网站下载数据集到本地(速度也很慢)
    # git clone https://hf-mirror.com/datasets/unsloth/OpenMathReasoning-mini
    # git clone https://hf-mirror.com/datasets/mlabonne/FineTome-100k
    # 从本地加载数据集
    reasoning_dataset = load_dataset("./OpenMathReasoning-mini", split = "cot")
    non_reasoning_dataset = load_dataset("./FineTome-100k", split = "train")
    
    # 查看数据集结构
    # 特征包含预期答案、题目类型、题目来源、生成模型,72B TIR模式下的通过率、题目本身、解答过程、推理模式
    print(reasoning_dataset)
    # 特征包含对话、来源、分数
    print(non_reasoning_dataset)
    
    # 将reasoning_dataset转换为对话格式
    def generate_conversation(examples):
        problems  = examples["problem"]
        solutions = examples["generated_solution"]
        
        # 初始化一个空列表,用于存储转换后的对话
        conversations = []
        
        # 同时遍历问题和解决方案列表,将它们配对成对话
        for problem, solution in zip(problems, solutions):
            # 为每对问题和解决方案创建一个对话结构
            # 每个对话包含两个角色的消息:用户(提问)和助手(回答)
            conversations.append([
                {"role" : "user",      "content" : problem},      # 用户角色的消息内容是问题
                {"role" : "assistant", "content" : solution},     # 助手角色的消息内容是解决方案
            ])
        
        return { "conversations": conversations, }
    
    # 使用tokenizer将推理数据集转换为模型可理解的对话模板格式
    # 参数tokenize=False表示只进行格式转换,不进行分词处理
    reasoning_conversations = tokenizer.apply_chat_template(
        reasoning_dataset.map(generate_conversation, batched = True)["conversations"],
        tokenize = False,
    )
    print(reasoning_conversations[0])
    
    # 接下来,处理处理非推理型数据集,并同样将其转换为对话格式。
    # 使用standardize_sharegpt函数,对该数据集的格式进行规范化处理。
    from unsloth.chat_templates import standardize_sharegpt
    dataset = standardize_sharegpt(non_reasoning_dataset)
    
    non_reasoning_conversations = tokenizer.apply_chat_template(
        dataset["conversations"],
        tokenize = False,
    )
    print(non_reasoning_conversations[0])
    
    # 查看数据集尺寸
    print(len(reasoning_conversations))
    print(len(non_reasoning_conversations))
    
    # 非推理类数据集规模大的多。希望模型保留一定推理能力,训练数据选取75%推理类数据搭配25%对话类数据
    chat_percentage = 0.25
    
    import pandas as pd
    non_reasoning_subset = pd.Series(non_reasoning_conversations)
    non_reasoning_subset = non_reasoning_subset.sample(
        int(len(reasoning_conversations)*(chat_percentage/(1 - chat_percentage))),
        random_state = 0,  
    )
    
    # 打印各类数据量及实际比例用于验证
    print(len(reasoning_conversations))  
    print(len(non_reasoning_subset))   
    print(len(non_reasoning_subset) / (len(non_reasoning_subset) + len(reasoning_conversations)))
    
    # 合并推理类数据和抽样后的非推理类数据
    data = pd.concat([
        pd.Series(reasoning_conversations),  
        pd.Series(non_reasoning_subset)      
    ])
    data.name = "text"  # 为合并后的数据系列命名
    
    from datasets import Dataset
    # 将pandas DataFrame转换为Hugging Face数据集格式
    combined_dataset = Dataset.from_pandas(pd.DataFrame(data))
    # 对数据集进行随机打乱,确保数据分布均匀
    combined_dataset = combined_dataset.shuffle(seed = 0) 
    

    2.4.3 模型训练

    为加快训练速度,训练仅迭代30步。若需完整训练,可将num_train_epochs设为1,并将max_steps设为None以取消步数限制。

    # trl库是Hugging Face开发的,用于通过强化学习来微调与对齐大型语言模型的工具
    # SFTTrainer用于监督微调训练,SFTConfig用于配置训练参数
    from trl import SFTTrainer, SFTConfig
    import torch
    # 初始化SFTTrainer训练器
    trainer = SFTTrainer(
        model=model,
        tokenizer=tokenizer,
        train_dataset=combined_dataset,  # 训练数据集
        eval_dataset=None,  # 评估数据集
        args=SFTConfig(
            dataset_text_field="text",  # 数据集中用于训练的文本字段名称
            per_device_train_batch_size=2,  # 每个设备的训练批次大小
            # 梯度累积步数,通过累积梯度来模拟更大的批次大小
            # 实际等效批次大小 = per_device_train_batch_size * gradient_accumulation_steps
            gradient_accumulation_steps=4,
            warmup_steps=5,  # 学习率预热步数,逐步增加到设定的学习率
            # num_train_epochs = 1,  # 训练轮数,注释掉表示不使用轮数限制
            max_steps=30,  # 最大训练步数,达到后停止训练
            learning_rate=2e-4,  # 学习率,长时间训练建议降低到2e-5
            logging_steps=1,  # 每多少步记录一次日志
            optim="adamw_8bit",  # 使用8位AdamW优化器,节省内存
            weight_decay=0.01,  # 权重衰减系数,用于防止过拟合
            lr_scheduler_type="linear",  # 学习率调度器类型,此处为线性衰减
            seed=0,
            report_to="none",  # 日志报告工具
        ),
    )
    
    # 获取编号为0的GPU设备属性信息,包括名称、总内存等
    gpu_stats = torch.cuda.get_device_properties(0)
    # 计算当前程序已保留的最大GPU内存,也就是PyTorch的CUDA分配器最高向操作系统申请了多少内存
    start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)
    # 计算GPU的总内存容量
    max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)
    # 打印GPU名称和总内存信息
    print(f"GPU = {gpu_stats.name}. Max memory = {max_memory} GB.")
    # 打印当前已保留的GPU内存信息
    print(f"{start_gpu_memory} GB of memory reserved.")
    
    # 开始训练
    # resume_from_checkpoint是否从之前保存的检查点恢复训练
    trainer_stats = trainer.train(resume_from_checkpoint=False)
    
    # 计算GPU的最大预留内存
    used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)
    # 计算LoRA训练额外占用的GPU内存
    used_memory_for_lora = round(used_memory - start_gpu_memory, 3)
    # 计算峰值内存占GPU总内存的百分比
    used_percentage = round(used_memory / max_memory * 100, 3)
    # 计算LoRA训练占用内存占GPU总内存的百分比,保留3位小数
    lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)
    # 打印训练总耗时
    print(f"{trainer_stats.metrics['train_runtime']} seconds used for training.")
    # 打印GPU峰值预留内存
    print(f"Peak reserved memory = {used_memory} GB.")
    # 打印训练过程中额外占用的GPU峰值内存
    print(f"Peak reserved memory for training = {used_memory_for_lora} GB.")
    # 打印GPU峰值内存占总内存的百分比
    print(f"Peak reserved memory % of max memory = {used_percentage} %.")
    # 打印LoRA额外占用内存占GPU总内存的百分比
    print(f"Peak reserved memory for training % of max memory = {lora_percentage} %.")
    

    2.4.4 模型推理

    推理阶段,据Qwen-3团队的建议:

    • 若用于普通对话任务,推荐参数设置为:temperature=0.7,top_p=0.8,top_k=20。
    • 若用于推理任务,推荐参数设置为:temperature=0.6,top_p=0.95,top_k=20。

    以下代码对比这两种生成模式。前者直接给结果;后者先思考解题步骤再给结果,更适合需解释过程的数学问题场景。

    # 定义对话消息列表,包含用户的问题
    # 这里问题是求解方程 (x + 3)^2 = 0
    messages = [{"role": "user", "content": "Solve (x + 3)^2 = 0."}]
    
    # 使用tokenizer的聊天模板处理消息
    # 将消息转换为模型可以理解的格式
    text = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True,
        enable_thinking=False,
    )
    
    # 导入TextStreamer,用于实时流式输出模型生成的内容
    from transformers import TextStreamer
    
    _ = model.generate(
        **tokenizer(text, return_tensors="pt").to("cuda"),
        max_new_tokens=256,  # 最大生成的新token数量,控制回答长度
        temperature=0.7,
        top_p=0.8,
        top_k=20,
        streamer=TextStreamer(
            tokenizer, skip_prompt=True
        ),  # 流式输出器,跳过提示部分只显示回答
    )
    
    # 再次定义相同的用户问题,用于演示思考模式
    messages = [{"role": "user", "content": "Solve (x + 3)^2 = 0."}]
    
    # 使用聊天模板处理消息,这次启用思考模式
    text = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True,
        enable_thinking=True,  # 启用思考模式,模型会先思考再给出答案
    )
    
    # 在思考模式下生成回答
    _ = model.generate(
        **tokenizer(text, return_tensors="pt").to("cuda"),
        max_new_tokens=1024,  # 思考模式需要更多token来容纳思考过程
        temperature=0.6,  # 稍低的温度,使思考过程更集中
        top_p=0.95,  # 更高的核采样参数,允许更多样化的思考
        top_k=20,  # 同样从概率最高的20个token中选择
        streamer=TextStreamer(tokenizer, skip_prompt=True),
    )
    

    2.4.5 模型保存

    Unsloth支持两条互补的持久化路线:

    1. 保留LoRA适配器:体积最小,便于继续微调或增量更新;
    2. 合并并量化导出:得到独立权重文件,方便直接部署或上传到 Hub。

    保存LoRA适配器

    训练完成后,只需把模型与分词器以save_pretrained写入同一目录即可:

    model.save_pretrained("lora_model")      # 仅保存 LoRA 参数
    tokenizer.save_pretrained("lora_model")
    

    后续加载时,用from_pretrained接口指定本地路径,Unsloth会自动把基础模型与LoRA权重重新组装:

    from unsloth import FastLanguageModel
    
    model, tokenizer = FastLanguageModel.from_pretrained(
        model_name="lora_model",   # 加载lora参数,同时加载训练时的基础模型
        max_seq_length=2048,
        load_in_4bit=True,
    )
    

    合并后导出,用于部署

    可将LoRA合并到基础模型中,并支持将合并后的模型一次性导出为float16、int4或GGUF(GPT-Generated Unified Format)格式,便于在GPU或CPU端侧进行高效推理。

    GGUF是一种高效的模型存储与交换格式,将大模型封装为单一文件,具备秒级加载能力。GGUF可直接用于llama.cpp系列工具,实现快速部署与应用。

    # 导出float16完整权重
    model.save_pretrained_merged("model-f16", tokenizer, save_method="merged_16bit")
    
    # 导出int4量化权重,会有精度损失
    model.save_pretrained_merged("model-int4", tokenizer, save_method="merged_4bit_forced")
    
    # 导出GGUF系列
    model.save_pretrained_gguf("model-q8",  tokenizer)  # 默认 Q8_0
    model.save_pretrained_gguf("model-f16", tokenizer,
                               quantization_method="f16")  # 16-bit GGUF
    model.save_pretrained_gguf("model-q4",  tokenizer,
                               quantization_method="q4_k_m") # 4-bit GGUF
    

    3 参考

  • SpringBoot项目使用默认的Tomcat可以替换成性能更好的Undertow

    前言

    今天我们来聊聊一个很有意思的现象:为什么越来越多的大公司禁止SpringBoot项目使用默认的Tomcat,而强制要求使用Undertow?

    有些小伙伴在工作中可能已经发现了这个趋势,但背后的原因你真的清楚吗?

    一、SpringBoot的默认选择与现状

    SpringBoot作为Java领域最流行的开发框架,其默认内嵌的Web容器是Tomcat。

    这让我们很多开发者养成了”开箱即用”的习惯,但大公司却在生产环境中纷纷转向Undertow。

    这背后到底隐藏着什么秘密?

    image

    从上图可以看出,虽然Tomcat是默认选择,但Undertow在高性能场景下更具优势。

    二、性能对比

    2.1 内存占用对比

    让我们先看一组实际测试数据。在相同条件下部署SpringBoot应用:

    容器启动内存堆内存占用非堆内存占用线程内存
    Tomcat120MB80MB25MB15MB
    Undertow85MB60MB15MB10MB
    优化比例-29%-25%-40%-33%

    从数据可以看出,Undertow在内存占用方面有明显优势。

    对于大规模部署的微服务架构,这种内存节省会累积成巨大的成本优势。

    2.2 并发处理能力

    在并发性能测试中,Undertow同样表现优异:

    // 性能测试代码示例
    @SpringBootTest
    class WebContainerPerformanceTest {
        
        @Test
        void testConcurrentPerformance() {
            // 模拟1000并发用户持续请求30秒
            LoadTest loadTest = LoadTest.configure()
                .threads(1000)
                .duration(30, TimeUnit.SECONDS)
                .build();
                
            // Tomcat测试结果
            TomcatResult tomcatResult = loadTest.runWithTomcat();
            
            // Undertow测试结果  
            UndertowResult undertowResult = loadTest.runWithUndertow();
            
            // 结果对比
            System.out.println("QPS - Tomcat: " + tomcatResult.getQps());
            System.out.println("QPS - Undertow: " + undertowResult.getQps());
            System.out.println("平均响应时间 - Tomcat: " + tomcatResult.getAvgResponseTime());
            System.out.println("平均响应时间 - Undertow: " + undertowResult.getAvgResponseTime());
        }
    }
    

    典型测试结果:

    • Tomcat:QPS 8500,平均响应时间 15ms
    • Undertow:QPS 12000,平均响应时间 8ms

    三、底层架构差异

    3.1 Tomcat的架构设计

    Tomcat采用传统的BIO/NIO连接器架构:

    image

    Tomcat的架构相对重量级,每个层次都有明确的职责划分,但也带来了额外的开销。

    3.2 Undertow的架构设计

    Undertow采用更加现代的XNIO基础架构:

    image

    Undertow的核心特点:

    1. IO线程与工作线程分离:IO线程处理网络IO,工作线程处理业务逻辑
    2. 事件驱动架构:基于回调的事件处理机制
    3. 零拷贝能力:支持直接缓冲区,减少内存拷贝

    四、内存管理

    4.1 直接内存使用

    Undertow在内存管理上更加高效,大量使用直接内存(Direct Buffer):

    // Undertow的内存管理示例
    public class UndertowMemoryManagement {
        
        // 使用直接缓冲区处理请求
        public void handleRequest(HttpServerExchange exchange) {
            // 获取直接缓冲区
            ByteBuffer buffer = exchange.getConnection().getBufferPool().allocate();
            
            try {
                // 直接操作缓冲区,避免拷贝
                readRequestData(exchange, buffer);
                processRequest(buffer);
                writeResponse(exchange, buffer);
            } finally {
                // 释放缓冲区
                exchange.getConnection().getBufferPool().free(buffer);
            }
        }
        
        // Tomcat通常需要多次内存拷贝
        public void tomcatHandleRequest(Request request, Response response) {
            // 从输入流读取数据(内存拷贝)
            byte[] inputData = readInputStream(request.getInputStream());
            
            // 处理数据(可能再次拷贝)
            byte[] outputData = processData(inputData);
            
            // 写入输出流(又一次拷贝)
            response.getOutputStream().write(outputData);
        }
    }
    

    这种零拷贝的设计在大文件传输和高并发场景下优势明显。

    4.2 连接池优化

    Undertow的连接管理更加精细:

    # Undertow配置示例
    server:
      undertow:
        # 线程池配置
        threads:
          worker: 16
          io: 4
        # 缓冲区配置
        buffer-size: 1024
        direct-buffers: true
        # 连接配置
        max-connections: 10000
        max-http-post-size: 10485760
    

    对比Tomcat的配置:

    # Tomcat配置示例
    server:
      tomcat:
        # 连接器配置
        max-connections: 10000
        max-threads: 200
        min-spare-threads: 10
        # 其他配置
        max-http-post-size: 10485760
        connection-timeout: 20000
    

    五、并发模型

    5.1 Undertow的XNIO架构

    Undertow基于JBoss的XNIO库,采用更加现代的并发模型:

    // XNIO工作线程模型示例
    public class XNIOWorkerModel {
        
        public void demonstrateWorkerModel() {
            // 创建Worker实例
            XnioWorker worker = Xnio.getInstance().createWorker(
                OptionMap.create(Options.THREAD_DAEMON, true)
            );
            
            // IO线程处理网络事件
            worker.getIoThread().execute(() -> {
                // 处理IO就绪事件
                handleIOReadyEvents();
            });
            
            // 工作线程处理业务逻辑
            worker.getWorkerThreadPool().execute(() -> {
                // 执行业务处理
                executeBusinessLogic();
            });
        }
    }
    

    这种设计的优势在于:

    1. IO线程专注网络:不被业务逻辑阻塞
    2. 工作线程池弹性:根据业务需求动态调整
    3. 事件驱动高效:基于事件回调,减少线程切换

    5.2 Tomcat的线程模型对比

    Tomcat的传统线程模型:

    image

    Tomcat的线程模型在极高并发下会出现:

    • 大量的线程上下文切换开销
    • 线程阻塞等待资源
    • 内存占用随线程数线性增长

    六、配置灵活性

    6.1 精细化配置能力

    Undertow提供了极其细致的配置选项,满足各种复杂场景:

    @Configuration
    public class UndertowConfig {
        
        @Bean
        public UndertowServletWebServerFactory undertowServletWebServerFactory() {
            UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
            
            factory.addBuilderCustomizers(builder -> {
                // 配置HTTP/2
                builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true);
                
                // 配置缓冲区
                builder.setSocketOption(Options.RECEIVE_BUFFER, 1024 * 16);
                builder.setSocketOption(Options.SEND_BUFFER, 1024 * 64);
                
                // 配置线程池
                builder.setIoThreads(Runtime.getRuntime().availableProcessors());
                builder.setWorkerThreads(200);
                
                // 配置连接数限制
                builder.setServerOption(UndertowOptions.MAX_CONNECTIONS, 10000);
            });
            
            return factory;
        }
    }
    

    6.2 处理器链机制

    Undertow的处理器链机制允许深度定制请求处理流程:

    public class CustomHandler implements HttpHandler {
        
        private final HttpHandler next;
        
        public CustomHandler(HttpHandler next) {
            this.next = next;
        }
        
        @Override
        public void handleRequest(HttpServerExchange exchange) throws Exception {
            long startTime = System.currentTimeMillis();
            
            try {
                // 前置处理:认证、日志等
                preHandle(exchange);
                
                // 调用下一个处理器
                next.handleRequest(exchange);
                
            } finally {
                // 后置处理:统计、清理等
                postHandle(exchange, startTime);
            }
        }
        
        private void preHandle(HttpServerExchange exchange) {
            // 认证检查
            if (!checkAuthentication(exchange)) {
                exchange.setStatusCode(401);
                exchange.endExchange();
                return;
            }
            
            // 请求日志记录
            logRequest(exchange);
        }
    }
    

    这种灵活的处理器链机制让Undertow在定制化需求面前游刃有余。

    七、实战案例

    7.1 某电商平台的容器迁移实践

    某大型电商平台在高峰期面临严重的性能瓶颈,迁移到Undertow后的效果:

    迁移前(Tomcat):

    • 单机QPS:8000
    • 平均响应时间:25ms
    • 内存占用:2GB
    • CPU使用率:85%

    迁移后(Undertow):

    • 单机QPS:15000(+87%)
    • 平均响应时间:12ms(-52%)
    • 内存占用:1.2GB(-40%)
    • CPU使用率:65%(-23%)

    7.2 配置优化示例

    # 生产环境Undertow优化配置
    server:
      undertow:
        # IO线程数(通常为CPU核心数)
        io-threads: 8
        # 工作线程数(根据业务调整)
        worker-threads: 200
        # 直接缓冲区
        direct-buffers: true
        buffer-size: 16384
        # 连接配置
        max-connections: 10000
        max-http-post-size: 10485760
        # 优雅关闭
        no-request-timeout: 60000
        drain-wait-time: 20000
        
      # JVM优化配合
      port: 8080
      compression:
        enabled: true
        mime-types: text/html,text/xml,text/plain,application/json
    

    八、如何迁移?

    8.1 Maven配置调整

    <!-- 排除Tomcat -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <!-- 引入Undertow -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>
    

    8.2 迁移注意事项

    有些小伙伴在迁移过程中可能会遇到以下问题:

    1. Servlet API兼容性:确保代码使用标准Servlet API
    2. WebSocket配置:Undertow的WebSocket配置与Tomcat不同
    3. SSL配置:证书和SSL配置可能需要调整
    4. 会话管理:如果使用分布式会话,需要验证兼容性

    总结

    通过上面的详细分析,我们可以总结出大公司选择Undertow的主要原因:

    1 性能优势明显

    • 更高的并发处理能力:XNIO架构更适应高并发场景
    • 更低的内存占用:直接内存和缓冲区优化减少内存使用
    • 更好的响应时间:事件驱动模型减少处理延迟

    2 资源利用高效

    • 精细化的资源控制:线程池、缓冲区等可精细配置
    • 更好的可扩展性:适应云原生和容器化部署
    • 更低的运维成本:减少服务器数量和资源消耗

    3 技术架构先进

    • 现代化的并发模型:更适应现代硬件架构
    • 灵活的扩展机制:处理器链支持深度定制
    • 更好的未来发展:为HTTP/2、Quic等新协议做好准备

    4 业务需求驱动

    • 大规模部署需求:微服务架构下容器性能至关重要
    • 成本控制压力:性能提升直接转化为成本降低
    • 技术竞争力:保持技术栈的先进性和竞争力

    有些小伙伴可能会说:”我的项目并发量不大,用Tomcat也挺好”。

    确实,对于小型项目或个人项目,Tomcat完全够用。

    但对于大公司来说,技术选型要考虑的是规模化效应。

    当你有成千上万个微服务实例时,每个实例节省几十MB内存,总体节省的资源就是天文数字。

    我的建议是:对于新项目,特别是预期有高并发需求的微服务项目,优先考虑使用Undertow。对于现有项目,如果遇到性能瓶颈,可以考虑迁移到Undertow。

    技术选型没有绝对的对错,只有适合与否。

  • Windows 11 安装 SQLSERVER 出现问题解决

    装 sd 开心版的时候需要 SQLServer,结果各种方法试过了,一个劲的装不上。

    也算是装上了,但是服务启动不了(若装),错误码不是 1067 就是 1068,网上的各种大法也是试了一遍,但是都不行,没办法,只能看日志一点点解决了。

    检查安装日志

    先看一下这个摘要日志:

    代码语言:python

    代码运行次数:0

    运行

    AI代码解释

    Overall summary:
      Final result:                  失败: 请查看下面的详细信息
      Exit code (Decimal):           -2068578302
      Start time:                    2025-04-22 22:37:34
      End time:                      2025-04-22 22:45:18
      Requested action:              Repair
    
    Setup completed with required actions for features.
    Troubleshooting information for those features:
      Next step for DQ:              使用以下信息解决错误,然后再次尝试运行安装过程。
      Next step for FullText:        使用以下信息解决错误,然后再次尝试运行安装过程。
      Next step for AdvancedAnalytics: 使用以下信息解决错误,然后再次尝试运行安装过程。
      Next step for SQLEngine:       使用以下信息解决错误,然后再次尝试运行安装过程。
      Next step for Replication:     使用以下信息解决错误,然后再次尝试运行安装过程。
    
    
    Machine Properties:
      Machine name:                  DESKTOP-8NNEK6T
      Machine processor count:       12
      OS version:                    Microsoft Windows 11 专业工作站版 (10.0.26100)
      OS service pack:               
      OS region:                     中国
      OS language:                   中文(中国)
      OS architecture:               x64
      Process architecture:          64 位
      OS clustered:                  否
    
    Product features discovered:
      Product              Instance             Instance ID                    Feature                                  Language             Edition              Version         Clustered  Configured
      SQL Server 2022      MSSQLSERVER          MSSQL16.MSSQLSERVER            数据库引擎服务                                  1033                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      MSSQLSERVER          MSSQL16.MSSQLSERVER            数据库引擎服务                                  2052                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      MSSQLSERVER          MSSQL16.MSSQLSERVER            SQL Server 复制                            1033                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      MSSQLSERVER          MSSQL16.MSSQLSERVER            SQL Server 复制                            2052                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      MSSQLSERVER          MSSQL16.MSSQLSERVER            全文和语义提取搜索                                1033                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      MSSQLSERVER          MSSQL16.MSSQLSERVER            Data Quality Services                    1033                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      MSSQLSERVER          MSSQL16.MSSQLSERVER            Data Quality Services                    2052                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      MSSQLSERVER          MSSQL16.MSSQLSERVER            机器学习服务和语言扩展                              1033                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      MSSQLSERVER          MSAS16.MSSQLSERVER             Analysis Services                        1033                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      MSSQLSERVER          MSAS16.MSSQLSERVER             Analysis Services                        2052                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      SQLSERVER            MSSQL16.SQLSERVER              数据库引擎服务                                  1033                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      SQLSERVER            MSSQL16.SQLSERVER              数据库引擎服务                                  2052                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      SQLSERVER            MSSQL16.SQLSERVER              SQL Server 复制                            1033                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      SQLSERVER            MSSQL16.SQLSERVER              SQL Server 复制                            2052                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      SQLSERVER            MSSQL16.SQLSERVER              全文和语义提取搜索                                1033                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      SQLSERVER            MSSQL16.SQLSERVER              Data Quality Services                    1033                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      SQLSERVER            MSSQL16.SQLSERVER              Data Quality Services                    2052                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      SQLSERVER            MSSQL16.SQLSERVER              机器学习服务和语言扩展                              1033                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      SQLSERVER            MSAS16.SQLSERVER               Analysis Services                        1033                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      SQLSERVER            MSAS16.SQLSERVER               Analysis Services                        2052                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022      SQLEXPRESS           MSSQL16.SQLEXPRESS             数据库引擎服务                                  1033                 Express Edition      16.0.1000.6     否          是         
      SQL Server 2022      SQLEXPRESS           MSSQL16.SQLEXPRESS             数据库引擎服务                                  2052                 Express Edition      16.0.1000.6     否          是         
      SQL Server 2022      SQLEXPRESS           MSSQL16.SQLEXPRESS             SQL Server 复制                            1033                 Express Edition      16.0.1000.6     否          是         
      SQL Server 2022      SQLEXPRESS           MSSQL16.SQLEXPRESS             SQL Server 复制                            2052                 Express Edition      16.0.1000.6     否          是         
      SQL Server 2022      SQLEXPRESS           MSSQL16.SQLEXPRESS             全文和语义提取搜索                                1033                 Express Edition      16.0.1000.6     否          是         
      SQL Server 2022      SQLEXPRESS           MSSQL16.SQLEXPRESS             机器学习服务和语言扩展                              1033                 Express Edition      16.0.1000.6     否          是         
      SQL Server 2022                                                          Data Quality Client                      2052                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022                                                          Integration Services                     2052                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022                                                          Scale Out 主要角色                           2052                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022                                                          Scale Out 辅助角色                           2052                 Developer Edition    16.0.1000.6     否          是         
      SQL Server 2022                                                          LocalDB                                  2052                 Express Edition      16.0.1000.6     否          是         
      SQL Server 2022                                                          Master Data Services                     2052                 Developer Edition    16.0.1000.6     否          是         
    
    Package properties:
      Description:                   Microsoft SQL Server 2022 
      ProductName:                   SQL Server 2022
      Type:                          RTM
      Version:                       16
      SPLevel:                       0
      Installation location:         D:\SQL2022\Developer_CHS\x64\setup\
      Installation edition:          Developer
    
    注意: 请阅读 aka.ms/useterms 上的 Microsoft SQL Server 软件许可条款。
    
    用户输入设置:
      ACTION:                        Repair
      AGTDOMAINGROUP:                <空>
      AGTSVCACCOUNT:                 <空>
      AGTSVCPASSWORD:                <空>
      AGTSVCSTARTUPTYPE:             Manual
      ASCONFIGDIR:                   Config
      ASSVCACCOUNT:                  NT Service\MSSQLServerOLAPService
      ASSVCPASSWORD:                 <空>
      ASTELSVCACCT:                  NT Service\SSASTELEMETRY
      ASTELSVCPASSWORD:              <空>
      ASTELSVCSTARTUPTYPE:           Automatic
      CONFIGURATIONFILE:             C:\Program Files\Microsoft SQL Server\160\Setup Bootstrap\Log\20250422_223734\ConfigurationFile.ini
      ENU:                           false
      EXTSVCACCOUNT:                 NT Service\MSSQLLaunchpad
      EXTSVCPASSWORD:                <空>
      FAILOVERCLUSTERGROUP:          <空>
      FAILOVERCLUSTERNETWORKNAME:    <空>
      FTSVCACCOUNT:                  NT Service\MSSQLFDLauncher
      FTSVCPASSWORD:                 <空>
      HELP:                          false
      IACKNOWLEDGEENTCALLIMITS:      false
      INDICATEPROGRESS:              false
      INSTANCENAME:                  MSSQLSERVER
      ISMASTERSVCACCOUNT:            NT Service\SSISScaleOutMaster160
      ISMASTERSVCPASSWORD:           <空>
      ISMASTERSVCPORT:               8391
      ISMASTERSVCSSLCERTCN:          <空>
      ISMASTERSVCSTARTUPTYPE:        Automatic
      ISMASTERSVCTHUMBPRINT:         F5F1C4E7D076BDA8504D3263E1DBF27ADD8D95D6
      ISSVCACCOUNT:                  NT Service\MsDtsServer160
      ISSVCPASSWORD:                 <空>
      ISSVCSTARTUPTYPE:              Automatic
      ISTELSVCACCT:                  NT Service\SSISTELEMETRY160
      ISTELSVCPASSWORD:              <空>
      ISTELSVCSTARTUPTYPE:           Automatic
      ISWORKERSVCACCOUNT:            NT Service\SSISScaleOutWorker160
      ISWORKERSVCCERT:               <空>
      ISWORKERSVCMASTER:             <空>
      ISWORKERSVCPASSWORD:           <空>
      ISWORKERSVCSTARTUPTYPE:        Automatic
      QUIET:                         false
      QUIETSIMPLE:                   false
      SQLSVCACCOUNT:                 NT Service\MSSQLSERVER
      SQLSVCPASSWORD:                <空>
      SQLTELSVCACCT:                 NT Service\SQLTELEMETRY
      SQLTELSVCPASSWORD:             <空>
      SQLTELSVCSTARTUPTYPE:          Automatic
      SUPPRESSPAIDEDITIONNOTICE:     false
      SUPPRESSPRIVACYSTATEMENTNOTICE: false
      UIMODE:                        Normal
    
      Configuration file:            C:\Program Files\Microsoft SQL Server\160\Setup Bootstrap\Log\20250422_223734\ConfigurationFile.ini
    
    Detailed results:
      Feature:                       Master Data Services
      Status:                        已通过
    
      Feature:                       Data Quality Services
      Status:                        失败
      Reason for failure:            该功能的某个依赖项出错,导致该功能的安装过程失败。
      Next Step:                     使用以下信息解决错误,然后再次尝试运行安装过程。
      Component name:                SQL Server 数据库引擎服务实例功能
      Component error code:          0x84B40002
      Error description:             SQL Server 功能“SQL_Engine_Core_Inst”所处的状态不支持修复,因为从未成功配置该功能。只能修复成功安装的功能。若要继续,请删除指定的 SQL Server 功能。
      Error help link:               https://go.microsoft.com/fwlink?LinkId=20476&ProdName=Microsoft+SQL+Server&EvtSrc=setup.rll&EvtID=50000&ProdVer=16.0.1000.6&EvtType=0x2841E06E%401204%402&EvtType=0x2841E06E%401204%402
    
      Feature:                       全文和语义提取搜索
      Status:                        失败
      Reason for failure:            该功能的某个依赖项出错,导致该功能的安装过程失败。
      Next Step:                     使用以下信息解决错误,然后再次尝试运行安装过程。
      Component name:                SQL Server 数据库引擎服务实例功能
      Component error code:          0x84B40002
      Error description:             SQL Server 功能“SQL_Engine_Core_Inst”所处的状态不支持修复,因为从未成功配置该功能。只能修复成功安装的功能。若要继续,请删除指定的 SQL Server 功能。
      Error help link:               https://go.microsoft.com/fwlink?LinkId=20476&ProdName=Microsoft+SQL+Server&EvtSrc=setup.rll&EvtID=50000&ProdVer=16.0.1000.6&EvtType=0x2841E06E%401204%402&EvtType=0x2841E06E%401204%402
    
      Feature:                       机器学习服务和语言扩展
      Status:                        失败
      Reason for failure:            该功能的某个依赖项出错,导致该功能的安装过程失败。
      Next Step:                     使用以下信息解决错误,然后再次尝试运行安装过程。
      Component name:                SQL Server 数据库引擎服务实例功能
      Component error code:          0x84B40002
      Error description:             SQL Server 功能“SQL_Engine_Core_Inst”所处的状态不支持修复,因为从未成功配置该功能。只能修复成功安装的功能。若要继续,请删除指定的 SQL Server 功能。
      Error help link:               https://go.microsoft.com/fwlink?LinkId=20476&ProdName=Microsoft+SQL+Server&EvtSrc=setup.rll&EvtID=50000&ProdVer=16.0.1000.6&EvtType=0x2841E06E%401204%402&EvtType=0x2841E06E%401204%402
    
      Feature:                       数据库引擎服务
      Status:                        失败
      Reason for failure:            在此功能的安装过程中出错。
      Next Step:                     使用以下信息解决错误,然后再次尝试运行安装过程。
      Component name:                SQL Server 数据库引擎服务实例功能
      Component error code:          0x84B40002
      Error description:             SQL Server 功能“SQL_Engine_Core_Inst”所处的状态不支持修复,因为从未成功配置该功能。只能修复成功安装的功能。若要继续,请删除指定的 SQL Server 功能。
      Error help link:               https://go.microsoft.com/fwlink?LinkId=20476&ProdName=Microsoft+SQL+Server&EvtSrc=setup.rll&EvtID=50000&ProdVer=16.0.1000.6&EvtType=0x2841E06E%401204%402&EvtType=0x2841E06E%401204%402
    
      Feature:                       SQL Server 复制
      Status:                        失败
      Reason for failure:            该功能的某个依赖项出错,导致该功能的安装过程失败。
      Next Step:                     使用以下信息解决错误,然后再次尝试运行安装过程。
      Component name:                SQL Server 数据库引擎服务实例功能
      Component error code:          0x84B40002
      Error description:             SQL Server 功能“SQL_Engine_Core_Inst”所处的状态不支持修复,因为从未成功配置该功能。只能修复成功安装的功能。若要继续,请删除指定的 SQL Server 功能。
      Error help link:               https://go.microsoft.com/fwlink?LinkId=20476&ProdName=Microsoft+SQL+Server&EvtSrc=setup.rll&EvtID=50000&ProdVer=16.0.1000.6&EvtType=0x2841E06E%401204%402&EvtType=0x2841E06E%401204%402
    
      Feature:                       Analysis Services
      Status:                        已通过
    
      Feature:                       SQL Browser
      Status:                        已通过
    
      Feature:                       SQL 编写器
      Status:                        已通过
    
      Feature:                       LocalDB
      Status:                        已通过
    
      Feature:                       Scale Out 辅助角色
      Status:                        已通过
    
      Feature:                       Scale Out 主要角色
      Status:                        已通过
    
      Feature:                       Integration Services
      Status:                        已通过
    
      Feature:                       Data Quality Client
      Status:                        已通过
    
      Feature:                       安装程序支持文件
      Status:                        已通过
    
    Rules with failures or warnings:
    
    Rules report file:               C:\Program Files\Microsoft SQL Server\160\Setup Bootstrap\Log\20250422_223734\SystemConfigurationCheck_Report.htm

    可以看到关键的一行 Exit code (Decimal): -2068578302,对应的十六进制代码是0x84B30002,表示 SQL Server Setup has encountered an error while setting up the SQL Engine service.,也就是说 SQL Server 的核心组件(数据库引擎服务)在启动或修复时挂了。(没啥用)

    检查端口

    习惯性的检查一下端口有没有被占用(SQL Server 默认使用 TCP 1433 端口):

    代码语言:bash

    AI代码解释

    netstat -ano | findstr :1433

    发现没被占用。

    检查权限问题

    打开 事件查看器 (eventvwr.msc) → Windows 日志 → 应用程序 或 系统,查看是否有 “权限被拒绝”、“无法读取注册表项” 等错误。

    有一些来自 Service 控制管理器和SQLServer 的信息

    整理一下,可以从这里入手:

    • 错误名:SQLException64
    • 崩溃模块:sqllang.dll(SQL Server 的语言解析模块)
    • 进程名:sqlservr.exe
    • 崩溃地址:000000006341878F(可能是 DLL 内存地址)
    • 相关小型转储文件:.mdmp, .log, .xml,可用于分析故障细节

    似乎看不出来什么具体问题,至少不是权限的问题,但还是习惯性的检查一下内存和系统文件完整: sfc /scannowDISM /Online /Cleanup-Image /RestoreHealth

    去官方论坛找答案

    看样子主要是扇区的问题,一种说法是 SQL Server 存储引擎逻辑检测磁盘扇区大小,并将调整事务日志文件元数据和内部边界以匹配扇区大小(512 或 4096 字节)。当 SQL Server 检测到写入日志条目时,将生成错误消息 9012。

    但是 Windows 10 驱动程序不会报告物理存储的源扇区大小,但是Windows 11 原生 NVMe 驱动程序已更新,会直接报告 NVMe 存储设备实际扇区大小。

    改进的 Windows 11 驱动程序忽略了常见 NVMe 存储设备正在使用的仿真。例如,显示 8 KB 或 16 KB 的扇区大小,而不是模拟 Windows 所需的 4 KB 扇区大小。

    所以 Windows 10 升级 Windows 11 可能会出现这样的问题。

    更改区块大小为支持值

    这个操作要格式化磁盘,删掉所有东西(所以不推荐),用一些分区软件就可以做到。

    SQL Server 安装时会读取磁盘的“物理扇区大小”(Physical Sector Size),有些 NVMe 固态盘在 Get-Disk 下虽然显示 LogicalSectorSize = 4096,但 PhysicalSectorSize 却是 512,甚至是 0(代表驱动返回异常)。

    为此微软提供了一个隐藏注册表项,可用于强制逻辑 NVMe 驱动返回模拟的“扇区大小”,以规避 SQL Server 安装器的检查逻辑。

    🛠 操作步骤(模拟为 4KB)
    1. 打开 PowerShell(管理员模式)
    2. 运行命令:

    代码语言:powershell

    AI代码解释

    New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\stornvme\Parameters\Device" `
    -Name "ForcedPhysicalSectorSizeInBytes" `
    -PropertyType MultiString `
    -Force `
    -Value "* 4095"
    1. 验证是否设置成功:

    代码语言:powershell

    AI代码解释

    Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\stornvme\Parameters\Device" `
    -Name "ForcedPhysicalSectorSizeInBytes"
    1. 重启电脑后,再次运行 SQL Server 安装器即可(重新安装)。

    不重新安装可能会出现 无法使用文件“…\master.mdf” 的情况,

    这是因为它最初采用扇区大小 4096(旧) 的格式,现在位于扇区大小为 8192 (新)的卷上。

    如果不想重新安装可以请将 master.mdf 移到其扇区大小小于或等于原始扇区大小的卷。**


    参数解释
    参数含义
    stornvme标准 NVMe 驱动服务名称(适用于大多数固态硬盘)
    ForcedPhysicalSectorSizeInBytes注册表项,强制驱动上报的物理扇区大小
    * 4095作用于所有设备,模拟返回“4095” 字节作为物理扇区大小(安装器识别为“非 4KB”,从而绕过检查)

    注意4095 是一个小 trick —— 它不是合法扇区大小,但可以绕开 SQL Server 对“64KB 不匹配”所触发的阻止提示。

    恢复原状方法

    如果安装完 SQL Server 后你想恢复默认设置:

    代码语言:powershell

    AI代码解释

    Remove-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\stornvme\Parameters\Device" `
    -Name "ForcedPhysicalSectorSizeInBytes"

    然后重启即可。

    不过这样 SQL Server 就无法启动了,重新修改回去依然可以使用。

    加 -T1800 启动参数

    看到其中一种方法是启用 SQL Server 跟踪标志 1800。在 SQL Server 中:

    Trace Flag 1800 的作用是: 启用为每个 NUMA 节点分配一个独立的内存分配器(memory node allocator)。它会强制 SQL Server 在启动时为每个 NUMA 节点设置一个内存分配器,以 改善 NUMA 架构下的性能,尤其是在高并发或内存压力较大时的场景。

    但是在我这里似乎没用。


    解决了

    可以正常运行了。

  • Docker Registry删除镜像

    问题描述:

    由于某些原因我们需要删除docker registry中的镜像,对于新手来说这个操作可能无从下手,下面我提供两种方式解决这个问题(推荐第一种):

    一、调用registry的接口进行删除

    步骤如下(请严格按照我的步骤操作,否则可能会失败):

    ①进入registry容器:

     docker exec -it registry /bin/sh 

    ②编辑容器内/etc/docker/registry/config.yml配置文件增加如下配置:

    storage:
       delete:
           enabled: true

    修改后效果如下:

    ③重启registry容器:

    docker restart registry

    ④查询镜像tag,接口请求格式为:

    curl <镜像仓库地址>/v2/<镜像名>/tags/list

    示例如下:

    curl http://172.15.110.35:5555/v2/openresty/tags/list

    响应如下:

    {"name":"openresty","tags":["1.15.8.1-20200728","test"]}

    ⑤查询对应tag镜像的digest_hash,接口请求格式为:

    curl --header "Accept: application/vnd.docker.distribution.manifest.v2+json" -I -X GET <镜像仓库>/v2/<镜像名称>/manifests/<镜像tag>

    示例如下:

    curl --header "Accept: application/vnd.docker.distribution.manifest.v2+json" -I -X GET http://172.15.110.35:5555/v2/openresty/manifests/test

    响应如下:

    
    HTTP/1.1 200 OK
    Content-Length: 952
    Content-Type: application/vnd.docker.distribution.manifest.v2+json
    Docker-Content-Digest: sha256:815386ebbe9a3490f38785ab11bda34ec8dacf4634af77b8912832d4f85dca04
    Docker-Distribution-Api-Version: registry/2.0
    Etag: "sha256:815386ebbe9a3490f38785ab11bda34ec8dacf4634af77b8912832d4f85dca04"
    X-Content-Type-Options: nosniff
    Date: Wed, 08 Dec 2021 03:35:33 GMT

    记住Docker-Content-Digest:后面的值

    ⑥调用接口删除镜像,接口请求格式为:

    curl -I -X DELETE <镜像地址>/v2/<镜像名称>/manifests/<Docker-Content-Digest>

    请求示例:

    curl -I -X DELETE http://172.15.110.35:5555/v2/openresty/manifests/sha256:815386ebbe9a3490f38785ab11bda34ec8dacf4634af77b8912832d4f85dca04

    响应示例为:

    HTTP/1.1 202 Accepted
    Docker-Distribution-Api-Version: registry/2.0
    X-Content-Type-Options: nosniff
    Date: Wed, 08 Dec 2021 02:59:01 GMT
    Content-Length: 0

    ⑦执行垃圾回收,清理镜像文件,命令格式为:

    docker exec -it <镜像仓库容器ID/名称> /bin/registry garbage-collect <镜像仓库配置文件>

    示例为:

    docker exec -it registry /bin/registry garbage-collect /etc/docker/registry/config.yml

    二、直接删除镜像的repositories(不建议新手使用)

    ①打开镜像的存储目录,删除镜像文件夹,格式为

    docker exec registry /bin/sh rm -rf  /var/lib/registry/docker/registry/v2/repositories/<镜像名称>

    示例为:

    docker exec registry /bin/sh rm -rf  /var/lib/registry/docker/registry/v2/repositories/openresty

    ②执行垃圾回收,清理镜像文件,命令格式为:

    docker exec <镜像仓库容器ID/名称> /bin/registry garbage-collect <镜像仓库配置文件>

    示例为:

    docker exec registry /bin/registry garbage-collect /etc/docker/registry/config.yml
  • docker “no space left on device” 解决方案

    问题原因:出现此问题一般是 docker 根目录空间不足导致

    解决方案:修改 Docker Root Dir 的值,指向一个更大空间的目录.

    1. 查看docker磁盘使用情况

    docker system df

    2. 查看docker挂载目录

    docker info  | grep "Docker Root Dir"

    默认目录为/var/lib/docker

    查看目录的占用情况

    df -hl /var/lib/docker

    3. 关闭docker

    systemctl stop docker

    4. 创建新的挂载目录

    mkdir -p /app/dockerdata

    5. 复制数据

    mv /var/lib/docker /app/dockerdata/

    6. 修改docker配置文件

    vim /lib/systemd/system/docker.service

    修改ExecStart=/usr/bin/dockerd-current下行后面加

    --graph /app/dockerdata/docker
    image

    7. 重启docker

    1. systemctl disable docker
    2. systemctl enable docker
    3. systemctl daemon-reload
    4. systemctl start docker

    8. 查看挂载目录

    docker info  | grep "Docker Root Dir"

    9. 修改完成

    image
  • 开源的远程桌面神器——RustDesk

    一、先吐槽两句

    你是不是跟我一样,隔三差五就被家里人、朋友、同事“远程召唤”》》》

    “晓凡啊,电脑又蓝屏啦!帮我远程看一下”

    “晓凡,Excel 怎么突然打不开了?帮我远程看一下”

    “凡哥,TeamViewer 又提示商业用途不让用了,怎么办? 向日葵免费使用时常到了,用不了了怎么办?”

    “凡哥,凡哥,你开发的凡财助手程序是不是有bug啊,点了这个按钮报错了,帮远程看看“ 。。。。。

    那你肯定用过 TeamViewerAnyDesk 或者向日葵。

    好用归好用,但要么弹窗提醒“商业用途请付费”,要么限速限得跟 2G 网似的。

    最让人头皮发麻的是:你的远程画面、文件传输,统统要先经过别人的服务器,鬼知道中间有没有被留底。

    于是我把目光投向了今天的主角——RustDesk

    今天咱们就花点时间,聊聊它到底是啥、能干啥、怎么玩?

    二、RustDesk 是什么?

    第一次看到这名字,很多人第一反应是:“Rust?是那个号称‘无 GC、无NULL、编译器当你亲妈’的新语言?”

    没错,RustDesk主要就是用 Rust 写的,它就是一款远程桌面软件,功能对标 TeamViewerAnyDesk、向日葵,但完全免费,完全开源。

    仓库地址: https://github.com/rustdesk/rustdesk

    目前已有 96.8k stars ,还在涨

    协议: AGPL-3.0,放心大胆商用

    RustDesk 仓库

    三、为什么它香?

    1. 跨平台爽到飞起
      WindowsmacOSLinuxiOSAndroid 全覆盖。跨平台
    2. 开箱即用
      官网下载安装包,双击,打开就能看到本机 ID 和密码,别人一输就连,连注册登录都省了。
    3. 安全可靠
      Rust 语言天生内存安全,端到端 AES-256 加密,自建服务器连 TLS 证书都能自己签,彻底杜绝中间人。默认它提供公共中继,但你可以把服务端一键部署到自己云主机。数据不过第三方,老板再也不用担心“数据外泄”。
    4. 功能一个不少
      文件传输、剪贴板同步、音频转发、屏幕录制、多显示器、锁屏、重启……甚至还能给远程电脑打命令行。
    5. 体积小,跑得飞快Windows 安装包 22MB 左右,老掉牙的笔记本也能跑。
      我 2014 年的 ThinkPad X240,4G 内存,1080p 分辨率下流畅无压力。

    四、三分钟上手

    第一步:下载

    GitHub 的 release 页面直接给 exedmgdebapk 全打好包。

    rustdesk下载

    嫌 GitHub 慢?晓凡同步放了一份在网盘上,贴心。
    链接:https://pan.quark.cn/s/fd9ce952dd75
    提取码:5zQ1

    第二步:安装

    一路下一步,唯一要注意的是:
    Windows 用户如果装了 360,可能会提示“驱动拦截”,点允许即可。RustDesk 会在本地装一个虚拟显示驱动,用来捕获画面,不是木马。

    第三步:获取 ID 和一次性密码

    打开软件,左侧会显示一串数字,比如 123 456 789,下面有一行临时密码。
    把这两串东西发给你的同事/爸妈/客户,对方在“远程 ID”里输入,点连接,输入密码,就能看到你屏幕了。

    获取 ID 和一次性密码

    第四步(可选):自建中继

    如果你对隐私有执念,或者公司网络屏蔽了默认中继,可以花 5 分钟搭一个。
    一条 Docker 命令搞定:

    docker run --name rustdesk -p 21115:21115 -p 21116:21116 -p 21116:21116/udp -p 21118:21118 \  -v /data/rustdesk:/root rustdesk/rustdesk-server hbbs -r your.domain.com

    再把客户端设置里的“中继服务器”改成自己的域名,完事。

    整个流程比泡一碗方便面还快。

    五、目录结构长啥样?

    我简单截一段源码目录,给同行们一个直观感受:

    rustdesk/├── src/│   ├── client.rs         # 客户端主逻辑│   ├── server/           # 音频/视频/剪贴板/输入服务│   └── platform/         # 各平台适配├── flutter/│   ├── lib/              # Flutter 写的桌面 & 移动端 UI│   └── web/              # Web 端└── libs/    ├── scrap/            # 屏幕捕获    ├── enigo/            # 键盘鼠标控制    └── clipboard/        # 剪贴板同步
    代码

    六、真实使用场景

    1. 远程办公
      公司开发机全在内网,我直接在笔记本上连回工位机,IDE 延迟低于 30 ms,敲代码跟本地几乎没差。
    2. 技术支持
      朋友开了家小工作室,买了 10 台电脑,用 RustDesk自建服务器,员工出问题直接远程解决,再也不用满城跑。
    3. 家庭监控
      把旧 Android 手机挂家里,装 RustDesk Android 客户端,随时连过去看娃写作业有没有摸鱼。
    4. 现场调试
      做嵌入式开发时,树莓派跑 Linux,无显示器,RustDesk 一键连进去,比串口爽多了。

    七、踩过的坑

    1. Windows 防火墙
      第一次连不上八成是 21115-21119 端口没放行,把防火墙入站规则开一下即可。
    2. 公司网络禁止 UDP
      P2P 打洞失败,会自动降级到 TCP 中继,速度会掉,自建中继可破。
    3. Android 被控端需额外插件
      Google Play 版本因为权限问题阉割了被控功能,要去 GitHub 下载完整 APK
    4. 端口被运营商封
      默认中继用 21115-21118,有用户反馈移动宽带被封。
      解决:自建中继,把端口改成 443,运营商一般不敢封 HTTPS。
    5. macOS 提示“无法打开,因为无法验证开发者”
      解决:系统设置 → 隐私与安全 → 允许任何来源,或者右键→打开。
    6. 安卓手机控制电脑,输入法抢焦点
      解决:手机端设置里关闭“软键盘同步”,或者电脑端把远程窗口全屏。

    八、进阶玩法

    1. 二次开发
      官方 Flutter UI已经给你写好了,改改 Logo、加几行业务代码就能变成“某某公司专属远程协助工具”,打包发客户倍儿有面子。
    2. 与内网穿透联动
      把 RustDesk 中继部署在 FRP/NPS 后面,实现“公司没公网也能远程”。

    九、和同类软件对比

    功能/软件RustDeskTeamViewerAnyDesk向日葵
    是否开源
    自建服务任意企业版企业版付费
    免费限速
    安全审计全透明黑盒黑盒黑盒
    客户端体积~10 MB~50 MB~40 MB~30 MB

    十、小结

    • 对个人免费,无广告;
    • 对公司可二开、可内嵌、可集成;
    • 安全可控,数据永远在自己手里;
    • 社区活跃,issue 回复快。

    如果你还在给 TeamViewer 交年费,或者每天被限速气得拍桌子,不妨花十分钟试试 RustDesk

    开源世界这么大,总有一款工具让你泪流满面地喊“真香”!

  • Git提交错了,修改

    引子
    写代码就像炒菜,锅铲一抖盐放多了还能加水,Git 提交错了也能“回锅”。

    但回锅方法不对,可能把整锅菜都糊掉。

    今天咱们就掰开揉碎聊聊:到底有哪些“提交错了”的场景?

    每种场景到底该怎么优雅地撤回?全部给你配好命令、画好流程,照着抄就行。

    一、先分清“锅”在哪儿

    首先我们得分清“锅”在哪儿,本地还是远程?

    Git 把仓库分成三大块:

    1. 工作区(Working Directory):你电脑上看得见的文件夹。
    2. 暂存区(Index / Stage):git add 之后放东西的地方。
    3. 本地仓库(Local Repo):git commit 之后放东西的地方。
    4. 远程仓库(Remote Repo):GitHub、GitLab、gitee 等远端服务器。

    搞错一次提交,先问自己一句:
    “锅”现在停留在哪一层?

    • 只在工作区?
    • 只在暂存区?
    • 已经 commit 但还没 push?
    • 已经 push?
    • 甚至 push 完别人已经拉下来继续开发了?

    不同位置,撤回姿势完全不同。下面分场景,逐个拆招。

    二、场景 1

    add 错了,还没 commit

    症状
    git add 了不该 add 的文件,比如把 node_modules 也扔进去了,但还没 commit。

    解决
    把东西从暂存区踢回工作区即可:

    # 全部撤回git reset HEAD . # 只撤回某个文件git reset HEAD package-lock.json
    add 错了,还没 commit

    三、场景 2

    commit 写错信息,或忘了加文件

    A. 只想改 commit message

    git commit --amend -m "新的提交说明"

    B. 漏了文件

    git add forgotten.javagit commit --amend --no-edit   # 不改动 message
    commit 写错信息,或忘了加文件

    注意:amend 会生成新的 commit-id,如果已经 push 过,就属于“改写历史”,需要强制推送(见后文)。

    四、场景 3

    commit 错了,但还没 push

    1. 最后一次 commit 想直接作废
    # 撤回 commit,改动保留在工作区git reset --soft HEAD~1# 或者git reset --mixed HEAD~1   # 默认模式,改动回到工作区
    1. 连改动都不要,彻底删除
    git reset --hard HEAD~1
    1. 倒数第 N 次提交都错了
    # 回退 3 个提交git reset --hard HEAD~3

    流程图

    ommit 错了,但还没 push

    注意:–hard 会丢改动,先确认没重要代码。

    五、场景 4

    已经 push,但没人基于它开发

    思路:先本地回退,再强制推送。

    步骤
    1)本地回退

    git reset --hard <回退到的commit-id>

    2)强制覆盖远端

    git push --force-with-lease origin main

    为什么用 --force-with-lease 而不是 --force
    前者会检查远端有没有人比你先 push,避免把同事的 commit 冲掉,更安全。

    已经 push,但没人基于它开发

    六、场景 5

    已经 push,且同事已拉取并继续开发

    此时“改写历史”会让同事陷入混乱,禁止 reset + force push。
    正确姿势:用“反转提交”(revert)。

    示例

    # 生成一个新的 commit,把错误提交的内容“反着做一遍”git revert <错误commit-id>git push origin main

    如果一次 revert 不够,可以连续 revert:

    git revert OLDEST_COMMIT^..NEWEST_COMMIT

    流程图
    main: A-B-C-D-E(错误)
    revert 后:A-B-C-D-E-F(撤销E)

    已经 push,且同事已拉取并继续开发

    优点:历史干净、无冲突风险;缺点:会多一个 commit,强迫症可能不爽。

    七、场景 6

    merge 错了,还没 push

    A. 刚 merge,发现合错分支

    git reset --hard HEAD~1   # 直接回到 merge 前

    B. merge 了很久,已产生大量后续 commit
    思路:用 git revert -m 反转 merge commit。

    git revert -m 1 <merge-commit-id>

    -m 1 表示保留 merge 的第一个父分支(通常是 main)。

    merge 错了,还没 push

    八、场景 7

    rebase 错了,想反悔

    rebase 过程中冲突太多,想直接放弃:

    git rebase --abort

    已经 rebase 完但后悔了:

    # 查看 reflog 找到 rebase 前的 HEADgit refloggit reset --hard HEAD@{2}

    九、场景 8

    cherry-pick 错了

    # 撤销刚 cherry-pick 的 commitgit cherry-pick --abort

    如果已经 commit,可用 revert 回滚单个 cherry-pick 的 commit。

    cherry-pick 错了

    十、万能后悔药:reflog

    Git 在本地会记录每一次 HEAD 的移动。
    不管 reset、rebase、merge 玩得多花,只要没 gc,都能找到“案发前”的位置。

    git reflog# 找到想回去的 idgit reset --hard 9f3e2a1
    image

    十一、一张总览流程图

    记住一图就行

    十二、踩坑小贴士

    1. 任何 reset –hard 前,先 stash 或备份分支:git branch backup
    2. 多人协作时,默认“不能强推”,可在服务端开启保护分支。
    3. 强制推送后,通知团队所有人执行 git pull --rebase 同步。
    4. 重要操作前,用 git log --oneline --graph 看一眼历史,心中有数。
    5. 养成 commit 粒度细、消息清晰的习惯,能减少 80% 回滚需求。

    十三、记住这一句话

    add 错了 reset

    commit 错了 amend/reset

    push 错了先问队友,没人用就 force,有人用就 revert。

    实在搞不清,reflog 带你穿越回过去。

  • 面向海量关系型数据的实时全文检索:从 Elasticsearch 到 Logstash 的架构解析

    引言

    当在企业应用中的关系型数据库的数据量从百万级攀升至千万甚至亿级时,要如何对这些海量数据进行高效、精准且功能丰富的查询?

    传统的数据库查询方式比如通过 LIKE '%keyword%' 实现的模糊匹配,数据量激增后性能会急剧下降,甚至导致数据库服务宕机。其根本原因在于关系型数据库的索引(如 B-Tree)主要是为精确匹配和范围查询设计的,而非为非结构化的文本内容检索优化。这会导致几个核心问题:

    1. 性能瓶颈:全表扫描或索引失效,查询耗时随数据量线性增长。
    2. 功能局限:无法实现分词、同义词、相关性排序、高亮显示等现代搜索应用的基本功能。
    3. 业务影响:缓慢的查询严重影响用户体验,制约了业务功能的创新与发展。

    为了解决这个问题,第一个想到的方案是引入专业的全文搜索引擎,将关系型数据库中的数据同步至搜索引擎中,由后者专门负责处理复杂的文本检索需求。这种“数据库 + 搜索引擎”的异构架构,能够充分发挥两者各自的优势,实现高性能、功能丰富的查询体验。

    根据我个人的调研,本文将围绕这一核心思想展开探索。将从:

    1. 核心原理:说明倒排索引、分词、相关性评分等全文检索背后的关键技术。
    2. 对比主流平台:对 Elasticsearch、OpenSearch、Solr 和 Meilisearch 进行技术选型与横向对比。
    3. 数据同步:重点研究讨论 Logstash JDBC 方案,同时分析其他多种数据同步方案的利弊。
    4. 构建完整架构:展示最终的系统架构图,并讨论部署和运维中的关键考量。

    因当前处于技术选型阶段,文中内容都处于研究阶段,在具体实践中不保证完全生效,具体实施还需之后进一步深入实践。本人也是搜索引擎初学者,会继续研究相关内容。


    2. 技术背景 – 全文检索的核心概念

    在深入探讨具体的搜索引擎技术之前,有必要先理解支撑所有现代搜索引擎高效运作的几个核心概念。

    2.1. 核心数据结构:倒排索引 (Inverted Index)

    关系型数据库为了加速查询,通常会为特定列创建比如 B-Tree 索引,这是一种“正向索引”,即从“文档 ID”映射到“文档内容”。而全文搜索引擎的核心数据结构恰恰相反,采用的是倒排索引(Inverted Index)

    倒排索引的核心思想是建立从“词元(Term)”到“包含该词元的文档列表”的映射。一个典型的倒排索引包含两个主要部分:

    1. 词元词典 (Term Dictionary):包含了文档中出现过的所有不重复的词元。为了快速查找,这个词典通常会使用 B-Tree 或哈希表等高效的数据结构进行存储。
    2. 倒排列表 (Postings List):对于词元词典中的每一个词元,都有一个与之关联的列表,该列表记录了所有包含这个词元的文档 ID。此外,为了进行相关性计算,这个列表通常还会存储词元在每个文档中出现的频率、位置等信息。

    下面是倒排索引工作原理的示意图:

    根据以上图表举个例子

    假设我们有以下两个中文文档:

    • 文档 1: “全文检索技术是搜索引擎的核心。”
    • 文档 2: “搜索引擎依赖倒排索引技术。”

    经过中文分词和处理后,生成的简化的倒排索引可能如下:

    词元 (Term)倒排列表 (Postings List)
    “全文”[文档 1]
    “检索”[文档 1]
    “技术”[文档 1, 文档 2]
    “是”[文档 1]
    “搜索”[文档 1, 文档 2]
    “引擎”[文档 1, 文档 2]
    “核心”[文档 1]
    “依赖”[文档 2]
    “倒排”[文档 2]
    “索引”[文档 2]

    当用户搜索 “核心 技术” 时,搜索引擎会:

    1. 查找 “核心” 的倒排列表:[文档 1]
    2. 查找 “技术” 的倒排列表:[文档 1, 文档 2]
    3. 对两个列表进行交集运算,得到结果:[文档 1]

    通过这种方式,搜索引擎避免了对所有文档进行全量扫描,而是直接定位到包含查询词的文档,从而实现了毫秒级的查询响应。

    2.2. 文本处理核心:分词与分析 (Tokenization and Analysis)

    原始的文本数据是无法直接用于构建倒排索引的,必须经过一个称为分析(Analysis)的过程。分析过程将一段文本转换成一系列可供索引的标准化词元(Tokens)。这个过程通常由一个分析器(Analyzer)完成,而分析器又由以下三个组件按顺序构成:

    1. 字符过滤器 (Character Filters):在文本被分词之前对其进行预处理。例如,去除 HTML 标签,或者将特定字符进行替换。
    2. 分词器 (Tokenizer):将连续的文本流切分成独立的词元。例如,一个标准的分词器会根据空格和标点符号将 “powerful search engine” 切分为 powerfulsearchengine。对于中文等语言,则需要更复杂的、基于词典或机器学习模型的分词器(如 IK Analyzer, Jieba)。
    3. 词元过滤器 (Token Filters):对分词器输出的词元进行进一步的加工和标准化。常见的操作包括:
      • 转为小写 (Lowercase):将所有词元统一转为小写,使得搜索不区分大小写。
      • 去除停用词 (Stop Words Removal):移除如 “a”, “is”, “the” 等常见但对搜索意义不大的词语。
      • 词干提取 (Stemming):将单词还原为其基本形式(词干)。例如,”’searches”’, ”’searching”’, ”’searched”’ 都会被还原为 ”’search”’。
      • 同义词处理 (Synonym Expansion):将一个词元扩展为其同义词,以扩大搜索范围。例如,搜索“笔记本”时,也能匹配到包含“手提电脑”的文档。

    通过精心的分词与分析策略,搜索引擎能够极大地提升搜索的准确性(Precision)和召回率(Recall)。

    2.3. 结果排序的艺术:相关性评分 (Relevance Scoring)

    全文检索的另一个核心优势在于它能够根据查询的相关性对结果进行排序,而不仅仅是返回匹配的文档。这种排序能力是通过相关性评分算法实现的。

    早期的搜索引擎广泛使用 TF-IDF (Term Frequency-Inverse Document Frequency) 模型。其核心思想是:

    • 词频 (TF):一个词元在单个文档中出现的频率越高,该文档与该词元的相关性就越强。
    • 逆文档频率 (IDF):一个词元在整个文档集合中出现的频率越低(即越罕见),它对区分文档的重要性就越大,权重也应该越高。

    现代搜索引擎,如 Elasticsearch 和 Solr,默认使用一种更先进的模型,称为 BM25 (Best Match 25)。BM25 是 TF-IDF 的一种改进,它解决了 TF-IDF 中词频对评分影响无限增长的问题(即一个词出现 100 次不应该比出现 50 次重要两倍),并引入了对文档长度的考量,使得评分更加合理。

    这些复杂的评分模型使得搜索引擎能够返回最符合用户查询意图的结果,这是 SQL LIKE 查询完全无法企及的。


    3. 搜索引擎对比分析

    简单的聊了一下搜索引擎原理之后,看看当前市场上常用的搜索引擎。有多种成熟的搜索引擎可供选择,选择以下四种进行性对比:ElasticsearchOpenSearchApache Solr 和 Meilisearch。它们各自在不同的场景下具有独特的优势和劣势。

    3.1. 方案概述

    • Elasticsearch: 当今最流行和功能最丰富的搜索引擎之一,基于 Apache Lucene 构建。它以其强大的分布式特性、易于使用的 RESTful API 和庞大的生态系统(特别是 ELK Stack:Elasticsearch, Logstash, Kibana,现还包括 Beats)而闻名,广泛应用于日志处理、应用程序性能监控(APM)、安全分析(SIEM)和复杂的全文检索场景。
    • OpenSearch: 由亚马逊云科技(AWS)牵头,从 Elasticsearch 7.10.2 版本分叉出来的开源项目。OpenSearch 旨在提供一个完全开放、由社区驱动、且遵循宽松的 Apache 2.0 许可的替代方案。其核心目标是保持与上游分叉版本的高度 API 兼容性,确保用户能够无缝迁移,并在此基础上独立发展其特性和生态系统。
    • Apache Solr: 同样基于 Apache Lucene,是 Elasticsearch 出现之前最主流的开源搜索引擎。它是一个非常成熟、稳定且功能强大的平台,在企业级搜索领域拥有深厚的积累,适合需要高度定制化和复杂查询的传统企业搜索应用,如电子商务、内容管理系统(CMS)和大规模数据目录。
    • Meilisearch: 一个相对较新的轻量级、开源搜索引擎,使用 Rust 语言编写,主打开发者体验易用性极致的即时搜索性能。它被设计为“开箱即用”,无需复杂的配置或运维,提供了极快的查询速度(通常 < 50ms)和对开发者极其友好的体验。专注于为终端用户提供搜索界面的应用程序提供极简的安装和集成体验,尤其适合中小型网站、移动应用、电子商务店铺和需要快速原型设计的场景。

    3.2. 方案对比

    ElasticsearchOpenSearchApache SolrMeilisearch
    核心架构分布式,面向文档分布式,面向文档分布式,面向文档/集合单体式
    核心优势强大的生态系统 (ELK),丰富的功能,大规模数据处理能力真正的 Apache 2.0 许可,完全开源,社区驱动,与 AWS 生态深度集成成熟稳定,高度可配置和可扩展,强大的文本分析功能极致的性能和易用性,开箱即用的相关性与拼写纠错
    易用性相对容易上手,Kibana 提供了强大的 UI与 Elasticsearch 类似,OpenSearch Dashboards 功能对等 Kibana学习曲线较陡峭,配置相对复杂,管理界面功能较弱极其简单,API 设计友好,几分钟内即可搭建并运行
    性能非常高,尤其擅长聚合和分析查询与同版本的 Elasticsearch 性能相当非常高,在某些特定场景下(如静态数据)可能优于 ES极高,专为“按键式”即时搜索优化,亚秒级响应
    可扩展性极佳,专为水平扩展设计,可轻松扩展至数百个节点与 Elasticsearch 相同,具备优秀的水平扩展能力极佳,通过 SolrCloud 模式支持大规模集群部署有限,当前版本主要为单机部署,扩展性是其短板
    生态与工具极其丰富,拥有 Logstash, Beats, Kibana 等完整工具链正在快速发展,兼容大部分 Elasticsearch 工具,拥有自己的生态项目丰富,拥有众多第三方集成,但不如 ELK Stack 整合度高正在增长,提供了多种语言的 SDK,但整体生态较小
    社区与支持庞大且活跃的社区,Elastic 公司提供商业支持快速增长的社区,由 AWS 和多个合作伙伴支持非常成熟的社区,由 Apache 软件基金会支持活跃且友好的社区,Meilisearch 公司提供商业支持
    许可协议SSPL + Elastic License (双重许可),对云服务商不友好Apache License 2.0 (ALv2),完全开源Apache License 2.0 (ALv2),完全开源MIT License,非常宽松的开源许可
    理想用例日志分析、企业级搜索、业务智能 (BI)、安全分析等复杂场景需要完全开源的 Elasticsearch 替代方案,尤其是在 AWS 上部署传统的企业搜索、文档检索、需要深度定制化的搜索应用前端应用搜索框、移动应用、需要极低延迟和良好用户体验的场景

    3.3. 选型决策

    结合“海量关系型数据”和“企业级应用”场景,最终选择使用 Elasticsearch。决策点如下:

    1. 十分完备的生态系统集成:解决方案需要一个强大的数据同步工具。Logstash 作为 ELK Stack 的核心组件,与 Elasticsearch 的集成是无缝且经过最广泛验证的。使用 Logstash JDBC 插件可以极大地简化从关系型数据库抽取数据的过程,其丰富的过滤器插件也为数据清洗和转换提供了便利。这种“全家桶”式的集成优势是其他方案难以比拟的。
    2. 成熟的大规模部署能力:Elasticsearch 从设计之初就是为了处理 PB 级数据和大规模集群而生。其自动分片、副本和故障转移机制非常成熟,能够保证系统在高并发、大数据量下的高可用性和水平扩展能力,这对于需要处理千万甚至亿级数据的企业级应用至关重要。
    3. 丰富的功能集与灵活性:除了核心的全文检索,Elasticsearch 还提供了强大的聚合(Aggregations)功能,可以轻松实现复杂的数据分析和 BI 报表,这为未来的业务扩展提供了可能。其灵活的 DSL (Domain-Specific Language) 查询语言也使得实现各种复杂的业务查询逻辑成为可能。
    4. 广泛的社区和商业支持:庞大的用户基础意味着遇到问题时,可以轻松地在社区论坛、博客找到解决方案。同时,Elastic 公司提供的商业支持也为企业在关键时刻提供了保障。

    关于其他方案的考量:

    • OpenSearch 是一个非常有力的竞争者,特别是对于希望避免 Elastic 许可风险或在 AWS 上深度使用的用户。在技术功能上,它与 Elasticsearch 非常接近。但在当前时间点,Elasticsearch 的生态成熟度和社区知识沉淀仍然略胜一筹。如果许可问题是首要考虑因素,OpenSearch 是最佳替代方案。我个人认为使用 OpenSearch 是很不错的替代方案,毕竟是完全开源的平台,在业务场景不复杂、用不到完整 ES 功能的情况下,OpenSearch 不失为一种很好的替代。当然当下场景下,就选择以 ES 为核心的场景进行研究与搭建。
    • Apache Solr 同样强大,但在易用性、API 设计和生态工具的整合度上,相比 Elasticsearch 稍显逊色。对于一个新项目而言,选择 Elasticsearch 通常能获得更快的开发效率和更现代的运维体验。且 Apache Solr 似乎在学习曲线上也更加陡峭,增添了很多学习成本,在从零开始搭建且考虑项目的时间成本,Apache Solr 选择还是靠后。
    • Meilisearch 性能惊艳,但其设计哲学和功能集更偏向于“小而美”的搜索体验,而非处理复杂分析和海量数据聚合的企业级平台。其当前的单体架构和有限的扩展性使其不适合大规模场景。

    从功能、生态、扩展性和成熟度这几个方面来考虑,需要在这几个指标质检之间取得最佳平衡,选择 Elasticsearch 作为构建这套端到端全文检索架构的基石。


    4. 数据同步:构建实时、可靠的数据管道

    选择了 Elasticsearch 作为搜索引擎平台后,面临下一个核心挑战:如何将关系型数据库中的数据高效、可靠且低延迟地同步到 Elasticsearch 中。

    数据同步是整个解决方案的生命线,其设计的优劣直接决定了最终用户搜索到的信息的时效性和准确性。一个优秀的数据同步方案须满足以下几个关键要求:

    • 可靠性:保证数据不重、不漏,即使在源数据库或目标引擎发生故障时也能恢复。
    • 时效性:从数据在源数据库中创建或更新,到它可以在 Elasticsearch 中被检索到,这个延迟应该尽可能短。
    • 性能:同步过程不能对源数据库造成过大的压力,影响线上业务的正常运行。
    • 可维护性:配置、监控和故障排查应该相对简单直观。

    本章首先详细介绍基于 Logstash JDBC 插件的同步机制,然后会继续将其与 Flink CDC、Beats/Fluentd 以及自定义 ETL 等其他主流方案进行深入对比。

    4.1. 核心方案:使用 Logstash 和 JDBC 插件

    Logstash 是 ELK 技术栈中负责数据处理和传输的强大引擎。它通过插件化的架构,可以从各种数据源(Inputs)读取数据,经过一系列的转换和处理(Filters),最终发送到各种目标(Outputs)。对于从关系型数据库同步数据的场景,Logstash JDBC 输入插件 (logstash-input-jdbc) 是最常用和成熟的官方解决方案。

    核心思路是:Logstash 定期轮询(Poll)源数据库的表,拉取(Pull)发生变化的数据,然后将其推送到 Elasticsearch。

    4.1.1. 全量同步 (Full Synchronization)

    在系统首次上线或进行数据迁移时,需要将数据库中的存量数据一次性全部导入到 Elasticsearch。这通过一个简单的 Logstash 管道配置即可实现。

    工作原理:配置 Logstash 执行一个 SQL 查询,该查询会选取表中的所有记录。Logstash 会将查询结果分批次地从数据库中拉取出来,转换为 JSON 文档,然后批量写入到 Elasticsearch 中。

    关键配置示例 (full-sync.conf)

    input {
      jdbc {
        # JDBC 连接信息
        jdbc_driver_library => "/path/to/mysql-connector-java.jar"
        jdbc_driver_class => "com.mysql.cj.jdbc.Driver"
        jdbc_connection_string => "jdbc:mysql://db.example.com:3306/my_database"
        jdbc_user => "logstash_user"
        jdbc_password => "password"
    
        # 执行全量查询的 SQL 语句
        statement => "SELECT id, title, content, author, created_at, updated_at FROM articles"
    
        # 分页设置,防止一次性加载过多数据到内存
        jdbc_paging_enabled => true
        jdbc_page_size => 100000
    
        # 设置一个计划,让任务只执行一次
        schedule => "* * * * *"
      }
    }
    
    filter {
      # 可选的数据转换,例如重命名字段、转换数据类型等
      mutate {
        rename => { "id" => "[@metadata][document_id]" }
      }
    }
    
    output {
      elasticsearch {
        hosts => ["http://es.example.com:9200"]
        index => "articles"
        document_id => "%{[@metadata][document_id]}"
      }
    }
    

    执行与控制:这个任务通常是一次性的。我们可以通过设置 schedule => "* * * * *" 让它在启动后立刻执行,并在完成后手动停止 Logstash 进程。为了避免对生产数据库造成冲击,全量同步通常选择在业务低峰期进行。

    4.1.2. 增量同步 (Incremental Synchronization)

    全量同步完成后,需要一个持续运行的任务来捕捉数据库中后续发生的新增和修改,并将其同步到 Elasticsearch。这同样可以通过 Logstash JDBC 插件实现,但需要依赖数据库表中的一个时间戳自增 ID作为更新标记。

    工作原理:增量同步依赖于一个不断更新的“检查点”。Logstash 会记录下上次同步到的位置(例如,最大的 updated_at 时间戳或最大的 id)。在下一次轮询时,SQL 查询会使用这个检查点作为 WHERE 条件,只拉取比该检查点更新的记录。

    关键配置示例 (incremental-sync.conf)

    input {
      jdbc {
        # ... (JDBC 连接信息与全量同步相同)
    
        # 增量查询的 SQL 语句
        # :sql_last_value 是 Logstash 内置的参数,用于存储上次的跟踪值
        statement => "SELECT id, title, content, author, created_at, updated_at FROM articles WHERE updated_at > :sql_last_value ORDER BY updated_at ASC"
    
        # 开启跟踪,并指定用于跟踪的列
        use_sql_last_value => true
        tracking_column => "updated_at"
        tracking_column_type => "timestamp"
    
        # 将上次跟踪的值存储在文件中,以便 Logstash 重启后能继续
        last_run_metadata_path => "/path/to/logstash_jdbc_last_run"
    
        # 设置轮询计划,例如每分钟执行一次
        schedule => "* * * * *"
      }
    }
    
    filter {
      # ... (与全量同步类似的转换)
    }
    
    output {
      elasticsearch {
        # ... (与全量同步相同的输出配置)
        action => "index" # "index" 操作会覆盖已存在的同 ID 文档,实现更新
      }
    }
    

    处理数据删除
    Logstash JDBC 插件本身无法直接感知数据库中的 DELETE 操作。处理删除通常有以下几种策略:

    1. 逻辑删除:在数据库表中不进行物理删除,而是增加一个 is_deleted 标志位。增量同步任务会同步这个状态到 Elasticsearch,应用程序在查询时需要过滤掉 is_deleted: true 的文档。
    2. 定期全量比对:定期(如每天凌晨)运行一个脚本,比对数据库和 Elasticsearch 中的文档 ID,找出并删除在 ES 中存在但在数据库中不存在的文档。但这种方式有延迟且开销较大。
    3. 删除日志表:在数据库中创建一个 deletions 表,当应用程序执行删除操作时,除了删除主表记录,还在该表中插入被删除记录的 ID。然后创建一个单独的 Logstash 管道来同步这个删除日志。

    在大多数场景下,逻辑删除是侵入性最小且最容易实现的方案。

    4.2. 其他备选数据同步方案对比

    虽然 Logstash JDBC 是一个可靠且易于上手的方案,但在某些特定场景下,其他工具可能提供更高的性能或更低的数据延迟。对几个主流的备选方案进行对比。

    方案类型工作模式优点缺点适用场景
    Logstash JDBCETL 工具拉取 (Pull)生态成熟、配置简单、功能灵活,与 ES 无缝集成,支持复杂数据转换。准实时,延迟取决于轮询频率;对源数据库有轮询压力;无法直接处理 DELETE对实时性要求不高(秒级到分钟级延迟可接受),希望快速实现、简化运维的通用场景。
    Kettle (PDI)ETL 工具拉取 (Pull)功能极其强大且专业,提供可视化界面(Spoon),对复杂 SQL 和 ETL 流程支持最好;具有完善的作业调度、日志、错误处理和性能监控;社区版免费。资源消耗较大;通常以批量 (Batch) 处理为主,实时性较差;部署和配置相对复杂。传统数据仓库构建、海量历史数据迁移、需要复杂多步骤数据清洗和业务逻辑转换 的批处理场景。
    Flink CDC流处理引擎推送 (Push)实时性极高(毫秒级),基于 Binlog/WAL,对源库性能影响小,能捕获 DELETE,提供端到端的数据一致性保证。架构复杂,需要额外部署和维护 Flink 集群,开发和配置门槛较高。对数据时效性有严苛要求的金融、电商、监控等场景,需要进行复杂流式计算。
    Beats / Fluentd数据采集器推送 (Push)轻量级,资源占用少。Beats 与 ELK 生态集成紧密。Fluentd 插件生态丰富。主要设计用于日志文件、指标等非结构化数据采集,不直接支持 JDBC。需要与其他组件(如 Kafka)配合才能用于数据库同步。主要用于采集应用日志、系统日志、Metrics 等,不适合作为数据库同步的主力方案。
    自定义 ETL自研脚本/应用拉取或推送灵活性最高,可完全根据业务逻辑定制,能实现最复杂的转换和集成逻辑。开发和维护成本极高,需要自行处理可靠性、并发、错误恢复等所有问题,容易产生技术债务。有非常特殊的业务需求,且现有工具无法满足,同时拥有强大的研发团队。

    4.3. 数据同步决策分析:为什么使用 Logstash JDBC?

    Logstash JDBC 方案在功能、成本和复杂度之间取得了一个比较好的平衡,适合在当前场景下作为数据同步的工具。

    1. 满足核心需求:对于我们这种面向 ToG 业务场景的应用需求,秒级甚至是分钟的的同步延迟已经可以满足业务需求。Logstash 的轮询机制虽然不是严格意义上的实时,但足以保证用户在创建或修改数据后的短时间内就能搜索到结果。
    2. 运维成本低:作为 ELK 的一部分,Logstash 的部署和运维与 Elasticsearch 一脉相承,相比引入一个全新的、重量级的 Flink 集群,使用 Logstash 可以显著降低整个系统的架构复杂度和维护成本。
    3. 灵活性与扩展性:Logstash 强大的 Filter 插件(如 grokmutatejsonruby)使得在数据进入 Elasticsearch 之前进行清洗、充实和转换变得非常容易,这对于保持索引数据的高质量至关重要。

    但其实这种选择不是一成不变的,架构应该具备演进的能力。可以从 Logstash JDBC 开始,快速搭建起整个系统。如果未来业务发展对数据同步的实时性提出了更高的要求(例如,需要毫秒级的延迟),届时再考虑升级到其他方案,比如 Flink CDC 这种更复杂的方案,当目标端(Elasticsearch)和核心业务逻辑已经稳定时,切换数据同步方案的风险和成本将是可控的。


    5. 更多的实施细节与架构考量

    理论方案的确立只是第一步,要能成功落地实施需要对架构中的每一个环节进行更深入细致的考量。本章将深入探讨从系统宏观架构到微观索引设计的具体实施细节,确保方案的健壮性、可扩展性和可维护性。

    5.1. 端到端系统架构图

    下图展示了整个解决方案的数据流和组件交互:

    架构分析

    1. 写路径:业务应用的所有写操作(增、删、改)仍然直接作用于关系型数据库,确保其作为“事实孤本 (Single Source of Truth)”的地位。
    2. 同步路径:Logstash 集群通过 JDBC 输入插件,以负载均衡的方式轮询数据库中的业务表。为了高可用可考虑部署多个 Logstash 实例,它们拉取增量数据、进行处理后,批量写入 Elasticsearch 集群。
    3. 读路径:对于全文检索类的查询需求,业务应用不再查询数据库,而是构建 Elasticsearch 的 DSL (Domain-Specific Language) 查询请求,直接发送给 Elasticsearch 集群。ES 返回查询结果后,应用进行处理和展现。
    4. 管理与监控:利用 Kibana 进行管理与监控,Kibana 不仅为开发和运维人员提供了强大的数据探索和可视化工具,还承担了对整个 Elasticsearch 集群的管理和监控职责。

    5.2. Elasticsearch 索引设计

    索引设计是 Elasticsearch 性能优化的核心。一个好的 Mapping 设计能够节省存储空间、提升索引和查询效率。

    核心原则:只索引需要被搜索的字段,并为每个字段选择最恰当的数据类型。重点考虑当前跨多字段的全文搜索的场景。

    示例:articles 索引的 Mapping 设计

    假设有一个数据量极大的 articles 表,搜索需求是输入一个关键词,在 title(标题)、content(内容)、甚至 tags(标签)等多个字段中进行全文检索。

    常规的做法是使用 multi_match 查询,同时搜索这些字段。但在数据量巨大、查询并发高的情况下,这种方式的性能可能不是最优的。

    可以利用 copy_to 功能来创建一个聚合的搜索专用字段。

    {
      "settings": {
        "number_of_shards": 3,
        "number_of_replicas": 1,
        "analysis": {
          "analyzer": {
            "default": {
              "type": "ik_max_word"
            },
            "default_search": {
              "type": "ik_smart"
            }
          }
        }
      },
      "mappings": {
        "properties": {
          "id": {
            "type": "keyword"
          },
          "title": {
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_smart",
            "copy_to": "full_text_search", 
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "content": {
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_smart",
            "copy_to": "full_text_search" 
          },
          "author": {
            "type": "keyword"
          },
          "tags": {
            "type": "keyword",
            "copy_to": "full_text_search" 
          },
          "full_text_search": { 
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_smart"
          },
          "created_at": {
            "type": "date"
          },
          "updated_at": {
            "type": "date"
          },
          "is_deleted": {
            "type": "boolean"
          }
        }
      }
    }
    

    设计解读

    • 分片与副本 (settings)number_of_shards (主分片数) 决定了索引的最大容量和并行处理能力,一旦设定不能修改。这里设为 3,意味着数据将被水平切分到 3 个分片上。number_of_replicas (副本数) 决定了数据的冗余备份数量,可以随时修改,用于高可用和提升读性能。1 个副本意味着每个主分片都有一个备份。
    • 分词器 (analysis):针对中文场景,配置了 ik_max_word 作为默认的索引分词器(最大限度地切分词语,提高召回率),ik_smart 作为搜索分词器(更智能地切分,提高准确率)。
    • 字段类型 (mappings)
      • id 和 author 使用 keyword 类型,因为它们通常用于精确匹配、排序或聚合,不需要分词。
      • title 和 content 使用 text 类型,因为它们是需要进行全文检索的核心字段,会经过分词器处理。
      • title 还额外配置了一个 fields.keyword 多字段,允许我们同时对标题进行不分词的精确匹配。
      • created_at 和 updated_at 使用 date 类型,以便进行日期范围查询。
      • is_deleted 使用 boolean 类型,用于实现逻辑删除。
    • 多字段全文搜索优化 (copy_to)
      • 功能说明copy_to 是 Elasticsearch 提供的一个强大功能,它允许将一个或多个字段的值,在索引时自动复制到一个新的、统一的字段中。这个新字段会包含所有源字段的内容,并按照自己的映射规则进行索引。
      • 应用场景:在上述例子中,我们希望用户输入一个关键词就能同时搜索 titlecontent 和 tags。我们通过在 titlecontent 和 tags 字段中设置 "copy_to": "full_text_search",将这三个字段的值全部复制到了名为 full_text_search 的新字段中。
      • 优势
        1. 简化查询:在查询时不再需要执行 multi_match 来搜索三个独立的字段,只需针对 full_text_search 这一个字段进行简单的 match 查询即可。这使得查询语句更简洁,意图更明确。
        2. 提升性能:对于数据量极大的表,针对单个字段的查询通常比在多个字段上进行联合查询更快。因为 Elasticsearch 只需在一个字段的倒排索引中进行查找,减少了查询协调的开销,尤其是在计算相关性分数 _score 时,针对单个字段的词频/文档频率计算会更高效。
        3. 灵活控制full_text_search 作为一个独立字段,可以拥有自己的分词器、权重等设置,而不会影响源字段。
      • 查询方式对比
        • 未使用 copy_toGET articles/_search { "query": { "multi_match": { "query": "Elasticsearch 性能优化", "fields": ["title", "content", "tags"] } } }
        • 使用 copy_to 后GET articles/_search { "query": { "match": { "full_text_search": "Elasticsearch 性能优化" } } }
        注意事项copy_to 会增加索引的存储开销,因为它实际上是存储了额外的数据。但在“以空间换时间”的性能优化策略中,对于读多写少的超大数据量场景,这种开销通常是值得的。

    5.3. Logstash 部署与高可用

    部署单个 Logstash 实例,系统的可能会有单点故障风险,为了确保数据管道的健壮性,可以考虑高可用部署。

    • 多实例部署:启动多个 Logstash 实例,并让它们运行相同的管道配置。由于 JDBC 输入插件会记录 sql_last_value 到共享文件或数据库中,它们可以协同工作而不会重复拉取数据(需要确保 last_run_metadata_path 指向一个共享存储或每个实例有独立但同步的跟踪机制)。
    • 持久化队列 (Persistent Queue):默认情况下,Logstash 的事件队列是基于内存的。如果 Logstash 进程意外崩溃,内存中的数据将会丢失。为了防止数据丢失,可以启用持久化队列。它会将事件缓存在磁盘上,直到它们被成功发送到 Elasticsearch。在 logstash.yml 中配置:queue.type: persisted path.queue: /path/to/logstash/queue

    5.4. 应用层集成

    应用层需要进行改造,以将搜索流量导向 Elasticsearch。

    • 引入客户端:在应用中添加对应语言的 Elasticsearch 官方客户端库,如 elasticsearch-java 或 elasticsearch-py
    • 双写/切换逻辑:在改造初期,可以采用“双写”模式,即写操作同时更新数据库和 Elasticsearch,用于验证新系统的稳定性。稳定后,切换为“只写数据库,由 Logstash 同步”的模式。
    • 查询构建:将原有的 SQL LIKE 查询改造为 Elasticsearch 的 DSL 查询。例如,一个简单的关键字查询:Python 示例 (elasticsearch-py)from elasticsearch import Elasticsearch es = Elasticsearch(hosts=["http://es.example.com:9200"]) def search_articles(query_string): response = es.search( index="articles", body={ "query": { "bool": { "must": [ { "multi_match": { "query": query_string, "fields": ["title", "content"] } } ], "filter": [ { "term": { "is_deleted": False } } ] } } } ) return response['hits']['hits']
    • 故障降级:在应用层面实现一个简单的降级策略。比如当检测到 Elasticsearch 集群无法连接时,可以临时切换回使用数据库的 LIKE 查询(即使性能较差),以保证核心功能的可用性,同时触发告警。

    6. 性能评估与监控

    6.1. 关键性能指标 (KPIs)

    可以关注以下几类核心指标:

    • 数据同步延迟 (Data Freshness):从一条记录在数据库中被修改,到它可以在 Elasticsearch 中被搜索到的端到端时间。这是衡量系统“近实时”能力的关键。根据业务需求设定目标同步延迟时间。
    • 索引吞吐率 (Indexing Throughput):Elasticsearch 每秒能够索引的文档数量(docs/sec)。这反映了数据写入的能力。目标:根据业务增量预估,通常需达到峰值增量的 2-3 倍。
    • 查询延迟 (Query Latency):搜索请求的响应时间。通常关注平均值、P95(95% 的请求耗时)和 P99(99% 的请求耗时)。目标:P95 < 200ms。
    • 查询吞吐量 (Query Throughput):系统每秒能够处理的查询请求数(QPS)。

    6.2. 性能压测与调优

    • 压测工具
      • Elasticsearch Rally: Elastic 官方的宏观基准测试工具,用于模拟真实负载对集群进行压测。
      • JMeter/Gatling: 通用的压力测试工具,可用于模拟应用层的搜索请求。
    • 常见瓶颈及调优策略
      • Logstash: 如果同步延迟过高,可能是 Logstash 处理能力不足。可以增加 pipeline.workers 的数量(通常等于 CPU 核数),调整 pipeline.batch.size 来优化写入效率。
      • Elasticsearch – 写入优化: 调整索引的 refresh_interval(例如从默认的 1s 延长到 30s),可以显著降低写入时的资源消耗,提升吞吐率。在全量同步期间,甚至可以暂时将其设置为 -1 关闭自动刷新,并在结束后手动刷新。
      • Elasticsearch – 查询优化: 优化 DSL 查询,避免使用开销大的查询类型(如脚本查询、通配符查询)。确保 JVM 堆内存设置合理(通常是物理内存的一半,且不超过 30GB)。
      • 硬件资源: 监控节点的 CPU、内存、磁盘 I/O。使用 SSD 是保证 Elasticsearch 高性能的基础。

    6.3. 监控与告警

    利用 ELK 技术栈自身,可以构建强大的监控系统。

    • 监控方案:使用 Metricbeat 来收集 Elasticsearch、Logstash、Kibana 以及操作系统的性能指标,将这些数据发送到专门的监控 Elasticsearch 集群中,然后通过 Kibana 创建仪表盘进行可视化。
    • 关键监控项
      • 集群健康状态 (Cluster Health)greenyellowred。红色状态是最高级别的告警,意味着有主分片丢失,数据不完整。
      • JVM 堆内存使用率 (JVM Heap Usage): 持续高于 85% 是一个危险信号,可能导致频繁的垃圾回收和性能下降。
      • CPU 使用率 (CPU Usage): 持续高位可能意味着查询压力大或存在低效查询。
      • 磁盘空间 (Disk Space): 必须监控磁盘使用率,防止因磁盘写满导致索引无法写入。
    • 告警集成:利用 Elasticsearch 的 Watcher 功能(商业特性)或开源的 ElastAlert 工具,可以针对上述关键指标设置阈值,当满足告警条件时,通过邮件、Slack 或其他方式发送通知给运维团队。

    7. 结论与展望

    7.1. 方案总结

    本文系统性、概括性的研究了一个特定场景下的全文检索方案:如何为存储在关系型数据库中的海量数据提供高性能、功能丰富的全文检索能力。以 Elasticsearch 为搜索引擎核心、以 Logstash 为数据同步管道的端到端解决方案来进行说明。

    该方案的核心优势在于:

    • 专业性:将检索任务从不擅长此道的数据库中剥离,交由专业的搜索引擎处理。
    • 生态整合:依托于成熟的 ELK 技术栈,在数据同步、查询、监控和管理等各个环节都能无缝集成,并得到强大的功能支持。
    • 可扩展性:架构基于分布式组件构建,无论是数据量增长还是查询并发量提升,都可以通过水平扩展节点来应对。

    7.2. 潜在挑战

    • 数据一致性:基于轮询的异步同步机制存在最终一致性的窗口期。应对策略是通过缩短轮询间隔来降低延迟,并通过完善的监控确保同步任务的健康运行。
    • 运维复杂度:引入 ELK 技术栈确实增加了系统的运维复杂度。应对策略是加强自动化运维建设(如使用 Ansible/SaltStack 进行部署配置),并建立起标准化的监控和应急响应流程。

    7.3. 未来展望

    若要继续升级,该架构可以向以下方向发展:

    • 实时同步:若业务对数据同步的延迟有毫秒级要求时,可以考虑将数据同步策略升级,比如将数据管道从 Logstash JDBC(Pull 模式)升级为 Flink CDC + Kafka(Push 模式),但会涉及到更复杂的维护和技术与学习成本问题。需要多方面综合考虑。
    • 智能化探索:在拥有了海量高质量的索引数据后,可以利用 Elasticsearch 的机器学习功能进行异常检测、日志模式分析等 AIOps 探索。也可以集成向量搜索(Vector Search)能力,实现更智能的语义搜索或以图搜图等应用。
    • 多数据中心容灾:对于可用性要求极高的关键业务,可以部署跨数据中心的 Elasticsearch 集群,并启用跨集群复制 (Cross-Cluster Replication, CCR) 功能,实现机房级别的容灾能力。
  • SpringBoot 常用跨域处理方案

    1.什么是跨域?

    跨域是浏览器为了保障安全而遵循的一种规则,是同源策略的一部分。

    • 同源:要求协议、域名、端口三者完全相同。
    • 跨域:只要协议、域名、端口中有任何一个不同,浏览器就会判定为跨域请求。

    跨域(Cross-Origin)是浏览器独有的安全策略,不存在于安卓、iOS、Node.js、Python、Java 等原生客户端或服务器端环境中。因为浏览器是一个开放的、执行不可信代码,也就是各个网站的 js 脚本的环境,同源策略是为了保证用户的信息安全。

    所以,如果平时测试接口用的是 postman 发送请求,不需要关心跨域问题,但是如果是前后端联调就必须处理跨域问题。

    2.浏览器处理跨域请求的方式

    浏览器遵循同源策略,不允许页面获取跨域请求返回的响应结果。

    比如:当前网页是 http://127.0.0.1:63342,然后向服务器 http://127.0.0.1:8080 发送 GET 请求获取数据。整个过程分成三步:

    • 浏览器发送请求
    • 服务器接收请求,处理业务,返回响应
    • 浏览器获取服务器返回的响应并根据响应渲染页面

    无论是跨域请求还是非跨域请求,浏览器都可以发送给服务器,并且接收服务器返回的响应数据。

    • 如果该请求是非跨域请求,则 js 脚本可以访问响应数据
    • 如果该请求是跨域请求,浏览器会拦截响应数据,不让 js 访问这些数据

    其实浏览器可以向不同的域名发送请求,但是浏览器会拦截响应内容,不让 js 访问,无论请求是否成功。

    3.跨域处理方案

    跨域处理的核心:让浏览器不要拦截跨域请求返回的数据。

    服务器的响应中如果有 Access-Control-Allow-Origin: * 这个响应头,就是告诉浏览器:”我是服务器,虽然我跟这个网页不是同源的,但是我允许这个网页跟我通信,我们之间的通信是安全的”,浏览器就不会拦截 js 对响应数据的访问。

    浏览器发送的请求可以分为简单请求和复杂请求:

    • 如果是简单请求,则浏览器直接发送。
    • 如果是复杂请求,则浏览器先发送一个预检请求,即 OPTIONS 请求,问一句:”我可以发送一个超级复杂的跨域请求吗?”,服务器需要返回针对 OPTIONS 的响应。如果服务器允许发送这个复杂请求,浏览器才会真正发送请求。

    常见的跨域处理方案有:代理服务器、后端服务器跨域配置。

    3.1代理服务器

    浏览器将请求发送到跟页面同源的代理服务器,代理服务器再将请求转发到目标服务器。因为服务器间通信不受同源策略限制。比如常见的用 nginx 作为代理服务器。

    请求处理过程:

    • 前端发送请求,请求经过 nginx,请求被转发到后端服务器。
    • 服务器返回原始响应,原始响应经过 nginx,nginx 自动添加 Access-Control-Allow-Origin: * 响应头,响应返回给前端,js 可以访问响应数据。
    # nginx.conf
    server {
        listen 63342;
        # 前端页面的域名或ip
        server_name 127.0.0.1;
    
        # 代理所有以 /api/ 开头的请求到后端服务器
        location /api/ {
            # 后端服务器地址
            proxy_pass http://127.0.0.1:8080/; 
    
            # 修改请求头,确保后端能收到正确的原始主机信息
            proxy_set_header Host $host; 
            proxy_set_header X-Real-IP $remote_addr; 
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
            proxy_set_header X-Forwarded-Proto $scheme;
    
            # 添加CORS响应头,允许所有来源的请求,生产环境应关闭
            add_header 'Access-Control-Allow-Origin' '*'; 
            
            # 允许的请求方法
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE, PATCH';
            
            if ($request_method = 'OPTIONS') {
                # 对于OPTIONS请求,直接返回204状态码,不需要转发到后端
                return 204;
            }
        }
    }
    

    3.2 后端跨域配置

    3.2.1 配置 CORS

    配置全局 CORS 规则,在所有响应头都配置可以跨域访问。

    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**") // 允许所有接口跨域
                    .allowCredentials(true) // 允许浏览器在跨域请求中发送认证信息
                    .allowedOriginPatterns("*") // 允许访问资源的源(协议、域名、端口)
                    .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的方法
                    .allowedHeaders("*") // 允许的请求头
                    .exposedHeaders("*"); // 哪些响应头可以暴露给前端js
        }
    }
    

    3.2.2 提供 CorsFilter

    提供一个 CorsFilter 的 Bean 作为过滤器。

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
    import org.springframework.web.filter.CorsFilter;
    
    @Configuration
    public class MyCorsFilter {
        @Bean
        public CorsFilter corsFilter() {
            CorsConfiguration config = new CorsConfiguration();
    
            config.addAllowedOriginPattern("*"); // 放行所有域名,生产环境请对此进行修改
    
            config.setAllowCredentials(true); // 是否发送cookie
    
            config.addAllowedMethod("*");  // 放行的请求方式
    
            config.addAllowedHeader("*"); // 放行的请求头
    
            config.addExposedHeader("*"); // 暴露头部信息
    
            // UrlBasedCorsConfigurationSource: 可以为不同的URL路径设置不同的CORS规则
            UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
            // 所有的URL路径都使用同一个CORS规则
            corsConfigurationSource.registerCorsConfiguration("/**", config);
    
            return new CorsFilter(corsConfigurationSource);
        }
    }
    

    3.2.3 @CrossOrigin 注解

    在接口类上或者接口方法上添加 @CrossOrigin 注解,表示整个类、单个接口的响应不会被拦截。

    @RestController
    @CrossOrigin
    public class DemoController {
    
        @PutMapping("/put")
        public Integer put(MultipartFile file) {
            System.out.println(file.getOriginalFilename());
            return 200;
        }
    
        @GetMapping("/get")
        public Integer get() {
            return 200;
        }
    }