V2EX rayeaster 的所有回复 第 1 页 / 共 2 页
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX    rayeaster    全部回复第 1 页 / 共 2 页
回复总数  29
1  2  
第 5 章第 3 节:最终一致性里的旧数据幻影

无论从哪个技术指标看,我们新的技术架构都表现非凡。系统运行流畅稳定且能够服务 10 万用户。以系统工程视角来说,这确实是赢得了一场架构升级之战。但如果站在用户角度,新架构却引入了一个十分奇怪,甚至有些魔幻的让人困扰的新问题。

设想一位名叫 Priya 的卖家。她经营着一家专卖定制珠宝的精品小店。她登录 dukaan 页面然后视线停留在一款店铺里最火的项链(售价 1000 卢比)。她决定搞一次闪促把价格降到 800 卢比( 20%折扣)。于是她修改价格点击“保存”按钮。系统给出的响应非常及时:“商品信息修改成功!”

为了再次确认价格已修改,Priya 尝试向顾客一样点击“查看店铺”按钮。很快就看到了那款项链,但是价格仍是 1000 卢比。

她心头一紧。是刚才没有保存成功嘛?她回到店铺商家后台页面,那里显示价格的确已经改成了 800 卢比。于是她回到店铺页面,但是价格却显示仍为 1000 卢比。她有点被搞晕了,甚至会有点恐慌。是她的店铺出问题了嘛?她的顾客会因为错误的价格而多付了钱?她只好一遍又一遍刷新页面。1000 卢比。仍是 1000 卢比。在经历了 5 秒疯狂的页面刷新之后,价格终于变成了正确的 800 卢比。

Priya 刚经历的这种情况可以被称作旧数据幻影。这让 Priya 成为同步延时的受害者。她的“保存”操作被立马送到了主库写入。但是她的“查看店铺”操作被送到了读库,而此时读库的数据与主库尚未完成同步。

正如前面所述,这并不是代码层面的 bug 。这是我们高性能的新架构内在的一个特性。我们牺牲了实时一致性以换取高可用性。欢迎来到最终一致性的世界。

**深入技术细节:最终一致性**

要明白这个概念,可以先跟大家普遍期待的情形做个对比。
- **强一致性**:这是大家生活中都习以为常的情形。当在银行转账时,余额应该马上在所有分行都得到更新。也就是说写入操作完成后,所有之后的读操作都保证能读到新的数据。默认情况,单数据库单服务器提供的就是强一致性。数据正确性的来源就是唯一的那台数据库。
- **最终一致性**:这是我们身处的分布式系统的情形。系统保证的是在所有写入操作停止之后,所有数据副本*最终*都会完成同步。但不保证完成同步的所需时间。它保证的是一致性一定会达成,但是不一定很快。

这的确是某种折中方案。我们牺牲了即刻的强一致性,换来的是可以同时服务上百万读请求的能力。对于我们 99.9%的用户来说(浏览店铺页面的顾客群体),价格修改延时 1 秒完全可以接受,甚至大部分人都不会注意到这些延时。但对于剩下的 0.1%的提供数据修改的用户(像 Priya 那样的卖家)来说,1 秒的延时却是不爽或者完全无法接受的体验。

我们无法根除同步延时,因为它来自物理层面的限制。但我们必须得想个法子让用户免受其苦。

**深入技术细节:数据过期策略**

如何解决上述难题呢?既然无法让系统更快完成同步,那就只能让我们的应用变得更聪明些。

策略 1:啥也不做(如果这能被接受)

对于大多数功能,短暂的延时完全不会有任何问题。举个例子,如果我们的管理员后台显示总店铺数,那这个数字比主库最新修改延时 30 秒也没啥关系。这种策略要求主动精确地识别出应用中哪些部分需要强一致性,哪些部分可以容忍最终一致性。

策略 2:写后重读方案( VIP 通道)

这正是我们针对 Priya 面临的困扰所采用的策略。逻辑很简单:对于某个具体的用户,在其完成某次写入操作之后,立刻后续的读请求也一并发送到主库,虽然这违反了之前我们关于读请求送到从库的规则。

这就类似给了 Priya 一张 VIP 通行证:

1. Priya 点击保存项链的新价格:发送写入操作到主库。
2. 我们的应用此时成功完成了这次价格修改的写入,然后会在 Priya 的会话中设置一个临时的标志位(类似浏览器的 cookie )表示:“该用户在接下来的 60 秒窗口期内享受 VIP 待遇。”
3. Priya 改完价格之后马上刷新店铺页面:读请求发送至 dukaan 应用。
4. 我们定制的数据库路由服务收到了来自 Priya 的读请求,并发现会话中包括存在关于“VIP”的标志位。
5. 于是数据库路由会把这次请求直接发送到主库,而不是像对待其它用户那样发送到只读从库。
6. 因为主库总是拥有最新的数据,这次读请求会返回正确的 800 卢比。Priya 能够立马看到修改后的效果,这样她对 dukaan 的信心就不会因为最终一致性导致数据延时而削弱。
7. 一分钟之后,VIP 标志位在 Priya 的会话中失效。下一次读请求会如往常一样发送至从库,此刻数据同步应该早已完成。

这种方案让我们能够同时享受两种一致性模型的优势:提供给普通用户的始终可用,以及提供给关键用户的强一致性。

**第 5 章关键知识点总结**

- **用只读从库拓展数据库是性能巨大提升,但不是没有代价**:为了最终一致性(技术实现更复杂)牺牲了强一致性的简洁。
- **同步延时是物理层面的现实,并不是 bug**。但主库和从库之间总会有延时。这个延时没法根除,只能从应用层面尽量降低影响。
- **最终一致性可能会导致让人不爽且疑惑的体验**。如果在用户修改之后刷新页面仍然看到修改前的旧数据,那这可能会严重损伤对产品的信任。
- **实施一个兜底的“写后重读”策略**。对于刚完成数据修改的用户,可以临时把相关读请求直接发送到主库。这可以让关注实时一致性的用户得到强一致性的体验,也不会牺牲只读从库的可用性。
第 5 章第 2 节:保安和 VIP 通道

问题已足够清晰。我们的图书馆只有一个通道,海量的读者洪流在仅有通道的中排起了长队,导致作家们无法及时完成新书的注册登记工作。解决方案是新开设一个专用通道。这样我们的作家们就有了专属的 VIP 的通道,前来借阅的芸芸大众也会有更宽敞的读者大厅。
在数据库技术架构中,这种策略被称作数据同步。
深入技术细节:数据库同步机制
正如字面含义,同步就是为同一个数据库创建并维护数据副本的过程。相比之前我们只有总包总揽的唯一数据库,现在我们有了一组各司其职的数据库集群。我们采用的正是最常见的一种同步形式:主-从同步。
让我们暂时抛开图书馆的类比,设想一个网红酒吧的场景。
数据库主库:VIP 厅
主库就类似酒吧里只服务尊贵 VIP 的专属区域。所有数据的正确性以主库为准。

主库会处理所有写入操作( INSERT ,UPDATE 以及 DELETE )。酒吧状态的任何改变,比如新抵达了一位 VIP 顾客,在场的客人点了一杯酒,或者有客人离店了都需要通过主库来完成。就好像 VIP 厅门口站着一位很严格很干练的保安,它会确保所有状态改变都是符合规则且被如实准确地记录在案。
对 Dukaan 来说,主库就是卖家的访问入口。当店家更新商品价格,增加某种新商品,或者删除某样下架的商品,这些请求都会直接发送到数据库主库。这些操作都十分重要,所以会优先通过没那么拥挤的专享主库来完成。

只读从库( Slave ):酒吧大厅
只读从库就如同酒吧里服务普通顾客的大厅。从库是 VIP 厅(主库)所有改变的完美即时复制,允许所有人随时访问。

从库只允许读操作( SELECT )。成千上万的顾客可以同时出现在宽敞的大厅里,享受音乐,欣赏周围的一切。他们可以看到 VIP 厅的活动,但是无法做出任何“写入”操作。
从库的任务就是承载巨大的读取流量。由于有了从库帮助承担那些只是“随便看看”的读请求,主库的资源就被解放出来得以去完成重要的写入操作。如果流量实在太大, 甚至我们可以同时拥有多个只读从库,就如同多个大厅。

我们需要的架构升级就是如上所述的职责分离。这样可以让我们单独地拓展数据库读取和数据库写入操作。
深入技术细节:实施数据库同步
理论听上去非常不错,具体应该如何操作呢?酒吧大厅里的人们该如何即时得知 VIP 区域发生的事情呢?
PostgreSQL 的流式同步机制
PostgreSQL 内置了超级好用的流式同步功能。

WAL (预写日志):我们的主库( VIP 厅)有一位勤勉的保安,它在一本特殊的日志中事无巨细地记录下了所有发生的变化。来了一位新顾客?记下来。价格变化了?记下来。这样一本日志被称作预写日志( WAL )。本质是按时间排序且实时变化的数据库完整修改记录。
数据流:我们建立一个只读从库把它配置成连接到我们的主库。从库的第一条指令便是“订阅 WAL”。然后主库开始像打开的水龙头一样哗哗地把预写日志里的每一条记录通过安全的专属网络连接实时复制到从库。
从库更新:制度从库收到主库发来的数据修改之后,会严格按照原有顺序一条不差地复制到自己的数据副本上。

以上流程的结果便是从库近乎完美地镜像了主库。就好像 VIP 厅的活动通过视频流转播到酒吧大厅巨大的屏幕上,让所有人都能实时看到。
更新 Django 应用以便使用只读从库
设置好数据库同步只完成了任务的一半。我们 Django 应用代码还不了解发生了什么。它现在仍然只能够与一个数据库通信。我们需要让它变得更聪明,就像酒吧保安那样决定谁能进 VIP 厅以及谁只能去大厅。
我们的应用代码需要若干主要的改动:

配置多个数据库连接:我们首先需要在 Django 的配置里从一个数据库连接增加到两个数据库连接:默认的( default )连接至主库,只读连接( read_replica )至新创建的制度从库。
创建一个数据库路由:接下来我们需要定制一个“数据库路由”。这是 Django 中一段特殊的代码,它负责拦截每一个数据库请求然后决定把该请求送往哪一个数据库。逻辑很简单但至关重要:

# 我们数据库路由的简化版本
class PrimaryReplicaRouter:
def db_for_read(self, model, **hints):
# 所有只读操作都送到从库
return 'read_replica'

def db_for_write(self, model, **hints):
# 所有写入操作都送到主库
return 'default'

有了数据库路由,现在我们的应用代码变得更聪明。每次当用户加载店铺页面(触发一系列 SELECT 数据查询),路由代码会把这些流量都发送到强大的只读从库。当卖家点击按钮保存新商品时(触发 INSERT 或者 UPDATE 写入操作),路由代码会把该写入操作发送到受到严密访问控制的主库。
这些改变实施之后的效果立竿见影且显著。商铺页面瞬间能完成加载。店家们反馈保存信息修改也很快。我们主库的 CPU 和输入输出负载恢复了正常。我们成功地拓展了数据库。
不可能三角:CAP 理论
数据库同步实施之后,效果就如同魔法。主库负责写,从库负责读,整个酒吧的人流瞬间变得通畅无比。卖家再也不必因为突然一波顾客流量暴涨而拖慢修改商品名录而头疼。顾客们也可以随意浏览店铺页面,而无需因为页面无法加载反复刷新。看上去我们找到了完美的系统。
但分布式系统的完美从不是免费的。
计算机行业里有一个很经典的理论,之前曾被我忽视,但现在成了我每天都必须面对的:CAP 原理。
CAP 表示一致性( Consistency ),可用性( Availability ) 以及 分区容忍( Partition olerance )。CAP 原理意为,对于分布式系统,上述 3 项性质只能 3 选 2 ,不可能 3 项同时满足。

一致性意味着酒吧所有人都能同时看到同一个状态。比如 VIP 厅更改了背景音乐歌单,那大厅的人们也能立刻听到相同的新歌曲。
可用性意味着酒吧永不打烊。无论发生什么,任何顾客任何时候来到酒吧都能得到应有的服务,即所有请求都可以得到某种响应。
分区容忍意味着即使走廊被阻塞,酒吧也可以继续营业。又或者连接 VIP 厅与大厅之间的音响设备有一些小故障,但是不影响酒吧接着奏乐接着舞。

关键在于:现实世界中分区必然存在。有时候是网络中断,网络包丢失,光缆损坏等等。所以在分区存在的情况下,现实的分布式系统必须要在一致性和可用性中 2 选 1 。
当我们引入只读从库时,虽然我们可能尚未意识到,但实际我们已经做出了选择。酒吧大厅(从库)在即使与 VIP 厅(主库)不同步的情况下仍然会继续保持营业,为众多顾客提供服务。结果就是大厅显示的可能是过时的信息。
具体例子
比如一个卖家在 VIP 厅(主库)修改某款裙子的价格:从 1000 卢比降到 800 卢比。主库马上记录下这条修改。

如果下次请求直抵主库,顾客将看到修改后的数据:800 卢比。
如果请求在数据同步复制之前就到达从库,顾客看到的仍是旧的价格:1000 卢比。

