PLG 可观测栈生产化:Prometheus/Loki/Grafana 上线前必须补的参数清单

PLG 能跑 ≠ 能生产,这 40 个参数决定它会不会炸 第 1 步:目标——别把"能跑"当成"能上线" 某开发者第一次搭 Prometheus + Loki + Grafana 的时候,docker-compose 一把梭,数据能出图、日志能搜到,觉得"这不就完了吗"。 直到有一天:磁盘写满、Loki 摄入速率爆了、Prometheus 查询超时、Grafana 裸奔在公网被扫——才意识到玩具和生产是两回事。 这篇把 PLG 生产化的参数和注意事项一次性讲透,分四块: 组件 生产化核心问题 Prometheus 数据存多久?查询会不会拖垮?挂了怎么办? Loki 日志会不会把磁盘写爆?摄入限额?标签会不会爆炸? Promtail 标签设计红线 + 采集可靠性 Grafana 认证、数据库、配置管理、备份 📌 定位:写给后端开发兼职运维的人——不追求架构极客,只求上线后别半夜被磁盘告警叫醒。 第 2 步:前置——先理解 PLG 各自的生产"命门" 四件套的生产风险完全不一样,先建立直觉: flowchart LR APP["应用/主机"] -->|"指标 scrape"| P["Prometheus时序数据库"] APP -->|"日志 push"| PT["Promtail日志采集"] PT -->|"HTTP push"| L["Loki日志存储"] P --> G["Grafana可视化"] L --> G classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#bfdbfe,font-weight:bold; classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb; classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold; class APP root; class P,L data; class PT,G process; 组件 生产命门 典型事故 Prometheus 内存(TSDB 缓存)、磁盘(时序数据)、查询并发 OOM、磁盘写满、查询拖垮 Loki 磁盘(日志量)、摄入速率、流数量(标签基数) 磁盘爆、摄入拒绝、流爆炸 Promtail 标签设计、位置文件、网络缓冲 流爆炸、重启重采日志 Grafana 认证、数据库、配置漂移 裸奔被入侵、配置丢失 第 3 步:Prometheus 生产参数——存储、查询、高可用 3.1 数据保留期(默认 15 天,必须显式设置) # docker-compose 启动参数 prometheus: command: - '--storage.tsdb.retention.time=30d' # 按时间保留(推荐) - '--storage.tsdb.retention.size=50GB' # 按大小保留(和上面二选一或都用) 为什么:默认 15 天,生产一般要 30 ~ 90 天。但保留期越长磁盘越大——50GB 磁盘 + 30 天保留,要提前算好每台机器的指标量(一般每 target 每小时几百 KB,几十个 target 一天约 1 ~ 2GB)。 ...

十一月 12, 2023 · 5 分钟 · 904 字 · yaomingye

微服务架构下 nginx 运维:后端开发如何借 AI 守住流量入口

nginx 不再是"服务器"了,它是你家微服务的门卫 第 1 步:目标——搞懂 nginx 在微服务里到底干嘛 先说个某开发者的真实转变。以前写单体应用,nginx 的活就是:把静态资源甩给浏览器,把动态请求转发给 Tomcat。配置嘛,抄一份改改就能跑。 后来上了微服务,Spring Cloud Gateway 成了统一入口,nginx 的位置就尴尬了——它既不用管静态资源,也不直接碰业务,但它卡在所有流量的最前面。 浏览器 ↓ nginx ← 我们这篇的主角:流量大门 ↓ Spring Cloud Gateway ← 路由/鉴权/限流 ↓ 微服务 A 微服务 B 微服务 C nginx 在这条链路上的职责变成了: 职责 说明 TLS 终结 HTTPS 证书在 nginx 上解掉,网关不用管证书 流量分发 把请求转发给 gateway(可能不止一个实例) 连接管理 复用客户端连接,减少网关压力 基础防护 隐藏版本号、限流、挡扫描器 日志入口 记录所有进站请求(网关日志不覆盖 nginx 这一层) 📌 关键认知:nginx 挂 = 整个系统挂。它前面没有别的挡箭牌,所以 nginx 的配置质量直接决定入口的稳定性。这也是为什么值得花一篇的篇幅讲清楚。 这篇的目标很实在:让你(或让 AI 代你)改 nginx 配置时,知道哪些是命门、哪些是锦上添花,别把入口配成瓶颈。 第 2 步:前置条件——先弄懂 nginx 的"体力"从哪来 动手配之前,必须理解 nginx 的工作模型。它跟 Tomcat 那种"一连接一线程"完全不同: ...

