Becomin' Charles

算法 | LNMP | Flutter | Mac

Becomin' Charles

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 的区别。

成长路径的问题

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

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

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

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

风险的问题

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

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

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

解决之道

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

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

Algorithm

Review

Tips

这次的 Tips 是关于 Linux 服务器管理的,Charles 前两周,颇费周章,把自己动的第一台 MacBook Pro,安装成了 Linux 服务器,装的发行版是 Ubuntu 19.04 Desktop 版。

一个朋友跟我说,这么激进啊,我嘿嘿一笑。谁知道,今天折腾的时候,才发现自食苦果。最近几年,明显感觉到 Linux 的系统管理发展迅猛,我们小时候玩 Linux 学会的那些知识,眨眼全部作废了,真让人不胜唏嘘啊。

这次碰到的问题是 DNS 服务地址的设定问题。在我的记忆里,就是 /etc/resolve.conf 里面写一行 nameserver 指令配置一下就可以了。现在实际试下来才知道,根本不对。我无论是把里面改写了还是新增记录,一重启解析服务,里面的东西就全部还原了。

后来,我又想起来可以在 /etc/network/interfaces 里面修改配置,写是写好了,但是完全不起作用,我用 ifdown enp3s0 && ifup enp3s0 命令重启网络,还是报错无法解析。

实在不行,去官方看文档,竟然发现 Ubuntu 19.04 的服务器管理文档根本没有发布!果然,在服务器选择方面,还是不应该太激进,还是要选可靠一点的版本。现在也不可能重装了,算了吧。再给我一次重来的机会,我一定会选择 LTS。

