对传统应用进行容器化改造

本文由 陈计节 翻译自 FP Complete 网站上的文章 CONTAINERIZING A LEGACY APPLICATION: AN OVERVIEW,原作者 Emanuel Borsboom。

以下为译文全文,如需阅读英文原文,请转到文末获取链接:


本文接下来简要介绍什么是容器化,要在 Docker 容器中运行传统应用的缘由,容器化的过程,其间可能遇到的问题,在用容器部署之后的其他步骤等。这将明显减轻部署工作的压力,并让应用朝着零停机部署和横向缩放的方向前进。


注:本文专注在简化应用的部署过程,并不包含需要对应用重新设计的内容,比如高可用和横向扩展。


概念



什么是“传统”应用?

并没有一个特定的定义能够描述所有的传统应用,但它们有一些共同的特性:

  • 使用本地文件系统来持久化存储,数据文件和应用的文件混合在一起。

  • 在同一个服务器上运行很多服务,比如 MySQL 数据库,Redis 服务器,nginx web 服务器,一个 Ruby on Rails 应用,以及一大堆定时任务

  • 使用大杂烩式的脚本和手工流程进行安装和升级(文档也很简陋)。

  • 配置是存储在文件里的,通常散落在多个位置,并与应用的文件混在一起。

  • 进程间的通信是借助本地文件系统进行的(比如在磁盘上放一个文件,另一个进程来读取),而不是TCP/IP。

  • 按照单个服务器上只运行一个应用的示例的方式来设计的。


传统应用的缺点

  • 自动化部署很困难。

  • 如果需要运行应用的多个不同的实例,很难让多个实例在同一个服务器上“共存”。

  • 如果服务器停机,由于需要手工流程所以需要较长的时间来恢复。

  • 部署新版本的过程基本是手动的,或者大部分是手动的,难以回滚。

  • 很有可能测试环境与生产环境有较大差异,导致一些生产环境问题不能在测试期间发现。

  • 很难通过增加新的实例来进行横向扩展。


什么是容器化?

将应用“容器化”的过程,就是让应用能够运行在 Docker 容器或类似技术中,它们能将操作系统环境和应用封装在一起(完整的系统镜像)。由于容器能给应用提供近似于完整系统的环境,这就为在不修改,或者少量修改应用的情况下,对应用的部署进行现代化改造提供了一种思路。这也是应用的架构持续能保持“云友好”的基础。


容器化的好处

  • 部署容易多了:使用新的容器镜像直接替换整个老版本。

  • 自动化部署也相对容易,甚至可以完全由 CI(Continuous Integration, 持续集成)来驱动。

  • 部署失败时的回滚只要切换到之前的镜像。

  • 应用升级非常容易,因为现在没有可能出错的“中间步骤”了(不管它是否影响整个部署过程的成功)。

  • 相同的容器镜像可以在不同的环境中充分测试,再直接部署到生产环境。这可以确保测试态与生产态的产品是完全一致的。

  • 系统更容易从宕机中恢复,因为可以迅速在新硬件资源上启动装有这个应用的新容器,并附加到同一数据源上。

  • 开发人员能在本地以容器的形式,在更逼真的环境里测试新功能。

  • 硬件资源的利用更高效,在单一主机上现在可以运行多个容器应用,而以前不能。

  • 容器化是支持零停机升级、金丝雀部署、高可用和横向扩展的坚实基础。


容器化之外的选择

  • 用 Puppet 和 Chef 之类的配置管理工具,能解决一部分的“传统”问题,比如环境一致性等。但它们不能支持“原子”部署,以及对应用+环境的完整回滚。而一种无法方便回滚的部署方案,仍然会在部署中途充满风险。

  • 虚拟机镜像是能实现部分上述能力的另一种方法,而且在有些情形中,相对于容器,使用完整的虚机进行“原子地”部署会更合适。但使用虚机的主要问题是,它对硬件的利用率更低效。因为虚机需要一些独占的资源(CPU、内存和磁盘等),而容器之间可以共享主机的资源。


如何容器化



一、准备工作


列出存储数据的文件系统位置

由于部署新版本应用是通过替换 Docker 镜像实现的,所以任何持久化的数据都应该存储在容器之外。如果运气不错的话,可能遇到应用已经将所有数据都写入了特定位置,不过多数传统应用常将它们的数据往磁盘上到处乱写,还有可能与应用本身的文件混在一起。Docker 的可加载存储卷(volume)让主机的文件系统能暴露给容器用作特定路径,这样数据可以在容器之间留存。所以,我们无论是哪种情况,我们都需要列出用于存储数据的位置。

