躬行笔记 - 2026年5月
type: channel
link: https://www.fuzhoupyy.work/index.php/2026/05/
description: 古人学问无遗力，少壮工夫老始成。 纸上得来终觉浅，绝知此事要躬行。
items: AI时代的隐形杀手：为什么你的团队代码产出翻倍，但交付速度却在原地踏步？
items: 从0到1掌握Ansible：让自动化运维不再是梦想
items: 救命！文件系统崩了怎么办？Linux系统文件系统错误检查修复完全指南
items: Puppet配置管理实战：让1000台服务器听话的秘密武器
items: 互联网的幕后英雄：深度解析BGP协议，看看网络是如何连接全世界的
items: 生产环境救急指南：Linux服务器忘记密码的N种解法，覆盖主流系统和云厂商
items: 一口气搞懂 MySQL MVCC：从隐藏字段到生产“背刺”的那些坑
items: 一条 SQL 从敲下回车，到在 MySQL 里“跑完一生”，中间到底经历了啥？
items: Jenkins 对接 Docker 做 CI/CD，我是怎么一步步跑通的，踩坑也一起说清楚
items: 面试必问！MySQL 事务到底是怎么实现的？这篇文章讲透了

AI时代的隐形杀手：为什么你的团队代码产出翻倍，但交付速度却在原地踏步？
type: item
date: 2026-05-14T14:58:41+00:00
description: 最近在和几个朋友聊天的时候，发现一个特别有意思的现象。大家都在说AI编程工具有多牛，claude code,kiro,codex等这些工具确实让写代码变得飞快，我自己用下来感觉效率至少提升了一半以上。但是奇怪的事情来了，虽然代码写得快了，但项目交付的速度好像并没有跟着提升多少，有时候甚至感觉更慢了。刚开始我以为是个例，结果跟圈子里其他人一聊，发现这个问题还挺普遍的。后来仔细想想，问题可能出在我们一直忽略的地方——代码审查。生产力悖论：写得快了，为什么交付还是慢？我记得去年刚开始用AI编程工具的时候，那种感觉简直不要太爽。以前写一个功能模块可能要大半天，现在有了AI助手，一个上午就能搞定好几个。当时还挺得意，觉得自己的生产力简直爆表了。但是很快就发现问题了。以前我一天可能提交1-2个PR（Pull Request），现在一天能提交5-6个。看起来很厉害对吧？但是这些PR在那里排队等审查的时间越来越长，有时候一个PR要等好几天才有人看。更要命的是，当你手头有3-4个未审查的PR在那里挂着的时候，脑子里总是惦记着这些事情，没法专心做新的功能。这种感觉就像是你同时在做好几件事，结果哪件事都做不好。我们团队的情况可能还算好的，听说有些团队的PR积压更严重，一个PR从提交到合并要一个星期，这完全失去了敏捷开发的意义。审查工作的性质变了以前代码都是人写的，虽然偶尔有bug，但整体逻辑是连贯的，审查起来相对轻松。现在AI生成的代码就不一样了，表面上看起来没问题，但仔细一看总感觉哪里不对劲。我举个例子，前几天我用AI写了一个数据处理的函数，代码跑起来没问题，测试也通过了。但是我的同事在审查的时候发现，这个函数处理边界情况的方式特别复杂，用了很多防御性编程的技巧，但实际上我们的业务场景根本不需要这么复杂的处理。这就是AI代码的特点：功能上没毛病，但可能过度设计了。审查的时候你不仅要看代码对不对，还要判断这个代码有没有必要，会不会给后续维护带来负担。这比以前单纯检查bug要费脑子多了。还有一个问题是，AI生成的代码风格可能跟团队的编码规范不太一致。虽然大部分静态检查工具能发现明显的问题，但有些细节还是需要人工判断。这就导致审查的工作量实际上是增加的，不是减少的。积压的恶性循环代码审查的延迟不是简单的时间延迟，它会触发一系列连锁反应。当你的PR在队列里等待的时候，你可能已经开始下一个功能的开发了。等审查意见回来的时候，你可能已经忘记当初为什么要这么写了，需要重新熟悉代码。我们团队就遇到过这种情况。有一次我提交了一个重构的PR，因为审查队列比较长，等了一个星期才有人看。这期间我又开发了两个新功能，等审查意见回来的时候，发现新功能和重构的代码有冲突，结果花了比原来更多的时间来解决冲突。更糟糕的是，当你同时维护多个未合并的分支时，心理负担会成倍增加。每次切换分支都需要重新理解上下文，这种上下文切换的成本是非常高的。有研究表明，开发者同时处理多个任务时，光是上下文切换就会消耗20-50%的时间。解决方案不是招更多人很多团队遇到这个问题的第一反应就是招更多的高级开发者来做代码审查，或者要求大家加快审查速度。但这些方法都没有抓住问题的本质。代码审查是一个工作流程，不是一个简单的关卡。我们需要把它当作一个系统来优化，而不是单纯地增加处理能力。首先，不是所有的PR都需要同样深度的审查。比如说，修改文档、调整配置文件、简单的bug修复，这些低风险的改动完全可以走快速通道。我们可以根据改动的类型、涉及的文件、代码复杂度等因素来自动评估风险等级。我们团队现在用的方法是给PR打标签，绿色标签表示低风险可以快速审查，黄色表示中等风险需要仔细看看，红色表示高风险需要多人审查。这样审查者可以根据自己的时间安排来选择处理哪些PR。其次，审查队列的可见性很重要。如果一个人手头有15个PR等着审查，另一个人只有2个，这种不平衡需要及时发现和调整。我们现在用一个简单的看板来跟踪所有人的审查队列，这样就能及时发现瓶颈。AI工具在审查中的应用虽然AI写代码可能会增加审查的复杂性，但AI同样可以帮助我们做审查。现在有很多工具可以自动检查代码风格、安全漏洞、性能问题等机械性的内容，让人类审查者可以专注于架构设计和业务逻辑。我们团队最近在试用一些AI代码审查工具，效果还不错。这些工具可以自动标注出可能有问题的代码段，提供修改建议，甚至可以解释为什么某段代码可能有问题。当然，最终的判断还是要靠人，但这些工具确实能提高审查的效率。另外，保持PR的大小也很重要。一个包含几百行改动的PR和几个只有十几行改动的小PR，审查的难度是完全不同的。小的、专注的PR不仅更容易审查，也不容易在队列里腐烂。工具和流程的结合光有好的流程还不够，还需要合适的工具来支撑。现在市面上有一些专门的代码审查管理工具，可以把所有仓库…

