进入计算机的时间维度
CPU主频:反映了CPU时钟周期,即CPU运行一个时间片的时间,标志着CPU的运算速度。
而,频率 = 1 / 周期。
频率单位:次/秒(1秒多少次);
周期单位:秒/次(1次多少秒)
所以,1GHZ主频的CPU,换算成周期,一个CPU时间片也就是:
那么,
1GHZ主频的CPU的时钟周期为1ns
2GHZ主频的CPU的时钟周期为0.5ns
3GHZ主频的CPU的时钟周期为0.3ns
再来看看计算机各个部件的速度
可以看到:
- CPU一个时钟周期,0.3ns
- 分支预测错误,5ns
- 互斥锁,加锁和解锁时间,25ns。
- 内存访问,120ns
- CPU上下文切换,1500ns。
- 固态硬盘访问,50-150μs
- 传统硬盘访问,1-10ms
- 网络访问最慢,几十毫秒。
上下文切换更恐怖的事情在于,这段时间里 CPU 没有做任何有用的计算,只是切换了两个不同进程的寄存器和内存状态;而且这个过程还破坏了缓存,让后续的计算更加耗时。
这幅图最有趣的地方在于它把计算机世界的时间和人类世界的时间做了对比,CPU的一个时钟周期如果按1秒算:
- 内存访问就是6分钟
- 固态硬盘是2-6天
- 传统硬盘是1-12个月
- 网络访问就是几年
计算机的主要矛盾
CPU和内存之间的速度瓶颈被称为冯诺依曼瓶颈,解决这个瓶颈的本质方法就是使用缓存。
而以上这种结构就是成本和需求权衡之后的结果。
正是由于计算机各个部件的速度不同,容量不同,价格不同,导致了计算机系统/编程中的各种问题以及相应的解决方案,来举几个例子。
案例1:最珍贵的资源应该被最高效利用
CPU的速度超级快,不能老是让它闲着,要充分地利用。
这里有两个强劲的理由:
1. 人类需要多个程序“同时”运行。
我们要把CPU的时间进行分片,让各个程序在CPU上轮转,造成一种多个程序同时在运行的假象,即并发。
2. 当CPU遇到IO操作(硬盘,网络)时,不能坐在那里干等“几个月”甚至“几年“。在等待的时候,一定要切换,去执行别的程序。
说起来简单,但是程序的切换需要保存程序执行的现场,以便以后恢复执行,于是需要一个数据结构来表示,这就是进程了。
如果一个进程只有一个“执行流”,如果进程去等待硬盘的操作,那这个程序就会被阻塞,无法响应用户的输入了,所以必须得有多个“执行流”,即线程。
案例2:缓存大概率数据,解决速度不一致
需要持久化的数据一定要保存到硬盘中,但是硬盘超级慢,支持不了大量的并发访问,那怎么办呢?
可以把最常访问的热点数据放到CPU的缓存中嘛,其实CPU也是这么做的,但是CPU的L1,L2,L3级缓存实在是太小,根本满足不了需求。
于是只好退而求其次,把热点数据放到速度稍慢的内存中,于是应用程序的缓存就出现了。
缓存虽然是解决了问题,但是也带来了更多的问题,例如:
缓存数据和数据库数据怎么保持一致性?缓存如果崩溃了该怎么处理?数据在一台机器的内存放不下了,要分布到多个机器上,怎么搞分布式啊,用什么算法?
案例3:I/O太慢,线程池、异步、非阻塞
考虑一个像Tomcat这样的应用服务器,对于每个请求都要用一个线程来处理,如果现在有一万个请求进来, Tomcat会建立1万个线程来处理吗?
不会的,因为线程多了开销会很大,线程切换起来也很慢,所以它只好用个线程池来复用线程。
现在假设线程池中有一千个可用线程(已经非常多了),它们都被派去访问硬盘,数据库,或者发起网络调用,这是非常慢的操作,导致这一千个线程都在等待结果的返回(阻塞了),那剩下的九千个请求就没法处理了,对吧?
所以后来人们就发明了新的处理办法,仅使用几个线程(例如和CPU核心数量一样),让他们疯狂运行,遇到I/O操作,程序就注册一个钩子函数放在那里,然后线程就去处理别的请求,等到I/O操作完成了,系统会给这个线程发送一个事件,线程就回过头来调用之前的钩子函数(也叫回调函数)来处理。
这就是异步,非阻塞的处理方式。
案例4:数据读写不再成为瓶颈时:返璞归真单线程
Redis使用单线程的方式来处理请求的,为什么用单线程就可以呢?它为什么不像Tomcat那样使用多线程和线程池呢?
因为它面对的仅仅是内存,内存的速度在计算机的体系中仅次于CPU,比那些网络操作不知道要快到哪里去了。
所以这个唯一的线程就可以快速地执行内存的读写操作,完成从许多网络过来的缓存请求了。单线程还有个巨大的优势,没有竞争,不需要加锁!
所以,我们软件中的很多问题,其根源都是计算机各个部件的速度差异导致的。
计算机工作原理
计算机:解放人类脑力,做复杂运算的机器。
由于计算是宇宙的一种基本活动,所以渐渐地,计算机能做的事情越来越多。其实,计算机就是对人类活动的一种极致的抽象。
计算机的一般内容:
(1) input
(2) storage
(3) process
(4) output
从电信号到计算
1. 电路信号的开和关表示1、0
2. 有了二进制
3. 二进制表示数字
4. 数字是数据的载体
5. 数据的表示:文本(符号编码)、声音(声波采样)、图像(像素点的三原色值)
6. 数据 布尔逻辑(逻辑门)
7. CPU逻辑运算
本质:二进制表示一切信息,逻辑门电路处理一切信息。
计算机之道
【抽象】抛弃细节
软件开发是一个高度复杂的智力活动,程序员经常需要面对、处理异常复杂的业务和逻辑,如果你不具备强大的抽象能力,无法把具体变成概念,进而驾驭概念进行思考,你就很难降低问题的复杂度,从而陷入泥潭,无法自拔。
其实一个抽象的东西形式优美,结构简单,很有可能是正确的,很可能抓住了事物的本质。相反如果连形式都丑陋不堪,十有八九不是好的成果。
抽象层次越高,接口的语意就越模糊,适用的范围就越广,到最后就会变成数学模型或者概念。
抽象成数学模型和算法通常是可遇而不可求的,这种情况下,我们需要退而求其次,试图抽象成若干个正交的概念,来降低复杂度。
“正交”在数学上指的是线性无关,最常见的例子就是坐标系下的x 轴和y轴,对于一个点来讲,它的x值的变化不会影响到y,y值得变化不会影响到x ,即x和y是正交的。
正交的威力在于互不影响,扩展方便。我们人类的大脑在思考问题的时候是有容量限制的,难以同时驾驭太多复杂的概念,如果我们的软件系统也能做成x,y,z坐标这样,就带来了无与伦比的好处,你在处理x轴相关的事情时,不用考虑其他的y和z 相关的东西,因为你知道他们不会受到影响,这样问题的复杂度就从3维一下子下降到1维,更容易把握了!
如果整个系统还没法抽象成正交的概念,那只好再退一步,在局部使用接口。在著名的《设计模式》一书中,其实在反复强调一点: 发现变化并且封装变化,针对接口编程而不是实现编程。很多人看书是只关注具体的模式,而忽略了模式的本质目的。
我们在开发的过程中要保持一种敏锐的感觉,发现可能的变化并且封装起来,只提供一个精心定义的接口让外界调用。这样你在接口后面所做的任何变化,外边就不受影响了。一组定义良好的接口一定是正交的,不然的话接口之间的依赖就会让实现非常麻烦。
说到底,软件设计和开发就是把现实中的问题映射到计算机的语言实现,但现实问题太复杂,细节太多,而且在不断的变化过程中,一般人很难同时对这么的细节进行思考,这时候就需要抽象。我们只有从纷繁复杂的现象中抽取事物的本质,从具体事物提炼出正交的概念,才能驾驭这些概念,才能在一个低复杂度的世界中进行思考。
【分层】我只想和邻居打交道
分层其实也是抽象的一种,它通过层次把复杂的,可能变化的东西隔离开来,某一层只能访问它的直接上层和下层,不能跨层访问。
例如网络协议分层、再比如Web开发的分层。
分层的好处就是隔离变化, 在接口不变的情况下,某一层的变化只会局限于本层次内。即使是接口变化,也仅仅会影响调用方。
【局部性原理】上帝的规矩
这个原理讲的是在一段时间里,整个程序的执行仅限于程序的某一个部分,相应的,程序访问的存储空间也局限于某一个内存区域,具体分为:
1. 时间局部性:是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。
2. 空间局部性:是指一旦程序访问了某个存储单元,则不久之后。其附近的存储单元也将被访问。
但是这个原理的用处很大,例如Java 虚拟机,本来是解释执行.class文件,性能不怎么样,但是利用局部性原理,就可以找到那些常用的,所谓的热点(Hotspot)代码,然后把他们编译成本地原生代码(native code),这样执行效率就和C/C 差不多了。
这个原理更大的用处就是下面要提到的缓存。
【缓存】飞机和驴车打交道
为什么需要缓存(Cache)?
本质的原因是速度的不匹配,且速度越快的设备越贵。
缓存的本质依据就是将大概率数据缓存起来,因为这些大概率数据被认为是具有数据局部性原理的数据。
CPU比内存快100多倍,比硬盘快1000多万倍。如果CPU每次做事的时候,都等着内存和硬盘,那整个计算机的速度估计慢的要死。所以根据局部性原理,操作系统会把经常需要用的数据从硬盘取到内存,CPU 会把经常用的数据从内存取到自己的缓存中。通过这种办法等待的问题能带到极大的缓解。
在Web 开发中,缓存更是非常常见的,由于数据库(硬盘)太慢,大部分Web系统都会把最常用的业务数据放到内存中缓存起来。
二八定律:又叫帕累托定律是19世纪末20世纪初意大利经济学家巴莱多发现的。他认为,在任何一组东西中,最重要的只占其中一小部分,约20%,其余80%尽管是多数,却是次要的,因此又称二八定律。
将热数据也就是二八定律中的20%放入缓存。
【异步调用】我怕等不及
当程序需要等待一个长时间的操作而被阻塞,无所事事,异步调用就派上用场了。
异步调用是说:我等不及你了,先去做别的事情,你做完了告诉我一声。
回到最早的那个CPU的例子,CPU速度太快,当它想读取硬盘文件的时候,是不会等待慢1000多万倍的硬盘的,它会启动一个DMA , 不用通过CPU,直接把数据从硬盘读到内存,读完以后通过中断的方式来通知CPU。
【分而治之】大事化小,小事化了
分而治之的基本思想是将一个规模比较大的问题分解为多个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,最后组合起来就可得到原问题的解。
由于子问题和原问题性质相同,所以很多时候可以用递归。
归并排序就是一个经典的例子,数据结构与算法书上到处都是,这里就不在赘述了。
代码之道与术
道
“道”这里指的设计,是对问题本质的洞察,是良好的抽象。
对一个好的系统设计来说,我觉得有这么一个重要的特点:系统由几个基本的概念组成,这几个概念体现了系统的本质,形成了系统的骨架,他们非常地稳定,生命周期很长。
系统的其他代码围绕着这几个基本概念生长,变化,扩展,不断地演进。
举个例子, Linus 把Git的数据结构设计得非常精良,据说在之后十年的开发中,feature 扩展了无数,基础数据结构却很少变动。
我相信Linus在开发第一版Git的时候,肯定对它要解决的问题--分布式源码配置管理--有着深刻的洞察,实际上Linux内核的开发就是一个典型的、分布式的架构和流程, Linus要做的就是创建一个匹配这个流程的工具,基于对问题的深刻理解,他设计出了Git的系统骨架。
在面向对象编程的领域,这个道就是面向对象的设计原则,Robert Mafrtin在《敏捷软件开发原则、模式与实践》中总结了SOLID原则。
Singleresponsibility principle :单一职责原则
Open/closedprinciple :开闭原则
Liskovsubstitution principle :李氏替换原则
Interfacesegregation principle :接口隔离原则
Dependencyinversion principle :依赖反转原则
但是这些核心的、优美的设计很有可能在进度的压力下出问题:做个简单粗暴的修改,就能省好几天功夫,不用晚上加班了,很少人能经受这样的巨大诱惑。所以要坚决守住这些核心的设计,防止腐化。
术
术就是编写代码时的具体技巧和最佳实践。
这些技巧和实践在很多书中都有描述,比如《Effective Java》中提到的很多:
使可变性最小化
接口优于抽象类
优先考虑泛型
通过接口引用对象
遵守普遍接受的命名惯例
… …
再比如《编写可读代码的艺术》和《代码简洁之道中》总结的技巧:
避免if嵌套的层次过深,形成“金字塔”代码
局部变量申明尽可能靠近使用的地方
如有可能,尽可能用常量
减少控制流变量
拆分超长表达式
函数应该短小,只做一件事情
把过多的参数封装成类
... ...
把这些“术”应用到编程当中,会让代码更加的专业。
很明显,这些技巧和实践比较容易学习,程序员学会了一门语言以后,应该去掌握它们,让自己的代码变得简洁可读。
相对于“术”,“悟道”就难得多,洞察问题本质,做出良好抽象,遵循设计原则,这都是内功,都需要不断修炼。这些“道”直接决定了系统的未来方向,设计得不好,再多的“术”也于事无补。
性能,可用性,可伸缩性,可扩展性
架构核心要素:
1. 功能性需求
2. 非功能性需求
[1] 性能
[2] 可用性/可靠性/容错性/鲁棒性
[3] 可扩展性
[4] 可伸缩性
架构(系统的组成、结构),直接决定了系统的各项功能性需求、非功能性需求,而这些每项需求又包含了若干评判它们的性能指标。例如,性能:QPS、HPS、TPS等。
性能(Performance)
性能是一个网站处理用户请求的表现能力。性能的指标通常包括:响应时间,并发数,吞吐量,性能计数器等。
其中吞吐量和性能计数器比较难理解一些,
(1) 吞吐量指单位时间内,系统处理的请求数量。 TPS(每秒的事务数),HPS(每秒的HTTP请求数),QPS(每秒的查询数)等等。性能一般通过缓存来解决。
(2) 性能计数器,它描述的是服务器或者操作系统的一组指标,包括对象与线程数,内存使用,CPU使用,磁盘和网络的I/O等等。
提高网站的性能,很多的手段,比如,浏览器访问优化,CDN加速,反向代理,分布式缓存,使用集群,代码和数据结构的优化,存储性能的优化等。
可用性(Availability)
可用性是在某个考察时间,系统能够正常运行的概率或时间占有率期望值。考察时间为指定瞬间,则称瞬时可用性;考察时间为指定时段,则称时段可用性;考察时间为连续使用期间的任一时刻,则称固有可用性。它是衡量设备在投入使用后实际使用的效能,是设备或系统的可靠性、可维护性和维护支持性的综合特性。在大型网站应用系统中,衡量的指标一般是服务的可用性用几个9来表示。
高可用性一般通过负载均衡,数据备份,失效转移,提高软件质量,特别是发布时的质量来实现和保证的。
可伸缩性(Scalability)
可伸缩性,是一种对软件系统计算处理能力的设计指标,高可伸缩性代表一种弹性,在系统扩展成长过程中,软件能够保证旺盛的生命力,通过很少的改动甚至只是硬件设备的添置,就能实现整个系统处理能力的线性增长,实现高吞吐量和低延迟高性能。
1. 纵向的可伸缩性——在同一个逻辑单元内增加资源来提高处理能力。这样的例子包括在现有服务器上增加CPU,或者在现有的RAID/SAN存储中增加硬盘来提高存储量。
2. 横向的可伸缩性——增加更多逻辑单元的资源,并令它们像是一个单元一样工作。大多数集群方案、分布式文件系统、负载平衡都是在帮助你提高横向的可伸缩性
可伸缩性,一般通过负载均衡:DNS域名解析负载均衡,反向代理负载均衡,IP负载均衡,数据链路层负载均衡,改进和提高分布式缓存的算法,利用NOSQL数据库的可伸缩性等等。
可扩展性(Extensibility)
可扩展性,通常和可伸缩性混为一谈。在软件范畴上,是软件系统本身的属性,或者进一步说是设计的属性,代码的属性。因为我们经常说设计的可扩展性,代码的可扩展性。也可以说是系统设计的松耦合性。
实现方式:一般通过事件驱动架构和分布式架构来实现一个网站系统的可扩展性。