躬行笔记 - 运维知识
type: channel
link: https://www.fuzhoupyy.work/index.php/category/yw/
items: 手把手教你落地网络入侵检测系统（NIDS），从选型到上线全记录
items: 别让你的Nginx裸奔！生产环境下的访问控制实战，全是填坑经验
items: 从零开始搞定Chef自动化部署，让服务器管理不再头疼
items: 蓝绿部署实战指南：从零到一搭建生产级无缝发布系统
items: 从零开始搞懂DDoS和CC攻击：运维人员必备的防护攻略
items: Linux 内存泄漏排查全攻略：从现象到根因，一文讲透生产环境实战经验
items: K8s持久化存储深度解析：PV、PVC、StorageClass三剑客的生产实战
items: 深度拆解Tomcat：从架构设计到生产实践，这些年踩过的坑都在这里了
items: 生产环境 Nginx 代理 WebSocket 总断连？这几个隐藏配置坑了多少人
items: Jenkins Pipeline 脚本踩坑记：我是如何被两种语法折磨并最终选择的

手把手教你落地网络入侵检测系统（NIDS），从选型到上线全记录
type: item
date: 2026-04-22T14:07:50+00:00
description: 说实话，搞安全这块东西，很多运维同学是又爱又恨。爱的是确实能帮你挡住不少事儿，恨的是部署起来各种坑，配不好还一堆误报，搞得你天天看告警看到想吐。我之前在客户公司负责过一套NIDS的完整落地，从前期调研到最后稳定运行，前前后后折腾了差不多两个月。今天就把这个过程掏出来跟大家聊聊，尽量把踩过的坑和一些实际经验都说清楚。先搞清楚你到底要监控什么很多人上来就想装Snort、装Suricata，工具先搞起来再说。我一开始也是这个思路，后来发现这样搞完全是在浪费时间。你得先坐下来想清楚几个问题：你的网络环境里，哪些流量是需要重点盯着的？敏感数据在哪？常见的威胁类型是什么？拿我们当时的情况来说，公司有一套对外的Web业务系统，还有内部的OA和数据库集群。对外的业务系统天天被扫描、被尝试SQL注入、XSS什么的，这块肯定是重点。内部的话，主要担心的是横向移动，就是万一有人进来了，在内网里到处乱窜。我当时列了一个简单的表格：监控区域流量类型关注的威胁优先级DMZ区HTTP/HTTPSWeb攻击、扫描探测高核心交换全流量横向移动、C2通信高数据库段DB协议流量异常查询、数据外泄中办公网混合流量恶意软件、钓鱼中这个表看起来简单，但真的很有用。后面选工具、写规则、部署探针的时候，都是围绕这个来的。不然你部署完了发现，关键的地方没覆盖到，不关键的地方一堆告警，那就白忙活了。工具选型：Snort还是Suricata？开源的NIDS工具，绕不开这两个。商业的像Palo Alto、FireEye那些，预算够当然好，但大多数公司的安全预算你懂的...所以我们还是聊开源方案。Snort 是老牌子了，社区大，规则多，文档也全。但它有个问题，单线程。在流量大的环境里，性能瓶颈很明显。我之前在测试环境里跑过，跑到2Gbps左右的流量就开始丢包了。Suricata 是后来者，天然支持多线程，性能上确实比Snort强不少。而且它兼容Snort的规则格式，也就是说Snort的规则你拿过来基本能直接用。另外Suricata原生支持输出EVE JSON格式的日志，对接ELK那套东西非常方便。我们最后选的是Suricata，原因很直接：核心交换那个位置的流量峰值能到5Gbps，Snort扛不住。不过也不是说Snort就不好。如果你的网络规模不大，流量也就几百兆，Snort完全够用，而且上手更简单一些。还有一个值得提的是 Zeek（以前叫Bro），严格来说它不算传统的IDS，更偏向于网络流量分析和日志生成。它不做实时告警，但生成的连接日志、DNS日志、HTTP日志这些，拿来做事后分析和威胁狩猎非常好用。我们后来也部署了Zeek，和Suricata配合着用，一个管实时告警，一个管深度分析。硬件和网络架构怎么搞工具选好了，接下来就是怎么把流量喂给它。NIDS是旁路部署的，不像防火墙那样串在链路上。它需要的是流量的镜像副本。一般有两种方式：1. 交换机端口镜像（SPAN/Mirror Port）这是最常见的方式。在核心交换机上配一个镜像端口，把需要监控的VLAN流量或者特定端口的流量镜像出来，接到NIDS传感器上。我们用的是华为的交换机，配置大概长这样：# 创建观察端口（连接NIDS传感器的端口） observe-port 1 interface GigabitEthernet 0/0/24 # 配置镜像源端口 interface GigabitEthernet 0/0/1 port-mirroring to observe-port 1 both如果是思科的设备：monitor session 1 source interface Gi0/1 both monitor session 1 destination interface Gi0/24这里有个坑要注意：镜像端口的带宽要够。如果你镜像的源端口是10G的，但目标端口只有1G，那流量大的时候肯定丢包。我们当时就吃了这个亏，一开始用的1G口做镜像，结果高峰期丢了将近30%的包，好多攻击流量直接漏掉了。后来换成了10G的口才解决。2. TAP设备如果预算允许，用网络TAP（Test Access Point）是更好的选择。TAP是一个硬件设备，直接接在链路上，把流量复制一份出来。它的好处是不依赖交换机的镜像功能，也不会因为交换机负载高而丢包。我们在DMZ区的出口就用了一个TAP，大概花了几千块钱，买的国产的，效果还行。传感器的硬件配置跑Suricata的服务器，配置不能太寒酸。我们用的配置是：CPU：Intel Xeon E5-2680 v4（14核28线程）内存：64GB DDR4网卡：Intel X710 10GbE（支持多队列）硬盘：500GB SSD（主要存日志，日志量大的话还得考虑外挂存储）网卡这块特别说一下，一定要选支持多队…

