来源《程序员》| 作者程序员杂志
多终端接入、开放平台给互联网带来了前所未有的用户量级和访问规模,SNS网站产生了海量的UGC(用户产生内容),而且这些内容依托关 系链扩散速度之快、传播范围之广是传统网站难以想象的,海量数据的计算存储也一直是近年互联网领域的热点。本文将从发展演进的层面探讨互联网的系统架构。
天下武功唯快不破
网站初期的架构一般采用“短平快”的架构思路,架构以简单清晰、容易开发为第一衡量指标。
互联网架构选型首先包括开发语言的选择,目前PHP、Java是主力语言。开发语言的选择一般从团队人员的知识储备、社区活跃度、商业应用的成熟度、招聘人才的人力成本等方面考量。
选择语言之后,一般会选择该语言的流行框架辅助研发,例如Java的SSH、Python的Django等。但这些框架并不是通常意义上的架构,架构一般可 分为物理架构、运行架构、逻辑架构、开发架构、数据架构等多个维度,框架往往只是代码架构的一部分。代码架构是指代码的组织形式、规范、设计模式等,框架 其实是常用设计模式的软件化。例如Struts是MVC模式的实现,Hibernate、iBATIS是ORM模式的实现。
在架构视图中,早期关注的主要是开发视图和数据视图,一般数据存储采用DB,初期数据的关注点主要是安全和备份,MySQL的Master-Slave模式可以满足该需求。
鸡蛋不要放在一个篮子里
采用“短平快”三板斧将网站开发出来之后,急需解决的是网站的可用性问题。可用性最基本的要求是不能有单点,对程序节点而言,前端可采用LVS、HAProxy、Nginx等负载均衡/反向代理设备。
DB的可用性就复杂了很多,数据库天然是有状态的,状态就是其中的数据,新增一个数据节点一般伴随着大量的数据复制和迁移。对金融行业而言,昂贵的商用存储是 解决之道,“IBM+Oracle+EMC”是该类系统的标配。互联网企业则一般采用较为廉价的方案,例如开源的DRDB+Heartbeat技术组合可 以在MySQL主库宕机时实现备机接管,接管时间可以控制在30秒内。
程序节点其实也可能存在状态,例如Web服务中常用的Session 就是保存在容器中的状态,这种状态保持要求所有相同用户的请求都在同一台机器上处理,无状态的程序节点才能水平扩展。无状态一般有两种设计思路,还以 Session为例,一种思路是把用户的状态保存在客户端Cookie,每次请求都把客户端的用户信息带到服务器端,淘宝的分布式Session就是该思 路的一种实现;另一种思路是状态保留在另外一个服务中,例如有些公司将Session放在分布式缓存中。
性能是生命线
去除单点之后的系统就可以水平扩展,架构如图1所示。但随着网站的推广运营,系统的规模开始扩大,此时可能会出现服务访问缓慢,甚至不可用的状况,如何提升系统性能就成了架构师的当务之急。
图1 去除单点之后进行水平扩展
存储架构和性能
互联网系统所有的性能瓶颈中,数据存储和访问速度往往是最重要也是最难解决的,选择合适的存储是系统的关键。存储的选择一般需要从多个方面考量,如成本、内容、用途和模型。目前主流的存储介质包括硬盘和内存两种。
对机械硬盘来说,1秒可以完成150次左右的随机I/O。而结合设计优良的Hash算法,内存查找可以每秒执行40万次左右。硬盘的随机读写能力决定了其读 写的最差性能,但操作系统在实现文件系统时会把最近读写过的数据缓存在内存中。由于磁盘访问和内存访问性能量级的差距,从操作系统的Cache命中率就可 以简单计算文件存储的性能,如果内存命中率可以达到80%,系统的I/O能力相较完全随机I/O将有5倍提升。
对于数据层服务器,大内存已成为标配(一般为100GB左右),如果DB中存储200GB的数据,根据8/2原则,Cache命中率应为87.5%,因此对MySQL而言,一般读写可以达到每秒1千次以上。
对于读写频率都很高、且可容忍数据丢失的场景,可以采用内存作为数据存储的介质。可靠的内存存储需要每次操作都记录Biglog,即使数据丢失也可以恢复,同时内存中的数据一般定期持久化到硬盘。
从功能角度考量,还可以分为持久化存储和Cache。持久化存储也可称为可靠存储,Cache是为了提升系统性能,在可靠存储的基础上建立的访问性能更加高效的数据读取节点,通常是内存存储,其架构一般如图2所示。
图2 持久化存储和Cache
存储的数据模型一般分为结构化存储和NoSQL存储。结构化存储以各种传统DB为代表,NoSQL技术的代表系统则有HBase、Memcached、 Redis等。各种NoSQL系统虽然特性各异,但相对传统DB而言,由于结构化信息的缺失,往往不能做各种关联查询,适用场景更多是主键查询,而且一般 是写少读多的系统。
对于大型互联网公司,为了某些场景下的性能优化,也会定制个性化的文件系统,例如为了适应大文件存储的场景,Google开发了GFS;为了更快读取海量商品的描述图片,TFS在阿里诞生。
虽然各类存储快速涌现,但DB作为结构化数据的传统存储设备,依然在架构中处于非常重要的地位。由于随机I/O的瓶颈,DB的性能天花板十分明显。在大型系 统中通常需要分库操作,分库一般有两个维度——水平切分和垂直切分。水平切分一般根据主键规则或某种规则将同类数据切分到不同的单元表中,原则是数据切分均匀,尤其是热点数据分布均匀。
垂直切分是把大表中的字段拆分到多张表。垂直切分一般按照数据访问频率的不同。逻辑关系的差别进行切分,例如将大字段、kv字段、计数等高频访问字段单独剥离存储都是常见的垂直切分方案。
除了切库之外, MySQL的分表也会有效减少单表大小,使数据变得更简单,甚至可以做到不下线变更,单表索引规模的下降也会带来性能的提升。
分库分表作为DB架构中重要的一环,使DB更加稳健,但它给业务代码带来了额外的复杂性,最好通过中间件来屏蔽DB的底层分布,对业务透明。
作为高性能网站必不可少的组件,Cache在各种主流架构中也起着重要的作用。
从部署模式上看,它可分为本地Cache和分布式Cache。本地Cache是指在应用进程中的Cache,通常的数据结构是一个MAP,其优点是结构简 单,效率较分布式Cache更高,缺点是一般应用程序服务器的内存有限,导致本地Cache容量受到局限,而且数据冗余度较高,每个应用服务器都需要一份 数据,更新比较烦琐,一般采用超时删除机制。
分布式Cache的容量较大,方便扩容和更新,其数据分布可采用一致性Hash算法,减少节点变化带来的数据迁移。
引入Cache不可避免的问题是服务器的宕机处理。Cache通常是一个集群,数据分布在多个节点,如果挂掉一个节点,只会影响部分数据,而且对于可靠性要求较高的系统,每个节点都可以有备份。
作为可靠存储的数据备份,Cache在架构设计上往往承担大部分读访问需求,其命中率尤为重要。Cache不命中有两种情况,一是数据在Cache中不存 在,二是在持久化存储中也不存在。对于后者的频繁访问会导致请求直接压在DB上,在设计时应尽量避免,可以通过维护Bitmap对持久化存储中没有的数据 进行拦截,直接返回,也可以简单地将这些数据对应空对象放进Cache。
Cache的存储一般是将索引和数据分离,对于索引数据可以全量缓存,对于体量较大的数据一般采用部分缓存的方式。
Cache的使用场景有一定的局限,对于较为静态的数据才有意义,这个临界值一般是5分钟。由于当前存储技术的进步,Cache也可以用其他高性能的存储介质代替,例如SSD的引入使得硬盘的随机读写能力提升数十倍,也会使得Cache的重要性有所下降。
程序架构和性能
对一般的系统而言,程序逻辑的主要作用是调用各种数据访问接口,该操作通常需要等待,所以除搜索等少数系统外,程序逻辑一般是非CPU密集型。该类系统中“线程”是稀缺资源,线程数和接口耗时构成了系统的QPS能力。
大型互联网系统的QPS可能为几万甚至峰值达到几十万,此时增加机器可以解决问题,但这些机器的利用率其实很低,因为大部分时间是在等待,此时引入异步变得非常重要,异步在同样的时间可以处理更多工作,拥有更好的性能。
图3 同步调用和异步调用
利用Nio的多路复用方式可方便地实现异步系统,当然也可用协程令代码更加清晰。业界流行的SEDA技术可将一次请求拆分为粒度更细的Actor,每个 Actor使用独立队列,前一个的输出是后一个的输入。SEDA通过该方式将请求中等待和非等待的环节分离,提升了系统的吞吐量,这种方式在小米等互联网 公司有较多应用。
除了异步之外,并行对系统也很重要,它可以有效缩短请求的响应时间。批量接口也可以有效减少系统调用次数,使得系统线程消耗更少,从而提升系统吞吐量。
对线程而言,还有一个重要的参数是超时时间。响应快的服务,超时时间可以长一些,对于响应慢的服务,超时时间可以短一些,尽快失败是保护自己的有效手段。
网络架构和性能
大型网站的网络接入一般是“DNS+负载均衡层+CDN”这种模式。对于大型互联网公司,往往有多个IDC提供对外服务,中国互联网的南北不互通使得解决不 同地域不同运营商的接入速度问题成了难题,该问题的解决一般需要公司自己开发DNS服务器,结合IP测速平台,引流用户请求到访问速度最快的节点。
大系统小做
业务逻辑复杂多变,如何保证程序逻辑的代码稳定是架构师需要解决的问题,良好的模块划分和扩展性强的接口设计都是解决这个问题的利器。
模块是和领域模型相关的一个概念,其往往指系统中高内聚的一个数据访问单元。例如对电商系统而言,最大的两个领域模型分别是商品信息和交易信息,每个领域模 型对应一系列数据,商品会有商品的基本信息、类目信息等,交易会包括交易的订单,这些“领域模型+数据+业务方法”就构成了一个个的模块,高度内聚的模块 是数据的访问的入口。例如交易时也需要去获取商品信息,但一般不会被允许直接调用商品模块的数据表,而是通过商品模块提供的接口进行访问,这样做有下面一 些优点。
对较小规模的应用,模块可部署在一起,但对大型系统而言,模块一般是单独部署,通过RPC交互,这样可以减少彼此之间的系统层面影响。例如Amazon就倾向将所有服务器程序暴露为接口。
分布式系统增加了系统交互的复杂性,也为系统引入了更多潜在的失败环节,但分布式系统可以带来的好处更明显。
逻辑层的接口设计如何做到稳定呢?首先接口的设计不应该是产品驱动的,而应该由数据驱动,尽量考虑接口以后的发展,接口的参数尽量是对象,参数不要采用boolean这种难以扩展的数据类型。
逻辑层的接口设计,一般有粗粒度和细粒度两种模式。粗粒度接口的优点是交互少,一次调用基本可以满足需求,缺点是业务逻辑较多,所以不稳定性增加,这里的不稳定性不仅是系统稳定性,还包括业务逻辑的稳定性。细粒度接口交互较多,但更加有利于重用性。
细粒度接口多次交互是否会带来明显的性能损耗呢?目前服务器1秒可以执行近万次TCP连接,网络传输对于千兆网卡而言,一般也不会造成瓶颈,尤其RPC框架般都会保持长连接。因此,通常情况下我们可以忽略RPC调用带来的性能损耗。
逻辑层接口尽量采用大系统小做的原则,该原则是腾讯架构中重要的价值观。一个接口不做太多事情,只做关键路径,非关键逻辑可以用消息队列或事件通知等方式剥离出来,使得关键路径更加稳定。这也体现了服务分级的思想,把最大的精力花在最重要的接口上。
除了按重要性划分服务之外,也可以按接入方划分,避免不同终端的Bug影响。还可以按快慢划分,例如为上传文件等功能提供单独的服务,避免长时间占用线程,影响系统稳定性。
模块化是程序的水平切分,有时也会采用垂直切分,这种架构有以下好处。
开放势不可挡
系统发展往往会带来平台化需求,例如微博的大多数服务除了满足PC访问,还要满足移动端及内外部平台,此时系统通常会抽象出一个接入层。
接入层设计
接入层一般分为:通信、协议和路由模块。
常用的通信方式有TCP、UDP和HTTP等,对开放平台等以外围接入为主的系统而言,HTTP因其简单方便是最合适的通信方式,而内部系统接入出于性能考量,可以直接用TCP或者UDP。
目前的主流协议有JSON、XML、Hessian等,对外部调用一般采用XML或者JSON,内部系统可以采用Hessian或其他自定义的二进制协议。
路由层是根据用户的信息将请求转发到后端服务层。LVS可看作是路由层,根据IP协议将不同来源的请求转发到不同IP Server,Nginx等具备反向代理的服务器也可以看作路由,根据不同URL转发到不同后端。我们经常会自定义路由层,通过用户调用的不同方法转发到 不同Server,或者根据用户的特征,将用户路由到我们的灰度测试机。
云平台概念
具有了平台化的接入功能,系统可以方便地接入内部或者外部系统,此时就具有了“云”的特征,对各种公有云或私有云来说,系统的隔离和自动扩容都十分重要,虚拟化等技术在该类系统中有了充分应用,例如阿里云等云平台都大量使用了虚拟化。
稳定压倒一切
代码稳定性
稳定性是系统架构中一以贯之的内容,可以从图4中理解它的含义。
正常情况下,图4左边系统的性能明显优于右边,但从架构角度考虑,右图要好于左图,因为突起的毛刺使得系统的容量骤降,很容易引发雪崩。性能考量不仅是系统的最优性能或者平均性能,最差性能往往也是系统出现问题的原因。
图4 两种稳定性对比
容灾
除了特别小型的系统,没有100%可用的系统。一般需要根据系统的情况制定合适的目标,该目标最通用的衡量维度是系统可用率。
系统可用率是可以提供服务的时间与总时间的比率,常用的系统可用率如表1所示。
而对于灾难,我们有下面几个环节可以介入。
系统容量的冗余和可水平扩展也是容灾的必备要求,无状态的系统对于系统扩容更友好。
作者杨光辉,淘宝北京研发中心技术专家,花名三玄。曾在互动百科、腾讯科技等公司任职,对服务器端架构和开发等有一定研究。
本文为《程序员》原创文章,未经允许不得转载,如需转载请联系market#csdn.net(#换成@)