企业互联网架构原理与实践
上QQ阅读APP看书,第一时间看更新

2.2 互联网架构的典型模式

2.2.1 分层

分层就是AKF扩展立方体中的水平扩展,将系统按水平方向分为多个层次,每层在应用中有专门的角色,各层之间相对独立,以更松散的方式协同发挥作用。

分层架构可以应用于一个子系统内部,也可以应用于子系统之间。例如在一个子系统内部,将一个应用分为MVC结构,即模型、视图和控制器。在一个互联网架构中可以分为前端展现层、网关接入层、服务处理层、数据缓冲层、数据持久层等。

分层要求功能内聚、逻辑清楚、边界清晰,理想的层次是可替代、可插拔的。各层之间也是有序的,需要按约定的层次顺序传递信息,避免跨层次调用以及反向调用。

分层降低了各层的依赖关系,功能更加内聚,应用更加标准化,有利于功能复用,也有利于各层独立维护、独立扩展。系统规模越大优势越明显。但从某一个具体调用过程看,分层结构增加了服务调用消耗,降低了性能。同时分层导致了级联修改,增加了系统复杂度,增加了开发维护成本。

分层思想尤其适用于对软件整体结构的梳理,通过水平拆分可以构建出子系统的层次关系,形成系统整体架构。一个较完整的大型企业互联网架构总体蓝图如图2-1所示。

图2-1 分布式系统分层模式实例

■ 企业业务架构的基础是搭建在企业数据中心或公有云基础上的企业IT基础设施,提供IaaS服务。基于历史IT资产投资利用以及数据保密的需要,大型企业仍然侧重于建设企业专有云,同时在部分领域使用公有云的能力,是一个混合云的架构。而小微企业倾向于全部使用公有云。

■ 第二层是容器及基于容器的资源调度平台。2014年1月Docker 1.0版本正式发布,2014年秋亚马逊正式推出了弹性容器服务。Docker生态发展迅猛,目前已经成为企业架构的标准部件与核心基础。

■ 第三层是企业基础中间件服务层,通过整合成熟的中间件,并对部分中间件容器化,为企业提供基础中间件服务能力。

■ 第四层是企业服务平台,利用成熟的开源框架,搭建提供面向服务的系统架构,是企业交易的核心。

■ 第五层是企业自主搭建的技术体系,包括企业自研基础技术体系、企业自有DevOps平台、企业自有大数据服务平台,它们与第三层和第四层共同组成了企业的PaaS服务平台。

■ 第六层是企业将自研的典型业务系统封装,提供标准服务的SaaS平台。

■ 最顶层是企业定制化的业务系统。

2.2.2 分割

分割就是垂直扩展,是按照业务方向将一个系统进行功能划分。分层更多关注的是技术层面,而分割更多关注的是业务层面。如一个零售系统可以分为用户服务、支付服务、订单服务、商品服务等。

分割服务时,需要根据业务的复杂程度选择分割的粒度。比如大型电商平台用户查看商品信息的频次与其他业务相比调用频率更高,此时可以将商品服务继续细化,拆分为商品搜索服务、商品详情服务。分割后,分析每个服务各自的业务场景,总结各自的技术进化路线。系统的切割不见得是一蹴而就、一次到底的,需要根据业务情况,逐渐进化。

通过将系统按功能分割,有利于分工协作,降低开发和部署过程中的冲突、合并等干扰,优化团队效率,也有利于隔离不同业务间的影响,按分割后的服务单独定制优化路线。但分割也带来了系统的复杂性,需要对调用关系进行治理,需要引入Microservice、Servicemesh等服务治理框架。

在总体分层架构的基础上应用分割的办法,可以形成企业的总体架构图,图2-2是一个零售行业的整体架构图,展示了分割模式。

图2-2 分布式系统分割模式实例

■ 在基础中间件部分,搭建Redis集群、MySQL集群等中间件高可用架构。

■ 在企业服务平台部分,搭建微服务或服务网格的核心架构。

■ 在基础技术平台部分,搭建单点登录、计划任务调度、监控系统等系统,在DevOps部分搭建Jenkins、自动化测试、代码分析、bug管理等系统,在大数据部分搭建ES、用户画像、推荐系统、风控系统等系统。

