在介绍微服务时,首先得先理解什么是微服务,顾名思义,微服务得从两个方面去理解,什么是"微"、什么是"服务", 微,狭义来讲就是体积小、著名的"2 pizza 团队"很好的诠释了这一解释(2 pizza 团队最早是亚马逊 CEO Bezos提出来的,意思是说单个服务的设计,所有参与人从设计、开发、测试、运维所有人加起来只需要2个披萨就够了 )。 而所谓服务,一定要区别于系统,服务一个或者一组相对较小且独立的功能单元,是用户可以感知最小功能集。
微服务最早由Martin Fowler与James Lewis于2014年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。
在传统的IT行业软件大多都是各种独立系统的堆砌,这些系统的问题总结来说就是扩展性差,可靠性不高,维护成本高。到后面引入了SOA服务化,但是,由于SOA早期均使用了总线模式,这种总线模式是与某种技术栈强绑定的,比如:J2EE。这导致很多企业的遗留系统很难对接,切换时间太长,成本太高,新系统稳定性的收敛也需要一些时间。最终SOA看起来很美,但却成为了企业级奢侈品,中小公司都望而生畏。
最期的单体架构带来的问题
单体架构在规模比较小的情况下工作情况良好,但是随着系统规模的扩大,它暴露出来的问题也越来越多,主要有以下几点:
微服务与单体架构区别
单体架构所有的模块全都耦合在一块,代码量大,维护困难,微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比较好解决。
单体架构所有的模块都共用一个数据库,存储方式比较单一,微服务每个模块都可以使用不同的存储方式(比如有的用redis,有的用mysql等),数据库也是单个模块对应自己的数据库。
单体架构所有的模块开发所使用的技术一样,微服务每个模块都可以使用不同的开发技术,开发模式更灵活。
微服务与SOA区别
微服务,从本质意义上看,还是SOA架构。但内涵有所不同,微服务并不绑定某种特殊的技术,在一个微服务的系统中,可以有Java编写的服务,也可以有 Python编写的服务,他们是靠Restful架构风格统一成一个系统的。所以微服务本身与具体技术实现无关,扩展性强。
微服务可以按照业务功能本身的独立性来划分,如果系统提供的业务是非常底层的,如:操作系统内核、存储系统、网络系统、数据库系统等等,这类系统都偏底层,功能和功能之间有着紧密的配合关系,如果强制拆分为较小的服务单元,会让集成工作量急剧上升,并且这种人为的切割无法带来业务上的真正的隔离,所以无法做到独立部署和运行,也就不适合做成微服务了。
能不能做成微服务,取决于四个要素:
从单体式结构转向微服务架构中会持续碰到服务边界划分的问题:比如,我们有user服务来提供用户的基础信息,那么用户的头像和图片等是应该单独划分为一个新的service更好还是应该合并到user服务里呢?如果服务的粒度划分的过粗,那就回到了单体式的老路;如果过细,那服务间调用的开销就变得不可忽视了,管理难度也会指数级增加。目前为止还没有一个可以称之为服务边界划分的标准,只能根据不同的业务系统加以调节。
拆分的大原则是当一块业务不依赖或极少依赖其它服务,有独立的业务语义,为超过2个的其他服务或客户端提供数据,那么它就应该被拆分成一个独立的服务模块。
微服务设计原则
特性
特点
缺点
目前微服务的开发框架,最常用的有以下四个:
Spring Boot:旨在简化创建产品级的Spring应用和服务,简化了配置文件,使用嵌入式Web服务器,含有诸多开箱即用微服务功能,可以和Spring Cloud联合部署。
Spring Cloud:微服务工具包,为开发者提供了在分布式系统的配置管理、服务发现、断路器、智能路由、微代理、控制总线等开发工具包。
传统的开发方式,所有的服务都是本地的,UI可以直接调用,现在按功能拆分成独立的服务,跑在独立的一般都在独立的虚拟机上的Java进程了。客户端UI如何访问他的?后台有N个服务,前台就需要记住管理N个服务,一个服务下线/更新/升级,前台就要重新部署,这明显不服务我们 拆分的理念,特别当前台是移动应用的时候,通常业务变化的节奏更快。另外,N个小服务的调用也是一个不小的网络开销。还有一般微服务在系统内部,通常是无状态的,用户登录信息和权限管理最好有一个统一的地方维护管理(OAuth)。
所以,一般在后台N个服务和UI之间一般会一个代理或者叫API Gateway,他的作用包括:
因为所有的微服务都是独立的Java进程跑在独立的虚拟机上,所以服务间的通行就是IPC(inter process communication),已经有很多成熟的方案。现在基本最通用的有两种方式。这几种方式,展开来讲都可以写本书,而且大家一般都比较熟悉细节了, 就不展开讲了。
一般同步调用比较简单,一致性强,但是容易出调用问题,性能体验上也会差些,特别是调用层次多的时候。RESTful和RPC的比较也是一个很有意 思的话题。一般REST基于HTTP,更容易实现,更容易被接受,服务端实现技术也更灵活些,各个语言都能支持,同时能跨客户端,对客户端没有特殊的要 求,只要封装了HTTP的SDK就能调用,所以相对使用的广一些。RPC也有自己的优点,传输协议更高效,安全更可控,特别在一个公司内部,如果有统一个的开发规范和统一的服务框架时,他的开发效率优势更明显些。就看各自的技术积累实际条件,自己的选择了。
而异步消息的方式在分布式系统中有特别广泛的应用,他既能减低调用服务之间的耦合,又能成为调用之间的缓冲,确保消息积压不会冲垮被调用方,同时能 保证调用方的服务体验,继续干自己该干的活,不至于被后台性能拖慢。不过需要付出的代价是一致性的减弱,需要接受数据最终一致性;还有就是后台服务一般要 实现幂等性,因为消息发送出于性能的考虑一般会有重复(保证消息的被收到且仅收到一次对性能是很大的考验);最后就是必须引入一个独立的broker,如 果公司内部没有技术积累,对broker分布式管理也是一个很大的挑战。
在微服务架构中,一般每一个服务都是有多个拷贝,来做负载均衡。一个服务随时可能下线,也可能应对临时访问压力增加新的服务节点。服务之间如何相互感知?服务如何管理?这就是服务发现的问题了。一般有两类做法,也各有优缺点。基本都是通过ZooKeeper等类似技术做服务注册信息的分布式管理。当 服务上线时,服务提供者将自己的服务信息注册到ZK(或类似框架),并通过心跳维持长链接,实时更新链接信息。服务调用者通过ZK寻址,根据可定制算法,找到一个服务,还可以将服务信息缓存在本地以提高性能。当服务下线时,ZK会发通知给服务客户端。
客户端做:优点是架构简单,扩展灵活,只对服务注册器依赖。缺点是客户端要维护所有调用服务的地址,有技术难度,一般大公司都有成熟的内部框架支持,比如Dubbo。
服务端做:优点是简单,所有服务对于前台调用方透明,一般在小公司在云服务上部署的应用采用的比较多。
分布式最大的特性就是网络是不可靠 的。通过微服务拆分能降低这个风险,不过如果没有特别的保障,结局肯定是噩梦。我们刚遇到一个线上故障就是一个很不起眼的SQL计数功能,在访问量上升 时,导致数据库load彪高,影响了所在应用的性能,从而影响所有调用这个应用服务的前台应用。所以当我们的系统是由一系列的服务调用链组成的时候,我们必须确保任一环节出问题都不至于影响整体链路。相应的手段有很多:
这里有一个图非常好的总结微服务架构需要考虑的问题,包括
服务之间需要创建一种服务发现机制,用于帮助服务之间互相感知彼此的存在。服务启动时会将自身的服务信息注册到注册中心,并订阅自己需要消费的服务。
服务注册中心是服务发现的核心。它保存了各个可用服务实例的网络地址(IPAddress和Port)。服务注册中心必须要有高可用性和实时更新功能。上面提到的 Netflix Eureka 就是一个服务注册中心。它提供了服务注册和查询服务信息的REST API。服务通过使用POST请求注册自己的IPAddress和Port。每30秒发送一个PUT请求刷新注册信息。通过DELETE请求注销服务。客户端通过GET请求获取可用的服务实例信息。 Netflix的高可用(Netflix achieves high availability )是通过在Amazon EC2运行多个实例来实现的,每一个Eureka服务都有一个弹性IP Address。当Eureka服务启动时,有DNS服务器动态的分配。Eureka客户端通过查询 DNS来获取Eureka的网络地址(IP Address和Port)。一般情况下,都是返回和客户端在同一个可用区Eureka服务器地址。 其他能够作为服务注册中心的有:
ZooKeeper服务注册和发现
简单来讲,ZooKeeper可以充当一个服务注册表(Service Registry),让多个服务提供者形成一个集群,让服务消费者通过服务注册表获取具体的服务访问地址(IP + 端口)去访问具体的服务提供者。如下图所示:
具体来说,ZooKeeper就是个分布式文件系统,每当一个服务提供者部署后都要将自己的服务注册到zookeeper的某一路径上:/{service}/{version}/{ip:port}, 比如我们的HelloWorldService部署到两台机器,那么zookeeper上就会创建两条目录:分别为/HelloWorldService/1.0.0/100.19.20.01:16888 /HelloWorldService/1.0.0/100.19.20.02:16888。
ZooKeeper提供了“心跳检测”功能,它会定时向各个服务提供者发送一个请求(实际上建立的是一个socket长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,并将其剔除,比如100.19.20.02这台机器如果宕机了,那么ZooKeeper上的路径就会只剩/HelloWorldService/1.0.0/100.19.20.01:16888。
服务消费者会去监听相应路径(/HelloWorldService/1.0.0),一旦路径上的数据有任务变化(增加或减少),ZooKeeper都会通知服务消费方服务提供者地址列表已经发生改变,从而进行更新。
更为重要的是ZooKeeper与生俱来的容错容灾能力(比如leader选举),可以确保服务注册表的高可用性。
服务高可用的保证手段,为了保证高可用,每一个微服务都需要部署多个服务实例来提供服务。此时客户端进行服务的负载均衡。
负载均衡的常见策略
容错,这个词的理解,直面意思就是可以容下错误,不让错误再次扩张,让这个错误产生的影响在一个固定的边界之内,“千里之堤毁于蚁穴”我们用容错的方式就是让这种蚁穴不要变大。那么我们常见的降级,限流,熔断器,超时重试等等都是容错的方法。
在调用服务集群时,如果一个微服务调用异常,如超时,连接异常,网络异常等,则根据容错策略进行服务容错。目前支持的服务容错策略有快速失败,失效切换。如果连续失败多次则直接熔断,不再发起调用。这样可以避免一个服务异常拖垮所有依赖于他的服务。
容错策略
熔断技术可以说是一种“智能化的容错”,当调用满足失败次数,失败比例就会触发熔断器打开,有程序自动切断当前的RPC调用,来防止错误进一步扩大。实现一个熔断器主要是考虑三种模式,关闭,打开,半开。各个状态的转换如下图。
我们在处理异常的时候,要根据具体的业务情况来决定处理方式,比如我们调用商品接口,对方只是临时做了降级处理,那么作为网关调用就要切到可替换的服务上来执行或者获取托底数据,给用户友好提示。还有要区分异常的类型,比如依赖的服务崩溃了,这个可能需要花费比较久的时间来解决。也可能是由于服务器负载临时过高导致超时。作为熔断器应该能够甄别这种异常类型,从而根据具体的错误类型调整熔断策略。增加手动设置,在失败的服务恢复时间不确定的情况下,管理员可以手动强制切换熔断状态。最后,熔断器的使用场景是调用可能失败的远程服务程序或者共享资源。如果是本地缓存本地私有资源,使用熔断器则会增加系统的额外开销。还要注意,熔断器不能作为应用程序中业务逻辑的异常处理替代品。
有一些异常比较顽固,突然发生,无法预测,而且很难恢复,并且还会导致级联失败(举个例子,假设一个服务集群的负载非常高,如果这时候集群的一部分挂掉了,还占了很大一部分资源,整个集群都有可能遭殃)。如果我们这时还是不断进行重试的话,结果大多都是失败的。因此,此时我们的应用需要立即进入失败状态(fast-fail),并采取合适的方法进行恢复。
我们可以用状态机来实现CircuitBreaker,它有以下三种状态:
保证核心服务的稳定性。为了保证核心服务的稳定性,随着访问量的不断增加,需要为系统能够处理的服务数量设置一个极限阀值,超过这个阀值的请求则直接拒绝。同时,为了保证核心服务的可用,可以对否些非核心服务进行降级,通过限制服务的最大访问量进行限流,通过管理控制台对单个微服务进行人工降级。
SLA:Service-LevelAgreement的缩写,意思是服务等级协议。 是关于网络服务供应商和客户间的一份合同,其中定义了服务类型、服务质量和客户付款等术语。 典型的SLA包括以下项目:
这里说的网关是指API网关,直面意思是将所有API调用统一接入到API网关层,有网关层统一接入和输出。一个网关的基本功能有:统一接入、安全防护、协议适配、流量管控、长短链接支持、容错能力。有了网关之后,各个API服务提供团队可以专注于自己的的业务逻辑处理,而API网关更专注于安全、流量、路由等问题。
最简单的缓存就是查一次数据库然后将数据写入缓存比如Redis中并设置过期时间。因为有过期失效因此我们要关注下缓存的穿透率,这个穿透率的计算公式,比如查询方法queryOrder(调用次数1000/1s)里面嵌套查询DB方法queryProductFromDb(调用次数300/s),那么Redis的穿透率就是300/1000,在这种使用缓存的方式下,是要重视穿透率的,穿透率大了说明缓存的效果不好。还有一种使用缓存的方式就是将缓存持久化,也就是不设置过期时间,这个就会面临一个数据更新的问题。一般有两种办法,一个是利用时间戳,查询默认以Redis为主,每次设置数据的时候放入一个时间戳,每次读取数据的时候用系统当前时间和上次设置的这个时间戳做对比,比如超过5分钟,那么就再查一次数据库。这样可以保证Redis里面永远有数据,一般是对DB的一种容错方法。还有一个就是真正的让Redis做为DB使用。就是图里面画的通过订阅数据库的binlog通过数据异构系统将数据推送给缓存,同时将将缓存设置为多级。可以通过使用JvmCache作为应用内的一级缓存,一般是体积小,访问频率大的更适合这种JvmCache方式,将一套Redis作为二级remote缓存,另外最外层三级Redis作为持久化缓存。
超时与重试机制也是容错的一种方法,凡是发生RPC调用的地方,比如读取Redis,DB,MQ等,因为网络故障或者是所依赖的服务故障,长时间不能返回结果,就会导致线程增加,加大CPU负载,甚至导致雪崩。所以对每一个RPC调用都要设置超时时间。对于强依赖RPC调用资源的情况,还要有重试机制,但是重试的次数建议1-2次,另外如果有重试,那么超时时间就要相应的调小,比如重试1次,那么一共是发生2次调用。如果超时时间配置的是2s,那么客户端就要等待4s才能返回。因此重试+超时的方式,超时时间要调小。这里也再谈一下一次PRC调用的时间都消耗在哪些环节,一次正常的调用统计的耗时主要包括: ①调用端RPC框架执行时间 + ②网络发送时间 + ③服务端RPC框架执行时间 + ④服务端业务代码时间。调用方和服务方都有各自的性能监控,比如调用方tp99是500ms,服务方tp99是100ms,找了网络组的同事确认网络没有问题。那么时间都花在什么地方了呢,两种原因,客户端调用方,还有一个原因是网络发生TCP重传。所以要注意这两点。
在抗量这个环节,Servlet3异步的时候,有提到过线程隔离。线程隔离的之间优势就是防止级联故障,甚至是雪崩。当网关调用N多个接口服务的时候,我们要对每个接口进行线程隔离。比如,我们有调用订单、商品、用户。那么订单的业务不能够影响到商品和用户的请求处理。如果不做线程隔离,当访问订单服务出现网络故障导致延时,线程积压最终导致整个服务CPU负载满。就是我们说的服务全部不可用了,有多少机器都会被此刻的请求塞满。那么有了线程隔离就会使得我们的网关能保证局部问题不会影响全局。
关于降级限流的方法业界都已经有很成熟的方法了,比如FAILBACK机制,限流的方法令牌桶,漏桶,信号量等。这里谈一下我们的一些经验,降级一般都是由统一配置中心的降级开关来实现的,那么当有很多个接口来自同一个提供方,这个提供方的系统或这机器所在机房网络出现了问题,我们就要有一个统一的降级开关,不然就要一个接口一个接口的来降级。也就是要对业务类型有一个大闸刀。还有就是 降级切记暴力降级,什么是暴力降级的,比如把论坛功能降调,结果用户显示一个大白板,我们要实现缓存住一些数据,也就是有托底数据。限流一般分为分布式限流和单机限流,如果实现分布式限流的话就要一个公共的后端存储服务比如Redis,在大Nginx节点上利用Lua读取Redis配置信息。我们现在的限流都是单机限流,并没有实施分布式限流。
API网关是一个串行的调用,那么每一步发生的异常要记录下来,统一存储到一个地方比如Elasticsearch中,便于后续对调用异常的分析。鉴于公司Docker申请都是统一分配,而且分配之前Docker上已经存在3个Agent了,不再允许增加。我们自己实现了一个Agent程序,来负责采集服务器上面的日志输出,然后发送到Kafka集群,再消费到Elasticsearch中,通过Web查询。现在做的追踪功能还比较简单,这块还需要继续丰富。
原文链接: https://blog.csdn.net/wuxiaobi ... 42020