当前位置: 首页 > news >正文

业务网关的设计与实践

在过去的两年里,主要在做业务网关的开发。今年春节后选择转岗去做更偏近业务的开发。公司的业务是金融相关,一直觉得金融相关的业务是有一定门槛并且是对职业生涯有帮助的,所以趁这个机会来深入了解这块业务。

仔细回想,在做业务网关的两年里,并没有遇到真正大的技术挑战。在这两年里,真正得到成长的是作为一个基础设施项目的owner,明确系统的定位,思考并把控系统的发展方向。因此,尝试在这篇文章中把自己过去两年的工作做一些总结记录。

文章目录

  • 业务网关概述
    • 定位
    • 应用架构
  • 发展方向
    • 方寸之间做文章
    • 高性能
    • 稳定性
    • 解耦
    • 轻运维
    • 白屏化

业务网关概述

上图为简易的请求链路图,其主要展示了请求从用户端发出到到达业务应用之间的链路。在完整的系统架构图中通常会把这部分概括为网关层,细分的话会先后经历CDN节点、流量网关、业务网关。CDN的作用大家都清楚,这里就不做赘述。流量网关和业务网关则各自有自己的定位和职责。

定位

  • 流量网关:主要职责是流量的统一入口。
    流量网关具备7层负载均衡的能力。同时作为流量的入口,大多数情况下不会配置太精确的路由匹配,通常是根据前缀的模糊匹配将请求转发到不同的业务集群。在定位上,流量网关离业务比较远,迭代频率非常低。通常一家公司不同业务共用一套流量网关。

  • 业务网关:主要职责是业务集群流量的统一入口。
    在大多数情况下,业务集群都是提供rpc接口的微服务集群,由业务网关进行统一的api管理,对外提供api。所以业务网关基本的能力就是精确的api管理能力(路由能力)以及协议转换能力。
    此外,业务网关,相对流量网关而言,和业务关系紧密得多(要注意的是紧密不代表耦合)。业务集群中需要的通用的业务能力会抽象出来在业务网关实现,比如鉴权、限流、封禁等通用能力。在一些规模比较大的公司中,业务线之间差异会很大,可能会各自维护多套不同的业务网关。比如字节跳动旗下,有短视频业务、tob业务、小说业务,这些业务差异太大,很难共用一套业务网关。

应用架构

上图是业务网关的应用架构图。从架构上,我把其分为四层。

  • 最外层为接口层。这一层主要是对外实现不同的协议,以及对应的路由管理模块。目前支持的协议为http协议和ws协议。路由模块基于前缀树的数据结构开发而成,基于公司特定的业务需求在http method+path的基础上支持更细粒度的路由能力。简单来说就是,一个http method+path对应多个后端接口,根据请求参数转发。每个具体的路由对应一个handler来处理接收的请求。handler由filter和invoker组合而成。其中filter为应用可配置的中间件,invoker为调用层的实现。
  • 接口层下面是应用层,这一层是业务网关“业务”二字的主要体现。在业务网关中,以filter的形式实现了可插拔、可扩展的能力。其中包括基础能力如单机限流、accesslog、metrics和trace等,这里的metrics和trace指的都是面向请求级别的指标,不包含网关内部指标观测;还有业务能力如鉴权、业务限流、合规、封禁、防篡改等能力。在实现上基础能力为默认加载,业务能力则根据业务方配置进行加载。
  • 应用层下面是调用层,调用层的实现为invoker。调用层负责在应用层处理完成后调用业务应用接口,其职责包括业务应用的连接管理(池化实现),http到grpc的协议转换,在业务集群中实现不同的负载均衡策略,针对业务实例的状态进行熔断。invoker上为了未来的扩展,同样预留了invoker middleware的机制,可以在其上扩展能力。其中动态熔断的能力就是以invoker middleware的形式进行扩展。
  • 除此三层外还有支持层的实现。其内容包括一些抽象出来的基础能力,包括服务发现、统一灰度能力、动态日志、诊断工具等,在golang中,就是通常我们会放在pkg下面的公有能力。

发展方向

前面提到做业务网关的两年里,真正的成长是作为owner来把控系统的发展方向,下面是我对业务网关的定位和发展方向的一些思考。绝大部分的思考都是站在技术角度,出于业务网关自身以及整体架构的健康发展。只有第一点不同,业务优先级要高于技术优先级,我们作为研发,要避免技术自嗨。

