Becomin' Charles

算法 | LNMP | Flutter | Mac

Becomin' Charles

前两天写了如何给 Python 项目设置 Virtualenv,然后又发现了一个新的小问题,于是,干脆把文章也改了,改成小集锦了,以后各种小问题,都记录到这篇文章里面好了,省得开新的文章了。

Mac 上如何给 Python 项目设置 Virtualenv

这两天,写了个小小的 Python 代码,基本功能实现了,需要完善一下的时候,突然想起来,VS Code 那么好用,应该也支持 Python 吧,我就不用在命令行用 vim 写那么辛苦了,主要是对中文字符的渲染不好看。

没想到,相当尴尬了,用 VS Code 打开以后,无法识别我在项目文件夹设置的 virtualenv 环境。而 Python 扩展的官方帮助赫然写着是支持的。介绍的设置方法什么的,完全都是扯淡,毫无帮助,还说会什么自动识别之类的,都是不起作用的。想起来了著名的直升机飞行员笑话。

最后,救世主还是 SO(StackOverflow),只有一个老哥提到了一句,在用 VS Code 打开一个文件夹的时候,就创建了一个 Workspace ,然后,你的目录下多了一个隐藏目录 .vscode ,这个目录里有一个 settings.json 文件,这个文件里,设定了一个 Workspace 级别的配置。

如果你使用 ⌘ + ⇧ + P 来设置 Python: Select Interpreter 的时候,是无论如何都没法发现你的项目目录下的 bin 目录的。这就很尴尬了。所以,方法是直接在 Workspace 的 settings.json 中,手动编辑加入使用的解释器路径:

1
2
3
4
5
{
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.pythonPath": "./bin/python3.7"
}

然后,重启 VS Code,就可以发现完全兼容了现在这个项目目录下的 virtualenv 环境了。又可以愉快的 Python 了。

我解释一下,我一般是进入一个项目目录,然后执行 virtualenv . ,这个项目目录会变成一个 virtualenv 环境,只会影响这一个项目,不会影响别的项目,这样我可以在每个不同的项目里,使用不同的 Python 环境。这会在项目根目录下多出来一些目录,bin,include,lib 等,加入到 .gitignore 文件就好了。

调整左侧目录树的缩进

连续用了几天的 VS Code,发现左侧目录树的展开折叠后,子目录的缩进实在是太小了,目录层级多了,展开以后真的看着很难受,很累眼睛,经常有迷路的感觉。

VS Code 左侧目录树的缩进感觉

放狗搜索了一下,在知乎找到了一个答案,原来可以在设置里面,通过设置 Tree:Indent 选项的值来控制缩进。

在设置里搜索 Tree:Indent 选项 可以找到

在这里,我们看到 VS Code 的缩进控制是用像素做单位的,真是吐血。调整为 24 或者 32 比较好,另外,下面那个选项,说的是,展开一个目录时候,绘制一条垂直线,就像上一张图 alt 目录下面有一条竖线一样,把 onHover 改成 always ,总是显示竖线,这样,目录树就会显得好辨认很多了。

如何左边展示目录树,右边展示 Outline

这个是困扰了我很久的问题,因为一直以来,目录树和大纲都缩在左边,好难受。今天终于知道了。

首先调出终端,然后终端可以停靠在右侧,终端所在的容器,可以停靠大纲的面板,把大纲拖去终端的面板就行了。

感觉上,这些面板有两种,一种是可以让别人停靠的,另一种是只能是停靠到某个面板的。想要左边一个 bar,右边一个 bar 的话,你需要两个可以停靠的容器面板。

不过,如果我想左边目录树,右边大纲,下面是 Console ,又怎么操作呢?

好久没有写代码提交 GitHub 了,真是惭愧!回到正题,今天提交了一个代码,冷不丁发现,在我的 Commit 记录里面,有一条被打上了 Verified 标记。原来 GitHub 的 Commit 支持签名验证了,我可以对每一个我的 Commit 进行签名(Commit Signature),这样,GitHub 的其他用户就知道这个“提交”来自一个可以信任的来源。我举个例子,如果有人设定了我的头像,我的名字,往我的版本库里 Push 了一个 Commit(我的 WordPress 插件官方仓库,被黑客提交过恶意代码,因为我不小心被钓鱼了,自爆一下黑历史),那么,有没有 Verified 就成为一个识别真伪的依据(虽然,我位微言轻,可能不太会有人假冒我,我也知道,杠头请退散)。

给每个 Commit 签名

作为个人开发者,给自己的每个 Commit 签名,可能有点多此一举(或许会有这么想的人),但是在一个多人合作开发的项目里,管理员可以要求所有的项目成员,都必须签名自己的 Commit,不接受未经签名的 PR,那就产生了一定的意义(虽然是什么意义我还没想得太明白)。

上面图里个 Verified 标记,是 GitHub 自动给打上的,因为一个项目的第一个 Commit 是在我生成项目的时候,由 GitHub 的 Web 站点自动提交的,是项目的初始化 Commit。这种情况由 GitHub 方面利用你在网站可信的登录态进行签名。但是我们知道,这是一个分布式版本控制,一般来说,Commit 都是在本地完成的,然后 Push 到云端,所以,要想让每个 Commit 都能够带有签名验证,需要在本地部署签名的过程。怎么做呢?

在 GitHub 验证一个邮箱

我们在 git 里面,进行 commit 操作的时候,都需要设定一个 user.name 和 user.email,所以,对 commit 的签名,是基于 email 的。第一个步骤就是在 GitHub 的 Settings 里面,设置一个用于关联签名算法的 email。你可能已经有了多个 email,在本地,需要设置那个关联了 GPG 签名的 email 来提交代码。

生成一个 GPG 的 key

刚才已经说了,签名的原理是使用 GPG,其实 GitHub 还支持 S/MIME,我不知道那是什么,所以就选 GPG了。

GitHub 支持的加密算法有:

  • RSA
  • ElGamal
  • DSA
  • ECDH
  • ECDSA
  • EdDSA

如果加密算法不在上述之中,可能无法被 GitHub 所验证。如果,你的环境没有安装 GPG,第一步你可能需要安装一下:

1
brew install gpg2 # 我的环境是 Mac 就用 Homebrew 安装了

我在 Mac 上安装的是 GPG 2.x,其实有 GPG 1.x 和 2.x 两个版本,显然大一点的更新一些,支持了很多新功能,不过,有可能你所在的系统环境只能选择 1.x。2.1.17 版本以上的可以使用如下命令来生成 key:

1
gpg --full-generate-key

虽然官方说了支持上面 6 种算法,但是在 GPG 指南这里说,必须选择 RSA,我不知道这个矛盾是为什么,以后再来探究。上面的命令会开启一个命令行交互式创建 key pair 的过程,问及算法的时候,用默认的就行了,我用的 GnuPG 2.2.19,默认选项是 RSA and RSA。当问到 key 的长度的时候,要填写 4096,因为官方指南要求这样,而 GnuPG 的默认值是 2048,这里需要注意。

接下来是过期时间,个人使用选择 does not expire,永不过期就可以了。如果是团队使用,看整个团队的安全策略如何。

接下来要求填写 ID 相关信息,会填写名字,邮箱,注释,这里邮箱是比较关键的,在第 1 步里,咱们预先准备了要关联的邮箱地址。就填上那个。然后是要求键入密码。这个密码的用途是保护你的私钥。如果你自信不会有人入侵你的个人电脑,那么你可以不填写密码,GnuPG 会很贴(fan)心(ren)地走两遍这个要求密码保护的过程,请耐心回车。(注:假如很不幸,一个黑客已经黑到你个人电脑了,用你的身份打开了你的 Term,这时候,如果你的私钥是有密码保护的,每当程序需要使用你的私钥的时候,都必须输入密码,这个情况下,私钥保护密码就是最后一道屏障。)

1
2
3
4
5
6
gpg --list-secret-keys --keyid-format LONG
/Users/hubot/.gnupg/secring.gpg
------------------------------------
sec 4096R/3AA5C34371567BD2 2016-03-10 [expires: 2017-03-10]
uid Hubot (Comment) <Hubot@a.com>
ssb 4096R/42B317FD4BA89E7A 2016-03-10

使用上述命令就可以查看你 LONG 格式的私钥了。这个私钥就是要用来对你的每个 Commit 进行签名的。

在 GitHub 登记你的公钥

然后,咱们需要在命令行打印出来自己的公钥,使用如下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
gpg --armor --export 3AA5C34371567BD2
# Prints the GPG key ID, in ASCII armor format

-----BEGIN PGP PUBLIC KEY BLOCK-----