从0到1掌握Ansible：让自动化运维不再是梦想
type: item
date: 2026-05-12T15:55:07+00:00
description: 最近在公司推进自动化运维的时候，发现很多同事对Ansible还是一知半解，要么就是简单用用，要么就是直接放弃。其实Ansible真的没那么复杂，我用了这么多年，今天就把我的实战经验分享给大家。说实话，刚开始接触Ansible的时候我也是懵的。那时候公司有几十台服务器需要管理，每次部署应用或者更新配置都要一台一台去操作，累死累活不说，还容易出错。后来接触到Ansible，真的是解放了双手。Ansible到底是个什么东西？简单来说，Ansible就是一个自动化工具。你可以把它理解成一个"遥控器"，能够同时控制多台服务器干活。比如你要在100台服务器上安装nginx，传统方式得一台台登录去装，用Ansible的话，写个脚本，一键搞定。我记得第一次用Ansible批量部署应用的时候，看着终端里刷刷刷地执行命令，那种感觉就像开了挂一样。原本需要一整天的工作，10分钟就搞定了。控制节点和受控节点的关系这个概念其实很好理解。控制节点就是你安装Ansible的那台机器，受控节点就是你要管理的那些服务器。我一般会把Ansible装在跳板机上，这样管理起来比较方便。控制节点需要能SSH到所有受控节点，这是最基本的要求。# 在控制节点安装Ansible yum install ansible -y # 或者用pip安装 pip install ansible受控节点其实什么都不用装，只要能SSH连接就行。这也是Ansible的一个优势，不像其他工具还要在目标机器上装agent。清单文件(Inventory)：管理服务器的花名册Inventory文件就像是你的服务器通讯录，记录着所有需要管理的机器信息。最简单的inventory文件长这样：[webservers] 192.168.1.10 192.168.1.11 192.168.1.12 [databases] 192.168.1.20 192.168.1.21但实际生产环境中，我会写得更详细一些：[webservers] web01 ansible_host=192.168.1.10 ansible_user=root ansible_ssh_private_key_file=/root/.ssh/id_rsa web02 ansible_host=192.168.1.11 ansible_user=root ansible_ssh_private_key_file=/root/.ssh/id_rsa [databases] db01 ansible_host=192.168.1.20 ansible_user=mysql ansible_become=yes db02 ansible_host=192.168.1.21 ansible_user=mysql ansible_become=yes [production:children] webservers databases这样写的好处是可以给不同的机器设置不同的连接参数。比如有些机器用密钥登录，有些用密码，有些需要sudo权限等等。模块(Modules)：Ansible的工具箱Ansible有几千个模块，每个模块负责不同的功能。刚开始的时候不用全部掌握，先学会常用的几个就够了。文件操作模块copy模块用来复制文件：- name: 复制配置文件 copy: src: /local/path/nginx.conf dest: /etc/nginx/nginx.conf owner: root group: root mode: '0644'file模块用来管理文件和目录：- name: 创建目录 file: path: /opt/myapp state: directory owner: www group: www mode: '0755'软件包管理yum模块在CentOS/RHEL上安装软件：- name: 安装nginx yum: name: nginx state: presentapt模块在Ubuntu/Debian上用：- name: 安装nginx apt: name: nginx state: present update_cache: yes服务管理systemd模块管理系统服务：- name: 启动nginx服务 systemd: name: nginx state: started enabled: yes我在实际使用中发现，掌握这几个模块就能解决80%的日常运维工作了。任务(Tasks)和剧本(Playbooks)：自动化的剧本Task就是一个具体的操作，比如安装软件、复制文件等。Playbook就是把多个Task组织起来，形成一个完整的自动化流程。我来分享一个实际的例子，部署一个简单的web应用：--- - name…

