💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、豆包、星火、月之暗面及文生图、文生视频 广告
# 第4章 示例:Web环境 我们所知的大多数公司都曾以一个很低的容器和宿主机比例(1~2个容器对应1台宿主机)成功地使用过Docker。也就是说,要在生产环境中成功运行Docker,并不是必须要运行Apache Mesos或Kubernates。在本示例中,将对RelateIQ公司[\[1\]](part0010.xhtml#anchor41)使用Docker运行了一年多的一个真实Web服务器生产环境做详细的说明。这个环境在运行Ubuntu的标准亚马逊云服务(AWS)实例上,使用Docker支撑其CRM Web应用。当初使用Docker的原因有三:一是Docker能快速生成和销毁容器,从而为客户提供零停机时间部署;二是因为Docker为不同Web版本提供依赖隔离;三是Docker支持即时回滚。图4-1所示为该环境的高层次示图。 ![图像说明文字](https://box.kancloud.cn/0fccb7fd306a6dbbdfd9f038d26df841_700x687.jpeg) 图4-1 相信吗?这个Web环境提供了如下功能:稳定的零停机时间部署、回滚、集中式日志、监控及分析JVM的一种方式。所有这些都是通过bash脚本编排Docker镜像获得的。图4-2所示为主机的详细情况。 ![图像说明文字](https://box.kancloud.cn/97bc1526b8a6d6280e6c9924b7f92e20_700x612.jpeg) 图4-2 这台Web服务器运行于单台AWS服务器上,并通过Docker运行着4个容器。部分容器被链接在一起,以便与Docker网桥上的其他容器进行通信。它给宿主机公开了多个端口,用于为性能分析提供HTTP服务和JVM监控。它使用了亚马逊ELB负载均衡器(健康检查在其上进行)。所有容器都将它们的日志保存在宿主机上,这样现有的日志方案(SumoLogic)依旧适用,同时有一个简单的bash编排脚本用于部署和设置新版本Web服务。 为了便于理解很多公司在生产环境中运行Docker时会遇到的问题,我们来看一些具体细节。 编排归根到底就是做两件事:一是获取已安装Docker的服务器,并且使之准备好运行容器的服务器;二是在服务器上启动并运行容器。 该服务器使用标准的基本Ubuntu AMI(亚马逊机器镜像)在AWS上部署,并通过[Chef](http://www.chef.io/)的标准配置管理系统对宿主机进行设置。其设置过程与当下的多数环境完全相同。服务器启动之后,Chef就会运行并设置ssh用户、ssh密钥,然后通过其包安装器安装基础包(如iostat),安装并配置监控代理(本例中是Datadog),集合一些临时磁盘空间用于数据或日志存储,安装并配置日志代理(SumoLogic),安装最新版Docker,最后创建bash设置脚本,并配置一个cron任务来运行它。 Chef在服务器上运行之后,宿主机就准备好在其上运行机器所需的任何容器了。Chef还配置了监控和日志软件,用于未来的调试。这个环境可以运行任何类型的容器服务,与当下运行的大多数服务器环境,甚至是物理环境也一般无二。现在,Docker已经安装完毕,宿主机也准备好核心操作工具,下面就可以让宿主机上的容器开始运行Web应用了。 早期运行Docker的大多数公司一般都是使用bash脚本来设置容器的,这个环境也不例外。这个环境使用一个cron任务,每5分钟运行一个bash脚本来进行容器的所有编排工作。脚本的核心功能是正确地设置容器并拉取最新版的Web服务器镜像。我们来深入看一下所使用的脚本片段。 这个脚本完成以下操作。 (1)检查容器是否正在运行(通常是的,这主要用于新机器的情况)。 (2)如果容器未运行,则部署Hipache和Redis容器并将它们链接在一起。 (3)拉取最新版的Web服务器镜像并运行。 (4)等待Web服务器健康检查通过,然后再将其添加到负载均衡器中。 (5)一旦上述操作成功,给服务器上的迷你负载均衡器`hipache`发送一条消息(本例中是使用netcat运行一个`redis-cli`命令),告知Docker为之分配的随机端口和IP地址。 (6)保持旧容器运行,以便在需要时进行回滚。 (7)清除旧镜像。 下面是脚本的一些片断(为适合阅读,删除了部分代码): ``` #!/bin/bash # 检查Hipache容器 STATE=$(docker inspect hipache | jq ".[0].State.Running") if [[ "$STATE" != "true" ]]; then set +e docker rm hipache >/dev/null 2>&1 set -e mkdir -p /logs/hipache/ docker run -p 80:80 -p 6379:6379 --name hipache -v /logs/hipache:/logs -d repo.com/hipache echo "$(date +"%Y-%m-%d %H:%M:%S %Z") lpush frontend:* default" sleep 5 (echo -en "lpush frontend:* default\r\n"; sleep 1) | nc localhost 6379 fi # 拉取最新镜像 IMAGE_ID=$(docker images | grep ${IMAGE_NAME} | grep $REMOTE_VERSION | head -n 1 | awk '{print $3}') if [ -z $IMAGE_ID ]; then docker pull $DOCKER_IMAGE_NAME fi echo $REMOTE_VERSION >$VERSION_FILE # 启动新容器 echo "$(date +"%Y-%m-%d %H:%M:%S %Z") launching $DOCKER_IMAGE_NAME, logging to $LOG_DIR" mkdir -p $LOG_DIR NEW_WEBAPP_ID="abcdefghijklmnopqrstuvwxyz" MAX_TIMEOUT=5 set +e until [ $MAX_TIMEOUT -le 0 ] || NEW_WEBAPP_ID=$(docker run -P -h $(hostname) --link hipache:hipache $(dockerParameters $BRANCH) -d -v $LOG_DIR:/logs $DOCKER_IMAGE_NAME); do echo -n "." sleep 1 let MAX_TIMEOUT-=1 done set -e # 检查Web应用容器是否已启动 NEW_WEBAPP_IP_ADDR=$(docker inspect $NEW_WEBAPP_ID | jq '.[0].NetworkSettings.IPAddress' -r) if [ -z "$NEW_WEBAPP_IP_ADDR" -o "$NEW_WEBAPP_IP_ADDR" = "null" ]; then echo "$(date +"%Y-%m-%d %H:%M:%S %Z") no new webapp ip, failed to start" # send_deploy_message $HOSTNAME $BRANCH $IMAGE_NAME "error" send_webhook $HOSTNAME $BRANCH $BUILD_ID $BUILD_NUMBER "failure" exit 1 fi echo -n "$(date +"%Y-%m-%d %H:%M:%S %Z") new instance $NEW_WEBAPP_ID starting, on ip $NEW_WEBAPP_IP_ADDR" # 5分钟 MAX_TIMEOUT=300 HEALTH_RC=1 set +e until [ $HEALTH_RC == 0 ]; do if [ $MAX_TIMEOUT -le 0 ]; then echo "$(date +"%Y-%m-%d %H:%M:%S %Z") failed to be healthy within 5 minutes, killing and exiting..." docker kill $NEW_WEBAPP_ID docker rm $NEW_WEBAPP_ID # send_deploy_message $HOSTNAME $BRANCH $IMAGE_NAME "error" send_webhook $HOSTNAME $BRANCH $BUILD_ID $BUILD_NUMBER "failure" exit 1 fi ${SCRIPT_HOME}/health.sh $NEW_WEBAPP_IP_ADDR HEALTH_RC=$? echo -n "." sleep 5 let MAX_TIMEOUT-=5 done set -e echo # 将自身作为后端添加到Redis中 (echo -en "rpush frontend:* http://${NEW_WEBAPP_IP_ADDR}:${WEBAPP_PORT}\r\n"; sleep 1) | nc localhost 6379 # 确保自己是Redis的第一个后端 (echo -en "lset frontend:* 1 http://${NEW_WEBAPP_IP_ADDR}:${WEBAPP_PORT}\r\n"; sleep 1) | nc localhost 6379 # 将Redis中所有其他后端移除 (echo -en "ltrim frontend:* 0 1\r\n"; sleep 1) | nc localhost 6379 ``` 如我们所见,这段脚本大部分都是一些很基础的bash指令。只要有一些bash脚本的经验,任何系统管理员或运维工程师都能完成此类编排。容器的编排可以很简单,但必须经过几次迭代,过一段时间脚本就会变得更强壮。即便是在出现失败的情况下,这个脚本也能正确工作,不会将未通过健康检查的新容器上线。随着与Docker相关的新技术不断出现,类似Apache Mesos和Kubernetes这样的系统将取代bash脚本来完成编排。下面来看这个环境在其他方面是如何工作的。 只要掌握窍门,运行Docker和单一容器的宿主机网络就很容易理解。Docker通过`docker run`命令将容器的端口公开给宿主机。服务器公开的端口包括负载均衡器监听的80端口(ssl只传递到负载均衡器)、用于Java优化的优化端口、Redis用于切换负载均衡器后端的端口,以及Web服务器自身的一个端口(后续章节详述)。服务器之外的负载均衡器只监控80端口。宿主机中的Web服务器会启动一个随机端口,来自80端口的请求会被Hipache代理转发到这个端口上。 由于这是一个Web服务,存储的需求不会太多。有时需要存储日志、文件的缓存或加载静态内容。本例中,使用的是宿主机的而非容器的存储。将数据保存在宿主机上的理由很简单。如果容器宕机了,我们仍然需要排查出现的问题。服务一般是将日志写入到某个文件路径中。本例中我们将Docker容器映射到宿主机文件系统中,并将持久化的日志文件从容器里重定向到宿主机上,以便未来进行日志分析。这通过`docker run`的`-v`卷参数很容易实现。 容器日志根据服务进行分类。例如,使用`/logs/Redis`、`/logs/hipache`和`/logs/webserver/`(如图4-3所示)。这里需要特别注意,Web服务器会根据请求的日期时间戳来记录错误和请求日志。容器记录日志时,其文件名类似这样:`/logs/webserver/2015-03-01.request.log`。如果文件存在,日志会自动追加到同一文件中。如果有另外一个或多个容器启动,日志同样会被追加到同一个文件中。通过Chef安装logrotate,可防止日志无限制地增长。 ![图像说明文字](https://box.kancloud.cn/55dcf18b6c79b3cabdddf150ece57376_700x1119.jpeg) 图4-3 在生产环境中,通常会有一个集中式日志服务器,因此服务器上的日志只是在被收集器取走前做临时保存。由于所有的容器都将日志写入宿主机中,所以无须为Docker采用一项全新的日志技术。非运行Docker的环境极可能也会采用相同的日志操作方式,以保持现存监控框架的不变。在这个环境中,很容易将创建或追加的日志文件发送到中央日志服务器(Splunk、Sumologic和Loggly)上以便分析。 这里需要特别注意的是,负载均衡器监控服务器的负荷,并根据需要自动将下一个请求发送给其他可用Web服务。宿主机是通过带有Docker插件(这里是Datadog)的宿主机上的监控代理来监控的。本示例中的监控是一个全栈监视器。这个代理监控着宿主机的使用情况,如CPU、内存、磁盘IO、JVM监控以及运行的容器数量。这个环境里的应用程序指标通过StatsD发送给中央收集器。这里的指标包括了:网页点击量、应用程序查询速度以及特定功能的延迟指标。 在这个环境中,使用了一个名为Yourkit的JVM优化工具来监控堆的运行情况。这样,运维团队或开发人员可以将自己的优化工具连接到宿主机上,从而通过调用栈发现应用程序的深层问题。其缺点之一是,每个容器都需要有单独的端口,如果宿主机上同时运行着两个容器,它们的端口也不能一样。所以需要通过一个快速的SSH或工具来检查这个端口。类似New Relic和Sysdig(在生态系统中提到过)的这类新技术可以对其进行监控。 由于所有的应用程序依赖都存储在容器镜像中,运维团队只需要管理服务器管理方面的依赖即可。这简化了Chef配置管理框架以及用于保持环境更新的脚本数量。 这个Web服务环境可以提供零停机时间部署。零停机时间部署通过Hipache以及一个由Redis支撑的实时Web查询引擎实现,由于Redis是单线程的,在此作为数据库非常完美。Hipache会将HTTP会话重定向给数据库列表中最顶部的服务器。新容器上线时,将发送一条更新列表的命令给Redis,然后这个新容器就能接收所有新的点击。会话状态保存于后台数据库中,因此容器可以短暂存活,并且不会造成客户端状态的丢失。 因为Docker镜像存储于服务器上且容器启动速度非常快,这个环境可以非常容易地在需要时回滚旧代码。由于宿主机上保存着多个容器,很容易使用类似脚本或其他编排工具来启动旧容器并取代新(错误部署的)容器。 RelateIQ已经在生产环境中运行本章所描述的设置一年多了,取得了巨大的成功。他们的团队将标准的运维工具应用到Docker中,创造出一个功能完整的Web编排层。使得他们无须进行重大的基础性变更即可尝试新技术。他们也能将Docker和当前的基础设施监控及日志方案相结合,使其易于在生产环境中运行。对这个环境有兴趣的读者,可以阅读有关该环境的[博客文章](https://www.salesforceiq.com/blog/zero-downtime-pushes-say-goodbye-to-the-workout-robot/)和[访谈](http://blog.heavybit.com/blog/2015/3/23/dockermeetup)。 在第5章中,我们将讲述RelateIQ如何使用AWS Beanstalk通过Docker为每个分支完整编排一个Web环境。 - - - - - - [\[1\]](part0010.xhtml#ac41) 现已被Salesforce收购并更名为SalesforceIQ。——译者注