Appearance
Python 原生类微服务开发
警告
最终这个项目还是改为重新开发了,最主要的问题还是在于一开始的选型存在问题,使得技术存在积重难返的问题。为此我考虑重新拆分服务器部分,改为实现 asgi 兼容的服务器和 asgi 兼容的应用框架两部分组成。这样能更方便的复用 uvicorn 等现成轮子
MetaService, NativeMicroBridge
其实比起意义不是很明确的 MetaService,我更喜欢 NativeMicroBridge 这个突出了原生和类似微服务组织方式但又与之不同的名称。就是这个缩写不太雅观(当然也有我自己的恶趣味在里面)
内网有一堆服务,有定时、同步或异步的调用方式,目前的调用方式都是各玩各的。想要设计一个 python 类微服务,能够统一实现调用入口、web 服务器、控制中心的功能
而且由于需要考虑面对公众高并发服务场景和内网大量数据传输需求。因此定一个简单的实现期望
- [x] web 服务器面对简单请求能实现单进程 1k qps 的处理能力(框架的性能不应当成为瓶颈),多核拓展能叠到 c10k(如何堆叠也是一个问题)
- [x] web 服务器面对大文件能实现单进程超过 10 gbps 的传输速度(内网实际上是 40g 直连,降低点要求)
- [x] 使用 python 原生组件来实现
- 思考
- [x] 测试口径是什么?按照 socket 通讯发送量?还是按照完整调用流程进行测试:肯定是完整流程
- [x] 如何实现控制中心的多核拓展功能?主进程只进行转发,处理交由子进程处理?:使用 fork 方式来完成扩展
- [x] 如何实现按需多核拓展,这个到底由谁进行伸缩扩容?
- [x] 需要依赖什么组件?请求的 event、包含配置相关的 context、注入依赖(status,log,socket)的 handler 组件?
- [x] 管理模块如何处理热更新和缩扩容?在切换后如何关闭原有程序(比如说 docker 的 always 属性如何关闭)
- [x] 如何实现针对响应包的 io 多路复用。网关提供一个自封协议?使用魔改版 http 协议
- [x] 如何处理限流和熔断。网关提供提供一个入口?:似乎必须由网关进行处理
- [x] 多个客户端注册到一个路径后,该如何实现负载均衡:考虑响应式处理
- [x] 管理模块和边车都要实现一个服务注册和转发确认平台?:对于内部程序和外部 socket 链接的处理都需要实现
- [x] 如何针对每一节点接收指标:使用类似资源表达式的方式(:source)
- [x] 如何实现多路复用转同步、可接收状态通知机制:asyncio。event 非常适合
- [x] 如何实现对接收到的报文做精准转发,而非将两次接受到的不同报文转发给不同虚拟端口:通过 socket_id 方式
- [x] HTTP 的链接生命周期由虚拟端口还是链接直接管理?虚拟接口和客户端链接该如何处理:由链接直接管理
简单的技术选型测试
用来对性能目标进行分析。测试机环境是一个跑在 5900x 上的 win11 虚拟机
- 列表 append+pop,运行一亿次的 qps 为 1000w。作为消息队列模型,似乎足够快了。使用队列模型则只有 30W,pop0 的复杂度大数量下完全不可用
- 集合 append+pop,运行一亿次的 qps 为 600w。作为消息队列模型,似乎也不算慢
- deque append+leftpop,运行一亿次的 qps 为 800w。作为消息队列模型,似乎也不算慢
- 子进程同步 socket 通讯,ping 完 pong,大概 qps 为 1W2,linux 下为 2W7
- 子进程异步 socket 通讯,ping 完 pong。单进程 qps 只有约 5000,多进程 qos 上限为 1w2。linux 下为 2W4。测试似乎是异步客户端的问题?
- 子进程协议 socket 通讯,ping 完 pong。单进程 qps 只有约 7000,多进程 qos 上限为 1w6。linux 下为 4W,似乎对大小不是特别敏感。兼容很麻烦
- 简单异步 http 客户端的 qps 似乎也就 400,需要考虑如何加速处理
- 使用 split,不 encode,win 下每秒能处理约 22W 的请求头。按照指针解析的方式速度也不快,原生的 indexof 速度可以到 484W,切分 header 220W 的速度
- asyncio 的异步库创建并通知的速度大概是 100W qps,但是设置通知的 qps 有 1000W
整体的查询流程大致为客户端访问网关代理,收到客户端的请求后根据子网关的注册记录将请求转发给子网关,子网关在由工作函数处理后将响应返回
这个框架需要能实现什么功能
开发目前计划了三个阶段
- 实现一个 http 函数解析服务,完成基础的 http 请求的解析
- 实现一个异步 http 请求工具,为内外部交换打通基础
- 实现主网关+子网关的框架原型,完成最基础的框架开发工作,后续逐步完善
- http 服务器原型:wrk 压测 c100k qps
- 支持 http 报文解析,支持流式解析仅单行请求头和正常带 body 的请求头
- 支持制造返回报文
- 支持路由注册框架
- 异步 socket 通讯模块:单进程跨 ovs 的 qps 接近 3K
- 能够异步等待,支持复用连接
- 能够处理简单的请求头
- 如何实现在复用和服务端链接的情况下完成请求
- 框架原型(如何 c10k?至少 c1k)
- 性能测试 在 t8q8 的情况下,目前来看传输能力差强人意
- linux 下本地环回直接请求框架本地能力为 25gbps,c40k。本地转发 2.5gbps,c10k
- 跨 ovs 请求直接请求框架本地能力为 3.5gbps,c10k。本地转发 2gbps,c3k
- 如果不采用重新注册式负载均衡,直接调用第一个可用链接,则转发效率能提升到原本的 300%
- 路由模块
- 支持根据 uri 将报文传输给注册 socket、本地侦听 socket 或者是本地程序(服务端和 agent 端都需要)
- 支持多个 agent 注册一个地址,采用接受响应后决定是否注册的机制来实现负载均衡(如何 qos,如何处理注册机制,如何负载均衡)
- 服务端:兼容支持解析完毕请求头后流式传输数据 body 和 websocket 等服务
- 客户端:只考虑对类标准 http 报文的支持
- 支持心跳、指标、注册、关闭等内部报文结构的处理,支持对 :source 之类的伪协议的处理(无 sid 时处理?)
- 链接管理
- 可以管理链接和传输方式,对外部请求报文的请求头做解析并进行链接控制
- 将外部通讯报文转译成内部通讯报文和将内部通讯报文转译成外部通讯报文并发送(NAT?)
- 支持链接的超时和重试等机制,支持记录链接的指标(传输字节数、链接时长、平均响应时间)
- 附加功能
- 支持单元测试
- 支持 cli 管理
- 性能测试 在 t8q8 的情况下,目前来看传输能力差强人意
框架需要能够同时支持通过子网关走网络注册以及网关本地启动一套完整服务的方式(方便调试程序)?
- 具体调度流程是否需要可视化展示?
- 本地该启动哪些服务、是否该按组启动服务?并且支持单进程内互相通讯?
- 定义
- 框架:期望开发的微服务框架,包含了用于转发请求的网关和程序
- 网关:将外部的 http 请求转换为内部调用的报文的 web 服务器
- 程序:框架承载的应用,类似 serverless 的函数
- 边车:这里指对程序提供一个统一的接入方式并将程序返回转换为响应报文的模块,考虑使用网关来替代,每一个边车对应一个子网关,通过向上级函数注册的方式调用
基础要求
- 传输代理
- 支持正反向模式,可选暴露端口和不暴露端口
- http:通过 http 请求方式调用,提供 api 面对外部的简单访问。提供一个面对外部服务的简单调用方式。需要考虑支持 SSE
- (未实现)web-socket:通过 websocket 方式调用。提供异步且类似 socket 的调用方式
- 依赖注入:将依赖的上下文、日志和指标模块等传递给程序。简化程序开发和管理难度
网关模块
网关模块期望以一个独立且兼容 web 服务器的形式运行,通过直接向管理模块发起连接或暴露一个端口并提供给管理模块的方式托管一个应用。实现 serverless 的功能,应用只需要考虑处理逻辑,不需要考虑如何处理响应报文
期望给程序一个统一的入口和出口,对外能兼容多种接入方式,同时实现统一的日志和指标暴露方式
管理模块
实现注册和配置中心的功能
管理模块需要实现接入独立的子网关或能够本地托管所有的程序模块。同时需要考虑对子网关的异常退出和扩容机制
- 注册:提供一个记录表,保存所有注册的应用的信息。能够实现依据请求的 uri 进行转发
- 配置:将所需要的变量(如生产/测试数据库信息)下发给程序
- 负载均衡:根据什么条件做负载均衡?如何处理响应时间不一的情况,怎么保证负载可用?
- 弹性伸缩:能够实现根据负载对程序扩容?该如何实现?扩容和缩容命令注册?
- 动态更新:能够实现自动化热更新托管的程序(如何实现)
- 链路追踪:分配一个追踪 id 给请求,对注册的程序进行分类命名
触发器
能够实现类似消息传递的功能,有如下几种方式
- 代理转发:作为一个 api 端点,将请求转发到对应的后台服务器中
- 消息队列:生产者向队列中写入数据,消费者异步从队列中取数据
- cron:根据预先定义的时间触发
需要那些模块
指定需要开发那些模块
通用能力
所有模块都需要的共同能力,这部分的设计是将大部分的模块抽象到 utils 基础模块上
http 请求解析和响应处理
这一块目前是使用 python 的异步协议,这个是调研过传输 io 消耗最低的方案了。由于考虑到黏包和性能问题,因此通过采用实现一个状态机通过流式处理的方式来实现解析
- 如何实现可靠的传输
- 确认传输的是什么、传输的包大小如何?
- 如何解析 path
- 实现一个 http 客户端,包含 get,post,put 等功能
日志模块和中间件注入
开发的程序是多种多样的,但是总有共性的处理模块,比如说一个统一且合理的日志模块就非常方便
同样的,最好要实现一个根据函数声明的数据,能够自动注入所需参数的函数(包含正则路径中的参数也需要自动注入)
python
@server.register(regex="/([0-9]+)")
def example(index: str, socket: Socket, log: Logger):
return socket.sid
@server.register(regex="/test)")
def example(log: Logger):
log.debug("recv a request")
依赖注入目前是通过 python 的code方法来获取参数的信息和类型引用。目前只自定义了一个简单的日志函数,没有做文件写入,调用定位等功能
指标模块
根据字典层级,自动生成 Prometheus 类型的指标,这个还没做好,主要是没想好如何实现在不干扰正常路径定义的前提下,生成所需的指标
管理专用能力
给管理模块专用的能力,目前由于框架还没开发完成,因此还没有研究
cron 表达式解析和生成能力
rt,之后用于定期触发任务,包括但不限于用户任务和定期清理任务。需要考虑如何实现兼容 corn 表达式和每隔多久一次的时间表达式
消息队列
能够高效率的 push 和 pop 数据,复杂度尽量为 O(1)。到底是 fifo 还是 filo,需要考虑要不要分配 id 来完成回滚
生命周期管理模块
管理创建、扩容、注册、退出模块。由于可能频繁的触发注册,因此性能需要尽可能的强
开发心得
网络传输的缺陷
网络进行分布式运算的八宗罪(8 Fallacies of Distributed Computing)
- The network is reliable. 网络是可靠的
- Latency is zero. 延迟是不存在的
- Bandwidth is infinite. 带宽是无限的
- The network is secure. 网络是安全的
- Topology doesn't change. 拓扑结构是一成不变的
- There is one administrator. 总会有一个管理员
- Transport cost is zero. 不必考虑传输成本
- The network is homogeneous. 网络都是同质化的
在开发完成基础框架后,才发现这些分析居然如此的鞭辟入里。本地函数调用即使和转发框架共同抢单核心占用的情况下,qps 也普遍比走网络转发的强 3-4 倍,极端情况下甚至是数量级的差距
其实这个翻译有一些歧义的地方。比如说第六条是说没人管理还是多人管理导致的故障。第 7 条是说宽带费用还是网络传输对数据操作的开销
整体看确实挺贴切的。但由于基本以内网甚至是本地环回进行传输,因此 1、4 基本上能满足。由于内网环境相对更简单且有秩序,因此 5、6、8 不需要被专门考虑。但是由于 socket 通讯的速度天生就比能够编译器级别优化的程序内联调用差数个数量级,因此 2、3、7 需要被着重考虑
针对网络调用的带宽和开销大的问题,在设计通过网络调用的时,我们需要使用传输时开销尽可能低的技术(如异步+协议)并设计一个封装和解析尽可能简单的协议
这里我考虑的主要是兼容 http,以及简单方便。而 http 本身设计也考虑了简单方便和可拓展,因此在传输协议上,仅对 http 的标准做了些简化和魔改
text
Socket-ID|METHOD PATH:Resource?arg¶ms=1 HTTP/1.1 # 修改http协议,调整为链接ID以便io多路复用
Host: www.example.com # 仅保留部分请求头降低体积
User-Agent: Mozilla/5.0
Cookie: userID=abc123; sessionID=xyz456
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
name=John&age=30&city=New+York
内部传输应答报文中主要考虑如下封装,使用 http 兼容的请求头/响应头作为信息标识。使用 content-type 和 content-length 来表示请求包的内容
text
HTTP/1.1 200 Socket-ID|OK # 修改http协议
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
name=John&age=30&city=New+York
IO 多路复用
由于 python 对 http2 的解析比较复杂,目前只考虑支持 http/1.1、websocket 和 sse。而且由于考虑将网关直接的互访可能会跨越反向代理服务器后面,因此考虑将内部通讯协议更改为小幅魔改,能兼容标准 http 协议的的 http 协议
由于 http/1.1 是一问一答类型的,因此架构设计简单了不少。但是在模型中,管理模块需要对多个客户端建立链接,和单个服务模块保持一个链接,因此需要实现一个简单的 io 多路复用
当前实现是通过对 socket 链接对端的 ip 和端口进行编号,附加到 http 请求头中的方法标识前和 http 响应头的 reason 前。这样即使是适配了 http2 的 io 多路复用也只需要更新伪链接
如何保证多个不同报文(如 sse 或 websocket 更新后)能传输给一个客户端做响应呢?
这个似乎应该交由管理和服务模块处理,考虑升级时将链接置入一个单独的列表?正好也实现一个伪 socket 通讯?
更新:需要能够发送分块直接传输报文的能力,不然分块传输就不好搞了
在子模块中,将收到的报文依据通讯编号划分成不同伪链接,实现 read 和 write 功能(不考虑流式传输)。但是在链接关闭后,需要实现一个机制能够通知关闭伪链接
兼容问题
目前测试了一下,大部分的生产服务器和大厂网页对 http 和链接的保活都挺不错的。但是 flask 用的框架 werkzeug 和测试服务器对长连接的保持会有问题,在不主动关闭链接的情况下,再次向链接发送数据时会发现连接被关闭
对 http 连接的超时和重试机制到底是否要做强验证,client 主要是期望作为内部服务的解析,应当在满足功能的前提下越简单越好。但是考虑到本框架是为了降低依赖(requests 也是依赖包)以及针对跨公网传输消息报文的需求,还是实现一些基础的弱验证吧(比如重试和超时)
http 标准
由于 http 标准比较多,有很多具体的实现细节也需要实践才知道。考虑兼容 http2 的流 id(其实就是自增 id)
当 socket 被关闭后、超时后。目前考虑实现的是直接丢弃这个链接,而非进行端口复用
- 特殊内容
- 大部分的状态码都需要 body,除了 204,301 之类的。如果没有 body,很多客户端的实现会默认进行等待(没有 content-length 也会等待,但是我发现 content-length 为 0 的时候似乎不会等待)
状态机
需要考虑 http 报文流式转发
实际上要完成两个状态机,一个是只接受类标准 http 报文的客户端。另一个是接收并可以流式传输 http、ws 报文的服务端(最后准备搞成同一个)
开发完框架之后发现,这个状态机实际上消耗了很大一部分的性能(30%),但是又不至于绝对性的拖后腿。需要考虑优化性能
由于 asyncio 的协议的特殊性质,只能异步的接收数据,不方便做主动的请求 socket 返回数据。虽然可以封装,但是不如在接收函数的基础上直接处理
由于考虑到 tcp 中存在的黏包以及 http 报文中的流式处理需求,用传统的函数来处理较为麻烦。因此采用状态机来对 http 报文进行解析
具体流程为
- 初始化状态为接收任何数据
- 接收到足够量的数据后进入处理模式
- 在处理模式中判断是否接收了完整的请求头,不是就继续接收数据直到匹配上完整的请求头。接收到后解析请求头并存放到缓存中
- 依据请求头来判断是否存在并接收完毕请求体部分,当接收完毕后转换到完成模式
- 将请求头和请求体放到缓存中,通知处理函数接受数据。将状态初始化,并把流中未处理的数据发送至循环开始
注册和负载均衡机制
由于 ack 包将一来一回的调用改成了一来二回,而且需要 ack 回包。因此是主要的瓶颈
实际上 socket 链接分为多个部分,直接链接统一当成客户端链接,发送 ws 升级报文的升级为 ws 链接,发送服务注册报文的升级成服务链接
服务连接该如何注册,处理起来很复杂。因为这涉及到了另一个重要的问题,那就是如何进行负载均衡。由于目前服务端的差距较大,因此考虑采用请求后发送类 ack 包来实现继续发送响应的能力
这就又有了一个问题,如何快速的处理 ack 包和管理当链接出现异常时的退出机制。当 ack 包被清除时不考虑删除注册的路由,仅当链接断开时清除?
当使用传统的一来二回,qps 接近 3k,不发送请求包,使用队列的情况下 qps 是 5k。如果不发送请求包的话,即使是直接调用,qps 也没有增长
因此需要考虑能否使用一个更简单的负载均衡算法来实现在不返回 ack 包的情况下也能根据处理能力来分配任务,毕竟能提升接近 100% 的 qps,非常香
功能划分
由于功能持续的完善和扩充,因此功能划分存在一些混淆问题
- handler:框架主体,负责服务器运行和处理注册等功能。职能为控制器
- connect:端口的高级封装,负责根据接收到的报文向框架请求路由通道。职能为传输管理
- tunnel:将报文解析并传递给函数,将同步的函数和传输转为异步处理。翻译函数的返回值并进行返回。职能为函数代理
由于 web 处理函数都是异步的,但是框架和异步协议是同步实现,因此需要一个模块来完成同步和异步协议的兼容,这个帮手就是 tunnel。起到了类似微服务中的边车代理的功能