救命！文件系统崩了怎么办？Linux系统文件系统错误检查修复完全指南
type: item
date: 2026-05-11T14:53:48+00:00
description: 前段时间我们生产环境遇到了一个特别头疼的问题，服务器突然断电重启后，发现有个分区挂载不上了，系统报错说文件系统有问题。当时真的是冷汗直冒，要知道那个分区里放着重要的业务数据啊！相信很多做运维的朋友都遇到过类似的情况，服务器异常关机、硬盘故障、甚至是误操作，都可能导致文件系统出现错误。今天我就把这些年积累的文件系统检查和修复经验分享给大家，希望能帮到遇到同样问题的兄弟们。什么是文件系统错误在开始讲具体操作之前，我觉得有必要先说说文件系统错误到底是怎么回事。Linux的文件系统就像是一个巨大的图书馆，每个文件都有自己的位置和索引。当系统正常运行时，所有的读写操作都会按照规定的流程进行，文件系统会维护各种元数据信息，比如inode表、目录结构、空闲块列表等等。但是当系统异常关机或者硬盘出现问题时，这些元数据可能会变得不一致。比如说，一个文件明明已经删除了，但是inode表里还记录着它的存在；或者某个数据块被标记为空闲，但实际上还被某个文件占用着。这些不一致的情况就是我们说的文件系统错误。我记得有一次，一台服务器的UPS电池没电了，突然断电导致ext4文件系统出现了大量的orphaned inodes（孤儿inode）。当时系统启动时就卡在那里不动了，后来才知道是文件系统自检发现了问题。fsck工具详解说到Linux文件系统检查和修复，就不得不提fsck这个神器了。fsck全称是File System Consistency Check，顾名思义就是用来检查文件系统一致性的。fsck其实不是一个单独的程序，而是一个前端工具，它会根据文件系统的类型调用相应的检查程序。比如对于ext4文件系统，它会调用fsck.ext4；对于xfs文件系统，它会调用fsck.xfs。fsck的工作原理fsck的工作过程大概可以分为几个阶段：阶段1：检查inode和块这个阶段主要检查每个inode的基本信息是否正确，包括文件类型、权限、链接数等。同时检查inode指向的数据块是否有效。阶段2：检查目录结构检查目录项是否指向有效的inode，目录的父子关系是否正确。阶段3：检查目录连接性确保每个目录都可以从根目录访问到，没有孤立的目录。阶段4：检查引用计数验证每个inode的链接计数是否与实际的目录项数量匹配。阶段5：检查块和inode映射检查块位图和inode位图的一致性，确保没有重复分配或遗漏。实战操作步骤好了，理论说得差不多了，现在来点实际的。当你遇到文件系统错误时，应该怎么处理呢？第一步：进入单用户模式这一步非常重要！在修复文件系统之前，必须确保文件系统没有被挂载，或者以只读方式挂载。如果文件系统正在被使用，fsck可能会造成数据损坏。如果是根文件系统出问题，你需要从救援模式启动：sudo systemctl isolate rescue.target或者在启动时进入单用户模式，在GRUB菜单中编辑启动参数，添加 single 或 1。如果是其他分区，直接卸载就行：sudo umount /dev/sdXn我之前就吃过这个亏，有一次图省事直接对挂载的文件系统运行fsck，结果把好好的文件系统搞坏了，数据都找不回来了。所以这个步骤千万不能省略！第二步：运行fsck检查卸载文件系统后，就可以开始检查了：sudo fsck /dev/sdXn不过我建议先用只读模式检查一下，看看到底有什么问题：sudo fsck -n /dev/sdXn-n 参数表示只检查不修复，这样可以安全地了解文件系统的状况。如果检查发现问题，fsck会给出详细的报告。比如可能会看到这样的输出：/dev/sdb1: Inode 1234 has illegal block(s). /dev/sdb1: Deleted inode 1234 has non-zero size. /dev/sdb1: UNEXPECTED INCONSISTENCY; RUN fsck MANUALLY.第三步：自动修复如果确认要修复，可以使用 -y 参数让fsck自动修复所有发现的问题：sudo fsck -y /dev/sdXn这个过程可能会比较长，特别是对于大容量的硬盘。我见过最夸张的一次，一个2TB的硬盘跑了整整6个小时才修复完成。第四步：重新挂载修复完成后，就可以重新挂载文件系统了：sudo mount -o remount,rw /dev/sdXn如果之前是单用户模式，现在可以退出到正常的多用户模式：sudo systemctl isolate multi-user.target不同文件系统的特殊处理虽然fsck是通用工具，但不同的文件系统还是有一些特殊的地方需要注意。ext4文件系统ext4是目前Linux最常用的文件系统之一，对于ext4，我们可以使用一些特殊的参数：# 强制检查，即使文件…

Puppet配置管理实战：让1000台服务器听话的秘密武器
type: item
date: 2026-05-08T13:43:39+00:00
description: 今天就来分享一下我这几个月使用Puppet管理多服务器配置的实战经验，包括一些踩过的坑和解决方案。Puppet基础概念快速入门在开始实战之前，先简单说说Puppet的核心概念，这些理解了后面的操作就很容易了。Puppet采用的是Master-Agent架构，Master节点存储配置清单（manifest），Agent节点定期向Master请求配置并应用。整个过程是声明式的，你只需要描述想要的最终状态，Puppet会自动计算如何达到这个状态。几个重要概念：Manifest：用Puppet DSL语言编写的配置文件，描述系统应该是什么样子Module：可复用的配置单元，包含manifest、文件、模板等Node：被管理的服务器节点Catalog：Master为每个节点编译生成的具体配置指令环境搭建实战我的测试环境是3台CentOS 7服务器，1台Master，2台Agent。生产环境类似，只是Agent数量更多。Master节点安装配置# 安装Puppet Server rpm -Uvh https://yum.puppet.com/puppet-release-el-7.noarch.rpm yum install -y puppetserver # 修改内存配置，默认2G太大了 vim /etc/sysconfig/puppetserver JAVA_ARGS="-Xms512m -Xmx512m" # 启动服务 systemctl enable puppetserver systemctl start puppetserver这里有个坑，Puppet Server默认内存配置是2G，小环境根本用不了这么多，而且会导致启动失败。我把它调到512M，测试环境够用了。Agent节点配置# 安装Puppet Agent rpm -Uvh https://yum.puppet.com/puppet-release-el-7.noarch.rpm yum install -y puppet-agent # 配置Master地址 vim /etc/puppetlabs/puppet/puppet.conf [main] server = puppet-master.example.com # 启动服务 systemctl enable puppet systemctl start puppetAgent第一次连接Master时需要证书认证，在Master上执行：/opt/puppetlabs/bin/puppetserver ca list /opt/puppetlabs/bin/puppetserver ca sign --all编写第一个配置管理模块我们从一个简单的nginx模块开始。在生产环境中，nginx的配置管理是个头疼的问题，不同环境的配置文件经常不一致。创建nginx模块结构cd /etc/puppetlabs/code/environments/production/modules mkdir -p nginx/{manifests,files,templates,tests}编写主配置文件# nginx/manifests/init.pp class nginx { package { 'nginx': ensure => installed, } service { 'nginx': ensure => running, enable => true, require => Package['nginx'], } file { '/etc/nginx/nginx.conf': ensure => present, source => 'puppet:///modules/nginx/nginx.conf', owner => 'root', group => 'root', mode => '0644', notify => Service['nginx'], require => Package['nginx'], } }这个配置很简单，确保nginx包安装、服务运行、配置文件正确。notify参数很重要，当配置文件变化时会自动重启服务。准备配置文件模板# 将标准的nginx配置放到files目录 cp /etc/nginx/nginx.conf nginx/files/应用到节点# site.pp node 'web-server-01.example.com' { include nginx } node 'web-server-02.example.com' { include nginx }这样两台web服务器就会自动安装和配置nginx了。每次Agent运行时（默认30分钟），都会检查配置是否符合要求，不符…

