善意提醒

如果您打开本站很慢,布局排版混乱,并且看不到图片,那么可能是因为您还没有掌握用科学的方法上网的本领。
显示标签为“Python”的博文。显示所有博文
显示标签为“Python”的博文。显示所有博文

2024-09-29

信创踩坑笔记

图片来自网络

1. 银河麒麟(Kylin V10)

银河麒麟算是好的,毕竟高仿 CentOS。看它的文档,似乎 V4 的时候还是 Ubuntu / Debian 系的,现在的 V10 已经转到 RedHat / CentOS 派系下了。

我们的程序基本都是静态链接,所以就还好。唯一动态链接的是 OpenSSL,这个在《编译自己的 CentOS7 OpenSSL 1.0.2u 动态链接库》这篇 Blog 中已经介绍过了。

1.1. ldconfig

说起来算个坑的,就是它的 SP2 版本的 ldconfig 无法正确读取被 patchelf 改过 SONAME 的库,说什么「已被截断」。搜了一下,说是属于 ldconfig 的 Bug。patchelf 改过 soname 的 so 库,里面的段顺序会变。但规范里面并没写顺序是有保障的,ldconfig 不应该自行假设顺序一定是那个样子。

有人说这是上游供应链(glibc)的 Bug。的确无论 SP3 的哪个版本都是好的。但我看 SP2 和 SP3 的 glibc 的版本也没看出来有什么变化,这里先存疑一下吧。
我的应对方案就是 SP2 下就不改 SONAME 了,难看就难看一些。

1.2. VMware

接下来这一个实际上应该放在最前面,不过只是因为我用了 VMware 来安装这些而已。不见得所有人都会遇到。

银河麒麟 V10 安装在 VMware 上的时候,显示分辨率默认是 800×600。在这种情况下,无法调整显示分辨率,因为「保存」按钮被挡在屏幕外面了。
解决方法是用 xrandr,例如:

xrandr -s 1440x900_60

最后一个参数是刷新率(60Hz)。有些 Blog 上说这里只能用 xrandr 列出来的选项,但其实未必,只要显示器支持就行。
另外,有些 Blog 上说这种方式只能临时调整,重启后效果就没了。的确如此,可是现在屏幕大了,你就可以用 UI 来正式调整显示分辨率了,不是吗?

1.3. 9090 端口

我们有个服务,默认启动在 9090 端口上。结果等产品要上线到生产环境的时候,运维人员去启动的时候得到了报错,一看才发现 端口被人占了。
问题是我自己之前搭环境测试的时候,没有遇到这种情况。估计因为我的银河麒麟 V10 是「最小化安装」,而客户那边生产环境则没有这样做吧。

2. 达梦(DM8)

总的来说,在 Windows 上安装达梦 8 的过程要顺畅很多,没遇到什么问题。Linux 下就开始有坑。

2.1 ulimit

准备阶段的第一个坑是 ulimit 的问题,之前已经先写了 Blog,这里就只给出链接了《Linux 下修改 ulimit 不能完全生效的问题》。这个坑严格来说不算麒麟也不算达梦的问题,而且不见得所有人都会遇上。

2.2. cdrom

第二个坑是挂载光驱。挂上来之后没有 x 权限,但不知道怎么回事,用 -o exec 也挂不出来 x 权限。最后只好用命令行强制运行解决:

bash DMInstall.bin

接下来的安装倒是没出多大问题。我在 Linux 上安装达梦的目的只是为了抠出里面的 bin、include 和 drivers 目录来,用来制作 Python 和 PHP 的运行环境而已。所以后续运维的踩坑经验我就无法提供提供了。

2.3. Python

我们的 Python 运行环境是用 Conda 搭的,有 2.7 和 3.5 两个版本。按照达梦官方文档的说法(那文档写得不好,链接恕我不放了,自己搜吧),需要安装达梦客户端,然后再编译驱动。照着操作就可以,没什么大坑。

2.3.1. ldconfig

我是从一台安装了达梦客户端的 Linux 机器上把 bin、include、drivers 这三个目录抠了出来,设了一下 LD_LIBRARY_PATH,然后去 Conda 环境里面编译驱动。这样做是没问题。

但是当我把目录放 /etc/ld.conf.d 里面之后,一跑 ldconfig,sshd 直接挂掉了。数个 SSH 会话同时被踢,吓了我一大跳,还以为被黑客入侵了。

好在 tty 登录没有问题,看起来是 OpenSSL 导致了 OpenSSH 不能用了。一看达梦的 bin 目录,libcrypto.so 和 libssl.so 就这样裸躺在里面,服了。最后还是只能用回 LD_LIBRARY_PATH 方案。

2.3.2. 多线程

