技术面试的系统设计题(二)

可扩展性

基础

现在您已经设计了一个可靠的抽象体系结构,下一步就是将其扩展。如果你从来没有建立过大规模的系统,这个任务看起来有点令人生畏。

除了接下来的一个视频和四篇博文的翻译,你也可以看看An Unorthodox Approach To Database Design : The Coming Of The Shard

可扩展的Web开发

视频地址
课件地址

比较建议看一看视频,视频里面讲的非常杂乱,感觉没有什么逻辑,所以我整理的也是乱七八糟。他说的东西也是非常基础的,但是巩固一下也是很不错的。

具体的一些技术方面的大致情况可以看看这篇博文,里面讲到了Web服务器从基础到复杂的扩展过程。比这个课更有逻辑。

垂直扩展

对于一台服务器使用更多的更好的硬件,如下资源

  • CPU
    • cores, L2 Cache, …
  • Disk
    • 接口:PATA, SATA, SAS, …
    • RAID
  • RAM

限制:没有那么多物理资源

水平扩展

不同于垂直扩展那样选用最贵最好的服务器,水平扩展接受了便宜的硬件设施,但是用了更多的便宜且更慢的服务器实现了扩展。也就是,使用了大规模的集群分布式。

负载均衡

通过Load Balancer将服务集群黑盒化,那么对于外界而言,他们始终以为访问的是一个服务器。

Load Balancer具有一个公开的IP地址,而后端服务器不需要公开的IP地址,而是用内网IP地址。类似NAT协议那样。

关于负载均衡的算法由很多,比如,Round Robin算法(一种以轮询的方式依次将一个域名解析到多个IP地址的调度不同服务器的计算方法。)

开源的DNS系统BIND对于同样的域名请求,可能会返回不同的IP地址。

  • 软件
    • ELB
    • HAProxy
    • LVS
  • 硬件
    • Barracuda
    • Cisco
    • Citrix
    • F5
共享存储

RR可能会导致服务器1总是受到请求,从而导致堵塞。即,因为负载均衡算法的缺陷,导致负载均衡效果不理想。

sticky session问题:如果你登陆一个网址好多次,你的cookies和session等信息会分布在不同的机器上。那么如果你的请求被分发到一个新的机器上,那么这些就不复存在了,你必须重新登陆,重新添加购物车,这明显是不行的。

为了让负载均衡器能够明白每一个Server所处的状态,为了让所有Server的信息都保持一致,我们会使用共享存储的设计。

但是同样的共享存储也是有一定缺陷的:

  1. 如果所有系统都使用一块硬盘,如果这块硬盘挂了,那么所有的服务器都不能使用了。
  2. 如果按照功能进行分机和存储相应的数据,如,处理用户登录请求和用户cookies的记录,视频媒体等存储。如果最关键的那台服务器宕机了,整个服务也就都没有用了。

于是,我们可以使用增加硬盘数量实现备份。比如,我们同时用两块硬盘存储同一份数据的两个副本,这样虽然增加了一些overhead,但是能够实现备份。(关于这点还有很多论文去解释和设计相应的系统,比如raft一致性协议等等)。

同时我们也可以使用两块硬盘存储一份数据的不同部分,当一块在写的时候,我们可以写另一块。并行地运行写入程序。

Cacheing

HTML文件

简单地将HTML存储下来,或者以HTML模板的形式存储一些不怎么需要更改的内容。

SQL Cache

查询的缓存,可见其他数据库的设计情况。

Memcached

其他第三方内存数据库,用于缓存。还有比如,reids等等。

但是也需要一些别的协议或者机制来维护这类的缓存,如,同步问题, 垃圾回收

复制

主从模式

多主机模式

负载均衡+复制

有些类似数据库读写分离或者数据库的备份

数据分块存储

简单易懂的可扩展

克隆

可扩展Web服务的公共服务器通常是做为黑盒,并隐藏在负载平衡器后面。此负载平衡器将负载(来自用户的请求)平均分配到您的组/应用程序服务器群集上。这意味着,例如,如果小明与服务器进行交互,则他的第一个请求可能在服务器2进行处理,然后由服务器9处理第二请求,然后第三请求上再次由服务器2处理。

