Python import机制探究

最近遇到一个需求,需要动态加载一堆 Python 模块,但是这些 .py 是 pb 文件生成的,无法控制命名,导致导入的时候各种报错。

目录结构

package1.py 内容

package2.py 内容

package3.py 内容

可以看见在 basic.package1 中导入了 package2、package3。我们在 main 文件中导入 basic 包。

这样肯定不行,报错了:ModuleNotFoundError: No module named 'basic'

sys.path

Google 一下或者稍微了解 Python 的就会知道,Python 导入非系统库需要指定 PYTHONPATH,Python 启动时会读取此环境变量并加入到 sys.path 中。我们可以打印下:

可以看到默认加载了当前项目目录、Python系统目录以及 dist-packages(通过 pip 安装的),我们把 temp_dir 目录加进去就行了。代码增加:

但还是会报错:ImportError: cannot import name 'package2' from 'calendar' (/usr/lib/python3.8/calendar.py),不过至少说明,加载路径是设置成功了,basic 包本身导入成功了,只是 from calendar import package2 执行失败了。

这个是一个经典错误,搞 Python 的都会遇到,那就是自己定义的包名不能和系统的重复,一般是改个名字或者加上命名空间,但是我们这个场景中,Python 文件是 pb 生成的,无法修改源文件,修改生成的文件也比较麻烦。也能解决,就是提高我们路径的优先级,改成 sys.path.insert(0, temp_dir) 加载完了再改回来。

BuiltinImporter

不过又有新的报错:ImportError: cannot import name 'package3' from 'math' (unknown location),有输出 import package2,看着还是导入了系统的 math 库,没有导入自己的,还没有显示路径。这里匪夷所思,卡了很久,继续单步调试代码和查阅资料,发现 Python 其实是支持多种导入方式,甚至可以自定义。importlib._bootstrap._find_spec 中,是循环 sys.meta_path 中的 Importer 哪个找到算哪个。我们可以打印下 sys.meta_path:

可以看见最前面有个 BuiltinImporter,唉,灵光一闪,math 就是一个内建库呀,这里结合源码猜测,C 的库都是通过这个导入的。去掉这个就能导入成功了(记得加载完了还原回来)。

终于成功了:

sys.modules

不过,这里只是个 demo。实际情况比这个复杂,还是会报 ImportError: cannot import name 'package2' from 'calendar' (/usr/lib/python3.8/calendar.py),这里很奇怪,已经保证了加载的优先级,但为什么还不行呢?没办法,扒一下源码,终于找到了一点端倪。importlib._bootstrap._find_and_load 方法中,会先在 sys.modules 里面找,如果有则直接返回,官方文档也有相应说明。

所以还得排除已加载的情况

最终到此才彻底解决,完整 demo 代码:https://github.com/iyaozhen/python-import-test

参考资料

https://docs.python.org/3/library/importlib.html

妙用Hook来研究Python的Import机制,https://zhuanlan.51cto.com/art/201701/527713.htm

Python3.7源码剖析之import,https://zhuanlan.zhihu.com/p/361720373

工作5年,后3年都重复着前2年?写在离职百度之际(QA篇)

最近各种原因换了个工作,回想起来校招(实习)就进百度了,一晃5年多了。最近又宣传“十四五”,也想着自己确实得好好总结下这五年,而且市面上做 QA 的比较少,希望能对新同学有些帮助。

重新review下工作

做程序员这一块,可能都会遇到第一个瓶颈,工作前2两年成长很快(特别是 QA,都是现学),但后面就慢了,感觉重复着之前的工作。这里以“捕鼠器案例”为引子(杨震原字节内部分享,来源王梦秋百度的内部分享),重新捋一下 QA 的工作:

工作第1年

刚工作接到的任务一般比较简单、直接,比如导师可能叫你去放一下捕鼠器。这个时候你一般看看文档,多观察下,就知道捕鼠器是用来捕老鼠的,你只要不给它挂在墙上就行。甚至前人已经放过捕鼠器了,你只要依样画葫芦隔一段距离放一下就行了。