接下来我们尝试在 Python 环境中登录达梦数据库。有一个小坑:达梦的 dmPython.connect 会开子线程来进行登录。我们的 Python 环境在 Python 解释器跑起来后把 nproc 改成了 0,结果就 CoreDump 了。看过 CoreDump 里面的 CallStack 才明白原因。看起来 C 代码里面的异常捕获,dmPython 完全没有做嘛。抛个 Python 异常也好啊。

2.4. PHP

达梦的文档在这一部分有比较大的问题。
首先,有点语无伦次。无论是 yum 还是源代码编译,都是指的 PHP 的安装方式,但达梦的部分应该都是一样的。然而文档中却说得好像不一样。
其次,版本混乱。安装代码中同时有 7 和 5 的库,显然是更新文档的时候没有更新完整。
最后,其交代的技术细节存在着问题,至少与事实不符。我按照其中列出的 DPI 库准备的环境,PHP 的 PDO_DM 库完全加载不起来。

我想办法得到了一份正确的 so 依赖列表,比达梦文档中给出的要多,多了不少。主要是因为 libdmcalc.so 和 libdmdta.so 导致的。我觉得我直接给出我的 so 库列表并不是一个好主意,毕竟 DM8 的版本肯定也在不时地变化。更有用的是介绍我的方法。

我的方法也很简单:找个编辑器打开达梦的两个 PDO 的 so 库,然后就搜「.so」。ldd 中看得见的,这样能搜到。看不见的,那种用 dl_open 打开的,这样也能搜到。然后顺藤摸瓜就行。貌似 DPI 的那些 so 库都还不会用 dl_open。
当然,将来达梦如果对 so 的内容进行混淆或加密,那我就不敢说这样能行了。希望它不会犯这个蠢。

后面就还好了。我一度担心 OpenSSL 又惹事,但大概是我们的 PHP 环境比较简单,没惹出什么麻烦。我直接把它们塞进了一个 docker 镜像里面,这部分的工作成果总算是被固化了下来。

2017-02-03

折腾 boost::python 的一些收获

最近从事了一些通过 boost 在 C++ 中调用 Python 脚本的工作。折腾下来有一些收获,记录一下,也许也可以帮到有些人。

一、关于万能变量类型


对于习惯了 Python 的 C++ 程序员而言,boost::python::object 这个东西是一个巨大的诱惑。它让你几乎可以像在 Python 里面那样使用弱类型的变量,同时还支持数组和字典之类的复杂变量类型,并且还支持嵌套。这简直就是一个万能变量类型,有了它,常见的需求几乎都可以满足了。

而且它还快,还容易用。它其实是 PyObject* 的一个封装,也就是说 PyObject* 其实功能也一样,但是没它容易使用。JsonCPP 里面的 Json::Value 也可以「万能」,但性能与 boost::python::object 相差颇多。这真的是一个巨大的诱惑。

但是在这里,我要给大多数 C++ 程序员泼一盆冷水。在单线程下,这个梦想可能真的是事实:但是在多线程下,boost::python::object 就是一个坑!

boost::python::object 为什么快?因为它基于 PyObject*,具有引用计数,所以赋值才飞快,浅拷贝嘛。但是引用计数的问题就是线程不安全。

当然,光是引用计数本身不会导致线程安全性问题,导致问题的是引用计数带来的临界区对象引用问题,而归根结底,是 Python C API 的「并发问题不归我管」的思路。boost::python 的封装机制使得对象引用不好控制,也不太可见。想法是好的:使用者不需要关心这些。但现实就很无奈了。

所以,尽管很不甘心,关于万能变量类型的实现,我还是老老实实地用回了 Json::Value。

二、类型转换与判断


要从 boost::python::object 转换成 C/C++ 原生变量类型,一般用 boost::python::extract<T>。转出来的对象调 check() 就可以确定是不是正确的类型。转 boost::python::list 或 boost::python::dict 也是一样。

有一点需要特别注意的是:用 boost::python::extract<bool> 的时候,很可能得不到你预想的结果。你会发现,int 也被转成 bool 了(check() 返回 true),反过来也一样。这个不是 Bug,是 boost::python 故意这样搞的。BOOST_PYTHON_BOOL_INT_STRICT 宏可以解决这个问题(必须改动 boost::python 源代码并重新编译,因为相关代码在 cpp 里面不在头文件里面),它使得 int 和 bool 将被严格区分。

但是 boost::python 之所以把 int 和 bool 混起来用也不是没有道理的。这样使得 MFC 里面的那些 BOOL(不是 bool)类型的函数形参(和返回值)可以直通 Python,封装起控件来尤其方便。如果你设了 BOOST_PYTHON_BOOL_INT_STRICT,就不能这样干了,必须显式转换。左是一刀,右也是一刀。你们自己掂量吧。

反正我是没有去加 BOOST_PYTHON_BOOL_INT_STRICT,而是自己通过 _stricmp(value.ptr()->ob_type->tp_name, "bool") 搞定的。