十一月 10, 2023 · 5 分钟 · 931 字 · yaomingye

给 1核2G 的阿里云 ECS 换上免费 HTTPS:从装证书到踩坑全记录

博客裸奔 HTTP 大半年,我给它上了个免费锁 第 1 步:目标——让博客地址栏出现小锁 事情是这样的。某开发者的博客在阿里云 1核2G 的小 ECS 上跑了大半年,一直用 http://yaocat.cloud 裸奔。也不是没想过上 HTTPS,但总觉得"麻烦"、“要花钱”、“反正没人看”。 直到有天朋友发来一个链接,浏览器地址栏赫然一个大红叉:“不安全”。虽然博客确实没什么人看,但顶着这个红叉自己心里也膈应。 查了一圈发现:HTTPS 现在完全免费,Let’s Encrypt 发的证书不要钱,还能自动续期。那还等什么,搞它。 目标拆一下: 事项 说明 ① 申请免费证书 Let’s Encrypt,用 acme.sh 工具 ② nginx 配置挂载 配置从容器里挪出来,重建不丢 ③ HTTPS 配置 443 端口 + HTTP 自动跳转 ④ 顺带优化 gzip 压缩 + 静态缓存 第 2 步:前置条件——这台机器长什么样 先交代一下环境: 项 值 服务器 阿里云 ECS,1核2G,Debian 11 (bullseye) 网站 Docker 容器跑 nginx:alpine,挂载 /var/www/blog 域名 yaocat.cloud,已解析到服务器公网 IP 端口 80 已开(HTTP 正常访问) 动手前先确认两件事能不能通: ...

十一月 8, 2023 · 4 分钟 · 681 字 · yaomingye

Prometheus 告警体系搭建:从 Alertmanager 到 AI ChatOps 一条龙

告警别只发通知,让 AI 替你干活 第 1 步:目标——从"看面板"到"手机响" 上一篇把 Prometheus + Grafana 搭起来,指标也采进来了。但有个问题:指标不会自己说话。 某开发者当时的状态是:白天盯着 Grafana 面板看 CPU 曲线,晚上睡觉心里发毛——万一凌晨 3 点服务挂了,谁叫我? 这篇文章的目标很直白: ❌ 旧世界:出问题 → 用户投诉 → 你爬起来开电脑 → SSH → 查日志 → 修复 ✅ 新世界:出问题 → 手机响 → 聊天框里说"查一下" → AI 排查完给你结论 具体拆成三个里程碑: 里程碑 内容 产出 ① 告警规则 Prometheus 检测 CPU/内存/磁盘异常 8 条可用的告警规则 ② Alertmanager 告警去重分组、统一出口 一个能收敛告警的中枢 ③ ChatOps 告警 → webhook → AI → Telegram 手机上收告警 + 动嘴指挥 📌 前置知识:假设你已经跑通了上一篇的 Prometheus + Grafana + node-exporter,知道 up 、 rate() 这些基本 PromQL。 ...

十一月 6, 2023 · 5 分钟 · 1062 字 · yaomingye

RocketMQ 实战避坑指南:从消息丢失到高可用,9大核心问题一网打尽

