写点什么

亿级流量架构演进实战 | 从零构建亿级流量 API 网关 02

用户头像
然行
关注
发布于: 刚刚

这不是一个讲概念的专栏,而且我也不擅长讲概念,每一篇文章都是一个故事,我希望你可以通过这些故事了解我当时在实际工作中遇到问题和背后的思考,架构设计是种经验,我有幸参与到多个亿级系统的架构设计中,有所收获的同时也希望把这些收获分享与大家。


承接上篇,统一了接口之后并没有彻底改变被客户端碾着走的局面,因为还有一个根本的点没有被解决,就是网关对上游服务的适配问题,说白了就是每当上游有一个新的 API 要发布,网关都需要进行开发适配,我们曾经出过一个 API 标准接入的解决方案去推动上游去改造,不过遇到了很大的阻力。这个痛点直到网关实现了 API 的服务泛化调用之后才有所突破,功能一经上线,API 发布在网关就不需要再适配一行代码,完全解耦了网关与平台的业务逻辑,使网关的效能得到释放。不过,内部协议直接被转化成外部协议使得 API 在定义和格式上变得晦涩难懂和似乎不受控制,而且上游 API 的变更让网关很难处理兼容性问题,这就是所谓的有得必有失吧。再后来随着开放平台、共建生态迎来了大潮,这时已经是 2015 年了,我们又反客为主迅速推动上游进行 API 标准化的接入和改造,这只能说之前网关更关注 API 接入的效率,后来更关注 API 接入的质量。


1。泛化调用

泛化调用在当时起到了非常重要的作用,虽然现在已经很少在网关直接粗暴的提供泛化调用的 API,但是泛化调用在其它地方有了更广泛的应用,比如 API 测试等等。

下面我们就结合着泛化调用来说下网关服务调用的那点事,回顾上文我画的一张 API 网关的架构示意图。首先,泛化调用是网关服务调用组件提供的一种服务调用方式,而整个服务调用概括的讲主要有路由寻址、协议转换、分发调度三个步骤。

1.1 路由寻址

一个 API 请求在调用上游服务之前,首先要通过请求方法名进行上游服务的路由寻址,寻址包括上游服务端的接口名、方法以及分组。具体来讲,网关会通过请求方法名在内存路由表中去寻找对应的服务实例地址,如果没有找到,就会去 API 元数据库中读取配置并完成服务的实例化。这是因为网关的泛化调用是一种基于配置的实现方式,所有 API 的方法和参数都以配置的方式存储在元数据库中。所以,这类 API 服务在网关是一种动态实例化的方式,它不是在服务端启动时就初始化好的服务,而是一种懒加载的方式。

在 API 服务初始化的时候我曾有过这样的设计考量,API 服务是否可以改成配置发布加载的方式?之所以没有这么做,主要因为 API 实在是太多了,有很多 API 都没有被调用过,另外就是 1~2 次调用 API 只会被初始化在少量的 API 网关上,对整体而言也不会有太多的资源占用。

1.2 协议转换

在获取到对应的服务地址之后,就需要对请求参数进行协议的转换和解析。当时在没有泛化调用之前,网关依赖上游提供二方包,并需要依据二方包的接口定义,才能完成请求参数的解析与协议的转换。而在有了泛化调用之后,网关可以不依赖于上游服务提供的二方包,就可以进行协议转换了,这是因为泛化调度在实现上采用了反射和动态代理的方式。

1.3 分发调度

泛化调用不仅可以去掉对上游二方包的依赖,网关还在泛化调用的基础上实现了一套通用协议的适配模型,基于代理模式实现对上游不同服务的分发调用,解决了之前每个服务在网关都需要开发一套适配逻辑的实现。

1.4 配置中心