三、清空 boost::python::list


用惯 STL 刚开始用 boost::python 的人可能会头大——怎么 dict 有 clear() 但 list 没有?
答案很简单,因为 Python C API 没提供,所以 boost::python 就没有。

说到底,boost::python 就是对 Python C API 的一个封装而已。你如果去看 boost::python 的源代码,甚至会发现里面不少的「成员函数」其实是跑去执行了一句 Python 脚本。不了解细节的 C++ 程序员很容易在这种地方栽跟头,所以我觉得 boost::python 绝对不会是一个终极形态。

要想清空数组,并不是完全没有办法。我找到的一个办法是 boost::python::delitem(list, boost::python::slice())。并发调用时记得用本文最后一节的办法加锁。但是我是真的不建议在多线程环境下用 boost::python,太多坑了。如果实在要用,在完成了 Python 脚本调用,把数据转换好之后,就赶紧离开 boost::python 区域吧。

四、boost::python::object 深拷贝


boost::python::object 默认的赋值操作都是作浅拷贝,极快。然而,有的时候需要深拷贝怎么办呢?

简单数据类型直接 extract 出来就是深拷贝(传值)了。会有问题的仅仅是复杂数据结构,具体地说,list 和 dict。

dict 又是有提供 copy(),理由还是——Python 里面的 dict 有 copy(),而 list 就没有。我尝试过最简单的办法,就是把一个 list 放到一个 dict 里面去 copy() 完再拿出来用。这样肯定可以达到目的。至于有没有更好的办法呢?我反正是自那之后就弃坑了,你们去研究吧。

五、多线程并发加锁


C++ 调 Python 的时候的加锁是一个不小的话题。不过这方面中文资料也算不少,有兴趣可以去搜来研究。我这里就简单地说一下跟 boost::python 相关的部分。

加锁是用 PyGILState_STATE,像这样:
PyGILState_STATE gstate;
gstate = PyGILState_Ensure();
……    // 调用Python脚本
PyGILState_Release(gstate);
这种例子网上很多,就不细说了。
关键在于,我前面也提到了,boost::python 里面很多看起来是 object 的成员函数的东西,其实都是对 Python 脚本的调用,所以都得加锁。

事实上,只要你用到了 boost::python 的地方,都得加锁。不管是 module 和 call 这种看起来就跟「调用」二字相关的,还是在对 boost::python::object 作数据处理,看起来人畜无害的,都得加锁,无论读 / 写,否则你就等着 Crash 吧,特别是服务端程序。

这也就是我一再警告避免在多线程下使用 boost::python 的原因。当然,我这边是不得不用,只能小心翼翼,如履薄冰,然后尽量控制不要让菜鸟程序员去写这种代码。由此可见,隐藏了实现细节并非全是好事,也并非全是对新手有益。

2015-03-11

在 Windows 上用 x64 的 Python 运行 GoAgent

首先声明:本文适用于至少懂编程,并且爱折腾的人。不一定要很熟悉 Python,但至少应该入过门了。

GoAgent 自带 Python27 运行环境,不过因为本机上也安装了 Python 2.7 x64,所以希望能直接使用我安装的 64 位版本。总结下来,要点有:
  • 无论你是否准备使用 x64 版 Python。GoAgent 与 Python 2.7.9 不兼容,应该安装 Python 2.7.8 版本。
  • 要运行 GoAgent 还需要 gevent 和 pyOpenSSL。可以手动安装,也可以通过 pip 来安装。推荐后者因为省事。
  • Python 2.7.8(无论是否 x64)及更早的版本,没有自带 pip。所以要去下载 get-pip.py,然后用 python 运行这个脚本安装 pip。
    ps: 用 get-pip.py 下载到的 pip.exe 的版本,比 Python 2.7.9 里面带的要新一些。所以就算有自带我也会换掉它。

另一个重点来了:
有些版本的 GoAgent 里面对于 crypt32.dll 的加载代码在 x64 的 Python 中有问题,需要修改。改成像这样就可以了:
crypt32 = ctypes.WinDLL(u'crypt32.dll')
crypt32_handle = crypt32._handle
释放的时候要这样:
ctypes.windll.kernel32.FreeLibrary.argtypes = [HMODULE]
ctypes.windll.kernel32.FreeLibrary(crypt32_handle)
在前面还要:
from ctypes.wintypes import *
否则 x64 下会报错,因为 HMODULE 和 long 的长度在 x64 下不一样了。

我用的 2.1.17 的 GoAgent 中,对于 crypt32.dll 的加载代码还位于 proxy.py 中。最新的 GoAgent 已经移到 proxylib.py 里面了。什么时候移的我没有去考察过。代码改法可能略有不同,不过思路应该是一样的。

