业务网关的设计与实践

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

仔细回想,在做业务网关的两年里,并没有遇到真正大的技术挑战。在这两年里,真正得到成长的是作为一个基础设施项目的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工具可用,随着公司发展,一般都会迭代出相应的控制面。
因为业务网关本身不是个复杂的组件,管理后台中也没有比较难理解的逻辑,所以管理后台的整个迭代过程还是比较简单的。这里就不赘述。

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


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/516784.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【数据处理包Pandas】数据载入与预处理

目录 一、数据载入二、数据清洗(一)Pandas中缺失值的表示(二)与缺失值判断和处理相关的方法 三、连续特征离散化四、哑变量处理 准备工作 导入 NumPy 库和 Pandas 库。 import numpy as np import pandas as pd一、数据载入 对…

开机自启动

对win10,给一种开机自启动的设置方法: 1. winr 打开 2. 输入shell:startup打开 开始\程序\启动 3. 把想要自启动的应用的快捷方式放在这里即可 亲测有用

第十一届蓝桥杯物联网试题(省赛)

对于通信方面,还是终端A、B都保持接收状态,当要发送的数组不为空再发送数据,发送完后立即清除,接收数据的数组不为空则处理,处理完后立即清除,分工明确 继电器不亮一般可能是电压不够 将数据加空格再加\r…

RPA自动化小红书自动化写文以及发文!

1、视频演示 RPA自动化小红书自动写作发文 2、核心功能点 采集笔记:采集小红书上点赞量大于1000的爆款笔记 下载素材:下载爆款笔记的主图 爆款改写:根据爆款笔记的标题仿写新的标题以及新的文案 自动发布:将爆款笔记发布到小红…

Oracle RAC One Node,双胞胎变独生子?

📢📢📢📣📣📣 哈喽!大家好,我是【IT邦德】,江湖人称jeames007,10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】!😜&am…

LeetCode刷题实战2:两数相加

在日常我们学习数据结构和算法的知识,一定不能只停留在看书、看视频层面,一定要自己多练习,纸上得来终觉浅,绝知此事要躬行。 题目内容 给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存…

Vue3:组件间通信-$attrs的使用

一、情景说明 我们之前学习了通过props实现&#xff0c;父给子传数据 那么&#xff0c;如果&#xff0c;父组件给子组件传递多个数据&#xff0c;但是&#xff0c;子组件只用props声明了一个数据 其他数据去哪里了呢&#xff1f; 二、案例 1、父组件 <Child :a"a&…

Linux 关闭防火墙命令(新手)

关闭防火墙 查看防火墙状态 systemctl status firewalld.service 临时关闭防火墙&#xff08;重启失效&#xff09; systemctl stop firewalld.service 永久关闭防火墙 systemctl disable firewalld.servicesudo systemctl enable firewalld&#xff0c;这种方式输入命令…

AcWing 731. 毕业旅行问题(每日一题)

原题链接&#xff1a;731. 毕业旅行问题 - AcWing题库 此题难度较大&#xff0c;是2019年字节跳动校招题&#xff0c;里面涉及位运算与状态压缩DP&#xff0c;不会的可以去学习&#xff0c;此题根据个人量力而行。 建议看一下y总的讲解&#xff1a;AcWing 731. 毕业旅行问题&…

【Unity 实用工具篇】| Unity中 实现背景模糊效果,简单易用

前言【Unity 实用工具篇】| Unity 实现背景模糊效果,简单易用一、实现背景模糊效果1.1 介绍1.2 效果展示1.3 使用说明及下载二、插件资源简单介绍2.1 导入下载好的资源2.2 功能介绍2.2.1 捕获特效2.2.2 高级选项

长连接详解

一分钟了解长连接 、短连接、心跳机制与断线重连 - 知乎 (zhihu.com) websocket 实现长连接原理_websocket 是长连接吗-CSDN博客

RedisDesktopManager 安装

简介&#xff1a;安装redis可视化工具 一、下载压缩包 Redis 可视化工具 链接&#xff1a;https://pan.baidu.com/s/1P2oZx9UpQbXDsxJ3GPUeOQ 提取码&#xff1a;6rft Redis 命令窗口版本 链接&#xff1a;https://pan.baidu.com/s/1mIuxCEWwD__aoqp1Cx8MFQ 提取码&#xf…

Linux 文件相关命令

一、查看文件命令 1&#xff09;浏览文件less 默认查看文件的前 10 行。 less /etc/services ##功能说明&#xff1a; #1.默认打开首屏内容 #2.按【回车】按行访问 #3.按【空格】按屏访问 #4.【从上向下】搜索用/111,搜索包含111的内容&#xff0c;此时按n继续向下搜&#x…

力扣刷题 45.跳跃游戏 II

目录 题干 解题思路 题干 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处: 0 < j < nums[i] i j < n…

稀碎从零算法笔记Day39-LeetCode:有向无环图中一个节点的所有祖先

感觉写的越来越难了hhh&#xff0c;一晚上只研究明白了2道题 题型&#xff1a;拓扑排序、BFS、图 链接&#xff1a;2192. 有向无环图中一个节点的所有祖先 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述&#xff08;红字为笔者添加&#xff0…

FPGA实现Canny算法(Verilog)

在边缘检测算法里面Sobel是比较简单的一个算法&#xff0c;但是其检测出来的边缘往往是比较粗的&#xff0c;效果不是很好&#xff0c;因为我们最理想的边缘肯定就是一个宽度为1的细线。 Canny算法在此基础上进行了改进&#xff0c;通过使用边缘的梯度信息进行非最大值抑制(NM…

基于单片机的数字万用表设计

**单片机设计介绍&#xff0c;基于单片机的数字万用表设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的数字万用表设计概要是关于使用单片机技术来实现数字万用表功能的一种设计方案。下面将详细概述该设计的各个…

C++ | Leetcode C++题解之第8题字符串转换整数atoi

题目&#xff1a; 题解&#xff1a; class Automaton {string state "start";unordered_map<string, vector<string>> table {{"start", {"start", "signed", "in_number", "end"}},{"signed…

Linux 多线程与线程控制(程序均有详细注释)

多线程与线程控制 线程的基本概念线程的特点页表多线程 线程控制线程的创建线程传参线程id资源回收---线程等待线程id和LWP 封装一个线程库线程互斥和线程同步线程互斥基本原理线程安全VS线程不安全锁的诞生可重入VS线程安全 死锁死锁的定义 线程同步条件变量接口 生成消费者模…

使用阿里云试用Elasticsearch学习:1.2 基础入门——数据输入和输出

什么是文档? 在大多数应用中&#xff0c;多数实体或对象可以被序列化为包含键值对的 JSON 对象。 一个 键 可以是一个字段或字段的名称&#xff0c;一个 值 可以是一个字符串&#xff0c;一个数字&#xff0c;一个布尔值&#xff0c; 另一个对象&#xff0c;一些数组值&#…