这个时候你就要稍微总结下了,放捕鼠器为什么有时没捕到老鼠,你可以会观察录像,看别人的捕鼠器怎么做的,这个时候稍作改进就会效果明显。具体的到QA的工作,你现在已经非常熟悉这块的业务,每次小功能的测试都游刃有余,而且通过学习你还会了 API/UI 自动化测试,效率也变高了。

工作第2年

由于表现还不错,你已经晋升为 T4 了。这个时候你会发现只放好老鼠夹子效果还是不好,具体到工作可能会发现线上还是有很多问题。你一想,捕鼠器是干啥,消灭老鼠,但消灭老鼠,不仅仅只有捕鼠器一种方法,或者说一种方法不够。

当一个人从完成任务到解决问题,能够再问一步,这个问题关联的问题是什么,进而去提出新的问题,这就是关键的突破点。

你可能会发现老鼠药更管用。类比测试工作上来说,你引入了更多的测试方法:diff 测试、性能测试、稳定性测试、兼容性测试、安全测试、ET测试等,工作开始变的顺风顺水。

工作第3-5年

这个时候你测试环节已经做的很不错了,能负责一个系统或者业务方向的测试了,一般的话也晋升为 T5(高级测试工程师)之列了。但这个时候就开始重复之前的工作了,项目不停迭代、新功能测也测不完。而且随着需求积累,回归测试也很多了,因此对于 QA 来说这种重复的感觉比 RD、FE 更强烈

这个时候还得回到原点来,捕老鼠是为了什么?为了保护粮食。那 QA 找 Bug 是为了什么?是为了“质量”。

(图 来源百度王梦秋分享 PPT)

这个时候介绍一个在百度内部非常重要的概念:全流程质量保障。作为 QA(Quality Assurance,质量保障),自己把自己定位成测试是不行的。

全流程质量保障

可能每个公司的项目流程都不一样,但都会包含以下几部分,当你跳出“测试”来看,就会发现能做的事情还有很多: Continue Reading...

Python使用logging为Flask增加logid

我们为了问题定位,常见做法是在日志中加入 logid,用于关联一个请求的上下文。这就涉及两个问题:1. logid 这个“全局”变量如何保存传递。2. 如何让打印日志的时候自动带上 logid(毕竟不能每个打日志的地方都手动传入)

logid保存与传递

传统做法就是讲 logid 保存在 threading.local 里面,一个线程里都是一样的值。在 before_app_request 就生成好,logid并放进去。

因为需要一个数字的 logid 所以简单使用 uuid.uuid1().time 一般并发完全够了,不会重复且趋势递增(看logid就能知道请求的早晚)。

打印日志自动带上logid

这个就是 Python 日志库自带的功能了,可以使用 Filter 来实现这个需求。

log_format 中我们用了很多系统自带的占位符,但 %(logid)s 默认没有的。每条日志打印输出前都会过 Filter,利用此特征我们就可以把 record.logid 赋值上,最终打印出来的时候就有 logid 了。

虽然最终实现了,但因为是通用化方案,所以有些复杂了。其实官方教程中介绍了一种更加简单的方式:injecting-request-information,看来没事还得多看看官方文档。

iTerm2进阶使用技巧

iTerm2 的优点这里不做赘述了,第一次使用的话可以先看看官网的介绍:featuresHighlights for New Users。本文主要是结合实际使用场景,介绍一些进阶使用技巧(基本的 Oh My Zsh、rzsz 等配置就不重复说明了)。

跳板机自动登录

现在很多公司登录服务器都需要先登录到一个跳板机然后再登录到目标机器,每次输入密码(一般还是动态的)很麻烦。一般的教程推荐使用 expext 解决这个问题,这里介绍一个更简单、直接的办法。

首先,要先解决登录到跳板机的连接复用问题,这个输入 ssh 本身的范畴,一般教程都是说在 ~/.ssh/config 增加下面的配置:

但这样有个问题,iTerm2 第一次登录的 tab 关闭后,就失效了,再登录就又要密码了。其实再增加一个配置即可。

之前的设置只是实现了连接复用,但 tab 关闭 ssh 进程结束后,连接也被销毁了,ControlPersist 项的意思是进程结束后连接还保持,再有相同的(ControlPath配置)连接,还能继续使用此连接。

再者,每次输入 ssh xxx 很麻烦(而且跳板机登录后还得输入 ssh xxx),当然你可以配置 alias+expext 但也不是太方便,特别是 Windows 转过来的。对此,iTerm2 本身也提供了很强大的 Profiles 功能,能一键登录目标机器(还可以设置快捷键)。

有些自己的常用机器,还可以复制一下这份基础的 relay 配置。

再加一行(Send text at start处)。

还可以把机器打个标签,然后页面上就能分组,方便选择。

还有一个,有时候会发现,有些机器一会儿没操作就会被断开 ssh,一方面可以设置 ServerAliveInterval,但作用不大,因为实际连接服务器的是 relay 跳板机。iTerm2 其实没有可以显式设置这个功能的地方,有些同学可能找到个这个配置: Continue Reading...

Python Requests Session set-cookie不生效的坑

我们知道 Python Requests库 中的 Session 模块有连接池和会话管理的功能,比如请求一个登录接口后,会自动处理 response 中的 set-cookie,下次再请求时会自动把 cookie 带上。但最近出现了一个诡异的事情,cookie 没有自动带上,导致请求 403。

一开始怀疑是登录接口错误了,没有 set-cookie,但抓包发现 response header 中有 set-cookie,打印请求的 response.cookies 也有需要的 cookie。又怀疑是 set-cookie 的格式不对或者其它问题,但用浏览器实际跑了下流程,发现系统一切正常,那基本就是 requests 库的问题了。

没办法,只能 debug 了,单步调试了几轮,基本了解了 requests 的处理方式,首先把请求参数转变为 Request 对象,然后对使用 prepare_request 对 Request 进行预处理,其中有一步 merge_cookies 的操作(还有各种其它处理),把传入的 cookies 和 self.cookies merge 到 RequestsCookieJar 对象上去,这一步也没啥问题,merged_cookies 变量也是对的。后续将预处理过的请求,通过内置的 http adapter 发送出去。http adapter 底层是通过 urllib3.poolmanager 获取到 urllib3.connectionpool 连接(这里是连接池的核心部分),再通过 conn.urlopen 实际发送请求。虽然跟踪了解到了整个请求逻辑,但最终发出的请求还是没有带上需要的 cookie。

问题定位一度陷入僵局,只能再回顾上面的流程,cookie 肯定就是在 merged_cookies 和 conn.urlopen 之间没的,再仔细观察发现,conn.urlopen 请求参数里面压根没有 cookie 字段。

查阅资料发现,urllib3 的作者说,连接池只处理底层连接,cookie 跟踪等事情应该上层来做。大胆猜测,那 cookie 应该是放在 header 里了,往前捣看看 request.headers 是怎么变动的(此时里面的 Cookie 字段确实不正确)。

再走查代码发现 prepare_request 里面是调用了 PreparedRequest.prepare,其中有一步 prepare_cookies,主要是调用了 cookielib.CookieJar.add_cookie_header 最终将 cookie 放到了 self.headers['Cookie']。但里面有个 request.has_header("Cookie") 的判断,header 中没有 Cookie 字段才会放,不知道为什么这么考虑(最新版 3.8 还是这样),估计是 merge cookie 比较麻烦,但问题确实就出在这里,这次请求之前,Requests Session 已经直接通过 headers['Cookie'] 设置了 cookie。复现代码(修改自官方示例):