mQINBF5bciABEADJ/FQa+ymIuKuZycgtPmLMPHEwOtYY21k7zU9ddDZH6ZGIxeN0
N9ljc6zp8/3cQDRrbrirHv/9WqaqSRb+EFcUog5LOfR/C6NeiGNW8AgUuFxGGFXK
s6VrAmzhIxDmKDkRdX9sHf7myiwBhtkFM0/8AGUdR8pjHKw+vA8IJzMhowWkiX1O
F1ZW81gKUYCLSfkty1HccGr4kFpE6r1R/w18hYH2zcr0dll0ox2LHfSHuuQzamew
hdR7B6S5Xi+EJjv7rujHaRWzLoPXktxUFme9LxdVblp6FD/lP79AkPhqSPAwzee+
ShlO9AScCCbsm8p3/KhmUn2yigbfd0eWvh4wm5HvbTCJ3/SLspMYrsF/VMHAJFRW
pmULevI5bdVR3fm7t/IgkjFasmbOZ9EqZNf43ljVi3SOyTmRX9GbxtvHXKL8tgL1
q7do0cArc/cKigEfssHe6gXChLZ6nDEzj/aNgOEcKo/cPVVCH4yzldEMvCB4aMYW
PET+7Io+FM1b69yOtFvKmJnGNpDbtySn1b6E0gWk/3uqzcspAzZMb6aIdZ6BcaXE
wU8zqRqcMXVnI6s2gvrMYrFCUB71ujzdGO9LWIu/y/FOdrzmrjXofOmdQom9Z+dW
cCo7LaTCE994HhLbqacsUROhjFCSzisH1yi0T0rD6oWSzsjFdewpEtjJGwARAQAB
tENDaGFybGVzIChHUEcga2V5IHVzZWQgZm9yIEdpdEh1YiBjb21taXQuKSA8Y2hh
cmxlc3RhbmdAZm94bWFpbC5jb20+iQJOBBMBCAA4FiEE6zd9skJwGmhOg6epkcp5
trRrrvQFAl5bciACGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQkcp5trRr
rvQ…… …… ……
-----END PGP PUBLIC KEY BLOCK-----

大概长上面那个样子的,很长很长,整段拷贝下来,然后去 GitHub 个人的 Settings 界面 SSH and GPG keys 标签里,登记这个公钥。登记完毕后,你能看到这个 key 关联的 email 地址显示出来了。

在 git 里面设定使用签名

咱们的公钥私钥对已经生成了,然后就是需要告诉 git 命令每次 Commit 的时候,都要使用私钥进行签名。

1
git config --global user.signingkey 3AA5C34371567BD2

上面的命令设定了全局的签名密钥,如果你使用多个身份在多个项目提交代码,那么不要使用 –global 参数,但是,要记得去每个项目里设置单个项目范围的签名私钥。然后,你要告诉 git 命令,以后每次 Commit 都要签名:

1
git config commit.gpgsign true

也可以手动控制,那么记得执行 git commit 的时候带上 -S 参数。

总结

到此,我们需要的设置已经全部设定完毕了,设定完毕后,执行一个 Commit,然后 Push,就可以在你的项目记录里看到,此次的 Commit 是带有 Verified 标记的了。

此外,有个关于隐私保护的话题。你在 GitHub 验证了一个邮箱,只有你的 Commit 关联了这个验证过的邮箱,在你贡献 PR 的时候,GitHub 才能在贡献人清单里显示出来你的帐号 ID。这也意味着,所有人,都可以看到你的这个电子邮箱,即隐私暴露。

在 GitHub 的 Settings 里面的 Emails 面板,有一个 keep my emails private 的选项,如果勾选了的话,那么关联了你的验证邮箱的 Commit 是无法被 Push 的,因为服务器知道你的隐私设定,会保护你不让你暴露你的私人邮箱。

听起来,如果你非要保护自己的隐私,就死锁了一样。不过,你可以选择,使用 GitHub 提供的匿名 noreply 邮箱,只是一个邮箱地址,并不能收发邮件。你在使用 GPG 进行签名的时候,可以把邮箱地址填成 GitHub 后台给你提供的匿名邮箱。这样,才能同时做到,既签名你的每个 Commit,又在你贡献 PR 的时候,追踪到你的 GitHub 帐号。

文首截图里的项目主页在此:https://github.com/charlestang/trip-table-parser 可以去 commits 标签页里看看带验证的 commit 的效果。

全文完。

附:

今天看了一篇文章,分析讲解了技术人员面试中,现场编写代码的意义,我觉得说得非常在理。

其实我在本科的时候,就有一次有机会面试谷歌的实习生。但是因为在徒手写代码的时候,失败了,成了我毕生的遗憾。也造成了我在这类面试中的恐惧心理。当时,我只是一名大三学生,我 C 语言课程成绩优秀,算法课成绩一般,但是我很有动手能力,做过不少网站和项目,还在学校的网站设计大赛拿奖。我认真做了学过课程的大作业,都是照着最高要求做的,比如实现数据库引擎,实现编译器等等。我自我感觉不错,可是,仍然在面试官让我写出围棋提子算法的时候,一脸茫然。

我觉得这非常不公平,只是因为我对这种冷僻的问题没有研究,对算法的套路不熟悉,就遭逢此让人遗憾一生的失败,实在难以服气。

工作多年后,我也成为了一名面试官,我就非常反对在面试中考徒手编程,但是后来我发现,识别一个优秀程序员,着实没有什么好的方法,如果不用徒手编程的话,我们识别一个好的程序员的概率更加低下。所以,我做出了退让,候选人必须过手写代码这一关,但是我不会考算法套路,我们都知道,很多人朗朗上口的深度优先,广度优先,回溯法,线性规划等等,在你实现一个电商业务系统的时候,或者在你实现一个对账系统的时候,或者在你实现一个网站管理后台的时候,用到的概率微乎其微,就算要用,也不会自己去写,因为经常会写出漏洞百出的代码,使用已经存在的类库要稳妥得多。所以对于真实工作来说,这些套路被一些同学喷成是屠龙之术,我觉得也无可厚非。

所以,我当时要求候选人写这样一种代码,仅仅根据直觉,就一下子知道解法是什么,只要智力正常,有初中数学水平就可以随口讲得头头是道。越是这样的题目,越能进入我惯用的题库。我举个例子,计算两个数字之和。这个看似很简单的题目,是很多 ACM 比赛的入门题,加法是任何编程语言内嵌的方法。如果候选人想得太过简单,我就会提示,编程语言的变量都有取值范围,我出的这道题目,不限制两数的取值范围。

这样一来,就不如表面那么简单,但是仍然知道怎么做。首先要选取合适的变量作为容器,其次要按步就班来计算求和。最后可能要考虑很多的情况,比如正负数,浮点数等等。但是,想不出来这个怎么做的人,少之又少。但是写不出来的人,比比皆是。

我此前的想法是,能用嘴说得头头是道,说明候选人智力正常,能用笔写出来,说明候选人是个熟手,不怕繁琐,是个耐烦的人,因为我们的日常工作琐事,往往就是需要细致耐心,有时候甚至是个重复的工作,如果不耐烦,畏惧繁琐,可能就不是很好的候选人。另外不能把自己的想法写出来,说明对自己惯用的语言并不熟练,不一定是个熟手。

我秉持这样的理念很多年。类似这样的题目,我知道还有不少。我有个同事也秉承类似的理念,跟我不同他坚持需要有一点绕弯的题目。他想考察候选人会不会奋力争取,能不能与面试官充分沟通,在提示之下到底能不能被循循善诱找出正确答案。一方面考察沟通,一方面考察智力,一方面还考察候选人的性格。我觉得这也是不错的方式。

有一次,我在拉钩或者知乎上回答别人对编码面试的质疑,我解释过我的理念,我觉得自己还是比较理直气壮的,我说,编码面试确实不能证明一个候选人有多好,但是,测试出了这个候选人的下限有多低。最不济,候选人还是一个可以熟练写出代码的人。

今天看到的文章是这篇《为什么时至今日编码面试依然这么糟糕?》,这篇文章从另一个角度解释了这个问题。我觉得这个作者解释得也比较好。

我们当然会在面试中看走眼,不过,这种看走眼,有两种情况,一个是放进来错误的人,另一个是拒绝了优秀的人。这两种情况来看,放进来错误的人,成本是非常高的,但是拒绝了优秀的人,成本则很低(对于优秀的顶级企业来说尤其如此)。

通过艰难的徒手编码测试,对一个人的心性,毅力,能力都是一种有效地证明,所以,抬高了录用的门槛,极大降低了“错放”的概率,哪怕增加了误拒的概率,但是综合来看成本是降低了。再加上企业有很好的品牌,就更不惧没有人来面试了。

最后,我也想说跟文章作者一样的话。作为候选人,你已经足够优秀了,这个世界上一定有适合你的岗位。一场面试什么都说明不了,因为面试官背后的种种想法,都是希望在统计意义上去优化成本和收益,对于个体的候选人来说,不具备什么绝对的说服力,运气成分很大。所以,千万不要因为过不了编码面试就气馁。

之前,在北京体验了一次智能门锁,可以用密码开门,不用钥匙,觉得好方便,一冲动,就把家里的锁换了,选了京造智能锁(当时随便找了个最便宜的,买下来 789),可以用指纹、密码、ID 卡、钥匙多种手段开门。更换异常顺利,从此少了一把很大的钥匙,感觉特别方便。这时候,我发现楼下单元门的门锁,还是需要使用钥匙或者 ID 卡才能开。我出门还是不能空手。