RocketMQ 避坑全攻略 0. 前言 0.1 为什么写这篇博客 消息中间件是分布式系统的必修课——这话没错,但很多团队引入 MQ 的时候只看到了"解耦"和"削峰填谷"的好处,却没意识到它同时带来了消息丢失、重复消费、积压、顺序错乱等一系列新问题。坦白说,踩过这些坑的开发者不在少数。 某开发者在生产环境第一次遇到 RocketMQ 积压十几万条消息的时候,第一反应是重启消费者——结果毫无悬念地失败了。后来花了一整天排查,问题竟然只是消费逻辑里多了一个 Thread.sleep(200) 。这种教训值得记下来。 0.2 读者需要的基础知识 用过 RocketMQ(至少本地跑过 Demo,知道 Producer / Consumer / Topic / Broker 是什么) 知道什么是生产者、消费者、Topic、Broker、NameServer 了解基本的分布式系统概念(如 CAP、最终一致性) 0.3 文章结构说明 按"问题 → 原因 → 原理 → 解决方案"的结构展开,每个问题独立成章。读者可以按需跳读,也可以从头串下来形成体系。 0.4 一句话总结 本文不是教"怎么用",而是教"怎么用好、怎么避坑"。 默认配置在生产环境就是定时炸弹。 1. 消息丢失 消息丢失是 MQ 使用中最致命的问题之一——订单丢了就是资损,通知丢了就是客诉。先按链路拆解一下丢消息的三个位置。 1.1 丢失场景分类 flowchart TD start([消息发送]) --> prod{生产端是否可靠?} prod -->|网络超时| prodLoss[生产端丢失] prod -->|异步未回调| prodLoss prod -->|重试耗尽| prodLoss prod -->|发送成功| broker[Broker 存储] broker --> persist{持久化策略?} persist -->|异步刷盘| brokerLoss[Broker 端丢失] persist -->|异步复制| brokerLoss persist -->|磁盘故障| brokerLoss persist -->|同步落盘| consume[消费端拉取] consume --> ack{ACK 策略?} ack -->|自动提交| consLoss[消费端丢失] ack -->|并发异常| consLoss ack -->|手动确认成功| done([消息可靠送达]) classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2.5px,color:#fce7f3,font-weight:bold; classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold; classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb; classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:2px,color:#fecaca,font-weight:bold; classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold; class start,done startEnd; class prod,persist,ack condition; class prodLoss,brokerLoss,consLoss reject; class broker,consume process; 1.1.1 生产端丢失 场景 原因 网络超时 客户端以为发送成功,实际 Broker 未收到。网络抖动 + 超时配置不合理导致 异步发送未回调 producer.send(msg) 后直接 return,异常被吞掉 重试耗尽 RetryTimesWhenSendFailed 次数用完,消息被丢弃 1.1.2 Broker 端丢失 场景 原因 异步刷盘 消息写入 PageCache 后返回成功,但尚未落盘,断电即丢 主从异步复制 Master 宕机时 Slave 未同步到最新数据 磁盘故障 物理损坏导致已落盘数据不可恢复 1.1.3 消费端丢失 场景 原因 自动提交 Offset consumeMessageBatchMaxSize 拉了一批消息,自动 ACK 后业务处理失败 并发消费异常 多线程中某条消息处理失败,但整体无法回滚 1.2 原理深度剖析 1.2.1 存储架构 RocketMQ 的存储核心是 CommitLog(顺序写文件)+ ConsumeQueue(按 Topic+Queue 建索引)+ IndexFile(按 Key 查询)。所有消息先追加到 CommitLog,再异步构建 ConsumeQueue 索引。 ...

十一月 4, 2023 · 6 分钟 · 1239 字 · yaomingye

日终对账系统设计:CSV 字段对照、两轮比对算法与数据库设计

