共计 12490 个字符,预计需要花费 32 分钟才能阅读完成。
简介
趣店的容器化过程经验过三个里程碑:docker、单集群脚本化治理、多集群平台化治理。为了兼顾日常业务的需要开发,每一个里程均是由小局部人主导推动,由点及面地进行推广,并通过在小范畴的试错中寻找最适宜趣店业务场景的容器化计划。容器化为趣店的服务隔离及服务器统一化治理提供了根底条件,并且通过容器化迁徙为趣店每月节俭至多 10 万元服务器费用。(因为迁徙工作以 PHP 服务作为试点,因而本文中的案例亦是以 PHP 为主)
趣店容器进化史疾速预览图
Docker
作为容器化推动的第一阶段,此阶段由开发主导,推广开发及测试环境容器化应用,并进行小局部服务线上容器化试用。
Docker 入门
容器化推动初期,此时咱们外部对于容器较为理解的人员并不多,开发不晓得应该如何应用容器,运维对于如何保护容器下的服务也没有教训,因而在这个阶段咱们着重对整体开发人员及运维人员进行高级容器入门分享,分享次要包含以下几个方面:
- Docker 环境搭建
次要用于疏导开发人员搭建本地 Docker 开发环境,进行初步的容器概念建模。
- Docker 命令解析
docker 命令解析分享材料
该分享次要解说 Docker 的罕用指令、拆解容器的部署流程并简要介绍通过 Swarm 进行集群部署的形式。
- Dockerfile 最佳实际
参考《Best practices for writing Dockerfiles》,分享如何以更优雅的形式编写 Dockerfile。
Docker 编排
咱们的局部开发人员尝试更深层次地利用容器化,例如基于 docker-compose 推广 docker 在本地开发环境落地。这一推广对于微服务一类单个我的项目依靠于多个服务的开发环境部署提供了极大的便当,同时也在开发环境的应用中进一步深入大家对容器的了解。在这一阶段开发了繁难的 K8s 编排脚本,对新上线的小服务尝试应用 K8s 部署服务。
单集群脚本化治理
思考到容器化仍处于尝试阶段且须要进行定制化脚本开发,因而第二阶段仍是以开发作为主导。本阶段开始对次要服务的小流量环境进行容器化迁徙,通过开发更欠缺的 K8s 编排脚本以优化服务的继续集成与部署。
容器化服务迁徙
随着全员对容器认知程度的进步,在这一阶段咱们的小局部开发开始尝试进行线上小流量环境的迁徙,迁徙过程也曾遇到一些问题。
坑
- 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/ && \
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 等常识体系的状况下强行线上推广。当然容器化并不是一味治百病的药,咱们目前仍然有小局部服务因为一些考量因素部署在物理服务器。容器化是为了进步各方的效率,切不可为了容器化而容器化。