记package-lock引发的一次事故
|去年因为升级npm包导致在开发环境崩了(相关记录)。昨天在生产环境遇见了一个更严重的问题:线上环境升级了thrift,其升级包的某个依赖未正确安装,导致node服务启动失败,线上走静态容灾两个多小时~
经过排查和总结,发现之前对于pageack-lock的机制理解存在误区,因此这里整理下package-lock的原理和注意事项,避免下次遇见相同的问题。
<!--more-->
1. 相关概念
1.1. 语义化版本
npm 包支持语义化版本,简单理解: XYZ 的格式,对应为: 主版本号.次版本号.修订号,版本号递增规则如下:
- 主版本号:当你做了不兼容的 API 修改,
- 次版本号:当你做了向下兼容的功能性新增,
- 修订号:当你做了向下兼容的问题修正。
打开package.json我们会发现,大部分依赖包前面的都会有^或~的符号,他们的意思是,在使用npm install命令时:
- ^会匹配最新的大版本依赖包,比如^1.2.3会匹配所有1.x.x的包,包括1.3.0,但是不包括2.0.0
会匹配最近的小版本依赖包,比如1.2.3会匹配所有1.2.x版本,但是不包括1.3.0- 如果没有任何前置修饰符,则表示安装对应制定的版本号
默认使用-S或-D,不指定版本安装时,会自动添加^修饰符,因此在某些时候会造成版本的不兼容(所以说发版本包的时候版本号不能乱取)
1.2. Lock机制
package-lock.json
是当 node_modules
或package.json
发生变化时自动生成的文件。这个文件主要功能是确定当前安装的包的依赖,以便后续重新安装的时候生成相同的依赖,而忽略项目开发过程中有些依赖已经发生的更新
Lock机制是为了保证多人开发的统一性。什么是统一性?就是无论何时来了一个新人、换了个新电脑,我们npm i的包都是一致的。
随着项目越来越大,依赖越来越多,很难保证每一个npm包的最新版本都是适合的、有用的。Lock机制可以最大化解决此类冲突。在多人协作时同步开发环境。至于什么时候用新的包,到时候再同步lock文件就是。
2. package-lock的机制
2.1. 一个栗子
这里参考了网上的一个举例,用来说明package-lock的使用方法。
假设我们创建了一个新项目,它将使用express。 在运行npm init之后,在撰写本项目时,最新的express版本是4.15.4。 (默认情况下,npm 将安装最新版本)
因此在package.json中,"express":"^ 4.15.4"被添加作为依赖项。 假设明天,express的维护者会发布一个 bug 修复,所以最新版本变成了4.15.5。 然后,如果有人想要为我的项目做贡献,他们会克隆它,然后运行 npm install, 因为4.15.5是一个更高版本的主要版本,这是为他们安装的。 我们都有express依赖,但我们有两个不同的版本。 理论上,它们应该还是兼容的,但是也许这个 bug 会影响我们正在使用的功能,而我们的应用程序在使用Express版本4.15.4与4.15.5进行比较时会产生不同的结果。
而package-lock.json的作用就是用来保证我们的应用程序依赖之间的关系是一致的, 兼容的.
2.2. 是否需要把package-lock文件提交git版本库
由于Lock机制的主要目的是保证多人开发的版本一致,package-lock文件为的是让开发者知道只要你保存了源文件,到一个新的机器上、或者新的下载源,只要按照这个package-lock.json所标示的具体版本下载依赖库包,就能确保所有库包与你上次安装的完全一样。
基于以上原因,因此建议将package-lock文件锁定安装时的包的版本号,并且上传到git,以保证其他人在npm install时大家的依赖能保证一致。
官方文档的建议也是是提交到版本库,因此不能写到.gitignore
This file is intended to be committed into source repositories, and serves various purposes:
如果确定不把package-lock写到版本库,则必须清楚自己做的是什么~这样不能体会到其他依赖包的语义化版本修复了
参考问题:
2.3. package-lock的规则变化
自npm 5.0版本发布以来,npm i的规则发生了三次变化。
- npm 5.0.x 版本,不管package.json怎么变,npm i 时都会根据lock文件下载
- npm 5.1.0版本后 npm install 会无视lock文件 去下载最新的npm
- npm 5.4.2版本后,
- 如果改了package.json,且package.json和lock文件不同,那么执行
npm i
时npm会根据package中的版本号以及语义含义去下载最新的包,并更新至lock - 如果两者是同一状态,那么执行
npm i
,都会根据lock下载,不会理会package实际包的版本是否有新。
- 如果改了package.json,且package.json和lock文件不同,那么执行
3. 事故分析
现在来分析下篇头提到的线上事故。
3.1. 过程描述
首先是环境版本,服务机器上使用的node版本是v8.4.0,对应的npm版本是v5.3.0,使用的是上面的版本2的规则。
然后介绍一下背景,由于历史问题,在我们的开发环境存在多个thrift编译版本,为了保证生产环境的thrift版本一致,在package.json
中对应的版本号是固定的未任何修饰符的版本。
此外,生产服务器上设置的npm镜像地址是由公司内网维护的,当内网上没有对应的私有npm包时,会从taobao镜像拉取。
我们在.gitignore
文件中将package-lock
文件移除了版本库,因此服务器上保存的lock文件由npm i 自动生成并控制。
在最近的需求中对thrift进行了升级,从thrift@0.9.1
升级到0.11.0
。出现问题的库是thrift的依赖node-int64
,从v0.3.1
升级到了v0.4.0
。
you
3.2. 还原步骤
首先安装固定版本thrift@0.9.1
,切换到thrift对应包目录下的package.json
"dependencies": {
"node-int64": "~0.3.0",
"nodeunit": "~0.8.0"
},
依赖的是~
小版本更新,会自动安装最新的0.3.x
版本的node-int64
然后然后修改package.json中的版本号为hrift@0.11.0
,删除node_modules
目录后重新npm i,查看对应package.json
中
"dependencies": {
"node-int64": "^0.4.0",
"q": "^1.5.0",
"ws": ">= 2.2.3"
},
同时切换到对应的node-int64
中,发现库版本已更新到0.4.0
的版本,说明更新依赖成功了,同时查看lock文件中的node-int64
版本,也已经更新为0.4.0
。
3.3. 分析
一般地,node_modules
目录不会提交到版本库,取而代之的是通过package.json
声明项目的依赖,其他人会通过克隆仓库然后执行npm i
安装依赖并初识化项目。
前面提交我们的lock文件没有进入版本库,因此thrift@0.9.1时的lock文件对应的node-int64
版本。重新更新部署时,脚本会删除node_modules并重新执行npm i,根据上面的规则2,npm install 会无视lock文件 去下载最新的npm,因此会更新package-lock的文件。这与我们上面还原步骤得到的结果一致。
那么,为什么会报node-int64
包不存在的错误呢?这就显得有点诡异了,按照上面的流程更新时应该会安装对应的依赖才对。
后来发现,在某次代码commit时,由于编辑器的缘故,提交了对应的lock文件到代码库,之后进行了部署,然后又重新删除了该lock文件。相当于服务器上的拉取到了代码库中的lock文件,对比文件发现提交的lock文件中对应的node-int64
版本为v0.3.3
。
参考上面规则2,修改package.json中的版本后,应该会忽略对应的lock文件,直接安装新版本并更新lock文件。这种规则也不会导致某个依赖包不安装的情况。
那么出现依赖包不存在的原因,只可能是在这次部署时执行npm i 时出现了某些异常,我们的持续部署流程出现了一些问题。由于事关线上用户访问,紧急的解决办法将node-int64手动写到package.json文件中,然后重新部署,这次依赖安装成功了,服务也正常启动了。
但是真正的原因是什么呢?我尝试搜索了"npm i 时某个依赖包未安装"等问题,却没有找到相关的答案,可能是一次环境导致的意外吧。由于找不到部署日志了,暂时只能排查到这里。
4. 小结
貌似到最后还没有解决这个问题,不过对应package-lock文件的作用有了大致了解。除了npm,其他JavaScript包管理工具甚至其他语言的包管理工具基本都存在类似的lock机制,如yarn-lock
、composer.lock
等。
对于日常工作中常用的一些工具,不能仅限于使用而不去了解其原理,貌似最近都很浮躁,缺少静下心思考的时候,需要改一改。