在上一篇文章以业务为核心的云原生系统建设中,我们给出了云原生系统建设的总图,从演变的角度讲述了云原生落地的三个阶段。
有同学留言说还是不够落地。所谓听了很多道理,还是过不好这辈子,同理看了很多文章,还是落地不好。
从今天的这篇文章开始,我们开始落地,从那时起,我们将进入大量的技术细节,学习落地,基本上可以回去编码落地。
事实上,在许多技术会议上,我们看到的是分层架构图,就像我们在上一节分为六个层次一样,这很容易误解希望登陆云原的企业,因为大多数公司的云原系统建设不是按层次建设的,不会IaaS建设完成,再建设完成PaaS,它必须根据业务的演变交替迭代。
一定是业务遇到了问题,需要底层技术,底层技术得到了改进,促进了业务的发展。如下图所示,应用层与技术底座之间的良性循环是云原生一词的本质。我称他为云原生怪圈。
虽然按照这个顺序,你会觉得系统有点乱,但这是最落地的进化路径。只有沿着这条进化路径发展,你才能感受到云起源这个词的准确含义。
许多企业应用层服务或微服务,技术基础跟不上,因此系统混乱,日夜加班,但指责服务不好,其他企业花大价钱销售技术基础平台,但应用层跟不上,不能促进业务发展,认为技术基础浪费钱,两种误解不是沿着循环逐渐演变。
为了解决系统混乱的问题,在实施之前,先做一个总结,整体梳理。
每一步落地,我们都要经常问自己以下几个问题,我们就成了四个基本问题:
遇到什么样的问题?
这个问题应该用什么技术来解决?如何解决这个问题?
实现这一技术有很多种,应该如何选择?
该技术是否有最佳实践,能否形成企业的相关规范?
整个落地演进过程要有一个起点:
在应用层中,它主要包括单个应用Online他提供外部服务,Offline服务,他是一些定期任务,MS他是一些后台服务,这基本上是一个单一的应用程序,因为外部服务是Online,下一次拆分也围绕着这项服务展开。
对于数据库,使用的是Oracle,在物理机上部署
在基础实施层层Vmware虚拟化
在线部署的方式是脚本化
接下来,我们将开始进化。
正如我们前面所说,建设中间平台的企业是有一定积累的企业,而不是初创企业,所以没有任何计划是不可能盲目触摸大象的,这是许多企业管理层不允许的。所以在动手之前,要有一个总的地图,就是规划,当然真正云原生演进的时候,我们不建议使用瀑布模型,而是迭代模型,但是迭代模型不代表漫无目的的迭代,而是地图要在心中,所以落地的第一个阶段是规划。
首先,组织结构是第一位的:建立一个架构师小组。即使人很少,只有两三个人,这个组织也必须有,这是未来的军事机械办公室,架构委员会的发起人,每个小组的水平连接,并实施标准和最佳实践。
有了人之后,我们应该从业务结构入手:整理业务流程和领域。在云原怪圈的循环中,我建议从业务层出发,因为IT服务于业务,只有业务方的需求,才是真正应该服务和花钱建设的地方。
从标题上可以看出,这是按照领域驱动设计的方式规划的。
许多技术人员在这里犯的错误是,从数据库开始,看看如何设计数据库结构,并以最容易拆分的方式拆分数据库。这是错误的,没有从业务的角度考虑问题。我们应该借鉴领域驱动设计的理念,从业务流程的梳理和业务领域的划分来划分不同的服务。虽然最终映射到数据库可能不舒服,但方向是正确的。只有这样,我们才能适应未来业务的快速变化。
就我个人而言,我认为方向比手段更重要。方向是正确的。目前没有疼痛,但目前没有疼痛。如果方向错误,问题仍然无法解决。
首先,我们要做的是整理业务流程。通过这个过程,我们可以了解业务运营的逻辑,让技术人员了解业务模式。
这里主要梳理的是电商业务。也许你对电商业务不是很感兴趣,但还是建议你看完这一节。因为架构设计的后部分应该基于对业务流程的理解。事实上,作为一名架构师,越是到越接近业务,而不仅仅是单点技术的一部分。电子商务平台是应用云原生架构的典型案例。尽管你现在的行业。可能是金融、制造或零售。就方法论而言,你总能从电商平台的流程和商业模式中找到类似的部分。别山之石,能攻玉。
以下是电子商务平台的典型业务流程图。
我们分析了具体的业务流程,这里就不赘述了。
下一步是划分业务领域。梳理业务流程后,我们可以根据他划分业务领域。这里不必严格遵守DDD为了方便,我在这里用脑图。
另外,对于每一项服务,我都给它起了名字,这里先不后面自然有用。
在实践中,你可能不需要在这个阶段划分得这么详细。这些服务是在后期的逐步拆分过程中演变而来的。
那么后台技术部门不应该从闷头开始就按这个拆?其实不是!
传统的领域驱动设计是瀑布模型,经过长时间的闭门讨论,贴纸,最终输出各种架构图,但当着陆时,发现情况发生了变化,因为从业务部门到技术部门的领域知识必须丢失信息,这也是DDD被批评的地方是业务方规划的时候这么说的。当着陆需求时,这是另一种说法,导致根据DDD落地好的领域,更难接受需求。
因此,随着新需求的不断到来,一种更加落地的方式是逐步拆分,而且变化很大,复用性是两个考虑因素。
所以赵本山说,不看广告,看疗效。服务拆分,DDD这是一张完整的地图,但具体怎么走,要不要调整,要随着新需求的不断到来,逐步拆分,DDD领域设计的时候,业务方会说不清,但是真的需求来的时候,却是实实在在的,甚至接口和原型都能做出来跟业务看。
让我们举一个现实的例子。例如,根据该领域的划分,对于电子商务业务,单一的电子商务服务应分为以下服务。
当需求到来时,技术部门可以感受到上一篇文章中提到的两种结构耦合现象:
耦合现象1:您更改代码,您需要在线,我需要配合
耦合现象二:明明有一定的功能,却拿不出来
第一个现象是有很多变化。在业务的某个阶段,有些领域确实比其他领域有更多的变化。如果耦合在一起并在线,稳定性将相互影响。例如,如果供应链越来越多,活动越来越多,物流越来越多,如果它们都耦合在一起Online里面,每一家物流公司都会影响订单,太可怕了。
第二种现象是可重复使用,如用户中心和认证中心,整个公司应该只有一套。
在《重构:现有设计改进代码》中,有三条规则—事不过三,三条重构。
这一原则也可以用作服务,也就是说,当物流模块的负责人发现他收到了第三家物流公司时,他应该考虑将其从原来的单一应用程序中分离出来。此外,当有一个功能时,领导或业务方发现它明显存在,需要再做一次。当这种现象第三次出现时,应将其分为独立服务。
这个根据业务需求逐步拆分的过程将使系统的修改能够帮助业务方。同时,系统处于可控状态。随着工具链、流程、团队和员工能力的提高,它逐渐与服务状态相匹配。
然后你可能会问,如果有一个系统,里面的代码已经被垃圾搞得一团糟,我再也看不下去了,但是暂时没有新的需求,应该拆分吗?
不需要不拆!再烂也不拆!我们不想解决所有的腐化问题,不要按DDD理想情况来了。
到目前为止,理论的划分已经基本结束,接下来,我们将移动代码!
我们来选择一个领域将他拆分出来,我们就选择最核心的交易领域吧。
在上面这个庞大的单体应用中,我们将订单拆分出来,需要考虑以下几个事情:
在诸多的功能中,将属于订单的功能梳理出来
梳理订单和其他模块之间的关系,从而知道将来会和哪些模块进行相互调用
将订单模块中的不同功能也进行划分,虽然目前不用拆分,为了将来拆分做准备
第一件事情,我们从上面的图中可以看出,黄色底色的就是属于订单的功能。
第二件事情,我们需要梳理一个关系图,如下所示。
第三件事情,将订单内部的功能也划分一下,因为将来可能会因为性能问题,进一步的划分。
接下来马上应该找拆分了,先别忙,你会不会拆出一堆Bug来,让原来单体应用玩儿挺好的,后来Bug成堆呢?这也是经常服务化被诟病的地方,Bug更多更不稳定。
所以首先要有持续集成,这是云原生架构的基石。
持续集成就是制定一系列流程,或者一个系列规则,将需要在一起的各个层次规范起来,方便大家在一起,强迫大家在一起。
接下来,我们一起来搭建一个持续集成的系统。
上面的这幅图呢,是一个持续集成的总体流程图,里面包含非常多的工具,这些工具呢,有非常多的选型。下面的这个脑图,就总结了持续集成中所常用到的主流工具。你可以根据自己公司的情况。来选择合适的工具。
系统的搭建只是其中一部分,要做好持续集成,还需要有规范,还需要配合敏捷开发的流程。
持续集成的规范都有哪些呢?
工程名规范:这个名称非常关键,以后在公司内部所有的系统中,只要看到这个名称,无论是开发,运维,发布人员,QA任何角色,在持续集成平台,云平台,容器平台,微服务平台等任何平台,都以这个名称为准绳,这样所有人看到名称,马上就知道这个工程什么功能以及应该如何操作他,如果是一个核心交易的前台工程,就应该小心一点,如果是一个后台的管理系统,则小的功能白天也可以发布。
代码结构规范:代码结构规范希望达到的效果是所有人打开一份代码,都能看到熟悉的结构,以及有大概的思路如何入手,这在快速迭代的场景下很重要,因为人员也可能在不同的团队频繁的调动。
代码设计规范:包括命名规范,例如package, class, interface,变量的命名;包括注释规范,我们要根据注释生成文档,这在注册中心和知识库还会提到;资源管理规范,例如对于文件,线程,网络,数据库连接等的使用;异常管理规范,如何捕捉异常和处理异常;日志规范,日志的分级,敏感信息过滤,格式规约;多线程规范,并发,加锁,线程安全;数据库操作规范;编码规范,例如方法长度,类长度,数据模型定义,相等判断,异常抛出等;
代码提交规范:提供注释规范,冲突处理规范,warning处理规范,格式化代码规范等。
单元测试规范:单元测试覆盖率,有效性,Mock规范。
我们构建了持续集成的系统,规范,流程,还有一个问题没有解决。
一旦订单中心从电商的单体应用中拆分出去了,这就存在当订单中心和其他业务相互调用的时候,如何知道订单中心在哪里的问题。我们假设原来的单体应用为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知识库。
注册中心主要用于多个服务之间相互调用的时候,知道对方在哪里?这其实还没有解决另外一个问题,也即和前端的沟通问题。
如果前端页面或者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 |
需要其他组件 |
需要其他组件 |
代码的迁移是一个细活,需要逐渐迁移,有以下的最佳实践。
第一,原有工程代码的标准化。
第二,先独立功能模块,规范输入输出,形成服务内部的分离
第三,先分离出新的jar,实现松耦合
当一个工程的结构非常标准化之后,接下来在原有服务中,先独立功能模块 ,规范输入输出,形成服务内部的分离。在分离出新的进程之前,先分离出新的jar,只要能够分离出新的jar,基本也就实现了松耦合。
第四,应该新建工程,新启动一个进程,尽早的注册到注册中心,开始提供服务,这个时候,新的工程中的代码逻辑可以先没有,只是转调用原来的进程接口。
接下来,应该新建工程,新启动一个进程,尽早的注册到注册中心,开始提供服务,这个时候,新的工程中的代码逻辑可以先没有,只是转调用原来的进程接口。
为什么要越早独立越好呢?哪怕还没实现逻辑先独立呢?因为服务拆分的过程是渐进的,伴随着新功能的开发,新需求的引入,这个时候,对于原来的接口,也会有新的需求进行修改,如果你想把业务逻辑独立出来,独立了一半,新需求来了,改旧的,改新的都不合适,新的还没独立提供服务,旧的如果改了,会造成从旧工程迁移到新工程,边迁移边改变,合并更加困难。如果尽早独立,所有的新需求都进入新的工程,所有调用方更新的时候,都改为调用新的进程,对于老进程的调用会越来越少,最终新进程将老进程全部代理。
第五,将老工程中的逻辑逐渐迁移到新工程,这个过程需要持续集成,灰度发布,微服务框架能够在新老接口之间切换。
第六,当新工程稳定运行,并且在调用监控中,已经没有对于老工程的调用的时候,就可以将老工程下线
数据库永远是应用最关键的一环,同时越到高并发阶段,数据库往往成为瓶颈,如果数据库表和索引不在一开始就进行良好的设计,则后期数据库横向扩展,分库分表都会遇到困难。
对于数据库的最佳实践,我画了一个脑图如下。
对于高并发架构,数据库是中军大帐,之前要有缓存做保护,缓存的最佳实践,我也画了一个脑图。
试点业务拆分完毕,总结服务化规范,建立《服务化拆分规范》,《服务化流程规范》,《接口定义,修改规范》,《日志规范》,《数据库设计规范》,《监控规范》,《工程规范》,《日志打点规范》,《质量平台规范》等,并有工具保障落地。
试点完毕之后,架构开始全面服务化历程,组织架构也应该进行调整,建设中台开发组,业务开发组,基础底座组。
前面咱们讲过,很多企业微服务化之所以失败,是因为:
组织不具备,没有适合微服务架构的组织架构,也没有熟悉微服务化经验的架构师和团队。
工具不具备,没有能够良好的管理微服务的工具链
流程不具备,没有能够良好管理微服务架构的流程和规范
现在看来,这些已经都具备了。
首先我们成立了架构委员会,而且已经拆分了核心交易链路,积累了一定的微服务化的经验,有了这些经验,再拆分其他的领域,应该会驾轻就熟。
在工具链方面,我们配备了持续集成,注册中心,API网关,发布平台等,并且都配备有知识库,可以良好的管理微服务。
在流程方面,我们已经积累了以下的流程和规范:《内部接口规范》《外部接口规范》《工程规范》《服务化流程规范》《接口修改规范》《日志规范》《数据库设计规范》《持续集成规范》
为了保证这些流程和规范能够执行,我们在持续集成流程中,加入质量卡点,在这些卡点上,都有相应的绩效看板,能够尽早的发现质量缺陷,并且和绩效进行关联。这样就能够保证微服务化在有序的进行。
接下来,万事俱备,只欠东风了。
架构委员会按照领域进行服务化的分组,然后分组进行服务化的拆分。
这个时候,每个组都有代表的架构师,每个组都要制定里程碑计划,计划多久拆分完毕,并且定时在架构委员会会议上进行review,就可以保证服务拆分有条不紊的进行。
随着服务化的不断展开,对运维组造成了压力,很多运维说,看着服务数目的不断增多,要开始失眠了。
这种压力主要来自于两个方面,第一是资源请求的频率增高,第二是线上SLA维护的压力增大。
我们先来看第一个压力,应用逐渐拆分,服务数量增多。随着服务的拆分,不同的业务开发组会接到不同的需求,并行开发功能增多,发布频繁,会造成测试环境,生产环境更加频繁的部署。而频繁的部署,就需要频繁创建和删除虚拟机。
然而在此之前,企业一直采取的资源层的管理方式是虚拟化,到了这个阶段,我们会发现资源申请的速度明显跟不上了。
如果还是采用原来审批的模式,运维部就会成为瓶颈,要不就是影响开发进度,要不就是被各种部署累死。
我们再来看第二方面的压力,也即SLA的压力。
首先是上线的时候,容易出错,上线依赖于人工和脚本,人是最不靠谱的,很容易犯错误,造成发布事故。而发布脚本、逻辑相对复杂,时间长了以后,逻辑是难以掌握的。而且,如果你想把一个脚本交给另外一个人,也很难交代清楚。
另外,并且脚本多样,不成体系,难以维护。线上系统会有Bug,其实发布脚本也会有Bug。
所以如果你上线的时候,发现运维人员对着一百项配置和Checklist,看半天,或者对着发布脚本多次审核,都不敢运行他,就说明出了问题。
再者,多种多样的中间件,每个团队独立选型中间件,没有统一的维护,没有统一的知识积累,无法统一保障SLA
那应该怎么办呢?这就需要进行运维模式的改变,也即基础设施层云化。
大规模云平台建设还是一件很复杂的事情,我画了一个脑图如下:
接下来,我们就要好好利用云平台的三大特性:统一接口,抽象概念,租户自助。
为了做到这一点,无论是基于公有云的统一接口也可以,基于私有云的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工具。
日志向来都是运维以及开发人员最关心的问题。运维人员可以及时的通过相关日志信息发现系统隐患、系统故障并及时安排人员处理解决问题。开发人员解决问题离不开日志信息的协助定位。
第一,日志采集。
日志采集有很多的工具可以选择,这里列举一些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)
需要通过精简提炼日志信息,对日志信息进行整合分析,以图表的形式将日志信息进行展示。
日常开发中我们的应用中一般都会有数据库相关的配置,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 |
高可用部署 |
支持 |
支持 |
支持 |
多数据中心 |
支持 |
支持 |
支持 |
一个经典的服务化已经差不多告一段落,系统耦合的问题,快速迭代的问题,都已经基本得到解决,如果没有高并发流量的压力,其实系统已经处于一种不错的状态,没必要进一步拆分了。
再进一步的拆分,就不是领域划分的问题了,而是为了承载高并发流量,如果有某部分逻辑是性能瓶颈,则就应该进一步将这部分独立出来,尽管从业务领域看来,他应该属于另外一个进程。
虽然前面云和IaC已经使得我们的迭代速度适应了服务化,而且实现了一定程度的租户自助。
但是前面为了高并发的一系列拆分,一顿操作猛如虎,又对基础设施层造成了压力。
微服务场景下,进程多,更新快,于是出现100个进程,每天一个镜像。虚拟机哭了,因为虚拟机每个镜像太大了。
VMs有客户机内核,隔离性更好,但是占用资源多,性能低。一般一个虚拟机镜像都在几百G,如果100个进程,每天一个镜像,直接就耗尽你左右的存储资源。
容器乐了,每个容器镜像小,每个镜像都在几百M的级别,没啥问题。
虚拟机怒了,老子不用容器了,微服务拆分之后,用Ansible自动部署是一样的。
这样说从技术角度来讲没有任何问题。
然而问题是从组织角度出现的。
一般的公司,开发会比运维多的多,开发写完代码就不用管了,环境的部署完全是运维负责,运维为了自动化,写Ansible脚本来解决问题。
然而这么多进程,又拆又合并的,更新这么快,配置总是变,Ansible脚本也要常改,每天都上线,不得累死运维。
所以这如此大的工作量情况下,运维很容易出错,哪怕通过自动化脚本。
这个时候,容器就可以作为一个非常好的工具运用起来。
除了容器从技术角度,能够使得大部分的内部配置可以放在镜像里面之外,更重要的是从流程角度,将环境配置这件事情,往前推了,推到了开发这里,要求开发完毕之后,就需要考虑环境部署的问题,而不能当甩手掌柜。
这样做的好处就是,虽然进程多,配置变化多,更新频繁,但是对于某个模块的开发团队来讲,这个量是很小的,因为5-10个人专门维护这个模块的配置和更新,不容易出错。
如果这些工作量全交给少数的运维团队,不但信息传递会使得环境配置不一致,部署量会大非常多。
容器是一个非常好的工具,就是让每个开发仅仅多做5%的工作,就能够节约运维200%的工作,并且不容易出错。
然而本来原来运维该做的事情开发做了,开发的老大愿意么?开发的老大会投诉运维的老大么?
这就不是技术问题了,其实这就是DevOps,DevOps不是不区分开发和运维,而是公司从组织到流程,能够打通,看如何合作,边界如何划分,对系统的稳定性更有好处。
所以,容器的本质是基于镜像的跨环境迁移。
镜像是容器的根本性发明,是封装和运行的标准,其他什么namespace,cgroup,早就有了。这是技术方面。
在流程方面,镜像是DevOps的良好工具。
容器是为了跨环境迁移的,第一种迁移的场景是开发,测试,生产环境之间的迁移。如果不需要迁移,或者迁移不频繁,虚拟机镜像也行,但是总是要迁移,带着几百G的虚拟机镜像,太大了。
第二种迁移的场景是跨云迁移,跨公有云,跨Region,跨两个OpenStack的虚拟机迁移都是非常麻烦,甚至不可能的,因为公有云不提供虚拟机镜像的下载和上传功能,而且虚拟机镜像太大了,一传传一天。
业内没有标准的虚拟机镜像,跨云迁移容易出问题。而且IaC工具不同的云平台也不一样,也会阻碍迁移,而基于容器的编排,就可以改变这一点。
所以跨云场景下,混合云场景下,容器也是很好的使用场景。这也同时解决了仅仅私有云资源不足,扛不住流量的问题。
对于大规模容器平台建设,我画了一个脑图。
如果一个应用要容器化,应该符合以下的规范。
每个容器应该只包含一个应用程序,如果有其他辅助应用程序,请使用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内的所有的应用容器会并行启动。
构建尽可能小的图像。
通过使用较小的基础镜像(如Alpine),您可以显著减少容器的大小。Alpine Linux是一款体积小,轻量级的Linux发行版,在Docker用户中非常受欢迎,因为它与许多应用程序兼容,同时仍然保持小体积。
在图片上加上规定的标签,不要使用latest。
在使用公共镜像之前,先扫描它。
微服务架构下,由于进行了服务拆分,一次请求往往需要涉及多个服务,每个服务可能是由不同的团队开发,使用了不同的编程语言,还有可能部署在不同的机器上,分布在不同的数据中心。因而需要链路追踪和应用性能管理,可以起到以下的作用。
第一,优化系统瓶颈。
<