方寸之间做文章

作为研发,我们都知道要在迭代时要保证系统的合理性。

在过去的工作中,我也一直以非常高的技术标准要求自己。但是最近两年我的一点感悟是,大部分情况下,我们技术只是手段而不是目的,是为了达成业务目标。所以要避免技术自嗨,一切以业务为主。

有些情况下业务需求可能会要求一些不合理的架构或者设计,那作为研发,我们只能在不合理中做到尽量的合理。也就是这一小节的标题,方寸之间做文章。当然,这不能作为降低标准的理由。在绝大部分情况下,技术的合理性和业务目标是不冲突的,合理的系统设计只会对业务有助力。

高性能

业务网关作为基础组件,性能要求本身就比较高。再加上当前公司对外提供的是高频交易服务,用户对响应时间是极其敏感的。性能要求比互联网行业的要高一个数量级。在构建高性能网关方面,我们有以下几点经验。

  • 内存缓存
    本地缓存是构建高性能网关的最重要一点。
    一个核心链路的请求在业务网关的处理中,至少要经过鉴权、封禁、合规等业务处理。而上述依赖的业务数据都是由中台的相关服务来维护,业务网关通过rpc与其交互获取业务数据。在aws中同az网络调用的rtt是0.3ms,请求redis的网络rtt是0.6ms。也就是说即使上述依赖服务server端做了redis缓存,一次rpc调用至少增加耗时0.9ms。这是完全不可接受的。
    因此在业务网关中大量采用了本地缓存的方式来缓存业务数据,业务网关和依赖服务之间通过rpc懒加载建立缓存数据+kafka通知数据更新的方式来保证数据的最终一致性。
    针对不同类型的业务数据,业务网关中还采用了不同的数据结构进行维护。涉及到技术细节,这里不做展开。
  • 控制日志量
    日志是非常影响性能。即使是异步日志,当请求量大时,也会对性能造成比较大的影响。
    在业务网关中,将日志分为access.log和app.log。access.log为请求级别,通过byte拼接而成。app.log中几乎没有额外日志,只会打印出错时的err日志。通常,access.log和具体的err日志足以应对绝大多数情况。
    除此之外,我还开发了动态日志的功能,可以根据配置动态地把某些请求的请求和响应打印出来。动态日志的能力支持多维度的组合,包括path、来源ip、服务等维度,可以严格地把动态日志的量控制在最小范围内。配合动态日志,虽然日志量控制严格,但是目前尚未日志出现难以满足要求的情况。
  • 序列化优化
    在大多数应用中,除io外,最耗时的基本都是序列化和反序列化。在网关中,因为涉及到协议转换,序列化和反序列化的占比相对更高。在业务网关序列化优化方面,主要有两个方向。
  1. 底层序列化优化。这方面的优化通常是选择性能更高的序列化库,或者是优化序列化的过程。这部分内容小公司可能很难有人力去自研。目前grpc的泛化协议转换普遍是通过dynamic message的方式进行的,整个过程对cpu和内存都有额外的损耗。我对此有些想法,但确实没有精力投入开发
  2. 业务层的序列化优化。在响应方面,业务网关对外提供了不同的组装方式,而之前针对不同的组装方式的序列化都是一视同仁,这会带来不小的浪费。在发现这个问题后,我将响应的序列化提升至更上层(invoker层迁移到filter中),针对不同的组装方分别进行处理,大大提升了效率,并提升了部分接口的响应时间。
    针对序列化这块,这里不展开,后面有时间再补充。

稳定性

作为基础组件和流量入口,业务网关的稳定性要求是非常高的。

我们都知道,计算+数据=服务。对应到生产环境的服务,计算可以认为是我们的进程,最常见的进程类型就是处理请求并给出响应;数据则是来自各种依赖,广义的存储比如rds、redis等,消息队列,配置中心比如nacos、etcd等,还有其他的服务。

稳定性方面,进程+依赖当然是一个整体,互相影响。但从治理的角度出发,我们不妨把进程和依赖分开来看。

普遍来看,我认为,进程的稳定性治理要远简单于依赖的稳定性治理。进程中只是接受请求处理并给出响应,消耗的资源仅有cpu、内存、网卡,因此只要进程有足够的资源,就不会有问题。一方面,我们有自适应限流等算法保证资源紧张时拒绝请求;另一方面,有hpa等自动扩容策略。另外,当进程出现问题时,通过重启也能快速地进行恢复(绝大部分都是无状态服务)。