虽然找到了问题的原因,但又不好解决,总不能不让直接操作 headers['Cookie'] 吧,先不说无法限制使用者,而且之前的代码已经这样做了,改动量非常之大。不过好在,现在自己用的框架是在 Requests Session 上封装了一层,操作 header 都是调用的统一的 update_headers 方法:

对 headers['Cookie'] 的操作拦截一下,变成对 cookies 的操作:

这样即不用改之前的调用方代码,也防止了后人掉坑。

 

参考资料

https://requests.readthedocs.io/en/master/user/advanced/#session-objects

https://stackoverflow.com/questions/2422922/python-urllib3-and-how-to-handle-cookie-support

https://urllib3.readthedocs.io/en/latest/reference/index.html#module-urllib3.poolmanager

https://urllib3.readthedocs.io/en/latest/reference/index.html#urllib3.connectionpool.HTTPConnectionPool.urlopen

https://docs.python.org/2/library/cookie.html

Filebeat核心配置详解

Filebeat简介

现在 ELK(Elasticsearch、Logstash 和 Kibana)日志分析系统非常火,但相关的介绍忽略了重要的一环:日志采集。虽然 Logstash 也能采集日志,但比较重、资源占用高,显然不适合线上和业务部署,所以一开始搞了个 logstash-forwarder 后来整合为 Filebeat。慢慢还发展成了一个 Beats 系列,支持采集各种各样的元数据。

Filebeat原理

说到实时查看日志,最常用得莫过于 tail -f 命令,基于此可以自己实现一个简单的日志采集工具,https://github.com/iyaozhen/filebeat.py/blob/master/filebeat.py#L237

但这太简陋了,无法保证不丢不重。我们看看 Filebeat 是怎么实现的,官方说明:https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-overview.html

简单来说 Filebeat 有两大部分,inputs 和 harvesters,inputs 负责找文件(类似 find 命令)和管理 harvesters,一个 harvester 则和一个文件一一对应,一行行读然后发送给 output(类似tail -f)。

当然还有很多细节问题,我们结合配置文件一一详解。

Log input配置详解

官方配置说明:https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-input-log.html

先看一个基本例子(下面所述基于7.x版本,6.x版本也基本适用)

inputs 可以配置多块(block),就是相同类型的放一块,这个也很好理解。paths 可以配置多个文件,文件路径和文件名都支持通配。

ignore_older 和 scan_frequency

这就有两个细节问题了,一是路径下的历史文件可能很多,比如配置了按天分割,显然旧文件我们一般是不需要的。二是扫描频率如何控制,通配设置复杂的话频繁扫文件也是很大的开销。

问题一则是通过 ignore_older 参数解决,意思就是多久前的旧文件就不管了。比如设置为 1h,表示文件时间在 1h 之前的日志都不会被 input 模块搜集,直到有新日志产生。

问题二则是通过 scan_frequency 参数控制,表示多久扫描一次是否有新文件产生。比如设置 10s(默认),一个新文件产生 10s 后会被发现,或者一个旧文件(上面 ignore_older)新产生了一行日志 10s 才发现这个文件。有人说我需要实时性,是不是这个值设置的越小越好,其实是错误的,前面我们介绍了 Filebeat 架构,input 模块只是负责发现新文件,新文件是相对已经被 harvester 获取的文件,第一次发现之后就已经在被 harvester 一行行实时读取了,所以这里基本上只影响日志切分时的实时性(这种场景下的短暂延迟是可以接受的)。

close_* 和 clean_*

那么被 harvester 获取的文件就一直拿着不放吗?文件重命名或者被删除后怎么办呢?这里重点介绍这两组配置。 Continue Reading...

Nginx+Tomcat偶现502分析

问题

业务为了负载均衡,前面放了个 Nginx,但最近 502 报警有点频繁,影响了 SLA,因此对这个问题做了较深入的研究。

502 Bad Gateway

简单来说就是 Nginx 找不到一个可用的 upstream,可能的原因有:

  1. 压根是配置错误
  2. 连接 upstream server 发生错误/超时
  3. upstream server 到了处理瓶颈

