古人学问无遗力，少壮工夫老始成。 纸上得来终觉浅，绝知此事要躬行。
手把手教你落地网络入侵检测系统（NIDS），从选型到上线全记录
说实话，搞安全这块东西，很多运维同学是又爱又恨。爱的是确实能帮你挡住不少事儿，恨的是部署起来各种坑，配不好还一堆误报，搞得你天天看告警看到想吐。我之前在客户公司负责过一套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（主要存日志，日志量大的话还得考虑外挂存储）网卡这块特别说一下，一定要选支持多队列的网卡，这样才能充分利用Suricata的多线程能力。Intel的X710和X520系列都可以。另外，接收镜像流量的网卡不要配IP地址，就让它处于混杂模式（promisc mode），只收不发。# 设置网卡为混杂模式 ip link set eth1 promisc on # 关闭网卡的一些offload功能，避免影响抓包 ethtool -K eth1 rx off tx off gro off lro off关闭offload这一步很重要，不然网卡会在硬件层面对数据包做合并处理，Suricata拿到的就不是原始的包了，可能导致检测失败。Suricata安装和基础配置以CentOS 7为例，安装过程记录一下：# 安装EPEL和依赖 yum install epel-release -y yum install gcc gcc-c++ make automake autoconf libtool pcre-devel \ libpcap-devel libyaml-devel file-devel zlib-devel jansson-devel \ nss-devel libcap-ng-devel libnet-devel lua-devel -y # 安装Rust（Suricata 6.0+需要） curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env # 下载并编译Suricata wget https://www.openinfosecfoundation.org/download/suricata-6.0.13.tar.gz tar xzf suricata-6.0.13.tar.gz cd suricata-6.0.13 ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var \ --enable-nfqueue --enable-lua make -j$(nproc) make install make install-conf当然你也可以直接用包管理器装，省事儿：# 添加OISF的源 yum install https://copr.fedorainfracloud.org/coprs/jasonish/suricata-stable/repo/epel-7/jasonish-suricata-stable-epel-7.repo yum install suricata -y装完之后，主配置文件在 /etc/suricata/suricata.yaml，这个文件巨长，有好几千行。但核心要改的地方其实没几个。# 定义你的内网地址段 vars: address-groups: HOME_NET: "[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]" EXTERNAL_NET: "!$HOME_NET" port-groups: HTTP_PORTS: "80" SHELLCODE_PORTS: "!80" SSH_PORTS: "22" DNS_PORTS: "53" # 配置监听接口 af-packet: - interface: eth1 cluster-id: 99 cluster-type: cluster_flow defrag: yes use-mmap: yes tpacket-v3: yes ring-size: 2048 block-size: 32768 # 配置规则文件路径 default-rule-path: /var/lib/suricata/rules rule-files: - suricata.rules # EVE日志输出配置 outputs: - eve-log: enabled: yes filetype: regular filename: eve.json types: - alert: tagged-packets: yes - http: extended: yes - dns: - tls: extended: yes - files: force-magic: yes - flow这里 HOME_NET 的配置特别重要，一定要设成你实际的内网地址段。Suricata的很多规则都是基于 "从外到内" 或者 "从内到外" 的方向来判断的，如果HOME_NET配错了，很多规则直接就不生效。af-packet 是Linux上推荐的抓包方式，性能比libpcap好很多。cluster_flow 模式会把同一个连接的数据包分配到同一个线程处理，避免乱序。规则管理这块水很深Suricata自带一个规则更新工具 suricata-update，用起来还挺方便的：# 更新规则 suricata-update # 查看可用的规则源 suricata-update list-sources # 启用ET Open规则集（免费的） suricata-update enable-source et/open # 启用后重新更新 suricata-updateET Open是Emerging Threats提供的免费规则集，质量还可以，覆盖面也比较广。如果你有预算，可以买ET Pro的订阅，规则更全更新也更快。但说实话，默认的规则集直接用，你会被告警淹没。我们刚上线的时候，一天产生了将近10万条告警，根本看不过来。大部分都是误报或者低价值的告警。所以规则调优是必须要做的事情。我的做法是分几步走：第一步：先跑几天，收集数据不要急着处理告警，先让它跑着，看看告警的分布情况。# 统计告警按signature排序 cat /var/log/suricata/eve.json | jq -r 'select(.event_type=="alert") | .alert.signature' | sort | uniq -c | sort -rn | head -30你会发现排在前面的往往就那么几条规则，贡献了80%以上的告警量。第二步：逐条分析，该禁的禁比如我们当时排在第一的是一条 ET POLICY GNU/Linux APT User-Agent Outbound likely related to package management 的规则。这条规则检测的是Linux服务器用apt下载软件包的行为，在我们的环境里完全是正常操作，直接禁掉。# 创建禁用规则列表 echo "2013504" >> /etc/suricata/disable.conf # 重新更新规则（会自动应用禁用列表） suricata-update第三步：针对业务写自定义规则通用规则集不可能覆盖你所有的业务场景。比如我们有个内部系统，正常情况下只有特定的几台服务器会访问数据库的3306端口，如果其他IP去访问了，那就很可疑。这种就得自己写规则：alert tcp ![$DB_CLIENT_1,$DB_CLIENT_2,$DB_CLIENT_3] any -> $DB_SERVER 3306 (msg:"Unauthorized MySQL Access Attempt"; flow:to_server,established; sid:1000001; rev:1; classtype:policy-violation;)写自定义规则的时候有个技巧，sid从1000001开始编号，这样不会和官方规则集冲突。还有一个我觉得特别实用的规则，检测内网机器对外发起DNS查询到非公司DNS服务器的情况。因为很多恶意软件会用DNS隧道来做C2通信，而且它们通常不会用你公司内部的DNS服务器：alert udp $HOME_NET any -> !$COMPANY_DNS 53 (msg:"DNS query to non-corporate DNS server"; sid:1000002; rev:1; classtype:policy-violation;)跟ELK对接，让告警可视化Suricata产生的eve.json日志，直接看JSON文件肯定不现实。我们用的是经典的ELK方案来做日志收集和可视化。在Suricata服务器上装Filebeat：# /etc/filebeat/filebeat.yml filebeat.inputs: - type: log enabled: true paths: - /var/log/suricata/eve.json json.keys_under_root: true json.add_error_key: true output.elasticsearch: hosts: ["10.0.0.100:9200"] index: "suricata-%{+yyyy.MM.dd}" setup.template.name: "suricata" setup.template.pattern: "suricata-*"然后在Kibana里建个Index Pattern，就能看到所有的告警了。我建了几个比较实用的Dashboard面板：告警总览：按时间线展示告警数量趋势，能快速发现异常的告警峰值Top 10告警规则：看哪些规则触发最多，方便持续调优Top 10源IP/目的IP：快速定位可疑的IP地址告警严重等级分布：饼图展示高中低告警的比例DNS查询Top 20：配合Zeek的DNS日志，看有没有奇怪的域名查询有了这些面板，每天早上花10分钟扫一眼，心里就有数了。别忘了HIDS，两者配合才完整NIDS看的是网络层面的流量，但有些东西它看不到。比如加密流量（HTTPS），NIDS只能看到TLS握手的信息，里面的内容是看不到的。再比如主机上的文件被篡改了、有人用sudo提权了，这些都是网络层面发现不了的。所以NIDS和HIDS（主机入侵检测系统）应该配合着用。HIDS推荐OSSEC或者它的分支Wazuh，能监控文件完整性、系统日志、rootkit检测这些。我们的做法是：NIDS负责盯网络流量，发现扫描、攻击尝试、异常连接这些；HIDS负责盯主机层面，发现文件变更、异常登录、可疑进程这些。两边的告警都汇总到ELK里面，做关联分析。比如NIDS告警说某个外部IP在对Web服务器做SQL注入尝试，然后HIDS告警说同一台Web服务器上的数据库配置文件被修改了，这两个事件关联起来看，那基本就可以确认是被打进来了。单看任何一边都不一定能确定。威胁情报集成光靠规则匹配其实是不够的，因为规则都是已知的模式。如果攻击者用了新的手法，规则库里没有，那就检测不到。引入威胁情报可以弥补一部分。简单来说就是把已知的恶意IP、恶意域名、恶意文件Hash这些信息喂给NIDS，让它去匹配。Suricata支持加载IP黑名单，你可以从一些免费的威胁情报源获取数据：Abuse.ch 的各种黑名单（SSL黑名单、恶意URL、Botnet C2等）AlienVault OTX 的开放威胁情报CIRCL 的被动DNS数据我写了个简单的脚本，每天自动拉取这些情报源，转换成Suricata的规则格式，然后reload：#!/bin/bash # 下载Abuse.ch的Botnet C2 IP黑名单 wget -q https://feodotracker.abuse.ch/downloads/ipblocklist_recommended.txt -O /tmp/c2_ips.txt # 转换成Suricata规则 > /var/lib/suricata/rules/threat_intel.rules while read ip; do "$ip" =~ ^#.*$ && continue -z "$ip" && continue echo "alert ip $ip any -> \$HOME_NET any (msg:\"TI - Known Botnet C2 IP: $ip\"; sid:$((2000000 + RANDOM)); rev:1; classtype:trojan-activity;)" >> /var/lib/suricata/rules/threat_intel.rules done < /tmp/c2_ips.txt # 热重载规则 suricatasc -c reload-rulessuricatasc -c reload-rules 这个命令可以在不重启Suricata的情况下重新加载规则，不会中断检测，很实用。日常运维和持续优化系统上线之后，不是说就不用管了。NIDS的运维其实是个持续的过程。规则更新我设了个crontab，每天凌晨3点自动更新规则：0 3 * * * /usr/bin/suricata-update && /usr/bin/suricatasc -c reload-rules性能监控Suricata自身的性能也要关注。它有个stats日志，记录了丢包率、内存使用等信息：# suricata.yaml中开启stats stats: enabled: yes interval: 30重点关注 capture.kernel_drops 这个指标，如果丢包率持续上升，说明要么是硬件性能不够了，要么是规则太多导致处理不过来。我们之前遇到过一次丢包率突然飙升的情况，排查下来发现是有人在内网做大文件传输，流量瞬间冲到了8Gbps，超过了传感器的处理能力。后来的解决办法是在镜像端口上做了过滤，只镜像需要监控的VLAN流量，不相关的大流量传输就不镜像了。误报处理这个是永远干不完的活儿。我的经验是建一个误报处理的文档或者wiki，每次处理完一个误报，就记录下来：哪条规则、什么情况触发的、怎么处理的（是禁用规则还是加了例外）。时间长了这就是一份很有价值的知识库。定期review每个月抽一天时间，把这个月的告警数据拉出来看一看。哪些规则从来没触发过？是因为确实没有这种威胁，还是因为部署位置不对导致看不到？哪些规则触发太多？是不是需要进一步调优？网络架构有没有变化，需不需要调整探针位置？关于合规和隐私这个点很多人会忽略，但其实挺重要的。NIDS抓的是网络流量，里面可能包含用户的个人信息、通信内容这些敏感数据。我们当时做了几件事：明确了NIDS只用于安全检测目的，不会用来监控员工的上网行为日志的访问权限做了严格控制，只有安全团队的人能看日志保留周期设为90天，过期自动删除在员工手册里加了一条关于网络安全监控的说明这些东西看起来是走形式，但万一真出了事，有没有这些合规措施，差别还是很大的。写到这里差不多了。整个NIDS的落地过程，说白了就是：想清楚要监控什么 → 选对工具 → 把流量引过来 → 装好配好 → 写规则调规则 → 接上日志平台 → 持续运营优化。每一步都不难，但每一步都有坑。最关键的其实是最后一步，持续运营。我见过不少公司花了大价钱买了商业IDS/IPS，装上之后就没人管了，告警从来不看，规则从来不更新，跟没装一样。工具再好，没有人去运营，就是个摆设。希望这篇文章对正在考虑部署NIDS的朋友有些帮助。如果你在部署过程中遇到什么问题，欢迎留言交流。觉得有用的话，帮忙点个赞转发一下，让更多人看到，感谢！公众号：运维躬行录个人博客：躬行笔记
别让你的Nginx裸奔！生产环境下的访问控制实战，全是填坑经验
咱们搞运维的，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的数据库文件。以前MaxMind提供免费的GeoLiteCountry，现在虽然收费了，但GeoLite2还是能用的，就是得注册个账号下载。下载解压后，放到服务器某个目录下，比如/usr/share/GeoIP/。配置文件里这么写：http { # 加载数据库文件 geoip_country /usr/share/GeoIP/GeoLite2-Country.mmdb; # 定义一个变量，判断是否允许访问 # 这里假设我们只允许中国(代码CN)访问 map $geoip_country_code $allowed_country { default no; CN yes; } server { location / { if ($allowed_country = no) { return 403 "Service not available in your region."; } # 其他配置... } } }这招在应对某些特定地域的攻击时特别有效。之前有个客户，是个本地生活服务的APP，只做本地生意，我直接给他配置了只允许本地IP段访问，外加GeoIP过滤，日志瞬间干净了，CPU负载都降了好几个点。不过这玩意儿也有坑。那个GeoIP数据库不是实时更新的，IP库这东西变动很频繁，偶尔会有误杀的情况。比如有些用户用了国外的代理，或者IP库定位不准，就会被挡在外面。所以配置完，一定要留好监控，看看有没有大量的403报错，或者给个友好的提示页面，别让用户以为网站挂了。防止恶意刷接口：请求频率限制刚才说的都是怎么控制“谁能进”，接下来咱们聊聊怎么控制“怎么进”。这就是限流，也是访问控制的重要一环。如果你的接口被人家用脚本狂刷，或者有人恶意CC攻击，你的服务器资源瞬间就会被耗尽。Nginx的ngx_http_limit_req_module模块就是专门干这个的。它用的是“漏桶算法”，不管你发请求多快，到了我这儿都得按规矩排队漏下去。配置分两块。第一块在http块里定义限流规则：http { # 定义一个叫 my_limit 的限流区域 # $binary_remote_addr 是根据客户端IP来限制 # zone=my_limit:10m 定义区域名字和共享内存大小，10M大概能存16万个IP状态 # rate=10r/s 表示每秒允许10个请求 limit_req_zone $binary_remote_addr zone=my_limit:10m rate=10r/s; }然后在server或者location里应用这个规则：location /api/ { # burst=20 允许突发流量，也就是短时间内可以多来20个请求排队 # nodelay 超过burst限制直接拒绝，不进行延迟处理 limit_req zone=my_limit burst=20 nodelay; # 被限制时返回的状态码，默认是503，我们可以改成429 Too Many Requests limit_req_status 429; proxy_pass http://backend; }这里的burst和nodelay参数特别有讲究。如果不加burst，请求非常平滑，每秒严格控制在10个，多一个都不行，用户体验可能不太好，感觉网站一卡一卡的。加了burst=20，就像给桶扩容了，瞬间来30个请求，前10个正常处理，后20个排队。但是如果不加nodelay，那排队的20个请求处理起来会很慢，因为要按rate的速度漏出去。加了nodelay，就是排队的那20个请求也会立马转发给后端，但是桶里的“令牌”用光了，再来请求就直接拒绝。这块参数怎么调，得看你的业务。如果是秒杀场景，burst设大点，nodelay必须加，保证瞬时并发。如果是普通的API，设小点，保护后端。我有次把burst设得太小，结果公司搞营销活动，流量稍微一上来，全是503报错，被运营妹子追着问是不是服务器炸了。所以说，这玩意儿得压测，得根据实际情况慢慢调优。神奇的Nginx变量与Map指令有些时候，我们的需求比较变态。比如，我想封禁所有User-Agent里包含"Java"或者"Python"字样的请求，防止爬虫；或者我想只允许特定URL带特定参数的请求。这时候if指令就派上用场了。虽然网上都说if is evil，但在location里做些简单的判断还是挺好用的。比如封杀特定UA：location / { # $http_user_agent 代表请求头里的User-Agent if ($http_user_agent ~* (python|curl|java|wget)) { return 403; } }这招对付那些不伪装UA的简单爬虫一抓一个准。再高级点的，比如我们想根据请求参数做白名单。有个接口，只有带着特定token参数的请求才能访问。这时候可以用map指令，这可是个好东西，能帮我们减少很多if判断，让配置更清爽。http { # $arg_token 就是url里的token参数 map $arg_token $is_allowed { default 0; "my_secret_key" 1; } server { location /private_api/ { if ($is_allowed = 0) { return 403; } proxy_pass http://backend; } } }这样配置，只有当URL是/private_api/?token=my_secret_key时，$is_allowed才是1，否则都是0，直接403。这比写一堆正则匹配要清晰得多，性能也更好。还有个大杀器：auth_request最后再提一个比较高级的功能，ngx_http_auth_request_module。这个模块允许你把认证逻辑委托给一个外部服务。啥意思呢？比如你们公司有个统一的SSO登录中心，或者有个专门的鉴权微服务。你不想在Nginx里维护复杂的密码文件，也不想写Lua脚本去连数据库。那就可以用这个。配置大概是这样的：location /private/ { auth_request /auth; # 这里的 /auth 是一个内部location error_page 401 = @error401; proxy_pass http://backend_service; } location = /auth { internal; # internal表示这个location只能内部重定向访问，外部直接访问404 proxy_pass http://auth_service/validate; proxy_pass_request_body off; # 不转发请求体，只转发头 proxy_set_header Content-Length ""; proxy_set_header X-Original-URI $request_uri; }流程是这样的：用户访问/private/，Nginx会先偷偷发一个子请求到/auth，也就是你的鉴权服务。如果鉴权服务返回200，Nginx就继续把请求转发给后端；如果返回401或403，Nginx就直接拦截。这个玩法扩展性极强。你可以把IP黑白名单、用户Token校验、甚至复杂的权限逻辑都放在那个auth_service里去写，用什么语言都行，只要返回特定的HTTP状态码就行。Nginx只负责挡在前面做调度。这在微服务架构里特别常见，实现了业务逻辑和网关配置的解耦。填坑总结写了这么多，其实Nginx的访问控制无非就是“你是谁”、“你从哪来”、“你要干什么”这三个问题。我们在生产环境落地的时候，往往不是单一使用某一种手段，而是组合拳。比如，先封禁恶意UA，再限制IP地域，然后对关键接口做频率限制，最后对后台管理页面做IP白名单+密码认证。还有几个小坑大家注意一下：配置完一定要测试：nginx -t是保命符，但有时候语法对不代表逻辑对，最好有个测试环境先验证。日志是关键：不管你配置得多么完美，总会有漏网之鱼或者误杀。一定要盯着日志看，特别是配置变更后的那段时间。版本差异：Nginx版本更新挺快，有些模块的指令在不同版本下可能有细微差别，遇到报错多看官方文档，别光百度。运维这活儿，就是要在便利性和安全性之间找平衡。锁得太死，业务跑不起来；放得太宽，半夜起来救火。咱们能做的，就是尽量把墙筑得高一点，门留得合适一点。今天的分享就到这儿吧，希望能给大家在Nginx安全加固方面提供点思路。这些都是我平时一点一点攒下来的经验，不一定是最完美的，但绝对是实战中能用得上的。如果你觉得这篇文章对你有帮助，或者帮你避开了几个坑，别忘了点个在看或者转发给身边搞运维的兄弟。咱们这行，技术得交流，经验得分享，一个人走得快，一群人走得远。公众号：运维躬行录个人博客：躬行笔记
从零开始搞定Chef自动化部署，让服务器管理不再头疼
最近公司的服务器数量又增加了不少，每次部署新应用都要重复执行一堆命令，配置文件改来改去的，实在是太麻烦了。我琢磨着得找个自动化工具来解决这个问题，经过一番调研，最终选择了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] end这个Recipe做了几件事：安装Nginx包、创建网站目录、使用模板生成配置文件、启用站点、确保服务运行。看起来是不是比写shell脚本清晰多了？模板的妙用Chef的模板功能特别实用。在templates/default/mysite.conf.erb中：server { listen <%= @port %>; server_name <%= @server_name %>; root /var/www/mysite; index index.html index.htm; location / { try_files $uri $uri/ =404; } access_log /var/log/nginx/<%= @server_name %>_access.log; error_log /var/log/nginx/<%= @server_name %>_error.log; }模板中的变量会在运行时被替换，这样同一个模板可以用于不同的环境，只需要改变attributes就行了。Attributes的灵活运用Attributes是Chef中用来存储配置数据的地方。在attributes/default.rb中定义默认值：default['nginx_setup']['server_name'] = 'example.com' default['nginx_setup']['port'] = 80 default['nginx_setup']['worker_processes'] = 'auto' default['nginx_setup']['worker_connections'] = 1024这些属性可以在不同层级被覆盖，比如在Role、Environment或者Node级别。这种机制让配置管理变得非常灵活。我在实际使用中，通常会为不同环境创建不同的Environment文件：# environments/production.rb name 'production' description 'Production environment' override_attributes( 'nginx_setup' => { 'server_name' => 'prod.example.com', 'worker_processes' => 4, 'worker_connections' => 2048 } )Data Bags存储敏感信息处理敏感信息时，Data Bags是个好选择。比如数据库密码、API密钥这些，不适合直接写在代码里。创建一个加密的Data Bag：knife data bag create credentials mysql --secret-file /path/to/secret_key然后在Recipe中使用：mysql_creds = Chef::EncryptedDataBagItem.load('credentials', 'mysql') mysql_service 'default' do port '3306' version '5.7' initial_root_password mysql_creds['root_password'] action [:create, :start] end这样密码就不会明文出现在代码中了，安全性大大提高。实战案例：部署一个完整的Web应用栈说了这么多理论，来个实际的例子。我曾经用Chef部署过一个典型的LAMP栈应用，包括Nginx作为反向代理、多个PHP-FPM实例、MySQL数据库。整个Cookbook的结构是这样的：webapp_stack/ ├── recipes/ │ ├── default.rb │ ├── nginx.rb │ ├── php.rb │ ├── mysql.rb │ └── app_deploy.rb ├── templates/ │ ├── nginx_vhost.conf.erb │ ├── php_pool.conf.erb │ └── app_config.php.erb ├── attributes/ │ └── default.rb └── metadata.rb在recipes/default.rb中，我按顺序调用其他Recipe：include_recipe 'webapp_stack::mysql' include_recipe 'webapp_stack::php' include_recipe 'webapp_stack::nginx' include_recipe 'webapp_stack::app_deploy'PHP-FPM的配置比较有意思，我用了一个循环来创建多个池：# recipes/php.rb node['webapp_stack']['php_pools'].each do |pool_name, pool_config| template "/etc/php/7.4/fpm/pool.d/#{pool_name}.conf" do source 'php_pool.conf.erb' variables( pool_name: pool_name, user: pool_config['user'], group: pool_config['group'], listen: pool_config['listen'], pm_max_children: pool_config['pm_max_children'] ) notifies :restart, 'service[php7.4-fpm]', :delayed end end这样可以很方便地根据需要创建多个PHP-FPM池，实现应用隔离。使用Role组织节点当服务器数量增多后，一个个管理Node会很麻烦。这时候Role就派上用场了。我通常会创建几个标准角色：# roles/web_server.rb name 'web_server' description 'Web server role' run_list( 'recipe[webapp_stack::nginx]', 'recipe[webapp_stack::php]' ) default_attributes( 'nginx_setup' => { 'worker_processes' => 2 } )# roles/db_server.rb name 'db_server' description 'Database server role' run_list( 'recipe[webapp_stack::mysql]' ) default_attributes( 'mysql' => { 'bind_address' => '0.0.0.0' } )然后给节点分配角色：knife node run_list add web01.example.com 'role[web_server]' knife node run_list add db01.example.com 'role[db_server]'调试技巧写Cookbook的过程中难免会遇到问题，分享几个调试技巧。使用chef-client的--why-run模式可以预览会执行哪些操作，不会真正执行：sudo chef-client --why-run在Recipe中可以用log资源输出调试信息：log "Current value of server_name: #{node['nginx_setup']['server_name']}" do level :info end如果某个资源执行失败，可以用ignore_failure让Chef继续执行：package 'some-optional-package' do ignore_failure true end但这个要谨慎使用，可能会掩盖真正的问题。版本管理和测试Cookbook也是代码，当然要做版本管理。我的做法是每个Cookbook都是一个Git仓库，在metadata.rb中管理版本：name 'webapp_stack' maintainer 'Your Name' maintainer_email 'your@email.com' license 'All Rights Reserved' description 'Installs/Configures webapp stack' version '1.2.3' depends 'mysql', '~> 8.0' depends 'php', '~> 4.0'版本号遵循语义化版本规范，这样在升级时能清楚知道变更的影响范围。测试方面，可以用Test Kitchen进行集成测试：# .kitchen.yml driver: name: vagrant provisioner: name: chef_zero platforms: - name: ubuntu-20.04 suites: - name: default run_list: - recipe[webapp_stack::default] attributes: webapp_stack: server_name: test.example.com运行kitchen test就能在虚拟机中测试Cookbook了。一些坑和经验使用Chef这段时间，也踩了不少坑。比如有一次，我在Recipe中直接修改了配置文件，结果每次运行chef-client都会触发服务重启，即使配置没有变化。后来才知道要用only_if或not_if来添加条件判断：template '/etc/nginx/nginx.conf' do source 'nginx.conf.erb' notifies :restart, 'service[nginx]', :delayed not_if { ::File.exist?('/etc/nginx/nginx.conf') && ::File.read('/etc/nginx/nginx.conf').include?('# Managed by Chef') } end还有一个经验是，不要在Recipe中使用太多Ruby的高级特性。虽然Chef支持完整的Ruby语法，但过于复杂的代码会让其他人难以理解和维护。保持简单直观是最好的。资源的执行顺序也很重要。Chef默认按照Recipe中的顺序执行，但有时需要用notifies和subscribes来控制资源间的依赖关系：template '/etc/app/config.yml' do source 'config.yml.erb' notifies :restart, 'service[app]', :immediately end service 'app' do action :nothing subscribes :restart, 'template[/etc/app/config.yml]', :immediately end性能优化当管理的节点数量增多后，性能问题就显现出来了。几个优化建议：合理使用lazy属性评估，避免不必要的计算：template '/etc/app/config.yml' do variables lazy { { db_host: search(:node, 'role:database').first['ipaddress'] } } end减少search的使用，search操作比较耗时，可以考虑用Data Bags或Attributes代替。使用chef-client的定时运行而不是持续运行模式，减少资源消耗。
蓝绿部署实战指南：从零到一搭建生产级无缝发布系统
蓝绿部署实战指南：从零到一搭建生产级无缝发布系统我在公司做运维这几年，最怕的就是发布新版本的时候出问题。记得有一次周五下午发版本，结果服务挂了两个小时，用户投诉电话打爆了，我在机房里忙活到晚上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 # 管理接口 stats socket /var/run/haproxy.sock mode 666 level admin stats timeout 2m切换脚本：#!/bin/bash # haproxy_switch.sh SOCKET="/var/run/haproxy.sock" # 获取当前蓝环境权重 BLUE_WEIGHT=$(echo "show stat" | socat stdio $SOCKET | grep "web_servers,blue" | cut -d, -f19) if [ "$BLUE_WEIGHT" = "100" ]; then # 切换到绿环境 echo "set weight web_servers/blue 0" | socat stdio $SOCKET echo "set weight web_servers/green 100" | socat stdio $SOCKET echo "已切换到绿环境" else # 切换到蓝环境 echo "set weight web_servers/blue 100" | socat stdio $SOCKET echo "set weight web_servers/green 0" | socat stdio $SOCKET echo "已切换到蓝环境" fiHAProxy的好处是可以实现渐进式切换，比如先把10%的流量切到新环境，观察一段时间没问题再全量切换。方式三：基于Kubernetes的蓝绿部署如果你们公司已经用了K8s，那蓝绿部署就更简单了。K8s的Service可以通过selector来控制流量指向。先创建两个Deployment：# blue-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: app-blue labels: app: myapp version: blue spec: replicas: 3 selector: matchLabels: app: myapp version: blue template: metadata: labels: app: myapp version: blue spec: containers: - name: app image: myapp:v1.0 ports: - containerPort: 8080 --- # green-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: app-green labels: app: myapp version: green spec: replicas: 3 selector: matchLabels: app: myapp version: green template: metadata: labels: app: myapp version: green spec: containers: - name: app image: myapp:v2.0 ports: - containerPort: 8080Service配置：apiVersion: v1 kind: Service metadata: name: app-service spec: selector: app: myapp version: blue # 当前指向蓝环境 ports: - port: 80 targetPort: 8080 type: LoadBalancer切换的时候只需要修改Service的selector：kubectl patch service app-service -p '{"spec":{"selector":{"version":"green"}}}'这种方式的优势是与K8s生态结合紧密，可以利用K8s的健康检查、自动扩缩容等功能。开源工具推荐1. Argo Rollouts这是专门为K8s设计的渐进式交付工具，支持蓝绿部署、金丝雀发布等多种策略。安装很简单：kubectl create namespace argo-rollouts kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml配置文件示例：apiVersion: argoproj.io/v1alpha1 kind: Rollout metadata: name: rollout-bluegreen spec: replicas: 2 strategy: blueGreen: activeService: rollout-bluegreen-active previewService: rollout-bluegreen-preview autoPromotionEnabled: false scaleDownDelaySeconds: 30 selector: matchLabels: app: rollout-bluegreen template: metadata: labels: app: rollout-bluegreen spec: containers: - name: rollouts-demo image: argoproj/rollouts-demo:blue ports: - containerPort: 8080Argo Rollouts的界面很友好，可以直观地看到部署状态，手动控制切换时机。2. FlaggerFlagger是另一个优秀的渐进式交付工具，支持多种Service Mesh和Ingress控制器。helm repo add flagger https://flagger.app helm upgrade -i flagger flagger/flagger \ --namespace=istio-system \ --set crd.create=false \ --set meshProvider=istio \ --set metricsServer=http://prometheus:9090Canary配置：apiVersion: flagger.app/v1beta1 kind: Canary metadata: name: podinfo namespace: test spec: targetRef: apiVersion: apps/v1 kind: Deployment name: podinfo progressDeadlineSeconds: 60 service: port: 9898 targetPort: 9898 analysis: interval: 1m threshold: 5 maxWeight: 50 stepWeight: 10 metrics: - name: request-success-rate threshold: 99 interval: 1m3. Jenkins Blue Ocean如果你用Jenkins做CI/CD，Blue Ocean插件提供了很好的流水线可视化界面，配合Pipeline脚本可以实现自动化蓝绿部署。Pipeline脚本示例：pipeline { agent any environment { BLUE_PORT = "8080" GREEN_PORT = "8081" CURRENT_ENV = "" } stages { stage('检测当前环境') { steps { script { def response = sh( script: "curl -s http://localhost/health | jq -r '.env'", returnStdout: true ).trim() if (response == "blue") { env.CURRENT_ENV = "blue" env.TARGET_ENV = "green" env.TARGET_PORT = GREEN_PORT } else { env.CURRENT_ENV = "green" env.TARGET_ENV = "blue" env.TARGET_PORT = BLUE_PORT } } } } stage('部署到目标环境') { steps { sh """ docker stop app-${TARGET_ENV} || true docker rm app-${TARGET_ENV} || true docker run -d --name app-${TARGET_ENV} -p ${TARGET_PORT}:8080 myapp:${BUILD_NUMBER} """ } } stage('健康检查') { steps { script { def maxRetries = 30 def retries = 0 while (retries < maxRetries) { def response = sh( script: "curl -s -o /dev/null -w '%{http_code}' http://localhost:${TARGET_PORT}/health", returnStdout: true ).trim() if (response == "200") { echo "健康检查通过" break } retries++ sleep(10) } if (retries >= maxRetries) { error("健康检查失败") } } } } stage('切换流量') { steps { sh """ sed -i 's/server 192.168.1.10:${CURRENT_ENV == 'blue' ? BLUE_PORT : GREEN_PORT};/server 192.168.1.10:${TARGET_PORT};/' /etc/nginx/conf.d/app.conf nginx -s reload """ } } } post { failure { // 回滚逻辑 sh """ sed -i 's/server 192.168.1.10:${TARGET_PORT};/server 192.168.1.10:${CURRENT_ENV == 'blue' ? BLUE_PORT : GREEN_PORT};/' /etc/nginx/conf.d/app.conf nginx -s reload """ } } }4. GitLab CI/CDGitLab自带的CI/CD功能也很强大，可以通过.gitlab-ci.yml配置蓝绿部署流水线。stages: - build - deploy-staging - switch-traffic - cleanup variables: BLUE_HOST: "blue.example.com" GREEN_HOST: "green.example.com" build: stage: build script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA deploy-to-staging: stage: deploy-staging script: - | if [ "$(kubectl get service production -o jsonpath='{.spec.selector.version}')" = "blue" ]; then export TARGET_ENV="green" export TARGET_HOST=$GREEN_HOST else export TARGET_ENV="blue" export TARGET_HOST=$BLUE_HOST fi - kubectl set image deployment/app-$TARGET_ENV app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - kubectl rollout status deployment/app-$TARGET_ENV environment: name: staging url: http://$TARGET_HOST switch-production: stage: switch-traffic script: - | if [ "$(kubectl get service production -o jsonpath='{.spec.selector.version}')" = "blue" ]; then kubectl patch service production -p '{"spec":{"selector":{"version":"green"}}}' else kubectl patch service production -p '{"spec":{"selector":{"version":"blue"}}}' fi environment: name: production url: http://api.example.com when: manual cleanup-old-version: stage: cleanup script: - | if [ "$(kubectl get service production -o jsonpath='{.spec.selector.version}')" = "blue" ]; then kubectl scale deployment/app-green --replicas=0 else kubectl scale deployment/app-blue --replicas=0 fi when: manual生产环境踩坑记录我在实施蓝绿部署的过程中踩了不少坑，这里分享几个典型的：坑1：数据库迁移问题有一次我们发新版本，新版本的数据库schema有变化。结果切换到新环境后发现数据不一致，只能紧急回滚。后来我们的解决方案是：数据库迁移必须向前兼容先发布兼容新旧schema的代码版本再执行数据库迁移最后发布纯新版本代码坑2：静态资源缓存问题前端的js、css文件有缓存，切换环境后用户可能还在使用旧的静态资源，导致接口调用失败。我们的做法是给静态资源文件名加上版本号或者hash值。坑3：健康检查不充分之前只检查了HTTP状态码是200就认为服务正常，结果有一次数据库连接池耗尽，HTTP接口能访问但是业务逻辑有问题。现在我们的健康检查会验证关键业务接口。坑4：流量切换不够平滑最开始用的是简单的全量切换，有时候会有短暂的502错误。后来改成先切换一小部分流量观察，没问题再全量切换。监控和回滚策略蓝绿部署最重要的是要有完善的监控和快速回滚机制。我们用Prometheus + Grafana做监控，关键指标包括：响应时间错误率吞吐量资源使用率告警规则：groups: - name: bluegreen-deployment rules: - alert: HighErrorRate expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.01 for: 2m labels: severity: critical annotations: summary: "错误率过高" - alert: HighResponseTime expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1 for: 5m labels: severity: warning annotations: summary: "响应时间过长"回滚脚本我写得特别简单粗暴：#!/bin/bash # rollback.sh echo "开始回滚..." # 获取当前环境 CURRENT=$(kubectl get service production -o jsonpath='{.spec.selector.version}') if [ "$CURRENT" = "blue" ]; then kubectl patch service production -p '{"spec":{"selector":{"version":"green"}}}' echo "已回滚到绿环境" else kubectl patch service production -p '{"spec":{"selector":{"version":"blue"}}}' echo "已回滚到蓝环境" fi echo "回滚完成，请检查服务状态"关键是要快，发现问题后30秒内必须能完成回滚。成本考虑蓝绿部署最大的缺点就是资源消耗翻倍，对于小公司来说成本压力不小。我们的优化策略：非生产环境不用蓝绿部署，用滚动更新就行蓝绿环境可以共享一些无状态的服务，比如Redis、MQ用云厂商的竞价实例降低成本部署完成后可以把备用环境的副本数调小，保留最小可用配置另外，数据库这种有状态的服务不适合做蓝绿部署，成本太高。我们一般是应用层做蓝绿，数据层用主从切换。蓝绿部署确实是个好东西，特别是对于那些不能接受服务中断的业务。虽然成本高了点，但是换来的稳定性和用户体验提升是值得的。我们公司自从用了蓝绿部署后，发布相关的故障基本没有了，周五下午发版本也不再是噩梦。当然，这套方案不是银弹，还是要根据自己的业务场景选择合适的部署策略。如果你们公司也在考虑蓝绿部署，建议先在测试环境搭一套练练手，把流程跑通了再上生产。毕竟运维这行，稳字当头嘛。好了，今天的分享就到这里。如果这篇文章对你有帮助，记得点个赞转发一下，让更多的运维兄弟看到。有什么问题也欢迎在评论区讨论，我会尽量回复大家。关注@运维躬行录，我会持续分享更多生产环境的实战经验，咱们一起在运维这条路上越走越稳！公众号：运维躬行录个人博客：躬行笔记
从零开始搞懂DDoS和CC攻击：运维人员必备的防护攻略
作为一个在运维岗位摸爬滚打好几年的人，我想和大家聊聊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都一样，这种很容易识别。但现在的攻击者也越来越狡猾，会模拟各种真实的浏览器特征。行为分析更高级的清洗技术会分析用户行为。正常用户访问网站是有一定规律的，比如先访问首页，然后点击链接进入详情页，停留一段时间后再进行下一步操作。而攻击流量往往缺乏这种"人性化"的行为模式。比如一个IP在1秒钟内访问了100个不同的页面，这明显不是正常用户的行为。机器学习算法现在很多云厂商都在用AI技术来做流量清洗。通过分析历史数据，训练模型来识别攻击模式。这种方法的好处是能适应新的攻击手法，但也需要大量的数据和计算资源。阿里云的AI防护系统每天要分析TB级别的流量数据，不断优化识别算法。确实挺厉害的。各大云厂商的防护产品对比作为一个经常和各种云服务打交道的人，我对主流厂商的DDoS防护产品还是比较了解的。阿里云DDoS高防阿里云的DDoS高防应该是国内最成熟的产品之一了。我用过他们的高防IP和高防包，防护能力确实很强。高防IP适合那种可以更换IP的场景，比如游戏服务器。你把域名解析到高防IP上，所有流量先经过阿里云的清洗中心，干净的流量再转发到你的源服务器。高防包则适合不能更换IP的场景，直接对你现有的ECS进行防护。价格相对便宜一些，但防护能力也会差一点。阿里云的优势是技术比较成熟，清洗算法不断在优化。而且他们有全球的清洗中心，能就近清洗，延迟相对较低。腾讯云大禹防护腾讯云的DDoS防护产品叫大禹，分为基础防护、DDoS高防包和DDoS高防IP。我觉得腾讯云的一个优势是和他们的CDN结合得比较好。如果你同时用了腾讯云的CDN，可以在CDN层面就进行一些基础的防护，效果会更好。而且腾讯云在游戏行业有很深的积累，对游戏类的攻击防护经验比较丰富。如果你是做游戏的，腾讯云可能是个不错的选择。华为云Anti-DDoS华为云的Anti-DDoS产品我接触得相对少一些，但从技术架构上看还是很不错的。华为在网络设备方面的积累很深厚，这个优势在DDoS防护上也体现出来了。他们的清洗设备据说用的是华为自研的芯片，处理性能比较强。而且华为云在政企市场做得不错，如果你的业务对合规性要求比较高，华为云可能更合适。百度云DDoS防护百度云的DDoS防护产品在AI算法方面有一些优势，毕竟百度在AI领域积累比较深。他们的智能防护能够自动学习和适应新的攻击模式。AWS Shield防护AWS的DDoS防护产品主要是Shield，分为Shield Standard和Shield Advanced两个版本。Shield Standard是免费提供的基础防护，自动保护所有AWS资源免受常见的网络层和传输层DDoS攻击。这个服务默认就开启了，不需要额外配置，能防护SYN/UDP洪水、反射攻击等常见的Layer 3/4攻击。Shield Advanced则是付费的高级防护服务，每月费用大概3000美元起。它提供更强的防护能力，包括：更大规模的DDoS攻击防护实时攻击通知和详细报告DDoS Response Team (DRT) 24/7专家支持攻击期间的费用保护，避免因攻击导致的AWS账单激增与AWS WAF集成，提供应用层防护AWS的一个优势是和他们的全球CDN服务CloudFront结合得很好。流量先经过CloudFront的边缘节点进行初步过滤，然后再到达你的源服务器。而且AWS的基础设施本身就很强大，能够吸收大规模的攻击流量。不过AWS Shield的价格相对较高，特别是Shield Advanced，比较适合对成本不太敏感的大型企业。对于国内用户来说，还要考虑网络延迟的问题，毕竟AWS的主要节点还是在海外。实战防护配置说了这么多理论，我想分享一些具体的防护配置。Nginx层面的防护# 基础限流配置 http { limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s; limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; limit_conn_zone $binary_remote_addr zone=conn_limit:10m; # 针对不同接口的限制 location /login { limit_req zone=login burst=3 nodelay; limit_conn conn_limit 5; } location /api/ { limit_req zone=api burst=20 nodelay; limit_conn conn_limit 10; } }iptables防火墙规则# 限制单个IP的连接数 iptables -A INPUT -p tcp --dport 80 -m connlimit --connlimit-above 20 -j DROP # 限制SYN包的频率 iptables -A INPUT -p tcp --syn -m limit --limit 1/s --limit-burst 3 -j ACCEPT iptables -A INPUT -p tcp --syn -j DROP # 防护ICMP攻击 iptables -A INPUT -p icmp -m limit --limit 1/s -j ACCEPT iptables -A INPUT -p icmp -j DROP应用层防护代码在应用程序中也可以加入一些防护逻辑：import redis import time from flask import request, abort redis_client = redis.Redis(host='localhost', port=6379, db=0) def rate_limit(key, limit, window): """简单的滑动窗口限流""" current = time.time() pipe = redis_client.pipeline() pipe.zremrangebyscore(key, 0, current - window) pipe.zcard(key) pipe.zadd(key, {str(current): current}) pipe.expire(key, int(window) + 1) results = pipe.execute() if results[1] >= limit: return False return True @app.before_request def check_rate_limit(): client_ip = request.remote_addr if not rate_limit(f"rate_limit:{client_ip}", 100, 60): # 每分钟最多100个请求 abort(429) # Too Many Requests监控和告警体系完善的监控告警系统对于及时发现和处理攻击非常重要。关键监控指标我一般会监控这些指标：网络流量（入站/出站）并发连接数请求响应时间错误率（4xx/5xx状态码）服务器资源使用率（CPU、内存、磁盘IO）数据库连接数和查询响应时间告警规则设置# Prometheus告警规则示例 groups: - name: ddos_detection rules: - alert: HighTrafficVolume expr: rate(nginx_http_requests_total[5m]) > 1000 for: 2m labels: severity: warning annotations: summary: "High traffic detected" - alert: HighErrorRate expr: rate(nginx_http_requests_total{status=~"5.."}[5m]) / rate(nginx_http_requests_total[5m]) > 0.1 for: 1m labels: severity: critical annotations: summary: "High error rate detected"应急响应流程当攻击发生时，有一个清晰的应急响应流程很重要：第一阶段：快速识别查看监控告警，确认是否为攻击分析流量来源和攻击类型评估攻击规模和影响范围第二阶段：紧急处置启用高防IP或CDN防护在防火墙层面封禁明显的攻击IP调整限流策略，降低服务压力如果必要，临时下线部分非核心功能第三阶段：深度防护分析攻击特征，调整防护策略优化应用程序，提高抗攻击能力联系云厂商或安全厂商，获得专业支持准备备用方案，如备用域名、备用服务器等第四阶段：事后总结分析攻击过程和应对效果总结经验教训，完善防护体系更新应急预案和操作文档成本效益分析谈到防护，成本是个绕不开的话题。不同规模的业务需要不同的防护策略。小型网站（日PV < 10万）基础防护：使用CDN的基础DDoS防护成本：每月几百到一千元能防护10G以下的攻击中型网站（日PV 10万-100万）推荐方案：CDN + 高防包成本：每月几千到一万元能防护50G以下的攻击大型网站（日PV > 100万）推荐方案：高防IP + 多重防护成本：每月几万到十几万元能防护几百G的攻击未来发展趋势从技术发展趋势来看，我觉得有几个方向值得关注：AI智能防护机器学习在DDoS防护中的应用会越来越广泛。通过分析大量的攻击数据，AI能够识别出更复杂的攻击模式，提高识别准确率。边缘计算防护随着5G和边缘计算的发展，防护节点会更加分散，能够就近处理攻击流量，降低延迟，提高用户体验。云原生安全容器化和微服务架构的普及，推动防护技术向云原生方向发展。防护能力会更好地集成到应用架构中，实现更细粒度的防护。零信任安全模型传统的边界防护模式正在向零信任模型转变，每个请求都需要验证，这种模式对DDoS和CC攻击也有很好的防护效果。实用工具推荐在日常运维中，我经常用到这些工具：流量分析工具Wireshark：网络抓包分析ntopng：实时网络流量监控iftop：查看网络连接和流量压力测试工具Apache Bench (ab)：简单的HTTP压力测试wrk：现代化的HTTP基准测试工具JMeter：功能强大的压力测试工具监控工具Prometheus + Grafana：监控和可视化Zabbix：企业级监控解决方案Nagios：传统但稳定的监控工具法律和合规考虑在防护DDoS攻击时，也要注意法律合规问题：数据保护在收集和分析攻击数据时，要注意保护用户隐私，遵守相关的数据保护法规。证据保全如果攻击造成了重大损失，要及时保存相关证据，为后续的法律追责做准备。合规报告某些行业可能需要向监管部门报告重大安全事件，要了解相关的合规要求。写在最后DDoS和CC攻击虽然可怕，但只要我们了解其原理，采取合适的防护措施，还是能够有效应对的。关键是要有预防意识，不要等到被攻击了才想起来防护。防护不是一次性的工作，而是一个持续的过程。攻击手法在不断进化，我们的防护策略也要跟着调整。同时，要平衡安全性和成本，选择适合自己业务的防护方案。最重要的是，要建立完整的安全体系，包括技术防护、监控告警、应急响应等各个环节。只有这样，才能在面对攻击时从容应对。技术在进步，攻击手法也在进化，我们需要不断学习和适应。希望这篇文章能对大家有所帮助，在实际工作中如果遇到了相关问题，也欢迎交流讨论。防患于未然，永远比事后补救要好。在平时就做好防护准备，真正遇到攻击时才不会手忙脚乱。如果这篇文章对你有帮助，记得点赞转发哦！也欢迎在评论区分享你的经验和想法。公众号：运维躬行录个人博客：躬行笔记
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感兴趣，或者在使用过程中有什么问题，欢迎留言交流。咱们一起学习，一起进步！如果觉得这篇文章对你有帮助，记得点赞、在看、转发三连哦！你的支持是我继续分享的动力！公众号：运维躬行录个人博客：躬行笔记
Linux 内存泄漏排查全攻略：从现象到根因，一文讲透生产环境实战经验
做运维这么多年，内存泄漏这事儿真是没少碰。有些时候程序跑着跑着内存就蹭蹭往上涨，重启一下好了，过两天又不行了。这种问题最烦人，不像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>/cmdline：cat /proc/12345/cmdline | tr '\0' ' '找到嫌疑进程后，就要深入分析了。深入分析进程内存使用 /proc 文件系统Linux的/proc目录是个宝藏，每个进程的信息都在这里。/proc/<pid>/status里有进程的基本信息：cat /proc/12345/status Name: java Umask: 0022 State: S (sleeping) ... VmSize: 4325432 kB # 虚拟内存 VmRSS: 3521344 kB # 物理内存 VmData: 1234567 kB # 数据段 VmStk: 136 kB # 栈 VmExe: 4832 kB # 代码段 VmLib: 32456 kB # 共享库VmRSS就是进程实际占用的物理内存，这个值最关键。/proc/<pid>/maps记录了进程的内存映射：cat /proc/12345/maps 00400000-00483000 r-xp 00000000 fd:00 123456 /usr/lib/jvm/java-11/bin/java ... 7f3c40000000-7f3c40200000 rw-p 00000000 00:00 0 7f3c40200000-7f3c48000000 ---p 00000000 00:00 0能看到进程加载了哪些库，有哪些内存区域。如果发现有很多匿名映射（没有对应文件），可能是堆外内存或者内存映射文件。更详细的信息在/proc/<pid>/smaps：cat /proc/12345/smaps | head -30 7f3c40000000-7f3c40200000 rw-p 00000000 00:00 0 Size: 2048 kB Rss: 1024 kB Pss: 1024 kB Shared_Clean: 0 kB Shared_Dirty: 0 kB Private_Clean: 0 kB Private_Dirty: 1024 kB Referenced: 1024 kB Anonymous: 1024 kB AnonHugePages: 0 kB ShmemHugePages: 0 kB ShmemPmdMapped: 0 kB Swap: 0 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Locked: 0 kBRss是实际占用的物理内存，Pss是按比例分摊的共享内存。Anonymous是匿名内存，通常是堆和栈。Private_Dirty是私有脏页，被进程修改过还没写入磁盘的内存。可以写个脚本统计各类内存的总量：cat /proc/12345/smaps | grep -E "^Size|^Rss|^Pss" | awk '{arr[$1]+=$2} END {for(k in arr) print k, arr[k]" kB"}'使用 pmap 命令pmap其实就是把/proc下的信息整理了一下，看起来更方便：pmap -x 12345 Address Kbytes RSS Dirty Mode Mapping 0000000000400000 4832 4832 0 r-x-- java 00000000010b3000 68 68 68 rw--- java ... 00007f3c40000000 65536 65532 65532 rw--- [ anon ] ... total kB 4325432 3521344 3521344-x参数显示扩展信息。Address是内存起始地址，Kbytes是区域大小，RSS是实际物理内存，Dirty是脏页。最后一行是汇总。如果发现某个内存区域特别大，或者[ anon ]类型的区域特别多，就要注意了。[ anon ]通常是堆内存或者堆外内存。pmap -X能显示更详细的信息，包括内存区域的保护属性、设备号等。使用 strace 追踪系统调用如果想知道进程在做什么内存操作，可以用strace：strace -p 12345 -e trace=mmap,munmap,brk这会追踪进程的内存分配系统调用。mmap是映射内存，munmap是解除映射，brk是调整堆大小。输出类似这样：mmap(NULL, 1048576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3c41000000 brk(0x56789000) = 0x56789000 munmap(0x7f3c41000000, 1048576) = 0如果看到大量的mmap但没有对应的munmap，那很可能就是泄漏了。strace的缺点是会影响性能，生产环境慎用。而且输出量大，分析起来比较费劲。内存泄漏检测工具ValgrindValgrind是排查内存问题的神器，不仅能检测泄漏，还能检测非法访问、未初始化变量等问题。用法很简单：valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./your_program程序退出后会输出详细报告：==12345== HEAP SUMMARY: ==12345== in use at exit: 1,024 bytes in 1 blocks ==12345== total heap usage: 100 allocs, 99 frees, 10,240 bytes allocated ==12345== ==12345== 1,024 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==12345== at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==12345== by 0x400544: main (test.c:10) ==12345== ==12345== LEAK SUMMARY: ==12345== definitely lost: 1,024 bytes in 1 blocks ==12345== indirectly lost: 0 bytes in 0 blocks ==12345== possibly lost: 0 bytes in 0 blocks ==12345== still reachable: 0 bytes in 0 blocks ==12345== suppressed: 0 bytes in 0 blocksdefinitely lost就是确定泄漏的内存，possibly lost是可能泄漏，still reachable是程序结束时还没释放但可能是有意为之。Valgrind的原理是在程序和CPU之间加了一层虚拟化，拦截所有内存操作。这导致程序运行速度会变慢10-100倍，所以生产环境基本没法用，只能在测试环境跑。对于长期运行的服务，可以让它运行一段时间后正常退出，Valgrind会输出报告。或者用kill -SIGTERM让它优雅退出。AddressSanitizer (ASan)ASan是编译器级别的内存检测工具，GCC和Clang都支持。编译时加上参数：gcc -fsanitize=address -g -O1 test.c -o test运行时如果检测到内存问题，会直接报错：================================================================= ==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x60700000eff8 READ of size 4 at 0x60700000eff8 thread T0 #0 0x400544 in main test.c:15 #1 0x7f3c4a0e082f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f) ...ASan比Valgrind快很多，性能损耗大概只有2倍左右，可以在生产环境开启（如果性能要求不是特别高的话）。ASan还能检测很多Valgrind检测不到的问题，比如栈缓冲区溢出、全局缓冲区溢出等。GDB 调试GDB不仅能调试崩溃，排查内存问题也很好用。先attach到进程：gdb -p 12345查看内存映射：(gdb) info proc mappings Start End Size Offset Name 0x400000 0x408000 0x8000 0x0 /path/to/binary ...查看堆信息：(gdb) call malloc_info(0, stdout)这会输出malloc的状态信息（如果glibc版本支持的话）。还可以在malloc和free上设置断点：(gdb) break malloc Breakpoint 1 at 0x7f3c4a123456 (gdb) commands Type commands for breakpoint(s) 1, one per line. End with a line saying just "end". >backtrace >continue >end这样每次malloc都会打印调用栈。但这会让程序变得很慢，因为malloc调用非常频繁。更好的方法是条件断点，比如只追踪大于1MB的分配：(gdb) break malloc if $rdi > 1048576$rdi是第一个参数（x86_64架构），也就是分配的大小。glibc 内存分配调试glibc提供了一些环境变量来调试内存分配：MALLOC_TRACE=/tmp/malloc.log ./your_program这会记录所有的内存分配和释放操作。然后用mtrace命令分析：mtrace your_program /tmp/malloc.log输出类似：Memory not freed: ----------------- Address Size Caller 0x0000000000602010 0x400 at 0x400544: main (test.c:10)还有MALLOC_CHECK_环境变量：MALLOC_CHECK_=3 ./your_program值可以是0-3：0：不输出错误1：输出错误到stderr2：立即abort3：输出错误并abort这对检测double free、堆溢出等问题很有用。custom malloc hooks如果想要更精细的控制，可以自己实现malloc钩子：#include <malloc.h> static void *(*old_malloc)(size_t) = NULL; static void *my_malloc(size_t size, const void *caller) { void *ptr = old_malloc(size); fprintf(stderr, "malloc(%zu) = %p from %p\n", size, ptr, caller); return ptr; } void __attribute__((constructor)) init_hook() { old_malloc = dlsym(RTLD_NEXT, "malloc"); __malloc_hook = my_malloc; }编译成动态库，用LD_PRELOAD加载：gcc -shared -fPIC -ldl hook.c -o hook.so LD_PRELOAD=./hook.so ./your_program这样就能追踪所有的malloc调用了。可以记录调用栈、分配大小等信息，后期分析。Java 程序内存泄漏排查Java程序有垃圾回收，理论上不应该泄漏。但实际上还是会有问题。区分堆内和堆外内存先用jstat看堆内存：jstat -gc 12345 1000 S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 10240 10240 0.0 8192.0 81920.0 65536.0 204800.0 102400.0 51200 48000 6144 5800 1234 12.345 12 3.456 15.801如果老年代（OC/OU）使用率不高，Full GC（FGC）次数也不多，那堆内存应该没问题。用jmapdump堆内存分析：jmap -histo:live 12345 | head -20这会输出对象统计，看哪种对象数量多、占用大。如果堆内存没问题，但进程RSS还是很高，那可能是堆外内存。Java的堆外内存包括：DirectBuffer：NIO用的直接内存Metaspace：类元数据线程栈：每个线程1MB左右native内存：JNI调用分配的Native Memory Tracking (NMT)NMT是JDK自带的工具，能追踪JVM内部内存使用。启动时加参数：-XX:NativeMemoryTracking=summary或者-XX:NativeMemoryTracking=detail更详细。运行时用jcmd查看：jcmd 12345 VM.native_memory summary输出：Native Memory Tracking: Total: reserved=2048MB, committed=1024MB - Java Heap (reserved=512MB, committed=512MB) - Class (reserved=256MB, committed=128MB) - Thread (reserved=256MB, committed=256MB) - Code (reserved=128MB, committed=64MB) - GC (reserved=128MB, committed=64MB) - Internal (reserved=256MB, committed=128MB)能清楚看到各类内存的占用情况。如果Thread那项很大，可能是线程太多；如果Internal很大，可能是DirectBuffer。DirectBuffer 泄漏DirectBuffer是堆外内存，不受GC管理。如果用完不释放，会一直占用。用JMX可以查看DirectBuffer使用情况：import java.lang.management.ManagementFactory; import java.lang.reflect.Method; public class DirectBufferInfo { public static void main(String[] args) throws Exception { Class<?> clz = Class.forName("sun.misc.SharedSecrets"); Method method = clz.getDeclaredMethod("getJavaNioAccess"); method.setAccessible(true); Object nioAccess = method.invoke(null); Method method2 = nioAccess.getClass().getDeclaredMethod("getDirectBufferPool"); method2.setAccessible(true); Object pool = method2.invoke(nioAccess); Method getCount = pool.getClass().getDeclaredMethod("getCount"); Method getTotal = pool.getClass().getDeclaredMethod("getTotalCapacity"); Method getUsed = pool.getClass().getDeclaredMethod("getMemoryUsed"); System.out.println("Count: " + getCount.invoke(pool)); System.out.println("Total: " + getTotal.invoke(pool)); System.out.println("Used: " + getUsed.invoke(pool)); } }Netty框架有内置的泄漏检测，启动参数：-Dio.netty.leakDetection.level=PARANOID会在日志里输出泄漏报告，包括创建ByteBuf的调用栈。常见内存泄漏场景排查了这么多案例，总结下来常见的泄漏场景就那么几类。缓存没有清理往静态Map里加数据，用完不删：public class Cache { private static Map<String, Object> map = new HashMap<>(); public static void put(String key, Object value) { map.put(key, value); } }这种最坑，因为静态变量生命周期和程序一样长，里面的对象永远不会被回收。解决办法是用WeakHashMap，或者定期清理。监听器没有注销注册了监听器，忘了注销：button.addActionListener(listener); // 窗口关闭时忘了 removeActionListener被监听的对象持有监听器的引用，监听器无法回收。线程没有停止创建了线程，没有停止机制：new Thread(() -> { while (true) { // do something } }).start();线程一直在运行，线程对象和它引用的对象都无法回收。连接没有关闭数据库连接、网络连接用完没关：Connection conn = dataSource.getConnection(); // 忘了 conn.close()连接池里的连接会被耗尽，导致后续请求失败。堆外内存泄漏使用DirectBuffer或者JNI调用native代码，分配的内存不受GC管理，需要手动释放。预防措施与其事后排查，不如事前预防。代码层面使用try-with-resources，确保资源释放缓存设置上限和过期时间使用WeakReference、SoftReference处理可选缓存线程池不要无限制创建线程测试层面压测时监控内存，长时间运行观察趋势使用内存分析工具做静态检查单元测试覆盖资源释放逻辑运维层面设置内存告警阈值定期重启服务（治标不治本，但能争取时间）使用cgroups限制进程内存上限排查思路总结最后总结一下排查思路：确认是内存泄漏还是正常波动：看趋势，泄漏是持续上涨不会下降定位问题进程：用top、ps找出内存占用高的进程分析进程内存分布：用pmap、/proc/smaps看是哪类内存区分堆内堆外：Java程序先用jstat看堆内存选择合适工具：开发环境用valgrind/ASan，生产环境用gdb/pmap分析代码：根据工具报告定位问题代码排查内存泄漏确实需要耐心，有时候要反复测试才能定位。但只要方法对路，总能找到问题根源。公众号：运维躬行录个人博客：躬行笔记如果觉得文章有用，帮忙点个赞、转发一下，让更多人看到。你的支持是我持续输出的动力！有问题也可以在公众号后台留言，看到会回复的。
做了这么多年运维，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支持三种访问模式：ReadWriteOnce (RWO)只能被一个节点以读写方式挂载。这是最常用的模式，适合大部分应用。ReadOnlyMany (ROX)可以被多个节点以只读方式挂载。适合配置文件、静态资源等场景。ReadWriteMany (RWX)可以被多个节点以读写方式挂载。这个模式对存储系统要求比较高，不是所有存储都支持。我遇到过一个坑，就是用了不支持RWX的存储系统，结果Pod一直处于Pending状态。后来查了半天才发现是访问模式的问题。实际生产案例案例1：MySQL数据库持久化# StorageClass apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: mysql-storage provisioner: kubernetes.io/aws-ebs parameters: type: gp2 encrypted: "true" --- # PVC apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mysql-pvc spec: accessModes: - ReadWriteOnce storageClassName: mysql-storage resources: requests: storage: 100Gi --- # Deployment apiVersion: apps/v1 kind: Deployment metadata: name: mysql spec: replicas: 1 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:8.0 env: - name: MYSQL_ROOT_PASSWORD value: "password123" volumeMounts: - name: mysql-storage mountPath: /var/lib/mysql volumes: - name: mysql-storage persistentVolumeClaim: claimName: mysql-pvc这个配置我用了好几年，非常稳定。关键点是：使用加密的EBS卷保证数据安全100Gi的存储空间足够大部分应用使用挂载到MySQL的数据目录案例2：多Pod共享存储有时候需要多个Pod共享同一份数据，比如Web应用的静态文件。# 使用NFS的PV apiVersion: v1 kind: PersistentVolume metadata: name: shared-pv spec: capacity: storage: 50Gi accessModes: - ReadWriteMany nfs: server: nfs-server.example.com path: /shared/data --- # PVC apiVersion: v1 kind: PersistentVolumeClaim metadata: name: shared-pvc spec: accessModes: - ReadWriteMany resources: requests: storage: 50Gi这种场景下必须使用支持RWX的存储系统，NFS是个不错的选择。性能优化技巧选择合适的存储类型不同的存储类型性能差异很大：SSD：IOPS高，延迟低，适合数据库HDD：容量大，成本低，适合日志、备份网络存储：可共享，但性能一般我的经验是，数据库一定要用SSD，日志可以用HDD，配置文件用网络存储。合理设置存储大小存储大小不是越大越好，要根据实际需求来：预留20-30%的空间用于扩展考虑备份和快照的空间需求定期清理无用数据监控存储使用情况我一般会用Prometheus监控存储使用率：# 存储使用率告警规则 groups: - name: storage.rules rules: - alert: PVCStorageUsageHigh expr: (kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes) * 100 > 85 for: 5m labels: severity: warning annotations: summary: "PVC storage usage is high"常见问题排查PVC一直Pending这是最常见的问题，可能的原因：没有合适的PV可以绑定StorageClass配置错误存储资源不足排查方法：kubectl describe pvc my-pvc kubectl get pv kubectl get storageclassPod挂载失败挂载失败通常是因为：节点不支持该存储类型权限问题存储系统故障我遇到过一次EBS卷挂载失败，最后发现是AWS的配额限制导致的。数据丢失这个问题最严重，预防措施：使用Retain回收策略定期备份重要数据测试恢复流程最佳实践总结根据我这些年的经验，总结几个最佳实践：生产环境优先使用动态供应，配置合适的StorageClass重要数据使用Retain回收策略，手动管理PV生命周期合理选择访问模式，大部分情况用RWO就够了监控存储使用情况，及时发现问题定期备份数据，做好灾难恢复准备存储选型建议数据库：高性能SSD + RWO + Retain策略文件服务：网络存储 + RWX + Delete策略日志收集：大容量HDD + RWO + Delete策略临时数据：本地存储 + RWO + Delete策略如果觉得这篇文章对你有帮助，别忘了点个赞、转发一下，让更多的运维小伙伴看到。你们的支持是我持续输出干货的动力！关注@运维躬行录，获取更多实战干货，我们一起在运维的道路上躬行实践！公众号：运维躬行录个人博客：躬行笔记
Docker Swarm 搞定高可用集群，生产环境再也不怕服务挂掉了
Docker Swarm是什么？Docker Swarm 是 Docker 官方推出的容器集群管理工具，基于 Go 语言实现。代码开源在：https://github.com/docker/swarm 使用它可以将多个 Docker 主机封装为单个大型的虚拟 Docker 主机，快速打造一套容器云平台。Docker Swarm 是生产环境中运行 Docker 应用程序最简单的方法。作为容器集群管理器，Swarm 最大的优势之一就是 100% 支持标准的 Docker API。各种基于标准 API 的工具比如 Compose、docker-py、各种管理软件，甚至 Docker 本身等都可以很容易的与 Swarm 进行集成。大大方便了用户将原先基于单节点的系统移植到 Swarm 上，同时 Swarm 内置了对 Docker 网络插件的支持，用户可以很容易地部署跨主机的容器集群服务。Docker Swarm 和 Docker Compose 一样，都是 Docker 官方容器编排工具，但不同的是，Docker Compose 是一个在单个服务器或主机上创建多个容器的工具，而 Docker Swarm 则可以在多个服务器或主机上创建容器集群服务，对于微服务的部署，显然 Docker Swarm 会更加适合。为什么选 Docker Swarm 而不是 K8s说实话，Kubernetes 确实强大，但对我们这种中小团队来说，K8s 的学习成本和维护成本都太高了。光是搭建一套高可用的 K8s 集群就得折腾好几天，更别说日常维护了。Docker Swarm 就不一样了，它是 Docker 原生自带的编排工具，学过 Docker 的基本上一个小时就能上手。而且功能足够强大，服务发现、负载均衡、滚动更新、健康检查这些该有的都有，完全能满足大部分生产场景的需求。客户现在的业务规模大概有 30 多个微服务，每天访问量在百万级别，用 Docker Swarm 完全够用。如果你的规模跟我差不多，真心建议别一上来就搞 K8s，Swarm 真的够用了。环境准备5 台服务器，配置如下：3 台 Manager 节点：16G 内存，4 核 CPU2 台 Worker 节点：8G 内存，2 核 CPU为什么 Manager 要 3 台？这个后面会详细说。系统用的是 CentOS 7.9，Docker 版本是 20.10.14。在开始之前，有几件事得先做好：1. 主机名和 hosts 配置每台机器都要设置不同的主机名，不然等会儿集群建起来都叫 localhost，看着就头疼。# 在 manager1 上执行 hostnamectl set-hostname manager1 # 其他机器类似，改成 manager2、manager3、worker1、worker2然后修改 /etc/hosts 文件，把所有节点的 IP 和主机名都写进去：192.168.1.101 manager1 192.168.1.102 manager2 192.168.1.103 manager3 192.168.1.104 worker1 192.168.1.105 worker22. 关闭防火墙或者开放端口Docker Swarm 需要几个端口：2377/tcp：集群管理通信7946/tcp+udp：节点间通信4789/udp：覆盖网络流量生产环境建议还是开防火墙，只开放这些端口。测试环境图省事直接关了也行：systemctl stop firewalld systemctl disable firewalld3. 时间同步这个特别重要！如果节点时间不同步，集群选举会出问题，服务调度也会乱套。我们用 chrony 做时间同步：yum install -y chrony systemctl start chronyd systemctl enable chronyd初始化 Swarm 集群准备工作做完了，现在开始正式搭建集群。在 manager1 上执行初始化命令：docker swarm init --advertise-addr 192.168.1.101执行完会输出一串命令，大概是这样的：Swarm initialized: current node (dxn1zf6l61qsb1josjja83ngz) is now a manager. To add a worker to this swarm, run the following command: docker swarm join --token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv-8vxv8rssm7h7g6w7br5d7vwlh 192.168.1.101:2377 To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.这个 token 特别重要，一定要保存好，后面添加节点都要用它。现在集群已经有一个 Manager 节点了，查看一下集群状态：docker node ls输出类似这样：ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION dxn1zf6l61qsb1josjja83ngz * manager1 Ready Active Leader 20.10.14能看到 manager1 是 Leader 状态。添加其他 Manager 节点前面说了要用 3 个 Manager 节点，为什么要奇数个？这个跟 Raft 共识算法有关。Docker Swarm 用 Raft 算法来管理集群状态，需要半数以上的 Manager 节点在线才能正常工作。如果你有 2 个 Manager，挂掉 1 个，剩下 1 个不满足"半数以上"的条件，集群就不可用了。但如果是 3 个 Manager，挂掉 1 个，剩下 2 个还能正常工作。所以奇数个 Manager 是最佳实践，一般 3 个就够用了，能容忍 1 个节点故障。如果想要更高的容错能力，可以用 5 个，能容忍 2 个节点故障。[2]在 manager1 上获取 Manager 加入令牌：docker swarm join-token manager会输出一个命令，复制到 manager2 和 manager3 上执行：docker swarm join --token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv-8vxv8rssm7h7g6w7br5d7vwlh 192.168.1.101:2377执行完后再看集群状态：docker node lsID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION dxn1zf6l61qsb1josjja83ngz * manager1 Ready Active Leader 20.10.14 rfm5vi4vpxpmhjw6vz0t4wqdv manager2 Ready Active Reachable 20.10.14 y6jcg5z4m7x0tz1f0qz3n5k8s manager3 Ready Active Reachable 20.10.14看到 3 个 Manager 都在线了，manager1 是 Leader，其他两个是 Reachable 状态。添加 Worker 节点Worker 节点只负责运行容器，不参与集群管理。在 manager1 上获取 Worker 加入令牌：docker swarm join-token worker复制输出的命令到 worker1 和 worker2 上执行：docker swarm join --token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv-8vxv8rssm7h7g6w7br5d7vwlh 192.168.1.101:2377再看集群状态：docker node lsID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION dxn1zf6l61qsb1josjja83ngz * manager1 Ready Active Leader 20.10.14 rfm5vi4vpxpmhjw6vz0t4wqdv manager2 Ready Active Reachable 20.10.14 y6jcg5z4m7x0tz1f0qz3n5k8s manager3 Ready Active Reachable 20.10.14 n8xkp2z3m5q1rt7v9w2j4h6c0 worker1 Ready Active 20.10.14 t3h7m9k2p8q5wv1n4x6j0r2b8 worker2 Ready Active 20.10.145 个节点的集群就搭建完成了。创建覆盖网络集群搭好了，但服务之间怎么通信呢？这就需要创建覆盖网络（Overlay Network）。覆盖网络允许不同节点上的容器互相通信，就像在同一个局域网里一样。docker network create --driver overlay --attachable my-overlay--attachable 参数很重要，它允许独立容器连接到这个网络。如果不加这个参数，只有 Swarm 服务才能用这个网络。查看网络：docker network lsNETWORK ID NAME DRIVER SCOPE n33wq0p5m6q7 ingress overlay swarm r2t5v8x1p4m9 my-overlay overlay swarm部署一个测试服务光搭集群没意思，咱们部署一个实际的服务试试。我这里用 Nginx 做演示，部署 3 个副本：docker service create \ --name nginx-test \ --replicas 3 \ --network my-overlay \ --publish 80:80 \ nginx:latest参数解释一下：--name：服务名称--replicas：副本数量，这里启动 3 个容器--network：使用的网络--publish：端口映射，把容器的 80 端口映射到宿主机的 80 端口查看服务状态：docker service lsID NAME MODE REPLICAS IMAGE PORTS k8z2m9p4n7q1 nginx-test replicated 3/3 nginx:latest *:80->80/tcpREPLICAS 显示 3/3，说明 3 个副本都正常运行。查看服务详情：docker service ps nginx-testID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS t3h7m9k2p8q5 nginx-test.1 nginx:latest worker1 Running Running 2 minutes ago r2t5v8x1p4m9 nginx-test.2 nginx:latest manager2 Running Running 2 minutes ago n8xkp2z3m5q1 nginx-test.3 nginx:latest worker2 Running Running 2 minutes ago 能看到 3 个容器分别运行在 worker1、manager2 和 worker2 上。Swarm 会自动把容器调度到不同的节点，保证负载均衡。现在随便访问哪个节点的 80 端口，都能看到 Nginx 的欢迎页面。这就是 Swarm 的负载均衡功能，外部请求会自动分发到各个容器。服务高可用测试说了半天高可用，到底高可用在哪里？我们来模拟一下节点故障。直接把 worker1 关机：# 在 worker1 上执行 shutdown -h now然后等个几十秒，再查看服务状态：docker service ps nginx-testID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS t3h7m9k2p8q5 nginx-test.1 nginx:latest worker1 Shutdown Shutdown 30 seconds ago k7v2m9p4n7q1 nginx-test.1 nginx:latest manager3 Running Running 10 seconds ago r2t5v8x1p4m9 nginx-test.2 nginx:latest manager2 Running Running 5 minutes ago n8xkp2z3m5q1 nginx-test.3 nginx:latest worker2 Running Running 5 minutes ago看到了吗？原来在 worker1 上运行的容器自动关闭了，Swarm 在 manager3 上重新启动了一个新的容器。整个过程完全自动，不需要人工干预。这就是高可用的核心：当某个节点挂掉，上面的服务会自动迁移到其他健康节点上运行。再测试一下 Manager 节点故障。我们把 manager1（Leader）关机：# 在 manager1 上执行 shutdown -h now等一会儿，在 manager2 或 manager3 上查看集群状态：docker node lsID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION dxn1zf6l61qsb1josjja83ngz manager1 Down Active Unreachable 20.10.14 rfm5vi4vpxpmhjw6vz0t4wqdv * manager2 Ready Active Leader 20.10.14 y6jcg5z4m7x0tz1f0qz3n5k8s manager3 Ready Active Reachable 20.10.14 n8xkp2z3m5q1rt7v9w2j4h6c0 worker1 Ready Active 20.10.14 t3h7m9k2p8q5wv1n4x6j0r2b8 worker2 Ready Active 20.10.14manager1 显示 Down 和 Unreachable，而 manager2 自动变成了 Leader。这就是 Raft 协议的功劳，剩下的 2 个 Manager 节点重新选举，manager2 成了新的 Leader。集群依然正常工作，服务不受影响。部署真实业务服务测试服务跑通了，现在部署一个真实的业务。我们有个用户中心服务，用 Spring Boot 写的，依赖 MySQL 和 Redis。1. 创建 MySQL 服务docker service create \ --name mysql \ --replicas 1 \ --network my-overlay \ --mount type=volume,source=mysql-data,target=/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=your_password \ -e MYSQL_DATABASE=user_center \ --constraint 'node.role==worker' \ mysql:5.7这里用了 --constraint 'node.role==worker'，强制 MySQL 只运行在 Worker 节点上。因为 Manager 节点已经够忙了，数据库这种重服务还是放 Worker 上比较好。--mount 参数挂载了数据卷，保证数据持久化。如果不挂载卷，容器重建后数据就丢了。2. 创建 Redis 服务docker service create \ --name redis \ --replicas 1 \ --network my-overlay \ --mount type=volume,source=redis-data,target=/data \ --constraint 'node.role==worker' \ redis:6.23. 创建用户中心服务docker service create \ --name user-center \ --replicas 2 \ --network my-overlay \ -e SPRING_PROFILES_ACTIVE=prod \ -e MYSQL_HOST=mysql \ -e REDIS_HOST=redis \ --publish 8080:8080 \ your-registry.com/user-center:latest注意这里的环境变量 MYSQL_HOST=mysql 和 REDIS_HOST=redis，直接用服务名就能访问，这就是服务发现的好处。Swarm 内置了 DNS 服务器，会自动把服务名解析到对应的容器 IP。服务部署完了，看看整体状态：docker service lsID NAME MODE REPLICAS IMAGE PORTS k7v2m9p4n7q1 mysql replicated 1/1 mysql:5.7 r2t5v8x1p4m9 redis replicated 1/1 redis:6.2 t3h7m9k2p8q5 user-center replicated 2/2 your-registry.com/user-center:latest *:8080->8080/tcp所有服务都正常运行。使用 Docker Compose 文件部署上面的命令行方式适合单个服务，如果服务多了，管理起来很麻烦。Docker Swarm 支持用 Docker Compose 文件部署，方便多了。创建一个 docker-compose.yml 文件：version: '3.8' services: mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: your_password MYSQL_DATABASE: user_center volumes: - mysql-data:/var/lib/mysql networks: - my-overlay deploy: replicas: 1 placement: constraints: - node.role == worker redis: image: redis:6.2 volumes: - redis-data:/data networks: - my-overlay deploy: replicas: 1 placement: constraints: - node.role == worker user-center: image: your-registry.com/user-center:latest environment: SPRING_PROFILES_ACTIVE: prod MYSQL_HOST: mysql REDIS_HOST: redis ports: - "8080:8080" networks: - my-overlay deploy: replicas: 2 update_config: parallelism: 1 delay: 10s failure_action: rollback restart_policy: condition: on-failure delay: 5s max_attempts: 3 networks: my-overlay: external: true volumes: mysql-data: redis-data:这个文件里加了几个重要的配置：deploy.update_config：滚动更新配置，一次更新 1 个容器，间隔 10 秒，失败自动回滚deploy.restart_policy：重启策略，失败后重启，最多重试 3 次部署服务：docker stack deploy -c docker-compose.yml myapp查看 stack：docker stack lsNAME SERVICES myapp 3查看 stack 里的服务：docker stack services myapp用 stack 的好处是一次性部署多个服务，而且更新的时候也很方便，改完配置文件重新执行 deploy 命令就行。滚动更新实战业务迭代是常事，更新服务是家常便饭。传统方式更新服务，得先停掉旧版本，再启动新版本，中间会有服务中断。Docker Swarm 的滚动更新就优雅多了，它会逐步用新版本替换旧版本，保证服务始终可用。假设我们要把 user-center 服务从 v1.0 更新到 v2.0：docker service update \ --image your-registry.com/user-center:v2.0 \ --update-parallelism 1 \ --update-delay 10s \ --update-failure-action rollback \ myapp_user-center参数说明：--update-parallelism 1：一次更新 1 个容器--update-delay 10s：更新间隔 10 秒--update-failure-action rollback：更新失败自动回滚更新过程中查看状态：docker service ps myapp_user-centerID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS k7v2m9p4n7q1 myapp_user-center.1 your-registry.com/user-center:v2.0 worker1 Running Running 5 seconds ago t3h7m9k2p8q5 \_ myapp_user-center.1 your-registry.com/user-center:v1.0 worker1 Shutdown Shutdown 6 seconds ago r2t5v8x1p4m9 myapp_user-center.2 your-registry.com/user-center:v1.0 worker2 Running Running 2 minutes ago 能看到 worker1 上的容器已经更新到 v2.0，worker2 上的还是 v1.0。等 10 秒后，worker2 上的也会更新。如果新版本有问题，比如容器启动失败，Swarm 会自动停止更新并回滚到旧版本。这个功能太实用了，再也不用担心更新把服务搞挂了。服务扩缩容业务高峰期，服务扛不住了，需要快速扩容。Docker Swarm 扩容只需要一条命令：docker service scale myapp_user-center=5瞬间就把 user-center 服务扩展到 5 个副本。Swarm 会自动把新容器调度到空闲的节点上。查看扩容结果：docker service lsID NAME MODE REPLICAS IMAGE PORTS t3h7m9k2p8q5 myapp_user-center replicated 5/5 your-registry.com/user-center:v2.0 *:8080->8080/tcp缩容也一样简单：docker service scale myapp_user-center=2Swarm 会自动选择要停止的容器，保证负载均衡。私有镜像仓库配置生产环境的镜像一般都存在私有仓库里，Docker Swarm 拉取私有镜像需要认证。假设我们用 Harbor 搭建了私有仓库，地址是 registry.example.com。1. 创建 Docker 登录认证在任意 Manager 节点登录私有仓库：docker login registry.example.com输入用户名和密码，登录成功后，认证信息会保存在 ~/.docker/config.json 文件里。2. 创建 Docker Secret把认证信息创建成 Swarm Secret：docker secret create registry-auth ~/.docker/config.json3. 创建服务时使用 Secretdocker service create \ --name user-center \ --replicas 2 \ --network my-overlay \ --secret source=registry-auth,target=/root/.docker/config.json \ --publish 8080:8080 \ registry.example.com/user-center:latest这样服务就能从私有仓库拉取镜像了。监控和日志集群跑起来了，监控不能少。我们用 Portainer 来管理 Swarm 集群，用 ELK 收集日志。部署 PortainerPortainer 是个轻量级的容器管理界面，支持 Docker Swarm。docker service create \ --name portainer \ --replicas 1 \ --publish 9000:9000 \ --mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \ --mount type=volume,src=portainer-data,dst=/data \ portainer/portainer-ce:latest访问任意节点的 9000 端口，就能看到 Portainer 的管理界面。可以查看所有服务、容器、网络、卷，还能在 Web 界面直接操作，非常方便。[5]日志收集容器的日志默认存在本地，不方便查看和分析。我们用 ELK（Elasticsearch + Logstash + Kibana）来收集日志。不过 ELK 部署比较复杂，这里就不展开了。简单点的方案是用 Filebeat 收集日志，发送到 Elasticsearch，然后用 Kibana 查看。踩过的坑搭建过程中遇到不少坑，记录下来给大家避雷。坑 1：Manager 节点不要跑太多服务刚开始我把所有服务都往 Manager 节点上放，结果 Manager 负载太高，集群管理都受影响。后来加了 --constraint 'node.role==worker'，把重服务都限制在 Worker 节点上，问题解决。坑 2：数据卷挂载要小心MySQL 这种有状态的服务，一定要挂载数据卷，不然容器重建后数据就丢了。而且挂载的时候要用 type=volume，不要用 type=bind，因为 bind 挂载依赖宿主机的目录结构，迁移起来很麻烦。坑 3：端口冲突多个服务不能用同一个宿主机端口。我有次部署两个服务都用 8080 端口，结果第二个服务启动失败。解决办法是每个服务用不同的端口，或者用 Ingress 路由。[4]坑 4：节点时间不同步前面说过，节点时间不同步会导致集群选举出问题。我有次重启服务器后，时间同步服务没起来，结果集群状态一直不正常。后来加了监控，定期检查时间同步状态。坑 5：网络问题Docker Swarm 用覆盖网络实现跨节点通信，如果网络配置有问题，容器之间会无法通信。有次我忘了开放 4789/UDP 端口，结果容器之间 ping 不通。排查了半天才发现是防火墙的问题。生产环境的一些建议在测试环境跑通了，上生产环境还得注意几点：1. 资源限制给每个服务设置资源限制，防止某个服务占用太多资源影响其他服务：docker service update \ --limit-cpu 2 \ --limit-memory 4G \ --reserve-cpu 1 \ --reserve-memory 2G \ myapp_user-center2. 健康检查给服务加上健康检查，Swarm 会定期检查容器状态，不健康就自动重启：healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s3. 备份策略定期备份 Manager 节点的 /var/lib/docker/swarm 目录，里面存着集群的状态数据。如果所有 Manager 都挂了，可以用备份恢复集群。4. 监控告警部署监控（Prometheus + Grafana），设置告警规则。集群状态、服务状态、资源使用情况都要监控。5. 灰度发布更新服务时，不要一次性全部更新。可以先更新 1 个副本，观察没问题后再继续更新。用 --update-parallelism 参数控制。写在最后Docker Swarm 虽然不如 Kubernetes 强大，但对中小团队来说，绝对够用了。搭建简单，维护成本低，功能也齐全。我们公司用了快一年，除了刚开始踩了几个坑，后面一直很稳定。最重要的是，它真的解决了我们单点故障的问题。现在随便挂一台服务器，服务都能自动迁移到其他节点，再也不用半夜起来重启服务了。如果你们团队也在考虑容器编排方案，不妨先试试 Docker Swarm。等业务规模大了，再考虑迁移到 Kubernetes 也不迟。这篇文章写了好久，把搭建过程和踩过的坑都详细记录下来了。如果你也在用 Docker Swarm，或者准备搭建 Swarm 集群，希望这篇文章能帮到你。有问题欢迎在评论区留言，我会尽量回复。如果觉得文章有用，点个赞再走呗，也欢迎转发给需要的朋友。公众号：运维躬行录个人博客：躬行笔记关注公众号，获取更多运维干货文章，一起成长，一起进步！
提到搭建环境，很多兄弟可能还停留在“手工匠人”的模式。打开AWS控制台，点那个EC2，选镜像，配安全组，搞子网，再加个负载均衡……这一套下来，脑子都大了。最要命的是，你根本记不住上次那个安全组的入站规则到底是开了80端口还是8080端口。这种“雪花服务器”，配置漂移，环境不一致，简直是运维人的噩梦。这就是为什么我们今天必须得聊聊基础设施即代码这档子事儿。这玩意儿不是什么新概念，但真正能落地用好的，真能救命。咱们今天挑两个最重量级的选手——Terraform和CloudFormation，来一场从入门到实战的“保姆级”教学。别被那些英文单词吓到了，咱们用大白话把它拆开了揉碎了讲。咱们先说说这俩到底是啥如果你还没接触过IaC，可能觉得这俩词儿挺高大上。其实吧，说白了，就是把你的服务器、网络、数据库这些基础设施，像写代码一样定义出来。以前我们要建个仓库，得找施工队，拿图纸，一砖一瓦砌墙。现在有了IaC，就像是给了你一个3D打印机，你写好配置文件，告诉它我要几层楼、几个窗户、门朝哪开，一按按钮，房子自己就长出来了。CloudFormation，这可是AWS的亲儿子。它是AWS自家出的模板服务，专门用来管理AWS的资源。你想想，亲儿子肯定最了解家里的情况，所以它对AWS新特性的支持是最快的，而且跟AWS的控制台集成得非常深。你写个JSON或者YAML文件，往S3桶里一扔，CloudFormation引擎一读，哗啦啦给你创建一堆资源。如果中间出错了，它还能自动回滚，就像撤销键一样，这点特别稳。Terraform，则是HashiCorp公司搞的一个开源工具。这家公司简直是运维界的瑞士军刀厂，Vagrant、Consul都是他们家的。Terraform最大的特点就是“花心”，它不绑定任何一家云厂商。它支持AWS、Azure、Google Cloud，甚至还能管理本地的VMware、Kubernetes。这就好比你找了个全能装修队，既能给你家盖别墅，也能给隔壁老王家盖洋房，用的还是同一套图纸。这对于多云架构来说，简直是神器。那到底选哪个？如果你公司只死磕AWS一家，CloudFormation确实稳，毕竟是亲生的。但如果你不想被一家云厂商绑定死，或者将来有上多云的打算，那我强烈建议你从Terraform入手。咱们今天也主要拿Terraform来举例，顺便把CloudFormation的坑也给你填上。工欲善其事，必先装个“破”软件咱们别光说不练，先把环境搭起来。如果你用的是Mac，那简单，打开终端，如果你装了Homebrew，直接敲：brew install terraform要是Linux或者Windows，去官网下个二进制包，解压后把路径加到环境变量里就行。这没什么技术含量，我就不贴那些流水账似的教程了。装好后，敲个 terraform version，只要能蹦出版本号，那就说明成了。CloudFormation这玩意儿不需要你单独装软件，它就在AWS的控制台里，或者说它本身就是AWS的一部分。你只要有个AWS账号，有AK/SK密钥，用AWS CLI或者直接在网页上点都行。不过为了显得咱们专业点，咱们还是推荐用CLI操作，毕竟天天点鼠标那是文员干的事儿。Terraform入门：从Hello World开始咱们先来搞定Terraform。这玩意儿的核心就是那个 .tf 文件。别看到代码就头大，Terraform用的语言叫HCL（HashiCorp Configuration Language），这语言设计得特别人性化，基本上你只要会点英语，就能猜个八九不离十。咱们先建个文件夹，叫 terraform_demo，进去之后，先建个 main.tf 文件。这就像是咱们写文章的大纲，所有资源都在这儿定义。第一步，咱们得告诉Terraform，我们要用哪个云厂商的服务。这叫Provider。terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 4.0" } } } provider "aws" { region = "ap-northeast-1" # 咱们选东京区域，速度还行 # 这里默认会读取你环境变量里的 AWS_ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY }你看，这段代码多直白。terraform 块里声明了我们要用 hashicorp/aws 这个provider，版本大概是4.0以上的。然后 provider "aws" 里指定了区域。这就好比你去饭店，先告诉服务员我要点川菜，然后找个位置坐下。接下来，咱们来点实际的，建个最简单的EC2服务器（虚拟机）。resource "aws_instance" "my_server" { ami = "ami-0c55b159cbfafe1f0" # 这是一个Amazon Linux 2的镜像ID，不同区域不一样，得去控制台查 instance_type = "t2.micro" # 穷人版配置，免费额度能覆盖 tags = { Name = "MyFirstTerraformServer" } }这段代码里，resource 是关键词，后面跟着资源类型 aws_instance 和资源名称 my_server。花括号里面就是参数了。镜像ID（AMI）这东西比较烦人，每个地区都不一样，你得去AWS控制台那个Launch Instance界面里抄一个现成的。t2.micro 是那种能跑得动但快不起来的小水管机型，咱们测试用足够了。代码写好了，接下来就是见证奇迹的时刻。在终端里，先敲 terraform init。这个命令是干啥的呢？它是去下载刚才咱们指定的那个AWS插件。就像你装完游戏得更新一下补丁包一样。你会看到进度条哗啦啦地走，最后提示你 Terraform has been successfully initialized!。然后，敲 terraform plan。这个命令是我最喜欢Terraform的地方。它不会真的去建服务器，而是先预演一遍。它会告诉你，如果你执行了这个计划，AWS上会发生什么变化。你会看到绿色的 + 号，表示要增加资源。如果看到红色的 - 号，那就是要删除。这给了咱们一个后悔的机会。以前用脚本跑，回车键一按，想收都收不回来，手抖一下就是事故。确认无误后，最后一把梭哈：terraform apply。敲完这个命令，它会让你确认一下，输入 yes。然后你就看着终端里的日志在跑，AWS那边的控制台里，实例状态从 Pending 变成了 Running。不用登录控制台，不用点那些眼花缭乱的下一步按钮，一台服务器就这么诞生了。那种感觉，真的比手动点出来爽多了。这就是代码的力量，这就是“声明式”的魅力——我不告诉你怎么做，我只告诉你我要什么，剩下的你自己搞定。CloudFormation：AWS的亲儿子怎么玩？既然Terraform这么好用，那CloudFormation是不是就没人用了？也不是。在很多大厂，为了合规和安全，CloudFormation依然是首选。毕竟它是AWS亲生的，有些AWS的新功能，Terraform可能还得等插件更新，CloudFormation立马就能用。CloudFormation的玩法稍微硬核一点。它不支持HCL，只认JSON或者YAML。说实话，JSON那大括号套中括号的格式，写多了能把人看瞎，所以咱们一般都用YAML。咱们还是建个文件，叫 template.yaml。AWSTemplateFormatVersion: '2010-09-09' Description: 'My First CloudFormation Stack' Resources: MyEC2Instance: Type: 'AWS::EC2::Instance' Properties: ImageId: 'ami-0c55b159cbfafe1f0' InstanceType: 't2.micro' Tags: - Key: Name Value: 'MyFirstCfnServer'你看，这个结构也很清晰。Resources 下面定义资源。MyEC2Instance 是资源的逻辑名称，Type 指定了资源类型，Properties 里面写参数。这跟Terraform的逻辑其实是通的，只是语法换了身衣服。运行CloudFormation，咱们得用AWS CLI。aws cloudformation create-stack --stack-name my-first-stack --template-body file://template.yaml这命令一敲，AWS就开始干活了。你可以去控制台的CloudFormation界面看，它会创建一个叫 Stack 的东西。这个Stack就像是一个容器，把所有相关的资源都包进去了。CloudFormation有个特别牛的功能叫“回滚”。如果创建过程中某一步失败了，比如安全组配置写错了，它会自动把刚才创建成功的那些资源全给删了，保证环境干净，不留一堆烂摊子。这点对于强迫症运维来说，简直是福音。以前用Shell脚本建到一半报错，你得手动去删那一堆半成品，删漏了还得花钱，那叫一个酸爽。进阶一点：变量和状态管理刚才咱们写的都是“硬编码”，把镜像ID直接写死在代码里。这在生产环境是大忌。真正的运维，代码和配置是分离的。在Terraform里，咱们可以定义变量。建个 variables.tf 文件：variable "instance_type" { description = "The type of EC2 instance" default = "t2.micro" }然后在 main.tf 里引用：instance_type = var.instance_type。这样，以后想换个机型，改变量文件就行，不用动主逻辑。甚至可以在运行 terraform apply 的时候，通过命令行传参，或者搞个 terraform.tfvars 文件专门存环境变量。这里得提一句Terraform最让人头疼，但也最核心的东西：状态文件。你每次运行Terraform，它都会在当前目录下生成一个 terraform.tfstate 文件。这文件千万别小看，它记录了当前资源在云上的真实状态。Terraform之所以能那么聪明地知道是该创建还是该修改，全靠这个文件比对。这就带来了一个团队协作的问题。如果你把代码传到了Git，但状态文件在本地，那同事拉取代码后运行，状态就对不上了。所以，生产环境一定要把状态文件存到远端，比如S3桶里，或者用Terraform Cloud。配置远端状态也不难，在 main.tf 里加个backend配置：terraform { backend "s3" { bucket = "my-terraform-state" key = "prod/terraform.tfstate" region = "ap-northeast-1" } }这样，大家就共享一份状态，谁在改代码，谁在跑Apply，一目了然。这事儿CloudFormation就不用操心，因为状态都在AWS自己肚子里存着呢，你只要管好模板就行。这也是CloudFormation省心的地方。咱们再来聊聊“销毁”运维不仅仅是建，还得会拆。以前手动删资源，经常漏删个EIP或者EBS卷，结果月底账单一来，莫名其妙多出几百块。Terraform在这方面简直是神器。你想想，它能创建，就能销毁。只要一句 terraform destroy。这命令一敲，它会根据状态文件，把你刚才创建的所有资源，一个个按顺序删得干干净净。那种看着资源一个个消失，最后只剩下一个空目录的感觉，真的是极度舒适，解压神器。CloudFormation也一样，删掉Stack，里面的资源自然就跟着没了。不过这里有个坑，大家要注意。有些资源如果加了 prevent_deletion 的标签，或者状态文件丢失了，销毁的时候就会报错。特别是状态文件丢了，那真是叫天天不应，只能去控制台手动删，然后导入状态，那过程简直是渡劫。所以，状态文件的备份和权限管理，一定要重视，一定要重视，一定要重视！重要的事情说三遍。实战中的那些坑和心得理论讲了一堆，咱们聊点实战里的“坑”。1. 依赖关系的坑Terraform虽然能自动推断依赖，比如你建个EC2，它知道得先有VPC和Subnet。但有些隐式依赖它看不出来。比如你得等RDS数据库建好，才能往里面导数据。这时候就得显式指定 depends_on。resource "aws_instance" "web" { # ... depends_on = [aws_instance.db] }如果不加这个，Terraform可能会并行创建，结果Web服务器起起来了，数据库还没好，应用启动报错，排查半天才发现是顺序问题。2. 配置漂移这是最恶心的。你代码里写的是t2.micro，结果哪天为了临时救急，你在控制台手动把它改成了t3.medium。这时候，Terraform状态文件里记的还是t2.micro。下次一跑 terraform apply，它一看实际跟状态不符，二话不说给你改回去。这时候业务如果不兼容t2.micro，直接就崩了。解决办法就是：管住手！ 既然上了IaC，就别去控制台瞎点。实在要点，点完记得刷一下状态文件，用 terraform refresh 同步一下。或者用Terraform的 lifecycle 忽略某些变更，但这属于掩耳盗铃，不推荐。3. 模块化写到后面，你会发现 main.tf 越来越长，甚至好几个项目配置都差不多。这时候就该上模块了。把通用的配置抽出来，打个包。比如搞个 modules/web-server，以后新项目直接引用：module "my_web" { source = "./modules/web-server" instance_count = 3 }这就像写代码封装函数一样，复用性极强。Terraform官方Registry上还有好多别人写好的模块，拿来稍微改改就能用，简直是摸鱼利器。但要注意，用别人的模块得看清楚版本，有时候作者更新了接口，你一拉代码就崩了，所以版本锁定很有必要。结尾：从“搬砖”到“建筑师”说了这么多，其实Terraform和CloudFormation只是IaC世界的冰山一角。Ansible、Pulumi这些工具也都各有千秋。但不管用哪个工具，核心思想都是一样的：把基础设施当软件来管理。一旦你习惯了这种“代码即基础设施”的模式，你就再也回不去那个手动点控制台的时代了。你会发现自己从每天重复造轮子的“搬砖工”，变成了设计架构、编写蓝图的“建筑师”。以前熬夜配环境，现在代码提交，CI/CD流水线一跑，环境自动生成。那种掌控感，才是运维工作的核心价值所在。别光看，赶紧找个AWS或者阿里云的免费账号练练手。哪怕先建个VPC，再建个Bucket，慢慢你就会发现其中的乐趣。遇到报错了别怕，看日志，查文档，Stack Overflow上搜一搜，这都是必经之路。运维这条路，技术更新快，但万变不离其宗。掌握了IaC，你就在云原生时代站稳了脚跟。咱们共勉！公众号：运维躬行录个人博客：躬行笔记如果这篇文章对你有点启发，哪怕只是帮你避了一个坑，或者让你对IaC有了那么一丢丢兴趣，哪怕只是觉得这哥们写得还挺接地气，都请帮忙点赞、在看、转发三连。咱们下期见，聊聊怎么用Ansible把服务器配置管理玩出花来！