不过,楼下单元门上有个对讲门铃,按了房间号码以后,会唤响室内安装的一部对讲话机,接听后,按开锁键,就可以打开楼下的门锁。我就想到,能不能按一下我自己家的门铃,然后,用手机 App,通过网络控制,完成开锁的动作。这样,我出门只要有手机,就可以了,不需要钥匙或者 ID 卡。

机械方案,误入歧途

楼宇门禁室内机的样子

先给大家看一下,楼宇门禁系统的室内机的样子,看起来就像一个电话分机一样,一般安装在家里门口的位置。上面有一个按钮,开锁键。这个东西的淘宝名称叫做“楼宇门禁非可视对讲室内机”,这个名字我也是折腾了很久才搞明白的,免费赠送给大家了,这年头搞清楚搜什么很重要。

想要手机控制开锁,就要想办法按下那个按钮,我想起以前在微博上看过一个人发的小视频。

在微博上看到过一次这个小视频

这个视频里展示了一个手机控制的机械杆,机械杆转动,形成了一个按压按钮的效果。显然,我遇到的问题有人遇到过,也有人解决过。当我想到要开门的时候,刚好想起了这个视频。废了一番力气,从历史记录里翻出来了。

这个方案,是一个通过网络控制一个机械的外挂的方案。想到这个方案以后,就开始研究人家怎么做的。其实,玩这一套,是一个领域,叫 IoT,意思是 Internet of Things,中文叫“物联网”。图里使用到的那个机械设备,核心部分是一个电机,这个电机的淘宝名称叫做“舵机”,更加正式的名字叫“步进伺服电机”,本质的原理是,通过脉冲信号的数量,来控制电机转动的角度,实现比较精确的控制。因为用在飞机模型,或者船只模型里面,用来控制舵板之类的装置,得名为舵机。

图里的方案,大概使用了一个叫 Node-RED 的解决方案,大概是一个嵌入式开发板,封装了比较高级的语言(NodeJS)和开发环境,方便玩家快速开发出如是视频中的解决方案。

这个方案看起来轻巧,无侵入(不破坏原来的设备),轻便。但是,并不是全无缺点。对我来说,最大的缺点就是门槛比较高,光是搞清楚视频里的这些东西叫什么名字,淘宝怎么买,就花了不少精力,看得见,叫不出名字,越来越普遍了。另外,视频里可以看出来,电机带动的机械杆,是水平转动的,但是按钮是垂直按压的,那么水平扫动,怎么才能转换成垂直按压呢?

力学原理示意图

上面的那个方案,如果想要同样效果,需要一个前提条件,就是你要按压的按钮是带有导角的,不然的话,就不能用视频里的方案,然而,非常不幸的,我家的那部室内机,开锁按钮是圆柱形的,侧面看上去,就像图里左边的形状,没有导角,也就无法通过水平扫动得到一个垂直方向的作用力。

我能想的办法,只能直接产生一个垂直方向的作用力,一开始我思维定势在舵机上面,就变成了如何把舵机输出的扭矩转换成垂直按压的作用力,可能需要一套连杆齿轮之类的经典机械结构,简直痛苦,我并不懂这个。

空想方案一

空想方案二

我想了各种空想方案,但是,发给小伙伴,他们都看不懂我在说什么,所以,读者你看不懂,也没什么,不是你的问题:D

后来一个对机械有点知识的同学告诉我,还有一种设备,是集成好的电动液压杆,才打开了一个新的大门,原来还可以用液压杆,直接能产生一个方向的作用力,还足够大,没有扭动转换按压的问题,也没有力矩杠杆问题,不用考虑功率问题。

但是终究还是太过麻烦了,想想就头皮发麻。乱七八糟一堆,怎么安装?怎么固定?怎么供电?主控怎么连接?全都没有头绪,需要太多知识了。所以,这个阶段,全部停留在空想,我没有任何行动,仅限于搜淘宝和思维试验,以及群里和小朋友讨论。

弱电电子方案,看到了希望曙光

外挂一套机械装置可能还是太过麻烦了,而且稳定性实在是堪忧,以至于,整个项目陷入了僵局。有一天,我一个人待在家里,实在无聊,我就开始拆卸门上的那个室内话机,打开一看,竟然也没有那么难以理解。

非可视对讲室内机1

非可视对讲室内机-电路板正面

非可视对讲室内机-电路板背面

看到这个电路板,我惊呆了,没想到如此之简单,以前我严重高估了困难性。从电路板正面,我们三个元器件和 6 根线,很好理解,信号进入线,顶部两根蓝色的,(震惊它的精巧,竟然只有两根),下面 4 股线是听筒,可以想见,里面是麦克风和扬声器。右上角是开关,左边是挂机的按钮,右下角是一个二极管小灯。

就算我电路知识全部还给老师,我仍然能理解到,开关的本质是一种短路,那么输入信号线只有两根,又直接连接了开关,从背面可以看到(这是一个单层 PCB 电路板,非常简单,看到的就是实际的情况)。可见,开锁的原理,极其简单,就是短路输入的两股信号线即可。也就是说,我们需要的东西是“通过网络控制的开关”,或者类似的东西。一个同事告诉我,叫“继电器”。

于是开始了新的搜索旅程,开始在淘宝找 Wi-Fi 门禁开关,或者继电器。于是,我找到了这么一个东西:

淘宝找到的智能 Wi-Fi 门禁开关

其实,继电器我也不是没有找到过,大概长什么样子呢,看下面的图:

Wi-Fi 智能继电器模块

我为什么买了上面的那个集成好的开关,而没有买下面的继电器模块呢,原因可能是我的个人洁癖。这个模块上面的蓝色的东西,就是继电器,从参数看,能支撑 250V 电压 10A 电流,而我想开关的线,无非是那么细的两根线,可以猜到,上面根本不会有什么电压或者电流的。那我为什么要用一个强电的继电器,去控制一个弱电电路呢?(这种洁癖是病,得治)

上面那个集成好的开关,显然也是一个 Wi-Fi 芯片加一个继电器,不过显然是一个弱点继电器,就显得非常小巧。而且,一体化集成方案,也非常紧凑。就是价格差距有点大,上面的要 98 块,下面的模块只要 30 多。

要想解决问题,还是要穿透很多迷雾

一个插曲,我买那个开关的时候,店家问我要怎么玩,我说了我的想法。淘宝小二告诉我,说不行的。我说,你估计不懂电子电路吧,你就告诉我一下,图里的几根线,分别干什么,以及怎么给这个电路板供电就行了。她很负责的要电话联系我,电话里,非跟我说不行的,说那个开关,只能安装在主机的附近,不能安装在室内机,肯定不起作用的,她亲自安装过。我问她,你安装过不起作用的原因是什么,你是否真的懂呢?她斩钉截铁说,她当然懂,她卖这个吃这口饭的,很了解。然而,我问不起作用的原因,终究是没说出来。

你要干一个事情,人家告诉你不行,然后,你可能要冒一些损失的风险,比如退换货的成本,时间浪费等等。但是,我想,这个东西的原理我应该是看懂了的,不太会错,她虽然说不行,拼着损失 100 块,我也要试一试好死心。

卖家终究没讲明白,这个东西怎么供电,但是给我发来了安装图,配合宝贝页面的图示,果然这个东西都是只介绍怎么安装在主机附近。很多公司玻璃门,都用的这种按钮,进门的时候刷工卡,出门的时候按一下按钮,门就开了。

所以,我只能去查一般的楼宇门禁系统的规格。对,关键词是“楼宇门禁”,这也是一个要命的关键词,你说不对,很难搜。我发现,大多数楼宇门禁的直流电供电箱,规格都是 12V1.5A,我就猜那个开关既然设计为和门禁主机共享电源,可能接受的是 12V 电源。想想,这东西还是能解决的,我要想办法使用 12V DC 供电。

东西买到手了,我才发现,我并找不到一个 12V 的直流电源。我手上最多的东西是充电宝,USB 线之类的东西,这类如无例外都是 5V 的直流电供电。我剪断了一根 USB 线,抽出正负两极,尝试驱动这个开关,发现根本无法点亮,可见 5V 是肯定不行的。

正好,我有一个喜欢玩嵌入式的小伙伴同事,他很热心帮我分析我买的这个 Wi-Fi 开关,我们分析电路板上的元器件,发现 Wi-Fi 主控芯片在网上非常流行,几乎是这类设备的事实标准,这个 MCU 芯片的标准电压是 3.3V,电路板上的继电器芯片,也搜索到了,供电电压是 5V,那么为什么 5V 的直流电拖不动呢?

我们又发现了上面有两个 DC-to-DC 的电压转换芯片。我们猜测,第一个是把 12V 转成 5V 的,第二个是把 5V 转成 3.3V 的。所以,只接受 12V。到这里,我就问高手,我说,这个电压转化,是什么原理,是把高于 5V 的都降低到 5V,还是只能把正正好好 12V 变成 5V?他回答我说,是把高于 5V 降低到正好 5V。当然,高出的部分是有极限的,太高的电压就直接击穿了。那么就有了一个大胆的猜测,只要供电高于 5V,这个东西就能跑起来。高手说:善。

然而我还是没有高于 5V 的直流电。你看,走了那么远,最后被这种事情绊住。结果,高手突然跑过来雪中送炭,接济了我一块 9V 的干电池。这个我也见过,竟然从来没想到过!(后面再说高手为啥这么热心)

