《The Accidental CTO》是印度电商平台 Dukaan 联合创始人兼 CTO Subhash Choudhary 的工程实录:他和负责业务的 Suumit Shah 一起,把帮小商家开网店的工具从一台月租 5 美元的 512MB 服务器,做成扛住百万卖家的基础设施。本篇覆盖目录与第 1–7 章:从凌晨 3 点的崩溃,经 MVP 选型、应用与数据库分家、负载均衡、读副本、预发布环境,到 Redis 缓存;后半程(微服务、Kafka、Docker、Kubernetes、边缘网络,直到从 AWS 迁往裸金属)留待下篇。

第1章 凌晨 3 点的电话

故事线:凌晨 3:14,Suumit 来电:“快起来!全都挂了!“SSH 登录要等”像一个世纪那么久”,htop 满屏红色:CPU 全部 100%,内存塞满,Swap 占满。根因简单到难堪:服务器总内存只有 512MB——“我们试图在电话亭里办一场摇滚演唱会,而电话亭终于塌了。”

关键决策:架构是单体(Monolith)——注册、商品目录、订单、商家后台、支付全塞进同一个 Django 项目,像一本所有配方装订在一起的总菜谱。从单体起步不是错误,反而是创业公司最好的选择:开发、测试、部署都简单,正是它让两个人在 48 小时内搭出可用的电商平台——“如果一开始就选择微服务,我们大概到现在还在争设计方案。“但副作用隐蔽:紧耦合,一个小 Bug 就可能毁掉整本书,新人必须先啃下整本总菜谱;最致命的是无法只扩展某一部分——店铺首页(主菜)流量爆炸时,低频的商家后台(前菜)被绑在同一台服务器上陪葬。软件单点叠硬件单点,是一颗定时炸弹。

数字:服务器 512MB RAM、单核 CPU、20GB SSD,月租 5 美元;作者的手机有 8GB RAM,是它的 16 倍;目录里有几百万个商品。诊断靠 ssh(登录迟迟不出提示 = 服务器只剩一口气)和 htop。

关键结论:

  • 你的第一台服务器一定会出问题,不是会不会,而是什么时候;目标是失败后快速恢复,并从中学到东西。
  • 先掌握基本功:用 CPU(厨师速度)、RAM(操作台空间)、Disk(储藏间)这套模型思考服务器。
  • 学会最基本的诊断工具。看不见,就修不好。ssh 和 htop 是系统管理员的听诊器。
  • 从单体起步是特性,不是缺陷。早期开发速度压倒一切,不要为初始产品过度设计。
  • 早期选择有保质期:能把你带到第一个 1 万用户的架构,未必能把你带到 10 万用户。

坑:应用要 CPU、数据库要写磁盘、用户请求要 RAM——资源争用全挤在一块切菜板上,直到崩溃才被正视。

第2章 WhatsApp PDF 问题(起点)

故事线:2020 年封锁期间,印度小商家把整门生意塞进 WhatsApp:发模糊不可搜索的 PDF,顾客敲易错的文字订单,店主手工确认,靠 UPI 付款截图对账——几千张截图对不上”谁付了什么钱”。Suumit 的念头:不要 PDF,不要长聊天,只要一个链接——商家自己的线上 dukaan(店)。

关键决策:做 MVP。“MVP 不是产品,它是一个实验”,目标是学习,用最小成本验证核心假设:“给小商家一个极其简单的开店工具,他们会用吗?“不造汽车,先造滑板。滑板是核心闭环:创建店铺(手机号 + OTP + 店名)、添加商品(名称、价格、一张图,不做分类、规格、库存)、生成分享链接贴进 WhatsApp;支付、配送追踪、账号、主题、分析全砍掉。挑战:48 小时上线。技术栈原则只有快:语言 Python(语法干净,不跟工具打架);框架 Django——“自带电池”的预制房屋套件,Django Admin 用 15 分钟免费送出本要一天才能做好的管理后台,成为上线后的任务控制中心,ORM 充当 SQL 翻译器(Product.objects.filter(store_id=123))并规避一大类 SQL 注入;数据库 PostgreSQL——结实、可靠、标准兼容,其 LISTEN/NOTIFY 特性后来成了第 7 章实时缓存失效的秘密武器。替代方案:Node.js + Express 像一盒高质量乐高而非预制房,太多东西要自己搭;Ruby on Rails 理念相似,最终是熟悉度问题——“在拼速度的局里,你永远押注自己最顺手的武器。”

