<?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/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/</link><language>zh-cn</language><lastBuildDate>Tue, 04 Nov 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://shanechang.com/zh-cn/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/index.xml" rel="self" type="application/rss+xml"/><item><title>ASGI到底是什么?从一头雾水到豁然开朗的理解之旅</title><link>https://shanechang.com/zh-cn/p/understanding-asgi-from-confusion-to-clarity/</link><guid isPermaLink="true">https://shanechang.com/zh-cn/p/understanding-asgi-from-confusion-to-clarity/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.D4v3duLW_217oEx.webp&quot; alt=&quot;Featured image of post ASGI到底是什么?从一头雾水到豁然开朗的理解之旅&quot; /&gt;&lt;h2 id=&quot;scope的隐身术-让人抓狂的神秘变量&quot;&gt;scope的隐身术: 让人抓狂的神秘变量&lt;/h2&gt;
&lt;p&gt;故事要从我开发一个需要实时推送数据的小功能说起。没啥花头, 就是想服务器一有新消息, 客户端立马能收得到。于是我自然而然地用上了FastAPI, 结果一头扎进文档, 发现满篇都是&lt;code&gt;scope&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;没错, 就是这个&lt;code&gt;scope&lt;/code&gt;。看文档到哪都能看到它的影子, 教程代码里有它, Stack Overflow的解答也是默契地假设你已经会用它了。但最让我抓狂的是: **我在自己的代码里根本没看到过它的踪影!**路由, WebSocket端点, 统统没露面。scope去哪了?难道是传说中的”空气变量”?&lt;/p&gt;
&lt;p&gt;更离谱的是, &lt;code&gt;receive&lt;/code&gt;和&lt;code&gt;send&lt;/code&gt;也差不多, 大家都在谈, 一到自己写代码就消失了。AI助手讲解得头头是道, 但我死活没法把这些虚空中的参数和自己写的Litestar, Advanced Alchemy代码联系起来。&lt;/p&gt;
&lt;p&gt;这时候我才幡然醒悟: 原来我一直在学各种框架, 却从没搞懂这些框架的地基是什么。只有”刨地三尺”, 搞懂Python异步Web服务器最底层的原理, 才能明白这些玄学参数到底干嘛用的——不是FastAPI的套路, 也不是Django Channels的玩法, 而是底层协议的真相。&lt;/p&gt;
&lt;p&gt;于是, 这就是我的ASGI探险记。如果你也曾经对ASGI, WebSocket一头雾水, 觉得Python异步Web开发跟传统Flask/Django完全不是一个画风, 这篇文章就是为你写的。读完你就能明白ASGI到底是什么, 它为啥存在, 以及它如何优雅统一了HTTP, SSE, WebSocket等各种通信方式。&lt;/p&gt;
&lt;h2 id=&quot;第一重顿悟-asgi其实就是个君子协定&quot;&gt;第一重顿悟: ASGI其实就是个”君子协定”&lt;/h2&gt;
&lt;p&gt;最让我拍案叫绝的发现是: &lt;strong&gt;ASGI不是框架, 也不是库, 而是一个规范——规定了Web服务器如何跟你的Python应用对话。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;就像电源插座一样, 国家标准一出, 不管是小米还是海尔, 都能造出插得上的电器。ASGI也是一样, 定义了Web服务器和异步Python应用之间的”对接协议”。&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;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;async def app(scope, receive, send):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 应用逻辑&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是这么朴实无华。无论你用FastAPI, Starlette, Django Channels, 还是自己手撸, 从本质上看, ASGI应用就是一个可调用对象, 接收这三个参数:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/strong&gt;: 一个字典, 包含了连接的元信息。可以理解为”场景说明书”, 告诉你这是啥连接, 谁发起的, 要干啥。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;receive&lt;/code&gt;&lt;/strong&gt;: 一个异步函数, 你调用它能收到客户端发来的消息。就像查邮箱收信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;send&lt;/code&gt;&lt;/strong&gt;: 另一个异步函数, 用它给客户端发消息。等于往邮箱里塞信寄出去。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更妙的是, 这套”三件套”通吃所有场景——普通HTTP, 流式Server-Sent Events, 双向WebSocket, 甚至连应用的启动和关闭都能用这一套。只要scope说明了”这次玩哪种花样”, 你就能对症下药。&lt;/p&gt;
&lt;p&gt;不过刚开始让我迷糊的是——“会话”到底指啥?一次请求?一场对话?服务器的整个寿命?&lt;/p&gt;
&lt;p&gt;答案其实很灵性: **得看scope的type。**接下来, 咱们一个个举例聊聊。&lt;/p&gt;
&lt;h2 id=&quot;http-简单明了的一次性买卖&quot;&gt;HTTP: 简单明了的一次性买卖&lt;/h2&gt;
&lt;p&gt;先从最熟悉的HTTP说起。你可能早就用惯了, 但不一定知道ASGI在背后默默打工。&lt;/p&gt;
&lt;p&gt;当有客户端发起HTTP请求时, ASGI会生成一个scope, 大致长这样:&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;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;method&quot;: &quot;GET&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;path&quot;: &quot;/api/users/123&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [...],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;query_string&quot;: b&quot;format=json&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;client&quot;: (&quot;192.168.1.5&quot;, 54321),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;server&quot;: (&quot;10.0.0.1&quot;, 8000),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意&lt;code&gt;type: &quot;http&quot;&lt;/code&gt;, 这就是告诉你, 这是一锤子买卖, 请求-响应搞定即走。&lt;/p&gt;
&lt;p&gt;流程如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端发请求 → 服务器生成scope并调用你的app&lt;/li&gt;
&lt;li&gt;你的应用用&lt;code&gt;receive()&lt;/code&gt;收请求体 (可能是分段的)&lt;/li&gt;
&lt;li&gt;用&lt;code&gt;send()&lt;/code&gt;发响应头 (状态, 头信息)&lt;/li&gt;
&lt;li&gt;再用&lt;code&gt;send()&lt;/code&gt;发响应体 (具体内容)&lt;/li&gt;
&lt;li&gt;scope消失, 连接结束&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这就像自动贩卖机: 你投币, 选饮料, 拿饮料, 完事走人。贩卖机根本不记得你是谁。&lt;/p&gt;
&lt;p&gt;重点: HTTP在ASGI里是&lt;strong&gt;无状态的&lt;/strong&gt;。每次请求独立, scope活不了一秒, 处理完就销毁。&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;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 先发响应头&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.start&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;status&quot;: 200,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [[b&quot;content-type&quot;, b&quot;application/json&quot;]],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 再发响应体&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.body&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;body&quot;: b&apos;{&quot;user&quot;: &quot;Shane&quot;, &quot;id&quot;: 123}&apos;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;短平快, 简单明了。这是最基础的模式。&lt;/p&gt;
&lt;h2 id=&quot;server-sent-events-服务器单向直播&quot;&gt;Server-Sent Events: 服务器单向直播&lt;/h2&gt;
&lt;p&gt;如果说HTTP是买瓶饮料就走, Server-Sent Events (SSE) 则像是你在贩卖机旁边蹲着, 机器一上新立马通知你。&lt;/p&gt;
&lt;p&gt;SSE本质上还是HTTP, 但不再遵循一次性请求-响应的老路, 而是:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端发起HTTP请求&lt;/li&gt;
&lt;li&gt;服务器回响应头 (记得加&lt;code&gt;Content-Type: text/event-stream&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接保持不断开&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;服务器随时发送数据&lt;/li&gt;
&lt;li&gt;直到有一方主动断开&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;scope依旧是&lt;code&gt;type: &quot;http&quot;&lt;/code&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;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.start&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;status&quot;: 200,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [[b&quot;content-type&quot;, b&quot;text/event-stream&quot;]],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 持续推送&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.body&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;body&quot;: b&quot;data: {\&quot;new_order\&quot;: 42}\n\n&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;more_body&quot;: True,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.body&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;body&quot;: b&quot;data: {\&quot;new_order\&quot;: 43}\n\n&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;more_body&quot;: True,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意&lt;code&gt;more_body: True&lt;/code&gt;, 意思就是”别断, 还会有!”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SSE还是HTTP&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;进度更新&lt;/li&gt;
&lt;li&gt;只需单向推送, 不需要客户端反馈&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比WebSocket简单, 功能比普通HTTP强。&lt;/p&gt;
&lt;h2 id=&quot;websocket-双向持久对话&quot;&gt;WebSocket: 双向持久对话&lt;/h2&gt;
&lt;p&gt;轮到WebSocket出场, ASGI的奥义也就此揭晓。&lt;/p&gt;
&lt;p&gt;WebSocket跟HTTP/SSE都不一样, 因为它是&lt;strong&gt;双向, 持久, 状态化&lt;/strong&gt;的。不是你一句我一句的买卖, 而是持续的对话, 谁都可以主动说话。&lt;/p&gt;
&lt;p&gt;如果HTTP像写信, SSE像广播, &lt;strong&gt;WebSocket就是打电话&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;scope结构:&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;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;websocket&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;path&quot;: &quot;/ws/chat/room-42&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [...],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;query_string&quot;: b&quot;user=shane&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;client&quot;: (&quot;192.168.1.5&quot;, 54322),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;server&quot;: (&quot;10.0.0.1&quot;, 8000),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;type: &quot;websocket&quot;&lt;/code&gt;, 完全不同的玩法。&lt;/p&gt;
&lt;p&gt;消息类型:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;客户端到应用:&lt;/strong&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;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.connect&quot;}       # 客户端请求建立连接&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.receive&quot;, &quot;text&quot;: &quot;Hello!&quot;}  # 客户端发消息&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.disconnect&quot;}    # 客户端断开&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;应用到客户端:&lt;/strong&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;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.accept&quot;}        # 应用同意连接&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.send&quot;, &quot;text&quot;: &quot;Welcome!&quot;}   # 应用发消息&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.close&quot;}         # 应用断开&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有木有发现, HTTP是你问我答, WebSocket是持续在线, 谁都能随时插话。&lt;/p&gt;
&lt;p&gt;scope也能活很久, 可能几秒, 也可能几小时。你可以在一个scope里互发上百条消息, 状态一直保留着。&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;实时推送&lt;/li&gt;
&lt;li&gt;需要真·双向实时通信的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但这里还有个经典疑问……&lt;/p&gt;
&lt;h2 id=&quot;websocket握手的迷思-升级的秘密&quot;&gt;WebSocket握手的迷思: 升级的秘密&lt;/h2&gt;
&lt;p&gt;初学时我被”WebSocket升级”整蒙了: &lt;strong&gt;WebSocket不是HTTP, 怎么开始的?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答案让人大跌眼镜: &lt;strong&gt;WebSocket一开始就是个HTTP请求。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;啥?不信?往下看。&lt;/p&gt;
&lt;p&gt;WebSocket的设计就是要兼容原有Web基础设施 (端口, 代理, SSL等), 所以不是重新发明协议, 而是走”先HTTP, 后升级”的套路。&lt;/p&gt;
&lt;p&gt;流程:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 客户端发特殊HTTP请求:&lt;/strong&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;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;GET /ws/chat HTTP/1.1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Host: example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Upgrade: websocket&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Connection: Upgrade&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sec-WebSocket-Key: X3JJHMbDL1EzLkh9GBhXDw==&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sec-WebSocket-Version: 13&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是普通GET, 只是加了些特殊头, 表示”我要升级成WebSocket”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 服务器回101响应:&lt;/strong&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;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;HTTP/1.1 101 Switching Protocols&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Upgrade: websocket&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Connection: Upgrade&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sec-WebSocket-Accept: &amp;#x3C;computed response&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;101 Switching Protocols&lt;/code&gt;就是同意升级, 从此用WebSocket协议交流。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 协议切换, TCP连接保持&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;底层TCP连接没断, 但HTTP已结束, 之后两边说的是WebSocket的”暗号”。&lt;/p&gt;
&lt;p&gt;你可以这么理解: 打电话给餐厅 (HTTP), 请求转接到某个包间 (Upgrade), 服务员说”马上接通” (101响应), 之后你就跟包间的人直接聊天了 (WebSocket) 。线路没变, 协议变了。&lt;/p&gt;
&lt;h2 id=&quot;asgi在握手流程中扮演什么角色&quot;&gt;ASGI在握手流程中扮演什么角色?&lt;/h2&gt;
&lt;p&gt;最让我一头雾水的是: &lt;strong&gt;ASGI是在哪一环介入的?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答案是: ASGI只在协议升级 (握手) 之后才介入。&lt;/p&gt;
&lt;p&gt;分层解释:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;服务器层 (Uvicorn/Hypercorn等, 自动完成) :&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接收HTTP升级请求&lt;/li&gt;
&lt;li&gt;校验WebSocket头&lt;/li&gt;
&lt;li&gt;回复101响应&lt;/li&gt;
&lt;li&gt;切换TCP连接协议&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;ASGI应用层 (你的代码) :&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务器创建WebSocket scope&lt;/li&gt;
&lt;li&gt;发送&lt;code&gt;websocket.connect&lt;/code&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;&lt;strong&gt;协议层 (服务器)&lt;/strong&gt;: 判断是不是有效的WebSocket升级请求&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务层 (应用)&lt;/strong&gt;: 判断要不要让这个连接进来 (比如权限校验)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;握手早就由服务器搞定了, 你收到&lt;code&gt;websocket.connect&lt;/code&gt;时, 客户端已经连接上, 等你点头。你发&lt;code&gt;websocket.accept&lt;/code&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;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 收到握手后服务器转发的消息&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;message = await receive()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# message = {&quot;type&quot;: &quot;websocket.connect&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 业务判断, 比如认证&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if user_is_authenticated:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    await send({&quot;type&quot;: &quot;websocket.accept&quot;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 开始双向消息循环&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;else:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    await send({&quot;type&quot;: &quot;websocket.close&quot;, &quot;code&quot;: 1008})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 拒绝连接&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理解了这点, FastAPI, Starlette各种WebSocket玩法就都清楚了——协议交给服务器, 业务你说了算。&lt;/p&gt;
&lt;p&gt;你和客户端都能随时断开连接, 谁也不是大爷, 谁都能说”我不玩了”。&lt;/p&gt;
&lt;h2 id=&quot;lifespan-横空出世的第四种scope&quot;&gt;Lifespan: 横空出世的第四种scope&lt;/h2&gt;
&lt;p&gt;就在我以为ASGI只有HTTP/SSE/WebSocket三种模式时, 突然冒出来个lifespan, 彻底打乱了我的世界观。&lt;/p&gt;
&lt;p&gt;原以为lifespan是”一次连接的生存期”, 比如WebSocket连着多久。事实证明, &lt;strong&gt;我错得离谱&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;lifespan根本跟一次连接没关系, 而是&lt;strong&gt;整个应用的生命周期&lt;/strong&gt;——服务器进程从启动到关闭。&lt;/p&gt;
&lt;p&gt;你可以这么总结:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP scope: 活一次请求 (百毫秒)&lt;/li&gt;
&lt;li&gt;SSE scope: 活一次流式会话 (几分钟到几小时)&lt;/li&gt;
&lt;li&gt;WebSocket scope: 活一次双向会话 (几秒到几小时)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lifespan scope: 活整个应用 (几天到几个月)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务器启动时, 先创建lifespan scope, 给你发startup消息;关机时再发shutdown消息。&lt;/p&gt;
&lt;p&gt;scope结构:&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;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;lifespan&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;lifespan.startup&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;lifespan.shutdown&quot;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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;li&gt;缓存预热&lt;/li&gt;
&lt;li&gt;监控/指标采集器初始化&lt;/li&gt;
&lt;li&gt;优雅关闭各种资源&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;打个比方: HTTP/SSE/WebSocket是你招待每个顾客, lifespan是你每天开店 (准备食材, 开火) 和打烊 (关门, 清扫) 。&lt;/p&gt;
&lt;p&gt;你肯定不想每次请求都加载2G模型吧?只需要在&lt;code&gt;lifespan.startup&lt;/code&gt;一次性加载, 所有请求复用。同理, 关机时优雅关闭连接池。&lt;/p&gt;
&lt;p&gt;FastAPI里的&lt;code&gt;@app.on_event(&quot;startup&quot;)&lt;/code&gt;和&lt;code&gt;@app.on_event(&quot;shutdown&quot;)&lt;/code&gt;, 其实就是在帮你处理这些lifespan消息。&lt;/p&gt;
&lt;p&gt;lifespan和其他三种scope是正交的, 不是通信场景, 而是应用本身的生命周期管理。&lt;/p&gt;
&lt;h2 id=&quot;asgi的终极思维模型&quot;&gt;ASGI的终极思维模型&lt;/h2&gt;
&lt;p&gt;经历了这些疑惑和顿悟, 我终于形成了这样一个清晰的思维框架:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ASGI是个统一的接口, 用来描述”通信会话”, 会话的类型有:&lt;/strong&gt;&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;scope类型&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;存活时间&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;通信方向&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;典型场景&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;HTTP&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;毫秒级&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;请求→响应&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;API调用, 页面加载, 表单提交&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;SSE&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;分钟到小时&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;服务器→客户端&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;实时推送, 仪表盘, 通知&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;WebSocket&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;秒到小时&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;双向&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;聊天, 协作, 游戏, 实时控制&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Lifespan&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;应用生命周期&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;N/A&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;启动/关闭, 资源管理&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;这四种都用同一个接口: &lt;code&gt;async def app(scope, receive, send)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;scope字典告诉你遇上了啥会话, receive/send让你与之交互。合同不变, 玩法多变。&lt;/p&gt;
&lt;p&gt;这样一来, &lt;strong&gt;中间件也能通吃所有类型&lt;/strong&gt;。比如认证中间件, 可以根据scope类型分别处理HTTP, WebSocket, SSE。日志中间件也是一样, 写一次通用所有。&lt;/p&gt;
&lt;p&gt;这就是为啥你一旦理解ASGI, FastAPI用起来就很顺畅。所有路由, WebSocket端点, 启动事件, 本质上都是ASGI应用, 遵守同一个简单协议。&lt;/p&gt;
&lt;p&gt;隐身的&lt;code&gt;scope&lt;/code&gt;其实一直都在, 只是框架帮你藏起来了。等你需要深入理解WebSocket, 优雅管理资源, 写中间件的时候, 就会发现ASGI这层基础有多香。&lt;/p&gt;
&lt;h2 id=&quot;从迷茫到通透&quot;&gt;从迷茫到通透&lt;/h2&gt;
&lt;p&gt;刚开始时, &lt;code&gt;scope&lt;/code&gt;像个幽灵, 文档总假设你早就会。现在我终于明白: 它不过是个用来描述通信模式的字典。&lt;/p&gt;
&lt;p&gt;美妙的是, 这种理解一通百通。读FastAPI的WebSocket例子, 能明白底层怎么回事;Starlette的启动事件, 其实就是lifespan消息的处理;排查连接断了, 也能思考是HTTP scope正常结束, 还是WebSocket意外关闭。&lt;/p&gt;
&lt;p&gt;一旦下潜到”协议层”, 理解ASGI底层, 而非只会用框架, 迷雾就散了, 自信心up!&lt;/p&gt;
&lt;p&gt;如果你正在开发实时功能, 异步API, 或者单纯想吃透现代Python Web后端, 希望这段旅程能帮你像我一样拨云见日。下次看到文档里的scope, 别慌, 你已经明白它的底细。&lt;/p&gt;
&lt;p&gt;说不定哪天你也会撸出极简ASGI应用, 写出专属中间件, 或者终于搞懂框架背后的魔法。&lt;/p&gt;
&lt;p&gt;ASGI的兔子洞随时欢迎你深入探索——但至少现在, 你已经站在了入口。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;(文章原创, 部分内容用AI润色。)&lt;/em&gt;&lt;/p&gt;</description><pubDate>Tue, 04 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>