我于是用 9V 电池直接测试了那个开关,点亮了!让我无比兴奋!虽然,离成功还很遥远,但是我觉得已经超过 50% 的概率了。

首次测试成功,我高兴得跳起来了

得知可以用 9V 电池点亮以后,怎么安装,以及怎么确保稳定运转就成为提上日程的事情了。从上面的图里,可以看到,我买的是一个 86 盒,就是家里一般的那种开关插座面板的规格。如果家里事先掏好一个洞的话,安装就简单了。只剩下了走线的问题了,如果没有洞,就比较麻烦。

同事建议我安装明盒,这个大家去淘宝搜“86 明盒”,就知道我说的是什么,明盒的麻烦是,要凸起一大坨。而且,可能还要在墙上打眼,膨胀螺丝等等问题。

我也是犹豫了很久,后来觉得,还是先试验成功再做决定。回了家里,我打算接线,发现这个电路板其实很小,就想,能不能干脆塞进那个话机的外壳里面,舍弃这个 86 的面板。反正我要的只是遥控开关功能,实体按钮话机上本来就有一个了,再多一个也不是特别有必要。

拧下来一比划,真的可行。于是我就开始了安装。

将电路板拆下来直接放入室内机外壳

将电路板拆下来直接放入室内机外壳

反正 9V 电池可以驱动,我就想到,把电池也放进去,图里就是我的安装过程,电池用双面胶贴上,然后电路板塞进。

[embed]https://v.youku.com/v_show/id_XNDMwNTY1MjMwOA==.html[/embed]

上面的视频就是我塞进去以后的效果,然后,我热血沸腾地跑到楼下去按门铃试验了。

[embed]https://v.youku.com/v_show/id_XNDMwNTY1OTc4MA==.html[/embed]

成功了!我觉得无比振奋,高兴得手舞足蹈。整个事情到这里其实就告一段落了,从想到这个点,到痛苦的寻找,最终还是被我做出来了,而且安装到了话机的里面,简直完美!虽然从 IoT 这个领域,或者从智能家居这个领域来说,我这个东西还是很简单的一个小东西,不知道为什么就是很有成就感。

最后的困难,供电

到上面那一步,我高兴得睡不着觉,四点多才睡下。心里唯一的忐忑,就是那个电池能用多久。想起来专家跟我说过,Wi-Fi 模块比较费电,恐怕难以为继。我睡前就在思谋这个事情,如果一节电池可以撑很久,我以后就用电池方案了。

然后我就想算个账,一个电池大概能用多久。已知,Wi-Fi 模块待机 0.1W,Wi-Fi 芯片的电压 3.3V,电池设计容量是 400mAh。根据公式 Q = It,电量等于电流乘以时间,再根据功率公式 W = IV,可以算出来待机电流是 30mA 左右,那么理论待机时间是 400mAh 除以 30mA,才 13个小时!

算出来这个又阴云密布了,早上 9 点我起床一看,已经没电了,毫不意外,因为就算达到理论值,也只有 13 小时,我还没算电路和服务器相连心跳的耗电,还有之前点亮后,反复配对几次的耗电,撑到早上没电,实属正常啊。

搞了半天还是要解决供电。

后来我去淘宝买了一个 12V 的直流电源插头,是以前斐讯盒子的电源转换器,然后又买了10米的延长线,从我的弱电箱一直延伸到门口的室内话机里面,用电烙铁在话机的外壳上烫了个洞,把电线伸进去,直接从交流转直流给这个智能模块供电,总算是全部搞定了。

[embed]https://v.youku.com/v_show/id_XNDMwNTgwMTQzMg==.html[/embed]

软件部分,智能助手的连接

整体解决方案完整原理图

折腾了很久,总算全部搞定了,最后在家里走了5米多的明线有点不够完美。不过第一次从调研,设计到最后实现,能完全搞定,并且稳定可靠,我已经很满意了。

一直没有提及软件部分,这里简单提一下。我买的这个开关,用的 Wi-Fi 芯片,是圈里非常流行的芯片,很多 IoT 设备都是用的这款,这家开关集成的芯片背后的厂商叫易微联,也生产很多其他智能设备。我只是把一个智能门禁开关改造了一下和家里的门禁室内机相连了。

然后,很惊喜发现,易微联作为三方厂商,和小米的米家智能也可以联合,就赶快把这个开关绑定到了米家。虽然在米家里没法直接操作,但是可以通过语音来操控,我可以喊“小爱同学!打开门铃开关”,这样,就可以响应门铃,自动开门了,以后人在家的时候,只要喊一声就可以了,不用跑过去门口开门。

而从外面回来,可以用手机遥控给自己开门。非常方便。

总结,以及还有什么可能

这次的项目完结,我感觉是对我这么多年来知道的各种电子,计算机,软件知识的一次大盘活,还有动手能力,设计能力等等,非常有成就感。

中间我做的过程种,给我支招的那个高手同事,听了我的想法,他自己也心动想搞一个,不过最后他用了跟我不同的方案,一看他就更有经验一点。他买了一个 433 遥控弱电继电器(整套方案 20 以内)。433 本质上是一个射频的频率,433MHz,常见的遥控器使用的频段,比如遥控车库门,单位会议室的遥控投影幕布,遥控电风扇之类的,可能是红外的,也可能就是这种 433 的。

这个模块非常小,比一元硬币还小,相应的其功耗也极其低,不支持 Wi-Fi 和智能,这带来的好处是,一节 9V 电池,可以供电几个月到半年。而且方案简单可靠。很难坏掉。也不担心停电。

怎么解决 Wi-Fi 控制呢,可以用一个树莓派之类的小型机器来遥控那个 433 模块的遥控器。这样,也可以解决智能的问题,后来他买到了一个可以和天猫精灵结合的设备,通过天猫精灵控制,也可以语音控制。

如果计算完整功能的成本来看,我的整套方案在 130 左右,他的方案算上天猫精灵的部分也差不多 100 多,成本差距不大。我的方案集成度高一点,但是没有他的方案简洁。他把遥控和智能拆解成两部分来实现,遥控那一侧尽可能简单可靠,低功耗。智能那一侧就有各种办法。各有所长。不过我个人反思一下,可能他的方案更好一点。毕竟是专家,经验丰富了。

因为老板想搞 K8S,但是我连 Docker 都不懂,就觉得还是要学一点点 Docker 的,之前还是看了一点点的,甚至折腾过一个开发环境的方案,但是,很长时间不弄了以后,就全都还回去了。

这次我又想自己搭建一个基于 Docker 的开发环境,以前只是把 Docker 当成一个易于分发的开发环境来思考,所以,我记得以前费了很大的力气,做出了一个单一的 image,把 PHP + nginx + Redis + Memcahed 全部压到一个 image 里面了,然后用 Volume 映射代码,MySQL 连接本地网络公用的实例,形成一个开箱即用的开发环境。

这次,因为稍微看了点编排的概念,开始纠结,这些东西真的应该压到一个 image 里面么?为啥不是多个 docker image,然后,怎么想办法编排一下子?不是传说有个最优实践是一个 container 里面只放一个服务嘛?

第一个纠结的就是 nginx 和 PHP 到底应该放在一个 image 里面还是不同的 image 里面呢?

网上搜了一下,发现还是有不少文章讲 nginx 和 PHP 分开放无法访问的问题。看来,显然有人做过尝试了,而且遇到了问题。就看看他们遇到了什么问题。经过一番分析,我感觉我想明白了这个事情,到底应该放在一起,还是分开放。

分开或者合并的原理

其实,经常配置 nginx 和 PHP 的话,就会知道,这俩在原理上,分开和合并都是完全可能的。而且,从提供的接口层面,我们看不出来到底鼓励怎么做。

常见的配置方法是,使用 fastcgi 的方式来配合 nginx 和 PHP,我这两年的经验,用 debian 的 apt-get 安装的默认配置看,nginx 和 PHP 的连接方式,是用的 UNIX sock 文件,这种情况下显然是必须在一台机器上了。不过,显然 fastcgi 是支持 TCP 协议的,就是大家很熟悉的 9000 端口,流行的配置文件都是 tcp://127.0.0.1:9000 这样的编写方式。这个本地 IP 地址,看起来也是部署在一台机器的。

不过呢,既然支持 TCP,就必然可以分布在不同的机器上面,原理上完全成立的。

网上流行的问题是什么?

那么那些把 nginx 和 PHP 放到不同 image 的同学遇到了什么问题呢?其实,是路径问题。

其实,我想,因为部署在一起的方式太过于流行了(可能的根本原因是互联网的绝大部分网站的规模很小,都在单台服务器上),以至于很多人没有注意过路径这个问题。

nginx 是一个服务器应用程序,每次要伺服的时候,都要从一个文件根目录出发,寻找需要伺服的文件路径。而 PHP 的 FPM 进程,也是一个服务器应用程序,它也有一个问题,就是需要从一个文件根目录出发,去寻找需要解释的文件路径。