■ 在基础业务服务部分提供会员、支付、验证码、营销、订单、评论等基础业务服务。

2.2.3 分片

分片对应于扩展立方体的Z轴扩展,是将数据分配到不同的节点上进行分片存储和计算。业务处理可以通过分层和分割解决扩展问题,传统关系型数据库就变成了下一个瓶颈。解决方法为提升硬件处理能力,或采用传统的关系型数据库提供的集群处理方案,如MySQL cluster,Oracle Rac等。这些集群处理方案都存在局限,随节点数增加,总体性能提高,但单机性能下降,无法支持无限扩展,面对互联网海量数据依然力有不逮。

为了解决传统关系型数据库的问题,产生了NoSQL数据库,包括键值(key-value)存储数据库,典型代表如Redis;列存储数据库,典型代表如HBase;文档型数据库,典型代表如MongoDB;倒排索引数据库,典型代表如ES;图结构数据库,典型代表如Neo4j等。NoSQL数据库是为大数据而生,结构上天然支持数据分片扩展,如Redis cluster的slot,ES的shard index都是典型的分片结构。

NoSQL数据库是关系型数据库的有效补充,但满足ACID的关系型数据库依然是系统架构中必不可少的,为了解决关系型数据库的扩展问题,可以采用分库分表的办法。分库是指将原本在一个数据库中的数据,重新按业务归类分布到不同的数据库中,从而将压力分散至不同的数据库。比如将单一的零售系统的数据库,拆分为用户库、商品库、订单库等。分表是指将原本在一个数据表中的数据,重新归类分布到不同数据表中。分表无法缓解数据库压力,但有利于突破单表的数据存储和查询性能瓶颈,如将用户数据表按用户ID拆分成N张表,可以解决海量用户的查询问题。采用分表不分库的办法,可以避免分布式事务。

对数据库分库分表的同时配合使用多主多从的分片方式,可以有效避免数据单点,提升数据架构的可用性。分库分表一般需要配合系统分割,与服务的划分相对应。

分库分表提升了整体数据处理能力,但也带来了数据路由、数据聚合、分布式事务等问题。一般需要采用ShardingSphere、mycat等中间件屏蔽数据分片带来的复杂性。

在典型的应用场景中,对数据ACID要求高的继续使用关系型数据库,并进行分库分表处理,同时结合NoSQL数据库,发挥NoSQL数据库的特长。例如,针对海量数据,可以先在MySQL上进行分库/分表,然后通过ShardingSphere分库分表中间件完成数据CRUD管理,最后将数据同步到Redis和HBase中,在Redis中进行数据热点缓存,在HBase中进行大表存储,当查询时可以使用Redis或HBase进行查询,避免对关系型数据库分片数据的Join查询。

针对海量图片和视频文件的存储处理,一般可以使用Fastdfs等分布式文件存储系统或OSS分布式对象存储系统来进行管理。

2.2.4 缓存

缓存是将数据放在距离使用者最近的位置的一种方式,通过缓存可以减少系统交互,降低系统I/O。缓存的主要方式包括CDN缓存、前端缓存(浏览器缓存、App缓存)、反向代理缓存、应用端缓存、分布式缓存。

(1)CDN缓存,CDN(内容分发网络,Content Delivery Network)是由分布在不同地理区域的节点组成的分布式网络,可以将源站内容分发至最接近用户的节点,当分布在各地的用户请求访问和获取网站内容时,无需访问主站,系统自动调用离终端用户最近的CDN节点上已缓存的资源。CDN可以分担源站压力,避免网络拥塞,提高资源访问速度和成功率。

(2)前端缓存,前端缓存是指在用户侧的缓存策略,包括用户侧的浏览器缓存和App客户端缓存等。浏览器缓存,在浏览器首次向服务器发起该请求后,会根据响应报文中HTTP头中设置的缓存策略,决定是否缓存应答结果,是则将应答结果和缓存策略存入浏览器缓存中,后续的请求会根据缓存策略判断是否将请求发送到服务器端。App客户端缓存,是指利用移动设备的内存、文件系统或嵌入式数据库等方式缓存数据。