数字:服务器是 DigitalOcean 最便宜的 Droplet:512MB RAM、1 vCPU、20GB SSD,每月 5 美元,Ubuntu LTS 镜像,机房选班加罗尔(第一批用户在印度,就近降低延迟),从下单到拿到公网 IP 约 60 秒。链路:Nginx(服务员:回静态文件 + 反向代理)→ Gunicorn(后厨经理)→ Django。域名 mydukaan.io 上线。

关键结论:

  • MVP 是验证核心假设的实验,不是最终产品的缩小版;写代码前,先定义你的”滑板”。
  • 最初的技术栈优先考虑速度和熟悉度,“上市速度”是最重要的指标。
  • 选一个扎实的数据库是长期主义下注,PostgreSQL 这样的地基未来能少踩很多坑。
  • 基础设施从小、从简单开始,每月 5 美元足以撑起最初几千用户,需求未被验证前不要过度花钱。
  • 理解 Web 服务器(Nginx)与应用服务器(Gunicorn)的分工——“服务员 + 后厨经理”是现代 Web 应用对外服务的核心骨架。

坑:为省钱选的 512MB 机器就是第 1 章那颗定时炸弹——当时看是最负责任的决定,但没人为”成功”做过预案。

第3章 伟大的分家:拆分应用与数据库

故事线:MVP 链接发进几个商家 WhatsApp 群后迎来爆炸:十几个用户 → 几百 → 几千只用几天,且自传播(店主的顾客里很多本身就是小商家)。随后应用变慢,我们每隔几小时重启一次,直到那通凌晨 3 点的电话。诊断结论:数据库正在把其他一切活活拖死——厨师大部分时间不在做菜,而在来回跑储藏间。

关键决策:先认清两类负载:应用工作是 CPU 密集型(思考:跑 Django 逻辑),数据库工作是 I/O 密集型(取放:读写磁盘)——我们逼着一个厨师兼职图书管理员,两边都做不好。方案是”伟大的分家”:应用服务器(Nginx + Gunicorn + Django,专为思考优化)与数据库服务器(只跑 PostgreSQL,专为记忆优化)各一台,对活体系统做开胸手术。迁移四步:①新库服务器选存储优化型(NVMe SSD、更多 RAM),除 PostgreSQL 什么都不装,防火墙只放行应用服务器 IP;②pg_dump 导出整库快照——像书记员读完图书馆每本书,写出《如何重建这整座图书馆》;③scp 传输、psql 恢复;④切换:挂维护页阻断新写入 → 重复备份恢复做最终同步 → 数据库 HOST 从 localhost 改指新库 → 重启 Gunicorn → 疯狂测试 → 解除维护。分家后新瓶颈现身:网络延迟——数据库往返从近乎免费变成页面延迟的大头,换硬件也消不掉。对策:用 select_related/prefetch_related 消灭 N+1 查询——10 个店铺及其商品从 11 趟图书馆压成 2 趟,“一次带着完整购物清单去拿齐”;引入连接池 PgBouncer——厨房和图书馆之间拿着一串已开好钥匙的保安。数据库哲学路口:不换 NoSQL。数据高度结构化、关系就是业务逻辑;电商对数据完整性零容忍,ACID(下单买 5 件商品,要么全部写入并更新库存,要么全部失败)不可谈判;瓶颈是读不是写——“几百万顾客同时查看已有商品”,读密集有成熟解法:读副本。“我们没有’大数据问题’。为了追时髦去选 NoSQL,就像用大锤砸核桃。”