互联网的幕后英雄：深度解析BGP协议，看看网络是如何连接全世界的
type: item
date: 2026-05-07T13:25:10+00:00
description: 前几天在公司部署多线BGP的时候，突然想起刚入行那会儿，对BGP这个协议完全是一头雾水。当时配置BGP，我就像个无头苍蝇一样到处查资料，结果越看越懵。现在回想起来，其实BGP这个东西说复杂也复杂，说简单也简单，关键是要理解它到底在干什么。今天就和大家聊聊BGP这个协议，它可是互联网能够正常运转的核心技术之一。不夸张地说，没有BGP，我们现在刷抖音、看视频、逛淘宝这些日常操作都会变得困难重重。BGP到底是个什么东西？BGP全称是Border Gateway Protocol，中文叫边界网关协议。听起来很高大上，但其实你可以把它理解成互联网上的"导航系统"。想象一下，你要从北京开车去上海，你需要知道走哪条路对吧？可能有好几条路线可以选择：走京沪高速、走国道、或者绕道其他城市。BGP就是帮助网络数据包选择最优路径的协议。我记得有一次，我们公司的网络出现了奇怪的问题：访问某个网站特别慢，但是ping延迟看起来还行。后来排查发现，是BGP路由出了问题，数据包绕了一大圈才到达目标服务器。这就像你本来可以直接从北京到上海，结果导航让你先去了趟新疆，然后再回到上海，能不慢吗？BGP是一个路径向量协议，它不像OSPF或者RIP那样只关心距离，而是关心整个路径。每个BGP路由器都会维护一张路由表，记录着到达各个网络的最佳路径。BGP的工作原理BGP的工作机制其实挺有意思的。它基于TCP连接，端口号是179。BGP路由器之间会建立邻居关系，然后互相交换路由信息。AS（自治系统）的概念在讲BGP之前，必须先理解AS的概念。AS就是Autonomous System，自治系统。你可以把它理解成互联网上的一个"国家"或者"省份"。每个AS都有自己的编号，比如中国电信的AS号是4134，中国联通是4837。我之前在一家IDC公司工作的时候，我们申请了自己的AS号。申请AS号的过程还挺复杂的，需要证明你有足够的网络资源和技术能力来管理一个自治系统。BGP的分类BGP分为两种：eBGP（External BGP）：不同AS之间的BGPiBGP（Internal BGP）：同一个AS内部的BGPeBGP就像国际贸易，不同国家之间的商品流通；iBGP就像国内贸易，一个国家内部的商品流通。路由选择过程BGP选择路由的时候有一套复杂的算法，主要考虑这些因素：Weight（权重）：这是思科设备特有的属性，数值越大越优先Local Preference（本地优先级）：在AS内部使用，数值越大越优先AS Path长度：经过的AS数量越少越好Origin类型：IGP > EGP > IncompleteMED值：Multi-Exit Discriminator，数值越小越优先我记得有一次配置BGP的时候，就是因为没有正确设置Local Preference，导致流量全部走了备用链路，主链路闲置。当时还被老板批评了一顿，说浪费带宽资源。BGP在互联网中的作用1. 实现全球互联BGP最重要的作用就是让全世界的网络能够互相连通。想想看，全球有几万个AS，如果没有一个统一的协议来协调路由，互联网根本无法正常工作。比如说，当你在中国访问美国的一个网站时，数据包可能需要经过十几个不同的AS。每个AS都需要知道如何将数据包转发到下一跳，这就需要BGP来协调。2. 负载均衡和冗余BGP还能实现负载均衡。比如一个大型网站可能在多个地方都有服务器，BGP可以根据网络状况将用户的请求分配到最合适的服务器上。我们公司之前做过一个项目，客户要求在北京、上海、广州三地都部署服务器，然后通过BGP实现智能调度。用户访问的时候，会自动选择最近的服务器，大大提升了用户体验。3. 故障恢复当网络出现故障时，BGP能够快速重新计算路由，选择备用路径。虽然BGP的收敛速度不是最快的（通常需要几分钟），但它的稳定性很好。有一次，某个运营商的主干网络出现故障，我们监控到BGP路由表发生了大量变化。虽然用户感觉到了一些延迟，但网络服务并没有完全中断，这就是BGP的功劳。BGP的实际应用场景多线BGP机房现在很多IDC都提供多线BGP服务。什么意思呢？就是一台服务器可以同时接入电信、联通、移动等多个运营商的网络，用户无论使用哪个运营商的网络，都能获得比较好的访问速度。我记得刚开始接触多线BGP的时候，觉得这个技术特别神奇。一个IP地址，怎么能在不同的运营商网络中都能正常访问呢？后来才明白，这就是BGP路由宣告的作用。配置多线BGP的时候，需要向每个运营商宣告相同的IP段，但是可以通过调整AS Path、MED等属性来控制流量走向。比如：# 向电信宣告路由 router bgp 65001 neighbor 202.96.1.1 remote-as 4134 network 1.2.3.0 mask 255.255.255.…

