从0开始学习微服务阅读笔记

本文为极客时间专栏从0开始学习微服务的阅读笔记。

在了解微服务之前,先来了解一下单体应用。

在上学那会,做过一些企业站,技术往往是基于LAMP(Linux + Apache + MySQL + PHP),这种业务较为简单的企业站,就是单体应用。所有的业务代码都是放在一个PHP程序中,代码只需要我自己来维护就可以了,测试、上线、运维全部搞定。

但这种单体应用要是放到稍微有点规模的互联网公司中必然是行不通的。一个公司里有很多的人,不可能公司这么多技术人员共同维护一套代码,这样子团队的写作必然是个问题。系统的健壮性也会比较差,一旦单体应用中的一个模块出问题后往往会影响到其他的模块,导致整个系统不可用,比如PHP的服务业界通常使用php-fpm来管理,而php-fpm处理连接的方式一个请求一个线程,当一部分请求因为延时高时会消耗过多的线程资源,导致其他请求没有可用的线程可以处理。

单体应用的其他缺点不再罗列,比如代码膨胀过度、发布较慢、系统高可用性差、团队协作成本高等。

为了解决单体应用的这些缺点,方法只有一个就是将单体应用拆分为多个服务即服务化,服务之间通过RPC的方式相互调用。

即服务化后,业界又提出了微服务的概念。

维基百科中有如下定义:

2014年,Martin Fowler 与 James Lewis 共同提出了微服务的概念,定义了微服务是由以单一应用程序构成的小服务,自己拥有自己的行程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用 HTTP API 通讯。同时服务会使用最小的规模的集中管理 (例如 Docker) 能力,服务可以用不同的编程语言与数据库等元件实作。

但看这个定义,看的我一脸懵逼,实在太过抽象。其实微服务也没有一个特别明确的定义。

那么服务化和微服务之间有什么不同之处呢?

  1. 服务拆分粒度更细。
  2. 每个微服务都独立部署和维护。
  3. 微服务需要服务治理。由于微服务会将服务变多,势必需要一个服务管理平台来对微服务进行管理。

将单体应用向微服务拆分的方式可以分为纵向拆分和横向拆分。这里以一点资讯app为例来解释纵向拆分和横向拆分。

一点资讯分为信息流、正文页等模块,而这些功能都依赖于用户信息获取模块,纵向拆分即将信息流、正文页拆分为微服务,横向拆分即将信息流、正文页都依赖的用户信息获取模块拆分为单独的服务。

微服务架构

采用微服务后会带来一系列复杂的问题,新技术在解决了一部分问题的同时,总会带来一些新的问题,比如服务怎么定义接口、服务的发布方式和服务发现、服务的监控、服务的治理(包括依赖关系梳理、熔断机制等)、故障快速定位等。

那么一个标准的微服务架构应该长什么样子呢?

服务提供方在服务启动时向服务注册中心注册服务,声明自己能够提供的服务及当前服务的地址等信息。

服务调用者请求注册中心,查询所要调用服务的地址,并通过约定好的协议向服务提供者发起请求,获取到结果后按照数据协议格式将数据进行反序列化。

在整个服务的调用过程中,服务的请求耗时、调用量、调用成功与否等信息都会作为监控记录下来,服务的调用关系会通过trace系统记录下来,以便用于后续的故障定位和追踪。如果服务调用失败,则需要服务治理的方式来保证调用方的正常运行。

服务的接口定义

需要解决的问题是服务的接口有哪些?每个接口的输入什么?每个接口的输出是什么?

RESTful API

HTTP协议的接口定义通常使用该协议,常见Wiki或者Swagger的方式来管理。

IDL文件

IDL(interface description language)用于描述接口,使得不同的编程语言不同的平台服务之间可以相互通讯。常见实现包括Thrift和protobuf,protobuf作为序列化方式的一种,通常会使用gRPC进行通讯。

XML文件方式

服务提供者将接口描述信息保存在xml配置文件,服务启动后会加载配置文件,将服务暴露出去。

服务消费者将要调用的接口信息写在xml配置文件中,进程启动后加载xml配置文件。