数字:切换停机约 3 分钟;单次网络往返 1–2ms;一个页面 10–50 次往返、仅往返开销即 100ms;N+1 优化把 11 次查询压成 2 次。

关键结论:

  • 应用服务器与数据库服务器分离,是系统扩展的第一关键步。
  • 每个解决方案都会制造下一个问题:进入分布式系统后,网络延迟成为新的主要瓶颈。
  • 网络调用很贵,能少就少——用 select_related 和 prefetch_related 写出更聪明的代码。
  • 用 PgBouncer 这类连接池优化连接本身,系统在高负载下更稳更快。

坑:在创业世界”你买到的永远只是时间”,扩展像打地鼠,按下一个另一个又冒头——分家解决了资源争用,却造出了网络延迟这个更高级的问题。

第4章 交通警察:负载均衡入门

故事线:短暂的平静后,一场完美风暴:苏拉特一位手工纺织品大卖家把店铺链接丢进超大 Facebook 群,一家技术博客又发了篇文章,流量海啸涌来。警报响起:“CRITICAL: CPU 连续 5 分钟维持 100%“。Suumit 的短信只有一句:“Ab kya hua?”(现在又怎么了?)。数据库安然无恙,应用服务器却是一片屠宰场——CPU 钉死 100%。一个厨师,一千个顾客。

关键决策:扩容两条路。纵向扩展(scale up)=把好厨师换成世界级超级名厨:不改代码最省事,但贵得飞快(能力翻倍、价格往往涨 4–8 倍)、有天花板(没有更大的怪兽卡车)、且仍是单点故障(硬件故障或安全补丁重启,业务立刻离线)。横向扩展(scale out)=保留好厨师再请 3 个一模一样的:成本小步递增、理论无上限(4 台、40 台、400 台)、真正超能力是容错——4 个厨师病 1 个,餐厅慢一点但不会关门。必须选后者,于是需要交通警察:负载均衡器,好比繁忙超市里的收银经理,把新顾客引导到当前最合适的柜台,并做健康检查——有收银员晕倒就停开那个柜台,超市照常营业。算法二选一:Round Robin(发扑克牌式轮询,简单但默认所有请求一样重——Server B 还在跑 10 秒的慢任务,下一单仍会派给它);Least Connections(实时跟踪活动连接数,把你带去”此刻最短的那一条队伍”)。Dukaan 选后者。工具零新增:Nginx 本来就是服务员,事实证明它也是世界级负载均衡器——一段配置即可:upstream 定义两台内网 IP 组成的舰队,least_conn; 指定算法,proxy_pass 转发流量。

数字:新拉起一台完全一样的应用服务器(共 2 台);此后扩展就是”再开一台机器、把 IP 填进 upstream、重载配置”,几分钟加一个厨师。新链路:用户 → Nginx 负载均衡器 → 连接更少的 App Server → 共享数据库;某台挂掉,流量自动全部导去另一台——第一次做出具备容错能力的系统。

关键结论:

  • 横向扩展才是高可用和大规模增长的长期道路:更省钱、更灵活、消除单点故障。
  • 负载均衡器是横向扩展得以成立的核心交通警察,分发请求并在故障出现时自动绕行。
  • 可以从简单方案起步:Nginx 同时扮演 Web 服务器和负载均衡器,减少早期复杂度。
  • Least Connections 是比 Round Robin 更聪明、通常更均匀的默认算法。
  • 瓶颈永远在迁移:解决一个性能问题,负载只会转移到链路中的下一个薄弱环节。

坑:10 个厨师同时冲向同一座图书馆——应用层扩上去了,那台单独的、仍然”一个大块头”的数据库开始冒汗。瓶颈只是挪了地方。

第5章 数据库夜店门口的保安:读副本

