极优化|一次入库性能优化过程
SaaS形式的地理大数据平台,为我们应用地理数据提供了极大便利性,对地理数据应用场景也带来了更大的想象空间。但是要面临一个问题,数据要上传到云端(当然敏感数据可以在私有云使用)。这个过程有几个关键点需要解决,比如入库效率、中文编码、不同坐标系、不同的文件格式等等。最近一次迭代对GeoJSON大文件入库性能进行了优化,以一个135MB的GeoJSON为例,占用内存从2.5GB+优化到100MB以下。最终优化方法很简单,仅仅是对使用到的GDAL库进行了升级,但是这个问题的严重性以及分析问题、解决问题的过程蛮有意思,记录下来供参考(文末有彩蛋)。
极海云一位房地产行业用户,用正方形格网对目标城市进行覆盖,格网内统计不同的指标,用于对城市进行量化评估,最终用于支撑房地产投资决策。格网数量28万+,数据保存成GeoJSON格式,文件体积135MB。在极海公有云 (https://geohey.com) 上传失败。此次优化完成后数据上传成功,用户制作专题图如下图所示(截图来自用户公开的地图链接)。
通过系统日志发现(如下图),数据入库过程中由于占用内存过大(约2.6GB),在系统内存不充分的情况下,数据入库进程OOM评分高,触发OOM Killer,被Kill掉了。极海云采用Docker和Kubernetes对微服务进行管理和运维,对每个容器所能使用的内存进行了限制。在测试环境,通过对Docker容器限制内存使用量,这一现象得到了复现。
OOM(Out of Memory Killer)是操作系统为保证有足够的内存可用而不至于崩溃,以最小代价Kill掉内存占用多的进程,是对操作系统的一种保护机制。操作系统会为每个进程打分(oom_score),占用内存越大生命周期越短则评分越高,越被优先Kill。如上图所示,入库进程评分278,被操作系统Kill。
极海云微服务运行在Kubernetes中,这一问题会变得复杂和严重。我们为每个微服务设定了所能使用的资源上限,使用内存超过这个上限,会被Kill;如果把资源上限抬高,那么在Kubernetes在为微服务寻找节点时,可能找不到有足够资源的节点,导致服务无法启动出现异常。根本的解决办法是把内存使用限制到一个可接受的范围,并且内存占用量和数据量的大小无关。
GeoJSON是一种针对地理空间数据的标准格式,本质上是一种JSON结构的文本文件。GeoJSON有诸多缺点,比如体积大、难以按流(streaming)的方式读取和解析等,好处是GeoJSON易于人来读取和理解,所以在商业领域比较常用。一些库的早期版本通常采用把整个数据读取到内存进行解析的方式,因为这样做简单直接,我们使用的GDAL2.2中的GeoJSON驱动也是如此。这样就造成一个135MB的GeoJSON文件解析起来需要占用2.5G+的内存资源。
本着你遇到的问题别人也可能遇到过的原则,发现在GDAL的2.3.0发行版修复了这一问题。的确如前面所描述的,GDAL2.2以前的版本读取GeoJSON要占用文件本身20-30倍大小的内存。浏览源码发现新版本使用了流(streaming)读取的方式控制内存占用量不超过100MB。到这一步,解决问题就简单了,升级GDAL即可。升级过程涉及到源码编译及构建镜像,这是一个需要耐心的过程,具体不再表述,下一小节直接给出编译源码及构建镜像的Dockerfile文件。
极海云采用微服务架构,采用K8S对微服务进行运维管理。为了使镜像体积最小化,采用Alpine Linux作为基础镜像的操作系统,这样一般的微服务打包成镜像体积可优化到20MB左右。但是在我们制作数据入库程序的镜像时,由于使用了依赖GNU Libc的库,Alpine并不兼容,而Alpine的维护者认为在Alpine中使用GNU Libc这很不符合Alpine的3S(Small/Simple/Secure)哲学。所以我们放弃了这种“不符合哲学”的行为,转而使用CentOS作为基础镜像的操作系统。当然,我们要忍受下镜像体积略大,优化后我们构建了70MB左右的基础镜像。
如何更优雅的构建镜像,优化点很多,从我们的实践来看,最为有效的方式是多阶段构建(Multi-Stage Builds),尤其是涉及到需要从源代码编译,会产生大量最终并不需要的文件的情况来说。简单来说就是在一个镜像上下文中进行构建,让后仅仅把最终需要使用的文件COPY到最终保留的镜像中。下方是构建包含了GDAL 2.4.2的镜像构建文件,采用多阶段构建的方式,供参考。
FROM centos:7.4.1708 as builder
LABEL author="cuifd@geohey.com"
WORKDIR /tmp
ENV GDAL_VERSION=2.4.2 PROJ_VERSION=5.2.0 MAKE_JOBS=4
RUN yum -y install gcc gcc-c++ automake autoconf make
COPY ./ ./
## build proj
RUN tar zxvf proj-${PROJ_VERSION}.tar.gz && \
cp proj.4/* proj-${PROJ_VERSION}/src/ && \
cd proj-${PROJ_VERSION} && \
./configure --prefix=/usr --disable-static && \
make -j ${MAKE_JOBS} && make install && make install DESTDIR="/build_proj"
## gdal needs postgresql libs
RUN yum -y install postgresql postgresql-devel
# build gdal
RUN tar zxvf gdal-${GDAL_VERSION}.tar.gz && cd gdal-${GDAL_VERSION} && \
./configure --prefix=/usr --without-libtool \
--with-hide-internal-symbols \
--with-libtiff=internal --with-rename-internal-libtiff-symbols \
--with-geotiff=internal --with-rename-internal-libgeotiff-symbols \
--with-pg \
--enable-lto && \
make -j ${MAKE_JOBS} && make install DESTDIR="/build"
FROM centos:7.4.1708 as final
# install nodejs
RUN curl --silent --location https://rpm.nodesource.com/setup_8.x | bash - && \
yum -y install nodejs && \
yum -y install epel-release && \
yum -y install unar && \
yum clean all
# copy libpq
COPY --from=builder /usr/lib64/libpq.so.5 /usr/lib64/libpq.so.5.5 /usr/lib/
COPY --from=builder /build_proj/usr/share/proj/ /usr/share/proj/
COPY --from=builder /build_proj/usr/include/ /usr/include/
COPY --from=builder /build_proj/usr/bin/ /usr/bin/
COPY --from=builder /build_proj/usr/lib/ /usr/lib/
COPY --from=builder /build/usr/share/gdal/ /usr/share/gdal/
COPY --from=builder /build/usr/include/ /usr/include/
COPY --from=builder /build/usr/bin/ /usr/bin/
COPY --from=builder /build/usr/lib/ /usr/lib/
# set LD_LIBRARY_PATH for gdal
ENV LD_LIBRARY_PATH=/usr/lib
从发现问题、分析问题到构建镜像、更新到生产环境,耗时5天。中间有个小插曲,GDAL目前最新版本是3.0.2,我们想一步到位升级到最新版本。升级过程中发现GDAL 3.0.2进行了全新的架构调整,使我们对Proj.4做的扩展无法兼容(这一部分的升级和测试花去了宝贵的2天时间),最终采用了GDAL 2.4.2版本。总结起来,有趣的是,整个优化过程没有重构一句代码,但貌似也只有开发工程师最适合完成这次优化。
云原生正日益流行,极海云早已从传统架构迁移至K8S环境,朝着云原生不断迈进。微服务、容器化、云原生,让程序开发者面临的开发方式和交付方式发生着巨大变化。这是新的挑战—编码之外,需要了解更多。极海采用DevOps的方式组织生产和运维,开发工程师遇山开山遇水搭桥,前端开发/后端开发/数据库/容器化/K8S/DevOps...,都练就成了斜杠工程师。目前极海正在招聘开发(斜杠)工程师,如果你也想“被虐”,联系我们(hr@geohey.com)。