首页 文章 精选 留言 我的

精选列表

搜索[工程师],共10000篇文章
优秀的个人博客,低调大师

Artifacts Workflow新时代:提示词/Skill工程师凉了

在早期的 AI Agent 时代(比如 2023 年的 AutoGPT 或 CrewAI),大家天天把“给 Agent 写 Skill(技能)”挂在嘴边。 但是现在在 Artifacts Workflow 中,你作为大导演,**不要去碰底层工具的封装**。所有的“能力扩展”都可以通过自然语言,让 AI 去调用现成插件,或者让 AI 写代码武装它自己。 Skill 解决的是:“这个团队长期积累的方法论如何复用?” ,**Skill 可以把经验固定下来**(但是也是负担) 更好的方式: 让 AI 先探索,让让AI把规则写成Skill 一些宏观的工程约束 也就是说,早期大家疯狂写 Skill,现在很多人开始转向 **Artifacts + Rules + Agent Workflow。** - 以前:人 → 写 Prompt → 写 Skill → 写 Tool → 调用 AI - 现在:人(导演) → 定义目标和验收标准 → AI 自己规划 → AI 调用工具 → AI 生成中间产物 → 完成任务 **Artifacts Workflow 是 AI 原生开发的主线;Skill 不是消失,而是从“提前设计能力”变成“从实践中提炼出来的组织资产”。** ![](https://developer.qcloudimg.com/http-save/2280589/f11d3da4af5cd2e211bca785853c4efb.webp) ## 认知刷新:什么是旧时代的 Skill?什么是现在的替代品? - **旧时代的 Skill**:你需要自己写一段冗长的 Python 或 Node.js 代码,教 AI “如何去查询数据库”、“如何去搜索网页”、“如何去读取某个 API”。这需要你懂极深的底层逻辑。 - **现在的替代品(2026 硅谷共识)**: 1. **IDE 内置能力(Cursor 的黑魔法)**:在 Cursor 里,AI 天生自带“读写全工程代码”、“运行终端命令”、“查看报错信息”的超能力。你根本不需要给它写什么 Skill,它自己就有手有脚。 1. **MCP(模型上下文协议)插件**:如果你需要 AI 去查线上的 Postgres 数据库,或者去读 GitHub 上的外部库,你不需要写代码,只需要在配置文件里**填一行插件名字**,就像给手机下 App 一样简单。 ## 遇到需要“能力(Skill)”的场景,如何让 AI 自己搞定? 既然你不懂,那就利用你的“无限 Token”特权,**把配置 Skill 的工作也丢给 AI 自己做**。 你可以完全用大白话让 AI 帮你接管: ### 场景一:在 Cursor 中,让 AI 自己去运行和纠错(利用内置能力) 你不需要写任何自动化测试脚本或编译 Skill,Cursor 的 Composer 面板已经打通了终端。 - **你的无代码指令:** “根据我们制定的 `.spec/architecture/openapi.yaml` 契约,完成 NestJS 后端的接口代码。写完之后,**请你自己使用 Cursor 的终端能力(Terminal),运行** **`npm run start:dev`。如果你在终端里看到了报错,请你自己读取错误日志并修复代码,直到终端不再报错为止。**” - **结果**:AI 会直接接管终端面板,疯狂敲命令、看报错、改代码。这就是最顶级的内置 Skill 展现。 ### 场景二:需要外部能力时,让 AI 帮你写 MCP 配置文件 假设你在做 BI 项目,需要 AI 去真实连接一个测试用的数据库来查看表结构,但 AI 默认没有“查数据库”的 Skill。 - **你的无代码指令:** “我现在需要你具备读取本地 PostgreSQL 数据库的能力。我完全不懂怎么配置 Skill 或 MCP。请你一步步告诉我,我需要在项目的哪个文件里粘贴什么内容,才能让你连上 `postgres://localhost:5432/bi_test`?请直接给我配置代码。” - **结果**:AI 会立刻告诉你(比如在 Claude Desktop 中): 机器学习与人工智能 “没问题。请在您的 `claude_desktop_config.json` 中粘贴以下内容。这段配置会自动下载官方的 Postgres MCP 插件,您无需写任何代码……” ### 场景三:极度硬核的定制化需求(让 AI 自己给自己写工具) 如果你确实遇到了一个全网都没有现成插件的诡异需求(比如:你需要 AI 每隔 10 分钟去公司内网的一个老旧接口拉取销售数据来生成图表测试数据)。 - **你的无代码指令:** “我需要你具备一个定制化的能力:去请求 `http://internal.company.com/api/sales` 并把结果转成 JSON。我不懂怎么写 Skill。**请你用 Node.js 为自己编写一个标准的 MCP Server 脚本,命名为** **`sales-mcp.js`,并告诉我如何启动它。**” - **结果**:AI 此时不仅是一个写业务代码的程序员,它变成了**它自己的工具开发商**。它会输出完整的脚本,你只需要无脑 `node sales-mcp.js` 跑起来即可。 ![](https://developer.qcloudimg.com/http-save/2280589/9a55419126c694972a87b6535543bd3b.webp) [https://mp.weixin.qq.com/s/iqR0A44ZZhNMIyrR9eToqwmp.weixin.qq.com/s/iqR0A44ZZhNMIyrR9eToqw](https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/iqR0A44ZZhNMIyrR9eToqw)

优秀的个人博客,低调大师

没有处理过遗留项目,别自称资深工程师

大家都不喜欢维护遗留项目,我也不例外。命运总爱跟人开玩笑,最近一个遗留项目正好落到了我手上。虽然在这个项目上工作的经历并没有减少我对遗留系统的厌恶,反而让我对当下所采用的流程与实践有了更深刻的认识。 我为自己所在的团队感到自豪,因为我们遵循了许多业界最佳实践: 编写简洁且易维护的代码,并配以自动化测试 积极参与代码合并请求和任务评审 合并到主分支后,当天就能将应用推向生产环境 高度采用敏捷开发模式 当然,一切并非尽善尽美。合并请求中偶尔会出现一些无关痛痒的建议和讨论;运维团队有时也会搞砸一些事情(至少在我们开发人员看来是这样);而产品负责人也时不时催促我们加快推出某些“简单”的新功能……总的来说,情况还算不错。 穿越回Ant时代 由于团队表现出色,公司决定将我们的开发效率借调给另一个由其他部门负责的产品。令我们略感失望的是,这个项目不仅使用的是较老版本的Java,其代码风格也与我们的习惯大相径庭。 任务要求我们添加几个简单的监控指标,比如应用是否正常运行、运行时长、数据处理是否足够迅速等。由于项目正处于维护模式,已经有段时间没有添加新功能了。按理说,添加这些指标对我们来说应该是小菜一碟。 然而,当我们卷起袖子开始工作时,首先发现这个项目竟然使用了一种非常古老的构建方式——Ant构建文件。那是一种庞大的XML文件,详细描述了如何构建整个项目:从编译、测试到打包,每个环节都必须显式配置,包括源码路径、目标路径以及资源位置。过去,这种做法在许多编程语言中都很常见:写好一个构建文件,复制到每个新项目中,再不断调整直至适应新项目需求。 例如,一个简单的“Hello World”项目的Ant构建文件可能如下所示: <project> <target name="clean"> <delete dir="build"/> </target> <target name="compile"> <mkdir dir="build/classes"/> <javac srcdir="src" destdir="build/classes"/> </target> <target name="jar"> <mkdir dir="build/jar"/> <jar destfile="build/jar/HelloWorld.jar" basedir="build/classes"> <manifest> <attribute name="Main-Class" value="oata.HelloWorld"/> </manifest> </jar> </target> <target name="run"> <java jar="build/jar/HelloWorld.jar" fork="true"/> </target> </project> 难道就没有更便捷的方式吗?正因如此,“约定优于配置”的理念应运而生。这一理念主张:开发者只需关心那些偏离约定的特殊情况,而现代构建工具正是基于这一思想,通过提供可覆盖的默认配置,免去了重复配置的麻烦。正因为如此,大多数Java源码统一存放在 src/main/java 目录下,而编译后的文件则放在 target 目录中,避免了繁琐的重复设置。 这一发现启发了我们:或许同样的原则也能应用到我们当前项目中的应用配置上。面对一个庞大且大部分数值雷同(如应用端口)的配置文件,采用默认值机制无疑能让配置文件变得更精简。 被我们视为理所当然的事情 回到遗留项目,我们顺利构建并打包了应用!那枯燥的部分终于过去,可以安心开始编码了。但问题随之而来:如何将我们负责监控的指标组件嵌入到这套老旧的代码库中?在我们的常规开发框架中,这一切通常是自动处理的,因此我们一度认为这毫不费事。 但实际上,将指标组件“注入”到遗留代码的各个角落,最佳方案是什么呢?初看起来,单例模式似乎是最简单的选择;不过,开发社区普遍认为单例是一种反模式。为什么呢?毕竟,我们钟爱的某某框架不也依赖单例吗?如果不是,那它到底采用了什么机制?依赖注入究竟是什么?其底层又是如何运作的? 这一连串问题促使我们重新审视那些一直视为理所当然的基本概念。虽然在这种情况下使用单例并非最糟,因为代码大部分缺乏单元测试,但要让我们心安理得,代码必须经得起推敲。经过尝试,我们最终采用了一种不同的方案,写出了既简洁又清晰的代码——既没有依赖单例,也未引入多余的抽象层。 开发者角色的局限性 项目的最后一步是部署,只有部署成功后才能进行测试。但问题来了——这一次,我们既不负责部署,也不负责测试。部署工作由运维团队完成,而测试则交由专门的测试团队。为什么开发者就不能全程掌控,从开发到上线,而要先提交工单,再等待其他团队的配合呢? 首先,由于代码测试覆盖率不足,手动测试不可避免;其次,公司的基础设施也不允许我们自行部署应用。 这一系列经历使我们开始思考职责分离的原因,以及现行模式为何更为合理。事实证明,这个项目的任务交付时间和迭代周期远远超过平时(通常几天就能交付的工作,此次竟拖延了数周),这无疑验证了分工合作的必要性。 通过旧实践理解现代方法 到了月底,我们的监控指标终于在生产环境中顺利运行。虽然我对遗留项目的看法依旧——我仍然讨厌它们,也不奢望你会因此改变看法——但这段经历给了我们宝贵的启示。 我们无法选择所分配到的项目,但我们可以调整对待遗留系统的态度。与其心存无奈,不如把它当作一个提问、学习与成长的机会。通过了解过去的做法及其局限,我们不仅掌握了当下的最佳实践,更获得了背后历史的宝贵经验。 一旦你积累了这种深厚的知识,其他开发者自然会认可并信赖你的专业能力。如果你希望成为这样的人,就得勇于钻研那些看似繁琐的遗留项目。 原文:https://www.infobip.com/developers/blog/seniors-working-on-a-legacy-project 作者:Alen Kosanovic

优秀的个人博客,低调大师

LLM 工程师入门:生成式AI的简易指南

编者按: 大模型发展了近两年,Baihai IDP 也分享了近百篇LLM各环节的技术洞察,有前沿探讨、有落地实践、有应用经验。但回头来看,我们似乎从来没有认真、从0开始探讨过LLM的基本原理。 最近,一些企业客户和伙伴来询问,是否有LLM的从0到1的科普贴。他们说: "虽然在很多场景中,LLM都已经渗透入我们的工作生活,但对其内部的运作机制,仍有很多谜团待解决。 在应用落地时,LLMs 这种"黑箱式"的运作模式,不仅使我们难以完全信任这些模型的输出结果,也阻碍了我们对其进一步研究和优化的步伐。如果我们无法理解 LLMs 的工作原理,就很难评估它们的局限性,进而制定出有针对性的解决方案。" 因此,我们把这篇LLM基础原理文章推荐给大家。 本文为希望深入了解生成式AI的开发者、技术爱好者、AI落地的领导者和研究者们编写,以通俗易懂的语言,系统地剖析了大语言模型的内部结构和训练流程,从 token、next token predictions,到马尔可夫链、神经网络等核心概念,循序渐进地揭示了 LLM 是如何生成文本的。 作者 | Miguel Grinberg 编译 | 岳扬 毫无疑问,随着大语言模型[1](LLMs)的新闻不断出现在我们的日常生活,生成式人工智能[2](GenAI)已经成为了我们无法忽视的存在。或许你早已体验过 ChatGPT[3] ,甚至把它当作日常生活的小助理了。 面对这场 GenAI 变革,许多人心中都有一个疑问:这些模型表面上的智能(intelligence)究竟源自何处?本文将试图用浅显易懂的语言,不涉及复杂数学公式,来揭秘生成式文本模型的工作原理,让你认识到它们并非魔法,而是计算机算法的产物。 01 What Does An LLM Do? 首先,我要澄清人们对大语言模型工作原理的一个重大误解。人们通常认为,这些模型能够回答我们的问题或与我们进行对话,但实际上,它们所能做的是基于我们提供的文本输入,预测下一个单词(更准确地说,是下一个 token)。 现在,让我们一步步揭开 LLMs 神秘面纱背后的"真面目",从 token 开始探索。 1.1 Tokens token 是大语言模型(LLM)处理文本时的基本单元。虽然我们可以简单地将 token 视为单个单词,但 LLM 的目的是以最高效的方式对文本进行编码。因此在很多情况下,token 可能是比单个单词短或长的字符序列。 标点符号和空格同样以 token 的形式存在,它们可以单独表示为一个 token,也可以与其他字符组合。 LLM 所使用的所有 token 构成了它的词汇表(vocabulary),这个词汇表能够用来表达所有可能的文本内容。大语言模型通常采用 BPE(Byte Pair Encoding)[4]算法来根据输入数据集创建 token 词汇表。以 GPT-2 语言模型[5](开源模型,可供深入研究)为例,其词汇表拥有 50,257 个 token。 每个 token 在 LLM 的词汇表中都有一个独一无二的标识符(通常是一个数字编号)。LLM 通过分词器将常规文本字符串转换为一系列 token 编号。 如果您对 Python 有所了解,并且想要尝试对 token 进行操作,可以安装 OpenAI 提供的 tiktoken 软件包: 然后请在 Python 命令行中尝试以下操作: 在本实验中,我们可以观察到,GPT-2 语言模型的词汇表中,token 464 对应的是单词"The",而 token 2068 则对应" quick",这个 token 包括了单词之前的空格。在该模型中,句号由 token 13 表示。 由于 token 是通过算法来决定的,因此我们可能会遇到一些奇特的情况,比如 GPT-2 会将单词"the"的以下三种形式编码为不同的 token: BPE 算法并不会总是将整个单词直接转化为一个 token。事实上,那些使用频率较低的单词不会被单独表示为一个 token,而是需要通过多个 token 的组合来进行编码。下文这个例子,展示了 GPT-2 模型是如何用一个由两个 token 组成的序列来编码某个单词的: 1.2 Next Token Predictions 如上文所述,语言模型会根据给定文本预测之后可能出现的 token。如果用 Python 伪代码来展示这个过程,下文就演示了如何使用这些模型来预测下一个 token: 这个函数将用户输入的提示词转换成的 token 列表作为模型输入。这里假设每个单词都是一个单独的 token。为了简单起见,此处使用了每个 token 的文字形式,但实际上,每个 token 都是以数字的形式传递给模型的。 该函数返回的是一种特有的数据结构,它为词汇表中的每个 token 分配了一个在输入文本后紧接着出现的概率。如果使用的是 GPT-2 模型,那么返回的将是一个包含 50,257 个浮点数的列表(list),列表中每个数字代表相应 token 紧接着文本内容出现的概率。 在上述案例中,可以设想训练效果良好的语言模型会为"jumps"这个 token 分配一个较高的概率,以接续短语"The quick brown fox"[6]。同样地,如果模型训练得当,那么像"potato"这样的随机单词接在这个短语后面的概率就会低很多,几乎接近于0。 为了做出合理的预测,语言模型需要经过一个训练过程。在训练期间,模型会学习大量文本内容。训练结束后,模型就能利用它从训练文本中构建的特有数据结构,来计算给定 token 序列的下一个 token 的概率。 这与你的预期是否有所不同?我希望现在这个概念看起来不再那么神秘了。 1.3 生成长文本序列 由于模型只能预测下一个出现的 token,因此要想让它生成完整的句子,就必须在 for 循环中多次运行模型。每一次循环迭代,都会根据返回的概率列表选择一个新的token。这个新 token 会被加入到下一次循环迭代中模型的输入序列中,如此循环往复,一直持续到生成足够的文本为止。 下面是一个更完整的 Python 伪代码示例,演示了这个过程: generate_text() 函数需要用户提供提示词内容,比如可以是一个问题。 tokenize() 这个辅助函数负责将用户的提示词转换成一系列 token,这个过程会用到 tiktoken 或类似的库。在 for 循环中,get_token_predictions() 函数负责调用AI模型,获取下一个 token 的概率列表,与上文案例中描述的过程相同。 select_next_token() 函数的作用是根据模型给出的下一个 token 的概率列表,从候选 token 中挑选出最合适的 token 来放入输入序列。这个函数可以采取最简单的方法,即选择概率最高的 token ,这在机器学习中被称为"greedy selection"。然而,为了增加生成文本的多样性,该函数通常会采用更高级的策略,使用一个随机数生成器来选择 token,即使是在随机选择 token 的情况下,也会优先选择那些概率较高的 token。通过这种方式,即便是给出相同的输入提示词,模型也能生成不同的文本响应。 为了进一步增加 token 选择过程的灵活性,可以通过调整超参数来改变 LLMs 返回的概率分布。 这些超参数为传递给文本生成函数的参数,能够帮助用户控制 token 选择过程的"greediness"(译者注:模型在选择下一个 token 时所表现出的倾向性,是倾向于选择概率最高的token(即最可能的token),还是允许一些不太可能的token(即概率较低的token)被选中。)。如果你以前经常使用 LLMs,那么就很可能对 "temperature" 超参数比较熟悉。当 temperature 值较高时,token 的概率分布会被 flattened out(译者注:模型会考虑更多的token,包括那些概率较低的token,使得概率分布更加均匀。),这样做的结果是,之前不太可能被选中的 token 现在有更大的机会被选中,从而使生成的文本看起来更具创造性和新颖性。除了 temperature 之外,还有两个超参数 top_p 和 top_k,它们分别用来控制在选择过程中高概率 token 被选中的数量。 通过调整这些超参数,可以进一步影响文本生成的风格和多样性。 一旦模型选定了下一个token,循环就会继续迭代。此时,模型将接收到一个新的输入序列,这个输入序列的末尾添加了新 token(译者注:上一次迭代选择的 token)。num_tokens 参数决定了循环的迭代次数,也就是生成文本的长度。生成的文本可能会(并且经常)在句子中间结束,因为大语言模型(LLM)并没有句子或段落的概念,它只是逐个处理 token。为了防止生成的文本内容在句子中间就结束了,我们可以将 num_tokens 参数视为一个最大数量值,而不是一个确切的 token 数量。在这种情况下,我们可以在模型生成一个句号 token 时结束循环。 如果你已经阅读到此处,并且理解了前文的所有内容,那么恭喜你,你现在对 LLMs 的工作原理已经有了较为深入的理解。各位读者是否对更多技术细节感兴趣?在下一节中,我将介绍更多技术细节,同时尽量避免提及关于这项技术的复杂数学知识。 02 Model Training 不幸的是,要想不涉及数学知识就讨论模型的训练过程,实在是件不容易的事。接下来,我将首先向大家展示一种非常简单的训练方法。 我们的目标是预测 token 后面可能出现的其他 tokens,因此,训练模型的简单方法就是从训练数据集中提取所有连续的 tokens 对,然后用这些数据来构建一个概率表(table of probabilities)。 让我们用一个简短的词汇表(vocabulary)和数据集(dataset)来做这件事。假设模型的词汇表有以下五个 token : 为了使这个例子简明扼要,我们不把空格和标点符号算作 token。 我们使用以下三个句子组成的训练数据集: I like apples I like bananas you like bananas 我们可以制作一个 5x5 的表格,每个单元格记录的是该行 token 后面跟着该列 token 的频次。表格如下: 该数据集中,"I like" 出现了两次,"you like" 一次,"like apples" 一次,而 "like bananas" 则是两次。 现在我们知道了训练数据集中每对 tokens 的出现频率,就可以推算出它们相互跟随的概率。做法是将表格中每一行的数字转换成概率。比如,表格里"like"这一行,我们看到它后面跟着 "apples" 一次,跟着 "bananas" 两次。这意味着在"like"之后出现 "apples" 的概率是33.3%,而出现 "bananas" 的概率则是66.7%。 下面是计算出所有概率的完整表格。那些空白单元格代表的概率自然就是0%。 对于 "I"、"you" 和 "like" 这些行的概率计算比较轻松、直接,但 "apples" 和 "bananas" 这两行就有点难办了,因为数据集中并没有 "apples" 和 "bananas" 后面跟着其他 token 的情况。这就好比在我们的训练数据中出现了"缺口"。为了确保模型在遇到这种未训练的情况时也能有所输出,我决定将 "apples" 和 "bananas" 后续 token 的概率均匀分配给其他四个 tokens。这种做法可能会导致一些不太自然的输出结果,但至少模型在处理这两个 token 时不会卡壳。 训练数据中的这种"缺口"问题不容小觑。在真正的 LLMs 中,由于训练数据集规模非常庞大,我们不太可能遇到像本文这个简单例子中这么明显的缺口。但是,由于训练数据的覆盖面不足,那些较小的、较难发现的缺口确实是存在的,并且相当常见。 LLMs 在这些训练不足的区域所做的 tokens 预测质量可能会不高,而且这些问题往往不易被察觉。这也是 LLMs 有时会出现"幻觉[7]"(即生成的文本虽然朗朗上口,但可能包含与事实不符的内容或前后矛盾之处。)的原因之一。 借助上面的概率表,你现在可以构思一下 get_token_predictions() 函数的实现方法。以下是用 Python 伪代码表示的一种实现方式: 是不是比想象中还要简单些呢?这个函数可以接收来自用户提示词的 tokens 序列。它提取出这个序列中的最后一个 token ,并找到概率表中对应的那一行。 假如你用 ['you', 'like'] 作为 input tokens 调用这个函数,它会给出 "like" 对应的那一行,这使得 "apples" 有 33.3% 的可能性放入句子的下一部分,而 "bananas" 则有 66.7% 的可能性。根据这些概率值可以得知,上面提到的 select_next_token() 函数在三次中有一次会挑选 "apples"。 当 "apples" 被选为 "you like" 的后续词时,我们得到了句子 "you like apples"。这个句子在训练数据集中并不存在,但它却是完全合理的。希望这个小例子能让您开始意识到,这些模型是如何通过重新利用训练中学到的 patterns(译者注:在数据集中识别和学习的重复出现的 tokens 序列组合。) 和拼接不同的信息片段,来形成看似原创的想法或概念的。 2.1 上下文窗口(The Context Window) 在上一部分,我用来训练那个微型语言模型的方法,称为马尔可夫链[8](译者注:Markov chain,一种数学系统,用于描述一系列可能的事件,其中每个事件的发生概率只依赖于前一个事件。)。 这种技术存在的问题是,它只根据一个 token(输入序列中的最后一个 token)来预测接下来的内容。在此之前的文本在决定如何延续文本内容时并不起作用,因此我们可以认为这种方法的上下文窗口仅限于一个 token,这是非常小的。由于上下文窗口太小,模型会不断"忘记"自己的思路,从一个词跳到下一个词,显得杂乱无章。 为了提高模型的预测能力,我们可以构建一个更大的概率表。如果想使用两个 token 的上下文窗口,就需要在概率表中添加代表所有两个 token 组合的新行。以前文的例子来说,五个 token 将会在概率表增加 25 行两个 token 组合的新行,再加上原有的 5 个单 token 行。这次除了考虑两个 token 的组合外,还需要考虑三个 token 的组合,因此必须再次训练模型。在 get_token_predictions() 函数的每次循环迭代中,如果条件允许,将使用输入序列的最后两个 tokens 来在一个更大的概率表中找到与这两个 tokens 相对应的行。 但是,两个 tokens 的上下文窗口仍然不够。为了让生成的文本不仅在结构上能够自洽,还能在一定程度上具备一些意义,需要一个更大的上下文窗口。如果没有足够大的上下文窗口,新生成的 token 就无法与之前的 tokens 所表达的概念(concepts)或想法(ideas)建立联系。那么我们应该如何是好?将上下文窗口扩展到 3 个 tokens 会在概率表中增加 125 行新内容,但质量可能仍然不尽人意。我们又需要多大的上下文窗口呢? OpenAI 的开源模型 GPT-2 使用的上下文窗口其大小为 1024 个 tokens。如果我们要用马尔可夫链(Markov chains)实现这样一个大小的上下文窗口,概率表的每一行都需要代表一个长度在 1 到 1024 个 tokens 之间的序列。以前文例子中使用的 5 个 tokens 的词汇表为例,1024 个 token 长度的序列共有 5 种可能的 token 组合。我在 Python 中进行了计算: >>> pow(5, 1024) 55626846462680034577255817933310101605480399511558295763833185422180110870347954896357078975312775514101683493275895275128810854038836502721400309634442970528269449838300058261990253686064590901798039126173562593355209381270166265416453973718012279499214790991212515897719252957621869994522193843748736289511290126272884996414561770466127838448395124802899527144151299810833802858809753719892490239782222290074816037776586657834841586939662825734294051183140794537141608771803070715941051121170285190347786926570042246331102750604036185540464179153763503857127117918822547579033069472418242684328083352174724579376695971173152319349449321466491373527284227385153411689217559966957882267024615430273115634918212890625 这个行数多得吓人!这还只是整个概率表的一小部分,因为我们还需要长度从 1023 个 tokens 到 1 个 token 的所有序列,以确保在模型输入中没有足够 token 时,较短的 token 序列也能被处理。虽然马尔可夫链很有趣,但它们也确实存在很大的可扩展性问题。 而且,1024 个 tokens 的上下文窗口现在也已经不算特别大了。在 GPT-3 中,上下文窗口增加到了 2048 个 tokens,然后在 GPT-3.5 中增加到了 4096 个 token。GPT-4 最开始拥有 8192 个 tokens 的上下文窗口,后来增加到了 32K,再后来又增加到了 128K(没错,128,000 个 tokens !)。现在,具有 1M 或更大上下文窗口的模型也开始出现了,更大的上下文窗口允许模型更好地理解和利用之前输入的文本信息,从而在生成新的文本时保持更高的连贯性和准确性。 总之,马尔可夫链让我们以正确的方式思考文本生成问题,但它们也存在一些重大问题,使得我们无法将其视为一个可行的解决方案。 2.2 从马尔可夫链到神经网络 显然,我们必须放弃使用概率表的想法,因为一个合理上下文窗口的概率表需要的 RAM 大得惊人。我们可以采取的替代方案是用函数(function)来代替表格(table),这个函数是通过算法生成的,而不是存储在一个大表格中。这正是神经网络擅长的领域。 神经网络是一种特殊的函数,它接受 inputs ,对其进行计算,并返回 output 。对于语言模型来说,input 代表提示词转换的 tokens,output 是对下一个 token 的预测概率列表。 我说过神经网络是一种"特殊"的函数,是因为除了函数的逻辑之外,它们对 input 进行的计算还受到一系列外部定义参数的控制。一开始,神经网络的参数是未知的,因此,函数产生的输出是完全无用的。神经网络的训练过程包括寻找参数,找到使函数在对训练数据集进行评估时表现最佳的参数,其假设是,如果函数在训练数据上表现良好,那么它也会在其他数据上表现良好。 在训练过程中,神经网络的参数会通过反向传播算法[9],以极小的增量进行迭代调整。这个过程涉及大量的数学计算,因此在这里不会详细展开。每次调整后,神经网络的预测能力都会略有提升。更新参数后,神经网络会再次评估其在训练数据集上的表现,并根据评估结果进行下一轮的调整。这个过程会一直持续下去,直到神经网络在训练数据集上对下一个 token 的预测达到令人满意的效果。 下面帮助各位读者了解一下神经网络的规模,GPT-2 模型大约有 15 亿个参数,而 GPT-3 将参数数量增加到 1750 亿,GPT-4 据说有大约 1.76 万亿个参数。使用目前这一代硬件来训练这样大规模的神经网络需要相当长的时间,通常是几周甚至几个月。 有趣的是,由于参数数量众多,而且都是通过长时间的迭代过程计算出来的,没有人类干预,因此很难理解模型是如何工作的。训练好的大语言模型就像一个黑盒子,非常难以调试,因为模型的大部分"思维"都隐藏在参数中。即使是那些训练它们的人,也很难解释其内部的工作原理。 2.3 模型层、Transformers 和注意力机制 你可能非常想知道,神经网络函数内部究竟进行了哪些神奇的运算,使得它们在这些经过精心调优的参数的帮助下,能够接收 input tokens 列表,并以某种方式"猜"出下一个 token 的合理概率。 神经网络就像是一套复杂的操作链条⛓,链条的每个链环都被称为一个"模型层"。第一层接收 inputs (译者注:神经网络接收的初始数据。),对其进行某种形式的转换。经过转换的 input 接着进入下一层,再次进行转换。这一过程一直持续到数据最终到达最后一层,并在这里进行最后一次转换,从而产生最终的模型输出。 机器学习专家们设计了各种类型的模型层,它们能够对输入数据执行数学转换。同时,他们还找到了模型层的不同组织方式和分组方法,以达到预期的效果。有些模型层是通用的,适用于多种类型的输入数据;而有些模型层则是专门为处理特定类型的数据而设计的,比如图像数据或大语言模型中的经过分词后的文本。 在大语言模型的文本生成场景中,目前最流行的神经网络架构是 Transformer[10]。使用这种架构设计的 LLM(大语言模型)被称为 GPT,即 Generative Pre-Trained Transformers[11]。 Transformer 模型的独特之处在于它们执行的一种称为 Attention[12] 的模型层计算方式,这使得它们能够从上下文窗口中的 tokens 中推断出它们之间的关系(relationships)和模式(patterns),随后将这些关系和模式反映在下一个 token 的预测概率中。 Attention 机制最初被用于语言翻译场景,是一种找出输入序列中哪些 tokens 对理解其意义最为重要的方法。这种机制使现代翻译器能够通过关注(或集中"注意力"于)重要的单词或tokens,从根本上"理解"一个句子。 03 Do LLMs Have Intelligence? 您可能已经开始思考,大语言模型(LLMs)在生成文本时是否展现出某种智能了? 我个人并不认为 LLMs 具备推理或创造原创思想的能力,但这并不意味着它们一无是处。由于 LLMs 对上下文窗口中的 tokens 进行了巧妙的计算,所以 LLMs 能够识别出用户提示词中存在的 patterns,并将它们与训练期间学到的类似 patterns 相匹配。 它们生成的文本大部分都是由零碎的训练数据组成的,但它们将单词(实际上是 tokens)拼接在一起的方式非常复杂,在很多情况下,它们生成的模型输出结果既有原创感,又非常有用。 鉴于 LLM 容易产生幻觉,我不会信任未经人工验证的情况下,任何直接将 LLMs 的 output 发送给用户的工作流程。 在未来的几个月或几年里,是否会有更大的 LLM 出现?它们能够实现真正的智能吗?由于 GPT 架构的诸多限制,我觉得这是不太可能实现的,但谁知道呢,也许随着未来创新成果的不断涌现,我们会实现这一目标。 04 The End 感谢您与我一起坚持到最后!我希望本文已经能够激发你的学习兴趣,让你决定继续学习大模型相关知识,并最终面对那些令人畏惧的数学知识,如果您想详细了解每一个细节的话,就无法回避这些数学知识。在这种情况下,我强烈推荐 Andrej Karpathy 的《Neural Networks: Zero to Hero》[13]系列视频。 Thanks for reading! Hope you have enjoyed and learned new things from this blog! About the authors Hi, my name is Miguel. I'm a software engineer, but also tinker with photography and filmmaking when I have time. I was born in Buenos Aires, Argentina, but I lived most of my adult life in Portland, Oregon, USA, the place that comes to mind when I think of home. As of 2018 I'm living in Ireland. END 本期互动内容 🍻 ❓通过阅读本文,你能够理解为什么 LLMs 会产生"幻觉"或者不可靠的输出吗?你认为造成这些问题的原因是什么? 🔗文中链接🔗 [1]https://en.wikipedia.org/wiki/Large_language_model [2]https://en.wikipedia.org/wiki/Generative_artificial_intelligence [3]https://chat.openai.com/ [4]https://en.wikipedia.org/wiki/Byte_pair_encoding [5]https://github.com/openai/gpt-2 [6]https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog [7]https://en.wikipedia.org/wiki/Hallucination_(artificial_intelligence) [8]https://en.wikipedia.org/wiki/Markov_chain [9]https://en.wikipedia.org/wiki/Backpropagation [10]https://en.wikipedia.org/wiki/Transformer_(deep_learning_architecture) [11]https://en.wikipedia.org/wiki/Generative_pre-trained_transformer [12]https://en.wikipedia.org/wiki/Attention_(machine_learning) [13]https://karpathy.ai/zero-to-hero.html 原文链接: https://blog.miguelgrinberg.com/post/how-llms-work-explained-without-math

优秀的个人博客,低调大师

腾讯工程师独家分享:Alluxio线程池调优

Alluxio线程池结构与吞吐量调优 导语 Alluxio 是一个开源的数据编排系统,致力于解决解决大数据分析及 AI 场景下的一些痛点问题,它可以加速数据查询和AI模型训练的速度,提升系统在高并发场景下的高可用能力。这些应用场景决定了 Alluxio 需要具备大吞吐量特性,本文首先介绍 Alluxio Master 的线程池结构,基于线程池分析结果提出 Alluxio 吞吐量调优方案。 业务背景 本次线程池结构分析与调优的对象是 Alluxio 的开源版本,且 Alluxio 的配置项均为默认配置项,分析的场景是游戏 AI 的特征计算阶段。特征计算需要大量读取用户数据分析用户操作,然后针对计算结果进行游戏环境的还原。特征计算任务初期会有数千至上万的进程同时对底层分布式存储节点发起访问,产生流量洪峰。在这种读密集场景下,Alluxio 的高吞吐可以有效缓解存储端压力。 Alluxio 默认线程池结构与 JVM 参数 在业务运行过程中,通过 jstack 生成系统线程信息,导入FastThread分析。分析结果如下: Alluxio Master 节点有 1432 个线程,其中 RUNABLE 状态的线程数仅占46%,有大量线程处于 WATING 和 TIME_WAITING 状态,Master 节点线程数较多,容易发生 OOM,需要根据线程池工作与线程间的调用关系,适当调整线程数量。 Alluxio Master 上有八个线程组,分别为:Alluxio、master、grpc、ForkJoinPool.commonPool、Gang.worker、qtpXXX、MetricsMaster、LockPool Evictor。它们之间的工作与线程调用关系如下图所示。接下来将结合线程模型和 Alluxio 源码分析这些线程组的作用及调优方向。 Alluxio 线程组 Alluxio 线程组共600个线程,它们的状态如下: Name Status Count Alluxio-client-netty-event-loop-RPC-* Runnable 256 alluxio-ufs-sync-prefetch-* Time_Wating 211 alluxio-ufs-sync-* Time_Wating 128 alluxio-master-* Time_Wating 4 alluxio-master-*-StateMachineUpdater Wating 1 由上表可知,Alluxio 线程组中共有5种线程,其中,负责client的线程均处于 RUNNABLE 状态,其余线程处于 TIME_WATING 和 WATING 状态,下面将介绍每个线程的功能。 Alluxio-client-netty-event-loop-RPC 这是 Netty 框架的线程池,属于 NioEventLoopGroup 类型。在这次采样数据中,该线程池256个线程均处于 RUNNABLE 状态,该线程用于 client 端与 server 端建立连接时使用,可以通过下列配置项进行配置,它的默认值为0。 alluxio.user.network.netty.worker.threads Alluxio-ufs-sync 该线程池主要用于并发地执行元数据同步操作,它是 ThreadPoolExecutor 类型。具体的元数据同步操作由 Alluxio-ufs-sync-prefetch 线程组完成。在本次采样中,线程全部处于 TIME_WATING 状态。该线程池的核心线程数与最大线程数相等,它的默认值为系统当前的核心数,可以通过下列配置项修改。 alluxio.master.metadata.sync.executor.pool.size Alluxio-ufs-sync-prefetch 该线程池主要在一个元数据同步操作内,并发地从 UFS 中获取元数据。在本次采样中,线程全部处于 TIME_WAITING 状态。该线程池的核心线程数与最大线程数相等,默认值为系统当前的线程数的 10 倍,可以通过下列配置项修改。 alluxio.master.metadata.sync.ufs.prefetch.pool.size Alluxio-master 这个线程组有5个线程: Name Status Alluxio-master-1_19200-*-alluxio-master-2_19200-GrpcLogAppender-LogAppenderDaemon TIME_WAITING Alluxio-master-1_19200-*-alluxio-master-0_19200-GrpcLogAppender-LogAppenderDaemon TIME_WAITING Alluxio-master-1_19200-*-LeaderStateImpl TIME_WAITING Alluxio-master-1_19200-*-SegmentedRaftLogWorker TIME_WAITING Alluxio-master-1_19200-*-StateMachineUpdater WAITING 本次提取的堆栈信息是从 Alluxio-master-1 节点中取出的,它是 Leader 节点。这五个线程用于支持 Alluxio-master 的具体工作:上表的前两个线程是 master-1 与两个 raft follower 节点进行通信的守护线程,用于 raft 集群中追加日志时进行通信。第三个线程为 Leader 节点特有的,主要用于 Leader 选举的相关操作。第四个线程为 RaftLog 相关的线程,这个线程会处理与 Raft log 相关的 I/O OPS 相关的操作。第五个线程与 StateMachine 相关,它是 Ratis 用于状态机更新的线程。 master 线程组 Master 线程组共256个线程,均处于 WATING 状态。它们组成了一个 ForkJoinPool 类型的线程池,ForkJoinPool 是ExecutorService 的补充,它采用分而治之的思想,比较适合计算密集型任务。Alluxio 用该线程池处理 RPC 请求,它从 RPC Queue 不断取出积压的任务,然后进行处理,这个线程池的创建源码为: ExecutorServiceBuilder#executorService = new ForkJoinPool(parallelism, ThreadFactoryUtils.buildFjp(threadNameFormat, true), null, isAsync, corePoolSize, maxPoolSize, minRunnable, null, keepAliveMs, TimeUnit.MILLISECONDS); GRPC 线程组 GRPC 线程组由4种线程构成,在本次采样中共262个线程。这四种线程属于 GRPC 框架,它们为 Alluxio 提供 RPC 通信服务。 Name Status Count grpc-default-worker-* RUNNABLE 256 grpc-default-boss-* RUNNABLE 2 grpc-default-executor-* TIME_WAITING 3 grpc-shared-destroyer WAITING 1 MetricsMaster 线程组 该线程组共4个线程,它们是一个 FixedThreadPool 类型的线程池,即该线程池的核心线程数与最大线程数相等。该线程池主要用于并行获取从 worker 或者 client 提交的 Metric 数据,并根据数据更新集群的指标信息,这些信息可以通过 Grafana 与Prometheus 相结合地方式直观地检查系统状态。该线程池的核心线程数是可配置的,通过下列配置项完成。 alluxio.master.metrics.service.threads=5(默认值) LockPool Evictor 线程组 LocakPool Evictor 线程组由2个 SingleThreadExecutor 类型的线程池组成,它们作为锁池使用。该线程池的源码如下: private final LockPool<Long> mInodeLocks = new LockPool<>((key) -> new ReentrantReadWriteLock(), ServerConfiguration.getInt(PropertyKey.MASTER_LOCK_POOL_INITSIZE), ServerConfiguration.getInt(PropertyKey.MASTER_LOCK_POOL_LOW_WATERMARK), ServerConfiguration.getInt(PropertyKey.MASTER_LOCK_POOL_HIGH_WATERMARK), ServerConfiguration.getInt(PropertyKey.MASTER_LOCK_POOL_CONCURRENCY_LEVEL)); private final LockPool<Edge> mEdgeLocks = new LockPool<>((key) -> new ReentrantReadWriteLock(), ServerConfiguration.getInt(PropertyKey.MASTER_LOCK_POOL_INITSIZE), ServerConfiguration.getInt(PropertyKey.MASTER_LOCK_POOL_LOW_WATERMARK), ServerConfiguration.getInt(PropertyKey.MASTER_LOCK_POOL_HIGH_WATERMARK), ServerConfiguration.getInt(PropertyKey.MASTER_LOCK_POOL_CONCURRENCY_LEVEL)); 其中,mInodeLocks 用于提供 inode 锁:要锁定一个 inode,必须在该池中得它的id然后获取它的读锁;mEdgeLocks 用于提供边锁,这里的边指的是 inode 树中的一条边,边从父 inode id 指向子 inode id。 这两种锁池中的锁均为可重入的读写锁,锁池的初始数量、最小锁数量、最大锁数量、并发度均可以配置。 配置项 含义 默认值 alluxio.master.lock.pool.initsize 初始值 1000 alluxio.master.lock.pool.low.watermark 锁池低水位 500000 alluxio.master.lock.pool.high.watermark 锁池高水位 1000000 alluxio.master.lock.pool.concurrency.level 并发度 100 qtpXXX 线程组 该线程组用于提供 Jetty 服务,Jetty 是一个开源的 Servlet 容器,对外提供 web 服务。它们属于 QueueThreadPool 类型的线程池。在本次采样结果中共14个线程,这个线程池的最大线程数为254个,最小线程数为8。 Gang.worker 线程组 Gang worker 线程组用于 JVM 的垃圾回收。该线程组的线程数可以通过修改 JVM 参数进行 -XX:ParallelGCThreads 进行修改。 调优原理与结果 审计日志 在吞吐量测试过程中,我们在编译器研发团队的帮助下,通过 Kona Profile 采样,并对采样结果进行分析,发现 Alluxio 在运行过程中,生成审计日志时存在明显的锁竞争,blocking queue 的 size 成为了瓶颈点。 基于这种现象,我们选择在非生产环境下关闭审计日志,在生产环境下调高审计日志的 blocking queue size 的方式调优性能。在调整后发现,系统吞吐量明显提升。 调整 UFS-SYNC-PREFETCH 线程池 在 Alluxio Master 运行业务时,大量的任务积压在 master rpc queue 中,ForkJoinPool 线程池的处理速度受限于物理机的资源成为了瓶颈点。Alluxio-UFS-SYNC-PREFETCH 线程池用于执行元数据的同步工作,这个线程数默认为系统 CPU 核数的10倍,在实际系统中并不需要这么多的线程数,因此这种配置存在线程浪费情况。因此,我们将该线程数调整为2倍的 CPU 核数,调整后发现没有出现性能下降的情况。 JVM GC 线程数调优 Alluxio 在腾讯自研的 KonaJDK 11 上作测试,使用 JDK 默认参数会出现集群 Full GC 时间过长的情况,Master 停顿时间过长会导致 Leader 切换,这种切换的成本很高。通过分析 GC 日志,我们发现默认的 GC 并行线程数较小,仅为30。考虑到我们使用的为64服务器,我们将 GC 并行线程数调整为40,发现 GC 停顿时间明显降低,系统的稳定性增强。 后续调优方向展望 Alluxio 作为存储引擎和计算框架之间的中间件,承载着数据缓存、访问加速、缓解存储压力等任务,这些功能都对 Alluxio 系统的吞吐量提出了较高的要求。在接下来工作中,我们将继续改进方案,不断提升系统吞吐量性能。为此,我们设计了几种方案。 将 Follower Master 开启读服务 现阶段,Alluxio 仅有 Leader Master 接受 Client 端的读写请求,这限制了 Alluxio 吞吐量的提升。这会产生单点瓶颈效应,我们计划在后续将 Follower Master 接受 Client 端的读请求,这样会有效提升系统吞吐量。 这种架构的优势在于可以充分利用现有的节点,不需要有非常大的代码改动,但缺点为系统提升仍会有瓶颈,不具备无限扩展能力。 开发 Observer Master 为了使集群在理论上拥有无限扩展能力,可以借鉴 Zookeeper 的 Observer Node 思想,开发 Oberserver Master 节点,系统模型如下: 这种模型可以添加多个 Observer Master 节点,它们的元数据同步可以主动 tail raft group 中的 log,并回放到本地状态机上,worker 的 block 信息可以周期性地进行同步。这种架构的优势为理论上可以无限扩展,但劣势是需要额外的节点资源。且这种方案代码改动较多、开发难度大,block 位置信息的同步开销也比较大。 总结 本次初衷是基于线程池结构对 Alluxio 吞吐量性能进行调优,根据本次测试采样结果分析了性能瓶颈点,调优相关瓶颈点后得到性能提升。 基于 Alluxio Master 的源码,本文介绍了 Alluxio Master 的线程池结构与每个线程的功能。在调优过程中,利用分析结果调整审计日志的 blocking queue,调整 UFS-SYNC-PREFETCH 线程数,调优 JVM 参数。通过实验证明,Alluxio 吞吐量提升7倍。 最后,本文提出了 Alluxio 吞吐量提升的未来优化方向。 想要获取更多有趣有料的【活动信息】【技术文章】【大咖观点】,请关注[Alluxio智库]:

优秀的个人博客,低调大师

[灵魂拷问]MySQL面试高频100问(工程师方向)

前言 本文主要受众为开发人员,所以不涉及到MySQL的服务部署等操作,且内容较多,大家准备好耐心和瓜子矿泉水. 前一阵系统的学习了一下MySQL,也有一些实际操作经验,偶然看到一篇和MySQL相关的面试文章,发现其中的一些问题自己也回答不好,虽然知识点大部分都知道,但是无法将知识串联起来. 因此决定搞一个MySQL灵魂100问,试着用回答问题的方式,让自己对知识点的理解更加深入一点.此文不会事无巨细的从select的用法开始讲解mysql,主要针对的是开发人员需要知道的一些MySQL的知识点,主要包括索引,事务,优化等方面,以在面试中高频的问句形式给出答案. 索引相关 1. 什么是索引? 索引是一种数据结构,可以帮助我们快速的进行数据的查找. 2. 索引是个什么样的数据结构呢? 索引的数据结构和具体存储引擎的实现有关, 在MySQL中使用较多的索引有Hash索引,B+树索引等,而我们经常使用的InnoDB存储引擎的默认索引实现为:B+树索引. 3. Hash索引和B+树所有有什么区别或者说优劣呢? 首先知道Hash索引和B+树索引的底层实现原理: hash索引底层就是hash表,进行查找时,调用一次hash函数就可以获取到相应的键值,之后进行回表查询获得实际数据.B+树底层实现是多路平衡查找树.对于每一次的查询都是从根节点出发,查找到叶子节点方可以获得所查键值,然后根据查询判断是否需要回表查询数据. 那么可以看出他们有以下的不同: hash索引进行等值查询更快(一般情况下),但是却无法进行范围查询. 因为在hash索引中经过hash函数建立索引之后,索引的顺序与原顺序无法保持一致,不能支持范围查询.而B+树的的所有节点皆遵循(左节点小于父节点,右节点大于父节点,多叉树也类似),天然支持范围. hash索引不支持使用索引进行排序,原理同上. hash索引不支持模糊查询以及多列索引的最左前缀匹配.原理也是因为hash函数的不可预测.AAAA和AAAAB的索引没有相关性. hash索引任何时候都避免不了回表查询数据,而B+树在符合某些条件(聚簇索引,覆盖索引等)的时候可以只通过索引完成查询. hash索引虽然在等值查询上较快,但是不稳定.性能不可预测,当某个键值存在大量重复的时候,发生hash碰撞,此时效率可能极差.而B+树的查询效率比较稳定,对于所有的查询都是从根节点到叶子节点,且树的高度较低. 因此,在大多数情况下,直接选择B+树索引可以获得稳定且较好的查询速度.而不需要使用hash索引. 4. 上面提到了B+树在满足聚簇索引和覆盖索引的时候不需要回表查询数据,什么是聚簇索引? 在B+树的索引中,叶子节点可能存储了当前的key值,也可能存储了当前的key值以及整行的数据,这就是聚簇索引和非聚簇索引. 在InnoDB中,只有主键索引是聚簇索引,如果没有主键,则挑选一个唯一键建立聚簇索引.如果没有唯一键,则隐式的生成一个键来建立聚簇索引. 当查询使用聚簇索引时,在对应的叶子节点,可以获取到整行数据,因此不用再次进行回表查询. 5. 非聚簇索引一定会回表查询吗? 不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询. 举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行 select age from employee where age < 20的查询时,在索引的叶子节点上,已经包含了age信息,不会再次进行回表查询. 6. 在建立索引的时候,都有哪些需要考虑的因素呢? 建立索引的时候一般要考虑到字段的使用频率,经常作为条件进行查询的字段比较适合.如果需要建立联合索引的话,还需要考虑联合索引中的顺序.此外也要考虑其他方面,比如防止过多的所有对表造成太大的压力.这些都和实际的表结构以及查询方式有关. 7. 联合索引是什么?为什么需要注意联合索引中的顺序? MySQL可以使用多个字段同时建立一个索引,叫做联合索引.在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引.具体原因为: MySQL使用索引时需要索引有序,假设现在建立了"name,age,school"的联合索引,那么索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。当进行查询时,此时索引仅仅按照name严格有序,因此必须首先使用name字段进行等值查询,之后对于匹配到的列而言,其按照age字段严格有序,此时可以使用age字段用做索引查找,以此类推. 因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面.此外可以根据特例的查询或者表结构进行单独的调整. 8. 创建的索引有没有被使用到?或者说怎么才可以知道这条语句运行很慢的原因? MySQL提供了explain命令来查看语句的执行计划,MySQL在执行某个语句之前,会将该语句过一遍查询优化器,之后会拿到对语句的分析,也就是执行计划,其中包含了许多信息. 可以通过其中和索引有关的信息来分析是否命中了索引,例如possilbe_key,key,key_len等字段,分别说明了此语句可能会使用的索引,实际使用的索引以及使用的索引长度. 9. 那么在哪些情况下会发生针对该列创建了索引但是在查询的时候并没有使用呢? 使用不等于查询, 列参与了数学运算或者函数 在字符串like时左边是通配符.类似于'%aaa'. 当mysql分析全表扫描比使用索引快的时候不使用索引. 当使用联合索引,前面一个条件为范围查询,后面的即使符合最左前缀原则,也无法使用索引. 以上情况,MySQL无法使用索引. 事务相关 1. 什么是事务? 理解什么是事务最经典的就是转账的栗子,相信大家也都了解,这里就不再说一遍了。事务是一系列的操作,他们要符合ACID特性.最常见的理解就是:事务中的操作要么全部成功,要么全部失败.但是只是这样还不够的. 2. ACID是什么?可以详细说一下吗? A=Atomicity 原子性,就是上面说的,要么全部成功,要么全部失败.不可能只执行一部分操作. C=Consistency 系统(数据库)总是从一个一致性的状态转移到另一个一致性的状态,不会存在中间状态. I=Isolation 隔离性: 通常来说:一个事务在完全提交之前,对其他事务是不可见的.注意前面的通常来说加了红色,意味着有例外情况. D=Durability 持久性,一旦事务提交,那么就永远是这样子了,哪怕系统崩溃也不会影响到这个事务的结果. 3. 同时有多个事务在进行会怎么样呢? 多事务的并发进行一般会造成以下几个问题: 脏读: A事务读取到了B事务未提交的内容,而B事务后面进行了回滚. 不可重复读: 当设置A事务只能读取B事务已经提交的部分,会造成在A事务内的两次查询,结果竟然不一样,因为在此期间B事务进行了提交操作. 幻读: A事务读取了一个范围的内容,而同时B事务在此期间插入了一条数据.造成"幻觉". 4. 怎么解决这些问题呢?MySQL的事务隔离级别了解吗? MySQL的四种隔离级别如下: 未提交读(READ UNCOMMITTED) 这就是上面所说的例外情况了,这个隔离级别下,其他事务可以看到本事务没有提交的部分修改.因此会造成脏读的问题(读取到了其他事务未提交的部分,而之后该事务进行了回滚). 这个级别的性能没有足够大的优势,但是又有很多的问题,因此很少使用. 已提交读(READ COMMITTED) 其他事务只能读取到本事务已经提交的部分.这个隔离级别有 不可重复读的问题,在同一个事务内的两次读取,拿到的结果竟然不一样,因为另外一个事务对数据进行了修改. REPEATABLE READ(可重复读) 可重复读隔离级别解决了上面不可重复读的问题(看名字也知道),但是仍然有一个新问题,就是 幻读,当你读取id> 10 的数据行时,对涉及到的所有行加上了读锁,此时例外一个事务新插入了一条id=11的数据,因为是新插入的,所以不会触发上面的锁的排斥,那么进行本事务进行下一次的查询时会发现有一条id=11的数据,而上次的查询操作并没有获取到,再进行插入就会有主键冲突的问题. SERIALIZABLE(可串行化) 这是最高的隔离级别,可以解决上面提到的所有问题,因为他强制将所以的操作串行执行,这会导致并发性能极速下降,因此也不是很常用. 5. Innodb使用的是哪种隔离级别呢? InnoDB默认使用的是可重复读隔离级别. 6. 对MySQL的锁了解吗? 当数据库有并发事务的时候,可能会产生数据的不一致,这时候需要一些机制来保证访问的次序,锁机制就是这样的一个机制. 就像酒店的房间,如果大家随意进出,就会出现多人抢夺同一个房间的情况,而在房间上装上锁,申请到钥匙的人才可以入住并且将房间锁起来,其他人只有等他使用完毕才可以再次使用. 7. MySQL都有哪些锁呢?像上面那样子进行锁定岂不是有点阻碍并发效率了? 从锁的类别上来讲,有共享锁和排他锁. 共享锁: 又叫做读锁. 当用户要进行数据的读取时,对数据加上共享锁.共享锁可以同时加上多个. 排他锁: 又叫做写锁. 当用户要进行数据的写入时,对数据加上排他锁.排他锁只可以加一个,他和其他的排他锁,共享锁都相斥. 用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的. 一种是真正的入住一晚,在这期间,无论是想入住的还是想看房的都不可以. 锁的粒度取决于具体的存储引擎,InnoDB实现了行级锁,页级锁,表级锁. 他们的加锁开销从大大小,并发能力也是从大到小. 表结构设计 1. 为什么要尽量设定一个主键? 主键是数据库确保数据行在整张表唯一性的保障,即使业务上本张表没有主键,也建议添加一个自增长的ID列作为主键.设定了主键之后,在后续的删改查的时候可能更加快速以及确保操作数据范围安全. 2. 主键使用自增ID还是UUID? 推荐使用自增ID,不要使用UUID.因为在InnoDB存储引擎中,主键索引是作为聚簇索引存在的,也就是说,主键索引的B+树叶子节点上存储了主键索引以及全部的数据(按照顺序),如果主键索引是自增ID,那么只需要不断向后排列即可,如果是UUID,由于到来的ID与原来的大小不确定,会造成非常多的数据插入,数据移动,然后导致产生很多的内存碎片,进而造成插入性能的下降. 总之,在数据量大一些的情况下,用自增主键性能会好一些. 关于主键是聚簇索引,如果没有主键,InnoDB会选择一个唯一键来作为聚簇索引,如果没有唯一键,会生成一个隐式的主键. If you define a PRIMARY KEY on your table, InnoDB uses it as the clustered index. If you do not define a PRIMARY KEY for your table, MySQL picks the first UNIQUE index that has only NOT NULL columns as the primary key and InnoDB uses it as the clustered index. 3. 字段为什么要求定义为not null? MySQL官网这样介绍: NULL columns require additional space in the rowto record whether their values are NULL. For MyISAM tables, each NULL columntakes one bit extra, rounded up to the nearest byte. null值会占用更多的字节,且会在程序中造成很多与预期不符的情况. 4. 如果要存储用户的密码散列,应该使用什么字段进行存储? 密码散列,盐,用户身份证号等固定长度的字符串应该使用char而不是varchar来存储,这样可以节省空间且提高检索效率. 存储引擎相关 1. MySQL支持哪些存储引擎? MySQL支持多种存储引擎,比如InnoDB,MyISAM,Memory,Archive等等.在大多数的情况下,直接选择使用InnoDB引擎都是最合适的,InnoDB也是MySQL的默认存储引擎. InnoDB和MyISAM有什么区别? InnoDB支持事物,而MyISAM不支持事物 InnoDB支持行级锁,而MyISAM支持表级锁 InnoDB支持MVCC, 而MyISAM不支持 InnoDB支持外键,而MyISAM不支持 InnoDB不支持全文索引,而MyISAM支持。 零散问题 1. MySQL中的varchar和char有什么区别. char是一个定长字段,假如申请了 char(10)的空间,那么无论实际存储多少内容.该字段都占用10个字符,而varchar是变长的,也就是说申请的只是最大长度,占用的空间为实际字符长度+1,最后一个字符存储使用了多长的空间. 在检索效率上来讲,char > varchar,因此在使用中,如果确定某个字段的值的长度,可以使用char,否则应该尽量使用varchar.例如存储用户MD5加密后的密码,则应该使用char. 2. varchar(10)和int(10)代表什么含义? varchar的10代表了申请的空间长度,也是可以存储的数据的最大长度,而int的10只是代表了展示的长度,不足10位以0填充.也就是说,int(1)和int(10)所能存储的数字大小以及占用的空间都是相同的,只是在展示时按照长度展示. 3. MySQL的binlog有有几种录入格式?分别有什么区别? 有三种格式,statement,row和mixed. statement模式下,记录单元为语句.即每一个sql造成的影响会记录.由于sql的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制. row级别下,记录单元为每一行的改动,基本是可以全部记下来但是由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大. mixed. 一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row. 此外,新版的MySQL中对row级别也做了一些优化,当表结构发生变化的时候,会记录语句而不是逐行记录. 4. 超大分页怎么处理? 超大的分页一般从两个方向上来解决. 数据库层面,这也是我们主要集中关注的(虽然收效没那么大),类似于 select * from table where age &gt; 20 limit 1000000,10 这种查询其实也是有可以优化的余地的. 这条语句需要load1000000数据然后基本上全部丢弃,只取10条当然比较慢. 当时我们可以修改为 select * from table where id in (select id from table where age &gt; 20 limit 1000000,10) .这样虽然也load了一百万的数据,但是由于索引覆盖,要查询的所有字段都在索引中,所以速度会很快. 同时如果ID连续的好,我们还可以 select * from table where id &gt; 1000000 limit 10 ,效率也是不错的,优化的可能性有许多种,但是核心思想都一样,就是减少load的数据. 从需求的角度减少这种请求….主要是不做类似的需求(直接跳转到几百万页之后的具体某一页.只允许逐页查看或者按照给定的路线走,这样可预测,可缓存)以及防止ID泄漏且连续被人恶意攻击. 解决超大分页,其实主要是靠缓存,可预测性的提前查到内容,缓存至redis等k-V数据库中,直接返回即可. 5. 关心过业务系统里面的sql耗时吗?统计过慢查询吗?对慢查询都怎么优化过? 在业务系统中,除了使用主键进行的查询,其他的我都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们. 慢查询的优化首先要搞明白慢的原因是什么? 是查询条件没有命中索引?是load了不需要的数据列?还是数据量太大? 所以优化也是针对这三个方向来的, 首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写. 分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引. 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表. 6. 上面提到横向分表和纵向分表,可以分别举一个适合他们的例子吗? 横向分表是按行分表.假设我们有一张用户表,主键是自增ID且同时是用户的ID.数据量较大,有1亿多条,那么此时放在一张表里的查询效果就不太理想.我们可以根据主键ID进行分表,无论是按尾号分,或者按ID的区间分都是可以的. 假设按照尾号0-99分为100个表,那么每张表中的数据就仅有100w.这时的查询效率无疑是可以满足要求的. 纵向分表是按列分表.假设我们现在有一张文章表.包含字段 id-摘要-内容.而系统中的展示形式是刷新出一个列表,列表中仅包含标题和摘要,当用户点击某篇文章进入详情时才需要正文内容.此时,如果数据量大,将内容这个很大且不经常使用的列放在一起会拖慢原表的查询速度.我们可以将上面的表分为两张. id-摘要, id-内容.当用户点击详情,那主键再来取一次内容即可.而增加的存储量只是很小的主键字段.代价很小. 当然,分表其实和业务的关联度很高,在分表之前一定要做好调研以及benchmark.不要按照自己的猜想盲目操作. 7. 什么是存储过程?有哪些优缺点? 存储过程是一些预编译的SQL语句。1、更加直白的理解:存储过程可以说是一个记录集,它是由一些T-SQL语句组成的代码块,这些T-SQL语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后再给这个代码块取一个名字,在用到这个功能的时候调用他就行了。2、存储过程是一个预编译的代码块,执行效率比较高,一个存储过程替代大量T_SQL语句 ,可以降低网络通信量,提高通信速率,可以一定程度上确保数据安全。 8. 说一说三个范式 第一范式: 每个列都不可以再拆分. 第二范式: 非主键列完全依赖于主键,而不能是依赖于主键的一部分. 第三范式: 非主键列只依赖于主键,不依赖于其他非主键. 在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由.比如性能. 事实上我们经常会为了性能而妥协数据库的设计. 9. MyBatis中的# 乱入了一个奇怪的问题…..我只是想单独记录一下这个问题,因为出现频率太高了.#会将传入的内容当做字符串,而有什么区别?∗∗乱入了一个奇怪的问题.....我只是想单独记录一下这个问题,因为出现频率太高了.#会将传入的内容当做字符串,而会直接将传入值拼接在sql语句中. 所以#可以在一定程度上预防sql注入攻击.

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册