故事线:跨过 10 万用户门槛,问题从”能不能活”变成”能不能快”。抱怨从”打不开”变成”店铺要 5 到 6 秒才开""保存转很久”,集中在白天商业高峰。应用层毫无压力,瓶颈全在数据库。95/5 法则:95 个只想”问这本书在哪”的读者,把 5 位要登记新书的作者堵在同一支队伍里——海量简单读拖慢了关键写。

关键决策:把读写分开,即主从复制。用夜店比喻:Master 是 VIP 区——写操作的唯一真相源头,门口的铁面保安确保每笔变更合法、准确、被记录,卖家走这里;读副本是主舞池——VIP 区的近实时镜像,向公众开放只看不改,吸收海量读流量,必要时可多开几个舞池。实现靠 PostgreSQL 流复制:主库预写日志 WAL 像勤勉保安的记录本,实时按序记下每个变化;副本订阅 WAL,经私有网络接收变更流并按相同顺序应用——如同 VIP 区的一切直播到主舞池。应用侧两处改造:settings 配两个连接,再加一个十几行的数据库路由器——读走 read_replica,写走 default,应用第一次学会扮演保安。上线后店铺页秒开、保存利索,主库压力骤降。但副本一上线就踏进 CAP 定理:一致性、可用性、分区容错最多同时严格保证两项;网络分区一定会发生,真实系统终究在一致性与可用性之间取舍——我们选了可用性。一致性是光谱:强一致性(DJ 一切歌,舞池必须瞬间同曲,代价是通道一阻塞系统宁可停住也不许有人听错);最终一致性(更新尽快流到舞池但不承诺瞬时);因果一致性(保证”我改了东西,所以我应该看见改动”)。

数字:卖家数 5 万 → 8 万 → 近 10 万;高峰时段上午 11 点–下午 5 点;主库 CPU 80–90%、磁盘 I/O 顶满;查询比例 95 读 / 5 写;复制延迟通常毫秒级,压力大时飙到 1–2 秒。Priya 把项链 ₹1000 改 ₹800,保存成功后刷店铺页还是 ₹1000,疯狂刷新 5 秒后才变 ₹800——“旧数据的幽灵”,不是 Bug 而是最终一致性的教科书案例:99.9% 的顾客察觉不到一秒延迟,触发修改的那 0.1% 完全无法接受。落地解法”VIP 通行证”(Read Your Own Writes):写成功后在用户会话打 60 秒临时标记,期间其读请求直接发往 Master,过期后回归副本(那时数据早已同步)——对大众保留可扩展性,对发起变更的人提供”仿佛强一致”的体验。另一个可选项是什么都不做:后台”店铺总数”晚 30 秒无伤大雅,关键是区分哪里必须强一致。

关键结论:

  • 读副本带来巨大性能收益,但一定有代价:用”强一致性的简单”交换”最终一致性的复杂”。
  • 复制延迟是物理现实,不是 Bug。消不掉,只能设计系统去适应它。
  • 最终一致性会制造非常困惑的用户体验,刚改完就看到旧值会迅速侵蚀信任。
  • 实现 Read Your Own Writes:对刚完成写操作的用户临时把读请求导向主库,在最关键的地方保留即时一致,又不丢掉副本的扩展能力。

坑:把副本当强一致的第二个主库来用,就会撞上”旧数据的幽灵”。

第6章 “兄弟,别在生产上测!“:预发布环境

故事线:上线流程危险得可怕:本机”跑得通”就推 GitHub,脚本自动直接部署到生产——从想法到用户屏幕不到 5 分钟,“不带安全绳在玩高空飞人”。周二下午,新开发者 Rohan 的按价格排序功能在只有 15 个商品的测试店铺上瞬间完成,推上去 5 分钟后,2000 个商品的大卖家 gavranmisal.com、Jain Shikanji 全部打不开,电话打爆 Suumit。服务器正常,是代码问题:一条 ORDER BY price 在商品量大时极低效,超时拖死整页渲染——一个只炸头部大客户的 Bug。紧急回滚,但伤害已造成:100% 自己的责任。

