趣店容器进化史

简介

> 趣店的容器化进程经历过三个里程碑:docker、单集群脚本化管理、多集群平台化管理。为了兼顾平常业务的需求开发,每个里程均是由小部分人主导推进,由点及面地进行推广,并经过在小范围的试错中寻找最适合趣店业务场景的容器化方案。容器化为趣店的服务隔离及服务器统一化管理提供了基础条件,而且经过容器化迁移为趣店每个月节省至少10万元服务器费用。(因为迁移工做以PHP服务做为试点,所以本文中的案例亦是以PHP为主)php

趣店容器进化史快速预览图mysql

Docker

> 做为容器化推动的第一阶段,此阶段由开发主导,推广开发及测试环境容器化使用,并进行小部分服务线上容器化试用。linux

Docker入门

> 容器化推动初期,此时咱们内部对于容器较为了解的人员并很少,开发不知道应该如何使用容器,运维对于如何维护容器下的服务也没有经验,所以在这个阶段咱们着重对全体开发人员及运维人员进行初级容器入门分享,分享主要包括如下几个方面:nginx

  • Docker环境搭建

> 主要用于引导开发人员搭建本地Docker开发环境,进行初步的容器概念建模。git

  • Docker命令解析

docker命令解析分享资料github

> 该分享主要讲解Docker的经常使用指令、拆解容器的部署流程并简要介绍经过Swarm进行集群部署的方式。web

  • Dockerfile最佳实践

> 参考《Best practices for writing Dockerfiles》,分享如何以更优雅的方式编写Dockerfile。redis

Docker编排

> 咱们的部分开发人员尝试更深层次地应用容器化,例如基于docker-compose推广docker在本地开发环境落地。这一推广对于微服务一类单个项目依托于多个服务的开发环境部署提供了极大的便利,同时也在开发环境的使用中进一步深化你们对容器的理解。在这一阶段开发了简易的K8s编排脚本,对新上线的小服务尝试使用K8s部署服务。sql

单集群脚本化管理

> 考虑到容器化仍处于尝试阶段且须要进行定制化脚本开发,所以第二阶段还是以开发做为主导。本阶段开始对主要服务的小流量环境进行容器化迁移,经过开发更完善的K8s编排脚本以优化服务的持续集成与部署。docker

容器化服务迁移

> 随着全员对容器认知水平的提升,在这一阶段咱们的小部分开发开始尝试进行线上小流量环境的迁移,迁移过程也曾遇到一些问题。

  • CoreDNS负载异常致使部分请求错误

> 现象:在这一阶段的迁移过程当中因为K8s的CoreDNS负载异常,咱们已迁移服务曾出现短暂的不可用(因服务分区部署的关系咱们及时将部署于K8s服务的服务流量摘除) > > 解决方案:容器化迁移是各方(运维、开发、K8s服务提供商)的磨合阶段,在这一阶段应提早准备及演练运行于K8s的服务异常状况下的流量切换方案。因为业务服务对K8s基础服务的强依赖关系,基础服务的监控、异常转移均需提早完善及演练。

镜像管理

> 镜像管理做为容器化迁移不可或缺的一部分,自建的镜像仓库可以更好的保障内部服务镜像的安全性(镜像可能包含服务源码),且部署于内网的镜像仓库可以极大提升部署速度。为简化镜像的管理与维护,咱们在内网部署开源的Harbor服务管理内部镜像。

CI/CD

> 在这一阶段咱们经过自研的脚本(集成编排文件生成、镜像构建、部署)及Jenkins实现服务的CI/CD。因为这一阶段的CI/CD流程还是试验阶段并没有十分完善,这里暂时不展开叙述,较为完善的流程可参考下一阶段迁移的CI/CD。

日志收集

  • 编排日志

> 编排日志目前咱们没有特地收集,大部分状况下仍是部署或者调度出现问题的时候由运维进入集群内经过Kubectl查看日志状况。

  • 容器日志

> 因为大部分服务的日志都是往指定目录输出,目前并无很好的利用容器的标准输出做为容器内部服务日志输出的统一出口,因此容器日志当前仍处于待挖掘阶段。

  • 服务日志

- Nginx

- PHP

