读《云原生模式》笔记及思考

Posted 咦这里有个按钮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了读《云原生模式》笔记及思考相关的知识,希望对你有一定的参考价值。

虽然之前使用过各微服务组件,但对于为什么要使用它们一直存在着疑问,读了《云原生模式——设计拥抱变化的软件》,有了相对清晰的认识。

1、云和云原生

云是基础硬件设备经过虚拟化技术抽象而成的共享池,按需提供标准化的虚拟基础设施资源即IaaS(基础设施即服务), 该模式底层可以使用配置相对较低的硬件设备,实现了低成本;从池中按需获取资源,不受硬件设备的边界限制,实现资源更加高效的利用;无需考虑底层物理硬件的差异,只要经由虚拟化,所有的资源都是标准化的。

云原生平台是在云虚拟基础设施之上,使得应用直接部署于其上即可运行的环境,小到一个配置好jdk的Docker容器,大到由kubernetes管理的包含服务注册、配置中心、数据库、消息总线、日志聚合管理等服务的集群环境,都可称作云原生平台,提供云原生平台的服务即PaaS(平台即服务) 。

符合云原生环境,按照云的思想模式而开发的软件,就是云原生软件。它应该是为了高度分布式而设计的,应该能够正常运行在一个不断变化的环境中(环境中的各服务随时可能失效),且自身也在不断变化(不断地升级迭代)。

2、云原生带来了什么

在如今的大数据时代,海量的数据和用户对产品越来越高的要求标准,使分布式成为必然,而云原生最是符合分布式的需求。通过将云基础设施进一步抽象化为云原生平台,许多运维工作都变得简单了:无需搭建环境,一个镜像即搞定,甚至有了kubernetes,连分布式部署也变得极为简单,只需告诉平台我们期望的,比如多少个应用实例、多少个数据库实例,剩下的选择在哪部署、如何部署等问题都由平台搞定,这正是依赖于云技术提供的标准性才能得以实现。平台内置的各种工具还能帮助我们监控集群运行状态,通过比对器定期比对集群真实情况和我们的期望情况,自动进行集群的扩容及收缩。持续交付也变得简单了,只要在集群中逐渐用新的实例替换旧的实例,辅以负载均衡,使得“永不停机”成为可能。

在高度分布式的云环境下,我们不能再将程序出错看作一个小概率事件,当一个服务存在成百上千个实例,出错可以说是必然的,因此我们所设计的程序应该能够拥抱变化,应对随时可能发生的环境问题。解决方案很简单,就是冗余,我们需要有足够多的冗余资源来应对各种突发状况,比如灾备服务器。幸运的是,虚拟化技术突破了硬件个体的边界,所有的资源都被高效利用。一个极致的表现就是“无服务”,即当一个服务在被调用时,它的环境、应用才被即时创建,服务提供完毕后这些资源立即被回收。资源的高效利用和底层设备较低的成本,使得允许大量的冗余资源存在。