(3)反向代理缓存,反向代理服务器部署在网站应用的最前端,在2.2.7节中描述了反向代理静态资源缓存的办法。

(4)应用缓存,是指将数据存储在应用系统本地线程堆栈中,Java语言可存储在Map、Set、List等集合类型数据结构中,也可以使用Guava cache或者EHcache等第三方提供的类库。

(5)分布式缓存,是指在业务系统进程外单独部署的高可用、可扩展的缓存系统,如Memcached、Redis等。

2.2.5 并行

串行是指多个任务依次执行,同一时间内只有一个任务在执行。并行是指在系统中同时执行两个或多个任务。图2-3是一个下单操作的串行处理流程和并行处理流程。与串行相比,并行能充分利用硬件处理能力,大幅提升效率。

并行主要应用于数据处理与任务处理。

(1)并行数据处理:并行往往与数据分片结合,通过多个进程或线程对不同的分片数据进行并发处理。MapReduce就是一种针对数据分片的并行处理架构。

(2)并行任务执行:并行还可应用于同时执行多个无依赖关系的服务调用,先并发执行再统一处理结果。区分串行与并行场景,灵活应用并发模式可以大幅提升系统处理能力。

图2-3 串行与并行

2.2.6 异步

同步是指当发出一个功能调用时,在没有得到结果之前,阻塞线程,该调用不返回。异步的概念和同步相对,当一个过程调用发出后,调用者不能立刻得到结果,可以继续处理其他工作,实际处理调用的部件在处理完成后,通过状态、通知和回调来通知调用者。

异步调用极大地提高了系统的吞吐能力。例如,在浏览器端,同步调用在得到返回结果前,浏览器不能做其他任何事情,而使用AJAX异步请求则实现了浏览器的局部刷新,极大地提升了用户体验。

(1)异步线程:Java中通过CompletableFuture实现了对异步调用的编排,通过thenAccept、thenApply、thenCompose等方式可以将前面异步处理的结果交给另外一个事件处理线程来异步处理。一般情况下,在一个成熟的体系架构中已经封装了请求调用的模型,如Dubbo中提供了消费者和服务者的异步调用方法。应该尽量使用框架中的异步模型,避免另起炉灶。

(2)消息队列:消息的生产者将消息发送到消息队列以后,由消息的消费者从消息队列中获取消息,然后进行业务逻辑的处理,消息的生产者和消费者是异步处理的,彼此不会等待阻塞。消息队列可以使消息生产者和消费者解耦,隔离失败,使消费者具有更好的伸缩性。消息队列还可以起到削峰填谷的作用,将请求的信息纳入到消息队列中,缓存消费者的处理速度。常用的消息队列包括:RabbitMQ、ActiveMQ、RocketMQ、Kafka等。

(3)Servlet 3.0:Servlet在3.0之前,采用Thread-Per-Request的方式处理请求,每一次HTTP请求由一个线程负责处理,而处理线程往往要执行访问数据库等操作,处理操作是非常慢的,线程并不能及时地释放回线程池以供后续使用,吞吐受到极大限制。在Servlet 3.0以后引入了异步处理,在HttpServletRequest对象中可以获得一个AsyncContext对象,该对象构成了异步处理的上下文,Request和Response对象都可从中获取。AsyncContext可以从当前线程传给另外的线程,并在新的线程中完成对请求的处理并返回结果给客户端,初始线程可以还给容器线程池以处理更多的请求。

2.2.7 隔离

隔离是指将不同的资源加以区分进行隔离控制。通过资源隔离可以限定问题传播范围,避免雪崩效应,同时隔离可以为不同场景的资源划分泳道,避免互相堵塞,互相影响。隔离的应用方式很多,下面列出常见的隔离方式。

(1)服务隔离,在面向服务的架构体系中,对服务进行隔离控制,避免一个服务出现问题引起系统雪崩,导致系统被流量冲垮,保障应用高可用性。主要措施包括限流、熔断、降级。限流:监控应用流量的QPS或并发线程数等指标,当达到指定阈值时对流量进行控制。熔断:在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,防止请求发生堆积。降级:在服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略地不处理或换种简单的方式处理,从而释放服务器资源以保证核心事务正常运作或高效运作。如库存系统宕机时,为保证售卖,可以暂时使库存调用返回有库存。

(2)线程隔离,是指在同一进程内根据业务情况,分别设置不同的线程池进行隔离,每个线程池单独配置,如图2-4所示。如Tomcat、Dubbo都可以根据业务场景完成线程隔离。

图2-4 线程隔离

(3)进程隔离,是指在将单个系统拆分为多个子系统后,对子系统以及子系统依赖的关联资源同时进行隔离。如在电商秒杀场景中,因为秒杀的业务抖动较大,为保护其他业务,需要将秒杀场景相关的系统进程、缓存、DB等资源独立。又如AB测试场景,需要隔离出两套环境,用不同的流量进行测试。

(4)动静分离,静态页面是指网站页面中几乎不变的页面(或者变化频率很低),例如静态html页面、js/css等样式文件,只需要CDN、Nginx、Squid/Varnish等中间件就可以处理。动态页面是指基于不同用户不同场景访问,服务端动态渲染生成的不一样的页面,例如JSP、PHP等文件,需要部署到Tomcat、Weblogic等容器上。

静态页面只需要在服务器上存储,无需再进行计算处理,访问路径短,访问速度快,并发能力强。而动态页面访问路径长,访问速度相对较慢(数据库的访问,网络传输,业务逻辑计算),扩展不易。因此可以将两者不同特征的页面分开部署,如图2-5所示,实现动静分离。将静态页面与动态页面以不同域名区分,同时将静态文件在架构上尽量前置部署,例如将静态资源部署到CDN、Nginx上,充分利用服务器距离用户物理位置近、反向代理中间件并发处理能力强的特点,提升静态资源的加载速度,减轻服务器的压力。

图2-5 动静分离

针对动态文件,可以采用动态页面静态化的办法,将原本需要动态生成的站点提前生成好,使用静态页面加速技术来访问。此种办法适用总数据量不大,生成静态页面数量不多但需要频繁访问的业务,在技术上可以使用Apache的mod_rewrite以及Nginx的rewrite等功能实现,也可以自己模拟HttpClient调用服务请求返回静态内容生成静态文件。

(5)前后端分离,在早期的开发架构中,前后端一体,组织结构中不区分前后端,开发人员在写业务逻辑的同时也需要编写JSP处理展示逻辑,JSP中含有html、css、js以及控制展示的服务端代码或标签。随着AJAX的出现,出现了前后端分离的架构,其核心是前端面向接口编程,前端页面使用Ajax调用后端的接口,接口返回JSON后,前端控制JSON数据的展示方式,在部署上前后端要分开独立部署。从组织结构上分为前端工程师和后端工程师,前端工程师需要掌握html、css、js等技能,后端工程师需要掌握Java等后端技术栈。

(6)读写分离,是指让主数据库处理事务性增、改、删操作,而从数据库处理查询操作。通过数据库复制把事务性操作导致的变更同步到从数据库。通过读写分离可以减少主库压力,尤其适用于写少读多的场景。在数据库层面利用数据库自身提供的主从复制功能,或者其他binlog复制工具实现主从同步以及只读库的复制扩展。在客户端可以使用MyCat、ShardingSphere等中间件配置读写路由逻辑,避免代码侵入。

(7)在线与离线分离,将实时任务与非实时任务进行分割。

在线任务一般是指用户对时间敏感,希望每次操作都能在极短的时间内看到结果的操作,如下单购买等用户操作。一般情况下,系统反应时间小于2s用户体验最好;2~8s,用户可以忍受;大于8s,用户将流失。离线任务一般数据量大,用户对时间不敏感。常见的离线任务场景包括积分计算、日志处理、BI分析等。如用户进行消费产生的积分并不需要立刻看到,可以延迟几分钟甚至可以在第二天再看到。在实现中一般通过定时周期性执行一个Job任务,定期执行离线任务,数据经过抽取、清洗、计算等过程进行加工处理。在技术上综合使用Job,Kettle、Spring Bach、消息队列、MapReduce、Spark等技术。