这两个价格可能都算是“合理”的,取决于所处是 VIP 厅还是大厅。但是从卖家角度看,这显然有问题。价格刚才已经被修改了,为啥店铺仍然显示旧的价格?
CAP 为什么重要
CAP 不只是书本上遥远的理论,它是添加从库,分发数据或者跨区域同步时必然需要处理的隐型不可能三角。当我们开始拥抱数据同步,我们就一定会碰到某些读取会与之前的写入没有及时同步。这不是代码错误( bug )。CAP 理论提醒我们分布式系统里没有银弹,为了某些目的却总得选择一款毒酒喝下去。
一致性的阴影
如果你认可 CAP 理论,下一个问题来了:如果不能同时满足 CAP 的 3 项,那我们所需的一致性到底是什么?实际答案可能不止一种。分布式系统的一致性要求各有不同,每一款分布式软件也都依赖各自设计哲学侧重做出了不同的选择。
以下是常见的 3 种类型:


强一致性
这是人们直观上最容易理解的情况。如果卖家把价格改成了 800 卢比,那么此后任何读取,无论请求发送至哪个服务器(主库或者从库),都必须返回相同的 800 卢比。
在酒吧的类比中,VIP 厅的 DJ 一旦修改背景音乐曲目,大厅里的人们也会立刻毫无意外地听到相同的新歌曲。
强一致性让人感觉一切有条不紊,但代价是可用性。如果 VIP 厅和大厅之间的联络即使短暂被阻塞一小会,整个酒吧 也会宁愿暂停营业,也不想让人们听到“错误”的歌曲。


最终一致性
这种情况就是只读从库发挥的场景。VIP 厅的变化会尽快传播到大厅,但也许会稍有延迟。如果倒霉的话,可能会先仍听到一段旧歌曲的旋律,最终才能听到新歌的响起。
从用户角度看,这也许令人困扰:刚明明已经保存了新数据修改,但是店铺页面仍然显示的旧数据。虽然一段时间之后,最终所有数据同步都会完成,所有显示都会一致,但是这个“一段时间”可能是 1 秒,也可能是 5 秒,无法准确得知。


因果一致性
这是一种试图维护因果顺序的中间方案。比如某个名叫 Priya 的卖家给她家卖的项链降价了然后查看自家店铺页面,因果一致性可以保证她自己是能够马上看到降价之后的价格,但是其它用户却不一定能马上看到。
在酒吧的类比中,如果 DJ 修改了播放曲目,VIP 厅里见证这个改动的人们都能立马听到新曲目,但是大厅里的人们却不一定能即刻同步听到。
虽然因果一致性不保证完美地全局同步,但它保证“我修改了某项数据,我立马能看到修改后的结果。”


选择适合的方案
不同的系统做出的选择各有差别。银行系统需要强一致性,比如顾客肯定不愿意看到某个分行显示余额有 10000 卢比,但是另一家分行显示余额只有一半。社交网络应用可能更倾向最终一致性,比如给你点赞的计数延时了几秒钟,大家都能接受。因果一致性在面向终端用户的应用中越来越流行,因为它考虑到了用户个人即时的期待从而做出了一定平衡。
对于 Dukaan ,我们通过只读从库建立了一个实现最终一致性的系统。这就是卖家 Priya 有时候看到旧数据的原因。这并不是 bug ,而是教科书上关于最终一致性的经典场景。
但如同每一个改进,新架构引入了一个新的不易察觉的潜在危险副作用。
新问题:同步延时
主库到从库的同步数据流虽然很快耗时很短,但的确不是“瞬间”。一般总会有毫秒级别的延时。在高负载情况下,延时甚至可能飙升到 1 秒或者 2 秒。这个延时被称作同步延时。
这意味着大厅的人们接受到的 VIP 厅状态变化比实际发生总是晚了一点点( 1 秒之内)。
这会导致一系列潜在的令人困惑的问题。比如当店家把某款商品价格从 100 卢比降到 90 卢比(送往主库的写操作)之后马上刷新店铺页面(发送到读库),结果看到仍是修改前的 100 卢比,因为此时数据同步尚未完成。这种情况怎么办?
这就是最终一致性导致的令人困惑的危险局面。
第 5 章:数据库俱乐部的保镖:只读副本

作为创业公司,当用户数从几千达到 10 万级别的时候,业务重心会发生显著变化。早期主要关注的是冷启动获客以及确保刚上线的服务能正常工作。出现的问题也多半是显而易见的,比如服务器宕机,网站应用崩溃了。解决方案也比较简单粗暴:重启服务器,或者加钱买更好配置的服务器。

但是当用户数达到 10 万的里程碑之后,新类型的问题就会出现。肉眼可见的火情会被缓慢悄无声息的发热取代。系统可能不会突然崩溃,但是可能会逐渐力不从心,变得愈发迟缓。问题重心不再是有没有顾客用,而是如何提高性能满足更多用户的需求。所以相应的解决办法不再那么简单粗暴,需要更多如外科手术般的精准下药。就像盖房子,此时不能仅满足于屋里的灯还能亮,而是要开始思考建筑物本身的结构是否足够稳固等更深层的问题。

我们通过水平拓展的服务器集群以及负载均衡技术成功解决了厨房产能的问题。现在我们面临的问题是储藏室/图书馆开始变得非常拥挤以至于甚至让人都迈不开步。

**第 1 节:图书馆里的交通阻塞**

用上负载均衡之后的确很爽。应用层的流量处理充满技术美感。当我们实时监控到流量和应用服务器 CPU 占用率暴涨的时候,我们只需要点击几下鼠标就能增加一个新服务器到集群然后就能看到工作负荷神奇地被自动分配到新加的服务器,然后其它服务器的负载就会降下来。应用服务层面完全可拓展,一切尽在掌握。

我们的用户数很快就彪过了 5 万,接着是 8 万,迅速接近起初难以想象的 10 万大关。每个店铺卖家都拥有各自的顾客群体,意味着浏览 Dukaan 网站的用户人数可能是百万级别的。我们正在服务的流量已经远超预想。

熟悉的恐惧感又悄悄回来了。我们又开始收到来自用户的抱怨,但这次不是关于网站宕机了,而是响应太慢了。

- “我的顾客需要等待 5 到 6 秒才能看到我家店铺”
- “有时候当我保存一个新商品的时候,网页要加载很久才能保成功。”

这种网页响应迟缓的情况在印度日常工作高峰时间段(上午 11 点至下午 5 点)达到顶峰。我和苏米特目不转睛盯着监控图表。应用服务器看上去表现还行,CPU 的工作负荷分配均衡且极少超过 50%。负载均衡服务工作地很出色。

但我们那台数据库服务器的性能图表却是完全不同的情景。CPU 占用率基本维持在 80%到 90%的高位。硬盘输入输出数据已经爆表,表明硬盘已经达到基线负荷了。曾经扮演我们救星的独立强大的数据库服务器现在正被负载压得喘不过气。我们的图书馆已经满满当当挤满了人,我们的英雄图书馆管理员变得不堪重负。

**确定瓶颈:只是看一看的人数实在太多了**

简单地说“数据库响应太慢了”就如同一位医生说“这个病人确实生病了。”一样毫无意义。这不是精确的诊断,只是表面的观察。如果要实现有效治疗,必须搞清楚具体明确的病因。我们需要深入数据库内部,确认它正为之头疼忙碌的具体任务。

**深入技术细节:区分数据库操作(只读与写入)**

说到底,数据库主要有两种不同的任务类型,理解它们之间的差异非常重要。

1. **写入**:**改变**的数据的操作。主要指令包括 INSERT (增加新数据),UPDATE (修改已有数据)以及 DELETE (删除数据)。
- 类比:可以把这些想象成图书馆管理员调整藏书的工作。INSERT 就是新书到库。UPDATE 就像图书馆管理员修改索引目录卡片上的一个错别字。DELETE 类似从书架上移除一本年代久远且已损坏的旧书。
- 这些操作很关键。必须谨慎处理避免伤及馆藏图书的完整性。通常来说,这些写入需要“某种锁机制”确保不会有两个图书馆管理员同时修改同一个信息。所以一般这些操作相对处理速度不会太快,并且更耗费资源。在 Dukaan 场景里,这些操作对应的就是店家增加商品,修改价格或者顾客下了一个订单。

2. **只读查询**:这是只涉及**读取数据**的操作。主要指令是 SELECT 。
- **类比**:就像一位普通市民走进图书馆想要查阅某本馆藏书籍。它并不会修改任何信息。只是查找书籍,阅读,然后把书籍放回书架。只是信息的消费。
- 这样的操作一般都比写入处理速度更快,也消耗更少资源。对于 Dukaan 来说,这就对应着顾客浏览店铺和商品名录产生的流量洪流。

我们为了确定解决问题的关键方向特意安装了一款分析数据库请求的工具。我们发现我们面对的问题在互联网应用行业十分常见,甚至大家都给这个现象专门起了一个名字。

**95/5 法则(读写差异)**

我们的分析揭示了令人的不平衡状态。在数据库每 100 次请求中:
- 95 次是 SELECT 只读查询;
- 只有 5 次是 INSERT ,UPDATE 或者 DELETE 写入操作。

这其实很符合我们的观察。一位店家每天可能只会更新他们的商品(数据写入)寥寥数次,但是它的店铺会被数以千计的顾客反复查看(数千次只读操作)。我们的系统承载的绝大部分流量都来自只读请求。

**只读操作如何拖累写入操作**

这就是我们面临的问题核心:我们的数据库按照同等优先级处理只读和写入。它只有一个任务队列。

设想我们的图书馆管理员如何面对只有唯一入口以及唯一等待排队队伍的场景。在排队的人群中,95%只是想问“我在哪能这本书?”(这其实是一个很快的只读查询)。但同时剩余的 5%可能是作家群体想要完成新书的馆藏登记,这样的处理请求通常需要填写很多表格并更新庞大的书籍索引(类似更慢的写入操作)。

作家们不得不与常规入馆的庞大读者等在同一个长长的队伍中。简单的数据只读查询请求量实在太大以至于产生了堵塞反而拖累了关键的数据写入请求。这就是问为什么店家保存新商品的时候会感觉很慢很慢,因为他们重要的写入操作已被淹没在成百上千来自匿名顾客对店铺的浏览请求之后,完全得不到被数据库处理的机会。

解决方案也比较明确了。不能让所有人都挤在同一个队伍里。需要为作家们单独创建一个专享专用的通道,同时为进馆借阅的众多读者提供一个更宽敞的空间。意味着我们需要把只读操作与写入操作分离开来。
第 4 章第 3 节:我们的第一位交警


前面讨论的理论已经很完备。我们制定了一个构建服务器集群的计划,还需要一个负载均衡服务来分配流量。现在该撸起袖子把这些理论变成现实了。
首要问题是负载均衡服务该用哪款软件呢?可供的选择有很多:既可以花大钱买专用于负载均衡的硬件,也可以购买由云服务商提供的产品,比如亚马逊的弹性负载均衡( AWS Elastic Load Balancer )。但我们还只是一个预算非常有限的创业小作坊。我们需要的负载均衡服务不仅要强大可靠,最好还能是免费的。
最佳选择已在眼前,就在我们的服务器上。
深入技术细节:Nginx 作为负载均衡
目前我们主要使用 Nginx 作为网络服务器,它是一个称职的服务员,可以效率很高地返回静态文件给用户并把网络请求转发到 Django 应用。其实,Nginx 同时也可提供世界级的负载均衡服务。只需要在配置文件里面加上几行,我们就可以让我们的服务员同时变身为聪明的超市经理。
这对于我们绝对是巨大的好消息。因为我们无需学习然后安装新的复杂软件来实现负载均衡。我们能够继续信任手头熟悉的工具:Nginx 。
实现起来确实非常简单。我在 DigitalOcean 再次购买了一款同配置(每月 5 美元)的服务器。现在我们就拥有了两位可以同时炒菜的厨师。然后我 SSH 登录到我们的域名( dukaan.app )指向的第一台服务器。这台服务器现在多了个角色:作为负载均衡服务器。
我打开 Nginx 配置文件(/etc/nginx/nginx.conf )并添加了两小段配置。
我们的 Nginx 负载均衡配置

# 定义处理应用逻辑的应用服务器集群。这个集群被称为"app_servers"。
upstream app_servers{
# 这就是见证奇迹之处:Nginx 将依据这条配置采用前述的最少连接算法(流量会被送给拥有最少连接的服务器)
least_conn;

# 列出应用服务器集群的内网 IP (出于安全和效率考虑)
server 10.132.2.31; # 我们第 1 台应用服务器
server 10.132.4.55; # 我们第 2 太应用服务器
# 如果需要拓展,这里可以添加更多服务器
}