3、云原生模式

  • 请求/响应 ——> 事件驱动

    前者是我们已经非常熟悉的交互模式了,微服务间通过Http协议互相调用,在Controller层的接口负责响应请求。这种模式简单高效,但各服务间形成了高度耦合,只要被调用方不可用,调用方便无法正常运行,这显然无法支持微服务的快速迭代。

    事件驱动模式的核心是消息队列,所有微服务变为与消息队列进行交互,可以往消息队列广播事件,也可以从消息队列消费事件。这么做的好处是,使微服务间完全解耦,一方的运行状态不受其它任何一方的影响。在该模式下的最佳实践还包括命令查询责任隔离(CQRS),说白了就是查询接口和写入接口不应在同一个Controller中,因为读写接口用到的实体类往往不是完全相同的,甚至读写接口可以使用不同的协议,进而实现更高的灵活度。

  • 水平伸缩和无状态

    水平伸缩指在集群中添加或减少应用实例,无状态指每个实例不保留业务相关的数据;无状态是实现灵活的水平伸缩的必要条件。想象如下场景,一个认证服务器是多实例的,它将用户的token保存在本地内存中,那么当该实例宕机时,即使负载均衡可以将请求路由到其它正常的实例中,但那个实例并没有用户token,因此用户需要重新登录,无疑会影响用户体验。

    解决方案是构建专门存储状态的服务,比如redis,认证服务只需将token保存在redis中,并且每个实例都访问同一个redis服务获取状态,就能实现认证服务的无状态了,服务集群的水平伸缩也可以放心地由平台完成。

  • 配置服务

    分布式下,不可能每次修改配置文件都去重新部署每一个实例,因此需要一种自动化、灵活的获取配置的方式。最原始的方法是System.get()获取环境变量,但这样的代码在项目中肯定越少越好,因为它的可读性差,且你无法一目了然哪些变量是从应用外部获取的。因而,进一步地抽象出配置层,在Spring中的表现为@Value(" {sth}") 从属性文件注入属性值。并且,属性文件应只是一个中间件,属性文件通过ipaddress= {INSTANCE_IP:127.0.0.1} 这样的方式从环境变量中获取属性,且拥有一个默认值。再进一步的,使用配置服务器,也就是配置中心,它底层通过Git来实现配置的版本控制。最佳实践为:实例的属性文件作为系统配置和应用程序配置的公共配置层,系统配置数据通过环境变量注入,应用程序配置数据通过配置服务器注入。

  • 应用程序的生命周期

    使用配置中心后,自然而然引出的一个问题就是:当我们修改了配置中心的配置,应用实例应该在什么时候启用这些配置?最简单的方式就是重启应用,在应用启动时应用新的配置,这也是我们推荐的方式;spring的refresh()方法提供了无需重启应用也可以应用新配置的能力,但可能引发未知问题,而且问题场景难以重现。确定了应用新配置的基本方案(即重启应用)之后,接下来的问题是如何重启?

    在分布式下常用的两种方式为蓝/绿升级和滚动升级,前者就是创建和已有实例一样多的新实例,这些新实例应用了新的配置,或者是新版本的代码,待新实例创建完成,一次性把流量转移到新实例,旧实例再全部停用;滚动升级则是分批地用新实例替换旧实例,在这过程中,保持实例数量,新旧实例都会接收到请求流量。

    两种方式的差别在于,蓝/绿升级简单粗暴,容易实现,但需要更多的资源,因为在转换过程中,需要有原来双倍的实例运行。滚动升级更加灵活,可以真正地实现“永不停机”,且所需资源也较少,但其需要应用有同时支持新旧版本的能力,因为新旧实例是在同时提供服务的。

    云原生软件的特性:可重复性,即在正确配置后,应不允许通过ssh进入容器的运行时环境,只有这样,运行实例才是可重复的,否则一个被人为改动的运行时环境是不可复制的;正确的做法是把一切管理操作都交给云原生平台。那么,该如何进行故障排除呢?这就需要完善的日志和指标,云原生平台也提供了这样的基础服务。

  • 服务发现与动态路由

    在分布式环境下,我们需要把一个应用的多个实例抽象为一个服务,对于客户端来说,它面对的仅仅是一个服务,访问该服务就叫服务发现,而在这个服务背后可能存在着多个实例,需要动态路由来将请求分发到具体的实例上。

    服务发现必须涉及的一个话题是负载均衡,其实现方式有服务端的负载均衡和客户端的负载均衡,后者将路由表保存在本地,且比前者少一次网络跳转,因此可能有更高的性能,但实现较为复杂。那么接下来的问题是,如何保证路由表及时更新?方式一是负载均衡服务器不断地去访问各实例,获取其状态并更新路由表,方式二是各实例定期将自己的状态广播给负载均衡服务器。

  • 客户端的服务保障——重试

    上面谈到名称服务是注重可用性的,客户端需要自己去对可能发生的不一致性或错误兜底。

    最常见的方案就是重试,重试需要考虑的几个重要因素:一是等待多久无响应后执行重试?等待太久可能导致客户端超时,等待时间太短又无意义;二是重复次数,不限制重复次数可能导致重试风暴,进而服务崩溃;三是重试时间间隔,同样是为了避免重试风暴。此外,应只在安全的情况下重试,比如转账操作的重试就是不安全的。

    第二个方案是回退,即当主逻辑执行失败时执行的兜底操作,一个好的方式是提前将正确返回的结果缓存在Redis(还记得最好要保证服务器无状态么),当执行逻辑失败时,取出Redis中的缓存返回给客户端。

  • 服务端的服务保障——断路器和网关

    上面提到了客户端为了防止对服务器发出重试风暴,应设定重试次数和重试间隔时间,但是这项保障工作不能完全丢给客户端,服务端也必须设法保护自己。

    断路器就是用于应对类似重试风暴的风险的。当服务器出现故障且负载不断增大,抑或是负载过大导致服务器故障,断路器都将“打开”,不允许流量通过,并直接返回错误响应,最小化客户端的等待时间;若想尝试服务器是否恢复,断路器可以置于“半开”状态;当服务器健康,断路器处于“闭合状态”。

    如果说断路器是每个服务保护自己的盾牌,网关就是保护整个集群所有服务的保护伞,它提供的服务包括:1、统一的身份验证和授权;2、数据加密;3、类似断路器的全局限流机制;4、记录全局访问日志。为了提供以上服务,网关可能需要和特定的服务相交互。

    在分布式中,更好的实践是每个服务实例对应一个网关,网关可以集成断路器、客户端负载均衡、日志监控等功能,使应用开发者可以更专心于业务开发。网关比如Zuul是可以通过添加依赖嵌入应用的,但这势必对代码及配置文件产生改动,不符合云原生的理念,那么,如何避免把网关和应用代码耦合在一起呢?答案是使用Sidecar,kubernetes为此提供了支持,在pod中可以运行多个容器,且容器间可通过localhost互相通信,只要把服务和网关放在一个pod中,就能达到以上效果。

  • 日志收集

    在云原生世界中,每个服务只要将日志输出到stdout和stderr,云原生平台就能接收这些日志并聚合,进一步的,可通过ELK技术栈进行集群的实时监控。

    除了日志外,框架一般都会暴露一些指标接口,供获取服务状态信息,获取这些指标的方式一般有拉和推,拉就是指标服务器定期访问各服务的指标接口拉取指标信息,推则是各服务将指标信息推送给指标服务器,这也许不好实现,因为需要各个服务进行配合改造,但别忘了我们有云原生平台,通过上面提及的Sidecar,在pod中放置一个代理服务器,即可监控目标服务的流量,虽然无法获取详细的服务状态信息,但可以用完全无侵入的方式实现对服务的HTTP状态码、等待时间等信息进行监控,也是个根棒的福利了。

  • 云原生数据

    云原生软件的特性同样适用于数据层,即冗余的、可伸缩的、模块化的。冗余性很好理解,比如灾备数据库;在云原生环境中,数据库大多设计为可水平伸缩的,即数据库实例数量可动态调整;在此,我们将重点讨论模块化,当多个服务共享一个数据库,虽然服务间看似独立,但共享数据库将建立起传递依赖性,比如争夺数据库的锁资源。为了解决这个问题,实现服务真正的“自治”,每个微服务应有自己的数据库;为了微服务间的解耦,应使用消息队列中间件,消息提供者和消费者仅和消息队列相联系,实现响应式的数据传递;为了保证服务的可用性,可使用缓存。

    一个特殊的问题是,如何追溯数据的源头?数据源应该来自于某个微服务么?一个好的实践是将事件日志作为数据源,所有的微服务在监听到事件时,第一件事就是将事件发布到事件日志,各微服务按需从事件日志中获取数据存到本地,从而使所有服务都转化为事件驱动;在该情况下,事件日志应是永久保留的。


以上是关于读《云原生模式》笔记及思考的主要内容,如果未能解决你的问题,请参考以下文章

容器云原生技术基础及云原生应用思考

对于云原生数据系统的思考

云原生底层系统思考

云原生背景下的运维价值思考与实践

云原生|我对云原生软件架构的观察与思考

云原生已来,只是分布不均