随着业务发展,服务越来越多,如何协调线上运行的各个服务,保障服务的 SLA,以及小服务资源浪费的问题,需要能够基于服务调用的性能 KPI数据进行容量管理,合理分配各个服务的资源占用。
线上业务发生故障,需要对故障业务做服务降级、流量控制,快速恢复业务。
为了满足服务线下管控、保障线上高效运行,需要有一个统一的服务治理框架对服务进行统一、有效管控,保障服务的高效、健康运行。

1 服务治理技术的历史变迁

  1. 第一代服务治理 SOA Governance:以 IBM为首的 SOA解决方案提供商推出的针对企业 IT系统的服务治理框架,它主要聚焦在对企业 IT系统中异构服务的质量管理、服务发布审批流程管理和服务建模、开发、测试以及运行的全生命周期管理。
  2. 第二代以分布式服务框架为中心的服务治理:随着电商和移动互联网的快速发展,以阿里为首的基于同一分布式服务框架的全新服务治理理念诞生,它聚焦于对内部同构服务的线上治理,保障线上服务的运行质量。相对比传统 IT架构的服务治理,由于服务的开发模式、部署规模、组网类型、业务特点等差异巨大,因此服务治理的重点也从线下转移到了线上服务质量保障。
  3. 微服务架构+云端服务治理:2013年至今,随着云计算和微服务架构的发展,以 AWS为首的基于微服务架构+云服务化的云端服务治理体系诞生,它的核心理念是服务微自治,利用云调度的弹性和敏捷,逐渐消除人工治理。

微服务架构可以实现服务一定程度的自治,例如服务独立打包、独立部署、独立升级和独立扩容。通过云计算的弹性伸缩、单点故障迁移、服务健康度管理和自动容量规划等措施,结合微服务治理,逐步实现微服务的自治。

1.1 SOA Governance

SOA Governance的定位:面向企业 IT系统异构服务的治理和服务生命周期管理,它治理的服务通常是 SOA服务。
传统的 SOA Governance包含以下四部分内容:

  1. 服务建模:验证功能需求与业务需求,发现和评估当前服务,服务建模和性能需求,开发治理规范。
  2. 服务组装:创建服务更新计划,创建和修改服务以满足所有业务需求,根据治理策略评估服务,批准组装完成。
  3. 服务部署:确保服务的质量,措施包括功能测试、性能测试和满足度测试,批准服务部署。
  4. 服务管理:在整个生命周期内管理和监控服务,跟踪服务注册表中的服务,根据 SLA上报服务的性能 KPI数据进行服务质量管理。

SOA Governance 工作原理如下:

传统 SOA Governance 缺点如下:

  1. 分布式服务框架的发展,内部服务框架需要统一,服务治理也需要适应新的架构,能够由表及里,对服务进行细粒度的管控。
  2. 微服务架构的发展和业务规模的扩大,导致服务规模量变引起质变,服务治理的特点和难点也随之发生变化。
  3. 缺少服务运行时动态治理能力,面对突发的流量高峰和业务冲击,传统的服务治理在响应速度、故障快速恢复等方面存在不足,无法更敏捷地应对业务需求。

1.2 分布式服务框架服务治理

  1. 分布式服务矿机的服务治理定位:面向互联网业务的服务治理,聚焦在对内部采用统一服务框架服务化的业务运行态、细粒度的敏捷治理体系。
  2. 治理的对象:基于统一分布式服务框架开发的业务服务,与协议本身无关,治理的可以是 SOA服务,也可以是基于内部服务框架私有协议开发的各种服务。
  3. 治理策略:针对互联网业务的特点,例如突发的流量高峰、网络延时、机房故障等,重点针对大规模跨机房的海量服务进行运行态治理,保障线上服务的高 SLA,满足用户的体验。常用的治理策略包括服务的限流降级、服务迁入迁出、服务动态路由和灰度发布等。

以分布式服务框架 Dubbo为例,它的服务治理体系如下:

1.3 AWS 云端微服务治理

随着云计算的发展,Dev&Ops 逐渐流行起来,基础设施服务化(IaaS)为大规模、批量流水线式软件交付提供了便利,AWS 作为全球最大的云计算解决方案提供商,在微服务云化开发和治理方面积累了非常多的经验,具体总结如下:

  1. 全公司统一服务化开发环境,统一简单化服务框架(Coral Service),统一运行平台,快速高效服务开发。
  2. 所有后端应用服务化,系统由多项服务化组件构成。
  3. 服务共享、原子化、重用。
  4. 服务由小研发团队负责服务开发、测试、部署和治理,运维整个生命周期支撑。
  5. 高度自动化和 Dev&Ops 支持,一键式服务部署和回退。
  6. 超大规模支持:后台几十万个服务,成千上万开发者同时使用,平均每秒钟有 1~2 个服务部署。
  7. 尝试基于 Docket 容器部署微服务。
  8. 服务治理是核心:服务性能 KPI统计、告警、服务健康度管理、灵活的弹性伸缩策略、故障自动迁移、服务限流和服务降级等多种治理手段,保障服务高质量运行。

2 应用服务化后面临的挑战

2.1 跨团队协作问题

  1. 服务提供者 S分布式部署,存在多个服务实例,如果做端点调试,路由模块会动态分发消息,随机路由,服务提供者 S无法确定要连接的 IP地址。
  2. 如果打断点,其它消费者也正在进行服务调用,调试会被干扰,需要通知所有的开发者不要调用服务 S,显然不可能。

2.2 服务的上下线管控

需要结束某些服务的生命周期,服务提供者直接将服务下线,导致依赖该服务的应用不能正常工作。服务下线时,应先标记为过时,然后通知调用方尽快修改调用,通过性能 KPI接口和调用链分析,确认没有消费者再调用此服务,才能下线。

2.3 服务安全

针对内部应用,服务框架通常采用长链接管理客户端连接,针对非信任的第三方应用,或者恶意消费者,需要具备黑白名单访问控制机制,防止客户端非法链路过多,占用大量的句柄、线程和缓存资源,影响服务提供者的运行质量。

2.4 服务 SLA 保障

由于非核心服务跟系统其它服务打包部署在同一个 Tomcat等容器进程中,一旦非核心服务需要停止,也影响其它合设的服务,如何高效的关停非核心服务,但又不影响其它合设的服务,需要服务治理框架统一考虑。
另外超时时间也要方便的在线可视化的修改,不需要重启即可动态生效。

2.5 故障快速定界定位

由于分布式和大规模的部署,导致服务的 SLA将很难有效保障。

3 服务治理

分布式服务框架的服务治理目标如下:

  1. 防止业务服务架构腐化:通过服务注册中心对服务强弱依赖进行分析,结合运行时服务调用链关系分析,梳理不合理的依赖和调用路径,优化服务化架构,防止代码腐化。
  2. 快速故障定界定位:通过 Flume 等分布式日志采集框架,实时收集服务调用链日志、服务性能 KPI数据、服务接口日志、运行日志等,实时汇总和在线分析,集中存储和展示,实现故障的自动发现、自动分析和在线条件检索,方便运维人员、研发人员进行实时故障诊断。
  3. 服务微管控:细粒度的运行期服务治理,包括限流降级、服务迁入迁出、服务超时控制、智能路由、统一配置、优先级调度和流量迁移等,提供方法级治理和动态生效功能,通过一系列细粒度的治理策略,在故障发生时可以多管齐下,在线调整,快速恢复业务。
  4. 服务生命周期管理:包括服务的上线审批、下线通知,服务的在线升级,以及线上和线下服务文档库的建设。