在设计系统时,需要区分用户场景,适时将离线任务从在线任务中抽取出来,采用离线任务对应的架构设计方法。离线任务的架构设计主要包括:全量与增量、推送还是拉取、流式计算还是批量计算。

全量与增量是指将全量数据通过数据预处理提前完成计算,再通过时间戳、数据状态等字段标识出新产生或发生变化的数据,每次工作任务只处理此部分数据,将数据处理的范围控制在最小。

推送与拉取是指数据由生产者推送给数据消费者,还是消费者从生产者产生的数据中进行抽取。比如日志分析一般采取拉取系统日志的办法,拉取的方式对原系统侵入较小。而积分计算,可以通过在应用系统中埋点推送到消息队列中。推送的方式比较容易理解,处理容易。

流式与批量:在流式计算中输入是持续的,在时间上是无界的,即没有固定的时间周期,计算结果是持续输出的。流式计算的典型是ES日志分析,业务系统产生日志后,ES拉取日志到索引服务器,服务器依次将每一条日志插入倒排索引库。批量计算,一般先有一部分数据集,然后针对数据集进行计算,并将结果一次性输出。批量计算的典型场景是数据报表,比如要计算年报数据,需先抽取各类业务数据到宽表,然后对宽表数据按日汇总,得出日报,进而加工成月报、年报。流式计算与批量计算的比较,如图2-6所示。

图2-6 流式计算与批量计算

(8)冷热分离是指将热点数据与不常用的业务和数据进行分离的办法。进程隔离中提到的秒杀业务隔离其实也是一种热点业务的分离,但一般情况下的冷热分离是指冷热数据的分离。用户一般只会偶尔关注历史数据(一个月以前已经完结的订单很少有用户再去查看),而历史数据会占据较多的资源,此时可按照时间维度将历史数据归档,归档后节省下的资源用于热数据的处理。归档可以是异构的,数据可以存储到不同类型的存储中间件或存储介质中,在消费端可以单独定制数据查询服务,路由到归档数据库中。需注意数据冷热分离与冷备热备在概念上的区别,在数据库备份过程中,如果备份操作时数据库不提供服务,此种方式就称为冷备,反之称为热备。

(9)其他隔离方式:隔离方式还有很多种,不一而足,例如爬虫分隔,即通过负载均衡等机制将网络爬虫与正常流量分隔。

2.2.8 容错

在一个分布式系统中,从时间维度和容量维度看,故障是不可避免的。容错的目的就是在一个分布式系统中,当系统运行时,即使有错误发生仍能保证不间断提供服务。容错的主要思路是设计备用方案,在组件发生故障时通过备份方案代替故障组件发挥作用。

(1)硬件环境容错,各服务器厂家都生产有相应的高可靠服务器,解决思路是服务器内提供双份硬件,如双路电源、内存等。在IDC机房中一般通过多路网络、多路供电措施、同时备用UPS设备、柴油发电设备等综合措施保障机房的可靠性。

(2)应用集群容错,应用集群将无状态的服务或系统应用镜像后,在多个服务器上进行部署,客户端通过负载均衡算法访问服务,若其中一个服务发生问题,自动将此服务排除在集群外,其他服务继续发挥作用。

(3)数据集群容错,是指关系型数据库或NoSQL数据库通过共享存储或者主主复制、主从复制方式实现集群。主主复制,是双主库都可以进行写操作,数据变更通过复制的方式传递给另一个库,当一个主库发生错误时,另一个主库立即接管发挥作用。主从复制,是从库从主库复制数据,从库平时只能读不能写,当主库发生错误时从库升级为主库发挥作用。

(4)容灾备份,通过整套环境的备份,起到灾难后立刻接管的作用,包括多机房部署,同城灾备、异地灾备等形式。多机房部署是在同一数据中心的多个分区部署相同的分布式系统,保证相同的应用或者数据库,数据库通过数据传输软件进行同步。同城灾备是分布式系统在一个城市的不同数据中心部署。异地灾备是将分布式系统部署在不同的地域。

