(给ImportNew加星标,提高Java技能)
1.微服务监控系统
一旦请求服务出现异常,我们必须得知道是在哪个服务环节出了故障,就需要对每一个服务,以及各个指标都进行全面的监控;
监控系统能为我们提供具体的指标数据进行追踪和跟进。
在微服务架构中,监控系统按照原理和作用大致可以分为三类:
日志监控(Log)
调用链调用监控(Tracing)
度量监控(Metrics)
日志类比较常见,我们的框架代码、系统环境、以及业务逻辑中一般都会产出一些日志,这些日志我们通常把它记录后统一收集起来,方便在跟踪和丁文问题的需要时进行查询;
日志类记录的信息一般是一些事件、非结构化的一些文本内容;
日志的输出和处理的解决方案常用的有 ELK Stack 方案,用于实时搜索,分析和可视化日志数据;
开源实时日志分析 ELK 平台能够完美的解决我们上述的问题,ELK 由ElasticSearch、Logstash和Kiabana 三个开源工具组成。
组件介绍
Elasticsearch 是个开源分布式搜索引擎,它具备分布式、零配置、自动发现、索引自动分片、索引副本机制、RESTful 风格接口、多数据源、自动搜索负载等特性;
Logstash 是一个完全开源的工具,它可以对你的日志进行收集、过滤,并将其存储供以后使用(如搜索);
Kibana 也是一个开源和免费的工具,可以为 Logstash 和 ElasticSearch 提供的日志分析友好的 Web 界面,可以帮助您汇总、分析和搜索重要数据日志;
Kafka 用来接收用户日志的消息队列。
工作流程图
Logstash 收集 AppServer 产生的日志记录 Log;
将日志 log 存放到 ElasticSearch 集群中,而 Kibana 则从 ES 集群中查询数据生成图表;
生成的日志图表返回给 Browser 进行渲染显示,分别支持各种终端进行显示。
调用链监控是用来追踪微服务之前依赖的路径和问题定位;
主要原理就是子节点会记录父节点的 id 信息。例如阿里的鹰眼系统就是一个调用链监控系统;
一个请求从开始进入,在微服务中调用不同的服务节点后,再返回给客户端,在这个过程中通过调用链参数来追寻全链路的调用行程。通过这个方式可以很方便的知道请求在哪个环节出了故障,系统的瓶颈出现在哪一个环节,定位出优化点。
「调用链监控」是在微服务架构中非常重要的一环。它除了能帮助我们定位问题以外,还能帮助项目成员清晰的去了解项目部署结构。
微服务如果达到了成百个服务调用,时间久了之后,项目的结构很可能就会出现超级混乱的调用,见下图:
在这种情况下,团队开发者甚至是架构师都不一定能对项目的网络结构有很清晰的了解,那就更别谈系统优化了。
生成项目网络拓扑图
根据「调用链监控」中记录的链路信息,给项目生成一张网络调用的拓扑图;
通过这张图,我们就可以知道系统中的各个服务之间的调用关系是怎样的,以及系统依赖了哪些服务;
可以让架构师监控全局服务状态,便于架构师掌握系统的调用结构。
快速定位问题
微服务架构下,问题定位就变得非常复杂了,一个请求可能会涉及到多个服务节点;
有了调用链监控系统就能让开发人员快速的定位到问题和相应模块,提升解决问题效率。
优化系统
通过记录了请求在调用链上每一个环节的信息,可以通过得出的服务信息找出系统的瓶颈,做出针对性的优化;
还可以分析这个调用路径是否合理,是否调用了不必要的服务节点,是否有更近、响应更快的服务节点;
通过对调用链路的分析,我们就可以找出最优质的调用路径,从而提高系统的性能。
主要原理就是子节点会记录父节点的 id 信息,要理解好三个核心的概念 Trace、Span 和 Annotation。
Trace
Trace 是指一次请求调用的链路过程,trace id 是指这次请求调用的 ID;
在一次请求中,会在网络的最开始生成一个全局唯一的用于标识此次请求的 trace id。这个 trace id 在这次请求调用过程中无论经过多少个节点都会保持不变,并且在随着每一层的调用不停的传递;
最终,可以通过 trace id 将这一次用户请求在系统中的路径全部串起来。
Span
Span 是指一个模块的调用过程,一般用 span id 来标识。在一次请求的过程中会调用不同的节点、模块、服务,每一次调用都会生成一个新的 span id 来记录;
这样就可以通过 span id 来定位当前请求在整个系统调用链中所处的位置,以及它的上下游节点分别是什么。
Annotation
指附属信息,可以用于附属在每一个 Span 上自定义的数据。
具体流程:
从图中可见,一次请求只有一个唯一的 trace id=12345,在请求过程中的任何环节都不会改变;
在这个请求的调用链中,Span A 调用了 Span B,然后 Span B 又调用了 Span C 和 Span D,每一次 Span 调用都会生成一个自己的 span id,并且还会记录自己的上级 span id 是谁;
通过这些 id,整个链路基本上就都能标识出来,记录了调用过程。
CAT
CAT 是由大众点评开源的一款调用链监控系统,基于JAVA开发,有很多互联网企业在使用,热度非常高;
它有一个非常强大和丰富的可视化报表界面,这一点其实对于一款调用链监控系统而来非常的重要;
在 CAT 提供的报表界面中有非常多的功能,几乎能看到你想要的任何维度的报表数据;
CAT 有个很大的优势就是处理的实时性,CAT 里大部分系统是分钟级统计。
Open Zipkin
Zipkin 由 Twitter 开源,支持的语言非常多。它基于 Google Dapper 的论文设计而来,国内外很多公司都在用,文档资料也很丰富。
用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。
Pinpoint
Pinpoint 中的服务关系依赖图做得非常棒,超出市面上任何一款产品;
Pinpoint 运用 JavaAgent 字节码增强技术,只需要加启动参数即可。因为采用字节码增强方式去埋点,所以在埋点的时候是不需要修改业务代码的,是非侵入式的。非常适合项目已经完成之后再增加调用链监控的时候去使用的方案;
但是也是由于采用字节码增强的方式,所以它目前仅支持 Java 语言。
方案选型比较
度量类监控主要采用时序数据库的解决方案;
它是以事件发生时间以及当前数值的角度来记录的监控信息,是可以聚合运算的,用于查看一些指标数据和指标趋势;
所以这类监控主要不是用来查问题的,主要是用来看趋势的,基于时间序列数据库的监控系统是非常适合做监控告警使用的。
Metrics 一般有 5 种基本的度量类型:
Gauges(度量)
Counters(计数器)
Histograms(直方图)
Meters(TPS计算器)
Timers(计时器)
Prometheus
Promethes 是一款 2012 年开源的监控框架,其本质是时间序列数据库,由 Google 前员工所开发;
Promethes 采用拉的模式(Pull)从应用中拉取数据,并还支持 Alert 模块可以实现监控预警。它的性能非常强劲,单机可以消费百万级时间序列。
从图的左下角可以看到,Prometheus 可以通过在应用里进行埋点后 Pull 到 Prometheus Server 里。如果应用不支持埋点,也可以采用 exporter 方式进行数据采集。
从图的左上角可以看到,对于一些定时任务模块,因为是周期性运行的,所以采用拉的方式无法获取数据,那么 Prometheus 也提供了一种推数据的方式,但是并不是推送到 Prometheus Server 中,而是中间搭建一个 Pushgateway。定时任务模块将 metrics 信息推送到这个 Pushgateway 中,然后 Prometheus Server 再依然采用拉的方式从 Pushgateway 中获取数据。
需要拉取的数据既可以采用静态方式配置在 Prometheus Server 中,也可以采用服务发现的方式(即图的中间上面的 Service discovery 所示)。
PromQL 是 Prometheus 自带的查询语法,通过编写 PromQL 语句可以查询 Prometheus 里面的数据。Alertmanager 是用于数据的预警模块,支持通过多种方式去发送预警。WebU 用来展示数据和图形,但是一般大多数是与 Grafana 结合,采用 Grafana 来展示。
OpenTSDB
OpenTSDB 是在 2010 年开源的一款分布式时序数据库,当然其主要用于监控方案中;
OpenTSDB 采用的是 Hbase 的分布式存储,它获取数据的模式与 Prometheus 不同,它采用的是推模式(Push);
在展示层,OpenTSDB 自带有 WebUI 视图,也可以与 Grafana 很好的集成,提供丰富的展示界面;
但 OpenTSDB 并没有自带预警模块,需要自己去开发或者与第三方组件结合使用。
InfluxDB
InfluxDB 是在 2013 年开源的一款时序数据库,在这里我们主要还是用于做监控系统方案;
它收集数据也是采用推模式(Push)。在展示层,InfluxDB 也是自带 WebUI,也可以与 Grafana 集成。
监控是微服务治理的重要环节,架构采用分层监控,一般分为以下监控层次。如下图所示:
系统层
系统层主要是指 CPU、磁盘、内存、网络等服务器层面的监控,这些一般也是运维同学比较关注的对象。
应用层
应用层指的是服务角度的监控,比如接口、框架、某个服务的健康状态等,一般是服务开发或框架开发人员关注的对象。
用户层
这一层主要是与用户、与业务相关的一些监控,属于功能层面的,大多数是项目经理或产品经理会比较关注的对象。
监控指标
延迟时间:主要是响应一个请求所消耗的延迟,比如某接口的 HTTP 请求平均响应时间为 100ms;
请求量:是指系统的容量吞吐能力,例如每秒处理多少次请求(QPS)作为指标。
错误率:主要是用来监控错误发生的比例,比如将某接口一段时间内调用时失败的比例作为指标。
单体应用的架构下一旦程序发生了故障,那么整个应用可能就没法使用了,所以我们要把单体应用拆分成具有多个服务的微服务架构,来减少故障的影响范围。
但是在微服务架构下,有一个新的问题就是,由于服务数变多了,假设单个服务的故障率是不变的,那么整体微服务系统的故障率其实是提高了的。
假设单个服务的故障率是 0.01%,也就是可用性是 99.99%,如果我们总共有 10 个微服务,那么我们整体的可用性就是 99.99% 的十次方,得到的就是 99.90% 的可用性(也就是故障率为 0.1%)。可见,相对于之前的单体应用,整个系统可能发生故障的风险大幅提升。
当某个服务出现故障,我们要做的就是最大限度的隔离单个服务的风险,也就是「 容错隔离 」的方法。不仅保证了微服务架构的正常运行,也保证了系统的可用性和健壮性。
单机可用性风险
单机可用性风险指的是微服务部署所在的某一台机器出现了故障,造成的可用性风险;
这种风险发生率很高,因为单机器在运维中本身就容易发生各种故障,例如硬盘坏了、机器电源故障等等,这些都是时有发生的事情;
不过虽然这种风险发生率高,但危害有限,因为我们大多数服务并不只部署在一台机器上,可能多台都有,因此只需要做好监控,发现故障之后,及时的将这台故障机器从服务集群中剔除即可,等修复后再重新上线到集群里。
单机房可用性风险
这种风险的概率比单机器的要低很多,但是也不是完全不可能发生,在实际情况中,还是有一定概率的。比如最为常见的就是通往机房的光纤被挖断,造成机房提供不了服务;
如果我们的服务全部都部署在单个机房,而机房又出故障了,但是现在大多数中大型项目都会采用多机房部署的方案,比如同城双活、异地多活等;
一旦某个机房出现了故障不可用了,立即采用切换路由的方式,把这个机房的流量切到其它机房里就能正常提供服务了。
跨机房集群可用性风险
跨机房集群只是保证了物理层面可用性的问题,如果存在代码故障问题,或者因为特殊原因用户流量激增,导致我们的服务扛不住了,那在跨机房集群的情况下一样会导致系统服务不可用;
所以我们就需要提前做好了「容错隔离」的一些方案,比如限流、熔断等等,用上这些方法还是可以保证一部分服务或者一部分用户的访问是正常。
超时
这是简单的容错方式。指在服务之间调用时,设置一个主动超时时间作为时间阈值;
超过了这个时间阈值后,如果“被依赖的服务”还没有返回数据的话,“调用者”就主动放弃,防止因“被依赖的服务”故障无法返回结果造成服务无法处理请求的问题。
限流
顾名思义,就是限制最大流量。系统能提供的最大并发有限,同时来的请求又太多,服务无法处理请求,就只好排队限流;
类比就跟生活中去景点排队买票、去商场吃饭排队等号的道理;
常见的限流算法有:计算器限流、漏桶算法、令牌漏桶算法、集群限流算法。
降级
与限流类似,一样是流量太多,系统服务不过来。这个时候可将不是那么重要的功能模块进行降级处理,停止服务,这样可以释放出更多的资源供给核心功能的去用;
同时还可以对用户分层处理,优先处理重要用户的请求,比如 VIP 收费用户等;
例如淘宝双十一活动会对订单查询服务降级来保证购买下单服务的可用性。
延迟异步处理
这个方式是指设置一个流量缓冲池,所有的请求先进入这个缓冲池等待处理;
真正的服务处理方按顺序从这个缓冲池中取出请求依次处理,这种方式可以减轻后端服务的压力,但是对用户来说体验上有延迟;
技术上一般会采用消息队列的异步和削峰作用来实现。
熔断
可以理解成就像电闸的保险丝一样,当流量过大或者错误率过大的时候,保险丝就熔断了,链路就断开了,不提供服务了;
当流量恢复正常,或者后端服务稳定了,保险丝会自动街上(熔断闭合),服务又可以正常提供了。这是一种很好的保护后端微服务的一种方式;
熔断技术中有个很重要的概念就是:断路器,可以参考下图:
断路器其实就是一个状态机原理,有三种状态:
Closed:闭合状态,也就是正常状态;
Open:开启状态,也就是当后端服务出故障后链路断开,不提供服务的状态;
Half-Open:半闭合状态,就是允许一小部分流量进行尝试,尝试后发现服务正常就转为Closed状态,服务依旧不正常就转为Open状态。
Hystrix 原理图
当我们使用了 Hystrix 之后,请求会被封装到 HystrixCommand 中,这也就是第一步;
第二步就是开始执行请求,Hystrix 支持同步执行(图中 .execute 方法)、异步执行(图中 .queue 方法)和响应式执行(图中 .observer);
第三步判断缓存,如果存在与缓存中,则直接返回缓存结果;
如果不在缓存中,则走第四步,判断断路器的状态是否为开启,如果是开启状态,也就是短路了,那就进行失败返回,跳到第八步;
第八步需要对失败返回的处理也需要再做一次判断,要么正常失败返回,返回相应信息,要么根本没有实现失败返回的处理逻辑,就直接报错;
如果断路器不是开启状态,那请求就继续走,进行第五步,判断线程、队列是否满了。如果满了,那么同样跳到第八步;
如果线程没满,则走到第六步,执行远程调用逻辑,然后判断远程调用是否成功。调用发生异常了就挑到第八步,调用正常就挑到第九步正常返回信息。
图中的第七步,非常牛逼的一个模块,是来收集 Hystrix 流程中的各种信息来对系统做监控判断的。
Hystrix 断路器的原理图
Hystrix 通过滑动时间窗口算法来实现断路器的,是以秒为单位的滑桶式统计。它总共包含 10 个桶,每秒钟一个生成一个新的桶,往前推移,旧的桶就废弃掉;
每一个桶中记录了所有服务调用的状态,调用次数、是否成功等信息,断路器的开关就是把这 10 个桶进行聚合计算后,来判断当前是应该开启还是闭合的。
访问安全就是要保证符合系统要求的请求才可以正常访问服务来响应数据,避免非正常服务对系统进行攻击和破坏;
微服务会进行服务的拆分,服务也会随业务分为内部服务和外部服务,同时需要保证哪些服务可以直接访问,哪些不可以;
总的来说,访问安全本质上就是要作为访问的认证。
传统单体应用的访问示意图:
在应用服务器里面,会有一个 auth 模块(一般采用过滤器来实现)。当有客户端请求进来时,所有的请求都必须首先经过这个 auth 来做身份验证,验证通过后,才将请求发到后面的业务逻辑;
通常客户端在第一次请求的时候会带上身份校验信息(用户名和密码),auth 模块在验证信息无误后,就会返回 COOKIE 存到客户端,之后每次客户端只需要在请求中携带 COOKIE 来访问,而 auth 模块也只需要校验 COOKIE 的合法性后决定是否放行;
可见,在传统单体应用中的安全架构还是蛮简单的,对外也只有一个入口,通过 auth 校验后,内部的用户信息都是内存、线程传递,逻辑并不是复杂,所以风险也在可控范围内。
在微服务架构下,一般有以下三种方案:
网关鉴权模式(API Gateway)
服务自主鉴权模式
API Token模式(OAuth2.0)
通过上图可见,因为在微服务的最前端一般会有一个 API 网关模块(API Gateway)。所有外部请求访问微服务集群时,都会首先通过这个API Gateway。可以在这个模块里部署 auth 逻辑,实现统一集中鉴权。鉴权通过后,再把请求转发给后端各个服务;
这种模式的优点就是,由 API Gateway 集中处理了鉴权的逻辑,使得后端各微服务节点自身逻辑就简单了,只需要关注业务逻辑,无需关注安全性事宜;
这个模式的问题就是,API Gateway 适用于身份验证和简单的路径授权(基于 URL),对于复杂数据、角色的授权访问权限,通过 API Gateway 很难去灵活的控制。毕竟这些逻辑都是存在后端服务上的,并非存储在 API Gateway 里。
服务自主鉴权就是指不通过前端的 API Gateway 来控制,而是由后端的每一个微服务节点自己去鉴权;
它的优点就是可以由更为灵活的访问授权策略,并且相当于微服务节点完全无状态化了。同时还可以避免 API Gateway 中 auth 模块的性能瓶颈;
缺点就是由于每一个微服务都自主鉴权,当一个请求要经过多个微服务节点时,会进行重复鉴权,增加了很多额外的性能开销。
如图,这是一种采用基于令牌 Token 的授权方式。在这个模式下,是由授权服务器(图中 Authorization Server)、API 网关(图中 API Gateway)、内部的微服务节点几个模块组成。
流程如下:
客户端应用首先使用账号密码或者其它身份信息去访问授权服务器(Authorization Server)获取 访问令牌(Access Token);
拿到访问令牌(Access Token)后带着它再去访问API网关(图中 API Gateway),API Gateway 自己是无法判断这个 Access Token 是否合法,所以走第 3 步;
API Gateway 去调用 Authorization Server 校验 Access Token 的合法性;
如果验证完 Access Token 是合法的,那 API Gateway 就将 Access Token 换成 JWT 令牌返回;
注意:此处也可以不换成 JWT,而是直接返回原 Access Token。但是换成 JWT 更好,因为 Access Token 是一串不可读无意义的字符串,每次验证 Access Token 是否合法都需要去访问 Authorization Server 才知道。但是 JWT 令牌是一个包含 JSON 对象,有用户信息和其它数据的一个字符串,后面微服务节点拿到 JWT 之后,自己就可以做校验,减少了交互次数。
API Gateway 有了JWT之后,就将请求向后端微服务节点进行转发,同时会带上这个 JWT;
微服务节点收到请求后,读取里面的 JWT,然后通过加密算法验证这个 JWT,验证通过后,就处理请求逻辑。
这里面就使用到了 OAuth2.0 的原理,不过这只是 OAuth2.0 各类模式中的一种。
OAuth2.0 是一种访问授权协议框架。它是基于 Token 令牌的授权方式,在不暴露用户密码的情况下,使应用方能够获取到用户数据的访问权限。
例如:你开发了一个视频网站,可以采用第三方微信登陆,那么只要用户在微信上对这个网站授权了,那这个网站就可以在无需用户密码的情况下获取用户在微信上的头像。
OAuth2.0 的流程如下图:
资源服务器:用户数据、资源存放的地方,在微服务架构中,服务就是资源服务器。在上面的例子中,微信头像存放的服务就是资源服务器;
资源拥有者:是指用户,资源的拥有人。在上面的例子中某个微信头像的用户就是资源拥有者;
授权服务器:是一个用来验证用户身份并颁发令牌的服务器;
客户端应用:想要访问用户受保护资源的客户端、Web应用。在上面的例子中的视频网站就是客户端应用;
访问令牌:Access Token,授予对资源服务器的访问权限额度令牌;
刷新令牌:客户端应用用于获取新的 Access Token 的一种令牌;
客户凭证:用户的账号密码,用于在 授权服务器 进行验证用户身份的凭证。
授权码(Authorization Code)
授权码模式是指客户端应用先去申请一个授权码,然后再拿着这个授权码去获取令牌的模式。这也是目前最为常用的一种模式,安全性比较高,适用于我们常用的前后端分离项目。通过前端跳转的方式去访问授权服务器获取授权码,然后后端再用这个授权码访问授权服务器以获取访问令牌。
工作流程图
第一步,客户端的前端页面(图中 UserAgent)将用户跳转到授权服务器(Authorization Server)里进行授权,授权完成后,返回授权码(Authorization Code)。
第二步,客户端的后端服务(图中 Client)携带授权码(Authorization Code)去访问 授权服务器,然后获得正式的访问令牌(Access Token)。
面的前端和后端分别做不同的逻辑,前端接触不到 Access Token,保证了 Access Token 的安全性。
简化模式(Implicit)
简化模式是在项目是一个纯前端应用,在没有后端的情况下,采用的一种模式;
因为这种方式令牌是直接存在前端的,所以非常不安全,因此令牌的有限期设置就不能太长。
工作流程图
第一步,应用(纯前端的应用)将用户跳转到授权服务器(Authorization Server)里进行授权。授权完成后,授权服务器直接将 Access Token 返回给 前端应用,令牌存储在前端页面。
第二步,应用(纯前端的应用)携带访问令牌(Access Token)去访问资源,获取资源。
在整个过程中,虽然令牌是在前端 URL 中直接传递,但令牌不是放在 HTTP 协议中 URL 参数字段中的,而是放在 URL 锚点里。因为锚点数据不会被浏览器发到服务器,因此有一定的安全保障。
用户名密码(Resource Owner Credentials)
这种方式最容易理解了,直接使用用户名、密码作为授权方式去访问授权服务器,从而获取 Access Token。
这个方式因为需要用户给出自己的密码,所以非常的不安全性。一般仅在客户端应用与授权服务器、资源服务器是归属统一公司、团队,互相非常信任的情况下采用。
客户端凭证(Client Credentials)
这是适用于服务器间通信的场景。客户端应用拿一个用户凭证去找授权服务器获取Access Token。
传统的 PaaS 技术虽然也可以一键将本地应用部署到云上,并且也是采用隔离环境的形式去部署,但是其兼容性非常的不好。
因为其主要原理就是将本地应用程序和启停脚本一同打包,然后上传到云服务器上,然后再在云服务器里通过脚本启动这个应用程序。
这样的做法,看起来很理想。但是在实际情况下,由于本地与云端的环境差异,导致上传到云端的应用运行的时候经常各种报错,需要各种修改配置和参数来做兼容服务环境。甚至在项目迭代过程中不同的版本代码都需要重新去做适配,非常耗费精力。
然而以Docker为代表的容器技术却通过一个小创新完美的解决了这个问题。
在 Docker 的方案中,它不仅打包了本地应用程序,而且还将本地环境(操作系统的一部分)也打包了,组成一个叫做「 Docker镜像 」的文件包。所以这个「 Docker镜像 」就包含了应用运行所需的全部依赖,我们可以直接基于这个「 Docker镜像 」在本地进行开发与测试,完成之后,再直接将这个「 Docker镜像 」一键上传到云端运行即可。
Docker 实现了本地与云端的环境完全一致,做到了真正的一次开发随处运行,避免了类似“我在本地正常运行,传到云端就不可以了”的说辞。
先来看一下容器与虚拟机的对比区别:
虚拟机是在宿主机上基于 Hypervisor 软件虚拟出一套操作系统所需的硬件设备,再在这些虚拟硬件上安装操作系统 Guest OS,然后不同的应用程序就可以运行在不同的 Guest OS 上,应用之间也就相互独立、资源隔离。但是由于需要 Hypervisor 来创建虚拟机,且每个虚拟机里需要完整的运行一套操作系统 Guest OS,因此这个方式会带来很多额外资源的开销。
Docker容器中却没有 Hypervisor 这一层,虽然它需要在宿主机中运行 Docker Engine,但它的原理却完全不同于 Hypervisor,它并没有虚拟出硬件设备,更没有独立部署全套的操作系统 Guest OS。
Docker容器没有那么复杂的实现原理,它其实就是一个普通进程而已,只不过它是一种经过特殊处理过的普通进程。我们启动容器的时候(docker run …),Docker Engine 只不过是启动了一个进程,这个进程就运行着我们容器里的应用。
但 Docker Engine 对这个进程做了一些特殊处理,通过这些特殊处理之后,这个进程所看到的外部环境就不再是宿主机的环境(它看不到宿主机中的其它进程了,以为自己是当前操作系统唯一一个进程),并且 Docker Engine 还对这个进程所使用得资源进行了限制,防止它对宿主机资源的无限使用。
对比下来就是,容器比虚拟机更加轻量级,花销也更小,更好地利用好主机的资源。
Docker 容器对这个进程的隔离主要采用两个核心技术点 Namespace 和 Cgroups。
总结来说就是,Namespace 为容器进程开辟隔离进程,Cgroups 限制容器进程之间抢夺资源,从此保证了容器之间独立运行和隔离。
Namespace 是 Linux 操作系统默认提供的 API,包括 PID Namespace、Mount Namespace、IPC Namespace、Network Namespace 等。
以 PID Namespace 举例,它的功能是可以让我们在创建进程的时候,告诉Linux系统,我们要创建的进程需要一个新的独立的进程空间,并且这个进程在这个新的进程空间里的 PID=1。也就是说这个进程只看得到这个新进程空间里的东西,看不到外面宿主机环境里的东西,也看不到其它进程。
不过这只是一个虚拟空间,事实上这个进程在宿主机里 PID 该是啥还是啥,没有变化,只不过在这个进程空间里,该进程以为自己的 PID=1。
打个比方,就像是一个班级,每个人在这个班里都有一个编号。班里有 90 人,然后来了一位新同学,那他在班里的编号就是 91。可是老师为了给这位同学特别照顾,所以在班里开辟了一块独立的看不到外面的小隔间,并告诉这个同学他的编号是 1。由于这位同学在这个小空间里隔离着,所以他真的以为自己就是班上的第一位同学且编号为 1。当然了,事实上这位同学在班上的编号依然是 91。
Network Namespace 也是类似的技术原理,让这个进程只能看到当前 Namespace 空间里的网络设备,看不到宿主机真实情况。同理,其它 Mount、IPC 等 Namespace 也是这样。Namespace 技术其实就是修改了应用进程的视觉范围,但应用进程的本质却没有变化。
不过,Docker容器里虽然带有一部分操作系统(文件系统相关),但它并没有内核,因此多个容器之间是共用宿主机的操作系统内核的。这一点与虚拟机的原理是完全不一样的。
Cgroup 全称是 Control Group,其功能就是限制进程组所使用的最大资源(这些资源可以是 CPU、内存、磁盘等等)。
既然 Namespace 技术 只能改变一下进程组的视觉范围,并不能真实的对资源做出限制。那么为了防止容器(进程)之间互相抢资源,甚至某个容器把宿主机资源全部用完导致其它容器也宕掉的情况发生。因此,必须采用 Cgroup 技术对容器的资源进行限制。
Cgroup 技术也是 Linux 默认提供的功能,在 Linux 系统的 /sys/fs/cgroup 下面有一些子目录 cpu、memory 等。Cgroup 技术提供的功能就是可以基于这些目录实现对这些资源进行限制。
例如,在 /sys/fs/cgroup/cpu 下面创建一个 dockerContainer 子目录,系统就会自动在这个新建的目录下面生成一些配置文件,这些配置文件就是用来控制资源使用量的。例如可以在这些配置文件里面设置某个进程ID对CPU的最大使用率。
Cgroup 对其它内存、磁盘等资源也是采用同样原理做限制。
一个基础的容器镜像其实就是一个 rootfs,它包含操作系统的文件系统(文件和目录),但并不包含操作系统的内核。rootfs 是在容器里根目录上挂载的一个全新的文件系统,此文件系统与宿主机的文件系统无关,是一个完全独立的,用于给容器进行提供环境的文件系统。
对于一个Docker容器而言,需要基于 pivot_root 指令,将容器内的系统根目录切换到rootfs上。这样,有了这个 rootfs,容器就能够为进程构建出一个完整的文件系统,且实现了与宿主机的环境隔离。也正是有了rootfs,才能实现基于容器的本地应用与云端应用运行环境的一致。
另外,为了方便镜像的复用,Docker 在镜像中引入了层(Layer)的概念,可以将不同的镜像一层一层的迭在一起。这样,如果我们要做一个新的镜像,就可以基于之前已经做好的某个镜像的基础上继续做。
如上图,这个例子中最底层是操作系统引导。往上一层就是基础镜像层(Linux 的文件系统),再往上就是我们需要的各种应用镜像,Docker 会把这些镜像联合挂载在一个挂载点上,这些镜像层都是只读的。只有最上面的容器层是可读可写的。
这种分层的方案其实是基于联合文件系统 UnionFS(Union File System)的技术实现的。它可以将不同的目录全部挂载在同一个目录下。
举个例子,假如有文件夹 test1 和 test2 ,这两个文件夹里面有相同的文件,也有不同的文件。然后我们可以采用联合挂载的方式,将这两个文件夹挂载到 test3 上,那么 test3 目录里就有了 test1 和 test2 的所有文件(相同的文件有去重,不同的文件都保留)。
这个原理应用在 Docker 镜像中。比如有 2 个同学,同学 A 已经做好了一个基于 Linux 的 Java 环境的镜像,同学 S 想搭建一个 Java Web 环境,那么他就不必再去做 Java 环境的镜像了,可以直接基于同学 A 的镜像在上面增加 Tomcat 后生成新镜像即可。
随着微服务的流行,容器技术也相应的被大家重视起来。容器技术主要解决了以下两个问题。
环境一致性问题
例如 Java 的 jar/war 包部署会依赖于环境的问题(操着系统的版本,JDK 版本问题)。
镜像部署问题
例如 Java、Ruby、NodeJS 等等的发布系统是不一样的,每个环境都得很麻烦的部署一遍,采用 Docker 镜像,就屏蔽了这类问题。
部署实践
下图是 Docker 容器部署的一个完整过程:
更重要的是,拥有如此多服务的集群环境迁移、复制也非常轻松,只需选择好各服务对应的 Docker 服务镜像、配置好相互之间访问地址就能很快搭建出一份完全一样的新集群。
目前基于容器的调度平台有 Kubernetes(K8S)、Mesos、Omega。
本文主要介绍了微服务架构下的服务监控、容错隔离、访问安全以及结合容器技术实现服务发布和部署。初窥了微服务架构的模块,相信对微服务架构会有所帮助。再深入就需要有针对性的技术实践才能加深了解。
转自:Joyo,
链接:joyohub.com/micro-server-pro/
- EOF -
1、5分钟快速了解Docker的底层原理
2、手把手教你搭建一个监控告警平台
3、Java 云原生微服务框架 Quarkus 入门实践
看完本文有收获?请转发分享给更多人
关注「ImportNew」,提升Java技能
点赞和在看就是最大的支持❤️