聊聊最近的一个项目/你做了什么/你印象最深的功能点

在线广告竞价平台 背景简述

那么我来说一下最近深度参与过的一个项目,就是广告竞价平台,这个平台主要分三大业务线,merchant后台(商户)、operating(运营)以及traffic(流量)平台

merchant后台管理广告主也就是商户的一些平台资质的材料的上传,广告物料的上传和维护,钱包以及充值和支付功能,

operting后台集成了审核系统,主要用来审核商户运营资质,同时对广告物料进行编排和管理,以及im实时通讯客服系统

traffic后台负责流量平台的管理,广告的检索以及投放功能,并且支持现金提现。流量主盈利的主要方式目前平台支持的就是cpm(Cost Per Mille), 即千次广告展现的费用,cpc(CPC(Cost Per Click), 即按照广告被点击的次数来计费。关键词竞价、信息流广告大多是这种模式。),cpa(cost per action),即按照用户行为来计费,一般为注册行为,注册成本。也包括CPD(CostPer Download)每次下载成本CPI(Cost Per Install)每次安装成本

组织架构

5人研发团队,3个RD(一个前端),一个QA(测试),一个PM(产品经理),半个月更新一版(上线),Leader(部门经理)负责分配任务

产品研发流程

需求分析 -》需求评审(QA做用例评审) -》分配任务(leader) -》 填写工时 =》项目研发 =》code review =》 合并测试分支 =》 测试提测 =》bug fix(bug 修复) =》合并主分支 =》 项目上线

相关数据

50w用户数(uv),日活10w左右,单日有效审批增量2w左右审批任务,因为只做大陆广告主业务,所以白天上午10-12点,峰值qps在400-500左右,最高记录达到过600,因为有渠道商做引流操作。有少量的海外用户,谷值不到20的qps

主要负责模块

我个人主要负责的就是涵盖merchant和operating业务闲的审核系统,我刚接手的时候实际上这套系统的审核效率非常低,您也知道,传统的审核逻辑是通过用户状态来进行审批动作,比如0代表待审,1代表审核中,2代表通过等等,这种审批逻辑在待审任务和审核员量级小的情况没有问题,但是如果审批量大或者审批员较多就会出现资源分配不一致的问题,也就是生产者-消费者问题。

设计思路

为了解决生产者和消费者过度耦合的效率低下问题,我设计了一个缓冲区,生产者不会直接和消费者产生关系,而是通过缓冲区解耦,这个缓冲区就是异步任务队列,队列容器我采用redis数据库,因为redis性能优势比较明显,同时内置的list数据类型比较契合队列这种数据结构,工具类内置了,初始化方法,入队方法,出队方法,队列长度,以及查重唯一方法。每当商户提交表单,此时并不会修改状态,而是将表单数据入库,同时将商户uid进行入队操作,遵循fifo原则,在消费者端使用多线程的方式进行消费,也就是出队操作,每一个线程对应一个审核员,通过消费方法进行传参,每次将出队的商户uid和线程传入的审核员id进行组合分配,然后在mysql端进行update操作,达到异步分配审核的目的。

加权队列

在此基础上,我还开发了基于异步任务队列的加权队列,用来对审核任务进行优先级分类和管理,将渠道方用户的审核顺序进行加权操作,主要设计了两套方案,一套是基于排序字段的逻辑,即队列中嵌套一个tuple,tuple中除了商户uid以外,加入一个倒序的等级字段,入队后通过等级字段进行倒序排序,从而达到加权的目的,第二套方案就是维护一个多队列的结构,优先级队列和普通用户队列进行顺序合并,达到加权的目的,celery也是采用这种多队列的方式进行加权,这两套方案各有优劣吧,第一套虽然可以将指定的商户进行加权,但是性能上要损耗一些,成本比较高,第二套方案不需要进行排序操作,但是原子性的细化操作不好处理。

消费模式

第一版采用多线程的消费方式其实是有一些问题的,那就是怎样保证缓冲容器中数据状态的一致性,多线程消费方式是并发的,也就是出队之后如果没有来得及更改队列长度,其他线程就会发生争抢问题,导致审核状态的不一致,比如重申问题,所以在执行消费方法的时候,需要先获取线程锁,再做出队和数据库更新操作,操作结束之后再释放锁,这个过程中,如果redis或者mysql宕机的话,还可能出现死锁问题,所以需要异常捕获机制的参与。

协程消费

项目后期,我改变了多线程异步消费的思路,采用协程的方式,主要考虑到协程虽然是单线程的,但是它是用户态调度,可以通过调度器自由的分配审核员的优先级,同时协程性能要比线程优秀,不会触发python的gil全局解释器锁,优化了出队的步骤,所以协程消费可以有效的提高审核效率。