3.1 服务治理架构设计

分布式服务框架的服务治理分三层:

第二层为服务治理 SDK层,主要由如下组成:

  1. 服务治理元数据:服务治理元数据主要包括服务治理实体对象,包括服务模型、应用模型、治理组织模型、用户权限模型、数据展示模型等。元数据模型通过 DataMapper和模型扩展,向上层界面屏蔽底层服务框架的数据模型,实现展示层和服务架构的解耦,元数据也可以用于展示界面的定制扩展。
  2. 服务治理接口:服务治理 Portal调用服务治理接口,实现服务治理。例如服务降级接口、服务流控接口、服务路由权重调整接口、服务迁移接口等。服务接口与具体的协议无关,它通常基于分布式服务框架自身实现,可以是 Restful接口,也可以是内部的私有协议。
  3. 服务治理客户端类库:由于服务治理服务本身通常也是基于分布式服务框架开发,因此服务治理 Portal需要继承分布式服务框架的客户端类库,实现服务的自动发现和调用。
  4. 调用示例:客户端 SDK需要提供服务治理接口的参数说明、注意事项以及给出常见的调用示例,方便前端开发人员使用。
  5. 继承开发指南:服务治理 SDK需要提供继承开发指南。

3.2 运行态服务治理功能设计

运行态服务治理首先要做到可视:当前系统发布了哪些服务,这些服务部署在哪些机器上,性能 KPI数据如何,指标是否正常等。
由于性能 KPI数据的统计周期、统计指标和报表呈现方式差异比较大,因此服务框架很难抽象出一套放之四海而皆准的性能统计功能,因此在设计的时候需要注意以下两点:

  1. 扩展性:服务性能 KPI数据采集由插件 Handler 负责,平台和业务均可以通过扩展性能统计插件 Handler的方式扩展采集指标和采集周期等。
  2. 原子性:服务提供者和消费者只负责原始数据的采集和上报,不在本节点内做复杂的汇总操作,汇总和计算由性能汇聚节点的 Spark等大数据流式框架负责。

3.3 线下服务治理

为了解决消费者提供者之间的文档过时、错误问题,需要简历服务文档中心,方便线上运维人员查看和多团队之间的协作,它的工作原理如下:

基于 java DOC工具进行扩展,将规则内置到 IDE开发模板中,并通过 CI构建工具做编译检测,将不符合要求的服务接口输出到 CI构建报告并邮件发送给服务责任人。
服务的上线审批、下线通知机制需要建立并完善起来,工作原理如下:

3.4 安全和权限管理

安全涉及到两个层面:

  1. 服务的开放和鉴权机制。
  2. 服务治理的安全和权限管理。

服务治理的使用者通常分三类:

  1. 开发或者测试:主要定位问题,协助运维人员做服务治理。
  2. 运维人员:主要日常运维巡检,查看服务性能 KPI是否正常,是否有报警,利用服务治理进行故障恢复。
  3. 管理者:主要关心运营层面的 KPI数据,只看不管。

4 个人总结

服务治理总体结构图如下:

当系统当前资源非常有限时,为了保证高优先级的服务能够正常运行,保障服务 SLA,需要降低一些非核心服务的调度频次,释放不防资源占用,保障系统的整体运行平稳。

1 设置服务优先级

服务优先级调度有多种策略:

  1. 基于线程调度器的优先级调度策略。
  2. 基于优先级队列的优先级调度策略。
  3. 基于加权配置的优先级调度策略。
  4. 基于服务迁入迁出的优先级调度策略。

2 线程调度器方案

Thread.setPriority()。
线程优先级被线程调度器用来判定何时运行哪个线程,理论上,优先级高的线程比优先级低的线程获得更多的 CPU时间。而实际上,线程获得的 CPU时间通常由包括优先级在内的多个因素决定。
服务在发布的时候,可以根据用户的优先级配置策略,将服务优先级映射到线程优先级中,然后创建多个不同的优先级线程,分别调度对应的服务,工作原理如下:
![][1]
算法简单,开发工作量小,但是不同的操作系统上,有自己不同的策略,这对于需要某些精确控制执行比例的服务是不可接受的。

线程优先级可以用来提高一个已经能够正常工作的服务的运行质量,但是却无法保证精确性和跨平台移植性。因此,通常不建议使用线程调度器实现服务的优先级调度。

3 Java 优先级队列

Java的 PriorityQueue是一个基于优先级堆的无界优先级队列。
![][2]
缺点在于:如果持续有优先级高的消息需要处理,会导致优先级低的消息得不到及时处理而积压。而积压到一定程度之后,低优先级的消息可能已经超时,即便后续得到执行机会,由于已经超时也需要丢弃掉,在此之前,它灰一直占用优先级队列的堆内存,同时导致客户端业务线程被挂住等待应答消息直到超时,从资源调度层面看,PriorityQueue 的算法并不太适合分布式服务框架。

4 加权优先级队列

分布式服务框架的服务优先级调度并不是只处理高优先级的消息,而是按照一定比例优先调度高优先级的服务,采用加权优先级队列可以很好地满足这个需求。
原理如下:它由一系列的普通队列组成,每个队列与服务优先级 1:1 对应。当服务端接收到客户端请求消息时,根据消息对应的服务优先级取值将消息投递到指定的优先级队列中。
工作线程按照服务优先级的加权值,按比例从各个优先级队列中获取消息,然后按照优先级的高低将消息设置到工作线程的待处理消息数组中,由于只有本工作线程会读写消息数组,因此该数组是线程安全的。
![][3]
缺点在于:如果优先级等级比较多,对应的优先级队列就会膨胀,如果优先级队列发生积压,这将导致内存占用迅速飙升。

5 服务迁入迁出

前面的几种优先级调度策略是比较传统的做法,基于服务迁入迁出的则是利用分布式服务框架的服务动态发现机制,通过调整服务运行实例数来实现优先级调度。
原理如下:

  1. 当系统资源紧张时,通过服务治理 Portal的服务迁入迁出界面,将低优先级服务的部分运行实例从服务注册中心中迁出,也就是动态去注册。
  2. 消费者动态发现去注册的服务,将这部分服务实例的地址信息从路由表中删除,后续消息将不会路由到已经迁出的服务实例上。
  3. 由于只迁出了部分服务实例,被迁出的低优先级服务仍然能够正常处理,只不过由于部署实例的减少,得到调度的机会就同比降低了很多,释放的资源将被高优先级服务使用。通过资源的动态调配,实现服务的优先级调度。
  4. 当业务高峰期结束之后,通过服务治理 Portal将迁出的服务重新迁入,低优先级的消息恢复正常执行,优先级调度结束。

缺点:自动化程度较低,对运维人员的要求较高。

6 个人总结

