tmux 终端复用实战:SSH 断了会话不丢,一个连接管理所有终端

一个 SSH 连接,管所有终端 说一个真实的痛点。远程 SSH 到服务器后用 k9s 看集群,发现两件事很烦:k9s 启动要等一两秒,而且它会把我的 Ctrl+B 截断——Ctrl+B 本来是我切换 SSH 终端的快捷键,进了 k9s 就失灵;想切终端只能新开一个 SSH 连接,重新认证、重新进目录、重新找上下文,来回切几次心态就崩了。 tmux 就是来解决这类问题的标准工具(运维和开发都推荐):一个 SSH 连接里开多个终端窗口,会话持久保存——SSH 断了,会话还在。这篇记录安装、核心概念、常用操作,以及我在服务器上的真实演示。 1. 动机先行:三个痛点,一个回应 痛点 没有 tmux 时 有 tmux 后 多终端切换 新开 SSH 连接(重新认证、丢上下文) 一个连接内 Ctrl+B 切窗口 快捷键冲突 k9s 等全屏工具截断 Ctrl+B k9s 放进 tmux 窗口,切换归 tmux 管 SSH 断线 正在跑的任务中断、上下文全丢 会话在服务器上继续跑,重连 attach 恢复 第三点是 tmux 最被低估的价值:tmux 的会话属于服务器上的 tmux 进程,不属于你的 SSH 连接——SSH 断了、电脑合盖了、网络抖了,会话照跑不误,重连回来 tmux attach 一切如初。 2. 安装 apt-get install -y tmux tmux -V # tmux 3.5a (Debian 源里就有, 一行搞定) 3. 原理:会话 / 窗口 / 窗格三件套 tmux 的层级关系一句话讲清: ...

二月 24, 2024 · 4 分钟 · 752 字 · yaomingye

内网穿透实战:SSH 反向隧道让外网随时随地访问家里的服务器

一条隧道,把 NAT 后的服务器接到公网 家里有台 debian 服务器,跑着 mihomo、Docker、kind 集群,人在公司或外地的时候想连上去干活——但它在家庭局域网后面,没有公网 IP,外面根本摸不到它。某开发者的解法:让它自己主动"爬"出去,在唯一有公网 IP 的阿里云 ECS 上挂一个隧道入口。从此不管在哪,一条命令直达家里的服务器,而且安全到脚本小子无从下手。 这篇文章完整记录这个方案:先讲透原理(NAT 为什么挡人、反向隧道为什么能钻出去),再对比工具选型,然后给出全部配置过程和真实踩过的三个坑。 这次要做什么 目标:通过公网 ECS 中转,实现在任意网络 SSH 访问位于家庭 NAT 后的 debian 服务器 产出:ssh debian-lan 一条命令直达;局域网内原有直连不受影响 安全:至少挡住脚本小子——不暴露额外公网端口、隧道账号无 shell、全链路密钥认证 原理:NAT 挡住了什么,隧道就钻什么 第一步:理解 NAT 的"单向门" NAT(Network Address Translation,网络地址转换)让内网设备共享一个公网出口,但它是一扇单向门:内网设备主动出站,路由器放行并记住映射;公网侧想主动连进来,路由器没有对应记录,直接丢弃。 flowchart TD subgraph WAI["公网侧"] U["笔记本\n(任意网络)"] S["ECS 公网 IP"] end subgraph LAN["家庭局域网 (NAT 后)"] D["debian 服务器\n192.168.x.x"] end U -->|"① 出站可达"| S S -.->|"② 入站被 NAT 丢弃"| D D -->|"③ 出站可达"| S style S fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#ffffff,font-weight:bold style U fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff style D fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff 图中 ② 那条虚线就是死路:别人永远无法主动找到你。但注意 ① 和 ③——出站永远是通的。这就是全部突破口。 ...

十一月 16, 2023 · 4 分钟 · 735 字 · yaomingye

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

给 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

日终对账系统设计: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

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

支付服务:每个"为什么"背后都是真金白银 为什么支付服务和普通业务完全不一样 写业务代码,最常见的是 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

从语法迷雾到内存物理本质:C 指针 / Java 引用 / Go 接口 / C++ 类的底层统一模型

