>  기사  >  웹 프론트엔드  >  Node 서비스의 Docker 미러링을 수행하는 방법은 무엇입니까? 극한 최적화에 대한 자세한 설명

Node 서비스의 Docker 미러링을 수행하는 방법은 무엇입니까? 극한 최적화에 대한 자세한 설명

青灯夜游
青灯夜游앞으로
2022-10-19 19:38:452151검색

Node 서비스의 Docker 미러링을 수행하는 방법은 무엇입니까? 극한 최적화에 대한 자세한 설명

이 기간 동안 저는 다양한 카테고리에 대한 액세스 생성 및 배포를 촉진하고 클라우드에서는 서비스 내용을 수정하고 통일된 제품 버전 관리를 위해 Docker를 활용하는 것을 고려하고 있습니다. 이 글에서는 제가 Docker를 서비스하면서 쌓은 최적화 경험을 여러분의 참고용으로 공유하겠습니다. [권장 관련 튜토리얼: nodejs 동영상 튜토리얼]

예제부터 시작하세요. Docker를 처음 접하는 대부분의 학생들은 아래와 같이 프로젝트의 Dockerfile을 작성해야 합니다.

FROM node:14
WORKDIR /app

COPY . .
# 安装 npm 依赖
RUN npm install

# 暴露端口
EXPOSE 8000

CMD ["npm", "start"]

Build, package, upload, all in 한 번 가세요. 그럼 이미지 상태를 보세요. 젠장, 간단한 노드 웹 서비스의 볼륨은 실제로 놀라운 1.3G에 도달했으며, 이미지 전송 및 구성 속도도 매우 느립니다.

docker 镜像优化前

이것은 중요하지 않습니다. 이미지는 하나의 인스턴스만 배포하면 됩니다. 예, 하지만 이 서비스는 고주파 통합 및 배포 환경을 위해 모든 개발 학생에게 제공되어야 합니다(고빈도 통합을 달성하기 위한 솔루션은 이전 기사 참조). 우선, 이미지 크기가 너무 크면 필연적으로 이미지 가져오기 및 업데이트 속도에 영향을 미치고 통합 경험이 더 나빠집니다. 둘째, 프로젝트가 시작된 후 동시에 수만 개의 테스트 환경 인스턴스가 온라인에 있을 수 있습니다. 이러한 컨테이너 메모리 소비 비용은 어떤 프로젝트에서도 허용되지 않습니다. 최적화된 솔루션을 찾아야 합니다.

문제를 발견한 후 Docker의 최적화 계획을 연구하기 시작했고 이미지 수술을 준비했습니다.

Node 프로젝트 제작 환경 최적화

가장 먼저 해야 할 일은 당연히 프런트 엔드에서 가장 친숙한 영역인 코드 자체의 크기를 최적화하는 것입니다. 이전에는 프로젝트를 개발할 때 Typescript를 사용하여 문제를 해결하기 위해 tsc를 사용하여 프로젝트를 직접 패키징하여 es5를 생성한 후 직접 실행했습니다. 여기에는 크게 두 가지 문제가 있는데, 하나는 개발 환경 TS 소스 코드가 처리되지 않았고 프로덕션 환경에서 사용되는 JS 코드가 압축되지 않았다는 것입니다.

tsc 打包

다른 하나는 참조된 node_modules가 너무 크다는 것입니다. ts-node, typescript 등과 같은 개발 및 디버깅 환경을 위한 많은 npm 패키지가 여전히 포함되어 있습니다. 이제 js로 패키징되었으므로 이러한 종속성은 자연스럽게 제거되어야 합니다.

일반적으로 서버측 코드는 프론트엔드 코드처럼 노출되지 않기 때문에, 물리적인 머신에서 실행되는 서비스는 안정성에 더 관심이 있고 추가 용량에 신경쓰지 않기 때문에 이런 곳은 일반적으로 처리되지 않습니다. 그러나 Dockerization 이후 배포 규모가 커질수록 이러한 문제는 매우 분명해지며 프로덕션 환경에서 최적화되어야 합니다.

사실 우리 프론트엔드는 이 두 가지 최적화 방법에 대해 매우 익숙합니다. 이 글의 초점이 아니라면 간단히 언급하겠습니다. 첫 번째로 Webpack + babel을 사용하여 Typescript 소스 코드를 다운그레이드하고 압축합니다. 오류 문제 해결이 걱정된다면 소스 맵을 추가할 수 있지만 나중에 설명할 Docker 이미지에는 약간 중복됩니다. 두 번째로는 npm 패키지의 종속성 및 devDependency를 정리하고, 프로덕션 환경에서 사용하기 쉽도록 런타임에 필요하지 않은 종속성을 제거합니다. npm install --production 종속성을 설치합니다.

프로젝트 이미지 크기 최적화