服务的优先级调度与动态流控不同,流控最终会拒绝消息,导致部分请求失败。优先级调度是在资源紧张时,优先执行高优先级的服务,在保障高优先级服务能够被合理调度的同时,也兼顾处理部分优先级低的消息,它们之间存在一定的比例关系。

优先级调度本身并不拒绝消息,但是如果在运行过程中发生了流控,则由流控负责拒消息。通常对于高优先级的管理类消息,例如心跳消息、指令消息等们不能被流控掉。

  1. 业务高峰期,为了保证核心服务的 SLA,往往需要停掉一些不太重要的业务,例如商品评论、论坛或者粉丝积分等。
  2. 某些服务因为某种原因不可用,但是流程不能直接失败,需要本地 Mock 服务端实现,做流程放通。例如图书阅读,如果用户登录余额鉴权服务不能正常工作,需要做业务放通,记录消费话单,允许用户继续阅读,而不是返回失败。

这就是服务降级,分为容错降级和屏蔽降级两种模式。

1 屏蔽降级

在一个应用中,服务往往是合设的,尽管可以通过线程池隔离等方式保证服务之间的资源隔离,但是 100%的隔离是不现实的。特别是对缓存、网络 I/O、磁盘 I/O、数据库连接资源等公共依赖无法隔离,在业务高峰期时,服务往往存在激烈的竞争,导致订购等核心服务运行质量下降,影响系统的稳定运行和客户体验。

此时需要对非核心服务做强制降级,不发起远程服务调用,直接返回空、异常或者执行特定的本地逻辑,减少自身对公共资源的消费,把资源释放出来供核心服务使用。

1.1 屏蔽降级的流程

1.2 屏蔽降级的设计实现

屏蔽降级通常用于服务运行态治理,开发时不会配置,当外界的触发条件达到某个临界值时,由运维人员/开发人员决策,通过服务治理控制台,进行人工降级操作,它的取值有如下三种:

  1. mock = force: return null。不发起远程服务调用,直接返回空对象。
  2. mock = force: throw Exception。不发起远程服务调用,直接抛出指定异常。
  3. mock = force: execute Bean。不发起远程服务调用,直接执行本地模拟接口实现类。

屏蔽降级操作是可逆的,当系统压力恢复正常水平或者不再需要屏蔽降级时,可以对已经屏蔽降级的服务恢复正常。恢复之后,消费者重新调用远程的服务提供者,同时服务状态被修改为正常状态。

2 容错降级

当非核心服务不可用时,可以对故障服务做业务逻辑放通,分布式服务框架的业务放通实际属于容错降级的一种。
容错降级不仅仅只用于业务放通,它也常用于服务提供方在客户端执行容错逻辑,容错逻辑主要包括两种:

  1. RPC异常:通常指超时异常、消息解码异常、流控异常、系统拥塞保护异常等。
  2. Service异常:例如登录校验失败异常、数据库操作失败异常等。

2.1 容错降级的工作原理


容错降级与屏蔽降级的主要差异:

  1. 触发条件不同:容错降级时根据服务调用结果,自动匹配触发的;而屏蔽降级往往是通过人工根据系统运行情况手工操作触发的。
  2. 作用不同:容错降级时当服务提供者不可用时,让消费者执行业务放通:屏蔽降级的主要目的是将原属于降级业务的资源调配出来供核心业务使用。
  3. 调用机制不同:一个发起远程服务调用,一个只做本地调用。

业务放通的 Mock接口实现往往放在消费者端,主要在于提供端可能为多个消费者服务,为了解耦,单独的消费者自己进行放通。

  1. mock = fail: throw Exception。将异常转义。
  2. mock = fail: execute Bean。将异常屏蔽掉,直接执行本地模拟解耦实现类,返回 Mock接口的执行结果。

与屏蔽降级不同的是,通常在开发态,就需要指定容错降级的策略。

无论是屏蔽降级还是容错降级,都支持从消费者或者服务提供者两个维度去配置,从而消费端配置更灵活,实现差异化降级策略。
服务降级策略配置的优先级:消费者配置策略 > 服务提供者配置策略。屏蔽降级 > 容错降级。

2.2 运行时容错降级


如果开发态没有指定容错降级策略,系统上线运行后,需要临时增加容错降级策略,服务框架也需要支持在线动态增加容错降级策略,它的工作流程与屏蔽降级类似。

而在实际项目中,利用容错降级做业务放通是主要的应用场景。

3 业务层降级

实际业务开发过程中,可能会存在比较复杂的业务放通场景,例如“调用 A 服务 + 执行本地方法调用”组合成一个流程,针对这个流程的执行结果做放通,这种场景由于本地方法调用并不经过分布式服务框架,因此需要业务自己做放通处理。

服务降级并不能 100% 满足所有业务放通场景,需要业务层开发自己的降级框架。

4 个人总结

在服务化之前,业务往往需要自己实现放通逻辑或者框架,不同的业务模块,甚至不同的开发者都自己实现了一套私有的放通流程,这对项目的开发和运维都会造成很多麻烦。

更为严重的是由于没有统一的服务降级策略和框架,无法在服务治理 Portal 上进行统一线上降级,在应对业务高峰时,运维人员会力不从心,往往需要一大群开发在背后支撑,运维效率非常低下。
基于分布式服务框架的服务降级功能,有效提升线上的服务治理效率,保证服务的 SLA,尽管服务降级更多是为了提升服务线上运行质量,但是它反向对服务的设计和开发也有约束。它要求服务在设计之初就要做如下识别:

  1. 哪些服务是核心服务、哪些是非核心服务?
  2. 哪些服务支持降级,降级策略是什么?
  3. 除了服务降级之外,是否还存在更为复杂的业务放通场景,它的策略是什么?

系统的高效、健康运行仅仅依赖线上服务治理和运维是解决不了的,需要通过分布式服务框架的特性反向映射到设计和开发态,从设计阶段就开始考虑未来如何高效运维,才能在根本上提升服务和产品的质量,这也是矛盾对立和统一的一个具体体现。

当资源成为瓶颈时,服务框架需要对消费者做限流,启动流控保护机制。

1 静态流控

主要针对客户端访问速率进行控制,它通常根据服务质量等级协定(SLA)中约定的 QPS做全局流量控制,例如订单服务的静态流控阈值为 100QPS,则无论集群有多少个订单服务实例,它们总的处理速率之和不能超过 100QPS。

1.1 传统静态流控设计方案

在软件安装时,根据集群服务节点个数和静态流控阈值,计算每个服务及诶单分摊的 QPS阈值,系统运行时,各个服务节点按照自己分配的阈值进行流控,对于超出流控阈值的请求则拒绝访问。

服务框架启动时,将本节点的静态流控阈值加载到内存中,服务框架通过 Handler拦截器咋服务调用前做拦截计数,当计数器在指定周期 T到达 QPS上限时,启动流控,拒绝信的请求消息接入。注意:

  1. 服务实例通常由多线程执行,因此计数时需要考虑线程并发安全,可以使用 Atomic原子类进行原子操作。
  2. 达到流控阈值之后拒绝新的请求消息接入,不能拒绝后续的应答消息,否则这会导致客户端超时或者触发 FailOver,增加服务端的负载。