服务提供者通常需要提供服务的超时时间等参数,而消费者端也需要该信息,为了让消费者端能看到生产者端的配置,可以将信息放在配置中心中,但这却增大了配置中心的内容,当一旦配置变化需要同步时,同步的数据会变多。

注册中心

要想实现服务之间的调用,通常会使用反向代理和服务注册中心的方法。

反向代理的方法业界一般使用较多的包括nginx、haproxy以及业界新秀envoy等。

在微服务架构中更多提及的方案为采用服务注册中心的方法,而注册中心的稳定性就显得尤其重要。

业界注册中心采用较多的方案为zookeeper、etcd、consul、eureka。这些注册中心的存储数结构要么是树状接口,要么是key-value形式,其中k-v形式的可以变更key的值以实现类似树状结构。

注册中心的设计

注册中心API及功能

注册中心需要提供以下api以供服务提供方和服务调用方使用。

  1. 服务注册接口,提供服务提供方使用
  2. 服务反注册接口,提供服务提供方以便销毁服务
  3. 心跳汇报接口,服务提供方通过心跳汇报服务是存活状态的。一旦当服务出现异常时,服务注册中心应该立即将服务从注册中心剔除。
  4. 服务订阅接口,服务调用方用于获取服务提供方的实例列表
  5. 服务变更接口,服务调用方用于获取最新可用服务。一旦注册中心探测到有新的服务实例或者实例减少,应该立即通知所有订阅该服务的服务调用者更新本地的节点信息。

注册中心存储哪些服务信息

通常可以按照“服务名-分组-节点信息”三层结构来存储,其中节点信息包括:节点ip地址、节点端口号、请求失败时重试次数、请求结果是否压缩。

分组的划分原则包括:

  1. 按照业务的核心程度
  2. 按照机房维度
  3. 线上环境、测试环境

注册中心如何工作

服务提供者注册节点
  1. 查看注册节点是否在白名单内,即是否可以向注册中心注册
  2. 查看注册的服务名、服务分组是否存在
  3. 将节点信息添加到对应的存储位置
服务提供者反注册
  1. 查看服务名、服务分组对应的服务是否存在
  2. 将节点删除
服务消费者查询节点信息
  1. 从本机内存中查找服务信息
  2. 如果有本地快照存储可以从中查找
服务消费者订阅服务变更
  1. 消费者获取到服务信息后,在本地保留cluster的sign值
  2. 每个一段时间从注册中心获取cluster的sign值,如果不一致,就从注册中心拉取服务节点,并更新内存环境和本地快照

服务框架

完整的服务框架包括:通讯框架、通讯协议、序列化和反序列化。

开源RPC框架

跟语言相关的框架:Dubbo、Motan(微博)、Tars(腾讯)、Spring Cloud

跨平台的开源RPC框架:gRPC、Thrift

服务监控

常用的开源监控软件包括:ELK、Graphite、TICK、Prometheus

服务追踪

使用分布式会话跟踪技术,利用traceid

开源方案包括OpenZipkin、jaeger等

服务治理

通过一系列的手段保证在意外情况下,服务仍然能够正常运行。

节点管理

服务调用失败可能是服务提供者自身出现了问题,也可能是网络问题导致。

1.注册中心自动摘除机制

服务提供者和注册中心之间保持心跳,当超时后注册中心自动摘除服务提供者。

2.服务消费者摘除

将服务提供者的探活机制放到消费者端,消费者在探测到服务提供者失败后自动摘除服务提供者。这种情况可以避免服务提供者和注册中心之间网络出现异常,但是服务提供者和服务消费者之间可以通讯的情况。

负载均衡

常用的包括随机算法、轮询、加权轮询算法、最少活跃调用、一致性hash算法(可以配合着静态注册中心达到比较好的效果)

自适应最优选择算法:在客户端维护一份每一个服务节点的性能统计快照,每隔一段时间去更新快照。在发起请求时,根据二八原则,将服务节点分成两部分,找出20%的那部分响应最慢的节点并降低权重。也可称为动态加权轮询算法。1分钟的更新时间间隔是个不错的选择。

个人感觉自适应最优选择算是个不错的选择,但还可以针对业务场景继续优化,比如权重进行动态调整。

服务路由

