微服务、容器

618 阅读17分钟

微服务

传统的单机应用都是以JAR包依赖的方式进行本地方法调用,而这样的强依赖关系导致系统越来越庞大,不容易部署维护,而且系统可用性差。所以我们把应用拆分成一个个独立的服务,这些服务通过RPC远程方法调用进行通信,这样就做到了模块间的解耦,每个模块不会受到其他模块的更新发布影响,解决了单体应用膨胀、协作开发效率低下的问题。那么对应每个服务中进行更小维度的服务号,就比如朋友圈模块再拆封成动态模块、评论模块、点赞模块等,就叫做微服务。

[服务发布和引用] 就是服务提供者的每个服务如何对外描述,服务消费者如何引用这个服务。比如要去定义服务的接口名、参数以及返回值类型等。 这里就对应到了consol和dubbo Restful API:用于HTTP协议的服务描述 XML:用于RPC协议的服务描述

【RPC】 是什么:远程过程调用,通过制定协议来规定对象的传输和序列化格式,为了帮助我们屏蔽网络通信的复杂,使得我们调用远端方法像调用本地方法一样,提供方只需要透出接口,在调用方的JVM里,注入的接口实际上绑定的是RPC框架提前生成的代理类,调用接口时会走到代理方法,由代理方法完成序列化、远程调用、反序列化,然后返回响应给调用方。(RPC就是把拦截到的方法参数,转化成可以在网络中传输的二进制,并且保证服务提供方能够正确还原出其语义,并返回响应,使得服务调用方像调用本地方法一样调用远程方法) 我们要想实现两个JVM的通信,肯定需要把JVM中的对象进行序列化和反序列化。那么这里牵扯到两点:序列化的效率、协议的可扩展性。 序列化:序列化框架的核心就是可逆的序列化协议,JVM中的对象根据协议转换成二进制流,传到对端后再根据协议转化回来。 兼容性和扩展性:支持多种入参出参,支持方法迭代/框架升级后依然可用。 安全性:没有漏洞,否则会被黑客入侵服务。 转化效率:快速。 序列化后的对象体积:网络传输,体积越小传输越快。 在使用RPC框架序列化时,也要注意对象(集合)不要过大,对象要尽量简单,继承层次尽量不超过两层。 协议的可扩展:把协议分为协议头和协议体,协议头中的固定字段记录协议体的长度。但这样协议头必须固定无法扩展,所以我们要在最前面定义固定部分,用于读取协议头的长度,然后根据协议规则分段解析,再读取协议体。 【RPC架构】: BootStrap模块:让Spring托管RPC框架,利用BeanFactory完成RPC接口的自动注入(动态代理)。链路追踪、过滤器链。 集群模块:服务发现、连接管理、负载均衡、路由、容错、配置管理。 传输模块:为了屏蔽业务代码中的网络通信,采用TCP协议,完成二进制数据的收发。 协议模块:对象与二进制数据的转化(序列化)、序列化协议的封装(断句规则)。还可以对转化后的二进制数据进行阈值压缩,减少二进制数据体积从而减少传输时的拆包次数,提高效率。 而且,我们要把每一个功能抽象成接口,将接口与实现分离,实现开闭原则。

RPC框架的异步(由服务调用方决定何时调用get()): image.png 而如果服务提供方的业务逻辑接口执行过慢,我们就应该使用异步RPC。我们让接口的返回值为CompletableFuture,接口在执行完成后的回调方法中给服务调用方发起通知,服务调用方再执行complete()方法。 RPC提供方向外透出接口,也不能让外界调用啊,只能在自己的RPC系统中调用。所以我们可以设置一个授权平台,连接服务提供方和调用方。但是RPC的所有流量都走授权平台,肯定是性能瓶颈,而且单点问题。所以我们可以使用单向秘钥,提前在服务提供方配置好它允许的调用方ID组成的私钥,然后调用方请求时要携带自己的ID私钥,若一致则可以调用。

[服务如何发布和订阅] 注册中心:服务提供者向注册中心注册自己提供的服务以及地址;消费者订阅注册中心中的服务地址,并可以获取服务清单列表。当服务提供者节点发生变更时,注册中心会同步变更,消费者会感知到这个变更并刷新本地内存中的缓存清单。所以一个注册中心就要提供服务注册/解注册接口、心跳汇报接口、服务订阅接口、服务变更查询接口等核心操作。

而且注册中心要满足集群部署..目录存储..服务健康状态监测..服务状态变更通知..白名单机制等。

Eureka:它属于应用内的注册与发现,就是它提供客户端和服务端的SDK,我们的应用需要引入Eureka的SDK,直接在应用内完成服务注册与发现。因为是应用内SDK,所以它只适用于提供者和消费者是同一种服务的情况。

Consul:它提供了一个第三方的服务管理器Registrator,能够监听服务部署的Docker实例来判断是否存活并负责服务提供者的注册和解注册;它还提供了Consul Template,可以定时从注册中心获取最新的服务清单。