1.2 传统方案的缺点

  1. 云端服务的弹性伸缩性使服务节点数处于动态变化过程中,预分配方案行不通。
  2. 服务节点宕机,或者有新的服务节点动态加入,导致服务节点数发生变化,静态分配的 QPS需要实时动态调整,否则会导致流控不准。

当应用和服务迁移到云上之后, PaaS 平台的一个重要功能就是支持应用和服务的弹性伸缩,在云上,资源都是动态分配和调整的,静态分配阈值方案无法适应服务迁移到云上。

1.3 动态配额分配制

原理:由服务注册中心以流控周期 T为单位,动态推送每个节点分配的流控阈值 QPS。当服务节点发生变更时,会触发服务注册中心重新计算每个节点的配额,然后进行推送,这样无论是新增还是减少服务节点数,都能够在下一个流控周期内被识别和处理。

而在生产环境中,每台机器/VM 的配置可能不同,如果每个服务节点采用流控总阈值/服务节点数这种平均主义,可能会发生性能高、处理快的节点配额很快用完,但是性能差的节点配额有剩余的情况,这会导致总的配额没用完,但是系统却发生了静态流控的问题。
解决方案一:根据各个服务节点的性能 KPI数据(例如服务调用平均时延)做加权。
解决方案二:配额指标返还和重新申请,每个服务节点根据自身分配的指标值、处理速率做预测,如果计算结果表明指标会有剩余,则把多余的返还给服务注册中心;对于配额已经使用完的服务节点,重新主动去服务注册中心申请配额,如果连续 N次都申请不到新的配额指标,则对于新接入的请求消息做流控。

结合负载均衡进行静态流控,才能够实现更精确的调度和控制。消费者根据各服务节点的负载情况做加权路由,性能差的节点路由到的消息更少,这样保证了系统的负载均衡和配额的合理分配。

1.4 动态配额申请制

尽管动态配额分配制可以解决节点变化引起的流控不准问题,也能改善平均主义配额分配缺点如下:

  1. 如果流控周期 T比较大,各服务节点的负载情况变化比较快,服务节点的负载反馈到注册中心,统一计算后再做配额均衡,误差会比较大。
  2. 如果流控周期 T比较小,服务注册中心需要实时获取各服务节点的性能 KPI数据并计算负载情况,经过性能数据采集、上报、汇总和计算之后会有一定的时延,这会导致流控滞后产生误差。
  3. 如果采用配额返还和重新申请方式,则会增加交互次数,同时也会存在时序误差。
  4. 扩展性差,负载的汇总、计算和配额分配、下发都由服务注册中心完成,如果服务注册中心管理的节点数非常多,则服务注册中心的计算压力就非常大,随着服务节点数的增加服务注册中心配额分配效率会急速下降、系统不具备平滑扩展能力。

而动态配额申请制,工作原理如下:

  1. 系统部署的时候,根据服务节点数和静态流控 QPS阈值,拿出一定比例的配额做初始分配,剩余的配额放在配额资源池中。
  2. 哪个服务节点使用完了配额,就主动向服务注册中心申请配额。配额的申请策略是,如果流控周期为 T,则将周期 T分成更小的周期 T/N(N为经验值,默认值为 10),当前的服务节点数为 M个,则申请的配额为(总 QPS配额-已经分配的 QPS)/ M * T / N。
  3. 总的配额如果被申请完,则返回 0 配额给各个申请配额的服务节点,服务节点对新接入的请求消息进行流控。

动态配额申请制的优点:

  1. 各个服务节点最清楚自己的负载情况,性能 KPI数据在本地内存中计算获得,实时性高。
  2. 由各个服务节点根据自身负载情况去申请配额,保证性能高的节点有更高的配额,性能差的自然配额就少,实现合理资源,流控的精确性。

2 动态流控

动态流控的最终目标是为了保命,并不是对流量或者访问速度做精确控制。
触发动态流控的因子是资源,资源又分为系统资源和应用资源两大类,根据不同的资源负载情况,动态流控又分为多个级别,每个级别流控系数都不同,也就是被拒绝掉的消息比例不同。每个级别都有相应的流控阈值,这个阈值通常支持在线动态调整。

2.1 动态流控因子

动态流控因子包括系统资源和应用资源两大类,常见的系统资源包括:

  1. 应用进程所在主机/VM 的 CPU使用率。
  2. 应用进程所在主机/VM 的 内存使用率。

使用 java.lang.Process 执行 top、sar 等外部命令获取系统资源使用情况。
常用的应用资源:

  1. JVM 堆内存使用率
  2. 消息队列积压率
  3. 会话积压率

具体实现策略是系统启动时拉起一个管理线程,定时采集应用资源的使用率,并刷新动态流控的应用资源阈值。

2.2 分级流控

不同级别拒掉的消息比例不同,例如一级流控拒绝掉 1/8 的消息;发生二级流控时,拒绝掉 1/4 消息。
为了防止系统波动导致的偶发性流控,无论是进入流控状态还是从流控状态恢复,都需要连续采集 N次并计算平均值,如果连续 N次平均值大于流控阈值,则进入流控状态。
而在一个流控周期内,不会发生流控级别的跳变。

3 并发控制

并发控制针对线程的并发执行数进行控制,它的本质是限制对某个服务或者服务的方法过度消息,耗用过多的资源而影响其它的服务的正常运行。有两种形式:

  1. 针对服务提供者的全局控制。
  2. 针对服务消费者的局部控制。

4 连接控制

通常分布式服务框架服务提供者和消费者之间采用长连接私有协议,为了防止因为消费者连接数过多导致服务端负载压力过大,系统需要针对连接数进行流控。

5 并发和连接控制算法

并发连接的控制算法原理如下图:

基于服务调用 Pipeline 机制,可以对请求消息接收和发送、应答消息接收和发送、异常消息等做切面拦截(类似 Spring 的 AOP 机制,但是没采用反射机制,性能更高),利用 Pipeline 拦截切面接口,对请求消息做服务调用前的拦截和计数,根据计数器做流控,服务端的算法如下:

  1. 获取流控阈值。
  2. 从全局 RPC上下文中获取当前的并发执行数,与流控阈值对比,如果小于流控阈值,则对当前的计数器做原子自增。
  3. 如果等于或者大于流控阈值,则抛出 RPC流控异常给客户端。
  4. 服务调用执行完成之后,获取 RPC上下文中的并发执行数,做原子自减。

客户端的算法如下:

  1. 获取流控阈值。
  2. 从全局 RPC上下文中获取当前的并发执行数,与流控阈值对比,如果小于流控阈值,则对当前的计数器做原子自增。
  3. 如果等于或大于流控阈值,则当前线程进入 wait状态, wait超时时间为服务调用的超时时间。
  4. 如果有其它线程服务调用完成,调用计数器自减,则并发执行数小于阈值,线程被 notify,退出 wait,继续执行。

6 个人总结

流量控制是保证服务 SLA(Sevice-Level Agreement)的重要措施,也是业务高峰期故障预防和恢复的有效手段,分布式服务框架需要支持流控阈值、策略的在线调整,不需要重启应用即可生效。

服务上线之后,由于功能变更、BUG修复,以及服务升级,需要对服务采用多版本管理。

