分布式架构演进

分布式架构演进


架构设计的三大目标

高性能、高可用、可扩展

​ 架构设计要切忌过度设计,最适合自己业务的才是最好的,并不是说大家都用分布式架构,你也要用;单体架构太low就一定不用。一个架构带来好处的时候也一定会带来弊端:比如将单体服务微服务化后,可以帮助实现服务的敏捷开发和部署。但是,由于将原本一体化架构的应用,拆分成了多个通过网络通信的分布式服务,为了在分布式环境下,协调多个服务正常运行,就必然引入一定的复杂度;而原本单体服务很容易做到的事务、单点登录到了分布式架构中也会难度加倍。


单体服务

分层架构

​ 软件架构分层在软件工程中是一种常见的设计方式,它是将整体系统拆分成 N 个层次,每个层次有独立的职责,多个层次协同提供完整的功能。

三层架构

​ 一种常见的分层方式是将整体架构分为表现层、逻辑层和数据访问层:

  • 表现层,顾名思义嘛,就是展示数据结果和接受用户指令的,是最靠近用户的一层;
  • 逻辑层里面有复杂业务的具体实现;
  • 数据访问层则是主要处理和存储之间的交互。

这是在架构上最简单的一种分层方式。其实,我们在不经意间已经按照三层架构来做系统分层设计了,比如在构建项目的时候,我们通常会建立三个目录:Web、Service 和 Dao,它们分别对应了表现层、逻辑层还有数据访问层。
在这里插入图片描述

MVC

​ 还有一种软件分层架构是MVC(Model-View-Controller)架构。它将整体的系统分成了 Model(模型),View(视图)和 Controller(控制器)三个层次,也就是将用户视图和业务处理隔离开,并且通过控制器连接起来,很好地实现了表现和逻辑的解耦,是一种标准的软件分层架构。

在这里插入图片描述

分层架构的优势

  • 分层的设计可以简化系统设计,让不同的人专注做某一层次的事情。

  • **再有,分层之后可以做到很高的复用。**比如,我们在设计系统 A 的时候,发现某一层具有一定的通用性,那么我们可以把它抽取独立出来,在设计系统 B 的时候使用起来,这样可以减少研发周期,提升研发的效率。

  • **最后一点,分层架构可以让我们更容易做横向扩展。**如果系统没有分层,当流量增加时我们需要针对整体系统来做扩展。但是,如果我们按照上面提到的三层架构将系统分层后,那么我们就可以针对具体的问题来做细致的扩展。比如说,业务逻辑里面包含有比较复杂的计算,导致 CPU 成为性能的瓶颈,那这样就可以把逻辑层单独抽取出来独立部署,然后只对逻辑层来做扩展,这相比于针对整体系统扩展所付出的代价就要小的多了。

  • 横向扩展是高并发系统设计的常用方法之一,既然分层的架构可以为横向扩展提供便捷, 那么支撑高并发的系统一定是分层的系统

分层架构的劣势

  • 它最主要的一个缺陷就是增加了代码的复杂度。
  • 如果我们把每个层次独立部署,层次间通过网络来交互,那么多层的架构在性能上会有损耗。这也是为什么服务化架构性能要比单体架构略差的原因,也就是所谓的“多一跳”问题。

架构的演进

​ 假设有一天突然领导让你去开发一个电商系统,很急很关键,让你最好三天搞完上线。那这个时候你可能会采用最简单的架构:三层架构+一台数据库服务器存储业务数据完事儿。

在这里插入图片描述

在系统开发的初期,这种架构确实给你的开发运维,带来了很大的便捷,主要体现在:

  • 开发简单直接,代码和项目集中式管理;
  • 只需要维护一个工程,节省维护系统运行的人力成本;
  • 排查问题的时候,只需要排查这个应用进程就可以了,目标性强。

数据库

我们先看一份常用系统操作响应时间的统计数据:

在这里插入图片描述