注册中心的关注点 数据一致性:为了保证注册中心的高可用,那注册中心往往采用集群部署,所以我们就需要保证多数据中心之间的节点数据一致性。也就是C(一致性)A(可用性)P(分区容错性)理论。 P:网络故障导致网络被分成了互不连通的区域,导致区域之间数据无法共享。所以分区容错一般就是把数据复制到其他分区的节点上,保证分区后的数据一致性。 CA:但一旦要多分区数据共享,就会出现一致性和可用性问题。即多节点数据一致、多节点都更新成功才可用。 所以节点的数据越多,AP就越能保证,但C一致性无法保证;但我们要P的基础上要保证数据一致性就要增加机器,那么A可用性就无法保证。

CP:牺牲可用性来保证数据强一致性,比如ZK。ZK集群内只有一个Leader,这个Leader会向其他Followers同步数据信息,来保证数据的强一致性。但如果多个ZK之间出现网络分区问题,就会出现误选举出多个Leader,注册中心就不可用了。

AP:牺牲一致性来保证数据的可用性,比如Eureka。Eureka服务器单独保存服务清单,当出现网络分区时,每台服务器都可以完成独立的服务。但只因如此,它可能会出现数据信息不一致的情况。

Nacos:注册中心与配置中心,可以支持AP和CP 但注册中心更加注重的是可用性,注册中心一旦不可用,整个微服务的通信双方就无法通信导致服务崩溃了。而且强一致性在高并发写的情况下,会压力剧增导致集群崩溃,所以我们一般采用AP的架构。我们一般采用数据总线的形式来完成集群数据一致性,各个节点通过异步推拉结合 + 版本号去重的方式,高效地同步数据。 健康检测:亚健康状态,即节点无法正常通信,我们可以计算一段时间内节点的可用/总调用,得到可用率,若可用率低于某一值,则标记为亚健康,作为健康节点的后备。 负载均衡:我们需要能够根据流量和节点能力自动控制节点访问量的负载均衡插件。所以我们不应该仅仅用简单的一致性哈希、随机算法等仅保证均匀分布请求,我们还应该根据节点自身的能力来判断。所以我们可以根据每个服务节点的负载指标、CPU核数、内存大小、处理请求的平均耗时等来控制一个百分比来分配流量。

[服务如何调用] 我们得到了服务提供方的地址,就要发起调用。所以我们要关注于:服务通信采取什么网络协议、数据传输采用什么方式(同步异步、单连接多路复用)、数据压缩采用什么格式。

zuul:网关,配置规则来为请求URL提供路由服务。

HTTP调用: 使用时需要注意的点:①框架设置的默认超时时间是否合理 ②设置超时重试,但接口的幂等性是否支持超时重试 ③框架的最大连接数是否有限制,导致HTTP的瓶颈为最大连接数。 连接超时参数:等待连接时间无非的TCP三次握手时间,若设置过长会导致影响下游业务。发生连接超时时,要排查清楚连接双方,若客户端负载均衡连接服务端,则要排查服务端;若客户端通过Nginx转发到服务端,则排查Nginx(因为客户端与Nginx建立的连接呀)。 读取超时参数:即客户端从Socket上读取到数据的最大超时时间。但这只是客户端接受服务端数据的超时时间,而客户端的请求可能已经打到服务端,所以服务端依然会执行接口,所以引起超时的原因可能是网络数据传输慢、服务端接口执行慢。所以我们在排查时需要结合服务端的业务状态。而且HTTP调用一般是同步获取,即客户端线程也会等待。若我们设置的超时时间过长,客户端线程会一直等待导致拖慢其他模块。

重试:重试其实就是RPC框架捕捉到出错的HTTP请求,然后重新请求一次。但网络抖动可能出现在响应失败上,重试最终造成服务提供节点的幂等性问题;而且重试的接口可能很慢,导致引发用户方面的接口超时;而且重试一般是负载均衡到其他节点,但万一又打向了这个节点就不好了。所以我们可以通过在重试前判断是否超时、重设超时时间、配置重试白名单过滤节点来解决。

[服务如何监控] 指标收集、数据处理、数据展示、服务追踪

在一个微服务中,我们对系统的监控大致分为用户体验(用户体验速度 前端埋点)、业务监控(业务结果 订单数、交易金额等)、应用监控(接口调用次数、响应时间、报错次数)、中间件监控、基础平台监控(CPU、内存、磁盘等)。如果我们对于系统的每个部分都采取不同的监控方式,那么在发生故障时,只能上下游各个环节一起碎片化协作,无法做到全链路的定位,,会导致滞后性。所以我们应该把每个模块的监控信息自动关联起来,做到实时、直观、整体。我们可以使用代理对每个模块采集数据,导入Monitor Serivce确定当前节点的状态并持久化,前端每3S拉取并显示。 ①节点信息采集:对于Web应用,我们可以采集最近3S的接口调用次数、平均响应时间、出错次数、接口status等;对于MySQL或Redis,代理每3秒尝试连接数据库并简单的读写; ②接入监控系统:对于Web应用,我们需要利用SDK提供的统计方法得到一些数值,然后通过接口透出给前端