1 服务多版本管理设计

管理的对象包括服务提供者和消费者:

  1. 服务提供者:发布服务的时候,支持指定服务的版本号。
  2. 服务消费者:消费服务的时候,支持指定引用的服务版本号或者版本范围。

1.1 服务版本号管理

服务的版本号是有序的,在服务名相同的情况下,两个相同服务名的不同服务版本的版本号可以比较大小。完整的版本号由“主版本号(Major)+副版本号(Minor)+微版本号(Micro)”构成:

  1. 主版本号:表示重大特性或者功能变更,接口或功能可能会不兼容。
  2. 副版本号:发生了少部分功能变更,或者新增了一些功能。
  3. 微版本号:主要用于 BUG修改,对应于常见的 SP补丁包。

1.2 服务提供者

服务开发完成之后,需要将一个或者多个服务打包成一个 jar/war 包,为了便于对服务进行物理管理,打包后的名称中会包含服务的版本号信息,例如 com.huawei.orderService_1.0.1.jar。
在微服务架构中,微服务独立开发、打包、部署和升级,因此微服务的版本和软件包的版本可以一一映射。但是在实际开发中,尤其是大规模企业应用开发,单独为每个服务打包和部署目前尚未成为主流,它会增加服务软件包的管理和线上治理成本,因此目前的主流模式仍然是多个服务提供者合一个大的 jar/war 包,这就会存在一个问题:项目开发后期,有些服务进行了版本升级,有些服务没有,这样当它们被打包成同一个软件包时,就会导致版本号不一致。

每个服务都指定一个版本号,对开发而言也比较麻烦。一个比较好的实践就是微服务+全局版本模式。对于经常发生功能变更、需要独立升级的服务,将其独立拆分出来进行微服务化,实现单个微服务级的打包和部署。

对于其它服务,服务框架提供全局版本功能,在 Maven组件工程开发时,只需要为整个工程配置一个版本号,该组件工程包含的所有服务都共用该版本号。如果组件工程包含的某个服务发生了版本变更,就统一升级全局版本号,其它未发生功能变更但是打包在一起的服务做级联升级。这样做的一个原因是服务被打包在一起后,无论其它服务是否需要升级,只要软件包中的一个服务发生了版本升级,其它合设的服务也必须与其一起打包升级,它们之间存在物理上的耦合,这也是为什么微服务架构提倡微服务独立打包、部署和升级的原因。

1.3 服务消费者

与服务提供者不同,服务消费者往往不需要指定具体依赖的服务版本,而是一版本范围,例如:version=“[1.0.1, 2.0.8]”。

  1. 消费者关心的是某个新特性从哪个服务版本中开始提供,它并不关系服务提供者的版本演进以及具体的版本号。
  2. 消费者想使用当前环境中服务的最新版本,但不清楚具体的版本号,希望自动适配最新的服务版本。

当然需要指定一个默认的服务提供者版本号。

1.4 基于版本号的服务路由

服务提供者将服务注册到服务注册中心时,将服务名+服务版本号+服务分组作为路由关键信息存放到注册中心,服务消费者在发起服务调用时,除了携带服务名、方法名、参数列表之外,还需要携带要消费的服务版本信息,由路由接口负责服务版本过滤,如下图:

1.5 服务热升级

在业务不中断的情况下,实现系统的平滑升级,考虑到版本升级的风险,往往需要做多次滚动升级,最终根据升级之后新版本服务的运行状况决定是继续升级还是回退。这就意味着在同一时刻,整个集群环境中会同时存在服务的多个版本咋线运行,这就是热升级相比于传统 AB Test等升级方式的差异,如下图:

核心点如下:

  1. 升级的节点需要重启,由于自动发现机制,停机升级的节点自动被隔离,停机并不会中断业务。
  2. 服务路由规则的定制:如果是滚动式的灰度发布,在相当长的一段时间(例如一周)内线上都会存在服务的多个版本。哪些用户或者业务需要路由到新版本上,需要通过路由策略和规则进行制定,服务框架应该支持用户配置自定义的路由规则来支持灵活的路由策略。
  3. 滚动升级和回退机制:为了降低服务热升级对业务的影响,同时考虑到可靠性,在实际工作中往往采用滚动升级的方式,分批次进行服务的热升级,实现敏捷的特性交付,滚动升级如下图:

2 与 OSGI 的对比

OSGI,成立于 1999年,全名原为:Open Services Gateway initiative,但现在这个全名已经废弃。
致力于家用设备、汽车、手机、桌面、其它环境指定下一代网络服务标准的领导者,推出了 OSGI 服务平台规范,用于提供开放和通用的架构,使得服务提供商、开发人员、软件提供商、网关操作者和设备提供商以统一的方式开发、部署和管理服务。

目前最广泛和应用是 OSGI规范5(Release 5),共由核心规范、标准服务(Standard Services)、框架服务(Framework Services)、系统服务(System Services)、协议服务(Protocol Services)、混合服务(Miscellaneous Services)等几部分共同组成。
核心规范通过一个分层的框架,实现了 OSGI最为成功的动态插件机制,它主要提供了:

  1. OSGI Bundle 的运行环境。
  2. OSGI Bundle 间的依赖管理。
  3. OSGI Bundle 的生命周期管理。
  4. OSGI 服务的动态交互模型。

OSGI 两个最核心的特性就是模块化和热插拔机制,分布式服务框架的服务多版本管理和热升级是否可以基于 OSGI来实现?下面围绕着模块化和插件热插拔这两个特性进行详细分析。

2.1 模块化开发

在 OSGI中,我们以模块化的方式去开发一个系统,每个模块被称为 Bundle,OSGI 提供了对 Bundle的整个生命周期管理,包括打包、部署、运行、升级、停止等。
模块化的核心并不是简单地把系统拆分成不同的模块,如果仅仅是拆分,原生的 Jar包+Eclipse工程就能够解决问题。更为重要的是要考虑到模块中接口的导出、隐藏、依赖、版本管理、打包、部署、运行和升级等全生命周期管理,这些对于原生的 Jar包而言是不支持的。
传统开发的模块划分通常由两种方式:

  1. 使用 package来进行隔离。
  2. 定义多个子工程,工程之间通过工程引用的方式进行依赖管理。
    存在的问题:无法实现资源的精细划分和对依赖做统一管理。以 Jar包依赖为例,依赖一个 Jar包就意味着这个 Jar包中所有 public的资源都可能被引用,但事实上也许只需要依赖该 Jar包中的某几个 public接口。无法对资源做细粒度、精确的管控,不知道 public的接口都被哪些模块依赖和使用,消费者是谁,更为复杂的场景是如果消费者需要依赖不同的接口版本,那该肿么办?

OSGI 很好地解决了这个问题,每个 OSGI工程是一个标准的插件工程,实际就是一个 Bundle。实现了 package级的管理依赖关系,而 Maven则是 Jar包级的管理依赖。
而分布式服务:

  1. 服务提供者通过 service export将某个服务接口发布出去,供消费者使用。
  2. 服务消费者通过 service import导入某个服务接口,它不关心服务提供者的具体位置,也不关心服务的具体实现细节。
    这样就比 OSGI的 package导入导出功能粒度更细。
    利用 Maven的模块化管理 + 分布式服务框架自身的服务接口导入导出功能,解决了模块化开发和精细化依赖管理难题,完成可以替代 OSGI的相关功能,

