代码开发完成后,需要构建,部署到服务器并运行,才能被用户访问。不同的代码需要不同的环境。比如JS代码的构建需要node环境,Java代码需要JVM环境。一般我们会把他们隔离开,单独部署。现在一台物理主机的性能很高,可以同时运行很多服务,而且我们有环境隔离的需求,所以我们会利用虚拟化技术,将一台物理主机变成多台虚拟主机来使用。现在主流的虚拟化技术是docker,它是一种基于容器的虚拟化技术。它可以在一台机器上运行多个容器,每个容器都有一个独立的操作系统环境,比如文件系统、网络端口等。所以它的标志是这样的:它是如何实现这个隔离的容器的?这取决于操作系统的机制:linix提供了一种叫做命名空间的机制,可以为进程、用户、网络等分配一个命名空间,这个命名空间下的资源都是独立命名的。比如PID命名空间,也就是进程的命名空间,会将命名空间中的进程id改为1,而linux的初始进程id为1,所以它是这个命名空间中所有进程的父进程。IPC命名空间可以限制只有本命名空间内的进程可以相互通信,不能与命名空间外的进程通信。挂载命名空间会创建一个新的文件系统,命名空间中的文件访问是在这个文件系统之上的。命名空间有6种类型:PID命名空间:进程id的命名空间IPC命名空间:进程通信的命名空间Mount命名空间:文件系统挂载的命名空间Network命名空间:网络的命名空间User命名空间:用户和用户组NamespaceUTS命名空间:主机名和域名的命名空间通过这6个命名空间,Docker实现了资源的隔离。但是仅仅隔离命名空间是不够的。还是有问题。比如一个容器占用资源过多,会影响到其他容器。如何限制容器的资源访问?这就需要linux操作系统的另一种机制:ControlGroup。创建一个ControlGroup可以为它指定参数,比如使用了多少cpu,内存,磁盘,然后加入到这个组中的进程都会受到这个限制。这样在创建容器的时候,先创建一个ControlGroup,指定资源限制,然后将容器进程添加到这个ControlGroup中,这样就不会出现容器占用过多资源的问题。那是完美的吗?其实还有一个问题:每个容器都是一个独立的文件系统,相互独立,而且这些文件系统可能大部分都是一样的,同样的内容占用大量的磁盘空间,会导致浪费。那么如何解决这个问题呢?Docker设计了分层机制:每一层都是不可修改的,也叫镜像。想修改怎么办?会创建一个新的层,在这个层上进行修改,然后这些层会通过一种叫做UnionFS的机制合并成一个文件系统:这样,如果在多个容器中修改文件,只需要创建不同的层即可,底层基础图像是相同的。Docker利用这种分层镜像存储和写时复制机制,大大降低了文件系统的磁盘占用率。而且这种镜像是可以重复使用的,上传到镜像仓库,其他人拉下来后就可以直接使用了。比如下面的Docker架构图:docker文件系统的内容以镜像的形式存储,可以上传到registry仓库。dockerpull拉下来后,dockerrun之后就可以运行了。回顾Docker实现原理的三大基本技术:Namespace:实现各种资源的隔离ControlGroup:实现容器进程的资源访问限制UnionFS:实现容器文件系统的分层存储、copy-on-write、mirror合并不可能。上图中的dockerbuild是什么?一般我们生成镜像都是通过dockerfile来描述的。例如:FROMnode:10WORKDIR/appCOPY。/appEXPOSE8080RUNnpminstallhttp-server-gRUNnpminstall&&npmrunbuildCMDhttp-server./distDokcer分层存放,修改时会新建一个层,所以这里的每一行都会新建一个层。这些指令的含义如下:FROM:基于一个基础镜像进行修改WORKDIR:指定当前工作目录COPY:将容器外的内容复制到容器中EXPOSE:声明当前容器要访问的网络端口,对于例如,这里的服务将使用8080RUN:执行容器中的命令CMD:容器启动时执行的命令。上面dockerfile的作用不难看出。就是复制node环境下的项目,进行依赖的安装和构建。我们可以通过dockerbuild基于这个dockerfile生成镜像。然后执行dockerrun运行镜像,再执行http-server./dist启动服务。这是docker运行节点静态服务的示例。但实际上,这个例子并不是很好。从上面的过程描述可以看出,构建过程只是为了获取产品,容器运行时就不再需要了。可不可以把构建分到一个镜像,然后把产品分到另一个镜像,让产品单独运行?它确实如此,并且是推荐的用法。构建一个dockerfile,运行一个dockerfile,难道不是必须的吗?不,docker支持多阶段构建,例如:#buildstageFROMnode:10ASbuild_imageWORKDIR/appCOPY。/appEXPOSE8080RUNnpminstall&&npmrunbuild#productionstageFROMnode:10WORKDIR/appCOPY--from=build_image/app/dist。/distRUNnpmi-ghttp-serverCMDhttp-server./dist我们将两张图片的生成过程写成了dockerfile,这是docker支持的多阶段构建。在第一个FROM中,我们写成asbuild_image,也就是将第一个镜像命名为build_image。当第二个图像COPY之后,您可以指定--from=build_image以从该图像复制内容。这样最后就只剩下第二张图了。这个镜像只有生产环境需要的依赖,体积更小。传输速度和运行速度会更快。最好的做法是将构建图像与运行图像分开。一般我们都是在jenkins里面跑。我们在push代码的时候,通过webhooks触发jenkinsbuild,最后生成runtimeimage上传到registry。部署的时候拉下镜像docker,然后dockerrun即可完成部署。node项目的dockerfile我们知道怎么写,但是前端项目呢?可能是这样的:#buildstageFROMnode:14.15.0asbuild-stageWORKDIR/appCOPYpackage.json./RUNnpminstallCOPY。.RUNnpmrunbuild#productionstageFROMnginx:stable-perlasproduction-stageCOPY--from=build-stage/app/dist/usr/share/nginx/htmlCOPY--from=build-stage/app/default.conf/etc/nginx/conf.d/default.confEXPOSE80CMD["nginx","-g","daemonoff;"]也是在构建阶段通过镜像构建,然后制作镜像复制产品,然后使用nginx运行静态服务。在公司部署前端项目一般都是这样。但不一定。因为公司部署了前端代码服务作为CDN的源站,CDN会从这里抓取文件,然后缓存到各个区域的缓存服务器中。阿里云等云服务商提供对象存储服务,可以直接上传静态文件到oss,无需自己部署;但是,如果是内部网站或者私有部署,还是需要用docker部署。总结一下,Docker是一种虚拟化技术。它的实现原理是通过容器依赖LinuxNamespace、ControlGroup、UnionFS这三种机制。Namespace用于资源隔离,ControlGroup用于容器资源限制,UnionFS用于文件系统镜像存储、copy-on-write和镜像合并。一般我们通过dockerfile描述镜像构建过程,然后通过dockerbuild构建镜像并上传到registry。镜像可以通过dockerrun运行,对外提供服务。使用dockerfile进行部署的最佳实践是分阶段构建。构建阶段生成一个单独的镜像,然后将产品复制到另一个镜像,并将该镜像上传到注册中心。这样镜像最小,传输速度和运行速度都比较快。前端代码和节点代码都可以用docker部署,前端代码的静态服务同时作为CDN的源站。但是,我们不必自己部署它。可以直接使用阿里云的OSS对象存储服务。了解Docker的实现原理,知道如何编写dockerfile以及dockerfile的阶段性构建,就可以满足大部分前端部署需求。