[服务如何治理] 配置中心: 为什么:拆分微服务后,每个系统都有自己的配置,而且由于服务治理的需要,每个系统都需要动态配置,以达到动态降级、切流量、扩缩容、IP管理的目的。而且如果都配置在本地,那么改一次配置就要重新打包发布上线(若在代码中还需要重新编译),非常麻烦。所以我们需要统一的配置中心来管理。【也就是说,配置中心主要解决的是修改配置后的重启问题】 如何存储:在MySQL或Redis中以KV形式存储配置文件的具体路径 如何动态变更:客户端轮询。且每次访问MD5值,若与之前的发生变化,则说明有动态变更,则拉取。(减少中心服务器带宽)。所以我们就可以只关注于配置中心,应用程序会自动拉取最新配置。 可用性:客户端内存缓存、文件缓存。 保护: 服务提供方限流:通过注册中心下发限流阈值,实现服务提供方集群对某一服务调用方IP的限流,服务提供方单点均摊集群的总限流,一般采用令牌桶算法、滑动窗口等,但这样每一次扩容就得重新配置呀。所以我们可以起一个单独的限流服务,流量请求先打到限流服务,负载均衡得到服务单点后判断是否超过阈值,若超过了则直接返回限流异常,则我们的单点阈值就可以很轻松地配置了。 服务调用方熔断:服务调用方调用的下游服务出现异常后,熔断器会收集异常指标,达到熔断条件时就会触发熔断器,再次访问次下游服务时就直接返回失败。因为熔断是维护在服务调用方的,所以我们可以在动态代理发出请求时,根据请求IP直接做熔断处理,就不用再序列化和传输了。

[服务优雅关闭] 在服务重启的时候,连接还没有断开,所以注册中心和调用方无法感知与预测,所以可能就会把请求打到正在重启的机器上,导致请求失败。所以我们可以提前在操作系统中注册钩子函数,在开始关闭时,提供方不再接受请求(返回异常)、并且对已经连接的调用方通知下线。 [服务优雅启动] 在运行过程中,JVM会把高频的代码编译成机器码、被加载的类缓存到JVM缓存中,避免重复编译和加载。而我们重启后,这些缓存就没有了,那么之前的稳定状态就成了高负载状态。我们应该进行启动预热,随着时间的增加而给他相应的流量。所以我们可以在注册中心上维护服务提供方的启动时间,把这个指标作用在负载均衡上。而且启动时,我们要等服务提供方的类完全启动后,再把它注册到注册中心上,即延迟保留。我们可以使用钩子函数,在服务完全启动后执行,预热JVM指令、加载资源、注册到注册中心上。

容器

容器实际上是一种特殊的进程,在Linux上启动容器时,Linux会为这个容器进程设置一些参数,为这个容器应用进程隔离不相关的资源,实现进程空间的隔离。所以我们在启动容器后执行ps,可以看到此容器进程pid=1,但在我们的宿主机上它其实并不是1,只是说Linux的Namespace机制为它创建了一个彼此隔离的进程空间。这就使得我们的应用程序放在容器中,那相当于这个容器与主机的其他进程是有隔离边界的,互不干扰,所以容器就像一个集装箱一样,可以把应用程序搬来搬去。(虚拟机是新创建进程,比容器多一个虚拟OS)。所以,在Linux上的多个容器是共享操作系统内核的。所以虽然容器进程被在表面上被隔离起来,但它能够使用到达CPU、内存等资源,是可以随时被宿主机上的其他进程占用的,这显然不符合沙盒的特性。所以就有了Linux Cgroups,为一组进程(tasks文件中的进程组)设置资源限制,使得它所需要的资源与其他进程隔离,我们在docker run --..为它指定参数。

既然容器要符合沙盒特性,就要做到随处移动随处使用,当我们应用程序所依赖的环境都是要基于它初始的操作系统中的底座,即文件系统和目录(容器使用宿主主机的操作系统内核)。所以就有了镜像,即rootfs(根文件系统)。容器进程启动时,Mount Namespace会为其重新挂载根目录"/",把这个容器进程原本的文件目录挂载到Linux的跟目录上,而且这个挂载对宿主机不可见。那么容器进程的视图就转变为了操作系统的根视图,它具有该镜像进程运行所需要的全部文件系统和目录。那这个挂载到根目录上、用来为容器进程提供隔离后的执行环境所需的文件系统,就是容器镜像,也叫rootfs(根文件系统)。

镜像是分层的,我们可以以增量的形式来修改镜像,把它变成我们需要的版本。这就依赖于联合文件系统,一层对应着一个增量,使用镜像时Docker就会把这些增量联合挂载到统一的一个目录上,表现为一个完整的文件目录,供容器进程统一使用。

制作容器镜像:

①编写Dockerfile文件(FROM基础镜像、RUN install依赖、ENV环境等),然后执行docker build -t name制作镜像。

②docker run [-p port宿:port器] imgName imgTag:启动镜像。

③docker target:给镜像起一个完整的名字。

④docker commit:把一个正在运行的容器直接提交给镜像保存,也就是把原先容器的镜像只读层和当前的最上面可写可读层,打包组成一个新的镜像。

⑤docker push上传镜像到Docker Hub

docker image is:查看本机所有镜像

docker image pull :拉取镜像到docker主机仓库中