这样数据库很容易就成为我们的系统的瓶颈,毕竟在有索引的情况下,数据库查询也要在十几ms。而数据库的优化又可以分为几部分:

  • 系统慢的原因出现在和数据库的交互上?——可以通过“池化”的思想来解决。

  • 业务是否是读多写少?——读写分离

  • 随着系统逐渐发展,数据库中存储的数据也越来越多,单个表的数据量超过了千万甚至到了亿级别,这时即使你使用了索引,索引占用的空间也随着数据量的增长而增大,数据库就无法缓存全量的索引信息,那么就需要从磁盘上读取索引数据,就会影响到查询的性能了——分库分表:

    ​ 按照业务类型来拆分——垂直拆分

    ​ 将单一数据表按照某一种规则拆分到多个数据库和多个数据表中,比如按照时间——水平拆分

微服务化

分库分表后,你的系统可能是长这个样子:

在这里插入图片描述

​ 从整体上看,数据库分了主库和从库,数据也被切分到多个数据库节点上。但是你的工程的部署方式还是采用一体化架构,也就是说所有的功能模块,比方说电商系统中的订单模块、用户模块、支付模块、物流模块等等,都被打包到一个大的 Web 工程 中,然后部署在应用服务器上。但随着业务量的增加,单体架构的劣势逐渐体现出来:

  • 首先,在技术层面上,数据库连接数可能成为系统的瓶颈

    ​ 数据库的连接是比较重的一类资源,不仅连接过程比较耗时,而且连接 MySQL 的客户端数量有限制,最多可以设置为 16384(在实际的项目中,可以依据 实际业务来调整)。

    ​ 这个数字看着很大,但是因为你的系统是按照一体化架构部署的,在部署结构上没有分层, 应用服务器直接连接数据库,那么当前端请求量增加,部署的应用服务器扩容,数据库的连 接数也会大增,给你举个例子。 我之前维护的一个系统中,数据库的最大连接数设置为 8000,应用服务器部署在虚拟机 上,数量大概是 50 个,每个服务器会和数据库建立 30 个连接,但是数据库的连接数,却 远远大于 30 * 50 = 1500。因为你不仅要支撑来自客户端的外网流量,还要部署单独的应用服务,支撑来自其它部门的 内网调用,也要部署队列处理机,处理来自消息队列的消息,这些服务也都是与数据库直接 连接的,林林总总加起来,在高峰期的时候,数据库的连接数要接近 3400。

    ​ 所以,一旦遇到一些大的运营推广活动,服务器就要扩容,数据库连接数也随之增加,基本 上就会处在最大连接数的边缘。这就像一颗定时炸弹,随时都会影响服务的稳定。

  • 第二点,一体化架构增加了研发的成本,抑制了研发效率的提升。由于代码部署在一起,每个人都向同一个代码库提交代码,代码冲突无法避免;同时,功能之间耦合严重,可能你只是更改了很小的逻辑,却导致其它功能不可用,从而在测试时需要对整体功能回归,延长了交付时间。模块之间互相依赖,一个小团队中的成员犯了一个错误,就可能会影响到,其它团队维护的服务,对于整体系统稳定性影响很大。

  • 第三点,一体化架构对于系统的运维也会有很大的影响。想象一下,在项目初期,你的代码可能只有几千行,构建一次只需要一分钟,那么你可以很 敏捷灵活地频繁上线变更修复问题。但是当你的系统扩充到几十万行,甚至上百万行代码的 时候,一次构建的过程,包括编译、单元测试、打包和上传到正式环境,花费的时间可能达 到十几分钟,并且,任何小的修改,都需要构建整个项目,上线变更的过程非常不灵活。

微服务化的拆分

​ 随着业务的扩增,我们会发现我们的工程可以按照业务维度做垂直拆分。但是仅仅是对工程做拆分是不够的,试想一下我们有一个社交系统,用户注册了之后可以在好友圈发送消息,然后关注他的人都可以看到。如果这个时候我们仅仅是对工程做拆分,把与用户相关的逻辑,部署成一个单独的服务,但是无论是内容还是互动,都会查询用户库获取用户数据,所以,即使我们做了业务池的拆分,但实际上,每一个业务池子都需要连 接用户库,并且请求量都很大,这就造成了用户库的连接数比其它都要多一些,容易成为系统的瓶颈。所以,我们需要按照业务的维度同时拆分工程跟数据库。

在这里插入图片描述

​ 通过按照业务做横向拆分的方式,解决数据库层面的扩展性问题:

在这里插入图片描述