2.2 插件热部署和热升级

OSGI 另外一个非常酷的特性就是动态性,即插件的热部署和热升级,它可以在不重启 JVM的情况下安装部署一个插件,实现升级不中断业务。
OSGI 的插件热部署和热升级原理就是基于自身的网状类加载机制实现的,下面我们分析在分布式服务框架中,如何实现服务热部署和热升级:

  1. 服务是分布式集群部署的,通常也是无状态的,停掉其中某一个服务节点,并不会影响系统整体的运行质量。
  2. 服务自动发现和隔离机制,当有新的服务节点加入时,服务注册中心会向消费者集群推送新的服务地址信息;当有服务节点宕机或重启时,服务注册中心会发送服务下线通知消息给消费者集群,消费者会将下线服务自动隔离。
  3. 优雅停机功能,在进程退出之前,处理完消息队列积压的消息,不再接受新的消息,最大限度保障丢失消息。
  4. 集群容错功能,如果服务提供者正在等待应答消息时系统推出了,消费者会发生服务调用超时,集群容错功能会根据策略重试其它正常的服务节点,保证流程不会因为某个服务实例宕机而中断。
  5. 服务多版本管理,支持集群中同一个服务的多个版本同时运行,支持路由规则定制,不同的消费者可以消费不同的服务版本。

相比于 OSGI在 JVM内部通过定制类加载机制实现插件的多版本运行和升级,使用分布式服务框架自身的分布式集群特性实现服务的热部署和热升级,更加简单、灵活和可控。

3 个人总结

服务多版本在实际项目中非常实用,用于实现服务的热部署和热升级,同时支持按照消费者做差异化路由,同时也方便演进到微服务架构,来迁移到服务的独立打包、部署、运行和运维。

服务消费者和提供者之间进行通信时,除了接口定义的请求参数,往往还需要携带一些额外参数,例如消费提供者的 IP地址、消息调用链的跟踪 ID等;这些参数不能通过业务接口来进行传递,需要底层的分布式服务框架支持这种参数传递方式。

1 内部参数

1.1 业务内部参数传递

  1. 硬编码,在业务逻辑中进行 API调用,参数通过 API接口进行引用传参。
  2. 业务编排引擎对业务流程进行编排,参数往往通过抽象的编排上下文进行传递。
  3. 通过专业的 BPM流程引擎进行业务逻辑编排,参数通过流程上下文进行传递。

硬编码通常会直接通过方法参数进行参数传递,如下图:

还有一种比较常用的方法就是通过线程上下文进行参数传递。通常情况下业务逻辑处理过程很少发生线程切换,因此通过线程上下文进行隐式传参可以不与某个具体方法接口耦合,对业务解耦接口没有侵入性。例如 Spring 的资源和事务线程绑定机制,利用的就是 JDK 提供的线程上下文。使用线程上下文传参是一种隐式传参,上面的方法调用可以简化成如下图:

最后一种方式就是 BPM流程引擎,流程引擎通过流程上下文进行参数传递,用户可以在流程编排界面声明流程级参数和全局参数,流程引擎通过流程上下文进行参数传递。

1.2 服务框架内部参数擦混地

服务框架内部由多个模块组成,模块之间的调用通常会发生线程切换;另外,当服务框架通过反射调用服务接口实现类时,也需要向业务代码传递一些额外的参数,这些参数如何传递?下面我们分别对这两类场景进行分析:

  1. 通信框架将数据报反序列化成业务请求对象之后,需要将消息封装成 Task,丢到后面的业务线程池中执行,此时会发生线程切换,如下图:

    由于发生了线程切换,如果通过线程变量的方式传递参数,需要遍历线程上下文,将线程变量复制到业务线程池的线程变量中,非常麻烦。如果后续新增系统参数,往往会忘记复制,导致参数不一致。
    这种场景中,一般会选择通过消息上下文进行参数传递,例如在业务请求参数中定义了 Map 扩展参数,用于跨线程的参数传递。
  2. 当服务框架回调业务接口实现类时,由于是通过反射调用,业务接口的参数已经定义好了,无法传递其它参数(例如消费者的 IP地址、调用链 ID等参数)。为了解决这个问题,需要利用线程变量,因为平台调用服务实现类不会发生线程切换,所以通过线程变量传参是安全的。

2 外部参数

主要用途:

  1. 服务框架自身的参数传递,例如分布式事务中事务上下文信息传递。
  2. 业务之间的参数传递,例如业务调用链 ID的传递,用于唯一表示某个完整业务流程。

2.1 通信协议支持

消费者的自定义参数传递到服务端,需要有一个载体,它就是通信协议。一个设计良好的协议,往往支持用户自定义扩展消息头,在协议消息头中,可以预留一个 Map<String, byte[]> 类型的字段,用于服务框架或者用户自定义参数扩展。

2.2 传参接口定义

服务框架需要提供参数设置 API接口,用于业务跨进程的参数传递。建议使用线程变量,例如平台定义一个 RPCContext 线程变量供业务传参使用。
服务框架在业务请求参数传递到通信框架时,需要遍历 RPCContext,将框架和业务设置的参数复制到通信线程中,由通信线程在序列化时将请求参数设置到消息头中,传递到通信对端,如下图:

3 最佳实践

3.1 防止参数互相覆盖

由于使用的是 Map 或线程变量,因此需要防止参数互相覆盖:

  1. 服务框架系统参数和业务参数的互相覆盖。
  2. 业务之间的参数覆盖。

一些系统参数往往默认会被平台占用,例如 IP、timeStamp、Host、ServiceName、Group 等常见字段。因此系统平台需要规范,对于业务来说需要有一个全局的业务传参规则。

3.2 参数声明周期管理

预防内存泄漏!通常,服务框架能对参数生命周期进行自动管理,例如对于服务端,服务调用前设置参数,服务调用后清空参数。
但是对于消费者比较复杂,如果在将参数序列化到请求头发送之后自动清空参数,后续应答返回之后消费者可能需要继续访问之前的参数,但是有可能后续不使用该参数或者忘记删除该参数就会导致参数堆积。如果参数名每次都不同,则会发生内存泄漏。

最好的方式就是平台的提供一个删除模式参数的 API,允许用户手动删除,如果用户不指定就自动删除。

4 个人总结

参数传递涉及到上下文、通信框架的线程切换、以及自动删除的自动管理等,而服务框架需要提供不同的参数传递模式以适应业务。

灰度发布是指在黑与白之间,能够平滑过渡的一种发布方式。AB test 就是一种灰度发布方式:让一部分用户继续用 A,一部分用户开始用 B;如果用户对 B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到 B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。

1 服务灰度发布流程设计

服务灰度发布的主要作用如下:

  1. 解决服务升级不兼容问题。
  2. 及早获得用户的意见反馈,完善产品功能,提升服务质量。
  3. 缩小服务升级所影响的用户范围,降低升级风险。
  4. 让用户及早参与产品测试,加强用户互动。