但依赖的治理则要复杂的多,比如rds有慢查询,redis有大key、热key、缓存击穿,依赖服务不可用导致服务雪崩等。进程和各种依赖之间还有一层网络通信,这都让依赖服务的稳定性治理复杂很多。不同的依赖都需要根据具体的场景进行分析。但是还是有一些通用的方法论:1. 区分强依赖和弱依赖;2. 熔断降级。

关于自适应限流和动态熔断等方法,可以见服务治理小记。

解耦

业务网关的定位就是作为业务线的流量入口,并提供通用的业务能力。业务网关的价值很大一部分也体现在通用业务能力上。在实际的迭代过程中,通用的业务能力其实没有明确的界限。业务线会提各种需求过来,如何保证能解决业务问题,又不和业务系统耦合,当然要case by case的分析。但我也总结出了几条原则来帮助其他同学。

  • 接口级别的控制能力
    业务网关对外提供能力的最小粒度为接口级别。如果业务线提出的需求需要根据请求的参数做不同的控制,那这部分功能很大可能和业务耦合得比较深并且不具备通用性。
  • SDK封装业务能力
    在业务网关的早期,业务能力的迭代,采用了业务方提供prd网关研发开发的模式。在这种模式下,网关研发需要比较深入地理解业务需求,这带来了比较高的理解和沟通成本。同时,这些业务需求后续迭代的时候都需要业务方和网关研发比较紧密地协作。虽然是通用业务能力,但是却带来了组织上的耦合。
    针对这个问题,我提供的解决思路是让业务方研发提供sdk来提供业务能力,业务网关仅仅是提供一个占位符。比如在去年的一个安全防篡改的需求中,需求对请求进行解密,响应进行加密。这个需求中,就由安全同学提供sdk提供加解密的能力。业务网关只是在此之上封装能力并提供给各业务方。
    当然,有些业务需求并没有所谓的业务方研发,通常就需要网关同学在维护对应的业务sdk。

轻运维

在理想的情况下,我的期望是业务网关的基本的能力迭代的足够完善后,就将其托管给业务开发和业务运维。由于业务网关以filter的形式扩展业务能力,所以业务开发可以在不了解整体架构的情况通过filter机制自行迭代业务能力。作为业务网关的研发,只提供技术支持排查框架的问题及优化。这种情况下,我们的研发在运维、维护、oncall上的时间成本占比就会很少。可以有精力去开发更多的产品。

但以公司当前的情况来看,该想法至少一两年内难以实现。退而求其次,我通过以下的手段来控制运维和维护的成本。

  • 控制集群数量
    出于隔离的目的,业务网关应该为每个业务线单独隔离部署。但是随着集群数量的增多,运维成本会随之成倍的提高。因此,对于核心的、请求量大的业务,会独立部署业务网关集群;对于边缘业务,会多个业务线共用集群。
  • 提高业务自助排障能力
    提高业务的自助排障能力可以大幅地减少维护成本。通常来说,这部分都离不开文档。但是我个人认为,应该通过设计来减少文档在自助排障能力中的比重,而不是无脑地让业务同学来查看繁重的文档。我们在系统层面做了一下设计。
  1. 日志隔离
    在业务网关中,我们按照属性将日志分为access.log和app.log。
    access.log中有除具体请求参数和响应结果外的所有数据。绝大部分情况下,业务方只看access.log就足够。acces.log中最常用的字段就是错误码和调用时间。一者表示请求的结果,一者表示请求的耗时。错误码在下面会单独介绍。
    app.log中包含除access.log外的所有日志,没有再按日志级别进行分组,这是因为app.log的量很少。在性能和稳定性中都提到过,app.log中的日志是严格控制的,只有报错的详细信息的err日志。通过动态日志的方式来进行弥补。
  2. 错误码拆分
    在最开始的设计中,错误码分为业务错误码和系统错误码。
    其中业务错误码为5位,常见的例如鉴权失败、被封禁等等都属于业务错误码。业务错误码是比较好识别的。
    系统错误码为4位,最开始只有5000和6000。5000表示内部错误,只有在序列化失败和内部逻辑错误等场景,基本不出现。6000表示依赖错误,调用上游和依赖服务(用户服务、封禁服务等)的非业务层报错,比如超时、连接失败等都会用6000表示。6000的错误码是比较常见的,尤其是开发、测试环境的oncall中基本都是因为6000导致。针对此问题,我对系统错误码进行了拆分,比如60xx表示上游服务的系统错误、61xx表示用户服务的系统错误。这样配合文档,就极大地降低了运维成本。