我只能照着 18.04 LTS 版本的文档去查找和阅读了,得知在 18.04 里面引入了 netplan (https://netplan.io/)这个套件来管理网络,号称用更加人性化的配置方式来管理整套网络了。怎么个人性化呢,就是选择了人类更容易读写的 YAML,好吧。

但是,我一尝试,发现 Desktop 版,根本没有安装过 netplan,虽然说配置文件的目录布局都有了,但是竟然软件没装,apt 来安装,发现——不能解析!气死我了:

1
2
# 观看 resolv.conf 的文档,顺藤摸瓜找到用 resolvectl 命令可以动态设置 DNS
resolvectl dns enp3s0 192.168.1.1

总算是把 DNS 给设置出来了,然后使用 apt install netplan 安装好 netplan 网络管理包。然后,我们来看看现在的 Ubuntu 的网络管理。这里发生了很多的变化,我都没法理解的,先记录在这里把。比如第一点,关于以太网设备的名字,以前我们熟悉的那套 eth0,eth1 之类的,被称作 Linux kernel 风格。现在你用 ifconfig 命令看到的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enp3s0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
inet 192.168.1.9 netmask 255.255.255.0 broadcast 192.168.1.255
inet6 fe80::cabc:c8ff:fe8d:9d3 prefixlen 64 scopeid 0x20<link>
ether c8:bc:c8:8d:09:d3 txqueuelen 1000 (Ethernet)
RX packets 29433 bytes 2466486 (2.4 MB)
RX errors 0 dropped 17848 overruns 0 frame 0
TX packets 7051 bytes 1450602 (1.4 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device interrupt 16

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 15196 bytes 1083107 (1.0 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 15196 bytes 1083107 (1.0 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

以上,我的以太网卡的名字变成了 enp3s0 ,这个名字的来源还不太懂。然后我发现,现在官方文档推荐用 ip a 命令来查看网卡了了。

1
2
3
4
5
6
7
8
9
10
11
12
13
○ → ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether c8:bc:c8:8d:09:d3 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.9/24 brd 192.168.1.255 scope global enp3s0
valid_lft forever preferred_lft forever
inet6 fe80::cabc:c8ff:fe8d:9d3/64 scope link
valid_lft forever preferred_lft forever

以上,命令的结果是这样的。[1]

在 netplan 里,配置文件要好写得多:

Share

参考文献:

  1. https://help.ubuntu.com/lts/serverguide/network-configuration.html

今天跟一个朋友约定,一起开始认真学习算法。于是翻出了买了多年,但是从未看过的《算法导论》。认认真真看起来。记得多年前,就一直在这本书上受挫,这是除了《代码大全》外,另一本归入“买了永远不会看”分类的书。我现在的想法是,我看完这本书,并认真完成所有的练习,是否肯定可以获得一定程度上的收获呢?哪怕是花去人生中的很多年,也一定要完成。

  1. 给出一个真实世界的例子,其中包含着下列的某种计算问题:排序,确定多矩阵相乘的最佳顺序,或者找出凸壳。

这是1.1节的第一个练习题,如果认真看题目,并思考的话,会发觉,这其实是一个相当困难的题目。反正我的第一个直觉就是,我不可能想出来真实世界中有什么地方是要用到多矩阵相乘,或者找出凸壳这种东西的。对于矩阵的认识,我停留在大学时代学过的线性代数的残留记忆的水平,约等于是一无所知。当年也是如此,学习线性代数,也只是当成一门数学来学习,从来没有想到过,这门学科可以用在真实生活中的什么地方。而美国随便一本计算机教材的第一章,第一节,要求学生思考真实生活中什么地方用到这个数学的知识,正是以把知识和生活紧密结合为导向,向学生传递知识,如果学生真的认真思考,很难想见这些学生会没有成就,学生对此问题的理解会不深。

而反思自己,无论是学习线性代数的时候,还是学习算法的时候,我从来没有想到过,某个数学概念和或者某个算法跟真实生活的联系,也难怪,学过了就忘记,要是记住了,那才真的是奇怪。

要说真实世界用到排序的例子,那是相当简单的,我决定每个人至少能说出个十种八种。我就说一个我最近生活中常常碰到的,就是在12306购买火车票的时候,我最常用的,就是按照发车时间顺序排序,或者在使用手机查询列车时刻表的时候,按照列车行程时间排序,以便我买到车程最短的列车。

  1. 除了运行速度以外,在真实世界问题背景中,还可以用那些效率指标?

关于算法效率指标,我马上就能想到的还有一个,就是算法耗费的内存。我们常说时间换空间,或者空间换时间,就是说这二者是一对矛盾,所以,衡量算法效率,除了时间,就是空间。也就是存储。

  1. 选择你原来见过的某种数据结构,讨论一下其长处和局限性。

先选一个最简单的,就是数组。这是一种线性的数据结构。其长处是按照下标读取元素的速度相当快而且简单。缺点是,一旦需要数组元素变动,操作就会极为复杂,比如在中间插入一个元素,后面所有的原素都要跟着挪动位置。另外一个缺点就是数组的原素个数是固定的。数组的空间是静态的。

最近又提起了兴趣去折腾 VPS,买好一台新的 VPS 服务器后,第一件事情就是登上去设置环境,当然,SSH 登录必不可少,这也是远程操作一台服务器的先决条件。不过 SSH 服务器,默认不是按照最优的方式去配置的。所以,我打算自己总结一下 SSH 服务的最佳实践。

安装 ssh 服务器

如果购买的是一般云计算的 VPS 服务器,当然是没有这个过程了,不可能没有安装 ssh 服务器的。如果想把自己淘汰的旧电脑变成一台 Linux Server 的话,可能就会遇到这个问题。例如,我昨天把自己 2009 年的一台 Macbook Pro 安装上了 Linux,我选择了使用最为广泛的 Ubuntu,选择了最新的 19.04 版本,主要希望它能对硬件有比较好的兼容性。

一般来说,在个人电脑上,最好的选择是安装 Desktop 版本,因为你毕竟有屏幕、键盘等输入输出设备,往往还有无线网卡,装成 Server 的话,想让它兼容你电脑上的各种硬件是很痛苦的,但是如果装成 Desktop 的话,想把它改造成一个提供服务的 Server 是很简单的。所以,我们一般都安装 Desktop 版本。

比较邪门的就是 Ubuntu 的 19.04 Desktop,竟然连 ssh server 都没有装,只有一个 ssh-client。

1
2
3
4
5
6
7
8
9
# 1.我觉得 apt-get 一点不好用,装个 aptitude 包管理器
apt-get install aptitude
# 2.检查 ssh 安装状态
aptitude search ssh
# 3.安装 ssh server
aptitude install openssh-server
# 4.检查服务状态
systemctl status ssh
# 5.到这里基本安装完毕了,以上在 Ubuntu 19.04 上测试过

禁止 root 用户直接登录服务器

新的 VPS 分配的时候,都是默认设置 root 用户,并且设法把密码发送给管理员知道。root 用户是 Linux 系统权限最高的用户,一旦泄漏了,后果不堪设想,黑客可以使用 root 的权限完全控制一台服务器。

比较好的做法是,禁止 root 账户直接登录到服务器上,因为如果允许 root 登录,就可能让黑客通过攻击直接得到 root 的权限。

比较好的实践方式是,设定一个非 root 的账户,用于日常登录使用,需要的时候,使用 sudo 临时取得权限,或在本地 shell 登录到 root 帐号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 使用 adduser 添加一个用户
adduser charles
# 使用 passwd 创建密码
passwd charles
# 使用 usermod 改变这个用户的 shell 为自己最喜欢的 zsh
usermod -s /bin/zsh charles
# 登录用户
su charles
# 进入用户的 home 目录
cd
# 创建 .ssh 目录
mkdir .ssh
# 创建公钥文件,用于注册可以登录的客户端
touch .ssh/authorized_keys
# 为了让这个用户得到执行 root 权限的能力,加到 sudoers 里面
gpasswd -a charles wheel
# 将一些环境变量带到 sudo 的环境(高级)
visudo

上面的代码,在系统里建立了一个新的用户,给这个用户创建一个密码,密码可以用于使用 sudo 命令。

然后使用 gpasswd 将用户加到 wheel 用户组,在 CentOS 上,就可以让用户得到 sodu 权限。 多年来我积累了一套自己使用非常习惯的 shell 配置,也建议读者这么做,可以去看看我的做法 https://github.com/charlestang/env.git 就会明白我在说什么。实现这套配置,环境变量少不了,比如 vim 的配置问题。

你可能会发现,sudo 后面使用 vim 的话,很多高级配置都无法生效。其他的命令也可能有这个问题。产生的原因其实是在一个用户的环境下设置的环境变量,使用 sudo 的时候,就会失去。可以通过代码片段的 visudo 命令,打开配置文件,指定要代入到 sudo 场景下的环境变量。

使用 sudo 会带来很多的不变,但是,作为一个运维人员,还是要养成不使用 root 的好习惯。各种小问题都是可以克服的。

使用基于公钥验证的登录

一般来说,购买一台服务器,新分配的时候都是默认用户名密码登录的,国内的阿里云、腾讯云都有这样的,国外的 Linode,前两天体验的 Vultr 也是。

但是,密码验证这件事情,在密码学里叫 PSK,pre-shared key,预先共享的。也就是至少有两方知道这个密码。而且,在公有云,是云平台设定的第一个密码,所以云平台是知道的。

更安全的方式,是使用非对称加密的方式,也就是基于公钥验证的登录。首先要在本地创建一个公私钥对,然后,把公钥注册到服务器的 authorized_keys 文件里。

1
2
3
# 在自己的电脑上创建 RSA 的公私钥对
ssh-keygen -b 4096 -t rsa
# 这个命令会在你的 ~/.ssh/ 目录创建 id_rsa 和 id_rsa.pub(公钥)文件

插图节选自《HTTPS权威指南》

关于非对称密钥的长度问题,我看到 CentOS 7 的文档里面介绍,默认的 RSA 的长度是 2048 位,文档里认为足够了,而且建议的最短长度是 1024 位。上面的例子里,我特意用了 4096 位,哈哈,希望能用 30 年……插图选自一本技术书籍,内容是2012年,权威机构对密码长度和对抗强度的一个估算。

公钥文件配置在服务器的非 root 用户的 ~/.ssh/authorized_keys 文件里面,内容贴进去就可以了。一般来说,服务器缺省设置,都是支持使用公私钥验证进行登录的。下面是对/etc/ssh/sshd_config的配置;

1
2
3
4
5
6
7
# 开启公钥验证
PubkeyAuthentication yes
# 不允许 root 帐号登录
PermitRootLogin no
# 不允许使用密码验证登录
PasswordAuthentication no
ChallengeResponseAuthentication no

更换SSH服务的默认端口

SSH 服务的默认端口是 22,这个端口已经是全网皆知的了,也是网络上自动化工具攻击的首要目标,希望自己的服务器安全的话,就首先要把这个端口给更换掉。

推荐使用 10000 号以上的端口,设置的时候,可以这样:

1
2
3
4
5
6
7
# 1. 先新增一个监听的端口号,先不要删除 22 端口
Port 22
Port 10022
# 2. 重启 sshd 服务
> systemctl restart sshd
# 3. 退出登录,然后用自己的电脑去测试能否连接新的端口号
# 4. 如果登录成功,删除 Port 22,然后再次重启 sshd 服务

为什么按照上面的顺序操作?很多云服务器,都有默认的安全组或者防火墙配置,极有可能,默认情况下,除了 22 端口,其他都访问不了。所以,不要忙着取消 22 端口,而是先新增一个,测试一下联通性。不然,可能你退出了,就连不回来了。虽然,也不会有什么大的后果,但是会带来不少的麻烦。

优化SSH服务器端的性能

有一些常见的缺省设置,可能导致SSH服务的连接缓慢,简单调整参数,就可以使连接速度变快:

1
2
3
4
# 关闭 DNS,默认情况下,服务器会解析连接上来的服务器的域名(Hostname)
UseDNS no
# 这种验证方式也是拖慢连接速度的常见问题,其实在 CentOS 7中此项默认已经是 no 了
GSSAPIAuthentication no

总结

感觉还有很多的细小的点没有总结全,不过这个帖子可以放着,慢慢总结吧。

我发现,每隔一段时间,运维 Linux 服务器的方法,就会变迁一次,害得我总是要重复学习这件事情,真是太不友好了。Linux 服务器运维的方法不是一种半衰期很长的技巧么?世道都变了啊……

Ubuntu 桌面系统初始化

这两天安装了一个 Ubuntu 19.04 Desktop 到我的最老的 Macbook Pro 上面,打算当成家庭的 Server 使用的。

1
2
3
4
5
6
7
8
9
# 1. 说实在的,我就看不出来这个新版的 apt 命令有什么好用的
# 当然,底层的命令 apt-get 和 apt-cache 更难用
apt install aptitude
# 2. 替换掉 vim-tiny,不知道这么多年过去了,为什么还是这样
aptitude remove vim-tiny
aptitude install vim
# 3. 桌面版连个 netstat 命令也没有,装一下(推荐使用 ss 命令代替)
# 不是梯子的 ss,我没写错,就是 ss 命令
aptitude install net-tools

CentOS 检查系统已注册服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 注意:从 CentOS 7 开始,已经不推荐使用 chkconfig 了
# 检查有哪些注册了的服务(SysV 流派的系统服务,迟早会被新的方式取代的一种)
chkconfig --list
# 关闭这种流派的服务:在2,3,4,5四个run level下关闭名叫 agentwatch 的服务
chkconfig --level 2345 agentwatch off
# 删除指定名字的服务:删除名叫 agentwatch 的服务
chkconfig --del agentwatch
# 检查有哪些注册了的服务(systemd 流派的系统服务,CentOS 7+)
systemctl list-units
# 只列出 service 类型的
systemctl list-units --type service
# 禁用服务:禁用一个名叫 aegis.service 和 agentwatch.service 的服务
systemctl disable aegis
systemctl disable agentwatch
systemctl status agentwatch
rm /etc/init.d/agentwatch
rm /etc/systemd/system/aliyun.service
rm /usr/sbin/aliyun-service
rm /usr/sbin/aliyun-service.backup
# 如果手动暴力删除了一些 service 的配置文件
systemctl reset-failed
systemctl list-units --type service
# 执行上述两个命令,发现没有残留了

CentOS 上安装支持 BBR 的 kernel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 查看当前的发型版本
cat /etc/*release*
# 导入新的 repo,参见 http://elrepo.org/tiki/tiki-index.php
rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
# 安装与 CentOS 7 对应的 rpm 包
yum install https://www.elrepo.org/elrepo-release-7.0-3.el7.elrepo.noarch.rpm
# 检索对应的 kernel 包
yum --enablerepo=elrepo-kernel search kernel-ml
# 安装正确的 kernel 包
yum --enablerepo=elrepo-kernel install -y kernel-ml.x86_64
# 确认安装了哪些 kernel 包
rpm -qa grep kernel
# 查看目前的配置
egrep ^menuentry /etc/grub2.cfg cut -f 2 -d "'"
# 这步骤之后,机器需要重启,然后确认现在内核的版本号
uname -a
# 启用 BBR
echo 'net.core.default_qdisc=fq' tee -a /etc/sysctl.conf
echo 'net.ipv4.tcp_congestion_control=bbr' tee -a /etc/sysctl.conf
sysctl -p
# 确认 BBR 是否启动
lsmod grep bbr

在 Linode 上申请了一台 CentOS 7

最近,腾讯云审核非常严格,于是我又购买了一台 Linode 的服务器来玩,以防万一,按照以前我的性格,我会选 Debian 9 发型版的,但是最近比较偏爱 CentOS,就选择了 CentOS 7,其实,CentOS 8 也出来了,不过真的鬼使神差还是选了 7。

按照我一贯的做法,先要更换 SSH 的端口号的,主要是为了安全,感觉在互联网上非常奇怪,出现一台新的开放端口的机器后,常用端口就立刻会被不断攻击,所以,我习惯的做法是立刻把各种要命的服务都改成冷门的端口号。

没想到,Linode 上生成的 CentOS 7 实例机器,竟然默认了非常多的安全设置,让我完全没有想到。

首先是 SELinux,我挣扎了半小时,还是放弃了,这么多年来没有打起勇气搞明白这东西,感觉实在太麻烦了。

编辑 /etc/selinux/config 文件,将 SELinux 切换到 disabled,然后重启服务器。可以彻底关闭 SELinux,然后我在 56000 端口开启 SSH,没想到还是连不上,iptables -L 发现,竟然还有 iptables,当我尝试 systemctl stop iptables 的时候,发现告诉我,系统没有 iptables.service 的 unit,这就让我摸不着头脑了。

网上继续搜索,发现,CentOS 7 默认使用的是 firewalld 这个服务,真是让人晕头转向,又简单研究了一下 firewalld 的用法。

1
2
3
4
5
6
7
8
# 检测 firewalld 是否开启,显示 running 就是开启着
firewall-cmd --state
# 列出当前开启了哪些服务,发现有 dhcpv6-client 和 ssh
firewall-cmd --list-service
# 然后加入你要增加的端口
firewall-cmd --permanent --service=ssh --add-port=56000/tcp
# 重新加载配置
firewall-cmd --reload

这里我 Link 一篇文档,非常赞,以备查阅《Understanding Firewalld in Multi-Zone Configurations》。这篇文档详细介绍了 firewalld 的原理,以及操作范例,看完基本都明白了,很不错。

在这台 CentOS 上,我想使用 yum 源来安装 ss-libev,没想到遇到了很大的阻碍,以前我在阿里云的 CentOS 实例上,轻松可以 yum install 的东西,在这里竟然遇到了很大的阻碍,折腾了两三天竟然还没有装成功,Linode 的默认实例配置里,添加了 ss-libev 的源后,发现缺少两个关键的依赖,libsodium 和 libmbedlts 两个东西,我尝试手动编译安装了 libsodium 发现并不行,因为 rpm 是个体系,手动编译不能补充这个依赖,它只认自己数据库里面的数据。

这下有点麻烦,于是我想到了换源,是不是 Linode 服务器提供的 yum 源有问题,尝试换成 163 的源,然而不管用,再尝试换成阿里云的源(我先尝试了这个,拷过来是不行的,因为可能是私有的域名或者只对国内 IP 开放更新的),换阿里云的办法是:

1
2
3
4
5
6
7
8
# 首先是备份原有的文件
cd /etc/yum.repos.d
mv CentOS-Base.repo CentOS-Base.repo.linode.backup
# 然后是下载阿里云的 repo 文件代替
curl -o CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
# 然后是更新缓存
yum makecache
yum update

然后搞笑的是,我更换了阿里云的源发现,竟然仍然无法正常用 yum 安装 ss-libev,还是那两个依赖解决不了,实在无语了暂时还没搞明白怎么回事。然后我打算放弃治疗手动安装算了,libev 的 GitHub 官方页面推荐使用 snapcraft.io 来安装,所以我顺便看了一眼怎么安装 snapcraft.io,第一个步骤是给 yum 增加 epel repository。

1
sudo yum install epel-release

然后我灵机一动,难道这就是我苦寻的遗失的源?一试,果不其然,那我也不用编译安装了,直接成功 yum install 了。到此,我又把阿里云的源换回去了,说实在不怎么信任阿里云。万一参杂点私货呢?对吧。

这里再记录一下,怎么用 Screen 去后台长期运行一个程序:

1
2
3
screen -dmS SessionName /bin/cmd -o options
# -d -m 这两个参数组合起来的意思是,不要启动 screen,而是直接 detach 状态执行命令
# -S 的意思是给这个 Session 取个名字

我这里 Link 一篇文档以备查阅《使用 Screen 管理你的远程会话》。

低配置的服务器临时增加 swap 来编译

今天在以前买的腾讯云服务器上编译 PHP ,发现竟然因为内存不够被杀掉了,得增加 swap 文件来,解决内存不够无法编译的问题:

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
27
28
29
30
31
32
# 首先检查一下现在系统的内存,单位用 MB
free -m

# 看到服务器没有配置任何 swap,total = 0
# 看看是否有已经定义的 swap 文件
swapon -s

# 看到没有任何文件
# 然后来格式化一个 swap 分区,512M
dd if=/dev/zero of=/swapfile bs=1024 count=512k

# 然后在上面创建一个 swap 空间
mkswap /swapfile

# 激活 swap,然后使用上面的 swapon -s 检查是否成功
swapon /swapfile

# vi /etc/fstab,在里面最后一行配置
/swapfile swap swap defaults 0 0

# 修改文件权限
chown root:root /swapfile
chmod 0600 /swapfile

# 最后用 free -m 看看 swap 的 total 增加了没有
# 看看操作系统依赖 swap 的频繁度
cat /proc/sys/vm/swappiness

# 默认值是 60,太频繁使用 swap 会拖慢速度,毕竟内存更快
sysctl vm.swappiness=10

# 改低这个参数,done

使用上面的步骤,就完成了 swap 的添加,然后再次尝试编译,成功了。

以上内容引自:《How To Add Swap on CentOS 6》

这个过程的逆过程就比较简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 关闭 swap
swapoff -a

# 确认
swapof -s

# 看到没有文件了
vi /etc/fstab

# 去掉关于 swap 的配置
rm /swapfile

# 就彻底删掉了 swapfile

在 Ubuntu 20.04 上安装 MySQL

其实毫无难度的,就是用 apt install mysql-server 就可以了。我这里想说的是,装完了以后,怎么登录进去呢?整个安装过程是没有交互式的。

你可以 sudo mysql,直接用 root 权限调用客户端,就会以 root 身份登录了,不需要密码。

1
mysql -uroot -p

首次安装好数据库后,不需要密码,直接按回车就可以登录进去了。值得一提的是,现在创建用户和密码的方法变了。

1
create user admin@localhost identified by '123@qwe';

上面的用户定义了一个本地用户,密码是 123@qwe,我以前喜欢用Grant语句直接授权和创建用户和密码,现在似乎是不行了。

1
grant all privileges on db_name.* to admin@localhost;

上面的语句是授权用户访问一个数据库的语句,privileges 关键字不是必须的,可以省略。

然后需要执行:

1
flush privileges;

关于这一点,腾讯有一片文章介绍得不错:

《如何在 Ubuntu 20.04 上安装 MySQL》

如何在 Ubuntu 20.04 上安装 PHP

也说个简单的做法,就是用 apt install php-fpm,如果你是安装 php 这个包,你会发现海量的依赖,会把整个 apache 2 都给带出来。因为默认是这样的。

不过 nginx 伺服静态文件要好一点,所以,现在流行 LNMP 多一点,你可能不想安装 apache 2,那么你就应该安装 php-fpm 这个包。

使用 apt 的好处是,以后升级的时候,简单一点。如果没有逼到非要自己编译,最好不要自己编译,实在麻烦而且无趣,当然并不难。

首次在 CentOS 服务器上安装 MySQL

安装服务器比较简单:

1
yum install mysql-server.x86_64

装完服务器后,服务器默认是不启动的,使用 systemctl 命令进行启动

1
systemctl start mysqld.service

然后,root 的初始密码是什么呢?

1
mysql -uroot -p

你会发现,MySQL 初始并未设置密码,比较安全的方式是,你登录成功后,马上设置一个新的密码,使用命令:

1
2
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';
FLUSH PRIVILEGES;

即可完成 root 密码的设定。

最近我遇到一个难题,我在阿里云买了一台服务器,用它搭建了一个“获取全世界互联网信息的服务”。我家用的是电信宽带,使用起来非常流畅,但是莫名其妙的是,在公司就不能很好地运作。公司用的也是电信的宽带服务,是写字楼园区专供的企业电信宽带服务。

跟公司的 IT 抱怨很久,他们也没法解决,说我对服务器发出的请求,是从离开公司端口后开始丢包的,只能去投诉运营商,这个借口真好,原因是什么也没法查,解决当然也无法解决啦,好,不去管这些烦心事情了。

后来我同事告诉我一个神奇的工具,以及解决方案,终于曲线救国了。我把阿里云服务器上的服务,桥接到我家里,然后在公司的时候,我就连接到我自己家里的服务器。为了保持家里和阿里云的可靠连接,我需要搭建一条可靠的隧道。这里就用到了一种ARQ协议。我以前不知道什么是ARQ,今天看了一下维基百科,觉得还是很有意思的,本文就介绍一下什么是ARQ。

要记住:重要的事情说三遍

ARQ 的全称是 Automatic Repeat reQuest,不要问我为什么选择这三个大写字母,我也觉得选择很奇怪,估计大写缩写有一定的规则吧,我并不了解。但是从字面意义上看,就是说,把请求自动再发一次。就好像,你跟别人说话,那人没听到,你就再说一遍,一直到他听到为止,这就好像是我们开玩笑的:重要的事情说三遍一样。只是一个比喻。

从这个名字里,我们可以了解到另外一件事情,ARQ 虽然被称为是一种协议,但是定义里看更像一种策略,或者一种解决方案建议,具体怎么去实现,还是有多种办法的,也有很多的变体,后面会说到这个问题。

ARQ,也可以是 Automatic Repeat Query 的缩写,是一种在数据传输时,使用确认(Acknoledgements,就是我们常说的ACK,接收方发送一个消息,告诉发送方,自己是否正确接到了一个包体)和超时(Timeouts,在收到一个确认消息之前,等待的一个确定的时间段)机制,在不可靠的网络上,实现可靠的数据传输的错误控制方法。如果发送方在超时之前没有收到确认,通常会重新传输相应的包体,直到收到确认或者重试超过一定的次数。

维基百科 Automatic Repeat Request

祭出此图:ARQ 一般在数据链路(Data Link)和传输(Transport)层实现

ARQ 的常见策略

ARQ 协议有很多种常见的策略:

  • 停止并等待 ARQ(Stop-and-wait ARQ):这是最为简陋的一种ARQ实现方式,用于远程通信的两端,保证消息传送没有缺失和按序到达。一句话概括这种策略,就是每说一句话,都要等到确认才说第二句,否则就一直重复上一句。很显然,这种方式就是串行发送消息帧,而且要等待确认才继续,一帧内容传送就要最短等待一个往返消耗的时间,有时候还要加上超时,所以效率非常低下,速度也非常慢,实现起来很简单。这种协议很容易出问题,比如,接收方的确认消息丢失了,或者确认太慢了,导致了发送方等待过久,发送方就会重发,结果接收方就收到两个一样的帧,这时候,接收方就没法确认,到底是上一帧重复了,还是又来了一个新的帧。发送方也可能会收到两个确认,无法识别是重复确认同一帧还是不同。于是,这种协议会在头里加上一个比特(bit)的序号,取值只有 0 或者 1,接收方收到的时候,是按照 010101…… 这样的交错顺序的,否则就是重复帧,简单丢弃就可以了,发送方收到的确认也带序号,采用同样策略处理。所以,这种 ARQ 也叫做“翻转比特协议”(alternating bit protocol);
  • 后退N帧 ARQ(Go-Back-N ARQ):这种协议相对上面一种来说,其实利用了统筹的思想,这种策略下,发送方在等待超时的间歇,可以继续发送数据帧,可以连续发出的帧数量,叫窗口大小,显然窗口大小的设定和超时时间,数据传输速度匹配,是比较优化的选择。同样,所有发送的帧,都要带有序号,接收方严格按照序号收纳数据帧,先收 1,再收 2,如果这时候 3 丢了,来了一个 4,那么接收方会丢弃。同时,接收方每收到一个帧,都要确认一个序号,就是自己收到的有效的最大序号,在这个例子里,接收方一直确认 2。发送方在发完一个窗口所有的帧以后,检查最大的有效确认,然后从最大的有效确认后面一个开始重发,比如上面的例子里,发送方已经发到 8 了,但是因为最大的有效确认只有 2,那么必须从 3 开始重新发送。其实,TCP 协议,使用的就是这种 ARQ 策略(变体)。从这个机制的原理可以看出来,这个协议的特点,有一个滑动窗口,但是这个窗口只对发送方来说存在,接收方只是一帧一帧处理数据,所以,仍然存在很多的浪费,一些信息会反复发送多次。如果把收到的不想要的帧,先缓存起来,说不定将来会用到,就可以提高效率,这就引出了另一种策略;
  • 选择性重发/拒绝 ARQ (Selective Repeat/Reject ARQ):其实理解了上一种,这种就很容易了,就是把收到的非预期帧缓存起来,然后告诉发送方自己缺哪一帧,服务器不会连续重发,而只发送接收方缺失的帧,如果接收方续上了编号,就快速向前跳动编号,这样极大减少了数据的重复发送,提高了效率。

回退N帧 ARQ(a)和 选择性重发 ARQ(b)的原理图

总结

ARQ 是一种在数据传输过程中,错误控制的策略,保证了数据的完整性和顺序性。核心目的是在不可靠的网络上实现可靠的数据传输。比如,在短波无线电传输,GSM 网络,电报等领域都有很广泛的应用。在当今因为各种原因导致的复杂互联网环境上,某些点对点传输的服务,也变得不那么可靠,可以考虑引入 ARQ 策略。

在服务器端开发方面,ARQ 的思想,也可以给我们在服务实现时候,很多的启发。此外,这种策略的学习和理解,也是理解更复杂的网络协议的基础。

SOCKS 代理协议是网络上使用非常普遍的一种协议,最近因为想要自己搭建一个穿透局域网安全网关的代理,所以,顺便仔细学习了一下 SOCKS 的相关资料,还算有点意思,特此记录一下。

SOCKS 协议的故事

话说,一个叫做 David Koblas 的系统管理员,他在 MIPS Computer Systems 公司工作。就是这个人发明了 SOCKS 代理协议。MIPS 公司在 1992 年,被一家叫做 Silicon Graphics 的公司给控制了(应该是收购了吧),这家公司后来把自己的品牌名改为了 SGI(一家很高端的软硬件制造商,2009年4月破产了)。也就是在这一年 1992 年,David 在 Usenix Security Symposium 安全研讨会上,发布了一篇论文,名字就叫 SOCKS,将此协议公之于众。

最早,整个互联网采用的是盲目信任的方式进行连接和组织的。当时,专家们在聚会的时候,讨论的都是怎么让网络更加简单和高效,一台主机在网络上到底怎么才能更可靠的被别的主机所连接。一直到 1988 年,一个叫做 Morris 的蠕虫,给整个互联网一记重拳,后来大家讨论将一个子网络接入到互联网的时候,安全也成了一个必须要讨论的重要话题。

不过,提升网络的安全性可没有什么容易的办法,想出来的大部分办法都是通过减少内网服务器向公网暴露的机会,来最小化攻击的概率,也就是我们常喜欢用的“最小原则”,如果没有必要,就不进行授权。很多网络在接入互联网的时候,都选择了单一出入口的方式。将本地的子网络,置于防火墙的保护之后,再接入到互联网,如此一来就极大减少了被攻击的机会,网络的安全性自然就提高了。

但是本地网络置于防火墙之后,再访问外部网络的资源就会非常不便。为了解决这个问题,人们想出了各种方案。内外网隔离(笨拙,使用极其不便,但是维护成本很低),单一机器授权(只有一台机器可以和外网进行双向访问,使用仍然非常不便,而且维护成本高昂,因为有很多用户的访问权限要分配和收回),还有安全路由器(常见的一种策略是允许所有的出口流量,但是对进口流量禁止所有 1024 以下端口的访问,这种策略的问题是,一旦路由被攻破,整个网络就置于威胁之下)。

OSI 7层网络模型

在这些方案之外,代理防火墙(Proxy Firewall)解决方案就被提出来了。路由器是在 OSI 模型的“网络层”进行安全过滤,可以降低客户端的成本,但是不够完善,维护起来也比较困难,估计当年的硬件不像现在可以随意修改配置吧。代理防火墙的方案,平衡了使用的便捷和维护的复杂度,是一种折衷。

代理防火墙工作在 OSI 模型的“会话层”,也就是“传输层”和“应用层”中间的地方。著名的 SOCKS 就是这样一种“解决方案”。它是一种非常轻薄的解决方案,在客户端,提供了一套开发类库,叫 SOCKS 库,对照着标准 socket 的 API,提供了五个 API 函数,名字跟 socket 的一样,只是用 R 作为前缀。在服务器端,提供了一个叫做 sockd 的伺服软件,这个软件部署在防火墙系统所在的一台主机上,通过简单的配置文件就可以完成应用层的过滤,允许或拒绝哪些目的地址和端口被接入,是非常容易维护的,给网络管理员带来了极大的方便。

在 1992 年公布之前,SOCKS 解决方案已经在 MIPS 内部使用了长达三年之久,没有遇到明显的问题和瓶颈,所以 David 认为这是一个久经烤验的成熟方案。

SOCKS 的版本发展

上面的故事里,我们可以看到 SOCKS 最初提出的时候,其性质是一个解决方案,包含一个类库和一个服务端后台伺服程序。但是,NEC 公司的李英达?(Ying-da Lee),看到了它的优美之处,把它提炼出来,发展成了一种协议,变得更加通用,而且,大家都可以根据协议提出自己的实现。这位李同学,提出了 SOCKS 协议的第四个版本。也是流传非常广泛的版本。

从李同学开始,SOCKS 的定位也变得非常明确,就是在防火墙服务器上,提供一种 TCP 会话的转发,允许用户可以透明的穿透防火墙的阻拦。这种协议的优势在于,它完全独立于应用层的协议,可以用在很多的场景,telnet,http,ftp 都不在话下,并且可以在 TCP 会话开始之前,完成访问权限的检查,之后只要做来回往复的转发即可。而且由于此协议完全不关心应用层的协议,所以应用层通信可以加密,保护自己通信的内容不被代理所看到。

SOCKSv4 开始,这个协议得到了非常广泛的应用,所以,广大群众对这个协议进行了扩展,就有了 SOCKSv4a 版本,这个版本只对 v4 进行了比较小的改动,主要是允许在协议头里填充域名,代替IP地址,用以防止客户端不能正确解析出目标IP的场景。(举个例子,某个局域网有一台服务器,域名是 test.oa.com,与之对应的 IP 地址也是内网的,这种情况下,外部用户是很难知道到底 IP 地址是什么的,因为没有登记在公网的 DNS 服务里面)到这里后,SOCKS 就成了一种非常通用的双向通信的轻量级代理协议了。也有了大量的标准实现。得到了海量的需要网络连接的应用程序的支持,成为了“电路级网关”(circuit-level gateway)的事实标准。

现在更加流行的 SOCKSv5 版本就是一个更加完善的版本了,主要增加了:

  • 强力的身份验证方案
  • 验证方法的协商机制
  • 地址解析的代理
  • UDP协议应用的代理支持

SOCKS 协议的内容

SOCKSv4的内容

上面的插图展示了SOCKS协议第四版的内容,可以看见,这里展示的是前两个包体,客户端发起,服务器回复,连接成功后,后续的操作就透明了。

SOCKSv4a 对第 4 版的扩展,非常小,就是增加了一个域名,只要在 IP 那个字段,前三个字节都填 0x00,就会由服务器来负责解析正确的地址,并利用回包空闲的两个字段来返回正确的 IP 地址和端口号码。

到了现在普遍使用的 SOCKSv5,协议变复杂了很多。首先,因为有强力的验证,而且支持多种验证方法,就有了一个协商验证方法的过程,然后,进行身份验证,最后再进行通信指令。

SOCKS5 的协商过程

上面的图,展示了 SOCKS5 连接的协商过程。协议的具体内容,对应着上面的握手环节,增加了几种不同的包体:

SOCKS5 的协议包体结构

SOCKS 协议的不足

从上面的 SOCKS5 的时序图上,我们可以看到,由于承担了额外的验证协商的功能,导致 SOCKS5 在建立的时候,需要额外消耗多达三次握手,如果不需要验证身份,也需要两次握手,这就增加连接时候的延迟。

另外,因为 SOCKS5 协议本身完全不关注应用层的内容,所以,客户端和目标服务器的通信,加密完全依赖客户端和服务器的通信协议,如果服务器使用的是 HTTP 协议,那么通过代理走的流量,就相当于是明文在内网传输。安全性上不是很高。

不过,我们可以通过在外面包裹一层 TLS 来解决这个问题,就形成了 SOCKS5 over TLS 的解决方案,有效加密了通信的内容。TLS 负责加密连接,SOCKS5 负责代理协议控制。

互联网上已经公布了 SOCKSv6 的草案,主要内容就是SOCKS5的问题修复,第一个就是多次握手已经不太适用于移动互联网和卫星通信等网络环境,需要被优化。客户端会尽可能多的发送信息给服务器,并且要求创建 socket 之前,不等待验证的结论;连接请求模仿 TCP Fast Open 的语义,具体就是在连接请求的包体里,带上一部分载荷,连同第一个SYN包一同发给服务器;根据选项可以不向后兼容;可选支持 0-RTT 的验证方法(一个IP包体到达服务器再返回客户端的过程消耗,叫 1-RTT)。

不过,目前来说,还没有得到广大软件厂商的支持。而且据说 TLS 1.3 也在积极探索 0-RTT 的解决方案,不过也还在协议草案的早期。

参考文献: