本文由 网易云 发布。java
做者:刘超,网易云解决方案架构师mysql
在不少微服务化的文章中,不多会把持续集成放在第一篇,由于大多数的文章都会将如何拆的问题,例如拆的粒度,拆的时机,拆的方式。程序员
为何须要拆呢?由于这是人类处理问题的本质方式:将一个大的复杂问题,变成不少个小问题解决。redis
因此当一个系统复杂到必定程度,当维护一个系统的人数多到必定程度,解决问题的难度和沟通成本大大提升,于是须要拆成不少个工程,拆成不少个团队,分而治之。spring
然而当每一个子团队将子问题解决了,整个系统的问题就解决了么?你能够想象你将一辆整车拆成零件,而后再组装起来的过程,你就能够想象拆虽然不容易,合则更难,须要各类标准,各类流水线,才能将零件组装称为车。sql
咱们先来回顾一下拆的过程。数据库
最初的应用大多数是一个单体应用:编程
单体应用json
一个Java后端,后面跟一个数据库,基本上就搞定了。后端
随着系统复杂度的增长,首先Java程序须要作的是纵向的拆分。
纵向拆分
服务化架构
在高并发状况下,仅仅纵向拆分还不够,于是须要作真正的服务化。
一个服务化的架构如图所示。
经过简单的描述,你们能够发现,从一个简单的单体应用,变成如此复杂的微服务架构,除了关心怎么拆的问题,还必须关注:
答案固然就是集成,从一开始就集成,而且不断的集成,反复的将拆分的模块从新组合,看看是否可以顺利组合起来,而且保证功能的不变。
要是不没事儿就组合一下,天知道几个月之后还能不能合的起来。
别忘了程序是人写的,你和你媳妇长时间不沟通都对不上默契,别说两个程序员了。
集成就是在一块儿。
集成的逻辑
为何须要一个统一的代码仓库Git来作代码管理呢?是为了代码集成在一块儿。
为何须要进行构建build呢?就是代码逻辑须要集成在一块儿,编译不出错。
为何要单元测试呢?一个模块的功能集成在一块儿可以正确工做。
为何须要联调测试Staging环境呢?须要将不一样模块之间集成在一块儿,在一个类生产的环境中进行测试。最终才是部署到生产环境中,将全部人分开作的工做才算真正的合在了一块儿。
持续集成解决的问题
持续集成就是制定一系列流程,或者一个系列规则,将须要在一块儿的各个层次规范起来,方便你们在一块儿,强迫你们在一块儿。
这些概念都容易混淆,他们之间是什么关系呢?
持续
集成,持续交付,持续部署,敏捷开发,DevOps的关系
敏捷开发Agile是一种开发流程,是一种快速迭代的开发流程,每一个开发流程很是短,长到一个月,短到两个星期,就会是一个周期,在这个周期中,天天都要开会同步,天天都要集成。正是由于周期短,才须要持续的作这件事情,若是一个开发周期长达几个月,则不须要持续的集成,最后留几个星期的集成时间一块儿作也是能够的,可是这样就不能达到互联网公司的快速迭代,也是咱们经常看到传统公司的作法。
持续集成每每指对代码的提交,构建,测试的过程,也就是上述的在一块儿的过程。
持续交付是指将集成好的交付物,例如war,jar,或者容器镜像,部署在联调环境,或者预发环境的过程。
持续部署是指将交付物持续部署在生产环境的过程。
咱们常说CICD,CD有时候指的是Delivery交付,有的是指Deployment部署,对于非生产环境,自动部署是没有问题的,对于生产环境,每每仍是须要有专人来进行更为严肃的部署过程,不会彻底的自动化。
接下来就是DevOps,DevOps不仅是CICD,除了技术和流程,还包含文化。例如容器化带来的一个巨大的转变是,原来只有运维关心环境的部署,不管是测试环境,仍是生产环境,都是运维搞定的,而容器化以后,须要开发本身写Dockerfile,本身关心环境的部署。由于微服务以后,模块太多了,让少数的运维可以很好的管理全部的服务,压力大,易出错,然而开发每每分红不少的团队,每一个模块本身关心本身的部署,则不易出错,这就须要运维一部分的工做让研发来作,须要研发和运维的打通,若是公司没有这个文化,研发的老大说咱们不写Dockerfile,则DevOps是搞不定的。
持续集成的流程
这是一个持续集成的流程,可是运行起来更加的复杂。
首先,项目开发的流程使用的是Agile,用常见的Scrum为例子。
Scrum
天天早上第一件事情,就是开站会standup meeting,为何要站着呢?由于时间不能太长,微服务的一个模块,大概须要5-9人的团队规模,若是团队规模太大了,说明服务应该进行拆分了,这个团队规模,是可以保证比较短的时间以内过完昨天的状态的。
必定要你们一块儿开,而不要线下去更新Jira,虽然看起来同样,可是执行起来彻底不同。只有你们一块儿开,一块儿看燃尽图,一块儿说我昨天作了什么,今天打算作什么,有什么阻碍,才可以让你们都了解状况,不要指望你们会去看别人的Jira,经验告诉你,不会的。
并且这个站会对于开发是比较大的压力,例如你的一个功能block了依赖方的开发,在会议上会暴露出来,你们都知道这件事情了,一天block,两天block,第三天你都很差意思去说了,这会强迫你将大任务,好比原来写1周干一件什么事情,写成小时级别,这样天天你都有的说,昨天完成了一个task,而不是周只在那里说干一样一件事情,并且一旦有了block,team lead会知道这件事情,会帮你赶忙解决这个事情,推动整个项目的进展。让一个技术人员在团队面前认可这件事情我尝试了几天,的确搞不定了,也是一种压力。
站会中的内容其实在前一天晚上就要开始准备了。
持续集成要求天天都提交代码,这样才能下降代码集成的风险,不能埋头写一周一块儿提交,这样每每集成不成功。怎么样才能鼓励天天都提交代码呢?一个就是次日的站会,你这个功能代码提交了,单元测试经过了,次日才能说作完了,不然不算,这就逼得你,将大任务拆成小任务,天天都屡次提交。
并且Git的提交方式,是后提交者有责任去merge,保证代码的编译经过和测试经过,你会发现,若是你不及时提交,等你改了一大片代码,别人都提交完了,这一大片的冲突都是你来merge,测试用例不经过的你来fix,因此逼的你有一个小的功能的改动,就尽早提交,pull一下发现没有人提交,赶忙提交。
提交不是立刻进入主库,而是须要代码审核,这是把控代码质量的重要的环节。
代码质量的控制每每每一个公司都有文档,甚至你能够从网上下载一篇很长很长的Java代码规范。可是咱们经常看到的例子是,规范是有,可是虱子多了不咬人,规范太多的,谁也记不住,等于没有规范。
因此建议将复杂的规范经过项目组内部的讨论,简化为简单的10几条军规,深刻人心,你们都容易记住,而且容器执行。
代码审核每每须要注意下面的几方面:
固然还有一些不容易一眼看出来的,能够经过一段时间经过统一的代码review,来修改这些问题:
代码审核完毕以后提交上去以后,一个是要经过静态代码审查,能够发现一些可能带来代码风险的问题,例如异常过于宽泛等。
在就是要经过单元测试。咱们应该要求每一个类都要有单元测试,而且单元测试覆盖率要达到必定的指标。单元测试要有带Mock的模块内的集成测试。
在编译过程当中会触发单元测试,单元测试不经过,已经代码覆盖率,都会统计后发邮件,抄送全部的人,这对于研发来说又是一个压力。
当有一天你的提交break掉了测试,或者代码覆盖率很低,则就像通报批评同样,你须要赶忙去修改。
单元测试完毕以后,就会上传成果物,或者是war或者是jar,通常会用nexus,由于有版本号,有md5,能够保证安装在环境中的就是某个版本的某个包,咱们还遇到过有使用FTP的,这样一个是很难保证版本号的维护,升级和回滚比较难弄,另外一个是没有md5,极可能包不完整都有可能的,并且一旦发生,很难发现。
若是使用了容器,则还须要编译Dockerfile,使用Docker镜像做为交付,可以实现更好的环境一致性,保证原子的升级和回滚。
天天下班前,当天的代码须要提交到库中去,晚上会作一次统一的环境部署和集成测试。
天天晚上凌晨,会有自动化的脚本将Docker镜像经过编排部署一个完整的环境,而后跑集成测试用例,集成测试用例应该是基于API的,不少的公司是基于UI的,这样因为UI变化太快,还有UI不能覆盖全部的场景,因此仍是建议UI和API分离,经过API进行集成测试,有了天天的测试,才能保证天天晚上的版本都是能够交付的版本,也保证咱们微服务拆分的时候,尽管改了不少,不会由于新的修改,破坏掉原来可以经过的测试用例,保证不会有了新的,坏了旧的。
这个集成测试或者叫回归测试天天晚上都作,都是在一个全新的环境中,这就是持续部署和持续交付。
若是某一天测试不经过,则会发出邮件来,是由于当天谁的哪一个提交,致使测试不经过,抄送全部人,这是另外一个压力。
因此次日的站会上,昨天你完成了哪些功能,是否提交了,是否完成了单元测试,是否经过了集成测试,就都知道了,你须要给你们一个解释,而后进入到新一天的开发。
到了两周,一个周期完毕,能够上线到生产环境了,能够通知有权限的运维进行操做,可是也是经过自动化的脚本进行部署的。
这就是整个过程,层层保证质量,从中能够看到,敏捷开发,持续集成,持续交付,持续部署,DevOps是互相联系的,少了任何一个,整个流程都玩不转。
代码结构每每包括:
若是使用Dubbo RPC,则API接口每每在一个单独的jar里面,被服务端和客户端共同依赖,可是使用了springcloud的restful方式就不用了,只要在各自的代码里面定义就能够了,会变成json的方式传递,这样的好处是当jar有多个版本依赖,须要升级的时候,关系很是复杂,难以维护,而json的方式比较好的解决了这个问题。
这个模块提供了哪些接口,只要到API接口这个package下面找就能够了。由于不管是Dubbo仍是springcloud,接口的调用都会重试,于是接口须要实现幂等。
访问外部服务的包,这将全部对外的访问独立出来,好处一是能够抽象出来,在服务拆分的时候,可能会用到,例如原来支付的逻辑在下单的模块中,要讲支付独立出来,则会有一个抽象层,涉及到老的支付方式,仍是调用本模块中的逻辑,涉及到新接入的支付方式使用远程调用,有了这一层方便的多。好处二是能够实现熔断,当被调用的服务不正常的时候,在这里能够返回托底数据。好处三是能够实现Mock,这样对于单元测试来说很是好,不用依赖于其余服务,就能够本身进行测试。
DTO和访问数据库的包,看到了这些数据结构,会帮助程序员快速掌握代码逻辑,不知道你们有没有这个体验,你去看一个开源软件的代码,首先要看的是他的数据结构,数据结构和关系看懂了,代码逻辑就比较容易懂了,若是数据结构没看懂,则光看逻辑,就容易云里雾里的。
还有就是核心的代码逻辑和对接口的实现。在这里面是软件代码设计的内功所在,可是却不是流程可以控制的。
上面也说过了,Dubbo和Springcloud会对接口进行重试,于是接口须要保持幂等。也即屡次调用,应该产生一致的结果,例如转帐1元,由于调用失败或者超时重试的时候,最终结果还应该是转帐1元,而非调用两次变成转帐2元。
对于代码的设计,这里常说的就是SOLID原则。
以上是模块内部常见的设计原则,对于模块之间,则是对于云原生应用常说的十二要素原则。
十二要素原则
在代码仓库中,还须要管理的是配置文件,每每在src/main/resource下面。
配置的管理原来多使用profile进行管理,对于dev, test, production使用不一样的配置文件。
然而当配置很是多的时候,比较的痛苦,并且配置不断的修改,每次上线各类配置须要仔细的核对,眼睛都花了,才敢上线。
咱们能够将配置分为下面的三类:
在梳理配置的时候,能够按着三类归类,分门别类管理。
在使用了容器以后,不少的内部配置项可固化在配置文件中,放在容器镜像中,须要启动的时候修改的,则经过环境变量,在启动容器的时候,在编排文件中进行修改。
依赖的内部服务的地址,在容器平台kubernetes里面,能够经过配置服务名进行服务发现,仅仅在配置文件中配置名称就能够了,不用配置真实的地址,kubernetes能够根据不一样的环境,不一样的namespace自动关联好,大大简化了配置。固然也能够用服务中心Dubbo和Springcloud作内部服务的相互发现。
依赖的外部服务的地址,例如mysql,redis等,每每不一样的环境不一样,也能够经过配置kubernetes外部服务名的方式进行,而不用一一核对,担忧测试环境连上了生产环境的IP地址。
还有一些集中配置项,须要动态修改的,例如限流,降级的开关等,须要经过统一的配置中心进行管理。
代码能够很好的版本化,应用也能够用镜像进行原子化的升级和回滚。
惟一比较难作到的就是数据库如何版本化管理。
有一个开源工具 flyway 能够比较好的作这件事情。
在代码中,flyway须要有如下的结构:
在数据库中,flyway会自动增长SCHEME_VERSION表。
当服务启动的时候,java类的migration方法会被调用,它会按照指定路径中sql语句的版本号进行排序而且按照这个排序去执行,当每个sql文件被执行后,元数据的表就会按照格式进行更新。
当服务重启的时候,Flyway 再次扫描sql的时候,它就会检查元数据表中迁移版本,若是要执行的迁移脚本的版本小于或者等于当前版本,Flyway将会忽略,再也不重复执行。
可是flyway历来不解决数据库升级和回滚的代码兼容性问题。
太多的人问这个问题了,代码能够灰度发布,数据库咋灰度?代码升级了,发现不对能够回滚,数据库咋回滚。
若是能够停服的话,天然是使用数据库快照备份的方式进行回滚了。
若是不能够停服,没办法,只有在代码层面作兼容性。每次涉及数据库升级的都是大事情,代码固然应该有个开关,保证随时能够切回原来的逻辑。
了解网易云:
网易云官网:https://www.163yun.com/
新用户大礼包:https://www.163yun.com/gift
网易云社区:https://sq.163yun.com/