最近的几个项目对于 IP 查询需求愈发强烈,使用免费数据库和在线接口已经不能够满足我们的需求。于是我们使用了阿里云(IP地理位置库)淘宝IP地址库首页推荐)的离线版本,在使用过程中发现了一些文档的不足,缺失了容器场景的使用案例。

本篇记录 Python 版本 SDK 的踩坑过程,希望帮助后面的同学节约折腾时间。另外希望看到本文的产品 PD 同学可以尽快推动文档完善。

写在前面

查询 IP 的场景分为在线和离线两种模式,对于有大量大尺寸访问日志的场景,一般因为以下原因,会放弃在线公有云 API 调用:

  • 对于机器资源要求颇高,需要更多的节点、更大的带宽
  • 跨区域存在很高的时延
  • 需要针对存量日志进行流式处理或者切分,执行效率上也有着非常大的限制
  • 服务需要额外的运维成本

所以需要转变思路,寻找更高效的解决方案:

  • 内网部署,减少网络开销
  • 简化接口实现,提升执行效率
  • 使用集群提供更高性能的服务能力

官方目前提供了三个版本的 SDK (Java 、Python 、C++),下载了解发现 Python 和 C++ 版本本质都是 C++ 实现,从性能角度考虑,我们放弃了 Java 版本,先从 Python 版本入手。因为当前生产服务都使用了容器部署维护,所以这里也需要考虑容器化实现。

基于官方 Python SDK 进行容器化封装

因为在服务器使用,所以这里需要参考官方 Python SDK (Linux环境)的文档:https://help.aliyun.com/document_detail/182378.html,截止本文产生,文档更新日期为 2020-10-10 14:22:40

文档中虽然提示可以在 Linux x86_64 环境使用,虽然提示兼容列表未指明(RHEL/ CentOS/Ubuntu),但是本着绿色环保,还是要试试是否能使用 Alpine 启动执行,毕竟占用资源更低,更加“节能环保”嘛。

因为IP库依赖 botan2 ,所以尝试 RUN apk add botan2,发现:

虽然在Alpine官方找到了 ** botan-dev** 这个库,包含 **botan2**,但是版本是 2.11.0-r5,低于阿里云文档中要求的 2.13.0 ,只好另辟蹊径。

封装 Python 版 Alpine 容器镜像

官方推荐使用源码构建的方式来安装 botan2,那么我们就按照这个方式来吧。阿里云文档中记录的依赖编译方式也很简单,下载源码执行命令即可:

但是这里有一个小问题,botan 作者在 GitHub 开源仓库未曾发布过 tgz 版本的压缩包,这个地址是错误的,需要使用 https://github.com/randombit/botan/archive/2.13.0.tar.gz,将源码下载到本地稍后构建镜像时使用。

这里直接给出 alpine 环境下编译 botan2 的 Dockerfile,已配好基础构建工具环境:

倒杯水等编译完成,执行 find / | grep botan | grep -v /app 会看到 botan 安装包含以下位置:

  • /usr/local/share/doc/botan-2.13.0/*
  • /usr/local/lib/libbotan-2*
  • /usr/local/include/botan-2/*
  • /usr/local/bin/botan

构建核心依赖完毕之后,就可以处理程序了,这里有一个 trick 的地方,我们使用了官方 Python SDK 中的 Ubuntu 环境的 libgeoipclient.so ,如果是基于 Alpine 构建,性能会更好(和官方沟通,暂时没有这个系统的构建产物)。

官方 SDK GeoIpClient.py 中声明了引用 libbotan 的地址,默认是将文件放置于相对目录ubuntu内,为了符合直觉,我们将该文件中的地址目录修改为 lib,并根据自己情况删除掉了 Python2 的兼容实现(都2020年啦)。同样,因为容器内使用,去掉了一些没有必要的系统库引用(示例,实际使用结合当时最新的官方 SDK 修改即可)。

接着将官方文档中的示例也简化一下:

将上面的内容保存为 example.py 和 SDK 放置到一起,准备继续封装容器使用:

执行 docker build -t alidns-geoip:python-alpine3.12 . 完成镜像构建,再执行 docker run --rm -it alidns-geoip:python-alpine3.12 会看到程序已经可以顺利运行了。

优化 Alpine 镜像尺寸

Python 官方容器 python:3.9.0-alpine3.12 只有44 MB,但是观察我们构建出来的镜像,会看到镜像居然有 396MB 的巨大身材,完全丧失了使用 Alpine 的优势,所以需要继续进行一些基础优化。

使用多阶段构建大法,先将编译 botan 和我们的运行环境分离。

构建完毕,整个容器镜像会从接近 400 MB 的庞然大物瘦身为 56.7MB ,相对可以接受了。

这里进行一些补充说明:

  • 因为没有 gcc / g++ 等工具,所以需要安装 libstdc++工具包,让 c++ 编译等程序能够正常运行。
  • 因为构建阶段安装目录为 /usr/local/lib/,所以这里最好也将构建阶段产物复制到新容器相同的目录,避免 Python 加载动态链接库后报找不到文件到错误,当然,Python SDK 中的类库取值也需要改为 botan_lib = '/usr/local/lib/libbotan-2.so.13'
  • 授权文件和离线数据库使用文件 mount 的形式使用即可,方便后续持续更新官方数据库。

封装 Python 版 Ubuntu 容器镜像

和官方维护的同学沟通,得知对方使用 Ubuntu 虚拟机进行该版本的库文件编译,考虑到持续使用,这里也对 Ubuntu 版本进行镜像封装,如果后续基础库不兼容 Alpine 运行,不影响已上线业务继续使用。

这里直接使用官方 Python SDK 离线包里的文件即可:

验证容器内基础查询性能

分别进入 Ubuntu 和 Alpine 封装的容器执行 time python example.py,会得到类似下面的结果:

这个执行结果对于低频率的请求或许还能接受,但是对于要处理大量日志数据时,就完全不可用了,差不多一秒只能处理一个半请求,** QPS 为 1**。

究其原因,如果使用默认的 SDK 示例,每次调用应用,都会使用 Python 先从硬盘加载数据文件,以及加载加解密库对数据文件进行解密,然后再进行数据查询,推测如果内容可以一直在内存里,可以减少大量IO(可能还有解密)计算,节约不少时间。

那么就来试试看吧。

使用 CS 模式优化程序性能

把使用模式变成 CS 模式,最简单的便是启动一个 Server,对外提供某种协议让外部进行通信调用,以 Python 3 著名的 Web Server 示例代码为模版进行简单修改:

可以看到,上面这段不足 50 行的代码里,基本只做了三件事:

  • 在端口 8000 启动一个Web 服务,处理路径中包含 IP 字符串的请求
  • 验证请求IP格式是否有效
  • 调用阿里云官方 SDK 进行IP数据查询

然后使用官方示例中的 IP 执行 time curl localhost:8000/47.116.2.4 分别在两个系统镜像启动的容器中进行测试。

Ubuntu 镜像的测试结果类似下面:

可以看到最差情况下(第一次)的时间缩短到了原本的 1/3,而后续两次调用更是缩减到了 0.1 ms(感谢 Python 强大的 Cache 机制),也就是说单实例 QPS 提到了 5 ~ 100,这个数据看起来能够满足小样本离线/内网环境进行数据分析,但是数据量如果再大一些,还是会有问题。

于是继续使用 Alpine 镜像进行验证,然而 Alpine 镜像会出现一些 Bug,只有第一次调用会有返回,后续调用会 hang 住:

虽然使用端口映射,我们在容器外可以正常使用,但是因为上面测试过程中的奇怪问题,即使我们封装了小巧的 Alpine 镜像,暂时也不好使用其在生产环境处理大量的数据。只好退而求其次使用 Ubuntu 版本的镜像,希望官方后续会推出 Alpine 环境的程序库吧。

最后

本篇我们对官方 Python 进行了一些简单的修改和扩充,结合容器进行了镜像构建,实现了使用容器调用阿里云地理位置库。

翻阅之前的博客文章,你可以使用流行的水平扩展容器实例数量来提升 QPS ,解决稍微大一些的数据量的分析,但是如前文提到的,你将不可避免的会遇到切割日志、不得不添加机器数量,甚至添加异步队列等方式来解决这个简单的 IP转换 需求。

下一篇,将从另外一个角度入手,进一步解决处理超大尺寸日志时的性能、维护成本的问题。

–EOF