现在你可以考虑考虑让应用里所有输出的数据写入到文件系统的同一目录去了,这样能明显简化容器化版本的部署工作。不过,如果修改应用难以达成,这也并不是必须的。


找出会随部署环境变化的配置数据

为了确保一致性,同一个镜像要在多套环境中使用(比如,测试和生产),因此必须要列出所有在不同环境中会变化的配置值,在启动容器的时刻再设置值。容器中的程序到时候可以从环境变量,或者从配置文件中获取这些配置的值。

你可以现在就考虑修改应用并支持从环境变量中读取配置,以便简化容器化的过程。同样的,如果不好修改应用,这也是不一定是必要的。


找出容易移出去的服务

在同一机器上,我们的应用可能要依赖一些其他服务,它们如果独立性比较高、使用 TCP/IP 通信,就很容易能移出去。举例来说,如果在同一机器上运行 MySQL 或 PostgreSQL 数据库,或者类似 Redis 的缓存,那就容易移出去了。可能同时还需要调整配置,才能支持指定机器名(hostname)和端口(port)而不是直接认为应用运行在 localhost。


二、创建容器镜像


创建用于安装应用的 Dockerfile

如果已经有基于脚本或者 Chef、Puppet 之类的配置管理工具的自动化安装能力,那这个过程就很简单了。挑选一个喜欢的系统镜像、安装所有依赖,然后运行自动化脚本就行了。

如果目前的安装过程是手动的,就需要写一些脚本了。不过,由于镜像的状态是已知的,在这儿编写脚本要比基于可能存在不一致性的原生系统来的容易。

如果提前找出了要移出去的服务,那么在脚本里就不应该安装它们了。

下面是一个简单的示例 Dockerfile:


# 基于官方 Ubuntu 16.04 Docker 镜像
FROM ubuntu:16.04

# 安装所依赖的 Ubuntu 软件包
RUN apt-get install -y <REQUIRED UBUNTU PACKAGES> \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*

# 将应用的文件复制到镜像里
ADD . /app

# 运行安装脚本
RUN /app/setup.sh

# 切换到应用的目录
WORKDIR /app

# 指定应用的启动脚本
COMMAND /app/start.sh


制作用于配置的启动脚本

如果应用已经在使用环境变量中读取配置值了,那这一步可以跳过了。如果要从文件里读取特定环境相关的配置值,那启动脚本就要能从环境变量里读取配置值,并将这些值更新到配置文件中去。

这里有一个启动脚本的例子:


#!/usr/bin/env bash
set -e

# 把环境变量 $MYAPPCONFIG 的值添加到配置文件中
cat >>/app/config.txt <<END
my_app_config = "${MYAPPCONFIG}"
END

# 用环境变量 $MYAPPARG 作为应用的启动参数
/app/bin/my-app --my-arg="${MYAPPARG}"



推送镜像

镜像生成之后(使用 docker build),需要推送到 Docker 仓储(Registry)中才能从部署机器上拉取到(如果要在生成镜像的同一台机器上运行,就不需要)。

可以使用 Docker Hub 来存储镜像(用付费账号可以创建私有仓库),大多数云服务商也提供容器仓储(比如 Amazon ECR)。

给镜像设置标签(比如 docker tag myimage mycompany/myimage:mytag)之后,就可以推送到仓库了(比如 docker push mycompany/myimage:mytag)。每次在应用新版本生成镜像时打上新的标签,这样既能明确当前所运行的版本,还能保留旧版本的镜像以便回滚。


三、如何部署


部署容器是个很大的话题,接下来只关注直接使用 docker 命令运行容器的部分。在现实世界中,应该考虑使用 docker-compose(对于所有容器都运行在同一机器上的简单情形)和 Kubernetes (在集群中编排容器)之类的工具。


被移出来的服务

提前移出来的服务可以运行在单独的 Docker 容器中,然后链接(link)到我们的应用所在容器。另外,还可以用云上托管的服务。举个例子,在 AWS 上,可以使用 RDS 作为数据库、用 Elasticache 作为缓存,这样可以极大地简化你的工作,因为他们能为你解决后期维护,高可用和备份等需求。

运行 Postgres 数据库容器的例子:

docker run -d \
   --name db \
   -v /usr/local/postgresql/data:/var/lib/postgresql/data \
   postgres


容器化之后的应用

要在 Docker 容器中运行一个应用,只要用一个命令行:

docker run -d \
   -p 8080:80 \
   --name myapp \
   -v /usr/local/myappdata:/var/lib/myappdata \
   -e MYAPPCONFIG=myvalue \
   -e MYAPPARG=myarg \
   --link db:db \
   myappimage:mytag

其中的 -p 参数将容器里的 80 端口公开并映射到主机上的 8080 端口,-v 参数设置要在容器里加载的、用于持久化数据的存储卷(格式是 主机上的路径:容器中的路径)-e 参数设置一个用于配置的环境变量值(这些参数可以指定多次,从而设置多个卷和环境变量),而 --link参数将数据库所在容器以链接的方式传入,这样应用就可以与数据库通信了。容器会根据 Dockerfile 中的 COMMAND 指令指定的脚本来启动。


对应用进行升级

如果要升级到应用的新版本,只要停掉旧版的容器(比如 docker rm -f myapp),并用新的镜像标签启动新的容器就可以了(可能有短暂的停机时间)。回滚操作也类似,只要换用旧版的镜像标签。


更多相关考量



“init” 进程(PID 1)

传统应用通常有多个进程,如果没有 “init” 守护进程(PID 1)的清理,就容易出现孤儿进程(orphan processes)发生累积的情况了。Docker 默认并不提供这样的守护进程,所以推荐自己用 ENTRYPOINT 在 Dockerfile 里添加一个。dumb-init 是众多初始守护进程中的比较轻量级的一个。phusion/baseimage 是一个包含 init 初始守护进程和其他一些服务的全功能基准镜像。 请查看我们博客上关于这个主题的文章:Docker 守护进程:PID-1, 孤儿进程, 僵尸进程和信号。


守护进程和定时任务

在使用 Docker 容器时,一般只会在每个容器中运行一个进程。理想情况下,所有守护进程和定时任务都应该移到其他容器中去,不过对于传统应用来,这也不一定都行得通,主要是经常要求对应用进行重新设计。要运行多个进程也不是一定不行,但确实会需要一些额外的一些配置,因为标准的基准镜像里并不包含进程管理和调度能力。小型进程管理程序,比如 runit,比 systemd 之类的完整功能的子系统更适合在容器中用。phusion/baseimage 是一个包含 runit 和定时能力和其他一些服务的全功能基准镜像。


存储卷的权限

在容器里,所有进程通常都以 root 身份运行(不过也不是必须的)。传统的应用对用户的需求通常复杂一些,可能要用其他用户来运行(或者用不同的用户运行多个进程)。这可能给存储卷的使用带来一些麻烦,因为 Docker 默认让加载的卷的所有权指向 root,也就是说非 root 进程就不能写入到这些卷了。有两个方法可以解决这个问题:

第一种方式是在在创建容器之前,先在主机上创建好目录,由有正确的 UID/GID 的用户持有所有权。注意,由于容器里和主机上的用户不能匹配,所以需要用容器里用户的 UID/GID,而不仅仅是用户名要一致。

另一种方式是在容器里,在启动过程中调整加载点的所有权。这就需要在切换到用来启动应用的非 root 用户之前,还在以 root 身份运行期间处理。


数据库迁移

数据库结构迁移在部署工作中经常是一大挑战,因为数据库结构通常与应用是严格耦合的,这对迁移的时机提出了要求,而且这也让回滚到旧版本变得更难,因为数据库迁移并不一定容易回滚。

完成这种迁移的方法是引入一个过渡步骤。如果需要对数据库结构做出与旧版本不兼容的变更,那就将这个变更分为两次部署。比如,如果想将数据移到另一处,两个步骤是:

将数据同时写入旧的位置和的位置,并只从新的位置读取。这意味着,如果把应用回滚到前一个版本,在回滚之前新产生的新数据是不会丢的。

不再向旧的位置写入数据。 要注意的是,如果希望部署期间没有停机时间,就意味着在同一时间会有应用的多个版本在运行,相应的也会带来更多挑战。


数据备份

对容器化的应用进行备份通常比较简单。数据文件可以从主机上备份,而不需要担心数据会与应用程序的文件混在一起,因为它们已经严格地分开了。如果将数据库迁移到了像 RDS 这样的托管服务,他们就会处理好备份(至少自己的工作会简化一些)。


迁移已有数据

在生产环境中,要把现有应用迁向容器化的版本,就需要对旧的已有数据进行迁移。这个工作往往因地制宜,不过最简单的就是停掉旧版本,把数据备份直接恢复给新版本用。这个过程应该提前做好,也不可避免地会需要一定的停机时间。