对账系统的三张表、两轮比对和七种差异 对账系统是支付服务的最后一道防线——回调可能丢、消息可能漏、金额可能错,这些不会自己暴露。每天拿渠道的官方记录和自己数据库里的记录对一遍,丢钱多钱才能发现。 本文从零梳理一个日终对账系统的设计:数据表怎么建、两渠道 CSV 字段怎么映射、算法怎么做才能既快又不依赖 JOIN。 一、对账系统的心智模型 对账就一句话:渠道说每天收了多少钱,我们说每天收了多少钱,两边对一下,不一致就逐笔查。 flowchart TD cron["@Scheduled 次日 10:30"] --> download["下载渠道对账单 CSV"] download --> parse["解析 CSV → 批量写入 recon_temp"] parse --> total_check{"总额校验:渠道总额 = 我方总额?"} total_check -->|"相等"| done["对平 ✅ 关闭批次"] total_check -->|"不等"| detail["逐笔对比(HashMap 撮合)"] detail --> result["差异写入 recon_result"] classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2.5px,color:#fce7f3,font-weight:bold classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold class cron startEnd class download,parse,detail process class total_check condition class done data class result process 选择次日 10:30 触发是因为两家渠道的对账单都在次日 10 点前生成完毕——去早了拿不到文件。 ...

十一月 2, 2023 · 4 分钟 · 759 字 · yaomingye

微信支付 vs 支付宝:聚合支付渠道集成中的十一处暗坑

渠道集成的痛:微信和支付宝处处不一样 聚合支付系统要同时对接支付宝和微信支付。初看两边的官方文档,觉得差不多——都是「下单→拿凭证→前端拉起→回调通知」这个流程。真到写代码的时候才发现,每一步都不一样,甚至同是微信生态,App 和小程序之间还有差异。 这篇文章把两渠道在 SDK 选型、下单参数、签名机制、回调处理、退款流程、对账单格式六个环节的具体差异整理出来,顺便也记录微信 App 和小程序之间那几处让人想砸键盘的细节。 一、SDK 选型和核心对象 先看 SDK 本身的差异,这决定了后面所有代码怎么组织。 支付宝 微信支付 Maven artifact alipay-sdk-java:4.40.308.ALL wechatpay-java:0.2.17 核心对象 DefaultAlipayClient (就是 HTTP 客户端) RSAAutoCertificateConfig (配置 + 签名 + 证书管理) HTTP 层 自带老版 HttpClient 自带 OkHttp,可注入自定义实例 证书管理 无(公钥手动配) AutoCertificateService 自动下载平台证书 + 后台线程轮换 Service 封装 无,裸调 client.execute(request) AppService / JsapiService / RefundService 封装请求-响应对映 ⚠️ 新手提示:支付宝的 DefaultAlipayClient 就是个 HTTP 客户端,一行 new 就行。微信的 RSAAutoCertificateConfig 是重量级对象——创建时会初始化证书下载、后台轮询线程,要通过工厂 + Caffeine 缓存复用,别每次请求都 new。 两者最大的思维差异:支付宝把 SDK 当 HTTP 工具用,微信把 SDK 当基础设施用。 ...

十月 31, 2023 · 4 分钟 · 641 字 · yaomingye

支付服务那些坑:从先落库到分表预案,一笔钱背后的设计决策

