0%

引言

Docker已经妥妥的成为了容器的业界标准,虽然不时的还有一些什么rkt, lxd将会是接下来的潮流之类的预言,但是至今也还未见能挑起大梁的后起之秀出现,我等俗人还是先顺应当前的潮流吧。而build镜像则是如何把docker使用好的占比非常大的一块内容,今天的话题也是始于群里面一个如何能更快的build docker image的问题。

Best Practice

其实我们并没有讨论什么坏实践,更多的是在围绕我们觉得好的实践是什么和疑问来展开的,这次我就不按照讨论的节奏来总结了,直接上汇总的结果了。总体来讲,build镜像的目标就是尽可能的让镜像小并能兼顾高效和安全

官方的best practice

Docker官方有一个best practice的文档,这个里面写了一些非常好的通用的实践,估摸着大部分人应该都看过了,我在这里摘抄一下核心的内容,鉴于文档里面都有详细的说明和例子,我简单的说一下我的理解。

  • 构建可随时终止的容器(Create ephemeral containers),这一点咋看起来似乎和build image没有什么关系,但是其实是一个大的宗旨,就是说在build镜像的时候,你要以镜像将来创建出来的容器是可随时终止和替换的为目标,就和12因子应用里面对于进程的定义类似,容器只是业务运行的一个临时栖所,你不应该把有状态或者需要传承的东西放在镜像里面。
  • 使用.dockerignore排除不相干的文件(Exclude with .dockerignore),docker是一个C/S架构的应用,build这个过程是在S端进行的,所以build的时候C端会把当前目录的文件当作build context传给S端,如果传了很多在build image的过程中用不到的文件,就会有无谓的性能和时间上的损失,所以合理的使用.dockerignore文件排除不相干的文件。
  • 使用多阶段构建(Use multi-stage builds),这个不多讲,很多的程序是或者业务最终需要的其实是一些编译的产出物,但是编译的过程会需要很多在运行时不会用到的依赖,多阶段构建就是为了解决这个问题的。
  • 只安装必须的文件包(Don’t install unnecessary packages),这个不解释。
  • 解耦业务(Decouple applications),这个说白了就是docker设计的核心理念,一个容器应该只有一个进程,现实中可的业务需求真的很难完全做到这个,但是尽量吧。
  • 最小化层数(Minimize the number of layers),这一点,我个人其实持保留态度,因为他和接下来的使用Cache这个在某种程度上是有冲突的,具体根据情况做取舍吧。
  • 使用Cache(Leverage build cache),这个作为如何快速的构建镜像的金律,人人都知道,不多说。
  • 使用多行参数并排序(Sort multi-line arguments),程序员的基础素养,写清晰易读的代码。

我们的best practice

除了官方的之外,我们在平时使用中也有一些适合自己的实践,列在下面供大家参考。

  • 尽可能使用Alpine版本的基础镜像,这个可以理解,小嘛,但是里面的工具和服务都是定制过的,通用性相对差一些,和其他的流行的版本还是有一些差别的,可能需要你对使用到的工具的参数,服务的配置做一些处理。

  • 锁死镜像的版本,非精细化的版本tag其实是会随时更新的,比如ruby:2,你也不知道哪天它会从2.1升到2.2了,很可能这个变化就导致了不期望的或者和之前不一致的结果,所以有一个办法就是带上镜像的sha256签名,比如ruby:2@sha256:15083783ce61a90002eb175a0de2c198afb74b49bd87d52d329e2f4a57b21562,这样你永远拿到的都是那个唯一的版本,如果你需要知道某一个tag的最新的sha256,推荐一个小工具 dfresh。不过这个实践你要灵活使用,比如说我的项目需求是即使这个代码库已经没有新的feature开发了,但是还是需要定期的更新语言和依赖到最新的版本,以确保我的代码不是僵尸代码,而且是有最新的安全补丁的,这种情况下,你可能也需要类似于dfresh这样的工具更新基本镜像的实际版本,或者说如果你能有办法知道某一个workable的基础镜像的sha256也是可以的。

  • 干净的依赖安装,这个可能是很多人都没有关注到一个点,比如说我们使用的是nodejs,大部分人的Dockerfile中相关的部分可能会是下面这样的。

    1
    2
    3
    4
    FROM node:10

    COPY . .
    RUN yarn install

    这个看起来好像没有什么问题,但是如果你的dockerignore文件如果没有处理好,有可能就会把本机的node_modules在安装之前就一起copy进去了,这样其实你就污染了build环境,同样的代码,别人打出来的包和你的包有可能是不一致的,即使看起来我们也使用了lock文件。推荐的写法是:

    1
    2
    3
    4
    5
    FROM node:10

    COPY yarn.lock .
    RUN yarn install
    COPY . .

    甚至在有必要的时候使用Volume这个黑魔法。

  • 必要的时候使用Volume,Volume是一个你掌握好了就像金手指开挂,掌握不好就给自己挖了一个大坑一样的东西,所以我个人建议你在使用之前去了解一下Volmue的原理,可以去看一下队长之前的容器化的课程中相关的部分,从大概1小时10分时开始。简单的来讲,在dockerfile中,你可以使用它锁死某个目录的状态,比如说下面的这个dockerfile,你猜你用它生成的镜像起一个容器的时候,/app目录下面都有什么文件,内容是什么?

    1
    2
    3
    4
    5
    6
    FROM alpine:3

    WORKDIR /app
    RUN echo 1 > file
    VOLUME /app
    RUN echo 2 > file && touch new

    如果不知道的话自己试一试吧,然后在需要的时候合理的利用这个黑魔法。

  • 不要使用root帐号,这个其实是安全实践中的最小化权限原则,在容器中使用root用户是有一定程度的安全风险,尤其是如果使用了privileged模式的情况下,这篇文章给了一个栗子。

  • 不要把ssh key保留在镜像中,这个也是一个安全方面的风险,私钥打包在镜像中和把密码明文放在代码库里面基本上是一个性质的。这个需求多是源于要连接一个私有的仓库之类做一些事情,一种解决方法是把本地的key文件或者目录绑到容器的对应目录,这种方式会受限于key文件的命名规则。另外一种方式是把key文件的内容当作arg传入,这种方式需要你在生成临时key文件并在同一层做完操作然后删除,否则这个文件就保留在镜像中了。还有一种办法是使用docker的buildkit来做,但是这个目前还处于测试阶段,另外注意在pipeline中使用时开启静默模式,否则你的pipeline的log会很难看。

  • 慎用profile/rc文件,这类文件是在使用tty/pty/console,有用户session的时候才会加载的文件,所以这里面配置的东西在build的时候是不会有的,这种情况下,你可能会很迷惑,明明我用这个镜像起一个容器能看到这些配置生效了,为什么在build的时候就不生效呢?

  • 慎用latest tag,latest是默认的一个tag,它代表的我最近一次打出来的镜像,并不是代表这个可用的最新的版本,因为有可能最新的这次build的image是有问题的,而且很大程度上说,也很难定位当前的latest对应的等价的tag是什么。如果造化弄人,那些小概率事件让你碰到,比如说有可能有一些粗心大意的小伙伴会每次都把latest镜像push到registry,而这个latest是有问题的,如果你的业务这个时候重新拉了latest tag的镜像,可能画面就会很美好了。

  • 使用WORKDIR而非cd命令,cd用多了会产生混乱,尤其是使用相对目录的情况下,调整顺序绝大部分情况下会产生错误。

其他

  • 如何更快的build镜像,这个是最初引发这个讨论的让人头疼的主题,但是追根究底来讲其实是一个网速问题,因为我们在安装和编译过程中是会下载大量的依赖的。所以,目前用的比较多的方法有二。使用cache或私有仓库是一个方法,不论是docker的cache还是agent的cache,亦或是一个proxy的cache也可以,实在不行创建自己私有的包管理仓库吧,这也是很多企业最后的选择,省时间也省流量。另外一个办法就是做一个base image,但是这个取决于所需依赖和安装包的更新频率,很多的时候,一些团队采用的折衷策略就是把这个base image做一个nightly的更新,但是不管怎么样,都是有额外的管理成本在其中的,自己取舍吧。
  • 一些操作到底是放在dockerfile中还是entrypoint的脚本中? 这个只能说是个人偏好了,没有所谓的对错,震宇举了一个很好的例子,就如同jenkinsfile一样,你是把命令直接写在step里面,还是写在一个脚本里面然后调用它。我个人坚持步骤和逻辑分离的原则,在dockerfile中我更倾向于尽量少的做逻辑处理和配置相关的操作,这些内容放在初始化的脚本里面更符合我的习惯,除了逻辑部分集中之外,另外一个好处是在测试改动的时候不需要重新build镜像,把这个文件挂载进容器就可以了,anyway,使用你自己舒服的方式来做事。

参考资料

写在前面

我也不知道为什么发cal的时候把话题起成了“再说devops”(至于为什么写成devops,请参考我的这篇blog《关于devops的拼写》),因为我们之前其实也没有说过这个。但是可能觉得我们其实平时在不停的说devops,所以就内心里面这个是一个再次重提的话题。

什么是devops

Wiki上的定义如下,这个也是我们能看到的绝大部分资料上的定义。

1
DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

不过一千个人心中有一千个哈姆雷特,虽然我们都认同了这个定义,但是对于组成这个定义的那些词语的解释却是各自一表。

今天的讨论中刚开始也是这样,有基于概念本身阐述的。

1
devops是一种打破dev和ops之间边界,把两个角色的工作磨合到一起,从计划制定开始,到需求分析,开发,测试,部署上线。使用一些自动化工具和项目管理实践,提升项目的交付质量和速度。

也有结合自己经历过的devops组织结构模式的变化,提出来自己对于devops深层的本质的理解。

1
2
1. 建立统一标准。
2. 屏蔽技术壁垒。

而在这个讨论过程中,就有一些更具体的问题,我们在devops实践过程中碰到的一些做法对不对?比如说:为什么我们能看到devops这个角色或者职位,到底devops是一个工种,他要去负责dev+ops的活,还是一种站在对方的角度思考问题的文化?如果我们不了解对方的专业领域的知识,我们又怎么去理解对方,打破壁垒?这个讨论中小伙伴们都拿出了自己的案例来说明自己的观点。在我看来,如同人类社会发展一样,从原始社会,奴隶社会到封建社会,再到我们今天的资本主义社会和社会主义社会,下一步是共产主义社会。目前我们能看到的这些不同的做法和组织形态,都可以算的上是devops,只不过是成熟度不同所在不同阶段而已。

下面这张图是Jez Humble提出来的devops的5大基石/支柱,也算是在圈内认同度比较高的一个解释框架。

img

请注意最下面部分那句 “In DevOps, it’s always people over process over tools“,所以如同我上面的所述,当我们还知识关注于工具和实施自动化的时候,相对的还处于比较初级的阶段。之后再进一步开始引入精益,建立成熟的流程,就高级了一些。最后上升到关注人文,能够共担责任的时候,就可以称得上完美的devops实践了。hmm~这么说来,倒是更像马斯洛的需求层次理论,我们之后讨论的内容应该更能佐证我的这个观点吧。

怎么才能更好的推动devops

这个是最现实也是最无力的问题,毕竟devops怎么听都是一个很美好的东西,但是为什么我们在推行devops的时候还是遇到了各种各样的阻力?

大家用自己的亲身经历总结出来的最有效的方法是:切身的痛点(利益)才是内在驱动力。不论是从上向下的被动模式还是从下往上的主动模式,一定是驱动的人从中看到了这个事情能给他带来所谓的“价值”,也就是我前面括号里面赤果果的翻译:利益。

至于什么是“价值”,讨论到最后终是变成了最正确的废话:对方真正看中的东西。不同的人和角色在不同的时段对于看重的东西是不一样的,了解他们在当前关注的能给他带来的“价值”的点,然后有对应的方案就比较好推动了。

而另外一个切入点,就是有足够的权威,当对方足够信任你或者你有足够的威信的时候,你说的就是对的,即使它是错的。如果你不幸搞错了,可能实施的人觉得是自己哪里没做对,然后失败了。现实就是这么冰冷,无情而残酷。

国内外devops实施的差别

这个是很有趣的一件事情。在国内做devops,最终十有八九就是以采用了一些devops工具或者开发了一个devops平台来宣告成功。而在国外,你要和行内人士说devops平台,他肯定不能理解,为什么有了一个平台就算是落地devops了?这个看起来更多的是我们主流的发展思路不同导致的,如同我国在国际上的优势领域多是效率驱动型的一样,我们在devops上也是以提升效率为主,很少讨论人文层面的比较虚无的东西。而国外很多的时候是反着来,所以也就如小伙伴笑称的:国内关注结果,国外享受过程。

相关资料

原文发表于: https://blog.94xychen.net/posts/sign-your-git-commit/

最近在Github 上发现一个有意思的功能: 如果一个提交被作者签名了, 并且签名可被验证的话, 提交上会显示一个绿色的Verified的标志.如下图所示:


这篇文章, 我们就来聊聊为什么要签名你的提交以及如何去做.

什么是签名?

在真正的进入正文之前, 我想先简单的普及一下签名这个密码学的概念, 有些同学听到密码学这个词就觉得好复杂而心生畏惧, 其实大可不必, 如果只是应用的话, 你大可不需要了解每一个算法的细节, 你只需要知道他们的特点以及应用场景就完全足够了, 这里我就会以一个非常简化的模型来解释数字签名是如何实现的.

签名, 在字面意思上就是给某个东西署上名字, 而表面这个内容我们是同意的, 或者说, 这就是我写的. 在现实生活中, 我们常用的签名方式就是1)签字, 2)摁手印. 当我们通过这种方式来给一个东西签名以后, 别人就可以通过字迹以及指纹比对的方式来确认这个东西到底是不是我签署的.

而在现实生活中, 对签名的校验(字迹以及指纹比对)通常需要权威机构来做, 而在数字世界, 我们通常没有一个权威机构来帮我们校验某个东西是否真的是来自于某个人的, 于是, 采用密码学技术的签名就派上用场了. 在数字世界里, 数字签名的作用和现实生活中还是一模一样的, 只是我们实现的技术有不同而已.

下面我们就来看一下, 密码学工具如何做到的吧.
首先, 我们先要从密码学工具箱中挑出几件工具出来, 在我接下来构想的简单模型中, 数字签名会用到的工具有两种: hash散列算法(数字世界的指纹), 非对称密码. 我们接下来看一下他们的特性, 具体的更多细节还请自行学习.

hash散列算法

hash 散列算法可以将一个任意长度的内容转化成一个固定长度的内容,(不同的算法实现有不同的长度, 我们假设是64个字节),我们称它为内容的指纹(finger print). 而且没有办法没有办法直接通过转换后的内容直接推算出原内容.

并且, hash算法都有雪崩效应, 也就是说, 即使是改动原内容的一个bit, 生成出来的指纹将会有很大的差别.
用它, 我们可以校验内容的完整性.

非对称密码算法

相对于对称密码算法, 非对称密码算法的密钥有一对而不是一个.
它的特性主要是: 用其中一个密钥加密的内容, 不能通过同一个密钥解密, 只能通过相对应的另一个密钥来解密, 反之亦然(当然, 不同的算法有不同的特性.).

于是, 我们可以将这一对密钥区分成公钥和私钥, 公钥直接放在互联网上, 而私钥自己保管好(这是关键, 私钥就代表你自己), 如果别人需要和你进行加密通信, 就可以直接用你的公钥加密, 而不需要像对称密码算法一样先和你交换密钥(当然, 这里又涉及到了公钥可行度的问题, 密码学通常通过证书来解决. 而且, 由于非对称密码算法的性能还是不如对称密码算法, 所以非对称密码在实际的加解密过程中通常扮演安全密钥交换通道的作用, 密码学有太多的东西可以讲, 以后有机会再说.).

简单的签名实现


我们先有请密码学中的李雷和韩梅梅: Alice 和 Bob. 假设Alice 想给Bob 发一条消息说: Hello Bob. Do you have time tonight?. 如果Bob 收到这条消息, 他怎么知道这条消息就是Alice 发的呢? 万一是Bob的情敌要骗他出来干仗呢? 于是, Alice 做了两件事:

  1. 用hash散列算法计算出了这条消息的指纹: c01228362b0b8f707c018fe24cca6ac179e2619d1fcfa47cdd19fa1235feb251
  2. 用自己的私钥给这条指纹加密: ZmI3NmY3M2M4ZTQ3YmRlMGE3ZDI1ZGM2MjViOGUzNDg=

然后将加密后的指纹随同那条消息一同发送给了Bob.

那么Bob如何就能知道这条消息就是Alice 发出来的呢? Bob 只需要做这几件事:

  1. 用Alice 的公钥解密指纹密码(非对称密码算法的特性), 得到: c01228362b0b8f707c018fe24cca6ac179e2619d1fcfa47cdd19fa1235feb251
  2. 用于Alice 相同的hash 算法对Alice 发送过来的内容进行计算, 得到hash指: c01228362b0b8f707c018fe24cca6ac179e2619d1fcfa47cdd19fa1235feb251

如果两者一样, 证明这条消息是Alice发出来的, 如果不是, 要么内容被篡改, 要么不是Alice 发出来的.

这样, 一个简易的签名和验证就完成了, 本质上, 签名的验证就是验证发布者是否持有某个私钥, 这个私钥就代表你自己, 所以私钥的保管至关重要!

为什么要给Commit 签名?

在了解了签名的作用以后, 我相信给git Commit签名的原因就已经很明了了: 别人可以验证这个commit 真的是你提交的, 而不是:

  • 某个盗用了你的Access token的人去提交的.
  • 某个盗用了你Github/Github enterprise账号的人去提交的.
  • 某个同事改了你的代码, 并且 --amend 了你的提交, 并且force push了你的分支(强烈不建议这么做, 因为可能有生命危险)

总而言之, 给Commit 签名, 可以让别人验证所有的这个工作, 来自于一个可信可验证的来源.

如何给Commit 签名?

有两种方式可以给你的Commit 签名:

  1. 在Github 上面编辑的代码, Github会自动的签名(由此可推测, Github 给每个用户都生成了一个公私钥对, 而且还没有给我们暴露出来)
  2. 在自己的机器上用Git 在提交时进行签名, 并且让Github 可验证.

第一种方式不需要解释, 我们看一下第二种方式如何操作:

工具准备

首先我们需要有一个自己的非对称密钥对. 我们这里使用gpg 这个工具来生成和管理我们的密钥对.(gpg 是GNU Privacy Guard的缩写, 是常用的加密/解密/签名/校验等密码学操作的命令行工具, 更多详情以后介绍.)

安装GPG:

1
brew install gnupg

安装完以后, 我们需要写一个配置到我们的shell 配置中, 不然的话, gpg 不能正常的弹出密码询问框而报错:

1
2
echo 'export GPG_TTY=$(tty)' >> ~/.your_shell_config # For bash/zsh user. config are usually .bashrc/.zshrc
echo 'set -x GPG_TTY (tty)' >> ~/.config/fish//config.fish # For fish user

生成密钥对

安装好gpg以后, 用gpg 生成密钥对.

1
gpg --full-generate-key

它会弹出一系列问题让你输入, 如实写入就好啦.

生成完了以后, 你就可以用如下命令查看你的密钥了:

1
gpg --list-secret-keys --keyid-format LONG

输出类似于:

1
2
3
4
5
6
7
➜  ~ gpg --list-secret-keys --keyid-format LONG
/Users/xiyuchen/.gnupg/pubring.kbx
----------------------------------
sec rsa4096/507BB1CAC6286AF9 2020-02-16 [SC]
1888037BDEAA06CFDE3117FE507BB1CAC6286AF9
uid [ultimate] ninety-four-xychen <94xychen@gmail.com>
ssb rsa4096/F73836ED174C17A7 2020-02-16 [E]

至于如何更好的管理你的公私钥(备份和导入等)以后有机会在专题介绍.

签名你的提交.

git commit 命令给我们提供了利用gpg来签名commit的选项: -S[<keyid>], --gpg-sign[=<keyid>], 我们可以在写提交代码的时候加上-S<keyid> 来签名你的提交:

1
git commit -S507BB1CAC6286AF9 -m 'commit message'

到这一步, git签名就已经完成了, 但是, 每个提交都要写-S 加 keyid 还是有些麻烦的, 我们通过修改git 的配置(配置哪个层级自己选择, 我选择的是全局)来让git自动签名每一个提交:

1
2
$ git config --global user.signingkey 507BB1CAC6286AF9
$ git config --global commit.gpgsign true

让Github可验证

到这一步, 如果我们直接将提交推送到Github上, 提交上将会出现一个Unverified的标签, 这是因为, 虽然我们给提交签名了, 但是, Github还是不知道这个签名到底来自于谁. 于是, 我们就要告诉Github: 我们这个用户所对于的公钥是哪个.

这样, Github 用这个公钥校验完提交以后, 就可以说, 这个提交就是这个用户提交的了.

首先, 我们要导出我们的公钥:

1
gpg --export -a <keyid>

输出类似:

1
2
3
4
5
6
7
8
9
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQINBF5I7cwBEACxmyzZXpE8ldOqSV+RwPW3FyEj2pPY46kMbWHdbyGlm4Q2phUv
ZSYwxQTj8+MncpPQi3LUjH+VDpq9dwPzlKRVqiBrXZ4z1vjQV3YBk9cwloASLDCW
.....
X0KKdfAF6kIwIWe1jFXg76rNKly/PMj0E1kuUfTe7hHJWa/II8cloEhWSmSiuVum
do90
=syTd
-----END PGP PUBLIC KEY BLOCK-----

在Github 的这个页面将public key的内容上传上去:

恭喜你, 到这里, 你的提交就是Verified了.
你就可以像我一样帅气的拥有全Verified 提交记录了, ( •́ὤ•̀)你酸了没?.

Advanced Tips

缓存密码

熟话说, 安全与便利不可兼得, 在这种情况下也不例外,

当我们配置完上面的一切以后. 提交的时候, gpg 总会弹出一个密码询问框, 让你输入你创建密钥对时使用的密码, 去解开加密保存的私钥, 从而使用私钥去签名.

如果你用了像1password这样的密码管理工具的话, 你还可以很便利的用快捷键调出密码管理界面, 找到你的私钥解密密码, 复制粘贴.

但是, 如果你没有用这种方式, 每次提交还要去输入密码还是挺痛苦的, 不然的话很多人就会设置一个弱密码…这里还是强烈建议用密码管理工具生成强密码, 并且管理密码的.

在设置了强密码的前提下, 我们可以稍微的牺牲一些安全性, 通过配置gpg-agent的 default-cache-ttl, 让我们解密后的私钥在内存中存在的时间稍微长一些(默认10分钟), 比如, 一天:

1
2
3
4
# ~/.gnupg/gpg-agent.conf

default-cache-ttl-ssh 86400
max-cache-ttl-ssh 86400

话说回来, 安全于便利始终是个取舍.

在本地查看签名

1
git log --show-signature

输出类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
commit e412a1f98b4ddc34590d7f773c5c09c5326ca62c (HEAD -> master)
gpg: Signature made Sun Feb 23 16:54:34 2020 CST
gpg: using RSA key 1888037BDEAA06CFDE3117FE507BB1CAC6286AF9
gpg: Good signature from "ninety-four-xychen <94xychen@gmail.com>" [ultimate]
Author: ninety-four-xychen <94xychen@gmail.com>
Date: Sun Feb 23 15:56:31 2020 +0800

add new post: secuity-sign-your-git-commit

commit 87e8443f9880feb3d56d0bf62ed171b6c0e2d10f
gpg: Signature made Sun Feb 23 13:54:04 2020 CST
gpg: using RSA key 1888037BDEAA06CFDE3117FE507BB1CAC6286AF9
gpg: Good signature from "ninety-four-xychen <94xychen@gmail.com>" [ultimate]
Author: ninety-four-xychen <94xychen@gmail.com>
Date: Sun Feb 23 13:54:04 2020 +0800

Discards building step on pipeline.

commit f182369d56f5fc8506ea843b9e1ae25fd552a60b (origin/master)
gpg: Signature made Wed Feb 19 22:46:44 2020 CST
gpg: using RSA key 1888037BDEAA06CFDE3117FE507BB1CAC6286AF9
gpg: Good signature from "ninety-four-xychen <94xychen@gmail.com>" [ultimate]
Author: ninety-four-xychen <94xychen@gmail.com>
Date: Wed Feb 19 22:46:44 2020 +0800

Fixed an appearance issue on landing page.

引子

想和大家聊这个话题是因为我最近在帮客户梳理他自己的关于监控的一些想法,虽然我自己算是有一套体系,但是客户想站在更高的一个抽象的层面去推销他自己的理念,觉得我的那个太过于技术化,于是乎,我就想听听大家关于这一方面的见解,然后就有了今天的这个话题。

监控的原则

我自己关于监控的原则改变是从13年接触云服务开始,基于基础设施和平台变成了服务带来的关于架构设计思路变化导致的监控方式的连锁反应形成的。毕竟以前ops是要管物理设备的,但是当这些东西变成服务的时候,我们就不需要再去操心这些东西了,这也是我觉得devops理念在那之后才能有快速发展的基础。所以当我们的基础设施和平台具有自动伸缩的能力,虚拟化成为成熟的服务,以及随着容器化和编排的进一步成熟,牲口模式也成为架构设计所采用的通用原则,我们的监控的理念也进一步升级,更多的是去站在业务的角度关注可用性和性能,典型的就是Google SRE实践里面推行的,而不再关注具体资源的状态和性能。但是我们并不是说完全不要这些了,我们还是会尽可能的去监控和记录我们能需要的一切东西,毕竟这些在发生问题时定位根因是非常有用的。

但是中间有同学提到了,我们的一些项目还是需要去管理物理设备的,对于这种东西,我们的设计分层原则基本上就是经典的IPS(A)分层,I(nfrastructure)层面更多的关注设备的存活状态,性能,容量等,P(latform)层去看系统和基础服务本身的状态,是需要关注的最多的一个地方,除了系统和服务本身的存活状态,性能,容量等之外还需要关注他们高可用的状态,其本身对于资源的利用情况等等,以确保服务在稳定的基础上最大化的利用资源,并能为可伸缩提供支撑,Software/Service/Application这个基本上就是我上面说的关注点,但是没有devops之前,ops在这一块更多的是关心业务的存活性,而且还是基于具体资源的业务存活性,很少会整体去评估业务的状态。至于业务方面的东西,就看产品和开发的博弈结果了。但是随着devops的成熟,使用业务相关指标已经成为团队的共识了。

监控我们常用的工具有:Zabbix, Nagios, Prometheus, NewRelic, Cloudwatch, ELK等。

报警通知和响应

当拥有一个完备的监控系统的时候,我们还需要报警通知和响应机制来把最后这一步给完善了,毕竟,我们要确保服务尽可能的高可用,还是需要在发生无法自我修复问题的时候及时的感知到并作出响应的。这个包含两个内容:1. 通知值守人员;2. 接到消息之后如何应对。

报警通知是一个非常恼人的话题,它又包含多个小的点。第一,噪音消除,毕竟我们很大程度上会监控非常多的东西,对于一个资源或者服务我们在不同的层面监控了不同的内容,或者使用多个工具做交叉监控以避免监控工具的单点故障。所以当某个东西有问题的时候,所有的对应的监控都会发生状态变化。如果不经过滤的把消息全发出去,可想而知会发生什么,所以如何消除报警噪音是一个永恒的话题,一般会采用尽可能的合并同类信息以及基于更低层的监控状态抑制的依赖失败的方式来消除不必要的噪音。第二,就是什么情况需要报警,这个要基于意义来出发,我们不想把有限的精力投入到意义不大的事情上去。我个人的原则是:如果这个问题如果不处理就会影响到业务可用性了,不管是外部的SLA还是内部使用的SLO,那就是需要报警的。如果这个事情并不是很紧急,比如说我的某个系统发生了高可用的主备切换,老的主设备有可能发生了故障,但是这个问题并不会直接影响到我的业务的性能和可用性,那我可能采用其他的渠道来感知,而不是报警的方式。所以,我的理念是:一旦接到报警,必须放下手头的事情,立即响应。因为如果说不是所有的报警是需要马上投入解决的内容,那有可能会让值守的人员造成懈怠,错过夹杂在连续的不用立即响应的报警中的需要立即响应的报警信息,这个比较绕,多读两遍吧。当然,你可以把一些实际中并非需要马上响应的事情在你在实践中强制值守人员马上响应,形成习惯。第三,报警信息的具体内容,报警内容携带足够多且明确的内容也是我们一直追求的,如果我们收到了一条类似于Critical: Minimum HealthCheckStatus LessThanThreshold 1.0, Triggered by: Amazon Cloudwatch这样的报警,我估计绝大部分值守人员也是一头雾水,不知道到底发生了什么事情,等到他登陆了AWS,从Cloudwatch里面找到一些有用的信息的时候,估摸也差不多浪费了3,5分钟了,起码这个月4个9的目标是没戏了。关于怎么结合业务和你的通知工具发送内容只能靠大家伙自己摸索了,毕竟这个并没有什么通用的标准,但是不管怎么样,确保值守人员能在看到消息的时候尽可能的获取到最多的有效信息是关键。有小伙伴在这里提到了某个项目采用了唯一错误编码的方式,可以用这个代码追踪到具体出错的代码所在的文件和行数,虽然他没有办法提供太多的架构设计和实现上的细节,但是听起来也挺高大上的。

接到消息如何应对就是我们常说的IRP(Incident Response Plan),这个东西也是各家有各家的玩法,毕竟做响应的团队组成的角色都是可能有很大的差异的,有一些只有单纯的ops,或者会加入dev,有一些可能会把业务人员也纳入进来,怎么让其他的成员了解面对的问题并加入响应是重点。另外这个Plan里面也需要覆盖如何升级(Escalation)这一块,毕竟有些问题可能不单纯的是技术问题,比如隐私,商业数据丢失或者泄漏等,需要更高层面的角色根据当地的法律来做应对措施的,简而言之,怎么有效的组织有相应能力的人员来解决实际的问题是核心。

报警通知我们用的工具有:Pagerduty,常用的IM工具。

一个美好的监控系统

虽然监控系统里面很多的时候会包含一个展示台,但是这个不是我们今天要讨论的内容,所以就没有展开。但是轩在聊的时候描述了一个非常美好的画面,就是我们在接收到报警之后,能有一个地方,可以看到这个报警或者某个请求所有的相关的信息的,最好是可以图形化的,从这个请求进入我们系统的第一个地方比如说负载均衡器或者CDN开始,到最终这个请求出现问题或者处理完成,这中间不但有这个业务的调用链,也有对应的当时具体处理这个请求的资源的相关状态。抛开这个系统的复杂度不谈,只是所需要的基础信息的标准制定和收集就是一个很大的挑战,毕竟就目前我们碰到的现实情况,都是业务为导向,保证交付效率才是王道,除非真的要到了一定的体量,而且这一块的欠缺影响到整体商业推广和问题溯源了,需要高层次的角色来在整个公司内推动这个事情,否则,所有的一切都是一个美好的愿望而已。但就如Facebook,AWS,阿里等这些行业领头羊,我所了解到他们也只是部分实现,并没有完成这样一个完美的支撑系统,从我有限的使用AWS和阿里的技术支持的结果来看,AWS至少在信息收集和溯源上还是比较成熟了,阿里么,我只能遗憾的说,还处于散兵游勇状态。但是我觉得总有一天,这个东西一定会有,并且成为大型系统的监控工具基础组件。

OFC的野望

OFC是Opportunities For Consistency的缩写,这个是我们一个客户的实践,旨在提炼出一些好的可通用的实践,推行到所有的项目组里面,这样的话,不同的人员转换项目的时候,在相应的方面有统一的上下文,降低一些学习成本。我们之后也会考虑把我们各个项目的一些好的实践可以抽离成一些OFC并推行开来,目前我们已经有的OFC文档里面包含了一篇关于监控的,所以就顺便提了下这个,如果各位小伙伴们项目中有自己觉得不错的可以推广的类似的实践,请联系我们哦。

关于智能监控

这个话题其实其中一位小伙伴想听的内容,但是很可惜,我们参与讨论的人都只有有限的知识储备,所以并没有说太多。虽然我了解到BATJ有一些这方面的实践,但是看到的都是零星的介绍,并没有具体的技术类的资料。不过从大部分可见的信息和我们的需求来看,主要还是结合历史数据来做趋势预测和异常分析比较多。其中有小伙伴提到Nagios新版本有相关的功能,我简单的搜索一下,只找到这个How To Use Capacity Planning的文档。我比较熟悉的AWS Cloudwatch也有提供一个Anomaly Detection的功能。总体来看,这一块如果自己做,是需要结合一些统计算法和ML算法来实现的,对于专业知识的依赖度还是比较高。但是好的一点是,平台和工具里面开始提供类似的功能,这个大大的降低了我们的门槛,如果你们有这方面的资料或者经验,也可以留言分享给大家。

相关资料

很久以前,其实也没有很久,大概也就4年前吧,我和小宝就一直想着做一个专门扯淡的活动,成立了一个“爱扯淡”的民间组织,而且还注册了一个域名ichegg.org用来准备输出我们的一些成果的,但是后来搞了两次活动,之后因为一些原因就偃旗息鼓了,域名也被小宝用来当个人博客了。多少次午夜梦回,我还是会时不时的想起这个伟大的梦想,想去把它做下去,今天这个就当是我再次踏上扯淡的征程的开始吧,非常感谢今天来参加扯淡的各位,也希望以后有更多的人参与到这个伟大的活动中来。

BTW, 这个主题相关的内容并不是成熟思考过整理出来的有逻辑结构的内容,只是就我们讨论的内容做的一些简单的归纳整理,毕竟轩还是一再强调我们所做的事情是需要输出的,如果没有输出,可能意义真趋近于零了,所以,花点时间写一下,聊胜于无吧。也希望你们看了的人能从中真的学到点什么,那就更好了。

  • Hi, TOC安全小组又和大家见面了。

  • 为了缓解大家的工作压力,让大家在工作之余有更多的“放松”时刻,我们推出了《安全故事会》系列内容,计划每周更新一篇公司内部或者业内有名的翻车现场,希望大家在围观的同时能够有所得。

背景

  • 本期要分享的是内部的一个案例:某项目其中一项任务是对数据库中的用户敏感信息进行加解密,为了评估加解密后是否会对当前系统有性能影响,需要对此功能在staging环境中进行短信发送业务流程的模拟测试,该测试导致了神秘效果……

事件回放

2020年1月上旬:

  • PM:我们需要一些性能测试,来了解此次新功能对系统的影响

  • QA:Get,我要做性能测试

  • Dev:Get,稍等我给你个服务调用流程和API地址

  • Ops:什么,要做性能测试?那要注意发送短信的接口,不要真的发出去了

  • Dev:这是短信发送API,按这个格式发送,我们先测试下

  • QA & Dev:这是我的手机号,这是我的国家码,调用成功了!咦,短信呢,没收到短信,是不是这个短信在非生产环境不会真实发送?再用浏览器发送,咦,收到了,是不是这个API只能浏览器调用?

2020年1月下旬:

  • QA & Dev: 测试脚本终于写完了,本地测试也通过了,走到QA环境测一把。经过了艰难的调试,小规模测试终于通过了,不玩了回家过年去了……

2020年2月初:

  • QA:终于复工了,Ops来帮忙部署到stagging环境

  • Ops:要大规模测试了,这个测试分支的短信接口一定已经被屏蔽了吧?(也可能Ops此时忘记了还有短信接口这回事……)好了,部署完毕,可以测试了

  • QA:终于测试完毕了,好了,分析下报告。