(5)服务容错,在分布式系统面向API的编程中,需考虑超时、重试、混序等异常现象,保证服务幂等,即任意多次客户操作或服务调用所产生的影响均与一次执行的影响相同。典型的异常场景包括:因网络重发或系统bug重发收到的多次付款请求;前端页面的重复提交;创建订单时网络超时;等待支付回调结果时,收到退款请求等情况。解决的办法如下。

1)幂等提交,采用会话token校验机制。在生成表单页面时,服务器会先生成一个token保存于session,并把该token传给表单页面,用于后续校验。当表单提交时会带上token,服务器端判断session保存的token和表单提交subtoken是否一致,若不一致或session的token为空或表单未携带token则不通过。首次提交表单时session的token与表单携带的token一致,则判断通过,并删除session保存的subtoken。当再次提交表单时,由于session的token为空则不通过。从而实现了防止表单重复提交。

2)状态机幂等,采用乐观锁的机制,通过唯一索引+版本号进行更新,保证状态正确。涉及状态机(状态变更图)相关的业务,需要设置状态字段,如果状态机已经处于下一个状态,这时来了一个上一个状态的变更,就需要采用乐观锁的机制,通过唯一索引+状态字段进行更新,使不符合状态机要求的操作不能成功,保证了有限状态机的幂等。

3)流水幂等,在数据库层建立正确的唯一索引,通过唯一索引保证数据幂等。对于服务调用,需要在调用接口中增加交易流水号字段(唯一索引),一般通过将消费者渠道来源编码与序列号组装成交易流水号(source+seq),交易时根据交易流水号进行判断,调用此交易流水是否已经处理过,如果处理过则直接返回结果。

(6)超时重试,针对超时未返回结果的业务,一般要进行重试处理,多次重试后仍无结果的,可以通过交易补偿或异步消息等方式保证最终一致。如支付业务,在最终未收到支付回调,或支付回调处理失败时,可以按照对账文件进行补账,这也是交易补偿的一种方式。

2.2.9 安全

互联网天然的开放性对系统安全提出了巨大的挑战。系统安全是一个综合工程,涉及弱电施工、网络拓扑、主机管理、软件架构等多个环节,按领域可以分为管理安全、物理安全、主机安全、网络安全、应用安全和数据安全几类。围绕安全的矛与盾的攻守是个长期过程,应定期进行全路径的安全体检,在软件开发过程中应该定期进行代码检查和漏洞扫描。

(1)物理安全,物理安全是信息安全的前提,是保护机房设备、设施免遭地震、水灾、火灾或其他人为破坏的措施。

(2)主机安全,是指对主机的操作系统、访问控制的安全保护。如操作系统安全升级等。

(3)网络安全,是针对整个网络拓扑结构的安全保护。如防火墙保护、入侵检测(IPS)等。

(4)应用安全,是指业务系统的安全保护,如用户密码加密、接口调用鉴权、XSS攻击防范、SQL注入防范、垃圾与敏感信息过滤、App安全加固、短信轰炸保护等。

(5)数据安全,是指对图片文件、数据库数据、队列消息等数据资产的鉴权访问、数据加密、数据备份等保护措施。

(6)管理安全,是指与安全相关的组织结构、管理制度等。

2.2.10 治理

在一个分布式系统中需要一个全局的治理结构,实现全局的监控、调度、保护和异常处理,并统筹协调与大规模集群模式相适应的开发、测试与运维工作。

(1)系统监控:在面向服务的分布式系统中除了传统的对主机负载、I/O、内存以及应用情况进行监控外,还要关注系统调用链情况、服务稳定相关的熔断情况,在发现问题时自动报警。

(2)交易治理:在面向服务的架构中,通过注册中心、控制台、网关等对服务地址、负载均衡策略进行管理,在应对高并发流量时,通过自动或手动方式进行限流、熔断、降级或者进行扩展处理。

(3)DevOps:统筹协调开发、运维、测试的一体化整合,通过Docker、K8s,Jenkins等基础设施集成其他工具实现测试、检查、部署的持续集成。