原文 《Everything I know about good system design》 – Sean goedecke
优秀系统的特点:#
- 这个系统可以长时间运行而不出错。
- 大多数的组件都是无状态的。尽可能减少有状态的组件。
1. 长时间运行而不出错。#
显而易见,这就是优秀的系统的设计目标。
这一点往往比较反直觉, 优秀的系统由于不会经常出现问题, 所以可能不太被人注意到。
而如果经常出现问题, 那么关注的人就会越来越多。
所以系统应该尽快能的设计的简单, 而不是复杂。 越复杂的系统, 约容易出现问题。
2. 尽可能减少有状态组件。#
有状态意味着在崩溃后可能无法直接重启, 而导致系统性问题。
比较明显的是, 如果一个组件依赖数据库, 或依赖本地存储, 那么一旦所在物理机或数据库出现变动, 这个组件可能无法快速迁移或更新。
尽可能将组件设计到无状态组件中, 无状态意味着可以随时重启。
比如如果多个组件需要同时写 MySQL, 那么应该设计为其中某一个专门写 MySQL, 其他组件设计为无状态的组件, 负责通知这个组件写 MySQL。
这样多个需要考虑与数据库强交互的组件就变成了一个需要与 MySQL 强交互的组件。
数据库#
由于状态管理是系统设计中最重要的部分,因此最重要的组件通常是状态所在的位置:数据库。
1. 模式与索引#
- 如果表不够灵活,那么更新表结构的时候会非常痛苦。
- 如果表太灵活(比如把大多数的内容塞到 JSON 字段中),那么往往应用程序会变的更加复杂。
要保持这种中立的设计非常困难,所以简化目标为:表应该具有人类可读的特点。
2. 数据库存在瓶颈需要优化#
尽可能的使用 Join 来或得更多的数据,而不是下发多次查询之后在服务器内存中拼接。 这一点举个比较常见的例子:
select user_id from user where user in {a group user}; -- 然后触发了几百次下面的请求: select user_relation_id from user_relation where user_id = ? .
尽可能的读写分离,并且读请求尽量不调度到写节点上。
小心暴增的流量,数据库一点过载,大多时候表现的是性能下降而不是不可用。
Slow operations, fast operations#
The general pattern for this is splitting out the minimum amount of work needed to do something useful for the user and doing the rest of the work in the background.
处理这类情况的通用模式是,拆分出为用户提供有用信息所需的最少工作量,其余工作则在后台进行。
响应速度的确会被认为是系统质量的一部分。
后台系统的设计上也是有特点的:
- 如果后台系统的任务需要立即执行,你可以把他放在任何地方,包括数据库。
- 如果后台任务不需要立即执行,你最好不要把他放在
Redis
中,因为Redis
是基于内存的,除了价格昂贵以外,一旦 Redis 重启,数据将会丢失。
Caching#
在一下场景中考虑使用缓存:
- 任务的执行比较昂贵或缓慢,可以使用缓存减少计算的次数并且掩盖上一次计算的缓慢。
- 大量用户的结果或多次操作的结果完全一致,而且请求不需要重复执行。
- 状态无关紧要,用户并不关心最新的状态。
关于 Caching 使用的一些建议:
- 减少缓存的使用,因为缓存引入了一个额外的状态的(作者认为所有存储都是一个状态)管理。
You should never cache something without first making a serious effort to speed it up.
缓存并不意味的只能使用 Redis 这种内存型数据库,你还可以考虑使用 S3 或任何存储。 你需要区分缓存的目的。 如果你是为了提高影响速度, 可以使用基于内存的缓存。 如果你是为了临时记录一个临时任务的结果, 你完全可以考虑使用块存储。 他们都是缓存技术的实现。
事件#
事件将各个服务的串联调用解耦开。
在这些场景你可以考虑使用事件:
当然,前提一定是这个事件会触发的行为非常多,或者会出现大量的行为,否则你都应该直接使用 HTTP 或 RPC 等技术实时调用。
- 需要触发的行为与主路径无关。
- 需要触发的行为用户不关心实时性。
比如新用户创建后,需要发送一封欢迎邮件。(用户可能不关心,不是实时性的) 一个文章发出后,需要判断可能涉及的风险。(用户不关心)
Pushing & Pulling#
推拉模型是数据同步的常见问题,如何选择?
从 Prometheus、VM 等常见的监控领域, Pull 模型可以简化服务端设计, 但是可能会对客户端造成影响。
比如需要获取一次当前的时序数据, 在没有缓存组件的场景下, Pull 可能会导致客户端的完全重载数据。
Push 模型带来的问题可能是流量峰值问题, 在集体 Push 的时候,流量到达高峰, 服务端不得不使用 Queues 来削峰。
这种判断的方法可以参考下面的维度:
- 当数据量变化不大的时候,即使有非常多需要同步的数据也可以考虑使用 Push 模型。因为变化不大, Push 带来的流量不会过于庞大。
- 如果数据量变化非常大的时候而且实时性不高的时候,可以考虑 Pull 模型,让服务端合理调度采集的进度和目标。
当然,存在一些数据量变化大,而且要求实时性的时候,可能需要考虑优化数据的流向链路。
Hot Paths#
你的系统一定存在主要而且不容问题的数据路径。
比如门户网站的计费系统, 他不可以出错, 这条路径有更高的 SLO, 其 SLA 也会更加严格。 你需要确保你的主路径不会出现任何问题, 或出现问题后可以快速恢复。
这方面, 你可以通过 UA 等用户指标获得, 如果与预期相悖, 你应该考虑系统的定位。
日志与指标#
作者提到了一个问题, 大多数工程师不愿意写日志的原因: 思维流程被打断,添加日志导致代码变丑。
这的确是一个天然的问题, 不过呢, 日志本身可以提供非常多的有效信息。 当系统出现问题的时候, 这是你唯一可以获得与问题相关的信息的手段。 良好的日志非常重要。
指标也是, 系统的状态和趋势可以通过指标直接反应出来。 指标在我看来分为两大类,
- 系统底层强相关的指标,比如 CPU 利用率,HTTP 请求的 QPS、Latency 和 ErrorRatio.
- 业务指标,比如注册用户数,用户驻留时间。
你需要区分每个组件关注的系统指标以及其对应的合理的计算方式。 比如 CPU 你应该关注平均值和峰值。 但是对于 HTTP 请求的响应时间,可能更多的是 P99 延迟等。 因为 HTTP 延迟的平均值意义不大,极有可能被个别操作降低整体平均值。
重试与优雅失败#
重试并不意味着有效的问题解决手段, 因为重试可能会导致服务端更大的压力。
你必须思考你系统出现问题后的应对方案。 这可能会比较复杂。
最后#
系统设计应该是巧妙的结合了常用的手段。