> 除去常规的Nginx access_log,咱们在迁移过程当中还须要重点关注Nginx error_log及PHP error_log,极少部分请求可能会因迁移过程当中的操做不当而引起异常,此时可经过排查服务的错误日志及时发现并修复问题。

  • 业务日志

> 因为咱们的业务日志输出并没有统一规范,所以没法经过常规的容器标准输出采集日志,而是经过Volume的方式将Pod的输出日志挂载至节点主机目录,再经过节点主机的Filebeat + Kafka将日志统一收集至日志服务器。

监控

  • 宿主机资源监控(Master、Node)

> 主机的资源监控包括:CPU、内存、磁盘、网卡流量等等,尽量详细地收集主机监控信息对于异常状况下的问题排查有着极大的帮助。

  • 基础组件监控(如:CoreDNS)

> 围绕于集群服务的各类基础组件:kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy、CoreDNS等等,也须要归入监控范围,避免由于单个基础组件的异常影响整个集群内部业务服务的稳定性。

  • Pod

- Nginx

- PHP-FPM

> Pod部署了可用于输出Nginx-FPM和PHP实时状态的Exporter,经过常规的Prometheus + Grafana方案实现K8s服务的监控。

网络拓扑

  • NodePort

  • Service

  • Pod

> 在这一阶段考虑到现有服务是逐步迁移,为保持原有线上灰度测试方案的可用性,并未使用常规的Ingress做为外部流量的入口。

多集群平台化管理

> 最终阶段咱们基于开源平台进行二次定制化开发,由运维、开发共同主导。这一阶段的主要工做是经过定制化开发打通 开发-测试-审批-线上部署 的完整流程,并对现有的线上服务全量迁移至K8s集群。

开源平台选型

  • Wayne(360)

  • Rancher(Rancher Labs)

  • KubeSphere(青云)

  • tke(腾讯)

K8s多集群管理平台对比

> 在最开始的开源平台选型阶段咱们综合对比了目前较为主流的4大开源平台:Wayne、Rancher、KubeSphere、tke,因为咱们现有业务均为多区部署所以平台是否支持多集群管理成为咱们最重要的考察因素。各项因素综合对比后最终咱们选用Wayne做为基础进行二次定制化开发。可是因为咱们基于Wayne开发的版本360团队有较长时间未更新维护,致使最新版须要修复少许bug后才能正常使用。 > > 说明:此对比截止时间为2019年12月,此期间各平台可能有新的功能迭代

网络拓扑

  • Ingress

  • Service

  • Pod

> 因为咱们的服务大部分为微服务,继续使用Nodeport的方式每一个项目均须要占用大量的集群端口号,所以在全量服务迁移阶段咱们调整为使用常规的Ingress做为外部流量的入口。

CI/CD

> 在这一阶段咱们进一步对CI/CD流程进行了完善,镜像经过CI Runner的方式自动构建,减小上线过程的等待时间,并经过界面化的方式完成多集群部署,打通从镜像构建、审批、部署上线的完整流程。

  • 镜像构建流程

镜像构建流程

> 由上图能够看出,经过Gitlab的CI流程咱们完善了代码合并后自动构建镜像并推送镜像至镜像仓库的流程。在K8s接口化的服务端咱们已提早配置好每一个服务的Deployment基础模板,构建成功后调用接口写入对应版本信息便可生成待发布的Deployment模版。

  • 代码上线流程

代码上线流程

> 因为咱们的代码上线过程须要监测每次上线是否会对线上数据形成波动,所以上线环节全程由开发手动在平台化后台操做没有实现全流程自动化。

  • 配置上线流程

ENV上线流程

> 配置上线则相对简单大部分配置变动后只须要重启Pod便可,所以这一部分作了自动化处理。

平台化服务迁移

> 平台化服务迁移对于运维的工做量较大,因为各服务配置差别较大,运维须要根据每一个服务的不一样配置Deployment基础模板。而咱们数百个微服务因为种种历史缘由没有保持环境统一,运维梳理环境迁移服务的过程当中容易疏漏一些细微的环境配置差别,有些差别可能又是在小部分场景下才会触发异常,所以也列出来便于你们避坑。

  • Pod可用链接数不足预期