关键决策:Suumit 定调:“这就像厨师第一次试新菜,就直接端给总理吃。我们需要安全网。“补齐三个环境:开发环境(厨师私人测试厨房,可以乱、可以试错——Rohan 只在小饭上演练,没模拟大型宴会);Staging(彩排舞台——完整、平行、尽可能镜像生产,正是缺失的一环);生产环境(坐满付费顾客的主餐厅,任何失误都公开且伤名誉)。Staging 黄金准则:尽可能与生产一致,Bug 最爱躲在微妙差异里——Staging 内存更大就发现不了内存泄漏,Python 版本更新就会 Staging 正常、上线因库兼容崩掉,网络规则不同就会功能只在 Staging 可用。“你不可能在一个中学礼堂大小的纸板舞台上给百老汇大戏做彩排。“为此复制整套架构:规格一致的 Droplet、版本钉死的 Ubuntu/Python/Django/PostgreSQL/Nginx/Gunicorn、Staging 版的负载均衡 + 两台应用服务器 + 主从数据库——成本几乎翻倍,但这是保险费:用可预测的月费买一张”避免线上事故伤害声誉”的保险单。数据更难:绝不能直接克隆生产库——姓名、手机号、邮箱、私密订单放进权限更宽的环境,甚至可能违法。解法是每晚自动跑的数据灌入与脱敏流水线:先 pg_dump 做完整快照(灌入),再清洗(脱敏)——姓名换成 “Test User 1234”、联系方式假名化、价格与订单额随机扰动,但绝不删行:2000 个商品的店仍是 2000 个商品,500 个订单的用户仍是 500 个订单——规模真实、敏感信息为零。最后把手工冲刺换成部署流水线,目标是让部署变得无聊——“无聊是好事。无聊意味着网站没在着火。“五步:GitHub 发 Pull Request → 至少一位其他工程师人工 Code Review(盯 Bug、低效查询、可读性、安全漏洞)→ GitHub Actions 自动跑单元与集成测试、通过后自动部署到 Staging → 人工 QA 按清单总彩排(5 个商品的店能用,脱敏后 5000 商品的店呢?移动端呢?)→ 合入 master 触发生产部署。第 4 步本该挡住 Rohan 的 Bug。

数字:测试店铺约 15 个商品,事故店铺 2000 个商品;紧急回滚 10 分钟;Staging 使服务器成本翻倍。

关键结论:

  • 预发布环境是避免自伤型事故的刚需保险,相比一次线上事故的损失,成本几乎可以忽略。
  • Staging 必须是 Production 的镜像:硬件、软件、架构越一致,越能提前拦住真实世界的 Bug。
  • 绝对不要把原始生产数据直接用于 Staging:自动化灌入和脱敏,既保留真实规模,又保护隐私。
  • 部署流水线用可靠自动化替代混乱手工步骤,强制引入代码评审、自动化测试、人工 QA 多层质量关。
  • 好流程的目标是让部署变得无聊。无聊意味着可预测,可预测意味着可靠,可靠就是一切。

坑:最大的敌人不是流量和硬件,而是自己——“快速行动、先做出来再说”的文化最初让你起飞,后来却可能把你亲手烧掉。

第7章 速度即一切:用 Redis 做缓存

故事线:卖家跨过 100 万大关,挑战从可用性变成性能——“页面加载每慢 1 秒,转化率都可能明显下滑”。明星卖家 gavranmisal.com(普纳超火的餐饮商家)投诉店铺要 5 到 6 秒才开:“很多人甚至还没看到菜单就直接走了。这是在直接损失我的生意。“但监控一切正常:应用服务器 CPU 很少超 30%,读副本稳如泰山。系统”能扛住”和用户”觉得快”之间存在巨大断层。

