简介
趣店的容器化进程经历过三个里程碑:docker、单集群脚本化管理、多集群平台化管理。为了兼顾日常业务的需求开发,每一个里程均是由小部分人主导推动,由点及面地进行推广,并通过在小范围的试错中寻找最适合趣店业务场景的容器化方案。容器化为趣店的服务隔离及服务器统一化管理提供了基础条件,并且通过容器化迁移为趣店每月节省至少 10 万元服务器费用。(由于迁移工作以 PHP 服务作为试点,因此本文中的案例亦是以 PHP 为主)
趣店容器进化史快速预览图
Docker
作为容器化推进的第一阶段,此阶段由开发主导,推广开发及测试环境容器化使用,并进行小部分服务线上容器化试用。
Docker 入门
容器化推进初期,此时我们内部对于容器较为了解的人员并不多,开发不知道应该如何使用容器,运维对于如何维护容器下的服务也没有经验,因此在这个阶段我们着重对全体开发人员及运维人员进行初级容器入门分享,分享主要包括以下几个方面:
主要用于引导开发人员搭建本地 Docker 开发环境,进行初步的容器概念建模。
docker命令解析分享资料
该分享主要讲解 Docker 的常用指令、拆解容器的部署流程并简要介绍通过 Swarm 进行集群部署的方式。
参考《Best practices for writing Dockerfiles》,分享如何以更优雅的方式编写 Dockerfile。
Docker 编排
我们的部分开发人员尝试更深层次地应用容器化,例如基于 docker-compose 推广 docker 在本地开发环境落地。这一推广对于微服务一类单个项目依托于多个服务的开发环境部署提供了极大的便利,同时也在开发环境的使用中进一步深化大家对容器的理解。在这一阶段开发了简易的 K8s 编排脚本,对新上线的小服务尝试使用 K8s 部署服务。
单集群脚本化管理
考虑到容器化仍处于尝试阶段且需要进行定制化脚本开发,因此第二阶段仍是以开发作为主导。本阶段开始对主要服务的小流量环境进行容器化迁移,通过开发更完善的 K8s 编排脚本以优化服务的持续集成与部署。
容器化服务迁移
随着全员对容器认知水平的提高,在这一阶段我们的小部分开发开始尝试进行线上小流量环境的迁移,迁移过程也曾遇到一些问题。
坑
现象:在这一阶段的迁移过程中由于 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 将日志统一收集至日志服务器。
监控
主机的资源监控包括:CPU、内存、磁盘、网卡流量等等,尽可能详细地收集主机监控信息对于异常情况下的问题排查有着极大的帮助。
围绕于集群服务的各种基础组件:kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy、CoreDNS 等等,也需要纳入监控范围,避免因为单个基础组件的异常影响整个集群内部业务服务的稳定性。
- Nginx
- PHP-FPM
Pod 部署了可用于输出 Nginx-FPM 和 PHP 实时状态的 Exporter,通过常规的 Prometheus + Grafana 方案实现 K8s 服务的监控。
网络拓扑
在这一阶段考虑到现有服务是逐步迁移,为保持原有线上灰度测试方案的可用性,并未使用常规的 Ingress 作为外部流量的入口。
多集群平台化管理
最终阶段我们基于开源平台进行二次定制化开发,由运维、开发共同主导。这一阶段的主要工作是通过定制化开发打通 开发-测试-审批-线上部署 的完整流程,并对现有的线上服务全量迁移至 K8s 集群。
开源平台选型
Wayne(360)
Rancher(Rancher Labs)
KubeSphere(青云)
tke(腾讯)
K8s多集群管理平台对比
在最开始的开源平台选型阶段我们综合对比了目前较为主流的 4 大开源平台:Wayne、Rancher、KubeSphere、tke,由于我们现有业务均为多区部署因此平台是否支持多集群管理成为我们最重要的考察因素。各项因素综合对比后最终我们选用 Wayne 作为基础进行二次定制化开发。但是由于我们基于 Wayne 开发的版本 360 团队有较长时间未更新维护,导致最新版需要修复少量 bug 后才能正常使用。
说明:此对比截止时间为 2019 年 12 月,此期间各平台可能有新的功能迭代
网络拓扑
由于我们的服务大部分为微服务,继续使用 Nodeport 的方式每个项目均需要占用大量的集群端口号,因此在全量服务迁移阶段我们调整为使用常规的 Ingress 作为外部流量的入口。
CI/CD
在这一阶段我们进一步对 CI/CD 流程进行了完善,镜像通过 CI Runner 的方式自动构建,减少上线过程的等待时间,并通过界面化的方式完成多集群部署,打通从镜像构建、审批、部署上线的完整流程。
镜像构建流程
由上图可以看出,通过 Gitlab 的 CI 流程我们完善了代码合并后自动构建镜像并推送镜像至镜像仓库的流程。在 K8s 接口化的服务端我们已提前配置好每个服务的 Deployment 基础模板,构建成功后调用接口写入对应版本信息即可生成待发布的 Deployment 模版。
代码上线流程
由于我们的代码上线过程需要监测每次上线是否会对线上数据造成波动,因此上线环节全程由开发手动在平台化后台操作没有实现全流程自动化。
ENV上线流程
配置上线则相对简单大部分配置变更后只需要重启 Pod 即可,因此这一部分做了自动化处理。
平台化服务迁移
平台化服务迁移对于运维的工作量较大,由于各服务配置差异较大,运维需要根据每个服务的不同配置 Deployment 基础模板。而我们数百个微服务由于种种历史原因没有保持环境统一,运维梳理环境迁移服务的过程中容易疏漏一些细微的环境配置差异,有些差异可能又是在小部分场景下才会触发异常,因此也列出来便于大家避坑。
坑
现象:在线上压测过程发现部署于 K8s 中的服务当单 Pod QPS 达到 1 万左右开始出现 TCP 连接异常,无法继续增压。
解决方案:单 Pod 可用的连接数极大的依赖于节点服务器,单 Pod 无法支撑更大连接数时需考虑调优各节点服务器的内核参数,如调整最大打开文件限制(包括用户级别与系统级别)、最大追踪 TCP 连接数、系统 TIME_WAIT 数量等。
现象:Filebeat 采集的日志中出现部分业务日志丢失。
解决方案:由于 Kafka 对单条消息大小的限制,如果单行日志过大会导致日志无法被采集,此时应规范业务日志的输出,避免出现单行大日志。
现象:流量从物理机器迁移至 K8s 后部分服务请求出现 HTTP Code 413 或下游服务接收到的请求数据为空。
解决方案:Nginx 及 PHP-FPM 层面对上传文件大小、POST body 大小均有限制,因此需要将限制大小配置值调整至与原物理机器一致。
现象:服务从物理机器迁移至 K8s 后部分计划任务无法正常执行,部分后台异步导出队列执行异常。
解决方案:通常情况下我们会使用一台物理服务器同时部署服务喝执行计划任务,而大部分计划任务、队列可能需要使用大量的内存用于统计之类的逻辑,此时应调整 K8s 计划任务及队列 Pod 的内存上限限制,同时可能还需要修改 PHP 的内存大小限制,并视计划任务情况调整最大执行时间避免因计划任务超时触发失败重试。
现象:单 K8s 集群中出现小部分节点资源负载较高,而其余节点较为空闲。
解决方案:此时可通过 K8s 的反亲和性配置将重资源的 Pod 分散部署在各节点服务器中,避免小部分节点服务器同时部署重资源 Pod 出现资源争抢。
基础镜像调优
对于单 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 服务通常以多种方式结合使用,因此通过环境变量配置的方式,我们的基础镜像亦支持多种运行模式按需切换,提高基础镜像的可复用性。
. - Dockerfile 示例
FROM php:7.0-fpm-stretch
LABEL maintainer="ZoaChou <zoachou@mudoom.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/ && \
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 && ./configure --with-php-config=php-config \
&& make && make install \
&& echo "extension=igbinary.so" > /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" > /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" > /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" > /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 等知识体系的情况下强行线上推广。当然容器化并不是一味治百病的药,我们目前依然有小部分服务因为一些考量因素部署在物理服务器。容器化是为了提高各方的效率,切不可为了容器化而容器化。
评论