System Design Primer

System Design Primer

典型的系统架构图

性能与可扩展性

如果服务性能的增长与资源的增加是成比例的,服务就是可扩展的。通常,提高性能意味着服务于更多的工作单元,另一方面,当数据集增长时,同样也可以处理更大的工作单位。1

另一个角度来看待性能与可扩展性:

  • 如果你的系统有性能问题,对于单个用户来说是缓慢的。
  • 如果你的系统有可扩展性问题,单个用户较快但在高负载下会变慢。

来源及延伸阅读

延迟与吞吐量

延迟是执行操作或运算结果所花费的时间。

吞吐量是单位时间内(执行)此类操作或运算的数量。

通常,你应该以可接受级延迟最大化吞吐量为目标。

来源及延伸阅读

可用性与一致性

CAP理论


来源:再看CAP理论

在一个分布式计算系统中,只能同时满足下列的两点:

  • 一致性 ─ 每次访问都能获得最新数据但可能会收到错误响应
  • 可用性 ─ 每次访问都能收到非错响应,但不保证获取到最新数据
  • 分区容错性 ─ 在任意分区网络故障的情况下系统仍能继续运行

网络并不可靠,所以你应要支持分区容错性,并需要在软件可用性和一致性间做出取舍。

CP ─ 一致性和分区容错性

等待分区节点的响应可能会导致延时错误。如果你的业务需求需要原子读写,CP是一个不错的选择。

AP ─ 可用性与分区容错性

响应节点上可用数据的最近版本可能并不是最新的。当分区解析完后,写入(操作)可能需要一些时间来传播。

如果业务需求允许最终一致性,或当有外部故障时要求系统继续运行,AP是一个不错的选择。

来源及延伸阅读

一致性模式

有同一份数据的多份副本,我们面临着怎样同步它们的选择,以便让客户端有一致的显示数据。回想 CAP理论中的一致性定义 ─ 每次访问都能获得最新数据但可能会收到错误响应

弱一致性

