前言
CNCF 与 Cloud Native 这两个技术词汇最近频频走进了程序员的视野,一切和他能搭上边的软件意味着标准、开放、时尚,也更能俘获技术哥哥们的心;这篇文章不想去带你们重温这个词汇后面的软件体系,笔者以为单凭用到了这些开源软件,不等于咱们本身的软件就已是 Cloud Native,在使用哑铃和成为肌肉男之间还隔着科学使用和自律锻炼两道工序;在此,笔者想根跟你们聊聊让咱们的应用真正变得 Cloud Native 时的理论依据:微服务的十二要素。这篇文章也是先从做者自身项目的角度(一个基于 EDAS 的微服务架构),来阐述对这十二条要素的前两条 —— 仓库(Code Base)与依赖(Dependency)的理解java
Code Base 的原文释义是:"一份基准代码,多份部署,基准代码和应用之间老是保持一一对应的关系;不一样环境中的相同应用,应该来源于同一份代码"。个人理解有两个:git
一个应用,产生自同一个仓库。
一个仓库,只产生一个应用。
为何推演出这么两个结论呢?让咱们先看一个实际的项目。程序员
为何是一个应用?
给你们举一个一个仓库包含多个应用的反例,笔者本身的一个项目是一个的微服务的架构,和大部分的微服务架构同样,一开始是由一个单体的应用拆解而来,拆解以后,大体简化成四个服务:微服务网关(Gateway),两个后台服务(UserService, OrderService),后台管理控制台服务(Admin),简单的架构示意图以下:web
在拆分的过程一开始为了项目上线的减小风险,将拆分以后的应用都放在了一个 GIT 仓库中进行管理,同时也共用了同一个库。重构以后仓库的目录以下:json
~/workspace/java/app/ $ tree -L 2
.
├── README.md
├── service-api # 通用的 API 接口定义
│ ├── userservice-api # 服务 UserService 的声明
│ ├── orderservice-api # 服务 OrderService 的声明
│ ├── rpc-api # 远程服务调用相关的接口声明
│ ├── common-api # UserService 与 OrderService 都依赖的声明
| .....
├── service-impl # 对应 API 的相关具体业务实现
│ ├── userservice-impl
│ ├── orderservice-impl
│ ├── common-impl
| .....
├── web-app # Web 应用工程
│ ├── admin
│ ├── userservice
│ ├── orderservice
│ ├── gateway
一开始这些服务之间的发布和改动彼此都不受影响,这一过程持续了大约两个迭代,随着迭代的不断进行和新人的加入,后来咱们线上发现一个很奇怪的现象,每次用户进入刷新订单的地址列表的时候,会伴随这一次用户 Token 的刷新而致使用户被踢出,线上的排查过程在 EDAS 的分布式链路跟踪系统 EagleEye 的帮助下,立刻就定位到了出问题的代码:api
// User Service 中
public class User {架构
public void refresh() { // 刷新登陆 token }
}app
// Order Service 中
public class OrderUser extends User {分布式
// 函数少了一个字母,致使 refresh 调用了父类的 refresh public void refesh() { // 刷新地址列表 }
}
这个故障,我先邀请你们一块儿思考一下几个问题:ide
从编码角度,如何避免上述重写的方法由于名字误写形成故障?
从设计角度,OrderUser 和 User,是不是继承关系?
这个问题的根因是什么?
以上的几个问题中,第一个问题的答案,不少同窗都知道,就是使用 Java 自带的 Annotation @Override,他会自动强制去检查所修饰的方法签名在父子类中是否一致。第二个问题,须要从领域边界来讲,这是一个典型的边界划分的问题,即:订单中的用户,和会员登陆中的用户,是否是相同的“用户”?会员中的用户,其实只须要关心用户名密码,其余都是这个用户的属性;而订单中的用户,最重要的确定是联系方式,即一个联系方式,肯定一我的。虽然他们都叫作用户,可是在彼此的上下文中,确定是不同的概念。因此这里的 OrderUser 和 User 是不能用继承关系的,由于他们就不是一个 "IS A" 的关系。
__仓库共享__,加上没有多加思考的模型,致使依赖混乱;若是两个 User 对象之间代码上能作到隔离,不是那么轻易的产生“关系”,这一切或许能够避免。
为何是一个仓库?
严格意义上说,一个应用的全部代码都确定来源于不一样的仓库?咱们所依赖的三方库如(fastjson, edas-sdk 等)确定是来源于其余的仓库;这些类库是有确切的名称和版本号,且已经构建好的"制品",这里所说的一个仓库,是指源码级别的“在制品”。可能在不少的项目中不会存在这样的状况,以 GIT 为例,他通常发生在 submodule 为组织结构的工程中,场景通常是啥呢?在咱们这个工程中确实是有一个这样的例子:
为了解掉第一个问题,咱们决定拆仓库,仓库的粒度按照应用粒度分,同时把 common 相关的都拆到一个叫作 common 仓库中去;业务服务都好说,这里特殊处理的是 admin 应用,admin 是一个后台管理应用,变化频度特别大,须要依赖 UserService 和 OrderService 一大堆的接口。关于和其余仓库接口依赖的处理,这里除了常见的 Maven 依赖方式以外,还有另一个解决方案就是 git submodule,关于两个方案的对比,我简单罗列在了下表之中:
优势 缺点
Maven 依赖 可指定已固化的版本进行依赖 必须发布成二方包
Submodule 依赖 灵活、可直接共享代码库 变动不可控
我以为若是这个项目组只有一两我的的时候,不会带来协做的问题;上面的方案随便哪个都是不须要花太多时间作特殊讨论,挑本身最熟悉最拿手的方案确定不会有错,所谓小团队靠技术吗,说的就是这么个道理;咱们当时是一个小团队,同时团队中也有同窗对 submodule 处理过相似的状况,因此方案的选择上就很天然了。
后来随着时间的推移,团队慢慢变大,就发现须要制定一些流程和和规范来约束一些行为,以此保障团队的协做关系的时候;这时候发现以前靠一己之力打拼下来的地盘在多人写做下变得脆弱不堪,尤为是另一个 submodule 变成一个团队进行维护的时候,submodule 的版本管理几乎不可预期,并且他的接口变更和改动是彻底不会理会被依赖方的感觉的,由于他也不知道是否被依赖;长此以往,你就会明白什么叫作你的项目被__腐化__了。简单理解__腐化__这个词就是,你已经开始惧怕你所作的一切改动,由于你不知道你的改动是否会引来额外的麻烦。从这个角度也能够去理解为何一门语言设计出来为何要有 __private__、__public__这些表示范围的修饰词。正由于有这些词的存在,才让你的业务代码的高内聚成为的有可能,小到设计一个方法一个类、再引伸到一个接口一个服务、再到一个系统一个仓库,这个原则始终不变。
上述问题带来的解法很简单,就是变成显示依赖的关系,所谓显示依赖是指的两个依赖之间是肯定的。什么是肯定的?肯定 == No Supprise !对,无论何时,线上仍是线下,我依赖你测试环境的接口返回是一个整数,到了线上,返回的也必须是一个整数、不能变成浮点数。而让肯定性变得可行的,不是君子协定;只能是一个版本依赖工具。好比说 Java 中的 Maven 正式的版本依赖。
结语职责内聚、依赖肯定,是咱们的应用变得真正 Cloud Native 的前提。没有了这些基本的内功,懂的开源软件再多、对微服务栈再熟悉,也会有各类意想不到的事情出来,试想一下,若是应用的职责处处分散,那到时候扩容到底扩谁呢?若是依赖方变得及其不肯定,谁又来为每次发版的不肯定的成本买单?Be Cloud Native,请从应用代码托管的住所开始。