<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>系统设计 on 张欣耕的个人博客</title><description>Recent content in 系统设计 on 张欣耕的个人博客</description><link>https://shanechang.com/zh-cn/tags/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/</link><language>zh-cn</language><lastBuildDate>Sat, 22 Nov 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://shanechang.com/zh-cn/tags/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/index.xml" rel="self" type="application/rss+xml"/><item><title>无状态API与有状态数据库的分界线</title><link>https://shanechang.com/zh-cn/p/backend-boundaries/</link><guid isPermaLink="true">https://shanechang.com/zh-cn/p/backend-boundaries/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.BLbO8Wh-_Z1JfbR6.webp&quot; alt=&quot;Featured image of post 无状态API与有状态数据库的分界线&quot; /&gt;&lt;p&gt;曾经的我, 把数据库当成了魔法盒子。&lt;/p&gt;
&lt;p&gt;教程都千篇一律: 先创建个 engine, 再搞个 session factory, 然后写个依赖把 session yield 出去。用起来没毛病, 于是我就到处复制粘贴。但为啥要这么做?其实我也没搞明白。&lt;/p&gt;
&lt;p&gt;直到有一天, 轮到我写后台任务 (background worker), 这些套路突然就”不灵光”了。我盯着自己的代码, 脑袋一片浆糊:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为啥数据库 engine 要一直留在内存里不放?&lt;/li&gt;
&lt;li&gt;为啥每个请求都得新建 session?&lt;/li&gt;
&lt;li&gt;既然 API 要”无状态”, 我们还保存啥东西啊?&lt;/li&gt;
&lt;li&gt;S3 算无状态还是有状态?Redis 又算啥?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总感觉哪儿不对劲。&lt;/p&gt;
&lt;h2 id=&quot;那个挥之不去的矛盾&quot;&gt;那个挥之不去的矛盾&lt;/h2&gt;
&lt;p&gt;大家都说: “API 就得无状态!”&lt;/p&gt;
&lt;p&gt;但现实中的后端, 总有点啥东西是要保留住的:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据库 engine 一跑就常驻内存&lt;/li&gt;
&lt;li&gt;Session factory 藏 app 状态里&lt;/li&gt;
&lt;li&gt;S3 客户端塞在 context 里&lt;/li&gt;
&lt;li&gt;Redis 连接全局共享&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这到底是无状态, 还是有状态?&lt;/p&gt;
&lt;p&gt;很长一段时间, 我的脑子就把这归为: “反正大家都这么写。” 其实就是: “虽然我没懂, 但用了也没出啥大问题, 先不管。“&lt;/p&gt;
&lt;h2 id=&quot;一张图-理清一切&quot;&gt;一张图, 理清一切&lt;/h2&gt;
&lt;p&gt;后来, 我终于找到了让一切豁然开朗的心智模型。&lt;/p&gt;
&lt;p&gt;想象一下, 你的系统被一条横线切成两半:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;横线以上&lt;/strong&gt;: 请求处理器和后台任务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表现得很”无状态”&lt;/li&gt;
&lt;li&gt;每次请求都是全新的一轮工作&lt;/li&gt;
&lt;li&gt;不会偷偷带着上次的数据过来&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;横线以下&lt;/strong&gt;: 数据持久化世界&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Postgres 的表和行&lt;/li&gt;
&lt;li&gt;S3 的桶和对象&lt;/li&gt;
&lt;li&gt;Redis 的键值对&lt;/li&gt;
&lt;li&gt;这些地方, 才是真正的”状态长存”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;横线上&lt;/strong&gt;: 连接两个世界的”桥梁”——客户端对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据库 engine 和它的连接池&lt;/li&gt;
&lt;li&gt;S3 客户端以及配置&lt;/li&gt;
&lt;li&gt;Redis 客户端和连接管理&lt;/li&gt;
&lt;li&gt;它们是你无状态代码和有状态世界之间的桥梁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一旦按这张图理解, 所有迷雾都消散了。&lt;/p&gt;
&lt;p&gt;你的代码并没有”耍赖”, 只是合理地维护了访问有状态世界的通道。&lt;/p&gt;
&lt;h2 id=&quot;为什么数据库看起来格外隆重&quot;&gt;为什么数据库看起来”格外隆重”?&lt;/h2&gt;
&lt;p&gt;说实话, 数据库的”仪式感”明显比 S3, Redis 这种服务高多了。这事儿其实有原因。&lt;/p&gt;
&lt;h3 id=&quot;连接池-绝对不能马虎&quot;&gt;连接池, 绝对不能马虎&lt;/h3&gt;
&lt;p&gt;把数据库想象成一家只有 100 个餐桌的餐厅。&lt;/p&gt;
&lt;p&gt;如果每个顾客 (请求) 都自己扛一张桌子进来还不带走, 分分钟桌子堆满餐厅。更惨的是, 20 个服务员 (worker), 每人每秒再带 10 张桌子……餐厅直接变桌子批发市场。&lt;/p&gt;
&lt;p&gt;正确姿势是这样的:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;餐厅自带一波桌子 (连接池/engine)&lt;/li&gt;
&lt;li&gt;顾客吃完就撤, 桌子轮着用&lt;/li&gt;
&lt;li&gt;下一个顾客直接坐现成的桌子&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码里也是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个进程一个 engine, 内部管理连接池&lt;/li&gt;
&lt;li&gt;每个请求从池子里借一根连接&lt;/li&gt;
&lt;li&gt;请求完活儿, 连接还回池子&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这不是写法风格, 这是”活命法则”。&lt;/p&gt;
&lt;h3 id=&quot;事务-得有自己的小黑屋&quot;&gt;事务, 得有自己的”小黑屋”&lt;/h3&gt;
&lt;p&gt;数据库的 session 不只是连接, 更是”事务边界”。&lt;/p&gt;
&lt;p&gt;想象 session 是你专用的小本本, 想改啥就先写上。最后决定”提交”, 就把本本交上去;要是”反悔”, 直接撕掉。&lt;/p&gt;
&lt;p&gt;如果把本本给所有人轮流写, A 以为刚下好订单, B 又觉得刚取消了, 最后数据库懵圈: 你们到底想干嘛?&lt;/p&gt;
&lt;p&gt;所以 session 必须一请求一份, 专人专用, 开始到结束都干净利落。&lt;/p&gt;
&lt;h2 id=&quot;s3-又是个啥角色&quot;&gt;S3 又是个啥角色?&lt;/h2&gt;
&lt;p&gt;S3 的存在感有点特殊, 和数据库不太一样。&lt;/p&gt;
&lt;p&gt;S3 也保存数据啊, 今天传个文件, 明天还能下回来, 妥妥的有状态。&lt;/p&gt;
&lt;p&gt;但关键区别在于: 你和 S3 是通过 HTTP 打交道的。每次上传, 下载, 都是独立的一锤子买卖。没有”事务”什么的, 没有哪个本本帮你记着还没提交的改动。&lt;/p&gt;
&lt;p&gt;拿个生活例子:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据库&lt;/strong&gt;: 你进档案室, 手里拿着本子修修改改, 写完决定存档还是作废。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;S3&lt;/strong&gt;: 你寄快递, 每次都是独立一件, 快递公司不会帮你记着上次的内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然 S3 也有”状态”, 但数据库因为连接和事务的存在, 需要更多的”仪式感”。&lt;/p&gt;
&lt;h2 id=&quot;无状态到底是啥意思&quot;&gt;“无状态”到底是啥意思?&lt;/h2&gt;
&lt;p&gt;谜底揭晓时刻到了。&lt;/p&gt;
&lt;p&gt;大家说”无状态 API”, 其实不是说你啥都不能放内存里。&lt;/p&gt;
&lt;p&gt;真正的意思是: &lt;strong&gt;每个请求都不依赖于上一个请求的”隐形”状态。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也就是说:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;❌ 别让数据库 session 跨请求存活&lt;/li&gt;
&lt;li&gt;❌ 别把”当前用户”写进全局变量&lt;/li&gt;
&lt;li&gt;❌ 别靠某个会变的全局对象来传递数据&lt;/li&gt;
&lt;li&gt;✅ 可以共享 engine, 客户端, 配置等基础设施&lt;/li&gt;
&lt;li&gt;✅ 每个请求新建 session 或 context&lt;/li&gt;
&lt;li&gt;✅ 状态的读写交给数据库, 缓存, 存储等外部系统&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;共享对象只是”工具”, 真正的业务状态应该留在分界线下。&lt;/p&gt;
&lt;h2 id=&quot;背景任务里-分界线最关键&quot;&gt;背景任务里, 分界线最关键&lt;/h2&gt;
&lt;p&gt;这些理论, 等你写后台 worker 的时候才会真切体会。&lt;/p&gt;
&lt;p&gt;用 Web 框架时, 框架会帮你把 session 的创建和销毁都打点得妥妥的。你只管写业务逻辑就行。&lt;/p&gt;
&lt;p&gt;但一旦到后台 worker, 轮到你自己决定:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;什么东西是整个 worker 共享的?&lt;/li&gt;
&lt;li&gt;什么东西是每个任务独立新建的?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是分界线的选择题。&lt;/p&gt;
&lt;p&gt;一个干净的套路如下:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# worker 初始化时 (长生命周期)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;context[&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;db_session_factory&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; SessionFactory(engine)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;context[&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;s3_client&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; S3Client(config)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 每个任务中 (短生命周期)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; process_document&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(context, document_id):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    session &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; context[&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;db_session_factory&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;].create()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    s3 &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; context[&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;s3_client&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;    # 用全新 session 干活&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;    # session 自己管理事务&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;    # 用完就丢&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工厂和客户端常驻边界, session 和业务逻辑则在分界线上方, 各干各的井水不犯河水。&lt;/p&gt;
&lt;h2 id=&quot;为什么这个心智模型很重要&quot;&gt;为什么这个心智模型很重要?&lt;/h2&gt;
&lt;p&gt;说实话, 没碰过坑之前我也是”教程怎么写我就怎么抄”。&lt;/p&gt;
&lt;p&gt;FastAPI 教程说: “这样写。“我就跟着写。Litestar 文档说: “放这儿。“我就塞这儿。&lt;/p&gt;
&lt;p&gt;但一旦你开始:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多 worker 协同&lt;/li&gt;
&lt;li&gt;写后台任务&lt;/li&gt;
&lt;li&gt;混用数据库, S3, Redis, 消息队列&lt;/li&gt;
&lt;li&gt;调试奇怪的连接问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这时候, 复制粘贴就救不了你。你得明白&lt;strong&gt;为啥&lt;/strong&gt;要这么设计。&lt;/p&gt;
&lt;p&gt;对我来说, 真正的转折点是看清了这条分界线:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;分界线下&lt;/strong&gt;: 持久化系统 (Postgres, S3, Redis), 业务状态的老巢&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分界线上&lt;/strong&gt;: 客户端对象 (engine, session factory, S3 client), 桥梁&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分界线上方&lt;/strong&gt;: 无状态 (ish) 请求处理器, 只管干活儿&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一旦你脑子里有了这张图, 很多”神秘套路”其实都是有理有据的设计选择。&lt;/p&gt;
&lt;p&gt;你不是因为 Python 的什么 bug 才一直留着 engine, 而是因为连接池很贵, 只能共享。&lt;/p&gt;
&lt;p&gt;你不是因为教程让你每次新建 session 就照做, 而是因为事务必须有干净的边界, 连接池也需要反复利用连接。&lt;/p&gt;
&lt;p&gt;你不是在”自相矛盾”地把 S3 client 共享, 数据库 session 独立, 而是认清了不同系统的交互模式各不相同。&lt;/p&gt;
&lt;h2 id=&quot;总结一下-tldr&quot;&gt;总结一下 (TL;DR)&lt;/h2&gt;
&lt;p&gt;如果你以后忘了细节, 只想回来看核心观点:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;无状态不是”啥都不能共享”, 而是”每个请求都不依赖以前的隐形状态”。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分界线心智图:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;下方&lt;/strong&gt;: 持久化系统 (数据库, S3, Redis)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线上&lt;/strong&gt;: 客户端基础设施 (engine, session factory, client)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上方&lt;/strong&gt;: 每请求独立的业务逻辑 (session, service, handler)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;数据库之所以显得特殊, 是因为:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;连接池有硬性上限&lt;/li&gt;
&lt;li&gt;事务要求干净的边界&lt;/li&gt;
&lt;li&gt;session 记录着变更, 直到 commit/rollback&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实践中:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;共享: engine, 客户端, 配置, 工厂&lt;/li&gt;
&lt;li&gt;独立新建: session, 请求上下文, 事务边界&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以后再看到 &lt;code&gt;engine&lt;/code&gt;, &lt;code&gt;sessionmaker&lt;/code&gt;, &lt;code&gt;Session&lt;/code&gt; 或 &lt;code&gt;s3_client&lt;/code&gt;, 你就知道它们该在架构的哪一层, 为什么要那样设计。&lt;/p&gt;
&lt;p&gt;这些不是魔法套路, 而是你有意识地划清了你和有状态世界之间的那一道线。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;(本文由人类撰写, 部分内容借助 AI 优化。)&lt;/p&gt;</description><pubDate>Sat, 22 Nov 2025 00:00:00 GMT</pubDate></item><item><title>为什么AI无法成为工程师</title><link>https://shanechang.com/zh-cn/p/why-ai-cannot-engineer/</link><guid isPermaLink="true">https://shanechang.com/zh-cn/p/why-ai-cannot-engineer/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.CTct8cc3_1GfOkr.webp&quot; alt=&quot;Featured image of post 为什么AI无法成为工程师&quot; /&gt;&lt;p&gt;上个月, 我让Claude帮我撸了一个客户后台。十分钟后, 漂亮的React组件摆在我面前, API结构清晰, 登录验证在我本地跑得飞快。那一刻我差点以为自己是哈利波特。&lt;/p&gt;
&lt;p&gt;两天后, 我们把这玩意儿给真实用户用, 灾难现场就上演了: API在并发下直接跪了, 登录令牌一半时间在用户操作中间就过期, 数据库查询在我测试数据只有10条时还很快, 真数据一上来, 30秒都查不出来。&lt;/p&gt;
&lt;p&gt;代码美得像艺术品。用?也能用——勉强算是。但本质上, 这就是定时炸弹, 我只能怪自己。需要建筑师的时候, 我却只找了个瓦匠。&lt;/p&gt;
&lt;h2 id=&quot;看起来像摩天大楼-没人敢住&quot;&gt;看起来像摩天大楼, 没人敢住&lt;/h2&gt;
&lt;p&gt;想象一下建一栋摩天大楼。你得有两波人:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;建筑师和结构工程师&lt;/strong&gt;负责设计。他们要琢磨承重墙放哪, 电线水管怎么绕, 什么材料能抗住高空风, 地震来了楼会不会晃歪。还得考虑消防通道, 电梯井, 甚至人怎么进出都要细细盘算。画一大堆图纸, 标一堆标准。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;施工队&lt;/strong&gt;才是真正搬砖的。他们会搅拌混凝土, 焊钢筋, 装玻璃窗。只要给他们详细蓝图, “这儿用几号钢筋, 间距6英寸, 混凝土48小时别动”, 他们就能盖出结实安全的楼。&lt;/p&gt;
&lt;p&gt;有意思的是, 施工队也懂点门道: 知道钢筋混凝土才牢靠, 承重墙不能乱打洞, 材料遇热会膨胀。虽然不会算具体受力, 但知道哪些事千万不能做。&lt;/p&gt;
&lt;p&gt;所以, 理论上, 如果你让施工队不用图纸直接干, 他们也能盖出个楼来: 墙能立起来, 水电能接, 屋顶也有。&lt;/p&gt;
&lt;p&gt;能撑一阵。&lt;/p&gt;
&lt;p&gt;但真有人住进去, 地震来了, 着火了, 你才发现——电路乱成一锅粥, 水压上不去, 消防门找不到, 风一吹楼就晃。外表像楼, 其实不结实, 不安全, 没法住, 是个”高价装修的危楼”。&lt;/p&gt;
&lt;p&gt;你真的需要工程师和建筑师。&lt;/p&gt;
&lt;h2 id=&quot;我们把两份工混成了一份&quot;&gt;我们把两份工混成了一份&lt;/h2&gt;
&lt;p&gt;回到软件圈。其实我们也有两种角色, 但现在大家已经傻傻分不清。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;软件工程师&lt;/strong&gt;应该主导系统设计, 琢磨扩展性, 可靠性, 安全性, 可维护性。定规范, 立规矩, 想各种情况: 一万用户同时访问会咋样?部分功能挂了怎么办?数据一致性怎么保证?凌晨3点出bug怎么排查?这些人给出”蓝图”, 决定你的代码到底能不能上线, 能不能扛住大场面。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;程序员&lt;/strong&gt;则负责写代码。他们擅长语法, 算法, 设计模式, 能把需求转成具体实现。只要有好规范和架构, 他们能写出高质量, 维护方便的代码。&lt;/p&gt;
&lt;p&gt;但问题来了: 工程师也得写代码啊!不会写代码的建筑师, 设计出来的楼根本站不住。同理, 不懂代码运行原理的软件工程师, 也架不住”代码打脸”。&lt;/p&gt;
&lt;p&gt;于是, 两份工混成了一份: 大多数公司说”我们招软件工程师”, 其实是招”设计+实现一肩挑”的全能型选手。大家脑袋里画蓝图, 手上撸代码。这么搞了好多年, 竟然还行。&lt;/p&gt;
&lt;h2 id=&quot;欢迎ai程序员闪亮登场&quot;&gt;欢迎AI程序员闪亮登场&lt;/h2&gt;
&lt;p&gt;现在, 大语言模型 (LLM) 能写代码了。我的感受是: &lt;strong&gt;LLM是顶级程序员, 但完全不是工程师。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LLM像那位啥都能盖的施工大哥, 你说”给我做个Todo List”, 它立马给你写好。代码优雅, 结构清晰, 变量名感人, 跑起来贼顺溜。&lt;/p&gt;
&lt;p&gt;但工程的部分全没了:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不考虑负载&lt;/strong&gt;: 用内存数组存Todo, 测着快, 用户一多直接爆炸&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没处理异常&lt;/strong&gt;: 数据库断了咋办?代码八成直接挂&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全感人&lt;/strong&gt;: 有登录验证, 但防不住常见攻击, SQL注入一查一个准&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能盲点&lt;/strong&gt;: 每次都遍历所有用户的todo?等着内存爆炸吧&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运维无视&lt;/strong&gt;: 日志, 监控几乎没有, 出事了只能靠烧香&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码看起来像模像样, 规范也全对, 理想情况下能跑。但一旦遇到真实世界的幺蛾子——并发, 网络断, 奇葩输入, 恶意攻击——它就跟那座没人敢住的摩天楼一样, 外表光鲜, 里头一团糟。&lt;/p&gt;
&lt;h2 id=&quot;这事儿到底意味着啥&quot;&gt;这事儿到底意味着啥&lt;/h2&gt;
&lt;p&gt;以前我觉得”工程师”和”程序员”的区分纯粹是咬文嚼字。现在我明白了, 这区别比你想的要命。&lt;/p&gt;
&lt;p&gt;你让LLM”写个认证系统”, 其实就是让施工队”盖个楼”, 却没给任何要求。比如:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;楼里住几千人还是一户人家?&lt;/li&gt;
&lt;li&gt;抗不抗地震, 火灾?&lt;/li&gt;
&lt;li&gt;消防标准咋定?&lt;/li&gt;
&lt;li&gt;水电怎么布?&lt;/li&gt;
&lt;li&gt;预算多少?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LLM肯定给你盖出来点啥, 样子还挺唬人, 但根本不靠谱。因为, 没人做”工程师”的活。&lt;/p&gt;
&lt;p&gt;举个我司Empath Legal的真实例子。我们帮律师做陪审团筛选工具。我要是让LLM来: “写个API, 分析陪审员问卷, 给出洞察。”&lt;/p&gt;
&lt;p&gt;LLM能写出能用的东西:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解析问卷&lt;/li&gt;
&lt;li&gt;调用LLM分析文本&lt;/li&gt;
&lt;li&gt;格式化结果返回&lt;/li&gt;
&lt;li&gt;代码结构合理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但它绝对不会主动考虑这些:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;业务需求&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;问卷50多页, 律师要30秒内出结果&lt;/li&gt;
&lt;li&gt;用户是律师, 一切都得有出处, 不能瞎编&lt;/li&gt;
&lt;li&gt;分析结果必须有确定性, 法庭上要靠得住&lt;/li&gt;
&lt;li&gt;按用量计费, 得严格追踪消耗&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;技术护栏&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;问卷得按章节分块, 不能随便拆&lt;/li&gt;
&lt;li&gt;得有流式输出, 不能让律师30秒看个空白&lt;/li&gt;
&lt;li&gt;需要缓存, 防止重复分析白烧钱&lt;/li&gt;
&lt;li&gt;LLM API偶尔抽风, 异常要兜底&lt;/li&gt;
&lt;li&gt;全链路审计, 合规要查&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;系统设计&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用同步还是异步? (异步, 30秒要求)&lt;/li&gt;
&lt;li&gt;LLM中途挂了怎么办? (幂等重试)&lt;/li&gt;
&lt;li&gt;LLM供应商限流咋办? (排队+背压)&lt;/li&gt;
&lt;li&gt;数据一致性怎么保证? (部分完成咋处理)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LLM能写”通路”, 但如果我不自己做工程设计, 梳理约束, 定架构, 那代码上线一秒钟就翻车。&lt;/p&gt;
&lt;h2 id=&quot;新的分工时代&quot;&gt;新的分工时代&lt;/h2&gt;
&lt;p&gt;这不是说LLM不行——它们太厉害了。但我们得搞清楚它们在干啥。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LLM是超级程序员&lt;/strong&gt;: 给定明确需求和约束, 能写出一手好代码。会用模式, 能优化, 能调试, 能写文档。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;人类要做工程师&lt;/strong&gt;: 理解业务, 设计系统, 搭好护栏, 预判边界和异常。要问: “我们到底要解决什么?会出啥乱子?怎么扩展?”&lt;/p&gt;
&lt;p&gt;想开点: 繁琐的底层活可以丢给AI, 我们只用搞定”人性化需求, 系统设计, 架构取舍”这些有趣的活儿。&lt;/p&gt;
&lt;p&gt;但前提是: 你得先做工程师的活。如果只是”LLM, 给我造个X”, 不给上下文, 不提约束, 不画蓝图, 出来的就是只可远观的危楼。&lt;/p&gt;
&lt;h2 id=&quot;跟ai编程工具打交道的正确姿势&quot;&gt;跟AI编程工具打交道的正确姿势&lt;/h2&gt;
&lt;p&gt;我的经验:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;烂提示:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“给我写个SaaS的支付系统”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;优质提示:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“写个支付系统, 满足这些需求:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持按积分计费 (用户买积分, 功能扣积分)&lt;/li&gt;
&lt;li&gt;必须安全处理并发扣除 (两次API不能都扣成功, 积分只够一次)&lt;/li&gt;
&lt;li&gt;需要账单审计, 方便对账&lt;/li&gt;
&lt;li&gt;支付渠道挂了要优雅降级 (走缓存积分, 记录日志后续补算)&lt;/li&gt;
&lt;li&gt;性能要求: 每次扣积分延迟&amp;#x3C;50ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据库用PostgreSQL行级锁扣积分。操作需幂等, 重试不重复扣。全链路日志方便排查。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;第二种提示才是”工程师干的活”:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;业务模型 (积分制)&lt;/li&gt;
&lt;li&gt;核心约束 (并发安全)&lt;/li&gt;
&lt;li&gt;合规要求 (审计)&lt;/li&gt;
&lt;li&gt;可靠性 (故障降级)&lt;/li&gt;
&lt;li&gt;性能指标 (延迟)&lt;/li&gt;
&lt;li&gt;技术方案 (PostgreSQL行锁)&lt;/li&gt;
&lt;li&gt;运维考虑 (可排查)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样, LLM才能写出上线无压力的代码, 因为你给了它”蓝图”。&lt;/p&gt;
&lt;h2 id=&quot;一点扎心的真相&quot;&gt;一点扎心的真相&lt;/h2&gt;
&lt;p&gt;很多老开发, 其实早就习惯了”脑中设计+手上实现”一条龙。每次写代码, 都是先做工程师, 再做程序员, 帽子换得飞快。&lt;/p&gt;
&lt;p&gt;但有了LLM, 编程部分AI包了。你如果不认真做工程师的活, 只会下”魔法提示”, 最后得到的, 是个只在demo里能用, 真环境就翻车的项目。&lt;/p&gt;
&lt;p&gt;扎心的是: &lt;strong&gt;很多自称工程师的, 其实只是程序员&lt;/strong&gt;。代码写得贼溜, 但从没真正搞过系统设计, 要么一直在实现别人的架构, 要么项目本身太小没啥复杂度。&lt;/p&gt;
&lt;p&gt;在LLM时代, 这个区别终于变得重要了。单纯的写代码技能正在被AI取代。真正有价值的是”工程思维”: 理解需求, 设计架构, 权衡取舍, 预判故障。&lt;/p&gt;
&lt;h2 id=&quot;该怎么进化你自己&quot;&gt;该怎么进化你自己&lt;/h2&gt;
&lt;p&gt;如果你正在学编程, 不要只学语法和框架。学会像工程师一样思考:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写完功能, 问问: “一千个人同时用会咋样?”&lt;/li&gt;
&lt;li&gt;写代码前, 先想: “都有啥可能出错?每种情况该怎么兜底?”&lt;/li&gt;
&lt;li&gt;研究真实系统设计, 不要只看教科书里的小例子&lt;/li&gt;
&lt;li&gt;学会写别人能看懂, 能实现的详细需求说明&lt;/li&gt;
&lt;li&gt;不光要会”怎么做”, 更要懂”为什么这么做”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你已经在开发岗位, 注意分清自己干的是哪一头。只是对着规范写功能?那你就是程序员, 未来LLM会把这工作抢走。赶紧加强工程能力: 系统设计, 需求分析, 架构思维。&lt;/p&gt;
&lt;p&gt;如果你在面试招人, 别再考”能不能写FizzBuzz”, 改问”设计个URL短链系统, 能扛1000请求/秒, 故障咋办?”写代码的活儿越来越自动化了, 剩下的全靠脑子。&lt;/p&gt;
&lt;h2 id=&quot;最后的笑点&quot;&gt;最后的笑点&lt;/h2&gt;
&lt;p&gt;说个小秘密: 这篇文章, 其实是我和Claude合写的。不是我不会写, 而是Claude作为”写作程序员”比我还专业。我给它核心思路, 写作架构, 风格要求 (要有建筑比喻, 要举案例), 它写得比我快还顺。&lt;/p&gt;
&lt;p&gt;Claude写了优美的段落。但那些最关键的决定——讲什么, 怎么安排, 用什么例子, 读者能不能get到——全是”工程师”我来定的。&lt;/p&gt;
&lt;p&gt;未来的软件开发, 不会是”全自动无人区”。而是人类做擅长的事——理解复杂需求, 做取舍, 设计健壮系统;AI做擅长的事——把规范变成精美, 可靠的代码。&lt;/p&gt;
&lt;p&gt;但前提是: 你得记得自己是工程师。否则, 盖出来的, 还是那种没人敢住的高楼。&lt;/p&gt;
&lt;p&gt;这种故事, 咱都不想再听第二遍吧。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;你用AI编程工具踩过什么坑?你发现自己开始更多地思考工程问题, 还是还停留在”写个X就行”?欢迎留言分享——翻车现场往往比成功故事更有料!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(本文由人类原创, 部分内容参考AI协助优化。)&lt;/em&gt;&lt;/p&gt;</description><pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate></item><item><title>12-Factor App: 用乐高积木思维打造云时代软件</title><link>https://shanechang.com/zh-cn/p/twelve-factor-app-lego-cloud-era/</link><guid isPermaLink="true">https://shanechang.com/zh-cn/p/twelve-factor-app-lego-cloud-era/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.B0hq-9yw_Z1Q0puh.webp&quot; alt=&quot;Featured image of post 12-Factor App: 用乐高积木思维打造云时代软件&quot; /&gt;&lt;h2 id=&quot;开篇小剧场&quot;&gt;开篇小剧场&lt;/h2&gt;
&lt;p&gt;想象一下, 大半夜三点, 你的应用突然”爆红”, 访问量从100飙到10万。团队一半呼呼大睡, 另一半正在群里发疯。你的应用能像武林高手一样优雅应对, 还是一秒崩溃成渣?&lt;/p&gt;
&lt;p&gt;创业那会儿, 这种场景天天在我脑海飘——直到我偶然读到了12-Factor App方法论, 瞬间醍醐灌顶。从那之后我一直觉得, 我们这些后辈不过是在巨人的肩膀上眺望未来罢了。&lt;/p&gt;
&lt;p&gt;简单来说: **12-Factor App就像云时代的乐高积木。**你写的不是一个死板的”独立应用”, 而是一个随时可以拼装, 拆卸, 扩展, 迁移的模块。外部世界发生什么, 运行在哪, 怎么部署, 统统交给平台操心。你只专注做好自己的”积木”, 保证哪里需要就能安上, 怎么拼都顺滑——这才是真正的无限可扩展。&lt;/p&gt;
&lt;h2 id=&quot;三大支柱-拆解12-factor的架构思维&quot;&gt;三大支柱: 拆解12-Factor的架构思维&lt;/h2&gt;
&lt;p&gt;在灌输12条原则之前, 先给大家一个彩色脑图: 12-Factor App里, 所有事儿都能归到三大类:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;应用本身&lt;/strong&gt;: 核心逻辑, 无状态, 不可变&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口&lt;/strong&gt;: 应用和外部世界沟通的桥梁&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平台&lt;/strong&gt;: 充当”调度大管家”, 决定一切如何运转&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;你可以把应用想象成大厨。大厨 (应用) 有自己的绝活;厨房怎么布置, 食材从哪来, 菜端到哪去 (接口), 这些是厨房的活儿;而餐厅老板 (平台) 则决定哪个厨师上班, 雇多少人, 订单怎么分配。&lt;/p&gt;
&lt;p&gt;这种分工才是魔法的起点。下面我们一起来看看每个factor如何巩固这套体系。&lt;/p&gt;
&lt;h2 id=&quot;第一章-地基代码与依赖-先打牢&quot;&gt;第一章: 地基——代码与依赖, 先打牢&lt;/h2&gt;
&lt;h3 id=&quot;factor-1-唯一代码库-不再我这能跑&quot;&gt;Factor 1: 唯一代码库, 不再”我这能跑”&lt;/h3&gt;
&lt;p&gt;还记得那句”我这儿能跑啊!”吗?每次听到都想翻白眼。你花俩小时排查, 最后发现生产环境跑的是哪一版代码自己都说不清……&lt;/p&gt;
&lt;p&gt;12-Factor里, 代码库就像一张详细到每个房间门牌号的地图。X轴是版本, Y轴是部署环境。任何一份代码, 任何一个时刻都能精确定位。凌晨出事儿也不慌, 分分钟查出问题跑在哪。&lt;/p&gt;
&lt;p&gt;说来惭愧, 我大学时就盲目用github存代码 (以为这就是”云存储”), 后来和小伙伴协作才发现, 原来版本管理和踩地雷差不多, 踩错了就得挨一下。痛了才长记性。&lt;/p&gt;
&lt;h3 id=&quot;factor-23-依赖与配置把外部输入分清楚&quot;&gt;Factor 2&amp;#x26;3: 依赖与配置——把外部输入分清楚&lt;/h3&gt;
&lt;p&gt;这俩原则是一对”好基友”, 但各有分工。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;依赖&lt;/strong&gt;就像做菜的食材清单: Python包, Node模块, 系统库, 统统写清楚, 缺一不可。别再想着”环境里大概有吧”, 一切明明白白列出来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;配置&lt;/strong&gt;就像调音台上的旋钮。相同设备, 不同场合拧法不同。比如数据库地址, API密钥, 功能开关——这些会随环境变化, 但代码本身不变。&lt;/p&gt;
&lt;p&gt;关键点: **凡是部署时需要变的, 都是配置。**如果你现在把代码开源会把密码一起暴露, 说明配置和代码没分干净。&lt;/p&gt;
&lt;p&gt;我个人最喜欢的组合是Python的dotenv + dataclass: 简单, 好用, 又不失强大。&lt;/p&gt;
&lt;h3 id=&quot;factor-4-后端服务你的外挂资源&quot;&gt;Factor 4: 后端服务——你的”外挂资源”&lt;/h3&gt;
&lt;p&gt;数据库, 消息队列, 邮件服务……这些既是数据的入口, 也可能是出口。12-Factor的建议很简单: &lt;strong&gt;统统当作”外挂资源”看待。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你的应用不关心PostgreSQL是在本地, 机房, 还是云厂商的服务, 只要有个URL和凭证就行。这样哪天从自建数据库迁到云服务, 只是改个配置的事, 代码不用动。&lt;/p&gt;
&lt;p&gt;要命的事情从来不是代码写错, 而是新代码引入的新bug。哈哈, 谁懂谁痛。&lt;/p&gt;
&lt;h2 id=&quot;第二章-流水线从源码到上线-三步走&quot;&gt;第二章: 流水线——从源码到上线, 三步走&lt;/h2&gt;
&lt;h3 id=&quot;factor-5-构建-发布-运行三段式火箭&quot;&gt;Factor 5: 构建, 发布, 运行——三段式火箭&lt;/h3&gt;
&lt;p&gt;做面包的朋友有感触: 凌晨四点揉面 (构建), 九点开门前出炉一批 (发布), 白天顾客来买 (运行) 。小作坊能现做现卖, 但要是面包做砸了, 顾客就得等半天。要是提前批量做, 大家都开心。&lt;/p&gt;
&lt;p&gt;三阶段细拆:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;构建阶段&lt;/strong&gt;: 源码+依赖, 打包编译成可执行文件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发布阶段&lt;/strong&gt;: 上述产物+环境配置, 生成有编号的release&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行阶段&lt;/strong&gt;: 在生产环境部署并启动release&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个release都是”冻住”的快照, 既定构建+既定配置。v427出毛病?一秒回退到v426, 不用一边掉头发一边查diff。&lt;/p&gt;
&lt;p&gt;好处?构建阶段出错时开发还醒着, 运行阶段出事直接回滚, 不用凌晨三点生产上debug。&lt;/p&gt;
&lt;h3 id=&quot;factor-6-进程无状态-随时重启&quot;&gt;Factor 6: 进程——无状态, 随时”重启”&lt;/h3&gt;
&lt;p&gt;你的应用进程要像流水线上的工人, 手上不留活儿。数据要存?用数据库。临时缓存?上Redis。文件?丢S3。&lt;/p&gt;
&lt;p&gt;进程无状态, 就变得”可抛弃”。想启动, 想停止, 想扩容, 想宕掉都随意, 用户毫无察觉。说白了, 这才是真”自由身”。&lt;/p&gt;
&lt;h3 id=&quot;factor-7-端口绑定自带服务器的服务&quot;&gt;Factor 7: 端口绑定——自带”服务器”的服务&lt;/h3&gt;
&lt;p&gt;应用要自给自足, 通过端口对外暴露服务。别想着运行时再注入web服务器, 搞复杂容器。直接让你的Python应用带上Gunicorn, Ruby用Puma/Thin, Java打包Jetty。总之就是: 我监听5000端口, 想接入随时来。&lt;/p&gt;
&lt;p&gt;这样你的应用就像乐高积木, 今天是web服务, 明天可能就变成别的应用的后端资源。只要URL+端口, 怎么拼都行。&lt;/p&gt;
&lt;h2 id=&quot;第三章-运维野外生存的艺术&quot;&gt;第三章: 运维——野外生存的艺术&lt;/h2&gt;
&lt;h3 id=&quot;factor-8-并发横向扩容才王道&quot;&gt;Factor 8: 并发——横向扩容才王道&lt;/h3&gt;
&lt;p&gt;做本地应用时, 大家都想着多线程, 多连接池, 多event loop, 拼命榨干一台机器。12-Factor要你换思路: 应用保持简单, 一个进程搞定一件事, 真要抗压, 让平台多开几个进程。&lt;/p&gt;
&lt;p&gt;有点像开餐馆。传统思路是让一个厨师手脚更快, 炒更多锅, 12-Factor则是多雇厨师, 每人干好自己的活儿。要扩容?加人就行, 别把厨师累趴下。&lt;/p&gt;
&lt;h3 id=&quot;factor-9-可丢弃性秒启秒停-优雅下线&quot;&gt;Factor 9: 可丢弃性——秒启秒停, 优雅下线&lt;/h3&gt;
&lt;p&gt;你的进程要像凤凰, 随时准备”死而复生”。启动快, 关停要优雅 (做完手头活再退出) 。&lt;/p&gt;
&lt;p&gt;这种”生死看淡”的哲学, 才是现代部署的基石。滚动更新, 自动扩容, 自愈机制, 全靠进程说死就死, 说活就活。&lt;/p&gt;
&lt;h3 id=&quot;factor-10-开发-生产一致性缩小差距-别掉坑&quot;&gt;Factor 10: 开发-生产一致性——缩小差距, 别掉坑&lt;/h3&gt;
&lt;p&gt;老套路:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时间差&lt;/strong&gt;: 代码今天写, 下个月上线&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;人员差&lt;/strong&gt;: 开发写代码, 运维上线&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具差&lt;/strong&gt;: 本地用SQLite, 生产用PostgreSQL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;12-Factor的目标是: 这些差距都砍掉!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码写完几小时内上线&lt;/li&gt;
&lt;li&gt;开发亲自参与部署&lt;/li&gt;
&lt;li&gt;各环境用同一套后端服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;做多线程编程都知道mutex是防止”竞态条件”, 这里也是一样。你要把环境差距控制到最小, 才能保证每次上线都”有条不紊”, 而不是”天降bug”。&lt;/p&gt;
&lt;h2 id=&quot;第四章-可观测性日志与运维小黑屋&quot;&gt;第四章: 可观测性——日志与运维”小黑屋”&lt;/h2&gt;
&lt;h3 id=&quot;factor-11-日志专注输出-其他交给平台&quot;&gt;Factor 11: 日志——专注输出, 其他交给平台&lt;/h3&gt;
&lt;p&gt;日志该怎么写?就像写日记一样, 谁看不关心, 只管往stdout (标准输出) 里倒。&lt;/p&gt;
&lt;p&gt;一开始我也觉得这法子怪怪的, 后来发现真香!开发时日志直接出现在终端, 生产时平台负责收集, 转发, 分析, 爱存哪存哪, 爱送给谁送给谁, 应用本身完全不用操心。&lt;/p&gt;
&lt;p&gt;本质还是那句话: 应用只管”说”, 平台负责”听”。&lt;/p&gt;
&lt;h3 id=&quot;factor-12-一次性运维任务和应用同根同源&quot;&gt;Factor 12: 一次性运维任务——和应用同根同源&lt;/h3&gt;
&lt;p&gt;数据库迁移, 控制台调试, 数据修复脚本……这些运维任务必须在和正式应用一模一样的环境下跑。代码, 配置, 依赖都一致。&lt;/p&gt;
&lt;p&gt;为啥?因为最怕的是脚本在预发环境正常, 到线上就跪了。只要环境一致, “works on my machine”的锅就不会再甩来甩去了。&lt;/p&gt;
&lt;h2 id=&quot;终极启示-本质是契约精神&quot;&gt;终极启示: 本质是契约精神&lt;/h2&gt;
&lt;p&gt;其实12-Factor App的精髓从来不是那12条清单, 而是&lt;strong&gt;应用和平台之间的契约&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;应用承诺:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无状态, 可随时抛弃&lt;/li&gt;
&lt;li&gt;明确声明所有需求&lt;/li&gt;
&lt;li&gt;通过标准接口沟通&lt;/li&gt;
&lt;li&gt;日志只往stdout写&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;平台承诺:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提供配置&lt;/li&gt;
&lt;li&gt;管理进程&lt;/li&gt;
&lt;li&gt;路由请求&lt;/li&gt;
&lt;li&gt;处理日志&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有了这份契约, 奇迹就发生了: 你的应用可以今天跑在Heroku, 明天跑在Kubernetes, 后天随便上什么新平台。用户量翻百倍也不怕, 代码一行不用改。就像乐高积木, 哪里需要就插哪里, 怎么拼都顺手, 这才是云时代软件的正确打开方式!&lt;/p&gt;</description><pubDate>Tue, 19 Aug 2025 00:00:00 GMT</pubDate></item></channel></rss>