​ 再比如,我们在做社区业务的时候,会有多个模块需要使用地理位置服务,将 IP 信息或者经纬度信息,转换为城市信息。比如,推荐内容的时候,可以结合用户的城市信息,做附近 内容的推荐;展示内容信息的时候,也需要展示城市信息等等。

​ 那么,如果每一个模块都实现这么一套逻辑就会导致代码不够重用。因此,我们可以把将 IP 信息或者经纬度信息,转换为城市信息,包装成单独的服务供其它模块调用,也就是, 我们可以将与业务无关的公用服务抽取出来,下沉成单独的服务。

​ 按照以上两种拆分方式将系统拆分之后,每一个服务的功能内聚,维护人员职责明确,增加 了新的功能只需要测试自己的服务就可以了,而一旦服务出了问题,也可以通过服务熔断、 降级的方式减少对于其他服务的影响。

​ 另外,由于每个服务都只是原有系统的子集,代码行数相比原有系统要小很多,构建速度上 也会有比较大的提升。

​ 我们可以通过DDD:领域驱动设计来帮助我们做从业务层面,对我们的工程做微服务拆分。

晚点会对这部分做详细总结整理


缓存

​ 但是仅仅是将工程微服务化还不够,随着并发的增加,存储数据量的增多,数据库的磁盘 IO 逐渐成了系统的瓶颈,我们需要一种访问更快的组件来降低请求响应时间,提升整体系统性能。而这个时候我们可以通过多级缓存配合使用,比如:

  • ​ 在负载均衡层使用静态缓存
  • ​ 在应用层和数据库层之间,可以借助缓存中间件实现分布式缓存,例如Redis、Memcache、Mongodb等
  • ​ 在应用层使用本地缓存

我们需要将请求尽量挡在上层,因为越往下层,对于并发的承受能力越差。

使用了缓存那就要考虑数据一致性:

  • ​ 分布式缓存——Cache Aside(旁路缓存)策略
  • ​ 本地缓存——Read/Write Through(读穿 / 写穿)策略

同时,也要考虑缓存会遇到的问题:

  • ​ 缓存击穿
  • ​ 缓存穿透
  • ​ 缓存雪崩

消息队列

​ 在加上缓存之后,随着业务的发展,你可能会遇到一些存在高并发写请求的场景,或者可以异步处理的场景。这个时候就要考虑使用消息队列。

​ 消息队列的几个主要作用:

  • 解耦
  • 异步
  • 削峰填谷

​ 既然要使用消息队列,那就要考虑选型:

  • RabbitMQ
  • RocketMq
  • Kafka

​ 但是使用消息队列也会碰到一些问题,比如为了保证消息一定会被发送到,消息至少会被发送一次。那如何保证产生的消息一定会被消费到,并且只被消费一次呢?

  • 消息的可靠性保证
  • 消息的幂等性处理

​ 还有另外一个问题:单体架构中,我们可以使用事务来保证对数据库中的一组数据进行操作的同时成功或者同时失败。那我们怎么保证本地事务与消息队列发送/消费消息的事务性?——消息的事务处理


分布式架构的问题

​ 现在我们对单体架构按照业务进行了拆分,同时又加上了缓存来提高性能,使用消息队列来帮助帮助我们的工程抗住更高的并发:

在这里插入图片描述

​ 那接下来又会冒出来一系列因分布式集群架构带来的问题:

  • ​ 我们的服务是基于分布式集群来实现部署的,如何保证一些共享资源的原子性?——分布式锁
  • ​ 分布式集群架构如何实现单点登录?——分布式会话
  • ​ 如何保证不同资源服务器的数据一致性?——分布式事务
  • ​ 服务跨网络怎么通信?——分布式通信
  • ​ 服务跨网络之后怎么感知对方的地址互相调用?——注册中心
  • ​ 如何将访问的请求“均衡”地分配到多个处理节点上?——负载均衡
  • ​ 如何将一些服务共有的功能整合在一起,独立部署为单独的一层,用来解决一些服务治理的问题?——API网关
  • ​ 如何防止微服务的雪崩效应?——熔断、降级处理
  • ​ 微服务化后,如何定位问题?——链路追踪
  • ​ 微服务化后,如何生成唯一主键?——分布式全局ID

​ 当然问题不止这么些,还有应用监控与调优、容器化部署等等方面,慢慢整理吧~


本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>