因为最为流行的部署方式是放在一起的,往往也包含了静态文件和动态文件部署在一起的问题(前后端不分离是更为流行的做法),所以,用到的文件根目录,都是在一起的,所以,很显然,如果分开部署 nginx 和 PHP 的话,一定会遇到文件路径寻址的问题。

nginx 配置文件里,会用 root 变量指定一个 server 寻址的根目录,合并部署的时候,和 PHP 的根目录是一样的,用 document_root 变量(就是 root 的别名)传递给 fastcgi,但是,分开部署的时候,一个 server 的 root 变量,指的 nginx 所在的计算机的路径,但是 fastcgi 需要使用的 SCRIPT_FILENAME 参量,里面的路径,要用的是 PHP 所在的计算机的路径。既然是两台计算机,路径可以吻合,也可以不吻合,所以,分开部署的话,还能正确使用,是有一定概率的。你怎么知道 nginx 的 image 和 PHP 的 image 正好基于一个发型版?在 Docker 的世界下,两个 image 来自天南海北的两个人制作的可能性很高。

怎么解决路径问题?

要说怎么解决这个问题,其实,说到这里,知道了原理,就非常好解决,梳理好两个服务器程序应该使用的路径参数就好了。

document_root 这个变量,一般会继承 server 段落的 root 变量的配置,或者 http 段落的 root 的配置。如果这个 root 和 PHP 所在的机器,驴唇不对马嘴,那么可以猜测一定跑不起来。

解决方法是,把 PHP 所在机器的 root 在 location 段落里重新设定。或者,设置 SCRIPT_FILENAME 这个 fastcgi_param 的时候,用绝对路径直接写,不要用 $document_root$script_name 这种变量的写法。

然而,像我这么纠结的人,还是很不满意的,因为这种写法让我觉得恶心。为什么呢?因为耦合了。

nginx 在一台机器上,以服务的面貌提供自己的服务,而 PHP 在另一台机器上,也以服务的面貌提供自己的服务。但是,如果 nginx 的配置,必须知道 PHP 那台机器的文件路径,我想,这就是它知道了它不应该知道的事实,这就是耦合,这就是丑陋。

其实,nginx 作为一个服务,从客户端那里得到了 script_name,当然,它自己解释不了,也不拥有这个文件,所以,用 fastcgi 把 script_name 传递给 PHP 所在的服务就行了。这是最最必要的操作了。能不能不用搞清楚 PHP 所在的计算机的路径呢?当然可以,只要使用相对路径就行了。

那就需要 PHP 的 fastcgi 启动的时候,知道自己的根目录在什么地方,然后传过来相对路径,都可以自己找到正确的位置,从而解决了一个耦合。PHP 的 FPM 当然可以这么配置,只是因为一起部署的缺省配置太过流行,咱们从没注意过这个可能性而已。

到底应该放在一个 image 里还是分开?

答案是:视情况而定。(KAO!跟没说一样)

其实,PHP 的 FPM 是支持一个叫 pool 的特性的,我们可以在一个 pool 里面通过 chroot 和 chdir 之类的特性来把访问限制在一个特定的路径里,就是代码所在的根目录。

但是,那样的话,如果你一台机器上有多个网站的源代码,你就必须把根路径指向多个网站的共同根目录,不然的话,PHP 就只能伺服其中一个。

我们知道,世界上绝大多数网站的规模很小,所以,一台 Linux 可以同时支持很多网站的使用,所以,绝大多数缺省配置,FPM 只配置了一个 pool。这种情况下,nginx 传递相对路径的时候,必须加一个网站名的前缀。懂道理的话,会很简单啦,怎么都不会搞混。但是,显然增加了这套架构的学习成本,不是每个人都能很快搞那么明白的。

所以,详细回答一下“到底应该放一个 image 还是分开?”这个问题。

如果,你只是在本地,做一个给自己用的开发环境,我强烈建议放在一个 image 里面。一个程序员,往往会开发 N 多个网站的代码,放在一个里面,最省资源。配置也最为熟悉和简单,网上随手一搜,搜出来的配置很大概率可以部署成功。

如果,在线上环境,部署一个流量弹性范围很广,或者增长可能性很高的服务的时候,分开部署的优势比较大。因为,nginx 的性能是非常好的,远远好于 PHP。分开部署后,PHP 的 FPM 进程不够用了以后,可以不断扩容,增加 container 数量就行了。但是,这种方案的话,学习成本较高,需要程序员对这几个服务的配置有比较深的理解,就算自动扩容,执行动作感觉也不是单纯增加一个 container 就行的,毕竟一个 container 就有一个入口 IP,还要把扩容出来的入口 IP 告诉 nginx 所在的 container。

结论

其实吧,最流行的方案,恰恰是最正确的方案。比如,你可以直接下载到 LNMP 完备的 image,这种东西需求量最大,所以最流行。因为都是单个程序员用来解决自己开发环境的。就算拿去用在生产,问题也不大,小流量的服务和网站,才是这个世界的主流。不过想明白为什么是这个样子,就要花点心思。

在运维和管理 Linux 服务器的时候,我们最常用的一个命令就是 netstat,我常用这个命令来查看当前服务器上有哪些进程正在侦听端口,主要用来诊断网络服务的工作状态。

不过,最近有一次安装好一个 Ubuntu 发型版,发现默认没有安装 netstat,觉得非常奇怪,自己手动安装后,发现 man pages 提示,netstat 命令已经是 deprecated 了,建议使用 ss 命令代替。

This program is mostly obsolete. Replacement for netstat is ss. Replacement for netstat -r is ip route. Replacement for netstat -i is ip -s link. Replacement for netstat -g is ip maddr.

netstat man pages

netstat 的用法

netstat 有许多许多参数,我一般就用一种组合,以至于后来已经想不起来为什么是这几个参数了:

1
netstat -npl

得到的结果是这样的:

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
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 655/systemd-resolve
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 890/sshd
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN 30790/cupsd
tcp 0 0 0.0.0.0:18025 0.0.0.0:* LISTEN 890/sshd
tcp6 0 0 :::22 :::* LISTEN 890/sshd
tcp6 0 0 ::1:631 :::* LISTEN 30790/cupsd
tcp6 0 0 :::9090 :::* LISTEN 15415/./prometheus
tcp6 0 0 :::18025 :::* LISTEN 890/sshd
udp 0 0 127.0.0.53:53 0.0.0.0:* 655/systemd-resolve
udp 0 0 0.0.0.0:631 0.0.0.0:* 30792/cups-browsed
udp 0 0 0.0.0.0:5353 0.0.0.0:* 757/avahi-daemon: r
udp 0 0 0.0.0.0:42360 0.0.0.0:* 757/avahi-daemon: r
udp6 0 0 :::58232 :::* 757/avahi-daemon: r
udp6 0 0 :::5353 :::* 757/avahi-daemon: r
Active UNIX domain sockets (only servers)
Proto RefCnt Flags Type State I-Node PID/Program name Path
unix 2 [ ACC ] STREAM LISTENING 35116 1304/gnome-session- @/tmp/.ICE-unix/1304
unix 2 [ ACC ] SEQPACKET LISTENING 1448 1/init /run/udev/control
unix 2 [ ACC ] STREAM LISTENING 34277 1270/systemd /run/user/1000/systemd/private
unix 2 [ ACC ] STREAM LISTENING 34282 1270/systemd /run/user/1000/gnupg/S.gpg-agent.ssh
unix 2 [ ACC ] STREAM LISTENING 33510 1270/systemd /run/user/1000/gnupg/S.gpg-agent
unix 2 [ ACC ] STREAM LISTENING 33511 1270/systemd /run/user/1000/pulse/native
unix 2 [ ACC ] STREAM LISTENING 33512 1270/systemd /run/user/1000/gnupg/S.gpg-agent.extra

最常用的就是这个命令组合,展示的结果有两个段落,第一个段落展示的是 TCP/UDP 协议的侦听情况,第二个段落展示的是 socks 文件的侦听情况。参数 n 的意思是展示数字格式的 IP 地址,不然会展示主机名称或者是域名,参数 p 的意思显示进程的名字(有时候显示不出来),l 的意思,是关注处于 LISTENING 状态的 socket。

通过如上命令,我们看到了系统所有打开的 socket,如果你启动一种网络服务也好,自己开发一个网络服务打开端口也好,通过这个命令都应该能看到自己打开的端口,如果看不到,应该就是没有能够正确打开端口,要好好查询是什么原因。所以这是一个很好用的调试命令。

ss 的用法

上面介绍了 netstat 的最最基本的一种用法,其他用法当然还有很多,但是先略过不表,如果想使用 ss 命令来代替 netstat 的话,我们怎样达到类似的效果呢?

1
ss -atlp

这是我自己摸索的一个参数组合,目前我背诵得还不是很流利,每次还需要看一下文档:

1
2
3
4
5
6
7
8
9
State          Recv-Q           Send-Q                      Local Address:Port                       Peer Address:Port
LISTEN 0 128 127.0.0.53%lo:domain 0.0.0.0:* users:(("systemd-resolve",pid=655,fd=13))
LISTEN 0 128 0.0.0.0:ssh 0.0.0.0:* users:(("sshd",pid=890,fd=5))
LISTEN 0 5 127.0.0.1:ipp 0.0.0.0:* users:(("cupsd",pid=30790,fd=7))
LISTEN 0 128 0.0.0.0:18025 0.0.0.0:* users:(("sshd",pid=890,fd=3))
LISTEN 0 128 [::]:ssh [::]:* users:(("sshd",pid=890,fd=6))
LISTEN 0 5 [::1]:ipp [::]:* users:(("cupsd",pid=30790,fd=6))
LISTEN 0 128 *:9090 *:* users:(("prometheus",pid=15415,fd=3))
LISTEN 0 128 [::]:18025 [::]:* users:(("sshd",pid=890,fd=4))

这是 ss 命令呈现出来的结果,可以看到,格式和 netstat 很不一样,不像 netstat 命令那么紧凑和直观。这是很多人诟病这个命令的原因之一。当然,批判这种批判的声音认为,人们只是死守了一种习惯,不愿前行。当然了,这么说也未尝不对,就拿 Charles 个人来说,就算我 2010 年参加工作,才学会 netstat 命令,那我到现在也使用了将近十年,从来没有变过,当然看得无比顺眼啦。

当然,也有一种理由是老外提出来的,说 ss 这个命令的名字不好,其实 ss 可能是 socket statistics 的意思,缩写以后,竟然只有两个字母,不太好联想,不像 netstat 那么直观。当然这是我的解释,不是老外抱怨的理由,他们抱怨的是,每每提及 ss,他们会联想起希特勒!是不是匪夷所思,我是 80 后,我这个年代的人,对这个都没有什么印象,关键我们用中文为主,估计大家看到 ss 最多联想到梯子,怎么都不会想到希特勒。这个大纳粹有一个武装部队,以前叫党卫队特别机动部队,后来改名叫武装党卫队。它的德语简称正是SS。

不说闲话了,说说几个参数,a 参数是显示所有的意思,t 参数意思是显示 TCP 协议的,l 代表正在 LISTENING 状态的,p 代表进程信息。从上面的表里,我们看到 p 参数打印的信息,组织得不如 netstat 精炼。但是更为完善一点,显示了进程名字和 PID 以及 FD。但是因为用了两重小括号,key/value 的格式,再加引号,看起来脏乱差。当然,我们可以用一些命令去格式化它,不过还是太麻烦了。

更换的原因是什么?

这可能是我最为好奇的事情。不过网上我搜索了不少的资料,基本都语焉不详。这也有点让我有点无奈。

大体上,我们能看出来,主要是 net-tools 这个包,将要被 iproute 这个包给替换。理由大概是,1,这个包太老了,2,这个包不支持很多内核新的特性(但是没有说是哪些特性),界面不够优化使用困难(对命令行不友好),3,net-tools 里面的 ifconfig 确实缺点多多,4,未来不再想维护 net-tools 了。

Luk Claes and me, as the current maintainers of net-tools, we’ve been thinking about it’s future. Net-tools has been a core part of Debian and any other linux based distro for many years, but it’s showing its age.
It doesnt support many of the modern features of the linux kernel, the interface is far from optimal and difficult to use in automatisation, and also, it hasn’t got much love in the last years.
On the other side, the iproute suite, introduced around the 2.2 kernel line, has both a much better and consistent interface, is more powerful, and is almost ten years old, so nobody would say it’s untested.
Hence, our plans are to replace net-tools completely with iproute, maybe leading the route for other distributions to follow. Of course, most people and tools use and remember the venerable old interface, so the first step would be to write wrappers, trying to be compatible with net-tools.
At the same time, we believe that most packages using net-tools should be patched to use iproute instead, while others can continue using the wrappers for some time. The ifupdown package is obviously the first candidate, but it seems that a version using iproute has been available in experimental since 2007.

https://serverfault.com/questions/633087/where-is-the-statement-of-deprecation-of-ifconfig-on-linux

也有从原理层面分析的:现在的 netstat 和 ifconfig 命令,都是通过读写 /proc 目录下的虚拟文件来完成任务的,这个东西在小型业务系统上,是没问题的,但是在大规模系统里,可能会伤害系统的性能之类的。相比之下,ss 和 ip 两个命令,使用的是 Linux 内核的 netlink sockets 特性。有着根本上的不同。虽然,老命令也可以用新原理重写,但是其实并没有人那么做,主要因为不同程序员团体的一些 political issues ,大家意见不合……

当然,深层次的还有,我们使用这样的调试命令,本质上还是希望获知内核的状态的,其实,内核已经改变了 networking 模块的整个原理,另一方面我还要求命令像从前那样去展示信息,展示层面的格式和真实原理已经背离,所以,从长远看,替代这两个命令才是必然。

结论

咱们这些做技术的,也还是要与时俱进比较好,虽然,以前的那些命令熟悉,好用,手到擒来,甚至无法忘记,但是新的还是要保持学习。很多发型版已经默认不带有 net-tools 包了,虽然仍然可以手动安装回来,但是,这背后的态度已经很明确了。另一方面,我们做技术,也要谨防自己的大脑僵化,还是要保持对新事物的好奇心和热情。

Algorithm

这次跟大家分享的题目是我在练习使用回溯法时候的一道练习题:

给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。

例如,给出 n = 3,生成结果为:

1
2
3
4
5
6
7
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]

以下是我的解法:

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
func generateParenthesis(num int) []string {

result := []string{}

//约束条件:左括号的数量,消耗快于右括号的消耗数量
//结束条件:右括号数量消耗完毕
//单步前进:消耗一个左括号或者右括号
var put func(left int, right int, str string)

put = func(left int, right int, str string) {
if right == 0 {
result = append(result, str)
} else {
if left > 0 {
put(left - 1, right, str + "(")
}
if right > left {
put(left, right - 1, str + ")")
}
}
}

put(num, num, "")
return result
}

因为是回溯法的练习题,所以,一开始我就知道了应该用回溯法去解这道题目。回溯法是一种很基础的算法,经过一周的练习,我感觉,回溯法本质上就是一种穷举法的应用。一般,我们可能想到的穷举法,都是循环,两重循环,有两个下标,可以按部就班,没有重复和遗漏地从头穷举到尾。这种类型的穷举法,我们光凭直觉就可以写出来,并且写正确,能不能跑出来,就不一定了,因为效率问题嘛~

回溯法的穷举是另一种类型,就是我们可能很直观想到怎么去穷举这种问题,但是很难用代码去表达出来。需要一个关键的数据结构的帮助,我们才能有效编写算法。最经典的例题,其实就是“八皇后问题”。在一个国际象棋棋盘上,放上八个皇后,不能互相攻击。给出一种解法,或者要求给出所有解法。

直觉就是,现在第一行第一格放一个皇后,然后在第二行找个位子放第二个,以此类推,直到所有八个皇后都放上去了,如果放到一半发现所有的格子都不能放了,那么上一个可能放错了,把上一个皇后换到下一个可以用的格子,继续按照原有策略试,以此类推,不断修改上一个皇后的位置,上一个的上一个的位置,一直到找到一个正确的摆法,也可以一直到找到所有的摆法。

这个思路,描述起来很简单,但是如果用代码去写,我相信对于很多同学来说,还是非常困难的。以前,我对于自己写不出八皇后这个问题的代码这件事情,一直很自责,对自己的智商感到抱歉。不过,现在,经过学习我发现,其实只是因为我没有认真去学习回溯法,以及没有认真训练,所以,我不能掌握这种方法。我不应该为自己不是最顶尖聪明的人类而感到抱歉,我只应该为自己没有早一点开始训练,感到愧疚。

这种类型的题目,就是我说的,不是那么显然地能找到按部就班没有重复和遗漏的迭代变量的题目。主要的一个原因就是,很多时候,循环的层数可能是不确定的,给代码编写带来了很大的困难。比如,“八皇后问题”,经常被写成“N皇后问题”,八皇后你可以不厌其烦写八重循环,那么N皇后怎么个写法?

前面提到的很关键的那个数据结构,就是栈。先进后出的线性数据结构。这种数据结构的长度其实是可变的。我们只要每穷举一个步骤,就把此步骤的相关数据都保存在栈里,然后下一个,出了问题,就弹出上一个,这个过程是可以重复的,所以我们就有了能力穷举尝试完所有的步骤。与上面直观感觉描述的解法相吻合了。

说起来,回溯法的核心关键,正是去使用栈。每探索一个步骤,就用栈记住一切,然后,继续下一个步骤。栈这个东西,跟另一个程序的核心特性结合很紧密,于是往往可以让我们把代码表达得很简洁。那个特性就是让人又爱又恨的“递归”。

我想,每个程序员,在学习递归的时候,都掌握过这个知识点,就是函数可以自己调用自己,形成递归,那么上一重没有完成的函数,怎么办呢,它的所有信息保存在栈上,直到下一重返回。结合回溯法,如果利用递归来使用栈,就可以把代码表达得很简洁和优美。因为递归方法使用栈,都是隐性的,可以节省大量的代码。让我们专注在算法逻辑地撰写上面,而不是去处理压栈弹出等数据结构操作。

