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如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记
最近有接触到nodejs，今天就来讲讲在 Windows 上安装 Node.js ，直接通过命令行安装不再需要点击安装程序——这要归功于微软官方的 Windows 程序包管理器 winget 。以下是如何在几秒钟（三秒真男人！！！）内完全通过终端使用 npm 安装 Node.js 的方法。1. 打开 PowerShell按 Win + X → 选择 Windows PowerShell（管理员） 或终端（管理员） 。2. 查找可用的 Node.js 版本首先，列出所有可用的 Node.js 软件包：winget search OpenJS.NodeJS你会看到类似这样的结果：名称 ID 版本 源 ----------------------------------------------- Node.js OpenJS.NodeJS 25.1.0 winget Node.js 14 OpenJS.NodeJS.14 14.21.3 winget Node.js 16 OpenJS.NodeJS.16 16.20.2 winget Node.js 18 OpenJS.NodeJS.18 18.20.8 winget Node.js 20 OpenJS.NodeJS.20 20.19.5 winget Node.js 21 OpenJS.NodeJS.21 21.7.2 winget Node.js 22 OpenJS.NodeJS.22 22.21.0 winget Node.js 23 OpenJS.NodeJS.23 23.11.0 winget Node.js (LTS) OpenJS.NodeJS.LTS 24.11.0 winget选择 LTS 版本（推荐）或最新版本并安装：winget install -e --id OpenJS.NodeJS.LTS或者安装最新版本：winget install -e --id OpenJS.NodeJS⚠️ 建议大多数用户使用 LTS（长期支持）版本——它更稳定，并且会获得更长时间的安全更新。3. 重新打开命令行界面（重要！）安装完成后， 关闭并重新打开 PowerShell 或终端 。这将刷新 PATH 环境变量，以便识别 node 和 npm 。然后验证安装情况：node --version npm --version你应该能看到 Node.js 和 npm 的版本号（npm 与 Node.js 捆绑在一起）。4. 故障排除：PowerShell 中无法识别 npm。如果看到类似 npm : File C:\Program Files\nodejs\npm.ps1 cannot be loaded because running scripts is disabled on this system 的错误，则表示 PowerShell 的执行策略阻止了 npm 脚本。以下三种方法可以解决这个问题：选项 A：更改执行策略（推荐）以管理员身份运行 PowerShell 并设置执行策略：Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser然后关闭并重新打开 PowerShell。这样既可以运行本地脚本（例如 npm），又能防止未经签名的远程脚本运行。选项 B：改用命令提示符如果您不想更改 PowerShell 设置，请改用命令提示符 (cmd) 或带有命令提示符配置文件的 Windows 终端 。npm 在 cmd 中无需任何策略更改即可正常工作。选项 C：绕过当前会话暂时绕过当前 PowerShell 会话的策略：Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process这只会影响当前会话，关闭 PowerShell 时会重置。5.（可选）升级 npm保持 npm 更新：npm install -g npm@latest为什么要使用 winget？✅ 官方（由微软和 OpenJS 基金会维护）✅ 自动将 Node.js 和 npm 添加到 PATH 环境变量中（只需重新打开终端即可生效）✅ 完全可脚本化（非常适合自动化和持续集成）✅ 适用于全新机器的可复现设置就这样——你只需一条命令就在 Windows 上安装了 Node.js 和 npm，重新打开终端后即可使用。如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记
这个开源知识库太香了！PandaWiki搭建全过程，踩坑经验全分享
最近公司要搭建一个内部知识库，领导让我调研几个方案。说实话，市面上的知识库产品真的是五花八门，什么Confluence、Notion、语雀啥的，但要么收费贵得离谱，要么功能不够用。偶然间发现了PandaWiki这个开源项目，试用了一下发现还真不错。界面简洁，功能够用，关键是免费！今天就把我搭建的整个过程分享给大家，希望能帮到有需要的朋友。什么是PandaWiki项目地址：https://github.com/chaitin/PandaWiki演示体验demo：https://192.168.198.133:2443/doc/editor/019a131f-12df-759c-a1a9-194d3c5ae47cPandaWiki是一个基于Go语言（好多项目都是go开发的，看来确实得学go了！！！）开发的轻量级知识库系统，支持Markdown编辑，有着不错的用户体验。相比其他重量级的知识库系统，它的部署和维护都相对简单。我之前用过MediaWiki，那个配置起来真的是要命，光是数据库配置就能折腾半天。PandaWiki就不一样了，基本上几个命令就能跑起来。环境准备在开始之前，我们需要准备一台Linux服务器。我这里用的是Debian 13（老演员了！），配置是2核4G，对于小团队使用完全够了。首先更新一下系统：apt update && apt upgrade -y然后安装一些必要的工具：apt install -y wget curl git vim开始部署PandaWiki一条命令搞定安装！！舒服！bash -c "$(curl -fsSLk https://release.baizhi.cloud/panda-wiki/manager.sh)"选择安装还是升级！会让你确认安装目录，无脑回车就行，也是通过docker部署(最喜欢看这种安装界面了哈哈哈哈)还有交流群二维码！！哈哈哈初始化配置打开浏览器访问 http://你的服务器IP:8080，应该能看到PandaWiki的初始化页面。第一次访问会要求你创建管理员账户。我建议用户名不要用admin这种太常见的，密码也要设置复杂一点。初始化完成后，你就能看到一个干净的知识库界面了。界面还是挺清爽的，左侧是目录树，右侧是内容区域。让你配置大模型很丰富注册靠谱白嫖一个（不是广告！）基本使用新手文档：https://192.168.198.133:2443/doc/editor/019a131f-12da-76b3-bafe-3cfbe1069e69创建文档点击"新建文档"按钮，可以选择创建普通文档或者文件夹。PandaWiki支持Markdown语法，写起来还是很顺手的。我试着创建了几个测试文档，发现它的Markdown渲染效果还不错，代码高亮、表格、图片都支持得挺好。我这里直接通过rss来导入自己博客的文档！通过rss订阅可以直接拉取很多地方的操作文档！这是拉取到的博客文章：去发布导入本地文本这就是财富！！！发布！！！使用统计！常用配置使用体验用了一段时间下来，感觉PandaWiki还是挺不错的。界面简洁，功能够用，性能也还可以。我们团队大概20多个人在用，响应速度还是很快的。当然也有一些不足的地方，比如权限管理相对简单，插件生态也不够丰富。但对于大多数中小团队来说，这些功能已经够用了。最重要的是它是开源免费的，不用担心突然收费或者服务停止。而且代码托管在GitHub上，如果有需要还可以自己修改。我感觉很nice！！会给出具体的引用，我准备用这个来替代我的博客搜索了哈哈哈写在最后PandaWiki作为一个轻量级的知识库解决方案，确实有它的优势。部署简单，维护方便，功能够用，关键是免费开源。虽然在一些高级功能上可能不如商业产品，但对于大多数团队来说已经足够了。特别是对于预算有限的小团队，这绝对是个不错的选择。如果你也在寻找知识库解决方案，不妨试试PandaWiki。反正是免费的，试试又不会怀孕。部署过程中如果遇到问题，可以查看官方文档或者GitHub上的issue。社区虽然不大，但还是比较活跃的。好了，今天的分享就到这里。如果这篇文章对你有帮助，记得点个赞转发一下。有问题的话也可以在评论区留言，我看到会回复的。如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记
Python sh库：让运维脚本告别subprocess的痛苦，一行代码搞定系统命令
最近在整理服务器监控脚本的时候，发现自己写的代码里到处都是subprocess.Popen()，看着就头疼。想起刚入行那会儿，为了执行个简单的ls命令，愣是写了十几行代码处理各种异常和输出，现在想想真是太蠢了。直到遇到了Python的sh库，我才发现原来执行系统命令可以这么优雅。今天就来聊聊这个让我爱不释手的库，相信看完之后你也会和我一样，再也不想碰subprocess了。为什么我会选择sh库说实话，Python标准库里的subprocess功能很强大，但用起来真的很繁琐。每次想执行个简单命令都要考虑一堆问题：怎么获取输出？怎么处理错误？怎么传参数？import subprocess # 传统的subprocess写法，光看着就累 result = subprocess.run(['ls', '-la'], capture_output=True, text=True, check=True) print(result.stdout)而用sh库呢？import sh print(sh.ls('-la'))就这么简单！一行代码搞定，看着就舒服多了。sh库的安装和基本使用安装很简单，pip一下就行：pip install sh基本使用更简单，直接把系统命令当成Python函数调用就可以了：import sh # 执行ls命令 print(sh.ls()) # 带参数的命令 print(sh.ls('-la', '/home')) # 管道操作也很简单 print(sh.grep(sh.ps('aux'), 'python'))我第一次看到这种写法的时候，真的被震撼到了。这不就是我一直想要的那种简洁的方式吗？实际项目中的应用场景在我日常运维中，sh库真的帮了大忙。比如说服务器健康检查脚本：import sh from datetime import datetime def check_server_status(): # 检查磁盘使用情况 disk_usage = sh.df('-h') print(f"磁盘使用情况：\n{disk_usage}") # 检查内存使用 memory_info = sh.free('-h') print(f"内存使用情况：\n{memory_info}") # 检查CPU负载 load_avg = sh.uptime() print(f"系统负载：{load_avg}") # 检查特定进程 try: nginx_processes = sh.pgrep('nginx') print(f"Nginx进程数：{len(nginx_processes.strip().split())}") except sh.ErrorReturnCode_1: print("警告：Nginx进程未运行！")这种写法比subprocess清爽太多了，而且代码可读性特别好。处理命令输出和错误sh库在处理命令输出方面也很贴心：import sh # 获取命令输出 output = sh.ls('/home') print(type(output)) # <class 'sh.RunningCommand'> print(str(output)) # 转换为字符串 # 实时处理输出 def process_line(line): print(f"处理行: {line.strip()}") sh.tail('-f', '/var/log/syslog', _out=process_line)错误处理也很直观：import sh try: # 执行可能失败的命令 result = sh.ls('/nonexistent') except sh.ErrorReturnCode_2 as e: print(f"命令执行失败: {e}") print(f"错误输出: {e.stderr}") print(f"标准输出: {e.stdout}") print(f"退出码: {e.exit_code}")高级功能：管道和重定向sh库对管道的支持让我印象深刻，写起来就像在shell里一样自然：import sh # 管道操作 result = sh.ps('aux') | sh.grep('python') | sh.wc('-l') print(f"Python进程数量: {result}") # 更复杂的管道 log_errors = sh.tail('-n', '1000', '/var/log/app.log') | sh.grep('ERROR') print(log_errors) # 重定向到文件 sh.dmesg(_out='/tmp/dmesg.log') # 从文件读取输入 sh.sort(_in='/tmp/unsorted.txt', _out='/tmp/sorted.txt')这种写法真的很接近shell脚本的感觉，但又保持了Python的灵活性。在自动化部署中的应用我们团队的自动化部署脚本大量使用了sh库，效果很不错：import sh import os def deploy_application(app_name, version): print(f"开始部署 {app_name} 版本 {version}") # 停止服务 try: sh.systemctl('stop', app_name) print("服务已停止") except sh.ErrorReturnCode: print("服务停止失败，继续部署...") # 备份当前版本 backup_dir = f"/backup/{app_name}-{datetime.now().strftime('%Y%m%d_%H%M%S')}" sh.mkdir('-p', backup_dir) sh.cp('-r', f'/opt/{app_name}', backup_dir) print(f"已备份到 {backup_dir}") # 下载新版本 download_url = f"https://releases.example.com/{app_name}-{version}.tar.gz" sh.wget(download_url, '-O', f'/tmp/{app_name}-{version}.tar.gz') # 解压和部署 sh.tar('-xzf', f'/tmp/{app_name}-{version}.tar.gz', '-C', '/opt/') # 更新配置文件权限 sh.chown('-R', 'app:app', f'/opt/{app_name}') sh.chmod('-R', '755', f'/opt/{app_name}') # 启动服务 sh.systemctl('start', app_name) sh.systemctl('enable', app_name) # 验证部署 try: status = sh.systemctl('is-active', app_name) if 'active' in str(status): print("部署成功！") else: print("部署可能有问题，请检查服务状态") except sh.ErrorReturnCode: print("服务启动失败！")日志分析的实际应用在日志分析方面，sh库也表现得很出色（看看就行，都用python了，谁还这样分析呀！！！）：import sh from collections import defaultdict import re def analyze_nginx_logs(log_file): # 分析访问最多的IP top_ips = sh.awk('{print $1}', log_file) | sh.sort() | sh.uniq('-c') | sh.sort('-nr') | sh.head('-10') print("访问最多的IP地址：") print(top_ips) # 分析状态码分布 status_codes = sh.awk('{print $9}', log_file) | sh.sort() | sh.uniq('-c') print("\n状态码分布：") print(status_codes) # 分析最耗时的请求 slow_requests = sh.awk('$NF > 1.0 {print $0}', log_file) | sh.sort('-k11', '-nr') | sh.head('-20') print("\n响应时间超过1秒的请求：") print(slow_requests) # 统计每小时的请求量 hourly_stats = sh.awk('{print $4}', log_file) | sh.cut('-c14-15') | sh.sort() | sh.uniq('-c') print("\n每小时请求量统计：") print(hourly_stats) # 使用示例 analyze_nginx_logs('/var/log/nginx/access.log')需要注意的坑参数传递的问题import sh # 错误的写法 # sh.find('/home -name "*.log"') # 这样会报错 # 正确的写法 sh.find('/home', '-name', '*.log') # 或者使用关键字参数 sh.find('/home', name='*.log')处理包含空格的路径import sh # 包含空格的路径需要特别处理 path_with_spaces = "/home/user/my documents" sh.ls(path_with_spaces) # sh库会自动处理空格长时间运行的命令import sh # 对于长时间运行的命令，建议设置超时 try: result = sh.rsync('-av', '/source/', '/destination/', _timeout=3600) except sh.TimeoutException: print("同步超时！")与其他库的对比说实话，除了sh库，我也用过其他一些执行系统命令的库：subprocess vs shsubprocess功能更全面，但代码冗长sh库更简洁，但在某些复杂场景下可能不够灵活os.system vs shos.system太原始，不推荐在生产环境使用sh库提供了更好的错误处理和输出控制pexpect vs shpexpect适合需要交互的场景sh库更适合执行简单的系统命令在容器化环境中的应用现在容器化部署很流行，sh库在这方面也很有用：import sh def manage_containers(): # 列出所有运行中的容器 running_containers = sh.docker('ps', '--format', 'table {{.Names}}\t{{.Status}}') print("运行中的容器：") print(running_containers) # 检查容器资源使用情况 container_stats = sh.docker('stats', '--no-stream', '--format', 'table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}') print("\n容器资源使用情况：") print(container_stats) # 清理无用的镜像 sh.docker('image', 'prune', '-f') print("\n已清理无用镜像") manage_containers()总结说了这么多，其实sh库最大的优势就是让Python代码更接近shell脚本的直觉性，同时又保持了Python的强大功能。在我的日常工作中，它已经成为了不可或缺的工具。当然，任何工具都有其适用场景。对于简单的系统命令执行，sh库绝对是首选；但如果需要复杂的进程控制或者跨平台兼容性，可能还是需要考虑其他方案。最重要的是，在使用任何执行系统命令的库时，都要时刻注意安全性。特别是在处理用户输入或者在生产环境中运行时，一定要做好输入验证和权限控制。希望这篇文章能帮到正在为subprocess复杂语法而头疼的朋友们。如果你有什么好的使用技巧或者踩过的坑，也欢迎在评论区分享交流。毕竟，工具是为了提高效率而存在的，选择最适合自己项目需求的工具才是最重要的。sh库只是众多选择中的一个，但对于大部分运维场景来说，它确实是个很不错的选择。如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记
CDN优化实战：从命中率30%到95%的血泪史，AWS CloudFront踩坑全记录
最近客户cdn命中率异常，让我优化下缓存策略。经过一番折腾，总算把命中率提上来了！！！今天就把这些年踩过的坑和一些实用的经验分享给大家。其实CDN优化这事儿，说简单也简单，说复杂也复杂。很多人以为接入个CDN就万事大吉了，结果发现命中率低得可怜，钱花了不少效果却不明显。我之前就遇到过这种情况，CDN命中率只有30%多，简直是在烧钱。先说说CDN的那些关键指标做CDN优化，你得先知道怎么看数据。就像开车要看仪表盘一样，不然怎么知道车子跑得怎么样？命中率（Hit Ratio）这个是最重要的指标，没有之一。命中率就是CDN直接返回缓存内容的请求占总请求的比例。一般来说，静态资源的命中率应该在90%以上，如果你的命中率只有50-60%，那肯定有问题。在AWS CloudFront里查看命中率很简单，进入CloudFront控制台，选择你的分发，然后点击"Monitoring"标签页。这里会显示Cache hit rate的图表，可以看到不同时间段的命中率变化。回源率（Origin Request Rate）这个和命中率是相对的，回源率高说明CDN经常要去源站拿数据，这样就失去了CDN的意义。正常情况下回源率应该控制在10%以下。响应时间包括CDN响应时间和回源响应时间。CDN响应时间一般应该在100ms以内，如果超过这个数值就要检查是不是节点选择有问题。我记得有次优化一个电商网站，发现图片加载特别慢。一查监控发现CDN响应时间居然有2秒多，后来才发现是缓存策略配置错误，每次都要回源验证。带宽使用情况这个主要是看成本，CDN的费用很大一部分就是带宽费用。通过监控带宽使用情况，可以了解流量分布，优化成本。在AWS CloudFront的监控面板里，你还能看到：Origin latency（回源延迟）Error rate（错误率）Requests（请求数）Bytes downloaded（下载字节数）这些数据都很有用，特别是错误率，如果突然飙升可能是源站出问题了。命中率低的常见原因和解决方案说到命中率低，我遇到过各种奇葩的情况。缓存策略配置不当这是最常见的问题。很多人图省事，直接用默认配置，结果发现很多本该缓存的内容都没有缓存。比如说，有些动态参数会导致URL不同，CDN就认为是不同的资源。像这种情况：https://example.com/image.jpg?timestamp=1234567890 https://example.com/image.jpg?timestamp=1234567891CDN会认为这是两个不同的文件，实际上内容是一样的。解决办法是在CloudFront的Behavior设置里配置Query String参数处理，可以选择忽略某些参数或者只转发特定参数。TTL设置太短Time To Live设置太短会导致缓存频繁过期。我见过有人把图片的TTL设置成5分钟，这不是瞎搞吗？静态资源的TTL至少要设置几个小时，甚至几天。在CloudFront里可以设置三种TTL：Minimum TTL：最小缓存时间Maximum TTL：最大缓存时间Default TTL：默认缓存时间一般我会这样设置：图片、CSS、JS等静态资源：24小时到7天HTML页面：1-6小时API接口：根据业务需求，可能几分钟到几小时源站响应头配置问题有时候源站返回了不合适的Cache-Control头，比如no-cache或者max-age=0，这会导致CDN不缓存内容。我之前遇到过一个案例，开发在Nginx配置里给所有静态资源都加了no-cache，结果CDN命中率惨不忍睹。后来改成这样：location ~* \.(jpg|jpeg|png|gif|css|js)$ { expires 7d; add_header Cache-Control "public, immutable"; }预热不充分新上线的CDN或者清除缓存后，需要一段时间来"预热"。这期间命中率会比较低，这是正常现象。但如果长时间命中率都上不去，就要检查其他配置了。CloudFront支持手动预热，可以通过Invalidation功能清除缓存，然后用脚本批量访问重要资源来预热缓存。CDN优化策略详解现在说说aws托管的缓存策略AWS CDN托管的缓存策略详细说明策略名称缓存内容不缓存内容使用场景Amplify缓存大多数静态资源和部分动态内容不缓存包含特定身份验证头的请求Amplify应用程序的通用缓存策略Amplify-Default缓存静态资源和基于常用头部的内容，保留某些Cookie不缓存带有特定认证头的请求，某些动态API响应Amplify应用的标准配置，平衡缓存与个性化Amplify-Default-V2与Amplify-Default类似，但有优化的TTL设置与Amplify-Default类似需要改进的缓存性能的Amplify应用Amplify-DefaultNoCookies缓存静态资源和页面内容不缓存任何Cookie信息，不缓存认证请求不需要基于Cookie区分用户的Amplify应用Amplify-DefaultNoCookies-V2与DefaultNoCookies类似，但有更长的TTL与DefaultNoCookies类似追求更高缓存命中率的静态Amplify应用Amplify-ImageOptimization缓存图像文件(jpg, png, gif等)，支持不同的图像格式不缓存非图像内容，不保留大多数请求头图像密集型网站，图片库，电商产品展示Amplify-ImageOptimization-V2与ImageOptimization类似，增加了WebP等现代格式支持与ImageOptimization类似需要现代图像格式支持的应用Amplify-StaticContent缓存JS, CSS, 字体, 图像等静态资源，长TTL不缓存HTML或动态内容，不保留Cookie静态资源的长期缓存，提高重复访问性能Amplify-StaticContent-V2与StaticContent类似，但有更优化的压缩设置与StaticContent类似需要最佳静态资源性能的应用CachingDisabled不缓存任何内容所有内容都直接从源站获取实时数据，个性化内容，需要实时源站响应的场景CachingOptimized缓存大多数内容，启用压缩，优化TTL不缓存POST/PUT请求，特定动态API路径通用Web应用，需要平衡缓存与新鲜度的场景CachingOptimizedForUncompressedObjects缓存未压缩的对象，自动应用适当的TTL不缓存已经压缩的内容，不处理某些动态请求源站不提供压缩的情况，CDN负责压缩Elemental-MediaPackage缓存视频分段，流媒体清单文件不缓存用户特定的媒体请求，认证令牌视频流媒体，直播，点播视频服务UseOriginCacheControlHeaders根据源站的Cache-Control头决定缓存行为如果源站指示不缓存，则不缓存；忽略查询参数源站已有完善缓存策略的情况UseOriginCacheControlHeaders-QueryStrings根据源站Cache-Control头和URL查询参数缓存如源站指示不缓存则不缓存基于查询参数提供不同内容且源站控制缓存的场景AWS CloudFront 源请求策略详细说明策略名称转发内容不转发内容使用场景AllViewer转发所有查看者请求参数，包括查询字符串、标头和Cookie不限制任何参数的转发需要将所有客户端请求信息传递给源站的场景，适用于高度依赖客户端信息的动态内容AllViewerAndCloudFrontHeaders-2022-06转发所有查看者请求参数和截至2022年6月的所有CloudFront标头不限制任何参数或CloudFront标头的转发需要完整客户端信息以及CloudFront添加的信息(如设备类型、地理位置)的场景AllViewerExceptHostHeader转发除Host标头外的所有查看者请求参数不转发Host标头当源站需要使用不同的主机名处理请求，但仍需其他所有客户端信息的场景CORS-CustomOrigin转发与CORS(跨源资源共享)相关的标头到自定义源不转发与CORS无关的其他标头需要支持跨域请求的自定义源站(非S3)场景，如API或Web应用CORS-S3Origin转发与CORS相关的标头到S3源不转发与CORS无关的其他标头需要支持跨域请求的S3存储桶场景，如从浏览器直接访问S3资源Elemental-MediaTailor-PersonalizedManifests转发Elemental MediaTailor个性化清单所需的标头不转发与媒体个性化无关的标头视频流媒体场景，特别是需要个性化内容或广告插入的流媒体服务HostHeaderOnly仅转发Host标头到源站不转发任何其他查看者请求参数简单的静态内容分发场景，源站仅需要知道请求的主机名UserAgentReferrerHeaders转发User-Agent(用户代理)和Referer(引用页)标头到源站不转发其他查看者请求参数源站需要了解访问者浏览器类型和来源网站的场景，如内容适配或引用统计这些源请求策略控制CloudFront将哪些信息从查看者(客户端)请求中转发到源站。选择合适的策略可以优化源站请求处理，减少不必要的信息传递，同时确保源站获得所需的上下文信息来正确响应请求。AWS CloudFront 响应标头策略详细说明策略名称添加/修改内容不添加/不修改内容使用场景CORS-and-SecurityHeadersPolicy添加CORS相关标头和安全标头到响应中不修改源站提供的其他标头需要同时支持跨域请求和增强安全性的Web应用，如需要跨域访问的API且需要防XSS、点击劫持等安全保护CORS-With-Preflight添加CORS相关标头并支持预检(OPTIONS)请求不添加安全标头，不修改源站提供的其他标头需要支持复杂跨域请求的场景，特别是涉及非简单请求(如带自定义标头或使用PUT/DELETE方法)的APICORS-with-preflight-and-SecurityHeadersPolicy添加CORS相关标头(含预检支持)和安全标头不修改源站提供的其他标头需要全面CORS支持(包括复杂请求)和安全增强的Web应用，如SPA(单页应用)与后端API交互SecurityHeadersPolicy仅添加安全相关标头到每个响应不添加CORS标头，不修改源站提供的其他标头注重安全性但不需要跨域支持的网站，如企业内部应用或对安全有高要求的公共网站SimpleCORS仅添加基本CORS标头，支持简单跨域请求不添加安全标头，不支持预检请求，不修改源站提供的其他标头只需基本跨域支持的简单场景，如只允许GET请求的公共API或静态资源这些响应标头策略控制CloudFront如何修改从源站返回给客户端的HTTP响应标头。适当的策略可以增强Web应用的安全性，启用跨域资源共享功能，而无需修改源站配置。安全标头通常包括内容安全策略(CSP)、X-XSS-Protection、X-Frame-Options等，用于防止常见的Web安全威胁。当然要是这些不满足还可以自定义！！其他一些缓存策略分层缓存策略不同类型的资源用不同的缓存策略。我一般会创建多个Behavior，针对不同的路径模式设置不同的缓存规则：/api/* - TTL: 5分钟，转发所有参数 /images/* - TTL: 7天，忽略查询参数 /css/* - TTL: 1天，启用压缩 /js/* - TTL: 1天，启用压缩 /* - TTL: 1小时，默认行为启用压缩CloudFront支持自动压缩，可以显著减少传输数据量。在Behavior设置里开启"Compress Objects Automatically"就行了。不过要注意，压缩会消耗一些CPU资源，对于已经压缩过的文件（如图片）就没必要再压缩了。合理使用Origin Shield这是CloudFront的一个高级功能，相当于在CDN和源站之间加了一层缓存。对于回源比较频繁的场景很有用，可以减少源站压力。我在一个视频网站项目中用过Origin Shield，效果很明显。因为视频文件比较大，用户分布又比较集中，开启Origin Shield后回源请求减少了60%多。HTTP/2和HTTP/3支持现在的CDN基本都支持HTTP/2了，CloudFront也不例外。HTTP/2的多路复用特性可以显著提升页面加载速度，特别是对于有很多小文件的网站。智能路由CloudFront会自动选择最优的边缘节点，但有时候你可能需要手动干预。比如某些地区的节点质量不好，可以通过地理位置限制来避免使用这些节点。AWS CloudFront的一些实用技巧用了这么久CloudFront，发现了一些比较实用的技巧。Lambda@Edge这个功能很强大，可以在CDN边缘节点运行代码。我用过几个场景：动态修改响应头根据用户地理位置返回不同内容简单的A/B测试图片格式转换（WebP支持检测）比如这个检测WebP支持的例子：exports.handler = (event, context, callback) => { const request = event.Records[0].cf.request; const headers = request.headers; const accept = headers.accept && headers.accept[0] && headers.accept[0].value; if (accept && accept.includes('image/webp')) { request.uri = request.uri.replace(/\.(jpg|jpeg|png)$/i, '.webp'); } callback(null, request); };实时日志分析CloudFront可以把访问日志实时推送到Kinesis Data Streams，然后用Lambda处理。我用这个功能做过实时监控，当错误率超过阈值时自动发送告警。多源站配置CloudFront支持配置多个源站，可以实现负载均衡和故障转移。我一般会配置主源站和备用源站，当主源站出问题时自动切换到备用源站。自定义错误页面通过配置Custom Error Pages，可以为不同的HTTP错误码返回自定义页面。这样用户体验会好很多，而且可以减少无效的回源请求。监控和告警设置CDN优化不是一次性的工作，需要持续监控和调优。CloudWatch监控CloudFront的所有指标都会推送到CloudWatch，可以设置各种告警：命中率低于阈值告警错误率超过阈值告警回源延迟过高告警带宽使用异常告警我一般会设置这些告警规则：命中率 < 85% 持续10分钟 -> 发送告警 4xx错误率 > 5% 持续5分钟 -> 发送告警 5xx错误率 > 1% 持续5分钟 -> 发送告警 回源延迟 > 2秒 持续5分钟 -> 发送告警第三方监控工具除了CloudWatch，我还会用一些第三方工具来监控CDN性能，比如Pingdom、GTmetrix等。这些工具可以从用户角度测试网站性能，发现一些CloudWatch监控不到的问题。成本优化CDN的费用不便宜，特别是流量大的网站。Price Class选择CloudFront有三个价格等级，包含的边缘节点数量不同。如果你的用户主要在北美和欧洲，选择Price Class 100就够了，没必要选择包含全球所有节点的Price Class All。缓存策略优化提高命中率不仅能改善性能，还能降低成本。因为CDN的费用主要是按流量计算的，命中率高意味着回源流量少，总体费用就低。如果命中率低就会出现流量放大效应：• 正常命中率80%：1GB实际需求 = 1.25GB CDN流量• 当前命中率可能<20%：1GB实际需求 = 5GB+ CDN流量压缩和格式优化启用压缩可以减少传输数据量，WebP格式的图片比JPEG小30-50%。这些优化都能直接降低CDN费用。我之前优化过一个图片网站，通过启用压缩和WebP转换，CDN费用降低了40%多。常见问题排查做CDN优化经常会遇到各种问题，分享几个排查思路。缓存不生效首先检查响应头，看看有没有Cache-Control、Expires等缓存相关的头。然后检查CloudFront的Behavior配置，确认TTL设置正确。可以用curl命令测试：curl -I https://your-domain.com/test.jpg看响应头里的X-Cache字段，Hit表示命中缓存，Miss表示没有命中。某些地区访问慢可能是CDN节点选择有问题。用不同地区的VPS测试访问速度，确定是哪些地区有问题。然后检查是不是需要调整Price Class或者配置地理位置限制。缓存穿透如果某个资源一直无法缓存，可能是URL参数或者响应头有问题。检查是不是有随机参数，或者源站返回了no-cache头。我遇到过一个奇葩问题，开发在图片URL后面加了随机数防止缓存，结果CDN命中率为0。后来改成用版本号，问题就解决了。总结CDN优化是个系统工程，需要从多个维度来考虑。命中率是最重要的指标，但不是唯一的指标。要结合业务特点，制定合适的缓存策略。AWS CloudFront功能很强大，但配置也比较复杂。建议先从基础配置开始，逐步优化。不要一开始就搞得很复杂，容易出问题。最重要的是要持续监控和优化，CDN不是配置好就不管了。用户行为会变化，业务需求也会变化，CDN策略也要跟着调整。我现在维护的几个网站，CDN命中率都在95%以上，用户访问速度提升了3-5倍。虽然前期折腾了不少时间，但效果还是很明显的。如果你也在做CDN优化，遇到问题可以多交流。这块水还是挺深的，大家一起学习进步。如果这篇文章对你有帮助，别忘了点赞转发支持一下！想了解更多运维实战经验和技术干货，记得关注微信公众号@运维躬行录，领取学习大礼包！！！我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记