在网关实现了统一接口和泛化调用之后,服务端似乎进入开挂的时代,主要是彻底释放了服务端的研发效能,需要哪个 API 客户端去配置一下就好了,不过好景不长,此时网关已经进入了深水区,出现的问题就比较难解决了。接下来面对的就是 API 元数据的即时更新问题,由于泛化调用是依赖于配置的,所以当配置变更后,就需要对网关的服务实例进行重新初始化,所以为了解决这个问题,一个网关 API 元数据配置中心的系统就这样诞生了,以下简称为配置中心。

配置中心的演进主要有 3 个版本,第一个版本是定时检查数据库变更配置的处理方式,第二个版本是消息广播变更配置的处理方式,第三种是基于 Zookeeper 监听配置的处理方式。下面我们就逐一来看下这三个不同版本的实现方式。

v1 定时检查数据库变更配置

其实无论哪个版本都是采用数据库作为元数据的持久化存储的,不同之处主要是在触发网关进行服务重载的机制不同。

由于泛化调用是一种懒加载的实现方式,所以只有当一个请求需要获取元数据信息时,网关才会去查询元数据库,并将获取到的配置信息缓存到网关的内存中,同时进行服务实例的初始化。而当数据发生变更时,网关是如何感知到配置变更的呢?所以网关采用的是线程轮训的方式,通过对比数据进行判断,如果对比数据不一致,就进行服务实例的重载,如果一致,就不做任何处理。不过,这个方案有个硬伤,就是即时性的问题,由于线程轮训的间隔不能过快,以防止对数据库造成不必要的压力,而且,元数据库变更也不会很频繁,过于频繁的轮训也是一种资源的浪费,所以,我们设置的时间间隔大概是 10 分钟,当然,网关只有一个轮询线程,不是每个 API 都有一个。

除了线程轮训的方式,我也思考过有没有别的方式,比如使用缓存定时过期的方式,缓存过期了就去数据库里查询一次,Guava 的 LocalCache 就可以实现多种本地缓存策略 ,不过这种方式比较适合在防止热点数据穿透缓存的场景里,在网关里缓存过期了是要与服务重载相关联的,所以什么场景下网关去检查缓存是否过期了,这之间并没有建立起直接的关系,总不能每调用一次接口就去检查一下缓存吧。而且缓存穿透本身也是有风险的,尤其是冷数据加载,可能直接将下层的数据库打爆,这种方式造成的线上问题也是屡见不鲜。

v2 消息广播变更配置

后来我们又上线了一版消息广播变更配置的版本,改造点是网关的配置中心客户端里将线程轮训的方式替换消费 MQ 的方式,MQ 是在配置中心 OPS 里变更配置时进行生产,以广播的方式发送给网关所有实例。

v3 基于 Zookeeper 监听配置

再后来终于迎来主流版本 —— 基于 ZooKeeper 构建配置中心的实现方式,ZooKeeper 是一个分布式的协同系统,它有很多优势,比如,基于树型的存储方式、分布式的部署架构、Leader 选举机制、基于长连接的双向通道等,我还是比较推崇它的。

基于 ZooKeeper 的 Watcher 特性,我们把网关的 API 配置信息存到 ZooKeeper 节点里,在网关的配置中心客户端加载到 API 服务后,就去订阅 Zookeeper 中对应节点的数据变更事件,当 ZooKeeper 数据节点变更后,ZooKeeper 就可以以事件驱动的方式通知到网关实例,从而进行配置变更和服务重载。

如何解决配置中心的一些小问题?

其实在整个演变过程中,除了技术选型有了大的调整外,在细节方面我们也在不断优化,与线上的各种小问题进行坚持不懈的斗争。

小问题 1:首次获取配置失败

为什么强调一下首次,因为重启服务器就会造成内存数据的丢失,网关就需要去配置中心重新获取配置,这就是首次获取,不过这时如果获取配置失败了,网关就尴尬了。所以为了应对异常情况下可能无法获取到配置信息的情况,最开始的解决办法是接口调用时传入一个默认值,在异常情况下会返回默认值,简单又有效。不过这种方式在某些场景下,又往往会产生一些意想不到是小事故,我说的就是那种有新老流程需要切流而设置的开关场景,默认配置设置为 false 走老流程,可是在逐步切流完成之后,这个配置开关和老流程并应该被遗弃,而是由于各种原因活了下来,随后又随着各种演进,老流程逐渐变成了僵尸代码,然后某一次重启一个网络抖动就把系统的僵尸炸醒了,紧接着报警就想起了,而这种情况,我还真的遇到不止一次。

