簡介
> 趣店的容器化進程經歷過三個里程碑: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/ && \ 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等知識體系的狀況下強行線上推廣。固然容器化並非一味治百病的藥,咱們目前依然有小部分服務由於一些考量因素部署在物理服務器。容器化是爲了提升各方的效率,切不可爲了容器化而容器化。/zhoushangzhi@qudian.com