协程是一种用户态的轻量级线程,协程的调度完全由用户控制,不像进程和线程是系统态,同时协程是单线程的,即可以共享内存,又不需要系统态的线程切换,同时也不会触发gil全局解释器锁,所以它性能比线程要高。具体使用场景和线程一样,适合io密集型任务,所谓io密集型任务就是大量的硬盘读写操作或者网络tcp通信的任务,一般就是爬虫和数据库操作,文件操作非常频繁的任务,比如我负责开发的审核系统,需要同时对mysql和redis有大量的读写操作,所以我后期将多线程改造成协程进行消费。协程我使用的python原生协程库asyncio库,首先通过asyncio.ensure_future(doout(4))方法建立协程对象也就是事件循环体系,然后根据当天审核员数量指定开启协程数,和多线程以及多进程的区别是,协程既可以直接传实参,也可以传不定长参数,很方便,然后通过await asyncio.gather(*tasks)方法启动协程,需要注意的是,如果是脱离tornado事件循环的单独协程任务,主方法需要声明成async方法,并且通过asyncio.run(main())来启动。协程虽然是python异步编程的最佳方式,但是我认为它也有缺点,那就是异步写法导致代码可读性下降,同时对编程人员的综合素质要求高,并不是所有人都能理解协程的工作方式,以及python原生协程的异步写法。

幂等性操作ack确认机制

事实上,当审核任务出队之后,如果在消费端出现意外,这个意外包含但不限于出对后tornado宕机、mysql宕机等等,导致出队任务没有进行流程化处理,所以我采用了ack验证机制,也就是缓冲区队列从单队列升级为双队列,把rpop出队改成redis内置的rpoplpush的原子性操作,出队后立即进入确认队列,在消费端完成审核任务后,对ack队列进行确认移除操作,如此,一次审批任务才算完结,如果任务生命周期内,任务一直存在于确认队列没有出队,那么轮询任务会将任务id移出确认队列,重新在缓冲区队列进行入队操作,这样就避免了,僵审任务的问题。

遇到的问题

在开发过程中,主要有几个问题,具有代表性:

重复消费

任务重复消费问题,也就是任务的全局唯一性问题, 两个人同时审批一个,一个人同时有两个审批任务重复提交,这里我入队之前会设置一个拦截器,这个拦截器基于redis中的set数据类型,在审批动作流转过程中,被审批人uid在集合中存储,审批结束后释放,如果审批中再次入队会被拦截器拦截下来。

任务丢失

这是QA曾经给我提的bug,任务丢失问题主要有三种可能性,第一种就是商户提交资料后,后台服务接收到了用户提交的资料,并且在mysql中写入了,但是入队的时候,redis服务挂了,导致入队失败,这种情况导致任务丢失,商户提交材料,但是永远没机会被审核,这个问题我采用一种轮询的方式进行检测,只检测用户状态为待审的用户,如果用户状态为待审并且没有出现在拦截器集合中,那么我们认为该任务异常,启动重新入队的操作

第二种就是队列中丢失,比如redis宕机,导致队列中数据丢失,这块主要是redis的数据持久化问题导致的,默认的rdb全量延迟持久化方式改成aof的增量实时持久化方式,防止重启或者宕机导致的数据丢失

第三种就是出队后没有及时进行审批操作导致的丢失,这里也可以通过轮询拦截器进行一致性比对来解决

内存溢出

当峰值审核量非常大的时候,单台redis可能承受不住压力,导致审核队列崩溃的问题,此时的解决方案主要通过搭建redis集群的方式解决负载均衡问题,我是通过docker搭建了三台redis服务,一主两从,三哨兵实力,集群结构是sential,也就是哨兵的监控模式,当某一台服务宕机后,哨兵会选举一台丛机升级成主机,以达到高可用的服务架构,

哨兵选举机制:

slave 优先级,通过 slave-priority 配置项,给不同的从库设置不同优先级(后台有人没办法),优先级高的直接晋级为新 master 掌门。 slave_repl_offset与 master_repl_offset进度差距(谁的武功与之前掌门的功夫越接近谁就更牛逼),如果都一样,那就继续下一个规则。其实就是比较 slave 与旧 master 复制进度的差距; slave runID,在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。(论资排辈,根据 runID 的创建时间来判断,时间早的上位);

这就是审核系统的介绍。

为什么不直接用开源的celery、KafKa、RabbitMq?

事实上,开源软件确实可以帮我们节省开发时间,但开源自身的发展方向一定有取舍,只会关注全球最通用的客户需求,新功能优先级的设定就不一定满足当下客户需求,这时自研就有它很香的地方,越来越多的客户也在享受自研的红利,自研比开源更快的适配客户需求并实际落地。

另外,由于出队业务我们采用了原生协程的方式,它可以直接集成到tornado的ioloop事件循环中作为回调方法运行,这样,我们只需要维护一个服务即可,降低了维护和监控成本,而如果采用celery,我们需要多维护两套服务,一套异步任务服务,一套定时任务服务。

以现实中的事情为例子,像华为、小米为什么要自研芯片?一直用美国的高通芯片不就行了吗?事实也一再教育了华为,美国一旦对其芯片技术进行制裁和断供,华为连5g芯片手机都上不了市,所以自研技术体现在核心竞争力,而不是躺平,等着别人施舍。

最后,您肯定也带过团队,一个团队需要成长,如果只局限于调用三方开源库,团队内部根本接触不到底层核心技术,久而久之,这个团队就失去了核心竞争力,成为了“调包侠”,所以,一个技术团队能否在业务上攻克技术难题,就看它的自研能力是否过硬,可以不重复造轮子,但是不能没有造轮子的能力,就像核武器一样,你可以不用,但是你不能没有。

results matching ""

    No results matching ""