所以,后来在配置中心客户端里做了一点过度的设计,为啥说是过度,因为废弃的代码还是要删掉的,不能指望着别人去保障你的业务。所谓的过度设计其实只是增加了通过本地 Properties 文件对配置数据进行落地存储的容灾策略,其策略是服务器重启后,如果无法从 ZooKeeper 获取配置后,就从本地 Properties 文件获取上一次的配置数据进行加载,如果也无法从本地 Properties 文件获取配置,才返回设置的默认值。

这里再说下在 v1 基于数据库通过线程轮询的实现方式里,网络抖动导致大量回源查询,如果查询没有设好超时时间和重试次数,就可能会产生大量的异常日志和线程阻塞,严重的还会把服务器拖死了。

小问题 2:更新配置失败了

这里的更新失败不是指数据库更新失败,而是指发送 MQ 或写 Zookeeper 失败,这里提醒一下,千万不要把这步操作放到数据库事务里,否则一个网络抖动你的数据库可能就死了,这样写的我看到过好几例。大多数场景下配置中心都是要一定先数据库成功后再进行别的操作,这里我们取了个巧,就是先发送 MQ 或写 Zookeeper 然后再保存数据库,因为这里的每个操作都是幂等的,而且数据保存失败了也容易识别出来,所以即使数据库保存失败进行了重试,对 MQ 和 Zookeeper 也不会有任何影响。

之前曾考虑在保存操作完成后在后端启动一个线程,对两个数据源的数据进行校对,如果不一致就进行订正,订正成功就结束线程,订正失败的话,就短暂休眠,然后继续订正。对比来看,这种实现方式就已经有些复杂了,更别说分布式事务了,所以一个先后策略的调整就可以解决的问题,不一定非要把系统做的很复杂,这在我后来的架构修炼之路上,遇到过很多这样的情况,大道至简方是王道。

再后来随着微服务框架的逐步完善和成熟,配置中心已经有越来越的产品被推出,比如 spring cloud config、diamond、apollo、disconf 等等,而不必要自己去开发了。


3。总结

言而总之,本篇文章重点讲述了流量调度的配置中心、泛化调用。下篇文章,我将继续介绍架构演进构建 TCP 长连接网关。如果你觉得有收获,欢迎你把今天的内容分享给更多的朋友。


4。扩展阅读

故事 1:从零构建亿级流量 API 网关

01 | API网关:统一接入、分层架构、高可用架构

02 | 流量调度:配置中心、泛化调用

故事 2:架构演进构建 TCP 长连接网关

03 | TCP 网关:Netty 框架、Protobuf 格式、业务线程池

04 | TCP 长连接:心跳、Session 管理、断线重连

故事 3:架构演进重构消息 PUSH 系统

05 | 消息 PUSH:消息推送、消息送达率、APNs

故事 4:从焦油坑爬出来的交易系统

06 | 交易平台:订单管道、订单状态机、服务编排、任务引擎

07 | 微服务化:服务治理、领域设计

故事 5:烦人的焦油开始到处都是

08 | 新老系统:业务整合、数据融合、系统迁移

09 | 高可用架构:隔离部署、系统监控与日志、可灰度、可降级

故事 6:稳定性架构与大促保障

10 | 大道至简:系统复杂度、三明治架构

11 | 大促保障:自动化测试、故障演练、性能压测

发布于: 刚刚阅读数: 2
用户头像

然行

关注

还未添加个人签名 2018.04.26 加入

还未添加个人简介

评论

发布
暂无评论
亿级流量架构演进实战 | 从零构建亿级流量API网关 02