并行–异步–水平扩展

业务场景需要

所有一切脱离了业务场景需要来谈论的架构,都是纸老虎,所有的设计都离不开业务的需要

如果对业务的需要贴合程度不够,造成的过度设计或者过简设计,都是无法拟合场景的,无论你的设计有多么的牛X,无法拟合场景的设计,我们一般都叫做垃圾设计。

但是这里有一个预留的要求,业务场景是会随着业务的不断扩展而发生变化的,大家都说软件设计没有银弹,这里就是造成没有银弹的罪魁祸首之一,所以好的架构要能在一定程度上把握将来的一些变化,并且能够在一定的范围内快速的适配业务的变化。

最开始业务场景是这样的,前台有一个服务于B端的业务系统,这个业务系统呢,各种业务交错,场景也多变,然后可能需要在某些节点上去使用AI提供的能力,但是AI提供的能力呢,又是由多个模型所共同来承担的,并且可能不同的节点上,需要的ai的能力还不太一致,所以就要求这些独立的ai模型,能够自由的组合搭配,相当于是一个可以任意合体的变形金刚,来完成不同的功能。

1, 原始服务时代

最开始的设计就是一个简单异步并行请求,发送到各个模块去进行处理,各个模块处理完毕以后通知主控,然后由主控来复合合并各个模块的数据,然后由主控去判断是否可以给请求方回调。

这里两个层次的异步,基本都是通过http的restful service来完成的。由于是并行分发,所以在很大程度上受制于最慢的那个子模块的返回速度。对于原始请求方,也是要求在请求的时候提供回调地址,然后按照约定好的协议给原始请求方回调。

这里利用了一个redis在中间做数据的共享,基本上所有模块都依赖与这个共享的存储,相同的key在生命周期里面,按照模块的多少去收集各个模块的数据,处理完毕以后清理。

峰值考虑

吞吐考虑

异步回传

动态增加模块

细节问题1

这里有个比较坑的情况,如果并发同时对一个key 有两次操作,那么这个key的处理由于依赖于一个存储,就会乱,这里加入了一个批次号的概念,保证key+批次号在两层异步的处理过程当中唯一。

细节问题2

如果有一个模块不返回或者是挂掉了,或者操作的时间很长,

细节问题3

如果要水平的扩展一个模块的处理能力,通过简单的加进程和机器的方式,

细节问题4

预处理和后处理的那些坑

批量处理

2, 队列时代

遇到的问题

当我们开始对大并发的请求进行测试的时候,我们发现,前端框架在并发为500的时候,单台的机器直接被打挂了,由于我们的aicore承载了几乎所有的中心的中转和揉合的服务,这个一定是不能垮掉的,于是我们加了一台机器做了简单的负载均衡,以为可以很简单的解决问题。

但是不幸的是,由于python的单进程限制,我们并没有在进程以及子线程里面进行优化,于是这个比较渣的web.py的前端框架,还是被打挂了。于是我们很简单的用了一个redis模拟队列来解决这个打挂的问题,也就是利用redis的高ops,来完成峰值的吞吐,然后放到后面的while里面去各种消费,高峰期间我们开了五个线程三台机器来完成分发的任务,基本上算是完成了简单的队列问题。但是随之而来新问题又总是不可避免的

单节点的挂机

由于redis是我们并没有采用cluster版本,于是在一个redis挂掉之后,我们的整个服务也就都挂掉了,所以必须找到一个可以多节点的队列来解决这个问题

节点吃满

Redis本身所存储的value值,其实是有限的,并且对于二进制的长数据,效率也不是十分高效,曾经有一个小气的500mb的redis内存的节点,由于模拟队列采用的是单key而导致了redis直接内存死锁了,在单key情况下,队列的LRU是不起作用的。

于是我们开始寻找新的替代品。

3, 异步队列时代

timeout机制

由于我们要最大化吞吐,所以所有的请求都不能挂,并且要加入超时以及执行时间的控制,对于执行时间太长的,为了不阻塞异步队列,我们要干掉那些处理没完或者是处理不了的内容,于是在关键的执行步骤当中,我们加入了timeout的机制

错误丢弃机制

由于模型的覆盖不是100%的,有些时候喂入的数据不一定能够正常的处理,所以对于没法处理报错的数据,我们就要开始采用丢弃策略。

死亡恢复机制

由于是异步多模块协作,如果一个模块挂掉,会导致后续的请求,由于挂掉的这个模块而没有返回,所以这里要求模块能在自己挂掉休眠10秒以后自动重启,这里用了二级cache来解决这个遇到的问题,如果还是要死,第三层就是pm2的自动重启保护了。

错误跟踪机制

错误总是花样百出的,你永远不知道喂到模型里面的东西是什么鬼东西,所以在发现喂入的东西有问题的时候,要把他们记录在小黑板上,方便后续的分析。这里采用了sentry来收集错误异常信息