기본 이미지를 최대한 간소화하세요

우리는 컨테이너 기술이 운영 체제 수준에서 프로세스 격리를 제공한다는 것을 알고 있습니다. 즉, Docker 컨테이너 자체는 독립적인 운영 체제에서 실행되는 프로세스입니다. , Docker 이미지는 독립적으로 실행될 수 있는 운영 체제 수준 환경으로 패키징되어야 합니다. 따라서 이미지 크기를 결정하는 중요한 요소는 바로 이미지에 패키지된 Linux 운영 체제의 크기입니다.

일반적으로 종속 운영 체제의 크기를 줄이려면 두 가지 측면을 고려해야 합니다. 첫 번째는 Linux에서 Python, cmake, telnet 등 불필요한 모든 종류의 도구 라이브러리를 최대한 제거하는 것입니다. 두 번째는 보다 가벼운 Linux 배포 시스템을 선택하는 것입니다. 일반 공식 이미지는 위의 두 가지 요소를 기반으로 각 릴리스의 거세된 버전을 제공해야 합니다.

공식적으로 제공되는 node 버전인 node:14를 예로 들어 보겠습니다. 기본 버전의 기본 운영 환경은 Ubuntu이며, 이는 최대의 호환성을 보장하는 대규모의 포괄적인 Linux 배포판입니다. 불필요한 도구 라이브러리 종속성을 제거한 버전을 node:14-slim 버전이라고 합니다. 가장 작은 이미지 분포를 node:14-alpine이라고 합니다. Linux alpine은 기본 도구만 포함된 매우 간소화된 경량 Linux 배포판입니다. 자체 Docker 이미지 크기가 4~5M에 불과하므로 Docker 이미지의 가장 작은 버전을 만드는 데 매우 적합합니다.

저희 서비스에서는 서비스 실행을 위한 종속성이 결정적이므로 기본 이미지의 크기를 최대한 줄이기 위해 프로덕션 환경의 기본 이미지로 Alpine 버전을 선택합니다.

分级构建

这时候,我们遇到了新的问题。由于 alpine 的基本工具库过于简陋,而像 webpack 这样的打包工具背后可能使用的插件库极多,构建项目时对环境的依赖较大。并且这些工具库只有编译时需要用到,在运行时是可以去除的。对于这种情况,我们可以利用 Docker 的分级构建的特性来解决这一问题。

首先,我们可以在完整版镜像下进行依赖安装,并给该任务设立一个别名(此处为build)。

# 安装完整依赖并构建产物
FROM node:14 AS build
WORKDIR /app

COPY package*.json /app/
RUN ["npm", "install"]
COPY . /app/

RUN npm run build

之后我们可以启用另一个镜像任务来运行生产环境,生产的基础镜像就可以换成 alpine 版本了。其中编译完成后的源码可以通过--from参数获取到处于build任务中的文件,移动到此任务内。

FROM node:14-alpine AS release
WORKDIR /release

COPY package*.json /
RUN ["npm", "install", "--registry=http://r.tnpm.oa.com", "--production"]

# 移入依赖与源码
COPY public /release/public
COPY --from=build /app/dist /release/dist

# 启动服务
EXPOSE 8000

CMD ["node", "./dist/index.js"]

Docker 镜像的生成规则是,生成镜像的结果仅以最后一个镜像任务为准。因此前面的任务并不会占用最终镜像的体积,从而完美解决这一问题。

当然,随着项目越来越复杂,在运行时仍可能会遇到工具库报错,如果曝出问题的工具库所需依赖不多,我们可以自行补充所需的依赖,这样的镜像体积仍然能保持较小的水平。

其中最常见的问题就是对node-gypnode-sass库的引用。由于这个库是用来将其他语言编写的模块转译为 node 模块,因此,我们需要手动增加g++ make python这三个依赖。

# 安装生产环境依赖(为兼容 node-gyp 所需环境需要对 alpine 进行改造)
FROM node:14-alpine AS dependencies

RUN apk add --no-cache python make g++
COPY package*.json /
RUN ["npm", "install", "--registry=http://r.tnpm.oa.com", "--production"]
RUN apk del .gyp

详情可见:https://github.com/nodejs/docker-node/issues/282

合理规划 Docker Layer

构建速度优化

我们知道,Docker 使用 Layer 概念来创建与组织镜像,Dockerfile 的每条指令都会产生一个新的文件层,每层都包含执行命令前后的状态之间镜像的文件系统更改,文件层越多,镜像体积就越大。而 Docker 使用缓存方式实现了构建速度的提升。若 Dockerfile 中某层的语句及依赖未更改,则该层重建时可以直接复用本地缓存

如下所示,如果 log 中出现Using cache字样时,说明缓存生效了,该层将不会执行运算,直接拿原缓存作为该层的输出结果。

