

























10 rayeaster 2 月 12 日第 1 章 凌晨 3 点响起的手机 第一节: 服务器崩了 我的手机突然响了起来,但不是那种普通的来电铃声,而是一种类似尖叫的声音。 那声音伴随着一种特定的手机振动(通常被手机厂商用于提示最高等级的恐慌情绪)。振动的手机在我床边廉价的木桌上发出了剧烈而仿佛暴怒的嗡嗡声。那是一种不光能让你从睡梦中惊醒并且能够让你立马进入高度戒备状态的声音。此刻,时钟上的时间微微透着令人不安的红光:凌晨 3 点 14 分。 其实甚至在我睁开眼之前,我的心就已经开始砰砰直跳。因为一般来说,在这样的午夜时分收到来电只有两个原因:要么是家里人有急事,要么就是公司业务出大事了。手机显示的来电号码确认了后一种情形。一个再熟悉不过的名字闪入我的视线:苏米特( Suumit )。 苏米特.萨( Suumit Shah )是我的至交和公司合伙人,他敏锐的商业头脑对于我这样的码农而言非常重要。他在凌晨 3 点打电话给我那只会有一个原因。公司出大事了。 我迅速在手机上滑动接听,虽然我的声音听上去有些沙哑和疲惫:“咋啦,兄弟?” “苏巴什( Subhash )! 网站全挂了!” 透过手机麦克风,苏米特的声音听上去就像是一声令人肾上腺素飙升和无比紧张的枪响:“快起来!网站全都不能用了!” 他不用再多说什么。我已经翻身下了床,光脚踩在冰凉的地板上,全身就像被电击一样。我摸索着找到了我的笔记本电脑, 熟悉的苹果标志泛着白光就像黑暗的房间里的一座灯塔。 我此刻思绪万千,一大堆可能出现的服务灾难场景在脑海中无序地飘过: 是被黑客攻击了嘛? DDoS 攻击?难道是哪个外国写脚本的小屁孩闲着无聊来搞垮我们的网站用以取乐? 或者是我们自己的程序员上传了一段有问题的代码?难道是某个多余的分号导致网站完全崩了? 是不是我们的云服务商挂了?如果是这种情况,那我们还能做啥? “网站页面显示不出来。手机端应用也在报错。全都挂了。完完全全的彻底崩了。”苏米特用紧张并且焦虑的声音继续说着。我甚至能听到他在电话那头来回踱步。 “好的好的,我已经在处理了。哥们淡定。” 我尽量让自己听上去比真实的内心更稳定一点。 一定要保持冷静。 救火第一条原则就是不要火上浇油去添乱。 略带睡意,我用笨拙的手指开始在键盘敲击。我打开终端( terminal )准备登录。这个黑底绿字的界面是通往我们所有线上服务的指令中枢。 我敲下了回车键。光标开始闪烁,继续闪烁,仍在闪烁。 正常来说,登录提示符应该很快就出现。但现在这个情况。。。感觉不妙。非常不妙。这意味着服务器不只是出现问题了,而是像一个已经进入生命倒计时的垂危病人甚至无法从病床下来应门。在等待了很长很长一段时间之后(长到我甚至觉得仿佛比一辈子还要久),登录提示符终于出现了。还好,至少服务器还没有完全挂,但也差不远了。 我开始思考原因:如果服务器响应如此缓慢,那应该不是简单的应用代码问题( bug )。应该是有更深层的问题。可能出在系统层面。就像是服务器本身在苟延残喘。 我输入了我的第一个诊断命令。这是用来检查服务器基本体征的一个简单工具: htop 屏幕上显示的检查指令输出结果令我脊背发凉。满屏的红色。 每一个进程( process )似乎都处于异常状态。CPU 使用率已经飙升到 100%。结果显示服务器几乎所有内存都已经被占用。甚至连服务器临时救急的 SWAP 存储空间都已经满了。 看上去服务器并不是在苟延残喘。 它其实已经凉透了,而我们只不过目睹了它超负荷状态下的最后几次抽搐(登录和检查指令响应)。 紧接着我马上知道了我们网站彻底歇菜的原因。简单得甚至有些尴尬:显示屏上方清楚地展示着服务器的总内存容量是 512MB 。 五百壹拾贰兆字节。 我的智能手机拥有 8GB 的内存,比这台承载了我们公司所有业务的服务器要大 16 倍。数千家电商客户以及他们数以百万记的商品名录,Dukaan (我们的公司)全部的希望和梦想都运行在这台比我口袋里的手机还要弱的服务器上。 这不是一次复杂的黑客行为或者代码错误。原因很简单:就是服务器不够用了。 就好像我们在一个小小的电话亭举办一场大型的摇滚演唱会,最终电话亭被挤爆了。 盯着屏幕,手机仍响在耳侧,我突然瞬间清醒。像我这样一个不是计算机专业科班出身,没有正儿八经大规模系统扩容经验的人,到底是如何承担起这一切的? 要弄明白这个问题,先得弄懂我们正尝试驯服的这头野兽。也就是需要如庖丁解牛一样深入理解此刻正在作妖的家伙:服务器。 |
11 rayeaster 2 月 12 日第 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 点的服务器屏幕尖锐地喊出了一个无可辩驳的事实: 我们的厨房对于我们的抱负而言,实在是太小了,小到足以致命。 |
12 rayeaster 2 月 13 日第 1 章第 3 节: 我们伟大而危险的大单体应用 如果说服务器就像厨房, 那如何描述我们大厨( CPU )正在使用的菜谱(代码)呢? 在软件行业的术语里,我们称之为服务架构。我们采用的架构非常经典, 是那种几乎每一个初创公司早期都会采用的架构。 这个名字听上去像某种远古巨石阵一般巨大,有点唬人。但事实上,它非常简单。单体应用是指你所有的代码逻辑都在一处一起运行。比如我们的用户注册,商品名录,订单管理,商户仪表盘,支付,所有功能都同处在一个 Django ( Python 流行的 web 框架)项目里面。 可以把它想象成一本巨大的百科全书般的食谱。它包含从开胃小食,主食,甜品,以及饮品在内的所有菜谱,很显然这本书会超级厚。 为什么我们要选择从大单体应用起步(为什么它是正确的上手选择!) ● 变得愈发沉重以致难以使用。 查找某个食谱可能耗时更长。搞清楚为什么甜品部分的一个改动可能影响到开胃餐部分变得几乎不可能。在软件行业的术语里,我们称之为紧耦合。 最重要的是(也是那天晚上导致我们网站崩溃的原因), 大单体应用让你别无选择:如果你仅仅想为食谱书中某一部分(对应软件应用的某个功能)扩容( Scale ),那你只能先把整本书都扩容。 比如我们电商客户的商铺主页面访问量很高 (好比是食谱上的主食部分很受欢迎)。但另一方面我们的销售仪表盘(好比是食谱上的开胃餐部分) 被访问的次数就要少得多。可是由于他们同处于一个大单体应用中,我们的服务器资源得同时提供给所有功能。 处理主食的巨大消耗(厨师/CPU )让其它厨房任务处于资源嗷嗷待哺的状态,最终导致整个系统的崩盘。 我们那运行在小得可怜的服务器上的大单体应用成了一个潜在的完美毁灭风暴:一个软件层面的单点( Single Point of failure )运行在一个硬件层面的单点。 这简直就是一枚时间炸弹,最终在周二凌晨 3 点 14 分被引爆了。 第 1 章的关键知识点总结 ● 你创业历程中第一台服务器肯定会有崩溃的一天。这不是一个会不会崩溃的问题,而是何时崩溃的问题。所以我们的目标应该是如何从崩溃中迅速恢复并从中总结经验, 而不是绞尽脑汁试图去避免崩溃。 |
13 rayeaster 2 月 15 日第 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 定义的最核心业务流程: 店家创建店铺:只需要一个单页面,用户输入手机号,接收一条短信验证码,然后允许为店铺去个自定义的昵称名字。齐活。店家的网店就生成了。无需邮箱,无需密码,无需各种复杂的表格填写。 很显然,这根本称不上是一个功能齐备的“平台”,也不是什么“电商解决方案”。不过就是个用兼容手机端的单页面来替换丑陋 PDF 的简单小工具。这是我们想到的用来解决目前 WhatsApp 商家难题的最简单方法。 确定好这个 MVP 之后,我们决定给自己上上难度。不要很长的开发周期,不要繁琐的需求讨论。我们打算只用一周末的时间完成开发并发布上线。 于是,48 小时的两人黑客松正式启动。时间不等人。现在需要做出第一个重大的技术决策:用什么打造我们的 Dukaan 滑板? |
14 rayeaster 2 月 16 日第 2 章第 2 节 选择技术栈 两天的时间不能浪费一分一秒。MVP 已经定义了我们将要做的“事情”, 也就是我们的数字版滑板。现在还需要找到“如何”完成它的工具,我们有哪些选择呢? 在软件行业,你选择的一系列工具被称作“技术栈”。就好比是盖房子。首先你得决定采用什么主建材。是用砖块,木材还是钢材?采用何种地基?需要哪些建筑工具?这些选择将决定造房子需要花多长时间,造好的房子有多坚固,以及以后想增加一个房间是否容易。 对于一个周末的黑客松项目来说,只有一个最重要的因素决定着类似的选择:构建的速度。我们暂时不需要考虑那些高大上的可扩展架构,或者最先进最时髦的技术栈。我们需要的是能在最短时间内帮助我们从零打造出可用产品的技术栈。 这意味着我们需要一个熟悉的,可靠的,能帮我们完成很多“脏活”的工具箱。 深入技术细节:编程语言和框架 这是我们第一个也是最重要技术选择:选哪个编程语言以及何种编程框架? 为什么选择 Python ? 编程语言就是你往计算机下达任务的指令“词汇表”。我们选择 Python 是因为它赖以成名的简洁语法。代码读上去就像是普通的英文。当你跟时间比赛时,最好不要在调试工具上浪费时间,比如试图搞明白某个复杂的编码机制或者漏写了一个分号。Python 代码写起来毫无障碍,让我们可以专注在 MVP 问题本身。而且 Python 社区相当活跃,每一种需要的功能都可以找到现成的库来调用。 为什么选择 Djanjo:开箱即用的框架 编程语言仅仅是”词汇表”。好的框架就是完整的指令手册。框架提供了合理的编码结构,顶层设计,以及一系列预先构建好的组件,这些都可以让你无需从零起步。 还是以盖房子为例。你当然可以自己去砍树,自己去加工板材,甚至自己打磨钉子。或者,你也可以购买一个预制的房屋套装,包含所有的墙壁和门窗。你只需要把它们组装起来,然后根据个人喜好做些微调。 这就是 Django 的价值。它是构建互联网应用的预制盖房套装。它的设计哲学就是著名的开箱即用( Batteries-Included )。这意味着你直接立马可以上手开始构建。对于我们需要在 48 小时内完成的 MVP 而言,Django 的两项功能绝对是大杀器: Django 管理员后台:这是 Django 的杀手锏。只需要几行代码,Django 就可以生成一个完整的安全的并且看上去非常专业的管理员后台。这主要提供给我们作为创始团队登录用以查看全部业务数据。 当有新用户创建一个网店,我们就会在管理后台收到通知。我们可以查看相关商品名录,帮助修改错误的输入(仅在必要时)或者排查用户在使用过程中碰到的其它问题。但如果从零开始,弄出这样一个管理员后台可能需要花费一天的时间。Django 不花我们一分钱,只需要 15 分钟就可以准备好。这个管理员后台就是我们的任务指挥中心。 深入技术细节:我们考虑过的其它语言和框架 Django 当然不是唯一的选择。技术层面看,总有很多不同的方法可以达到同一个目的。关键是要根据任务性质和特点选择最合适的工具。 Node.js 以及 Express 框架:这也是一个非常流行的组合。Node.js 允许使用 JavaScript 语言编写服务端代码,而 JavaScript 通常是运行在客户端浏览器中。这对于很多全栈型创业团队非常友好。Express 框架则非常灵活,也遵循极简设计的原则。所以对于我们而言,这反而是它的不足。与其说 Express 是一个预制的盖房套装,不如说更像是一盒子高质量的乐高积木。它提供了很多很底层的功能,但是需要自行组装更高阶的模块。由于我们只有 48 小时,所以这样的自由度不是我们所需,我们需要的是 Django 开箱即用的丰富组件和编码结构。 有了合适的编程框架,我们需要决定在哪持久化保存我们的业务数据,比如店铺名称,商品明细,价格等等。我们需要一个数据库。如果说编程框架是盖房套装,那数据库无疑就是房子的地基。必须得稳定,可靠并且易于查找数据。 为什么选择关系型数据库? 我们决定使用一款关系型数据库( Relational Database )。原因很简单:我们的数据将保存在数据库的表( Table )中,就像一张巨型且功能强大的 Excel 表格。店铺信息存储在店铺表中,产品信息存储在产品表中。更关键的是,可以在这些表之间建立关系( Relationship ),比如每一款产品肯定属于某一个店铺。 这样的关系结构对于电商业务是完美的选择。数据间有了明确的联系和规则。肯定没人想看到一个不属于任何店铺的产品信息,或者一个没有顾客信息的订单。关系型数据库会强制约束数据之间的关系结构,保证数据始终一致,不多也不少。 为什么选择 PostgreSQL ? 在众多的关系型数据库中(比如 MySQL ,微软的 SQL Server 等等),我们最终选择了 PostgreSQL (常被简称为 Postgres )。 原因呢?其实对于 MVP 而言,Postgres 及其主要竞争对手 MySQL 都是不错的选择。但我们出于以下几点更倾向于 Postgres 。Postgres 在开发社区中的口碑相当不错,令人称赞地健壮,可靠并且符合通用的技术标准规范。绝对是开发中可以放心使用的主力选择。更重要的是,我知道 Postgres 有一些后面我们可能能用上的高级功能。比如其中一个功能是订阅通知机制( LISTEN/NOTIFY ),这将是我们在第 8 章提到的实时缓存系统背后的秘密武器。对于我们要完成的 MVP ,这个特性还用不上,但即使暂时用不到全部功能,从一开始就选择一个功能强大的地基,在将来无疑也是会受益颇多。 综上,我们的盖房图纸已经就绪。技术栈选择如下: 编程语言:Python |
15 rayeaster 2 月 17 日第 2 章第 3 节 打好地基 理论部分的讨论随着我们技术栈的选型敲定可以暂告一段落。现在该花点时间去实作了。我们需要为代码在互联网运行找一个落脚点,也就是说我们需要一台服务器。 创建一个 DigitalOcean 账号:就是很标准的注册步骤。 创建一个水滴服务器:这就是我们施展的舞台。创建时你会看到一个简洁干净的引导页面。 选择一个操作系统镜像:“镜像”( Image )是预先构建好的软件模版,通常用于服务器的操作系统以及相关基础软件。我们选择了标准的 Ubuntu 镜像,这是一个 Linux 的发行版,也是互联网应用服务器端的主流选择。Ubuntu 拥有强大的社区,安全且免费。我们选择是长期支持版本( Long Term Support ),这样可以在若干年内都得到安全方面的更新。 选择付费套餐:这决定了服务器的性能(也就是盖房子的地块大小)。我们直接拉到页面最底部选择了最便宜的那一款:512MB 内存,1 个虚拟 CPU ,20GB 的 SSD 硬盘。每个月只需要 5 美元。对于一个零用户零收入的 MVP 项目而言,这应该是一个理性的选择。狗窝虽小,但也是属于我们自己的狗窝。 选择服务器区域:这决定了服务器在物理层面机房所属的地点。我们选择了班加罗尔。原因?因为我们知道首批用户肯定来自印度。就近选择服务器会让应用访问更快延时更小(因为数据传输耗时更短)。 最后点击“创建水滴”按钮。 这几步做完之后,我们等待了大概 1 分钟,DigitalOceam 就把我们的服务器准备好了。我们现在骄傲的成为了一个公网 IP 地址(也就是我们服务器在互联网的专属地址)的主人。我们准备用来盖房的小小地块已经准备就绪。 网络服务器( Nginx ):对应服务员。我们采用的网络服务器是 Nginx (英语发音同"Engine-X")。它是你的餐厅里一位友善且高效的服务员。每一位顾客进来后首先打招呼就是它。它在同时处理数以千计的网络连接以及简单快速任务的表现堪称惊艳。Nginx 的主要职责在于: 返回静态文件:比如用户请求访问一张图片,一个 CSS 或 JavaScript 文件,Nginx 都可以从储藏室(硬盘)直接读取然后返回给用户。这不需要麻烦大厨亲自处理这些请求。这对于系统的整体效能提升具有重大意义。 应用服务器( Gunicorn ):后厨经理。我们选择的应用服务器是 Gunicorn ,就类似我们的厨房经理。它从 Nginx 得到网络请求然后把请求转换成我们主厨( Django )能够理解的某种形式。它会同时管理着多个负责不同工序的厨师(就是服务器中的工作进程 Worker Process )。应用服务器是外部世界(由 Nginx 负责打交道)和我们的应用代码(由 Django 编写)之间的关键连接部分。 这样,整个流程看上去虽然简单但潜力十足;互联网的用户请求首先抵达 Nginx 。Nginx 要么直接返回静态文件,要么吧请求转发给 Gunicorn 。然后 Gunicorn 运行 Django 代码处理请求,生成 HTML 页面,最后把处理好的结果返回给 Nginx ,最终得以让用户看到网站。 MVP 不是最终产品的初级版本,而是一场验证核心假设问题的科学实验。它的目标是尽可能获取有用的反馈和经验,不是打造完美的产品。先花时间想清楚验证所需的最简陋“滑板”是什么再开始动手写代码。 |
16 rayeaster 2 月 17 日第 3 章 解耦应用和数据库 第 1 节 上线次日 应用逻辑层面的工作(厨师的操作):这大部分是需要“思考”的工作,主要由 Django 代码实现并由 CPU 负责执行。所以这项逻辑执行工作的瓶颈在于 CPU:比如找到正确的产品用于展示,计算订单的总价,判定用户是否完成了登录。就类似主厨会不停忙着查看菜谱,切配食材,炒制以及试味等等。这个类型的工作需要动作麻利的主厨(一个比较高端的 CPU )以及一个相对较大的操作台面空间(内存)才能比较高效地完成。 而我们面临的问题就在于我们正在强迫我们优秀的大厨( CPU )还得全职兼任图书馆管理员。好比是要一个大厨在一个繁忙喧杂的图书馆烹饪一份精致的大餐。从书架来回奔命(磁盘读写)让大厨根本无暇顾及他的本职工作:烹饪(执行代码)。这就导致两个工作都没法完成。 服务器 1:应用服务器。这台服务器将专为依赖 CPU 的任务进行优化。它将运行 Nginx ,Gunicorn 和 Django 代码。它只负责进行“思考”。 服务器 2:数据库服务器。这台服务器将专为依赖输入输出的任务进行优化。它将运行我们的 PostgreSQL 数据库。它只负责“记录”。 这是当时我做出的优化方案。一次彻底的解耦。听上去符合逻辑,应该是个正确的选择。但这也意味着我们需要在保持 Dukaan 网站可被访问的条件下进行一次复杂的开胸外科手术。我们需要把存储着我们所有用户,所有商品以及我们创业公司所有数据的完整数据库从一台服务器迁移到另外一台服务器。 |
17 rayeaster 2 月 18 日第 3 章第 2 节:迁移计划书 迁移数据库的决定已经做出。接下来就要付诸行动了。此刻感觉就像是站在悬崖边,但不得不往下跳。现在的问题是能不能在跳下去之前弄个降落伞。 迁移前:单台服务器(例如公网 IP:104.248.62.77 )搞定全部:Nginx ,Gunicorn ,Django 以及 PostegreSQL 。 应用服务器 (公网 IP 同迁移前:104.248.62.77 ): 负责运行 Nginx ,Gunicorn 以及 Django 。 因此应用服务器不再同处于 localhost (意为在此同一台机器上)的数据库进行通信。现在它需要通过网络连接与新的专用数据库服务器对话。 启动维护模式:第一步是禁止任何新数据的写入。我们在网站上发布了一个“正在维护”的页面,任何此时访问 mydukaan.io 的用户都将被告知:“Dukaan 网站正在升级,请 5 分钟后再回来。” 整个迁移过程中,我们的网站只有 3 分钟不能访问。 |
18 rayeaster 2 月 19 日第 3 章第 3 节:新瓶颈 完成上述数据库迁移后,我们赢得了过去数周内第一次喘息之机。 暂停手头的活儿。 以上整个过程就是一次完整的网络请求。所花时间被称为网络延时。 获取店铺诸如名字等的完整信息。(一次往返) 一个简单的页面加载可以轻松产生 10 次,20 次甚至 50 次往返于数据库的网络请求。在数据库分离之前,这些请求的耗时可以忽略不计。但现在,它们在物理层面有实实在在的开销:50 次网络请求 乘以 2 毫秒/次网络延时 等于 100 毫秒的整体延时。 N+1 查询难题:我们同样受困于互联网应用开发中最常见的性能杀手:N+1 查询问题。设想一下,你需要获得( 1 ) 10 家店铺的信息以及( 2 )每家店铺排名第一的商品信息。最无脑的代码可能是这样: 先查询一次数据库,获得 10 家店铺的信息数据。 解决方案( select_related 以及 prefetch_related ):幸运的是 Django 有内置的解决方案。它有一个称作 prefetch_related 的功能,我们可以让 Django 这样做:“当读取那 10 家店铺的数据信息时,因为我肯定也需要读取各家店铺的商品数据信息,所以干脆请把店铺的相关商品数据一并取回。”Django 很聪明,它明白这个任务只需要 2 次数据库读取,而不是 11 次。第一次读取拿到 10 家店铺的数据信息,第二次读取就把全部 10 家店铺的所有商品数据信息都拿回来并与前面拿到的店铺数据整合到一起,供我们的应用代码使用。这就如同是我们的“购物清单”。在所有代码中完成这些优化的效果立竿见影,显著降低了网络请求的次数,让整个应用的响应更加迅速。 |
19 rayeaster 2 月 19 日第 3 章第 4 节:分叉口-我们为什么坚持使用 SQL 如前所述,我们成功实施了数据库与应用服务器的分离。PostgreSQL 数据库现在拥有专属的强大服务器,不再与应用代码逻辑挤在一起。这对于关系型数据库而言是一个经典的拓展升级。 SQL 的宇宙(关系型数据库): 类比:SQL 数据库就像一个清晰明了的 Excel 文件,其中包含了多个互相关联的表格。 NoSQL 的宇宙(非关系型数据库): 类比:NoSQL 数据库就像一个文件目录,其中塞满了各式各样的 Word 文件或者 JSON 文件。每个文件都可能拥有完全不一样的内容结构。 快速总结一下 SQL 和 NoSQL 的不同点: 特性 SQL ( PostgreSQL ) NoSQL (比如 MongoDB ) 我们为什么选择 SQL 路径 我们的数据是高度结构化的:比如每个订单都有一个顾客,已购商品清单,以及一个总价。一个商品总会有名字,价格,以及库存量。我们业务逻辑依赖于这样严格的约束关系。我们不需要 NoSQL 的灵活性,而是需要 SQL 的严格一致。 我们并没有海量数据的烦恼。我们面对的是电商领域经典的业务难题。如果我们选用一个流行的 NoSQL 数据库,倒像是不合时宜地杀鸡用牛刀。PostgreSQL 就是那个精准契合我们业务场景的可靠且强大的工具。我们相信采用 PostgreSQL 作为基础数据库足以满足业务增长的需求,甚至应对独角兽级别( 10 亿美金)的业务量也没问题。 架构扩展的关键第一步是分离应用服务器以及数据库服务器。这样可以让各组件充分发挥潜能,不至于消耗在资源竞争上。 |
20 rayeaster 2 月 20 日第 4 章:网络世界的交警:走进负载均衡 第 1 节: 崩溃的厨房 垂直拓展(性能升级 Scaling Up ) 这可能是一种最容易想到的办法。如果厨房动作太慢,那就把厨师换成一个能以两倍效率工作的世界顶级大厨。 优点:操作很简单。无需更改代码或者架构。只需要多花点钱就可以让问题大事化小。 很容易导致开销飙升。两倍性能的服务器的价格不一定是两倍。有可能是 4 倍或者 8 倍。价格往往指数增长。 水平拓展(服务器集群 Scaling Out ) 这个方法虽然不如前者那样容易想到,但却是更加强大的一种选择。与其重金重新聘请一位超级大厨,不如保留当前的厨师然后再请 3 位类似水平的厨师。这样让他们同时一起工作也能加大厨房的产能。 优点: 性价比很高。小型服务器的硬件通常不贵且易于替换。增加同配置的小型服务器成本可控,不会很离谱。 缺点:系统会变得更加复杂。比如当有服务员拿着顾客新点的单来到厨房时,那这个菜该交给厨房里四位水平相近的哪位厨师来处理呢?这个决定该如何做出? 我们的选择显而易见了。垂直拓展更像一个创口贴式的临时方案。它不适合作为长期的技术选择。我们希望建立的是可以服务数百万用户的公司,所以我们需要一个能够随着业务增长而演进的技术架构。水平拓展是我们的必修课。 |
21 rayeaster 2 月 20 日第 4 章第 2 节:网络交警 水平拓展的决策是我们创业路上一个重要的转折点。我们将从单服务器转向集群模式。但是如果没有能有效调动军队的指挥官和指令系统,那再大的集群也没用。我们现在已经拥有了随时可以开工的一群厨师,还需要一个懂得如何为厨师分配顾客点单的首席服务员。 “您好先生,请到 3 号柜台结账。” 可以看出来,这位经理就扮演了类似负载均衡的角色。他们的工作都是确保每一个结账柜台的工作人员不会超负荷,也不会有人因为没有顾客前去结账而无所事事。他们为客流削峰填谷,保证系统高效运行。负载均衡同时也负责健康检查。如果某个工作人员突然晕倒(类似服务器宕机),经理会立刻停止给他再安排结账顾客,而是会把顾客引导去其他正常工作的柜台。这样系统就能保持稳定的运行。 轮询( Round Robin )分配:简单而蠢萌的方法 这是最基本的负载均衡算法。也很容易按字面理解:简单的循环分配请求给所有的服务器。 第 1 个请求分配给服务器 A 。 就像给一群玩家发扑克牌。每位玩家都会轮流得到一张牌。 优点:超级简单,基本无需负载均衡服务额外思考。 最少连接( Least Connections )分配:更聪明的方法 这是一种更聪明的动态算法。负载均衡服务会实时监控集群中每一个应用服务器当前有效连接的数量。当新的网络请求到达时,负载均衡服务会把它送到当前连接数量最少的服务器。 优点:这种方法天然考虑到了有些请求会处理得比其它请求慢一些的情况。如果某台服务器正忙着处理复杂的请求,它会同时维持更多的用户连接,所以负载均衡会暂时不考虑继续给它新增任务,以便它尽快完成手头的活。这让工作负荷的分配更加公平也更有效率。 对于 Dukaan 网站来说,我们的选择已然明了。“最少连接”算法是更聪明更健壮的办法,能够更好地应对我们不可预知的用户流量。 |
22 rayeaster 2 月 21 日第 4 章第 3 节:我们的第一位交警 前面讨论的理论已经很完备。我们制定了一个构建服务器集群的计划,还需要一个负载均衡服务来分配流量。现在该撸起袖子把这些理论变成现实了。 # 定义处理应用逻辑的应用服务器集群。这个集群被称为"app_servers"。 # 列出应用服务器集群的内网 IP (出于安全和效率考虑) server{ location / { 好了。上面配置中的 upstrem 部分定义了我们的应用服务器集群。least_conn;那行指定了我们智能的流量分配策略。proxy_pass 指令让 Nginx 开始做流量分配。保存并重启 Nginx 之后,我们的负载均衡服务就已经上线开始运行了。 用户访问 dukaan.app 网站。请求到达作为负载均衡的 Nginx 。 如果 1 号服务器宕机了,Nginx 作为负载均衡服务将通过健康检查及时发现这种情况并停止转发流量给 1 号服务器。接下来所有流量将全部转发给 2 号服务器。网站此时仍然在线。系统实现了一定程度的容错性。流量爆发和服务器宕机都能应对自如。此刻我们又感觉膨胀了。 水平拓展是可以同时实现高可用和大规模的唯一长期技术路径。虽然比垂直拓展更复杂,但是性价比更高,更灵活,并且能消除单点隐患。 |
23 rayeaster 2 月 22 日第 5 章:数据库俱乐部的保镖:只读副本 作为创业公司,当用户数从几千达到 10 万级别的时候,业务重心会发生显著变化。早期主要关注的是冷启动获客以及确保刚上线的服务能正常工作。出现的问题也多半是显而易见的,比如服务器宕机,网站应用崩溃了。解决方案也比较简单粗暴:重启服务器,或者加钱买更好配置的服务器。 但是当用户数达到 10 万的里程碑之后,新类型的问题就会出现。肉眼可见的火情会被缓慢悄无声息的发热取代。系统可能不会突然崩溃,但是可能会逐渐力不从心,变得愈发迟缓。问题重心不再是有没有顾客用,而是如何提高性能满足更多用户的需求。所以相应的解决办法不再那么简单粗暴,需要更多如外科手术般的精准下药。就像盖房子,此时不能仅满足于屋里的灯还能亮,而是要开始思考建筑物本身的结构是否足够稳固等更深层的问题。 我们通过水平拓展的服务器集群以及负载均衡技术成功解决了厨房产能的问题。现在我们面临的问题是储藏室/图书馆开始变得非常拥挤以至于甚至让人都迈不开步。 **第 1 节:图书馆里的交通阻塞** 用上负载均衡之后的确很爽。应用层的流量处理充满技术美感。当我们实时监控到流量和应用服务器 CPU 占用率暴涨的时候,我们只需要点击几下鼠标就能增加一个新服务器到集群然后就能看到工作负荷神奇地被自动分配到新加的服务器,然后其它服务器的负载就会降下来。应用服务层面完全可拓展,一切尽在掌握。 我们的用户数很快就彪过了 5 万,接着是 8 万,迅速接近起初难以想象的 10 万大关。每个店铺卖家都拥有各自的顾客群体,意味着浏览 Dukaan 网站的用户人数可能是百万级别的。我们正在服务的流量已经远超预想。 熟悉的恐惧感又悄悄回来了。我们又开始收到来自用户的抱怨,但这次不是关于网站宕机了,而是响应太慢了。 - “我的顾客需要等待 5 到 6 秒才能看到我家店铺” 这种网页响应迟缓的情况在印度日常工作高峰时间段(上午 11 点至下午 5 点)达到顶峰。我和苏米特目不转睛盯着监控图表。应用服务器看上去表现还行,CPU 的工作负荷分配均衡且极少超过 50%。负载均衡服务工作地很出色。 但我们那台数据库服务器的性能图表却是完全不同的情景。CPU 占用率基本维持在 80%到 90%的高位。硬盘输入输出数据已经爆表,表明硬盘已经达到基线负荷了。曾经扮演我们救星的独立强大的数据库服务器现在正被负载压得喘不过气。我们的图书馆已经满满当当挤满了人,我们的英雄图书馆管理员变得不堪重负。 **确定瓶颈:只是看一看的人数实在太多了** 简单地说“数据库响应太慢了”就如同一位医生说“这个病人确实生病了。”一样毫无意义。这不是精确的诊断,只是表面的观察。如果要实现有效治疗,必须搞清楚具体明确的病因。我们需要深入数据库内部,确认它正为之头疼忙碌的具体任务。 **深入技术细节:区分数据库操作(只读与写入)** 说到底,数据库主要有两种不同的任务类型,理解它们之间的差异非常重要。 1. **写入**:**改变**的数据的操作。主要指令包括 INSERT (增加新数据),UPDATE (修改已有数据)以及 DELETE (删除数据)。 2. **只读查询**:这是只涉及**读取数据**的操作。主要指令是 SELECT 。 我们为了确定解决问题的关键方向特意安装了一款分析数据库请求的工具。我们发现我们面对的问题在互联网应用行业十分常见,甚至大家都给这个现象专门起了一个名字。 **95/5 法则(读写差异)** 我们的分析揭示了令人的不平衡状态。在数据库每 100 次请求中: 这其实很符合我们的观察。一位店家每天可能只会更新他们的商品(数据写入)寥寥数次,但是它的店铺会被数以千计的顾客反复查看(数千次只读操作)。我们的系统承载的绝大部分流量都来自只读请求。 **只读操作如何拖累写入操作** 这就是我们面临的问题核心:我们的数据库按照同等优先级处理只读和写入。它只有一个任务队列。 设想我们的图书馆管理员如何面对只有唯一入口以及唯一等待排队队伍的场景。在排队的人群中,95%只是想问“我在哪能这本书?”(这其实是一个很快的只读查询)。但同时剩余的 5%可能是作家群体想要完成新书的馆藏登记,这样的处理请求通常需要填写很多表格并更新庞大的书籍索引(类似更慢的写入操作)。 作家们不得不与常规入馆的庞大读者等在同一个长长的队伍中。简单的数据只读查询请求量实在太大以至于产生了堵塞反而拖累了关键的数据写入请求。这就是问为什么店家保存新商品的时候会感觉很慢很慢,因为他们重要的写入操作已被淹没在成百上千来自匿名顾客对店铺的浏览请求之后,完全得不到被数据库处理的机会。 解决方案也比较明确了。不能让所有人都挤在同一个队伍里。需要为作家们单独创建一个专享专用的通道,同时为进馆借阅的众多读者提供一个更宽敞的空间。意味着我们需要把只读操作与写入操作分离开来。 |
24 rayeaster 2 月 23 日第 5 章第 2 节:保安和 VIP 通道 问题已足够清晰。我们的图书馆只有一个通道,海量的读者洪流在仅有通道的中排起了长队,导致作家们无法及时完成新书的注册登记工作。解决方案是新开设一个专用通道。这样我们的作家们就有了专属的 VIP 的通道,前来借阅的芸芸大众也会有更宽敞的读者大厅。 主库会处理所有写入操作( INSERT ,UPDATE 以及 DELETE )。酒吧状态的任何改变,比如新抵达了一位 VIP 顾客,在场的客人点了一杯酒,或者有客人离店了都需要通过主库来完成。就好像 VIP 厅门口站着一位很严格很干练的保安,它会确保所有状态改变都是符合规则且被如实准确地记录在案。 只读从库( Slave ):酒吧大厅 从库只允许读操作( SELECT )。成千上万的顾客可以同时出现在宽敞的大厅里,享受音乐,欣赏周围的一切。他们可以看到 VIP 厅的活动,但是无法做出任何“写入”操作。 我们需要的架构升级就是如上所述的职责分离。这样可以让我们单独地拓展数据库读取和数据库写入操作。 WAL (预写日志):我们的主库( VIP 厅)有一位勤勉的保安,它在一本特殊的日志中事无巨细地记录下了所有发生的变化。来了一位新顾客?记下来。价格变化了?记下来。这样一本日志被称作预写日志( WAL )。本质是按时间排序且实时变化的数据库完整修改记录。 以上流程的结果便是从库近乎完美地镜像了主库。就好像 VIP 厅的活动通过视频流转播到酒吧大厅巨大的屏幕上,让所有人都能实时看到。 配置多个数据库连接:我们首先需要在 Django 的配置里从一个数据库连接增加到两个数据库连接:默认的( default )连接至主库,只读连接( read_replica )至新创建的制度从库。 # 我们数据库路由的简化版本 def db_for_write(self, model, **hints): 有了数据库路由,现在我们的应用代码变得更聪明。每次当用户加载店铺页面(触发一系列 SELECT 数据查询),路由代码会把这些流量都发送到强大的只读从库。当卖家点击按钮保存新商品时(触发 INSERT 或者 UPDATE 写入操作),路由代码会把该写入操作发送到受到严密访问控制的主库。 一致性意味着酒吧所有人都能同时看到同一个状态。比如 VIP 厅更改了背景音乐歌单,那大厅的人们也能立刻听到相同的新歌曲。 关键在于:现实世界中分区必然存在。有时候是网络中断,网络包丢失,光缆损坏等等。所以在分区存在的情况下,现实的分布式系统必须要在一致性和可用性中 2 选 1 。 如果下次请求直抵主库,顾客将看到修改后的数据:800 卢比。 这两个价格可能都算是“合理”的,取决于所处是 VIP 厅还是大厅。但是从卖家角度看,这显然有问题。价格刚才已经被修改了,为啥店铺仍然显示旧的价格? 强一致性 最终一致性 因果一致性 选择适合的方案 |
25 rayeaster 2 月 24 日第 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 点击保存项链的新价格:发送写入操作到主库。 这种方案让我们能够同时享受两种一致性模型的优势:提供给普通用户的始终可用,以及提供给关键用户的强一致性。 **第 5 章关键知识点总结** - **用只读从库拓展数据库是性能巨大提升,但不是没有代价**:为了最终一致性(技术实现更复杂)牺牲了强一致性的简洁。 |
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。