白屏化

白屏化是基础组件迭代的一个重要方向。
在早期,业务网关只对外提供接口来开放操作能力,这对使用方的同学来说其实是有相当的门槛的。在迭代到快两年的时候,开始开发管理后台的项目,将业务网关控制面的能力通过页面的方式开放出去。其实大部分基础组件都会经历这个过程。比如常见的kafka、redis、etcd等中间件,最开始的时候只有命令行cli工具可用,随着公司发展,一般都会迭代出相应的控制面。
因为业务网关本身不是个复杂的组件,管理后台中也没有比较难理解的逻辑,所以管理后台的整个迭代过程还是比较简单的。这里就不赘述。

以上是我做业务网关的一些总结。时间匆忙,后续会慢慢完善。
也欢迎大家来评论交流。


如果觉得本文对您有帮助,可以请博主喝杯咖啡~

相关文章:

  • 配置 施耐德 modbusTCP 分布式IO子站 RPA0100
  • HTML中js简单实现石头剪刀布游戏
  • [技术闲聊]我对电路设计的理解(二)-突飞猛进的第一年
  • 『python爬虫』巨量http代理使用 每天白嫖1000ip(保姆级图文)
  • 接口测试用例设计
  • 前端路径问题总结
  • 物联网实战--入门篇之(六)嵌入式-WIFI驱动(ESP8266)
  • 2024-04-04 问AI: 在深度学习中,微调是什么?
  • 大数据实验三-HBase编程实践
  • intellij idea 使用git ,快速合并冲突
  • 实现 select 中嵌套 tree 外加搜索
  • ROS 2边学边练(12)-- 创建一个工作空间
  • 提高空调压缩机能效的通用方法
  • 957: 逆置单链表
  • php获取拼多多详情api接口、商品主图
  • 【Leetcode】101. 对称二叉树
  • Babel配置的不完全指南
  • Java 23种设计模式 之单例模式 7种实现方式
  • JavaScript类型识别
  • JAVA之继承和多态
  • JDK 6和JDK 7中的substring()方法
  • Js实现点击查看全文(类似今日头条、知乎日报效果)
  • JS学习笔记——闭包
  • Laravel Telescope:优雅的应用调试工具
  • magento 货币换算
  • Quartz实现数据同步 | 从0开始构建SpringCloud微服务(3)
  • Vim Clutch | 面向脚踏板编程……
  • 高程读书笔记 第六章 面向对象程序设计
  • 聊聊springcloud的EurekaClientAutoConfiguration
  • 模仿 Go Sort 排序接口实现的自定义排序
  • 排序算法学习笔记
  • 区块链分支循环
  • 融云开发漫谈:你是否了解Go语言并发编程的第一要义?
  • 使用Swoole加速Laravel(正式环境中)
  • 手写双向链表LinkedList的几个常用功能
  • 详解移动APP与web APP的区别
  • 原生 js 实现移动端 Touch 滑动反弹
  • 再谈express与koa的对比
  • Spark2.4.0源码分析之WorldCount 默认shuffling并行度为200(九) ...
  • 树莓派用上kodexplorer也能玩成私有网盘
  • ​LeetCode解法汇总2583. 二叉树中的第 K 大层和
  • ​软考-高级-信息系统项目管理师教程 第四版【第23章-组织通用管理-思维导图】​
  • # include “ “ 和 # include < >两者的区别
  • #13 yum、编译安装与sed命令的使用
  • #单片机(TB6600驱动42步进电机)
  • #我与Java虚拟机的故事#连载12:一本书带我深入Java领域
  • $(document).ready(function(){}), $().ready(function(){})和$(function(){})三者区别
  • (1)安装hadoop之虚拟机准备(配置IP与主机名)
  • (6)设计一个TimeMap
  • (C语言)fgets与fputs函数详解
  • (Demo分享)利用原生JavaScript-随机数-实现做一个烟花案例
  • (LeetCode) T14. Longest Common Prefix
  • (附源码)php新闻发布平台 毕业设计 141646
  • (三)c52学习之旅-点亮LED灯
  • (图)IntelliTrace Tools 跟踪云端程序