生产环境救急指南：Linux服务器忘记密码的N种解法，覆盖主流系统和云厂商
type: item
date: 2026-05-06T15:06:34+00:00
description: 说实话，做运维这么多年，密码忘记这事儿我自己都干过好几次，更别提帮别人处理的次数了，有时候是接手别人的机器没交接清楚，有时候是自己太久没登录给忘了，还有的时候是同事离职了账号没留……反正各种奇葩情况都遇到过。今天就把这些年的经验整理一下，把主流Linux发行版和云厂商的密码重置方法都捋一遍，希望能帮到有需要的朋友。为啥密码会忘？先聊聊原因吧，这个其实挺重要的，知道原因才能避免下次再犯。我见过最多的几种情况：一是服务器太多，密码策略又要求定期更换，换着换着就乱了；二是接手别人的机器，文档没留或者留错了；三是测试环境随手设了个简单密码，结果上线后找不到记录；还有一种就是纯碎自己脑子短路，设了个自认为能记住的密码，过两天就忘得一干二净。不管啥原因吧，反正密码忘了就是忘了，得想办法解决。传统物理服务器/虚拟机的密码重置2.1 Ubuntu/Debian 系列的单用户模式这个方法算是比较经典的了，适用于你能接触到服务器控制台的情况，不管是物理机的显示器键盘，还是虚拟机的VNC控制台。操作步骤其实不难，但不同版本有点小区别。Ubuntu 20.04/22.04/24.04 的操作：重启机器，在GRUB启动界面出现的时候，赶紧按住Shift键（有些机器是ESC），让GRUB菜单显示出来。选中第一项，也就是默认的启动项，按键盘上的e键进入编辑模式。在编辑界面里往下找，找到以linux开头的那一行，挺长的，一般在中间位置。找到ro quiet splash这部分，把ro改成rw，然后在后面加上init=/bin/bash。改完之后按Ctrl + X或者F10启动，这时候系统会进入一个简单的shell环境，不需要密码。然后就是重置密码了：passwd root # 输入新密码两次如果系统提示Authentication token manipulation error，可能是文件系统只读挂载了，先执行：mount -o remount,rw / passwd root密码改完之后，重启系统：exec /sbin/init # 或者直接强制重启 reboot -fDebian 12/13 的操作：Debian的操作跟Ubuntu差不多，但Debian 13稍微有点变化，它的GRUB配置界面可能不太一样。同样是重启进GRUB，按e编辑，找到linux那行，在行尾加上init=/bin/bash，然后按Ctrl+X启动。进入shell后：# 先确保根目录可写 mount -o remount,rw / # 修改密码 passwd root # 重启 exec /sbin/init这里有个坑，Debian 13有时候默认不安装/sbin/init这个路径，如果执行报错，可以试试：/sbin/reboot -f或者直接在虚拟化管理界面强制重启。2.2 RedHat/CentOS/Rocky/Alma 系列的救援模式红帽系的系统操作稍微不太一样，它有个专门的救援模式可以用。CentOS 7/8/9、Rocky Linux 8/9、AlmaLinux 8/9 的操作：重启系统，在GRUB菜单选择内核的时候，按e进入编辑模式。找到以linux16或linux开头的那一行，在末尾加上rd.break。按Ctrl+X启动，系统会进入紧急模式。这时候系统会挂载/sysroot目录，但是只读的，要先重新挂载：mount -o remount,rw /sysroot然后切换到chroot环境：chroot /sysroot修改密码：passwd root这里有个重要步骤，SELinux的标签要重置，否则重启后可能登录不了：touch /.autorelabel最后退出并重启：exit exit系统会自动重启，第一次启动可能会花点时间重置SELinux标签，耐心等待就好。RHEL 8/9 的操作：RHEL的操作跟CentOS基本一致，但如果你有订阅账号，还可以用官方的救援镜像。通过订阅管理的Red Hat Customer Portal可以下载救援ISO，挂载启动后选择Rescue installed system，它会自动找到你的系统并挂载，然后你chroot进去改密码就行。不过说实话，大多数情况下用GRUB编辑的方法就够了，不需要专门去搞救援镜像。2.3 使用Live CD/USB重置密码如果单用户模式进不去，或者GRUB被加密了，可以用Live CD/USB启动系统，然后手动挂载磁盘改密码。找个同架构的Linux Live CD，比如Ubuntu Live或者SystemRescueCd，启动后进入shell。查看磁盘分区：fdisk -l # 或者 lsblk找到原来的根分区，挂载：mkdir /mnt/root mount /dev/sda2 /mnt/root …