语法迷雾之下,物理内存里只有「数值」与「地址」 一、导言:被抽象概念包围的困惑 某开发者学了三年 Java,突然被扔去写 C——struct 是什么? *p 又是什么? void * 是什么东西?回头再看 Go,interface 怎么不用 implements ?再翻翻 C++ 源码,一个 class 里又是 virtual 又是 this——每个语言都有一套自己的"数据创造论",把内存的本质层层包裹起来。 Java 说「一切皆对象」,C 说「一切皆指针」,Go 说「用组合不要继承」,C++ 说「我全都要」。刚入门的读者站在这些口号中间,CPU 和 RAM 到底是怎么看待这些概念的? 破局点只有一句话:CPU 和 RAM 根本不懂面向对象。物理内存里永远只有两样东西——「数值」与「地址」。 int 是数值,指针是地址,对象的字段是数值和地址的排列组合,接口变量是两个地址凑一对。所有语言的语法特性,最终在内存里都还原为这个二元模型。 flowchart LR subgraph APP["应用层"] JAVA["Java: 对象/引用"] CPP["C++: class/虚表"] GO["Go: struct/interface"] C["C: struct/指针"] end subgraph COMPILER["编译器/Runtime"] LANG["语法糖脱糖"] LAYOUT["内存布局计算"] VTABLE["虚函数表生成"] end subgraph HARDWARE["物理层"] VAL["数值"] ADDR["地址"] end JAVA & CPP & GO & C -->|"编译/解释"| COMPILER COMPILER -->|"最终形态"| HARDWARE classDef appStyle fill:#2d2522,stroke:#ea580c,stroke-width:2px,color:#f8fafc; classDef compStyle fill:#1e293b,stroke:#0284c7,stroke-width:2px,color:#f8fafc; classDef hwStyle fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0; class JAVA,CPP,GO,C appStyle; class COMPILER compStyle; class VAL,ADDR hwStyle; 这篇文章的任务:把 C、C++、Java、Go 的语法糖一颗颗剥开,露出底下那个统一的物理内存画卷。 ...

十月 17, 2023 · 6 分钟 · 1191 字 · yaomingye

内存管理的破局者:虚拟地址、物理地址与页表——从哲学追问到公式推导

虚拟地址、物理地址与页表:从懵圈到通透只差这篇文章 一、起源:一个看似简单的哲学追问 1.1 初始疑惑:虚拟地址和物理地址一样,也是面向 CPU 的编号吗? 一个常见的困惑:虚拟地址(Virtual Address,VA,程序看到的地址)和物理地址(Physical Address,PA,硬件真正使用的地址)都是数字,那它们到底有什么区别?某位开发者曾盯着 printf("%p", ptr) 打印出的十六进制数,天真地以为这个数字就是内存条上的物理位置——直到他发现进程的虚拟内存空间比物理 RAM 还大,才意识到事情没那么简单。 核心区别在于它们服务的 CPU 阶段不同: VA(虚拟地址):面向程序与 MMU(Memory Management Unit,内存管理单元)。CPU 在执行 LOAD 指令时,发出的第一个请求就是 VA。程序中的每一个指针、每一个变量地址,都是在 VA 空间中定义的。 PA(物理地址):面向 RAM 与内存控制器。经过 MMU 翻译之后,VA 变成 PA,才是内存条上真正的硬件编号。内存控制器只认 PA,它不关心也不理解虚拟化这件事。 来看一段最直观的 C 代码: #include <stdio.h> #include <stdlib.h> int main() { int x = 42; int *ptr = &x; // ptr 的值是一个虚拟地址,不是物理地址 printf("变量 x 的虚拟地址: %p\n", (void *)ptr); printf("变量 x 的值: %d\n", *ptr); return 0; } ptr 里存储的地址是 VA。当 printf 读取 *ptr 时,CPU 发出 LOAD [ptr] 指令,硬件自动经过 MMU 翻译为 PA 之后才去 RAM 中取值。整个过程对程序员透明——这也是为什么很多人长期把 VA 误当作「内存的真实坐标」。 ...

十月 15, 2023 · 7 分钟 · 1369 字 · yaomingye
Cat Radio