1 架构词汇
1.1 高可用
- 负载均衡(负载均衡算法)
- 反向代理
- 服务隔离
- 服务限流
- 服务降级(自动优雅降级)
大家都见过女生旅行吧,大号的旅行箱是必备物,平常走走近处绰绰有余,但一旦出个远门,再大的箱子都白搭了,怎么办呢?常见的情景就是把物品拿出来分分堆,比了又比,最后一些非必需品的就忍痛放下了,等到下次箱子够用了,再带上用一用。而服务降级,就是这么回事,整体资源快不够了,忍痛将某些服务先关掉,待渡过难关,再开启回来。
- 失效转移
- 超时重试(代理超时、容器超时、前端超时、中间件超时、数据库超时、NoSql超时)
- 回滚机制(上线回滚、数据库版本回滚、事务回滚)
- 服务熔断
在股票市场,熔断这个词大家都不陌生,是指当股指波幅达到某个点后,交易所为控制风险采取的暂停交易措施。相应的,服务熔断一般是指软件系统中,由于某些原因使得服务出现了过载现象,为防止造成整个系统故障,从而采用的一种保护措施,所以很多地方把熔断亦称为过载保护。
1.2 高并发
- 应用缓存
- HTTP缓存
- 多级缓存
- 分布式缓存
- 连接池
- 异步并发
1.3 分布式事务
- 二阶段提交(强一致)
- 三阶段提交(强一致)
- 消息中间件(最终一致性),推荐阿里的RocketMQ
1.4 队列
- 任务队列
- 消息队列
- 请求队列
1.5 扩容
- 单体垂直扩容
- 单体水平扩容
- 应用拆分
- 数据库拆分
- 数据库分库分表
- 数据异构
- 分布式任务
1.6 网络安全
- SQL注入
- XSS攻击
- CSRF攻击
- 拒绝服务(DoS,Denial of Service)攻击
2 架构必备工具
2.1 操作系统
- Linux(必备)、某软的
2.2 负载均衡
- DNS、F5、LVS、Nginx、OpenResty、HAproxy、负载均衡SLB(阿里云)
2.3 分布式框架
- Dubbo、Motan、Spring-Could
2.4 数据库中间件
- DRDS (阿里云)、Mycat、360 Atlas、Cobar (不维护了)
2.5 消息队列
- RabbitMQ、ZeroMQ、Redis、ActiveMQ、Kafka
2.6 注册中心
- Zookeeper、Redis
2.7 缓存
- Redis、Oscache、Memcache、Ehcache
2.8 集成部署
- Docker、Jenkins、Git、Maven
2.9 存储
- OSS、NFS、FastDFS、MogileFS
2.10 数据库
- MySql、Redis、MongoDB、PostgreSQL、Memcache、HBase
2.11 网络
- 专用网络VPC、弹性公网IP、CDN
3 网站架构演化发展历程
最初假设的网站中,我们把应用系统网站、文件和数据库都放在一台服务器上,一台服务器包打天下。
随着业务扩展,一台服务器无法满足性能需求,将应用程序、数据库、文件分别部署在不同的服务器上,并根据服务器用途不同,配置不同的硬件,达到性能最佳的效果。
随着业务扩展,一台数据库、网站、文件服务器再高性能也无法大量数据处理、高并发用户访问时,必须考虑采用集群方式。
- 应用服务器作为网站的入口,会承担大量的请求,我们往往通过应用服务器集群来分担请求数。应用服务器前面部署负载均衡服务器调度用户请求,根据分发策略将请求分发到多个应用服务器节点。常用的负载均衡技术硬件的有F5,价格比较贵,软件的有LVS、Nginx、HAProxy等。
- 随着用户量的增加,数据库成为最大的瓶颈,改善数据库性能常用的手段是进行读写分离以及分表,读写分离顾名思义就是将数据库分为读库和写库,通过主备功能实现数据同步。分库分表则分为水平切分和垂直切分,水平切换则是对一个数据库特大的表进行拆分,例如订单、物流信息表等。垂直切分则是根据业务不同来切换,如订单、计税等等不同的主题放在不同的数据库中。这种情况下,关联查询是没有的,通过程序可以比较容易的去解决,还有就是采用分布式事务,来保证数据的一致性。我们这里还有一个做法,一个大的数据表拆分为当前操作表和历史记录表, 当前操作表只保留正在操作的数据,完成后转入历史记录表,这样可以提高当前操作数据的效率。
- 用户一天天增加,业务量越来越大,产生的文件越来越多。通常情况下,一个目录下的文件建议不能超过1万个,否则对于文件的查找和轮询都会非常慢,会导致整个系统无法正常运行。我们一般是按照"\应用程序名\模块名称\日期"的目录结构组织的,对于文件数目仍旧很大的应用,应该再细分。当单台的文件服务器已经不能满足需求,就需要分布式的文件系统支撑。常用的分布式文件系统有NFS。我们用的是MS的分布式文件系统(DFS),与AD域相关性较大。
- 因为应用服务器是集群方式,用户前后两次请求可能访问的不是一台服务器。因此已经不能像以前一样使用状态(Application、Session、Cache、ViewState等),应用系统必须是无状态的(当然了,用的负载均衡具有会话保持的时候,一个用户只会定位到一台服务器)。系统的缓存应该保存在专门的缓存服务器上,如果必须有状态,也应该保存在专门的缓存服务器中。作为第一批吃螃蟹者,我们用了微软的AppFabric作为缓存服务器,因为当时版本很低,问题也不少,后来我们弃用了AppFabric,使用Redis作为缓存服务。现在,AppFabric已经改进了不少,运行在Azure云上,应该是不会存在以前的问题了。
随着业务进一步扩展,应用程序变得非常臃肿,这时我们需要将应用程序进行业务拆分,如我们做的综合业务管理系统分为门户、联系处置、业务信息、指标、数据查询分析等业务板块。每个业务板块是一个独立的应用负责相对独立的业务运作。业务板块之间通过消息队列进行通信来实现。数据库也进行相应的拆分,不同的主题放到不同的数据库中。同时,最好搭建静态资源服务器,将公用的css、js、images等都存放到静态资源服务器中。
对于海量数据的查询,我们使用nosql数据库加上搜索引擎可以达到更好的性能。并不是所有的数据都要放在关系型数据中。常用的NOSQL有mongodb和redis,搜索引擎有lucene,我们使用的Solr、ElasticSearch等基于Lucene内核实现的更易用的搜索引擎。数据量大的话,Solr等也要做成集群。
再往下走,系统需要与其他系统进行交互,系统也要给各种前端(例如网站、安卓、IOS)提供服务,这样我们就要在逻辑层之上建设应用服务层,提供对客户端的和对外的SOA服务接口。这样又涉及到DTO、WebService、WCF和WebApi(Rest)等概念。但是最重要的是,SOA方式下,包括前面的MQ方式下,事务一致性无法得到保障的,必须采用一定的机制例如事务补偿机制来确保事务的最终一致性。各个业务板块所在的服务器,在不同时段的压力也不同,为了尽量做到服务器集群内各服务器的压力平摊, 还需要提供更好的机制,记录下每个服务器的压力、资源情况、连接数等等,以便将新的请求转向到压力最小的服务器上。
业务继续发展,就是CDN,再往下就是搭建几个中心,将系统部署在各个中心,各地用户访问距离他最近的中心,中心间数据保持同步。
上面讲了应用系统方面比较多,数据方面也要做许多工作。上面已经介绍了分库分表方式。应用系统做大了,势必有许多的数据资源,尤其是现在大数据这个名词非常火爆的情况下,数据分析和处理是一个系统必须要做的事情。这样做的好处是,将数据的查询、分析等独立出来,不影响正式运行中的系统,另外是通过分析挖掘确实能得到许多意想不到的价值。
这时,主要的工作是搭建数据仓库,然后进行后续的分析和处理。使用ETL/ELT将数据定期从正式环境中导入到数据仓库中,按照不同的主题搭建一个个的数据集市。对于数据量比较小的系统,可以使用关系数据库+多维数据库的方式;对于大型系统,就要使用按列存储、并行数据库等方式了。对于数据的分析可以以报表、KPI、仪表盘驾驶舱等方式提供上层领导决策,也可以使用数据挖掘、机器学习和训练等方式实现价值发现、风险控制等。
一般情况下,企业是没有那么大的财力和人员去做上述内容的,因此使用云成为企业的一个选择。无论是Azure、阿里云、亚马逊等都会提供一个个的服务。我们就以阿里云为例,ECS提供虚拟服务器、SLB提供负载均衡、RDS提供数据库服务、OSS提供存储服务、DRDS是分布式数据服务、ODSP(现在改名叫MaxCompute)提供大数据的计算服务、RocketMQ提供MQ、OCS提供分布式缓存服务、以及CDN、OTS、ADS等等就不一一列举了。
对了,现在还有Docker这个利器,无论在企业还是云中都可以使用,我们在自己内部使用的Redis、Memcached、RabbitMQ、Solr等都部署在Docker中,确实比较方便。
衡量分布式系统的指标
评价分布式系统有一些常用的指标。依据设计需求的不同,分布式系统对于这些指标也有不同的要求。
性能
无论是分布式系统还是单机系统,都会对性能(performance)有所要求。对于不同的系统,不同的服务,关注的性能不尽相同、甚至相互矛盾。常见的性能指标有:
- 系统的吞吐能力,指系统在某一时间可以处理的数据总量,通常可以用系统每秒处理的总的数据量来衡量;
- 系统的响应延迟,指系统完成某一功能需要使用的时间;
- 系统的并发能力,指系统可以同时完成某一功能的能力,通常也用 QPS(query per second)来衡量。
上述三个性能指标往往会相互制约,追求高吞吐的系统,往往很难做到低延迟;系统平均响应时间较长时,也很难提高 QPS。
可用性
系统的可用性(availability)指系统在面对各种异常时可以正确提供服务的能力。系统的可用性可以用系统停服务的时间与正常服务的时间的比例来衡量,也可以用某功能的失败次数与成功次数的比例来衡量。可用性是分布式的重要指标,衡量了系统的鲁棒性,是系统容错能力的体现。
可扩展性
系统的可扩展性(scalability)指分布式系统通过扩展集群机器规模提高系统性能(吞吐、延迟、并发)、存储容量、计算能力的特性。可扩展性是分布式系统的特有性质。分布式系统的设计初衷就是利用集群多机的能力处理单机无法解决的问题。然而,完成某一具体任务的所需要的机器数目即集群规模取决于系统的性能和任务的要求。当任务的需求随着具体业务不断提高时,除了升级系统的性能,另一个做法就是通过增加机器的方式扩展系统的规模。好的分布式系统总在追求“线性扩展性”,也就是使得系统的某一指标可以随着集群中的机器数量线性增长。
一致性
分布式系统为了提高可用性,总是不可避免的使用副本的机制,从而引发副本一致性的问题。根据具体的业务需求的不同,分布式系统总是提供某种一致性模型,并基于此模型提供具体的服务。越是强的一致的性模型,对于用户使用来说使用起来越简单。例如通常我们总是希望某次更新后可以立刻读到最新的修改,如果成功更新后的数据依旧有可能不一致读到旧数据,那么用户就需要在写入数据时加入序列号等信息,并在读取数据时首先自行实现过滤去重后再使用数据。
数据分布方式
所谓分布式系统顾名思义就是利用多台计算机协同解决单台计算机所不能解决的计算、存储等问题。单机系统与分布式系统的最大的区别在于问题的规模,即计算、存储的数据量的区别。将一个单机问题使用分布式解决,首先要解决的就是如何将问题拆解为可以使用多机分布式解决,使得分布式系统中的每台机器负责原问题的一个子集。由于无论是计算还是存储,其问题输入对象都是数据,所以如何拆解分布式系统的输入数据成为分布式系统的基本问题,本文称这样的数据拆解为数据分布方式,在本节中介绍几种常见的数据分布方式,最后对这几种方式加以对比讨论。
哈希方式
哈希方式是最常见的数据分布方式,其方法是按照数据的某一特征计算哈希值,并将哈希值与机器中的机器建立映射关系,从而将不同哈希值的数据分布到不同的机器上。所谓数据特征可以是 key-value 系统中的 key,也可以是其他与应用业务逻辑相关的值。例如,一种常见的哈希方式是按数据属于的用户 id 计算哈希值,集群中的服务器按 0 到机器数减 1 编号,哈希值除以服务器的个数,结果的余数作为处理该数据的服务器编号。工程中,往往需要考虑服务器的副本冗余,将每数台服务器组成一组,用哈希值除以总的组数,其余数为服务器组的编号。
可以将哈希方式想象为一个大的哈希表,每台(组)机器就是一个哈希表中的桶,数据根据哈希值被分布到各个桶上面。
只要哈希函数的散列特性较好,哈希方式可以较为均匀的将数据分布到集群中去。哈希方式需要记录的元信息也非常简单,任何时候,任何节点只需要知道哈希函数的计算方式及模的服务器总数就可以计算出处理具体数据的机器是哪台。
哈希分布数据的缺点同样明显,突出表现为可扩展性不高,一旦集群规模需要扩展,则几乎所有的数据需要被迁移并重新分布。工程中,扩展哈希分布数据的系统时,往往使得集群规模成倍扩展,按照数据重新计算哈希,这样原本一台机器上的数据只需迁移一半到另一台对应的机器上即可完成扩展。
针对哈希方式扩展性差的问题,一种思路是不再简单的将哈希值与机器做除法取模映射,而是将对应关系作为元数据由专门的元数据服务器管理。访问数据时,首先计算哈希值并查询元数据服务器,获得该哈希值对应的机器。同时,哈希值取模个数往往大于机器个数,这样同一台机器上需要负责多个哈希取模的余数。在集群扩容时,将部分余数分配到新加入的机器并迁移对应的数据到新机器上,从而使得扩容不再依赖于机器数量的成本增长。
哈希分布数据的另一个缺点是,一旦某数据特征值的数据严重不均,容易出现“数据倾斜”(data skew)问题。例如,某系统中以用户 id 做哈希分数据,当某个用户 id 的数据量异常庞大时,该用户的数据始终由某一台服务器处理,假如该用户的数据量超过了单台服务器处理能力的上限,则该用户的数据不能被处理。更为严重的是,无论如何扩展集群规模,该用户的数据始终只能由某一台服务器处理,都无法解决这个问题。
在这种情况下只能重新选择需要哈希的数据特征,例如选择用户 id 与另一个数据维度的组合作为哈希函数的输入,如这样做,则需要完全重新分布数据,在工程实践中可操作性不高。另一种极端的思路是,使用数据的全部而不是某些维度的特征计算哈希,这样数据将被完全打散在集群中。然而实践中有时并不这样做,这是因为这样做使得每个数据之间的关联性完全消失,例如上述例子中一旦需要处理某种指定用户 id 的数据,则需要所有的机器参与计算,因为一个用户 id 的数据可能分布到任何一台机器上。如果系统处理的每条数据之间没有任何逻辑上的联系,例如一个给定关键词的查询系统,每个关键词之间并没有逻辑上的联系,则可以使用全部数据做哈希的方式解决数据倾斜问题。