小明每次请求都应该得到相同(或者说一致)的结果,这与哪台服务器处理他的请求无关。

这是可扩展性的第一条黄金法则:每台服务器都包含完全相同的代码库,不会在本地磁盘或内存中存储任何与用户相关的数据,如会话或配置文件图片。

会话需要存储在所有应用程序服务器均可访问的集中式数据存储中。它可以是外部数据库或外部持久性缓存,如Redis。外部持久缓存比外部数据库具有更好的性能。换句欢说,通过外部存储是数据存储不驻留在应用程序服务器上。相反,它位于应用程序服务器的数据中心内或附近的节点。

但是如何同时部署多台呢?如何确保将代码更改并成功发送到所有服务器,而不是有些服务器运行新代码而有些服务器仍旧用旧代码?幸运的是,这个棘手的问题已经由伟大的工具Capistrano解决了。这需要一些学习,特别是如果你的Ruby不够熟练中。

在将您的会话“外包”并从所有服务器都能访问的代码块提供相同的代码后,就可以从这些服务器之一(AWS称之为AMI - Amazon Machine Image)创建映像文件。将此AMI用作“超级克隆”所有的新实例都基于。每当你开始一个新的实例/克隆,只要做一个最新的代码的初始部署就足够了!

数据库

现在,所需的更改比仅添加更多克隆的服务器更为激进,甚至可能需要一些勇气。你可以选择2个路径:

路径#1:是坚持使用MySQL,并保持野蛮的运行。聘请数据库管理员告诉他做主从复制和读写分离(从从机读取,写入主机),并通过添加RAM来升级主服务器。在几个月的时间里,你的数据库管理员会想出“分片”,“反规范化”和“SQL调优”这样的词汇,并且会在接下来的几周里担心必要的加班。那时,每一个新的动作来保持你的数据库运行将比以前更昂贵和耗时。如果您选择了Path#2,而您的数据集仍然很小并且易于迁移,那么您可能会变得更好。

路径#2:从头开始正确反规范化,并且在任何数据库查询中不再包含连接。你可以使用MySQL,像NoSQL数据库一样使用它,或者你可以切换到一个更好,更容易扩展的NoSQL数据库,比如MongoDB或者CouchDB。现在需要在应用程序代码中完成联接。越早执行此步骤,您将来必须更改的代码越少。但即使您成功切换到最新,最好的NoSQL数据库,并让您的应用程序执行数据集连接,您的数据库请求很快也会变得越来越慢。你将需要引入一个缓存。(关于这一点,我不是很赞同,还是要根据应用场景分类,不是所有的场景适合使用NoSQL的,而且不同的NoSQL适用的地方也不同,HBase和Redis就很不一样)

缓存

对于“缓存”,这儿指像Memcached或Redis这样的内存缓存(或者说,内存数据库)。不要执行基于硬盘文件的缓存,它会使服务器的克隆和自动扩展付出高昂的代价。

内存缓存通常是一个简单的KV键值存储,它应该作为应用程序和数据存储之间的缓冲层。无论何时您的应用程序必须读取数据,它应该首先尝试从缓存中检索数据。只有当它不在缓存中时,才会尝试从主数据源获取数据。

缓存将每个数据集保存在RAM中,所以能够尽可能快地处理请求。例如,Redis可以在标准服务器上托管时每秒执行几十万次读取操作。还写操作,特别是增量。

有两种缓存数据的模式。一个旧的和一个新的:

#1 - 缓存的数据库查询

这仍然是最常用的缓存模式。每当您对数据库执行查询时,都会将结果数据集存储在缓存中。key是我们查询的hash值。下次运行查询时,首先检查它是否已经在缓存中。

这种模式有几个问题。主要问题是数据到期。缓存复杂查询时,很难删除缓存的结果。当一条数据发生变化时(例如一个表格单元格),您需要删除可能包含该表格单元格的所有缓存查询。这样将会非常混乱。

#2 - 缓存的对象

这是我强烈的建议,我总是喜欢这种模式。一般来说,将数据视为一个对象,就像你已经在你的代码(类,实例等)中做的那样。让您的类从您的数据库中组合一个数据集,然后将该类的完整实例或组合数据集存储在缓存中。

听起来很理论,我知道,但看看你通常的代码。例如,您有一个名为“Product”的类,它有一个名为“data”的属性。这是包含产品的价格,文本,图片和客户评论的数组。属性“data”由类中的几个方法填充,执行多个难以缓存的数据库请求,因为许多事情相互关联。

现在,请执行以下操作:当您的类完成数据数组的“组装”时,直接将数据数组或更好的类的完整实例存储在缓存中!这样,只要有事情发生变化,就可以轻松地摆脱对象,并使代码的整体操作更加快速和合理。

最好的部分是:它使异步处理成为可能!应用程序只访问最新的缓存对象,几乎从不接触数据库!

缓存对象的一些想法:

  1. 用户会话(user sessions)从不使用数据库
  2. 完全呈现的博客文章
  3. 活动流
  4. 用户< - >朋友关系

一般来说,我更喜欢Redis的Memcached,因为我喜欢Redis的额外数据库特性,如持久性和内置的数据结构,如列表和集合。有了Redis和一个聪明的钥匙,你甚至有可能完全摆脱一个数据库。但是如果你只是需要缓存,就可以使用Memcached。

异步(Asynchronism)

请想象一下,你想在你最喜欢的面包店买面包。所以你走进面包店,点了一个面包,但那里没有面包!相反,在你点了的两个小时之后,你的面包才能被做好。这很烦人,不是吗?

为了避免这样的“请稍等” - 情况,需要完成异步。面包店有什么好处,也许对你的网络服务或网络应用也有好处。

一般来说,有两种方法/范例可以完成不同步。

异步#1

让我们想想前面的面包店例子。异步处理的第一种方式是“晚上烘烤面包,早上卖”的方式。没有等待时间。提到一个网络应用程序,这意味着提前完成耗时的工作,并以较低的请求时间完成完成的工作。

很多时候,这个范例被用来将动态内容转化为静态内容。一个网站的页面(可能是用一个巨大的框架或CMS构建的)在每次更改时都被预先渲染并作为静态HTML文件存储在本地。通常这些计算任务是定期完成的,也可能是由cronjob每小时调用一次的脚本。整体通用数据的预先计算可以极大地改善网站和网络应用程序,并使其具有很高的可扩展性和性能。试想一下,如果脚本会将这些预渲染的HTML页面上传到AWS S3或Cloudfront或其他付款云网络,就可以想象网站的可扩展性!您的网站将可以达到超级响应,可以处理数以每小时百万计的游客!

异步#2

回到面包店。不幸的是,有时顾客有特殊的要求,比如生日蛋糕-“生日快乐,小明”!面包店无法预见到这种客户的意愿,所以当客户在面包店时要开始工作,并告诉他在第二天回来。引用一个Web服务,意味着异步处理任务。

这是一个典型的工作流程:

用户来到您的网站,并开始一个非常计算密集的任务,这将需要几分钟时间完成。因此,您的网站前端将作业发送到任务队列,并立即发回给用户:您的任务正在被执行(或者等待执行),请继续浏览页面。任务队伍被一群worker检查,如果有新的任务出现,那么worker就干这个任务,几分钟后发出一个信号表明任务已经完成。前端不断地检查新的“任务已经完成” - 信号,看到任务完成并通知用户。(这只是一个简单的例子,检查的机制也有很多,可以参见数据库并发执行里面如何执行查询任务的。)

如果你现在想深入细节和实际的技术设计,我建议你看看RabbitMQ网站上的前3个教程。 RabbitMQ是帮助实现异步处理的许多系统之一。你也可以使用ActiveMQ或简单的Redis列表。基本的想法是有一个worker可以处理的任务或任务队列。

异步似乎很复杂,但绝对值得您花时间来了解它并自己实现。后端变得几乎可以无限扩展,前端变得活泼,这对整体用户体验是有利的。

