易栈网-膘叔

浏览模式: 标准 | 列表 | 图文
清理docker使用空间

清理docker使用空间

先说明:服务器上慎重,本地可以多折腾(小心volumn中存放的数据,是不是你原来直接存在image里了。如果不是,随意,大不了重新build)

原文来自:https://blog.fundebug.com/2018/01/10/how-to-clean-docker-disk/
其实主要就几个方式:

1. docker system命令

在谁用光了磁盘?Docker System命令详解中,我们详细介绍了docker system命令,它可以用于管理磁盘空间。

docker system df命令,类似于Linux上的df命令,用于查看Docker的磁盘使用情况:

docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 147 36 7.204GB 3.887GB (53%)
Containers 37 10 104.8MB 102.6MB (97%)
Local Volumes 3 3 1.421GB 0B (0%)
Build Cache 0B 0B

可知,Docker镜像占用了7.2GB磁盘,Docker容器占用了104.8MB磁盘,Docker数据卷占用了1.4GB磁盘。

docker system prune命令可以用于清理磁盘,删除关闭的容器、无用的数据卷和网络,以及dangling镜像(即无tag的镜像)。docker system prune -a命令清理得更加彻底,可以将没有容器使用Docker镜像都删掉。注意,这两个命令会把你暂时关闭的容器,以及暂时没有用到的Docker镜像都删掉了…所以使用之前一定要想清楚吶。

执行docker system prune -a命令之后,Docker占用的磁盘空间减少了很多:

docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 10 10 2.271GB 630.7MB (27%)
Containers 10 10 2.211MB 0B (0%)
Local Volumes 3 3 1.421GB 0B (0%)
Build Cache 0B 0B

2. 手动清理Docker镜像/容器/数据卷

对于旧版的Docker(版本1.13之前),是没有docker system命令的,因此需要进行手动清理。这里给出几个常用的命
删除所有关闭的容器

docker ps -a | grep Exit | cut -d ' ' -f 1 | xargs docker rm

删除所有dangling镜像(即无tag的镜像):

docker rmi $(docker images | grep "^<none>" | awk "{print $3}")

删除所有dangling数据卷(即无用的volume):

docker volume rm $(docker volume ls -qf dangling=true)

Fundebug提供实时、专业的错误监控服务,为您的线上代码保驾护航,欢迎大家免费使用!

3. 限制容器的日志大小

有一次,当我使用1与2提到的方法清理磁盘之后,发现并没有什么作用,于是,我进行了一系列分析。

在Ubuntu上,Docker的所有相关文件,包括镜像、容器等都保存在/var/lib/docker/目录中:

du -hs /var/lib/docker/
97G /var/lib/docker/

Docker竟然使用了将近100GB磁盘,这也是够了。使用du命令继续查看,可以定位到真正占用这么多磁盘的目录:

92G	/var/lib/docker/containers/a376aa694b22ee497f6fc9f7d15d943de91c853284f8f105ff5ad6c7ddae7a53

由docker ps可知,nginx容器的ID恰好为a376aa694b22,与上面的目录/var/lib/docker/containers/a376aa694b22的前缀一致:

docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a376aa694b22 192.168.59.224:5000/nginx:1.12.1 "nginx -g 'daemon off" 9 weeks ago Up 10 minutes nginx

因此,nginx容器竟然占用了92GB的磁盘。进一步分析可知,真正占用磁盘空间的是nginx的日志文件。那么这就不难理解了。我们Fundebug每天的数据请求为百万级别,那么日志数据自然非常大。

使用truncate命令,可以将nginx容器的日志文件“清零”:

truncate -s 0 /var/lib/docker/containers/a376aa694b22ee497f6fc9f7d15d943de91c853284f8f105ff5ad6c7ddae7a53/*-json.log

当然,这个命令只是临时有作用,日志文件迟早又会涨回来。要从根本上解决问题,需要限制nginx容器的日志文件大小。这个可以通过配置日志的max-size来实现,下面是nginx容器的docker-compose配置文件:

nginx:
image: nginx:1.12.1
restart: always
logging:
driver: "json-file"
options:
max-size: "5g"

重启nginx容器之后,其日志文件的大小就被限制在5GB,再也不用担心了~

4. 重启Docker

还有一次,当我清理了镜像、容器以及数据卷之后,发现磁盘空间并没有减少。根据Docker disk usage提到过的建议,我重启了Docker,发现磁盘使用率从83%降到了19%。根据高手指点,这应该是与内核3.13相关的BUG,导致Docker无法清理一些无用目录:

it’s quite likely that for some reason when those container shutdown, docker couldn’t remove the directory because the shm device was busy. This tends to happen often on 3.13 kernel. You may want to update it to the 4.4 version supported on trusty 14.04.5 LTS.

The reason it disappeared after a restart, is that daemon probably tried and succeeded to clean up left over data from stopped containers.

我查看了一下内核版本,发现真的是3.13:

uname -r
3.13.0-86-generic

如果你的内核版本也是3.13,而且清理磁盘没能成功,不妨重启一下Docker。当然,这个晚上操作比较靠谱。

--EOF
没有拷贝的很全,比如作者说为什么用docker。说白了,想用就用呗,如果你有旧版本的系统要兼容。docker才是真心方便(不过一个image 2~300M还是挺纠结的。PHP+debian的镜像)
CHROME使用旧版UI

CHROME使用旧版UI

有些人就是这样,不喜欢新版的UI,还好,有方法,原文就来自:https://zhuanlan.zhihu.com/p/43865953

2018.09.05 谷歌浏览器更新至了V69.0.3497.81版本,标签页UI等做了扁平化的改变。个人看来并不好看,特别是https绿色状态的显示也取消了

这篇经验为主的祥助分享一下如何恢复V68旧版UI的界面样式

Chrome V69浏览器新建标签页,输入:chrome://flags/#top-chrome-m

回车后运行

Ctrl+F搜索内容:UI Layout for the browser’s top chrome

将模式修改至Normal,重新启动浏览器

再次新建标签页,输入:chrome://flags/#simplify-https-indicator
回车后运行

Ctrl+F搜索内容:Simplify HTTPS indicator UI
将模式修改为Enabled (show Secure chip for non-EV HTTPS pages)

重启浏览器

重启Chrome浏览器,就会发现标签页UI,https绿色状态都恢复旧版样式了
还算方便。但是我个人感觉,新版 UI还是不错的,可能是因为这个旧UI看了快10年了吧,换换口味也是好的
server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none

server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none

在服务器执行composer update的时候报了这个错:

server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none
主要还是签名的SSL证书可能有点问题,所以这时候有个偷懒的办法:
git config --global http.sslverify false
或者你也可以这样:
export GIT_SSL_NO_VERIFY=1
然后就OK了
facebook webdriver headless

facebook webdriver headless

下载了webdriver后,每次调用的时候,都会弹出一个chrome的界面,而且处理完后还不会自动关闭(当然可能是因为我的姿势问题),所以看了一下facebook 的webdriver,嗯配合chromedriver,很多事情就都解决了!

上一段测试代码:

$options = DesiredCapabilities::chrome();

$chrome = new ChromeOptions();
$chrome->addArguments(['--headless']);
$options->setCapability(ChromeOptions::CAPABILITY,$chrome );


$driver = RemoteWebDriver::create('http://localhost:9515', $options);
$driver->get('http://www.baidu.com');
$result = $driver->findElement(WebDriverBy::id('wrapper'))
->getText();
echo $result;

还确实是比较方便。我要测试一下时间。然后和phantomjs做一下对比。我想应该不会太慢吧

How To Add Swap on Ubuntu 14.04

How To Add Swap on Ubuntu 14.04

当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap空间中,等到那些程序要运行时,再从Swap中恢复保存的数据到内存中。这样,系统总是在物理内存不够时,才进行Swap交换。这个是SWAP 交换分区的作用。 实际上,我们更关注的应该是SWAP分区的大小问题。 设置多大才是最优的。

一般来说可以按照如下规则设置swap大小:

  1. 4G以内的物理内存,SWAP 设置为内存的2倍。
  2. 4-8G的物理内存,SWAP 等于内存大小。
  3. 8-64G 的物理内存,SWAP 设置为8G。
  4. 64-256G物理内存,SWAP 设置为16G。

实际上,系统中交换分区的大小并不取决于物理内存的量,而是取决于系统中内存的负荷,所以在安装系统时要根据具体的业务来设置SWAP的值。

实际上在启动腾讯云的时候,swap是禁用的(没注意是系统原因还是其他)。所以就需要打开Swap,DO的网站上就有现成的教程:https://www.digitalocean.com/community/tutorials/how-to-add-swap-on-ubuntu-14-04,可以参考 一下

我这里不转贴。不过原文里有几个命令可能运行不了,比如使用:dd if=/dev/zero of=/swapfile bs=1G count=4,1G一个块可能不行,换成256的话,就OK了。原文中的:fallocate,debian可能无法执行。

然后一步步的执行到底就OK了。当然,要将分区信息加到fstab中,才会每次启动都会带上。

旧版debian增加PHP7.2源

旧版debian增加PHP7.2源

原来的dobdeb对于jessie等都不再支持7.2了,所以新的7.2只能从sury上进行获取

执行:
#!/bin/bash
# To add this repository please do:

if [ "$(whoami)" != "root" ]; then
SUDO=sudo
fi

${SUDO} apt-get -y install apt-transport-https lsb-release ca-certificates
${SUDO} wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
${SUDO} sh -c 'echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'
${SUDO} apt-get update
记不住没关系,打开网址:https://packages.sury.org/php/,就有Readme了。复制粘贴运行一下就行!
invalid command code ., despite escaping periods, using sed

invalid command code ., despite escaping periods, using sed

linux上面,如果替换文件中的内容,其实还是比较方便的。比如:sed -i 's/xxx/yyy/g' xxx.txt

然而同样的命令,如果放到mac下面,就会报:sed: 1: "xxx.txt": invalid command code o
到stackoverflow.com就会发现已经有人回复了:https://stackoverflow.com/questions/19456518/invalid-command-code-despite-escaping-periods-using-sed
If you are on a OS X, this probably has nothing to do with the sed command. On the OSX version of sed, the -i option expects an extension argument so your command is actually parsed as the extension argument and the file path is interpreted as the command code.

Try adding the -e argument explicitly and giving '' as argument to -i:

find ./ -type f -exec sed -i '' -e "s/192.168.20.1/new.domain.com/" {} \;
See this.
所以上面的代码就改为:sed -i '' -e 's/xxx/yyy/g' xxx.txt ,搞定
参考:
1、https://stackoverflow.com/questions/19456518/invalid-command-code-despite-escaping-periods-using-sed
2、https://stackoverflow.com/questions/7573368/in-place-edits-with-sed-on-os-x
转的关于公众号接收信息的返回

转的关于公众号接收信息的返回

以下是内容:

首次访问时post的数据
获取ticket访问时get的数据($encryptMsg)
{"signature":"bb539c3ecaddf06040912a836cdeb2060f649b1a","timestamp":"1491385757","nonce":"776481090","encrypt_type":"aes","msg_signature":"472008a1054eab89651ae9c484395e4e54abb01e","action":"get_ticket"}

消息接入时get的数据($param)
{"action":"news","amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;b":"/wx570bc396a51b8ff8","signature":"550a39d2ffa8dbe0b0f8f6cfadd8a9b8981b3752","timestamp":"1491385766","nonce":"947166308","openid":"ozy4qt5QUADNXORxCVipKMV9dss0","encrypt_type":"aes","msg_signature":"d67c434cb7e54a0b51b2cdceba38650ea62a35a2"}

消息接入时get的数据($param)
{"action":"news","amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;b":"/wx570bc396a51b8ff8","signature":"595e8376b3587a8d68c68132417931b6fd0b8f9d","timestamp":"1491385775","nonce":"898963343","openid":"ozy4qt5QUADNXORxCVipKMV9dss0","encrypt_type":"aes","msg_signature":"1d6291da6617638c95d4be800d8394547a8d4137"}

消息接入时get的数据($param)
{"action":"news","amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;b":"/wx570bc396a51b8ff8","signature":"72e0b354fd7b6643335d1035471617441e57e945","timestamp":"1491385783","nonce":"1115275135","openid":"ozy4qt5QUADNXORxCVipKMV9dss0","encrypt_type":"aes","msg_signature":"3d16bee6896984fbf2ba333ef2be389739ddd30a"}

获取ticket访问时get的数据($encryptMsg)
{"signature":"fa4b87ee9574913ac68bd1044c35a48a1bb41a40","timestamp":"1491385789","nonce":"429149520","encrypt_type":"aes","msg_signature" :"9626625a665c03f128462ace8a5dff0cf2c5d0f8","action":"get_ticket"}

通过$encryptMsg与$param获取 component_verify_ticket
ticket@@@bYIJwVAxqf75BhEVRNhaaf1KTit5zxiqbZ5KR7xJumzniEEy6DTIWHeic1-L_z7LuhPwwSNLm_V_8hkyRYX-PQ

通过$encryptMsg与$param验证微信接入测试请求($data)
{"ToUserName":"gh_3c884a361561","FromUserName":"ozy4qt5QUADNXORxCVipKMV9dss0","CreateTime":"1491385765","MsgType":"text","Content":"QUERY_AUTH_CODE:queryauthcode@@@mFZ_yC83hgDZhf-s6pL6UGloyMYzH8MTCEQddsa8U5ceEze4zflgNNk4sU1kkGUkZn37-Su4t9C5gaR09gjkKQ","MsgId":"6405453086801493549"} {"ToUserName":"gh_3c884a361561","FromUserName":"ozy4qt5QUADNXORxCVipKMV9dss0","CreateTime":"1491385774","MsgType":"text","Content":"TESTCOMPONENT_MSG_TYPE_TEXT","MsgId":"6405453125456199215"} {"ToUserName":"gh_3c884a361561","FromUserName":"ozy4qt5QUADNXORxCVipKMV9dss0","CreateTime":"1491385783","MsgType":"event","Event":"LOCATION","Latitude":"111.000000","Longitude":"222.000000","Precision":"333.000000"}

微信接入测试回复 $data
{"ToUserName":"gh_3c884a361561","FromUserName":"ozy4qt5QUADNXORxCVipKMV9dss0","CreateTime":"1491385765","MsgType":"text","Content":"QUERY_AUTH_CODE:queryauthcode@@@mFZ_yC83hgDZhf-s6pL6UGloyMYzH8MTCEQddsa8U5ceEze4zflgNNk4sU1kkGUkZn37-Su4t9C5gaR09gjkKQ","MsgId":"6405453086801493549"} {"ToUserName":"gh_3c884a361561","FromUserName":"ozy4qt5QUADNXORxCVipKMV9dss0","CreateTime":"1491385774","MsgType":"text","Content":"TESTCOMPONENT_MSG_TYPE_TEXT","MsgId":"6405453125456199215"} {"ToUserName":"gh_3c884a361561","FromUserName":"ozy4qt5QUADNXORxCVipKMV9dss0","CreateTime":"1491385783","MsgType":"event","Event":"LOCATION","Latitude":"111.000000","Longitude":"222.000000","Precision":"333.000000"}

1、通过$data获取$query_auth_code
queryauthcode@@@mFZ_yC83hgDZhf-s6pL6UGloyMYzH8MTCEQddsa8U5ceEze4zflgNNk4sU1kkGUkZn37-Su4t9C5gaR09gjkKQ

2、通过$data获取$access_token
dZiS58MkUvLOkSBvX2SDu6-P6hGqN5GJj8T8jOlhszo80KuwMgytZb2GIoNlsUMeSpM_WLBMc65KDG80h2-zjh-8PWdE2br8Pchdk6MFJjg13vCjhDVyPyZ3_1XbTmg2GSWgADDLXB

微信接入测试回应 回应数据
1、{"ToUserName":"ozy4qt5QUADNXORxCVipKMV9dss0","FromUserName":"gh_3c884a361561","CreateTime":1491385775,"MsgType":"text","Content":"TESTCOMPONENT_MSG_TYPE_TEXT_callback","FuncFlag":0}
2、{"ToUserName":"ozy4qt5QUADNXORxCVipKMV9dss0","FromUserName":"gh_3c884a361561","CreateTime":1491385783,"MsgType":"text","Content":"LOCATIONfrom_callback","FuncFlag":0}

解密回应数据
1、\n\n\n1491385775\n\n
2、\n\n\n1491385783\n\n

微信接入测试请求
{"ToUserName":"gh_3c884a361561","FromUserName":"ozy4qt5QUADNXORxCVipKMV9dss0","CreateTime":"1491385765","MsgType":"text","Content":"QUERY_AUTH_CODE:queryauthcode@@@mFZ_yC83hgDZhf-s6pL6UGloyMYzH8MTCEQddsa8U5ceEze4zflgNNk4sU1kkGUkZn37-Su4t9C5gaR09gjkKQ","MsgId":"6405453086801493549"}
{"ToUserName":"gh_3c884a361561","FromUserName":"ozy4qt5QUADNXORxCVipKMV9dss0","CreateTime":"1491385774","MsgType":"text","Content":"TESTCOMPONENT_MSG_TYPE_TEXT","MsgId":"6405453125456199215"}
{"ToUserName":"gh_3c884a361561","FromUserName":"ozy4qt5QUADNXORxCVipKMV9dss0","CreateTime":"1491385783","MsgType":"event","Event":"LOCATION","Latitude":"111.000000","Longitude":"222.000000","Precision":"333.000000"}

获取微信开放平台授权信息
{"authorization_info":{"authorizer_appid":"wx570bc396a51b8ff8","authorizer_access_token":"dZiS58MkUvLOkSBvX2SDu6-P6hGqN5GJj8T8jOlhszo80KuwMgytZb2GIoNlsUMeSpM_WLBMc65KDG80h2-zjh-8PWdE2br8Pchdk6MFJjg13vCjhDVyPyZ3_1XbTmg2GSWgADDLXB","expires_in":7200,"authorizer_refresh_token":"refreshtoken@@@5lb5Jq2giPuJ0gjjyD9J_3y7VhyVgVt6kIuYlGkATFg","func_info":[{"funcscope_category":{"id":1}},{"funcscope_category":{"id":15}},{"funcscope_category":{"id":4}},{"funcscope_category":{"id":7}},{"funcscope_category":{"id":2}},{"funcscope_category":{"id":3}},{"funcscope_category":{"id":11}},{"funcscope_category":{"id":6}},{"funcscope_category":{"id":5}},{"funcscope_category":{"id":8}},{"funcscope_category":{"id":13}},{"funcscope_category":{"id":9}},{"funcscope_category":{"id":10}},{"funcscope_category":{"id":12}}]}}

ComponentAccessToken
ticket:PrJUyVJYEAlQ75bCbBLk4DkdHefqRM4r_ZV7ivJuahmG1Hap41QHJQ9MVsY4JOdlp21dR7gmUxuUa4eeTM8D1az4fhY8yYjBBGkC7KLyvSEGclB0vgza1xzk65m0GNAQVGMfABAFQW
COPY自:http://blog.lwp8800.com/news/detail.html?id=22
cannot create/resize docker.qcow2

cannot create/resize docker.qcow2

mac下面安装过docker,但后来删除过。然后系统又重做过。结果再次安装docker的时候报错,如题(详细 路径不贴了)

找了一下资料,各种各样的都有,最后还是选择了:
sudo rm -r /var/tmp/com.docker.vmnetd.socket
sudo rm -r ~/Library/Application\ Scripts/com.docker.helper/
sudo rm -r ~/Library/Containers/com.docker.*
sudo rm -r ~/Library/Preferences/com.docker.docker.plist
sudo rm -r ~/Library/Group\ Containers/group.com.docker/
然后再次运行Docker,一切都解决了
语义化版本 2.0.0

语义化版本 2.0.0

关于版本

复制于:https://semver.org/lang/zh-CN/

还可以看一下这个:https://getcomposer.org/doc/articles/versions.md
下述内容就是我偷懒复制来的了,本来也基于CC3.0

摘要

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

  1. 主版本号:当你做了不兼容的 API 修改,
  2. 次版本号:当你做了向下兼容的功能性新增,
  3. 修订号:当你做了向下兼容的问题修正。

先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

简介

在软件管理的领域里存在着被称作“依赖地狱”的死亡之谷,系统规模越大,加入的套件越多,你就越有可能在未来的某一天发现自己已深陷绝望之中。

在依赖高的系统中发布新版本套件可能很快会成为恶梦。如果依赖关系过高,可能面临版本控制被锁死的风险(必须对每一个相依套件改版才能完成某次升级)。而如果依赖关系过于松散,又将无法避免版本的混乱(假设兼容于未来的多个版本已超出了合理数量)。当你专案的进展因为版本相依被锁死或版本混乱变得不够简便和可靠,就意味着你正处于依赖地狱之中。

作为这个问题的解决方案之一,我提议用一组简单的规则及条件来约束版本号的配置和增长。这些规则是根据(但不局限于)已经被各种封闭、开放源码软件所广泛使用的惯例所设计。为了让这套理论运作,你必须先有定义好的公共 API 。这可以透过文件定义或代码强制要求来实现。无论如何,这套 API 的清楚明了是十分重要的。一旦你定义了公共 API,你就可以透过修改相应的版本号来向大家说明你的修改。考虑使用这样的版本号格式:XYZ (主版本号.次版本号.修订号)修复问题但不影响API 时,递增修订号;API 保持向下兼容的新增及修改时,递增次版本号;进行不向下兼容的修改时,递增主版本号。

我称这套系统为“语义化的版本控制”,在这套约定下,版本号及其更新方式包含了相邻版本间的底层代码和修改内容的信息。

语义化版本控制规范(SemVer)

以下关键词 MUST、MUST NOT、REQUIRED、SHALL、SHALL NOT、SHOULD、SHOULD NOT、 RECOMMENDED、MAY、OPTIONAL 依照 RFC 2119 的叙述解读。(译注:为了保持语句顺畅, 以下文件遇到的关键词将依照整句语义进行翻译,在此先不进行个别翻译。)

  1. 使用语义化版本控制的软件“必须 MUST ”定义公共 API。该 API 可以在代码中被定义或出现于严谨的文件内。无论何种形式都应该力求精确且完整。

  2. 标准的版本号“必须 MUST ”采用 XYZ 的格式,其中 X、Y 和 Z 为非负的整数,且“禁止 MUST NOT”在数字前方补零。X 是主版本号、Y 是次版本号、而 Z 为修订号。每个元素“必须 MUST ”以数值来递增。例如:1.9.1 -> 1.10.0 -> 1.11.0。

  3. 标记版本号的软件发行后,“禁止 MUST NOT ”改变该版本软件的内容。任何修改都“必须 MUST ”以新版本发行。

  4. 主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 API 不应该被视为稳定版。

  5. 1.0.0 的版本号用于界定公共 API 的形成。这一版本之后所有的版本号更新都基于公共 API 及其修改内容。

  6. 修订号 Z(x.y.Z | x > 0)“必须 MUST ”在只做了向下兼容的修正时才递增。这里的修正指的是针对不正确结果而进行的内部修改。

  7. 次版本号 Y(x.Y.z | x > 0)“必须 MUST ”在有向下兼容的新功能出现时递增。在任何公共 API 的功能被标记为弃用时也“必须 MUST ”递增。也“可以 MAY ”在内部程序有大量新功能或改进被加入时递增,其中“可以 MAY ”包括修订级别的改变。每当次版本号递增时,修订号“必须 MUST ”归零。

  8. 主版本号 X(X.y.z | X > 0)“必须 MUST ”在有任何不兼容的修改被加入公共 API 时递增。其中“可以 MAY ”包括次版本号及修订级别的改变。每当主版本号递增时,次版本号和修订号“必须 MUST ”归零。

  9. 先行版本号“可以 MAY ”被标注在修订版之后,先加上一个连接号再加上一连串以句点分隔的标识符号来修饰。标识符号“必须 MUST ”由 ASCII 码的英数字和连接号 [0-9A-Za-z-] 组成,且“禁止 MUST NOT ”留白。数字型的标识符号“禁止 MUST NOT ”在前方补零。先行版的优先级低于相关联的标准版本。被标上先行版本号则表示这个版本并非稳定而且可能无法达到兼容的需求。范例:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。

  10. 版本编译信息“可以 MAY ”被标注在修订版或先行版本号之后,先加上一个加号再加上一连串以句点分隔的标识符号来修饰。标识符号“必须 MUST ”由 ASCII 的英数字和连接号 [0-9A-Za-z-] 组成,且“禁止 MUST NOT ”留白。当判断版本的优先层级时,版本编译信息“可 SHOULD ”被忽略。因此当两个版本只有在版本编译信息有差别时,属于相同的优先层级。范例:1.0.0-alpha+001、1.0.0+20130313144700、1.0.0-beta+exp.sha.5114f85。

  11. 版本的优先层级指的是不同版本在排序时如何比较。判断优先层级时,“必须 MUST ”把版本依序拆分为主版本号、次版本号、修订号及先行版本号后进行比较(版本编译信息不在这份比较的列表中)。由左到右依序比较每个标识符号,第一个差异值用来决定优先层级:主版本号、次版本号及修订号以数值比较,例如:1.0.0 < 2.0.0 < 2.1.0 < 2.1.1。当主版本号、次版本号及修订号都相同时,改以优先层级比较低的先行版本号决定。例如:1.0.0-alpha < 1.0.0。有相同主版本号、次版本号及修订号的两个先行版本号,其优先层级“必须 MUST ”透过由左到右的每个被句点分隔的标识符号来比较,直到找到一个差异值后决定:只有数字的标识符号以数值高低比较,有字母或连接号时则逐字以 ASCII 的排序来比较。数字的标识符号比非数字的标识符号优先层级低。若开头的标识符号都相同时,栏位比较多的先行版本号优先层级比较高。范例:1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0- rc.1 < 1.0.0。

为什么要使用语义化的版本控制?

这并不是一个新的或者革命性的想法。实际上,你可能已经在做一些近似的事情了。问题在于只是“近似”还不够。如果没有某个正式的规范可循,版本号对于依赖的管理并无实质意义。将上述的想法命名并给予清楚的定义,让你对软件使用者传达意向变得容易。一旦这些意向变得清楚,弹性(但又不会太弹性)的依赖规范就能达成。

举个简单的例子就可以展示语义化的版本控制如何让依赖地狱成为过去。假设有个名为“救火车”的函式库,它需要另一个名为“梯子”并已经有使用语义化版本控制的套件。当救火车创建时,梯子的版本号为 3.1.0。因为救火车使用了一些版本 3.1.0 所新增的功能, 你可以放心地指定相依于梯子的版本号大等于 3.1.0 但小于 4.0.0。这样,当梯子版本 3.1.1 和 3.2.0 发布时,你可以将直接它们纳入你的套件管理系统,因为它们能与原有相依的软件兼容。

作为一位负责任的开发者,你理当确保每次套件升级的运作与版本号的表述一致。现实世界是复杂的,我们除了提高警觉外能做的不多。你所能做的就是让语义化的版本控制为你提供一个健全的方式来发行以及升级套件,而无需推出新的相依套件,节省你的时间及烦恼。

如果你对此认同,希望立即开始使用语义化版本控制,你只需声明你的函式库正在使用它并遵循这些规则就可以了。请在你的 README 文件中保留此页连结,让别人也知道这些规则并从中受益。

FAQ

在 0.y.z 初始开发阶段,我该如何进行版本控制?

最简单的做法是以 0.1.0 作为你的初始化开发版本,并在后续的每次发行时递增次版本号。

如何判断发布 1.0.0 版本的时机?

当你的软件被用于正式环境,它应该已经达到了 1.0.0 版。如果你已经有个稳定的 API 被使用者依赖,也会是 1.0.0 版。如果你很担心向下兼容的问题,也应该算是 1.0.0 版了。

这不会阻碍快速开发和迭代吗?

主版本号为零的时候就是为了做快速开发。如果你每天都在改变 API,那么你应该仍在主版本号为零的阶段(0.y.z),或是正在下个主版本的独立开发分支中。

对于公共 API,若即使是最小但不向下兼容的改变都需要产生新的主版本号,岂不是很快就达到 42.0.0 版?

这是开发的责任感和前瞻性的问题。不兼容的改变不应该轻易被加入到有许多依赖代码的软件中。升级所付出的代价可能是巨大的。要递增主版本号来发行不兼容的改版,意味着你必须为这些改变所带来的影响深思熟虑,并且评估所涉及的成本及效益比。

为整个公共 API 写文件太费事了!

为供他人使用的软件编写适当的文件,是你作为一名专业开发者应尽的职责。保持专案高效一个非常重要的部份是掌控软件的复杂度,如果没有人知道如何使用你的软件或不知道哪些函数的调用是可靠的,要掌控复杂度会是困难的。长远来看,使用语义化版本控制以及对于公共 API 有良好规范的坚持,可以让每个人及每件事都运行顺畅。

万一不小心把一个不兼容的改版当成了次版本号发行了该怎么办?

一旦发现自己破坏了语义化版本控制的规范,就要修正这个问题,并发行一个新的次版本号来更正这个问题并且恢复向下兼容。即使是这种情况,也不能去修改已发行的版本。可以的话,将有问题的版本号记录到文件中,告诉使用者问题所在,让他们能够意识到这是有问题的版本。

如果我更新了自己的依赖但没有改变公共 API 该怎么办?

由于没有影响到公共 API,这可以被认定是兼容的。若某个软件和你的套件有共同依赖,则它会有自己的依赖规范,作者也会告知可能的冲突。要判断改版是属于修订等级或是次版等级,是依据你更新的依赖关系是为了修复问题或是加入新功能。对于后者,我经常会预期伴随着更多的代码,这显然会是一个次版本号级别的递增。

如果我变更了公共 API 但无意中未遵循版本号的改动怎么办呢?(意即在修订等级的发布中,误将重大且不兼容的改变加到代码之中)

自行做最佳的判断。如果你有庞大的使用者群在依照公共 API 的意图而变更行为后会大受影响,那么最好做一次主版本的发布,即使严格来说这个修复仅是修订等级的发布。记住, 语义化的版本控制就是透过版本号的改变来传达意义。若这些改变对你的使用者是重要的,那就透过版本号来向他们说明。

我该如何处理即将弃用的功能?

弃用现存的功能是软件开发中的家常便饭,也通常是向前发展所必须的。当你弃用部份公共 API 时,你应该做两件事:(1)更新你的文件让使用者知道这个改变,(2)在适当的时机将弃用的功能透过新的次版本号发布。在新的主版本完全移除弃用功能前,至少要有一个次版本包含这个弃用信息,这样使用者才能平顺地转移到新版 API。

语义化版本对于版本的字串长度是否有限制呢?

没有,请自行做适当的判断。举例来说,长到 255 个字元的版本已过度夸张。再者,特定的系统对于字串长度可能会有他们自己的限制。

关于

语义化版本控制的规范是由 Gravatars 创办者兼 GitHub 共同创办者 Tom Preston-Werner 所建立。

如果您有任何建议,请到 GitHub 上提出您的问题。

授权

创用 CC 姓名标示 3.0 Unported 授权条款 http://creativecommons.org/licenses/by/3.0/