ZStack源码剖析:如何在百万行代码中快速迭代
本文首发于泊浮目的专栏:https://segmentfault.com/blog...
前言
ZStack是下一代开源的云计算IaaS(基础架构即服务)软件。它主要面向的是未来的智能数据中心,通过提供的API来管理包括计算、存储和网络在内的数据中心的各种资源。跟OpenStack相比,ZStack具有易用、稳定、灵活、超高性能等特点。其单管理节点可以管理1万台物理机规模集群,多个管理节点构建的集群可以做到使用一个数据库、一套消息总线管理10万台物理机、数百万个虚拟机节点、并发处理数万个API。
以下是ZStackV2.2的服务架构图
官网地址:http://www.zstack.io/核心开源引擎ZStack GitHub:https://github.com/zstackio/z...
ZStack-Utility GitHub:https://github.com/zstackio/z...
阅读源码如果不想使用IDE,建议配合https://github.com/buunguyen/...。
本文将对核心引擎-ZStack的源码进行剖析。在ZStack官网上我们可以看到其每个版本的发布都是携带了许多的新特性。在笔者看来,能够快速迭代的原因首先是来自于每位工程师的辛勤付出。除此之外,因其还有些软件工程领域中沉淀下来的最佳实践:
- 良好的架构设计
- 覆盖较为全面的测试
- 恰当好处的使用设计模式
良好的架构设计
异步架构
Iaas的核心应该做的是管控层,而不是数据层。故ZStack仅仅也是做出一些“决策”而已——在设计系统的时候,应不考虑在这些决策的执行上消耗大量的资源。在面对大量请求或者“决策”的时候,如果使用多线程来处理阻塞式IO模型时会遇到一些问题:
- 阻塞模型的吞吐量受到线程池大小的限制;
- 创建并使用许多线程会耗费额外的时间用于上下文切换,影响系统性能。
而非阻塞、异步的消息驱动系统可以只运行少量的线程,并且不阻塞这些线程,只在需要计算资源时才使用它们。这大大提高了系统的响应速度,并且能够更高效地利用系统资源。
故,ZStack采用了异步架构,分别由三个部分组成:
- 异步消息
- 异步方法
- 异步HTTP 请求
如果在系统中的一部分采用异步设计,是不行的。这样还是会因为同步而没法享受异步带来的“福利”。故此整个系统都得采用异步架构。
- 相对的,开发者们在编写异步代码的时候得格外小心。
- 在系统设计中,异步调用可以减少系统在IO上出现瓶颈的可能性。
扩展链接 : ZStack--可拓展性的秘密武器1:异步架构
无状态服务
在ZStack
中,每一个服务都是独立存在的。为了方便的管理更多的物理机,ZStack
推荐采用集群部署MN。但这样就会遇到一个问题,不同MN下面有着不同的几个服务存在,在这里我们设其为X个服务。在10个MN部署的情况下,可能就是10X个服务。那么在一个资源需要操作时,我需要发送向对应的MN。那么如何找到那个MN呢?最直观的想法就是在各个MN中保存相应的“服务表”,这即是一种状态。那么在分布式系统中,采用有状态的服务绝对不是一个好的选择,它会严重影响系统的扩展性。ZStack
巧妙的采用了一致性哈希算法+MQ解决了这个问题。
- 这在系统设计中实为是一种使用一致性hash技术的负载均衡
扩展链接:ZStack—可拓展性秘密武器2:无状态的服务
无锁架构
解决并发的问题不一定要用显式的锁,也可以对同一资源做操作的任务做成队列使其串行执行。
注意:并发 != 并行
扩展链接:ZStack--可拓展性秘密武器3:无锁架构
松耦合架构
项目模块化
在Intellij
中打开ZStack的代码,会发现大多数目录底下都会有一个pom.xml
文件,ZStack采用了模块化项目。模块化的好处在工程实践中不言而喻的,比如:
- 可以在不影响整个系统的情况下替换某个模块
- 开发者只要专心的在自己的模块中工作即可
- 减少系统耦合度,提高内聚,减少资源循环依赖,增强系统框架设计
- ...
下面来看一下ZStack
中代码的结构:
代码结构
截图于2017.9.22
名称 | 简介 |
---|---|
build | 用于Java部分的编译、打包、部署等 |
conf | 配置文件及SQL文件的放置;Spring Service配置存放;持久化文件配置 |
core | 核心模块。实现系统的核心功能——包括数据库、消息总线、工作流实现等等 |
coregroovy | ZStack的最新测试采用了Groovy,这里是对测试库做的支持 |
header | 消息以及Entity的定义 |
plugin | 顾名思义。其中不少组件都以插件化开发,提供较高的灵活性 |
sdk | 测试库使用的SDK |
simulator | 对于测试库支持的又一模块,主要用户simulator agent的行为 |
testlib | 测试库 |
test | 测试模块 |
工具类 | 工具包。目前仅仅支持了doc生成 |
utils | 代码中使用的工具类 |
其他 | 功能实现模块 |
通过MQ来解耦合
在ZStack
中,每个功能实现模块都会被称为服务——一个独立的服务。各个服务之间的通信由MQ来承担。这就像是传统的CSE,C和E是不耦合的,通过S来交互。同样的,一个服务需要向另一个服务发起调用,只需往消息总线发送消息,并指定这个服务ID(Service ID)即可。如果某个服务的代码需要大量重构或者做成微服务,只要提供相同的服务并注册到MQ上就可以了。这就是事件驱动架构(Event Driven Architecture)的一种典型实现。
CSE:Controller、Service、Entity。注:称作Domain或者Model都是不专业的。Domain是一个领域对象,往往我们再做传统Java软件web开发中,这些Domain都是贫血模型,是没有行为的,或是没有足够的领域模型的行为的,所以,以这个理论来讲,这些Domain都应该是一个普通的entity对象,并非领域对象,所以请把包名改为:com.xxx.entity。
举个简单明了的例子。如果每个对象的行为都是通过消息来决定的(比如一个方法需要message得到回复后才能do something...),那么这个对象仅仅对消息总线产生了依赖。在测试中,将会发挥巨大的威力——我们只需要改变handle message处的行为,就可以使一个对象行为做出相应的变化。这样可以使mock的单位变得更小,同时也可以变得更加灵活。
试想如果通过函数调用:
//方法a中的代码 xxService.method1(); xx2Service.method2();
在测试中该如何解耦?但如果通过MQ——即一个消息来调用xxService.method1()
,那么方法a对xxService就没有了直接的依赖。
使用Spring
不了解Spring的人可以看:看起来很长但还是有用的Spring学习笔记
在代码中,每当我们New出一个对象时,这个模块便对这个对象产生了依赖。当我们需要测试的时候就不得不去Mock它。当依赖的对象or Field 有成千上万个的时候,这就是一场灾难了。代码变得愈发不可测,坑就越多,开发者在扩展or维护项目的时候就会愈发的乏力。这就像是我们之前提到的MQ,服务1->MQ->服务2
,由于中间隔了一个MQ,于是服务1和服务2没有必然的关系。同样的,从对象1->调用->对象2
到对象1->调用->Spring提供的IOC容器->对象2
,这样使对象与对象之间也没有了直接调用关系,对象1只要知道它要调用的对象实现了其需要的Interface
就是可以调用的。
除了Autowired
的正确使用姿势。在ZStack中,还有一类很有意思的代码,一般称之为xxxExtensionPoint
。其本质就是定义一个接口,然后其实现类作为Bean通过XML注册到IOC中。在需要使用的时候,通过Spring获取到所有实现该接口的对象,调用其函数。这样就会使代码变得非常的灵活。
例如,ZStack
分为多个版本——开源版、企业版、混合云版等。如果一个服务在不同版本中的处理逻辑需要稍许不同,那么就可以在开源版的代码中注册一个接口,在另一个版本的服务中实现该接口。这样也不会影响到开源版的原有逻辑。从模块上看我们代码的是松耦合并且无法直接调用的,但是在内存中,却是可以调用得到的。
覆盖较为全面的测试
在ZStack
中
- 开发者Fix每一个Bug都是需要补充相应的Case;
- 每一个Feature在进去之前更会由开发工程师与QA工程师同时制定测试场景并Cover;
- 每一个PR(pull request)进去之前都会通过PR系统跑过所有的Case,以防止Bug的Regression;
由于ZStack
源码做到了一定的解耦合(上述提到)与无状态,使得集成测试得以进行。
其首席架构师Frank.Zhang曾说过:我们开发者在写代码的时候往往就应该考虑该怎么写测试了。想了解ZStack的测试框架,可以看: ZStack WiKi :管理节点基于模拟器的Integration Test框架
恰当好处的使用设计模式
在ZStack
中,设计模式有较为良好的实践。笔者有机会将会在之后的系列文章分析其中的典型案例以及在代码中使用极其频繁的核心工具。