如果大家了解深度优先搜索,就会发现,其实这个压栈的处理,也很像深度优先搜索,所以,有些人写代码,经常把回溯法的主方法取名为 dfs,就是这个原因。我们都知道,深度优先搜索,是用在图上面的搜索算法,回溯法用到的题目,其实也是一种图,只不过是一种隐式的图,图的节点和边的定义没有明确给出,扩展的规则隐含在题目里的一种图。所以,回溯法有时候等同于深度优先搜索,也不奇怪了。

上面我写的括号生成的解法,是用 Go 语言实现的,也是偶然得到的一种灵感,写得非常简洁,思路也很清晰,所以特此分享给大家。以后我也想写一个文章,专门介绍,怎么理解回溯法,以及怎么在不同的题目里面去运用,怎么在最差情况下发挥得还不错。:)

Review

本期想给大家分享的文章是:《An introduction to RabbitMQ, a broker that deals in messages

broker 字典里是经纪人的意思,其实也可以叫中介,或者中间商,消息队列本质上就是处理消息的一种中间商,这是套用了基本经济学里面的概念。

消息队列中间件,就像是一种专门处理系统间消息的中间商或者说中介。RabbitMQ 就是这样一种中间件。它实现了很多消息队列的协议,最重要的一种就是 AMQP,也就是高级消息队列协议。这种协议的概念模型关注三种实体,队列,绑定,和交换。

AMQP Models

接着文章逐一介绍了中间件系统的一些重要实体,比如发布者,消费者,绑定和交换的一些形式,比较有趣的就是 Topic 话题模式。

如果不大了解复杂的消息中间件系统,这是一篇很好的入门文章。

Tip

本周要分享的一个 Tip 是打造好自己的工作环境,看到了一篇文章,介绍了如何自定义自己的 Shell 环境,觉得非常有趣:

如何把你的终端主题改成任何你想要的样式

这篇文章介绍了如何在 Mac 上安装一个叫 Powerlevel9K 的软件,来把 zsh 和 iTerm 打造成非常赏心悦目的样子。如果还不了解什么是 zsh 和 iTerm 的话,那么我也极力推荐大家试一试。

Share

这次分享给大家的是一篇短小的知识:

Linux 系统资源管理:什么是 cgroups?

综述

CGroups 是 Control Groups 的缩写,是 Linux 内核的一种机制,可以分配限制监控一组任务(进程)使用的物理资源(CPU、内存、磁盘I/O、带宽或这几种资源的组合)。

这里有几个基本概念需要知道:

  • 任务(Task)—— 一个系统进程在这个机制的语境下,称为一个任务。
  • 控制组(Control Group)—— 控制组就是一组按照某种规则分组的进程,并且关联了一组参数或者限制。控制组是层次结构化的,一个控制组可以从自己的父节点继承属性。
  • 子系统(Subsystem)—— 也叫做资源控制器,或者控制器。是一种单一资源的抽象,比如 CPU 时间,或者内存等。
  • 层次结构(Hirerarchy)—— 一种层次化的结构,可以用来附着(attach)一些子系统。因为控制组是层次结构化的,它们所形成的一棵树称作一个层次结构。

CGroups 提供了一个虚拟文件系统 /proc/cgroup,作为交互的接口,用于设置和管理各个子系统。本质上来说,CGroups 是内核附加在程序上的一系列钩子(Hooks),通过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的。[7]

CGroups 发展的时间线

核心特性

  • 资源限制 —— 一组进程不可超过内存的使用限制,包括文件系统的 Cache
  • 优先级 —— 一组进程相对可以获得较大的 CPU 时间和 I/O 占用
  • 审计 —— 衡量一组任务的资源利用,比如,这种数据可以用来计费
  • 控制 —— 冻结或者恢复一组进程

几个概念间的关系

一个层次结构,可以附着一个或者多个子系统(来源 RedHat)

一个子系统不能附着第二个已经附着过子系统的层次结构

一个任务不能是同一个层次结构下的不同控制组的成员

fork 出来的进程严格继承父进程的控制组

CGroups 的基本使用

在实践中,系统管理员会用 CGroups 干类似这些事情[8]:

  • 隔离一个进程集合(比如:nginx的所有进程),并限制他们所消费的资源,比如绑定CPU的核。
  • 为这组进程 分配其足够使用的内存
  • 为这组进程分配相应的网络带宽和磁盘存储限制
  • 限制访问某些设备(通过设置设备的白名单)

因为是一个 /proc 目录下的伪文件系统,我们总是可以用 Shell 的命令来操作和管理 CGroups,但是更简单的办法是安装一个 libcgroup 的包[6],它提供了好几个命令行工具和相关的文档。这个包里面提供一个 cgconfig 的服务,以及 /etc/cgconfig.conf 配置文件的通过系统运维脚本 service 就可以有效实现层次结构的创建和子系统的附着,以及参数设置等工作,非常方便。具体可以参阅 RHEL 6 的指南。

CGroups 的实现方式

Linux 内核中关于 CGroups 的源码结构示意图[7]

上面是一幅 Linux 内核源码中,跟 CGroups 有关的数据结构的关系图。task_struct 结构体就是描述进程的数据结构,里面通过一个指针 cgoups 关联到一个辅助的数据结构叫 css_set,css_set 又通过一个辅助的数据结构叫 cg_cgroup_link 关联到 cgroup,至此完成了进程和 cgroup 的映射。因为一个进程可以属于多个 cgroup(必须分属不同的 Hirerarchy),一个 cgroup 也可以关联多个进程,所以,cg_cgroup_link 就是一个处理多对多关系的“表”。

cgroup 里面有 sibling,children,parent 指针,显示 cgroup 结构体,本质上是一个“树”的节点(node)数据结构。所以,cgroup 是树形的结构。有一个 root 指针,指向了树的根 cgroupfs_root。树根,其实就是我们说的“层次结构”(Hirerarchy)。

cgroup_subsys 就是我们说的“子系统”的数据结构。这是一个抽象数据结构,需要被各个子系统去分别实现,所以这里包含了很多函数指针(如 attach)。cgroup_subsys_state 存储了一些各个子系统共用的元数据。各个子系统各自的结构体,按照自己的特点再来定义各自的控制信息结构体。参考[7]

从逻辑层面看 CGroups 的内核数据结构[10]

上图引自美团技术博客的一篇文章,从逻辑结构层面,描述了进程、子系统、群组和层次结构的关系。见参考文献[10]。

结语

CGroups 是内核提供的操作系统层面的虚拟化关键技术。是 Docker 这类杀手级应用的理论基础。本文只是对作者学习此概念时候看到的一系列文章的内容进行了编纂和综述,尝试从一个门外汉的角度去对该项技术形成一定的理解。如需细致学习,还请以参考文献中的文章内容为标准。

通过研究和学习这些资料,总结出一个基本判断就是,Linux 内核提供了比较完备的虚拟化技术,即使没有 Docker 这样的系统化的应用技术,我们也可以实现对一些机器资源的灵活管理。内核本来就提供了这样的工具。通过学习完全是可以掌握的。但是,Docker 这类技术,则提供了更为友好,高效的操作接口,极大提升了工程效率,降低了学习难度,更值得在生产中推广。

参考文献:

  1. https://lwn.net/Articles/199643/ Rohit Setch 提交 patch
  2. https://lwn.net/Articles/236038/ Paul Menage 接手容器的开发
  3. https://en.wikipedia.org/wiki/Cgroups Wiki:cgroups
  4. https://www.kernel.org/doc/Documentation/cgroup-v1/cgroups.txt
  5. https://www.kernel.org/doc/Documentation/cgroup-v2.txt
  6. RHEL 6 资源管理指南
  7. Docker背后的内核知识——CGroups资源限制
  8. Docker基础技术——Linux CGroups
  9. CGroup 的介绍、应用实例和原理描述
  10. Linux资源管理之cgroups简介

unpreview

左耳朵耗子,陈皓发起的 ARTS 打卡活动

Algorithm

给定一个排序数组,你需要在**原地**删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在**原地修改输入数组**并在使用 O(1) 额外空间的条件下完成。

示例 1:

给定数组 nums = [1,1,2], 函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。 你不需要考虑数组中超出新长度后面的元素。

示例 2:

给定 nums = [0,0,1,1,1,2,2,3,3,4],函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。你不需要考虑数组中超出新长度后面的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func removeDuplicates(nums []int) int {

l := len(nums)

if l == 0 l == 1 {
return l
}

i := 0

for j := 1; j < l; j ++ {
if nums[j] == nums[i] {
continue
} else {
nums[i + 1] = nums[j]
i ++
}
}

return i + 1

}

这道题目是一道简单的题目,正在恢复对算法的训练,所以从简单的开始吧。以上答案是用 Go 语言完成的,我估计已经是我第四遍或者第五遍的代码了。我之前肯定刷过这道题目,但是今天仍然没有非常流畅自然地写对。还是写错了一次,然后我擦掉重写,发现这次写得比较简洁了。

思路很简单,第一个下标指向第一个位置,然后第二个下标从第二个位置开始起遍历,发现一样就继续往后跳,发现不一样,就把目标拷贝到第一个下标后面一个位置。直到第二个下标遍历完。

