古人学问无遗力，少壮工夫老始成。 纸上得来终觉浅，绝知此事要躬行。
从零到一搭建AI智能体：hello-agents项目实战部署全记录
最近在GitHub上发现了一个非常不错的AI Agent项目——hello-agents，这个项目是由Datawhale团队开源的，专门用来帮助大家快速上手AI智能体的开发。作为一个天天和各种开源项目打交道的运维人员，我觉得这个项目特别适合想要入门AI Agent开发的同学。说实话，之前我对AI Agent这块了解得不多，主要还是专注在传统的运维工作上。但是最近公司开始讨论要不要引入一些AI工具来提升运维效率，我就开始关注这个领域了。正好碰到这个hello-agents项目，就想着自己动手试试看。项目地址：https://github.com/datawhalechina/hello-agents文档地址：https://hello-agents.datawhale.cc/#/README.md项目初体验首先说说这个项目给我的第一印象吧。整个项目的文档写得挺清楚的，不像有些开源项目，README写得跟天书一样。hello-agents的目录结构也比较清晰，主要包含了几个核心模块：agents/ - 智能体的核心实现examples/ - 各种示例代码docs/ - 详细的文档说明tests/ - 测试用例我记得第一次clone这个项目的时候，整个过程还是比较顺利的：git clone https://github.com/datawhalechina/hello-agents.git cd hello-agents不过说实话，刚开始看代码的时候还是有点懵的，毕竟AI Agent这个概念对我来说还比较新。但是项目里的examples目录真的帮了大忙，里面有很多现成的例子可以参考。环境搭建的那些坑按照项目文档的说明，环境搭建这块主要是Python环境的配置。我在公司的测试服务器上试了一下，系统是CentOS 7.9，Python版本是3.8.10。首先是安装依赖包：pip install -r requirements.txt这里遇到了第一个问题，有几个包的版本冲突了。特别是transformers和torch这两个包，版本要求比较严格。我花了不少时间在这上面，最后发现还是用虚拟环境比较好：python -m venv hello-agents-env source hello-agents-env/bin/activate pip install -r requirements.txt虚拟环境这个东西真的是好用，避免了很多版本冲突的问题。不过在生产环境中，我一般还是倾向于用Docker容器来部署，这样更加稳定一些。安装过程中还遇到了一个小插曲，就是网络问题。你知道的，有些Python包的下载速度特别慢，我就换了一下pip源：pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/这样速度就快多了。第一个智能体的诞生环境搭建好之后，我就迫不及待地想要跑一个简单的例子。项目里有一个基础的聊天机器人示例，我觉得这个比较适合入门。看了一下代码结构，主要是这样的：from hello_agents import Agent, LLMConfig # 配置大语言模型 llm_config = LLMConfig( model_name="gpt-3.5-turbo", api_key="your-api-key", base_url="https://api.openai.com/v1" ) # 创建智能体 agent = Agent( name="助手小王", description="一个友好的AI助手", llm_config=llm_config ) # 开始对话 response = agent.chat("你好，请介绍一下自己") print(response)不过这里有个问题，就是API Key的配置。项目文档里建议使用环境变量来存储敏感信息，这个做法我是赞同的。在生产环境中，绝对不能把API Key直接写在代码里。export OPENAI_API_KEY="your-actual-api-key" export OPENAI_BASE_URL="https://api.openai.com/v1"然后在代码中这样读取：import os llm_config = LLMConfig( model_name="gpt-3.5-turbo", api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL") )第一次运行的时候，我用的是OpenAI的API，但是你知道的，网络访问有时候不太稳定。后来我改用了国内的一些兼容OpenAI API的服务，效果还不错。深入了解Agent架构在实际使用过程中，我发现这个项目的架构设计还是挺有意思的。不同于传统的单纯聊天机器人，hello-agents引入了智能体的概念，每个Agent都有自己的角色设定、记忆能力和工具使用能力。比如说，我可以创建一个专门负责系统监控的Agent：monitoring_agent = Agent( name="监控专家", description="负责系统监控和告警处理的专业AI助手", system_prompt=""" 你是一名经验丰富的系统运维专家，擅长： 1. 分析系统性能指标 2. 处理各种告警信息 3. 提供运维建议和解决方案 """, llm_config=llm_config )这样设计的好处是，不同的Agent可以专注于不同的领域，提供更加专业的服务。项目中还支持多Agent协作，这个功能我觉得特别有用。比如可以让一个Agent负责收集信息，另一个Agent负责分析，最后由第三个Agent来生成报告。这种工作流程在实际的运维工作中很常见。工具集成实战hello-agents最吸引我的一个功能就是工具集成。Agent不仅可以聊天，还可以调用各种外部工具来完成具体的任务。项目中提供了一些基础的工具示例，比如文件操作、网络请求、数据库查询等。我试着集成了一个系统监控的工具：import psutil def get_system_info(): """获取系统基本信息""" return { "cpu_percent": psutil.cpu_percent(interval=1), "memory_percent": psutil.virtual_memory().percent, "disk_usage": psutil.disk_usage('/').percent, "load_average": psutil.getloadavg() } # 将工具注册到Agent monitoring_agent.register_tool("get_system_info", get_system_info)这样，当用户询问系统状态时，Agent就可以自动调用这个工具来获取实时的系统信息。实际使用下来，效果还是很不错的。不过在工具集成这块，我也遇到了一些问题。主要是安全性方面的考虑，毕竟Agent可能会执行一些系统命令，如果不加限制的话，风险还是比较大的。项目中提供了一些安全机制，比如工具权限控制、命令白名单等，这些在生产环境中是必须要考虑的。记忆管理的巧妙设计在使用过程中，我发现hello-agents的记忆管理设计得挺巧妙的。不像简单的聊天机器人只能记住当前对话，这个项目中的Agent可以维护长期记忆和短期记忆。短期记忆主要是当前对话的上下文，这个比较好理解。长期记忆就比较有意思了，Agent可以记住之前的交互历史，甚至可以学习用户的偏好和习惯。# 配置记忆管理 memory_config = { "short_term_size": 10, # 短期记忆保留最近10轮对话 "long_term_storage": "sqlite", # 长期记忆存储方式 "memory_retrieval": True # 启用记忆检索 } agent = Agent( name="记忆助手", llm_config=llm_config, memory_config=memory_config )在实际测试中，我发现这个记忆功能确实很有用。比如我之前问过某个服务器的配置信息，过了几天再问相关问题时，Agent还能记得之前的内容，这样就不用重复输入了。不过记忆管理也带来了一些新的问题，主要是存储空间和隐私保护。长期记忆会占用不少存储空间，而且用户的对话历史也涉及到隐私问题。项目中提供了一些配置选项来控制这些，但在实际部署时还是需要根据具体情况来调整。性能优化的一些心得在测试过程中，我也关注了一下性能方面的问题。毕竟作为运维人员，系统性能是我们最关心的事情之一。首先是响应速度。AI Agent的响应速度主要取决于大语言模型的推理速度。如果使用云端API，网络延迟也是一个重要因素。我测试了几种不同的配置：使用OpenAI GPT-3.5：响应速度一般，但质量较高使用本地部署的开源模型：响应更快，但需要较多的硬件资源使用国内的API服务：网络延迟较小，性价比不错对于我们的使用场景，我最终选择了国内的API服务，主要考虑的是稳定性和成本。另外一个性能相关的问题是并发处理。如果有多个用户同时使用Agent，系统的并发处理能力就很重要了。项目中提供了一些异步处理的机制，可以提高系统的吞吐量：import asyncio async def handle_multiple_requests(): tasks = [] for user_input in user_inputs: task = agent.chat_async(user_input) tasks.append(task) responses = await asyncio.gather(*tasks) return responses这种异步处理方式在高并发场景下效果还是很明显的。部署到生产环境测试得差不多了，我就开始考虑怎么把这个项目部署到生产环境。作为运维人员，部署这块我还是比较有经验的。首先是容器化。我写了一个Dockerfile：FROM python:3.8-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["python", "app.py"]然后是服务编排。我用docker-compose来管理整个服务栈：version: '3.8' services: hello-agents: build: . ports: - "8000:8000" environment: - OPENAI_API_KEY=${OPENAI_API_KEY} - DATABASE_URL=${DATABASE_URL} volumes: - ./data:/app/data restart: unless-stopped redis: image: redis:alpine ports: - "6379:6379" restart: unless-stopped nginx: image: nginx:alpine ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf depends_on: - hello-agents restart: unless-stopped部署过程中还遇到了一些小问题，主要是权限和网络配置方面的。不过这些都是常见问题，很快就解决了。监控和日志也是必不可少的。我配置了一些基本的监控指标，比如响应时间、错误率、资源使用情况等。这样可以及时发现和处理问题。实际应用场景探索部署完成后，我开始探索一些实际的应用场景。作为运维人员，我首先想到的就是把Agent集成到我们的运维工作流程中。比如说，我创建了一个专门处理告警的Agent。当监控系统产生告警时，这个Agent可以自动分析告警信息，查询相关的历史数据，然后生成初步的处理建议。虽然不能完全替代人工处理，但确实可以提高效率。alert_agent = Agent( name="告警处理专家", system_prompt=""" 你是一名专业的运维工程师，负责处理各种系统告警。 当收到告警信息时，你需要： 1. 分析告警的严重程度 2. 查询相关的历史数据 3. 提供可能的解决方案 4. 如果需要人工介入，明确指出需要关注的重点 """, tools=["query_metrics", "check_logs", "get_system_status"] )另外一个应用场景是文档管理。我们运维团队有很多技术文档，平时查找起来比较麻烦。我用hello-agents搭建了一个文档助手，可以根据用户的问题快速找到相关的文档内容。这个功能的实现主要是基于向量检索，先把文档内容转换成向量，然后根据用户问题进行相似度匹配。项目中提供了相关的工具，使用起来还是比较方便的。遇到的一些坑和解决方案在实际使用过程中，我也遇到了不少问题，这里分享一些主要的坑和解决方案。第一个大坑是token消耗。AI Agent在处理复杂任务时，会消耗大量的token，成本很快就上去了。我的解决方案是：优化prompt，减少不必要的输出使用更便宜的模型处理简单任务实现token使用量监控和限制第二个问题是模型幻觉。有时候Agent会给出看起来很合理但实际上错误的信息，这在运维场景下是很危险的。我的应对策略是：对关键操作增加人工确认环节使用多个Agent交叉验证重要信息建立知识库，让Agent基于已知信息回答问题第三个问题是稳定性。API服务偶尔会出现故障或限流，影响Agent的正常工作。我的解决方案包括：配置多个API服务作为备份实现请求重试机制添加降级处理逻辑未来的一些想法使用hello-agents这段时间下来，我对AI Agent的应用前景还是很看好的。特别是在运维领域，有很多重复性的工作可以通过Agent来自动化处理。我现在在考虑的几个方向：与现有的监控系统深度集成，实现更智能的告警处理开发专门的运维知识库，让Agent能够提供更准确的技术支持探索多Agent协作，处理更复杂的运维场景当然，这些都还在规划阶段，需要更多的实践验证。hello-agents这个项目给我最大的感受就是，AI Agent技术已经比较成熟了，完全可以在生产环境中使用。虽然还有一些限制和问题，但总体来说是一个很有价值的工具。对于想要入门AI Agent开发的同学，我强烈推荐试试这个项目。文档写得不错，示例代码也很丰富，上手难度不大。而且作为开源项目，可以根据自己的需求进行定制开发。总结这次hello-agents项目的实践让我对AI Agent有了更深入的了解。从环境搭建到功能测试，再到生产部署，整个过程虽然遇到了一些问题，但总体还是比较顺利的。最重要的是，我看到了AI Agent在实际工作中的应用潜力。虽然现阶段还不能完全替代人工，但作为辅助工具，确实可以大大提高工作效率。如果你也对AI Agent感兴趣，不妨试试这个项目。记住，最好的学习方式就是动手实践。遇到问题不要怕，多看文档，多试试，总能找到解决方案的。好了，今天的分享就到这里。如果你觉得这篇文章对你有帮助，欢迎点赞转发，让更多的朋友看到。有什么问题或者想法，也欢迎在评论区交流讨论。公众号：运维躬行录个人博客：躬行笔记
一封邮件的「江湖」：我在生产环境踩出来的 SMTP 与 IMAP 实战笔记
SMTP 与 IMAP ，这俩协议天天见，结果很多朋友只知道“收件服务器、发件服务器”那两栏要填啥，真正出问题了，全是一脸懵。既然问得多，我干脆把这几年在生产环境里折腾邮件系统的经验捋一遍，写成一篇文章。别讲那些教科书式的定义了，咱就从“问题怎么排”和“系统怎么设计”角度，把 SMTP 和 IMAP 的区别、组合方式、以及常见坑说清楚，保证你看完之后，再碰到邮件系统告警，心里不慌。先把话说白：SMTP 干送，IMAP 管收我自己脑子里一直有个特别土的比喻：SMTP = 快递公司IMAP = 你小区门口的快递柜 + 你手机上的那个取件小程序SMTP 把邮件从发件人一路「运」到收件人的邮箱服务器，IMAP 让收件人从服务器上「取」邮件、整理邮件、同步状态。用一句话总结：SMTP 负责把邮件丢进对方的邮箱，IMAP 负责让你舒服地翻、看、删这些邮件。这俩协议一个负责“上行投递”，一个负责“下行访问”。搞邮件系统的时候，脑子里先把这个定位弄明白，后面很多设计就顺了。SMTP：邮件世界的物流总线1. SMTP 的“线路”到底长啥样我们平时说的“发件服务器”就是 SMTP 服务器。一封邮件从你的电脑发出去，大致会经历这样一条路线：客户端（Outlook、Foxmail、手机邮箱 App）连接企业 SMTP 服务器（通常 25 / 587 / 465 端口）企业 SMTP 把邮件转给对方邮箱的 MX 服务器对方的 SMTP/MDA 把邮件写入对方的邮箱存储这条线中间可能会跳过好多个中转 SMTP，就跟快递中转站一样。日志里经常能看到一长串 Received: 头，就是一路走过来的“盖章记录”。我做日志排查的时候，最常用的就是沿着 Received 头一点点往前追，看哪一段抛的错、哪一段延迟高。2. 端口和加密，那点绕人的细节很多人被“端口”坑过。常见组合是这么几个：25：传统 SMTP 明文端口，现在经常被运营商限速、封禁587：提交端口（submission），配合 STARTTLS，用得比较多465：SMTPS，直接加密，很多客户端默认用这个在企业里，我一般这么配置：对内客户端：开放 587 + 强制 STARTTLS对外互联：25 端口继续保留，但只允许运营商出口465 是否开，看客户端兼容性，有些旧客户端只认 465真实场景里我踩过一个坑：某次我们把 25 端口只保留在外联网，内网都改 587，结果一堆老脚本和老系统里写死了 25，SMTP 直连，全部挂掉。后来我干脆在 25 上做了一个“内部访问自动 301 提示”的 banner，提示改 587，不然问题单会被淹死。3. SMTP 的三件“大事”：认证、队列、反垃圾3.1 发邮件必须先“报个到”现代 SMTP 基本都要求认证：AUTH LOGIN / PLAIN / CRAM-MD5 等机制配合 TLS 保证密码不明文传输不认证会怎样？两种情况：对内：防止内部被植入木马变成垃圾邮件“肉鸡”对外：很多对方 SMTP 直接把匿名邮件拒掉在公司里，我一般会配：不同系统用不同 SMTP 账号，方便追踪邮件头里加 X-App-ID 之类的字段，遇到问题可以快速定位哪个系统发的3.2 队列系统，SMTP 的“缓冲胃”邮件有个特点：不能保证实时可达，只能保证“尽力而为 + 重试”。所以 SMTP 必须要有一套队列机制：对方服务器暂时不可达 → 暂存队列，按策略重试对方返回 4xx（临时错误）→ 延迟重投对方返回 5xx（永久错误）→ 退信给发件人我实战里遇到过一个典型问题：某天监控提示队列数暴涨，mailq 一看堆了几万封日志里全是 451 4.7.1 Service unavailable - try again later一查，对方某运营商的邮箱服务器在限流最后只好：调整该域名的重试间隔，防止队列被塞满对内部用户明显错误的收件地址，提前做格式校验，少无效投递这类队列问题，在 SMTP 层是常态，没监控就等着挨打。3.3 反垃圾：SPF、DKIM、DMARC 那些事现实世界里，SMTP 最大的痛就是垃圾邮件。我们在线上搭 SMTP，绕不过这三兄弟：SPF：发件 IP 是否在域名授权列表里DKIM：邮件内容是否被发件域名用私钥签名DMARC：发件域在收件方那边的策略（拒收 / 隔离 / 报告）我有一次帮一个客户排查：他们从企业邮箱发给某大厂邮箱，总是进“垃圾邮件”查收件方反馈的报告，发现 SPF 记录里少了一条新上线的网关 IP加上 SPF 再测试，投递成功率直接上去了。这里的经验就是：但凡你有多个出口 IP 做 SMTP，一定要统一维护 SPF，不然就会出现“有时候正常、有时候进垃圾”的玄学问题。IMAP：让你随时随地翻邮箱的幕后功臣1. IMAP 的「访问模式」到底差在哪IMAP 和 POP3 最大的区别，归结到两个字：同步。POP3 更像“把邮件拉到本地，服务器删不删看配置”IMAP 是“所有操作都在服务器上，客户端只是一个视图”所以 IMAP 支持：多终端同步：手机、电脑、Webmail 看的是同一个状态服务器端文件夹：分类整理都在服务器标记同步：已读 / 未读 / 星标 / 标签 等各种状态我们运维日常遇到用户抱怨：“为什么手机上删了邮件，电脑上还在？”十有八九就是 POP3，且客户端设置成“保留服务器副本 + 本地删除”。换成 IMAP，就没这个问题了。2. 端口、加密和实际部署的小细节默认端口：143：IMAP 明文993：IMAPS（IMAP over TLS）我一般在生产里这么搞：对内：143 + 强制 STARTTLS对外：只开 993，直接 TLS，省去各种降级风险明文登录一律禁止，除非是测试环境实际踩坑经验：某次我们对 IMAP 服务器做证书更新，忘了把中间证书链传完结果一堆 Apple 设备连不上，一直提示“无法验证身份”最后发现是 iOS 对证书链要求更严格，补上中间证书后恢复这个坑提醒我：邮件协议虽然老，但 TLS 这一层的现代要求一点不低，证书、协议栈、加密套件全都要盯着。3. IMAP 的几个「易忽视」能力3.1 服务器端搜索很多人觉得 IMAP 慢，其实问题在服务器实现上。IMAP 原生就支持：按发件人、主题、日期、关键字搜索按“未读”、“带附件”这些条件过滤如果后端存储和索引没做好：一旦用户邮箱几万封，搜索就会卡死客户端请求一条 SEARCH，服务器 CPU 飙升我在一个项目里，把 IMAP 后端从简单的 Maildir 换成了带索引的存储（例如 dovecot + fts），搜索性能从“几秒”级降到“几百毫秒”，用户体验差距挺明显。3.2 多终端并发访问IMAP 允许一个账号同时在多个终端登录，但是后台要考虑的点就多了：并发连接限制（防止一个用户占满整个进程）带宽和 IO 压力（多终端同步大附件）IDLE / PUSH 通知（长连接保持）我踩过一个很现实的坑：某次我们在 IMAP 前加了一个反向代理结果 proxy 的 timeout 太短，IDLE 连接老被断用户就抱怨“客户端老提示重新连接”最后只好对 IMAP 端口做专门的负载，保证 IDLE/长连接不卡住。SMTP + IMAP：一个完整邮件系统是怎么落地的光知道“谁发谁收”还不够，真正搭个邮件系统，需要考虑一堆实际问题。我以一个典型的中小企业邮件系统为例，把架构和过程说一说。1. 一个常见的企业邮件系统架构大体会这么几层：入口层：MX 记录指向的外网 SMTP负载均衡 + 防火墙 + WAF（可选）网关层：反垃圾 / 反病毒（如 rspamd / amavis）灰名单、白名单、RBL 黑名单查询业务层：SMTP 服务（发送、转发）IMAP 服务（访问）Webmail（给没客户端的人用）存储层：邮箱存储（本地磁盘 / 分布式存储）备份与归档认证与目录：LDAP / AD 做账号管理单点登录 / OAuth 等这种结构在实际部署中，最容易被忽略的是网关层和存储层。很多人一开始用一台服务器搞定 SMTP+IMAP+存储，等用户一多，磁盘满了，性能扛不住，再拆已经来不及了。我踩坑就是这么来的，后来干脆：一开始就把存储挂在独立 NAS/对象存储上邮件头部统一经过网关清洗，再进内部系统2. 常见业务场景下的协议选择我按几个实际遇到的场景，聊聊 SMTP + IMAP 的玩法。2.1 企业办公邮件这个不用多说，基本标配：发件：客户端配置 SMTP（587/465），启用 AUTH + TLS对外的所有发件 IP 做 SPF + DKIM收件：客户端配置 IMAP（993）对外 MX 记录指向网关 → 内网 SMTP → 存储细节上，我会：为不同部门配置不同的发件限制（附件大小、收件人数量）对 IMAP 做连接数限制，防止某些客户端疯狂开连接2.2 系统通知邮件（不需要收件箱）比如：Web 系统注册验证码业务系统告警邮件账单通知等这类场景，只需要 SMTP 发件，常见做法：配置一个或多个专用发件账号，例如 noreply@xxx.comSMTP 发件走单独的 IP 和队列，避免和正常办公邮件混在一起对退信地址做集中收集，便于监控投递情况有一次我们遇到，业务系统的验证码邮件大批进垃圾箱，最后发现：这条业务线的 noreply 账号走的是第二出口 IP但 DKIM 还没在那条 IP 出口网关上配置收件方看到签名不一致，给了较低评分调好 DKIM 与 SPF 后，退信率明显下降。2.3 多设备访问 + 大附件环境比如高管、销售，手机 + 电脑 + iPad 三端都要收邮件，而且动不动发 PPT、PDF 五六十兆。这类需求里，IMAP 就是核心：邮件统一存储在服务器多终端只是“窗口”，不用担心本地丢失后端需要考虑大附件的分片存储、备份、归档我曾经帮一个销售团队把邮箱从 POP3 切到 IMAP：切之前：每个人电脑上都有自己的 .pst、.dbx 文件某些电脑坏了，历史邮件找不回切之后：统一存储在服务器甚至可以给他们开共享邮箱，团队协作方便很多这件事让我越发觉得：邮件协议看着老，但 IMAP 在“协同”这块还挺香。运维视角下的常见故障与排查思路写到这里，如果不聊聊故障排查，就太虚了。我挑几个日常经常遇到的问题，分享一下我自己的排查路径。1. 用户说“发不出去邮件”这句投诉太常见了，背后可能是：客户端配置错误SMTP 被运营商拦截被垃圾网关拒绝认证失败…我每次处理，都习惯先问三句话：是所有人都发不出去，还是你一个？是对所有域名都不行，还是某个特定域？客户端有没有看到错误代码或提示？然后对应去看 SMTP 日志，典型错误码有：5xx：永久错误（账号不存在、拒收、黑名单）4xx：临时错误（对方服务器忙、限流）530/550 之类，很多是认证相关有一次，用户说“发不出去”，日志里全是：554 5.7.1 Relay access denied原因是：该用户在外网直接连的 SMTP，没有认证我们服务器不允许未认证的 relay最后给他解释：外网访问必须先输账号密码，不然你被盗用发垃圾邮件，我们也会被拉黑。2. 用户说“收不到某个域的邮件”这种一听就知道是“收件路径”问题。排查步骤差不多是这样：先用 nslookup -type=mx 看对方是否有 MX 记录在自己网关上抓取 postfix / exim 日志，看有没有相关连接如果连日志都没有，要考虑是不是被上游拦截 / DNS 配错我遇到一个特别坑的：某客户把自己的 MX 记录指向了一个外部安全网关安全网关再转发到我们的邮件系统结果他们在网关上加了一个“某域黑名单”，忘了告诉我们导致我们这边怎么查都看不到对方邮件，最后还是通过对方提供的网关日志才定位。经验就是：邮件路径一旦有多跳，日志一定要能串起来，不然排查就会很难受。3. IMAP 同步异常、邮件状态乱掉这类问题一般表现为：某终端看不到新邮件已读 / 未读状态不同步文件夹结构错乱我习惯的做法是：让用户先在 Webmail 上确认邮件是否存在如果 Webmail 正常 → 问题在客户端Webmail 都看不到 → 可能是投递/存储问题对 IMAP 日志开启 debug，看客户端发了哪些命令检查后端存储是否有损坏，必要时跑一遍 fsck 或索引重建有一次，一个用户邮箱经常莫名其妙少邮件，最后发现：他用某国产客户端，默认配置是“只保留 30 天邮件”客户端会主动执行删除操作，IMAP 后端就跟着删了我们只好给所有用户发通知，建议使用官方推荐客户端，并且在服务器上加了一个“邮箱回收站”，人为提高容错。协议只是基础，策略才是生产的灵魂讲了这么多，其实 SMTP 和 IMAP 本身就那点东西，真正决定一个邮件系统好不好用、稳不稳定的，是你怎么设计策略。我最后再拉几个实践中比较关键的点，给你做个“备忘单”。1. 带宽和存储规划别小看邮件：每个用户每天几十封邮件附件越来越大，各种扫描件、PPT、视频多终端 IMAP 同步，后台 IO 压力很大我个人经验：用户数 < 500 的小公司：单机 + 备份够用，但要提前规划磁盘扩展用户数 500～2000：SMTP/IMAP 分离，存储挂 NAS/分布式文件系统更大规模：考虑专业邮件系统或 SaaS，自己搞成本高2. 安全策略：既要防垃圾，也别误伤自己发件侧：SPF、DKIM、DMARC 必配限制单日/单小时发件量对异常 IP（国外、云主机）多加一层验证收件侧：白名单（合作伙伴、内部系统）黑名单（已知垃圾源 IP / 域）灰名单（小批量试投，效果挺好）我有个习惯：所有安全规则上线前，一定先开“仅打分不拦截”模式至少一周，看日志、看评分，防止误伤正常邮件。3. 监控与告警邮件系统不怕小问题，就怕“悄悄挂了”。可以重点监几个指标：SMTP 队列长度每分钟投递成功/失败数量IMAP 并发连接数、响应时间关键进程存活、端口监听有一次凌晨，我们的队列突然暴涨，但 SMTP 服务还活着，监控没细到队列长度，结果第二天早上上百个用户投诉收不到邮件。从那以后，我直接把“队列长度”做成了红色大屏：一旦超过阈值，短信 + 电话轮番上。写在最后：把邮件系统当成一个“老但认真”的同事说句实在话，在现在这个时代，大家都在聊 IM、企业微信、各种协同工具，邮件看起来有点“老旧”。但从运维的视角看，邮件系统的价值一点都没降：关键通知、正式文件，依然靠邮件很多自动化系统（告警、报表）还是用邮件做兜底跨公司、跨组织的沟通，邮件依然是默认通道SMTP 和 IMAP 这两个协议，就像两个老同事：一个负责把信件送到每个人的桌上一个负责帮你把桌上的信收拾得整整齐齐你把他们伺候好了，整套信息系统就有了一个稳定可靠的“消息骨干”。写这篇文章，也算是我这几年踩坑的小结。希望你下次在配置邮件客户端、设计通知系统、排查投递问题的时候，脑子里能自然地把“SMTP 干送、IMAP 管收”这根线串起来，再往下细分认证、队列、反垃圾、安全、监控这些点，就不会被各种莫名其妙的问题搞得焦头烂额。如果你那边正好在搭邮件系统，或者被某个奇怪的退信码折磨，可以把问题整理一下，后面我可以专门写一篇“常见退信码实战排查”的文章，把那些年栽过的跟头再翻一遍。如果这篇实战笔记对你有点帮助，欢迎转发给同事一起看，也可以在评论区把你遇到过的奇葩邮件问题说出来，大家一起交流，互相少踩点坑。更多运维实践，我会持续更新在：个人博客：躬行笔记
生产环境翻过车之后，我才真正搞懂了 Nginx 金丝雀发布
说起金丝雀发布，我自己也是吃过亏的。之前在一家电商公司，有一次大促前夜做版本更新，直接全量切换，结果新版本有个缓存的 bug，商品价格显示异常，虽然只持续了十几分钟就回滚了，但那十几分钟的订单全部要人工核对，运营那边差点没把我们骂死。后来我们就开始研究灰度发布的方案，金丝雀发布就是其中最经典的一种。金丝雀发布到底是个啥这个名字听起来挺文艺的，其实来源很朴素。早年间煤矿工人下矿井之前，会带一只金丝雀进去，因为金丝雀对有毒气体特别敏感，如果金丝雀没事，说明矿井安全；如果金丝雀出了问题，矿工就赶紧撤。放到软件发布的场景里，道理是一样的。你不是一次性把新版本推给所有用户，而是先放一小部分流量（比如 5%、10%）到新版本上，观察一段时间，看看有没有报错、性能有没有下降、用户反馈怎么样。如果一切正常，再逐步扩大流量比例，最终完成全量切换。如果新版本有问题，影响的也只是那一小部分用户，及时回滚就行了，损失可控。跟全量发布比起来，金丝雀发布的好处很明显：风险可控，出了问题影响面小可以拿真实流量验证新版本，比测试环境靠谱多了回滚方便，改个配置 reload 一下就行不需要额外的硬件资源，Nginx 本身就能搞定当然它也不是万能的，比如涉及到数据库结构变更的发布，单靠流量层面的灰度是不够的，还需要配合数据层面的兼容方案。不过大多数场景下，金丝雀发布已经够用了。实现原理，其实不复杂用 Nginx 做金丝雀发布，核心就是利用 Nginx 的反向代理和负载均衡能力，把流量按照一定的规则分发到不同的后端服务上。简单来说就是你有两组后端服务：一组跑的是稳定的老版本（我们叫它 stable），一组跑的是待验证的新版本（我们叫它 canary）。Nginx 作为入口，根据你定义的规则，决定每个请求应该转发到哪一组。规则可以有很多种玩法：按权重分配：比如 90% 的流量走老版本，10% 走新版本按特定用户分配：比如某些测试账号、内部员工的请求走新版本按 Cookie 分配：带有特定 Cookie 的请求走新版本按 Header 分配：请求头里带了特定标记的走新版本按 IP 分配：某些 IP 段的请求走新版本实际生产中，这几种方式经常组合使用。比如先让内部员工全部走新版本测试一轮，没问题了再按权重放 5% 的外部流量进来。环境准备我这里用一个比较贴近真实场景的例子来演示。假设我们有一个 Web 应用，当前稳定版本跑在 8080 端口，新版本跑在 8081 端口。Nginx 监听 80 端口作为入口。服务器环境：CentOS 7 / Rocky Linux 8（Ubuntu 也一样，路径稍有不同）Nginx 1.20+（更低版本也行，基本功能都支持）两个后端应用实例先确认 Nginx 安装好了：nginx -v # nginx version: nginx/1.20.1如果没装的话：# CentOS/Rocky yum install -y nginx # Ubuntu/Debian apt install -y nginx为了演示方便，我用两个简单的后端来模拟。实际生产中这两个就是你的应用服务，可能是 Tomcat、Node.js、Go 服务之类的。用 Python 快速起两个模拟服务：# 终端1 - 模拟稳定版本（v1） mkdir -p /tmp/stable && echo "This is STABLE version v1.0" > /tmp/stable/index.html cd /tmp/stable && python3 -m http.server 8080 # 终端2 - 模拟金丝雀版本（v2） mkdir -p /tmp/canary && echo "This is CANARY version v2.0" > /tmp/canary/index.html cd /tmp/canary && python3 -m http.server 8081方案一：基于权重的流量分配这是最简单也是最常用的方式。通过 Nginx 的 upstream 模块，给不同的后端设置不同的权重，实现按比例分流。编辑 Nginx 配置文件：# /etc/nginx/conf.d/canary.conf upstream backend { # 稳定版本，权重 9，大约承担 90% 的流量 server 127.0.0.1:8080 weight=9; # 金丝雀版本，权重 1，大约承担 10% 的流量 server 127.0.0.1:8081 weight=1; } server { listen 80; server_name your-domain.com; location / { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }检查配置并重载：nginx -t nginx -s reload验证一下效果，多请求几次看看分布：for i in $(seq 1 20); do curl -s http://localhost; done你会发现大概每 10 次请求里有 1 次会返回 canary 版本的内容，其余都是 stable 版本。这种方式的好处是简单粗暴，配置改起来也方便。想调整比例的时候，改一下 weight 值，reload 就生效了。比如观察了一段时间没问题，想把金丝雀流量提到 30%：upstream backend { server 127.0.0.1:8080 weight=7; server 127.0.0.1:8081 weight=3; }不过这种方式有个问题，就是同一个用户可能一会儿访问到老版本，一会儿访问到新版本，体验不太一致。如果你的应用对这个比较敏感，就需要用下面的方案了。方案二：基于 Cookie 的精准分流这个方案可以保证同一个用户始终访问同一个版本，体验更一致。思路是通过 Cookie 来标记用户应该走哪个版本。# /etc/nginx/conf.d/canary_cookie.conf upstream stable { server 127.0.0.1:8080; } upstream canary { server 127.0.0.1:8081; } server { listen 80; server_name your-domain.com; # 根据 cookie 判断走哪个版本 set $backend "stable"; if ($http_cookie ~* "canary=true") { set $backend "canary"; } location / { proxy_pass http://$backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这样配置之后，默认所有请求都走 stable 版本。只有当请求的 Cookie 里带了 canary=true 的时候，才会转发到 canary 版本。怎么给用户种上这个 Cookie 呢？有几种方式：1、手动在浏览器里加，适合内部测试：打开浏览器开发者工具，在 Console 里执行：document.cookie = "canary=true; path=/";2、通过后端接口设置，适合灰度平台对接：后端提供一个接口，根据用户 ID 或者其他规则，决定是否给用户种上 canary Cookie。3、通过 Nginx 自身来做随机分配并种 Cookie：# /etc/nginx/conf.d/canary_split.conf upstream stable { server 127.0.0.1:8080; } upstream canary { server 127.0.0.1:8081; } # 利用 split_clients 模块按比例分配 split_clients "${remote_addr}${remote_port}" $variant { 10% canary; * stable; } server { listen 80; server_name your-domain.com; # 如果已经有 cookie 了，就按 cookie 走 set $target $variant; if ($http_cookie ~* "canary_version=canary") { set $target "canary"; } if ($http_cookie ~* "canary_version=stable") { set $target "stable"; } location / { # 给用户种上 cookie，下次来就固定走同一个版本了 add_header Set-Cookie "canary_version=$target; path=/; max-age=3600"; proxy_pass http://$target; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这个配置用了 split_clients 模块，它会根据你指定的变量（这里用的是客户端 IP 和端口的组合）做哈希，然后按比例分配。第一次访问的时候，10% 的用户会被分到 canary，同时种上一个 Cookie。后续再访问的时候，就直接按 Cookie 走了，不会再重新分配。split_clients 这个模块是 Nginx 自带的，不需要额外安装，很多人不知道有这个东西，其实挺好用的。方案三：基于请求头的分流这种方式在 API 网关场景下用得比较多。前端或者调用方在请求头里带上一个标记，Nginx 根据这个标记来决定路由。# /etc/nginx/conf.d/canary_header.conf upstream stable { server 127.0.0.1:8080; } upstream canary { server 127.0.0.1:8081; } server { listen 80; server_name your-domain.com; set $backend "stable"; # 根据自定义请求头判断 if ($http_x_canary = "true") { set $backend "canary"; } # 也可以根据 User-Agent 判断，比如特定版本的 App if ($http_user_agent ~* "MyApp/2.0") { set $backend "canary"; } location / { proxy_pass http://$backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }测试的时候带上自定义 Header：# 走金丝雀版本 curl -H "X-Canary: true" http://localhost # 走稳定版本（不带 header） curl http://localhost这种方式的好处是控制粒度很细，而且不依赖 Cookie，适合服务间调用的场景。我们之前做微服务灰度的时候就是用的这种方式，在网关层根据请求头做路由，Header 可以在整个调用链路上透传。方案四：基于 IP 的分流有时候你想让特定网段的用户（比如公司内网）先体验新版本，可以基于 IP 来做分流。# /etc/nginx/conf.d/canary_ip.conf upstream stable { server 127.0.0.1:8080; } upstream canary { server 127.0.0.1:8081; } # 定义一个 geo 变量，根据客户端 IP 来赋值 geo $canary_ip { default 0; 192.168.1.0/24 1; # 内网测试网段 10.0.100.0/24 1; # 办公网络 # 也可以指定单个 IP 203.0.113.50 1; } server { listen 80; server_name your-domain.com; set $backend "stable"; if ($canary_ip = "1") { set $backend "canary"; } location / { proxy_pass http://$backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }geo 模块也是 Nginx 内置的，不需要额外安装。它可以根据客户端 IP 地址来设置变量的值，支持 CIDR 格式的网段，用起来很方便。不过要注意一点，如果你的 Nginx 前面还有一层负载均衡（比如云厂商的 SLB），那拿到的 $remote_addr 可能是 SLB 的 IP 而不是真实客户端 IP。这时候需要用 $http_x_forwarded_for 或者配置 set_real_ip_from 来获取真实 IP：# 信任前置负载均衡的 IP set_real_ip_from 10.0.0.0/8; real_ip_header X-Forwarded-For; real_ip_recursive on;方案五：组合方案（生产推荐）实际生产中，我一般会把上面几种方式组合起来用。比如下面这个配置，优先级是：请求头 > Cookie > IP > 默认权重分配。# /etc/nginx/conf.d/canary_production.conf upstream stable { server 127.0.0.1:8080; } upstream canary { server 127.0.0.1:8081; } geo $canary_by_ip { default 0; 192.168.1.0/24 1; } split_clients "${remote_addr}" $canary_by_weight { 5% 1; * 0; } map $http_x_canary $canary_by_header { "true" 1; default 0; } map $cookie_canary_version $canary_by_cookie { "canary" 1; default 0; } # 综合判断：任意一个条件命中就走 canary map "$canary_by_header:$canary_by_cookie:$canary_by_ip:$canary_by_weight" $target_backend { "~1" canary; # 只要有一个是 1 就走 canary default stable; } server { listen 80; server_name your-domain.com; # 添加响应头，方便排查问题（生产环境可以去掉） add_header X-Backend $target_backend; location / { proxy_pass http://$target_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 超时设置 proxy_connect_timeout 5s; proxy_read_timeout 30s; proxy_send_timeout 30s; } # 健康检查接口（可选） location /health { access_log off; return 200 "OK"; } }这个配置里有个小技巧，就是用 map 指令做多条件的组合判断。"~1" 是正则匹配，只要拼接出来的字符串里包含 1，就走 canary。这样写比嵌套一堆 if 要优雅得多，而且 Nginx 官方也不推荐在 location 里大量使用 if，容易出一些奇怪的问题。另外我加了一个 X-Backend 的响应头，这个在调试的时候特别有用，你可以通过浏览器开发者工具或者 curl 看到当前请求走的是哪个版本：curl -I http://localhost # 看响应头里的 X-Backend 字段生产环境上线之后记得把这个响应头去掉，或者改成只在内网可见，别把内部信息暴露出去了。发布流程和注意事项光有配置还不够，金丝雀发布是一个完整的流程，我把我们团队实际在用的流程分享一下。发布前的准备工作：确认新版本在测试环境已经充分验证过准备好回滚方案，确认回滚操作步骤通知相关人员（开发、测试、产品），约定观察时间确认监控告警已经配置好，能及时发现问题发布过程：第一步，部署新版本到 canary 节点，但先不接入流量：# 先把 canary 的权重设为 0 或者注释掉 upstream backend { server 127.0.0.1:8080 weight=10; server 127.0.0.1:8081 down; # 先标记为 down }第二步，部署完成后，先让内部人员通过 Header 或 Cookie 的方式访问 canary 版本，做一轮冒烟测试。第三步，冒烟测试通过后，开放小比例流量（比如 5%）：# 修改配置，开放 5% 流量 split_clients "${remote_addr}" $canary_by_weight { 5% 1; * 0; }nginx -t && nginx -s reload第四步，观察监控指标，重点关注：错误率：新版本的 5xx 错误率是否正常响应时间：P99、P95 延迟有没有明显上升业务指标：订单量、转化率等核心指标有没有异常日志：有没有异常的错误日志第五步，如果一切正常，逐步扩大流量比例：5% → 10% → 30% → 50% → 100%。每次调整后至少观察 15-30 分钟。如果发现问题需要回滚，操作也很简单：# 方式一：把 canary 标记为 down upstream backend { server 127.0.0.1:8080 weight=10; server 127.0.0.1:8081 down; } # 方式二：直接把 canary 的权重设为 0 split_clients "${remote_addr}" $canary_by_weight { 0% 1; * 0; }nginx -t && nginx -s reloadreload 是平滑重载，不会中断现有连接，所以不用担心回滚操作本身会造成影响。还有几个容易踩坑的地方说一下：1、Session 一致性问题。如果你的应用是有状态的（比如 Session 存在本地），用户从 stable 切到 canary 的时候可能会丢失登录状态。解决办法是把 Session 放到 Redis 之类的外部存储里，或者用 JWT 这种无状态的认证方式。2、数据库兼容性。如果新版本涉及数据库表结构变更，一定要确保新老版本都能兼容当前的数据库结构。一般的做法是先做数据库变更（要向后兼容），再发布新版本代码。3、静态资源的问题。如果前端静态资源也有变更，要注意 CDN 缓存的影响。建议静态资源用带 hash 的文件名，避免缓存导致新老版本的资源混用。4、WebSocket 连接。如果你的应用用了 WebSocket，金丝雀发布的时候要注意长连接的处理。Nginx reload 不会断开已有的 WebSocket 连接，但新连接会按新的规则分配。监控配置参考做金丝雀发布，监控是必不可少的。这里给一个简单的 Nginx 日志配置，方便你区分不同版本的请求：# 自定义日志格式，加上 backend 标记 log_format canary_log '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" ' 'backend=$target_backend rt=$request_time'; server { # ... access_log /var/log/nginx/canary_access.log canary_log; }这样在日志里就能看到每个请求走的是哪个后端，配合 ELK 或者 Grafana + Loki 之类的日志分析工具，可以很方便地对比两个版本的各项指标。用 awk 简单统计一下各版本的请求量和错误率：# 统计各版本请求量 awk '{for(i=1;i<=NF;i++) if($i ~ /^backend=/) print $i}' /var/log/nginx/canary_access.log | sort | uniq -c # 统计各版本的 5xx 错误 awk '$9 >= 500 {for(i=1;i<=NF;i++) if($i ~ /^backend=/) print $i}' /var/log/nginx/canary_access.log | sort | uniq -c写在最后金丝雀发布说白了就是一个"小心试探"的过程，核心思想就是用最小的代价去验证新版本的稳定性。Nginx 作为实现工具，配置灵活，性能也好，基本上能覆盖大多数场景。当然，如果你的业务规模比较大，服务数量比较多，可能需要更专业的灰度发布平台，比如结合 Kubernetes 的 Istio、或者用 OpenResty 做更复杂的流量治理。但对于大多数中小团队来说，Nginx 原生的这些能力已经足够了，不需要引入太多额外的复杂度。我个人的建议是，不管你用什么方案，关键是要把发布流程规范化，每次发布都按照固定的步骤来，观察该观察的指标，该回滚的时候果断回滚。工具只是手段，流程和意识才是核心。如果这篇文章对你有帮助，欢迎转发给身边的运维和开发同学，大家一起少踩坑。也欢迎关注我，后续会继续分享更多生产实践相关的内容。公众号：运维躬行录个人博客：躬行笔记
线上系统出问题了？strace工具让你分分钟定位根因！
说到线上排查问题，我想起前段时间遇到的一个头疼事。有个服务突然变得特别慢，用户投诉一大堆，领导也在催。各种监控指标看起来都正常，CPU、内存、网络都没啥异常，但就是慢得要死。这种时候你说慌不慌？表面上风平浪静，实际上程序内部不知道在搞什么鬼。传统的监控工具告诉你的信息有限，日志也没发现啥异常。这时候就需要深入到系统调用层面去看看到底发生了什么。今天我就来聊聊strace这个神器，它可以说是我们运维人员手里的"透视镜"，能够清楚地看到程序和操作系统之间的每一次交互。什么是strace？strace就是system call trace的缩写，顾名思义就是用来跟踪系统调用的工具。你可以把它理解成一个"窃听器"，专门监听程序和内核之间的对话。每当程序需要做一些操作，比如读写文件、网络通信、创建进程等等，都需要通过系统调用来请求内核帮忙。而strace就能把这些"请求"全部记录下来，包括调用了什么函数、传递了什么参数、返回了什么结果，甚至连耗时多长都能告诉你。我记得刚开始接触strace的时候，看到那密密麻麻的输出还挺懵的。但用熟了之后真的是爱不释手，很多看似复杂的问题都能通过它快速定位。基础使用方法最简单的用法就是直接跟踪一个程序的执行过程：strace ./my_program这样就能看到my_program运行过程中的所有系统调用了。不过输出信息会很多，刷屏速度特别快，建议把结果重定向到文件里：strace -o output.txt ./my_program这样所有的跟踪信息就保存到output.txt文件中了，可以慢慢分析。有时候我们不关心所有的系统调用，只想看特定的几种，比如只关心文件操作相关的调用：strace -e open,read,write ./my_program这个参数特别有用，能够过滤掉很多无关的噪音，让你专注于关心的部分。实战案例：排查文件访问问题我来分享一个真实的案例。之前有个应用启动特别慢，用户反馈说要等好几分钟才能正常使用。一开始我们怀疑是数据库连接的问题，各种调优数据库配置，结果没什么效果。后来想到用strace来看看启动过程中到底在干什么：strace -o startup.log -e trace=file ./myapp这里我用了trace=file参数，只关注文件相关的系统调用。分析日志后发现了问题所在：openat(AT_FDCWD, "/usr/share/fonts/font1.ttf", O_RDONLY) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/usr/share/fonts/font2.ttf", O_RDONLY) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/usr/share/fonts/font3.ttf", O_RDONLY) = -1 ENOENT (No such file or directory) ...原来程序在启动时要加载几百个字体文件，但这些文件在服务器上根本不存在！每次尝试打开文件都会失败，然后程序还会重试，导致启动时间被大大延长。找到根因后解决就简单了，要么安装对应的字体包，要么修改程序配置不加载这些字体。最终选择了后者，启动时间从几分钟缩短到了几秒钟。这个案例说明了strace的威力，有时候问题的根因可能完全超出你的想象。跟踪运行中的进程除了跟踪新启动的程序，strace还能attach到已经运行的进程上：strace -p <PID>这个功能在生产环境中特别有用。比如某个服务突然CPU使用率飙高，但又不能重启，这时候就可以用strace attach上去看看它在干什么。我遇到过一个Java应用突然CPU占用100%的情况，用strace attach上去后发现：futex(0x7f8a4c000b54, FUTEX_WAKE_PRIVATE, 1) = 0 futex(0x7f8a4c000b54, FUTEX_WAKE_PRIVATE, 1) = 0 futex(0x7f8a4c000b54, FUTEX_WAKE_PRIVATE, 1) = 0 ...大量的futex调用在疯狂执行，这通常表示线程间的锁竞争非常激烈。后来配合jstack等工具进一步分析，发现是代码中的一个死锁导致的。分析网络相关问题strace在排查网络问题时也很给力。比如程序连接数据库很慢，可以这样跟踪：strace -e trace=network ./myapp或者更具体一点：strace -e connect,send,recv ./myapp我曾经遇到过一个奇怪的问题，应用连接Redis总是超时。从监控看Redis服务器完全正常，网络也没问题。用strace跟踪后发现：socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3 connect(3, {sa_family=AF_INET, sin_port=htons(6379), sin_addr=inet_addr("192.168.1.100")}, 16) = -1 EINPROGRESS (Operation now in progress) poll([{fd=3, events=POLLOUT}], 1, 5000) = 0 (Timeout)发现程序确实在尝试连接Redis，但connect调用返回了EINPROGRESS，然后poll等待5秒钟后超时。这说明网络层面的连接建立就有问题。进一步排查发现是防火墙规则的问题，某个安全策略阻止了到Redis服务器的连接。修改防火墙规则后问题立即解决。性能分析技巧strace还有一个很实用的功能就是统计系统调用的耗时：strace -c ./myapp这会在程序结束后显示一个统计表格，告诉你哪些系统调用耗时最多、调用次数最频繁等信息。对于性能优化来说特别有价值。比如某次分析一个批处理程序时，发现write调用占用了大部分时间：% time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 89.23 2.456780 2457 1000 write 5.12 0.141023 141 1000 read 3.45 0.094856 95 1000 open 2.20 0.060541 61 1000 close这说明程序在I/O写入上花费了太多时间。进一步分析发现程序每次只写很少的数据，但调用次数很频繁。后来通过缓冲机制批量写入，性能提升了好几倍。过滤和定制输出实际使用中，strace的输出往往非常多，需要合理过滤才能找到有用信息。除了前面提到的-e参数，还有一些其他的过滤选项：只看失败的系统调用：strace -e trace=all -e status=failed ./myapp只看特定的信号：strace -e signal=all ./myapp显示时间戳：strace -t ./myapp显示相对时间：strace -r ./myapp这些选项可以组合使用，根据具体需求来定制输出格式。注意事项和最佳实践使用strace时有几个需要注意的地方：性能影响：strace会显著降低程序的执行速度，因为每个系统调用都需要被拦截和记录。在生产环境使用时要谨慎，最好在低峰期或者测试环境中使用。权限问题：要跟踪其他用户的进程，通常需要root权限。而且有些系统调用可能涉及敏感信息，使用时要注意安全性。输出量大：对于长时间运行的程序，strace的输出可能会非常庞大。建议使用-o参数将输出重定向到文件，并且合理使用过滤选项。我个人的经验是，刚开始使用strace时不要被复杂的输出吓到。多看几次就会发现规律，很多系统调用的含义其实很直观。比如open就是打开文件，read就是读取数据，connect就是建立连接等等。结合其他工具使用strace虽然强大，但单独使用有时候还不够。我通常会结合其他工具一起使用：配合ps、top查看进程状态配合netstat、ss查看网络连接配合lsof查看打开的文件配合tcpdump分析网络包这样能够从多个角度全面分析问题，提高排查效率。比如某次排查一个内存泄漏问题时，先用top发现内存使用率持续增长，然后用strace跟踪发现程序在不停地分配内存但很少释放，最后配合valgrind等内存分析工具定位到了具体的代码位置。实战演练我建议大家平时多练练手，可以用strace跟踪一些常见的命令看看它们的行为：strace ls /tmp strace cat /etc/passwd strace ping -c 3 baidu.com通过观察这些简单命令的系统调用，能够加深对Linux系统工作原理的理解。还可以写一些简单的测试程序，故意制造一些问题，然后用strace来排查。比如：#include <stdio.h> #include <unistd.h> int main() { FILE *fp = fopen("/nonexistent/file.txt", "r"); if (fp == NULL) { perror("fopen failed"); return 1; } fclose(fp); return 0; }编译运行这个程序，然后用strace跟踪：gcc test.c -o test strace ./test你会看到程序尝试打开一个不存在的文件，系统调用返回错误，这样就能直观地理解程序的执行流程。总的来说，strace是每个运维人员都应该掌握的重要工具。它能够帮助我们深入理解程序的行为，快速定位各种奇怪的问题。虽然刚开始可能觉得输出信息有点复杂，但只要多练习几次，很快就能上手。在实际工作中，我经常把strace和其他监控工具结合起来使用，形成一套完整的问题排查流程。遇到问题时不要慌，先用基础监控工具看看大概情况，然后用strace深入到系统调用层面分析，基本上大部分问题都能找到根因。记住，工具只是手段，关键还是要理解背后的原理。多实践、多思考，相信大家都能成为排查问题的高手！如果觉得这篇文章对你有帮助，欢迎转发分享给更多的小伙伴。我们一起在运维的道路上互相学习，共同进步！公众号：运维躬行录个人博客：躬行笔记
手把手教你落地网络入侵检测系统（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感兴趣，或者在使用过程中有什么问题，欢迎留言交流。咱们一起学习，一起进步！如果觉得这篇文章对你有帮助，记得点赞、在看、转发三连哦！你的支持是我继续分享的动力！公众号：运维躬行录个人博客：躬行笔记