1.1 灰度环境准备

  1. 系统运维人员通过管理员账号登录灰度发布 Portal或者进入服务治理的灰度发布界面。
  2. 在生产环境中圈定本轮灰度发布的范围,它通常是一个应用集群,包括前后台服务,当然可能是单个服务。
  3. 将选择的服务灰度发布范围信息保存到服务注册中心,用于后续的规则下发和灰度升级历史记录查询等。
  4. 通知灰度升级查询内的服务实例下线,通常会采用优雅停机的方式让待升级的服务下线,保证升级不中断业务。
  5. 应用金城接收到优雅停机指令后,将本进程内缓存的消息处理完,然后优雅退出。
  6. 从软件仓库选择需要升级的服务安装镜像包,用于灰度环境的版本升级。
  7. 将升级包批量上传到灰度环境中,把原来的业务软件包做本地备份之后,升级服务版本。
  8. 灰度环境升级部署成功之后,返回灰度环境部署成功消息给灰度发布管理控制台,然后进行后续的灰度发布操作。

1.2 灰度规则设置

灰度环境准备完成之后,运维人员对灰度规则进行配置,灰度规则主要用于服务路由。
按照规则的不同,部分用户将调用老的服务,另一部分用户则会调用灰度环境中新发布的服务,常用的灰度规则分类如下:

  1. 按照接入门户类型分类,例如网上营业厅、手机客户端、营业厅实体店、自主业务办理终端等。
  2. 按照终端类型分类,例如 Android、IOS、Windows Phone 等。
  3. 按照区域进行划分,例如东北、华北、华中等。
  4. 其它策略。

1.3 灰度规则下发

灰度规则设置完成之后,需要将规则下发给参与消费路由的软负载均衡器 SLB、Web前台和后台服务,它的处理流程如下图:

灰度规则下发,主要由服务注册中心负责推送,推送的目标包括前端的 SLB负载均衡器、Web 前台集群和 App 后台服务集群,各节点将灰度规则缓存到本地内存中;消息或者服务路由时,通过路由插件解析灰度规则,将消息路由到指定到服务版本中。需要指出的是,灰度规则的通知范围是整个生产环境集群,包括灰度发布环境和非灰度生产环境。

1.4 灰度路由

通过 SLB定制的灰度发布插件,可以将 HTTP消息按照规则分发到不同的 Web前台;Web前台根据内置的服务框架 SDK,通过客户端灰度路由插件,解析灰度规则,将服务路由到灰度或者非灰度环境,实现服务的灰度路由。
灰度路由的流程如下图:

需要指出的是,如果灰度规则解析失败,实际上就无法区分哪些服务应该路由到灰度环境,这种场景下比较合适的做法就是将服务路由到非灰度环境。如果服务提供者无法处理或者处理失败,则需要对灰度发布做回退处理,并通知所有受影响的服务消费者。

1.5 失败回滚

失败回滚的流程如下:

1.6 灰度发布总结

灰度发布之后,需要对灰度发布之后的服务运行和运营情况进行分析,包括服务调用来源分析、服务性能 KPI数据、用户行为分析报告、用户问卷调查等,通过对这些数据进行分析来改进服务功能,完善产品,为新一轮灰度发布做铺垫。

2 个人总结

互联网产品在于不停地升级、升级,再升级,但是升级伴随着风险,新旧版本兼容的风险,用户使用习惯突然改变而造成用户流失的风险,系统宕机的风险等。为了避免这些风险,很多产品都采用了灰度发布的策略,其主要思想就是把影响集中到一个点,然后再发散到一个面,出现意外情况后就容易回退。
分布式服务框架支持服务的灰度发布,可以实现业务的快速试错和敏捷交付。

服务提供者需要支持通过配置、注解、API调用等方式,把本地接口发布成远程服务;对于消费者,可以通过对等的方式引用远程服务提供者,实现服务的发布和引用。

1 服务发布设计

服务发布流程:

1.1 服务发布的几种方式

  1. XML配置化方式:业务代码零侵入
  2. 注解方式:业务代码低侵入
  3. API调用方式:业务代码侵入较强

1.2 本地实现类封装成代理

对于服务提供者,将本地实现类封装成代理对象不是必需的:也可以利用一系列工具类解析服务提供者信息,然后将服务提供者的地址信息注册到服务注册中心。采用动态代理的好处如下:

  1. 不管是什么服务,它们的发布流程都是相似的,通过抽象代理层,可以对服务发布行为本身进行封装和抽象。
  2. 通过动态代理对象,可以对服务发布进行动态拦截,方便平台和业务对服务发布进行个性化定制。
  3. 便于扩展和替换。

1.3 服务发布成指定协议

同一个服务,允许发布成多种协议,例如 HTTP、Thrift 等。

1.4 服务提供者信息注册

服务按指定协议发布之后,需要将服务发布信息注册到注册中心,用于服务路由和服务治理。

服务注册的结构有多种方式,例如按照主机地址、按照服务名或者 URL。

2 服务引用设计

消费者导入服务提供者的接口 API定义,配置服务引用信息,即可在代码中直接调用远程服务:

2.1 本地接口调用转换成远程服务调用

原理在于根据导入的服务提供者接口 API和服务引用信息,生成远程服务的本地动态代理对象;它负责将本地的 API调用转换成远程服务调用,然后将结果返回给调用者。

2.2 服务地址的本地缓存

服务消费者和提供者的启动顺序无法控制,因此消费者需要检测指定服务目录,监听新的服务提供者注册和已发布服务的下线,工作原理如下:

2.3 远程服务调用

消费者从本地缓存的服务列表中按照指定策略路由,将请求消息封装成协议消息:调用相关协议的客户端将请求发送给服务提供者,业务线程按照服务调用方式选择同步等待或者注册监听器回调。

3 最佳实践

3.1 对等设计原则

例如,通过 XML的 Method 元素可以配置方法级参数,那么 API或者注解也应该支持方法级设置,以防止能力不对等。

3.2 启动顺序问题

服务注册中心如果后启动,需要服务提供者和消费者能够自动重连。

3.3 同步还是异步发布服务

通常情况下,服务全部准备就绪的时间比较短,而且系统启动之后也并不意味着所有服务都会被立即消费,因此,采用异步的方式发布服务也是可行的。
当然还有另外一些办法可减少系统启动时间。例如对于不经常访问的服务采用延迟发布的策略;还有就是服务的懒加载,只发布服务但是不初始化,等到消费者真正调用的时候才进行初始化服务。

3.4 警惕网络风暴

在大规模集群系统中,服务注册中心可能管理数十万条的服务注册信息以及上万个服务提供者和消费者节点。如果服务注册中心管理了大量经常变更的信息,就会发生频繁的变更通知:而这种海量的变更通知可能会挤占服务注册中心的网络带宽,严重时还会导致网络风暴。
因此,在设计时需要考虑如下几个要素:

  1. 哪些信息需要注册到服务注册中心,需要甄别。
  2. 服务注册中心能够管理的服务上限。
  3. 服务注册中心的网络带宽规划。
  4. 服务注册中心的磁盘空间规划。
  5. 服务注册中心的性能。

3.5 配置扩展