一口气搞懂 MySQL MVCC：从隐藏字段到生产“背刺”的那些坑
type: item
date: 2026-05-05T14:57:51+00:00
description: 我直接开干，不啰嗦背景，不讲 ACID 那些教科书话，咱就盯着一个点聊：MySQL 里的 MVCC 到底是个啥，底层咋实现，生产环境里它怎么背刺过我。整篇文章会有点长，我尽量用“人话”说清楚，顺手把我踩过的几个大坑拎出来，很多问题，说白了都是对 MVCC 理解不透彻导致的。MVCC 在 MySQL 里到底干啥用的先说结论：在 InnoDB 里，MVCC 负责普通 SELECT（快照读）的“读什么版本的数据”这个问题，目标只有一个：在读多写多的场景里，让“读”和“写”别老互相上锁卡着。感受一下下面两个场景：有个长事务在跑各种 SELECT，另一个事务在疯狂 UPDATE、你希望：写的事务可以顺利提交读的事务能看到一个自洽的历史画面，别一会儿多一条记录、一会儿少一条，自己都说不清刚才看到啥MVCC 就是干这个的。它不负责锁，锁是行锁、间隙锁那一挂的事；它只负责一个问题：给你一个“看起来稳定”的世界观。这个“世界观”怎么构出来的？靠三样东西：行记录上的隐藏字段undo 日志（回滚日志）Read View（读视图）下面一个个拆。InnoDB 行记录里那些你看不到的字段你在建表的时候写的字段是这样的：CREATE TABLE user ( id BIGINT PRIMARY KEY, name VARCHAR(50), balance INT ) ENGINE=InnoDB;但 InnoDB 真正存的时候，每一行后面还悄悄藏了几个字段（简化说法）：DB_TRX_ID：最近一次修改这行的事务 IDDB_ROLL_PTR：指向 undo 日志的指针（上一版本在哪里）DB_ROW_ID：如果你没定义主键，InnoDB 自己搞一个自增的隐藏 row id你可以把一行记录想象成这样一坨：(id, name, balance, DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID)重点是前两个：DB_TRX_ID 告诉你：这个版本是谁改的DB_ROLL_PTR 告诉你：上一版本在哪有这俩字段，才有可能把“多个版本”串成一条链。undo 日志：版本链是怎么长出来的很多人一听 undo，脑子里第一反应是“回滚用的”。其实在 InnoDB 里，MVCC 用的历史版本，全部躺在 undo 里。undo 大致分两种：insert undo：插入新行，用来回滚；事务提交后基本就没用了，很快就清理掉update undo：更新/删除用的，既服务回滚，也服务 MVCC 的历史版本你可以这么理解一个 UPDATE 的过程：UPDATE user SET balance = balance + 100 WHERE id = 1;InnoDB 干的事大致是：把当前行拷贝一份写到 undo 里，记上当前旧值、旧 DB_TRX_ID 等把主记录上的 balance 改成新值把主记录上的 DB_TRX_ID 改成当前事务 ID把主记录上的 DB_ROLL_PTR 指向刚刚那条 undo 记录这样一来，这条记录在物理上变成一条版本链：当前记录（最新版本） | v undo 版本（上一个） | v 更老的 undo 版本 | v ...普通 SELECT（快照读）其实读的不是“最新记录”，而是顺着这条链往后翻，翻到一个“按规则可见”的版本。这也是为啥：undo 清不掉，表数据明明不大，undo 表空间狂涨；一个表被频繁更新，又有长事务，版本链会越来越长，某个 select 会莫名其妙变慢。这两件事我后面单独说坑。Read View：这个事务眼里世界长啥样版本链有了，还少个东西：啥叫“对我可见”？InnoDB 的做法是：每次做快照读的时候，生成一个 Read View（读视图）。你可以粗暴理解成这个事务眼里的“活着的事务列表 + 水位线”。Read View 里主要关心这么几项（简化说法）：m_ids：生成视图时，系统里所有活跃（未提交）的事务 ID 列表min_trx_id：上面这个列表里最小的事务 ID（低水位）max_trx_id：当时系统分配给下一个事务的 ID（高水位）creator_trx_id：当前这个事务自己的 ID然后，InnoDB 就用这几个东西去判断一条记录的某个版本能不能被你看见。判断规则粗暴版（记住这个就够用了）：假设某个版本的 DB_TRX_ID = X，当前事务的 Read View 里有 min_trx_id, max_trx_id, m_ids, creator_trx_id：如果 X == creator_trx_id：这是你自己改出来的，必须能看到如果 X < min_trx_id：太老了，在你视图生成之前就已经提交了，可以看如果 X >= max_trx_id：太新了，在你视图之后才开始的事务写的，看不到如果 min_trx_i…

