前言作为Node语言的初学者,在实践后端开发的时候,既有欣喜,又有几分忐忑。好在大家很开放,给了很多建议和分享。至此三个基于Node.js+TypeScript+IMServer1的项目已经成功建立。也是时候总结一下自己最近的学习过程了。给大家分享一个作为载体成长过程的小开发任务。需求在完成Node项目的搭建后,接到了第一个Node后台开发任务:定时拉取企业微信的组织架构信息到业务数据库系统,并提供手机号查询用户查询接口。一开始,我对这个任务还是比较看好的,满怀信心地开发了它。初步方案设计完成后形成上述方案:在服务器部署初始化时(init.ts初始启动文件),启动node-schedule的定时任务,读取数据库中企业微信的企业配置,然后启动几个企业并行的组织结构更新过程。企业微信提供获取部门成员的明细,只需要并行更新各部门的信息写入mysql数据库即可。当查询接口到达服务器时,首先从数据库中查询手机号对应的会员。如果不存在,调用手机号从企业微信端获取useridAPI,再通过用户信息API获取最新的用户信息,避免定时更新。update更新时间间隔;如果存在,则直接返回数据库中的信息。开发过程中踩雷的整体业务逻辑并不复杂。在调试部署的过程中遇到了很多问题。这里我一一列举:1、访问频率受限的企业官方微信规定,同一时间对同一资源的请求数不能超过一定值(60)。由于部门详情请求接口使用的并行模式,超过了阈值,测试时官方封禁了该IP。2、进程太多导致SQL查询慢,没有考虑多地点部署(3个地点*5台服务器*8个worker),导致同时有120个update进程,导致数据库mysql读写混乱,并且非常消耗性能,导致数据库在读写压力比较大的时候,出现一些慢查询。3.无效手机号无法调用企业微信API。企业微信对手机号获取userid的接口有如下限制:当查询中出现一定数量的无效手机号时,会触发企业微信官方IP封禁。但是业务系统中存在大量离职后无效的手机号码,所以当查到数据库中不存在时,频繁调用上述接口会触发封号。4、数据库读写冲突由于存在多台服务器同时读写数据库,导致数据库存在一些重复和不足的情况。5、网络环境导致读写锁平衡失效,导致衍生问题。为了优化以上部分,任务读写锁的引入保证了单个进程可以更新。但是没有考虑到各地服务网络的情况,导致内网服务器一直持有读写锁,失去了负载均衡设计的有效性。也导致在配置上线前环境时,由于网络环境良好,上线前环境一直持有读写锁,进而影响实时数据上线。不考虑故障情况的告警和恢复深度优化设计下面介绍解决这些问题的思路和解决方案。1、访问频率受限这里针对“部门成员信息API”的并行请求,根据有效频率值转化为串行发送机制,调用速度设计为10次/秒。2、进程太多导致SQL查询慢这个解决方案比较明确,就是减少启动定时任务的进程数。由于后端服务一般分为测试环境、预启动环境和正式环境,所以在部署时可以通过设置环境变量“SCHEDULE_ENV”来管理各个定时器脚本是否需要在不同环境启动(以SKTE为例).每个server会启动8个worker进程,每个worker由“process.env.IMSERVER_WORKER_ID”变量标识,所以只能设计“worker1”进程启动定时任务;3.无效电话号码无法调用企业微信api这是技术调研时没有发现的情况,发现是之前技术调研的工作疏忽所致。首先,业务呼叫者无法知道手机号码是否有效,不应该关心这个限制。因此引入实时查询机制来解决部分新记录更新不及时的问题是不合理的。实时查询机制:“对于数据库中不存在的手机号,通过微信官方API进行实时查询,返回结果。”所以去掉了这个机制,提供了基于官方微信API的实时查询接口。当一方调用时,结果也同步更新到组织结构中。4.数据库读写冲突引入redis任务锁机制,保证同一时间只有一个进程可以进行数据库更新操作。其次,企业间的更新采用并行机制。由于彼此不冲突,不会造成同一条记录的读写冲突,也可以提高更新速度。5、网络环境导致读写锁平衡失效,导致衍生问题。在最初的设计中,我希望服务器能够根据自身的负载情况公平竞争任务锁,但实际情况是由于多次部署,其中一个稳定的内网环境总是可以优先获得任务锁,但是有没有所谓的公平。尤其是压测需要部署预启动环境时,如果没有设置只读db账号,也没有设置定时任务环境变量,这两个错误会导致在启动时需要调整一定的组织架构更新逻辑代码更新到该行。老逻辑一直在线执行。经过一系列的排查,我们发现预启动环境一直在获取读写锁,使用老逻辑更新数据库。因此增加环境变量控制定时任务的启动,区分压测环境下的数据库权限,增加只读模式。6.告警和错误恢复这里有一点前端思维的影响,这部分同样重要。告警方面,对接IMLog的NodeSDK。通过Kibana和Grafana的系统配置,可以有效监控组织架构的更新。在错误恢复方面,这里的错误主要出现在企业微信API的access_token过期的时候。经常出现在以下两种情况:企业微信正式让access_token过期。在更新组织架构的过程中,access_token刚好过期,也就是http传输上面的情况是不可避免的,直到企业微信刚好失效。这里使用中间件封装了node.fetch,增加了对response返回值的校验。如果企业微信API的返回值为“WX_CODE.INVALIDE_TOKEN”,则会发出预警并重置accessToken。exportdefault(app)=>{const{utils:{imlogHelper}}=app;constwrapperLogFetch=(originFetch,{traceId,header,client_ip,})=>async(...args)=>{constres=waitoriginFetch(...args);if(res.errcode===WX_CODE.INVALIDE_TOKEN){//更新逻辑wxService.clearAllRedisKey();imlogHelper({cmd:url,message:'accessToken_update_warning',body:JSON.stringify(res),trace_id:traceId,retcode,headers:header,});}returnres;};//覆盖context.fetch方法returnasync(ctx,next)=>{if(!ctx.logFetch){constriginFetch=ctx.fetch;const{traceId,ip:client_ip}=ctx.request;constheader=JSON.stringify(ctx.request.header);constlogFetch=wrapperLogFetch(originFetch,{traceId,header,client_ip,});ctx.logFetch=logFetch;}if(ctx.fetch!==ctx.logFetch){ctx.fetch=ctx.logFetch;}awaitnext();};}总结经过重新设计和验证,形成了上面的设计方案,优化点如下:首先,基于redis实现了setnx任务锁,用于单进程更新数据库同时;通过在部署时设置定时任务启动环境变量和数据库读写账号设置,保证不同环境的分离;通过企业并行方式,部门数据拉取接口串行方式,最大化性能,避免API调用ban;完善错误恢复机制和告警,实时查看运行状态。