关键决策:用 Django Debug Toolbar 摊开一次页面加载:渲染这一个页面,应用竟对读副本发起 114 次独立 SELECT——店铺详情、主题配置、分类、每个分类下的商品……且每个用户都从头完整做一遍,典型的”death by a thousand cuts(千刀慢死)“。而菜单一天只更新一两次,我们却让每个访客重新拼一遍整份菜单——“我们在不停重复执行同一项昂贵计算,而结果几乎总是一模一样。这就是低效的定义。“解法是缓存。适用判据:操作昂贵、被频繁请求、每次结果相同——只执行一次,把结果存进更快的临时位置。就像数学老师问 135 × 782,第一次掏计算器得 105,570,再问就张口而来。工具选 Redis:PostgreSQL 是基于磁盘的图书馆,大而永久但要跑到书架取书;Redis 是办公桌旁的大白板,抬眼就是答案——从 RAM 读比最好的 SSD 快几个数量级;代价是易失(重启即擦)且贵,但对本可随时重算的临时数据,这个权衡反而完美。数据模型是键值存储:key 如 store_catalog:gavranmisal.com,value 是打包好的完整 JSON。策略是 read-through 缓存:先 GET,命中直接返回;未命中就执行那 114 次查询组装数据、以 1 小时 TTL 写回 Redis 再返回。上线后第一个访客照旧慢(miss 并回填),之后一小时里每个访客都命中缓存、根本不碰 PostgreSQL。新问题:脏缓存。店主把 ₹150 改成 ₹120,写入主库,白板上却还挂着 ₹150,一小时内所有顾客都会飞快地看到错误的旧版本——“我们造出了一个极快、却可能对用户’撒谎’的系统。“这就是号称”计算机科学里最难的两个问题之一”的缓存失效。第一个想法是缩短狗绳:TTL 改 1 分钟——糟糕权衡:命中率是缓存核心指标,热门店铺命中率超 99%(1 次慢 miss 换几千次快 hit);改 1 分钟等于强迫每家店每分钟 miss 一次,重活从每小时 1 次变 60 次,把刚赢回的优势吃回去大半,“就像试图修一个漏水龙头,办法却是每分钟把总水闸开一下、关一下。太笨,也太低效。我们需要的是手术刀,而不是大铁锤。“真正解法是事件驱动的缓存失效,全用 PostgreSQL 内建能力:①products 表挂 Trigger——图书馆保险库门上的动作感应器,任何 INSERT/UPDATE/DELETE 都触发;②Trigger 执行 NOTIFY,向 product_changes 频道广播携带 store_id 的消息;③职责单一的小服务 Cache Invalidator 持续 LISTEN 该频道,收到消息即刻 DEL store_catalog:store-456。改价 → 主库 UPDATE → 触发 → 广播 → 缓存被删 → 下一个请求 miss 后用新价 ₹800 重建。“旧数据的幽灵,被彻底赶走了。”

数字:卖家数破 100 万;一次页面加载 114 次 SELECT;单次查询 5–10ms,114 × 10ms = 1140ms,光数据库时间就超 1 秒;页面加载 6 秒 → 200ms 以内;TTL 初值 1 小时(3600 秒),热门店铺命中率超 99%;135 × 782 = 105,570。

关键结论:

  • 速度就是功能。一个慢网站,本质上就是坏网站。缓存是提升应用性能最强有力的工具之一。
  • Redis 是缓存场景下极其优秀的工具:基于内存、键值简单,对临时数据比磁盘型数据库快几个数量级。
  • 缓存一定会引入数据一致性问题,仅靠 TTL 处理脏数据,通常既粗糙又低效。
  • 事件驱动的缓存失效是更高级也更正确的做法:底层数据一变,就主动删除对应缓存。
  • 别浪费数据库自带的高级能力:PostgreSQL 的 Trigger 和 LISTEN/NOTIFY,就是构建实时缓存失效系统非常强的内建基础设施。

坑:好的记忆必须搭配同样优秀的”遗忘能力”——只加缓存不管失效,性能方案会演变成真相错位的问题。