从1到2000个微服务,史上最落地的实践云原生25个步骤
Posted popsuper1982
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从1到2000个微服务,史上最落地的实践云原生25个步骤相关的知识,希望对你有一定的参考价值。
在上一篇文章以业务为核心的云原生体系建设中,我们给出了一张云原生体系建设的总图,并且从演进的角度讲述了云原生落地的三个阶段。
有的同学留言说,还是不够落地呀,所谓“听了很多道理,还是过不好这一生”,同理“看了很多文章,还是落地不好云原生”。
从今天这一篇开始,我们开始落地篇,从此会进入大量的技术细节,学了落地篇,基本可以回去编码落地了。
其实我们在很多的技术大会上,看到的都是分层架构图,就像上一节我们分的六个层次一样,这容易给希望落地云原生的企业造成误解,因为大部分公司的云原生体系的建设都不是按层次来建设的,不会IaaS完全建设完毕,再建设PaaS,一定是根据业务的演进,交替迭代出来的。
一定是业务遇到问题了,需要底层的技术,底层技术提升了,促进业务的发展。如下图所示,应用层和技术底座之间的良性循环,才是云原生这个词的本质,我把他称为云原生怪圈。
虽然按照这个顺序来讲,你会感觉体系有点乱,但是才是最落地的演进路径。而沿着这个演进路径发展,你才能感受到“云原生”三个字的准确含义。
很多企业应用层服务化或者微服务化,而技术底座没有跟上,从而系统陷入混乱,没日没夜加班,却怪服务化不好,另外一些企业花了大价钱卖了一个技术底座平台,但是应用层没有跟上,无法促进业务发展,嫌技术底座白花了钱,这两个误区都在于没有沿着这个怪圈逐渐演进。
为了解决体系混乱的问题,所以在展开落地篇之前,先来一个总论,整体梳理一下。
对于每一个落地的步骤,我们要经常问自己下面几个问题,我们成为四项基本问题吧:
遇到什么样的问题?
应该采取什么样的技术解决这个问题?如何解决这个问题的?
这个技术的实现有很多种,应该如何选型?
使用这个技术有没有最佳实践,能不能形成企业的相关规范?
整个落地的演进过程要有一个起点:
在应用层,是一个单体应用,主要包含Online服务,他是对外提供服务的,Offline服务,他是一些定时任务的,MS服务,他是一些后台服务,这基本是一个单体应用,因为对外服务是Online,接下来的拆分也是围绕这个服务展开。
对于数据库,使用的是Oracle,部署在物理机上
在基础实施层,用的是Vmware虚拟化
部署上线方式是脚本化
接下来,我们要开始演进了。
就像前面我们讲过,构建中台的企业都是有一定积累的企业,而非创业企业,因而不可能没有任何计划的的盲人摸象,这是很多企业的管理层不允许的。所以在动手之前,要有一个总的地图,就是规划,当然真正云原生演进的时候,我们不建议使用瀑布模型,而是迭代模型,但是迭代模型不代表漫无目的的迭代,而是地图要在心中,所以落地的第一个阶段是规划。
第一:规划——在架构委员会领导下的梳理与规划
首先,组织架构先行:成立架构师组。哪怕人很少,只有两三个人,但是这个组织一定要有,这是将来的军机处,是架构委员会的发起者,是横向拉通各个组,并落地规范与最佳实践的负责人。
有了人以后,接下来,我们应该从业务架构出发:进行业务流程和领域梳理。在云原生怪圈的循环中,我建议从业务层出发,因为IT是为业务服务的,只有业务方的需求,才是真正应该服务和花钱建设的地方。
(步骤1) 从1到2:领域驱动建模
从标题你可以看出,这是按照领域驱动设计的方式来进行规划的。
这里很多技术人员都会犯的错误是,从数据库出发,看数据库结构如何设计的,按照数据库最容易拆分的方式进行拆分。这样是不对的,没有站在业务的角度去考虑问题。应该借鉴领域驱动设计的思路,从业务流程的梳理和业务领域的划分出发,来划分不同的服务,虽然最后映射到数据库可能会拆分的比较难受,但是方向是对的,只有这样才能适应未来业务的快速变化。
我个人认为,方向比手段要重要,方向对,当前痛一点没什么,但是当前不痛,方向错了,还是解决不了问题。
首先,我们要做的是梳理业务流程,通过这个流程,可以了解业务运行的逻辑,使得技术人员对于业务模式有所理解。
这里主要梳理的是电商业务,也许你对电商业务不是非常感兴趣,但是仍然建议你把这一节看完。因为后面在架构设计的部分,都要基于对于业务流程的理解。其实作为一个架构师,越是到后期,越是要距离业务要近,而不仅仅是单点做一部分的技术。电商平台是一个典型的应用云原生架构的案例。虽然你当前所在的行业。有可能是金融,制造,或者零售。对于方法论来讲,你总能从电商平台的流程和业务模式中,找到类似的部分。他山之石,可以攻玉。
下面是电商平台的一个典型的业务流程图。
具体的业务流程,我们另外解析,这里不赘述。
接下来就是,划分业务领域。梳理好了业务流程,我们就可以根据他来划分业务领域。这里不必严格按照DDD的图,为了方便,我这里用了脑图。
另外,对于每一个服务,我都起了名字,这里先不用管它,后面自然有用。
在实践中,你在这个阶段可能没必要划分的这么细。这些服务都是在后期的逐渐拆分过程中演进出来的。
那接下来后台技术部门不应该闷头开始就按这个拆了?其实不是的!
传统的领域驱动设计是瀑布式的模型,经过长时间的闭门讨论,贴纸条,最终输出各种架构图,但是当落地的时候,发现情况变了,因为领域知识从业务部门到技术部门的传递一定有信息的丢失,这也是DDD落地被诟病的地方,就是业务方规划的时候是这样说的,落地来需求的时候,却是另外一种说法,导致根据DDD落地好的领域,接需求接的更加困难了。
所以一个更加落地的方式是,随着新需求的不断到来,渐进的进行拆分,而变化多,复用性是两大考虑要素。
所以赵本山说,不看广告,看疗效。对于服务拆分,DDD是一个完整的地图,但是具体怎么走,要不要调整,需要随着新需求的不断到来,渐进的进行拆分,DDD领域设计的时候,业务方会说不清,但是真的需求来的时候,却是实实在在的,甚至接口和原型都能做出来跟业务看。
这么说有点虚,我们举个现实的例子。例如按照领域的划分,对于电商业务来讲,一个单体的电商服务,应该拆分成下面这些服务。
需求到来的时候,技术部门是能感受到上一篇文章讲过的架构耦合导致的两个现象:
耦合现象一:你改代码,你要上线,要我配合
耦合现象二:明明有某个功能,却拿不出来
第一个现象就是变化多,在业务的某个阶段,有的领域的确比其他的领域有更多的变化,如果耦合在一起,上线,稳定性都会相互影响。例如图中,供应链越来越多,活动方式越来越多,物流越来越多,如果都耦合在Online里面,每对接一家物流公司,都会影响下单,那太恐怖了。
第二个现象就是可复用,例如用户中心和认证中心,根本整个公司应该只有一套。
在《重构:改善代码的既有设计》有一个三次法则——事不过三,三则重构。
这个原则也可以用作服务化上,也即当物流模块的负责人发现自己接到第三家物流公司的时候,应该就考虑要从原来的单体应用中拆分出来了。另外就是,当有一个功能,领导或者业务方发现明明有,还需要再做一遍,这种现象出现第三次的时候,就应该拆分出来作为一个独立的服务了。
这种根据业务需求逐渐拆分的过程,会使得系统的修改一定是能够帮助到业务方的,同时系统处在一种可控的状态,随着工具链,流程、团队、员工能力的增强慢慢匹配到服务化的状态。
那你可能会问,如果有个系统,里面的代码已经垃圾的一塌糊涂,我都看不下去了,但是暂时没有新需求进来,那应不应该拆分呢?
不!没有需求不拆!再烂也不拆!我们不是要解决所有的腐化问题,别按DDD的理想情况来。
至此,理论的划分基本就结束了,接下来,咱们就要动代码啦!
第二:试点——选一个项目试点,汲取经验,培养团队,建立规范
(步骤2) 从1到2:选取试点业务进行拆分
我们来选择一个领域将他拆分出来,我们就选择最核心的交易领域吧。
在上面这个庞大的单体应用中,我们将订单拆分出来,需要考虑以下几个事情:
在诸多的功能中,将属于订单的功能梳理出来
梳理订单和其他模块之间的关系,从而知道将来会和哪些模块进行相互调用
将订单模块中的不同功能也进行划分,虽然目前不用拆分,为了将来拆分做准备
第一件事情,我们从上面的图中可以看出,黄色底色的就是属于订单的功能。
第二件事情,我们需要梳理一个关系图,如下所示。
第三件事情,将订单内部的功能也划分一下,因为将来可能会因为性能问题,进一步的划分。
接下来马上应该找拆分了,先别忙,你会不会拆出一堆Bug来,让原来单体应用玩儿挺好的,后来Bug成堆呢?这也是经常服务化被诟病的地方,Bug更多更不稳定。
所以首先要有持续集成,这是云原生架构的基石。
(步骤3) 从1到2:持续集成平台建设
持续集成就是制定一系列流程,或者一个系列规则,将需要在一起的各个层次规范起来,方便大家在一起,强迫大家在一起。
接下来,我们一起来搭建一个持续集成的系统。
上面的这幅图呢,是一个持续集成的总体流程图,里面包含非常多的工具,这些工具呢,有非常多的选型。下面的这个脑图,就总结了持续集成中所常用到的主流工具。你可以根据自己公司的情况。来选择合适的工具。
系统的搭建只是其中一部分,要做好持续集成,还需要有规范,还需要配合敏捷开发的流程。
持续集成的规范都有哪些呢?
工程名规范:这个名称非常关键,以后在公司内部所有的系统中,只要看到这个名称,无论是开发,运维,发布人员,QA任何角色,在持续集成平台,云平台,容器平台,微服务平台等任何平台,都以这个名称为准绳,这样所有人看到名称,马上就知道这个工程什么功能以及应该如何操作他,如果是一个核心交易的前台工程,就应该小心一点,如果是一个后台的管理系统,则小的功能白天也可以发布。
代码结构规范:代码结构规范希望达到的效果是所有人打开一份代码,都能看到熟悉的结构,以及有大概的思路如何入手,这在快速迭代的场景下很重要,因为人员也可能在不同的团队频繁的调动。
代码设计规范:包括命名规范,例如package, class, interface,变量的命名;包括注释规范,我们要根据注释生成文档,这在注册中心和知识库还会提到;资源管理规范,例如对于文件,线程,网络,数据库连接等的使用;异常管理规范,如何捕捉异常和处理异常;日志规范,日志的分级,敏感信息过滤,格式规约;多线程规范,并发,加锁,线程安全;数据库操作规范;编码规范,例如方法长度,类长度,数据模型定义,相等判断,异常抛出等;
代码提交规范:提供注释规范,冲突处理规范,warning处理规范,格式化代码规范等。
单元测试规范:单元测试覆盖率,有效性,Mock规范。
(步骤4) 从2到10:构建注册中心与知识库
我们构建了持续集成的系统,规范,流程,还有一个问题没有解决。
一旦订单中心从电商的单体应用中拆分出去了,这就存在当订单中心和其他业务相互调用的时候,如何知道订单中心在哪里的问题。我们假设原来的单体应用为Online,而新的订单中心称为Order。
因而这里我们需要配备一个工具链,那就是注册中心。
如果我们要构建一个注册中心需要考虑哪些方面呢?
高可用性和一致性:注册中心是服务的中心管理节点,他的高可用是非常重要的。
注册中心的节点上维护着注册上了的服务列表,因而是有状态的,这就涉及到一致性的问题。说到一致性问题,就要说一说著名的CAP理论,也即在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可同时获得。在三个属性中,P也即分区容错性是必须能够保证的,接下来就要在C和A里面选择一个了,选择C,表示不同节点的列表是一致的,而牺牲可用性。如果选择A,也即根据节点自己的列表马上返回,而牺牲一致性。
健康检查:注册中心既然保存了服务的列表,就需要实时跟踪和更新这个服务列表。当已经注册上来的服务出现问题而下线的时候,注册中心应该能够及时发现并进行摘除,这就是所谓的健康检查。
负载均衡:注册中心要实现客户端在多个服务端之间进行负载均衡,当然轮询是最常见的,也是最容易实现的。其实还有很多其他的负载均衡方式,例如随机 (Random),轮询 (RoundRobin),一致性哈希 (ConsistentHash),哈希 (Hash),加权(Weighted)。
数据中心感知:有时候,我们的服务会将服务部署在多个数据中心,这就要求注册中心也能感知多个数据中心,本地数据中心肯定速度快,异地数据中心速度慢,应该有所区分。
开放与生态:微服务往往不是一个组件就能搞定的事情,因而需要生态的配合,例如Dubbo,SpringCloud,Kubernetes,Service Mesh等都是使用广泛的生态。所以注册中心能不能和他们搞到一起,这些生态能不能兼容注册中心,也是一个很重要的考量。
高可用性和一致性:注册中心是服务的中心管理节点,他的高可用是非常重要的。
注册中心的节点上维护着注册上了的服务列表,因而是有状态的,这就涉及到一致性的问题。说到一致性问题,就要说一说著名的CAP理论,也即在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可同时获得。在三个属性中,P也即分区容错性是必须能够保证的,接下来就要在C和A里面选择一个了,选择C,表示不同节点的列表是一致的,而牺牲可用性。如果选择A,也即根据节点自己的列表马上返回,而牺牲一致性。
健康检查:注册中心既然保存了服务的列表,就需要实时跟踪和更新这个服务列表。当已经注册上来的服务出现问题而下线的时候,注册中心应该能够及时发现并进行摘除,这就是所谓的健康检查。
负载均衡:注册中心要实现客户端在多个服务端之间进行负载均衡,当然轮询是最常见的,也是最容易实现的。其实还有很多其他的负载均衡方式,例如随机 (Random),轮询 (RoundRobin),一致性哈希 (ConsistentHash),哈希 (Hash),加权(Weighted)。
数据中心感知:有时候,我们的服务会将服务部署在多个数据中心,这就要求注册中心也能感知多个数据中心,本地数据中心肯定速度快,异地数据中心速度慢,应该有所区分。
开放与生态:微服务往往不是一个组件就能搞定的事情,因而需要生态的配合,例如Dubbo,SpringCloud,Kubernetes,Service Mesh等都是使用广泛的生态。所以注册中心能不能和他们搞到一起,这些生态能不能兼容注册中心,也是一个很重要的考量。
如何选择注册中心呢,我这里列了一个表格,供您参考。
Zookeeper | Consul | Eureka | Nacos | |
一致性 | CP | CP | AP | CP+AP |
健康检查 | 心跳通知 | TCP,HTTP,GRP,命令行 | 客户端健康检查 | TCP,HTTP,客户端健康检查 |
负载均衡 | 由Dubbo提供 | Fabio | Ribbon | 权重,Metadata, CMDB |
自我保护机制 | 无 | 无 | 支持 | 支持 |
数据中心感知 | 不支持 | 支持 | 支持 | 支持 |
生态 | Dubbo | SpringCloud,Kubernetes | SpringCloud | SpringCloud,Dubbo,Kubernetes |
仅有注册中心就够了吗?
仅仅注册还不够,别忘了咱们的使命。前面咱们讲过了。我们之所以将服务拆分出来。是为了解决架构腐化问题。仅仅拆分本身不能够解决这个问题。因而需要一定的工具来帮我们做到这件事情。
注册中心是每一个程序员都知道的事情。他可以非常的方便提供服务之间注册发现和调用。
但是他没有解决我们想解决的第一个问题,就是可观测性。接口是否符合规范,架构是否腐化,架构委员会有没有一个地方可以集中Review。注册中心显然没有解决可观测性的问题。
另外还有一个问题就是可复用性。将来我们将一个服务注册到注册中心。就是为了这个服务里面的功能,可以通过接口的方式。让其他服务进行调用。这个时候带来的问题就是。如果开发某个接口。并且注册到注册中心的是一个团队。而要调用这个接口。从注册中心获取这个接口的是另外一个团队。注册中心里面是没有一个文档告诉调用方,如何调用这个接口。这时候可复用性就带来了问题。将来谁想用某个接口,是通过文档还是找人。
为了解决上面的两个问题。仅仅有一个注册中心还是不够的。我们一定要在注册中心之上再封装一层。有一个可以适配多租户,多权限的管理界面。我们对于所有注册到注册中心上的接口都要制定。制定API接口规范。所有接口都要符合这些规范,并且每个接口都要配备相应的文档,文档与运行时一致。这样就形成了统一API知识库。
(步骤5) 从2到10:构建API网关
注册中心主要用于多个服务之间相互调用的时候,知道对方在哪里?这其实还没有解决另外一个问题,也即和前端的沟通问题。
如果前端页面或者APP直接连接后端的服务,在服务化拆分的场景下,就感觉比较痛苦了,原来只需要连接一个URL,突然后端变成了四个服务,要配置四个URL了,可是前端没有变,为什么要前端改?这也是一种耦合。
这个时候,我们就需要另一个技术组件,API网关。当一个服务拆分称为多个服务的时候, API网关对前端应用屏蔽服务拆分,前端无感知。
就像图中所示的一样,当服务从Online里面分离出来后,前端的请求仍然到API网关,由API网关做转发到后端拆分后的各个服务。
当然API网关所能做的事情绝不止这些。还有另外一个场景,当一个服务新从单体应用中拆分出来的时候,你肯定不放心,为了稳定性,API网关提供灰度能力。
如图中,新的订单中心从Online里面拆分出来之后,你可能不敢马上让他替换你的老订单中心,稳妥的做法是切一小部分流量过来,例如非VIP的客户,或者随机抽取百分之几的客户,如果发现有问题,可以马上切回去。
那设计一个API网关,我们都需要考虑哪些方面呢?
第一就是高可用和性能。
API网关多是无状态的,因而高可用可以通过部署多个节点达到,但是作为所有服务的入口,性能就十分关键了。当然性能也可以部分通过横向扩展去解决,但是我们往往不希望在这一层耗费太多的服务器资源,因而单节点的性能也是非常重要的。
第二就是安全问题,API网关是对外服务的大门,因而要有良好的安全策略,将非法访问拦截在外面。例如认证鉴权,黑白名单等。
第三是调用轨迹与调用监控,API网关应该对于所有的API访问所耗费的时间有监控,及时发现有性能问题的调用。
第四是请求代理,路由,负载均衡。API可以将外面来的请求,转发给不同的后端,这叫路由,例如上面我们讲的对前端透明,就是路由功能。另外对于相同类型的后端的多个实例,例如订单中心部署了三个实例,在这三个实例之间,可以进行负载均衡。
第五是灰度发布,分流。将流量在多个实例之间进行按照比例或者某种特征进行分发。
第六是流量控制。API网关作为微服务最外面的屏障,需要对于后端的服务做一定的保护,例如有熔断机制,有限流机制,当后端服务有问题,或者外部流量过大的时候,可以有一定的策略进行处理。
如何选择API网关呢,我这里列了一个表格,供您参考。
Kong | Ambassador | Spring Cloud Gateway | Zuul | |
配置语言 | Admin Rest api, Text file(nginx.conf 等) | YAML(kubernetes annotation) | REST API,YAML静态配置 | REST API,YAML静态配置 |
state | postgres,cassandra | kubernetes | 内存,文件 | 内存,文件 |
扩展功能 | 插件 | 插件 | 自己实现 | 自己实现 |
扩展方法 | 水平 | 水平 | 水平 | 水平 |
服务发现 | 动态 | 动态 | 动态 | 动态 |
协议 | http,https,websocket | http,https,grpc,websocket | http,https,websocket | http,https |
基于 | kong+nginx | envoy | 基于 Spring Framework 5,Project Reactor 和 Spring Boot 2.0 | zuul |
ssl 终止 | yes | yes | yes | no |
websocket | yes | yes | yes | no |
routing | host,path,method | host,path,header | host,path,method,header,cookie | path |
限流 | yes | yes | yes | 需要开发 |
熔断 | yes | no | yes | 需要其他组件 |
重试 | yes | no | yes | yes |
健康检查 | yes | no | yes | yes |
负载均衡算法 | 加权轮询,哈希 | 加权轮询 | ribbon,轮询,随机,加权轮询,自定义 | 轮询,随机,加权轮询,自定义 |
权限 | Basic Auth, HMAC, JWT, Key, LDAP, OAuth 2.0, PASETO, plus paid Kong Enterprise options like OpenID Connect | yes | 开发实现 | 开发实现 |
tracing | yes | yes | 需要其他组件 | 需要其他组件 |
(步骤6) 从2到10:试点业务服务拆分最佳实践
代码的迁移是一个细活,需要逐渐迁移,有以下的最佳实践。
第一,原有工程代码的标准化。
第二,先独立功能模块,规范输入输出,形成服务内部的分离
第三,先分离出新的jar,实现松耦合
当一个工程的结构非常标准化之后,接下来在原有服务中,先独立功能模块 ,规范输入输出,形成服务内部的分离。在分离出新的进程之前,先分离出新的jar,只要能够分离出新的jar,基本也就实现了松耦合。
第四,应该新建工程,新启动一个进程,尽早的注册到注册中心,开始提供服务,这个时候,新的工程中的代码逻辑可以先没有,只是转调用原来的进程接口。
接下来,应该新建工程,新启动一个进程,尽早的注册到注册中心,开始提供服务,这个时候,新的工程中的代码逻辑可以先没有,只是转调用原来的进程接口。
为什么要越早独立越好呢?哪怕还没实现逻辑先独立呢?因为服务拆分的过程是渐进的,伴随着新功能的开发,新需求的引入,这个时候,对于原来的接口,也会有新的需求进行修改,如果你想把业务逻辑独立出来,独立了一半,新需求来了,改旧的,改新的都不合适,新的还没独立提供服务,旧的如果改了,会造成从旧工程迁移到新工程,边迁移边改变,合并更加困难。如果尽早独立,所有的新需求都进入新的工程,所有调用方更新的时候,都改为调用新的进程,对于老进程的调用会越来越少,最终新进程将老进程全部代理。
第五,将老工程中的逻辑逐渐迁移到新工程,这个过程需要持续集成,灰度发布,微服务框架能够在新老接口之间切换。
第六,当新工程稳定运行,并且在调用监控中,已经没有对于老工程的调用的时候,就可以将老工程下线
(步骤7) 从2到10:服务分层拆分
(步骤8) 从10到50:可扩展性架构——数据库最佳实践
数据库永远是应用最关键的一环,同时越到高并发阶段,数据库往往成为瓶颈,如果数据库表和索引不在一开始就进行良好的设计,则后期数据库横向扩展,分库分表都会遇到困难。
对于数据库的最佳实践,我画了一个脑图如下。
(步骤9) 从10到50:可扩展性架构——缓存最佳实践
对于高并发架构,数据库是中军大帐,之前要有缓存做保护,缓存的最佳实践,我也画了一个脑图。
试点业务拆分完毕,总结服务化规范,建立《服务化拆分规范》,《服务化流程规范》,《接口定义,修改规范》,《日志规范》,《数据库设计规范》,《监控规范》,《工程规范》,《日志打点规范》,《质量平台规范》等,并有工具保障落地。
第三:服务化——试点结束,在架构委员会的领导下,在服务化规范的指引下,各组制定里程碑计划,逐步拆分
(步骤10) 从50到500:全面服务化开始
试点完毕之后,架构开始全面服务化历程,组织架构也应该进行调整,建设中台开发组,业务开发组,基础底座组。
前面咱们讲过,很多企业微服务化之所以失败,是因为:
组织不具备,没有适合微服务架构的组织架构,也没有熟悉微服务化经验的架构师和团队。
工具不具备,没有能够良好的管理微服务的工具链
流程不具备,没有能够良好管理微服务架构的流程和规范
现在看来,这些已经都具备了。
首先我们成立了架构委员会,而且已经拆分了核心交易链路,积累了一定的微服务化的经验,有了这些经验,再拆分其他的领域,应该会驾轻就熟。
在工具链方面,我们配备了持续集成,注册中心,API网关,发布平台等,并且都配备有知识库,可以良好的管理微服务。
在流程方面,我们已经积累了以下的流程和规范:《内部接口规范》《外部接口规范》《工程规范》《服务化流程规范》《接口修改规范》《日志规范》《数据库设计规范》《持续集成规范》
为了保证这些流程和规范能够执行,我们在持续集成流程中,加入质量卡点,在这些卡点上,都有相应的绩效看板,能够尽早的发现质量缺陷,并且和绩效进行关联。这样就能够保证微服务化在有序的进行。
接下来,万事俱备,只欠东风了。
架构委员会按照领域进行服务化的分组,然后分组进行服务化的拆分。
这个时候,每个组都有代表的架构师,每个组都要制定里程碑计划,计划多久拆分完毕,并且定时在架构委员会会议上进行review,就可以保证服务拆分有条不紊的进行。
随着服务化的不断展开,对运维组造成了压力,很多运维说,看着服务数目的不断增多,要开始失眠了。
这种压力主要来自于两个方面,第一是资源请求的频率增高,第二是线上SLA维护的压力增大。
我们先来看第一个压力,应用逐渐拆分,服务数量增多。随着服务的拆分,不同的业务开发组会接到不同的需求,并行开发功能增多,发布频繁,会造成测试环境,生产环境更加频繁的部署。而频繁的部署,就需要频繁创建和删除虚拟机。
然而在此之前,企业一直采取的资源层的管理方式是虚拟化,到了这个阶段,我们会发现资源申请的速度明显跟不上了。
如果还是采用原来审批的模式,运维部就会成为瓶颈,要不就是影响开发进度,要不就是被各种部署累死。
我们再来看第二方面的压力,也即SLA的压力。
首先是上线的时候,容易出错,上线依赖于人工和脚本,人是最不靠谱的,很容易犯错误,造成发布事故。而发布脚本、逻辑相对复杂,时间长了以后,逻辑是难以掌握的。而且,如果你想把一个脚本交给另外一个人,也很难交代清楚。
另外,并且脚本多样,不成体系,难以维护。线上系统会有Bug,其实发布脚本也会有Bug。
所以如果你上线的时候,发现运维人员对着一百项配置和Checklist,看半天,或者对着发布脚本多次审核,都不敢运行他,就说明出了问题。
再者,多种多样的中间件,每个团队独立选型中间件,没有统一的维护,没有统一的知识积累,无法统一保障SLA
那应该怎么办呢?这就需要进行运维模式的改变,也即基础设施层云化。
(步骤11) 从50到500:大规模云平台建设
大规模云平台建设还是一件很复杂的事情,我画了一个脑图如下:
(步骤12) 从50到500:IaC对接云平台
接下来,我们就要好好利用云平台的三大特性:统一接口,抽象概念,租户自助。
为了做到这一点,无论是基于公有云的统一接口也可以,基于私有云的OpenStack接口也可以,或者基于采购的云管平台的接口也是可以的,有了这些接口,就可以和发布平台做对接,来达到我们建设或者使用云平台之后,解放运维,加速开发的目标。
因为有了接口,我们就可以像调用代码一样操作这些基础设施,而不是靠人来配置,这就是我们常听到的Infrastructure as Code,IaC。当然这里的代码不是指脚本或者编程语言,而往往是一个编排的文本。
这里我们列举几个常用的IaC工具。
第一个是Terraform:他的理念是"Write, Plan, and create Infrastructure as Code",他可以对接所有主流云平台的接口,也即如果你用的是主流云平台的接口,你可以很顺利是使用它。但是如果你自己采购的云管平台,那就需要自己开发和对接了。
第二是Vagrant,他可以对接各种各样的云平台,用于创建虚拟机。他本身可以作为IaC工具的组件来使用。
第三是Puppet和Chef,这两者比较像,都是使用类似Ruby语言的编排语言,对于应用做统一的安装,配置,更新。他们其实不算完整的IaC工具,因为虚拟机的创建和生命周期管理,他们不管,但是他们可以和Vagrant结合起来,变成完整的IaC工具。
第四是Saltstack,他做的事情和Chef和Puppet很像,但是基于python语言的,不需要重新学一门编排语言,而且他还支持通过远程执行命令来安装和配置软件,而非通过拉取的方式。
第五是Ansible,他是使用YAML语言作为编排语言的,同样使用远程执行命令的方式来安装和配置软件,这样使得使用门槛比较低,而且可以开发很多插件,来对接公有云,实现虚拟机的生命周期的管理。
第六是Juju,他是Ubuntu社区的IaC工具,和Ubuntu集成的比较好,可以对接各种云平台实现虚拟机和应用的整个生命周期管理,生态比较丰富。
第七是NixOS,他其实更像是一个纯粹的Linux配置工具,主要用于管理Linux各种包冲突,升级,回滚的问题,他将原来散落各处的配置文件、系统文件(可执行文件和库)等,变为只由/etc/nixos/里的文件决定。系统更新和迁移比较省心,更新原子化,要么成功,要么回滚,多版本软件共存方便。但是如果将他作为一个IaC工具,还比较单薄,比较适合作为底层的工具。
第八,如果你用公有云的话,每个云都有自己的IaC工具,例如AWS CloudFormation,Azure Resource Manager,Google Cloud Deployment Manager。
第九,如果你用私有云,是OpenStack的话,OpenStack Heat也是一个IaC工具。
(步骤13) 从50到500:搭建并对接日志中心
日志向来都是运维以及开发人员最关心的问题。运维人员可以及时的通过相关日志信息发现系统隐患、系统故障并及时安排人员处理解决问题。开发人员解决问题离不开日志信息的协助定位。
第一,日志采集。
日志采集有很多的工具可以选择,这里列举一些Logstash、Filebeat、Fluentd、Logagent、rsyslog、syslog-ng。
Logstash插件多,灵活性高,基于JVM运行,性能以及资源消耗(默认的堆大小是 1GB)比较大,如果每台机器都安装则服务多了,占用资源比较多,如果是容器场景则更加明显的缺点。
Filebeat 就非常的轻量级,他只是一个二进制文件没有任何依赖,占用资源极少,可以解决Logstash的问题,但是相对的应用场景就要少很多,因而需要配合其他的工具进行配合,例如将所有节点的日志内容通过filebeat送到kafka消息队列,然后使用logstash集群读取消息队列内容,根据配置文件进行过滤。然后将过滤之后的文件输送到elasticsearch中,通过kibana去展示。
Fluentd 插件是用 Ruby 语言开发的,数量很多,几乎所有的源和目标存储都有插件,可以用 Fluentd 来串联所有的东西。Fluentd 创建的初衷是尽可能的使用 JSON 作为日志输出,因而会损失一定的灵活性,尤其是遇到大量非结构化数据的时候,虽然也提供了用正则表达式解析非结构化数据的方法。
rsyslog是Linux 的 syslog 守护进程,rsyslog 可以做的不仅仅是将日志从 syslog socket 读取并写入 /var/log/messages, rsyslog 是经测试过的最快的传输工具。它非常擅长处理解析多个规则,随着规则数目的增加,它的处理速度始终是线性增长的。但是他的缺点是灵活性不足,配置难度比较高,适合做底层资源的日志监控。
第二,缓冲。
很多日志收集组件都可以将ElasticSearch作为存储目标,当日志收集组件接收数据的能力超过了ES集群处理数据的能力时,你可以使用消息队列来作为缓冲。
使用消息队列,对数据丢失也提供了一定的保护。
对于日志收集这种数据量比较大的消息,我们使用Kafka作为消息队列选型。
第三,筛选(Logstash)
日志数据默认是无结构化的,经常包含一些无用信息,Logstash虽然对于日志的收集有点重,但是对于日志的过滤,因为插件丰富,还是非常好的。你可以使用Logstash的FILTER插件来解析你的日志,从中提取有效字段,剔除无用的信息,还可以从有效字段中衍生出额外信息。
第四,存储(ES)
选用ElasticSearch的原因主要为:可分布式的部署,方便拓展;处理海量数据可以应对多种需求;强大的搜索功能,基于Lucene可以实现快速搜索;活跃的开发社区,资料多、上手简单。
第五,展现(Kibana)
需要通过精简提炼日志信息,对日志信息进行整合分析,以图表的形式将日志信息进行展示。
(步骤14) 从50到500:搭建并对接配置中心
日常开发中我们的应用中一般都会有数据库相关的配置,redis相关的配置,log4j相关的配置 等常用配置,这些我们称为静态配置,在应用启动的时候就需要加载,修改配置需要重启应用。
还有一类配置和业务密切相关,应用在运行过程中需要监听这些配置的变化以方便修改运行模式或者响应对应的策略,例如并发控制数,业务开关等,可以用来做服务降级和限流,例如在数据库新老表做迁移的时候,我们可以用来配置进行动态切换模式:同步双写读老表,同步双写读新表,写新表读新表。
如果这些配置不能进行集中式管理,那么当我们的服务部署有成千上万的实例后,即使借助ansible这些运维工具,那么修改配置也将是一件超级麻烦而且极容易出错的事情,在做发布的时候也不在敏捷。
微服务架构下,我们需要一个集中式的配置管理系统,那么这个系统需要提供哪些功能才能解决上面的问题。
1、权限控制,当然不能所有的人都可以修改配置,如果能够继承公司的SSO或者LDAP当然更好。
2、审计日志,所有的修改需要记录操作日志,方便后续出现异议能够找到对应的操作人,也可以提供审批流程。
3、环境管理,开发,测试,生成环境下的配置肯定要做隔离,相同group内的配置可能大多相同,例如同一个IDC机房的应用也可以有namespace做区分。
4、配置回滚,当发现配置错误,或者在该配置下程序发生异常可以立即回滚到之前的版本,这需要该系统能够有版本管理的功能。
5、灰度发布,有时候我们新上线一个功能,想先通过少部分流量测试下,这个时候我们可以随机只修改部分应用的配置,当测试正常后在推送到所有的应用。
6、高可用,配置中心需要高可用,所以最好能支持集群部署,同时配置中心系统挂了之后最好能不影响应用,应用能够继续使用本地缓存的配置。
7、配置中心,应该能够在配置发生变更后实时通知到应用,应用端可能是一个监听器,配置也可能就是一个普通bean里面的属性,需要自动监听到变化并进行调整。
对于配置中心的选型,我列了一个表格如下。
Spring Cloud Config | Apollo | Disconf | |
静态配置管理 | 基于文件 | 支持 | 支持 |
动态配置管理 | 支持 | 支持 | 支持 |
统一管控 | 基于Git | 支持 | 支持 |
多环境管理 | 基于Git | 支持 | 支持 |
变更管理 | 基于Git | 无 | 无 |
本地配置缓存 | 无 | 支持 | 支持 |
配置更新策略(指定时间) | 无 | 无 | 无 |
配置锁 | 支持 | 无 | 无 |
配置校验(IP地址校验) | 无 | 无 | 无 |
配置生效时间 | 重启,手动刷新 | 实时 | 实时 |
配置更新推送 | 手工触发 | 支持 | 支持 |
配置定时拉取 | 无 | 支持 | 依赖事件驱动 |
用户权限管理 | 基于Git | 支持 | 支持 |
授权,审核,审计 | 基于Git | 支持 | 无 |
配置版本管理 | 基于Git | 提供发布历史和回滚功能 | 数据库中有操作记录,但无接口 |
实例配置监控 | 需要spring admin | 支持 | 支持 |
灰度发布 | 不支持 | 支持 | 不支持部分更新 |
告警通知 | 不支持 | 支持 | 支持 |
支持Spring boot | 支持 | 支持 | Java即可 |
支持Spring Cloud | 支持 | 支持 | Java即可 |
客户端语言 | Java | Java, .Net | Java |
依赖组件 | Eureka | Eureka | Zookeeper |
高可用部署 | 支持 | 支持 | 支持 |
多数据中心 | 支持 | 支持 | 支持 |
第四:微服务化——互联网场景,遭遇性能问题,进一步拆分
(步骤15) 从500到2000:全面实施微服务化
一个经典的服务化已经差不多告一段落,系统耦合的问题,快速迭代的问题,都已经基本得到解决,如果没有高并发流量的压力,其实系统已经处于一种不错的状态,没必要进一步拆分了。
再进一步的拆分,就不是领域划分的问题了,而是为了承载高并发流量,如果有某部分逻辑是性能瓶颈,则就应该进一步将这部分独立出来,尽管从业务领域看来,他应该属于另外一个进程。
虽然前面云和IaC已经使得我们的迭代速度适应了服务化,而且实现了一定程度的租户自助。
但是前面为了高并发的一系列拆分,一顿操作猛如虎,又对基础设施层造成了压力。
微服务场景下,进程多,更新快,于是出现100个进程,每天一个镜像。虚拟机哭了,因为虚拟机每个镜像太大了。
VMs有客户机内核,隔离性更好,但是占用资源多,性能低。一般一个虚拟机镜像都在几百G,如果100个进程,每天一个镜像,直接就耗尽你左右的存储资源。
容器乐了,每个容器镜像小,每个镜像都在几百M的级别,没啥问题。
虚拟机怒了,老子不用容器了,微服务拆分之后,用Ansible自动部署是一样的。
这样说从技术角度来讲没有任何问题。
然而问题是从组织角度出现的。
一般的公司,开发会比运维多的多,开发写完代码就不用管了,环境的部署完全是运维负责,运维为了自动化,写Ansible脚本来解决问题。
然而这么多进程,又拆又合并的,更新这么快,配置总是变,Ansible脚本也要常改,每天都上线,不得累死运维。
所以这如此大的工作量情况下,运维很容易出错,哪怕通过自动化脚本。
这个时候,容器就可以作为一个非常好的工具运用起来。
除了容器从技术角度,能够使得大部分的内部配置可以放在镜像里面之外,更重要的是从流程角度,将环境配置这件事情,往前推了,推到了开发这里,要求开发完毕之后,就需要考虑环境部署的问题,而不能当甩手掌柜。
这样做的好处就是,虽然进程多,配置变化多,更新频繁,但是对于某个模块的开发团队来讲,这个量是很小的,因为5-10个人专门维护这个模块的配置和更新,不容易出错。
如果这些工作量全交给少数的运维团队,不但信息传递会使得环境配置不一致,部署量会大非常多。
容器是一个非常好的工具,就是让每个开发仅仅多做5%的工作,就能够节约运维200%的工作,并且不容易出错。
然而本来原来运维该做的事情开发做了,开发的老大愿意么?开发的老大会投诉运维的老大么?
这就不是技术问题了,其实这就是DevOps,DevOps不是不区分开发和运维,而是公司从组织到流程,能够打通,看如何合作,边界如何划分,对系统的稳定性更有好处。
所以,容器的本质是基于镜像的跨环境迁移。
镜像是容器的根本性发明,是封装和运行的标准,其他什么namespace,cgroup,早就有了。这是技术方面。
在流程方面,镜像是DevOps的良好工具。
容器是为了跨环境迁移的,第一种迁移的场景是开发,测试,生产环境之间的迁移。如果不需要迁移,或者迁移不频繁,虚拟机镜像也行,但是总是要迁移,带着几百G的虚拟机镜像,太大了。
第二种迁移的场景是跨云迁移,跨公有云,跨Region,跨两个OpenStack的虚拟机迁移都是非常麻烦,甚至不可能的,因为公有云不提供虚拟机镜像的下载和上传功能,而且虚拟机镜像太大了,一传传一天。
业内没有标准的虚拟机镜像,跨云迁移容易出问题。而且IaC工具不同的云平台也不一样,也会阻碍迁移,而基于容器的编排,就可以改变这一点。
所以跨云场景下,混合云场景下,容器也是很好的使用场景。这也同时解决了仅仅私有云资源不足,扛不住流量的问题。
(步骤16) 从500到2000:大规模容器平台建设
对于大规模容器平台建设,我画了一个脑图。
(步骤17) 从500到2000:应用容器化最佳实践
如果一个应用要容器化,应该符合以下的规范。
每个容器应该只包含一个应用程序,如果有其他辅助应用程序,请使用pod。
容器是有主进程的,也即Entrypoint,只有主进程完全启动起来了,容器才算真正的启动起来,一个比喻是容器更像人的衣服,人站起来了,衣服才站起来,人躺下了,衣服也躺下了。衣服有一定的隔离性,但是隔离性没那么好。衣服没有根(内核),但是衣服可以随着人到处走。因而一个容器应该只包含一个应用程序,并和应用程序的周期完全一致,才方便管理,应该防止容器挂了,应用没挂,或者应用挂了容器没挂的情况,这样就很难发挥容器的优势。例如副本数,明明有三个副本,其中两个副本里面有应用挂了,而不可知,无法达到横向扩展的效果。
Docker中的应用程序应该支持健康检查(readiness、liveness)和优雅关机。
容器通过 Linux 信号来控制其内部进程的生命周期。为了将应用的生命周期与容器联系起来,需要确保应用能够正确处理 Linux 信号。
Linux 内核使用了诸如 SIGTERM、SIGKILL 和 SIGINIT 等信号来终止进程。但是,容器内的 Linux 会使用不同的方式来执行这些常见信号,如果执行结果同信号默认结果不符,将会导致错误和中断发生。
Dockerfile和Docker图像应该用层来组织,以简化开发人员的Dockerfile。
对于容器镜像,我们应该充分利用容器镜像分层的优势,将容器镜像分层构建,在最里面的OS和系统工具层,由运维来构建,中间层的JDK和运行环境,由核心开发人员构建,而最外层的Dockerfile就会非常简单,只要将jar或者war放到指定位置就可以了。
这样可以降低Dockerfile和容器化的门槛,促进DevOps的进度。
将共享层和命令放在Dockerfile的前面,这样就可以更快地构建docker映像,因为共享。
容器镜像由一系列镜像层组成,这些镜像层通过模板或 Dockerfile 中的指令生成。这些层以及构建顺序通常被容器平台缓存。例如,Docker 就有一个可以被不同层复用的构建缓存。这个缓存可以使构建更快,但是要确保当前层的所有父节点都保存了构建缓存,并且这些缓存没有被改变过。简单来讲,需要把不变的层放在前面,而把频繁改变的层放在后面。
例如,假设有一个包含步骤 X、Y 和 Z 的构建文件,对步骤 Z 进行了更改,构建文件可以在缓存中重用步骤 X 和 Y,因为这些层在更改 Z 之前就已经存在,这样可以加速构建过程。但是,如果改变了步骤 X,缓存中的层就不能再被复用。
删除Docker映像中的不需要的工具,这使映像更加安全,如果您想调试,可以使用ephemeral containers。
当由于容器崩溃或容器镜像不包含调试实用程序而导致 kubectl exec 无用时,临时容器对于交互式故障排查很有用。
尤其是,distroless 镜像能够使得部署最小的容器镜像,从而减少攻击面并减少故障和漏洞的暴露。由于 distroless 镜像不包含 shell 或任何的调试工具,因此很难单独使用 kubectl exec 命令进行故障排查。
使用临时容器时,启用进程命名空间共享很有帮助,可以查看其他容器中的进程。
如果你想初始化一些东西,使用 init containers。
Init 容器可以包含一些安装过程中应用容器中不存在的实用工具或个性化代码。例如,没有必要仅为了在安装过程中使用类似 sed、 awk、 python 或 dig 这样的工具而去FROM 一个镜像来生成一个新的镜像。
Init 容器可以安全地运行这些工具,避免这些工具导致应用镜像的安全性降低。
应用镜像的创建者和部署者可以各自独立工作,而没有必要联合构建一个单独的应用镜像。
Init 容器能以不同于Pod内应用容器的文件系统视图运行。因此,Init容器可具有访问 Secrets 的权限,而应用容器不能够访问。
由于 Init 容器必须在应用容器启动之前运行完成,因此 Init 容器提供了一种机制来阻塞或延迟应用容器的启动,直到满足了一组先决条件。一旦前置条件满足,Pod内的所有的应用容器会并行启动。
构建尽可能小的图像。
以上是关于从1到2000个微服务,史上最落地的实践云原生25个步骤的主要内容,如果未能解决你的问题,请参考以下文章云原生实践之 RSocket 从入门到落地:Servlet vs RSocket