简介
趣店的容器化过程经验过三个里程碑: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-stretchLABEL maintainer="zhoushangzhi <zhoushangzhi@qudian.com>"COPY sources-aliyun-0.list /etc/apt/sources.list.d/sources-aliyun-0.listRUN 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/localtimeCOPY nscd.conf /etc/nscd.confCOPY ./openresty /templatesCOPY ./supervisor/conf.d/ /etc/supervisor/conf.d/# add php-fpm-exporterCOPY ./bin/php-fpm_exporter_1.1.0_linux_amd64 /usr/local/bin/php-fpm-exporter# nginx rootENV INDEX_PATH=public# nginx model, fpm/upstreamENV MODE=fpm# nginx upstream portENV NGINX_UPSTREAM_PORT=12151# nginx fpm passENV NGINX_FPM_PASS=localhost# nginx upstream urlENV NGINX_UPSTREAM_URL=localhost# nginx worker numENV NGINX_WORKER_NUM=4# fpm max childrenENV FPM_MAX_CHILDREN=100# fpm start serverENV FPM_START_SERVERS=20# fpm max spare serverENV FPM_MAX_SPARE_SERVERS=60# fpm min spare serverENV FPM_MIN_SPARE_SERVERS=20# fpm max requestENV FPM_MAX_REQUESTS=1000# wether auto start nscdENV NSCD_START=true# wether auto start nginxENV NGINX_START=true# wether use supervisor to start init commandENV SUPERVISOR_START=true# exec before startENV POST_START=""# wether auto start nscdENV INIT_CMD_START=true# init commandENV INIT_CMD="php-fpm --nodaemonize"# init command process num, only use supervisor start avaliableENV INIT_CMD_PROCESS_NUM=1# wether auto start exporterENV EXPORTER_START=true# exporter listen address,see more:https://github.com/hipages/php-fpm_exporterENV PHP_FPM_WEB_LISTEN_ADDRESS=0.0.0.0:9146# php log 二级模块目录ENV PHP_LOG_SUB_MODULE="/"# php-fpm memory limitENV FPM_MEMORY_LIMIT=32M# php-cli memory limitENV PHP_MEMORY_LIMIT=128M# php upload_max_filesizeENV PHP_UPLOAD_MAX_FILESIZE=2M# php post_max_sizeENV PHP_POST_MAX_SIZE=8M # php error_log fileENV PHP_ERROR_LOGFILE=/tmp/php-error.log# nginx_client_max_body_sizeENV CLIENT_MAX_BODY_SIZE=20M# nginx_client_max_buffer_sizeENV CLIENT_BODY_BUFFER_SIZE=1MWORKDIR /home/apple/webEXPOSE 80COPY entrypoint.sh /usr/local/bin/CMD ["/bin/bash", "/usr/local/bin/entrypoint.sh"]
- Entrypoint示例
#!/bin/bashecho "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/*fiif [ "true" != "$NSCD_START" ]; then sed -i "s|autostart=true|autostart=false|" /etc/supervisor/conf.d/nscd.conffiif [ "true" != "$NGINX_START" ]; then sed -i "s|autostart=true|autostart=false|" /etc/supervisor/conf.d/nginx.conffiif [ "true" != "$EXPORTER_START" ] || [ "fpm" != "$MODE" ]; then sed -i "s|autostart=true|autostart=false|" /etc/supervisor/conf.d/exporter.conffiif [ "true" != "$INIT_CMD_START" ]; then sed -i "s|autostart=true|autostart=false|" /etc/supervisor/conf.d/php.conffiif [ -n "$POST_START" ]; then sh -c "$POST_START"fiif [ "true" != "$SUPERVISOR_START" ]; then $INIT_CMDelse supervisord -n -y 0fi
通过下面的示例能够看出为了实现可配置咱们应用了大量的环境变量,联合Entrypoint的替换脚本进步根底镜像的兼容性。
结语
以上是咱们趣店容器化历程的一些教训分享,整个容器化遵循循序渐进的准则,在大面积推广前需对开发及运维(甚至测试)人员进行常识遍及,防止在只有多数人把握容器、K8s等常识体系的状况下强行线上推广。当然容器化并不是一味治百病的药,咱们目前仍然有小局部服务因为一些考量因素部署在物理服务器。容器化是为了进步各方的效率,切不可为了容器化而容器化。