Python 打包到底怎么选？我把 6 种主流方式全跑了一遍，优缺点一次说透
最近在给团队做内部工具的时候，又被 Python 打包这事折腾了一遍。说真的，这玩意看起来简单，真要让你选个合适的方案，没个三五天根本摸不清门道。我团队那帮人，java、go、PHP 的都有，每次看我搞 Python 部署就笑话我。"你这啥玩意儿？打个包搞一下午？" 你还别说，真就一下午，还不一定能搞利索。所以这次我决定把市面上主流的 Python 打包方式都跑一遍，每个都写写感受，说说坑，给大家一个真实参考。写之前我没看任何官方文档，全凭自己动手踩坑，这样写出来的东西才有烟火气。先说结论：没有银弹，只有适合。不同场景用不同方案，强行用错工具只会让自己加班。那些年，我被 Python 打包支配的恐惧先聊聊为啥要写这篇文章。事情是这样的，上个月接了个活，要把公司一个数据处理脚本做成可执行文件，给运营同事用。他们电脑是 windows，没装 Python，也不会装。我当时心想，pyinstaller 一把梭呗，结果搞了一下午。为啥？因为这脚本依赖了 pandas、numpy、requests，还有一堆奇奇怪怪的库。打包出来 300 多 M，运营同事传个文件发邮件都不方便。更要命的是，第一次在他电脑上跑，直接报错说缺什么 dll，我人都麻了。后来换了几种方式，发现每种都有它自己的脾气。今天就把我试过的几种全讲讲。不过别期待太高，Python 打包这个领域，本身就是"能用"和"好用"之间的巨大鸿沟。你要是想找完美方案，那真的没有。PyInstaller：这个老朋友，我又爱又恨先说 PyInstaller，估计 90% 的 Python 开发者都用过它。用起来真的很简单。一行命令搞定：pyinstaller -F your_script.py-F 是单文件模式，打包出来就一个 exe，用户拿着就能跑。对开发者友好度这块，PyInstaller 真的没得说。我第一次用的时候，感觉这玩意简直是神器。我写了个小工具，打包成 exe 发给同事，他双击就能用，那种成就感，懂的人都懂。但坑也多。最典型的就是体积问题。我那个数据处理脚本，打包出来 280M。同事问我："你这啥程序啊，怎么比英雄联盟客户端还大？" 我无言以对。为啥这么大？因为 PyInstaller 是把整个 Python 解释器和所有依赖库都塞进去了。pandas、numpy 这些库本身就大，再加上 PyInstaller 的运行时，动辄上百 M 起步。还有更恶心的兼容性问题。我那次在 Windows 10 打包的 exe，拿到 Windows 7 上跑不起来。报什么 "api-ms-win-crt" 错误，查了半天是缺 VC++ 运行库。还有在某些精简版系统上，连 tkinter 都跑不起来，要手动指定 hidden import。pyinstaller --hidden-import=tkinter your_script.py这种坑，第一次遇到真的能让人崩溃。不过话说回来，PyInstaller 还是我最常用的。因为它跨平台，Windows、Linux、macOS 都能打，社区也活跃，遇到问题基本搜得到。对于一些内部小工具，不在乎体积的情况下，PyInstaller 仍然是首选。有一次我做个小爬虫工具，就一个 py 文件，依赖只有 requests 和 beautifulsoup4，打包出来才 30M，体验就很好。所以体积这事，看你依赖什么库。纯 Python 库小，带 numpy、pandas 这些科学计算库就炸。cx_Freeze：跨平台的另一种选择cx_Freeze 这玩意，我接触得比 PyInstaller 少，但也有故事。当时做项目要在 Linux 服务器上跑一个 Python 脚本，但服务器是个精简版系统，连 pip 都没有。我想着搞个绿色版，直接拷过去就能用。cx_Freeze 生成的目录结构比较合理，library 和可执行文件分开。看着比 PyInstaller 整洁一些。但这玩意用起来比 PyInstaller 麻烦。要写 setup.py：from cx_Freeze import setup, Executable setup( name="myapp", executables=[Executable("your_script.py")] )然后 python setup.py build，出一堆文件。配置项多，新手容易懵。优点是跨平台做得不错，生成的结构清晰，库和可执行文件分离，对于一些需要定制部署的场景比较友好。缺点就是配置复杂，文档也不如 PyInstaller 详细。我那次配置参数搞了半天，最后还是放弃了，转头用了 PyInstaller。要是你项目有特殊需求，比如要打进一些自定义的 .so 文件或者 dll，cx_Freeze 的配置反而更灵活。但对普通用户来说，没那必要折腾。Nuitka：把 Python 编译成 C，性能起飞这个是我最近才认真试的，用完直接说一句：真香。Nuitka 的思路不一样，它不是把 Python 解释器和代码一起打包，而是把 Python 代码编译成 C 代码，然后再编译成机器码。对，你没看错，是真的编译，不是那种伪编译。第一次跑的时候，我在 Linux 上测试一个数据处理脚本，编译完跑起来，速度直接快了一倍。我以为是我眼花了，又跑了几次，确实快了不少。nuitka --standalone --onefile your_script.py参数和 PyInstaller 类似，但生成的二进制文件运行效率高很多。体积方面也很惊艳。同样的脚本，PyInstaller 打包 280M，Nuitka 编译出来 80M。差距不是一点半点。但！坑也来了。Nuitka 的编译速度是真的慢。我那个 300 行的脚本，编译花了 4 分钟。PyInstaller 几秒钟搞定。Nuitka 几分钟后才有结果。还有就是兼容性。Nuitka 对一些冷门库的支持不如 PyInstaller 好。我有个脚本用了 pyexecjs，Nuitka 怎么都搞不定，最后还是退回 PyInstaller。所以我的建议是：如果你的项目对性能有要求，Nuitka 真的值得一试。但要是项目复杂、依赖多，PyInstaller 还是更稳。Nuitka 还有个加分项，它支持 Python 3.6 以上的所有版本，而且对一些 C 扩展库的支持做得不错，至少 numpy、pandas 这种常见的没问题。但我得提醒一句，Nuitka 安装本身就是个坑。在 Linux 上还好，pip 直接装就行。在 Windows 上，你要先装 C 编译器，Visual Studio Build Tools，几 G 的东西。不像 PyInstaller 那样 pip install 就能用。源码包与 wheel 包：最 Pythonic 的方式说完编译型的，再说说 Python 圈子最传统的打包方式——源码包和 wheel 包。这个就是用 setuptools 那一套，写个 setup.py 或者 pyproject.toml，然后 python setup.py sdist bdist_wheel。这玩意严格说不算"打包成可执行文件"，但它在 Python 生态里太重要了，不得不提。python setup.py sdist # 源码包 python setup.py bdist_wheel # wheel 包wheel 包的好处，装起来快。因为是预编译的格式，pip 安装 wheel 包的时候不需要本地编译，直接解压就行。我之前在树莓派上装过一个库，等编译等了 20 分钟。换 wheel 包，几秒钟。这种体验差异真的巨大。wheel 包还有个隐藏好处：可以指定平台和 Python 版本。xxx-cp39-cp39-linux_x86_64.whl，这名字一看就知道是哪个平台哪个版本用的。源码包（sdist）则更通用，里面就是 Python 源代码和 setup.py。pip 装了之后会自动编译，但有些纯 Python 包就直接拷贝了。这两种方式适合的场景：你要发布的是库，而不是应用。用户拿到你的包后还会 import 使用，而不是直接运行。我自己的项目，如果是写库给别人用，就用这种方式。如果是写工具给自己或者非技术人员用，就用 PyInstaller 或者 Nuitka 编译成可执行文件。Docker：容器化才是终极方案？说完传统的打包方式，再聊聊现在云原生时代的玩法——Docker。严格说 Docker 不是 Python 打包工具，但用 Docker 部署 Python 应用真的太香了，我现在新项目基本都是这套。Dockerfile 写起来简单：FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "your_script.py"]这一套下来，生成的镜像扔到任何装了 Docker 的机器上都能跑。不用管宿主机的 Python 版本，不用管系统库依赖，不用管这个那个环境变量。优点太多了：环境隔离得彻底。每个容器都是独立的 Python 环境，互不干扰。部署一致性。开发环境测试过了，生产环境肯定一样。这点对运维来说简直是救命的。资源利用高效。一个服务器可以跑几十上百个容器，密度比虚拟机高多了。但也有代价：镜像体积不小。最小的 python:3.9-slim 镜像也有几十 M，加点依赖就上百 M。不过相比 PyInstaller 的 280M，还是能接受的。需要懂 Docker。不是说门槛多高，但要是团队没人用过 Docker，学起来还是要点时间。启动速度比直接跑可执行文件慢。不过这点在生产环境一般不是问题。我个人现在的做法：所有线上服务都走 Docker，本地开发也尽量用 Docker。桌面端的小工具才用 PyInstaller 这种。Docker 还有一个隐藏的好处，可以多阶段构建。编译阶段用一个完整镜像，运行阶段用一个精简镜像，镜像体积能小很多。比如：FROM python:3.9 AS builder WORKDIR /app COPY requirements.txt . RUN pip install --user -r requirements.txt FROM python:3.9-slim WORKDIR /app COPY --from=builder /root/.local /root/.local COPY . . CMD ["python", "your_script.py"]这样最终的镜像能小一半不止。PyOxidizer：Rust 写的打包工具，性能怪兽这个是最近挖到的宝，PyOxidizer。它是用 Rust 写的，思路和 Nuitka 类似，但更激进——它把 Python 解释器嵌入到一个 Rust 二进制里。听起来很硬核是吧？用起来其实没那么复杂：# pyoxidizer.bzl python_run_file path = "your_script.py"然后 pyoxidizer build，搞定。这个工具的亮点：启动速度比 PyInstaller 快得多。Nuitka 我已经觉得很厉害了，PyOxidizer 比它还快。我做了个简单测试，启动一个简单的 Flask 应用：PyInstaller 打包：启动 2.3 秒Nuitka 编译：启动 1.1 秒PyOxidizer：启动 0.4 秒这差距真的不是一点半点。而且它的单文件模式更彻底。PyInstaller 的 -F 模式本质上还是先解压到临时目录再运行，PyOxidizer 是真的把解释器和代码都嵌入到一个可执行文件里，没有解压过程。但缺点也很明显：首先，这玩意还比较新，文档不算完善，很多功能要翻 GitHub Issues 才能搞清楚。其次，对 Windows 的支持还在完善中。我在 Windows 上用踩了几个坑，最后还是退回 Linux 环境。还有就是，配置用的是 Starlark 语法（一种 Python 风格的配置语言），和 Python 本身有点区别，新手要适应一下。我的评价是：PyOxidizer 是打包工具的未来，但现在还不够成熟。要是你项目允许用 Linux，对启动速度有极致要求，可以试试。实战案例：我是怎么给运营做工具的光说理论太空了，给大家讲讲我最近做的真实项目。需求是这样的：运营部门每周要处理一批 Excel 文件，提取数据生成报表。原来是人工处理，一人一天。现在让我搞个自动化工具。第一个版本，我用 PyInstaller：写了个 Python 脚本，用 pandas 处理 Excel，openpyxl 写报表，tkinter 做了个简单界面。PyInstaller 打包出来 280M，运营同事收到邮件当天就吐槽："这啥玩意这么大？"第二个版本，我换了 Nuitka：编译出来 80M，运行速度还快了不少。运营同事勉强接受了。但有个问题，他们公司电脑有些是老款 Windows 7，Nuitka 编出来的二进制文件跑不起来，要装 VC++ 运行库，IT 部门不太愿意装。第三个版本，我妥协了，用 Docker：在内部服务器上起了个 Web 服务，运营同事用浏览器访问。镜像大小 200M 左右，但在服务器上运行，稳定得很。运营同事也不用装任何东西，打开浏览器就能用。最后选了 Docker 方案。不是因为它最好，而是因为最适合运营同事的使用场景。这个案例给我的最大启发：打包方式的选择，不是看技术多牛，而是看用户场景。给技术人员用和给非技术人员用，完全是两个思路。选哪个？给你个参考说了这么多，到底选哪个？我列个表给你参考：场景推荐方案理由内部小工具，分发给同事PyInstaller简单，跨平台对性能有要求Nuitka编译型，启动快桌面应用，要给非技术人员PyInstaller 或 Web 化学习成本低发布 Python 库setuptools + wheel生态标配线上服务Docker部署一致性云原生环境Docker标准化极致启动速度PyOxidizer启动飞快我个人的优先级：Docker > Nuitka > PyInstaller > 源码包 > PyOxidizer日常开发基本都 Docker，性能要求高的脚本用 Nuitka，分发小工具才用 PyInstaller。这些坑你别再踩了最后说几个常见的坑，新手一定要避开：第一，不要在打包环境里瞎装东西。我见过有人在打包机器上装了一堆开发工具，结果打出来的包巨大，依赖混乱。最好用干净的虚拟环境打包，pip install 完依赖就打包，别多装。第二，注意隐藏依赖。有些库是动态导入的，PyInstaller 静态分析找不到，要在 spec 文件里手动指定 hidden import。这事不复杂，但很多人栽在这里。第三，路径问题。打包后的程序，__file__ 这个变量的行为会变。在 PyInstaller 单文件模式下，__file__ 指向的是临时解压目录，不是你的 exe 所在目录。要用 sys._MEIPASS 获取真实路径。第四，不要忽略杀毒软件。PyInstaller 打包的 exe 经常被杀软误报，尤其是国内各种管家。我之前有个工具，运营同事的电脑装了 360，直接给拦截了，还给我归到"高危"。这种情况要么加白名单，要么换签名，要么用 Web 方案。第五，不要忽视更新机制。PyInstaller 打包的 exe 没法自动更新，要重新打包让用户下载。Docker 容器可以拉取最新镜像，Web 应用根本不用管。这点对工具的长期维护影响很大。写在最后Python 打包这事，说简单也简单，说复杂也复杂。简单是因为大部分情况下 PyInstaller 就能搞定，复杂是因为真要做得完美，需要考虑的场景太多。我没有最好的方案给你，只有最适合的方案。选择之前想清楚三件事：用户是谁？技术人员还是非技术人员？部署环境是什么？Windows、Linux、还是云端？维护成本怎么算？频繁更新还是一锤子买卖？想清楚这三个问题，答案自然就有了。Python 打包这领域，未来肯定会越来越好。PyOxidizer 让我看到了一些希望，Nuitka 也在快速迭代。但现阶段，要学会"将就"着用，别追求完美。行了，今天就聊这么多。下次有机会讲讲 Python 项目结构怎么组织，那个也是个大坑。如果觉得这篇文章对你有帮助，点个在看、转发给身边搞 Python 的朋友。整理这些内容花了我不少时间，都是真实踩坑经验，不是网上抄的。日常分享运维、Python、效率工具的干货，关注我，少踩坑，少加班。公众号：耕云躬行录个人博客：躬行笔记
源站 IP 暴露被直接打穿？这套 AWS 纵深防御方案你一定用得上
上周半夜接到一个紧急电话，客户的业务挂了。打开监控一看，源站 ALB 的 CPU 直接拉满，连接数爆表。但奇怪的是 CloudFront 那边的请求量完全正常，WAF 日志也干干净净。我当时就猜到了——源站 IP 漏了，攻击者绕过了 CloudFront 直接打源站。后来确认就是这样，客户的源站域名之前在 DNS 里解析过公网 IP，虽然后来改成了 CloudFront 的 CNAME，但历史记录在 SecurityTrails 上一查就有，攻击者拿这个 IP 直接对着 ALB 开干，CloudFront 上的 WAF 规则再多也没用，因为流量根本没经过 CloudFront。这事儿让我觉得有必要把源站防护这个话题好好聊一聊。很多人觉得 CDN 前面挂个 WAF 就安全了，其实这只是第一道门，源站本身还在互联网上裸奔呢。你以为的安全，其实只防了一半我见过太多团队的架构是这样的：CloudFront 挂 WAF，后面接一个公网 ALB，ALB 后面是 ECS/EKS。WAF 规则配了一堆，IP Rate Limiting、Geo Block、Bot Control 全上了，看起来铜墙铁壁。但你想想，WAF 防的是什么？是经过 CloudFront 的流量。攻击者如果直接找到你源站的公网 IP 或域名，绕过 CloudFront 直接访问 ALB，你的 WAF 规则一条都匹配不上。找源站 IP 的手段多得很，我随便列几个：DNS 历史记录——你以前解析过的 IP 全留着呢，SecurityTrails 这种工具一搜就有证书透明度日志——你申请 SSL 证书的时候域名信息就公开了Shodan、Censys 这种全网扫描引擎，端口 Banner 证书一把梭还有各种泄露场景，邮件头里带源站 IP 的、API 返回值里暴露内网信息的、甚至有把源站域名硬编码在前端 JS 里的……有人可能会说，那我源站前面也挂一个 WAF 不就行了？CDN 一层、源站一层，双重保险。我以前也这么想过，后来发现根本行不通。源站 WAF 最大的问题是分不清谁是谁。 经过 CDN 转发过来的请求，TCP 源 IP 是 CDN 节点的地址，真实客户端 IP 在 X-Forwarded-For 里。而直连源站的请求，TCP 源 IP 就是攻击者自己的，X-Forwarded-For 可以随便编。这两种请求搅在一起，WAF 就抓瞎了——按 TCP 源 IP 限速吧，CDN 节点的 IP 就那么几个，正常用户的请求也全被限了；按 X-Forwarded-For 限速吧，直连请求伪造这个头部跟玩一样。结果就是误报漏报一大堆，运维成本翻倍，效果约等于零。所以思路得变：别在应用层纠结了，直接在网络层或传输层把非 CDN 流量掐死。两条路，看你走哪条我根据实际项目经验，总结了两种落地方案：你的场景推荐方案一句话概括用了好几家 CDN，源站在 AWSmTLS 双向认证靠证书验明正身，谁有证谁进来只用 CloudFront，源站在 AWSVPC Origin源站从互联网上消失，谁都找不到下面详细拆解。先说简单的：VPC Origin，让源站隐身如果你的架构只用 CloudFront 一家 CDN，那 VPC Origin 是最省心也最安全的方案，没有之一。原理其实很直白：把源站放到 VPC 私有子网里，不给它公网 IP，不给它公网域名。CloudFront 通过 AWS 内部的 ENI（弹性网络接口）直接连到你的私有子网，流量走的是 AWS 内网，不经过互联网。攻击者连目标都发现不了，更别提攻击了。传统架构下源站是有公网 IP 的：Client → CloudFront → ALB (公网IP，暴露了) Attacker ─────────────→ ALB (绕过CDN直接打)VPC Origin 架构下源站没有公网 IP：Client → CloudFront ──ENI──→ ALB (私有子网，无公网IP) Attacker ────────── ✗ ─────→ 不可达，根本找不到打个不太恰当的比方，传统架构相当于你家大门装了监控（WAF），但房子就在马路边上，谁都能看到门牌号。VPC Origin 相当于把你家搬到了一个没有门牌号的地下室，只有知道暗道的人（CloudFront）才能进来，外人连你住哪都不知道。配置过程前提条件有几个要注意的：VPC 得挂上 Internet Gateway，这是 AWS 的技术实现要求。别担心，这只是个标识，实际不会有互联网流量通过 IGW 到达私有子网，你的源站还是安全的源站必须在私有子网里，不能有 NAT Gateway，不能有 Internet 路由支持 ALB、NLB、EC2 实例创建 VPC Origin：aws cloudfront create-vpc-origin \ --vpc-origin-endpoint-config \ Name=my-vpc-origin,\ Arn=arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-private-alb/abc123,\ HTTPSPort=443,\ OriginProtocolPolicy=https-only配置安全组，限制只有 CloudFront 能进来：方式一，用 CloudFront Managed Prefix List：aws ec2 authorize-security-group-ingress \ --group-id sg-origin-xxx \ --ip-permissions IpProtocol=tcp,FromPort=443,ToPort=443,PrefixListIds=[{PrefixListId=pl-3b927c52}]方式二，用 CloudFront 托管安全组，这个限制更严格，只允许 CloudFront 的 ENI 访问：aws ec2 authorize-security-group-ingress \ --group-id sg-origin-xxx \ --ip-permissions IpProtocol=tcp,FromPort=443,ToPort=443,UserIdGroupPairs=[{GroupId=sg-cloudfront-vpc-origins-xxx}]我个人更推荐方式二，限制粒度更细。把 VPC Origin 关联到 Distribution 就简单了，在 CloudFront 控制台的 Origin 配置里选你刚创建的 VPC Origin 资源就行。踩坑提醒：gRPC 和 Lambda@Edge Origin 触发器不支持，如果你的业务用了这些得另想办法IPv6-only 子网不行NLB 做 VPC Origin 时不支持 TLS Listener，需要 TLS 终结的话得用 ALB部署大概要等 15 分钟，别急支持跨账户共享，通过 AWS RAM 实现，多团队协作时很方便DDoS？不存在的VPC Origin 最让我舒坦的一点就是——源站根本不在公网上，DDoS 从何谈起？所有面向互联网的流量 CloudFront 接着，CloudFront 自带 AWS Shield Standard，L3/L4 的基础防护就有了。你不用额外买 Shield Advanced，不用在源站做任何 DDoS 防护配置。省钱省心。再说复杂的：mTLS，多 CDN 场景的刚需如果你的业务同时用了 CloudFront + Cloudflare，或者还加了 Akamai，源站都在 AWS 的 ALB 后面——这在出海业务里太常见了，国内走一家 CDN，海外走 CloudFront，合规要求还得再加一家。这种多 CDN 架构下，VPC Origin 就用不了了（它只认 CloudFront），得靠 mTLS。为什么不是其他方案？我知道有人会想到这几种方案，我逐个说说为什么不行：Security Group 白名单——限制源站只允许 CDN 的 IP 段访问。能用，但 CDN 的 IP 段动辄几百个，Security Group 有 IP 数量限制容易超。而且 CDN 的 IP 范围会变，你得盯着更新，运维负担不小。IP 理论上还能被伪造。自定义 Header——CDN 回源时加个 X-My-Secret: abc123，源站校验这个 Header。简单是简单，但 Header 可以被抓包截获，也可以被伪造，安全性太低。一旦泄露就完了。Direct Connect 专线——安全倒是安全，但每个 CDN 厂商拉一条专线，成本高得吓人，开通周期还长，灵活性差。mTLS 优势在哪？ 认证发生在 TLS 握手阶段，比应用层早得多。就像你进小区，自定义 Header 是到了单元门口才查门禁卡，mTLS 是在小区大门口就验身份证——连小区都进不来，更别说单元门了。具体来说：传输层就完成身份认证，应用层没法伪造每个 CDN 用自己的客户端证书，源站 Trust Store 统一管理证书轮换可以平滑做，不影响业务想断某个 CDN 的访问？删掉对应的 CA 证书就行架构长这样 ┌─────────────────────────────────────────────┐ │ AWS Cloud │ ┌──────────┐ │ ┌───────────────┐ ┌──────────────┐ │ │CloudFront│──mTLS──▶│ ALB (mTLS) │─────▶│ EC2 / ECS │ │ │(CDN #1) │ │ │ Trust Store │ │ (源站) │ │ └──────────┘ │ └───────────────┘ └──────────────┘ │ ┌──────────┐ │ ▲ │ │Cloudflare│──mTLS──────────┘ │ │(CDN #2) │ │ │ └──────────┘ │ │ ┌──────────┐ │ │ │ Akamai │──mTLS──────────┘ │ │(CDN #3) │ │ │ └──────────┘ └─────────────────────────────────────────────┘三家 CDN 各自带自己的客户端证书回源，ALB 的 Trust Store 里存了三家的 CA 证书，有证的放行，没证的 TLS 握手阶段就拒了。配置实操以 CloudFront + ALB 为例走一遍完整流程，其他 CDN 原理一样，只是 CDN 侧证书出示的方式不同。CloudFront 侧：让 CloudFront 出示客户端证书几个前提条件别漏了：CloudFront 定价计划得是 Business、Premium 或 Pay As You Go，免费版不支持客户端证书要存到 ACM，而且必须在 us-east-1（CloudFront 的老规矩）证书的 EKU 属性必须是 TLS Client Authentication，不然 CloudFront 不认CLI 配置：aws cloudfront update-distribution \ --id E1EXAMPLE \ --distribution-config '{ "Origins": { "Items": [{ "Id": "my-alb-origin", "DomainName": "origin.example.com", "CustomOriginConfig": { "HTTPSPort": 443, "OriginProtocolPolicy": "https-only" }, "OriginMtlsConfig": { "ClientCertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/abc-123" } }] } }'注意几个点：同一个 Distribution 的不同 Origin 可以配不同的客户端证书，挺灵活的。但不兼容 VPC Origins、gRPC、WebSocket 和 Lambda@Edge Origin 触发器，用了这些功能的得另想办法。还有如果源站不要求客户端证书，CloudFront 不会主动出示，连接照常走，这个行为是合理的。ALB 侧：验证客户端证书ALB 的 mTLS 有两种模式。Passthrough 模式是 ALB 不验证证书，把证书链通过 Header 传给后端让后端自己验。Verify 模式是 ALB 用 Trust Store 直接验证，通过才放行。我强烈建议用 Verify 模式。Passthrough 把验证逻辑推给后端应用，增加复杂度还容易出漏洞。能在基础设施层解决的事别推到应用层。Verify 模式配置：创建 Trust Store，上传所有授权 CDN 的 CA 证书：aws elbv2 create-trust-store \ --name cdn-trust-store \ --ca-certificates-bundle-s3-bucket my-certs-bucket \ --ca-certificates-bundle-s3-key ca-bundle.pem修改 ALB Listener 启用 mTLS：aws elbv2 modify-listener \ --listener-arn arn:aws:elasticloadbalancing:... \ --mutual-authentication Mode=verify,TrustStoreArn=arn:aws:elasticloadbalancing:...:truststore/cdn-trust-storeALB 在 Verify 模式下会自动注入一些 Header，可以用于后端审计和细粒度授权：Header内容X-Amzn-Mtls-Clientcert-Serial-Number证书序列号X-Amzn-Mtls-Clientcert-Issuer颁发者X-Amzn-Mtls-Clientcert-Subject主题X-Amzn-Mtls-Clientcert-Validity有效期X-Amzn-Mtls-Clientcert-Leaf叶子证书比如你想区分请求是从 CloudFront 来的还是 Cloudflare 来的，看 Issuer 就行。做审计日志的时候也方便。多 CDN 证书管理——运维的大头配完了只是开始，证书的日常管理才是真正的考验。ALB Trust Store 支持在一个 CA bundle 文件里放多个 CA 证书，只要 CDN 出示的客户端证书是 bundle 中任一 CA 签发的，ALB 就放行。不同 CDN 的证书签发方式不一样：CDN签发方式CA 证书怎么拿CloudFront自建 Private CA 签发，导入 ACMaws acm-pca get-certificate-authority-certificateCloudflareZone-level AOP 用自建 CA；Global AOP 用 Cloudflare 公共 CAauthenticated_origin_pull_ca.pemAkamaiAkamai-signed 或 Third-party CAControl Center → CDN → mTLS Origin Keystore证书轮换我推荐"双 CA 并存"策略：先把新 CA 加到 bundle 里更新 Trust Store，等所有 CDN 节点都切到新证书了，再把旧 CA 从 bundle 里移除。整个过程零中断，不用停业务。撤销访问更简单，从 Trust Store 里移除对应 CDN 的 CA 证书，立刻阻断该 CDN 的回源连接。比如你跟某家 CDN 解约了，删掉它的 CA 证书就完事了。mTLS 架构下的 DDoS 防护mTLS 方案有个不得不面对的问题：源站 ALB 还是暴露在公网上的。虽然只有持合法证书的请求能通过 TLS 握手，但 L3/L4 层的网络攻击不需要完成 TLS 握手就能消耗资源。L7 层的攻击（HTTP Flood、CC 之类的）mTLS 天然免疫——攻击者没有合法的客户端证书和私钥，TLS 握手都完不成，HTTP 请求根本发不出来。但 L3/L4/L5 层（SYN Flood、UDP 反射、TLS 握手洪泛）需要 AWS Shield Advanced 来扛。Shield Advanced 在 AWS 网络边缘就把攻击流量清洗掉了，ALB 根本感知不到。一个月 $3,000，不便宜，但高价值业务该花还是得花。两种方案到底怎么选直接看对比： mTLSVPC Origin安全性高，传输层认证最高，网络层隔离多 CDN✅❌ 只认 CloudFront源站公网暴露还是需要公网域名完全没有DDoS 防护L7 天然免疫，L3/L4 要 Shield Advanced天然免疫，不需要额外防护证书管理要管不需要部署难度中等低兼容性限制不支持 VPC Origin/gRPC/WebSocket不支持 gRPC/Lambda@Edge成本mTLS 免费，Shield Advanced 可选 $3,000/月无额外费用我的建议是：能用 VPC Origin 就用 VPC Origin，配置简单安全性最高还不用管证书。只有确实需要多 CDN 的场景才上 mTLS，但证书轮换的运维成本你得提前有心理准备。简单画个决策流程：用了多家 CDN？ ├── 是 → mTLS，没得选 └── 否 → 只用 CloudFront？ ├── 是 → VPC Origin，最省心 └── 否 → 看看其他 CDN 有没有 mTLS 能力最后说两句源站防护这事儿，核心就一句话：让 CDN 成为源站的唯一入口，在网络层或传输层把非法直连掐死。 别在应用层折腾 WAF 规则了，越往底层做越靠谱。多 CDN 用 mTLS，TLS 握手阶段就验身份，伪造不了。纯 CloudFront 用 VPC Origin，源站从互联网上消失，攻击者连地址都找不到。不管哪种，都比"双 WAF"或者 IP 白名单强太多。安全防护这东西，永远是在和攻击者比谁更底层——你在应用层做规则匹配，人家在网络层绕过你；你在网络层封堵，他就更难搞了。把防线往下沉，才是正道。公众号：耕云躬行录个人博客：躬行笔记觉得有用的话，关注 @运维躬行录，转发给还在用"双 WAF"方案的朋友们看看。后面还会继续分享 AWS 网络和安全方面的实战踩坑，咱们下回聊。
我花了两周啃完LangChain，踩的坑比写的代码还多
最近被AI应用开发搞得有点上头。事情是这样的，公司领导突然说要搞个"智能知识库问答系统"，让内部的运维文档、故障处理手册能通过对话的方式查询。需求倒是不复杂，但我一个搞运维的，平时写写shell脚本、搞搞自动化部署还行，这突然让我去搞AI应用开发，属实有点赶鸭子上架了。调研了一圈之后，LangChain这个框架出现频率最高。GitHub上13万多star，各种教程满天飞，看着好像挺成熟的。于是我花了大概两周时间，从零开始学了一遍，中间踩了不少坑，也算是把核心的东西搞明白了。今天把这个过程记录下来，给同样想入门的兄弟们一个参考。项目地址：https://github.com/langchain-ai/langchainLangChain到底解决了什么问题在说LangChain之前，我得先说说为什么需要它。你想啊，现在的大模型（GPT-5、Claude4、通义千问这些）确实很强，但它们本质上就是一个"输入文本→输出文本"的黑盒子。你要做一个真正能用的AI应用，光有模型是远远不够的。举个具体的例子。我想做的知识库问答系统，需要完成这些事情：把公司几百份运维文档（PDF、Markdown、Word都有）读进来把文档切成小块存到向量数据库里用户提问的时候，先去向量数据库里找相关的文档片段把找到的片段和用户的问题一起发给大模型大模型根据这些上下文生成回答如果没有框架，这些东西全得自己从头写。文档解析要写、文本分割要写、向量存储要写、Prompt拼接要写……你写个demo可能还行，但要做成生产可用的东西，工作量大得吓人。LangChain就是把这些东西都封装好了，你像搭积木一样把各个组件拼起来就行。它提供了统一的接口来对接各种大模型、各种向量数据库、各种文档格式，省了巨多重复劳动。环境搭建这部分我直接贴命令了，没什么好说的：pip install langchain langchain-core langchain-community langchain-openai -i https://pypi.tuna.tsinghua.edu.cn/simple/ # 向量数据库，我选的FAISS，Facebook开源的，本地跑不需要额外服务 pip install faiss-cpu -i https://pypi.tuna.tsinghua.edu.cn/simple/ # 文档加载相关的 pip install pypdf unstructured python-docx -i https://pypi.tuna.tsinghua.edu.cn/simple/API Key的配置，我建议用.env文件别硬编码：# .env OPENAI_API_KEY=sk-xxxxxxxx OPENAI_API_BASE=https://api.openai.com/v1from dotenv import load_dotenv load_dotenv()这里有个坑要提一下。如果你用的是国内的代理API（比如某些转发服务），OPENAI_API_BASE这个环境变量一定要设对。我一开始设错了，报错信息还特别迷惑，排查了好久才发现是base_url的问题。从最简单的开始：调用大模型LangChain里调用模型非常简单：from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="gpt-4o-mini", temperature=0.7, max_tokens=2000 ) response = llm.invoke("解释一下Linux的iptables和nftables有什么区别") print(response.content)这个没什么好说的，就是最基础的调用。但问题来了——实际场景中你不可能每次都手动拼接问题字符串对吧？所以LangChain搞了个Prompt模板的机制。from langchain_core.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个资深的{role}，回答问题要结合实际生产经验，给出可操作的建议。"), ("human", "{question}") ]) # 这样用 messages = prompt.format_messages( role="Linux运维工程师", question="服务器突然SSH连不上了，怎么排查？" ) response = llm.invoke(messages)看着好像就是个字符串格式化对吧？但实际上Prompt模板的意义在于复用和标准化。你可以把写好的模板保存下来，不同场景换个参数就能用。而且后面做Chain的时候，模板是可以直接当组件来拼接的。LCEL：管道符拼接组件这个是LangChain的核心语法，叫LCEL（LangChain Expression Language）。说白了就是用|把各个组件串起来：from langchain_core.output_parsers import StrOutputParser chain = prompt | llm | StrOutputParser() # 一行搞定 result = chain.invoke({ "role": "运维工程师", "question": "Nginx出现502错误一般怎么排查？" }) print(result) # 直接就是字符串了StrOutputParser()是把模型的输出对象转成纯字符串。不加的话返回的是一个AIMessage对象，带了一堆元数据，一般用不着。这个管道符语法刚看到的时候觉得还挺酷的，但用多了会发现有些场景不太直观。比如你想在中间加个判断逻辑，或者需要把前一步的部分输出传给后一步，写起来就有点绕了。不过对于线性流程，确实比一步步调用要简洁很多。我后来做的一个比较实用的小东西：把运维告警信息扔给AI分析，让它判断严重程度和建议处理方案：from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from langchain_core.output_parsers import JsonOutputParser from pydantic import BaseModel, Field class AlertAnalysis(BaseModel): severity: str = Field(description="严重程度：critical/warning/info") root_cause: str = Field(description="可能的根因分析") suggestion: str = Field(description="处理建议") need_escalation: bool = Field(description="是否需要升级处理") parser = JsonOutputParser(pydantic_object=AlertAnalysis) alert_prompt = ChatPromptTemplate.from_messages([ ("system", """你是一个运维告警分析助手。根据告警信息进行分析，返回JSON格式的结果。 {format_instructions}"""), ("human", "告警内容：{alert_message}") ]) alert_chain = alert_prompt | ChatOpenAI(model="gpt-4o-mini", temperature=0) | parser # 测试 result = alert_chain.invoke({ "alert_message": "[CRITICAL] 服务器web-prod-03 磁盘使用率达到95%，/var/log分区剩余空间不足2GB", "format_instructions": parser.get_format_instructions() }) print(result) # {'severity': 'critical', 'root_cause': '日志文件未及时清理或轮转配置不当...', # 'suggestion': '1. 先清理旧日志释放空间 2. 检查logrotate配置...', # 'need_escalation': False}这个JsonOutputParser是个好东西，可以让大模型的输出直接变成结构化的Python字典。配合Pydantic定义schema，输出格式非常稳定。但也不是百分百靠谱，偶尔模型还是会输出不合规的JSON，生产环境里得加个try-except兜底。重头戏：RAG实现RAG（Retrieval-Augmented Generation）是我学LangChain最主要的目的。整个流程大概是这样的：加载文档 → 切分文本 → 向量化存储 → 检索 → 生成回答一步步来。加载文档from langchain_community.document_loaders import ( PyPDFLoader, DirectoryLoader, TextLoader, UnstructuredMarkdownLoader ) # 加载单个PDF loader = PyPDFLoader("./docs/nginx运维手册.pdf") docs = loader.load() print(f"加载了{len(docs)}页") # 批量加载目录下所有Markdown文件 loader = DirectoryLoader( "./docs/", glob="**/*.md", loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"}, show_progress=True ) docs = loader.load()这里有个坑——TextLoader默认编码不是UTF-8，如果你的文档里有中文，不指定encoding就会报错。我在这上面浪费了至少半小时才发现。另外DirectoryLoader加载大量文件的时候速度不快，几百个文件可能要跑个几十秒。如果文件特别多，可以考虑加个缓存机制，第一次加载完把处理好的数据存下来，后面就不用重新加载了。文本分割这一步非常关键，直接影响后面检索的效果：from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=80, separators=["\n\n", "\n", "。", "！", "？", "；", " "], length_function=len ) splits = text_splitter.split_documents(docs) print(f"分割成了{len(splits)}个片段")chunk_size这个参数我试了好几个值。设太大（比如1000以上），检索的时候会返回一大段不太相关的内容；设太小（比如200以下），上下文信息又不够完整。对于中文技术文档，我发现400-600之间效果比较好。chunk_overlap是重叠部分，防止在切分的时候把一句完整的话切断了。设个50-100基本够用。separators列表的顺序是有优先级的。它会优先尝试按\n\n（段落）来切，切不下就按\n（换行），再切不下就按句号切。对中文文档来说，记得把中文标点加上去。向量化和存储from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS embeddings = OpenAIEmbeddings() # 构建向量数据库 vectorstore = FAISS.from_documents(splits, embeddings) # 保存到本地，下次直接加载就不用重新计算embedding了 vectorstore.save_local("./faiss_index")这步需要注意的是，embedding计算是要调API的，要花钱。如果你的文档量比较大（几千个片段以上），这个费用就不能忽略了。OpenAI的embedding模型相对便宜，text-embedding-3-small目前是$0.02/1M tokens，但架不住量大。还有个经验：构建完向量数据库一定要save_local保存下来。不然每次启动程序都要重新计算，一是慢二是费钱。如果你不想用OpenAI的embedding（比如担心数据安全问题），也可以用本地模型：from langchain_community.embeddings import HuggingFaceEmbeddings embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-small-zh-v1.5", model_kwargs={'device': 'cpu'} )bge-small-zh-v1.5是智源开源的中文embedding模型，效果还不错，关键是完全本地运行，不用担心数据外泄。检索和生成from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser # 创建检索器 retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 4} # 返回最相关的4个片段 ) # 构建RAG的prompt rag_prompt = ChatPromptTemplate.from_template(""" 根据以下参考资料回答用户的问题。如果参考资料中没有相关信息，请明确告知用户你不确定，不要编造答案。 参考资料： {context} 用户问题：{question} 请给出详细的回答： """) def format_docs(docs): return "\n\n---\n\n".join([doc.page_content for doc in docs]) # 组装RAG链 rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | rag_prompt | ChatOpenAI(model="gpt-4o-mini", temperature=0) | StrOutputParser() ) # 提问 answer = rag_chain.invoke("Nginx配置反向代理后499错误怎么处理？") print(answer)这段代码里有个写法可能第一次看会有点迷糊：{"context": retriever | format_docs, "question": RunnablePassthrough()}它的意思是：question字段直接透传用户的输入，context字段则是先通过retriever检索文档，再用format_docs格式化成文本。两个字段并行处理，最后一起传给后面的prompt模板。说实话这个语法我适应了一会儿才看顺眼。但理解之后确实很简洁。RAG效果调优RAG跑起来之后，你大概率会遇到"检索不准"或者"回答不对"的问题。我的经验是从这几个方向调：检索阶段：search_kwargs里的k值，太小可能漏掉重要信息，太大会引入噪音chunk_size调整，找到最适合你文档特点的大小试试search_type="mmr"（最大边际相关性），它会在相关性和多样性之间做个平衡，避免返回的几个片段内容太重复retriever = vectorstore.as_retriever( search_type="mmr", search_kwargs={"k": 4, "fetch_k": 10} # 先取10个再从中选4个最多样的 )生成阶段：Prompt写好点，明确告诉模型"不知道就说不知道"temperature设低一点（0或0.1），让输出更稳定有时候把检索到的文档来源信息也附上，方便溯源Agent：让AI自己决定调什么工具这个是LangChain里我觉得最有意思的部分。简单说就是你给AI定义一些"工具"（函数），然后让它根据用户的问题自己判断该调用哪个工具。from langchain_core.tools import tool from langchain_openai import ChatOpenAI from langchain.agents import AgentExecutor, create_openai_functions_agent from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder @tool def check_disk_usage(hostname: str) -> str: """检查指定服务器的磁盘使用情况""" # 实际环境里这里会SSH过去执行df命令 # demo先模拟一下 import random usage = random.randint(30, 95) return f"服务器 {hostname} 磁盘使用率: {usage}%，/var/log 占用最大" @tool def check_service_status(service_name: str, hostname: str) -> str: """检查指定服务器上某个服务的运行状态""" import random status = random.choice(["active (running)", "inactive (dead)", "failed"]) return f"{hostname} 上的 {service_name} 状态: {status}" @tool def search_error_log(keyword: str, log_path: str = "/var/log/syslog") -> str: """在日志文件中搜索包含指定关键词的错误记录""" return f"在 {log_path} 中找到3条包含 '{keyword}' 的记录:\n[ERROR] 2024-01-15 Connection refused\n[ERROR] 2024-01-15 Timeout exceeded\n[ERROR] 2024-01-15 Permission denied" tools = [check_disk_usage, check_service_status, search_error_log] prompt = ChatPromptTemplate.from_messages([ ("system", """你是一个运维助手，可以帮助用户排查服务器问题。 你有以下工具可以使用，请根据用户的问题选择合适的工具来排查。 如果需要多步排查，可以依次调用多个工具。"""), ("human", "{input}"), MessagesPlaceholder(variable_name="agent_scratchpad") ]) llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) agent = create_openai_functions_agent(llm, tools, prompt) agent_executor = AgentExecutor( agent=agent, tools=tools, verbose=True, # 开启这个能看到AI的推理过程，调试必备 max_iterations=5 ) response = agent_executor.invoke({ "input": "帮我看看web-01服务器是不是磁盘满了，顺便查下nginx服务状态" }) print(response["output"])开启verbose=True之后，你能在控制台看到AI的完整推理过程——它会先分析用户的问题，决定调用哪个工具，拿到结果后再决定下一步该做什么。这个过程看着还挺有意思的，像是AI在"思考"。但Agent有个问题需要注意：它的行为不是100%可预测的。同样的问题，不同时候可能会走不同的路径。所以在生产环境里用Agent要谨慎，最好加上一些防护措施——比如限制最大迭代次数、对工具的输入做校验、对危险操作加二次确认等等。对话记忆如果你做的是对话式应用，记忆管理就绕不开了。LangChain提供了几种Memory：from langchain.memory import ConversationBufferWindowMemory # 只记住最近10轮对话 memory = ConversationBufferWindowMemory( k=10, memory_key="chat_history", return_messages=True )为什么不用ConversationBufferMemory（记住所有对话）？因为token是有限的也是要钱的。一个长对话聊个几十轮，光历史消息就能吃掉大半的上下文窗口。WindowMemory只保留最近的N轮，简单粗暴但管用。还有个ConversationSummaryMemory，它会用模型把历史对话压缩成摘要。听着很美好，但实际用下来发现摘要质量参差不齐，有时候重要细节会被压没了。个人感觉不如Window好用，除非你的对话真的特别长。生产环境的一些经验跑demo和上生产完全是两回事。我在落地过程中遇到的几个实际问题：1. 模型调用要有降级策略primary_llm = ChatOpenAI(model="gpt-4o") fallback_llm = ChatOpenAI(model="gpt-4o-mini") # 主模型挂了自动切备用 llm = primary_llm.with_fallbacks([fallback_llm])API调用偶尔会超时或者报错，尤其是网络不稳定的时候。加个fallback能避免整个服务挂掉。2. 监控token消耗from langchain_community.callbacks import get_openai_callback with get_openai_callback() as cb: result = rag_chain.invoke("什么是K8s的Pod？") print(f"本次调用：{cb.total_tokens} tokens，费用 ${cb.total_cost:.4f}")不监控token用量，月底账单可能会给你惊喜。特别是RAG场景，每次请求都带着一堆context，token消耗比普通对话大很多。3. 流式输出用户等5秒才看到回答，体验非常差。流式输出能让用户感觉响应很快：for chunk in rag_chain.stream("解释一下Docker的网络模式"): print(chunk, end="", flush=True)4. 异常处理大模型的输出不是100%可控的，JSON解析可能失败，工具调用可能出错。每个环节都要有兜底逻辑，不能让一个异常把整个服务搞崩。我踩过的一些坑最后说几个我实际踩过的坑，可能对你有帮助：LangChain的版本更新很快，API经常变。网上很多教程的代码直接复制过来跑不了，因为用的是老版本的写法。建议认准官方文档，别太依赖博客教程（包括我这篇，哈哈）langchain和langchain-community是分开的包，很多常用的loader、vectorstore都搬到community包里了FAISS在Windows上装faiss-cpu一般没问题，但faiss-gpu很难装，别折腾了如果你用国产模型（比如通义千问、智谱），LangChain也有对应的集成包，比如langchain-community里的ChatTongyiembedding计算比较费时间，几千个文档片段可能要跑个几分钟，做好索引持久化别每次重算总结LangChain说白了就是一个AI应用开发的脚手架。你不用它也能做出同样的东西，但用了它能省很多重复劳动。它把大模型调用、文档处理、向量检索、Agent编排这些常见需求都封装好了，你只需要关注业务逻辑就行。我的建议是：从RAG开始学，这是目前最实用也最好理解的场景。跑通一个简单的知识库问答之后，再去看Agent、Memory这些进阶的东西。别一上来就想着搞个多Agent协作系统，那玩意儿连LangChain的作者自己都还在迭代。好了就写到这。如果你也在搞AI应用开发，或者对LangChain有什么疑问，欢迎交流。这个方向变化太快了，多交流才能跟上节奏。如果觉得这篇文章对你有帮助，欢迎点赞、转发、在看三连，你的支持是我持续输出的动力。关注我，后续会继续分享更多AI应用开发和运维实践相关的干货内容。公众号：耕云躬行录个人博客：躬行笔记
生产环境救急指南：Linux服务器忘记密码的N种解法，覆盖主流系统和云厂商
说实话，做运维这么多年，密码忘记这事儿我自己都干过好几次，更别提帮别人处理的次数了，有时候是接手别人的机器没交接清楚，有时候是自己太久没登录给忘了，还有的时候是同事离职了账号没留……反正各种奇葩情况都遇到过。今天就把这些年的经验整理一下，把主流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 # 假设根分区是sda2切换到chroot环境：chroot /mnt/root passwd root exit重启后用新密码登录即可。云服务器密码重置云服务器的情况稍微复杂一点，因为你看不到真正的控制台，但云厂商一般都提供了密码重置功能，而且操作起来比物理机方便多了。3.1 阿里云ECS阿里云的密码重置算是比较方便的，有两种方式。方式一：控制台重置登录阿里云控制台，进入ECS实例列表，找到目标实例。如果实例是运行状态，可以直接点击「更多」→「密码/密钥」→「重置实例密码」。输入新密码，确认后，如果是运行中的实例，需要重启才能生效。如果实例是停止状态，密码会立即生效。这里有个细节要注意，如果你的实例是经典网络类型，重置密码后可能需要重启才能生效；如果是VPC类型，有些情况下不需要重启。方式二：VNC远程连接如果控制台重置密码的功能用不了（比如实例状态异常），可以用VNC远程连接。在实例详情页，点击「远程连接」，选择VNC方式。如果是第一次使用，会提示设置VNC密码，这个密码跟系统密码不是一回事，要区分开。连接上去后，就跟操作本地虚拟机一样了，可以用前面说的单用户模式重置密码。不过说实话，阿里云的密码重置功能已经很完善了，大多数情况下用控制台直接重置就行，没必要折腾VNC。3.2 腾讯云CVM腾讯云的操作跟阿里云差不多，也是控制台直接重置。登录腾讯云控制台，进入云服务器CVM列表，找到目标实例。点击「更多」→「密码/密钥」→「重置密码」。输入新密码，确认后，如果实例在运行，系统会提示需要重启才能生效。腾讯云有个地方要注意，如果你设置的是密钥对登录，密码登录可能会被禁用，这时候需要先把密码登录开启。在「登录设置」里可以修改。另外，腾讯云的VNC连接叫「标准登录」或「OrcaTerm」，在实例列表点击「登录」就能选择。3.3 华为云ECS华为云的密码重置也差不多，控制台找到实例，「更多」→「重置密码」。不过华为云有个特色功能叫「一键重置密码插件」，需要在创建实例时安装。如果创建时没装这个插件，控制台的重置密码功能可能用不了，需要用其他方式。如果没有安装一键重置密码插件，可以：关机实例进入「镜像服务」，使用该实例创建镜像用新镜像创建实例时设置新密码或者更简单的方法，使用云服务器控制台的「远程登录」功能，通过VNC连接进去，用单用户模式重置。3.4 AWS EC2AWS的密码重置方式有点不太一样，因为EC2默认是用密钥对登录的，密码登录默认是关闭的。如果是Amazon Linux或者RHEL之类的系统，可以用以下方式：方式一：EC2 Serial Console如果实例支持Serial Console访问，可以在控制台启用。EC2 Serial Console允许你通过串口连接到实例，然后可以用单用户模式重置密码。不过这个功能需要在实例上启用，而且需要实例支持。查看实例类型是否支持Nitro系统，大多数新实例都支持。方式二：使用用户数据脚本停止实例，修改用户数据，添加以下脚本：Content-Type: text/cloud-config #cloud-config password: YourNewPassword chpasswd: { expire: False } ssh_pwauth: True启动实例后，密码就会生效。但这种方式只在实例首次启动或者用户数据变化后首次启动时执行。方式三：挂载根卷到另一个实例停止目标实例，在控制台分离根卷（通常是/dev/xvda或/dev/sda1）。把分离出来的卷挂载到另一个可以访问的EC2实例上，挂载后chroot进去改密码，或者直接修改/etc/shadow文件。改完后，重新挂载回原实例，启动即可。这个方法比较麻烦，但是最可靠，几乎所有Linux发行版都适用。3.5 Azure虚拟机Azure VM的密码重置也有几种方式。方式一：Azure Portal重置登录Azure Portal，找到目标虚拟机，在左侧菜单找到「重置密码」。输入新用户名和密码，点击「更新」即可。这个功能底层是使用VM Agent执行的，如果VM Agent没装或者不工作，这个方法就没法用了。方式二：使用Run CommandAzure有个功能叫Run Command，可以在不登录的情况下执行命令。在虚拟机页面，找到「运行命令」，选择RunShellScript，然后输入：echo "root:YourNewPassword" | chpasswd点击运行，等待执行完成。方式三：使用Serial ConsoleAzure也支持Serial Console访问，在虚拟机页面找到「串行控制台」，连接后就可以像本地控制台一样操作了。进入GRUB后，用单用户模式重置密码即可。3.6 其他云厂商天翼云、移动云、联通云等运营商云的操作大同小异，都是控制台找到实例，点击重置密码，输入新密码，重启生效。京东云、百度云、金山云等也是类似的操作流程，就不一一赘述了。容器环境的密码重置？有人可能会问，Docker容器里的Linux密码忘了怎么办？这个其实不是问题，容器本身就不应该有密码登录的概念。容器的正确使用方式是通过docker exec或者kubectl exec进入容器，不需要密码。如果真要在容器里设置密码，也是改镜像或者持久化存储里的配置，而不是在运行中的容器里改。预防措施说完了怎么解决问题，再聊聊怎么预防。首先，密码管理工具用起来。不管是公司里的密码管理系统，还是个人的1Password、Bitwarden之类的，都比记在脑子里或者写在便签纸上靠谱。其次，SSH密钥对登录优先。密码容易忘，密钥不会忘，而且更安全。但要注意私钥要备份好，丢了比密码忘了还麻烦。第三，堡垒机或者跳板机用起来。通过堡垒机登录服务器，堡垒机上可以设置密码代填，不用记住每台服务器的密码。第四，文档要跟上。新服务器上线，密码要及时记录；交接的时候，文档要完整。这些都是运维的基本功。第五，定期巡检。时不时登录一下不常用的服务器，确认账号密码是否正确，有问题早发现早解决。一些踩过的坑最后分享几个我遇到过的坑。一次是给客户重置密码，改完之后死活登录不上，排查了半天发现是SELinux标签没重置，重启后验证通不过，登录被拒绝。从此以后，红帽系的系统改密码必执行touch /.autorelabel。还有一次，云服务器控制台重置密码，等了半天没生效，结果发现实例是停止状态下的重置，密码设置了但没重启，等了半天白等。所以现在如果实例是停止的，先启动，再确认密码是否生效。最坑的一次是接手一台老服务器，用各种方法都进不去，最后发现GRUB被加密了，密码不知道。这种情况下只能用Live CD启动，挂载磁盘改/etc/shadow文件。从那以后，我都习惯问一下之前的运维有没有给GRUB加密。总结密码忘记这事儿，说大不大，说小不小，关键是要知道怎么处理。物理机和虚拟机，单用户模式和Live CD是两大法宝；云服务器，控制台重置密码是最方便的方式；实在不行，VNC和Serial Console能救急。当然，最好的办法还是做好密码管理，别让自己陷入这种窘境。希望这篇文章能帮到有需要的朋友，有问题可以在评论区留言交流。公众号：耕云躬行录个人博客：躬行笔记如果这篇文章对你有帮助，欢迎关注转发，你的支持是我持续输出的动力！
Pingora实战：从零搭建比Nginx还快的反向代理，手把手教你玩转Cloudflare开源神器
前两天看到Cloudflare开源了Pingora这个项目，说是用Rust写的反向代理框架，性能比Nginx还要快，我就坐不住了。咱们做运维的，对新东西总是要保持好奇心，何况这玩意儿号称每秒能处理百万级请求，这不得赶紧试试？说实话，刚开始看到这个消息的时候我还挺怀疑的。Nginx可是咱们运维圈的老伙计了，用了这么多年，稳定可靠，你说一个新出来的框架能比它快？不过转念一想，Cloudflare可是全球最大的CDN服务商之一，他们每天处理的流量那都是天文数字，既然敢开源出来，肯定是有两把刷子的。抱着试试看的心态，我在测试环境折腾了几天，把整个安装配置流程都跑了一遍。不得不说，这东西确实有点东西，今天就把我的实践经验分享给大家，希望能帮到想尝鲜的朋友们。Pingora是个啥？咱们先聊聊Pingora到底是个什么东西。简单来说，它是Cloudflare用Rust语言开发的一个高性能反向代理框架[1]。Cloudflare大家应该都不陌生吧，全球最大的CDN服务商之一，每天处理的HTTP请求那是数以万亿计的。以前Cloudflare也是用Nginx，但是随着业务规模越来越大，Nginx的一些局限性就开始显现出来了。比如说，Nginx的配置文件虽然灵活，但是要做一些复杂的逻辑处理就比较麻烦；还有就是性能瓶颈，在某些极端场景下已经满足不了需求了。于是Cloudflare就开始自己造轮子，用Rust写了这个Pingora。根据官方的说法，Pingora在性能、安全性、可扩展性方面都有很大的提升[2]。特别是在高并发场景下，表现相当亮眼。我看过一些性能测试的对比数据，Pingora在某些场景下确实比Nginx快不少[3]。当然，这个"快"也要看具体场景，不是说什么情况下都快的。咱们做技术的，不能盲目迷信，得自己动手测试才知道。环境准备好了，说了这么多，咱们开始动手吧。不过在安装Pingora之前，得先把环境准备好。Pingora是用Rust写的，所以首先你得有Rust的环境。要是你系统里还没有Rust，那就得先装一下。我这边测试用的是debain13，其他Linux发行版应该也差不多。安装Rust其实挺简单的，官方提供了一个脚本：curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh运行这个脚本，一路回车就好。装完之后记得重启一下终端，或者手动source一下环境变量：source $HOME/.cargo/env然后验证一下安装是否成功：rustc --version cargo --version看到版本号输出就说明OK了。对了，还得提醒一下，Pingora对Rust的版本有要求，最好是1.70以上的版本。要是版本太老，可能编译的时候会报错。我一开始就是用的系统自带的旧版本，结果折腾半天编译不过，后来升级了Rust版本才解决。除了Rust，你还得有一些基础的编译工具，比如gcc、make之类的。debain13的话可以这样装：apt-get install -y gcc make openssl-devel pkg-config这些工具在编译Pingora的时候会用到，少了哪个都会报错，到时候再装就麻烦了。创建项目环境准备好了，接下来就是创建项目。Pingora不是一个独立的可执行程序，而是一个框架，你需要基于它来开发自己的代理服务。这个跟Nginx不太一样，Nginx是直接装好就能用的，Pingora更像是一个开发框架。用Cargo创建一个新项目：cargo new my_pingora_proxy cd my_pingora_proxy创建后的目录结构：然后编辑Cargo.toml文件，添加Pingora的依赖：[package] name = "my_pingora_proxy" version = "0.1.0" edition = "2024" [dependencies] pingora = { version = "0.1", features = ["proxy"] } async-trait = "0.1"这里要注意一下，Pingora的版本号可能会更新，具体可以去crates.io上看最新的版本。我写这篇文章的时候是0.1版本，现在可能已经更新了。添加依赖的时候，那个features = ["proxy"]不能少，不然编译的时候会提示找不到某些模块。我第一次搞的时候就忘了这个，折腾了好久才发现。写第一个代理程序依赖配好了，接下来就是写代码了。说实话，第一次看到Pingora的代码的时候，我还挺懵的。跟Nginx那种配置文件的方式完全不一样，Pingora是用代码来定义代理逻辑的。不过仔细看看，其实也不难理解。咱们先写一个最简单的反向代理，把请求转发到后端服务器。打开src/main.rs，把原来的内容删掉，换成下面的代码：use pingora::prelude::*; use std::sync::Arc; use async_trait::async_trait; // 确保导入了这个 trait pub struct MyProxy; #[async_trait] impl ProxyHttp for MyProxy { // 1. 必须定义上下文类型，通常用空元组 type CTX = (); // 2. 必须实现初始化上下文的方法 fn new_ctx(&self) -> Self::CTX {} async fn upstream_peer( &self, _session: &mut Session, _ctx: &mut Self::CTX, // 注意这里类型要与上面一致 ) -> Result<Box<HttpPeer>> { // 3. 这里的地址只能是 "IP:端口"，不能有 http:// let peer = Box::new(HttpPeer::new( "127.0.0.1:8080", false, "localhost".to_string(), // 通常建议填入域名 )); Ok(peer) } } fn main() { // 初始化服务器 let mut server = Server::new(None).unwrap(); server.bootstrap(); // 创建代理服务 let mut proxy = http_proxy_service( &server.configuration, MyProxy {}, ); // 监听端口 proxy.add_tcp("0.0.0.0:6199"); server.add_service(proxy); println!("Pingora Proxy 正在运行在端口 6199..."); server.run_forever(); }这段代码看起来有点长，但其实逻辑很简单。我来逐行解释一下。开头那几行是引入依赖，这个不用多说。然后定义了一个MyProxy结构体，这个就是咱们的代理主体。接下来是实现ProxyHttp trait，这个是Pingora的核心接口。里面的upstream_peer方法是用来指定后端服务器的，这里我把请求转发到了本地的8080端口。你可以根据实际情况改成你的后端地址。main函数里就是创建服务器、配置监听端口这些操作。我这里监听的是6199端口，你可以改成其他端口，只要不冲突就行。编译运行代码写好了，接下来就是编译运行了。直接用cargo：cargo build --release出现报错，处理下，简单来说：你的 Linux 系统中没有安装 cmake，或者 cmake 不在系统的环境变量路径中。由于 libz-ng-sys 这个库（它是 Pingora 的依赖之一）需要通过 cmake 来编译 C 代码，所以报错了。（感谢ai）第一次编译可能会比较慢，因为要下载和编译很多依赖包。我这边测试的时候花了大概五六分钟，具体时间要看你的网络情况和机器性能。编译完成后，会在target/release目录下生成可执行文件。运行它：./target/release/my_pingora_proxy如果没什么报错，应该就能看到程序跑起来了。不过这个时候你可能会发现，程序启动之后没有任何输出，也不退出。这是正常的，它在等待连接呢。测试一下光跑起来还不行，咱们得测试一下能不能正常工作。先准备一个后端服务。我这边就用Python简单起一个HTTP服务器：cd /tmp echo "Hello from backend" > index.html python3 -m http.server 8080这样就在8080端口起了一个简单的HTTP服务器。然后新开一个终端，用curl测试一下我们的代理：curl http://127.0.0.1:6199/如果一切正常，你应该能看到"Hello from backend"这个输出。这就说明我们的代理服务器工作正常，成功把请求转发到了后端。我当时第一次测试成功的时候还挺激动的，虽然只是个简单的代理，但毕竟是自己用Rust写的第一个能用的程序。哈哈，成就感满满。负载均衡配置简单的代理跑通了，咱们来点进阶的。实际生产环境中，后端通常不会只有一台服务器，这时候就需要负载均衡了[6]。Pingora做负载均衡也挺方便的，咱们稍微改一下代码：use async_trait::async_trait; use pingora::lb::selection::round_robin::RoundRobin; use pingora::lb::LoadBalancer; use pingora::prelude::*; use std::sync::Arc; // 定义我们的代理结构体，现在它持有一个负载均衡器 pub struct MyProxy { pub lb: Arc<LoadBalancer<RoundRobin>>, } #[async_trait] impl ProxyHttp for MyProxy { type CTX = (); fn new_ctx(&self) -> Self::CTX {} async fn upstream_peer( &self, _session: &mut Session, _ctx: &mut Self::CTX, ) -> Result<Box<HttpPeer>> { // --- 负载均衡核心逻辑 --- // 从负载均衡器中选择一个后端节点 let upstream = self .lb .select(b"", 256) // 这里的 b"" 是用于一致性哈希的 key，轮询模式下不生效 .ok_or_else(|| Error::explain(ErrorType::InternalError, "无可用后端节点"))?; println!("正在转发请求至: {:?}", upstream.addr); // 创建 HttpPeer，注意 upstream.addr 已经是 SocketAddr 类型 let peer = Box::new(HttpPeer::new(upstream, false, upstream.host().to_string())); Ok(peer) } } fn main() { let mut server = Server::new(None).unwrap(); server.bootstrap(); // 1. 配置后端列表 // 假设你本地运行了两个服务：8080 和 8081 let mut upstreams = vec![ "127.0.0.1:8080", "127.0.0.1:8081", ]; // 2. 创建负载均衡器 (使用轮询算法) let lb = LoadBalancer::from_backends(upstreams.as_slice()); let arc_lb = Arc::new(lb); // 3. 将负载均衡器注入代理服务 let mut proxy = http_proxy_service( &server.configuration, MyProxy { lb: arc_lb }, ); proxy.add_tcp("0.0.0.0:6199"); server.add_service(proxy); println!("Pingora 负载均衡代理已启动: 0.0.0.0:6199 -> [8080, 8081]"); server.run_forever(); }这段代码跟之前的区别在于，我们引入了RoundRobin，这是一个轮询的负载均衡算法。然后在upstream_peer方法里，通过lb.select()来选择一个后端服务器。backends数组里配置了三个后端服务器地址，你可以根据实际情况修改。Pingora会按照轮询的方式，依次把请求分发给这三台服务器。我测试的时候起了三个Python HTTP服务器，分别监听8080、8081、8082端口，然后用curl连续请求几次，确实能看到请求被分发到了不同的后端。效果还不错。健康检查光有负载均衡还不够，生产环境还得有健康检查。要是某台后端服务器挂了，负载均衡器得知道，不能再把请求往那台服务器上发了。Pingora支持健康检查，不过需要额外配置一下：use pingora::prelude::*; use pingora::load_balancing::{RoundRobin, health_check}; use std::sync::Arc; use std::time::Duration; pub struct MyProxy { lb: Arc<RoundRobin>, } #[async_trait::async_trait] impl ProxyHttp for MyProxy { async fn upstream_peer( &self, _session: &mut Session, _ctx: &mut Box<dyn Send + Sync>, ) -> Result<Box<HttpPeer>> { let peer = self.lb.select(b"").unwrap(); Ok(Box::new(peer)) } } fn main() { let mut server = Server::new(None).unwrap(); server.bootstrap(); let backends = vec![ "127.0.0.1:8080".to_string(), "127.0.0.1:8081".to_string(), "127.0.0.1:8082".to_string(), ]; let mut lb = RoundRobin::try_from(backends).unwrap(); // 配置健康检查 let health_check = health_check::TcpHealthCheck::new(); lb.set_health_check(health_check); lb.health_check_interval = Duration::from_secs(5); let lb = Arc::new(lb); let mut proxy = http_proxy_service( &server.configuration, MyProxy { lb: lb.clone() }, ); proxy.add_tcp("0.0.0.0:6199"); server.add_service(proxy); // 启动健康检查任务 tokio::spawn(async move { lb.run_health_check().await; }); server.run_forever(); }这段代码加了一个TCP健康检查，每5秒检查一次后端服务器的连通性。如果某台服务器连不上了，就会被标记为不健康，负载均衡器就不会再把请求发给它了。不过说实话，Pingora的健康检查功能目前还比较基础，不像Nginx那样有丰富的配置选项。毕竟是新项目，功能还在不断完善中。相信随着版本的更新，这块会越来越完善的。性能测试说了这么多，大家肯定关心性能到底怎么样。我这边简单做了个测试，用的wrk这个压测工具。测试环境：服务器：4核8G内存的云主机后端：简单的Nginx静态页面测试工具：wrk先测试Nginx：wrk -t4 -c100 -d30s http://127.0.0.1:80/然后测试Pingora代理到同样的后端：wrk -t4 -c100 -d30s http://127.0.0.1:6199/测试结果我就不贴具体数字了，因为测试条件有限，数据可能不太准确。但是从趋势来看，Pingora的性能确实不错，跟Nginx基本持平，某些场景下还稍微快一点。当然，这个测试比较简单，不能完全说明问题。实际生产环境的性能表现，还需要更全面的测试。不过从这个简单的测试来看，Pingora的性能确实有潜力。实际应用场景折腾了几天，我对Pingora有了更深的了解。那么问题来了，Pingora适合用在哪些场景呢？我觉得有这么几种情况可以考虑：需要高度定制化的代理逻辑。Nginx虽然功能强大，但是有些复杂的逻辑用配置文件很难实现，这时候用Pingora写代码就灵活多了。对性能要求极高的场景。虽然Nginx已经很快了，但是在某些极端场景下，Pingora可能还能再榨出一些性能来。想学习Rust网络编程的。Pingora的代码质量很高，是一个很好的学习案例。需要更好的安全性。Rust的内存安全特性，让Pingora在安全性方面有天然的优势。不过，Pingora目前还比较新，生态不如Nginx成熟，文档也不够完善。如果是关键业务，还是建议再观望一下，或者先在非核心业务上试试水。踩过的坑最后说说我在折腾过程中踩的一些坑，希望能帮大家少走弯路。第一个坑就是Rust版本问题。我一开始用的系统自带的Rust版本比较老，编译的时候各种报错。后来升级到最新版本才解决。所以大家一定要注意Rust版本，最好用最新的稳定版。第二个坑是依赖问题。Pingora依赖的一些库需要系统安装openssl-devel，不然编译会失败。CentOS用户记得yum install openssl-devel。第三个坑是文档不够详细。Pingora毕竟是个新项目，文档还不完善，有些功能得去看源码才能搞清楚怎么用。建议多看看GitHub上的examples目录，里面有一些示例代码可以参考[7]。第四个坑是性能调优。Pingora默认的配置可能不是最优的，需要根据实际情况调整。比如worker数量、连接池大小这些参数，都需要根据业务场景来优化。总结折腾了几天，总算把Pingora的基本功能都跑通了。整体感觉，Pingora确实是一个有潜力的项目，性能不错，代码质量也很高。不过目前还不够成熟，文档和生态都有待完善。对于想尝鲜的朋友，我建议可以先在测试环境玩玩，熟悉一下Pingora的架构和用法。但是要上生产环境的话，还是得谨慎，毕竟这是个新项目，稳定性还需要时间验证。我相信随着Pingora的不断完善，它会在反向代理领域占据一席之地。毕竟Cloudflare的技术实力摆在那里，而且他们自己就在大规模使用Pingora，这本身就是最好的证明。好了，今天的分享就到这里。如果你对Pingora感兴趣，或者在使用过程中有什么问题，欢迎留言交流。咱们一起学习，一起进步！如果觉得这篇文章对你有帮助，记得点赞、在看、转发三连哦！你的支持是我继续分享的动力！公众号：运维躬行录个人博客：躬行笔记
抓狂！明明开了系统代理，为啥CMD和PowerShell还是上不了网？深扒Windows代理机制的坑
昨晚本来打算早点睡的，结果刚躺下，手机就震动个不停。看了一眼，是咱们群里的小王发来的私信，一连串的表情包加感叹号，隔着屏幕都能感觉到他的崩溃。事情挺简单，也挺典型。这哥们最近在捣鼓那个啥“科学上网”工具，想在服务器上拉个Docker镜像，或者是用脚本去请求个外部API。他在Windows设置里把代理开得好好的，浏览器访问Google那是嗖嗖的快，一点毛病没有。可是一切换到CMD或者PowerShell里，立马就瞎火，curl 一下超时，wget 一下拒绝连接。他当时那个郁闷啊，跟我抱怨说：“这Windows是不是针对我？设置里都配好了，怎么到了命令行就成了聋子的耳朵——摆设？”其实吧，这事儿真不能怪Windows针对谁，这是很多刚入坑运维或者开发的小伙伴经常遇到的经典“玄学”问题。咱们平时在图形界面点点点，习惯了那种“所见即所得”的爽快，殊不知Windows底层的网络机制其实是个“双轨制”。今天咱们就趁着这个机会，好好扒一扒这里面的门道。别看这是个代理问题，里面涉及的底层逻辑，如果你没搞清楚，以后遇到更复杂的网络故障，一样得抓瞎。浏览器能通，为啥命令行不通？咱们得先把这个事儿理清楚。很多人有个误区，觉得我在“设置 -> 网络和 Internet -> 代理”里面填了个地址，这电脑上所有的流量就应该乖乖地走那个通道出去。其实不是的。Windows这系统，历史悠久，身上背着不少历史包袱。它的网络架构里，其实并存着两套主要的机制。一套是咱们老百姓常用的，叫 WinINET 代理设置；另一套，是更底层、更“极客”一点的 WinHTTP。你在Windows设置界面里配的那个代理，它实际上修改的是 WinINET 的配置。这玩意儿是给谁用的呢？主要是给浏览器用的，给那些跑在用户态的应用程序用的。比如你打开Edge、Chrome，或者是一些现代化的桌面软件，它们启动的时候，会自动去读WinINET的配置，“哦，原来用户设了代理，那我把请求发给代理服务器吧”。所以你的浏览器能通，是因为它听话，它主动去适配了这个配置。但是，问题来了。CMD和PowerShell，这俩货虽然长得黑乎乎的挺像黑客工具，但它们本质上只是命令行解释器。当你在这个黑框框里敲下 curl 或者 Invoke-WebRequest 的时候，它们默认是不会去读那个WinINET的配置的。这就好比你跟家里的管家（浏览器）说：“以后有人找我，先经过你筛选一下。” 管家听懂了，照做了。但是你忘了跟那个负责跑腿的快递员（CMD/PowerShell）说这事儿。快递员还是按照老规矩，直接去敲客户的门，结果客户（目标网站）在国外，他哪能敲得开？直接就被大防火墙给拦回来了。所以，CMD和PowerShell默认情况下，它是“直连”模式。它不知道你设了代理，它也不想管这闲事，它就觉得：“我有网关，我有DNS，我直接发请求不就完了吗？” 结果就是，请求发出去，石沉大海。那个让人误解的 curl 命令这里还得插一句，特别容易误导人的一个点。现在的Windows 10、Windows 11，居然自带 curl 命令了。以前咱们在Windows上用curl，那都是得自己去下载个exe，扔到System32里，或者配个环境变量。现在好了，微软良心发现，原生支持了。但是！这个坑就在于，PowerShell里有个别名机制。你在PowerShell里敲 curl，你以为你用的是那个大名鼎鼎的Linux下的curl工具吗？并不是。在PowerShell里，curl 其实是 Invoke-WebRequest 这个Cmdlet的别名。这就导致了一个很搞笑的现象：你在网上搜教程，人家Linux下是 curl -x socks5://...，你原封不动复制到PowerShell里跑，报错！参数不对！因为 Invoke-WebRequest 它是.NET Framework封装出来的东西，它的逻辑跟原生的curl.exe完全不是一码事。你要是真想在PowerShell里用原生的curl，得敲 curl.exe，或者把别名去掉。这一步搞不清楚，很多人配代理能配一天，最后把自己配懵了。我之前有个同事，就是死活搞不定这个，还在群里骂微软垃圾，说curl都不兼容。后来我让他敲个 Get-Alias curl 看一眼，他才恍然大悟，原来是“李鬼”遇上了“李逵”。既然默认不走，咱们怎么让它走？知道了原因，解决办法其实就呼之欲出了。既然CMD和PowerShell不会主动去读WinINET的配置，那咱们就得想办法把代理配置“喂”到它们嘴里。一般来说，有三种路子，咱们一种一种掰开了说。方法一：环境变量大法，简单粗暴这是最通用、最省事的办法。不管是CMD还是PowerShell，不管是curl还是git，绝大多数命令行工具，都会去读系统的环境变量。特别是 HTTP_PROXY 和 HTTPS_PROXY 这两个变量。咱们可以在当前的终端窗口里，临时设置一下。如果你是CMD：set HTTP_PROXY=http://127.0.0.1:7890 set HTTPS_PROXY=http://127.0.0.1:7890如果你是PowerShell：$env:HTTP_PROXY = "http://127.0.0.1:7890" $env:HTTPS_PROXY = "http://127.0.0.1:7890"这里的 127.0.0.1:7890 是你代理软件监听的地址和端口，这个每个人用的软件不一样，端口也不一样，得自己去看一眼。设置完这一步，你再试试敲命令。你会发现，原本不通的请求，现在“刷”的一下就通了。为啥？因为当你在终端里设了环境变量，当前这个窗口里启动的所有子进程，都能看到这两个变量。像Python的pip、Node的npm、Git、curl这些工具，它们在发起请求前，都会先去看看有没有这几个变量。如果有，它们就会乖乖地把请求发到这个代理地址去。这个方法的好处是“即插即用”，坏处是“一次性”的。你把这个窗口关了，下次再打开，这设置就没了。如果你想永久生效，那就得去系统环境变量里，把这俩变量加进去。但我一般不建议这么做，因为有些时候，你访问内网系统的时候，如果设了全局代理，反而会报错，还得回来删，挺麻烦的。方法二：命令行参数，精准打击如果你不想设环境变量，或者只是偶尔需要走代理，那可以在命令里直接带参数。如果你用的是PowerShell的 Invoke-WebRequest（也就是那个 curl 别名），它有个 -Proxy 参数：Invoke-WebRequest -Uri "https://www.google.com" -Proxy "http://127.0.0.1:7890"如果你用的是原生的 curl.exe，那就更熟悉了，跟Linux下一样：curl.exe -x "http://127.0.0.1:7890" https://www.google.com这种办法虽然稍微繁琐点，每次都要敲一长串地址，但是它胜在精准。不会影响其他的命令，也不会污染环境变量。对于那种只需要测试一下连通性的场景，这招最管用。方法三：netsh winhttp，系统层面的“硬核”设置刚才咱们说了，CMD和PowerShell不走系统代理，是因为它们没读WinINET。那有没有办法让它们读呢？或者说，有没有一种更底层的代理设置？这就得请出 netsh 这个神器了。Windows有一个叫 WinHTTP 的底层组件，很多系统服务（比如Windows Update）和命令行工具，其实是看这个组件的脸色的。我们可以通过管理员权限的CMD或PowerShell，执行下面这条命令：netsh winhttp set proxy 127.0.0.1:7890这命令一敲，相当于给系统的底层网络栈打了个补丁。你可以理解为，你在系统的大门口直接安了个安检机，不管是谁，只要是从WinHTTP这块出门的，统统都得走这个代理。设完之后，你可以敲 netsh winhttp show proxy 看一眼状态。但是，这个方法有个坑。有些老旧的程序，或者那些写死了直连逻辑的程序，它可能还是不走代理。而且，这个设置是全局的，如果你哪天把代理软件关了，忘了把这个设置撤回来，那你这台机器可能就彻底上不了网了，甚至连局域网都可能访问不了。因为它会死板地把所有请求都往那个已经关门的代理地址发。取消设置的命令是：netsh winhttp reset proxy。这个命令得记好了，关键时刻能救命。别忘了那个“Ping”的坑说到这儿，我还得提一个特别容易让人产生自我怀疑的事儿。很多时候，咱们配完代理，习惯性地想测试一下，于是敲了个 ping www.google.com。结果呢？大概率还是不通。这时候你心态可能就崩了：“我都设了环境变量了，我也设了netsh了，怎么还是不通？是不是代理软件坏了？”其实不是。这里有个非常底层的网络知识：Ping命令走的是ICMP协议，而绝大多数的代理软件，走的是HTTP/HTTPS或者SOCKS协议。代理，说白了，是应用层的事儿。你把你的HTTP请求封装一下，发给代理服务器，代理服务器帮你去跟目标服务器握手，再把数据拿回来给你。而Ping，它是网络层的东西，它发的是ICMP包。这个包没法被封装成HTTP请求。所以，你配了代理，Ping不通是正常的。Ping它根本就不支持代理！它就像是一辆不收费的救护车，不走普通车道（代理通道），它有自己的专用道，但可惜，防火墙把这条专用道给堵死了。所以，测试代理有没有通，千万别用Ping。得用 curl，或者直接用浏览器访问，或者用 Test-NetConnection （PowerShell自带的高级命令，可以指定端口）。比如：Test-NetConnection www.google.com -Port 443这个命令如果能通，说明TCP连接是通的，代理大概率是没问题的。实战中的那些“妖魔鬼怪”在实际的生产环境里，情况比这还要复杂。有时候你会发现，明明环境变量设了，curl也能通了，但是用 git clone 拉代码的时候，还是报错 Failed to connect to github.com port 443。这时候你得琢磨琢磨，Git这货是不是有它自己的小九九？确实，Git很多时候不走系统环境变量，它有自己的配置命令：git config --global http.proxy http://127.0.0.1:7890你看，这就是Windows生态的“百花齐放”（或者说“群魔乱舞”）。每个工具都有自己的脾气，都有自己的配置方式。NPM有 .npmrc，Pip有 pip.conf，Docker还得去改daemon.json。这就是为什么很多运维小伙伴觉得心累的原因。你以为你掌握了原理，结果换个工具，它又不按套路出牌。但万变不离其宗。只要你搞清楚了“WinINET是给浏览器用的”、“环境变量是给大多数CLI工具用的”、“WinHTTP是给系统服务用的”这三点，遇到问题，你就知道该往哪个方向去排查了。比如，你在CMD里跑不通，先看环境变量；环境变量设了还不通，看是不是工具本身有配置文件覆盖了环境变量；再不行，抓包看看。抓包也是个好习惯。装个Wireshark，或者用微软自带的 netsh trace start 抓个包看看。你会发现，当你没设代理的时候，你的机器在疯狂地往目标IP发SYN包，然后收不到ACK；当你设了环境变量后，你的机器开始往 127.0.0.1:7890 发数据了，这就说明代理配置生效了。总结一下今天聊得有点碎，主要是想把Windows这个代理机制给揉碎了讲清楚。咱们最后再捋一捋：系统设置的代理是给浏览器用的，那是WinINET，CMD和PowerShell这俩“老实人”默认是不认的。想让命令行走代理，最简单的办法是设环境变量 HTTP_PROXY 和 HTTPS_PROXY。Ping不通别慌，那是ICMP协议，跟HTTP代理不是一路人。工具特例，像Git、NPM这种，有时候得单独配，环境变量不一定好使。netsh winhttp 是把双刃剑，能解决很多系统级代理问题，但也容易把网搞挂，慎用。运维这活儿，很多时候就是在跟这些“默认行为”做斗争。系统觉得默认这样最安全、最快，但咱们实际场景里，往往需要咱们手动去干预。把这些细节搞懂了，以后再遇到这种“浏览器能通，命令行不通”的玄学问题，你就能淡定地端起杯子，喝口水，然后轻蔑一笑：“小样，又是环境变量没设对吧。”希望这篇文章能帮到像小王那样在深夜里抓狂的小伙伴。如果你觉得这篇文章帮你理清了思路，哪怕只是让你少走了一点点弯路，那我这大半夜敲的这几千字也就值了。路漫漫其修远兮，运维的路上坑多，但咱们填坑的过程，不就是成长的足迹嘛。公众号：运维躬行录个人博客：躬行笔记如果你觉得这篇文章对你有帮助，或者想看更多这种接地气的运维实战干货，别忘了点个关注，顺手转发给身边同样在“踩坑”的兄弟们。咱们下期见，在这个充满Bug的世界里，咱们一起“躬行”实践，寻找真理！
Nginx 为啥这么牛？深入聊聊事件驱动和非阻塞 I/O，看完这篇你就懂了
咱们干运维的，天天跟 Nginx 打交道，配置文件写得飞起，upstream、location 闭着眼睛都能配。但你要是问我，这玩意儿到底为啥能抗住几万甚至十几万的并发连接？以前我可能也就背背面试题：“哦，因为是异步非阻塞，基于事件驱动。”这话没毛病，但总觉得像隔靴搔痒，没说到点子上。今天咱们就撇开那些枯燥的教科书定义，用大白话，结合着咱们生产环境的实际情况，好好把这个事儿给唠透了。不然下次面试官问你“为什么 Nginx 不用多线程”，你总不能说“因为官方文档这么写的”吧。咱们先从“傻等”说起要搞懂 Nginx 的牛X之处，得先看看它的“前辈”或者说是反面教材是怎么干的。咱们以前用 Apache，或者自己写个简单的 Socket 服务器，最传统的模型是啥？就是 BIO（Blocking I/O），阻塞式 I/O。想象一下，你是个服务员（服务器进程），负责给客人点菜。传统的阻塞模式是这样的：你走到 1 号桌，拿着小本本准备点菜。1 号桌的大哥还在看菜单，没理你。这时候你干啥？你只能傻站在那儿等，手里的小本本也不能给别人用，整个人的状态就是“阻塞”了。这时候 2 号桌喊：“服务员，点菜！”你听到了吗？听到了，但你走不开啊，你被 1 号桌占用了。这就尴尬了。老板（系统管理者）一看这不行啊，一个服务员根本忙不过来，于是就开始招人。来一个客人，我就派一个专门的服务员盯着。这就是所谓的“多进程”或“多线程”模型。在早期的 Web 服务里，Apache 的 prefork 模式就是这么干的。来一个连接，fork 一个子进程。这招虽然解决了“一人一桌”的问题，但代价太大了。进程是啥？是资源的大户。每个进程都要占内存，都要占 CPU 时间片。你要是来个 1 万个并发，好家伙，1 万个进程在系统里跑，CPU 光忙着在这些进程之间切换上下文了，真正干活的时间没多少。这就好比饭店里挤满了服务员，客人没几个，服务员之间还互相撞来撞去，这生意还怎么做？这就是著名的 C10K 问题的根源。服务器资源是有限的，你用“人海战术”去堆连接，肯定得崩。换个思路：非阻塞 I/O既然“傻等”效率低，那咱们能不能不等？还是那个服务员的例子。这次你学聪明了，你走到 1 号桌，大哥还在看菜单。你这次不傻等了，你说：“大哥您先看，看好了喊我。”然后你立马转身去 2 号桌。这就是非阻塞 I/O。你尝试去读取数据（点菜），如果没有数据（客人没想好），你不阻塞在那儿，而是直接返回一个错误（EAGAIN/EWOULDBLOCK），告诉你“现在没数据，待会儿再来”。这听起来挺美好，但有个问题。你虽然不等了，可你不知道谁想好了啊。于是你只能像个没头苍蝇一样，一会儿跑去 1 号桌问“想好了吗？”，一会儿跑去 2 号桌问“想好了吗？”，一会儿跑去 3 号桌……如果你有 1 万桌客人，你光是跑路就把腿跑断了，CPU 也在不停地轮询，空转，这也是一种浪费。这叫非阻塞 I/O 的忙轮询。真正的大招：事件驱动机制这时候，如果有个大堂经理（操作系统内核），手里拿个大喇叭，这事儿就解决了。你不用一个个去问了。你就在那儿站着喝茶。哪桌客人想好了，经理就喊一声：“5 号桌要点菜！”这时候你过去处理一下。这就是事件驱动。在计算机的世界里，这个“大堂经理”就是操作系统内核提供的 I/O 多路复用机制。在 Linux 上，这玩意儿叫 epoll；在 BSD/Mac 上叫 kqueue；在老一点的系统上还有 select 和 poll。Nginx 之所以牛，核心就在这儿。它不搞人海战术，它搞的是“精英战术”。一个 worker 进程，就能处理成千上万个连接。咱们来拆解一下 Nginx 的工作流，看看它是怎么把这几个概念揉在一起的。1. Master 和 Worker 的分工Nginx 启动后，你会看到一个 master 进程和多个 worker 进程。Master 就像饭店老板，他负责管理 Worker，比如读取配置、绑定端口、平滑重启这些管理活儿，他不具体接客。真正干活的是 Worker 进程。重点来了，Worker 进程通常只有几个，一般咱们配置成跟 CPU 核心数一样多。为啥？因为少了浪费多核性能，多了反而增加上下文切换的开销。2. 抢占式处理Worker 进程是怎么工作的？它们都监听着同一个端口（比如 80 或 443）。当一个新连接来了，谁去接？这里有个细节，Nginx 利用了一把锁。当连接进来时，哪个 Worker 进程抢到了锁，哪个就去处理这个连接。一旦连接建立好了，这个连接后续的所有读写操作，都由这个 Worker 全权负责。这就避免了多个进程瞎抢一个连接导致的“惊群效应”。所谓惊群，就是一嗓子喊过来，所有 Worker 都醒过来抢，结果只有一个能抢到，其他的白醒了，这很伤性能。Nginx 处理得很漂亮。3. 事件循环这是 Nginx 的灵魂。每个 Worker 进程里，都跑着一个死循环，大概逻辑是这样的：while (true) { // 1. 等待事件发生 events = epoll_wait(连接池, 超时时间); // 2. 遍历处理事件 for (event in events) { if (event.type == '读事件') { // 处理读数据，比如接收 HTTP 请求 handle_read(event); } else if (event.type == '写事件') { // 处理写数据，比如返回 HTML 页面 handle_write(event); } } }你看懂了吗？它不是傻等某一个连接，而是通过 epoll_wait 这个系统调用，一次性问操作系统：“大哥，这 1 万个连接里，有哪些是有动静的？”操作系统内核维护着一个红黑树（存储所有连接）和一个链表（存储就绪的连接）。当网卡收到数据包，内核一看，哎，这是 8888 端口的连接，就把这个连接塞进就绪链表里。然后 Nginx 的 Worker 进程醒来，拿到的就是这一小撮“有事儿”的连接。这就好比大堂经理手里有个名单，他只喊那些举手的客人。你不需要去摇每个客人的肩膀问“吃不吃”，效率直接起飞。为啥这就能抗高并发？咱们得算笔账。假设你有一台 8 核的服务器，跑了 8 个 Nginx Worker 进程。此时有 10,000 个并发连接。如果是 Apache (prefork)，你可能需要维护几千个进程。每个进程占 10MB 内存（假设），那就是几十 GB 的内存。CPU 在这几千个进程间切换，缓存命中率低得令人发指。而在 Nginx 模型下：内存占用极低： 只有 8 个进程。每个连接只需要一点点内存来存状态（比如 socket 描述符、请求头 buffer）。这 10,000 个连接，大部分可能都是闲着的（比如用户在看网页，没点下一页），它们不占 CPU，只占一点点内存。这叫“闲连接不费电”。CPU 利用率极高： Worker 进程醒过来，就是干活。处理完一个事件，立马处理下一个，绝无空闲。没有无谓的上下文切换。这就像一个超级服务员，手里拿着对讲机。哪个桌按了服务铃，对讲机里就会响，他直接过去处理。处理完按个钮，继续等下一个。你说这效率能不高吗？一个容易踩的坑：阻塞操作说到这儿，可能有人会问：“既然 Nginx 这么强，为啥我有时候看 CPU 飙升，或者服务卡死？”这就得提提事件驱动模型的一个天敌：阻塞调用。Nginx 的 Worker 是单线程的（虽然可以多线程，但主要逻辑是单线程事件循环）。这意味着，一旦这个线程被卡住了，这Worker 管理的几万个连接就全废了。比如说，你在 Nginx 里写了个 Lua 脚本，或者用了某个第三方模块，里面搞了个长时间的数学计算，或者更糟糕的，去调了一个阻塞式的磁盘 I/O（虽然 Nginx 对文件 I/O 有处理，但在高并发下，磁盘慢也是大坑）。想象一下，那个超级服务员正在处理 5 号桌的点菜，突然 5 号桌让他帮忙算个“圆周率后一万位”。服务员要是真在那儿算，后面举手的 100 桌客人谁管？所以，Nginx 的设计哲学里，所有的操作都必须是非阻塞的。DNS 解析、磁盘读取、网络请求，能异步的全异步。如果必须阻塞，那也得尽量快，或者扔给别的线程池去干。这就是为什么我们在生产环境里，尽量别在 Nginx 里搞复杂的逻辑运算，除非你用 OpenResty 这种把 LuaJIT 集成得很好的方案，而且还得确保你用的库都是非阻塞的。再聊聊 Nginx 的细节优化光懂原理还不够，生产环境里咱们得落地。咱们看 Nginx 配置里几个跟这模型相关的参数，改好了性能翻倍。worker_processes这个参数通常设为 auto。以前咱们喜欢设成具体的数字，比如 4 或者 8。现在 Nginx 聪明了，设成 auto 它自己会检测 CPU 核心数。这对应了咱们上面说的，一个核心跑一个 Worker，减少 CPU 切换开销。worker_connections这个是单个 Worker 进程能处理的最大连接数。默认好像是 1024，这肯定不够看。咱们生产环境一般设成 65535，甚至更高。这取决于你的系统文件句柄限制。这里有个公式：最大并发连接数 = worker_processes * worker_connections。但是！注意这个但是。如果你开启了 keepalive（长连接），一个连接可能占用的资源时间更长。而且，如果做反向代理，每个请求进来，Nginx 还要跟后端服务器建立一个连接，所以实际上一个客户端请求可能占用 2 个连接数（前端一个，后端一个）。算并发量的时候心里得有数，别配小了导致 502 错误，那就尴尬了。use epoll在配置文件的 events 块里，咱们经常会看到 use epoll;。其实在 Linux 2.6+ 内核上，Nginx 默认就会用 epoll。写上这行主要是图个心安，显式指定一下。如果你是在 BSD 系统上，那就得用 kqueue。这就是告诉 Nginx：“嘿，用那个最高效的大堂经理模式。”accept_mutex这个就是刚才说的“锁”。默认是开启的。如果你的连接并发量特别特别大，瞬间涌进来几万个，这时候开锁反而可能成为瓶颈。但在绝大多数普通场景下，开着它能防止惊群，保护 CPU，建议保持默认或者按需关闭测试一下。真实案例：一次故障排查前两年有个金融客户，系统上线前压测。他们买的机器挺贵，配置特高，32 核 64G 内存。结果一压测，QPS（每秒查询率）死活上不去，卡在 5000 左右，CPU 使用率才 30%，看着让人抓狂。我上去一看，Nginx 的 error.log 里没报错，系统负载也低。这就典型的“有劲使不出”。后来排查发现，他们的后端服务响应特别慢，要 200ms。而 Nginx 的 worker_connections 设置得特别小，才 1024。咱们算算账。QPS 5000，每个请求耗时 200ms。那意味着同一时刻，系统里积压的请求数大概是 5000 * 0.2 = 1000 个。如果 Nginx 只有几个 Worker，每个 Worker 连接数才 1024，眼看就要爆了。而且因为后端慢，Nginx 的 Worker 进程虽然不费 CPU，但是占用的连接槽位被填满了，新的请求进不来，只能在队列里排队或者被丢弃。解决办法很简单，把系统的 ulimit 打开，文件句柄数设到 65535，然后把 Nginx 的 worker_connections 调到 3 万左右。重载配置，再压。QPS 瞬间飙到 2 万，CPU 这才呼呼地转起来。这就是典型的配置没跟上模型原理，硬件再好也白搭。总结一下说了这么多，咱们总结回顾一下。Nginx 之所以能成为 Web 服务器的扛把子，核心就在于它打破了传统的“一个进程处理一个请求”的线性思维。它采用了事件驱动和非阻塞 I/O 模型。它不再傻傻地等待 I/O 操作完成，而是利用操作系统的 epoll 机制，像那个精明的大堂经理一样，只关注“有事儿”的连接。这让它在处理海量并发连接时，能够用极少的 CPU 和内存资源，维持住极高的吞吐量。对于咱们运维来说，理解这个模型不仅仅是面试需要。在排查性能瓶颈、调优参数、甚至做架构选型的时候，这些底层的知识才是咱们判断问题的依据。别光盯着配置文件里的 server_name 和 root 指令，多看看 events 块里的东西，那才是 Nginx 的心脏。下次如果再遇到服务器负载不高但请求超时的情况，不妨往这方面想想，是不是连接数满了？是不是哪里阻塞了？技术这东西，原理通了，很多现象就解释得通了。希望这篇大白话能帮兄弟们把这些概念彻底嚼碎了咽下去。公众号：运维躬行录个人博客：躬行笔记
别再折腾 iptables 了！用 cproxy + wstunnel 轻松搞定 Linux 透明代理，内网穿透稳如老狗
最近有个朋友找我帮忙，说他们公司有个老旧的 Linux 服务器，跑着一堆内部服务，现在想让这些服务能通过公网访问，但又不能直接暴露端口，防火墙策略也卡得死死的。常规的反向代理方案搞不定，因为应用层根本没做任何代理适配，总不能一个个改代码吧？这时候我就想到了“透明代理”——对应用完全无感，流量在系统层面就被劫持转发，上层程序压根不知道自己走的是代理。听起来很玄乎？其实没那么复杂，关键就俩工具：cproxy 和 wstunnel。今天这篇，我就手把手带你把这套组合拳打出来，顺便聊聊我在生产环境里踩过的坑、调过的参，保证你照着做就能跑起来。先说说 cproxy 是啥cproxy 不是那种大厂开源的明星项目，它是个轻量级的透明代理工具，专门干一件事：把本地发出的 TCP 流量重定向到指定的 SOCKS5 或 HTTP 代理。它底层用的是 Linux 的 TPROXY 模块（不是 REDIRECT！），所以能保留原始目的地址，特别适合做透明代理。很多人一听到“透明代理”就想到 iptables + redsocks / proxychains，但那些方案要么配置复杂，要么兼容性差。cproxy 的优势在于：配置简单、性能不错、支持 UDP（虽然我们这次主要用 TCP），而且它能和 wstunnel 完美配合——后者能把任意 TCP 流量封装成 WebSocket，轻松绕过企业防火墙。场景还原：为什么需要这个组合？假设你有台内网机器 A（192.168.1.100），上面跑了个 Web 服务（比如 8080 端口）。现在你想让外网用户通过你的公网服务器 B（比如 example.com）访问这个服务，但：服务器 A 不能直接开公网 IP防火墙只允许出站 443（HTTPS）你不想改 Web 应用的代码或配置这时候，常规思路是用 frp/ngrok，但它们依赖中心节点，而且有些环境连 frp 的自定义端口都不让连。而 wstunnel 可以把流量伪装成正常的 WebSocket（走 443），再配合 cproxy 做透明劫持，整个链路就通了。实操开始：部署 wstunnel先在公网服务器 B 上起一个 wstunnel 服务端。wstunnel 的 GitHub 仓库是 https://github.com/erebe/wstunnel，安装很简单，直接下二进制就行：# 在服务器 B（公网）上执行 wget https://github.com/erebe/wstunnel/releases/latest/download/wstunnel-linux-amd64 -O /usr/local/bin/wstunnel chmod +x /usr/local/bin/wstunnel然后启动服务端，监听 443 端口（假装是 HTTPS）：wstunnel server --port 443 --restrictTo "127.0.0.1:8080" wss://example.com--restrictTo 是安全限制，只允许转发到本机的 8080 端口（后面我们会把内网流量打到这里）。注意：如果你已经有 Nginx 占用 443，可以用 --port 8443，然后用 Nginx 反代 /wstunnel 路径到 8443，这里为了简化先直接占 443。内网机器 A：部署 cproxy + wstunnel 客户端现在回到内网机器 A（192.168.1.100）。首先装 cproxy。它没有官方 release，但源码编译非常快：git clone https://github.com/txthinking/cproxy.git cd cproxy make sudo cp cproxy /usr/local/bin/接着，在 A 上启动 wstunnel 客户端，连接到公网服务器 B：wstunnel client wss://example.com --udp --tcp -L 127.0.0.1:1080这行命令的意思是：把本地 1080 端口作为 SOCKS5 代理，所有流量通过 wss://example.com 隧道转发。--udp --tcp 表示同时支持两种协议（虽然我们主要用 TCP）。现在，1080 端口就是一个“出口”，所有发往这里的流量都会被 wstunnel 打包成 WebSocket，从 443 出去。关键一步：用 cproxy 把流量劫持到 1080接下来就是透明代理的核心了。cproxy 的作用是：监听一个本地端口（比如 12345），把所有发往它的 TCP 连接，通过 SOCKS5 代理（127.0.0.1:1080）转发出去。启动 cproxy：cproxy -l 12345 -a 127.0.0.1:1080-l 是本地监听端口，-a 是上游 SOCKS5 地址。现在，只要把目标流量重定向到 12345，它就会自动走代理。用 iptables 把流量“骗”到 cproxy别慌，这里的 iptables 规则其实就两行，比你想的简单多了。我们要劫持的是本机发出的、目标为内网 Web 服务（192.168.1.100:8080）的流量？不对！等等，逻辑反了。实际上，我们的目标是：当有人访问公网服务器 B 的 443 时，流量最终要打到内网 A 的 8080。但 A 本身并不知道这个请求，所以我们需要在 A 上“假装”自己收到了这个请求。更准确地说：公网 B 收到请求后，通过 wstunnel 把数据转发到 A 的 127.0.0.1:8080（由 wstunnel server 的 --restrictTo 决定）。但 A 上并没有服务监听 127.0.0.1:8080 啊！这时候，我们需要在 A 上做一件事：把发往 127.0.0.1:8080 的流量，重定向到真正的 Web 服务（比如 192.168.1.100:8080）？也不对。我把自己绕晕了，重新理一遍：用户访问 https://example.com请求到达服务器 B 的 443（wstunnel server）wstunnel server 把流量转发到 本机的 127.0.0.1:8080（因为 --restrictTo=127.0.0.1:8080）但我们希望这个流量最终到达 内网机器 A 的 8080所以，服务器 B 上还需要一个本地转发！比如用 socat 或 nginx 把 127.0.0.1:8080 的流量打到 wstunnel client 的某个端口？太乱了。其实更优雅的做法是：让 wstunnel server 直接把流量转发到内网 A 的公网隧道出口。但 A 没有公网 IP 啊。啊！我犯了个经典错误——应该让 wstunnel client 主动建立反向隧道。正确姿势：在内网 A 上，wstunnel client 启动时，不仅要开 SOCKS5（1080），还要开一个 本地端口映射，把远程的某个端口映射到本地服务。比如：# 在内网 A 上执行 wstunnel client wss://example.com -L 127.0.0.1:1080 -R 8080:127.0.0.1:8080-R 8080:127.0.0.1:8080 表示：在 server 端监听 8080，所有流量转发到 client 的 127.0.0.1:8080。但 wstunnel 的 -R 参数默认是绑定 127.0.0.1，外部无法访问。所以我们需要改 server 启动命令：# 服务器 B 上 wstunnel server --port 443 --hostReverseProxy wss://example.com然后 client 用：wstunnel client wss://example.com -L 1080 -R :8080:127.0.0.1:8080这样，server 会把 8080 绑定到 0.0.0.0，外部就能访问了。但问题来了：如果我想透明代理本机的所有出站流量呢？比如让 curl、wget 自动走代理？这才是 cproxy 的主战场。回归主题：用 cproxy 做本机透明代理假设你现在在内网机器 A 上，想让所有对外的 HTTP/HTTPS 请求都走代理（比如访问 GitHub、Docker Hub），但又不想每个命令都加 proxy 参数。这时候，cproxy + iptables 就派上用场了。步骤：启动 wstunnel client，开 SOCKS5（1080）启动 cproxy，监听 12345，上游指向 1080用 iptables 把本机的出站流量重定向到 12345具体命令：# 清空规则（测试环境） iptables -t nat -F # 跳过本地回环和局域网 iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN iptables -t nat -A OUTPUT -d 192.168.0.0/16 -j RETURN iptables -t nat -A OUTPUT -d 10.0.0.0/8 -j RETURN iptables -t nat -A OUTPUT -d 172.16.0.0/12 -j RETURN # 把其他所有 TCP 流量重定向到 cproxy iptables -t nat -A OUTPUT -p tcp -j REDIRECT --to-port 12345注意：这里用的是 REDIRECT，不是 TPROXY。因为 cproxy 默认支持 REDIRECT 模式（虽然它也支持 TPROXY，但 REDIRECT 更简单）。现在，你在 A 上执行：curl https://httpbin.org/ip返回的 IP 就是公网服务器 B 的 IP！说明流量已经通过 wstunnel 走出去了。但有个坑：DNS 请求不会被重定向！因为 DNS 是 UDP，而上面的规则只处理 TCP。解决办法有两个：强制使用 TCP DNS（比如 curl --dns-servers 8.8.8.8 不行，得改 resolv.conf 加 options use-vc）用 cproxy 的 UDP 支持（启动时加 -u），再配 iptables 重定向 UDP 53不过大多数场景下，只要 TCP 能代理，DNS 可以单独配 DoH 或 DoT，或者直接用 8.8.8.8（如果网络允许）。性能和稳定性怎么样？我在一台 2 核 4G 的云服务器上跑过这个组合，单连接下载速度能达到 80Mbps（受限于 WebSocket 封装开销），延迟增加约 30~50ms。对于管理后台、API 调用完全够用。稳定性方面，wstunnel 支持自动重连，cproxy 也很轻量（内存占用 <10MB）。唯一要注意的是：不要用在高并发场景，毕竟它不是为百万连接设计的。为什么不用 clash / sing-box？有朋友问：现在 clash meta、sing-box 这么强，还搞 cproxy 干嘛？道理很简单：轻量、无依赖、一行命令搞定。clash 要配 YAML，sing-box 更复杂，而 cproxy + wstunnel 总共就三个命令，适合嵌入脚本、容器、临时调试。而且，很多老旧系统（比如 CentOS 6）跑不了 Rust 程序，但 cproxy 是 Go 编译的静态二进制，扔哪都能跑。最后提醒几个坑SELinux / AppArmor：某些系统会阻止 iptables 或 socket 操作，记得关掉或加规则。内核版本：TPROXY 需要较新的内核（>=3.0），但 REDIRECT 模式基本通吃。wstunnel 的证书验证：如果用自签名证书，client 要加 --insecurecproxy 默认只监听 127.0.0.1，如果想让局域网其他机器用，得改源码或加参数（目前不支持 bind 0.0.0.0）总结一下wstunnel：把 TCP/UDP 流量封装成 WebSocket，轻松穿透防火墙cproxy：轻量级透明代理，把本地流量重定向到 SOCKS5iptables：最后一公里，把流量“骗”到 cproxy三者结合，你就能在几乎任何 Linux 环境下实现“无感代理”，无论是内网穿透、科学上网，还是 CI/CD 中的依赖下载，都稳得很。下次再遇到“只能出 443”、“不能改应用配置”的需求，别再抓耳挠腮了，试试这个组合，说不定十分钟就搞定。如果你觉得这篇文章帮你省了三天调试时间，不妨点个赞、转给那个还在手动配 iptables 的同事。也欢迎留言告诉我你的使用场景，说不定下期就写你遇到的奇葩问题！公众号：运维躬行录个人博客：躬行笔记
那个让Claude直接操控我电脑的MCP协议，到底是个什么鬼东西？
昨晚熬夜折腾了一宿，头发又掉了几十根，总算把Anthropic搞的那个MCP（Model Context Protocol）给摸透了。说实话，刚听到这个词的时候，我内心是拒绝的。这年头，概念造得比母猪下崽还快，今天Agent，明天RAG，后天又来个什么Chain。咱们干运维的，最怕这种听着高大上、落地一地鸡毛的玩意儿。但是！这次好像真不一样。当我第一次在Claude Desktop里，直接让它“读取我本地的Nginx日志并分析502错误原因”，然后它真的直接读了我硬盘里的文件并且给出分析结果时，我承认，我有点起鸡皮疙瘩了。这玩意儿不是单纯的插件，它更像是一个给大模型开的“USB接口”。以前我们怎么用AI？复制粘贴代码，或者把日志脱敏了再喂给它，要么就是费劲巴拉地写个脚本调用API。现在好了，MCP直接把这个过程标准化了。废话不多说，今天咱们就来扒一扒这个MCP到底是个啥，以及作为一个手痒的IT人，怎么自己动手撸一个MCP服务，让AI变成你的私人定制运维专家。啥是MCP？别给我整虚的官方文档写得那叫一个晦涩，什么“开放标准”、“上下文交互”，看着就困。其实你把它想象成电脑的USB协议就完事了。以前（没有MCP的时候），你想让ChatGPT连你的数据库，你得自己写个特殊的插件；你想让Claude连你的Jira，又得写另一套代码。这就好比以前的手机充电口，诺基亚是圆孔，三星是扁口，苹果是宽口，乱得一塌糊涂。MCP就是Type-C接口。只要你的数据源（比如你的运维平台、你的本地文件夹、你的Postgres数据库）实现了MCP协议，不管是Claude，还是最近很火的Cursor编辑器，亦或是Zed，都能直接插上用。这里面有两个核心角色：MCP Host（主机）：比如Claude Desktop客户端，或者Cursor编辑器。它是大脑，负责提问和处理。MCP Server（服务端）：这就是我们要开发的东西。它负责干活，比如去查数据库、去执行个ls -l、去读个文件。这一打通，事情就变得有意思了。你可以写个Server，专门查你们公司的服务器负载。然后你在Claude里问：“现在哪台机器CPU飙高了？”Claude就通过MCP协议，调你的Server查数据，然后告诉你结果。听起来是不是有点像API？是，也不是。它比API更“智能”，因为它把工具的定义、资源的模板都标准化了，AI一看就知道怎么用，不需要你写那种几百行的Prompt去教它。准备动手：环境这块得支棱起来咱们是搞技术的，光说不练假把式。只要你会点Python，这事儿就不难。虽说官方也支持TypeScript，但咱们运维圈子，Python才是亲爹。先把环境弄好。我假设你电脑上已经有Python 3.10以上了。没有的话自己去面壁。我最近特别喜欢用 uv 这个包管理工具，速度快得离谱，比pip那种老牛拉破车强多了。当然，你习惯用pip也行。先建个目录，假装我们要搞一个“运维百宝箱”：mkdir ops-mcp-server cd ops-mcp-server python -m venv .venv source .venv/bin/activate # Windows下是 .venv\Scripts\activate接下来安装核心库。Anthropic官方出了个SDK，叫 mcp。pip install mcp[cli]装完之后，咱们别急着写代码。很多教程一上来就让你写类、写继承，那太累了。咱们用个偷懒的方法——FastMCP。这玩意儿就像FastAPI一样，装饰器一挂，代码跑起来，贼爽。对了，还得装个 uvicorn 之类的吗？其实MCP通常是走标准输入输出（Stdio）通信的，特别是在本地跑的时候，这样最安全，不需要暴露端口。这也是我觉得它设计得精妙的地方——它默认不是为了做成HTTP微服务，而是做成一个本地的“插件进程”。第一个吃螃蟹：写个“查进程”的工具咱们的目标很简单：让Claude能查我电脑上运行的进程。新建个文件 server.py。代码大概长这样，逻辑我都写注释里了，仔细看：from mcp.server.fastmcp import FastMCP import psutil # 初始化一个Server，名字随便起，就叫 "OpsTools" mcp = FastMCP("OpsTools") @mcp.tool() def list_top_processes(limit: int = 5) -> str: """ 列出当前系统占用CPU最高的几个进程。 Args: limit: 要显示的进程数量，默认为5个 """ # 既然是实战，就得真去读系统信息 # psutil这库搞运维的应该都熟 procs = [] for proc in psutil.process_iter(['pid', 'name', 'username', 'cpu_percent']): try: # 这是一个坑，psutil获取cpu_percent有时候第一次是0，但这只是个demo，无所谓了 procs.append(proc.info) except (psutil.NoSuchProcess, psutil.AccessDenied): pass # 按CPU使用率降序排序 procs.sort(key=lambda x: x['cpu_percent'], reverse=True) # 咱们搞得人性化一点，格式化一下输出 result = "TOP CPU 进程列表:\n" result += f"{'PID':<10} {'名称':<20} {'用户':<15} {'CPU%':<10}\n" result += "-" * 60 + "\n" for p in procs[:limit]: result += f"{p['pid']:<10} {p['name']:<20} {p['username']:<15} {p['cpu_percent']:<10}\n" return result @mcp.tool() def kill_process_by_pid(pid: int) -> str: """ 通过PID杀掉某个进程（慎用！）。 """ try: p = psutil.Process(pid) p_name = p.name() p.terminate() # 温柔一点，先terminate return f"成功终止进程: {p_name} (PID: {pid})" except psutil.NoSuchProcess: return f"找不到PID为 {pid} 的进程，是不是已经挂了？" except psutil.AccessDenied: return f"权限不足！你是不是没sudo？杀不掉 PID {pid}" except Exception as e: return f"出事了，杀进程失败: {str(e)}" # 这一行是启动的关键 if __name__ == "__main__": mcp.run()代码很简单对吧？这里面有个特别关键的点，就是 @mcp.tool() 这个装饰器。你不需要写任何Prompt告诉AI“我有这个工具，参数是什么什么”。FastMCP会自动读取你的函数签名（Type Hints）和文档字符串（Docstring），自动生成MCP协议所需的Schema。也就是说，你注释写得越清楚，AI用得越顺手。这简直是“代码即文档”的最高境界啊。怎么连上Claude？这才是坑最大的地方代码写好了，你直接运行 python server.py，你会发现...什么反应都没有，或者程序直接挂在那不动。别慌，这是正常的。因为它是设计成通过Stdio（标准输入输出）交互的，它在等指令呢。要让Claude Desktop用上它，你得去改配置文件。这个配置文件通常藏得很深。Mac用户在：~/Library/Application Support/Claude/claude_desktop_config.jsonWindows用户在：%APPDATA%\Claude\claude_desktop_config.json你要是找不到，就在Claude Desktop的菜单里找找“Settings” -> “Developer”之类的选项，通常有个按钮能直接打开这个配置目录。打开这个JSON文件，加上这一段：{ "mcpServers": { "my-ops-tools": { "command": "/你的路径/ops-mcp-server/.venv/bin/python", "args": ["/你的路径/ops-mcp-server/server.py"] } } }注意！敲黑板了！这里千万别直接写 python。一定要写你那个虚拟环境里的绝对路径！因为Claude启动的时候，它不知道你的环境变量是啥，如果用系统的python，很可能找不到 mcp 或者 psutil 库，然后就默默失败，连个报错都没有，急死你。Windows用户记得要把路径里的反斜杠 \ 写成双斜杠 \\，不然JSON解析会报错。这种低级错误我当年刚入行的时候没少犯，现在想起来还觉得蠢。改完配置文件，重启Claude Desktop。这时候，你会看到输入框旁边多了一个小锤子的图标（或者是类似连接的图标）。点开它，如果你看到了 list_top_processes 和 kill_process_by_pid，恭喜你，你的第一个MCP Server跑通了！见证奇迹的时刻现在，你在对话框里输入：“帮我看看现在电脑上哪个进程最吃CPU？”你会看到Claude稍微停顿一下（它在思考用哪个工具），然后它会显示“Used list_top_processes”。接着，它会把你代码里返回的那个格式化好的字符串展示出来，甚至还会基于这个结果给你一点建议。比如它可能会说：“兄弟，你的 chrome 占用太高了，要不要关几个标签页？”接着你回：“把那个PID为 12345 的进程给我杀了。”它就会调用 kill_process_by_pid。这种感觉，怎么形容呢？就像是你给AI装了一双手。以前它只能动嘴皮子，现在它能干活了。进阶：Resource和Prompts又是啥？光有Tool（工具）还不够。MCP协议里还有两个重要的概念：Resources（资源）和Prompts（提示词）。Resources 理解起来就是“数据源”。比如你有一个经常变动的日志文件，或者一个API返回的实时JSON数据。你可以把它定义成一个Resource。@mcp.resource("file://logs/app.log") def read_log() -> str: # 实际上这里可以写得很复杂，比如只读最后100行 with open("app.log", "r") as f: return f.read()这样，AI就可以像订阅RSS一样，随时获取这个资源的内容，而不需要通过“调用函数”这种动作。它更像是一种被动的上下文注入。Prompts 则是给人用的模板。比如你经常让AI帮你写SQL语句。你可以定义一个Prompt叫 sql-expert，里面预设好你们公司的表结构描述。在MCP里定义好Prompt后，你在Claude的界面里就能直接选这个Prompt，省去了每次都要复制粘贴一大堆背景信息的麻烦。说实话，对于咱们个人开发者或者小团队运维来说，Tool 是最有用的。Resource和Prompt在大规模协作的时候才显威力。遇到的那些坑（血泪史）别看我写得挺顺，实际开发的时候全是坑。调试极其困难：因为Server是Claude作为子进程启动的，你看不到它的控制台输出。如果你的代码里有 print("hello")，完了，这会破坏Stdio的通信协议，导致连接直接断开。解决办法：用 sys.stderr.write() 来打印日志，或者专门配个logging写文件。千万别往stdout打杂七杂八的东西。超时问题：如果你的工具执行时间太长（比如去爬个网页），Claude可能会觉得Server挂了。解决办法：搞异步。FastMCP支持 async def。尽量把耗时的操作做成异步的。依赖地狱：刚才说了，路径一定要写对。而且，如果你用了像 pandas 这种重型库，加载时间过长，有时候也会导致连接超时。安全隐患：兄弟们，这玩意儿是直接在你的环境里跑代码啊！你要是写了个 rm -rf / 的工具，然后AI抽风了给你执行了，那画面太美我不敢看。忠告：所有的写操作、删除操作，一定要在代码里做二次确认，或者干脆只给AI只读权限。别太相信大模型的节操，它们有时候会幻觉，明明你没让它删，它觉得删了比较好，就给你动手了。这东西对运维意味着什么？MCP这东西，可能会彻底改变我们做运维工具的方式。以前我们做个运维平台，得画前端页面，得写后端API，得做鉴权，得写文档教同事怎么用。以后呢？我可能只需要写几个Python函数，把它们暴露成MCP Server。同事们想查数据、想重启服务，直接对着他们的Claude或者IDE说话就行了。“帮我把测试环境的数据库回滚到昨天的快照。”“帮我查一下昨天晚上8点到9点的错误日志。”根本不需要GUI界面了！Chat Interface（聊天界面）就是新的GUI。而且，这东西还能联动。比如你用Cursor写代码，你可以搞一个Gitlab的MCP Server。你在写代码的时候，直接问Cursor：“这个函数是谁在哪次提交里改的？”Cursor直接通过MCP调Gitlab API查出来告诉你。这效率，提升的不是一星半点。当然，现在MCP还在早期阶段，生态还比较乱。但我敢打赌，2026年这玩意儿会遍地开花。各种SaaS软件都会自带MCP接口。总结这篇博客写得有点散，主要是想把那种探索过程中的兴奋和踩坑的痛感分享给你们。简单回顾一下：MCP是AI连接数据的USB接口。用Python的 mcp 库配合 FastMCP 可以快速开发。通过 claude_desktop_config.json 进行配置。切记别用 print 调试，路径要写绝对路径。安全第一，别给AI太大权限。这就是我眼里的MCP。它不神秘，它就是个干活的工具。但是把它用好了，你的AI就不再是个只会陪聊的傻白甜，而是一个手里拿着扳手和螺丝刀的真正助手。如果你也想试试，赶紧动手吧。哪怕写个简单的“查询本地天气”或者“读取每日记事本”的工具，那种成就感也是爆棚的。行了，今天就唠到这。代码要是跑不通，或者遇到了奇葩报错，欢迎在后台留言，咱们一起脱发，一起进步。最后，如果你觉得这篇文章对你有那么一点点启发，或者只是觉得我吐槽得比较在理，麻烦动动发财的小手：公众号：运维躬行录个人博客：躬行笔记
什么是虚拟机？虚拟机（VM）是由软件创建和运行的物理计算机的虚拟化版本。本质上，它就是一台“云端计算机”。创建步骤创建虚拟机：在 Azure 门户中搜索虚拟机。点击创建虚拟机，在下拉菜单中选择虚拟机。在“基本信息”选项卡上，选择现有资源组或为虚拟机创建一个新的资源组。为虚拟机创建一个实例名称。选择一个地区。如有必要，请选择可用区；否则，请选择“不需要基础架构冗余”。选择图像和兼容的安全类型“标准”。创建管理员帐户。选择用户名和密码。创建入站端口：HTTP (80) 和 RDP。确认资格。转到监控选项卡，禁用系统启动诊断。转到标签选项卡，为虚拟机创建一个标签。然后进行审核和创建。点击创建。部署完成后，转到资源，然后转到网络，增加虚拟机的屏幕使用时间。保存更改。进入虚拟机概览并连接虚拟机。请前往下载 RDP 文件。下载完成后，进入电脑的下载文件夹，通过远程桌面协议进行部署，然后输入你在管理员部分创建的密码。连接上创建的win10如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记