趣店的容器化程序經歷過三個里程碑: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服務提供商)的磨合階段,在這一階段應提前準備及演練運行於K8s的服務異常情況下的流量切換方案。由於業務服務對K8s基礎服務的強依賴關係,基礎服務的監控、異常轉移均需提前完善及演練。
映象管理映象管理作為容器化遷移不可或缺的一部分,自建的映象倉庫能夠更好的保障內部服務映象的安全性(映象可能包含服務原始碼),且部署於內網的映象倉庫能夠極大提高部署速度。為簡化映象的管理與維護,我們在內網部署開源的Harbor服務管理內部映象。
CI/CD在這一階段我們透過自研的指令碼(整合編排檔案生成、映象構建、部署)及Jenkins實現服務的CI/CD。由於這一階段的CI/CD流程仍是試驗階段並無十分完善,這裡暫時不展開敘述,較為完善的流程可參考下一階段遷移的CI/CD。
日誌收集編排日誌編排日誌目前我們沒有特意收集,大部分情況下還是部署或者排程出現問題的時候由運維進入叢集內透過Kubectl檢視日誌情況。
容器日誌由於大部分服務的日誌都是往指定目錄輸出,目前並沒有很好的利用容器的標準輸出作為容器內部服務日誌輸出的統一出口,所以容器日誌當前仍處於待挖掘階段。
服務日誌NginxPHP除去常規的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等等,也需要納入監控範圍,避免因為單個基礎元件的異常影響整個叢集內部業務服務的穩定性。
PodNginxPHP-FPMPod部署了可用於輸出Nginx-FPM和PHP實時狀態的Exporter,透過常規的Prometheus + Grafana方案實現K8s服務的監控。
網路拓撲NodePortServicePod在這一階段考慮到現有服務是逐步遷移,為保持原有線上灰度測試方案的可用性,並未使用常規的Ingress作為外部流量的入口。
多叢集平臺化管理最終階段我們基於開源平臺進行二次定製化開發,由運維、開發共同主導。這一階段的主要工作是透過定製化開發打通 開發-測試-審批-線上部署 的完整流程,並對現有的線上服務全量遷移至K8s叢集。
開源平臺選型Wayne(360)Rancher(Rancher Labs)KubeSphere(青雲)tke(騰訊)K8s多叢集管理平臺對比
說明:此對比截止時間為2019年12月,此期間各平臺可能有新的功能迭代
網路拓撲IngressServicePod由於我們的服務大部分為微服務,繼續使用Nodeport的方式每個專案均需要佔用大量的叢集埠號,因此在全量服務遷移階段我們調整為使用常規的Ingress作為外部流量的入口。
CI/CD在這一階段我們進一步對CI/CD流程進行了完善,映象透過CI Runner的方式自動構建,減少上線過程的等待時間,並透過介面化的方式完成多叢集部署,打通從映象構建、審批、部署上線的完整流程。
映象構建流程映象構建流程
由上圖可以看出,透過Gitlab的CI流程我們完善了程式碼合併後自動構建映象並推送映象至映象倉庫的流程。在K8s介面化的服務端我們已提前配置好每個服務的Deployment基礎模板,構建成功後呼叫介面寫入對應版本資訊即可生成待發布的Deployment模版。
程式碼上線流程程式碼上線流程
由於我們的程式碼上線過程需要監測每次上線是否會對線上資料造成波動,因此上線環節全程由開發手動在平臺化後臺操作沒有實現全流程自動化。
配置上線流程ENV上線流程
配置上線則相對簡單大部分配置變更後只需要重啟Pod即可,因此這一部分做了自動化處理。
平臺化服務遷移平臺化服務遷移對於運維的工作量較大,由於各服務配置差異較大,運維需要根據每個服務的不同配置Deployment基礎模板。而我們數百個微服務由於種種歷史原因沒有保持環境統一,運維梳理環境遷移服務的過程中容易疏漏一些細微的環境配置差異,有些差異可能又是在小部分場景下才會觸發異常,因此也列出來便於大家避坑。
坑Pod可用連線數不足預期解決方案:單Pod可用的連線數極大的依賴於節點伺服器,單Pod無法支撐更大連線數時需考慮調優各節點伺服器的核心引數,如調整最大開啟檔案限制(包括使用者級別與系統級別)、最大追蹤TCP連線數、系統TIME_WAIT數量等。
單行大日誌解決方案:由於Kafka對單條訊息大小的限制,如果單行日誌過大會導致日誌無法被採集,此時應規範業務日誌的輸出,避免出現單行大日誌。
上傳檔案/POST大小限制解決方案:Nginx及PHP-FPM層面對上傳檔案大小、POST body大小均有限制,因此需要將限制大小配置值調整至與原物理機器一致。
服務記憶體大小限制解決方案:通常情況下我們會使用一臺物理伺服器同時部署服務喝執行計劃任務,而大部分計劃任務、佇列可能需要使用大量的記憶體用於統計之類的邏輯,此時應調整K8s計劃任務及佇列Pod的記憶體上限限制,同時可能還需要修改PHP的記憶體大小限制,並視計劃任務情況調整最大執行時間避免因計劃任務超時觸發失敗重試。
部分節點資源負載異常解決方案:此時可透過K8s的反親和性配置將重資源的Pod分散部署在各節點伺服器中,避免小部分節點伺服器同時部署重資源Pod出現資源爭搶。
基礎映象調優理論與實踐(單服務容器 VS 多服務容器)對於單Pod是部署單服務還是多服務應視業務情況而定。例如,對於需要提供介面的PHP服務我們推薦使用多服務的方式,依賴Supervisor將Nginx、PHP-FPM部署於同一個Pod中,這樣可以降低Nginx需同時處理FastCGI請求及靜態資源請求帶來的K8s部署模板配置複雜度。但是單Pod部署多服務的場景需額外注意對各服務的可用性監控,避免出現其中的某個服務異常而K8s無法探測的情況。
可配置NginxPHP-FPM基礎映象的可配置對於容器化遷移至關重要,我們建議用盡可能少的基礎映象透過可配置的方式實現對各種不同服務部署環境的相容,降低服務環境差異帶來的基礎映象維護成本。例如將Nginx、PHP-FPM的上傳檔案大小限制、記憶體大小限制等引數透過環境變數的方式,利用Entrypoint機制在啟動Supervisor前先執行shell完成對環境配置的定製化替換。
執行模式可切換PHP-FPMCLI(佇列/計劃任務)Swoole由於PHP服務通常以多種方式結合使用,因此透過環境變數配置的方式,我們的基礎映象亦支援多種執行模式按需切換,提高基礎映象的可複用性。
PHP7基礎映象示例Dockerfile示例FROM php:7.0-fpm-stretchLABEL maintainer="zhoushangzhi <[email protected]>"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等知識體系的情況下強行線上推廣。當然容器化並不是一味治百病的藥,我們目前依然有小部分服務因為一些考量因素部署在物理伺服器。容器化是為了提高各方的效率,切不可為了容器化而容器化。