目录
- 数据密集型系统5个模块和三个原则
- SQL VS NOSQL
- JSON模型(文档模型)和传统的RDBMS
- 可扩展性要考虑哪2个维度?
- 可靠性的3方面,和解决方案
- 可维护性要注意的点
- 实战推特
1.五模块和三原则
作为一个开发者来说,目前绝大多数应用程序都是数据密集型的,而不是计算密集型的。CPU的计算能力不再成为这些应用程序的限制因素,而更加亟待解决的问题是海量的数据、数据结构之间的复杂性,应用的性能。
先看看我们经常打交道的数据系统:
存储数据,以便它们或其他应用程序稍后再找到它(数据库)
记住昂贵操作的结果,以加快读取速度。(缓存)
允许用户按关键字搜索数据或通过各种方式过滤数据(搜索索引)
将消息发送到另一个进程,异步处理(流处理)
周期性地压缩大量的累积数据(批处理)
而很多时候,我们所谓应用程序的绝大工作就是将这些数据系统进行组合,然后添加我们的运行逻辑,但是如何更加合理的整合这些数据系统,对我们来说仍然是一个值得学习和思考的问题。而数据系统也在慢慢变得越来越相似,不同的数据系统也在各自学习彼此的优点。如Redis这样的缓存系统可以支持数据落地,很多时候的应用场合我们可以替代传统的RDBMS。而Kafka这样的数据队列也可以支持数据落地来存储消息。更加深刻的理解这些数据系统,来更好的权衡架构设计,是一门很精深的课题
设计数据密集型应用的三原则
可靠性
具有容错性(面对硬件或软件故障,甚至是人为错误),系统仍应继续正常工作(在期望的性能水平上执行正确的功能)。
可扩展性
随着系统的增长(在数据量、流量或复杂度),应该有合理的方法来处理这种增长。
可维护性
随着时间的推移,许多不同的人将致力改善数据系统(既保持当前的行为,并使系统适应新的环境),他们都应该能够卓有成效地工作。
(1)可靠性
-
硬件故障
硬盘崩溃,内存出现故障时,电网停电,有人拔了网线,几乎硬件故障在数据中心总是不间断的出现。
解决方案:- 在软件与硬件层面考虑冗余,来确保硬件的故障不会演变为系统的故障。
-
软件错误
接受特定的错误输入,便导致所有应用服务器实例崩溃的BUG。例如2012年6月30日的闰秒,由于Linux内核中的一个错误,许多应用同时挂掉了。失控进程会占用一些共享资源,包括CPU时间、内存、磁盘空间或网络带宽。系统依赖的服务变慢,没有响应,或者开始返回错误的响应。级联故障,一个组件中的小故障触发另一个组件中的故障,进而触发更多的故障。
导致这类软件故障的BUG通常会潜伏很长时间,直到被异常情况触发为止。这种情况意味着软件对其环境做出了某种假设——虽然这种假设通常来说是正确的,但由于某种原因最后不再成立了。
解决方案:仔细考虑系统中的假设和交互;彻底的测试;
进程隔离;允许进程崩溃并重启;
测量、监控并分析生产环境中的系统行为。
如果系统能够提供一些保证(例如在一个消息队列中,进入与发出的消息数量相等),那么系统就可以在运行时不断自检,并在出现差异(discrepancy)时报警
-
人为的错误
人是很不可靠,从驾驶技术的演变就可以看出来,人为的疏失会带来巨大的灾难。而且,人经常犯错。
解决方案:最小化错误机会的方式设计系统。例如,精心设计的抽象,API和管理界面可以很容易地做“正确的事情”,阻止“错误的事情”。
人们犯最多错误的地方和那些可能导致失败的地方解耦。
全面测试,从单元测试到整个系统集成测试和手动测试。
允许快速和容易地从人为错误中恢复,以尽量减少在失败的情况下的影响。例如,使其快速回滚更改配置,逐步推出新的代码(所以任何意想不到的错误只影响一小部分用户),并提供工具来重新计算数据(如果原来旧的计算是不正确的)。
(2)可扩展性
即使一个系统今天工作可靠,但这并不意味着它将来一定会可靠地工作。一个常见原因是负载增加:也许系统已经从10000个并发用户发展到100000个并发用户,或者从100万个增加到1000万个。
“如果系统以特定的方式增长,我们应对增长的选择是什么?” “我们怎样才能增加计算资源来处理额外的负载?”
-
描述负载
首先,我们需要简洁地描述系统当前的负载,负载可以用几个数字来描述,我们称之为负载参数。
参数的选择取决于系统的体系结构,如:- 每秒对Web服务器的请求
- 数据库中的读写比
- 聊天室中的活跃用户数量
- 缓存的命中率
描述性能
一旦描述了系统上的负载,就可以讨论负载增加时发生的情况。可以从两方面看:
1.增加负载参数并保持系统资源(CPU、内存、网络带宽等)不变时,系统的性能如何受到影响?
2.当增加负载时,如果希望保持性能不变,需要增加多少资源?
所以我们需要有描述性能的尺子:
- 平均响应时间:给定n值的算术平均值,全部加起来,除以n。然而这不是一个很好的指标,因为它不告诉你有多少用户真正体验了延迟。
- 百分比响应时间:把响应时间列表,从最快到最慢排序,那么中间值是中间点:例如,如果平均响应时间是200毫秒,那意味着一半请求在少于200毫秒时返回,而一半请求花费的时间比那个要长。
- 高百分比的响应时间:可以看看高百分位数:95th,99th,和99.9th百分位数是常见的(简称P95,P99,和p999),来参考响应时间的阈值。
(3)可维护性
这部分教导了一些构建可维护系统的方法。软件的大部分成本不是在最初的开发中,而是在持续的维护中修复bug、保持系统运行、使其适应新业务、添加新特性。
可操作性
让操作运维团队保持系统运行的顺利。简单
让新工程师很容易理解系统,通过尽可能地从系统中删除尽可能多的复杂性。可进化性
让工程师很容易在将来对系统进行更改,以适应需求变化时的意料之外的用例。也被称为可扩展性、可修改性、可塑性。
2.SQL VS NoSQL
SCHEMA:
在SQL中,每条记录都符合固定SCHEMA,这意味着必须在数据输入之前决定和选择列,并且每行必须包含每列的数据。模式可以在以后更改,但它涉及修改整个数据库并脱机一段时间。
而在NoSQL中,SCHEMA是动态的。可以动态添加列,每个“行”(或等效的)不必包含每个“列”的数据。
可伸缩性:
在大多数常见情况下,SQL数据库是可垂直扩展的,即通过增加硬件的马力(更高的内存,CPU等),这可能变得非常昂贵。可以跨多个服务器扩展关系数据库,但这是一个具有挑战性且耗时的过程。通常要程序员自己实现。
另一方面,NoSQL数据库可以横向扩展,这意味着我们可以在NoSQL数据库基础架构中轻松添加更多服务器来处理大量流量。任何便宜的商品硬件或云实例都可以托管NoSQL数据库,从而使其比垂直扩展更具成本效益。许多NoSQL技术也会自动在服务器之间分配数据。
可靠性或ACID合规性(原子性,一致性,隔离性,持久性):
绝大多数关系数据库都符合ACID。因此,在数据可靠性和执行事务的安全保证方面,SQL数据库仍然是更好的选择。
大多数NoSQL解决方案都牺牲了ACID合规性以实现性能和可扩展性。
使用SQL数据库的原因
以下是选择SQL数据库的几个原因:
我们需要确保遵守ACID。 ACID合规性通过准确规定事务如何与数据库交互来减少异常并保护数据库的完整性。通常,NoSQL数据库牺牲了ACID合规性以实现可扩展性和处理速度,但对于许多电子商务和金融应用程序而言,符合ACID标准的数据库仍然是首选。
您的数据结构合理且不变。如果您的业务没有经历需要更多服务器的大规模增长,并且如果您只处理一致的数据,则可能没有理由使用旨在支持各种数据类型和高流量的系统。
总结,需要TRANSCATION,QPS不高,SCHEMA明确,支持使用关系型数据库。
使用NoSQL数据库的原因
当我们的系统的所有其他组件快速运转时,NoSQL数据库会阻止数据成为系统的瓶颈。大数据有助于NoSQL数据库取得巨大成功,主要是因为它处理的数据与传统的关系数据库不同。一些流行的NoSQL数据库例子是MongoDB,CouchDB,Cassandra和HBase。
存储大量通常几乎没有结构的数据。 NoSQL数据库对我们可以存储在一起的数据类型没有限制,并允许我们根据需要更改添加不同的新类型。使用基于文档的数据库,您可以将数据存储在一个位置,而无需事先定义数据的“类型”。
充分利用云计算和存储。基于云的存储是一种极好的节省成本的解决方案,但需要将数据轻松分布在多个服务器上以进行扩展。在现场或在云中使用商用(价格合理,体积更小)的硬件可以省去额外软件的麻烦,而像Cassandra这样的NoSQL数据库可以在多个数据中心开箱即用,而不会有太多麻烦。
快速开发。 NoSQL对快速开发非常有用,因为它不需要提前准备好。如果您正在进行系统的快速迭代,这需要频繁更新数据结构,而不会在版本之间造成大量停机,那么关系数据库会降低您的速度。
总结,QPS高,易于水平扩展(可利用云资源),数据SCHEMA弱。海量数据。选择NOSQL。
3. JSON模型(文档模型)和传统的RDBMS
目前大多数应用程序开发都是使用面向对象编程语言完成的,这导致了对SQL数据模型灵活性的批评:数据存储在关系表中,应用程序代码中需要在对象与表、行和列的数据库模型之间需要一个笨拙的转换层。(也就是我们日常使用的ORM)
LinkedIn是我们常用的职业档案网站,我们来看看使用不同数据模型的差异。
- 在传统的SQL模型中,最常见的规范化表示是将位置、教育和联系人信息放在单独的表中,带有外键表引用到用户表,如上图所示。问题是显而易见的,多表之间的依赖关系大大的复杂化了应用程序的编写。
- JSON模型减少了应用程序代码和存储层之间匹配问题,它会更加灵活。如上图所示,JSON表示相比多表模式具有更好的局部性。如果要获得如教育或职业信息,在 多表模型之中您需要执行多次查询(通过user_id查询每个表)或执行一个多表连接的操作。而在JSON的数据模型之中,所有相关信息都在一个位置,一次查询就足够完成了。
(注:在例子中的前一段,region_id和industry_id给出的ID,不是纯文本字符串“Greater Seattle Area”和“Philanthropy”。有如下几个考量:(1)避免歧义(2)可以统一更新(3)可以更好的本地化来适应不同的语言。使用id的优点是,因为它对人类没有意义,所以它不需要更改:id可以保持不变,即使它标识的信息是变化的。任何对人类有意义的东西都可能需要在将来某个时候改变,如果信息被复制,所有多余的副本都需要更新。这会导致写开销,并且不一致性的风险。区域和行业的列表可能很小,而且变化缓慢,以至于应用程序可以简单地将它们保存在内存中。)
文档型数据模型的灵活性:
当应用程序希望改变其数据格式的情况下,灵活性就显得至关重要了。 例如,假设我们在数据库中将每个用户的全名存储在一个字段中,而现在想要分别存储名称和姓氏。
- 文档数据库中,只需要开始使用新字段编写新文档,并在应用程序中有代码处理旧文档读取时的情况。
if (user && user.name && !user.first_name) {
user.first_name = user.name.split(" ")[0];
}
- 在关系型数据库模式中,通常按照这样的思路修改模型:
ALTER TABLE users ADD COLUMN first_name text;
UPDATE users SET first_name = split_part(name, ' ', 1);
UPDATE users SET first_name = substring_index(name, ' ', 1);
在一个大数据量的表上运行UPDATE语句可能在任何数据库上都很慢,因为每一行都需要重写。如果这是不可接受的,应用程序可以让first_name设置为其默认为填写在读的时候,通过这样的方式来模拟文档数据库的灵活性。
文档型的数据模型的主要优点是模式灵活性,在局部性更好的性能,如程序经常需要访问整个文档时具有更好的性能优势。对于特定的应用程序,它更接近应用程序所使用的数据结构。如果在应用程序中的数据具有类似文档的结构(即一对多关系树,通常是一次加载整个树),那么使用文档模型会是一个好的选择。关系型数据模型通过提供更好的连接支持、多对一和多对多关系,如果应用程序使用多对多关系,关系型数据模型会更加适合。通过在数据库中生成多个请求,可以在应用程序代码中模拟连接,但这也会将复杂性移动到应用程序中。
在历史上,数据最开始被表示为一棵大树(层次数据模型),但是这不利于表示多对多的关系,所以发明了关系模型来解决这个问题。最近,开发人员发现一些应用程序也不适合采用关系模型。新的非关系型“NoSQL”数据存储在两个主要方向上存在分歧:
文档数据库的应用场景是:数据通常是自我包含的,而且文档之间的关系非常稀少。
图形数据库用于相反的场景:任意事物都可能与任何事物相关联。
这三种模型(文档,关系和图形)在今天都被广泛使用,并且在各自的领域都发挥很好。一个模型可以用另一个模型来模拟 — 例如,图数据可以在关系数据库中表示 — 但结果往往是糟糕的。这就是为什么我们有着针对不同目的的不同系统,而不是一个单一的万能解决方案。
文档数据库和图数据库有一个共同点,那就是它们通常不会为存储的数据强制一个模式,这可以使应用程序更容易适应不断变化的需求。但是应用程序很可能仍会假定数据具有一定的结构;这只是模式是明确的(写入时强制)还是隐含的(读取时处理)的问题。
4. 实战推特
功能设计:
发布推文
用户可以向其粉丝发布新消息(平均 4.6k请求/秒,峰值超过 12k请求/秒)。
主页时间线
用户可以查阅他们关注的人发布的推文(300k请求/秒)。
处理每秒12,000次写入(发推文的速率峰值)还是很简单的。然而推特的扩展性挑战并不是主要来自推特量,而是来自扇出(fan-out)——每个用户关注了很多人,也被很多人关注。
大体上讲,这一对操作有两种实现方式。
-
发布推文时,只需将新推文插入全局推文集合即可。当一个用户请求自己的主页时间线时,首先查找他关注的所有人,查询这些被关注用户发布的推文并按时间顺序合并。所示的关系型数据库中,可以编写这样的查询:
SELECT tweets.*, users.* FROM tweets JOIN users ON tweets.sender_id = users.id JOIN follows ON follows.followee_id = users.id WHERE follows.follower_id = current_user
- 为每个用户的主页时间线维护一个缓存,就像每个用户的推文收件箱。 当一个用户发布推文时,查找所有关注该用户的人,并将新的推文插入到每个主页时间线缓存中。 因此读取主页时间线的请求开销很小,因为结果已经提前计算好了。
推特的第一个版本使用了方法1,但系统很难跟上主页时间线查询的负载。所以公司转向了方法2,方法2的效果更好,因为发推频率比查询主页时间线的频率几乎低了两个数量级,所以在这种情况下,最好在写入时做更多的工作,而在读取时做更少的工作。
然而方法2的缺点是,发推现在需要大量的额外工作。平均来说,一条推文会发往约75个关注者,所以每秒4.6k的发推写入,变成了对主页时间线缓存每秒345k的写入。但这个平均值隐藏了用户粉丝数差异巨大这一现实,一些用户有超过3000万的粉丝,这意味着一条推文就可能会导致主页时间线缓存的3000万次写入!及时完成这种操作是一个巨大的挑战 —— 推特尝试在5秒内向粉丝发送推文。
在推特的例子中,每个用户粉丝数的分布(可能按这些用户的发推频率来加权)是探讨可扩展性的一个关键负载参数,因为它决定了扇出负载。你的应用程序可能具有非常不同的特征,但可以采用相似的原则来考虑它的负载。
推特轶事的最终转折:现在已经稳健地实现了方法2,推特逐步转向了两种方法的混合。大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中。但是少数拥有海量粉丝的用户(即名流)会被排除在外。当用户读取主页时间线时,分别地获取出该用户所关注的每位名流的推文,再与用户的主页时间线缓存合并,如方法1所示。这种混合方法能始终如一地提供良好性能。
更多细节,参加我的另一篇
设计推特