最近在家办公。正在发愁摸哪条鱼的时候,群里的产品突然冲我来说,某单曝光异常,让我配合看看。我仔细询问了订单信息。亲爱的,我用了一个6年前开发的功能。要知道这个功能自上线以来就很少有人用了。我不知道为什么现在使用它。只能先放弃钓鱼了。合作解决问题,毕竟是靠这个吃饭的。需求背景在广告中,经常会有这样一种需求场景,即需要将某个广告投放给某一群指定用户。即假设有一个adid,指定了用户1和用户2,那么只有当用户1和用户2的流量请求来的时候,才会返回给adid,其他用户的流量不会返回给广告。通常情况下,指定用户多达数百万甚至数千万。直接用广告订单推送这些用户ID显然是不切实际的,所以当时的设计方案是广告商会包含指定用户ID的数据。打包上传到广告后台,然后生成url,url随着广告订单一起推送。在引擎中,有一个专门订阅广告订单消息的服务。如果发现广告订单是由指定用户下单,则实时获取并加载该url指向的数据包中的数据,以便在用户ID流量到来时匹配广告。初步设计在开始本节之前,我们不妨思考几分钟。如果让你实现这个功能,你会如何实现?好吧,让我们把时间拨回到2016年底,产品拉高需求的时候。看当时的要求,挺简单的。为了方便内容的理解,加载目标包的服务被称为Retargeting。Retargeting服务有两个基本功能:订阅广告订单消息队列。如果你拿到一个带有定向包的广告订单,下载定向包,然后获取里面的数据。建立倒排索引是不是很简单,代码也很容易写,可以使用libcurl下载定向包,也可以使用wget下载然后读取文件加载到内存中。当时因为时间紧,所以选择了wget方式来实现。因为数据量比较大,我们使用redis作为倒排索引的存储介质。假设有一个名为ad的广告订单,其中包含一个定向包地址url。这时候会做以下事情:1.使用如下命令下载url指向的定向包autocmd="wget-t3-c-r-nd-P/data1/data/–delete-after-np-A.txthttp://url.txt";autofp=popen(cmd.str().c_str(),"r");if(!fp){return;}2.以文件形式打开包//获取本地包的地址,如/data1/data/url.txtfp=fopen(path.c_str(),"r");std::stringid;charbuf[128]={'\0'};while(fgets(buf,128,fp)){id=buf;boost::replace_all(id,"","");boost::replace_all(id,"\r","");boost::replace_all(id,"\n","");noost::replace_all(id,"\^M","");如果(!id.empty()){ids.emplace_back(id);}}3.在Redis中创建倒排索引for(autoitem:ids){redis_client_->SAdd(item,adid);}好了,到这里,Retargeting服务功能基本实现了。在召回引擎中,当流量到来时,会先以用户ID为key,从redis中获取指定下发设备ID的adid,然后返回。代码编译好后,在测试环境下单,推送,然后模拟请求,召回,完善。问题是定向打包功能,尤其是KA广告商,是不屑使用的。毕竟他们有钱有势,想要的是一种类似褪黑激素的促进效果,即“只关注展示量,不管有没有效果”。.但随着国家政策的逐步调整,广告行业开始勒紧裤腰带过日子,昔日的广告大户也开始重视广告的效果。毕竟产品和效果是一体的。于是,他们开始挖掘一批用户,开始在后台投放,尝试投放效果。随着此类定向套餐的订单越来越多,之前实施的Retargeting也开始出现瓶颈。..毕竟这个功能用的不多,所以大多数情况下,当一个产品或者运营提出问题的时候,都会找借口回去。直到有一天,产品直接抛出一张图片,称某部门老大非常看重的广告主定向套餐曝光为0,并威胁说如果当天解决不了就下架通过其他渠道。..既然部门负责人来了,我们就得查清楚原因,于是我们开始检查订单的url是否有效,定向包裹设备的有效覆盖,一直到推送时间整个订单。一切正常。问题是ReTargeting服务,服务正常,加载也正常。无意中查看了消费进度,并不知道。昨天的订单刚消费完,也就是说当天要发货的订单还没有开始加载,难怪到现在还没有曝光。有了这个进展,有曝光才怪。顺便看了一下服务状态,亲,CPU占用这么低。..尝试优化既然CPU占用率这么低,可不可以从CPU占用率的角度进行优化,提高CPU占用率,提高业务处理能力,从而加快其加载速度。对于这个,一般稍微有点经验就会知道怎么优化,没错,就是用多线程。既然决定使用多线程,就得彻底。多线程处理订单。在每个订单中,多线程用于数据加载和处理。我们暂且称之为M*N多线程设计模型。如下:上图中,使用多线程优化了Retargeting服务。假设此时有m个定向包裹订单,同时会有m个线程处理,每个线程处理一个定向包裹订单,看起来很完美,等等,会不会有其他问题?不然当初为什么不这样设计呢?众所周知,对于多线程程序来说,“线程的执行顺序和完成时间是不可控的”。使用上面的设计方案,如果多个线程“同时处理多个不同的订单,那么就没有问题”,但是,对于另一种场景,这个方案是不可行的,如下:假设销售创建一个定向包裹订单此时ad0,先推送上线。然后我发现这个订单有问题,我立马下线了。那么此时消息队列中有两条消息,首先是ad0的在线消息,然后是ad0的离线消息。基于以上多线程设计模型,假设线程1在线执行订单,线程2离线执行订单,可能的结果如下:先执行在线订单加载,再执行离线订单加载,即符合预期的线下订单先完成订单,再完成线上订单。这种情况终于和我们预想的相反了。线上订单和线下订单同时执行,中间交叉。结果是不可控的。显然,这个方案是不可行的,虽然最大程度地优化了性能,但无法得到正确的结果。性能再好又有什么用呢?多线程的设计模式真的不适合我们的服务吗?你不妨调整一下自己的想法。在上述方案分析中,如果多个线程同时处理多个订单,就会出现问题。也就是说,在M*N的多线程设计模型中,正是因为M>1,导致结果不可预测。那么如果M=1呢?这样会避免上述问题吗?我们还是以上面的案例为例,因为消息队列是单线程处理的,所以总是先处理在线消息,再处理离线消息,结果总是符合我们的预期。既然方案已经定了,就可以直接写代码了。在这个解决方案中,我们使用多线程进行处理。如果每次来订单消息,则创建多个线程进行处理。处理完成后,线程被销毁。虽然这样做也是可以的,但是对性能会有影响,所以索性使用线程池来完成。在基础库中,有一个之前手摇的线程池,可以直接使用。for(autodid:ids){thread_pool.enqueue([did,adid,this]{RedisClient客户端;redis_client_pool_.Pop(&client);if(client){client->SAdd(did,adid);redis_client_pool_.Push(client);}});}}一次编译、部署、测试,没问题。开始上线,完成上线,看CPU利用率,完美:数据说明一切,对比优化前后同一订单的处理时间:性能提升近30倍,符合带着期待。..需求永远是自身技术提升和架构升级优化的动力源泉。有时候,一个简单的小优化,就能达到事半功倍的效果。