在写入之后,访问可能看到,也可能看不到(写入数据。尽力优化之让其能访问最新数据。

这种方式可以memcached等系统中看到。弱一致性在VoIP,视频聊天和实时多人游戏等真实用例中表现不错。打个比方,如果你在通话中丢失信号几秒钟时间,当重新连接时你是听不到这几秒钟所说的话的。

最终一致性

在写入后,访问最终能看到写入数据(通常在数毫秒内。数据被异步复制。

DNSemail等系统使用的是此种方式。最终一致性在高可用性系统中效果不错。

强一致性

在写入后,访问立即可见。数据被同步复制。

文件系统和关系型数据库(RDBMS)中使用的是此种方式。强一致性在需要记录的系统中运作良好。

来源及延伸阅读

可用性模式

有两种支持高可用性的模式:故障切换(fail-over)复制(replication

故障切换

工作到备用切换(Active-passive)

关于工作到备用的故障切换流程是,工作服务器发送周期信号给待机中的备用服务器。如果周期信号中断,备用服务器切换成工作服务器的IP地址并恢复服务。

宕机时间取决于备用服务器处于“热”待机状态还是需要从“冷”待机状态进行启动。只有工作服务器处理流量。

工作到备用的故障切换也被称为主从切换。

双工作切换(Active-active)

在双工作切换中,双方都在管控流量,在它们之间分散负载。

如果是外网服务器,DNS将需要对两方都了解。如果是内网服务器,应用程序逻辑将需要对两方都了解。

双工作切换也可以称为主主切换。

缺陷:故障切换

  • 故障切换需要添加额外硬件并增加复杂性。
  • 如果新写入数据在能被复制到备用系统之前,工作系统出现了故障,则有可能会丢失数据。

复制

主 ─ 从复制和主 ─ 主复制

这个主题进一步探讨了数据库部分:

域名系统


来源:DNS安全介绍

域名系统是把 www.example.com 等域名转换成IP地址。

域名系统是分层次的,一些DNS服务器位于顶层。当查询(域名) IP时,路由或ISP提供连接DNS服务器的信息。较底层的DNS服务器缓存映射,它可能会因为DNS传播延时而失效。DNS结果可以缓存在浏览器或操作系统中一段时间,时间长短取决于存活时间TTL

  • NS记录(域名服务) ─ 指定解析域名或子域名的DNS服务器。
  • MX记录(邮件交换) ─ 指定接收信息的邮件服务器。
  • A记录(地址) ─ 指定域名对应的IP地址记录。
  • CNAME(规范) ─ 一个域名映射到另一个域名或 CNAME 记录( example.com指向 www.example.com)或映射到一个 A 记录。

CloudFlareRoute 53 等平台提供管理DNS的功能。某些DNS服务通过集中方式来路由流量:

  • 加权轮询调度
    • 防止流量进入维护中的服务器
    • 在不同大小集群间负载均衡
    • A/B测试
  • 基于延迟路由
  • 基于地理位置路由

缺陷:DNS

  • 虽说缓存可以减轻DNS延迟,但连接DNS服务器还是带来了轻微的延迟。
  • 虽然它们通常由政府,网络服务提供商和大公司管理,但DNS服务管理仍可能是复杂的。
  • DNS服务最近遭受 DDoS攻击,阻止不知道Twitter IP地址的用户访问Twitter

来源及延伸阅读

内容分发网络(CDN)


来源:为什么使用CDN

内容分发网络(CDN)是一个全球性的代理服务器分布式网络,它从靠近用户的位置提供内容。通常,HTML/CSS/JS,图片和视频等静态内容由CDN提供,虽然亚马逊CloudFront等也支持动态内容。CDNDNS解析会告知客户端连接哪台服务器。

将内容存储在CDN上可以从两个方面来提供性能:

  • 从靠近用户的数据中心提供资源
  • 通过CDN你的服务器不必真的处理请求

CDN推送(push)

当你服务器上内容发生变动时,推送CDN接受新内容。直接推送给CDN并重写URL地址以指向你的内容的CDN地址。你可以配置内容到期时间及何时更新。内容只有在更改或新增是才推送,流量最小化,但储存最大化。

CDN拉取(pull)

CDN拉取是当第一个用户请求该资源时,从服务器上拉取资源。你将内容留在自己的服务器上并重写URL指向CDN地址。直到内容被缓存在CDN上为止,这样请求只会更慢,

存活时间(TTL)决定缓存多久时间。CDN拉取方式最小化CDN上的储存空间,但如果过期文件并在实际更改之前被拉取,则会导致冗余的流量。

高流量站点使用CDN拉取效果不错,因为只有最近请求的内容保存在CDN中,流量才能更平衡地分散。

缺陷:CDN

  • CDN成本可能因流量而异,可能在权衡之后你将不会使用CDN
  • 如果在TTL过期之前更新内容,CDN缓存内容可能会过时。
  • CDN需要更改静态内容的URL地址以指向CDN

来源及延伸阅读

负载均衡器


来源:可扩展的系统设计模式

负载均衡器将传入的请求分发到应用服务器和数据库等计算资源。无论哪种情况,负载均衡器将从计算资源来的响应返回给恰当的客户端。负载均衡器的效用在于:

  • 防止请求进入不好的服务器
  • 防止资源过载
  • 帮助消除单一的故障点

负载均衡器可以通过硬件(昂贵)或HAProxy等软件来实现。 增加的好处包括:

  • SSL终结 ─ 解密传入的请求并加密服务器响应,这样的话后端服务器就不必再执行这些潜在高消耗运算了。
  • Session留存 ─ 如果Web应用程序不追踪会话,发出cookie并将特定客户端的请求路由到同一实例。

通常会设置采用工作 ─ 备用双工作 模式的多个负载均衡器,以免发生故障。

负载均衡器能基于多种方式来路由流量:

四层负载均衡

四层负载均衡根据监看传输层的信息来决定如何分发请求。通常,这会涉及来源,目标IP地址和请求头中的端口,但不包括数据包(报文)内容。四层负载均衡执行网络地址转换(NAT)来向上游服务器转发网络数据包。

七层负载均衡器

七层负载均衡器根据监控应用层来决定怎样分发请求。这会涉及请求头的内容,消息和cookie。七层负载均衡器终结网络流量,读取消息,做出负载均衡判定,然后传送给特定服务器。比如,一个七层负载均衡器能直接将视频流量连接到托管视频的服务器,同时将更敏感的用户账单流量引导到安全性更强的服务器。

以损失灵活性为代价,四层负载均衡比七层负载均衡花费更少时间和计算资源,虽然这对现代商用硬件的性能影响甚微。

水平扩展

负载均衡器还能帮助水平扩展,提高性能和可用性。使用商业硬件的性价比更高,并且比在单台硬件上垂直扩展更贵的硬件具有更高的可用性。相比招聘特定企业系统人才,招聘商业硬件方面的人才更加容易。

缺陷:水平扩展

  • 水平扩展引入了复杂度并涉及服务器复制
    • 服务器应该是无状态的:它们也不该包含像session或资料图片等与用户关联的数据。
    • session可以集中存储在数据库或持久化缓存(Redis、Memcached)的数据存储区中。
  • 缓存和数据库等下游服务器需要随着上游服务器进行扩展,以处理更多的并发连接。

缺陷:负载均衡器

  • 如果没有足够的资源配置或配置错误,负载均衡器会变成一个性能瓶颈。
  • 引入负载均衡器以帮助消除单点故障但导致了额外的复杂性。
  • 单个负载均衡器会导致单点故障,但配置多个负载均衡器会进一步增加复杂性。

来源及延伸阅读

反向代理(web服务器)


资料来源:维基百科

反向代理是一种可以集中地调用内部服务,并提供统一接口给公共客户的web服务器。来自客户端的请求先被反向代理服务器转发到可响应请求的服务器,然后代理再把服务器的响应结果返回给客户端。

带来的好处包括:

  • 增加安全性-隐藏后端服务器的信息,屏蔽黑名单中的IP,限制每个客户端的连接数。
  • 提高可扩展性和灵活性-客户端只能看到反向代理服务器的IP,这使你可以增减服务器或者修改它们的配置。
  • 本地终结SSL会话-解密传入请求,加密服务器响应,这样后端服务器就不必完成这些潜在的高成本的操作。
    • 免除了在每个服务器上安装 X.509 证书的需要
  • 压缩-压缩服务器响应
  • 缓存-直接返回命中的缓存结果
  • 静态内容-直接提供静态内容
    • HTML/CSS/JS
    • 图片
    • 视频
    • 等等

负载均衡器与反向代理

  • 当你有多个服务器时,部署负载均衡器非常有用。通常,负载均衡器将流量路由给一组功能相同的服务器上。
  • 即使只有一台web服务器或者应用服务器时,反向代理也有用,可以参考上一节介绍的好处。
  • NGINXHAProxy等解决方案可以同时支持第七层反向代理和负载均衡。

不利之处:反向代理

  • 引入反向代理会增加系统的复杂度。
  • 单独一个反向代理服务器仍可能发生单点故障,配置多台反向代理服务器(如故障转移)会进一步增加复杂度。

来源及延伸阅读

应用层


资料来源:可缩放系统构架介绍

Web服务层与应用层(也被称作平台层)分离,可以独立缩放和配置这两层。添加新的API只需要添加应用服务器,而不必添加额外的web服务器。

单一职责原则提倡小型的,自治的服务共同合作。小团队通过提供小型的服务,可以更激进地计划增长。

应用层中的工作进程也有可以实现异步化

微服务

与此讨论相关的话题是 微服务,可以被描述为一系列可以独立部署的小型的,模块化服务。每个服务运行在一个独立的线程中,通过明确定义的轻量级机制通讯,共同实现业务目标。1

例如,Pinterest可能有这些微服务: 用户资料、关注者、Feed流、搜索、照片上传等。

服务发现

ConsulEtcdZookeeper 这样的系统可以通过追踪注册名、地址、端口等信息来帮助服务互相发现对方。Health checks 可以帮助确认服务的完整性和是否经常使用一个 HTTP 路径。ConsulEtcd都有一个内建的 key-value存储 用来存储配置信息和其他的共享信息。

不利之处:应用层

  • 添加由多个松耦合服务组成的应用层,从架构、运营、流程等层面来讲将非常不同(相对于单体系统
  • 微服务会增加部署和运营的复杂度。

来源及延伸阅读

数据库


资料来源:扩展你的用户数到第一个一千万

关系型数据库管理系统(RDBMS)

SQL这样的关系型数据库是一系列以表的形式组织的数据项集合。

校对注:这里作者SQL可能指的是MySQL

ACID用来描述关系型数据库事务的特性。

  • 原子性-每个事务内部所有操作要么全部完成,要么全部不完成。
  • 一致性-任何事务都使数据库从一个有效的状态转换到另一个有效状态。
  • 隔离性-并发执行事务的结果与顺序执行事务的结果相同。
  • 持久性-事务提交后,对系统的影响是永久的。

关系型数据库扩展包括许多技术:主从复制主主复制联合分片非规范化SQL调优


资料来源:可扩展性、可用性、稳定性、模式

主从复制

主库同时负责读取和写入操作,并复制写入到一个或多个从库中,从库只负责读操作。树状形式的从库再将写入复制到更多的从库中去。如果主库离线,系统可以以只读模式运行,直到某个从库被提升为主库或有新的主库出现。

不利之处:主从复制
  • 将从库提升为主库需要额外的逻辑。
  • 参考不利之处:复制中,主从复制和主主复制共同的问题。


资料来源:可扩展性、可用性、稳定性、模式

主主复制

两个主库都负责读操作和写操作,写入操作时互相协调。如果其中一个主库挂机,系统可以继续读取和写入。

不利之处: 主主复制
  • 你需要添加负载均衡器或者在应用逻辑中做改动,来确定写入哪一个数据库。
  • 多数主-主系统要么不能保证一致性(违反ACID,要么因为同步产生了写入延迟。
  • 随着更多写入节点的加入和延迟的提高,如何解决冲突显得越发重要。
  • 参考不利之处:复制中,主从复制和主主复制共同的问题。
不利之处:复制
  • 如果主库在将新写入的数据复制到其他节点前挂掉,则有数据丢失的可能。
  • 写入会被重放到负责读取操作的副本。副本可能因为过多写操作阻塞住,导致读取功能异常。
  • 读取从库越多,需要复制的写入数据就越多,导致更严重的复制延迟。
  • 在某些数据库系统中,写入主库的操作可以用多个线程并行写入,但读取副本只支持单线程顺序地写入。
  • 复制意味着更多的硬件和额外的复杂度。
来源及延伸阅读

联合


资料来源:扩展你的用户数到第一个一千万

联合(或按功能划分)将数据库按对应功能分割。例如,你可以有三个数据库:论坛用户产品,而不仅是一个单体数据库,从而减少每个数据库的读取和写入流量,减少复制延迟。较小的数据库意味着更多适合放入内存的数据,进而意味着更高的缓存命中几率。没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。

不利之处:联合
  • 如果你的数据库模式需要大量的功能和数据表,联合的效率并不好。
  • 你需要更新应用程序的逻辑来确定要读取和写入哪个数据库。
  • server link 从两个库联结数据更复杂。
  • 联合需要更多的硬件和额外的复杂度。
来源及延伸阅读:联合

分片


资料来源:可扩展性、可用性、稳定性、模式

分片将数据分配在不同的数据库上,使得每个数据库仅管理整个数据集的一个子集。以用户数据库为例,随着用户数量的增加,越来越多的分片会被添加到集群中。

类似联合的优点,分片可以减少读取和写入流量,减少复制并提高缓存命中率。也减少了索引,通常意味着查询更快,性能更好。如果一个分片出问题,其他的仍能运行,你可以使用某种形式的冗余来防止数据丢失。类似联合,没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。

常见的做法是用户姓氏的首字母或者用户的地理位置来分隔用户表。

不利之处:分片
  • 你需要修改应用程序的逻辑来实现分片,这会带来复杂的SQL查询。
  • 分片不合理可能导致数据负载不均衡。例如,被频繁访问的用户数据会导致其所在分片的负载相对其他分片高。
    • 再平衡会引入额外的复杂度。基于一致性哈希的分片算法可以减少这种情况。
  • 联结多个分片的数据操作更复杂。
  • 分片需要更多的硬件和额外的复杂度。

来源及延伸阅读:分片

非规范化

非规范化试图以写入性能为代价来换取读取性能。在多个表中冗余数据副本,以避免高成本的联结操作。一些关系型数据库,比如 PostgreSQLOracle支持物化视图,可以处理冗余信息存储和保证冗余副本一致。

当数据使用诸如联合分片等技术被分割,进一步提高了处理跨数据中心的联结操作复杂度。非规范化可以规避这种复杂的联结操作。

在多数系统中,读取操作的频率远高于写入操作,比例可达到100:1,甚至1000:1。需要复杂的数据库联结的读取操作成本非常高,在磁盘操作上消耗了大量时间。

不利之处:非规范化
  • 数据会冗余。
  • 约束可以帮助冗余的信息副本保持同步,但这样会增加数据库设计的复杂度。
  • 非规范化的数据库在高写入负载下性能可能比规范化的数据库差。
来源及延伸阅读:非规范化

SQL调优

SQL调优是一个范围很广的话题,有很多相关的可以作为参考。

利用基准测试性能分析来模拟和发现系统瓶颈很重要。

  • 基准测试-ab 等工具模拟高负载情况。
  • 性能分析-通过启用如慢查询日志等工具来辅助追踪性能问题。

基准测试和性能分析可能会指引你到以下优化方案。

改进模式
  • 为了实现快速访问,MySQL在磁盘上用连续的块存储数据。
  • 使用 CHAR 类型存储固定长度的字段,不要用 VARCHAR
    • CHAR 在快速、随机访问时效率很高。如果使用 VARCHAR,如果你想读取下一个字符串,不得不先读取到当前字符串的末尾。
  • 使用 TEXT 类型存储大块的文本,例如博客正文。TEXT 还允许布尔搜索。使用 TEXT 字段需要在磁盘上存储一个用于定位文本块的指针。
  • 使用 INT 类型存储高达2^3240亿的较大数字。
  • 使用 DECIMAL 类型存储货币可以避免浮点数表示错误。
  • 避免使用 BLOBS 存储实际对象,而是用来存储存放对象的位置。
  • VARCHAR(255) 是以8位数字存储的最大字符数,在某些关系型数据库中,最大限度地利用字节。
  • 在适用场景中设置 NOT NULL 约束来提高搜索性能
使用正确的索引
  • 你正查询(SELECTGROUP BYORDER BYJOIN)的列如果用了索引会更快。
  • 索引通常表示为自平衡的 B,可以保持数据有序,并允许在对数时间内进行搜索,顺序访问,插入,删除操作。
  • 设置索引,会将数据存在内存中,占用了更多内存空间。
  • 写入操作会变慢,因为索引需要被更新。
  • 加载大量数据时,禁用索引再加载数据,然后重建索引,这样也许会更快。
避免高成本的联结操作
  • 有性能需要,可以进行非规范化。
分割数据表
  • 将热点数据拆分到单独的数据表中,可以有助于缓存。
调优查询缓存
来源及延伸阅读

NoSQL

NoSQL-值数据库文档型数据库列型数据库图数据库的统称。数据库是非规范化的,表联结大多在应用程序代码中完成。大多数NoSQL无法实现真正符合ACID的事务,支持最终一致

BASE通常被用于描述NoSQL数据库的特性。相比 CAP理论BASE强调可用性超过一致性。

  • 基本可用-系统保证可用性。
  • 软状态-即使没有输入,系统状态也可能随着时间变化。
  • 最终一致性-经过一段时间之后,系统最终会变一致,因为系统在此期间没有收到任何输入。

除了在 SQL还是NoSQL 之间做选择,了解哪种类型的NoSQL数据库最适合你的用例也是非常有帮助的。我们将在下一节中快速了解下 -值存储文档型存储列型存储图存储数据库。

-值存储

抽象模型:哈希表

-值存储通常可以实现O(1)时间读写,用内存或SSD存储数据。数据存储可以按字典顺序维护键,从而实现键的高效检索。键-值存储可以用于存储元数据。

-值存储性能很高,通常用于存储简单数据模型或频繁修改的数据,如存放在内存中的缓存。键-值存储提供的操作有限,如果需要更多操作,复杂度将转嫁到应用程序层面。

-值存储是如文档存储,在某些情况下,甚至是图存储等更复杂的存储系统的基础。

来源及延伸阅读

文档类型存储

抽象模型:将文档作为值的键-值存储

文档类型存储以文档(XML、JSON、二进制文件等)为中心,文档存储了指定对象的全部信息。文档存储根据文档自身的内部结构提供API或查询语句来实现查询。请注意,许多键-值存储数据库有用值存储元数据的特性,这也模糊了这两种存储类型的界限。

基于底层实现,文档可以根据集合、标签、元数据或者文件夹组织。尽管不同文档可以被组织在一起或者分成一组,但相互之间可能具有完全不同的字段。

MongoDBCouchDB等一些文档类型存储还提供了类似SQL语言的查询语句来实现复杂查询。DynamoDB同时支持键-值存储和文档类型存储。

文档类型存储具备高度的灵活性,常用于处理偶尔变化的数据。

来源及延伸阅读:文档类型存储

列型存储


资料来源: SQLNoSQL,一个简短的历史

抽象模型:嵌套的 ColumnFamily<RowKey, Columns<ColKey, Value, Timestamp>> 映射

类型存储的基本数据单元是列(名/值对。列可以在列族(类似于SQL的数据表)中被分组。超级列族再分组普通列族。你可以使用行键独立访问每一列,具有相同行键值的列组成一行。每个值都包含版本的时间戳用于解决版本冲突。

Google发布了第一个列型存储数据库 Bigtable,它影响了Hadoop生态系统中活跃的开源数据库 HBaseFacebookCassandra。像BigTableHBaseCassandra这样的存储系统将键以字母顺序存储,可以高效地读取键列。

列型存储具备高可用性和高可扩展性。通常被用于大数据相关存储。

来源及延伸阅读:列型存储

图数据库


资料来源:图数据库

抽象模型: 图

在图数据库中,一个节点对应一条记录,一个弧对应两个节点之间的关系。图数据库被优化用于表示外键繁多的复杂关系或多对多关系。

图数据库为存储复杂关系的数据模型,如社交网络,提供了很高的性能。它们相对较新,尚未广泛应用,查找开发工具或者资源相对较难。许多图只能通过 REST API 访问。

相关资源和延伸阅读:图

来源及延伸阅读:NoSQL

SQL还是NoSQL


资料来源:从RDBMS转换到NoSQL

选取SQL的原因:

  • 结构化数据
  • 严格的模式
  • 关系型数据
  • 需要复杂的联结操作
  • 事务
  • 清晰的扩展模式
  • 既有资源更丰富:开发者、社区、代码库、工具等
  • 通过索引进行查询非常快

选取NoSQL的原因:

  • 半结构化数据
  • 动态或灵活的模式
  • 非关系型数据
  • 不需要复杂的联结操作
  • 存储TB (甚至PB)级别的数据
  • 高数据密集的工作负载
  • IOPS高吞吐量

适合NoSQL的示例数据:

  • 埋点数据和日志数据
  • 排行榜或者得分数据
  • 临时数据,如购物车
  • 频繁访问的(“热”)表
  • 元数据/查找表
来源及延伸阅读:SQLNoSQL

缓存


资料来源:可扩展的系统设计模式

缓存可以提高页面加载速度,并可以减少服务器和数据库的负载。在这个模型中,分发器先查看请求之前是否被响应过,如果有则将之前的结果直接返回,来省掉真正的处理。

数据库分片均匀分布的读取是最好的。但是热门数据会让读取分布不均匀,这样就会造成瓶颈,如果在数据库前加个缓存,就会抹平不均匀的负载和突发流量对数据库的影响。

客户端缓存

缓存可以位于客户端(操作系统或者浏览器服务端或者不同的缓存层。

CDN缓存

CDN 也被视为一种缓存。

Web服务器缓存

反向代理和缓存(比如 Varnish)可以直接提供静态和动态内容。Web服务器同样也可以缓存请求,返回相应结果而不必连接应用服务器。

数据库缓存

数据库的默认配置中通常包含缓存级别,针对一般用例进行了优化。调整配置,在不同情况下使用不同的模式可以进一步提高性能。

应用缓存

基于内存的缓存比如MemcachedRedis是应用程序和数据存储之间的一种键值存储。由于数据保存在RAM中,它比存储在磁盘上的典型数据库要快多了。RAM比磁盘限制更多,所以例如 least recently used (LRU)缓存无效算法可以将热门数据放在RAM中,而对一些比较冷门的数据不做处理。

Redis有下列附加功能:

  • 持久性选项
  • 内置数据结构比如有序集合和列表

有多个缓存级别,分为两大类:数据库查询对象

  • 行级别
  • 查询级别
  • 完整的可序列化对象
  • 完全渲染的HTML

一般来说,你应该尽量避免基于文件的缓存,因为这使得复制和自动缩放很困难。

数据库查询级别的缓存

当你查询数据库的时候,将查询语句的哈希值与查询结果存储到缓存中。这种方法会遇到以下问题:

  • 很难用复杂的查询删除已缓存结果。
  • 如果一条数据比如表中某条数据的一项被改变,则需要删除所有可能包含已更改项的缓存结果。

对象级别的缓存

将您的数据视为对象,就像对待你的应用代码一样。让应用程序将数据从数据库中组合到类实例或数据结构中:

  • 如果对象的基础数据已经更改了,那么从缓存中删掉这个对象。
  • 允许异步处理:workers通过使用最新的缓存对象来组装对象。

建议缓存的内容:

  • 用户会话
  • 完全渲染的Web页面
  • 活动流
  • 用户图数据

何时更新缓存

由于你只能在缓存中存储有限的数据,所以你需要选择一个适用于你用例的缓存更新策略。

缓存模式


资料来源:从缓存到内存数据网格

应用从存储器读写。缓存不和存储器直接交互,应用执行以下操作:

  • 在缓存中查找记录,如果所需数据不在缓存中
  • 从数据库中加载所需内容
  • 将查找到的结果存储到缓存中
  • 返回所需内容
def get_user(self, user_id):
    user = cache.get("user.{0}", user_id)
    if user is None:
        user = db.query("SELECT * FROM users WHERE user_id = {0}", user_id)
        if user is not None:
            key = "user.{0}".format(user_id)
            cache.set(key, json.dumps(user))
    return user

Memcached 通常用这种方式使用。

添加到缓存中的数据读取速度很快。缓存模式也称为延迟加载。只缓存所请求的数据,这避免了没有被请求的数据占满了缓存空间。

缓存的缺点:
  • 请求的数据如果不在缓存中就需要经过三个步骤来获取数据,这会导致明显的延迟。
  • 如果数据库中的数据更新了会导致缓存中的数据过时。这个问题需要通过设置  TTL强制更新缓存或者直写模式来缓解这种情况。
  • 当一个节点出现故障的时候,它将会被一个新的节点替代,这增加了延迟的时间。

直写模式


资料来源:可扩展性、可用性、稳定性、模式

应用使用缓存作为主要的数据存储,将数据读写到缓存中,而缓存负责从数据库中读写数据。

  • 应用向缓存中添加/更新数据
  • 缓存同步地写入数据存储
  • 返回所需内容

应用代码:

set_user(12345, {"foo":"bar"})

缓存代码:

def set_user(user_id, values):
    user = db.query("UPDATE Users WHERE id = {0}", user_id, values)
    cache.set(user_id, user)

由于存写操作所以直写模式整体是一种很慢的操作,但是读取刚写入的数据很快。相比读取数据,用户通常比较能接受更新数据时速度较慢。缓存中的数据不会过时。

直写模式的缺点:
  • 由于故障或者缩放而创建的新的节点,新的节点不会缓存,直到数据库更新为止。缓存应用直写模式可以缓解这个问题。
  • 写入的大多数数据可能永远都不会被读取,用TTL可以最小化这种情况的出现。

回写模式


资料来源:可扩展性、可用性、稳定性、模式

在回写模式中,应用执行以下操作:

  • 在缓存中增加或者更新条目
  • 异步写入数据,提高写入性能。
回写模式的缺点:
  • 缓存可能在其内容成功存储之前丢失数据。
  • 执行直写模式比缓存或者回写模式更复杂。

刷新


资料来源:从缓存到内存数据网格

你可以将缓存配置成在到期之前自动刷新最近访问过的内容。

如果缓存可以准确预测将来可能请求哪些数据,那么刷新可能会导致延迟与读取时间的降低。

刷新的缺点:
  • 不能准确预测到未来需要用到的数据可能会导致性能不如不使用刷新。

缓存的缺点:

  • 需要保持缓存和真实数据源之间的一致性,比如数据库根据缓存无效
  • 需要改变应用程序比如增加Redis或者memcached
  • 无效缓存是个难题,什么时候更新缓存是与之相关的复杂问题。

相关资源和延伸阅读

异步


资料来源:可缩放系统构架介绍

异步工作流有助于减少那些原本顺序执行的请求时间。它们可以通过提前进行一些耗时的工作来帮助减少请求时间,比如定期汇总数据。

消息队列

消息队列接收,保留和传递消息。如果按顺序执行操作太慢的话,你可以使用有以下工作流的消息队列:

  • 应用程序将作业发布到队列,然后通知用户作业状态
  • 一个worker从队列中取出该作业,对其进行处理,然后显示该作业完成

不去阻塞用户操作,作业在后台处理。在此期间,客户端可能会进行一些处理使得看上去像是任务已经完成了。例如,如果要发送一条推文,推文可能会马上出现在你的时间线上,但是可能需要一些时间才能将你的推文推送到你的所有关注者那里去。

Redis是一个令人满意的简单的消息代理,但是消息有可能会丢失。

RabbitMQ很受欢迎但是要求你适应AMQP协议并且管理你自己的节点。

Amazon SQS是被托管的,但可能具有高延迟,并且消息可能会被传送两次。

任务队列

任务队列接收任务及其相关数据,运行它们,然后传递其结果。它们可以支持调度,并可用于在后台运行计算密集型作业。

Celery支持调度,主要是用Python开发的。

背压

如果队列开始明显增长,那么队列大小可能会超过内存大小,导致高速缓存未命中,磁盘读取,甚至性能更慢。背压可以通过限制队列大小来帮助我们,从而为队列中的作业保持高吞吐率和良好的响应时间。一旦队列填满,客户端将得到服务器忙或者HTTP 503状态码,以便稍后重试。客户端可以在稍后时间重试该请求,也许是指数退避

异步的缺点:

  • 简单的计算和实时工作流等用例可能更适用于同步操作,因为引入队列可能会增加延迟和复杂性。

相关资源和延伸阅读

通讯


资料来源:OSI 7层模型

超文本传输协议(HTTP)

HTTP是一种在客户端和服务器之间编码和传输数据的方法。它是一个请求/响应协议:客户端和服务端针对相关内容和完成状态信息的请求和响应。HTTP是独立的,允许请求和响应流经许多执行负载均衡,缓存,加密和压缩的中间路由器和服务器。

一个基本的HTTP请求由一个动词(方法)和一个资源(端点)组成。以下是常见的HTTP动词:

动词 描述 *幂等 安全性 可缓存
GET 读取资源 Yes Yes Yes
POST 创建资源或触发处理数据的进程 No No Yes,如果回应包含刷新信息
PUT 创建或替换资源 Yes No No
PATCH 部分更新资源 No No Yes,如果回应包含刷新信息
DELETE 删除资源 Yes No No

多次执行不会产生不同的结果

HTTP是依赖于较低级协议(如TCPUDP)的应用层协议。

来源及延伸阅读:HTTP

传输控制协议(TCP)


资料来源:如何制作多人游戏

TCP是通过 IP网络的面向连接的协议。使用握手建立和断开连接。发送的所有数据包保证以原始顺序到达目的地,用以下措施保证数据包不被损坏:

如果发送者没有收到正确的响应,它将重新发送数据包。如果多次超时,连接就会断开。TCP实行流量控制拥塞控制。这些确保措施会导致延迟,而且通常导致传输效率比UDP低。

为了确保高吞吐量,Web服务器可以保持大量的TCP连接,从而导致高内存使用。在Web服务器线程间拥有大量开放连接可能开销巨大,消耗资源过多,也就是说,一个 memcached 服务器。连接池 可以帮助除了在适用的情况下切换到UDP

TCP对于需要高可靠性但时间紧迫的应用程序很有用。比如包括Web服务器,数据库信息,SMTP,FTPSSH

以下情况使用TCP代替UDP

  • 你需要数据完好无损。
  • 你想对网络吞吐量自动进行最佳评估。

用户数据报协议(UDP)


资料来源:如何制作多人游戏

UDP是无连接的。数据报(类似于数据包)只在数据报级别有保证。数据报可能会无序的到达目的地,也有可能会遗失。UDP不支持拥塞控制。虽然不如TCP那样有保证,但UDP通常效率更高。

UDP可以通过广播将数据报发送至子网内的所有设备。这对 DHCP 很有用,因为子网内的设备还没有分配IP地址,而IP对于TCP是必须的。

UDP可靠性更低但适合用在网络电话、视频聊天,流媒体和实时多人游戏上。

以下情况使用UDP代替TCP

  • 你需要低延迟
  • 相对于数据丢失更糟的是数据延迟
  • 你想实现自己的错误校正方法

来源及延伸阅读:TCPUDP

远程过程调用协议(RPC)


Source: Crack the system design interview

RPC中,客户端会去调用另一个地址空间(通常是一个远程服务器)里的方法。调用代码看起来就像是调用的是一个本地方法,客户端和服务器交互的具体过程被抽象。远程调用相对于本地调用一般较慢而且可靠性更差,因此区分两者是有帮助的。热门的RPC框架包括 ProtobufThriftAvro

RPC是一个“请求-响应”协议:

  • 客户端程序 ── 调用客户端存根程序。就像调用本地方法一样,参数会被压入栈中。
  • 客户端stub程序 ── 将请求过程的id和参数打包进请求信息中。
  • 客户端通信模块 ── 将信息从客户端发送至服务端。
  • 服务端通信模块 ── 将接受的包传给服务端存根程序。
  • 服务端stub程序 ── 将结果解包,依据过程id调用服务端方法并将参数传递过去。

RPC调用示例:

GET /someoperation?data=anId

POST /anotheroperation
{
  "data":"anId";
  "anotherdata": "another value"
}

RPC专注于暴露方法。RPC通常用于处理内部通讯的性能问题,这样你可以手动处理本地调用以更好的适应你的情况。

当以下情况时选择本地库(也就是SDK

  • 你知道你的目标平台。
  • 你想控制如何访问你的“逻辑”。
  • 你想对发生在你的库中的错误进行控制。
  • 性能和终端用户体验是你最关心的事。

遵循RESTHTTP API往往更适用于公共API

缺点:RPC

  • RPC客户端与服务实现捆绑地很紧密。
  • 一个新的API必须在每一个操作或者用例中定义。
  • RPC很难调试。
  • 你可能没办法很方便的去修改现有的技术。举个例子,如果你希望在 Squid 这样的缓存服务器上确保 RPC被正确缓存的话可能需要一些额外的努力了。

表述性状态转移(REST)

REST是一种强制的客户端/服务端架构设计模型,客户端基于服务端管理的一系列资源操作。服务端提供修改或获取资源的接口。所有的通信必须是无状态和可缓存的。

RESTful接口有四条规则:

  • 标志资源(HTTP里的URI ── 无论什么操作都使用同一个URI
  • 表示的改变(HTTP的动作) ── 使用动作, headersbody
  • 可自我描述的错误信息(HTTP中的status code ── 使用状态码,不要重新造轮子。
  • HATEOASHTTP中的HTML接口) ── 你的web服务器应该能够通过浏览器访问。

REST请求的例子:

GET /someresources/anId

PUT /someresources/anId
{"anotherdata": "another value"}

REST关注于暴露数据。它减少了客户端/服务端的耦合程度,经常用于公共HTTP API接口设计。REST使用更通常与规范化的方法来通过URI暴露资源,通过header来表述并通过GET、POST、PUT、DELETEPATCH这些动作来进行操作。因为无状态的特性,REST易于横向扩展和隔离。

缺点:REST

  • 由于REST将重点放在暴露数据,所以当资源不是自然组织的或者结构复杂的时候它可能无法很好的适应。举个例子,返回过去一小时中与特定事件集匹配的更新记录这种操作就很难表示为路径。使用REST,可能会使用URI路径,查询参数和可能的请求体来实现。
  • REST一般依赖几个动作(GET、POST、PUT、DELETEPATCH,但有时候仅仅这些没法满足你的需要。举个例子,将过期的文档移动到归档文件夹里去,这样的操作可能没法简单的用上面这几个verbs表达。
  • 为了渲染单个页面,获取被嵌套在层级结构中的复杂资源需要客户端,服务器之间多次往返通信。例如,获取博客内容及其关联评论。对于使用不确定网络环境的移动应用来说,这些多次往返通信是非常麻烦的。
  • 随着时间的推移,更多的字段可能会被添加到API响应中,较旧的客户端将会接收到所有新的数据字段,即使是那些它们不需要的字段,结果它会增加负载大小并引起更大的延迟。

RPCREST比较

操作 RPC REST
注册 POST /signup POST /persons
注销 POST /resign
{
“personid”: “1234”
}
DELETE /persons/1234
读取用户信息 GET /readPerson?personid=1234 GET /persons/1234
读取用户物品列表 GET /readUsersItemsList?personid=1234 GET /persons/1234/items
向用户物品列表添加一项 POST /addItemToUsersItemsList
{
“personid”: “1234”;
“itemid”: “456”
}
POST /persons/1234/items
{
“itemid”: “456”
}
更新一个物品 POST /modifyItem
{
“itemid”: “456”;
“key”: “value”
}
PUT /items/456
{
“key”: “value”
}
删除一个物品 POST /removeItem
{
“itemid”: “456”
}
DELETE /items/456

资料来源:你真的知道你为什么更喜欢REST而不是RPC

来源及延伸阅读:RESTRPC

安全

这一部分需要更多内容。一起来吧

安全是一个宽泛的话题。除非你有相当的经验、安全方面背景或者正在申请的职位要求安全知识,你不需要了解安全基础知识以外的内容:

  • 在运输和等待过程中加密
  • 对所有的用户输入和从用户那里发来的参数进行处理以防止 XSSSQL注入
  • 使用参数化的查询来防止SQL注入。
  • 使用最小权限原则

来源及延伸阅读

附录

一些时候你会被要求做出保守估计。比如,你可能需要估计从磁盘中生成100张图片的缩略图需要的时间或者一个数据结构需要多少的内存。2的次方表每个开发者都需要知道的一些时间数据(译注:OSChina上有这篇文章的译文)都是一些很方便的参考资料。

2的次方表

Power           Exact Value         Approx Value        Bytes
---------------------------------------------------------------
7                             128
8                             256
10                           1024   1 thousand           1 KB
16                         65,536                       64 KB
20                      1,048,576   1 million            1 MB
30                  1,073,741,824   1 billion            1 GB
32                  4,294,967,296                        4 GB
40              1,099,511,627,776   1 trillion           1 TB

来源及延伸阅读

每个程序员都应该知道的延迟数

Latency Comparison Numbers
--------------------------
L1 cache reference                           0.5 ns
Branch mispredict                            5   ns
L2 cache reference                           7   ns                      14x L1 cache
Mutex lock/unlock                           25   ns
Main memory reference                      100   ns                      20x L2 cache, 200x L1 cache
Compress 1K bytes with Zippy            10,000   ns       10 us
Send 1 KB bytes over 1 Gbps network     10,000   ns       10 us
Read 4 KB randomly from SSD*           150,000   ns      150 us          ~1GB/sec SSD
Read 1 MB sequentially from memory     250,000   ns      250 us
Round trip within same datacenter      500,000   ns      500 us
Read 1 MB sequentially from SSD*     1,000,000   ns    1,000 us    1 ms  ~1GB/sec SSD, 4X memory
Disk seek                           10,000,000   ns   10,000 us   10 ms  20x datacenter roundtrip
Read 1 MB sequentially from 1 Gbps  10,000,000   ns   10,000 us   10 ms  40x memory, 10X SSD
Read 1 MB sequentially from disk    30,000,000   ns   30,000 us   30 ms 120x memory, 30X SSD
Send packet CA->Netherlands->CA    150,000,000   ns  150,000 us  150 ms

Notes
-----
1 ns = 10^-9 seconds
1 us = 10^-6 seconds = 1,000 ns
1 ms = 10^-3 seconds = 1,000 us = 1,000,000 ns

基于上述数字的指标:

  • 从磁盘以30 MB/s的速度顺序读取
  • 100 MB/s1 Gbps的以太网顺序读取
  • SSD1 GB/s的速度读取
  • 4 GB/s的速度从主存读取
  • 每秒能绕地球6-7
  • 数据中心内每秒有2,000次往返

延迟数可视化

来源及延伸阅读