昨天在通过Dockerfile打包镜像时,碰到自启动的问题,一直搞到凌晨都没能解决,到了子时心里就开始发慌,心慌的时候是锻炼心性最好的时候,就像肌肉酸胀时是最锻炼肌肉的临界区。今早起床后头脑清醒了,重新梳理了下思路,很快就把问题解决了。特记录于此,希望有缘人能避免在这条弯路上耗费太多时间。
翻看了下我昨天打包的镜像,从v1.0到v11.0,一共做了11个版本,其实最后已经在球门前徘徊了,就差临门一脚,这一脚是早上的v12.0。是不是想起了小学语文课本里有个做板凳的故事。那是谁的三个板凳来着?但愿有朝一日也能有人记得我的12个镜像。其实不是那三个板凳让当事人留芳,而是希望当事人让这12个镜像能至少留痕。
此篇为补昨天的日志。本来昨天已经有了个开头,规划了一下未来几周的探索路线。结果刚出门就堵车了,今天就从昨日的开头续写吧。
有了集群加持,野心就变大了,想在本系列运维日志中实现一个中等规模的分布式网站架构。
- 里程碑1.0:一个django+uwsgi的api后端、一个mysql数据库后台、一个vue+nginx前端。
- 里程碑2.0:多前端+入口负载均衡(共享存储、反向代理自定义负载均衡策略)
- 里程碑3.0:多后端+内部负载均衡(搭建内部DNS,规范接口,DNS轮询)
- 里程碑4.0:分布式存储
- 里程碑5.0:分布式数据库
- 里程碑6.0:非关系数据库
- 里程碑7.0:大数据分析
这是一个5周滚动计划,随着计划的推进,后面的里程碑会进行修正与细化。这条探索路径侧重软硬件部署的研究,即纯技术探索。对于最能体现工程师和架构师水平的业务拆分,这里不做探索,毕竟后者包含更多艺术成分和具体业务环境下的权衡。(p.s.我猜曹雪芹当年写红楼也是这么个思路,先把结论写出来,后面再来铺排。)
里程碑1.0
一个django+uwsgi的api后端、一个mysql数据库后台、一个vue+nginx前端
上周是在本地进行django的uwsgi部署,这次上容器。首先拉取一个最基础的ubuntu镜像,基于这个镜像安装uwsgi和部署django。
运行ubuntu镜像时要以-it模式(交互模式)运行,使容器始终保持与标准输入的连接。因为ubuntu镜像是最原始的基础镜像,里面没有跑任何后台守护进程,若不给bash指定一个始终连接的标准输入,容器运行后就会立刻结束。指令如下:
docker run --name mydjango -d -p 82:80 -it ubuntu:latest bash
通过exec指令获取mydjango容器的bash:
docker exec -it mydjango bash
因为需要安装python、pip、uwsgi,先要更新apt源。查看一下这个容器中的系统版本:
cat /etc/issue
返回的结果是20.04版,和宿主机的版本一致,可以直接用本地的sources.list覆盖容器中的源(如果版本不一致,可查看我11月27日的日志),下面这段指令是在宿主机上执行:
docker cp /etc/apt/sources.list mydjango:/etc/apt/sources.list
返回容器中,更新源。问题开始出现了,虽然系统的版本号一致,但docker容器里这个裁剪版的ubuntu和linux发行版的内核是有差别的,apt几乎瘫痪。要想安装python,只能通过编译安装这一条路。遂到python官网下载了源码,解压拷贝到容器中,然而在容器里gcc编译器也是没有的,这条路似乎绕得有点远。何不直接从已配置了python的镜像起步呢?rackspacedot/python37是docker hub上排名第一的基础镜像包,几十万的下载量,看来需求量还是很大,就从它开始吧。事后我在想,花些时间把编译安装这条路走通,也可以到docker hub上发布配置了python最新版的基础镜像包吧。这就像基础科学和工程科学的区别,世界上总存在对基础感兴趣又耐得住寂寞的人,有兴趣就不寂寞。
python基础镜像拉取到本地,同样的方法运行后获取容器bash。我在django开发环境下是配置了虚拟环境的,尝试了直接将虚拟环境整体拷贝到容器中运行,发现python版本问题,虚拟环境(与开发环境一致)是3.8.5,而容器里的python是较早的版本。转念一想,既然都已经是容器了,跑的是单个应用,需要虚拟环境干嘛?直接把venv目录删除,在开发环境下通过pip freeze > requirements.txt生成项目依赖清单,到容器里pip install -r requirements.txt将依赖包一键安装到主环境中。不知什么原因,在安装uwsgi时出现了问题,感谢这个问题,让我又尝试并找到了配置镜像的正道。这个问题事后回想,可能只是网络的偶发故障导致的,实际上后来通过Dockefile编译镜像包时,安装uwsgi的内部指令其实是一样的,但却并没有再出现问题。难道是上帝关上那道门为的是打开这道大门?Thanks God。
刚才提到了Dockerfile,这个才是编译镜像包的正确姿势。
在制作自己的镜像时,首先准备好需要复制到镜像中的文件,包括程序文件和配置文件。再通过Dockerfile中的指令进行文件复制、程序运行和启动配置。其实你想想,你通过镜像生成一个容器后,如果还需要对容器进行调整,无非也就是复制一些文件进去,覆盖一些配置文件,运行一些程序。而这些工作,都可以通过Dockerfile脚本的形式完成。这样生成的镜像运行后就无需再调整。这个工作有点类似于制作操作系统安装包,前天还和一个老朋友探讨过操作系统安装包的制作,今天就体会到了,虽然只是容器环境下,复杂度不同,但感觉应该类似。不能如愚见指月,观指不观月。
Dockefile的主体包括如下几个部分:
FROM rackspacedot/python37:latest #指明基础镜像包
COPY xxx /xxx/ #将Dockerfile同目录下的xxx文件或目录拷贝到生成镜像中的/xxx/目录下
RUN xxx #这里相当于在bash里执行指令,每条指令用一个RUN来标记,完成service文件的chmod修改等
MAINTAINER name email #留下你的大名和邮箱
EXPOSE 10000 #暴露端口号
ENTRYPOINT xxx #这个是启动执行命令,只能有一条
CMD xxx #这个也是启动执行命令(或给ENTRYPOINT传递默认参数),若启动容器时附加了参数,则CMD中的命令会被忽略
无非就是这些,靠它们的组合,可以配置出充满想象力的镜像。
比如,我在开发环境下的django项目根目录的上层目录处新建了Dockerfile(要细品,别被绕晕了,公益活动就不配图了),因为这个项目根目录要整体拷贝进镜像包中,所以需要与Dockerfile在同一目录下。我还想在镜像中增加service文件,并通过service启动uwsgi。那这个service文件也放到此处,通过COPY指令拷贝到镜像中的/etc/init.d/目录下。关于linux的service,待会儿做个简单介绍,这也算是在绕弯路时收获的风景吧。
万事俱备,只需要在ENTRYPOINT处启动服务就可以了。当然,这个地方也只相当于执行了一条bash指令,如果这条指令不能持续运行,那容器运行后也会随着这条指令的结束而结束。要想让容器持续运行,这条ENTRYPOINT指令不能运行为后台服务。也有变通的方法,就是在CMD里执行一条类似于这样的指令tail -f xxx文件,这条tail -f指令是持续刷新显示xxx文件的内容,类似于一个死循环,这样容器也不会自动停止。但这样做不太优雅。最优雅的方式还是在ENTRYPOINT处,既然是用容器提供一个一直运行的服务,那就在ENTRYPOINT的指令里直接运行这个提供服务的不会停止的程序就可以了。
说一下ubuntu的service机制。我们在bash里运行service xxx start|stop|reload|status指令,其实都是调用了ubuntu服务文件目录下对应文件中的start|stop|reload|status函数。这个服务文件就是一段sh脚本,你也可以增加一些别致的函数,仅此而已。ubuntu的服务目录有很多级别,代表了系统加载过程中的不同阶段。从rc0.d-rc6.d和rcS.d,以及上文提到的init.d。关于它们之间的区别,网上有资料,我们只需要了解在这些目录下可以编写自己的service文件,service文件是一段sh脚本,里面需要提供start、stop、reload、status等函数,当然也可以只提供部分。这些目录里有了service文件,在bash中就可以通过service xxx start来启动服务,xxx服务名就是放进服务目录的service文件名。一个典型的service文件结构如下:
start()
{
uwsgi --ini /home/uwsgi.ini & #这是一段bash指令,后面加上&是后台执行的意思,不输出内容到屏幕
exit 0;
}
stop()
{
uwsgi --stop /home/uwsgi_pid.log #同样是一段bash指令,uwsgi可以通过pid的方式优雅停止,这里也给配置上,前提是在uwsgi启动时的配置文件中要指定生成这个pid文件:pidfile = /home/uwsgi_pid.log
}
case "$1" in #$1是service xxx指令所带的第一个参数,比如start或stop
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 0
;;
esac #shell的语法,case esac
exit 0
在构建镜像时,提前准备好这个service文件,就以uwsgiservice命名吧,通过COPY指令拷贝到镜像中的服务目录下(/etc/init.d/uwsgiservice)。我在这个service程序中调用了uwsgi指令,这就需要在service uwsgiservice start时uwsgi已经安装好,即通过RUN指令pip install -r requirements.txt,uwsgi模块已经在 requirements.txt中列出了。
实际配置我的v12.0镜像时,并没有使用服务。还记得前面说过,要想容器运行后不停止,最优雅的方式是在ENTRYPOINT处执行一个不会停止的程序,这个uwsgi就不会停止,直接ENTRYPOINT uwsgi --ini /home/uwsgi.ini就可以了。
配置好Dockerfile,就可以通过docker build -t xxx:tag .指令来制作镜像。这个Dockerfile的名字不要随便取,就叫Dockerfile,位置放到需要拷贝进镜像的文件和目录的同一目录下。-t xxx:tag指定镜像的名字和版本,细心看一下,最后还有一个点,表示当前目录,这意味着执行这条指令时,需要先切换到Dockerfile所在的目录下。当然也可以通过加-f指令来切换目录,但我是本着能少用一个参数就少用一个的原则。
镜像制作好后,docker images看一下,是不是已经列出来了。我打包进镜像的django只有一个功能,就是返回托管django项目的节点的IP地址。因为我想测试集群部署后的负载均衡,当任务被调度到不同的节点后,返回的IP地址是会不同的。python获取主机IP地址的方法有很多,我这里也给出一个最优雅的方式:
import socket
def get_host_ip():
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
finally:
s.close()
return ip
要想看懂这些内容,恐怕得需要有django的基础知识。不过你如果知道web框架那也好办,django就是一个python下的web框架,类似于php的thinkphp,java的struts等。
这第一个里程碑走到这里才走了三分之一,路漫漫其修远亦,想得到和办得到差距还是挺大的,一个筋斗云十万八千里,那是心的速度,实际的取经路却是八戒离开高老庄的那句话:此去千山万水,路途遥远,容我去与红尘俗世道个别。