server{
listen 80;
server_name dukaan.app;

location / {
# 以下这行配置就足以让 Nginx 把所有流量转发到上面我们定义的应用服务器集群"app_servers"
proxy_pass http://app_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

好了。上面配置中的 upstrem 部分定义了我们的应用服务器集群。least_conn;那行指定了我们智能的流量分配策略。proxy_pass 指令让 Nginx 开始做流量分配。保存并重启 Nginx 之后,我们的负载均衡服务就已经上线开始运行了。
架构更新
我们的系统架构再次演进。现在用户流量走向更复杂一些但也更具韧性。

用户访问 dukaan.app 网站。请求到达作为负载均衡的 Nginx 。
负载均衡服务检查两台应用服务器看看哪台当前活跃连接数量更少。
接着请求会被转发给有较少连接的那台服务器(比如 2 号服务器)。
2 号服务器运行着 Django 代码,需要数据来处理分配给它的用户请求。
2 号服务器连接到我们唯一那台数据库服务器读取所需数据。
处理完毕后,响应被原路返回给用户。

如果 1 号服务器宕机了,Nginx 作为负载均衡服务将通过健康检查及时发现这种情况并停止转发流量给 1 号服务器。接下来所有流量将全部转发给 2 号服务器。网站此时仍然在线。系统实现了一定程度的容错性。流量爆发和服务器宕机都能应对自如。此刻我们又感觉膨胀了。
新问题:图书馆开始拥挤了
启用负载均衡之后,网站确实完美地工作了一段时间。当流量增长时,我们也不会很慌。我们只需要购买第 3 台服务器,把它的内网 IP 加到 Nginx 的 upstream 配置里面然后重新启动 Nginx 就好了。分分钟,我们就可以雇佣新的厨师开始干活。
如果有 10 个厨师同时在炒菜,但他们都很焦躁,怎么办?
因为他们都需要食材。他们只能同时跑到同一个储藏室,对着同一个管理员咆哮希望尽快拿到属于自己的食材。
瓶颈再次转变:不再是单台服务器导致的 CPU 处理能力(通过水平拓展暂时解决了),而是上一章拯救我们的英雄,那只单台数据库巨兽。
当一整个服务器集群都在疯狂请求数据时,数据库服务器也开始力不从心。数据库服务器的 CPU 占用率开始攀升,数据库查询速度开始变慢。
我们的厨房已经完成了扩容,但我们的储藏室/图书馆仍然只有一个房间,一个管理员,而且即将过载。
第 4 章关键知识点总结

水平拓展是可以同时实现高可用和大规模的唯一长期技术路径。虽然比垂直拓展更复杂,但是性价比更高,更灵活,并且能消除单点隐患。
负载均衡是实现水平拓展的必需网络交警。它能够为一个服务器集群分配流量并且具备容错能力。
尽量从简单选择入手。强大的 Nginx 可以同时作为网络服务器和负载均衡服务,帮你降低起步的复杂度。
“最少连接”可作为默认的负载均衡算法。相比简单的轮询算法,它能够更公平地分配工作负荷。
技术瓶颈总会变化。刚解决某个性能问题之后,负载就会自然往下一个最薄弱环节聚集(麻绳专挑细处断)。现在我们的应用服务器暂时不再是问题,我们的数据库将燃起新的火苗。
第 4 章第 2 节:网络交警

水平拓展的决策是我们创业路上一个重要的转折点。我们将从单服务器转向集群模式。但是如果没有能有效调动军队的指挥官和指令系统,那再大的集群也没用。我们现在已经拥有了随时可以开工的一群厨师,还需要一个懂得如何为厨师分配顾客点单的首席服务员。
在技术场景中,这位首席服务员,亦或可作为交警的角色,是由负载均衡扮演的,它是整个体系至关重要的一环。
深入技术细节:负载均衡是什么?
其实就按字面意思理解就行了。负载均衡就是前置于应用服务器集群的一组服务,它的唯一工作就是在应用服务器之间均衡地分配请求流量,不会让某个应用服务器承担太多。
对于外部用户来说,负载均衡服务就是要访问的网站本身。所有用户都访问同一个指向负载均衡服务的域名(比如我们的 dukaan.app )。用户完全不知道在那个域名背后可能是一个拥有着 2 台或是 10 台甚至是 100 台服务器的集群正在准备处理请求提供响应。负载均衡就像一扇大门,把厨房里的复杂忙碌都遮挡了起来。
更好的类别是,负载均衡服务就像繁忙超市里的当班经理。
设想顾客已经排起了长龙(网络流量)等待付款。如果结账柜台只有一个工作人员,那排队的人很快就会越来越多。顾客会因为等待变得越来越焦躁,结账的工作人员也会焦头烂额。
现在超市经理决定再新开四个结账柜台(类似我们的应用服务器集群)。但顾客不是随机挑一个柜台去结账,我们聪明的经理会站在柜台前,积极地引导队伍里下一个顾客去到当前空闲的结账柜台:

“您好先生,请到 3 号柜台结账。”
“您好女士,柜台 1 现在空出来了,您可以到那边结账。”

可以看出来,这位经理就扮演了类似负载均衡的角色。他们的工作都是确保每一个结账柜台的工作人员不会超负荷,也不会有人因为没有顾客前去结账而无所事事。他们为客流削峰填谷,保证系统高效运行。负载均衡同时也负责健康检查。如果某个工作人员突然晕倒(类似服务器宕机),经理会立刻停止给他再安排结账顾客,而是会把顾客引导去其他正常工作的柜台。这样系统就能保持稳定的运行。
深入技术细节:负载均衡算法
超市经理需要一套规则(或者说某种策略)来决定下一位顾客前往的结账柜台。在负载均衡的世界,这些规则被称为算法。有的算法可能非常复杂,我们这里只需要搞明白两种最常见的类型:

轮询( Round Robin )分配:简单而蠢萌的方法

这是最基本的负载均衡算法。也很容易按字面理解:简单的循环分配请求给所有的服务器。

第 1 个请求分配给服务器 A 。
第 2 个请求分配给服务器 B 。
第 3 个请求分配给服务器 C 。
第 4 个请求再次分配给服务器 A 。
。。。以此类推。

就像给一群玩家发扑克牌。每位玩家都会轮流得到一张牌。

优点:超级简单,基本无需负载均衡服务额外思考。
缺点(蠢萌的部分):轮询假设每一个请求都大差不差,每一个服务器也都有类似的配置。但如果发送到服务器 B 的第 2 个请求是耗时 10 秒的复杂任务,而其它请求都是耗时 1 秒的简单任务呢?轮询可不管这些。即使此时服务器 B 仍然正在为之前那个复杂的任务忙碌,它仍会继续把第 4 个和第 5 个请求发给服务器 B ,而全然不顾服务器 A 此时可能完全无活可干。这会导致服务器之间工作负荷的分配不均。


最少连接( Least Connections )分配:更聪明的方法

这是一种更聪明的动态算法。负载均衡服务会实时监控集群中每一个应用服务器当前有效连接的数量。当新的网络请求到达时,负载均衡服务会把它送到当前连接数量最少的服务器。
这就像超市经理学聪明了,不是简单地按照编号告诉顾客该去哪个柜台结账,而是会根据排队人数多少给顾客挑一个人数最少的柜台。

优点:这种方法天然考虑到了有些请求会处理得比其它请求慢一些的情况。如果某台服务器正忙着处理复杂的请求,它会同时维持更多的用户连接,所以负载均衡会暂时不考虑继续给它新增任务,以便它尽快完成手头的活。这让工作负荷的分配更加公平也更有效率。
缺点:负载均衡的工作会稍微复杂一些,因为它现在不只是傻傻地按照服务器列表做轮询,而是需要时刻关注每个服务器的连接数量。

对于 Dukaan 网站来说,我们的选择已然明了。“最少连接”算法是更聪明更健壮办法,能够更好地应对我们不可预知的用户流量。
现在理论部分已经清楚了。我们的网络交警有了一整套分配规则。是时候付诸实际了。我们需要一个合适的负载均衡工具并用它来管理我们全新的服务器集群。
第 4 章:网络世界的交警:走进负载均衡

第 1 节: 崩溃的厨房
在创业旅程中解决第一次重大危机之后的一段时间通常比较暗流涌动,往往预示潜在的危险。
数据库分离之后,我们的系统运行良好,忙碌而有序。应用的响应体验很快,服务器也很稳定,以至于我们竟然产生了某种错觉:我们的架构足以应对未来可能出现的业务量爆发。就好像我们成功为网站的基础架构实施了开胸手术,让它起死回生。甚至是妙手回春。
我们的日常工作也从慌乱的紧急救火转向充满乐观的系统巡视。我们可以实时看到用户数量在攀升,关注服务器的负载图表(系统压力令人喜悦的正常且运行稳定),这些都让我们感到颇为满足。我们成功构建了一套真实有效能解决用户痛点且能实现业务增长的电商系统。我们似乎在某一瞬间觉得自己所向披靡。
不出意外的是,出乎我们意料之外的下一场危机悄然开始。
这次不是之前那样缓慢的性能下滑,而是一次突然的爆发。苏拉特市( Surat )的一位售卖精美手作纺织品的网红店家,在一个人数众多的脸书( Facebook )聊天群里分享了她家的 Dukaan 店铺链接。与此同时,还有一位技术博主也撰文提到了我们的公司。这两次曝光合力形成了一次完美的增长风暴,让我们体验了从未见过的海啸般的流量。
我的手机开始发出熟悉的警报嗡嗡声。这次不是苏米特打来的电话,而是来自我们的服务器监控系统。“应用服务器 CPU 占用率超高。”其中一条警报显示。一分钟之后,另外一条警报显示了相同的内容:“危险:CPU 占用率已持续 5 分钟达到 100%。”
几乎同时,我还没得及打开笔记本电脑,苏米特的短信就发过来了。短信内容简短,熟悉的沮丧溢于言表。
“发生什么了?”
我马上 SSH 登录进服务器。手指飞快敲下我最信任的诊断工具:htop 。我首先检查的是数据库服务器。看上去一切正常。CPU 占用很低,内存使用也没正常。我们新建的图书馆表现安静从容,井井有条地处理着接收到的所有书籍(数据)查询请求。
接下来我继续检查应用服务器。然而这里就完全是个血流成河的场景了。CPU 占用率一直处于 100%。进程列表放眼望去,已被 Gunicorn 的工作进程几乎占满,它们面对如洪水般涌入的用户请求,正陷入不断重试然后失败再重试的恶性循环。应用服务器已经完全过载了。虽然还没宕机,当时已经几乎毫无响应。对于外部用户来说,Dukaan 网站再一次崩溃了。
确认瓶颈:独自面对 1000 位顾客的主厨
原因很明显:我们的厨房扛不住了。
延续前面几章的类比,我们之前的优化是在厨房边新建了一个现代化的储藏室/图书馆。这样我们的主厨不再需要担心储藏室的管理难题。现在的问题是,有 1000 名顾客同时挤进了餐厅,每一个都高声叫嚷着希望主厨先做自己点的菜。
我们唯一的主厨(应用服务器)尽管此时已经因为数据库的分离变得更加高效,但面对 1000 名顾客的同时点餐,仍然力不从心。现实世界中,一位厨师同时能准备的菜肴显然是很有限的。对于我们的应用服务器来说,它已经触碰到了自身的能力天花板。点餐的排队甚至都要排到了餐厅门外,整个餐馆不得不暂停取号。
很明显我们需要更多的厨师。怎么办?这就引出了每一个业务快速增长的公司都需要面临的关键抉择。这是方向迥异的两条技术路径:垂直拓展或者水平拓展。
深入技术细节:垂直( Vertical )与水平( Horizontal )拓展
当服务器不堪重负时,通常有两个选择。

垂直拓展(性能升级 Scaling Up )

这可能是一种最容易想到的办法。如果厨房动作太慢,那就把厨师换成一个能以两倍效率工作的世界顶级大厨。
对于服务器而言,就是性能升级:在 DigitalOcean 上面点击关闭当前服务器的按钮。然后选择一个更强大的付费套餐:用 8 个 CPU 加 16GB 内存取代 2 个 CPU 和 4GB 内存。完工,现在应用就运行在一台性能猛兽上了。就好像把家用小轿车换成了一辆超大型的卡车巨兽。

优点:操作很简单。无需更改代码或者架构。只需要多花点钱就可以让问题大事化小。
缺点:这个方案有 3 个不足:

很容易导致开销飙升。两倍性能的服务器的价格不一定是两倍。有可能是 4 倍或者 8 倍。价格往往指数增长。
这种方案上限不高。服务器的性能总有极限,不可能无限升级。最终云服务商所能提供的最强大最贵的服务器也无法满足需求。那到时该怎么办?没有更大的巨型卡车可供选择了。
再强大的服务器也是一个单点( Point of Failure )。这是最关键的不足。虽然拥有一台看上去足够强劲也足够贵的服务器,但如果它发生硬件故障,或者因为系统安全补丁需要重启,整个业务都跟着下线了。这就像整个餐厅都完全依赖唯一的一位超级大厨。如果大厨生病了,餐馆就得关门歇业。




水平拓展(服务器集群 Scaling Out )

这个方法虽然不如前者那样容易想到,但却是更加强大的一种选择。与其重金重新聘请一位超级大厨,不如保留当前的厨师然后再请 3 位类似水平的厨师。这样让他们同时一起工作也能加大厨房的产能。
对于服务器而言,这就是集群策略:用一个小服务器的规模集群替代单一的大服务器。也就是用 4 辆常规的家用轿车替代巨兽卡车。

优点:

性价比很高。小型服务器的硬件通常不贵且易于替换。增加同配置的小型服务器成本可控,不会很离谱。
理论潜能不设限。如果需要更多处理能力,再增加一辆小轿车就行了。可以轻松地从 4 台服务器拓展到 40 台,甚至 400 台。横向拓展的系统就天生适合如此。
容错率高。这是水平拓展的超能力。即使有 1 位厨师因病回家(一台服务器宕机),剩下 3 位也能继续工作。这样虽然出餐会慢一点,但至少餐馆不至于关门歇业。单点隐患不复存在。


缺点:系统会变得更加复杂。比如当有服务员拿着顾客新点的单来到厨房时,那这个菜该交给厨房里四位水平相近的哪位厨师来处理呢?这个决定该如何做出?

我们的选择显而易见了。垂直拓展更像一个创口贴式的临时方案。它不适合作为长期的技术选择。我们希望建立的是可以服务数百万用户的公司,所以我们需要一个能够随着业务增长而演进的技术架构。水平拓展是我们的必修课。
既然决定了,那就得开始打造我们自己的服务器集群。也就意味着我们需要解决新架构带来的新问题。我们需要一个能够在厨师团队中高效合理分配顾客订单的新系统。
我们需要一个流量交警:负载均衡。
第 3 章第 4 节:分叉口-我们为什么坚持使用 SQL

如前所述,我们成功实施了数据库与应用服务器的分离。PostgreSQL 数据库现在拥有专属的强大服务器,不再与应用代码逻辑挤在一起。这对于关系型数据库而言是一个经典的拓展升级。
但在现代技术环境中,一个因此自然而生的问题是:为什么一定要用传统的关系型 SQL 数据库呢?换言之,为啥不考虑更快,水平拓展更容易的 NoSQL 数据库呢?比如我们常听到的一些流行选择:MongoDB 或者 Cassandra 。
这其实是我们刻意的技术路线选择。要明白其中原因,需要搞清楚数据库世界里的两个基本哲学。
深入技术细节:两个数据库的平行宇宙
我们有两个平行的数据库世界:SQL 以及 NoSQL 。这两个类别都包罗万象并且十分强大,但他们内在的运行逻辑迥异。

SQL 的宇宙(关系型数据库):
这个宇宙中包括广为人知的 PostgreSQL ,MySQL 以及微软的 SQL Server 等。


类比:SQL 数据库就像一个清晰明了的 Excel 文件,其中包含了多个互相关联的表格。
核心概念:结构和一致性。数据存储在有着严格类型和列定义的表( Table )中,比如一个 price 数据列必须保存的是数字类型,一个 created_at 数据列必须保存的是时间类型。各表之间的逻辑关系被严格约束。所以不可能出现一个商品属于一个不存在的店铺的情况。
超能力:ACID 原则。这是一整套确保业务逻辑绝对正确的可靠保证,包括原子性( Atomicity ),一致性( Consistency ),隔离性( Isolation )以及持久性( Durability )。在电商语境里,意味着如果顾客下单买了 5 样东西,数据库要么(成功情形)会在顾客订单记录中完整保存 5 样已购商品并且更新相应商品的库存,要么(失败情形)就干脆什么也不记录。绝不会出现数据前后不一只保存了某个部分的情况。
最佳场景:任何对数据完整性和一致性有着苛刻要求的应用。这显然包括电商,银行,金融系统或者任何中介预定平台。


NoSQL 的宇宙(非关系型数据库):
这个宇宙会更加多元化,主流的选择包括 MongoDB (文档型),Cassandra (超宽数据列),Redis (键值对)以及 DynamoDB 。


类比:NoSQL 数据库就像一个文件目录,其中塞满了各式各样的 Word 文件或者 JSON 文件。每个文件都可能拥有完全不一样的内容结构。
核心概念:灵活性和可拓展性。NoSQL 对于要保存的数据不做任何假设。比如某个商品的文档里面可能包含“颜色”这个字段,但并不是每个商品文档都有这个字段。这样的特性有利于在不做数据约束关系调整的情况下修改应用的功能。通常这些 NoSQL 从设计之初就适合做水平扩展(可以运行在多个廉价的服务器上)。
超能力:BASE 以及水平拓展能力。大部分 NoSQL 不强制严格的 ACID ,而是提供一种成为 BASE 的替代:常可用( Basically Availabel ),弱状态( Soft State )以及最终一致性( Eventual Consistency )。这意味着这类系统优先考虑的是系统可用,而不是时时刻刻的一致性(这个概念将在后面提及只读副本时继续讨论)。这有助于 NoSQL 以超高的数据写入速度进行海量数据的处理。
最佳场景:海量数据处理,社交媒体的信息流,物联网的传感器数据,实时的数据分析或者那些数据约束关系经常变化的应用。

快速总结一下 SQL 和 NoSQL 的不同点:

特性 SQL ( PostgreSQL ) NoSQL (比如 MongoDB )
数据模型 结构化的(数据表/一行行的数据) 灵活的(文档类型,键值对类型)
模式( Schema )需要提前定义好,严格遵循 动态可变的
拓展性 垂直拓展为主(更强大的服务器),可以采用只读副本 水平拓展为主(更多数量的服务器)
一致性 强一致( ACID 约束) 可调的最终一致性( BASE )
最佳场景 电商,金融等对数据一致要求严格的系统 社交媒体,大数据,物联网,分析类

我们为什么选择 SQL 路径
看看上面这张表,Dukaan 应用适合的技术选择一目了然。

我们的数据是高度结构化的:比如每个订单都有一个顾客,已购商品清单,以及一个总价。一个商品总会有名字,价格,以及库存量。我们业务逻辑依赖于这样严格的约束关系。我们不需要 NoSQL 的灵活性,而是需要 SQL 的严格一致。
数据一致性至关重要:对于电商平台来说,用户信任的基础就在于每一个订单都能被正确处理,每次库存更新能够被准确无误,每笔付款信息都能正确的显示。PostgreSQL 的 ACID 严格约束对于我们来说是不可获取的必需品。
我们的瓶颈在于读取数据,而不是写入数据:正如即将在第 5 章被探讨的那样,我们最大的挑战并不是需要每秒写入上百万的新增商品(很多数据写入的场景恰好是 NosQL 的主用武之地),而是如何满足数百万对现有商品信息读取的用户请求(这是一个读取数据很多的场景)。PostgreSQL 对于这个问题有一个很棒而且成熟可靠的解决方案:制度副本。

我们并没有海量数据的烦恼。我们面对的是电商领域经典的业务难题。如果我们选用一个流行的 NoSQL 数据库,倒像是不合时宜地杀鸡用牛刀。PostgreSQL 就是那个精准契合我们业务场景的可靠且强大的工具。我们相信采用 PostgreSQL 作为基础数据库足以满足业务增长的需求,甚至应对独角兽级别( 10 亿美金)的业务量也没问题。
第 3 章关键知识点总结

架构扩展的关键第一步是分离应用服务器以及数据库服务器。这样可以让各组件充分发挥潜能,不至于消耗在资源竞争上。
每一个问题解决都可能带来新的问题。转向分布式架构导致网络延时成为了必须考虑的性能瓶颈。
昂贵的网络请求能少则少。减少延时最有效的办法就是提高代码效率以减少并优化数据库的查询。可以尝试使用类似 select_related 以及 prefetch_related 这样的工具。
优化数据库连接。考虑使用类似 PgBouncer 这样的连接池以减少创建数据库连接的重复工作,这样可以帮助应用更具韧性,在高压下表现更稳健。
第 3 章第 3 节:新瓶颈

完成上述数据库迁移后,我们赢得了过去数周内第一次喘息之机。
数据库分离获得了成功。网站现在运行地很稳定,响应也很快,不再需要依赖每隔几小时的服务器重启就能搞定当前常规的新用户请求。厨房和储藏室都拥有了各自专属的空间,整个业务流程运行地很顺畅。我和苏米特很高兴。我们经历并成功挺过了第一次架构拓展升级的危机,得以让我们的网站变得更强大。这为我们的发展赢得了宝贵的时间。
但在创业公司的世界里,时间就是最宝贵的资源。每一次打补丁或者每一个被清除的瓶颈其实都预示着接下来还有其它问题正等待被发现解决。互联网创业公司的规模拓展就像打地鼠游戏,刚解决了一个问题,另一个马上就冒出了头。
我们的新问题有点棘手。它并不是那种灾难性质的崩溃或者火烧眉毛的服务器问题。它是一种不太令人察觉的慢。即使现在已有两台专属的强大服务器,有些页面访问起来感觉还是有点迟缓。我们解决了资源竞争的问题,但同时也引入了一个新的更复杂的问题:网络延时。
深入技术细节:网络请求的开销
当我们的 Django 应用和 PostgreSQL 数据库同处一台服务器时( localhost ),它们之间的通信瞬间就可以完成。有点像主厨从身后的架子上取某个食材,一个转身就能完成。“往返时延”( Trip Time )无限接近于零。
现在情况有所不同了。我们的厨房(应用服务器)和我们的储藏室/图书馆(数据库服务器)处于两栋不同的建筑中。就像相邻的两栋房子,我们的两台服务器同属班加罗尔的某个数据中心,它们之间通过极速的光纤相连。但无论连接有多快,主厨仍然需要完成以下工作:

暂停手头的活儿。
走出厨房。
穿过即使很短的“过道”走到储藏室/图书馆。
找到图书馆管理员并向它请求某本书(所需数据)。
等待图书馆管理员找到并取回书。
原路穿越“过道”返回厨房。

以上整个过程就是一次完整的网络请求。所花时间被称为网络延时。
对于一次网络请求而言,这个延时可能微乎其微,往往只需 1 到 2 毫秒(千分之一秒)。用户几乎不会察觉。但关键在于:对于一个用户看到的网站页面而言,并不是只需要从数据库往返请求一次。比如,为了给卖家展示店铺页面,我们的代码需要完成以下工作:

获取店铺诸如名字等的完整信息。(一次往返)
获取店铺的全部商品类别。(又一次往返)
获取某个类别的全部商品信息。(再一次往返)
获取下一个类别的全部商品信息。(再来一次往返)
。。以此类推

一个简单的页面加载可以轻松产生 10 次,20 次甚至 50 次往返于数据库的网络请求。在数据库分离之前,这些请求的耗时可以忽略不计。但现在,它们在物理层面有实实在在的开销:50 次网络请求 乘以 2 毫秒/次网络延时 等于 100 毫秒的整体延时。
于是乎,即便不考虑应用服务器和数据库服务器本身处理任务的耗时,光是网络延时就已经达到了 0.1 秒。这成为了我们新的瓶颈。我们得优化一下我们的 Python 代码,让它不要过于频繁地向数据库发出请求。
挑战网络延时:更聪明更少的请求
面对昂贵的网络请求,理想的解决办法就是减少请求次数。相比于每次往返只拿回一样物件,主厨应该带上一张购物清单,这样只用跑一次就可以把全部东西都拿回来。对于 Django 代码来说,这意味着我们需要大幅优化数据库的查询:

N+1 查询难题:我们同样受困于互联网应用开发中最常见的性能杀手:N+1 查询问题。设想一下,你需要获得( 1 ) 10 家店铺的信息以及( 2 )每家店铺排名第一的商品信息。最无脑的代码可能是这样:

先查询一次数据库,获得 10 家店铺的信息数据。
然后每家店铺查询一次获得排名第一的商品信息,即需要查询 N 次(这里是 10 次)。
所以一共需要查询 11 次数据库。这非常低效。


解决方案( select_related 以及 prefetch_related ):幸运的是 Django 有内置的解决方案。它有一个称作 prefetch_related 的功能,我们可以让 Django 这样做:“当读取那 10 家店铺的数据信息时,因为我肯定也需要读取各家店铺的商品数据信息,所以干脆请把店铺的相关商品数据一并取回。”Django 很聪明,它明白这个任务只需要 2 次数据库读取,而不是 11 次。第一次读取拿到 10 家店铺的数据信息,第二次读取就把全部 10 家店铺的所有商品数据信息都拿回来并与前面拿到的店铺数据整合到一起,供我们的应用代码使用。这就如同是我们的“购物清单”。在所有代码中完成这些优化的效果立竿见影,显著降低了网络请求的次数,让整个应用的响应更加迅速。
数据库连接池( PgBouncer ):与此同时,每一次数据库请求重新创建一个连接也很耗时。就如同主厨得找到图书馆大门的钥匙,然后拿着钥匙走到图书馆,用钥匙打开大门,取回要找的书,再用钥匙把图书馆大门锁上,最后返回厨房。这样一个过程实在太繁琐了。为了解决这个问题,我们需要一个新工具 PgBouncer 。它提供了一个数据库连接池。可以把它想象成位于厨房和图书馆之间的保安,它来负责进出大门(已提前解锁)的开合。当我们的应用需要找数据库要数据时,只需从 PgBouncer 拿到一个提前准备好的数据库连接就行了。这就避免了哪怕一次很简单的数据请求也得重新建立连接的麻烦事,从而进一步降低了我们整体的延时。
第 3 章第 2 节:迁移计划书

迁移数据库的决定已经做出。接下来就要付诸行动了。此刻感觉就像是站在悬崖边,但不得不往下跳。现在的问题是能不能在跳下去之前弄个降落伞。
我们花了数个小时详细以文字形式记录下迁移的每一个步骤,就如同对飞机起飞前做的航前检查。对于类似这种情况的高风险操作,最好不要临场发挥。提前做好计划,然后严格按计划行事最合适。否则一个小失误就可能是致命的。
架构设计:迁移前和迁移后的对比
目标是从单台不堪重负的服务器扩展成两台各司其职的组合。

迁移前:单台服务器(例如公网 IP:104.248.62.77 )搞定全部:Nginx ,Gunicorn ,Django 以及 PostegreSQL 。
迁移后:

应用服务器 (公网 IP 同迁移前:104.248.62.77 ): 负责运行 Nginx ,Gunicorn 以及 Django 。
数据库服务器(新公网 IP:142.93.218.155 ):只运行 PostgreSQL 数据库。



因此应用服务器不再同处于 localhost (意为在此同一台机器上)的数据库进行通信。现在它需要通过网络连接与新的专用数据库服务器对话。
以下便是我们制定的迁移计划书。如果以后你也需要完成类似的在线数据库迁移,那以下步骤即使看上去很吓人,那也得硬着头皮上。
步骤 1:提前准备好新的数据库服务器
如同搬家之前得先等新房造好,我们迁移数据库的第一步就是弄一台专属的服务器。
所以我们回到 DigitalOcean 的页面准备新建一个水滴( Droplet )服务器。不过这次我们选择的付费套餐跟之前的通用类型有所区别,这次我们特意挑了一个为存储优化过的类型。这种服务器类型拥有更快的 SSD 存储(称之为 NVMe )以及更多的内存空间,显然这是为了数据库的数据输入输出任务量身定制的。这将是我们全新进阶版本的图书馆。
服务器创建好之后,我通过 SSH 连接上去安装唯一需要的软件:PostgreSQL 数据库。其它比如 Nginx ,Python 或者任何其它应用层代码都不需要。这台服务器的职责很明确:好好保管我们的数据就行了。我还加上了防火墙配置,只允许从我们的应用服务器连接到这台数据库。互联网上的其它人就别妄想读取我们的数据了。这如同给予了我们的应用服务器一把特殊私密的进入图书馆的钥匙。
步骤 2:备份数据( pg_dump )
这是最关键的一步。如何复制一个时刻可能产生数据变化的数据库呢?简单地复制数据库文件没用,因为数据库文件在被复制时可能正被修改,移动文件可能导致数据永久损坏。
有一种办法是给数据库状态来一张完美的快照。对于 PostgreSQL 而言,pg_dump 就是这个魔法工具。
pg_dump 是一个可以完整读取整个数据库(包括所有的数据库表,所有数据,所有表之间的关系等等)并生成一个 sql 后缀超大单文件的命令行工具。这个文件包含了从零开始重建整个数据库所需的全部 SQL 指令。
可以设想一下:pg_dump 就是一个神奇的抄写员,他走进图书馆然后读完了每一本书,最后写下一本全新的大部头书籍:“重建图书馆馆藏的指南”。
所以,在我们之前那台不堪重负的服务器上,我运行了以下命令:
pg_dump -U postgres dukaan_prod > dukaan_backup.sql
我紧张地盯着服务器的 CPU 开始飙升。它正在很努力地创建数据库快照。几分钟之后,任务完成了。我们成功地生成了包含整个公司灵魂的备份文件:dukaan_backup.sql 。
步骤 3:传输并恢复备份
现在我们的备份数据已就绪,但是所处位置不对。我们需要把备份文件安全地从旧服务器转移到新的数据库专属服务器。为了完成这个任务,我们需要另外一个命令行工具 scp ( Secure Copy )。
scp dukaan_backup.sql [email protected]:/root/
这条指令将会安全地把我们的备份文件通过网络传输到新服务器。传输完毕后,新图书馆就拥有了完整的重建手册。
现在可以开始重建了。我 SSH 登录到新的数据库服务器,创建了一个新的(空空如也)名为 dukaan_prod 数据库,然后开始执行重建指令:
psql -U postgres -d dukaan_prod < dukaan_backup.sql
这条指令执行的内容与 pg_dump 正好相反。它从大部头的备份文件中一条一条读取指令然后执行。它会创建数据库表,插入数据并重建表之间的约束关系。我盯着屏幕,祈祷不要出错。几分钟之后,重建完成了。
步骤 4:切换
这一步是验证迁移是否正确完成的决定时刻,也是最危险的一步。我们将把 Dukaan 应用的连接从旧数据库切换到新数据库。这不可避免地导致网站会有几分钟不可访问。

启动维护模式:第一步是禁止任何新数据的写入。我们在网站上发布了一个“正在维护”的页面,任何此时访问 mydukaan.io 的用户都将被告知:“Dukaan 网站正在升级,请 5 分钟后再回来。”
增量数据的同步:在我们创建备份文件到此时做切换之间,网站产生了一些新的数据。比如一些新店铺,一些新产品的更新。所以我们需要再次重复上述步骤 2 (备份数据)和步骤 3 (传输并恢复备份)。因为这次网站已经处于维护模式,备份和恢复的速度会更快一些。这样可以确保我们的新数据库是跟原库完全一致的。
更新应用的数据库连接配置:这一步正是实施心脏外科手术的那一刀。在我们 Django 应用的配置文件中有一条关于数据库连接的配置项。类似 HOST:'localhost'这样。现在我们把它指向新的数据库专属服务器的 IP 地址:HOST:'142.93.218.155'。
重启并祷告:数据库新连接的配置保存完毕后,我敲下了重启应用服务器的指令:sudo systemctl restart gunicorn 。那之后的几秒里,我的心紧张地提到了嗓子眼。Dukaan 应用正在重启,然后将试图通过网络第一次向新数据库发起通话。
疯狂测试:Gunicorn 重启之后,我和苏米特开始尝试网页上的一切按钮。能不能登录?没问题。店铺信息能不能正确显示?可以。能不能添加一个新产品?也可以!成功了。数据库连接正确无误。
关闭维护模式:测试完成之后,我们长舒一口气,解除了网站的维护模式。

整个迁移过程中,我们的网站只有 3 分钟不能访问。
分离应用和数据库的升级成功完成。我们的应用现在拥有了自己独立的厨房,我们的数据也安全的保存在专属的图书馆中。用户们几乎立刻就高兴地告诉我们“网站感觉变快了。”我们顺利熬过了第一次重大架构升级。厨房变得更整洁,图书馆也变得井井有条,大厨和管理员现在可以尽情施展,完全不用担心会互相影响。
第 3 章 解耦应用和数据库

第 1 节 上线次日
Dukaan 就这样稀里糊涂的上线了。周末过后,我们在几个小企业主的 WhatsApp 聊天群里分享了指向我们刚发布的 MVP 的网址链接。我们其实并不知道会发生什么。也许会有零星的注册,一些礼貌性的反馈,然后就慢慢被大家遗忘。
但实际发生的事情出乎意料,我们的产品瞬间爆火。
事实证明我们假设完全正确。前文所述的在 WhatsApp 使用 PDF 做生意的问题切实捕捉到了商家们的痛点,他们亟需一个更好的解决方案。而我们构建的简单无脑小工具正好符合他们的需求。网址链接被分享到一个又一个 WhatsApp 聊天群。几天内,我们的用户就从几十个到了几百个,然后是几千个。每个店铺商家加好商品名录之后,会把他们的 mydukaan.io 链接分享给各自的顾客,而那些顾客自己往往也是小企业主。这简直是完美的用户增长。
世上没有比这更美妙的感觉了。每次刷新 Django 的管理员后台都可以看到来自印度全国各地的新店铺被创建出来。我们正实时注视着我们的简陋滑板一步步变成现实。但激动之余,一股不安悄然滋生。
因为 Dukaan 应用正变得越来越慢。
刚开始可以瞬间打开的网页现在需要花好几秒的时间。管理员后台有时候甚至会卡住不动。我们也开始接收到来自用户的抱怨:“网站打不开了,” 或是 “服务器崩了吗?”。我们就如同抓狂的消防员,每隔几小时就得重启一次服务器(这种临时解决办法正在失效)。MVP 的意外火爆正在压垮我们的小小后厨。
终于那一刻到来了。就是本书开头提到的半夜 3 点的电话。服务器彻底崩溃。
那晚是自我们 MVP 发布以来,第一波用户增长的巅峰。那台 5 美元每月承载了我们全部所托的 DigitlOcean 水滴服务器最终不堪重负倒下了。这其实是我们初版架构不可避免的必死终局。
第二天清晨,经历了辗转难眠的一夜之后,我和苏米特再次通话。虽然眼前大火已灭(我们再次重启了服务器),但我们知道这只是权宜之计。几小时之内服务器可能会再次崩溃。
苏米特紧张的说道:“苏巴什,反复重启也不是个事儿啊。“我们得找个更好的方案。到底是啥问题?”
过去几个小时我都在查看服务器的日志,即使眼睛感觉都要看废了也不敢放松从 htop 的输出数据中找出答案的努力。问题根源正浮出水面。
“应该是数据库的问题。” 我回答道,“数据库是导致崩溃的瓶颈所在。”
找到瓶颈:厨房里的鏖战
让我们再来回顾一下第 1 章提到的“只有一位厨师的后厨”比喻。我们的服务器空间狭小,主厨( CPU ),操作台面空间(内存)以及储藏室(硬盘)全都挤在一处。
正如第 1 章所述,当时服务器崩溃之后,我们的分析曾关注到一个关键的细节。主厨( CPU )耗费时间最多的地方并不是在烧菜(执行应用的 Python 代码)。主厨( CPU )正忙着往返于储藏室之间,慌乱地到处翻找或者整理食材(写入数据到数据库或者从数据库读取数据)。
数据库操作如此频繁以至于它们几乎耗尽了原本可用于 Dukaan 应用其它部分的资源。服务员( Nginx )只能拿着顾客的新订单等在门口,但主厨正因为混乱的食材进出储藏室之事而忙得不可开交,甚至连看服务员的一眼的机会都没有。这就是为什网站变得很慢,最终完全停止了响应。
为了解决这个问题,我们首先需要搞懂一个系统设计中的关键概念:互联网应用的不同组件分工确有所不同。
深入技术细节:互联网应用和数据库的负载
很明显并不是所有分工都有同等的重要性。一个互联网应用主要有两个核心任务:

应用逻辑层面的工作(厨师的操作):这大部分是需要“思考”的工作,主要由 Django 代码实现并由 CPU 负责执行。所以这项逻辑执行工作的瓶颈在于 CPU:比如找到正确的产品用于展示,计算订单的总价,判定用户是否完成了登录。就类似主厨会不停忙着查看菜谱,切配食材,炒制以及试味等等。这个类型的工作需要动作麻利的主厨(一个比较高端的 CPU )以及一个相对较大的操作台面空间(内存)才能比较高效地完成。
数据库层面的工作(储藏室/图书馆):这是典型的仓储和跑腿苦力活。数据库的主要职责就是从磁盘读取数据以及向磁盘写入数据。这项工作的瓶颈在于输入输出的速度。不需要太多“思考”,更倾向于物理世界的信息获取任务。可以设想一位图书馆管理员跑到书架去找某本顾客需要的书籍,或者一个储藏室经理正在往货架上摆放物品。这种类型的工作需要一个高效的储藏室(高端的 SSD 硬盘)以及配套的合理存取体系。

而我们面临的问题就在于我们正在强迫我们优秀的大厨( CPU )还得全职兼任图书馆管理员。好比是要一个大厨在一个繁忙喧杂的图书馆烹饪一份精致的大餐。从书架来回奔命(磁盘读写)让大厨根本无暇顾及他的本职工作:烹饪(执行代码)。这就导致两个工作都没法完成。
解决方案理论上很简单,但是实际操作上却很麻烦。
“我们需要把数据库和网站应用彻底分开。” 我告诉苏米特。“我们需要给数据库一个独立的空间。就像一个规范的图书馆总得有个专职的图书馆管理员。而且我们也得给我们的主厨弄一间独立的厨房。”
这就意味着我们从一台服务器需要扩展到两台服务器。这是我们架构上的第一个重要改进。


服务器 1:应用服务器。这台服务器将专为依赖 CPU 的任务进行优化。它将运行 Nginx ,Gunicorn 和 Django 代码。它只负责进行“思考”。


服务器 2:数据库服务器。这台服务器将专为依赖输入输出的任务进行优化。它将运行我们的 PostgreSQL 数据库。它只负责“记录”。


这是当时我做出的优化方案。一次彻底的解耦。听上去符合逻辑,应该是个正确的选择。但这也意味着我们需要在保持 Dukaan 网站可被访问的条件下进行一次复杂的开胸外科手术。我们需要把存储着我们所有用户,所有商品以及我们创业公司所有数据的完整数据库从一台服务器迁移到另外一台服务器。
如果这个过程稍有纰漏,我们的数据将万劫不复。比如订单数据可能会丢失。我们将辜负数以千计刚刚开始尝试信任我们的卖家。这是个无比巨大的风险。
第 2 章第 3 节 打好地基

理论部分的讨论随着我们技术栈的选型敲定可以暂告一段落。现在该花点时间去实作了。我们需要为代码在互联网运行找一个落脚点,也就是说我们需要一台服务器。
如果说我们编写的代码是设计图纸,我们选择的技术栈是建筑材料,那服务器就是我们的建筑工地。在盖房的例子中,就是我们浇筑地基之所在,也是世人最终看到我们所建房子的地方。
在创业早期选择服务器提供商事关成本,复杂度和功能之间的平衡。我们在一开始并不需要类似亚马逊云( Amazon Web Service )或者谷歌云( Google Cloud )这样顶级大厂的服务。这就好像买下了一整个工业园区的土地但是只为了盖一栋居所。这对于我们的 MVP 需求而言过于复杂也过于昂贵。我们只需要一块性价比相对更合适的居住用地。
基于此,我们最终选择了 DigitalOcean 这家云服务商。
深入技术细节:配置第一台服务器
DigitalOcean 成为众多开发者青睐选择的原因很直接:足够便宜。DigitalOcean 将其提供的服务器命名为“水滴”( Droplets ),听上去很接地气,不会让人产生闻之却步的惧怕。
DigitalOceam 水滴的简明指南
拿到属于我们的第一台 DigitalOcean 服务器只花了不到 5 分钟,对,就是仅有 512MB 内存且后来导致很多尴尬事故的那台服务器。整个流程非常简单:


创建一个 DigitalOcean 账号:就是很标准的注册步骤。


创建一个水滴服务器:这就是我们施展的舞台。创建时你会看到一个简洁干净的引导页面。


选择一个操作系统镜像:“镜像”( Image )是预先构建好的软件模版,通常用于服务器的操作系统以及相关基础软件。我们选择了标准的 Ubuntu 镜像,这是一个 Linux 的发行版,也是互联网应用服务器端的主流选择。Ubuntu 拥有强大的社区,安全且免费。我们选择是长期支持版本( Long Term Support ),这样可以在若干年内都得到安全方面的更新。


选择付费套餐:这决定了服务器的性能(也就是盖房子的地块大小)。我们直接拉到页面最底部选择了最便宜的那一款:512MB 内存,1 个虚拟 CPU ,20GB 的 SSD 硬盘。每个月只需要 5 美元。对于一个零用户零收入的 MVP 项目而言,这应该是一个理性的选择。狗窝虽小,但也是属于我们自己的狗窝。


选择服务器区域:这决定了服务器在物理层面机房所属的地点。我们选择了班加罗尔。原因?因为我们知道首批用户肯定来自印度。就近选择服务器会让应用访问更快延时更小(因为数据传输耗时更短)。


最后点击“创建水滴”按钮。


这几步做完之后,我们等待了大概 1 分钟,DigitalOceam 就把我们的服务器准备好了。我们现在骄傲的成为了一个公网 IP 地址(也就是我们服务器在互联网的专属地址)的主人。我们准备用来盖房的小小地块已经准备就绪。
SSH:进入服务器的钥匙
有了土地,我们需要能够进入这个地块开始盖房子。不能只靠普通的网页浏览器管理服务器,还需要一个特殊的工具 SSH ( Secure Shell )。
可以把你的服务器想象成一个远处密闭无窗的建筑。SSH 就是你加密过的魔法钥匙。你将使用类似 ssh [email protected] 这样的终端命令打开房门进入建筑。一旦 SSH 成功连接,你就拥有了一个可以直接发送指令给服务器的文本命令行:可以安装软件,创建文件目录,运行应用程序等等。
这就相当于我们的工作场所。闪烁的光标跳动在黑色的背景中,安全地连接到我们位于班加罗尔的服务器。现在可以完成我们地基的最后一块拼图了:让整个世界能够看到我们网站的软件。
Nginx+Gunicon: 服务员和后厨人员
仅仅运行一个 Django 应用是没法让网站能够被访问的。Django 本身只是用于构建互联网应用的编程框架,就好比是菜谱和主厨。但它不是为了应对来自互联网成千上万的原始网络请求而生。如果让 Django 直面网络请求,就如同让成千上万饥饿的顾客直接冲进后厨,对着主厨喊出各自的订单。这无疑是巨大的混乱。
所以你需要一个专用于管理网络请求洪流的体系。对于一个基于 Python 的互联网应用,这个体系包括两个部分:一个网络服务器( Web Server )以及一个应用服务器( Application Server )。

网络服务器( Nginx ):对应服务员。我们采用的网络服务器是 Nginx (英语发音同"Engine-X")。它是你的餐厅里一位友善且高效的服务员。每一位顾客进来后首先打招呼就是它。它在同时处理数以千计的网络连接以及简单快速任务的表现堪称惊艳。Nginx 的主要职责在于:

返回静态文件:比如用户请求访问一张图片,一个 CSS 或 Javascript 文件,Nginx 都可以从储藏室(硬盘)直接读取然后返回给用户。这不需要麻烦大厨亲自处理这些请求。这对于系统的整体效能提升具有重大意义。
作为反向代理:对于需要后厨料理(比如加载店铺的商品列表)的请求,Nginx 自己并不会直接处理。它会迅速记录好这个订单(请求),径直来到后厨,把请求交给应用服务器来处理。


应用服务器( Gunicorn ):后厨经理。我们选择的应用服务器是 Gunicorn ,就类似我们的厨房经理。它从 Nginx 得到网络请求然后把请求转换成我们主厨( Django )能够理解的某种形式。它会同时管理着多个负责不同工序的厨师(就是服务器中的工作进程 Worker Process )。应用服务器是外部世界(由 Nginx 负责打交道)和我们的应用代码(由 Django 编写)之间的关键连接部分。

这样,整个流程看上去虽然简单但潜力十足;互联网的用户请求首先抵达 Nginx 。Nginx 要么直接返回静态文件,要么吧请求转发给 Gunicorn 。然后 Gunicorn 运行 Django 代码处理请求,生成 HTML 页面,最后把处理好的结果返回给 Nginx ,最终得以让用户看到网站。
要把这些都搞定需要在我们的服务器上安装 Nginx 和 Gunicorn 软件并提供一些简单的配置文件把两者正确串联起来。这些准备就绪之后,我们的地基就已经浇筑完毕,网站已初具雏形。我们把域名 mydukaan.io 指向了我们服务器的公网 IP 。
我激动地在浏览器中输入这个域名(网址)并按下了回车键。
成了。我们每月 5 美元的服务器成功返回了一个简单的 Hello World 页面。
我们的 MVP 简陋滑板大功告成。48 小时的时间所剩无几。得找一些真正的用户来看看他们是否喜欢我们的 MVP 。
第 2 章关键知识点总结

MVP 不是最终产品的初级版本,而是一场验证核心假设问题的科学实验。它的目标是尽可能获取有用的反馈和经验,不是打造完美的产品。先花时间想清楚验证所需的最简陋“滑板”是什么再开始动手写代码。
根据时间紧迫程度选择最熟悉的技术栈。创业伊始,“多久能上线”是最重要的关注点。所以尽量选择那些能帮你干“脏活”的开箱即用框架比如 Django 或者 Rails 。
从长期考虑,选择靠谱的底层数据库。即使不会立马需要全部功能,但是从一开始就直接上马健壮强大的数据库比如 PostgreSQL 可以有助于迭代过程中的难题解决和功能扩展。
谨慎控制起步阶段的架构规模。5 美元一个月的服务器应该足够应对早期的数千位用户。除非确实有必要,否则不要过度设计系统或者在云服务商开销太多。
搞清楚网络服务器( Nginx )和应用服务器( Gunicorn )的职责角色。他们之间类似餐厅服务员和后厨经理的分工模式是现代互联网应用的核心基石。
第 2 章第 2 节 选择技术栈

两天的时间不能浪费一分一秒。MVP 已经定义了我们将要做的“事情”, 也就是我们的数字版滑板。现在还需要找到“如何”完成它的工具,我们有哪些选择呢?

在软件行业,你选择的一系列工具被称作“技术栈”。就好比是盖房子。首先你得决定采用什么主建材。是用砖块,木材还是钢材?采用何种地基?需要哪些建筑工具?这些选择将决定造房子需要花多长时间,造好的房子有多坚固,以及以后想增加一个房间是否容易。

对于一个周末的黑客松项目来说,只有一个最重要的因素决定着类似的选择:构建的速度。我们暂时不需要考虑那些高大上的可扩展架构,或者最先进最时髦的技术栈。我们需要的是能在最短时间内帮助我们从零打造出可用产品的技术栈。

这意味着我们需要一个熟悉的,可靠的,能帮我们完成很多“脏活”的工具箱。

深入技术细节:编程语言和框架

这是我们第一个也是最重要技术选择:选哪个编程语言以及何种编程框架?

为什么选择 Python ?

编程语言就是你往计算机下达任务的指令“词汇表”。我们选择 Python 是因为它赖以成名的简洁语法。代码读上去就像是普通的英文。当你跟时间比赛时,最好不要在调试工具上浪费时间,比如试图搞明白某个复杂的编码机制或者漏写了一个分号。Python 代码写起来毫无障碍,让我们可以专注在 MVP 问题本身。而且 Python 社区相当活跃,每一种需要的功能都可以找到现成的库来调用。

为什么选择 Djanjo:开箱即用的框架

编程语言仅仅是”词汇表”。好的框架就是完整的指令手册。框架提供了合理的编码结构,顶层设计,以及一系列预先构建好的组件,这些都可以让你无需从零起步。

还是以盖房子为例。你当然可以自己去砍树,自己去加工板材,甚至自己打磨钉子。或者,你也可以购买一个预制的房屋套装,包含所有的墙壁和门窗。你只需要把它们组装起来,然后根据个人喜好做些微调。

这就是 Django 的价值。它是构建互联网应用的预制盖房套装。它的设计哲学就是著名的开箱即用( Batteries-Included )。这意味着你直接立马可以上手开始构建。对于我们需要在 48 小时内完成的 MVP 而言,Django 的两项功能绝对是大杀器:

Django 管理员后台:这是 Django 的杀手锏。只需要几行代码,Django 就可以生成一个完整的安全的并且看上去非常专业的管理员后台。这主要提供给我们作为创始团队登录用以查看全部业务数据。 当有新用户创建一个网店,我们就会在管理后台收到通知。我们可以查看相关商品名录,帮助修改错误的输入(仅在必要时)或者排查用户在使用过程中碰到的其它问题。但如果从零开始,弄出这样一个管理员后台可能需要花费一天的时间。Django 不花我们一分钱,只需要 15 分钟就可以准备好。这个管理员后台就是我们的任务指挥中心。
ORM (对象关系映射器):这个听上去好像有点复杂,但其实很简单。如果要从数据库读取数据,一般来说你得通过编写 SQL 语句完成。就是类似 SELECT * FROM products WHERE store_id=123;这样。SQL 很强大,但也容易出错,且跟我们主要采用的 Python 代码风格迥异。Django 的 ORM 模块就扮演了翻译的角色。它允许我们用 Python 代码跟数据库打交道。上述 SQL 指令在 Django 中可以改写成:Product.objects.filter(store_id=123)。这让代码不仅易于编写和阅读,也能够避免很多比如 SQL 注入的安全隐患。采用自带 ORM 的 Django 框架让我们的代码更简洁,开发速度也更快。

深入技术细节:我们考虑过的其它语言和框架

Django 当然不是唯一的选择。技术层面看,总有很多不同的方法可以达到同一个目的。关键是要根据任务性质和特点选择最合适的工具。

Node.js 以及 Express 框架:这也是一个非常流行的组合。Node.js 允许使用 Javascript 语言编写服务端代码,而 Javascript 通常是运行在客户端浏览器中。这对于很多全栈型创业团队非常友好。Express 框架则非常灵活,也遵循极简设计的原则。所以对于我们而,这反而是它的不足。与其说 Express 是一个预制的盖房套装,不如说更像是一盒子高质量的乐高积木。它提供了很多很底层的功能,但是需要自行组装更高阶的模块。由于我们只有 48 小时,所以这样的自由度不是我们所需,我们需要的是 Django 开箱即用的丰富组件和编码结构。
Ruby on Rails:这可能是一个更接近 Django 的备选。Rails 与 Django 的设计哲学高度相似。它同样推崇“约定优先于配置”的理念,意味着它内置的很多合理决策能加速软件开发的过程。实话说,Rails 也的确是个不错的选择。最终选择 Django 可能更多是基于我个人的喜好和熟悉程度。因为我过去更多使用过 Python 和 Django ,所以在时间紧迫的情况下,押注最熟悉的工具总没错。
深入技术细节:数据库选型

有了合适的编程框架,我们需要决定在哪持久化保存我们的业务数据,比如店铺名称,商品明细,价格等等。我们需要一个数据库。如果说编程框架是盖房套装,那数据库无疑就是房子的地基。必须得稳定,可靠并且易于查找数据。

为什么选择关系型数据库?

我们决定使用一款关系型数据库( Relational Database )。原因很简单:我们的数据将保存在数据库的表( Table )中,就像一张巨型且功能强大的 Excel 表格。店铺信息存储在店铺表中,产品信息存储在产品表中。更关键的是,可以在这些表之间建立关系( Relationship ),比如每一款产品肯定属于某一个店铺。

这样的关系结构对于电商业务是完美的选择。数据间有了明确的联系和规则。肯定没人想看到一个不属于任何店铺的产品信息,或者一个没有顾客信息的订单。关系型数据库会强制约束数据之间的关系结构,保证数据始终一致,不多也不少。

为什么选择 PostgreSQL ?

在众多的关系型数据库中(比如 MySQL ,微软的 SQL Server 等等),我们最终选择了 PostgreSQL (常被简称为 Postgres )。

原因呢?其实对于 MVP 而言,Postgres 及其主要竞争对手 MySQL 都是不错的选择。但我们出于以下几点更倾向于 Postgres 。Postgres 在开发社区中的口碑相当不错,令人称赞地健壮,可靠并且符合通用的技术标准规范。绝对是开发中可以放心使用的主力选择。更重要的是,我知道 Postgres 有一些后面我们可能能用上的高级功能。比如其中一个功能是订阅通知机制( LISTEN/NOTIFY ),这将是我们在第 8 章提到的实时缓存系统背后的秘密武器。对于我们要完成的 MVP ,这个特性还用不上,但即使暂时用不到全部功能,从一开始就选择一个功能强大的地基,在将来无疑也是会受益颇多。

综上,我们的盖房图纸已经就绪。技术栈选择如下:

编程语言:Python
编程框架:Django
数据库:PostgreSQL
万事俱备,可以开工了。现在需要开始浇筑地基,准备建造承重墙了。换句话说,我们需要开始准备服务器了。
第 2 章: WhatsApp 应用里的 PDF 难题 (故事起源)

第 1 节 主意和滑板

每家创业公司都是解决某个问题而诞生。我们的公司( Dukkan )起步则源自一个粗糙的 PDF 文档以及全国(印度)因疫情封禁导致的混乱。

那是 2020 年。整个世界仿佛按下了一个巨大的暂停键。印度曾经熙熙攘攘充斥着喇叭声(车辆)和各种小商贩的喧闹街头彻底陷入了沉寂。日常生活的熟悉节奏被打乱了。对于数以百万计的小企业主而言,比如社区便利店的店主大叔,街坊里蔬菜档口的卖菜老板,售卖手作印度传统纱丽的老阿姨,这绝对是一场浩劫。他们的店铺关门,他们的顾客只能呆在家里,他们的生计正在慢慢消失。

唯一的出路是互联网,或者更具体一点,是 WhatsApp 这款移动应用。它变成了新的交易市场,新的店铺甚至新的特价柜台。但是这个新市场乱糟糟的,也不够高效,还常令人感到万分沮丧。

这就是我的联合创始人苏米特登场的时刻。他并不是想要打造一家多么高大上的大公司,他只是想帮助他的一个邻居杂货铺能够不至于关门大吉。在亲眼目睹了那家杂货铺通过 WhatsApp 进行了多次有些凌乱的对话最终完成了所有交易流程之后,他被这样的场景深深吸引,但又感到深深震惊。

因为整个交易过程在低效方面堪称登峰造极:


步骤 1:建立商品名录。店主需要给每一位潜在的顾客发送一个很多页的 PDF 文档。但是这个 PDF 文档多半是通过微软的 Word 应用制作出来的,并且格式混乱,分辨率很低,甚至充斥着各种错误。产品名称乱七八糟,价格也模糊不清,更无法进行搜索。比如,如果想知道这个商家有没有你最爱的那个牌子的饼干,你得不停滚动鼠标从长达五页的粗糙图片中试图找到答案。


步骤 2:顾客下单。在经过长时间仔细定睛搜寻想要买的商品之后,顾客不得不通过逐字逐句输入长长一串订单的信息,而这显然极易出错。比如:“一包 Maggi 牌面条,两公斤面粉,半公斤糖,然后再来一包那个蓝色的乐事。。。”

步骤 3:商家确认订单。店家通常同时在处理数十个类似的与不同顾客的对话,需要逐一对顾客输入的订单文字信息进行确认。“不好意思,女士,蓝色的乐事没货了,但是我们有绿色包装的。”于是新一轮的商品挑选和订单信息又得再来一遍。

步骤 4:支付货款。好不容易等到订单信息终于确定,店家就会把自己的 UPI (印度的统一支付平台)账号或者类似的二维码发送给顾客。顾客就可以用自己手机的谷歌支付( GPay )或者是 PhonePe 应用完成支付,最重要的是,会同时把支付成功的截图作为凭证一起发给商家。可想而知,店家的手机相册堪称一个支付凭证截图的坟场,根本没有办法从数千张图片中高效地找到某个顾客为某个订单支付的款项。

这完全是一场噩梦。无非是印度人从疫情的绝望中想出来的一个临时数字解决方案( jugaad )


于是有天晚上我的手机响了。是苏米特打来的。我通过手机能感受到他当时炸裂的激情。因为他当时不仅仅是普通的聊天,而显然是处于灵感迸发的亢奋。

“苏巴什,我跟你说,有件事太疯狂了。”他开门见山,甚至连日常的招呼寒暄都省掉了。“我刚亲眼目睹了某个大叔通过 WhatsApp 开店卖货的过程,简直不忍直视。有时候把订单搞忘了,有时候有把顾客的支付搞混了,整个流程到处出错。我们得做点什么。”

他绘声绘色地对我描述了上述提到的令人头疼的几个步骤,也提到了 PDF 和截图满屏飞的混乱场景。

“我们得给这些商家做个什么东西让他们做生意更容易。” 他的声音开始变得严肃起来,“就是一个手机应用,可以让商家上传商品名录,顾客可以直接下单,这样大家就都很轻松,就是给商家准备的线上店铺。”

一个名字呼之欲出:Dukaan ,为“商店”之意。

苏米特说的没错。问题不是市面上缺少足够的技术解决这样的难题,而是没有足够简单高效的解决方案。这些卖家其实并不需要诸如亚马逊或者 Shopify 这样复杂的大型电商系统。他们既没有时间也没有技术搞明白那些。他们需要一个就像使用 WhatsApp 那般简单的电商专属解决方案。

那通电话点燃了星星之火。但如果不付诸行动,再好的点子也是空想。得投入精力用心打造,才能把点子变成现实。而在创业赛道,想做成点什么事情也必须“兵贵神速”。所以我们没有花费 6 个月的时间去慢慢打磨出一款完美产品的资格。我们必须得在短暂的几天时间里就知道是否值得依循这个点子继续做下去。

这就引出了对于任何跃跃欲试的创业者或技术宅最重要的一个概念:最小可行产品( MVP ,Minimum Viable Product )

深入理解:最小可行产品( MVP )

MVP 这个词在技术领域经常被谈起。很多人认为 MVP 就是打造一个最终产品的早期版本,可能错误比较多,功能比较少。不过这个想法是错误的。

因为 MVP 不是打造一个产品,而是做一场试验。

MVP 的最终目标并不是赚钱或者收获百万用户。MVP 最主要的目标是学习。它是为了以最小的开销验证最重要的预期假设而设计的一款具有严肃科学性质的工具。对于我们而言,这个预期假设是:“如果我们为那些小企业主打造一款超简单的开网店工具,他们会愿意使用嘛?”

为了验证这个假设,我们并不需要一款完美无缺的产品。我们只需要找到一个能回答上述问题的最简方案。这就是 MVP 的哲学出发点。

可以设想这个场景:比如你的目标是解决“出行”的问题,那你不需要从制造一辆汽车开始。造车那可太麻烦了,需要引擎,轮子,座椅,底盘,电力系统等等。这需要耗费太长时间。而等到你造好汽车,那时候你才悲催的发现你的顾客其实想要的是一辆摩托车。

MVP 的思路是,先整个滑板吧。这就简单多了,但也能解决核心的问题所在:能够把人从 B 点运到 A 点。这足以让你验证核心的预期假设,即人们是否愿意通过某种带有轮子的载具出行?

如果这个假设可以通过滑板得到确认,那就可以利用用户反馈打造下一个产品版本:带扶手的滑板车。然后是自行车,然后是摩托车。最后,才是汽车。每个阶段,你可以不断学习改进并给用户输出他们确实需要的价值产品。

所以对我们的 Dukaan 来说,需要找到属于我们自己的最初滑板。那到底什么才是可以用来测试我们点子的最基本形态呢?我们删除了所有想得到的各种附加功能。不需要支付网关的集成,不需要货运追踪,只提供给商家,没有花里胡哨的皮肤模板,没有销售数据分析。我们最后在最精简的核心诉求上达成共识。


这是我们为 Dukaan 定义的最核心业务流程:

店家创建店铺:只需要一个单页面,用户输入手机号,接收一条短信验证码,然后允许为店铺去个自定义的昵称名字。齐活。店家的网店就生成了。无需邮箱,无需密码,无需各种复杂的表格填写。
添加商品:采用极简的产品录入表单。产品名,价格,再来一个按钮让用户从手机相册选择一张照片作为产品卖家秀就行了。不需要定义产品类别,不需要定义产品规格(比如尺寸,颜色等等),不需要记录库存数量。这些就是建立一个商品名录的最基本所需。
分享店铺链接:店家添加若干商品之后,Dukaan 应用就会自动为这个店铺生成一个独一无二的可分享的链接(比如 mydukaan.io/mystore 这样)。店家可以在 WhatsApp 聊天框里直接使用这个链接分享给客户。
以上就是我们的滑板。

很显然,这根本称不上是一个功能齐备的“平台”,也不是什么“电商解决方案”。不过就是个用兼容手机端的单页面来替换丑陋 PDF 的简单小工具。这是我们想到的用来解决目前 WhatsApp 商家难题的最简单方法。

确定好这个 MVP 之后,我们决定给自己上上难度。不要很长的开发周期,不要繁琐的需求讨论。我们打算只用一周末的时间完成开发并发布上线。

于是,48 小时的两人黑客松正式启动。时间不等人。现在需要做出第一个重大的技术决策:用什么打造我们的 Dukaan 滑板?
第 1 章第 3 节: 我们伟大而危险的大单体应用

如果说服务器就像厨房, 那如何描述我们大厨( CPU )正在使用的菜谱(代码)呢? 在软件行业的术语里,我们称之为服务架构。我们采用的架构非常经典, 是那种几乎每一个初创公司早期都会采用的架构。
是的,我们用的是一个巨大的单体应用( Monolith )。

这个名字听上去像某种远古巨石阵一般巨大,有点唬人。但事实上,它非常简单。单体应用是指你所有的代码逻辑都在一处一起运行。比如我们的用户注册,商品名录,订单管理,商户仪表盘,支付,所有功能都同处在一个 Django ( Python 流行的 web 框架)项目里面。

可以把它想象成一本巨大的百科全书般的食谱。它包含从开胃小食,主食,甜品,以及饮品在内的所有菜谱,很显然这本书会超级厚。

为什么我们要选择从大单体应用起步(为什么它是正确的上手选择!)
我想直截了当地明确这一点:从大单体应用开始构建没有问题。对于初创企业而言,它大部分时候都是最优可行方案。在创业初期,你唯一的目标就是尽快构建并推出产品,越快越好。因为你需要知道你打算推出的产品的确有人愿意买单。
大单体应用天然就是为速度而生:
1. 易于代码开发: 所有逻辑都在一处。无需操心复杂的服务间通信问题。只需要编写一个函数然后调用它就行了。
2. 易于测试: 可以在笔记本电脑上运行整个应用并轻松地联调测试所有功能。
3. 易于部署: 只需把所有代码打包放到服务器上就完工了。
我们构建的大单体应用允许仅靠苏米特和我两个人在 48 小时内就上线了一个可以正常运行的电商平台。如果我们采用更复杂的“微服务”架构(本书后面的章节会详述), 我们现在可能还在为系统设计的细节争论不休。
所以大单体应用就是我们的超能力,给予我们极速试错的机会。但就像所有超能力一样,它也有一个不易察觉的危险副作用。
大单体应用的隐型危险
随着我们的菜谱增加,问题逐步开始显露。

● 变得愈发沉重以致难以使用。 查找某个食谱可能耗时更长。搞清楚为什么甜品部分的一个改动可能影响到开胃餐部分变得几乎不可能。在软件行业的术语里,我们称之为紧耦合。
● 一个小错误可能会毁了整本食谱书。 一个菜谱里面的一个小小的笔误理论上可能导致整本书都难以理解. 类似的,一个小小的代码错误可能会让整个网站都崩掉。
● 雇佣一个各司其职的专业团队变得毫无意义。比如即使你聘请了一个专业的糕点大厨, 它可能仍然需要理解一整本巨无霸食谱书才能开始工作。类似的,大单体应用会让新加入的开发者很难迅速融入以致降低效率。

最重要的是(也是那天晚上导致我们网站崩溃的原因), 大单体应用让你别无选择:如果你仅仅想为食谱书中某一部分(对应软件应用的某个功能)扩容( Scale ),那你只能先把整本书都扩容。


比如我们电商客户的商铺主页面访问量很高 (好比是食谱上的主食部分很受欢迎)。但另一方面我们的销售仪表盘(好比是食谱上的开胃餐部分) 被访问的次数就要少得多。可是由于他们同处于一个大单体应用中,我们的服务器资源得同时提供给所有功能。 处理主食的巨大消耗(厨师/CPU )让其它厨房任务处于资源嗷嗷待哺的状态,最终导致整个系统的崩盘。


我们那运行在小得可怜的服务器上的大单体应用成了一个潜在的完美毁灭风暴:一个软件层面的单点( Single Point of failure )运行在一个硬件层面的单点。 这简直就是一枚时间炸弹,最终在周二凌晨 3 点 14 分被引爆了。


第 1 章的关键知识点总结

● 你创业历程中第一台服务器肯定会有崩溃的一天。这不是一个会不会崩溃的问题,而是何时崩溃的问题。所以我们的目标应该是如何从崩溃中迅速恢复并从中总结经验, 而不是绞尽脑汁试图去避免崩溃。
● 深入理解架构基础。 在学习复杂架构之前, 彻底搞明白有关服务器运行的原理。 思考这些概念:CPU (大厨的手速), 内存 (操作台空间), 以及磁盘 (储藏室).
● 掌握基本诊断工具库。如果看不到问题,那也没办法修复问题。学会使用 ssh 和 htop(或 top) 这些命令行工具。这些就是系统管理员的听诊器。
● 从大单体应用开始构建是一个实用选择( feature ),不是错误( bug )。创业初期,速度就是王道。尝试打造完美的早期产品属于用力过度。
● 认识到你的初期技术选择存在保质期。 为你赢得头 1 万个用户的架构很可能没办法帮你拿下接下来的 10 万个用户。为迭代进化做好准备。
第 1 章第 2 节 庖丁解服务器(一个厨师的厨房)

让我们先把这场半夜 3 点的惊慌无措搁在一边。在我们解决问题之前,得先理解问题。所以问题来了:“服务器”到底是啥?


请暂时忽略过于技术的解释。忘记在冰冷机房闪烁的指示灯。在本书剩余部分,我建议你用一个更简单浅显的概念来看待“服务器”:它就是只有一个厨师的餐厅后厨,所以很明显那个厨师会非常忙碌。

这个类比将会是有关服务器架构的最重要基础,其它东西都将在这个类比的基础上构建。

CPU: 厨师的速度

中央处理器 (Central Processing Unit) 就是掌勺的大厨。它是所有处理逻辑的核心大脑。这位厨师把原料 (数据)按照菜谱(代码)制作成一道可口的菜肴(一张网页,一个搜索结果,一笔完成的订单)。


● 更快的 CPU (用千兆赫作为单位, 即 GHz) 意味着你拥有手脚更麻利的厨师。 它可以用更快的速度进行切菜,炒菜和装盘。


● 多核 CPU 就像厨师多了几个机械臂。 比如 4 核 CPU 就像一位能够同时操作切菜,炒菜,煎炸和调味的厨师。它可以在同一时间处理多项任务。

我们那 512MB 的服务器只有一个单核 CPU 。按照这个厨师的类比,就如同我们只有一位独臂大厨但我们却要求他为 1 万人做一顿大餐。




RAM: 厨房的操作台面空间

内存( Random Access Memory )就是厨房的操作台面。这是理解服务器最关键的一步。内存就是厨师可以利用的工作空间。厨师( CPU )会使用内存来保存当前正在被制作的菜肴所需的所有原料以及锅碗瓢盆。

从厨房台面拿取所需非常快速。厨师甚至不需要思考, 想要什么直接伸手去拿就可以了。 更多的内存意味着更大的操作台面。如果拥有很大的操作台面,那厨师可以立马处理很多不同的点餐需求因为所有食材都在它面前的台面上摆放着可供随时取用。

但是如果台面放不下了,那厨师就会有棘手的麻烦。他得放下手头正在处理的活儿, 一头钻进后面的储藏室,从中花时间找到并拿回他所需的食材,还需要从台面上挪走一些东西以便腾出空间。这样一个操作明显会让所有事情都变得更慢。




这就是之前发生在我们服务器上的事情。我们仅有 512MB 的服务器就好比一个只有砧板大小的厨房台面。而我们自己的应用( Dukkan ),我们的数据库以及服务器上的操作系统本身 全都在争夺这一块小小的砧板空间。所以当台面放不下的时候,服务器开始使用 "SWAP"空间可以看成是储藏室中一个特殊的区域,在临时救急时也可用作台面空间。这肯定是效率极低的方案。厨师不得不花时间来回奔走于储藏室与台面之间,必然耽误了它真正用于烹饪的功夫。


磁盘: 厨房的储藏室

磁盘,无论是传统机械硬盘或者是固态硬盘,都可以看做厨房的储藏室和冰箱,是用来长期存储菜谱(代码),食材(数据)以及厨房用具(操作系统)的地方。

储藏室比操作台面( RAM )显然要大很多,但存取耗时也要慢很多很多. 你肯定不希望你的厨师即使为了拿一点点食盐也得跑去储藏室一趟。 最理想当然是食盐就摆在它的操作台面上。


资源竞争: 厨房里的无序


现在请设想一下我们面对的场景。在一个迷你的厨房里,我们的独臂厨师只有一个砧板大小的操作台面, 然后我们苛求它完成:

● 运行应用程序: 厨师需要看明白菜谱(我们的 Python 代码) 并完成菜肴的烹制。

● 管理数据库: 大厨还得兼任储藏室管理员,不停地把食材(我们的用户数据)分门别类记录好 ,保存好并按食谱准确地取用。

● 处理网络流量: 除了以上, 大厨还需要完成服务生的工作:跑到餐厅前台以最快速度为数千用户点单。

这就是资源竞争。每一项任务都在同一时刻高呼大厨希望优先得到处理。应用程序需要 CPU 来处理运算逻辑,数据库需要把数据写入磁盘,还有刚接收到的用户请求需要内存来暂时记录。所有这些都在竞争同一个有限的资源,所以结果就是完全的死局,无法动弹。

如果想看到厨房里发生的这些无序竞争,你可能需要安装一个监控摄像头。在服务器的世界里,我们的摄像头只是一个简单的命令:htop. 它是命令行工具 top 的优化版本,可以让你犹如现场直播般看到大厨正在忙啥。

虽然可能看上去挺复杂,但你只需要了解以下几点:

● 顶部的 CPU 柱: 如果它显示红色的 100%,那你的大厨就是过载了。


● 内存柱: 如果这个数字满了,说明你的操作台面已经放不下了。


● SWAP 柱: 如果这个数字开始增加,这不是好的信号。说明你的大厨正在尝试使用储藏室作为额外的操作台面。


● 进程列表: 这里展示了大厨正在处理的每一个任务并且能让你看到哪一个占用了最多的资源。

学会读懂这个 htop 的输出只是成为 CTO (要么像我这样误打误撞,要么按部就班)的第一步。从这里开始,我们将进行科学的诊断而不是盲目的瞎猜。对于我个人而言, 那晚半夜 3 点的服务器屏幕尖锐地喊出了一个无可辩驳的事实: 我们的厨房对于我们的抱负而言,实在是太小了,小到足以致命。
第 1 章 凌晨 3 点响起的手机

第一节: 服务器崩了


我的手机突然响了起来,但不是那种普通的来电铃声,而是一种类似尖叫的声音。

那声音伴随着一种特定的手机振动(通常被手机厂商用于提示最高等级的恐慌情绪)。振动的手机在我床边廉价的木桌上发出了剧烈而仿佛暴怒的嗡嗡声。那是一种不光能让你从睡梦中惊醒并且能够让你立马进入高度戒备状态的声音。此刻,时钟上的时间微微透着令人不安的红光:凌晨 3 点 14 分。

其实甚至在我睁开眼之前,我的心就已经开始砰砰直跳。因为一般来说,在这样的午夜时分收到来电只有两个原因:要么是家里人有急事,要么就是公司业务出大事了。手机显示的来电号码确认了后一种情形。一个再熟悉不过的名字闪入我的视线:苏米特( Suumit )。

苏米特.萨( Suumit Shah )是我的至交和公司合伙人,他敏锐的商业头脑对于我这样的码农而言非常重要。他在凌晨 3 点打电话给我那只会有一个原因。公司出大事了。


我迅速在手机上滑动接听,虽然我的声音听上去有些沙哑和疲惫:“咋啦,兄弟?”


“苏巴什( Subhash )! 网站全挂了!” 透过手机麦克风,苏米特的声音听上去就像是一声令人肾上腺素飙升和无比紧张的枪响:“快起来!网站全都不能用了!”


他不用再多说什么。我已经翻身下了床,光脚踩在冰凉的地板上,全身就像被电击一样。我摸索着找到了我的笔记本电脑, 熟悉的苹果标志泛着白光就像黑暗的房间里的一座灯塔。 我此刻思绪万千,一大堆可能出现的服务灾难场景在脑海中无序地飘过:


是被黑客攻击了嘛? DDoS 攻击?难道是哪个外国写脚本的小屁孩闲着无聊来搞垮我们的网站用以取乐?

或者是我们自己的程序员上传了一段有问题的代码?难道是某个多余的分号导致网站完全崩了?

是不是我们的云服务商挂了?如果是这种情况,那我们还能做啥?

“网站页面显示不出来。手机端应用也在报错。全都挂了。完完全全的彻底崩了。”苏米特用紧张并且焦虑的声音继续说着。我甚至能听到他在电话那头来回踱步。

“好的好的,我已经在处理了。哥们淡定。” 我尽量让自己听上去比真实的内心更稳定一点。 一定要保持冷静。 救火第一条原则就是不要火上浇油去添乱。

略带睡意,我用笨拙的手指开始在键盘敲击。我打开终端( terminal )准备登录。这个黑底绿字的界面是通往我们所有线上服务的指令中枢。

ssh [email protected]


我敲下了回车键。光标开始闪烁,继续闪烁,仍在闪烁。

正常来说,登录提示符应该很快就出现。但现在这个情况。。。感觉不妙。非常不妙。这意味着服务器不只是出现问题了,而是像一个已经进入生命倒计时的垂危病人甚至无法从病床下来应门。在等待了很长很长一段时间之后(长到我甚至觉得仿佛比一辈子还要久),登录提示符终于出现了。还好,至少服务器还没有完全挂,但也差不远了。

我开始思考原因:如果服务器响应如此缓慢,那应该不是简单的应用代码问题( bug )。应该是有更深层的问题。可能出在系统层面。就像是服务器本身在苟延残喘。

我输入了我的第一个诊断命令。这是用来检查服务器基本体征的一个简单工具:


htop


屏幕上显示的检查指令输出结果令我脊背发凉。满屏的红色。

每一个进程( process )似乎都处于异常状态。CPU 使用率已经飙升到 100%。结果显示服务器几乎所有内存都已经被占用。甚至连服务器临时救急的 SWAP 存储空间都已经满了。

看上去服务器并不是在苟延残喘。 它其实已经凉透了,而我们只不过目睹了它超负荷状态下的最后几次抽搐(登录和检查指令响应)。


紧接着我马上知道了我们网站彻底歇菜的原因。简单得甚至有些尴尬:显示屏上方清楚地展示着服务器的总内存容量是 512MB 。

五百壹拾贰兆字节。

我的智能手机拥有 8GB 的内存,比这台承载了我们公司所有业务的服务器要大 16 倍。数千家电商客户以及他们数以百万记的商品名录,Dukaan (我们的公司)全部的希望和梦想都运行在这台比我口袋里的手机还要弱的服务器上。

这不是一次复杂的黑客行为或者代码错误。原因很简单:就是服务器不够用了。 就好像我们在一个小小的电话亭举办一场大型的摇滚演唱会,最终电话亭被挤爆了。

盯着屏幕,手机仍响在耳侧,我突然瞬间清醒。像我这样一个不是计算机专业科班出身,没有正儿八经大规模系统扩容经验的人,到底是如何承担起这一切的?

要弄明白这个问题,先得弄懂我们正尝试驯服的这头野兽。也就是需要如庖丁解牛一样深入理解此刻正在作妖的家伙:服务器。
翻译自 Subhash Choudhary 的 Accidental CTO 一书
2025 年 5 月 13 日
回复了 sayyesorno 创建的主题 酷工作 [海外电商] [上海] 招聘资深前端
需要后端 Java 嘛?
1  2  
关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5619 人在线   最高记录 6679       Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 45ms UTC 06:21 PVG 14:21 LAX 23:21 JFK 02:21
Do have faith in what you're doing.
ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86