用于限定服务消费者可选择服务提供者节点的范围。

应用场景:分组调用(组的划分可以按照机房等维度)、灰度发布、流量切换(比如机房故障后,用于机房之间的流量调度)、读写分离(读接口部署在一起,写接口部署在一起)。

规则的写法

A.条件路由

  • 某个ip的消费者仅访问某个ip的服务提供者
  • 排除某个服务节点(所有的服务消费者都不能访问某个服务提供者)
  • 白名单和黑名单
  • 机房级别的隔离(可以基于ip地址的规则来做)
  • 读写分离(所有get类方法仅访问某些节点等)

B.脚本路由

使用脚本语言的形式来描述

路由的获取方式

  • 本地配置:存储在服务消费者本地
  • 配置中心配置,可以修改规则后动态下发

服务容错

手段包括超时、重试、双发、熔断等。

双发的思路为服务调用者在发起一次服务调用后,在给定的时间内(该时间要比服务超时的时间短)没有得到结果,再发起另外一个请求。

熔断的思路为在某一个时间内如果服务调用失败次数超过一定值,则触发熔断,不再向服务提供者发起请求。

断路器中的状态包括:Closed、Open、Half Open(半打开状态,用于探测后端服务是否已经正常)

Hystrix是最出名的熔断器,计算服务调用的失败率是通过滑动窗口来实现,滑动窗口内包含10个桶,每个桶为1秒内的服务调用情况。

FailOver,失败自动切换。消费者调用失败后,自动从可以节点中选择下一个节点重新调用,可设置失败的次数。通常适合只读的场景。

FailBack,失败通知。调用失败后,不能重试,而是根据失败的信息来决定后续的执行策略。通常用于写场景。

FailCache,失败缓存。调用失败后,隔一段时间后再重试。

FailFast,快速失败。调用失败后不再重试。

如何识别服务节点是否存活

如果注册中心为zk,在服务节点变化的时候,注册中心会向服务调用方推送服务节点变化的通知。但当网络抖动的时候,可能会存在节点的状态频繁变化的问题,导致服务消费者频繁收到节点变更通知,或者导致注册中心获取到的服务节点过少。可通过以下手段来避免该问题。

服务端故障时的应对策略

故障包括:集群故障、单个idc故障、单机故障

集群故障

比如触发bug、突发流量等

限流:限制超出接口阈值的部分请求

降级:一种思路为通过开关来实现。具体可以分为多种等级:一级降级对业务影响比较小,可以设置为自动降级;二级降级对业务有一定影响,设置为手工降级;三级降级需要谨慎操作。

单idc故障

基于dns的流量切换:延时稍高,比较适合入口流量。

基于rpc的分组流量切换:将之前单个机房内访问的流量切换为多个机房访问。

单机故障

可以通过自动重启服务的手段来解决。但要避免一次性重启服务过多的问题。

动态注册中心的保护机制

  1. 心跳开关保护机制,给注册中心设置一个开关,当开发打开时,即使网络频繁抖动,注册中心也不会通知消费者节点变更,后者设置一定的百分比打开该开关。正常情况下该开关可以不打开。
  2. 服务节点摘除保护机制,设定一个阈值比例,在出现网络抖动的情况下,注册中心也不会将超过这个阈值的节点给下掉,防止一下子下掉过多节点。该机制正常情况下,应该开启。

静态注册中心的保护机制

在服务消费者端来判断是否服务提供者是否存活。服务消费者调用某一个节点失败超过一定次数就将节点标记为不可用。并隔一段时间后再去探测该节点是否存活。

注册中心的服务正常情况下不改变的,只有当服务在发布的时候才去修改注册中心中的节点。注册中心中的节点变化后仍然通知服务消费者,只是在网络出现抖动的时候,不再去通知。

个人感觉这个思路更靠谱。

服务治理平台

1.服务管理 包括服务上下线、节点添加和删除、服务查询、服务节点查询等

2.服务治理 限流、降级、切流量

3.服务监控 可以包含服务的tracing监控等

4.问题定位

5.日志查询

6.服务运维 发布部署和扩缩容

配置中心

业内采用的开源配置中心包括:

  • Spring Cloud 功能相对较弱,变更配置需要通过git操作
  • Apollo 对Spring Boot的支持比较好