结论



虽然提前需要做一些工作,对传统的应用进行容器化的过程会帮助我们更好地对它进行管控和自动化,能把部署的压力降到最低。它给对应用进行现代化改造提供了一个明确的路径,并能支持零停机部署、高可用和横向扩展。

除了从零开始构建容器化应用,FP Complete 已经多次开展了上述实践。如果你想了解迈向现代化、无压力部署之路,可以了解一下我们的 DevOps 和咨询服务,或直接联系我们。


英文原文(请手动复制):https://www.fpcomplete.com/blog/2017/01/containerize-legacy-app

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/318846.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2021牛客暑期多校训练营2 L-WeChat Walk(分块)

L-WeChat Walk 每个大点记录一下邻接点的最大步数 每次修改的时候&#xff0c;枚举修改点的邻接的大点来更新 修改大点的时候直接判是不是比邻接点都大 代码抄的std好不容易才看懂~ Code1 #include<bits/stdc.h> using namespace std; template <class Tint> T…

牛客题霸 [求平方根] C++题解/答案

牛客题霸 [求平方根] C题解/答案 题目描述 实现函数 int sqrt(int x). 计算并返回x的平方根 题解&#xff1a; 要求返回平方根&#xff0c;我们就找一个i&#xff0c;使得ii<x&&(i1)(i1)>x 这样的i就是我们要找的答案 注意&#xff0c;x有可能为负数&#xf…

二分算法:平均值(洛谷 UVA1451)

解析 这道题寻找平均值的max&#xff0c;答案明显具有单调性&#xff0c;所以采用二分算法 从0到1不断取中点mid作为平均值的可能点&#xff0c;看是否存在不短于l的数列均值&#xff1e;mid不难得到以下代码&#xff1a; double st0,ed1;for(int i1;i<10;i){double mid(s…

P5287-[HNOI2019]JOJO【KMP】

正题 题目链接:https://www.luogu.com.cn/problem/P5287 题目大意 开始一个空串&#xff0c;nnn个操作 在末尾加入xxx个ccc字符&#xff08;保证和ccc和前面的字符不同&#xff09;返回到第xxx次操作之后 每次操作完成后求所有前缀的最长的borderborderborder长度和 1≤n≤…

牛客题霸 [数组中只出现一次的数字] C++题解/答案

牛客题霸 [数组中只出现一次的数字] C题解/答案 题目描述 一个整型数组里除了两个数字之外&#xff0c;其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。 题解&#xff1a; 用map来记录每个数字出现几次&#xff0c;然后再循环一遍看哪个数字出现一次&#x…

Asp.NetCore依赖注入和管道方式的异常处理及日志记录

前言在业务系统&#xff0c;异常处理是所有开发人员必须面对的问题&#xff0c;在一定程度上&#xff0c;异常处理的能力反映出开发者对业务的驾驭水平&#xff1b;本章将着重介绍如何在 WebApi 程序中对异常进行捕获&#xff0c;然后利用 Nlog 组件进行记录&#xff1b;同时&a…

2021“MINIEYE杯”中国大学生算法设计超级联赛(2)I love counting(Trie树)

I love counting O{Mlog⁡aMAX(BN/B)}O\{M\log{a_{\text{MAX}}}(\text{BN/B})\}O{MlogaMAX​(BN/B)} md考场写的莫队Trie一直T #include<bits/stdc.h> using namespace std; using lllong long; using piipair<int,int>; using plipair<ll,int>; constexpr …

字符串:凯撒密码(洛谷P1914)

解析 只需将每一位ascll码加n即可 但要注意的是c的ascll码是有上限的 我一开始是这么写的&#xff1a; for(int i1;i<l;i){s[i] n;while(s[i]>z) s[i] - 26;printf("%c",s[i]);}结果&#xff1a; (真的是随便打的&#xff0c;不巧有些攻击性。。&#xff0…

P6113-[模板]一般图最大匹配【带花树】

正题 题目链接:https://www.luogu.com.cn/problem/P6113 题目大意 给出一张无向图&#xff0c;求最大匹配。 1≤n≤103,1≤m≤51041\leq n\leq 10^3,1\leq m\leq 5\times 10^41≤n≤103,1≤m≤5104 解题思路 带花树的模板&#xff0c;我也不会讲/kel 所以看下面两篇大佬的博…

牛客题霸 [跳台阶] C++题解/答案