折腾好以后。把本机上的 Python.exe 复制一份改名成 Python27.exe。接下来就可以把 GoAgent 自带的 Python27.* 给删掉啦。

2013-09-05

GAE 升级 Python27 折腾手记

Google 把 GAE 的 Python 运行环境从 2.5 升级到 2.7,已经有好一些日子了。因为只扔了自己的两个小小的应用在上面,所以一直没去处理。最近总算有些时间,于是把这个折腾完了,还很花了一些工夫,特记录一下。

去看了 GAE 的在线文档。中文的文档是只有 2.5 的部分,繁体中文也只是对 2.7 给出了一个链接而已。技术方面的东西还是直接参考 英文资料 来得好。

具体的做法,官方文档上写得很清楚,就不在这里说了。但是我的升级过程并不是很顺利。最后总结一下有这么几个问题,供大家参考:
  • 按照文档,app.yaml 里面的 handle script 要从 .py 改成 .app,但千万别手贱去把 .py 文件真的改名成 .app。其实只是 app.yaml 里面要改而已,文件还得是 .py 为后缀名。
  • app.yaml 中还要引入 webapp2 这个 library,这个在官方文档中没有明确地指出。具体写法是:
libraries:
- name: webapp2
  version: "2.5.1"
其实 version 可以写 latest,但要承担最新库无法向下兼容的风险。
  • 要更新就索性别留旧版(Python 2.5)的程序在 GAE 上,否则页面顶部的警告一直会有。
其余过程还算顺利,至少升级到 2.7 算是成功了。

2013-04-10

修改 GoAgent 客户端以支持 Mega

为了能用来访问 Mega,对 GoAgent 客户端代码做了略微的修改。不过首先要说明一下为什么会有这个修改。

Mega 是个总部位于 New Zealand 的网盘服务。服务器当然全世界都有,但至少在我这边 ping 值不好。严重的时候 600ms 以上,并且丢包。这样的话,不管本地有多少带宽,实际上也是不可用的。总不可能花上一整天的工夫来传一个 ISO 吧。
开着 VPN 会快,但流量和费用都是问题。于是很自然地想到了是否可以通过 GoAgent 之类的 GAE 代理来访问。Google 服务器与 Mega 之间的带宽应该是不成问题的,而 Google 服务器与我之间的速度也是我可以在一定程度上控制的。不过测试下来发现 GoAgent 不支持 OPTIONS 这种 HTTP Method,而且这个局限性是 GAE 导致的。GAE 只支持GET / POST / HEAD / POST / DELETE 这五种 HTTP Method。偏偏 Mega 在登录和上传下载的时候都会发 OPTIONS 请求,于是这个方案一度被搁置了。
后来 Mega 的速度进一步下降,有时候一整天都传不完一个 100M 的文件。于是这个方案又被我拿出来考虑。这次我准备绕开服务端的限制,直接从客户端下手。OPTIONS 请求涉及的数据量是很小的,文件传输用到的 CONNECT 之类才是主要的带宽压力。因此可以让客户端在遇到 GAE 服务器无法处理的 HTTP 请求时,直接将其发到目标服务器。由于 Mega 目前还没有被 GFW 给 IP 黑洞,因此应该可以在一种「混合模式」下被通过 GoAgent 访问到。

下面介绍一下修改方法,以 GoAgent 2.1.15 版(2.1.17 还需要服务端改动才行)为例:

首先在 local/proxy.py 中找到这两行:
"""rules match algorithm, need_forward= True or False"""
need_forward = False
第一行是注释。而下面这个 need_forward,就是用来控制是否把一个请求直接送出(FWD),而不是送去 GAE 服务器进行中转。

在后面的 if 语句前,加入这样的内容:
if self.method != 'GET' and self.method != 'POST' and self.method != 'HEAD' and self.method != 'PUT' and self.method != 'DELETE':
    if host not in http.dns:
        http.dns[host] = list(set(http.dns_resolve(host)))
    need_forward = True
非 Python 程序员也应该很容易读懂这段代码,不过要提醒一下:Python 中缩进是很关键的,改代码时一定要用空格正确地缩进。
最后,别忘了把下面那个原来的 if 改成 elif。

这样改过之后的 GoAgent 客户端,在遇到 GAE 服务端不能处理的那些 HTTP 请求类型时,就会把它们直接发到目标服务器上。
从理论上讲,这个小修改不会对 GoAgent 的翻墙能力有任何的增加,但可以让它具有更大的适用范围。一些原来不能用 GoAgent 访问的站点(比如上面提到的 Mega),现在可以用它来访问了。GAE 的流量按天计算(VPN 一般按月,VPS 也是)。并且因为可以使用多个 GAE 账号,因此流量基本上是免费且无限的。Mega 那 50GB 的大空间,终于具有一定的可用性了。