进阶内容

做好容量规划

包括容量评估和调度决策两方面。

压测服务的单机最大容量可以采用区间加权的方式来计算,比如0 ~ 10ms区间权重为1,10 ~ 50ms区间权重为2,500ms以上权重为32,通过累加的方式计算出单机的权重,从而评估出单机最大容量。(该评估容量的方式非常实用)

调度策略可以根据水位线来做决定,一条是安全线,一条是致命线。当水位线处于致命线需要立即扩容,当水位线回到安全线以上时可以进行缩容。(水位线的方式很赞

缩容的思路是采用逐步缩容,每隔5分钟判断一次水位线是否在致命线以上,并按照10%、30%的比例进行缩容。

为了防止水位线的抖动,可以一分钟采集一个点,当5个点中的3个点都满足条件时才进行缩容。

多机房部署

1.主从架构

所有的写请求都发给主机房,主机房更新本机房的缓存和数据库,其他机房的缓存和数据库从主机房同步。主机房出现问题后,就没法更新了。

2.独立机房架构

缓存层,每个机房都有写请求,每个机房的写请求通过消息同步组件将写请求同步给另外一个机房。数据库mysql只有一个主库,另外机房的mysql同步数据到该机房。

以上缓存消息同步组件的实现大概如下:

包括两个模块reship和collector,reship负责将本机房的写请求发一份给别的机房,collector负责从别的机房读取写请求,并发给本机房的处理服务器。

可以通过消息队列和rpc调用来实现reship和collector的通讯。

3.多机房的数据一致性

可通过消息对账机制来保证一致性,原理为通过一个单独的程序后面来验证是否一个写请求是否已经被所有的机房都处理,如果没有处理,则重新发送写请求。

混合云部署

公有云上为了安全往往不部署数据库

DevOps实践

持续集成:确保每一次代码的merge request都通过,分为四个阶段:build(开发分支代码的编译与单元测试)、package(开发分支代码打包成docker镜像)、deploy(开发分支代码部署到测试环境)、test(集成测试)。包括了代码检查和单元测试环节。

持续交付:代码merge request到develop分支后,develop分支的代码能够在生产环境中测试通过,并进行小流量灰度验证,可以随时上线。分为五个阶段:build(develop分支的代码编译与单元测试)、package(develop分支代码打包)、deploy、test、canary(develop分支代码的小流量灰度验证)。

持续部署:合并develop代码到master分支,并打包成docker镜像,并可随时上线。包括:build、package、clear、production。

Service Mesh

Service Mesh技术以轻量级网络代理的方式与应用代码部署在一起,主要有两个关键点技术。

SideCar用于转发服务之间的调用,在服务消费者端SideCar用于将请求转发到服务提供者端的SideCar中,在服务提供者端的SideCar在接收到请求后转发给本机上的服务提供者。

SideCar实现方式有基于ipstables的网络拦截和直接转发请求两种方式。

Control plane用于基于SideCar的服务调用的治理,用来取代微服务中需要服务框架干的事情,包括服务发现、负载均衡、请求路由、故障处理、安全认证、监控上报、日志记录、配额限制等。

Istio

Pilot主要用于流量控制,包括Rules API(提供API,用于流量控制)、Envoy API(给Envoy提供API,获取服务注册信息、流量控制信息等)、抽象模型(对服务注册信息、流量控制进行抽象)、平台适配层(适配k8s、Mesos等多个平台,将平台特定的注册信息转换成平台无关的抽象模型)。

Pilot的流量控制功能包括服务发现和负载均衡、请求路由、超时重试、故障注入。

Mixer实现策略控制和监控日志收集等功能,理论上Envoy发起的每次请求都会发送到Mixer,可以异步发送。

Mixer的策略控制包括对服务的访问频率限制和访问控制。

Citadel用于保证服务之间的安全,需要Envoy的配合。Citadel中存储了秘钥和证书,通过Pilot将授权策略和安全命名信息分发给Envoy,Envoy和Envoy之间通过双向TLS证书来进行通讯,由Mixer来管理授权和审计。

ref

微服务架构技术栈选型手册