# 第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。——译者注
- 版权信息
- 版权声明
- 内容提要
- 对本书的赞誉
- 译者介绍
- 前言
- 本书面向的读者
- 谁真的在生产环境中使用Docker
- 为什么使用Docker
- 开发环境与生产环境
- 我们所说的“生产环境”
- 功能内置与组合工具
- 哪些东西不要Docker化
- 技术审稿人
- 第1章 入门
- 1.1 术语
- 1.1.1 镜像与容器
- 1.1.2 容器与虚拟机
- 1.1.3 持续集成/持续交付
- 1.1.4 宿主机管理
- 1.1.5 编排
- 1.1.6 调度
- 1.1.7 发现
- 1.1.8 配置管理
- 1.2 从开发环境到生产环境
- 1.3 使用Docker的多种方式
- 1.4 可预期的情况
- 为什么Docker在生产环境如此困难
- 第2章 技术栈
- 2.1 构建系统
- 2.2 镜像仓库
- 2.3 宿主机管理
- 2.4 配置管理
- 2.5 部署
- 2.6 编排
- 第3章 示例:极简环境
- 3.1 保持各部分的简单
- 3.2 保持流程的简单
- 3.3 系统细节
- 利用systemd
- 3.4 集群范围的配置、通用配置及本地配置
- 3.5 部署服务
- 3.6 支撑服务
- 3.7 讨论
- 3.8 未来
- 3.9 小结
- 第4章 示例:Web环境
- 4.1 编排
- 4.1.1 让服务器上的Docker进入准备运行容器的状态
- 4.1.2 让容器运行
- 4.2 连网
- 4.3 数据存储
- 4.4 日志
- 4.5 监控
- 4.6 无须担心新依赖
- 4.7 零停机时间
- 4.8 服务回滚
- 4.9 小结
- 第5章 示例:Beanstalk环境
- 5.1 构建容器的过程
- 部署/更新容器的过程
- 5.2 日志
- 5.3 监控
- 5.4 安全
- 5.5 小结
- 第6章 安全
- 6.1 威胁模型
- 6.2 容器与安全性
- 6.3 内核更新
- 6.4 容器更新
- 6.5 suid及guid二进制文件
- 6.6 容器内的root
- 6.7 权能
- 6.8 seccomp
- 6.9 内核安全框架
- 6.10 资源限制及cgroup
- 6.11 ulimit
- 6.12 用户命名空间
- 6.13 镜像验证
- 6.14 安全地运行Docker守护进程
- 6.15 监控
- 6.16 设备
- 6.17 挂载点
- 6.18 ssh
- 6.19 私钥分发
- 6.20 位置
- 第7章 构建镜像
- 7.1 此镜像非彼镜像
- 7.1.1 写时复制与高效的镜像存储与分发
- 7.1.2 Docker对写时复制的使用
- 7.2 镜像构建基本原理
- 7.2.1 分层的文件系统和空间控管
- 7.2.2 保持镜像小巧
- 7.2.3 让镜像可重用
- 7.2.4 在进程无法被配置时,通过环境变量让镜像可配置
- 7.2.5 让镜像在Docker变化时对自身进行重新配置
- 7.2.6 信任与镜像
- 7.2.7 让镜像不可变
- 7.3 小结
- 第8章 存储Docker镜像
- 8.1 启动并运行存储的Docker镜像
- 8.2 自动化构建
- 8.3 私有仓库
- 8.4 私有registry的扩展
- 8.4.1 S3
- 8.4.2 本地存储
- 8.4.3 对registry进行负载均衡
- 8.5 维护
- 8.6 对私有仓库进行加固
- 8.6.1 SSL
- 8.6.2 认证
- 8.7 保存/载入
- 8.8 最大限度地减小镜像体积
- 8.9 其他镜像仓库方案
- 第9章 CI/CD
- 9.1 让所有人都进行镜像构建与推送
- 9.2 在一个构建系统中构建所有镜像
- 9.3 不要使用或禁止使用非标准做法
- 9.4 使用标准基础镜像
- 9.5 使用Docker进行集成测试
- 9.6 小结
- 第10章 配置管理
- 10.1 配置管理与容器
- 10.2 面向容器的配置管理
- 10.2.1 Chef
- 10.2.2 Ansible
- 10.2.3 Salt Stack
- 10.2.4 Puppet
- 10.3 小结
- 第11章 Docker存储引擎
- 11.1 AUFS
- 11.2 DeviceMapper
- 11.3 BTRFS
- 11.4 OverlayFS
- 11.5 VFS
- 11.6 小结
- 第12章 Docker 网络实现
- 12.1 网络基础知识
- 12.2 IP地址的分配
- 端口的分配
- 12.3 域名解析
- 12.4 服务发现
- 12.5 Docker高级网络
- 12.6 IPv6
- 12.7 小结
- 第13章 调度
- 13.1 什么是调度
- 13.2 调度策略
- 13.3 Mesos
- 13.4 Kubernetes
- 13.5 OpenShift
- Red Hat公司首席工程师Clayton Coleman的想法
- 第14章 服务发现
- 14.1 DNS服务发现
- DNS服务器的重新发明
- 14.2 Zookeeper
- 14.3 基于Zookeeper的服务发现
- 14.4 etcd
- 基于etcd的服务发现
- 14.5 consul
- 14.5.1 基于consul的服务发现
- 14.5.2 registrator
- 14.6 Eureka
- 基于Eureka的服务发现
- 14.7 Smartstack
- 14.7.1 基于Smartstack的服务发现
- 14.7.2 Nerve
- 14.7.3 Synapse
- 14.8 nsqlookupd
- 14.9 小结
- 第15章 日志和监控
- 15.1 日志
- 15.1.1 Docker原生的日志支持
- 15.1.2 连接到Docker容器
- 15.1.3 将日志导出到宿主机
- 15.1.4 发送日志到集中式的日志平台
- 15.1.5 在其他容器一侧收集日志
- 15.2 监控
- 15.2.1 基于宿主机的监控
- 15.2.2 基于Docker守护进程的监控
- 15.2.3 基于容器的监控
- 15.3 小结
- DockOne社区简介
- 看完了