4 个人总结

好的分布服务框架对业务代码的侵入要足够低(使用 XML配置方式),将普通的 Java 接口发布成远程服务。

1 几个概念

1.1 服务提供者

1.2 服务消费者

1.3 服务注册中心

服务注册中心是分布式服务框架的目录服务器,相比于传统的目录服务器,它有如下几个特点:

  1. 高 HA:支持数据持久化、支持集群。
  2. 数据一致性问题:集群中所有的客户端应该看到同一份数据,不能出现读或者写数据不一致。
  3. 数据变更主动推送:当注册中心的数据发生变更时(增加、删除、修改)需要能够及时将变化的数据通知给客户端。

2 关键功能特性设计

服务注册中心的工作原理如下:

  1. 服务提供者在启动时,根据服务发布文件中配置的服务发布信息向注册中心注册自己提供的服务。
  2. 服务消费者在启动时,根据消费者配置文件中配置的服务消费信息向注册中心订阅自己所需的服务,消费者刷新本地缓存的路由表。
  3. 注册中心返回服务提供者地址列表,如果有变更,注册中心主动推送变更数据给消费者,消费者刷新本地缓存的路由表。
  4. 服务消费者从本地缓存的服务提供者地址列表中,基于负载均衡算法选择一台服务提供者进行调用。

2.1 支持对等集群

服务注册中心需要支持对等集群,如下图,其中某一个或者多个服务注册中心进程宕机,不会导致服务注册中心集群功能不可用。

对于客户端,无论服务注册中心集群配置多少个进程,客户端只需要连接其中某一个即可(服务端之间自己进行数据同步),如下图:

2.2 提供 CRUD 接口

客户端连接服务注册中心之后,需要能够对服务注册中心的数据进行操作:

  1. 查询接口:查询系统当前发布的服务信息和订阅的消费者信息。
  2. 修改接口:修改已经发布的服务属性或者消费者属性信息,通常用于运行态的服务治理。
  3. 新增接口:发布或者订阅新的服务。
  4. 删除接口:去注册已经发布的服务,或者消费者取消订阅关系。

2.3 安全加固

服务注册中心需要进行安全加固,安全加固主要涉及两部分:

  1. 链路的安全性。
  2. 数据的安全性。

链路的安全性指的是服务注册中心对客户端连接进行安全认证,认证策略非常多,最简单的就是基于 IP地址的黑名单校验,更加复杂的有基于用户名+密码的认证,或者基于秘钥+数字证书的认证。
认证失败,则关闭链路,拒绝客户端连接。

数据的安全性主要针对服务注册中心的数据进行权限控制:

  1. 非授权客户端既不能读取也不能写入数据。
  2. 普通运维人员只能读取数据,不能修改数据。
  3. 管理员既可以读取也可以修改数据。
  4. 不同的服务目录可以设置不同的访问权限,例如消费者只能查看它所在机房的服务。
    数据安全性工作原理如下:

2.4 订阅发布机制

对于服务提供者,可以根据服务名等信息动态发布服务;对于消费者,可以根据订阅关系主动获得服务发布者的地址信息等。订阅发布机制还有一个比较重要的机制就是对变化的监听和主动推送能力:

  1. 消费者可以监听一个或者多个服务目录,当目录名称、内容发生变更时,消费者可以实时地获得变更的数据或者变更后的结果信息。
  2. 服务提供者可以发布一个或者多个服务,动态修改服务名称、服务内容等,可以主动将修改后的数据或者修改后的结果推送给所有监听此服务目录的消费者。
    订阅发布机制有如下优点:
  3. 透明化路由:服务提供者和消费者互相解耦,服务提供者未知透明,消费者不需要再硬编码服务提供者地址。
  4. 服务健康状态监测:服务注册中心可以实时监测发布服务的质量,如果服务提供者宕机,由服务注册中心实时通知消费者。
  5. 弹性伸缩能力(动态发现):应用在云端部署之后,由于 VM资源占用率过高,动态伸展出一个新的服务提供者,服务注册中心会将新增的服务提供者地址信息推送给消费者,消费者刷新本地路由表之后可以访问新的服务提供者,实现服务的弹性伸缩。

2.3 可靠性

服务注册中心需要支持对等集群,任意一台宕机后,服务都能自动切换到其它正常的服务注册中心。
如果服务注册中心全部宕机,只影响新的服务注册、已发布服务的下线。(想想为什么)
服务提供者的健康状态监测也由服务注册中心负责监测,通过长连接心跳检测服务提供者的存在,宕机则实时推送消息,实现实时故障隔离。

3 基于 ZooKeeper 的服务注册中心设计

3.1 服务订阅发布流程设计

流程设计如下图:

3.2 服务健康状态监测

基于 ZK 客户端和服务端的长连接和会话超时控制机制,来实现服务健康状态监测。

3.3 对等集群防止单点故障

ZK 使用了原子广播(恢复服务和广播服务)实现故障转移以及同步。

4 个人总结

服务注册中心要保证可靠性、安全性、可扩展性。ZK 是常用的技术。

1. 第一次聚会:2018/06/08 南京

序号 达到日期 起始时间 起始地点 车次号 婚育概要
1 陈佳慧 2018-06-08 全天 南京~南京 未婚
2 孔令洲 2018-06-08 全天 南京~南京 未婚
3 高帅星 2018-06-08 全天 南京~南京 未婚
4 蒋鑫 2018-06-08 全天 南京~南京 未婚
5 王雨木 2018-06-08 16:10~18:45 长春~南京禄口 DZ6258 未婚
6 黄菡璐 2018-06-08 20:09~22:10 义乌~南京南 G1504 未婚
7 谢飘飘 2018-06-08 20:42~22:10 杭州东~南京南 G1504 未婚
8 陈圳 2018-06-08 18:39~22:12 汉口~南京南 D2374 未婚
9 吴琦薇 2018-06-08 20:35~22:27 上海~南京南 G7286 未婚
10 李文广 2018-06-08 20:35~22:27 上海~南京南 G7286 未婚
11 孙任 未婚
12 李婷婷 未婚
13 杨璐菡 未婚

第一次聚会视频,暂定,完成后上传优酷:

2. 第二次聚会:2019/06/06 南京

序号 达到日期 起始时间 起始地点 车次号 婚育概要
1 陈佳慧 2019-06-06 全天 南京~南京 未婚
2 孔令洲 2019-06-06 全天 南京~南京 未婚
3 高帅星 2019-06-06 全天 南京~南京 未婚
4 蒋鑫 2019-06-06 全天 南京~南京 未婚
5 王雨木 2019-06-06 15:35~18:20 长春~南京禄口 深航ZH9382 未婚
6 黄菡璐 2019-06-06 18:52~21:03 义乌~南京南 G7620 未婚
7 谢飘飘 2019-06-06 17:19~18:41 杭州东~南京南 G7456 未婚
8 陈圳 2019-06-06 18:00~20:58 汉口~南京南 D2264 未婚
9 吴琦薇 ? ? ? ? 未婚
10 李文广 2019-06-06 21:41~22:57 杭州~南京南 G7628 未婚
11 孙任 未婚
12 李婷婷 未婚
13 杨璐菡 未婚