后者的缺点是,发推现在需要大量的额外工作。平均来说,一条推文会发往约75个关注者,所以每秒4.6k的发推写入,变成了对主页时间线缓存每秒345K的写入。但是这个平均值隐藏了用户粉丝数差异巨大这一现实,一些用户有超过3000万的粉丝,这意味着一条推文就可能导致主页时间线缓存的3000万次写入。
在这个例子里,每个用户粉丝数的分布是探讨可扩展性的一个关键负载参数,因为它决定了扇出负载。
最后推特使用了两种方法的混合,大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中,但是少数拥有海量粉丝的用户(名流)会被排除在外。当用户读取主页时间线时,分别获取该用户关注的每位名流的推文,再与用户的主页时间线缓存合并。
描述性能
系统负载是从用户的视角来审视系统,是一种客观指标;系统性能是描述系统的一种实际能力。比如:
吞吐量:每秒可以处理的单位数据量,通常记为QPS
响应时间:从用户侧观察到的发出请求到收到回复的时间
延迟:日常中,延迟经常和响应时间混用指代响应时间,但严格来说,延迟只是把请求过程中排队等休眠时间,虽然其在响应时间中一般占大头;但是只有我们把请求真正处理耗时认为是瞬时,延迟才能等同于响应时间。
响应时间通常用百分位点来衡量,比如p95,p99,p999,它们意味着95%,99%,99.9%的请求都能在该阈值内完成。在实际中,通常使用滑动窗口滚动计算最近一段时间的响应时间分布,并通常以折线图或柱状图进行呈现。
响应时间的高百分位点(也称为 尾部延迟,即 tail latencies)非常重要,因为它们直接影响用户的服务体验。
应对负载的方法
在讨论了用于描述负载的参数和用于衡量性能的指标后,可以讨论可扩展性了:当负载参数增加时,如何保持良好的性能?
一般有下面两种方案:
纵向扩展,也称为垂直扩展:转向更强大的机器
横向扩展,也成为水平扩展:将负载分布到多台小机器
负载的两种扩展方式:
自动:负载不好预测而且变化较多,使用该方式较好。缺点是不容易跟踪负载,容易抖动,造成资源浪费。
手动:如果负载容易预测且不经常变化,最好手动。设计简单而且不容易出错。
针对不同应用场景来说的话:
如果规模很小,尽量先用性能好的机器,可以省去很多麻烦。
可以上云,利用云的可扩展性
自行设计可扩展的分布式架构
两种服务类型:
无状态服务:比较简单,多台机器,外层加一个gateway就行
有状态服务:根据需求场景,如读写负载、存储量级、数据复杂度、响应时间、访问模式来进行取舍,设计合乎需求的架构
没有万金油架构!
可维护性
软件的大部分开销并不在最初的开发阶段,而是在持续的维护阶段,包括修复漏洞、保持系统正常运行、调查失效、适配新的平台、为新的场景进行修改、偿还技术债和添加新的功能。
但是我们可以,也应该以这样一种方式来设计软件:在设计之初就尽量考虑尽可能减少维护期间的痛苦,从而避免自己的软件系统变成遗留系统。为此,我们将特别关注软件系统的三个设计原则:
可操作性:便于运维团队保持系统平稳运行
简单性:从系统中消除尽可能多的复杂度,使新工程师也能轻松理解系统
可演化性:工程师在未来能轻松地对系统进行更改,当需求变化时为新应用场景做适配
可操作性:人生苦短,关爱运维
一个优秀运维团队的典型职责如下:
- 监控系统的运行状况,并在服务状态不佳时快速恢复服务。
- 跟踪问题的原因,例如系统故障或性能下降。
- 及时更新软件和平台,比如安全补丁。
- 了解系统间的相互作用,以便在异常变更造成损失前进行规避。
- 预测未来的问题,并在问题出现之前加以解决(例如,容量规划)。
- 建立部署、配置、管理方面的良好实践,编写相应工具。
- 执行复杂的维护任务,例如将应用程序从一个平台迁移到另一个平台。
- 当配置变更时,维持系统的安全性。
- 定义工作流程,使运维操作可预测,并保持生产环境稳定。
- 铁打的营盘流水的兵,维持组织对系统的了解。
数据系统可以通过各种方式使日常任务更轻松:
- 通过良好的监控,提供对系统内部状态和运行时行为的 可见性(visibility)。
- 为自动化提供良好支持,将系统与标准化工具相集成。
- 避免依赖单台机器(在整个系统继续不间断运行的情况下允许机器停机维护)。
- 提供良好的文档和易于理解的操作模型(“如果做 X,会发生 Y”)。
- 提供良好的默认行为,但需要时也允许管理员自由覆盖默认值。
- 有条件时进行自我修复,但需要时也允许管理员手动控制系统状态。
- 行为可预测,最大限度减少意外。
简单性:管理复杂度
复杂度的表现:
状态空间的膨胀
组件间的强耦合
纠结的依赖关系
不一致的术语和命名
解决性能问题的hack
需要绕开的特例
过多地引入额外复杂度的原因一般是问题理解的不够本质,写出了流水账式的代码。如果你为一个问题找到了合适的抽象,那么问题就解决了一半,如:高级编程语言是一种抽象,隐藏了机器码、CPU 寄存器和系统调用。 SQL 也是一种抽象,隐藏了复杂的磁盘 / 内存数据结构、来自其他客户端的并发请求、崩溃后的不一致性。当然在用高级语言编程时,我们仍然用到了机器码;只不过没有 直接(directly) 使用罢了,正是因为编程语言的抽象,我们才不必去考虑这些实现细节。
如何找到合适的抽象?
- 从计算机领域常见的抽象中找。
- 从日常生活中常接触的概念找。
总之,一个合适的抽象,要么是符合直觉的;要么是和你的读者共享上下文的。
可演化性:降低改变门槛
系统的需求永远不变,基本是不可能的。更可能的情况是,它们处于常态的变化中,例如:
你了解了新的事实
出现意想不到的应用场景
业务优先级发生变化
用户要求新功能
新平台取代旧平台
法律或监管要求发生变化
系统增长迫使架构变化
如何应对?
组织流程方面:敏捷开发
系统设计:简单性与抽象性,合理抽象和封装,对修改关闭,对扩展开放