目录
文章目录
微服务架构的问题
微服务架构中,服务之间会有错综复杂的依赖关系,例如:一个前端请求一般会依赖于多个后端服务,称为 “1=>N 扇出”。在实际生产环境中,服务往往不是百分百可靠,服务可能会出错或者产生延迟,如果一个应用不能对其依赖的故障进行容错和隔离,那么该应用本身就处在被拖垮的风险中。在一个高流量的网站中,某个单一后端一旦发生延迟,可能在数秒内导致所有应用资源(线程,队列等)被耗尽,造成所谓的雪崩效应(Cascading Failure),严重时可致整个网站瘫痪。另外,微服务架构整个应用分散成多个服务,定位故障点非常困难。
-
服务组合
-
服务依赖:
微服务架构虽然逻辑设计上看是完美的,但就像积木搭建的华丽宫殿一样,经不起风吹草动。在解决了旧问题,也引入了新的问题:
- 微服务架构整个应用分散成多个服务,定位故障点非常困难。
- 稳定性下降。服务数量变多导致其中一个服务出现故障的概率增大,并且一个服务故障可能导致整个系统挂掉。事实上,在大访问量的生产场景下,故障总是会出现的。
- 服务数量非常多,部署、管理的工作量很大。
- 开发方面:如何保证各个服务在持续开发的情况下仍然保持协同合作。
- 测试方面:服务拆分后,几乎所有功能都会涉及多个服务。原本单个程序的测试变为服务间调用的测试。测试变得更加复杂。
为了好好解决这些问题,对故障的处理一般从两方面入手,一方面尽量减少故障发生的概率,另一方面降低故障造成的影响。
如何拆分服务
设计要素:
- Version
- Request ID
- Auth & Signature
- RateLimit
- Docs
- ErrorCode & Message
拆分方向:
- 大单体变独立服务:按照业务模块进行服务的拆解。
- 大数据库变独立数据库。
- 环境隔离,独立:在服务拆分后,大多会采取独立部署的方式,将两者之间的环境隔离开来,互不干扰,互不影响。
服务间如何通信
同步调用还是异步调用?
- 同步调用:简单,一致性强,但是容易出调用问题,性能体验上也会差些,特别是调用层次多的时候。
- 异步调用:既能减低调用服务之间的耦合,又能成为调用之间的缓冲,确保消息积压不会冲垮被调用方,同时能保证调用方的服务体验,继续干自己该干的活,不至于被后台性能拖慢。不过需要付出的代价是一致性的减弱,需要接受数据最终一致性。还有就是后台服务一般要 实现幂等性,因为消息发送出于性能的考虑一般会有重复(保证消息的被收到且仅收到一次对性能是很大的考验)。最后就是必须引入一个独立的 Broker(e.g. MQ),如果公司内部没有技术积累,对 Broker 分布式管理也是一个很大的挑战。
REST 还是 RPC?
- REST:基于 HTTP 协议,更容易实现,服务端技术选型也更灵活些,各个语言都能支持,同时能跨客户端,对客户端没有特殊的要求,只要封装了 HTTP SDK 就能调用,所以相对使用的广一些。
- RPC:传输协议更高效,安全更可控,特别在一个公司内部,如果有统一个的开发规范和统一的服务框架时,他的开发效率优势更明显些。
微服务框架
一个完整的微服务系统,它的底座最少要包含以下功能:
- 日志和审计,主要是日志的汇总,分类和查询。
- 监控和告警,主要是监控每个服务的状态,必要时产生告警。
- 消息总线,轻量级的 MQ 或 HTTP。
- 注册发现。
- 负载均衡。
- 部署和升级。
- 事件调度机制。
- 资源管理,如:底层的虚拟机,物理机和网络管理。
以下功能不是最小集的一部分,但也属于底座功能:
- 认证和鉴权。
- 微服务统一代码框架,支持多种编程语言。
- 统一服务构建和打包。
- 统一服务测试。
- 微服务 CI/CD 流水线。
- 服务依赖关系管理。
- 统一问题跟踪调试框架,俗称调用链。
- 灰度发布。
- 蓝绿部署。
API 网关
API 是服务价值的精华体现。
API 完成前后端分离。
配置中心
配置,除了支持普通配置文件方式的配置,框架层还可集成动态运行时配置,能够在运行时针对不同环境动态调整服务的参数和配置。
Service Mesh
文档
文档自动生成,文档的书写和同步一直是一个痛点,框架层如果能支持文档的自动生成和同步,会给使用API的开发和测试人员带来极大便利。Swagger是一种流行Restful API的文档方案。
微服务治理
监控
监控日志,框架一方面要记录重要的框架层日志、metrics和调用链数据,还要将日志、metrics等接口暴露出来,让业务层能根据需要记录业务日志数据。在运行环境中,所有日志数据一般集中落地到企业后台日志系统,做进一步分析和处理。
微服务架构中组件繁多,各个组件所需要监控的指标不同。比如Redis缓存一般监控占用内存值、网络流量,数据库监控连接数、磁盘空间,业务服务监控并发数、响应延迟、错误率等。因此如果做一个大而全的监控系统来监控各个组件是不大现实的,而且扩展性会很差。一般的做法是让各个组件提供报告自己当前状态的接口(metrics接口),这个接口输出的数据格式应该是一致的。然后部署一个指标采集器组件,定时从这些接口获取并保持组件状态,同时提供查询服务。最后还需要一个UI,从指标采集器查询各项指标,绘制监控界面或者根据阈值发出告警。
大部分组件都不需要自己动手开发,网络上有开源组件。小明下载了RedisExporter和MySQLExporter,这两个组件分别提供了Redis缓存和MySQL数据库的指标接口。微服务则根据各个服务的业务逻辑实现自定义的指标接口。然后小明采用Prometheus作为指标采集器,Grafana配置监控界面和邮件告警。这样一套微服务监控系统就搭建起来了:
链路跟踪
在微服务架构下,一个用户的请求往往涉及多个内部服务调用。为了方便定位问题,需要能够记录每个用户请求时,微服务内部产生了多少服务调用,及其调用关系。这个叫做链路跟踪。
我们用一个Istio文档里的链路跟踪例子来看看效果:
从图中可以看到,这是一个用户访问productpage页面的请求。在请求过程中,productpage服务顺序调用了details和reviews服务的接口。而reviews服务在响应过程中又调用了ratings的接口。整个链路跟踪的记录是一棵树:
要实现链路跟踪,每次服务调用会在HTTP的HEADERS中记录至少记录四项数据:
- traceId:traceId标识一个用户请求的调用链路。具有相同traceId的调用属于同一条链路。
- spanId:标识一次服务调用的ID,即链路跟踪的节点ID。
- parentId:父节点的spanId。
- requestTime & responseTime:请求时间和响应时间。
另外,还需要调用日志收集与存储的组件,以及展示链路调用的UI组件。
以上只是一个极简的说明,关于链路跟踪的理论依据可详见Google的Dapper。
了解了理论基础后,小明选用了Dapper的一个开源实现Zipkin。然后手指一抖,写了个HTTP请求的拦截器,在每次HTTP请求时生成这些数据注入到HEADERS,同时异步发送调用日志到Zipkin的日志收集器中。这里额外提一下,HTTP请求的拦截器,可以在微服务的代码中实现,也可以使用一个网络代理组件来实现(不过这样子每个微服务都需要加一层代理)。
链路跟踪只能定位到哪个服务出现问题,不能提供具体的错误信息。查找具体的错误信息的能力则需要由日志分析组件来提供。
日志分析
统一错误处理,对于框架层和服务的内部异常,如果框架层能够统一处理并记录日志,对服务监控和快速问题定位有很大帮助。
日志分析组件应该在微服务兴起之前就被广泛使用了。即使单体应用架构,当访问数变大、或服务器规模增多时,日志文件的大小会膨胀到难以用文本编辑器进行访问,更糟的是它们分散在多台服务器上面。排查一个问题,需要登录到各台服务器去获取日志文件,一个一个地查找(而且打开、查找都很慢)想要的日志信息。
因此,在应用规模变大时,我们需要一个日志的“搜索引擎”。以便于能准确的找到想要的日志。另外,数据源一侧还需要收集日志的组件和展示结果的UI组件:
ELK是Elasticsearch、Logstash和Kibana三个组件的缩写:
- Elasticsearch:搜索引擎,同时也是日志的存储。
- Logstash:日志采集器,它接收日志输入,对日志进行一些预处理,然后输出到Elasticsearch。
- Kibana:UI组件,通过Elasticsearch的API查找数据并展示给用户。
最后还有一个小问题是如何将日志发送到Logstash。一种方案是在日志输出的时候直接调用Logstash接口将日志发送过去。这样一来又(咦,为啥要用“又”)要修改代码……于是小明选用了另一种方案:日志仍然输出到文件,每个服务里再部署个Agent扫描日志文件然后输出给Logstash。
服务中心
前面的组件,都是旨在降低故障发生的可能性。然而故障总是会发生的,所以另一个需要研究的是如何降低故障产生的影响。
最粗暴的(也是最常用的)故障处理策略就是冗余。一般来说,一个服务都会部署多个实例,这样一来能够分担压力提高性能,二来即使一个实例挂了其他实例还能响应。
冗余的一个问题是使用几个冗余?这个问题在时间轴上并没有一个切确的答案。根据服务功能、时间段的不同,需要不同数量的实例。比如在平日里,可能4个实例已经够用;而在促销活动时,流量大增,可能需要40个实例。因此冗余数量并不是一个固定的值,而是根据需要实时调整的。
一般来说新增实例的操作为:
- 部署新实例
- 将新实例注册到负载均衡或DNS上
操作只有两步,但如果注册到负载均衡或DNS的操作为人工操作的话,那事情就不简单了。想想新增40个实例后,要手工输入40个IP的感觉……
解决这个问题的方案是服务自动注册与发现。首先,需要部署一个服务发现服务,它提供所有已注册服务的地址信息的服务。DNS也算是一种服务发现服务。然后各个应用服务在启动时自动将自己注册到服务发现服务上。并且应用服务启动后会实时(定期)从服务发现服务同步各个应用服务的地址列表到本地。服务发现服务也会定期检查应用服务的健康状态,去掉不健康的实例地址。这样新增实例时只需要部署新实例,实例下线时直接关停服务即可,服务发现会自动检查服务实例的增减。
服务发现还会跟客户端负载均衡配合使用。由于应用服务已经同步服务地址列表在本地了,所以访问微服务时,可以自己决定负载策略。甚至可以在服务注册时加入一些元数据(服务版本等信息),客户端负载则根据这些元数据进行流量控制,实现A/B测试、蓝绿发布等功能。
服务发现有很多组件可以选择,比如说ZooKeeper 、Eureka、Consul、etcd等。
服务注册、发现、负载均衡和健康检查,假定采用进程内LB方案,那么服务自注册一般统一做在服务器端框架中,健康检查逻辑由具体业务服务定制,框架层提供调用健康检查逻辑的机制,服务发现和负载均衡则集成在服务客户端框架中。
-
服务注册:
-
服务发现:
熔断、服务降级、限流
当一个服务因为各种原因停止响应时,调用方通常会等待一段时间,然后超时或者收到错误返回。如果调用链路比较长,可能会导致请求堆积,整条链路占用大量资源一直在等待下游响应。所以当多次访问一个服务失败时,应熔断,标记该服务已停止工作,直接返回错误。直至该服务恢复正常后再重新建立连接。
限流和容错,框架集成限流容错组件,能够在运行时自动限流和容错,保护服务,如果进一步和动态配置相结合,还可以实现动态限流和熔断。
- 服务降级:当下游服务停止工作后,如果该服务并非核心业务,则上游服务应该降级,以保证核心业务不中断。比如网上超市下单界面有一个推荐商品凑单的功能,当推荐模块挂了后,下单功能不能一起挂掉,只需要暂时关闭推荐功能即可。
- 限流:一个服务挂掉后,上游服务或者用户一般会习惯性地重试访问。这导致一旦服务恢复正常,很可能因为瞬间网络流量过大又立刻挂掉,在棺材里重复着仰卧起坐。因此服务需要能够自我保护——限流。限流策略有很多,最简单的比如当单位时间内请求数过多时,丢弃多余的请求。另外,也可以考虑分区限流。仅拒绝来自产生大量请求的服务的请求。例如商品服务和订单服务都需要访问促销服务,商品服务由于代码问题发起了大量请求,促销服务则只限制来自商品服务的请求,来自订单服务的请求则正常响应。
微服务框架
指标接口、链路跟踪注入、日志引流、服务注册发现、路由规则等组件以及熔断、限流等功能都需要在应用服务上添加一些对接代码。如果让每个应用服务自己实现是非常耗时耗力的。基于DRY的原则,小明开发了一套微服务框架,将与各个组件对接的代码和另外一些公共代码抽离到框架中,所有的应用服务都统一使用这套框架进行开发。
使用微服务框架可以实现很多自定义的功能。甚至可以将程序调用堆栈信息注入到链路跟踪,实现代码级别的链路跟踪。或者输出线程池、连接池的状态信息,实时监控服务底层状态。
使用统一的微服务框架有一个比较严重的问题:框架更新成本很高。每次框架升级,都需要所有应用服务配合升级。当然,一般会使用兼容方案,留出一段并行时间等待所有应用服务升级。但是如果应用服务非常多时,升级时间可能会非常漫长。并且有一些很稳定几乎不更新的应用服务,其负责人可能会拒绝升级……因此,使用统一微服务框架需要完善的版本管理方法和开发管理规范。
Service Mesh
另一种抽象公共代码的方法是直接将这些代码抽象到一个反向代理组件。每个服务都额外部署这个代理组件,所有出站入站的流量都通过该组件进行处理和转发。这个组件被称为Sidecar。
Sidecar不会产生额外网络成本。Sidecar会和微服务节点部署在同一台主机上并且共用相同的虚拟网卡。所以Sidecar和微服务节点的通信实际上都只是通过内存拷贝实现的。
Sidecar只负责网络通信。还需要有个组件来统一管理所有sidecar的配置。在Service Mesh中,负责网络通信的部分叫数据平面(data plane),负责配置管理的部分叫控制平面(control plane)。数据平面和控制平面构成了Service Mesh的基本架构。
Sevice Mesh相比于微服务框架的优点在于它不侵入代码,升级和维护更方便。它经常被诟病的则是性能问题。即使回环网络不会产生实际的网络请求,但仍然有内存拷贝的额外成本。另外有一些集中式的流量处理也会影响性能。
流量治理
微服务一方面是把原来在静态编译时产生的能力与能力之间的关联关系,通过架构拆分推演到动态的运行时。因此在运行时,服务与服务之间是需要进行通讯、协同,才能完成某一项具体的业务功能。当大家进行通讯、协同时,就一定要对其通讯过程进行管理,或者说要进行流量管理。例如,我们要知道怎样从一个微服务找到另一个微服务,以及怎样能保证一个微服务找到最佳的微服务实例跟它进行通讯,这是一个比较复杂的过程,其中包括 RPC 能力、服务注册发现能力、动态配置管理能力以及服务降级能力等等。
为了减轻业务开发同学的负担,不用重复的在每一个微服务中写一遍微服务的流量管理的通用能力,因此大家开发了很多框架,比如在 Java 体系中,著名的 Spring Cloud 提供了一个分布式微服务管理框架;在 Go 语言的开源生态中也有像 Go Mirco 这样的体系;在阿里巴巴内部我们也有像 HSF 这样的体系发展起来的微服务治理框架。
因此,从抽象层面可以看到一个服务包含了两个层面:
一个层面是本身的业务逻辑,也就是由微服务业务开发人员去编写的,功能实现与业务实现相关的代码。
另一个层面是为了实现微服务与微服务之间通讯、流量、服务治理的代码,我们会将其抽象成一个框架,如下图中标出的 Spring Cloud。这样的抽象带来了一个问题,就是所有的通用能力都依赖于这个具体的框架。
假设在公司之中,除了 Spring Cloud 之外,我们去引入另外一些服务框架,如阿里巴巴 HSF 如果希望和 Spring Cloud 框架上面编写的微服务进行通讯的话应该如何去操作?这就要求 HSF 与 Spring Cloud 之间互联互通以及协议之间的互相理解。但其实这些框架之间往往并不具备这个能力。更大的一个问题可能在于,云原生时代我们允许这些微服务的研发能用不同开发语言及模型来进行编程。因此,框架之间的系统并不是不是一对二的关系,也不是 仅仅是 Spring Cloud 与 HSF 的关系,可能是 Java 体系与 JavaScript、Python、Go 体系这些微服务框架都需要打通的问题,它变成了一个 N to M 的 problem,来解决多语言、复杂环境中微服务的治理与管理问题。
这时,当我们有了容器、容器平台、Pod 这些抽象,能够提供一个平台,而不是必须要完全依赖于业务中的代码或 框架时,有没有更好的办法来解决刚才提到的问题?
现在有一个比较流行的概念叫 Service Mesh——服务网格。它的本质就是为了更好地解决流量治理在多语言、多环境场景下的问题,它的主要思想如下:
- 第一就是希望把流量管理的这些框架能力从耦合在业务的二进制中抽象、剥离出来,形成一个流量管理的单独进程,并以 Sidecar 的模式部署在 Pod 中。通过操作系统级别的透明流量劫持工作,把所有的微服务之间的流量劫持到 Sidecar 中,然后通过 Sidecar 与 Sidecar 之间通讯进行流量的转发与管理。这样问题就简单多了,我们只需要让流量管理的 Sidecar 之间互相通讯、能够进行互联互通。目前比较知名、流行的开源流量劫持和管理 Sidecar 实现叫做 Envoy。
当然,单单有了这层流量劫持与管理还是不够的,还需要管控平面的支持。比如原来微服务体系做的服务注册、服务发现以及流量观测还是需要的,这些策略和规则需要下发给流量管理的 Sidecar 代理。因此,我们还需要构建一个管控平面来管理在 Pod 中部署的流量管理的数据平面的单点,让它们形成一个网状,形成一个集群。所以我们需要有一些管控平面的能力,在开源中比较流行的一个管控平面实现叫 Istio。主要实现了三个能力:流量的配置、流量的安全、流量的观测。
我们认为在云原生这个逐渐平台化的时代,大部分新的应用及场景都会尝试选用基于 Service Mesh 的技术进行微服务的流量治理。
来源:oschina
链接:https://my.oschina.net/u/4386695/blog/4548724