包含标签 复盘 的文章

对HTTP制作统一缓存的思考

背景 最近收到反馈,用户觉得小程序比较慢,统一分析后认为,我们系统在用户刷新的一些动作时,耗时比较长。 排查 由于业务的需求,用户在打开微信小程序的时候会调用一次服务端的接口 服务端在执行时会去调用高德地图/IPIP的接口,由于是外部服务的调用,所以耗时比较长(有些极端的会达到好几秒,但是普遍都是500ms+) 由于业务的原因,这些外部调用必须调用 解决思路 一开始是针对部分接口有了缓存的(Redis),但对每一个小的业务场景都使用了一个对象来处理缓存的使用,目前已经积累了五个了,按照堆屎山的逻辑,我应该再去配置一堆缓存对象来处理这个问题,然后在各个调用接口的地方使用缓存对象,但,我认为这不是一个好的设计了,原有的设计不太实用了,并且会造成代码逻辑的混乱,不好维护,以后即便缓存出现了问题,也难以排查。所以怎么解决呢? 启发 无意中看到代码中有http trace的逻辑(可以对http调用过程中的状态进行感知),我想到,是不是可以使用类似的逻辑来实现对HTTP调用统一的缓存呢?我只需要配置一下域名,路由,HTTP Method,是不是就可以快速完成HTTP调用呢? 实现 方案一:在HTTP Trace中制作缓存,但我发现HTTP Trace没有办法去拦截HTTP的Response数据,所以这条路行不通。 方案二:对HTTP的调用进行封装,这样确实是可以统一的进行HTTP缓存的逻辑,但是我想了一下,工作量有点接受不了,太多了。 方案三:我找到了一个aiohttp的client缓存三方库,它相当于对http的调用进行了封装,当然是当一个api gay来得快乐,但是,这个库对于缓存的过期机制的处理有点傻,没有使用Redis的过期机制,所以对其进行改动了一部分。 总结 当初在设计HTTP缓存的时候并没有考虑到这是一个通用的东西,也没有做得太通用,每一种业务调用都和缓存类进行强绑定,所谓“吃一堑,长一智”,下一次遇到类似的逻辑我一定不会写出强侵入式的逻辑。 在调研HTTP Trace的逻辑时,一开始是认为能走通的,但确实是没考虑到HTTP Trace没有办法拦截HTTP的Response数据的问题,导致自己做了一半了,不得不放弃,这也算是自己没有把事情的全流程考虑好,算是拿了半截就跑的典型错误,下一次还是要将全流程的思路写出来才算是调研成功。 目前看来,增加了缓存还是提升了很大的效率,算是告一段落。 现在看来,突然有点理解为什么以前遇到的项目会直接对Redis,Http的调用进行了包装,第一就是老生常谈的可能会换底层包的情况,另外一种可能就是我当前的情况。 ……

阅读全文

记一次线上服务内存优化历程

背景 近期看到企业微信告警,发现线上服务进程隔一段时间就会重启一次,遂开始进行排查。 线上服务描述 Python服务(使用aiohttp) K8S部署 容器中使用gunicorn,每个容器启动4个worker进程 排查过程 观察报警信息,只有一部分线上环境收到了影响。 观察报警时间,观察到只在整点时有一次服务重启告警(告警是进程自己发送的,也就是说只有一个进程重启了),并且不是必现。 观察资源信息,在服务重启前,重启信息的容器的内存有一次跳跃式增长。 最终,总结出几个信息: 某一个环境(该环境数据量大),整点时分,单进程重启,内存暴涨造成的。 而服务在整点时只有一个定时任务,这个定时任务是做多数据库的数据同步。 并且,任务的调起,是由外部的一个服务向本服务发送的一个HTTP请求(只会有一个进程来处理这个HTTP请求),所以,能够说通为什么只有一个进程会重启。 观察该HTTP请求的信息,查看该请求日志中的进程ID和重启的进程ID,最后发现是一致的,并且重启时间的定时任务日志中,没有定时任务完成得日志数据,所以最终推断出进程得重启是由该定时任务导致内存暴涨,从而导致进程重启。 但,还有一个问题没有被印证,进程重启的问题不是必现的,进而,我们需要拉长时间观察,最终发现,进程会因为定时任务导致内存有一次跳跃式的增长,但不一定会导致OOM,但有一个问题,内存虽然增加了,但是并没有看到内存回收,这个需要了解一下Python的内存管理机制,是因为进程执行过程中没有一个Arena中的内存被释放完,所以就不会被回收。 解决问题 通过多进程的方式 从上文的排查过程来看,我们可以从内存回收的角度来,在每次定时任务后保证申请的内存能够还给操作系统,避免造成Python一直持有内存不归还。但直接优化GC和内存管理对我一个普通的小开发来说不太现实,所以换个角度看,进程死亡,他的内存一定会归还,那这样就可以绕开Python内存管理造成的内存不归还的问题了。所以,我尝试了以下办法: 直接在http服务进程中做fork操作,但在aiohttp中执行fork视乎会出现各种各样的小问题,例如,fork出去的进程会copy一份父进程的内存,相当于和父进程使用了同一份async loop对象,在子进程中,会报错loop是异常的报错。所以我猜测,Python异步和同步fork操作应该不同。 在GitHub中查到了一个叫aiomultiprocess的三方库解决了办法1的问题,不会报出loop异常的问题。但,我开发时是未使用gunicorn的,所以在测试环境测试时,发现fork操作其实会卡死,很是困惑,遂Google到一篇文章Gunicorn+GeventWorker环境下fork进程意外结束的问题,这篇文章的解决办法有些trick,不太想用,放弃。 既然fork两条路都走不通,再想出了一个新的方式,脚本执行。我将定时任务中的逻辑,不再放到HTTP请求里面了,而是重新修改为脚本,在HTTP调用中,调起这个脚本。最终发现,行得通,上线。 上线后,发现内存确实是能够正常回收了,内存跳跃式增长不回收,变成了一个又一个的内存尖刺。但过了几周后,又发现了服务重启,查看原因是脚本占用内存太大,导致某个服务进程在申请内存时OOM,虽然告警的频率极低,但也算是一个隐患,需要优化它。 优化代码逻辑,降低内存消耗 从上文中看到,虽然解决了内存回收的问题,但随着业务的日益增长,数据量也会增长,数据同步逻辑会随着数据量的增长导致内存增长,而影响了服务进程。但是由于某些原因,不能新开一个服务来做这个事情,所以,查看了数据同步逻辑的代码,大概逻辑是:对比一个时间段内源数据库与目标数据的数据数量,如果相等,不迁移数据,如果不相等,同步数据。但同步数据并不是按照差异同步,而是使用了源数据库单时间段内的数据全量覆盖。有些暴力了,并且单看数据数量就判断是否需要同步数据也有失偏颇。 所以,最终修改为,根据最后更新时间来对比数据,同步有差异数据。 总结 解决本次的问题,从技术上来说,学习到了: Python的内存管理 了解了Gunicorn fork进程的一些机制 Python异步直接fork子进程的一些坑 从经验上看: 排查该类问题的一些思路以及逻辑。 对解决问题的思考,或者说对无法解决的问题,我如何的避开它,也就是如果规避一个问题的发生。 对数据同步,或者数据对比一些业务逻辑的思考。 ……

阅读全文