Step 2/3 : npm install
 ---> Using cache
 ---> efvbf79sd1eb

通过研究 Docker 缓存算法,发现在 Docker 构建过程中,如果某层无法应用缓存,则依赖此步的后续层都不能从缓存加载。例如下面这个例子:

COPY . .
RUN npm install

此时如果我们更改了仓库的任意一个文件,此时因为npm install层的上层依赖变更了,哪怕依赖没有进行任何变动,缓存也不会被复用。

因此,若想尽可能的利用上npm install层缓存,我们可以把 Dockerfile 改成这样:

COPY package*.json .
RUN npm install
COPY src .

这样在仅变更源码时,node_modules的依赖缓存仍然能被利用上了。

由此,我们得到了优化原则:

  • 最小化处理变更文件,仅变更下一步所需的文件,以尽可能减少构建过程中的缓存失效。

  • 对于处理文件变更的 ADD 命令、COPY 命令,尽量延迟执行。

构建体积优化

在保证速度的前提下,体积优化也是我们需要去考虑的。这里我们需要考虑的有三点:

  • Docker 是以层为单位上传镜像仓库的,这样也能最大化的利用缓存的能力。因此,执行结果很少变化的命令需要抽出来单独成层,如上面提到的npm install的例子里,也用到了这方面的思想。

  • 如果镜像层数越少,总上传体积就越小。因此,在命令处于执行链尾部,即不会对其他层缓存产生影响的情况下,尽量合并命令,从而减少缓存体积。例如,设置环境变量和清理无用文件的指令,它们的输出都是不会被使用的,因此可以将这些命令合并为一行 RUN 命令。

RUN set ENV=prod && rm -rf ./trash
  1. Docker cache 的下载也是通过层缓存的方式,因此为了减少镜像的传输下载时间,我们最好使用固定的物理机器来进行构建。例如在流水线中指定专用宿主机,能是的镜像的准备时间大大减少。

当然,时间和空间的优化从来就没有两全其美的办法,这一点需要我们在设计 Dockerfile 时,对 Docker Layer 层数做出权衡。例如为了时间优化,需要我们拆分文件的复制等操作,而这一点会导致层数增多,略微增加空间。

这里我的建议是,优先保证构建时间,其次在不影响时间的情况下,尽可能的缩小构建缓存体积。

以 Docker 的思维管理服务

避免使用进程守护

我们编写传统的后台服务时,总是会使用例如 pm2、forever 等等进程守护程序,以保证服务在意外崩溃时能被监测到并自动重启。但这一点在 Docker 下非但没有益处,还带来了额外的不稳定因素。

首先,Docker 本身就是一个流程管理器,因此,进程守护程序提供的崩溃重启,日志记录等等工作 Docker 本身或是基于 Docker 的编排程序(如 kubernetes)就能提供了,无需使用额外应用实现。除此之外,由于守护进程的特性,将不可避免的对于以下的情况产生影响:

  • 增加进程守护程序会使得占用的内存增多,镜像体积也会相应增大。

  • 由于守护进程一直能正常运行,服务发生故障时,Docker 自身的重启策略将不会生效,Docker 日志里将不会记录崩溃信息,排障溯源困难。

  • 由于多了个进程的加入,Docker 提供的 CPU、内存等监控指标将变得不准确。

因此,尽管 pm2 这样的进程守护程序提供了能够适配 Docker 的版本:pm2-runtime,但我仍然不推荐大家使用进程守护程序。

其实这一点其实是源自于我们的固有思想而犯下的错误。在服务上云的过程中,难点其实不仅仅在于写法与架构上的调整,开发思路的转变才是最重要的,我们会在上云的过程中更加深刻体会到这一点。

日志的持久化存储

无论是为了排障还是审计的需要,后台服务总是需要日志能力。按照以往的思路,我们将日志分好类后,统一写入某个目录下的日志文件即可。但是在 Docker 中,任何本地文件都不是持久化的,会随着容器的生命周期结束而销毁。因此,我们需要将日志的存储跳出容器之外。

最简单的做法是利用 Docker Manager Volume,这个特性能绕过容器自身的文件系统,直接将数据写到宿主物理机器上。具体用法如下:

docker run -d -it --name=app -v /app/log:/usr/share/log app

运行 docker 时,通过-v 参数为容器绑定 volumes,将宿主机上的 /app/log 目录(如果没有会自动创建)挂载到容器的 /usr/share/log 中。这样服务在将日志写入该文件夹时,就能持久化存储在宿主机上,不随着 docker 的销毁而丢失了。

当然,当部署集群变多后,物理宿主机上的日志也会变得难以管理。此时就需要一个服务编排系统来统一管理了。从单纯管理日志的角度出发,我们可以进行网络上报,给到云日志服务(如腾讯云 CLS)托管。或者干脆将容器进行批量管理,例如Kubernetes这样的容器编排系统,这样日志作为其中的一个模块自然也能得到妥善保管了。这样的方法很多,就不多加赘述了。