还有一个重点是轮询了 upstream server 后任然没有一个可用的。但是不管什么原因,都能在 Nginx 的 error log 中找到报错详情。

upstream prematurely closed connection

在 Nginx 中找到错误日志(开了 debug):

看了下 Nginx 代码,发现是 c->recv(); 读到的内容为 0,日志中也有显示 recv: fd:65 0 of 4096,说明没有获取到 response

同时也查了 Tomcat access 日志,请求还没到 Tomcat。看情况就像日志里面说的,连接被断掉了。

一开始怀疑是网络原因导致连接断掉了,但出现较频繁,期间内网也无网络故障,应该和网络无关。

再看日志发现报错前后有很多 keepalive 信息,尝试关掉 keepalive,502 就没了,但这会对性能有一定影响,感觉有点因噎废食了,还得继续研究。

keepalive

HTTP 持久连接(HTTP persistent connection,也称作 HTTP keep-alive 或 HTTP connection reuse)是使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,而不是为每一个新的请求/应答打开新的连接的方法。

一般 Nginx 会配置 keepalive 以提高性能:

Syntax: keepalive connections;

Activates the cache for connections to upstream servers.

The connections parameter sets the maximum number of idle keepalive connections to upstream servers that are preserved in the cache of each worker process. When this number is exceeded, the least recently used connections are closed.

可以配置每个 worker 对 upstream servers 最大长连接数量。同时这个长连接受 keepalive_requests(默认100) 和 keepalive_timeout(默认60s)配置的影响。

但 keepalive 也有缺陷,会加重 webserver 的负担,因为需要绑定一定数量的线程或者进程来维持长链接。注意 keepalive 并没有连接复用(即同一时间窗口不能处理多个请求,这个在 HTTP/2 中才实现),仅节省了新建/关闭连接的开销,类似连接池了。所以 webserver 一般都有类似 nginx 的 keepalive_requests、keepalive_timeout 配置,让空闲的连接断掉。

查看了 upstream(tomcat) 配置的 timeout 是20s,lighttpd 的 requests 是16,timeout 是 5s,都远小于 nginx 的配置。

原因就清楚了,upstream servers 先断了 keepalive 的长连接,但 nginx 仍使用了这个已经断掉的连接。

至于 nginx 为什么不主动检测一下连接是否可用呢?猜测应该是性能原因,一直检查连接池中的连接是否可用没必要,keepalive 协议本身也没业务心跳啥的。PS:商业版的 ngx_http_upstream_hc_module 可以主动监测(世界加钱可及)。

proxy_next_upstream

那么 Nginx 如何保证高可用呢?答案是重试。这就涉及另一个重要配置 proxy_next_upstream,在什么情况下进行重试,默认为 error timeout。按理说我们这个场景应该会重试,但没有重试。这是因为 Nginx 较新版本(1.9.13)多了个 non_idempotent 配置,默认 POSTLOCKPATCH 等请求不重试,因为这些操作是非幂等操作,会对服务端数据造成影响(比如发消息接口,重试可能会重复发消息)。日志中也发现 502 的全都是 POST 请求。

要完全解决这个问题就需要对 proxy_next_upstream 配置簇深入看看,我们先做个测试。 Continue Reading...

phpMyAdmin浏览数据页面卡死

我们使用phpMyAdmin经常是一进去就直接展开数据库,点击表名查看数据,这样可以清楚的了解表的结构方便写 SQL。默认情况下是显示 25 条数据,即使有几亿行也很快。

但最近我换了一个 read 账号登录,点击表名的时候直接卡死了

应该是查询卡住了,命令行连上数据库,show full processlist发现有个查询确实卡住了:

一开始只关注到状态是Creating Sort Index,再结合使用 write 账号登录打开却很快的现象,认为是 read 账号没有 create 权限,导致卡着了(没有权限的话应该是直接报错的)。但是查了下 read 账号其实给的就是全部权限,一度陷入了僵局。

调整心态,回到原点,还是从「使用 write 账号登录打开却很快」的现象出发,查看日志,发现 write 账号进入的话点击表名浏览数据使用的 SQL 没有ORDER BY,那就是自动加的,查了下,phpMyAdmin 会自动保存上一次对字段排序的操作,再次操作字段能取消排序,但问题是我现在已经无法进入浏览页面了。

那考虑是不是能在哪里设置,关闭这个。页面上有个浏览模式设置,但这个试了下好像没啥用。

还是看下官方文档吧,找到了设置项,需要把$cfg['Servers'][$i]['table_uiprefs']$cfg['RememberSorting']设置为false。这个功能还是好的,但在多人使用同一账号,数据量又比较大的情况下,还是关闭比较好,关闭也不影响排序功能,只是排序的操作不会保存下来。

Since release 3.5.0 phpMyAdmin can be configured to remember several things (sorted column $cfg['RememberSorting'], column order, and column visibility from a database table) for browsing tables. Without configuring the storage, these features still can be used, but the values will disappear after you logout.

然后还得把已经保存的设置项清除掉,位置在数据库:phpmyadmin(默认)表:pma__table_uiprefs,把这个表清空即可又回到之前丝滑的感觉了。

参考资料

https://stackoverflow.com/questions/33809816/phpmyadmin-automatically-adding-order-by-to-browse

https://docs.phpmyadmin.net/en/latest/config.html#cfg_Servers_table_uiprefs

tomcat设置remote JVM debug后stop失败

最近突然出现执行 tomcat/bin/shutdown.sh 停止 tomcat 失败的情况,报错如下:

看了一下 catalina.sh: line 365 就是 stop 那里执行失败了,然后调用系统的 kill -15 杀死进程

再仔细看看报错,发现有端口被占用、dt_socket failed to initialize 等提示,觉得应该是加了 debug 命令的原因,去掉就好了。

但这个感觉也不太好,不能因噎废食呀。搜了下相关情况,发现是 JAVA_OPTS 用的不对,应该用 CATALINA_OPTS,因为 CATALINA_OPTS 变量在 stop 的时候不会被使用(如上图)。

所以最优的做法是使用 CATALINA_OPTS 变量设置 remote debug 相关参数。

 

参考资料

解决tomcat shutdown时的地址被占用问题,https://my.oschina.net/u/1770666/blog/370620

https://stackoverflow.com/questions/11222365/catalina-opts-vs-java-opts-what-is-the-difference

现代化的PHP-写好注释

注释

有个段子:程序员最恨写注释和不写注释的人。个人认为注释主要作用是方便别人或者自己以后能快速理解这段代码逻辑和提升开发效率,这样要说的是如何使用注释来提升开发效率。

众所周知,PHP 是弱类型语言,变量的类型是可以变的,但在实际业务中,一个变量或者函数的返回值往往是确定的,确定的类型能提升我们的编码效率,也能把一些低级的错误(弱类型不代表无类型)扼杀在摇篮里。

PHP 主要通过类型声明来说明变量的类型,PHP7 开始允许更多的函数参数类型限定。比如下面这个例子,声明入参是 int 类型,但使用了 array 函数来处理,PhpStorm 能给出实时的提醒。

调用函数参数错误的话也有提示:

但 PHP 会有隐式类型转换,如上面传入浮点数的时候并没有提示错误,因为这也是合理的。不过 PHP 可以开启严格模式来检查此类错误。

PHP 内置的类型声明基本上够用了,但一般使用注释来实现更加丰富的功能。PHPDoc 是注释 PHP 代码的非正式标准,它有许多不同的标记可以使用。PhpStorm 能根据函数/类方法自动生成基本的标记:

基本标记 @param@return 也很好理解,参数和返回值,作用和 PHP 自带的类型声明差不多,主要用来限制调用方传参和使用返回值。除了这两个标记之外,还有很多其它比较好用的标记。

Continue Reading...