单体应用
早期在业务规模不大、开发团队人员规模较小的时候, 一般都是采用单体应用来开发和部署, 以我们的业务为例:
所有的业务都在
station
工程, 只需要通过uwsgi
启动Django
服务, 然后nginx
将流量转发到uwsgi
即可.
优点
学习成本低,开发上手快,测试、部署、运维也比较方便,甚至一个人就可以完成一个网站的开发与部署。
缺点
当业务规模逐渐扩大, 开发人员也不断增多时, 单体应用就会碰到各种各样的问题:
- 开发效率低
所有的业务都在一个工程, 所有的修改都在一个工程, 就会有很多合并代码的操作, 每次合并代码都要重新测试, 所有的开发人员都要参与其中, 效率很低.
- 部署效率低
每次一个小改动也得重启整个服务, 当一个工程越来越大, 代码和依赖也会越来越多, 每次发布拉代码, 装依赖, 起服务的时间也会大大的增加.
- 可用性很低
一旦服务中某个功能有问题可能就会影响整个服务
服务化(SOA)
为了解决单体服务的种种问题, SOA的思想出现了
面向服务的体系结构(SOA) 是一种分布式运算的软件设计方法。软件的部分组件(调用者),可以透过网络上的通用协议调用另一个应用软件组件(服务)运行、运作,让调用者获得服务。调用者不需要了解此服务的运作过程。
SOA
架构有这些特性:
服务封装
服务松耦合(Loosely coupled) - 服务之间的关系最小化,只是互相知道。
服务契约 - 服务按照服务描述文档所定义的服务契约行事。
服务抽象 - 除了服务契约中所描述的内容,服务将对外部隐藏逻辑。
服务的重用性 - 将逻辑分布在不同的服务中,以提高服务的重用性。
服务的可组合性 - 一组服务可以协调工作并组合起来形成一个组合服务。
服务自治 – 服务对所封装的逻辑具有控制权
服务无状态 – 服务将一个活动所需保存的信息最小化。
服务的可被发现性 – 服务需要对外部提供描述信息,这样可以通过现有的发现机制发现并访问这些服务。
我们现在的架构就有点像但却不是SOA的架构, 封装了几个单独的服务 order
, merchandise
等。通过手动修改 Nginx
配置来实现描述信息。但服务间并没有一个描述文件去定义服务调用前后的信息。
微服务
维基是这么介绍微服务的:
微服务 (Microservices) 是一种软件架构风格,它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模块化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic) 的 API 集相互通信。
我的理解: 微服务架构是细粒度的 SOA
, 从服务的定义到服务部署, 编排, 治理, 运维等等, 都有确切的解决方案。可以说微服务架构是随着 Docker
的盛行而流行的, 使用了 Docker
之后,由开发写各个服务所需的 Dockerfile
,然后统一的进行镜像的构建和线上环境的发布。
独立部署
每个服务都严格遵循独立打包部署的准则,互不影响。比如一台物理机上可以部署多个 Docker 实例,每个 Docker 实例可以部署一个微服务的代码。
独立维护
每个服务都可以交由一个小团队甚至个人来开发、测试、发布和运维,并对整个生命周期负责。
服务治理能力要求高
因为拆分为微服务之后,服务的数量变多,因此需要有统一的服务治理平台,来对各个服务进行管理。
总结来说,微服务架构是将复杂臃肿的单体应用进行细粒度的服务化拆分,每个拆分出来的服务各自独立打包部署,并交由小团队进行开发和运维,从而极大地提高了应用交付的效率。并再次基础上配上相应的服务监控、追踪、治理等组件。
微服务化面临的问题:
- 服务如何定义。 对于微服务来说, 每个服务都运行在自己的进程空间中, 互相调用采用接口的形式, 服务间的调用通过接口描述来约定, 约定包括: 通信协议, 接口名, 接口参数, 返回值等。
- 服务如何发布和订阅。 当拆分为微服务独立部署后,服务提供者该如何对外暴露自己的地址,服务调用者该如何查询所需要调用的服务的地址呢?这个时候你就需要一个类似登记处的地方,能够记录每个服务提供者的地址以供服务调用者查询,在微服务架构里,这个地方就是注册中心。
- 服务如何监控。 通常对于一个服务,我们最关心的是 QPS(调用量)、AvgTime(平均耗时)以及 P999(99.9% 的请求性能在多少毫秒以内)这些指标。这时候你就需要一种通用的监控方案,能够覆盖业务埋点、数据收集、数据处理,最后到数据展示的全链路功能。
- 服务如何治理。 拆分为微服务架构后,服务的数量变多了,依赖关系也变复杂了。比如一个服务的性能有问题时,依赖的服务都势必会受到影响。可以设定一个调用性能阈值,如果一段时间内一直超过这个值,那么依赖服务的调用可以直接返回,这就是熔断,也是服务治理最常用的手段之一。
- 故障如何定位。在单体应用拆分为微服务之后,一次用户调用可能依赖多个服务,每个服务又部署在不同的节点上,如果用户调用出现问题,你需要有一种解决方案能够将一次用户请求进行标记,并在多个依赖的服务系统中继续传递,以便串联所有路径,从而进行故障定位。
服务描述
服务调用首先要解决的问题就是服务如何对外描述。比如,你对外提供了一个服务,那么这个服务的服务名叫什么?如何调用这个服务的接口?调用这个服务需要提供哪些信息?调用这个服务返回的结果是什么格式的?该如何解析?这些就是服务描述要解决的问题。
我们 station
工程就有 thrift
通信协议的服务描述文件。
注册中心
在微服务架构下,主要有三种角色:服务提供者(RPC Server)、服务消费者(RPC Client)和服务注册中心(Registry),三者的交互关系请看下面这张图:
- RPC Server 提供服务,在启动时,根据服务发布文件中的配置的信息,向 Registry 注册自身服务,并向 Registry 定期发送心跳汇报存活状态。
- RPC Client 调用服务,在启动时,根据服务引用文件中配置的信息,向 Registry 订阅服务,把 Registry 返回的服务节点列表缓存在本地内存中,并与 RPC Sever 建立连接。
- 当 RPC Server 节点发生变更时,注册中心会同步变更,RPC Client 感知后会刷新本地内存中缓存的服务节点列表。
- RPC Client 从本地缓存的服务节点列表中,基于负载均衡算法选择一台 RPC Sever 发起调用。
现在主流的注册中心有: apache
的 zookeeper,后起之秀 etcd(要搭配其他组件) 和 consul。
每个注册中心必须提供下面这些东西:
1. 注册中心 API
注册中心必须提供一些基本的API, 如: 服务注册, 服务注销, 心跳, 服务订阅, 服务变更, 服务查询, 修改等接口
2. 集群部署
作为服务调用的桥梁, 注册中心的重要性不言而喻, 所以注册中心都是使用集群来部署, 用分布式强一致性算法(Raft/Paxos)来保证数据的强一致性。
3. 服务健康状态检测
服务在注册中心注册后, 注册中心还得知道服务节点是否可用, 一般的做法是保持一个长连接, 服务节点定期向注册中心发送心跳包, 如果注册中心超时未收到心跳包, 注册中心就会认为这个节点不可用, 并把这个节点从注册中心删除。
4. 服务状态变更通知
一旦注册中心探测到有服务提供者节点新加入或者被剔除,就必须立刻通知所有订阅该服务的服务消费者,刷新本地缓存的服务节点信息,确保服务调用不会请求不可用的服务提供者节点。
比如etcd,会提供 Watcher 机制,去实现状态变更后通知给服务调用方的,服务调用方通过 Watch 特定的服务,当这个服务的信息有变更时,注册中心会通知调用方,调用方读取到变更的数据,然后刷新本地缓存的服务器节点信息。
服务监控
对服务的监控是必不可少的,微服务也是一样,监控的指标也基本一致: QPS,PV,响应时间,错误率等等。
故障如何定位
要定位故障,就得知道每次用户请求经过了哪些服务调用,以及每个服务调用的详细信息。那么就要有一个服务追踪的东西去监控每一个用户请求经过了哪些服务的调用以及调用的具体信息。
比如我们现在的系统用的就是全局唯一的 request_id
来标识一个用户请求,由客户端发起请求时生成,贯穿整个调用链。但当调用链很复杂时,如果只有一个 request_id
是很难具体体现一个完整的调用过程信息的。通常还会加一个 span_id
,标识某个调用跨度,在方法/服务调用前生成,并记录时间。调用完之后将 trace_id/request_id
和 span_id
统一上报。
成熟的追踪系统有很多,比如: twitter
开源的 zipkin , google
的 Dapper, 阿里的 鹰眼, 美团的 MTrace 等。
服务如何治理
调用关系由本地调用改成远程调用后会增加很多问题,比如网络故障,节点宕机,节点负载过高等等。对于这些情况,都由对应的处理策略。
节点管理
1、注册中心主动摘除
前面说过,注册中心和服务提供节点之间会维持一个长链接,定期发送一个心跳包来检测服务提供者是否不可用,如果超时未收到服务提供者的 ping
,那么就认为次节点不可用,把这个节点从服务器列表里面踢出。
2、服务消费者主动摘除
仔细想想注册中心主动剔除策略还是会有点问题,考虑下面这种情况: 注册中心和服务提供者之间的网络出现延迟,导致注册中心超时没收到服务提供者的心跳包,那么注册中心就会把这个节点给摘除掉,而此时服务消费者和服务提供者之间其实并没有任何故障。所以就有了另一种策略,由服务消费者去剔除这个节点,当服务消费者调用服务提供者失败,就把这个节点从内存中摘除。
负载均衡
一般情况同一个服务,服务提供者会不止一个,那么就得选取一个节点去调用。通常的话有这几种方式。
随机 从可用节点随机选取一个节点进行调用,这样就不管每台机器的性能,每台机器的调用量基本一致。
权重 给每台机器配置权重,可以给不同性能的机器分配不同的权重,根据机器的性能来决定承受多少调用量。
- 最少活跃调用算法 这种算法是在服务消费者这一端的内存里动态维护着同每一个服务节点之间的连接数,当调用某个服务节点时,就给与这个服务节点之间的连接数加 1,调用返回后,就给连接数减 1。然后每次在选择服务节点时,根据内存里维护的连接数倒序排列,选择连接数最小的节点发起调用,也就是选择了调用量最小的服务节点,性能理论上也是最优的。
- 一致性 Hash 算法 指相同参数的请求总是发到同一服务节点。当某一个服务节点出现故障时,原本发往该节点的请求,基于虚拟节点机制,平摊到其他节点上,不会引起剧烈变动。
服务路由
对于服务消费者而言,在内存中的可用服务节点列表中选择哪个节点不仅由负载均衡算法决定,还由路由规则确定。路由规则就是通过一定的规则如条件表达式或者正则表达式来限定服务节点的选择范围。比如存在两个版本的服务: /api/v1
和 /api/v2
, 部分流量路由到 /api/v2
上,没问题了之后再把所有流量路由到 /api/v2
上,这也满足了灰度的需求。实现一般由两种:
- 静态 在服务消费者写死
- 动态 路由规则放在注册中心,服务消费者定期去请求注册中心来同步本地的路由规则,要想修改路由规则(灰度),只需修改注册中心的路由规则,等下一个同步周期到了,服务消费者本地的路由规则就会更新。
服务容错
前面说过,服务提供方可能因为一些原因导致调用失败,这里就需要一个容错的机制,一般有这几种:
- 失败自动切换 服务消费者调用失败后自动从下一个可用节点发起调用,可设置一定的重试次数,一般的读请求可以这么做。
- 失败通知 对于非幂等的写请求,不能像上面那样操作,一般写请求失败的话会先查询一遍服务端的状态,判断写请求是否成功,再来执行下一步操作。
- 快速失败 就是服务消费者调用一次失败后,不再重试。实际在业务执行时,一般非核心业务的调用,会采用快速失败策略,调用失败后一般就记录下失败日志就返回了。
- 失败缓存 就是服务消费者调用失败或者超时后,不立即发起重试,而是隔一段时间后再次尝试发起调用。比如后端服务可能一段时间内都有问题,如果立即发起重试,可能会加剧问题,反而不利于后端服务的恢复。如果隔一段时间待后端节点恢复后,再次发起调用效果会更好。
Be the first person to leave a comment!