别让你的Nginx裸奔！生产环境下的访问控制实战，全是填坑经验
type: item
date: 2026-04-22T14:07:30+00:00
description: 咱们搞运维的，Nginx那是吃饭的家伙，平时大家可能更多关注的是负载均衡、反向代理，或者怎么调优性能。但说句掏心窝子的话，安全这东西，平时不出事你觉得它多余，一出事那就是“救命稻草”。今天咱们不整那些虚头巴脑的理论，直接上生产环境的实战，聊聊Nginx访问控制那些事儿。这些配置都是我在无数次填坑过程中总结下来的，希望能给大伙提个醒，别让你的服务器在互联网上“裸奔”。咱们先从最简单的说起，很多人觉得访问控制不就是设个密码，或者封个IP嘛。其实没那么简单。拒绝不速之客：基于IP的访问控制这应该是最直接的手段了。就像咱们小区的门禁，只让业主进，外人一律挡在门外。Nginx里的allow和deny指令就是干这个活的。这俩模块是ngx_http_access_module自带的，默认就装了，不用你去折腾编译安装。我记得有一次，公司有个内部管理系统，原本是挂在内网域名下的，结果开发哥们为了方便测试，临时挂到了公网，也没做任何限制。结果没过两天，就被安全扫描器扫到了，虽然没出大事故，但那个报警邮件看得我后背发凉。后来我就给所有的内部后台都加上了IP白名单。配置起来其实特简单，打开你的nginx.conf或者对应的站点配置文件，在server块或者location块里加上几行就行：location /admin/ { allow 192.168.1.0/24; # 允许内网网段 allow 10.0.0.1; # 允许某个特定的跳板机 deny all; # 拒绝其他所有人 }这里有个小细节，很多人容易踩坑。Nginx匹配规则是从上往下匹配，一旦匹配成功就停止。所以顺序千万不能乱。你要是把deny all写在第一行，那后面所有的allow都白搭，谁都进不去，这叫“误伤友军”。我就干过这种傻事，配置完 reload 了一下，结果把自己关在外面了，只能灰溜溜地去机房连控制台改配置，那场面，别提多尴尬了。还有个事儿得注意，如果你用了CDN，比如阿里云CDN或者Cloudflare，那你在这个allow里配置的IP可能就不是用户的真实IP了，而是CDN节点的IP。这时候你得先用set_real_ip_from把CDN的IP段设置一下，用X-Forwarded-For或者X-Real-IP头来获取真实用户IP，然后再做限制。不然你限制了个寂寞，或者把正常用户给封了。给目录加把锁：基于用户的认证有时候，我们需要给某个测试环境、或者某个临时的API接口加个密码，不想让随便什么人都能访问。这时候IP限制就不太灵活了，比如出差在外，IP是动态的，咋办？这时候就得请出ngx_http_auth_basic_module模块了。这玩意儿就像给目录加了一把锁，只有输入对了用户名和密码才能进。配置分两步走。第一步，得先有个密码文件。别想着自己去手写加密密码，Linux底下有现成的工具，Apache的htpasswd工具最方便。一般服务器装了Apache或者httpd-tools就有。没有的话装一下：yum install httpd-tools -y然后生成密码文件，比如我们建个/etc/nginx/.htpasswd文件，添加一个叫pyy的用户：htpasswd -c /etc/nginx/.htpasswd pyy回车后它会让你输入两次密码。注意那个-c参数是create的意思，如果你是第一次创建文件用-c，如果是追加用户，千万别带-c，不然原来的文件会被覆盖掉，我就因为这个被同事投诉过，把他们的账号给弄丢了。文件有了，接下来就是Nginx配置：location /test/ { auth_basic "Restricted Area"; # 这个是提示信息，随便写 auth_basic_user_file /etc/nginx/.htpasswd; }配置完nginx -t测试一下，没问题就nginx -s reload。这时候你再去访问那个目录，浏览器就会弹出一个原生的登录框，丑是丑了点，但胜在实用。这个功能在临时分享一些敏感数据，或者保护还没上线的功能时特别好用。不过要注意，这个Basic认证是Base64编码传输的，安全性并不是特别高，如果不配合HTTPS，密码其实相当于明文传输，抓个包就能看见。所以，能上HTTPS尽量上HTTPS。更高级的玩法：结合GeoIP做地域封禁有时候我们不仅想封IP，还想封地区。比如公司的业务只在国内开展，但是每天日志里全是来自大洋彼岸的扫描攻击，看着都烦。这时候就可以用GeoIP模块。现在的Nginx默认应该都支持ngx_http_geoip_module，如果不支持，那你可能得重新编译安装一下，带上--with-http_geoip_module参数。不过现在很多都是动态加载模块，看情况而定。首先你得下载GeoIP的数据库文…

从零开始搞定Chef自动化部署，让服务器管理不再头疼
type: item
date: 2026-04-17T15:58:10+00:00
description: 最近公司的服务器数量又增加了不少，每次部署新应用都要重复执行一堆命令，配置文件改来改去的，实在是太麻烦了。我琢磨着得找个自动化工具来解决这个问题，经过一番调研，最终选择了Chef。说实话，刚开始接触Chef的时候，被它的概念搞得有点晕。什么Cookbook、Recipe、Node、Role的，感觉像是在学做菜一样。不过用了一段时间后发现，这玩意儿还真挺好使的，今天就把我踩过的坑和实践经验分享给大家。Chef到底是个啥简单来说，Chef就是一个自动化配置管理工具。你可以把它理解成一个"厨师"，按照你写好的"菜谱"（Cookbook），自动在服务器上"做菜"（配置软件和服务）。我觉得Chef最大的优势就是它用Ruby写配置，代码即配置的理念让运维工作变得更加程序化。不像以前写shell脚本那样，到处都是if-else判断，Chef的DSL（领域特定语言）让配置文件看起来更清晰。搭建Chef环境部署Chef主要有三个组件：Chef Server、Chef Workstation和Chef Client。我当时是在Ubuntu 20.04上搭建的，过程还算顺利。安装Chef其实挺简单的，在工作站上执行：curl -L https://omnitruck.chef.io/install.sh | sudo bash这个脚本会自动检测你的系统，下载对应版本的Chef。安装完成后，可以用chef --version检查一下版本。配置Chef Server稍微麻烦点。我是直接用的Chef官方提供的包，大概有200多MB：wget https://packages.chef.io/files/stable/chef-server/14.0.0/ubuntu/20.04/chef-server-core_14.0.0-1_amd64.deb sudo dpkg -i chef-server-core_14.0.0-1_amd64.deb sudo chef-server-ctl reconfigure配置过程需要等一会儿，它会自动安装PostgreSQL、RabbitMQ等依赖服务。配置完成后，创建管理员用户和组织：sudo chef-server-ctl user-create admin Admin User admin@example.com 'password' --filename admin.pem sudo chef-server-ctl org-create myorg 'My Organization' --association_user admin --filename myorg-validator.pem这里生成的.pem文件很重要，是后续连接Chef Server的凭证，一定要保存好。写第一个CookbookCookbook是Chef的核心概念，可以理解为一个配置包，里面包含了Recipe（具体的配置步骤）、Attributes（变量）、Templates（模板文件）等。创建Cookbook很简单：chef generate cookbook nginx_setup这会生成一个标准的Cookbook目录结构。我第一次写的是一个安装配置Nginx的Cookbook，在recipes/default.rb里写入：# 安装nginx包 package 'nginx' do action :install end # 创建网站目录 directory '/var/www/mysite' do owner 'www-data' group 'www-data' mode '0755' action :create end # 配置nginx template '/etc/nginx/sites-available/mysite' do source 'mysite.conf.erb' owner 'root' group 'root' mode '0644' variables( server_name: node['nginx_setup']['server_name'], port: node['nginx_setup']['port'] ) notifies :restart, 'service[nginx]', :delayed end # 启用站点 link '/etc/nginx/sites-enabled/mysite' do to '/etc/nginx/sites-available/mysite' notifies :restart, 'service[nginx]', :delayed end # 确保nginx服务运行 service 'nginx' do action [:enable, :start]…

蓝绿部署实战指南：从零到一搭建生产级无缝发布系统
type: item
date: 2026-04-16T15:06:11+00:00
description: 蓝绿部署实战指南：从零到一搭建生产级无缝发布系统我在公司做运维这几年，最怕的就是发布新版本的时候出问题。记得有一次周五下午发版本，结果服务挂了两个小时，用户投诉电话打爆了，我在机房里忙活到晚上11点才搞定。从那以后，我就开始研究各种发布策略，蓝绿部署就是其中最实用的一种。今天就把我这几年摸索出来的蓝绿部署经验分享给大家，全是生产环境踩过的坑和总结出的干货。什么是蓝绿部署蓝绿部署说白了就是准备两套一模一样的生产环境，一套叫蓝环境，一套叫绿环境。当前用户访问的是蓝环境，我要发新版本的时候，就把新版本部署到绿环境上，测试没问题了再把流量切过去。这样做的好处是什么？如果新版本有问题，我立马就能切回到旧版本，用户基本感受不到服务中断。我之前用滚动更新的方式，每次更新都要重启服务，虽然时间很短，但还是会有那么几秒钟的服务不可用。用了蓝绿部署之后，这个问题彻底解决了。蓝绿部署的核心原理整个流程其实很简单：当前蓝环境在线提供服务在绿环境部署新版本应用绿环境测试通过后，负载均衡器流量从蓝环境切换到绿环境绿环境成为新的生产环境，蓝环境变成备用环境关键点就是这个流量切换，必须要做到瞬间完成，不能有中间状态。实现蓝绿部署的几种方式方式一：基于Nginx的蓝绿部署这是我最常用的方式，成本低，配置简单。先准备两台服务器，192.168.1.10（蓝）和192.168.1.11（绿），前面放一台Nginx做负载均衡。Nginx配置文件关键部分：upstream blue_pool { server 192.168.1.10:8080 weight=1 max_fails=3 fail_timeout=30s; } upstream green_pool { server 192.168.1.11:8080 weight=1 max_fails=3 fail_timeout=30s; } # 当前生产环境指向蓝环境 upstream production { server 192.168.1.10:8080; } server { listen 80; server_name api.example.com; location / { proxy_pass http://production; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }切换的时候，写个脚本：#!/bin/bash # switch.sh NGINX_CONF="/etc/nginx/conf.d/app.conf" CURRENT_ENV=$(grep -A1 "upstream production" $NGINX_CONF | grep server | awk '{print $2}' | cut -d: -f1) if [ "$CURRENT_ENV" = "192.168.1.10" ]; then # 当前是蓝环境，切换到绿环境 sed -i 's/server 192.168.1.10:8080;/server 192.168.1.11:8080;/' $NGINX_CONF echo "已切换到绿环境" else # 当前是绿环境，切换到蓝环境 sed -i 's/server 192.168.1.11:8080;/server 192.168.1.10:8080;/' $NGINX_CONF echo "已切换到蓝环境" fi # 重新加载Nginx配置 nginx -s reload这个方案的优点是简单直接，缺点是需要手动执行切换脚本。不过对于中小型项目来说已经够用了。方式二：基于HAProxy的蓝绿部署HAProxy在流量切换方面比Nginx更灵活，支持动态权重调整。HAProxy配置：global daemon maxconn 4096 defaults mode http timeout connect 5000ms timeout client 50000ms timeout server 50000ms frontend web_frontend bind *:80 default_backend web_servers backend web_servers balance roundrobin # 初始状态：蓝环境权重100，绿环境权重0 server blue 192.168.1.10:8080 weight 100 check server green 192.168.1.11:8080 weight 0 check # 管理接口 …

从零开始搞懂DDoS和CC攻击：运维人员必备的防护攻略
type: item
date: 2026-04-15T14:45:00+00:00
description: 作为一个在运维岗位摸爬滚打好几年的人，我想和大家聊聊DDoS和CC攻击这个话题。这两种攻击方式虽然都能让你的服务器瞬间趴窝，但原理和防护方法却大不相同。今天就把我这些年踩过的坑和积累的经验分享给大家。DDoS攻击到底是个什么鬼DDoS全称是Distributed Denial of Service，翻译过来就是分布式拒绝服务攻击。听起来很高大上，其实原理挺简单的。你可以想象一下，平时你家门口最多同时来3-5个客人，你能应付得过来。但如果突然来了100个人，都要进你家坐坐，你家的门、客厅、厕所...肯定就瘫痪了。DDoS攻击就是这个道理，通过控制大量的"肉鸡"（被感染的计算机）同时向目标服务器发送请求，把服务器的带宽、CPU、内存等资源耗尽。DDoS攻击主要分几种类型：流量型攻击（Volume-based attacks）这种最直接，就是用大量的垃圾流量把你的带宽塞满。常见的有UDP洪水攻击、ICMP洪水攻击等。攻击者会发送大量的UDP包或者ping包，把你的网络带宽占满，正常用户就访问不了了。协议型攻击（Protocol attacks）这种攻击利用网络协议的缺陷，比如SYN洪水攻击。TCP三次握手大家都知道吧，客户端发SYN，服务器回SYN-ACK，然后等客户端的ACK。攻击者只发SYN不发ACK，服务器就一直等着，连接池很快就满了。应用层攻击（Application layer attacks）这种攻击针对应用程序，看起来像正常的请求，但会消耗大量的服务器资源。比如不断请求复杂的数据库查询，或者上传大文件等。CC攻击又是怎么回事CC攻击的全称是Challenge Collapsar，中文叫挑战黑洞。听这名字就知道不是什么好东西。CC攻击和DDoS攻击的区别在于，CC攻击更加"精准"。它不需要很大的流量，而是模拟正常用户的行为，不断地请求那些消耗资源比较大的页面或接口。CC攻击的特点是：流量不大，但请求频繁看起来像正常用户行为针对消耗资源较大的功能难以通过简单的流量限制来防护两种攻击的本质区别说到区别，我觉得可以用一个比喻来解释。DDoS攻击就像是在你家门口堵了一大群人，把路都堵死了，正常客人根本进不来。这种攻击比较"粗暴"，流量大，容易发现。CC攻击则像是派了几个人，每个人都很有礼貌地敲门，但一进来就要你做很复杂的事情，比如帮忙搬家具、修电器...虽然人不多，但你很快就累趴下了。从技术角度来说：DDoS主要消耗带宽和网络资源CC主要消耗服务器的CPU、内存和数据库资源DDoS流量特征明显，CC攻击更隐蔽DDoS防护相对简单粗暴，CC防护需要更精细的策略防护策略：不只是买个高防就完事很多人以为防DDoS和CC攻击就是买个高防服务器或者CDN，其实远没有这么简单。我这些年总结下来，防护需要多层次的策略。基础防护措施首先是服务器层面的优化。我一般会调整一些系统参数，比如增加TCP连接池大小，调整超时时间等。在nginx配置里也会做一些限制：# 限制单个IP的连接数 limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m; limit_conn conn_limit_per_ip 20; # 限制请求频率 limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=10r/s; limit_req zone=req_limit_per_ip burst=20 nodelay;这些配置能在一定程度上缓解攻击，但面对大规模DDoS还是不够的。应用层防护对于CC攻击，我会在应用层做一些防护。比如给那些消耗资源大的接口加上缓存，限制单个用户的请求频率，对异常请求进行识别和拦截。我记得有次优化一个电商网站的搜索功能，发现搜索结果页面的数据库查询特别复杂。于是我们加了Redis缓存，把热门搜索词的结果缓存起来，这样即使遇到CC攻击，数据库压力也不会太大。CDN和云防护现在各大云厂商的防护产品确实很强大。我用过阿里云的DDoS高防，腾讯云的大禹，华为云的Anti-DDoS等，效果都不错。这些产品的原理基本相似：在你的服务器前面部署一层防护节点，所有流量先经过这些节点进行清洗，正常流量才会转发到你的服务器。流量清洗的那些门道流量清洗听起来很神秘，其实原理并不复杂。就是在海量的网络流量中，把攻击流量识别出来并丢弃，让正常流量通过。特征识别最基础的方法是基于特征识别。比如DDoS攻击的流量往往有一些明显特征：源IP分布异常（大量来自同一地区或AS）请求模式单一（相同的User-Agent、请求路径等）流量突增（短时间内流量暴涨几十倍）我见过一些简单的攻击，所有请求的User-Agent都一样，这种很容易识别。但…

Linux 内存泄漏排查全攻略：从现象到根因，一文讲透生产环境实战经验
type: item
date: 2026-04-13T13:50:40+00:00
description: 做运维这么多年，内存泄漏这事儿真是没少碰。有些时候程序跑着跑着内存就蹭蹭往上涨，重启一下好了，过两天又不行了。这种问题最烦人，不像CPU高那样容易定位，也不像磁盘满了那样好处理。内存泄漏就像个隐形杀手，藏在代码深处，等你发现的时候往往已经酿成事故了。今天咱们不聊虚的，直接上干货，把Linux下排查内存泄漏的方法系统梳理一遍。从基础的命令行工具到高级的调试技巧，能用的都给大家整理出来。内存泄漏到底是咋回事说白了就是申请了内存用完不释放。程序向系统申请了一块内存空间，用完了却没告诉系统"我不用了"，这块内存就被标记为占用状态。时间一长，这种"僵尸内存"越来越多，可用内存越来越少，最后系统扛不住了，触发OOM Killer把进程干掉，或者整个系统卡死。Linux的内存管理机制本身是没问题的，问题出在应用程序层面。C/C++这种需要手动管理内存的语言是重灾区，Java、Go这种有垃圾回收的语言理论上好点，但也不代表不会泄漏。比如Java里的对象引用没释放，或者堆外内存用多了，照样会出问题。内存泄漏有个特点：渐进性。刚启动的时候没事，运行几天甚至几周才开始显现。所以排查起来特别费劲，你得知道是哪个时间点开始泄漏的，做了什么操作触发的。怎么发现内存泄漏监控是第一道防线。我们生产环境用的是Prometheus + Grafana，内存使用率曲线一目了然。正常的内存使用应该是波动的，有升有降，垃圾回收或者缓存清理后内存会下来。如果看到一条斜向上的直线，那八成有问题。命令行下最常用的是free命令：free -h total used free shared buff/cache available Mem: 15Gi 8.5Gi 2.1Gi 256Mi 4.9Gi 6.2Gi Swap: 8.0Gi 1.2Gi 6.8Gi重点看available这列，这才是真正可用的内存。free那列看起来少，但很多是缓存，系统需要的时候可以回收。如果available持续下降，那就要警惕了。top命令更直观，能看到每个进程的内存占用：top - 14:30:25 up 10 days, 3:45, 2 users, load average: 0.52, 0.58, 0.59 Tasks: 156 total, 1 running, 155 sleeping, 0 stopped, 0 zombie %Cpu(s): 3.2 us, 1.5 sy, 0.0 ni, 94.8 id, 0.5 wa, 0.0 hi, 0.0 si, 0.0 st MiB Mem : 16384.0 total, 2150.5 free, 8720.3 used, 5513.2 buff/cache MiB Swap: 8192.0 total, 6800.2 free, 1391.8 used. 6352.1 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 12345 app 20 0 4.2g 3.5g 123m S 3.2 22.1 12:34.56 java按M可以按内存排序。VIRT是虚拟内存，RES是实际物理内存，SHR是共享内存。排查内存泄漏主要看RES，这个才是真正占用的物理内存。还有个命令vmstat，能看内存变化趋势：vmstat 1 10 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 1 0 1425400 2150352 451232 5060992 0 0 2 15 12 15 3 1 95 0 0 0 0 1425400 2148320 451240 5061124 0 0 0 0 1234 2156 3 1 95 0 0看free列是不是在持续减少。si和so是swap in和swap out，如果这两个值很大，说明系统在频繁换页，内存已经紧张了。定位问题进程发现系统内存不够了，下一步就是找出是谁在搞鬼。top命令能看到进程级别的内存占用，但有时候进程下面有很多线程，想知道具体是哪个线程占的多，可以用：top -H -p <pid>这样能显示进程下所有线程的资源占用情况。还有一个很好用的命令是ps，可以按内存排序：ps aux --sort=-%mem | head -10这会列出内存占用最高的10个进程。有时候进程名都一样，比如Java程序都叫java，怎么区分？看命令行参数：ps aux | grep java或者看/proc/<pid>…

K8s持久化存储深度解析：PV、PVC、StorageClass三剑客的生产实战
type: item
date: 2026-04-12T15:02:37+00:00
description: 做了这么多年运维，K8s的存储这块一直是个让人又爱又恨的东西。说它复杂吧，确实概念挺多的，PV、PVC、StorageClass这些名词听起来就头大；说它简单吧，一旦搞明白了原理，用起来还是很顺手的。今天我就把自己这些年在生产环境中踩过的坑、总结的经验分享给大家，让你们少走点弯路。为什么需要持久化存储容器本身是无状态的，这个大家都知道。Pod一重启，里面的数据就没了，这对于数据库、文件存储这些有状态应用来说简直是灾难。我记得刚开始用K8s的时候，有次MySQL的Pod重启了，结果所有数据都丢了，那个心情...简直想死的心都有。从那以后我就深刻认识到，持久化存储在K8s中有多重要。K8s的持久化存储主要解决这么几个问题：数据持久性：Pod重启、迁移后数据不丢失数据共享：多个Pod可以访问同一份数据存储抽象：屏蔽底层存储实现细节PV、PVC、StorageClass的关系这三个概念的关系，我用一个比较接地气的比喻来解释：PV（PersistentVolume）就像是一个仓库，管理员提前准备好的存储空间。PVC（PersistentVolumeClaim）就像是租仓库的申请单，用户说我要多大的空间、什么类型的。StorageClass就像是仓库的分类标准，比如高性能仓库、普通仓库、便宜仓库等。具体来说：PV（持久卷）PV是集群级别的资源，由管理员创建，代表集群中的一块存储。它包含存储实现的具体细节，比如NFS、iSCSI、云存储等。apiVersion: v1 kind: PersistentVolume metadata: name: my-pv spec: capacity: storage: 10Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain nfs: server: 192.168.1.100 path: /data/nfs这个配置我在生产环境用过很多次，需要注意几个点：capacity定义存储大小accessModes定义访问模式，RWO（ReadWriteOnce）表示只能被一个节点挂载persistentVolumeReclaimPolicy定义回收策略PVC（持久卷声明）PVC是用户对存储的请求，它会去寻找合适的PV进行绑定。apiVersion: v1 kind: PersistentVolumeClaim metadata: name: my-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi这里有个坑要注意，PVC请求的存储大小必须小于等于PV提供的大小，而且访问模式也要匹配。StorageClass（存储类）StorageClass提供了动态创建PV的方式，不需要管理员手动创建PV。apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: fast-ssd provisioner: kubernetes.io/aws-ebs parameters: type: gp2 zone: us-west-2a静态供应 vs 动态供应静态供应静态供应就是管理员提前创建好PV，然后用户创建PVC去绑定。这种方式比较传统，适合小规模或者对存储有特殊要求的场景。我在一个项目中就是用的静态供应，因为客户要求数据必须存储在指定的NFS服务器上，所以只能手动创建PV。动态供应动态供应通过StorageClass自动创建PV，用户只需要在PVC中指定StorageClass即可。apiVersion: v1 kind: PersistentVolumeClaim metadata: name: dynamic-pvc spec: accessModes: - ReadWriteOnce storageClassName: fast-ssd resources: requests: storage: 20Gi这种方式更灵活，特别适合云环境。我现在基本都用动态供应，省事多了。生命周期管理PV和PVC的生命周期有几个关键阶段：Available（可用）PV已创建，但还没有被PVC绑定。Bound（已绑定）PV已经被PVC绑定，正在使用中。Released（已释放）PVC被删除，但PV还没有被回收。Failed（失败）自动回收失败。这里要特别说一下回收策略，有三种：Retain：保留数据，需要手动清理Delete：自动删除PV和底层存储Recycle：已废弃，不建议使用我在生产环境中一般用Retain策略，因为数据安全第一，宁可手动清理也不能误删数据。访问模式详解K8s支持三种访问模式：…

深度拆解Tomcat：从架构设计到生产实践，这些年踩过的坑都在这里了
type: item
date: 2026-04-08T14:09:46+00:00
description: 说起Tomcat，估计每个搞Java开发和运维的同学都不陌生。但是真要问你Tomcat的内部工作原理，能完整说清楚的人其实不多。我在生产环境摸爬滚打这些年，从最开始的小白到现在能独当一面，Tomcat这个"老朋友"没少让我吃苦头。今天就把我这些年对Tomcat的理解和实战经验分享给大家，不是那种照本宣科的理论，而是真正在生产环境中能用得上的干货。为什么要深入了解Tomcat前段时间公司新来了个小伙子，部署应用的时候直接用默认配置就上线了。结果呢？服务刚上线没几天，用户量一上来就开始各种502、504错误。领导找过来问情况，那个场面...想想都尴尬。这种事情其实很常见，很多人觉得Tomcat嘛，下载下来启动就能用，有什么难的？但实际上，默认配置的Tomcat在生产环境基本上就是个定时炸弹。Tomcat的核心架构到底是怎么回事我记得刚开始接触Tomcat的时候，各种概念搞得我云里雾里的。什么Connector、Container、Engine、Host...听起来就头大。后来慢慢摸索，发现其实Tomcat的架构设计还是很有意思的。Server - 最顶层的管理者Server就是整个Tomcat实例，一个JVM进程里只有一个Server。你可以把它理解成一个大管家，负责管理下面所有的组件。在server.xml里面，最外层的<Server>标签就是它。我之前遇到过一个问题，就是在同一台机器上部署多个Tomcat实例的时候，Server的shutdown端口冲突了。默认是8005端口，如果不改的话，第二个实例启动就会报错。这种小细节在生产环境特别容易被忽略。Service - 服务的组织者Service在Server下面，主要作用是把Connector和Engine组织在一起。通常情况下，一个Server里只有一个Service，叫做"Catalina"。Connector - 网络通信的门面Connector是Tomcat对外提供服务的入口，负责监听端口、接收请求。这里面有两个重要的组件：Coyote - 这是Tomcat的HTTP连接器，专门处理HTTP协议。它支持多种I/O模型，比如BIO、NIO、NIO2、APR。我在生产环境一般都用NIO，性能和稳定性都不错。记得有一次线上出现了大量的TIME_WAIT连接，排查了半天发现是BIO模式下的连接没有及时释放。后来切换到NIO模式，问题就解决了。Jasper - 这是JSP引擎，负责把JSP页面编译成Servlet。虽然现在前后端分离比较多，JSP用得少了，但在一些老项目里还是经常能见到。Container - 业务处理的核心Container是一个层次结构，从外到内分别是Engine、Host、Context、Wrapper。Engine - 整个Servlet引擎，一个Service只有一个Engine。它负责处理所有的请求，并将请求分发给合适的Host。Host - 代表一个虚拟主机，可以配置多个域名指向同一个Tomcat实例。比如你可以让www.example.com和api.example.com都指向同一个Tomcat，但处理不同的应用。Context - 代表一个Web应用，也就是我们部署的WAR包或者目录。每个Context有自己独立的类加载器，这样不同应用之间就不会互相影响。Wrapper - 代表一个Servlet，是最小的处理单元。请求处理的完整流程理解了架构，我们来看看一个HTTP请求在Tomcat中是怎么被处理的。当用户在浏览器输入网址访问我们的应用时，请求会这样流转：Connector接收请求 - Coyote监听到8080端口有请求进来，创建Request和Response对象协议解析 - 解析HTTP协议，提取请求头、请求体等信息请求分发 - Engine根据Host头信息找到对应的虚拟主机应用定位 - Host根据URL路径找到对应的Context（Web应用）Servlet匹配 - Context根据URL模式匹配到具体的Servlet业务处理 - Wrapper调用Servlet的service方法处理业务逻辑响应返回 - 处理结果通过Connector返回给客户端这个过程看起来复杂，但实际上Tomcat内部有很多优化。比如对象池、连接池等技术，避免频繁创建销毁对象。生产环境的配置优化说了这么多理论，现在来点实际的。我把这些年在生产环境踩过的坑和总结的经验分享一下。JVM参数调优这是最基础也是最重要的。我见过太多因为JVM参数配置不当导致的生产事故。export JAVA_OPTS="-server -Xms4g -Xmx4g -XX:NewRatio=1 -XX:+UseConcMarkSweepGC -XX:+CMSParallelRem…

生产环境 Nginx 代理 WebSocket 总断连？这几个隐藏配置坑了多少人
type: item
date: 2026-04-07T14:26:54+00:00
description: 前两天半夜正准备关机睡觉，微信突然响个不停。消息连着发了好几条，全是感叹号，说他们新上线的即时通讯服务炸了，客户端连两分钟都撑不住就掉线，重启服务好了一小会儿，马上又不行了。我看了一眼他发来的 Nginx 配置截图，再看了看报错日志，心里大概就有数了。这情况太典型了，很多兄弟在搞 WebSocket 代理的时候，都是照着网上的教程一顿抄，proxy_pass 一配，upstream 一挂，测试环境通了一下就觉得完事了。结果一上生产，并发上来，时间一长，各种稀奇古怪的问题全冒出来。WebSocket 这玩意儿，看着简单，不就是 HTTP 协议升个级嘛，但在 Nginx 这一层面，如果不把它的脾气摸透，真的能把人折腾死。今天咱们就借着这个实际案例，把 Nginx 代理 WebSocket 这件事彻底掰开了揉碎了讲讲，特别是那些没人告诉你、但生产环境一定会遇到的坑。咱们先得搞清楚，WebSocket 到底是个啥，为什么 Nginx 默认的 HTTP 代理配置搞不定它。平时咱们浏览网页，大多是 HTTP 协议。这玩意儿是个“哑巴”模式，客户端发个请求，服务器回个响应，这就完事了，连接断开。就像你去食堂打饭，你喊一句“阿姨来份红烧肉”，阿姨给你打一勺，交易结束，你俩就没关系了。下次想要米饭，得重新排队、重新喊。但是 WebSocket 不一样。它是一次握手，长久通话。就像你跟对象打电话，拨通那一刻（握手），双方的话筒（连接）就一直通着。你想说话就说，对方想回话就回，不用每次都重新拨号。这就是所谓的全双工通信。对于即时通讯、在线游戏、股票行情这种需要服务器主动推数据给客户端的场景，简直是神器。那问题来了，Nginx 作为一个反向代理，默认是处理 HTTP 请求的。它就像一个中间商，负责把客户的请求转给后端的服务器。对于普通的 HTTP 请求，Nginx 转发完就等着后端回包，然后发给客户，任务结束。但 WebSocket 来了，Nginx 就有点懵了。WebSocket 的建立过程，首先是发一个 HTTP 请求，但是这个请求头里带了点“私货”：Upgrade: websocket Connection: Upgrade这两句话的意思就是告诉服务器：“哥们，我不发普通网页请求了，咱们把协议升个级，换成 WebSocket 玩玩吧。”如果 Nginx 不做特殊配置，它看到这个请求，会把它当成普通 HTTP 请求转给后端。后端服务器（比如 Node.js、Go 或者 Java）收到请求，一看头信息，知道要升级协议，就会返回一个 101 Switching Protocols 的状态码。这时候，如果 Nginx 还傻傻地等着后端返回 HTTP 200 或者 404，那连接就断了。因为 101 状态码意味着协议切换，之后的数据流不再是标准的 HTTP 报文，而是 TCP 数据流了。所以，要让 Nginx 成功代理 WebSocket，核心就是得让它“睁一只眼闭一只眼”，允许协议升级，并且把这个升级的请求头原封不动地传给后端，同时还要保持这条连接长时间不断开。咱们来看看小张发给我的那个“有问题”的配置，估计很多人也是这么配的：upstream ws_backend { server 192.168.1.100:8080; } server { listen 80; server_name ws.example.com; location /ws { proxy_pass http://ws_backend; } }这配置看着挺干净，没毛病吧？测试环境测一下，发个消息，能收到。然后大家就以为搞定了。实际上，这配置在生产环境就是个“定时炸弹”。第一个坑，也是最直接的坑，就是握手失败。很多时候你会发现，浏览器控制台报 WebSocket connection failed，状态码 400 或者 502。这是因为 Nginx 默认转发请求的时候，可能会修改或者丢弃 Upgrade 和 Connection 这两个头。后端收不到这两个头，就不知道你要升级协议，自然就按普通 HTTP 请求处理了，结果就是握手失败。所以，咱们得明确告诉 Nginx，把这两个头给我传过去。标准配置得这么写：location /ws { proxy_pass http://ws_backend; # 关键配置：必须使用 HTTP 1.1 版本 proxy_http_version 1.1; # 透传 Upgrade 头 proxy_set_header Upgrade $http_upgrade; # 设置 Connection 头 proxy_set_header Connection "Upgrade"; }这里有个细节，proxy_http_version 1.1; 这一行千万…

Jenkins Pipeline 脚本踩坑记：我是如何被两种语法折磨并最终选择的
type: item
date: 2026-04-06T14:33:06+00:00
description: 最近在折腾公司的 CI/CD 流水线，想把原来那套老掉牙的构建脚本升级一下。本以为 Jenkins Pipeline 挺简单的，结果一上手就懵了——竟然有两种写法！这不是逼死选择困难症吗？我当时的内心OS：这玩意儿就像去饭店点菜，服务员问你要清汤还是麻辣的，你以为选完就完事了，结果又问你要不要加香菜、要不要加醋... 选择太多反而不知道怎么选了。Pipeline 的两种"口味"Jenkins Pipeline 主要有两种写法：Declarative Pipeline（声明式流水线）和 Scripted Pipeline（脚本式流水线）。Declarative Pipeline 就像是傻瓜相机，你只需要按快门就行，其他的都帮你调好了。它用的是一种 DSL（Domain Specific Language），说白了就是专门为 Pipeline 设计的语言。整个 Pipeline 必须包在 pipeline 块里面，然后用 stages 和 steps 来组织你的构建步骤。而 Scripted Pipeline 呢，更像是单反相机，功能强大但你得自己调参数。它直接用的是 Groovy 语言，灵活性爆表，但也意味着你得有点 Groovy 基础。整个脚本是在 node 块里执行的[4]。我的踩坑之旅第一坑：语法差异刚开始我用的是 Declarative Pipeline，写起来确实简单：pipeline { agent any stages { stage('Build') { steps { echo 'Building...' } } } }看着挺清爽的对吧？结构一目了然。后来有个需求，需要在构建过程中动态判断一些条件，我就想着用 if-else。结果发现 Declarative Pipeline 里不能直接写 if 语句！你得用 when 指令：stage('Deploy') { when { branch 'master' } steps { echo 'Deploying to production...' } }这时候我就想，要不试试 Scripted Pipeline？切换过去一看：node { stage('Build') { echo 'Building...' if (env.BRANCH_NAME == 'master') { echo 'This is master branch' } } }嘿，这下自由了！想怎么写就怎么写。但问题又来了...第二坑：错误处理Declarative Pipeline 的错误处理特别规范，有专门的 post 块：pipeline { agent any stages { stage('Test') { steps { sh 'make test' } } } post { always { echo 'This will always run' } success { echo 'Only on success' } failure { echo 'Only on failure' mail to: 'team@example.com', subject: 'Build Failed' } } }但在 Scripted Pipeline 里，你得自己写 try-catch：node { try { stage('Test') { sh 'make test' } echo 'Success!' } catch (e) { echo 'Failed!' throw e } finally { echo 'Cleanup' } }说实话，写多了 try-catch 真的很烦。有一次我忘了写 finally 块，结果构建失败后一些临时文件没清理，磁盘差点爆了。第三坑：并行执行这个是我觉得两种 Pipeline 差异最大的地方。Declarative Pipeline 的并行执行写法很直观：pipeline { agent any stages { stage('Parallel Stage') { parallel { stage('Unit Tests') { steps { echo 'Running unit tests...' } } stage('Integration Tests') { steps { echo 'Running integration tests...' } } } } } }而 Scripted Pipeline 的写法... 说实话第一次看到我是懵的：node { stage('Parallel Stage') { parallel( "Unit Tests": { echo 'Running unit tests...' }, "Integratio…