系统架构演变概述
在公司业务初创时期, 面对的主要问题是如何将一个想法变成实际的软件实现, 在这个时候整个软件系统的架构并没有搞得那么复杂, 为了快速迭代, 整个软件系统就是由 "App + 后台服务" 组成, 而后台服务也只是从工程角度将应用进行 Jar 包的拆分. 此时软件系统架构如下:
而此时整个软件系统的功能也比较简单, 只有基本的用户, 订单, 支付等功能, 并且由于业务流程没有那么复杂, 这些功能基本耦合在一起. 而随着 App 的受欢迎程度(作者所在的公司正好处于互联网热点), 所以 App 下载量在 2017 年迅猛增长, 在线注册人数此时也是蹭蹭往上涨.
随着流量的迅猛增长, 此时整个后台服务的压力变得非常大, 为了抗住压力只能不断的加机器, 平行扩展后台服务节点. 此时的部署架构如下:
通过这种方式, 整个软件系统抗住了一波压力, 然而系统往往还是会偶尔出点事故, 特别是因为 API 中的某个接口性能问题导致整个服务不可用, 因为这些接口都在一个 JVM 进程中, 虽然此时部署了多个节点, 但因为底层数据库, 缓存系统都是一套, 所以还是会出现一挂全挂的情况.
另一方面, 随着业务的快速发展, 以往相对简单的功能变得复杂起来, 这些功能除了有用户看得见的, 也会包括很多用户看不见的, 就好像百度搜索, 用户能看见的可能只是一个搜索框, 但是实际上后台对应的服务可能是成百上千, 如有些增长策略相关的功能: 红包, 分享拉新等. 还有些如广告推荐相关的变现功能等.
此外, 流量 / 业务的增长也意味着团队人数的迅速增长, 如果此时大家开发各自的业务功能还是用一套服务代码, 很难想象百十来号人, 在同一个工程在叠加功能会是一个什么样的场景. 所以如何划分业务边界, 合理的进行团队配置也是一件十分迫切的事情了!
为了解决上述问题, 适应业务, 团队发展, 架构团队决定进行微服务拆分. 而要实施微服务架构, 除了需要合理的划分业务模块边界外, 也需要一整套完整的技术解决方案.
在技术方案的选择上, 服务拆分治理的框架也是有很多, 早期的有如 webService, 近期的则有各种 Rpc 框架(如 Dubbo,Thirft,Grpc). 而 Spring Cloud 则是基于 Springboot 提供的一整套微服务解决方案, 因为技术栈比较新, 并且各类组件的支撑也非常全面, 所以 Spring Cloud 就成为了首选.
经过一系列的重构 + 扩展, 整个系统架构最终形成了以 App 为中心的一套微服务软件系统, 结构如下:
到这里, 整个软件系统就基于 SpringCloud 初步完成了微服务体系的拆分. 支付, 订单, 用户, 广告等核心功能抽离成独立的微服务, 与此同时各自微服务对应的数据库也按照服务边界进行了拆分.
在完成服务的拆分以后, 原来功能逻辑之间的代码调用关系, 转换成了服务间网络的调用关系, 而各个微服务需要根据各自所承载的功能提供相应的服务, 此时服务如何被其他服务发现并调用, 就成了整个微服务体系中比较关键的部分, 使用过 Dubbo 框架的同学知道, 在 Dubbo 中服务的注册 & 发现是依赖于 Zookeeper 实现的, 而在 SpringCloud 中我们是通过 Consul 来实现. 另外在基于 SpringCloud 的架构体系中, 提供了配置中心 (ConfigServer) 来帮助各个微服务管理配置文件, 而原本的 API 服务, 随着各个功能的抽离, 逐步演变成前置网关服务了.
聊到这里, 基于 SpringCloud 我们进行了微服务拆分, 而在这个体系结构中, 分别提到了 Consul,ConfigServer, 网关服务这几个关键组件, 那么这几个关键组件具体是如何支撑这个庞大的服务体系的呢?
SpringCloud 关键组件
Consul
Consul 是一个开源的, 使用 go 语言开发的注册中心服务. 它里面内置了服务发现与注册框架, 分布一致性协议实现, 健康检查, Key/Value 存储, 多数据中心等多个方案. 在 SpringCloud 框架中还可以选择 Eurke 作为注册中心, 这里之所以选择 Consul 主要原因在于 Consul 对异构的服务的支持, 如: grpc 服务.
事实上, 在后续的系统架构演进中, 在某些服务模块进一步向子系统化拆分的过程中, 就采用了 grpc 作为子系统服务间的调用方式. 例如, 支付模块的继续扩张, 对支付服务本身又进行了微服务架构的拆分, 此时支付微服务内部就采用了 grpc 的方式进行调用, 而服务注册与发现本身则还是依赖于同一套 Consul 集群.
此时的系统架构演进如下:
原有微服务架构中的模块服务在规模达到一定程度或复杂性达到一定程度后, 都会朝着独立的体系发展, 从而将整个微服务的调用链路变的非常长, 而从 Consul 的角度看, 所有的服务又都是扁平的.
随着微服务规模的越来越大, Consul 作为整个体系的核心服务组件, 在整个体系中处于关键的位置, 一旦 Consul 挂掉, 所有的服务都将停止服务. 那么 Consul 到底是什么样服务? 其容灾机制又该如何设计呢?
要保证 Consul 服务的高可用, 在生产环境 Consul 应该是一个集群 (关于 Consul 集群的安装与配置可以参考网络资料), 这是毫无疑问的. 而在 Consul 集群中, 存在两种角色: Server,Client, 这两种角色与 Consul 集群上运行的应用服务并没有什么关系, 只是基于 Consul 层面的一种角色划分. 实际上, 维持整个 Consul 集群状态信息的还是 Server 节点, 与 Dubbo 中使用 Zookeeper 实现注册中心一样, Consul 集群中的各个 Server 节点也需要通过选举的方式(使用 GOSSIP 协议, Raft 一致性算法, 这里不做详细展开, 在后面的文章中可以和大家单独讨论) 来选举整个集群中的 Leader 节点来负责处理所有查询和事务, 并向其他节点同步状态信息.
而 Client 角色则是相对无状态的, 只是简单的代理转发 RPC 请求到 Server 节点, 之所以存在 Client 节点主要是分担 Server 节点的压力, 作一层缓冲而已, 这主要是因为 Server 节点的数量不宜过多, 因为 Server 节点越多也就意味着达成共识的过程越慢, 节点间同步的代价也就越高. 对于 Server 节点, 一般建议 3-5 台, 而 Client 节点则没有数量的限制, 可以根据实际情况部署数千或数万台. 事实上, 这也只是一种策略, 在现实的生产环境中, 大部分应用只需要设置 3~5 台 Server 节点就够了, 作者所在的公司一套生产集群中的 Consul 集群的节点配置就是 5 个 Server 节点, 并没有额外再设置 Client 节点.
另外, 在 Consul 集群中还有一个概念是 Agent, 事实上每个 Server 或 Client 都是一个 consul agent, 它是运行在 Consul 集群中每个成员上的一个守护进程, 主要的作用是运行 DNS 或 HTTP 接口, 并负责运行时检查和保持服务信息同步. 我们在启动 Consul 集群的节点 (Server 或 Client) 时, 都是通过 consul agent 的方式启动的. 例如:
- consul agent -server -Bootstrap -syslog
- -ui
- -data-dir=/opt/consul/data
- -dns-port=53
- -recursor=10.211.55.3
- -config-dir=/opt/consul/conf
- -pid-file=/opt/consul/run/consul.pid
- -client=10.211.55.4
- -bind=10.211.55.4
- -node=consul-server01
- -disable-host-node-id &
以实际的生产环境为例, Consul 集群的部署结构示意图如下:
实际生产案例中并没有设置 Client 节点, 而是通过 5 个 Consul Server 节点组成的集群, 来服务整套生产集群的应用注册 & 发现. 这里有细节需要了解下, 实际上 5 个 Consul Server 节点的 IP 地址是不一样的, 具体的服务在连接 Consul 集群进行服务注册与查询时应该连接 Leader 节点的 IP, 而问题是, 如果 Leader 节点挂掉了, 相应的应用服务节点, 此时如何连接通过 Raft 选举产生的新 Leader 节点呢? 难道手工切换 IP 不成?
显然手工切换 IP 的方式并不靠谱, 而在生产实践中, Consul 集群的各个节点实际上是在 Consul Agent 上运行 DNS(如启动参数中红色字体部分), 应用服务在连接 Consul 集群时的 IP 地址为 DNS 的 IP,DNS 会将地址解析映射到 Leader 节点对应的 IP 上, 如果 Leader 节点挂掉, 选举产生的新 Leader 节点会将自己的 IP 通知 DNS 服务, DNS 更新映射关系, 这一过程对各应用服务则是透明的.
通过以上分析, Consul 是通过集群设计, Raft 选举算法, Gossip 协议等机制来确保 Consul 服务的稳定与高可用的. 如果需要更高的容灾级别, 也可以通过设计双数据中心的方式, 来异地搭建两个 Consul 数据中心, 组成一个异地灾备 Consul 服务集群, 只是这样成本会更高, 这就看具体是否真的需要了.
ConfigServer(配置中心)
配置中心是对微服务应用配置进行管理的服务, 例如数据库的配置, 某些外部接口地址的配置等等. 在 SpringCloud 中 ConfigServer 是独立的服务组件, 它与 Consul 一样也是整个微服务体系中比较关键的一个组件, 所有的微服务应用都需要通过调用其服务, 从而获取应用所需的配置信息.
随着微服务应用规模的扩大, 整个 ConfigServer 节点的访问压力也会逐步增加, 与此同时, 各个微服务的各类配置也会越来越多, 如何管理好这些配置文件以及它们的更新策略(确保不因生产配置随意改动而造成线上故障风险), 以及搭建高可用的 ConfigServer 集群, 也是确保微服务体系稳定很重要的一个方面.
在生产实践中, 因为像 Consul,ConfigServer 这样的关键组件, 需要搭建独立的集群, 并且部署在物理机而不是容器里. 在上一节介绍 Consul 的时候, 我们是独立搭建了 5 个 Consul Server 节点. 而 ConfigServer 因为主要是 http 配置文件访问服务, 不涉及节点选举, 一致性同步这样的操作, 所以还是按照传统的方式搭建高可用配置中心. 具体结构示意图如下:
我们可以单独通过 Git 来管理应用配置文件, 正常来说由 ConfigSeever 直接通过网络拉取 Git 仓库的配置供服务获取就可以了, 这样只要 Git 仓库配置更新, 配置中心就能立刻感知到. 但是这样做的不稳定之处, 就在于 Git 本身是内网开发用的代码管理工具, 如果让线上实时服务直接读取, 很容易将 Git 仓库拉挂了, 所以, 我们在实际的运维过程中, 是通过 Git 进行配置文件的版本控制, 区分线上分支 / master 与功能开发分支 / feature, 并且在完成 mr 后还需要手工 (通过发布平台触发) 同步一遍配置, 过程是将新的 master 分支的配置同步一份到各个 configserver 节点所在主机的本地路径, 这样 configserver 服务节点就可以通过其本地目录获取配置文件, 而不用多次调用网络获取配置文件了.
而另一方面, 随着微服务越来越多, Git 仓库中的配置文件数量也会越来越多. 为了便于配置的管理, 我们需要按照一定的组织方式来组织不同应用类型的配置. 在早期所有的应用因为没有分类, 所以导致上百个微服务的配置文件放在一个仓库目录, 这样一来导致配置文件管理成本增加, 另外一方面也会影响 ConfigServer 的性能, 因为某个微服务用不到的配置也会被 ConfigServer 加载.
所以后期的实践是, 按照配置的层次关系进行组织, 将公司全局的项目配置抽象到顶层, 由 ConfigServer 默认加载, 而其他所有的微服务则按照应用类型进行分组(通过 Git 项目空间的方式分组), 相同的应用放在一个组, 然后这个组下单独设立一个名为 config 的 Git 仓库来存放这个组下相关微服务的配置文件. 层次结构如下:
这样应用加载配置的优先级就是 "本地配置 ->common 配置 ->组公共配置 ->项目配置" 这样的顺序. 例如某服务 A, 在项目工程的默认配置文件 ("bootstrap.yml/application.yml") 中配置了参数 A, 同时也在本地项目配置 "application-production.yml" 配置了参数 B, 而与此同时, ConfigServer 中的 common 仓库下的配置文件 "application.yml/application-production.yml" 又分别存在参数 C, 参数 D, 同时有个组叫 "pay", 其下的默认配置文件 "application.yml/application-production.yml" 存在参数 E, 参数 F, 具体项目 pay-API 又存在配置文件 "pay-api-production.yml" 其覆盖了 common 仓库中参数 C, 参数 D 的值. 那么此时如果该应用以 "spring.profiles.active=production" 的方式启动, 那么其能获取到的配置参数 (通过链接访问: http://{spring.cloud.config.uri}/pay-API-production.YAML) 就是 A,B,C,D,E,F, 其中 C,D 的参数值为 pay-API-production.YAML 中最后覆盖的值.
而对于 ConfigServer 服务本身来说, 需要按照这样的组织方式进行配置类型匹配, 例如上述的例子中, 假设还存在 finance 的配置仓库, 而 pay 组下的服务访问配置中心的时候, 是不需要 finance 空间下的配置文件的, 所以 ConfigServer 可以不用加载. 这里就需要在 ConfigServer 服务配置中进行一些配置. 具体如下:
- spring:
- application:
- name: @project.artifactId@
- version: @project.version@
- build: @buildNumber@
- branch: @scmBranch@
- cloud:
- inetutils:
- ignoredInterfaces:
- - docker0
- config:
- server:
- health.enabled: false
- Git:
- uri: /opt/repos/config
- searchPaths: 'common,{application}'
- cloneOnStart: true
- repos:
- pay:
- pattern: pay-*
- cloneOnStart: true
- uri: /opt/repos/example/config
- searchPaths: 'common,{application}'
- finance:
- pattern: finance-*
- cloneOnStart: true
- uri: /opt/repos/finance/config
- searchPaths: 'common,{application}'
通过在 ConfigServer 服务本身的 application.YAML 本地配置中, 设置其配置搜索方式, 来实现这样的目的.
网关服务 & 服务熔断 & 监控
通过上面两小节的内容, 我们相对详细地介绍了基于 SpringCloud 体系中比较关键的两个服务组件. 然而在微服务架构体系中, 还有很多关键的问题需要解决, 例如, 应用服务在 Consul 中部署了多个节点, 那么调用方如何实现负载均衡?
关于这个问题, 在传统的架构方案中是通过 Nginx 实现的, 但是在前面介绍 Consul 的时候只提到了 Consul 的服务注册 & 发现, 选举等机制, 并没有提到 Consul 如何在实现服务调用的负载均衡. 难道基于 SpringCloud 的微服务体系中的应用服务都是单节点在提供服务, 哪怕即使部署了多个服务节点? 事实上, 我们在服务消费方通过 @EnableFeignClients 注解开启调用, 通过 @FeignClient("user")注解进行服务调用时, 就已经实现了负载均衡, 为什么呢? 因为, 这个注解默认是会默认开启 Robbin 代理的, 而 Robbin 是实现客户端负载均衡的一个组件, 通过从 Consul 拉取服务节点信息, 从而以轮询的方式转发客户端调用请求至不同的服务端节点来实现负载均衡. 而这一切都是在消费端的进程内部通过代码的方式实现的. 这种负载方式寄宿于消费端应用服务上, 对消费端存在一定的代码侵入性, 这是为什么后面会出现 Service Mesh(服务网格)概念的原因之一, 这里就不展开了, 后面有机会再和大家交流.
另一需要解决的关键问题是服务熔断, 限流等机制的实现, SpringCloud 通过集成 Netflix 的 Hystrix 框架来提供这种机制的支持, 与负载均衡机制一样也是在消费端实现. 由于篇幅的关系, 这里也不展开了, 在后面的文章中有机会再和大家交流.
此外还有 Zuul 组件来实现 API 网关服务, 提供路由分发与过滤相关的功能. 而其他辅助组件还有诸如 Sleuth 来实现分布式链路追踪, Bus 实现消息总线, Dashboard 实现监控仪表盘等. 由于 SpringCloud 的开源社区比较活跃, 还有很多新的组件在不断的被集成进来, 感兴趣的朋友可以持续关注下!
微服务之运维形态
在微服务体系结构下, 随着服务数量大量的增长, 线上的部署 & 维护的工作量会变得非常大, 而如果还采用原有的运维模式的话, 就能难满足需要了. 此时运维团队需要实施 Devops 策略, 开发自动化运维发布平台, 打通产品, 开发, 测试, 运维流程, 关注研发效能.
另外一方面也需要推进容器化 (Docker/Docker Swarm/k8s) 策略, 这样才能快速对服务节点进行伸缩, 这也是微服务体系下的必然要求.
微服务泛滥问题
这里还需要注意一个问题, 就是实施微服务架构后, 如何在工程上管控微服务的问题. 盲目的进行微服务的拆分也不是一件很合理的事情, 因为这会导致整个服务调用链路变得深不可测, 对问题排查造成难度, 也浪费线上资源.
重构问题
在早期单体架构方式向微服务架构的转变过程中, 重构是一个非常好的方式, 也是确保服务规范化, 业务系统应用架构合理化的很重要的手段. 但是, 一般来说, 在快速发展阶段也就意味着团队规模的迅速增长, 短时间内如何让新的团队有事可做也是一件非常考验管理水平的事情, 因为如果招了很多人, 并且他们之间呈现一种过渡的竞争状态的话, 就会出现让重构这件事变得有些功利的情况, 从而导致重构不彻底, 避重就轻, 导致表象上看是很高大上的微服务架构, 而业务系统实际上比较烂的情况.
另外, 重构是在一定阶段后作出的重要决策, 不仅仅是重新拆分, 也是对业务系统的重新塑造, 所以一定要考虑好应用软件的系统结构以及实施它们所需要付出的成本, 切不可盲目!
后记
基于 SpringCloud 的微服务架构体系, 通过集成各种开源组件来为整个体系服务支持, 但是在负载均衡, 熔断, 流量控制的方面需要对服务消费端的业务进程进行侵入. 所以很多人会认为这不是一件很好的事情, 于是出现了 Service Mesh(服务网格)的概念, Service Mesh 的基本思路就是通过主机独立 Proxy 进行的部署来解耦业务系统进程, 这个 Proxy 除了负责服务发现和负载均衡 (不在需要单独的注册组件, 如 Consul) 外, 还负责动态路由, 容错限流, 监控度量和安全日志等功能.
来源: https://yq.aliyun.com/articles/670105