如果你做了一些耗时的事情,试着总是异步地做。

例子

课程提供了一些公司的扩展性例子,参见原网址

https://www.hiredintech.com/classrooms/system-design/lesson/61

综合

一切都是一个权衡

这是系统设计中最基本的概念之一。

希望在这一点上,这对你来说不是一个惊喜。如果你看过真实的架构,就会发现很少有一种完美的方式来做事。每家公司都有不同的架构。设计一个可扩展的系统是一个优化任务:有大量的约束(时间,预算,知识,复杂性,当前可用的技术等等),并且你需要建立适合这些约束的最好的东西。每一种技术,每种模式对某些事物都是有益的,而对其他事物则不是那么好。了解这些优点和缺点,优点和缺点是关键。

记住:没有一个最佳的系统设计。

当然,有最好的做法,你可以使用。但是最后,这一切都归结为在市场时间,系统复杂性,开发成本,维护成本,可用性等诸多方面之间的平衡。

能够理解和讨论这些权衡是系统设计(以及系统设计问题)的全部内容。

在你的准备中,不要试图找到完美的东西。相反,关注每个可伸缩性模式的优点,缺点是什么,以及为什么人们比其他模式更喜欢它。

把它放在一起,保持最新状态

在这一点上,你可以做的最有用的事情是拿出一个或两个系统,其中包含你学到的可扩展性课程。

最后,你将在整个职业生涯中得到良好的服务,以便及时了解可伸缩性如何演变。例如,10年前,没有亚马逊网络服务,公司被迫管理自己的基础设施。如今,使用EC2,RDS,S3,Elastic MapReduce等服务,您可以建立一个巨大的公司。所以,虽然AWS没有改变基本的可扩展性原则,但它确实改变了人们在扩展时需要熟悉的技术格局。因此,保持最新是非常重要的(否则你会重新发明轮子)。

面试

那么面试时你应该做什么?

首先,请遵循系统设计流程。你已经知道如何应用它,所以我们将会简短。不要跳过步骤,不要做假设,当问及时开始广泛深入。

其次,请记住,系统设计问题是一个观念交流平台。准备讨论权衡利弊。准备提供替代方案,提出问题,找出并解决瓶颈问题,根据面试者的偏好进行广泛深入的讨论。

不要保守:每当面试者挑战你的架构选择,承认很少一个想法是完美的,并概述了你的选择的优点和缺点。开放讨论期间面试官提出的新约束,并即时调整架构。

最重要的是,玩得开心。梦想建筑是一个非常刺激的心理过程 - 享受和保持积极。你已经具备了正确的知识,只要在面试中应用,你就会做得很好。

Tiny URL

在上一章中,我们说到了我们的系统面临着几个挑战:

  1. 每秒400个请求
  2. 有3TB的数据去存储并且快速查询

那么我们就要在抽象设计的基础上进行修改,使其成为可扩展的设计,并能够解决上面的限制:

  1. 应用服务层:
    • 从单独一台服务器可以处理多少数据开始
    • 用负载测试去测试速率
    • 增加一个负载均衡器,以及逐渐增加集群:解决通信量,增加可用性
  2. 数据存储
    • 数据特性
      • 上亿个对象
      • 每个对象都特别小(<1k)
      • 对象与对象之间没有关系
      • 读 是 写 的9倍(每秒360次读,40次写)
      • 5TBs urls,36GBs hashes
    • MySQL
      • 广泛使用
      • 成熟的技术
      • 清晰的可扩展技术(sharding分片,master/slave replication, master/master replication)
      • 索引查询非常快
    • 映射表
      • hash: varchar(6)
      • origin: varchar(512)
    • 在hash属性上建立聚集索引(36GB+),我们希望将这个索引存在内存中。
      • 垂直扩展MySQL服务器
      • 数据分片:5片数据,600GBs数据,8GB索引
      • 读写分离,主从复制(从多台从机读取,写入主机,由主机更新数据到从机)

你可能感兴趣的:(计算机学科学习笔记)