几天之后……

  • PM:客户怎么突然联系我,说最近短信平台总共发送了几十万条短信,还都是给美国发的,疫情期间不应该这么多数据啊,Ops来查下是怎么回事

  • Ops:这几天短信量大,莫非是被攻击了,我这没收到什么异常告警啊?赶紧上生产看看,没有啥异常啊……难道是前几天的性能测试?啊,查下staging环境的日志,这短信接口的日志不太对啊,测试时候短信真的发出去了,这下问题有点大

  • Dev:短信发出去了?stagging和生产共用的同一套配置吗?

  • QA:短信发出去了?难道我测试调用的接口和生产是同一个?

  • PM:问题有点大,我们先确认下发出去了多少条、发到哪里了,还需要向上汇报,再告知客户

  • QA & Dev & Ops:代码分析、日志分析…… 原来这个接口不是这样调用的,是前端拼接后发送给后台的,后台接口中的国家码只是给数据库存储,调用第三方接口时候是需要自己拼接的。这样当时随机构造的手机号,因为都是1开头的,所以1被当做国家码,短信都发到了美国,所以当时浏览器测试可以收到短信,API测试收不到短信,我们总共向美国发送了xxx条短信,发出去的都是随机的验证码,没有敏感信息,也几乎没有一个手机号重复收到多条短信。

  • 客户技术人员:了解,这是你们要的短信发送记录。

  • 客户小Boss:没有发出敏感信息,全部发送到了美国,目前还没收到垃圾短信的投诉,我不希望这件事情扩大到大Boss那,TW可以直接和短信平台结算。

  • 项目全员:内部Retro

  • PM:和客户、财务、法务拉通,处理后续事宜

  • 项目全员+TP+U Head:Retro

于是,可能是TOC史上最昂贵的性能测试就这样诞生了

亡羊补牢

  1. 如何防范生产环境被攻击?已确认,在现有生产环境下,任何一个用户都可以通过浏览器抓包或其他方式识别到短信发送接口,进而无上限调用此接口进行恶意攻击,产生大量费用。可以通过接口访问频率监控+限制、基于历史数据对每日短信发送限额设置合理的上线阈值。

  2. 不同环境应该使用不同账号。可以对不同环境设置不同的限额和告警策略,还可以避免一旦生产账号泄露导致其他恶劣后果。

  3. 短信平台是否支持白名单,已确认只允许白名单IP的调用。

  4. 短信平台账号密码信息存在代码库。成本最低的修改方式是修改为secure variable的形式,直接注入容器环境变量。

  5. 非生产限额&告警。首先实现不同环境使用不同账号,然后对非生产环境设置一个较小的上线,以免误操作等产生大量额外费用。

  6. 内部短信API手机号码调用时如果没有region信息就报错。这样错误的调用就不会生效,不会去调用三方平台。

  7. 调用第三方短信平台的方式改为https,避免通信链路不安全导致API的认证信息泄露。

  8. API文档对内部短信API添加说明信息。

事件影响

  1. 手机用户:短信发送地区全部为美国,随机号码发送,重复号码发送量小,短信内容为随机的验证码,不含敏感信息,客户也并没有收到咨询或投诉电话,影响较小。

  2. 客户:短信量大,直接导致的经济损失和客户对我们的主观印象损失。

  3. 对公司:项目亏损,带来经济损失和声誉损失。

反思

  • 一次事故,必然是所有相关环节都失效了,才会发生,回顾此次事件,其实有这几个关键环节都没有把握住:

    1. 测试时未对浏览器和直接调用API的参数进行比对,直接调用API没收到短信时没有进一步追究原因。正确的做法是,通过其他方法,比如后台日志、浏览器抓包、阅读代码等,去真正理解API的调用。这提醒我们,有些项目因为文档、开发团队变动等难以追溯的历史原因,可能导致某些API的调用方式比较神奇,所以在出现一些不合理的现象时,一定要去定位根因,不要想当然,另外,对于技术相关性较强的需求,BA/QA可以和Dev确认更多细节,确保AC/测试用例或者手段是有效

    2. 不同角色之间的沟通没有到位,每个人都是只有部分信息(Dev认为环境是隔离的,QA认为浏览器调用使用的API和测试脚本API不是同一个API,Ops认为代码侧已经修改就不需要考虑环境隔离),而信息不全导致了对整套流程的认识出现了偏差。这提醒我们,在项目中,每个人的上下文要尽可能一致,不要担心沟通成本高

    3. 相关角色没有对调用收费相关API的安全意识,没有对安全需求进行识别,没有对此API进行重视,也没有相应的安全测试用例。这提醒我们,当测试涉及到API的大量调用时,一定要反复确认会不会产生额外的费用。

    4. 性能测试流程不完善,没有专门建卡详细列出测试的todo-list和风险项。这提醒我们,不要忽视一些必要的流程,比如之前还见到有的团队为测试申请的云资源忘记释放,最终导致大量费用,而这些问题除了靠相关人员意识外,是可以通过合适的流程去规避的。对一些高风险需求进行安全评估,如需要单独添加安全卡

    5. 在QA环境进行小规模测试时,并没有暴露此问题,因为我们没有对收费API有任何限额和监控,只对服务是否正常有监控,而问题的最终暴露是第三方供应商发现异常告诉客户的。这提醒我们,项目中的监控和告警,既要考虑服务状态,还要兼顾一些重要的业务数据,不能在业务数据出现异常时没有感知。

    6. 最终在部署版本开始测试前,没有再次确认短信是否真实发出。这提醒我们,在发布版本时,最好要有角色(Ops/TL或其他角色)进行把关,了解到这个版本的功能改动及可能存在的风险点

  • 如果短信平台没有提醒客户,这样的测试多做几次,会是什么后果?

  • 如果这些短信全部发送到了国内,被人投诉了,对客户造成了恶劣影响,又会是什么后果?

  • 如果发出的信息中包含敏感数据,会是什么后果?可能这会触及到ThoughtWorks的生命线