支付服务:每个"为什么"背后都是真金白银 为什么支付服务和普通业务完全不一样 写业务代码,最常见的是 CRUD。增删改查写熟了,觉得什么服务都差不多——直到被分配写支付服务。 支付和普通业务有本质区别:普通业务操作的是信息,支付操作的是钱。信息写错了能改,钱出去了就是真金白银的损失。更扎心的是,支付服务里每一个看似"怎么做都行"的设计决策,背后都藏着一个"做错了会怎样"的财务事故。 这一篇不讲支付怎么接入第三方(那是另一篇的活),专门讲设计决策:先落库还是先调渠道、支付单和退款单要不要分开、前端该直连支付还是走订单、状态机怎么拆、将来分库分表怎么留预案。这些决策不写明白,代码写对了也是悬的——哪天线上出了对不上账的事故,回头看全是今天的"小事"。 设计决策 1:先落库支付单,还是先调渠道 prepay? 踩坑现场 第一次写支付创建接口的人,几乎都会纠结这个顺序。有人觉得"先调渠道拿参数,再落库,这样能确认渠道成功"——听着有道理,其实是财务黑洞的开端。 为什么必须"先落库、后调渠道" 插入 pay_order 失败?→ 直接抛异常终止,永不调渠道 插入 pay_order 成功?→ 调渠道 prepay → 拿拉起参数 → 返回前端 这笔顺序是强同步串行的,理由有三个: ① pay_order 是"我方要收这笔钱"的唯一凭证。必须先记账,再去碰第三方。渠道 prepay 失败、渠道宕机时,本地已有待支付单——可重试、可追溯、可对账。反过来,渠道调好了本地啥都没有,这笔支付意图就丢了。 ② 反向顺序是财务黑洞。先调渠道拿参数、再落库,万一落库失败(DB 故障、唯一键冲突、事务回滚),渠道侧已经有一笔预支付交易、本地没有单。用户真拿着参数付了款,渠道回调过来本地无单可匹配——用户钱付了,我方账上没收,直接资金风险。 ③ prepay 本身不扣款。 alipay.trade.app.pay 只是"下单拿拉起参数",用户还没付款。所以先落库后调渠道,即使渠道失败也没有资金损失,重试即可。 顺带一个容易踩的长事务坑 创建接口标了 @Transactional ,prepay 这个外部 HTTP 调用被包进了本地事务——prepay 慢(外部网络)会长时间占用数据库连接,高并发下单时是隐患。严谨做法是把 prepay 移出事务: 事务 A:insert pay_order(本地,快) 无事务:调渠道 prepay(外部,慢) 事务 B:prepay 失败则更新 pay_order 状态 顺序本身不变,但别让外部调用拖住数据库事务。 设计决策 2:支付单和退款单,为什么分成两张表? 踩坑现场 看表结构时容易嘀咕:支付单和退款单字段挺像的——都有金额、订单号、渠道、用户、时间。为什么不合成一张表,用个"方向"字段区分? 为什么必须分开 ① 一对多是硬约束。一笔支付可以多次退款:买 399 退一件 99,再退一件 100——支付单只有一笔(399),退款单有两笔(99+100)。退款独立成表才能记录多次退款历史,塞进支付单就毁了。 ② 状态机本质不同。支付单管"收钱":待支付 → 已支付 → 关闭/失败;退款单管"退钱":待处理 → 处理中 → 成功/失败 + 审核流。两个状态机混在一张表必然打架。 ...

十月 29, 2023 · 5 分钟 · 858 字 · yaomingye

支付系统设计评审:一次增量讨论补足的五个缺陷