k8s 服务控制器的选择

镜像优化之外,服务编排以及控制部署的负载形式对性能的影响也很大。这里以最流行的Kubernetes的两种控制器(Controller):DeploymentStatefulSet 为例,简要比较一下这两类组织形式,帮助选择出最适合服务的 Controller。

StatefulSet是 K8S 在 1.5 版本后引入的 Controller,主要特点为:能够实现 pod 间的有序部署、更新和销毁。那么我们的制品是否需要使用 StatefulSet 做 pod 管理呢?官方简要概括为一句话:

Deployment 用于部署无状态服务,StatefulSet 用来部署有状态服务。

这句话十分精确,但不易于理解。那么,什么是无状态呢?在我看来,StatefulSet的特点可以从如下几个步骤进行理解:

  • StatefulSet管理的多个 pod 之间进行部署,更新,删除操作时能够按照固定顺序依次进行。适用于多服务之间有依赖的情况,如先启动数据库服务再开启查询服务。

  • 由于 pod 之间有依赖关系,因此每个 pod 提供的服务必定不同,所以 StatefulSet 管理的 pod 之间没有负载均衡的能力。

  • 又因为 pod 提供的服务不同,所以每个 pod 都会有自己独立的存储空间,pod 间不共享。

  • 为了保证 pod 部署更新时顺序,必须固定 pod 的名称,因此不像 Deployment 那样生成的 pod 名称后会带一串随机数。

  • Pod 이름이 고정되어 있으므로 StatefulSet에 연결된 Service클러스터 IP를 제공하지 않고도 Pod 이름을 액세스 도메인 이름으로 직접 사용할 수 있습니다. code> code>이므로 StatefulSet에 연결된 ServiceHeadless Service라고 합니다. StatefulSet 对接的 Service 中可以直接以 pod 名称作为访问域名,而不需要提供Cluster IP,因此跟 StatefulSet 对接的 Service 被称为 Headless Service

通过这里我们就应该明白,如果在 k8s 上部署的是单个服务,或是多服务间没有依赖关系,那么 Deployment 一定是简单而又效果最佳的选择,自动调度,自动负载均衡。而如果服务的启停必须满足一定顺序,或者每一个 pod 所挂载的数据 volume 需要在销毁后依然存在,那么建议选择 StatefulSet

本着如无必要,勿增实体的原则,强烈建议所有运行单个服务工作负载采用 Deployment

이를 통해 k8s에 단일 서비스가 배포되거나 여러 서비스 간에 종속성이 없는 경우 배포가 단순해야 하며 최상의 효과 선택이 있어야 한다는 점을 이해해야 합니다. 자동 스케줄링, 자동 로드 밸런싱. 서비스 시작 및 중지가 특정 순서를 충족해야 하거나 각 Pod에 탑재된 데이터 볼륨이 폐기 후에도 계속 존재해야 하는 경우 StatefulSet를 선택하는 것이 좋습니다. 필요한 경우가 아니면 엔터티를 추가하지 않는다는 원칙에 따라 단일 서비스를 실행하는 모든 워크로드는 배포를 컨트롤러로 사용하는 것이 좋습니다.

마지막에 작성

docker 镜像优化后

공부하다가 초반에 목표를 잊어버릴뻔해서 최적화 결과를 보기 위해 빠르게 Docker를 재구축했습니다.

이미지 볼륨에 대한 최적화 효과는 여전히 양호하여 약 10배에 도달하는 것을 볼 수 있습니다. 물론 프로젝트에 그렇게 높은 버전의 노드 지원이 필요하지 않은 경우 이미지 크기를 절반 정도 더 줄일 수 있습니다. Node 서비스의 Docker 미러링을 수행하는 방법은 무엇입니까? 극한 최적화에 대한 자세한 설명

미러 웨어하우스는 저장된 이미지 파일을 압축하고 node14로 패키지된 이미지 버전은 결국 50M 미만으로 압축됩니다.

물론 눈에 보이는 볼륨 데이터 외에도 실제로 더 중요한 최적화는 아키텍처 설계 수준에서 물리적 기계 중심 서비스에서 컨테이너형 클라우드 서비스로 전환하는 것입니다. 컨테이너화는 이미 가시적인 미래입니다. 개발자로서 기술을 생산성으로 전환하고 프로젝트 발전에 기여하려면 항상 최첨단 기술에 민감하고 적극적으로 실천해야 합니다.

🎜노드 관련 지식을 더 보려면 🎜nodejs 튜토리얼🎜을 방문하세요! 🎜

위 내용은 Node 서비스의 Docker 미러링을 수행하는 방법은 무엇입니까? 극한 최적화에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 cnblogs.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제