如需要哪些页面和按钮,每项操作要完成什么,才能实现软件的目的,这就是 功能需求(functional requirements)
非功能性需求(nonfunctional requirements),应用应当快速响应、可靠、安全、符合法律规定,易于维护
下面主要讨论下面几点
如X 推特的社交网络,可以发帖,关注其他用户。
用户每天发5亿个帖子,平均每秒5700条,偶尔会飙升至每秒 150000 条,假设每位用户平均关注200人,也有200名关注者。 大多数人只有几名关注者,而少数名人有超过1亿名关注者。
下面展示了,一张表存储用户、一张表存储帖子、一张表存储关注关系。
假设首先时间线,用来展示你所关注的人最近发布的帖子
SELECT posts.*, users.* FROM posts
JOIN follows ON posts.sender_id = follows.followee_id
JOIN users ON posts.sender_id = users.id
WHERE follows.follower_id = current_user
ORDER BY posts.timestamp DESC
LIMIT 1000帖子讲究时效性,客户端可以每隔5秒进行轮询请求拉取。
如果同时在线1000万,意味着每秒执行200万次查询。
让客户端轮询,不如由服务器把新帖子主动推送给当前在线的关注者。
这种预先计算并不断更新查询结果的过程称为 物化(materialzation),时间线缓存就是一个 物化视图(materialized view)。
过载系统无法恢复时,可能导致 重试风暴,等待处理的请求排起长队,响应时间可能因此增长到客户端超时并重发请求,挤压越来越多,可能需要引入重试算法 指数退避、熔断器、令牌桶算法 等。
如果增加计算资源能够显著提高系统的最大吞吐量,就称这个系统具有 可伸缩性(scalability)。
即使反复发送同一个请求,每次的响应时间也可能相差很大。许多因素都会带来随机的额外延迟,例如:上下文切换到后台进程、网络丢包和 TCP 重传、垃圾回收暂停、缺页迫使系统从磁盘读取数据、服务器机架的机械振动,等等。
响应时间的波动很大一部分往往来自排队延迟。服务器同时能处理的任务数量有限(例如受 CPU 核数限制),因此只需少数几个慢请求,就足以阻碍后续请求,这种现象称为 队头阻塞(head-of-line blocking)。
服务通常会报告 平均 响应时间(严格来说是 算术平均值:把所有响应时间相加,再除以请求数)。平均响应时间有助于估算吞吐量的上限。
将响应时间从快到慢排列,中位数 就是位于正中间的值。例如,如果响应时间的中位数是 200 毫秒,就意味着一半请求用时不到 200 毫秒,另一半则需要更长时间。因此,如果想知道用户通常要等多久,中位数是个很好的指标。
响应最慢的客户往往是在账户中积累了最多数据的人,价值往往很高。亚马逊认为,针对第 99.99 分位数(每 10,000 个请求中最慢的一个)进行优化,成本过高,收益又不足。降低极高分位数处的响应时间非常困难,因为它很容易受到不可控随机事件的影响,而且越往后收益越小。
如果后端服务要为一次最终用户请求执行多次调用,那么高分位数尤其重要。即使这些调用并行发出,最终用户请求仍要等待其中最慢的一次完成。
尾部延迟放大(tail latency amplification)
服务级别目标(SLO) 和 服务级别协议(SLA) 中,用来规定服务应达到的性能和可用性。
一项 SLO 可能要求服务的中位响应时间低于 200 毫秒、第 99 分位数的响应时间低于 1 秒,并要求至少 99.9% 的有效请求得到非错误响应。
SLA 则是一份合同,规定未达到 SLO 时会怎样处理(例如,客户可能有权获得退款)。
可靠性(reliability) 可以粗略理解为“即使出了问题,也能继续正确工作”。
为了更准确地描述所谓“出了问题”,我们要区分 故障(fault) 和 失效(failure)
例如,如果一块硬盘停止工作,我们会说这块硬盘失效了:若整个系统只有这一块硬盘,系统也就停止了提供所需服务。
如果我们谈论的是一个包含许多硬盘的系统,那么单块硬盘失效从整个系统的角度看只是一项故障;只要数据在另一块硬盘上还有副本,整个系统就可能容忍这项故障。
如果某些故障发生时,系统仍能继续向用户提供所需服务,我们就称它是 容错的(fault-tolerant)。如果系统无法容忍某个部分出现故障,这个部分就称为 单点故障(SPOF),因为它一旦发生故障,就会升级为整个系统的失效。
在这类容错系统中,通过故意触发故障来 提高 故障率有时反而是合理的,例如毫无预警地随机杀死某个进程。这称为 故障注入(fault injection)。
为各个硬件组件增加冗余,以降低整个系统的失效率。
云系统往往不那么强调单台机器的可靠性,而是力求在软件层面容忍节点故障,让服务实现高可用。
能够容忍整台机器丢失的系统,在运维上也有优势。如果需要重启机器(例如安装操作系统安全补丁),单服务器系统必须安排停机;而多节点容错系统可以逐个重启节点来安装补丁,不影响向用户提供服务。这称为 滚动升级(rolling upgrade)。
引发这类软件故障的缺陷往往会潜伏很久,直到一组不同寻常的条件将其触发。这时人们才发现,软件原来对运行环境作出了某种假设;这个假设通常都成立,却最终会出于某种原因不再成立。
软件系统由人设计和构建,维持系统运行的运维人员同样也是人。与机器不同,人类不只是照章行事;
针对大型互联网服务的研究发现,运维人员修改配置是服务中断的首要原因,而硬件故障(服务器或网络)只在 10%~25% 的中断中起了作用。
越来越多的组织开始形成 无责复盘(blameless postmortem) 的文化:事故发生后,鼓励所有参与者毫无保留地讲清事情经过,不必担心受到惩罚;这样,组织中的其他人才能从中学习,避免今后再发生类似问题。复盘过程也许会发现,业务优先级需要调整,长期遭到忽视的领域需要投入,相关人员的激励机制需要改变,或者还有其他系统性问题需要提请管理层关注。
可靠性非常重要,如果商务软件导致上市公司把财报报告的数字有误,电商中断服务导致卖家收入损失,家长把孩子的 所有照片视频保存在你的平台数据库损坏导致内容永久丢失。
英国邮局 Horizon 丑闻(Post Office Horizon scandal)。
系统今天能可靠运行,并不意味着将来也一定能够可靠运行。系统退化的一个常见原因是负载增加。
也许并发用户从 10,000 人增长到了 100,000 人,或者从 100 万人增长到了 1,000 万人;也许系统现在处理的数据量比过去大得多。
可伸缩性(scalability) 是描述系统应对负载增长能力的术语。
可伸缩性不一定从一开始就要考虑,不然可能会引入大量复杂工作影响工程快速推进。什么时候讨论项目的可伸缩性很重要。
描述通常是某项吞吐量指标,例如服务每秒收到的请求数、每天新增多少 GB 数据,或每小时完成的购物车结账次数。
可能需要知道数据库的读写比例、缓存命中率,或每位用户拥有的数据项数量(例如社交网络案例中的关注者人数)。有时平均情况最重要,有时瓶颈却由少数极端情况主导。一切都取决于具体应用的细节。
描述好系统负载以后,可以研究负载增加时会发生什么:
如果资源增加一倍,就能在性能不变的情况下处理两倍负载,我们称系统具备 线性可伸缩性 ,这通常是一件好事。
增加服务硬件资源最简单的办法,就是把服务迁移到更强大的机器上。单个 CPU 核心的速度已经不再显著提高,但你仍可以买到(或在云上租用)配有更多 CPU 核心、更大 RAM 和更多磁盘空间的机器。这种方法称为 纵向扩展(vertical scaling) ,也叫 向上扩展(scaling up) 。
在单台机器上运行多个进程或线程,可以获得并行处理能力。同一进程中的所有线程都能访问同一块 RAM,因此这种方法也称为 共享内存架构(shared-memory architecture)。共享内存方案的问题在于,成本增长得比线性更快:硬件资源多一倍的高端机器,价格通常远远不止两倍;受各种瓶颈限制,一台规模翻倍的机器又往往处理不了两倍的负载。
共享磁盘架构(shared-disk architecture) :多台机器分别拥有独立的 CPU 和 RAM,却把数据存储在共同访问的一组磁盘阵列中,机器与磁盘通过高速网络连接,例如 网络附加存储(NAS) 或 存储区域网络(SAN)。
无共享架构(shared-nothing architecture) (也称为 水平扩展(horizontal scaling) 或 向外扩展(scaling out))已经广受欢迎。这种方案采用包含多个节点的分布式系统,每个节点都拥有自己的 CPU、RAM 和磁盘;节点之间的一切协调,都通过普通网络在软件层完成。
大规模系统的架构通常高度依赖具体应用,不存在一套通用、放之四海而皆准的可伸缩架构(俗称 万金油(magic scaling sauce))。
例如,处理每秒 100,000 个请求、每个请求 1 kB 的系统,与每分钟只处理 3 个请求、每个请求却有 2 GB 的系统,看起来会截然不同——尽管二者的数据吞吐量同为 100 MB/s。
正在开发一个快速增长的服务,很可能每当负载增加一个数量级,就需要重新考虑架构。应用需求本身也很可能不断变化,因此提前为超过一个数量级之后的伸缩需求作规划,通常并不值得。
应用程序的需求经常变化,软件运行的环境也会变化(例如依赖项和底层平台),而且软件中总有缺陷需要修复。
维护工作本身也很困难。一个成功运行多年的系统,很可能仍在使用如今已经没有多少工程师了解的过时技术,例如大型机和 COBOL 代码;随着人员离开组织,关于系统为何如此设计、怎样设计的组织知识可能已经丢失;维护者也许不得不修正前人留下的错误。
今天构建的每个系统,只要足够有价值、能够长期存续,终有一天都会成为遗留系统。
可运维性:让运维更轻松,便于组织保持系统平稳运行。
良好的运维往往能绕开糟糕(或不完整)软件的局限,但即使软件很好,糟糕的运维也无法让它可靠运行。
大规模系统由成千上万台机器组成,纯靠人工维护,成本高得难以承受,因此自动化必不可少。然而,自动化是一把双刃剑:总会有一些边缘情况(例如罕见的故障场景)需要运维团队人工干预。自动化无法处理的恰恰是最复杂的问题,所以自动化程度越高,反而越需要一支技能 更强 的运维团队来解决这些问题。
自动化系统一旦出错,往往比依靠运维人员手工完成某些操作的系统更难排查。因此,对可运维性而言,自动化并非总是越多越好。一定程度的自动化依然很重要,最佳平衡点则取决于具体应用和组织的情况。
数据系统可以通过多种方式简化日常工作:
简单性:管理复杂度,采用人们熟知且前后一致的模式和结构,避免不必要的复杂度,使新工程师也能轻松理解系统。
小型软件项目可以拥有简单讨喜、富有表现力的代码;但随着项目不断扩大,代码往往变得非常复杂,难以理解。这种复杂度拖慢了每一个需要在系统上工作的人,进一步增加了维护成本。一个陷入复杂泥潭的软件项目有时被称为 大泥球(big ball of mud) 。
管理复杂度最好的工具之一是 抽象(abstraction)。一个好的抽象可以把大量实现细节隐藏在干净、简单易懂的外观之下,也可以广泛用于各种不同的应用。
能在项目代码里用常规团队公认的写法,最好别逞能自己骚操作,反而与简单背道而驰。
可演化性:让变化更容易,便于工程师将来修改系统,在需求变化时调整和扩展系统,以适应事先没有预料到的使用场景。
系统的需求永远不变,基本是不可能的。更可能的情况是,需求总在变化:你了解了新的事实,出现了事先未曾预料的使用场景,业务优先级发生变化,用户要求新功能,新平台取代旧平台,法律或监管要求改变,系统增长迫使架构发生变化,等等。
敏捷(Agile) 工作模式为适应变化提供了框架。敏捷社区还发展出了适合在频繁变化的环境中开发软件的技术工具和流程,例如测试驱动开发(TDD)和重构。
修改数据系统、使其适应不断变化的需求有多容易,与系统的简单性和抽象密切相关:松耦合、简单的系统通常比紧耦合、复杂的系统更容易修改。这个概念如此重要,因此我们用一个不同的词来指代数据系统层面的敏捷性:可演化性(evolvability) 。
大型系统有些操作不可逆,需要极为谨慎地执行。假设你要从一个数据库迁移到另一个数据库:如果新系统出了问题却无法切回旧系统,风险就远高于能够轻松回退的情况。尽量减少不可逆性,可以提高系统的灵活性。