一条 SQL 从敲下回车，到在 MySQL 里“跑完一生”，中间到底经历了啥？
type: item
date: 2026-05-04T14:27:47+00:00
description: 很多人写 SQL 写了好几年，EXPLAIN 也看过，慢查询也调过，但真要问一句：“我在客户端敲下 SELECT ... 回车后，MySQL 里面具体一步一步干了啥？”大概率只能说个连接 → 解析 → 优化 → 执行这种级别的回答。看个面试还能糊弄一下，上了生产就完全不够用了。我这篇就干一件事：以一条 SQL 为线，把它从网卡一直送到磁盘，路上遇到的每一个“角色”，具体做了什么、怎么做、有什么坑，都掰开揉碎说清楚。不讲历史故事，不聊产品背景，纯过程、纯细节、纯生产视角。为了方便，你脑子里先立个场景：SELECT id, name, age FROM user WHERE phone = '13800001234' AND status = 1;user 表用 InnoDB，引擎里有这些东西：PRIMARY KEY (id)二级索引 idx_phone (phone)status 上没有单独索引下面就按它的真实“生命轨迹”往下走。从客户端到 MySQL：连接那一瞬间其实已经有坑了SQL 想进 MySQL，先得有连接，这个在 Server 层里是连接器干的事。1.1 TCP 建链 + 握手不管你用的是 Navicat、JDBC、Go 客户端还是命令行，底层都是 TCP 连接。典型流程：客户端发起 TCP 三次握手MySQL 这边的 mysqld 进程接收连接，把它交给一个线程（或者线程池里的线程）连接建立后，进入 MySQL 的认证阶段1.2 身份认证 + 权限快照握手完成，MySQL 要做两件事：校验账号密码（mysql.user 等系统表）把当前用户的权限读出来，在这个连接上缓存一份快照这一步非常关键，有两个细节很多人没意识到：之后你所有执行的 SQL 权限判断，都是基于这份快照即使 DBA 后面改了你这个账号的权限，只要连接不断开，你的权限依然按老的来生产上见过好几次：DBA 改了权限说“已经禁止这个账号 DROP 了”，结果业务连接一直没断，照样能删表，删完大家面面相觑。1.3 连接数和连接复用这又跟 SQL 执行没那么直接，但会决定你能不能发出那条 SQL。max_connections 控制最大连接数连接池（比如 Druid、HikariCP）会复用连接，不会每次都重新握手连接器这块看似简单，其实夯住了整个 SQL 生命周期的入口：有连接，才有后面的故事。SQL 到达 Server 层：老版本还有查询缓存这一关连接建立后，客户端把 SQL 文本发给 MySQL。这时候 SQL 命令进了 Server 层，没开头说的那些架构图，我们直接走流程。说明一下：MySQL 8.0 之后已经没查询缓存了，如果你还在用 5.6/5.7，可以顺便带着看这个坑。2.1 查询缓存的“鸡肋人生”（老版本）在老版本里（5.6/5.7 部分版本没关）：SELECT 语句进来后，Server 层会先用 SQL 文本（+当前数据库、某些环境）作为 key，到查询缓存里查一把。命中：直接把结果集从内存拿出来返回给客户端没命中：SQL 继续往下走听起来很美好，但有两个现实问题：命中率极低生产里 SQL 条件各种拼，哪怕一个空格不一样、一个 LIMIT 不一样，都算完全不同的 key。失效极其粗暴某个表被 INSERT/UPDATE/DELETE 一下，这个表相关的缓存统统失效，维护成本巨大。所以真实情况就是：几乎所有 SQL 都得进缓存查一圈，然后发现没有，用了一堆 CPU 做无用功。这也是为啥 8.0 干脆直接把它删了。2.2 没缓存，那就老老实实走解析器我们的这条 SQL，在现在主流的 8.0 里，就不会走查询缓存，直接丢给解析器。解析器：SQL 文本被拆成“语法树”的全过程很多人以为解析就是“看下有没有语法错误”。MySQL 这里做得比你想象复杂不少。3.1 词法分析：从字符到 Token从左到右，一个字符一个字符扫过去，按规则切成一个个 Token：关键字：SELECT、FROM、WHERE…标识符：user、id、name运算符：=、AND常量：'13800001234'、1这一步就能抓出一部分低级错误，比如你写了个 SELEC id FROM user;，SELEC 不认，就直接在这一步报错了。3.2 语法分析：给你拼成一棵树Token 流出来之后，解析器会根据 MySQL 的语法规则，去组装一个语法树（Parse Tree / AST）。形象点说，你刚才那条 SQL，在解析器眼里，差不多就变成这样一种树状结构（概念上）：SELECTfields: id, name, ageFROMtable: userWHEREAND= (phone, '13800001234')= (status, 1)这一步会检查语法级别的错误，比如：SELECT …

Jenkins 对接 Docker 做 CI/CD，我是怎么一步步跑通的，踩坑也一起说清楚
type: item
date: 2026-05-02T16:25:42+00:00
description: 很多人一提到 CI/CD，就容易说得很玄，什么自动化交付、持续集成、持续部署，词都没毛病，但真到机器上落地，往往不是那么回事。页面点半天、插件装一堆、脚本复制来复制去，最后构建一跑就红，控制台一片报错，最常见的还是 Jenkins 找不到 Docker，或者镜像打出来了却推不上仓库。我自己在项目里折腾 Jenkins 和 Docker 集成，这种场景不算少。小一点的项目，目标很简单，代码提交后自动拉代码、自动构建、自动打镜像、自动推仓库、自动部署。听起来很顺，但中间每一步都可能掉坑。今天就把这个过程掰开了讲，不讲虚的，就按实际怎么做、怎么排错、怎么让它稳定跑起来来写。这篇内容不绕弯，直接从环境开始说。一、Jenkins 和 Docker 到底是怎么配合的这个事如果一句话说透，其实就是：Jenkins 负责“调度流程”，Docker 负责“打包和运行环境”。Jenkins 像个流程总控，代码一变更，它就去拉代码、执行构建命令、做测试、生成镜像、推送镜像、再触发部署。Docker 呢，负责把应用和运行环境一起封进镜像里，部署时不用担心“你机器能跑，我机器不能跑”。很多团队刚开始时，是 Jenkins 直接在服务器上执行 jar 包替换、重启进程。这种方式不是不能用，但时间一长就乱。依赖版本、运行参数、服务启动方式，全靠人工记忆，机器一多就更难控。后面引入 Docker 以后，镜像就成了交付标准件，部署会顺很多。说白了，Jenkins 是干活的，Docker 是装货的，镜像仓库就是中转站。二、先把基础环境弄对，不然后面都是白搭我这里以 Linux 服务器为例，比较常见，很多公司的 Jenkins 也是跑在 Linux 上。1）安装 Docker先确认 Docker 正常安装并能启动。docker --version systemctl status docker如果没装，CentOS 系可以这样：yum install -y yum-utils yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo yum install -y docker-ce docker-ce-cli containerd.io systemctl enable docker systemctl start dockerUbuntu 就换成 apt，思路差不多。装完别急着走，先跑个 hello-world 试一下：docker run hello-world能正常输出说明 Docker 基本没问题。2）安装 JenkinsJenkins 安装方式很多，war 包、yum、docker 跑都可以。我平时更偏向用宿主机安装，图个直观，排查问题也方便。比如在 CentOS 上：yum install -y java-11-openjdk yum install -y wget wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io-2023.key yum install -y jenkins systemctl enable jenkins systemctl start jenkins确认端口监听：ss -lntp | grep 8080首次登录要输初始化密码：cat /var/lib/jenkins/secrets/initialAdminPasswordJenkins 能打开只是第一步，很多人到这就以为差不多了，其实真正的坑从这里才开始。三、Jenkins 想调用 Docker，权限必须打通这个问题太常见了，几乎可以说是“新手必踩”。Jenkins 服务一般是 jenkins 用户在跑，而 Docker 命令默认只有 root 或 docker 组用户能执行。所以你在终端里手工敲 docker ps 没问题，不代表 Jenkins 任务里也能用。先看 Jenkins 是哪个用户：ps -ef | grep jenkins通常就是 jenkins。把它加入 docker 组：usermod -aG docker jenkins然后重启服务：systemctl restart docker systemctl restart jenkins这一步很多人做了还是不生效，我也碰到过。原因也不复杂，服务进程没重新拿到组权限，或者压根 Jenkins 不是用这个用户启动的。最直接的办法，是在 Jenkins 里新建一…