牛客题霸 [跳台阶] C题解/答案 题目描述 一只青蛙一次可以跳上1级台阶&#xff0c;也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法&#xff08;先后次序不同算不同的结果&#xff09;。 题解&#xff1a; 递归的入门题 如果只剩一个台阶&#xff0c;只有一种跳…

.NET Core实战项目之CMS 第四章 入门篇-Git的快速入门及实战演练

写在前面上篇文章.NET Core实战项目之CMS 第三章 入门篇-源码解析配置文件及依赖注入我带着大家通过分析了一遍ASP.NET Core的源码了解了它的启动过程&#xff0c;然后又带着大家熟悉了一遍配置文件的加载方式&#xff0c;最后引出了依赖注入以及控制反转的概念&#xff01;如果…

高精度:麦森数*(洛谷P1045)

P1045 [NOIP2003 普及组] 麦森数 解析 看似只是正常的一个高精 然而 暗藏杀机 一开始随手那么一写 。。。 (即使用了快速幂)时间复杂度过于感人 后来我们发现&#xff1a; 第一问位数的计算不必真的算出来&#xff0c;只需把2的p次幂转化为10的k次幂即可&#xff08;具体请…

2021“MINIEYE杯”中国大学生算法设计超级联赛(2)I love exam(背包)

I love exam 不知道为啥刚开始不写&#xff0c;那么简单的背包预处理dp&#xff0c;太菜了吧 fi,jf_{i,j}fi,j​对于第i门课来说花费j天得到的最大分数 gi,j,pg_{i,j,p}gi,j,p​考虑前i门课&#xff0c;花费j天复习得到的最大分数 #include<bits/stdc.h> using namespa…

P2012-拯救世界2【EGF】

正题 题目链接:https://www.luogu.com.cn/problem/P2012 题目大意 121212种东西排列成长度为nnn的序列&#xff0c;要求前四种出现奇数次&#xff0c;后四种出现偶数次&#xff0c;求方案。TTT组数据&#xff0c;对10910^9109取模。 1≤n<263,1≤T≤21051\leq n< 2^{63}…

彼之蜜糖,吾之砒霜——聊聊软件开发中的最佳实践

“描述一个事物&#xff0c;唯有一个名词定义它的概念&#xff0c;唯有一个动词揭露它的行为&#xff0c;唯有一个形容词表现它的特征。要做的&#xff0c;就是用心去寻找那个名词、那个动词、那个形容词……”—— 福楼拜 (Gustave Flaubert)我想讲个故事。很久很久以前&#…

贪心: Array Splitting(数列分段)(洛谷CF1175D)

解析 这题可以转化一下&#xff1a; &#xff08;《神笔马良》。。。。&#xff09; 计算这些长方形对应下标的总加和 我们可以一层一层往上垒,假设第i层起始点为xi&#xff0c;总和为sumi&#xff0c;再设从1到i的前缀和为si 显然第一层x11&#xff0c;sum1sn 对于第二层x2&…

牛客题霸 [合并有序链表] C++题解/答案

牛客题霸 [合并有序链表] C题解/答案 题目描述 将两个有序的链表合并为一个新链表&#xff0c;要求新的链表是通过拼接两个链表的节点来生成的。 题解&#xff1a; 首先判断l1和l2是否为空 然后依次比较l1和l2的值&#xff0c;然后存到新的链表里&#xff0c;当有一方全部结…

2021牛客暑期多校训练营2 G.League of Legends(转化+单调队列)

G.League of Legends Zechariah_2001题解 对于可以包含其他区间的大区间&#xff0c;要使得答案最优无非就是两种分组方式&#xff1a;单独一组或者与被包含的区间一组。单独一组那么贡献就是区间长度&#xff1b;如果说与被包含的区间一组&#xff0c;对答案贡献为0&#xff…

P5056-[模板]插头dp

正题 题目链接:https://www.luogu.com.cn/problem/P5056 题目大意 n∗mn*mn∗m的网格&#xff0c;求有多少条回路可以铺满整个棋盘。 解题思路 插头dpdpdp的&#xff0c;写法是按照题解上的写法。 状态用的是括号匹配&#xff0c;然后用了哈希邻接表&#xff08;挂表&#x…

牛客题霸 [用两个栈实现队列] C++题解/答案

题目描述 用两个栈来实现一个队列&#xff0c;完成队列的Push和Pop操作。 队列中的元素为int类型。 题解&#xff1a; 我们都知道栈的性质是先进后出&#xff0c;队列是先进先出 我们用两个栈来模拟出队列 可以先用一个栈来存数&#xff0c;当要输出时&#xff0c;最上面的是…