第一个下标 i 指向的元素,以及下标小于 i 的元素,是已经去重的。先看初始状态,i 指向 0,第一个元素,显然 i 以及 i 之前所有的元素都是去重的,因为一共只有 1 个元素,必然不会重复。

然后看一下循环,每一轮循环结束,如果 j 指向的元素,与 i 指向的相同,就把下标 j 往后 +1,如果不同,则把 j 指向的元素复制到 i + 1 指向的元素,而下标 i 也向后挪一个,如此循环结束后,仍然满足 i 以及 i 之前的元素都是不重复的。

循环结束后,i 正好指向着不重复的最后一个元素,其实每一轮结束后,i 都指向着不重复的最后一个元素。所以,最后不重复的元素的个数就是 i + 1 个。

以上使用循环不变式的方式分析了算法的正确性这个算法的时间复杂度是 O(n)。

Review

Avoiding Double Payments in a Distributed Payments System

本周 review 的文章是这一篇《在分布式系统中避免双重支付问题》,这是 Airbnb 技术博客本周的封面文章。介绍了 Airbnb 支付团队在整个系统向 SOA 架构迁移的过程中,如何构建分布式的支付系统。

他们构建了一个类库,名叫 Orpheus 这是古希腊的一个神“俄尔浦斯” 的名字。这个类库主要原理,是利用 Java 的 lamda 演算,封装了一个严格要求幂等性的类库,将一次分布式事务拆分成 Pre-RPC,RPC 和 Post-RPC 三个阶段,将分布式事务中,本地数据库事务和 RPC 分隔,并在强幂等性要求下工作,从而保障系统的最终一致性。

文章介绍了这么实现的原因和带有的问题,以及团队如此选择的 trade-off。是一篇高质量的设计思想文章,衷心向大家推荐。

Tip

想给大家分享的一个技巧点,就是在使用 Mac 的时候,经常需要使用 XCode 的命令行工具,比如 homebrew,比如我现在用 Visual Studio Code 做 Go 语言的 IDE,都需要用到这个命令行工具。怎么安装呢?

1
xcode-select --install

在 Mac 的 Shell 上执行上述命令,就可以激活 XCode 的 command line tools 的安装过程了。

Share

这个礼拜分享的文章,是眼前刚遇到的一个问题,虽然不是技术文章,但是也跟技术人息息相关。

《技术人走上管理岗位的困惑》

[ez-toc]

今天,老板跟我说,有一个年轻的同事要离职了,我听了很痛心,他是一个很不错的小伙子,老板对他寄予了很高的期望,他从我的团队调到了另一个研发团队,过去的目的是抓住那个团队的业务,把整个团队管理起来,但是实际上,他并没有达到预期,自己也待得很不爽,所以想要离开公司了。

我反思了一下,这本质上管理者的责任,没有能够有效地培养和引导,没有给人才建立有效的成长阶梯导致了人才的流失,非常遗憾。技术人的晋升和成长,或者说,不光是技术人,其他的岗位可能也是雷同的,应该说,专业型领导者的晋升和培养,是一个系统性的难题。这是需要管理者和候选人本人的双重努力才能完成的一个艰难蜕变。

管理意识问题

管理意识问题,可能是技术人晋升的最大障碍之一,我看到不少人因为这个问题,停留在原地无法寸进。

管理价值的迷思。很多技术人觉得,我就是一个专家,管理这种事情,充满了繁琐,不是我所喜欢的,甚至是我所厌恶的。所以,他就很少投入精力在这个领域,而是投入到自己喜爱的领域去。

其实,就我个人看来,技术人,特指程序员这个岗位,大体上有这么几个发展方向:技术专家,业务专家,综合管理。技术专家可能是大多数程序员所向往的一个发展方向,也是一条光荣的荆棘路,可以说,是非常困难的一条道路。不过呢,有问题的地方在于,虽然大多数人喜欢,但是,大多数人都不知道如何成为技术专家,以及想要成为技术专家的话,需要满足怎样的成长轨迹。成长轨迹,其实包含了方向和速度两个层面。方向层面,各有所好,容易忽略的是成长速度的问题。这个也是这条道路特别困难的原因,我见过不少工作了8年,10年,最后水平连高级程序员都算不上的人,非常可惜,只能是处在一个压力很大的境地了。

业务专家的话,是一条相对比较普遍的道路,如果技术人能够觉醒这个意识的话,是非常有利的。我们的世界错综复杂,其实需要海量的业务专家来推动整个社会的发展和进步,所以这种类型的技术人,永远都会有一碗饭吃,只要觉醒了这个意识,认真建立自己的领域知识体系,很容易建立竞争壁垒。遗憾的是,注意到这个问题的人比较少。

最后是综合管理,可能就是一般人嘴里说的“技术转管理”的那个“管理”。这样的人,技术有一定深度,业务比较精熟,最后,真正的特长就是掌握了管理能力。其实,这个领域是我认为的真正蓝海。因为明白这个道理的人实在太少了,又有很多同学很轻视这个方面,就更是留下了广阔的空间。未来这个地方的缺口一定是很大的。这个方向上,常见的问题,就是找不到管理的价值,无法正确理解管理的本质。

就我的理解来说,管理是一个很高级的技能。现在的世界越来越复杂,如果我们想要构建更大规模的系统,光靠一些散兵游勇是做不到的,一定要靠有能力的团队来完成。但是散兵游勇如何变成团队,就必须有高效的领导者。但是领导者是非常难以培养的,大家都是从管理去入手练习的。管理者这个角色,只是一个路径,领导者才是培养的目标。注意我特别区分了管理和领导的区别,也即 Manager 和 Leader 的区别。

成长路径的问题

这个就是管理者层面的问题了。就我们公司五年的发展轨迹看,这里一直是很严重的问题。早期业务冲刺,很难顾及到这个方面,现在这个问题很凸显,但是因为早期积累不足,也很难一蹴而就。创业公司的另一个麻烦就是,创业者自己本来可能就是基层的同学,在摸爬滚打过程中成长,也难有什么体系化,科学化的认识。意识到的时候,补起来很痛苦。所以这是很多创业公司活下来后,无法做大做强的重要瓶颈之一。

很多技术同学,走上管理岗位,都是被动的。就像我说的,公司要想在一个业务方面,有序开展生产,必须要有一个个层级的协调人,也就是基层和中层管理者。怎么找到他们呢?就是从既有的团队里拉起来一个,要求他充当这个角色。这本来是一个机遇,但是前一个章节说的,管理意识是一个很难跨越的障碍,不少同学都是因为技术过硬和业务精熟被挑出来的,但是挑出来后,他们可能对管理的价值并不由衷认同。

其实这时候,就需要团队的领导者或者管理者有意识地去培养,尝试去激发他们的管理意识,培养他们的热情,去认同这项工作的价值,然后言传身教一些具体的方法。管理职责和标准动作,都可以通过培训完成,但是怎么从管理过度到领导,就必须另一个领导者来启发和引领。这里最重要的一个环节,就是个人意识的激发和觉醒。也是我们做得最差的一个,基本都是放养,然后很多人无法完成转变,最后流失。

一旦意识觉醒以后,到底怎么继续往前,怎么去到更高的境界?这就是一个我自己也没有想清楚的问题了。目前,只能是修行在个人了。我的想法很不成熟也没法进一步去阐述了。

风险的问题

另一个制约技术人走上管理岗位的问题,就是风险问题。没有哪一家公司是长盛不衰的,也没有哪一种业务是一定可以保持增长和存活的。那么技术转了管理后,必然分出来很大一部分精力从事管理和领导的任务。这时候,其技术的成长去必然不能兼顾。再加上原来的思维定势,再次应聘的时候,往往还是退回纯技术岗位,又因为整个行业的问题都是类似的,寻找管理者和领导者的时候,往往无法鉴别,只能退回技术去找寻候选人,哪怕并不需要,于是,又变成了技术专家的面试。

其实很多公司需要的是领导者,但是面试都是挑技术专家。这很奇怪,但是现状就是如此。这种现状,给每个从事管理的人,制造了很高的门槛,不得不兼顾个人的技术成长,也是他们轻视管理的原因。就好像高考指挥棒在那里摆着,素质教育就没法开展。每个管理者都背负着巨大的降级风险,甚至失业风险。

背负这样的后顾之忧,成长就必然慢,供应就必然更加短缺,这是一个完整的负向激励闭环。非常地可惜。我自己的成长也同样置身于这种阴霾之下。

解决之道

以我目前的见识和眼光,我也看不到什么太好的办法。只能尝试去谈谈自己的看法。我觉得,这个其实是一个系统的问题,可能需要更多的人去认识到人的价值,领导者的价值,然后,为整个行业去打造这样一种环境,认同不同类型的人才的价值,才能系统性地去打开一个正向激励的循环。这里面,优秀的领军企业,应该承担好自己的社会责任。

从我自身来说,还是要在公司努力提升影响力,扩散领导力的价值。要增加跟底下一线的管理者的接触,增加言传身教的机会,让他们能够意识到这个问题,并有意识地去培养自己。同时考虑到现在整个环境的现状,还是要制造一些机会,和提供一切资源,帮助他们去对抗面临的风险问题。解决了后顾之忧,他们才能放心学习和成长。