面试必问！MySQL 事务到底是怎么实现的？这篇文章讲透了
type: item
date: 2026-05-01T14:36:48+00:00
description: 说实话，这个问题我被问过不止一次。每次有人来问我 MySQL 事务是怎么回事，我都发现大家普遍停留在「ACID 四个特性」这个层面，背得挺溜，但真要问你 MySQL 底层是怎么实现原子性的，怎么保证崩了数据不丢，怎么做到多个事务并发跑还互不干扰——很多人就开始含糊了。这篇文章我就把这块彻底说清楚。不搞那些花里胡哨的，直接从底层机制讲起，生产上遇到过的坑也会顺带提一嘴。先说说事务是什么事务这个概念说白了就是：把一组操作捆绑成一个整体，要么全部成功，要么全部失败，不允许中间状态存在。举个最经典的例子，转账。A 给 B 转 500 块，数据库层面是两步操作：A 的账户减 500，B 的账户加 500。这两步必须同时成功或者同时失败，不然 A 扣了钱 B 没收到，或者 B 收到了 A 没扣，这都是灾难性的数据错误。这就是事务要解决的核心问题。MySQL 里事务主要是 InnoDB 引擎实现的，MyISAM 不支持事务，这个要先知道。ACID 大家都背过：原子性（Atomicity）、一致性（Consistency）、隔离性（Isolation）、持久性（Durability）。但背概念没用，我们要知道 MySQL 是用什么技术手段实现这四个特性的。原子性 → 靠 undo log持久性 → 靠 redo log隔离性 → 靠 锁 + MVCC一致性 → 是上面三个共同作用的结果下面一个一个展开说。Undo Log：原子性的保障undo log 翻译过来叫回滚日志。它的核心思想很简单：在你修改数据之前，先把原来的数据记下来，万一事务失败了，就拿这个日志把数据恢复回去。你执行了一条 INSERT，undo log 里就记一条 DELETE；你执行了 UPDATE，undo log 里就记一条把数据改回去的 UPDATE；你执行了 DELETE，undo log 里就记一条 INSERT。这样，当事务需要回滚的时候，MySQL 就把 undo log 里的操作反向执行一遍，数据就回到了事务开始之前的状态。有一点要注意：undo log 一定是优先于数据修改落盘的，这个顺序不能乱。如果数据先改了，undo log 还没写，这时候崩了，你连回滚的依据都没有了。实际上 undo log 不只是用来回滚，它还承担着 MVCC 的职责，后面会说到。Redo Log：持久性的保障redo log 这块是我觉得 MySQL 设计里最精妙的地方之一。先说问题背景。MySQL 的数据最终是存在磁盘上的，但读写操作都是在内存里的 Buffer Pool 里进行的，不是每次改完数据都立刻写磁盘。这样做是为了性能，磁盘随机 IO 太慢了。但这就带来了一个风险：数据在内存里改了，还没来得及刷到磁盘，MySQL 突然崩了，数据就丢了。怎么解决？redo log 就是答案。redo log 记录的是数据页的物理修改，每次事务提交的时候，不需要立刻把数据页刷到磁盘，但必须先把 redo log 写到磁盘。redo log 是顺序写的，顺序写磁盘的速度比随机写快很多，这个性能差距在机械硬盘时代尤其明显。这个机制有个专业名字叫 WAL（Write-Ahead Logging），意思就是先写日志再写数据。MySQL 崩溃重启之后，会扫描 redo log，把已经提交但还没来得及刷盘的数据重新应用一遍，数据就恢复了。这就是为什么事务一旦提交，就算服务器崩了，数据也不会丢。redo log 有个重要的参数：innodb_flush_log_at_trx_commit，这个参数控制 redo log 的刷盘策略：设置为 1：每次事务提交都强制刷盘，最安全，但性能最差设置为 2：提交时写到操作系统的缓存，每秒刷一次盘，折中方案设置为 0：每秒刷一次盘，性能最好，但崩溃可能丢 1 秒数据生产环境一般金融类业务设置 1，对数据安全性要求没那么高的业务可以设置 2。设置 0 风险比较大，不建议。-- 查看当前配置 SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'; -- 查看 redo log 相关配置 SHOW VARIABLES LIKE 'innodb_log%';MVCC：隔离性的核心机制这块是整个事务机制里最复杂的，也是面试最爱考的。MVCC 全称 Multi-Version Concurrency Control，多版本并发控制。它解决的核心问题是：怎么让读操作不加锁，同时还能保证数据的隔离性？传统的做法是读写都加锁，读的时候其他人不能写，写的时候其他人不能读，这样数据是安全了，但并发性能很差。MVCC 的思路是给数据维护多个版本，不同的事务看到不同版本的数据，读操作基本不需要加锁。InnoDB 是怎么实现多版本的？Inno…