> 现象:在线上压测过程发现部署于K8s中的服务当单Pod QPS达到1万左右开始出现TCP链接异常,没法继续增压。 > > 解决方案:单Pod可用的链接数极大的依赖于节点服务器,单Pod没法支撑更大链接数时需考虑调优各节点服务器的内核参数,如调整最大打开文件限制(包括用户级别与系统级别)、最大追踪TCP链接数、系统TIME_WAIT数量等。

  • 单行大日志

> 现象:Filebeat采集的日志中出现部分业务日志丢失。 > > 解决方案:因为Kafka对单条消息大小的限制,若是单行日志过大会致使日志没法被采集,此时应规范业务日志的输出,避免出现单行大日志。

  • 上传文件/POST大小限制

> 现象:流量从物理机器迁移至K8s后部分服务请求出现 HTTP Code 413 或下游服务接收到的请求数据为空。 > > 解决方案:Nginx及PHP-FPM层面对上传文件大小、POST body大小均有限制,所以须要将限制大小配置值调整至与原物理机器一致。

  • 服务内存大小限制

> 现象:服务从物理机器迁移至K8s后部分计划任务没法正常执行,部分后台异步导出队列执行异常。 > > 解决方案:一般状况下咱们会使用一台物理服务器同时部署服务喝执行计划任务,而大部分计划任务、队列可能须要使用大量的内存用于统计之类的逻辑,此时应调整K8s计划任务及队列Pod的内存上限限制,同时可能还须要修改PHP的内存大小限制,并视计划任务状况调整最大执行时间避免因计划任务超时触发失败重试。

  • 部分节点资源负载异常

> 现象:单K8s集群中出现小部分节点资源负载较高,而其他节点较为空闲。 > > 解决方案:此时可经过K8s的反亲和性配置将重资源的Pod分散部署在各节点服务器中,避免小部分节点服务器同时部署重资源Pod出现资源争抢。

基础镜像调优

  • 理论与实践(单服务容器 VS 多服务容器)

> 对于单Pod是部署单服务仍是多服务应视业务状况而定。例如,对于须要提供界面的PHP服务咱们推荐使用多服务的方式,依赖Supervisor将Nginx、PHP-FPM部署于同一个Pod中,这样能够下降Nginx需同时处理FastCGI请求及静态资源请求带来的K8s部署模板配置复杂度。可是单Pod部署多服务的场景需额外注意对各服务的可用性监控,避免出现其中的某个服务异常而K8s没法探测的状况。

  • 可配置

- Nginx

- PHP-FPM

> 基础镜像的可配置对于容器化迁移相当重要,咱们建议用尽量少的基础镜像经过可配置的方式实现对各类不一样服务部署环境的兼容,下降服务环境差别带来的基础镜像维护成本。例如将Nginx、PHP-FPM的上传文件大小限制、内存大小限制等参数经过环境变量的方式,利用Entrypoint机制在启动Supervisor前先执行shell完成对环境配置的定制化替换。

  • 运行模式可切换

- PHP-FPM

- CLI(队列/计划任务)

- Swoole

> 因为PHP服务一般以多种方式结合使用,所以经过环境变量配置的方式,咱们的基础镜像亦支持多种运行模式按需切换,提升基础镜像的可复用性。

  • PHP7基础镜像示例

- Dockerfile示例

FROM php:7.0-fpm-stretch

LABEL maintainer="zhoushangzhi <zhoushangzhi@qudian.com>"

COPY sources-aliyun-0.list /etc/apt/sources.list.d/sources-aliyun-0.list

RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak \
        touch /etc/apt/sources.list \
        apt-get update \
        apt-get install -y --no-install-recommends apt-utils \
        libcurl4-gnutls-dev \
        libxslt-dev \
        libmagickwand-dev \
        gnupg \
        ca-certificates \
        apt-get install -y nscd \
        supervisor \
        procps \
        libpng-dev \
        libgettextpo-dev \
        libmcrypt-dev \
        libxml2-dev \
        libfreetype6 \
        libfreetype6-dev \
        libpng16-16 \
        libjpeg62-turbo \
        libjpeg62-turbo-dev \
        libmemcachedutil2 \
        libmemcached-dev \
        zlib1g \
        zlib1g-dev \
        $PHPIZE_DEPS \
        wget \
        unzip \
        vim \
        git \
        wget -O - https://openresty.org/package/pubkey.gpg | apt-key add - \
        apt-get -y install --no-install-recommends software-properties-common \
        add-apt-repository -y "deb http://openresty.org/package/debian $(lsb_release -sc) openresty" \
        apt-get update \
        apt-get -y install --no-install-recommends openresty \
        mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \
        docker-php-ext-configure gd \
        --with-gd \
        --with-freetype-dir=/usr/include/ \
        --with-png-dir=/usr/include/ \
        --with-gettext=/usr/include/ \
        --with-mcrypt=/usr/include/ \
        --with-jpeg-dir=/usr/include/ &amp;&amp; \
        NPROC=4 \
        docker-php-ext-install -j${NPROC} mysqli \
        pdo_mysql \
        bcmath \
        calendar \
        exif \
        gd \
        gettext \
        mcrypt \
        pcntl \
        shmop \
        sockets \
        sysvmsg \
        sysvsem \
        sysvshm \
        opcache \
        zip \
        wddx \
        xsl \
        pecl install msgpack imagick \
        cd /tmp \
        wget https://github.com/igbinary/igbinary/archive/2.0.4.zip \
        unzip 2.0.4.zip \
        cd igbinary-2.0.4 \
        phpize &amp;&amp; ./configure --with-php-config=php-config \
        make &amp;&amp; make install \
        echo "extension=igbinary.so" &gt; /usr/local/etc/php/conf.d/igbinary.ini \
        cd /tmp \
        wget https://github.com/php-memcached-dev/php-memcached/archive/php7.zip \
        unzip php7.zip \
        cd php-memcached-php7 \
        phpize \
        ./configure --prefix=/usr \
        --enable-memcached-sasl \
        --with-php-config=php-config \
        --enable-memcached-igbinary \
        --enable-memcached-json \
        --enable-memcached-msgpack \
        make \
        make INSTALL_ROOT="" install \
        install -d "/etc/php7/conf.d" \
        echo "extension=memcached.so" &gt; /usr/local/etc/php/conf.d/memcached.ini \
        cd /tmp \
        wget https://github.com/phpredis/phpredis/archive/3.1.2.zip \
        unzip 3.1.2.zip \
        cd phpredis-3.1.2 \
        phpize \
        ./configure --enable-redis-igbinary --with-php-config=php-config \
        make \
        make install \
        echo "extension=redis.so" &gt; /usr/local/etc/php/conf.d/redis.ini \
        cd /tmp \
        wget https://github.com/swoole/swoole-src/archive/v2.0.6.tar.gz \
        tar zxvf v2.0.6.tar.gz \
        cd swoole-src-2.0.6 \
        phpize \
        ./configure \
        make \
        make install \
        echo "extension=swoole.so" &gt; /usr/local/etc/php/conf.d/swoole.ini \
        docker-php-ext-enable igbinary redis msgpack imagick \
        rm -rf /tmp/* \
        rm -rf /var/lib/apt/lists/* \
        ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

COPY nscd.conf /etc/nscd.conf
COPY ./openresty /templates
COPY ./supervisor/conf.d/ /etc/supervisor/conf.d/
# add php-fpm-exporter
COPY ./bin/php-fpm_exporter_1.1.0_linux_amd64 /usr/local/bin/php-fpm-exporter

# nginx root
ENV INDEX_PATH=public
# nginx model, fpm/upstream
ENV MODE=fpm
# nginx upstream port
ENV NGINX_UPSTREAM_PORT=12151
# nginx fpm pass
ENV NGINX_FPM_PASS=localhost
# nginx upstream url
ENV NGINX_UPSTREAM_URL=localhost
# nginx worker num
ENV NGINX_WORKER_NUM=4
# fpm max children
ENV FPM_MAX_CHILDREN=100
# fpm start server
ENV FPM_START_SERVERS=20
# fpm max spare server
ENV FPM_MAX_SPARE_SERVERS=60
# fpm min spare server
ENV FPM_MIN_SPARE_SERVERS=20
# fpm max request
ENV FPM_MAX_REQUESTS=1000
# wether auto start nscd
ENV NSCD_START=true
# wether auto start nginx
ENV NGINX_START=true
# wether use supervisor to start init command
ENV SUPERVISOR_START=true
# exec before start
ENV POST_START=""
# wether auto start nscd
ENV INIT_CMD_START=true
# init command
ENV INIT_CMD="php-fpm --nodaemonize"
# init command process num, only use supervisor start avaliable
ENV INIT_CMD_PROCESS_NUM=1
# wether auto start exporter
ENV EXPORTER_START=true
# exporter listen address,see more:https://github.com/hipages/php-fpm_exporter
ENV PHP_FPM_WEB_LISTEN_ADDRESS=0.0.0.0:9146
# php log 二级模块目录
ENV PHP_LOG_SUB_MODULE="/"
# php-fpm memory limit
ENV FPM_MEMORY_LIMIT=32M
# php-cli memory limit
ENV PHP_MEMORY_LIMIT=128M
# php upload_max_filesize
ENV PHP_UPLOAD_MAX_FILESIZE=2M
# php post_max_size
ENV PHP_POST_MAX_SIZE=8M 
# php error_log file
ENV PHP_ERROR_LOGFILE=/tmp/php-error.log
# nginx_client_max_body_size
ENV CLIENT_MAX_BODY_SIZE=20M
# nginx_client_max_buffer_size
ENV CLIENT_BODY_BUFFER_SIZE=1M


WORKDIR /home/apple/web

EXPOSE 80

COPY entrypoint.sh /usr/local/bin/
CMD ["/bin/bash", "/usr/local/bin/entrypoint.sh"]

‍ - Entrypoint示例

#!/bin/bash

echo "replacing config"

set -xe \
        mkdir -p /etc/nginx/conf.d/ \
        mkdir -p /var/run/nscd/ \
        mkdir -p /var/log/nginx/ \
        if [ "fpm" = "$MODE" ]; then cp /templates/fpm.conf.template /etc/nginx/conf.d/default.conf; else cp /templates/upstream.conf.template /etc/nginx/conf.d/default.conf; fi \
        cp /templates/prometheus.lua /usr/local/openresty/site/lualib/prometheus.lua \
        cp /templates/nginx.conf /usr/local/openresty/nginx/conf/nginx.conf \
        sed -i "s|__CLIENT_MAX_BODY_SIZE__|$CLIENT_MAX_BODY_SIZE|" /usr/local/openresty/nginx/conf/nginx.conf \
        sed -i "s|__CLIENT_BODY_BUFFER_SIZE__|$CLIENT_BODY_BUFFER_SIZE|" /usr/local/openresty/nginx/conf/nginx.conf \
        sed -i "s|__NGINX_INDEX_PATH__|$INDEX_PATH|" /etc/nginx/conf.d/default.conf \
        sed -i "s|__NGINX_UPSTREAM_PORT__|$NGINX_UPSTREAM_PORT|" /etc/nginx/conf.d/default.conf \
        sed -i "s|__NGINX_FPM_PASS__|$NGINX_FPM_PASS|" /etc/nginx/conf.d/default.conf \
        sed -i "s|__NGINX_UPSTREAM_URL__|$NGINX_UPSTREAM_URL|" /etc/nginx/conf.d/default.conf \
        sed -i "s|__NGINX_WORKER_NUM__|$NGINX_WORKER_NUM|" /usr/local/openresty/nginx/conf/nginx.conf \
        sed -i "s|;pm.status_path = /status|pm.status_path = /status|" /usr/local/etc/php-fpm.d/www.conf\
        sed -i "s|pm.max_children = 5|pm.max_children = $FPM_MAX_CHILDREN|i" /usr/local/etc/php-fpm.d/www.conf \
        sed -i "s|pm.start_servers = 2|pm.start_servers = $FPM_START_SERVERS|i" /usr/local/etc/php-fpm.d/www.conf \
        sed -i "s|pm.max_spare_servers = 3|pm.max_spare_servers = $FPM_MAX_SPARE_SERVERS|i" /usr/local/etc/php-fpm.d/www.conf \
        sed -i "s|pm.min_spare_servers = 1|pm.min_spare_servers = $FPM_MIN_SPARE_SERVERS|i" /usr/local/etc/php-fpm.d/www.conf \
        sed -i "s|;pm.max_requests = 500|pm.max_requests = $FPM_MAX_REQUESTS|i" /usr/local/etc/php-fpm.d/www.conf \
        sed -i "s|;php_admin_value\[memory_limit\] = 32M|php_admin_value\[memory_limit\] = $FPM_MEMORY_LIMIT|i" /usr/local/etc/php-fpm.d/www.conf \
        sed -i "s|memory_limit = 128M|memory_limit = $PHP_MEMORY_LIMIT|i" /usr/local/etc/php/php.ini \
        sed -i "s|upload_max_filesize = 2M|upload_max_filesize = $PHP_UPLOAD_MAX_FILESIZE|i" /usr/local/etc/php/php.ini \
        sed -i "s|post_max_size = 8M|post_max_size = $PHP_POST_MAX_SIZE|i" /usr/local/etc/php/php.ini \
        sed -i "s|;error_log = php_errors.log|error_log = $PHP_ERROR_LOGFILE|i" /usr/local/etc/php/php.ini \
        sed -i "s|expose_php = On|expose_php = Off|i" /usr/local/etc/php/php.ini \
        sed -i "s|__INIT_CMD__|$INIT_CMD|" /etc/supervisor/conf.d/php.conf \
        sed -i "s|__INIT_CMD_PROCESS_NUM__|$INIT_CMD_PROCESS_NUM|" /etc/supervisor/conf.d/php.conf
  
if [[ $HOSTNAME =~ "cron" ]]; then
    JOBNAME=${HOSTNAME%-*}
    JOBNAME=${JOBNAME%-*}
    mkdir -p /data/logs/laifenqi/$JOBNAME/php
    rm -rf /home/apple/web${PHP_LOG_SUB_MODULE}storage/logs
    ln -s /data/logs/laifenqi/$JOBNAME/php /home/apple/web${PHP_LOG_SUB_MODULE}storage/logs
    chmod 777 /data/logs/laifenqi/$JOBNAME/*
else
    mkdir -p /data/logs/laifenqi/$HOSTNAME/nginx
    mkdir -p /data/logs/laifenqi/$HOSTNAME/php
    rm -rf /home/apple/web${PHP_LOG_SUB_MODULE}storage/logs
    ln -s /data/logs/laifenqi/$HOSTNAME/php /home/apple/web${PHP_LOG_SUB_MODULE}storage/logs
    chmod 777 /data/logs/laifenqi/$HOSTNAME/*
fi


if [ "true" != "$NSCD_START" ]; then
    sed -i "s|autostart=true|autostart=false|" /etc/supervisor/conf.d/nscd.conf
fi

if [ "true" != "$NGINX_START" ]; then
    sed -i "s|autostart=true|autostart=false|" /etc/supervisor/conf.d/nginx.conf
fi

if [ "true" != "$EXPORTER_START" ] || [ "fpm" != "$MODE" ]; then
    sed -i "s|autostart=true|autostart=false|" /etc/supervisor/conf.d/exporter.conf
fi

if [ "true" != "$INIT_CMD_START" ]; then
    sed -i "s|autostart=true|autostart=false|" /etc/supervisor/conf.d/php.conf
fi

if [ -n "$POST_START" ]; then
    sh -c "$POST_START"
fi

if [ "true" != "$SUPERVISOR_START" ]; then
    $INIT_CMD
else
    supervisord -n -y 0
fi

> 经过上面的示例能够看出为了实现可配置咱们使用了大量的环境变量,结合Entrypoint的替换脚本提升基础镜像的兼容性。

结语

以上是咱们趣店容器化历程的一些经验分享,整个容器化遵循按部就班的原则,在大面积推广前需对开发及运维(甚至测试)人员进行知识普及,避免在只有少数人掌握容器、K8s等知识体系的状况下强行线上推广。固然容器化并非一味治百病的药,咱们目前依然有小部分服务由于一些考量因素部署在物理服务器。容器化是为了提升各方的效率,切不可为了容器化而容器化。/zhoushangzhi@qudian.com

相关文章
相关标签/搜索