【编者的话】随着公司业务量的飞速发展,平台面临的挑战已经远远大于业务,需求量不断增加,技术人员数量增加,面临的复杂度也大大增加。在这个背景下,平台的技术架构也完成了从传统的单体应用到微服务化的演进。
这是平台最开始的情况,当时流量小,为了节约成本,并将所有应用都打包放到一个应用里面,采用的架构为 .NET SQL Server:
表示层:位于最外层(最上层),最接近用户。用于显示数据和接收用户输入的数 据,为用户提供一种交互式操作的界面,平台所使用的是基于.NET的Web形式。
业务逻辑层(Business Logic Layer):无疑是系统架构中体现核心价值的部分。它的关注点主要集中在业务规则的制定、业务流程的实现等与业务需求有关的系统设计,也即是说它是与系统所应对的领域(Domain)逻辑有关,很多时候,也将业务逻辑层称为领域层。 业务逻辑层在体系架构中的位置很关键,它处于数据访问层与表示层中间,起到了数据交换中承上启下的作用。由于层是一种弱耦合结构,层与层之间的依赖是向下的,底层对于上层而言是“无知”的,改变上层的设计对于其调用的底层而言没有任何影响。如果在分层设计时,遵循了面向接口设计的思想,那么这种向下的依赖也应该是一种弱依赖关系。对于数据访问层而言,它是调用者;对于表示层而言,它却是被调用者。
数据访问层:有时候也称为是持久层,其功能主要是负责数据库的访问,可以访问数据库系统、二进制文件、文本文档或是XML文档,平台在这个阶段使用的是 hibernate.net sqlserver。
第一代架构看似很简单,却支撑了平台的早期业务发展,满足了网站用户访问量在几万规模的处理需求。但是当用户访问量呈现大规模增长,问题就暴露出来了:
为了解决第一代架构面临的问题,团队制定了如下的策略,并形成了第二代应用架构(垂直应用架构)。
可以看到第二代架构解决应用级别的水平扩展扩展,经过优化后,该架构支撑了几十万用户的访问需求,在这一阶段有部分应用已经使用 Java 完成了 MVC 架构的重写。当然也存在一些问题。
为了解决第一代与第二代架构存在的问题,我们对平台进行了梳理优化。根据平台业务需要以及对第一二代架构的总结,我们确定了第三代架构的核心需求:
并以此为基础进行了平台的第三代架构的重构工作。
看第三代架构里面的组成,主要分为八个部分:
> 康威定律:任何组织在设计一套系统时,所交付的设计方案在结构上都与该组织的沟通结构保持一致。
在实践第三代架构时,我们对团队组织做了几个调整:
再谈谈整个团队的交付流程与开发模式,如果没有预先定义好,则很难让微服务架构发挥出真正的价值,下面我们先来看看微服务架构的交付流程。
使用微服务架构开发应用程序,我们实际上是针对一个个微服务进行设计、开发、测试、部署,因为每个服务之间是没有彼此依赖的,大概的交付流程就像上图这样。
设计阶段:
架构组将产品功能拆分为若干微服务,为每个微服务设计 API 接口(例如 REST API),需要给出 API 文档,包括 API 的名称、版本、请求参数、响应结果、错误代码等信息。
在开发阶段,开发工程师去实现 API 接口,也包括完成 API 的单元测试工作,在此期间,前端工程师会并行开发 Web UI 部分,可根据 API 文档造出一些假数据(我们称为“mock 数据”),这样一来,前端工程师就不必等待后端 API 全部开发完毕,才能开始自己的工作了,实现了前后端并行开发。
测试阶段:
这一阶段过程全自动化过程,开发人员提交代码到代码服务器,代码服务器触发持续集成构建、测试,如果测试通过则会自动通过Ansible脚本推送到模拟环境;在实践中对于线上环境则是先要走审核流程,通过之后才能推送到生产环境。提高工作效率,并且控制了部分可能因为测试不充分而导致的线上不稳定。
在以上交付流程中,开发、测试、部署这三个阶段可能都会涉及到对代码行为的控制,我们还需要制定相关开发模式,以确保多人能够良好地协作。
实践"绞杀者模式":
由于第三代架构跨度较大,并且面临了无法修改的.net遗留系统,我们采用绞杀者模式,在遗留系统外面增加新的Proxy代理微服务,并且在LB控制upstream的方式,而不是直接修改原有系统,逐步的实现对老系统的替换。
开发规范:
经验表明,我们需要善用代码版本控制系统,我曾经遇到一个开发团队,由于分支没有规范,最后一个小版本上线合代码居然化了几个小时,最后开发人员自己都不知道合到哪个分支。拿 GitLab 来说,它很好地支持了多分支代码版本,我们需要利用这个特性来提高开发效率,上图就是我们目前的分支管理规范。
最稳定的代码放在 master 分支上,我们不要直接在 master 分支上提交代码,只能在该分支上进行代码合并操作,例如将其它分支的代码合并到 Master 分支上。
我们日常开发中的代码需要从 master 分支拉一条 develop 分支出来,该分支所有人都能访问,但一般情况下,我们也不会直接在该分支上提交代码,代码同样是从其它分支合并到 develop 分支上去。
当我们需要开发某个特性时,需要从 develop 分支拉出一条 feature 分支,例如 feature-1 与 feature-2,在这些分支上并行地开发具体特性。
当特性开发完毕后,我们决定需要发布某个版本了,此时需要从 develop 分支上拉出一条 release 分支,例如 release-1.0.0,并将需要发布的特性从相关 feature 分支一同合并到 release 分支上,随后将针对 release 分支推送到测试环境,测试工程师在该分支上做功能测试,开发工程师在该分支上修改 bug。待测试工程师无法找到任何 bug 时,我们可将该 release 分支部署到预发环境,再次验证以后,均无任何 bug,此时可将 release 分支部署到生产环境。待上线完成后,将 release 分支上的代码同时合并到 develop 分支与 master 分支,并在 master 分支上打一个 tag,例如 v1.0.0。
当生产环境发现 bug 时,我们需要从对应的 tag 上(例如 v1.0.0)拉出一条 hotfix 分支(例如 hotfix-1.0.1),并在该分支上做 bug 修复。待 bug 完全修复后,需将 hotfix 分支上的代码同时合并到 develop 分支与 master 分支。
对于版本号我们也有要求,格式为:x.y.z,其中,x 用于有重大重构时才会升级,y 用于有新的特性发布时才会升级,z 用于修改了某个 bug 后才会升级。针对每个微服务,我们都需要严格按照以上开发模式来执行。
我们已经对微服务团队的架构、交付流程、开发模式进行了描述,下面我们聊聊归纳一下微服务开发体系。
Martin Flower的定义:
In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies
简单的说,微服务是软件系统架构上的一个设计风格,它倡导将一个原本独立的系统分成多个小型服务,这些小型服务都在各自独立的进程中运行,服务之间通过基于 HTTP 的 RESTful 轻量级 API 进行通信协作。被拆分的每个微服务围绕系统中的某项或一些耦合度较高的业务进行构建,并且每个服务都维护着自身的数据存储、业务开发、自动化测试案例以及独立部署机制。由于有了轻量级通信机制,这些微服务间可以使用不通的语言来编写。
微服务到底拆分到一个多大的粒度,更多时候是需要在粒度与团队之间找到一个平衡点,微服务越小,微服务独立性带来的好处就越多。但是管理大量微服务也会越复杂。基本上拆分需要遵循以下几个原则:
为了搭建好微服务架构,技术选型是一个非常重要的阶段,只有选择合适的"演员",才能把这台戏演好。
我们使用 Spring Cloud 作为微服务开发框架,Spring Boot 拥有嵌入式 Tomcat,可直接运行一个 jar 包来发布微服务,此外它还提供了一系列“开箱即用”的插件,例如:配置中心,服务注册与发现,熔断器,路由,代理,控制总线,一次性令牌,全局锁,leader选举,分布式 会话,集群状态等,可大量提高我们的开发效率。
工程结构规范
上图是我们实践中每个服务应该具有的项目组成结构。
其中:
API 网关实践
API 网关作为后端所有微服务和 API 的访问入口, 对微服务和 API 进行审计,流控, 监控,计费等。常用的 API 网关解决方案有:
在以上的方案里:
Consul 作为状态存储或者说配置中心(主要使用 Consul 的 KV 存储功能);Nginx 作为 API 网关, 根据 Consul 中 Upstreams 的相关配置,动态分发流量到配置的 Upstreams 结点;
Nginx 根据配置项, 连接到 Consul 集群;
启动的 API 或者微服务实例, 通过手工/命令行/发布部署平台, 将实例信息注册/写入 Consul;
Nginx 获取到相应的 Upstreams 信息更新, 则动态变更 Nginx 内部的 Upstreams 分发配置,从而将流量路由和分发到对应的 API 和微服务实例结点;
将以上注册和发现逻辑通过脚本或者统一的发布部署平台固化后,就可以实现透明的服务访问和扩展。
链路监控实践
我们发现,以前在单应用下的日志监控很简单,在微服务架构下却成为了一个大问题,如果无法跟踪业务流,无法定位问题,我们将耗费大量的时间来查找和定位问题,在复杂的微服务交互关系中,我们就会非常被动,此时分布式链路监控应运而生,其核心就是调用链。通过一个全局的ID将分布在各个服务节点上的同一次请求串联起来,还原原有的调用关系、追踪系统问题、分析调用数据、统计系统指标。
分布式链路跟踪最早见于 2010 年 Google 发表的一篇论文《 Dapper 》。
那么我们先来看一下什么是调用链,调用链其实就是将一次分布式请求还原成调用链路。显式的在后端查看一次分布式请求的调用情况,比如各个节点上的耗时、请求具体打到了哪台机器上、每个服务节点的请求状态,等等。它能反映出一次请求中经历了多少个服务以及服务层级等信息(比如你的系统 A 调用 B,B 调用 C,那么这次请求的层级就是 3),如果你发现有些请求层级大于 10,那这个服务很有可能需要优化了。
常见的解决方案有:
GitHub 地址:GitHub - naver/pinpoint: Pinpoint is an open source APM (Application Performance Management) tool for large-scale distributed systems written in Java.
对 APM 有兴趣的朋友都应该看看这个开源项目,这个是一个韩国团队开源出来的,通过 JavaAgent 的机制来做字节码代码植入(探针),实现加入 traceid 和抓取性能数据的目的。 NewRelic、OneAPM 之类的工具在 Java 平台上的性能分析也是类似的机制。官网:OpenZipkin · A distributed tracing system
GitHub 地址:GitHub - openzipkin/zipkin: Zipkin is a distributed tracing system
这个是 Twitter 开源出来的,也是参考 Dapper 的体系来做的。Brave 的 GitHub 地址: https://github.com/openzipkin/brave
这个组件通过实现一系列的 Java 拦截器,来做到对 http/servlet 请求、数据库访问的调用过程跟踪。然后通过在 Spring 之类的配置文件里加入这些拦截器,完成对 Java 应用的性能数据采集。GitHub 地址:GitHub - dianping/cat: Central Application Tracking
这个是大众点评开源出来的,实现的功能也还是蛮丰富的,国内也有一些公司在用了。不过 CAT 实现跟踪的手段,是要在代码里硬编码写一些“埋点”,也就是侵入式的。前面三个工具里面,如果不想重复造轮子,我推荐的顺序依次是Pinpoint—>Zipkin—>CAT。原因很简单,就是这三个工具对于程序源代码和配置文件的侵入性,是依次递增的。
我们的解决方案:
针对于微服务,我们在 Spring Cloud 基础上,对微服务架构进行了扩展,基于 Google Dapper 的概念,设计了一套基于微服务架构的分布式跟踪系统(WeAPM)。
如上图所示,我们可以通过服务名、时间、日志类型、方法名、异常级别、接口耗时等参数查询响应的日志。在得到的TrackID可以查询到该请求的整个链路日志,为重现问题、分析日志提供了极大方便。
断路器实践
在微服务架构中,我们将系统拆分成了一个个的微服务,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会出现因等待出现故障的依赖方响应而形成任务积压,最终导致自身服务的瘫痪。为了解决这样的问题,因此产生了断路器模式。
我们在实践中使用了 Hystrix 来实现断路器的功能。Hystrix 是 Netflix 开源的微服务框架套件之一,该框架目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix 具备拥有回退机制和断路器功能的线程和信号隔离,请求缓存和请求打包,以及监控和配置等功能。
断路器的使用流程如下:
启用断路器
@SpringBootApplication @EnableCircuitBreaker public class Application { public static void main(String[] args) { SpringApplication.run(DVoiceWebApplication.class, args); } }
代用使用方式
@Component public class StoreIntegration { @HystrixCommand(fallbackMethod = "defaultStores") public Object getStores(Map<String, Object> parameters) { //do stuff that might fail } public Object defaultStores(Map<String, Object> parameters) { return /* something useful */; } }
配置文件
资源控制实践
聊到资源控制,估计很多小伙伴会联系到 Docker,Docker 确实是一个实现资源控制很不错的解决方案,我们前期做调研时也对是否使用 Docker 进行了评审,但是最终选择放弃,而使用 Linux 的 libcgroup 脚本控制,原因如下:
为什么要有 group?
Linux 系统中经常有个需求就是希望能限制某个或者某些进程的分配资源。也就是能完成一组容器的概念,在这个容器中,有分配好的特定比例的 CPU 时间,IO 时间,可用内存大小等。
于是就出现了 cgroup 的概念,cgroup 就是 controller group,最初由 Google 的工程师提出,后来被整合进 Linux 内核中,Docker 也是基于此来实现。
libcgroup 使用流程:
安装
yum install libcgroup
启动服务
service cgconfig start
配置文件模板(以 memory 为例):
cat /etc/cgconfig.conf
看到 memory 子系统是挂载在目录 /sys/fs/cgroup/memory 下,进入这个目录创建一个文件夹,就创建了一个 control group 了。
mkdir test echo "服务进程号">> tasks(tasks 是 test 目录下的一个文件)
这样就将当前这个终端进程加入到了内存限制的 cgroup 中了。
总结一下,本文从我们微服务实践的背景聊起,介绍了微服务实践的工作方式,技术选型,以及相关的一些微服务技术。包括:API 网关、注册中心、断路器等。相信这些技术会给大家在实践中带来一些新的思路。
原文链接: 从 Spring Cloud 开始,聊聊微服务架构的实践之路