本文为利用Docker和容器服务轻松实现云原生应用系列的第一篇
最近对应用迁云的讨论很多,很多用户对云环境中的应用架构和运维方式还不了解。直接利用云服务器替换自有物理机并不是使用云的正确姿势。
Cloud Native Application(云原生应用)是当下一个热门名词,简单而言就是针对云计算的特性,来设计应用架构,并优化应用的交付、运维流程。Linux基金会旗下的云原生计算基金会 CNCF(Cloud Native Computing Foundation)开宗明义地描述了云原生系统所具有的几个关键特性
云原生应用可以满足我们的几个关键诉求:
下面我们将介绍,如何利用Docker和阿里云容器服务在云端实现应用的高可用。
云原生应用中一个很重要的理念就是解耦
微服务架构设计是一个很大的话题,有兴趣的同学可以参考微服务设计和 12 factors应用开发方式。我们今天只是谈应用层和数据层的分离。一方面它可以使得应用逻辑变成无状态的,支持水平扩展;另一方面区分无状态和有状态服务,可以使我们针对不同工作负载实现性能优化和扩展。
以前的两篇博文中介绍了如何将一个Ghost博客应用,重构成一个符合云原生应用架构的过程。
最初Ghost应用是利用嵌入式的SQLite来存储数据,并将所有媒体文件(图像、视频)保存到本地文件上。这个应用的性能受限于单机能力,无法水平扩展,也无法保证高可用。
而简单的重构之后,我们将应用的数据层剥离出来:把关系型数据储存在RDS的MySQL实例上,并将所有媒体文件保存到OSS对象存储之中。这样配合SLB的负载均衡,可以轻松地支持Ghost博客集群的水平扩容。同时由于所有数据不再保存到本地,这样即使特点ECS实例失效,请求也可以交由其他节点应用接管。而数据的可用性是通过RDS服务、OSS存储服务来保障的。
相应的Ghost的docker-compose模板如下
ghost:
image: registry.aliyuncs.com/acs-sample/ghost:0.7
links:
- db:mysql
ports:
- "2368"
environment:
- GHOST_URL=http://blog.${ACS_DEFAULT_DOMAIN}
- OSS_BUCKET=acs-sample-ghost
- OSS_BUCKET=acs-sample-ghost
- OSS_ACCESS_KEY_ID=***********
- OSS_ACCESS_KEY_SECRET=***********
- OSS_PREFIX=http://acs-sample-ghost.oss-cn-beijing.aliyuncs.com
- OSS_ENDPOINT=http://oss-cn-beijing.aliyuncs.com
labels:
aliyun.routing.port_2368: 'http://blog'
aliyun.scale: '3'
restart: always
db:
external:
host: rds******.mysql.rds.aliyuncs.com
ports:
- 3306
environment:
- MYSQL_DATABASE=blog
- MYSQL_USER=ghost
- MYSQL_PASSWORD=***********
阿里云扩展的注释:
aliyun.routing.port_xxxx
: 定义的虚拟域名访问方式aliyun.scale
: 定义了容器复本数量external
: 描述了容器对外部云服务的引用,可以方便地将容器和云服务组合在一起。需要注意的是Docker不是银弹,仅依赖容器技术无法解决所有问题。用户需要认真思考和判断什么样的应用可以运行在容器中。容器非常适合部署无状态的应用,因为这类应用可以方便地水平扩展和动态调度。然而对于有状态应用,比如数据库、消息队列,我的建议是可以在开发测试环境和非关键应用的生产环境中使用容器;然而对于核心应用,建议采用云服务提供的高可用中间件能力来实现。
解决了架构层面的水平伸缩能力和高可用,我们下面讨论一下应用部署中需要关注的可用性因素。
进程监控是最常用的可用性保证机制,传统上是利用Supervisor, Monit等工具来监控应用进程,并自动拉起失效进程。
Docker内置了进程监控能力,可以利用 docker run --restart=always ...
命令参数,或者在Compose模板中通过 restart: always
指明进程重启策略。
以上文为例,当Ghost容器运行之后,Docker Daemon会监控容器中PID1的NodeJS进程,如果进程退出会自动重启容器。可以参考restart policy了解更多信息。
然而仅仅根据进程状态判断应用的健康状态是不够的:因为有时候由于应用逻辑问题导致应用死锁或挂起,虽然进程还处于运行状态,但是已经无法接受新的请求。为此容器服务引入了服务的健康检查机制,可以通过
目前支持的健康性设置包括:
aliyun.probe.url
: 支持通过HTTP/TCP协议的URL请求对容器进行健康检查aliyun.probe.cmd
: 通过 shell 脚本检查对容器进行健康检查通过上面的方式,容器服务可以更加细粒度地判断应用健康状态,并会根据健康信息实现应用可用性的优化控制,比如:
aliyun.depends
标签或者使用Compose V2中的depends_on
指令,只有当所依赖的容器进入健康状态,容器才会被启动。这样可以非常优雅地保证应用中容器的启动顺序。aliyun.rolling_updates
标签来非常简单的配置。可以通过容器服务帮助文档了解更多详细信息
在Docker 1.12之后,Docker Engine将内置容器的健康检查,可以通过命令方式支持容器的健康状态检测,并自动重启失效容器。容器服务已经实现对Docker 1.12的支持和兼容。
一个常见误区是:用户认为应用的高可用性可以完全由云服务商的基础设施保证,自己无需关注。而真实情况是:虽然云服务提供了高可靠的基础设施,还要合理配置组合才能保障应用的可用性。
在云端和在自有物理机上部署应用有很多不同。在自有数据中心中,有经验的运维人员会根据应用负载特性选择合适的物理机。比如,根据应用的可用性要求,把应用部署到不同的物理服务器或不同机架的物理服务器上。然而在公共云和专有云中,由于虚拟化层支持虚拟机的迁移甚至热迁移,我们不应也不能再试图将应用和其部署位置简单绑定。
正确的姿势是:在理解云供应商提供的隔离性、可用性保证机制上,根据应用负载特征设计部署架构,并让系统来实现动态管理。
对可用性而言,在阿里云上可以有多种部署方式:跨多个ECS实例部署应用,防止单实例失效导致的应用无法访问;跨多个可用区(Availability Zone)的方式可以防止机房整体失效导致的应用中断;而跨多个地域(Region)的部署方式可以保证,即使一个地域的服务无法访问时,应用也可正常运行。此外阿里云上的RDS,SLB等云服务都提供了跨多个可用区的服务能力。关于云端高可用架构可以参见 双11技术攻略:企业云架构的正确姿势
随着微服务架构的引入,应用的部署更加富有挑战性。首先微服务和传统应用相比,部署组件的数量可能有10倍以上的区别;而且由于每个服务都支持快速的演进和动态地伸缩,除了需要更好地服务治理能力,也需要系统提供自动化的部署和管理能力保障应用SLA。
阿里云容器服务为Docker应用提供了内置的高可用支持,可以更好地支持微服务应用部署。
对于容器服务中的集群,用户可以选择新建ECS实例或者手动添加已有ECS实例,并支持手动和弹性扩容。如果用户应用关注网络性能,则可以选择自于同一个可用区的ECS实例,这样例之间的网络延迟会最小化。如果用户应用关注于高可用性。我们可以为集群添加来自多个可用区的ECS实例,配合SLB,RDS等多可用区支持能力,我们就会有一个高可用的容器基础架构。
下面是一个典型的高可用的容器集群的配置。其中ECS实例和RDS数据库实例部署在不同可用区之上。哪怕一个机房掉电也不会影响到集群中应用的可用性。
然而这还不够,因为在一个集群上的ECS实例上,通常会运行成百上千个应用。在调度容器高效地利用ECS节点资源的同时,如何避免由ECS节点/可用区失效导致的应用访问失败?
阿里云容器服务完全兼容Docker Swarm的集群管理和资源调度能力。Swarm允许在容器部署时,通过声明约束条件来指定容器和容器,容器和节点之间的亲和性和反亲和性。详细信息可以参考文档
比如,下面的命令可以让两个redis容器部署在不同的节点上
docker run -d --name redis_1 -e 'affinity:container!=redis_*' redis
docker run -d --name redis_2 -e 'affinity:container!=redis_*' redis
阿里云容器服务在此基础之上提供了更加丰富而简便的的服务级别的亲和性约束。在compose模板中,服务定义了一组容器的集合,它们拥有一致的镜像和配置。
如果指明了 aliyun.scale
标签,相同服务的不同容器复本会尽量分布到不同节点之上,防止因为ECS实例失效导致该服务所有容器全部无法访问。
服务之间可以通过 affinity:service
标签来描述服务直接容器的亲和性。比如下面的模板,描述了一个MySQL主从结构中,MySQL master和slave容器不会调度到一个ECS实例上
master:
image: tutum/mysql:5.6
environment:
- MYSQL_USER=user
- MYSQL_PASS=test
- REPLICATION_MASTER=true
- REPLICATION_USER=repl
- REPLICATION_PASS=repl
- affinity:service!=slave
slave:
image: tutum/mysql:5.6
environment:
- MYSQL_USER=user
- MYSQL_PASS=test
- REPLICATION_SLAVE=true
- affinity:service!=master
links:
- master:mysql
还有一个方式通过容器和节点的亲和性,可以细粒度的指定如果将容器部署到指定节点之上。可以参见分布式Zookeeper集群了解其中示例。
为了保证服务有更高的可用性,我们可以将服务中的容器调度在不同的可用区(availability zone)里。
比如,通过 availability:az
标签,我们可以非常简单地把如下web服务的Nginx容器部署到2个可用区之中。
web:
image: 'nginx:latest'
environment:
- availability:az==2
labels:
aliyun.scale: '8'
aliyun.routing.port_80: 'web'
restart: always
一个容器集群可以保证容器应用在节点或可用区级别的可用性。如果用户需要达到更高级别的可用性,可以在不同地域创建容器集群,并分别部署应用。阿里云容器服务也提供混合云能力,支持用户在自有数据中心和云端分别创建容器集群,并用统一的方式进行应用部署和管理。其简单部署架构如下:
在分布式架构中,节点失效是不可避免的。高可用部署让应用可以容忍一定的失效;另一方面我们需要能够让应用从故障状态中自动恢复回来。就像X战警中的金刚狼,虽然战斗力可能不是最强,但是超强恢复力让他成为决定战斗命运的角色。
阿里云容器服务支持容器自动重新调度策略。也就是当容器所属宿主机节点失效时,系统可以将该容器自动迁移到其他健康节点上继续运行,从而保证应用的SLA。
容器服务提供兼容 Docker Swarm 的容器重调度策略。 在下面一个示例中,Redis服务开启了重调度策略
redis:
image: redis
labels:
- com.docker.swarm.reschedule-policies=["on-node-failure"]
什么样的容器支持重调度?我们需要分析容器中应用对持久化状态的依赖。无状态应用可以简单地被调度到任何节点上正确执行的;而如果容器应用依赖于宿主机的本地存储,则是无法被重调度的。对于后者,需要进一步分离应用层和数据层逻辑才能支持迁移。比如在阿里云容器服务中,容器可以利用文件卷插件(volume plugin)提供的网络存储能力(NAS/NFS和OSSFS)来共享和保存数据。一旦节点失效,容器被调度到其他节点,容器服务会保证其相应文件卷的正确挂载。这样应用可以从网络中获得已保存的数据并继续运行。
云原生应用在架构、开发、运维等方面和传统软件应用有很多不同。利用Docker和阿里云容器服务可以轻松解决高可用部署和故障恢复的复杂性。
阿里云容器服务在兼容Docker原生编排技术的同时,提供了针对阿里云能力的优化和集成。开发者可以利用声明式的方式来描述容器应用的部署约束,以及集成阿里云服务,从而满足对可用性SLA的要求。
想了解更多容器服务内容,请访问 https://www.aliyun.com/product/containerservice