一份支付设计方案,是怎么被审出五个洞的 某开发者在设计一套统一支付服务。初版方案自认为考虑周全:支付订单表、退款表、渠道配置、对账批次,表格一张比一张漂亮。然后跟组里人过了一遍设计,被一个问题接一个问题地问到改稿——每一问都补出一个之前没想透的缺陷。 这篇就把这次增量讨论里补上的五个点记下来。它们不是"支付系统特有的冷知识",而是任何一个会分库分表、会对账、要复用的系统都可能踩的坑。 场景:一个要复用的统一支付服务 先交代背景。这套支付服务的目标是: 从"模拟支付"改造成真实支付——聚合支付宝、微信支付、微信小程序支付,未来还可能接更多国内渠道 拥有自己的数据库(此前完全没有,支付数据存在别处) 带一套每日对账系统——拉取渠道对账单、解析、批量入库、逐笔对比、算差异 日后作为独立支付服务接入其他项目(商城只是第一个业务方) 正是"要复用"和"要对账"这两点,引出了下面五个洞。 洞一:对账的逐笔对比写了 JOIN 初版对账设计里,逐笔对比是这么写的: -- 渠道账单临时表 LEFT JOIN 本地支付订单表 SELECT t.trade_no, t.amount, o.pay_amount, ... FROM recon_temp t LEFT JOIN pay_order o ON t.trade_no = o.merchant_order_no; 乍看没毛病——两张表同库、有公共键。但评审的人问了一句:"pay_order 分库分表之后,这个 JOIN 还能跑吗?" 不能。跨表 JOIN 需要分片键能路由到同一分片,而 recon_temp 和 pay_order 的分片键不同(一个按批次、一个按用户),JOIN 会退化成全分片广播扫描——每一个分片都扫一遍再合并,数据量一大就是灾难,更别说分片规则一变直接报错。 📌 前置知识:分库分表后,跨表 JOIN 要保证两张表的行落在同一个分片(同分片键)才能路由;分片键不一致时只能广播到所有分片再内存合并。 改法:双批查询 + 内存撮合。 flowchart TD A["渠道对账单文件"] -->|"解析"| B["recon_temp 当日明细"] C["pay_order 支付订单表"] -->|"按渠道×时间窗查询"| D["当日支付单子集"] B -->|"批查加载"| E["渠道侧内存 Map"] D -->|"批查加载"| F["平台侧内存 Map"] E -->|"按 merchant_order_no 匹配"| G["逐笔撮合"] F -->|"按 merchant_order_no 匹配"| G G --> H["命中 / 仅渠道 / 仅平台"] H --> I["差异写 recon_result"] classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb; classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold; classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold; class A,D process class B,C,F data class G,E condition class H,I process 两个单表查询各自按自己的分片键路由( recon_temp 按 batch_no 、 pay_order 按 user_id ),各自只取撮合需要的列,在内存里建 HashMap 按 merchant_order_no 精确匹配。全程无跨表 JOIN——这就是"无联表"原则,也是后续分库分表的前提条件。 ...

十月 27, 2023 · 3 分钟 · 621 字 · yaomingye

数据库金额字段该用 decimal 还是 bigint:从历史惯例到现代支付栈的选型之路

钱的字段到底该用 decimal 还是 bigint 某开发者最近在设计一套统一支付服务,走到金额字段这一步,跟数据库里的老订单表吵了一架:新表想用 bigint 存"分",老表是 decimal(10,2)。写代码前先把这个历史遗留问题捋清楚,发现这背后是一整段软件史。 数据库里的金额字段,可能是除了主键之外被争论最多的一种类型。打开任何一本数据库教材,都会看到一句名言——“钱的字段千万别用 float”。但这句话的下半句往往没人讲:不用 float,那到底用 decimal 还是 bigint? 教科书里写的是 decimal。现代支付 API 的契约里写的是"整数最小单位"——也就是 bigint 存分。两边都合理,为什么结论会分叉? 从一次选型冲突说起 设计支付服务时,金额字段出现了两个候选人: ** decimal(10,2) ** —— 存的就是 100.00 ,肉眼可读 ** bigint ** —— 存 10000 ,单位是分,代码里到处都是 ÷100 老 ERP 系统的订单表选了前者,支付服务想选后者。这不是口味问题,是两个时代的设计碰撞。要理解它,得先从 float 为什么被禁说起——因为 float 才是那个真正不配碰钱的类型。 📌 前置知识:浮点数、定点数、IEEE 754 这三个概念是本文的地基,建议先有个印象再往下看。 float 的罪与罚:二进制算不清十进制 先复现那个经典翻车现场: SELECT 0.1 + 0.2; 结果是 0.30000000000000004 。 float / double 用二进制科学计数法存储: M × 2^E ,M 是尾数,E 是指数。但十进制小数 0.1 转成二进制是无限循环小数: ...

十月 25, 2023 · 3 分钟 · 631 字 · yaomingye
Cat Radio