git rebase

分類 devops, git

变基

在 Git 中整合来自不同分支的修改主要有两种方法:merge 以及 rebase。 在本节中我们将学习什么是“变基”,怎样使用“变基”,并将展示该操作的惊艳之处,以及指出在何种情况下你应避免使用它。

变基的基本操作

请回顾之前在 分支的合并 中的一个例子,你会看到开发任务分叉到两个不同分支,又各自提交了更新。

分叉的提交历史。

Figure 35. 分叉的提交历史

之前介绍过,整合分支最容易的方法是 merge 命令。 它会把两个分支的最新快照(C3C4)以及二者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(并提交)。

通过合并操作来整合分叉了的历史。

Figure 36. 通过合并操作来整合分叉的历史

其实,还有一种方法:你可以提取在 C4 中引入的补丁和修改,然后在 C3 的基础上应用一次。 在 Git 中,这种操作就叫做 变基(rebase)。 你可以使用 rebase 命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。

在这个例子中,你可以检出 experiment 分支,然后将它变基到 master 分支上:

1
2
3
4
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master) 的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件, 然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用。 (译注:写明了 commit id,以便理解,下同)

将  中的修改变基到  上。

Figure 37. 将 C4 中的修改变基到 C3

现在回到 master 分支,进行一次快进合并。

1
2
$ git checkout master
$ git merge experiment

 分支的快进合并。

Figure 38. master 分支的快进合并

此时,C4' 指向的快照就和 the merge exampleC5 指向的快照一模一样了。 这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。 你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的, 但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。

一般我们这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁——例如向某个其他人维护的项目贡献代码时。 在这种情况下,你首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到 origin/master 上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。

请注意,无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。 变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。

更有趣的变基例子

在对两个分支进行变基时,所生成的“重放”并不一定要在目标分支上应用,你也可以指定另外的一个分支进行应用。 就像 从一个主题分支里再分出一个主题分支的提交历史 中的例子那样。 你创建了一个主题分支 server,为服务端添加了一些功能,提交了 C3C4。 然后从 C3 上创建了主题分支 client,为客户端添加了一些功能,提交了 C8C9。 最后,你回到 server 分支,又提交了 C10

从一个主题分支里再分出一个主题分支的提交历史。

Figure 39. 从一个主题分支里再分出一个主题分支的提交历史

假设你希望将 client 中的修改合并到主分支并发布,但暂时并不想合并 server 中的修改, 因为它们还需要经过更全面的测试。这时,你就可以使用 git rebase 命令的 --onto 选项, 选中在 client 分支里但不在 server 分支里的修改(即 C8C9),将它们在 master 分支上重放:

1
$ git rebase --onto master server client

以上命令的意思是:“取出 client 分支,找出它从 server 分支分歧之后的补丁, 然后把这些补丁在 master 分支上重放一遍,让 client 看起来像直接基于 master 修改一样”。这理解起来有一点复杂,不过效果非常酷。

截取主题分支上的另一个主题分支,然后变基到其他分支。

Figure 40. 截取主题分支上的另一个主题分支,然后变基到其他分支

1
2
$ git checkout master
$ git merge client

快进合并  分支,使之包含来自  分支的修改。

Figure 41. 快进合并 master 分支,使之包含来自 client 分支的修改

接下来你决定将 server 分支中的修改也整合进来。 使用 git rebase <basebranch> <topicbranch> 命令可以直接将主题分支 (即本例中的 server)变基到目标分支(即 master)上。 这样做能省去你先切换到 server 分支,再对其执行变基命令的多个步骤。

1
$ git rebase master server

将  中的修改变基到  上。

Figure 42. 将 server 中的修改变基到 master

然后就可以快进合并主分支 master 了:

1
2
$ git checkout master
$ git merge server

至此,clientserver 分支中的修改都已经整合到主分支里了, 你可以删除这两个分支,最终提交历史会变成图 最终的提交历史 中的样子:

1
2
$ git branch -d client
$ git branch -d server

最终的提交历史。

Figure 43. 最终的提交历史

变基的风险

呃,奇妙的变基也并非完美无缺,要用它得遵守一条准则:

如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基。

如果你遵循这条金科玉律,就不会出差错。 否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。

变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。 如果你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用 git rebase 命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。

让我们来看一个在公开的仓库上执行变基操作所带来的问题。 假设你从一个中央服务器克隆然后在它的基础上进行了一些开发。 你的提交历史如图所示:

克隆一个仓库,然后在它的基础上进行了一些开发。

Figure 44. 克隆一个仓库,然后在它的基础上进行了一些开发

然后,某人又向中央服务器提交了一些修改,其中还包括一次合并。 你抓取了这些在远程分支上的修改,并将其合并到你本地的开发分支,然后你的提交历史就会变成这样:

抓取别人的提交,合并到自己的开发分支。

Figure 45. 抓取别人的提交,合并到自己的开发分支

接下来,这个人又决定把合并操作回滚,改用变基;继而又用 git push --force 命令覆盖了服务器上的提交历史。 之后你从服务器抓取更新,会发现多出来一些新的提交。

有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交。

Figure 46. 有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交

结果就是你们两人的处境都十分尴尬。 如果你执行 git pull 命令,你将合并来自两条提交历史的内容,生成一个新的合并提交,最终仓库会如图所示:

你将相同的内容又合并了一次,生成了一个新的提交。

Figure 47. 你将相同的内容又合并了一次,生成了一个新的提交

此时如果你执行 git log 命令,你会发现有两个提交的作者、日期、日志居然是一样的,这会令人感到混乱。 此外,如果你将这一堆又推送到服务器上,你实际上是将那些已经被变基抛弃的提交又找了回来,这会令人感到更加混乱。 很明显对方并不想在提交历史中看到 C4C6,因为之前就是他把这两个提交通过变基丢弃的。

用变基解决变基

如果你 真的 遭遇了类似的处境,Git 还有一些高级魔法可以帮到你。 如果团队中的某人强制推送并覆盖了一些你所基于的提交,你需要做的就是检查你做了哪些修改,以及他们覆盖了哪些修改。

实际上,Git 除了对整个提交计算 SHA-1 校验和以外,也对本次提交所引入的修改计算了校验和——即 “patch-id”。

如果你拉取被覆盖过的更新并将你手头的工作基于此进行变基的话,一般情况下 Git 都能成功分辨出哪些是你的修改,并把它们应用到新分支上。

  • 检查哪些提交是我们的分支上独有的(C2,C3,C4,C6,C7)

  • 检查其中哪些提交不是合并操作的结果(C2,C3,C4)

  • 检查哪些提交在对方覆盖更新时并没有被纳入目标分支(只有 C2 和 C3,因为 C4 其实就是 C4’)

  • 把查到的这些提交应用在 teamone/master 上面

在一个被变基然后强制推送的分支上再次执行变基。

Figure 48. 在一个被变基然后强制推送的分支上再次执行变基

要想上述方案有效,还需要对方在变基时确保 C4'C4 是几乎一样的。 否则变基操作将无法识别,并新建另一个类似 C4 的补丁(而这个补丁很可能无法整洁的整合入历史,因为补丁中的修改已经存在于某个地方了)。

在本例中另一种简单的方法是使用 git pull --rebase 命令而不是直接 git pull。 又或者你可以自己手动完成这个过程,先 git fetch,再 git rebase teamone/master

如果你习惯使用 git pull ,同时又希望默认使用选项 --rebase,你可以执行这条语句 git config --global pull.rebase true 来更改 pull.rebase 的默认配置。

如果你只对不会离开你电脑的提交执行变基,那就不会有事。 如果你对已经推送过的提交执行变基,但别人没有基于它的提交,那么也不会有事。 如果你对已经推送至共用仓库的提交上执行变基命令,并因此丢失了一些别人的开发所基于的提交, 那你就有大麻烦了,你的同事也会因此鄙视你。

如果你或你的同事在某些情形下决意要这么做,请一定要通知每个人执行 git pull --rebase 命令,这样尽管不能避免伤痛,但能有所缓解。

变基 vs. 合并

至此,你已在实战中学习了变基和合并的用法,你一定会想问,到底哪种方式更好。 在回答这个问题之前,让我们退后一步,想讨论一下提交历史到底意味着什么。

有一种观点认为,仓库的提交历史即是 记录实际发生过什么。 它是针对历史的文档,本身就有价值,不能乱改。 从这个角度看来,改变提交历史是一种亵渎,你使用 谎言 掩盖了实际发生过的事情。 如果由合并产生的提交历史是一团糟怎么办? 既然事实就是如此,那么这些痕迹就应该被保留下来,让后人能够查阅。

另一种观点则正好相反,他们认为提交历史是 项目过程中发生的事。 没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能方便使用。 持这一观点的人会使用 rebasefilter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。

现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案。 Git 是一个非常强大的工具,它允许你对提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。 既然你已经分别学习了两者的用法,相信你能够根据实际情况作出明智的选择。

总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。

留言與分享

git flow

分類 devops, git

Git Flow

Git Flow 是一种基于 Git 的分支模型,旨在帮助团队更好地管理和发布软件。

Git Flow 由 Vincent Driessen 在 2010 年提出,并通过一套标准的分支命名和工作流程,使开发、测试和发布过程更加有序和高效。

Git Flow 主要由以下几类分支组成:masterdevelopfeaturereleasehotfix

Git Flow 安装

Linux

Debian/Ubuntu:

1
sudo apt-get install git-flow

Fedora:

1
2
sudo dnf install gitflow
sudo apt-get install git-flow

macOS

在 macOS 上,你可以使用 Homebrew 来安装 Git Flow:

1
brew install git-flow

源码安装

如果你的发行版的包管理器中没有 Git Flow,你也可以从源代码进行安装:

1
2
3
git clone https://github.com/nvie/gitflow.git
cd gitflow
sudo make install

安装完成后,你可以通过以下命令验证 Git Flow 是否成功安装:

1
git flow version

Windows

在 Windows 上,你可以通过以下方式安装 Git Flow:

  • 使用 Git for Windows: Git for Windows 包含了 Git Flow。你可以从 Git for Windows 安装 Git,然后使用 Git Bash 来使用 Git Flow。

  • 使用 Scoop: 如果你使用 Scoop 包管理工具,可以通过以下命令安装 Git Flow:

    1
    scoop install git-flow
  • 使用 Chocolatey: 如果你使用 Chocolatey 包管理工具,可以通过以下命令安装 Git Flow:

    1
    choco install gitflow

Git Flow 分支模型

master 分支

  • 永远保持稳定和可发布的状态。
  • 每次发布一个新的版本时,都会从 develop 分支合并到 master 分支。

develop 分支

  • 用于集成所有的开发分支。
  • 代表了最新的开发进度。
  • 功能分支、发布分支和修复分支都从这里分支出去,最终合并回这里。

feature 分支

  • 用于开发新功能。
  • develop 分支创建,开发完成后合并回 develop 分支。
  • 命名规范:feature/feature-name

release 分支

  • 用于准备新版本的发布。
  • develop 分支创建,进行最后的测试和修复,然后合并回 developmaster 分支,并打上版本标签。
  • 命名规范:release/release-name

hotfix 分支

  • 用于修复紧急问题。
  • master 分支创建,修复完成后合并回 masterdevelop 分支,并打上版本标签。
  • 命名规范:hotfix/hotfix-name

分支操作原理

  • Master 分支上的每个 Commit 应打上 Tag,Develop 分支基于 Master 创建。
  • Feature 分支完成后合并回 Develop 分支,并通常删除该分支。
  • Release 分支基于 Develop 创建,用于测试和修复 Bug,发布后合并回 Master 和 Develop,并打 Tag 标记版本号。
  • Hotfix 分支基于 Master 创建,完成后合并回 Master 和 Develop,并打 Tag 1。

Git Flow 命令示例

  • 开始 Feature 分支:git flow feature start MYFEATURE
  • 完成 Feature 分支:git flow feature finish MYFEATURE
  • 开始 Release 分支:git flow release start RELEASE [BASE]
  • 完成 Release 分支:合并到 Master 和 Develop,打 Tag,删除 Release 分支。
  • 开始 Hotfix 分支:git flow hotfix start HOTFIX [BASE]
  • 完成 Hotfix 分支:合并到 Master 和 Develop,打 Tag,删除 Hotfix 分支。

Git Flow 工作流程

1. 初始化 Git Flow

首先,在项目中初始化 Git Flow。可以使用 Git Flow 插件(例如 git-flow)来简化操作。

1
git flow init

初始化时,你需要设置分支命名规则和默认分支。

2. 创建功能分支

当开始开发一个新功能时,从 develop 分支创建一个功能分支。

1
git flow feature start feature-name

完成开发后,将功能分支合并回 develop 分支,并删除功能分支。

1
git flow feature finish feature-name

3. 创建发布分支

当准备发布一个新版本时,从 develop 分支创建一个发布分支。

1
git flow release start release-name

在发布分支上进行最后的测试和修复,准备好发布后,将发布分支合并回 developmaster 分支,并打上版本标签。

1
git flow release finish release-name

4. 创建修复分支

当发现需要紧急修复的问题时,从 master 分支创建一个修复分支。

1
git flow hotfix start hotfix-name

修复完成后,将修复分支合并回 masterdevelop 分支,并打上版本标签。

1
git flow hotfix finish hotfix-name

实例操作

以下是一个实际使用 Git Flow 的综合实例。

初始化 Git Flow

1
git flow init

创建和完成功能分支

1
2
git flow feature start new-feature # 开发新功能
git flow feature finish new-feature

创建和完成发布分支

1
2
git flow release start v1.0.0 # 测试和修复
git flow release finish v1.0.0

创建和完成修复分支

1
2
git flow hotfix start hotfix-1.0.1. # 修复紧急问题
git flow hotfix finish hotfix-1.0.1

优点和缺点

优点

  • 明确的分支模型:清晰的分支命名和使用规则,使得开发过程井然有序。
  • 隔离开发和发布:开发和发布过程分离,减少了开发中的不确定性对发布的影响。
  • 版本管理:每次发布和修复都会打上版本标签,方便回溯和管理。

缺点

  • 复杂性:对于小型团队或简单项目,Git Flow 的分支模型可能显得过于复杂。
  • 频繁的合并:在大型团队中,频繁的分支合并可能导致合并冲突增加。

Git Flow 是一种结构化的分支管理模型,通过定义明确的分支和工作流程,帮助团队更好地管理软件开发和发布过程。虽然它增加了一定的复杂性,但对于大型项目和团队协作,Git Flow 提供了强大的支持和管理能力。

留言與分享

查看是否支持 SSL

首先在 MySQL 上执行如下命令, 查询是否 MySQL 支持 SSL:

1
2
3
4
5
6
7
mysql> SHOW VARIABLES LIKE 'have_ssl';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| have_ssl | YES |
+---------------+-------+
1 row in set (0.02 sec)

have_sslYES 时, 表示此时 MySQL 服务已经支持 SSL 了. 如果是 DESABLE, 则需要在启动 MySQL 服务时, 使能 SSL 功能.

使用 OpenSSL 创建 SSL 证书和私钥

首先我们需要使用 openssl 来创建服务器端的证书和私钥. 我使用的 openssl 版本为:

1
2
>>> /usr/local/Cellar/openssl/1.0.2j/bin/openssl version
OpenSSL 1.0.2j 26 Sep 2016

新建一个 ~/temp/cert 目录, 用于存放生成的证书和私钥

1
2
mkdir ~/temp/cert
cd ~/temp/cert

创建 CA 私钥和 CA 证书

然后, 我们先来生成一个 CA 私钥:

1
openssl genrsa 2048 > ca-key.pem

当有了一个 CA 私钥, 我们接下来就可以使用这个私钥生成一个新的数字证书:

1
openssl req -sha1 -new -x509 -nodes -days 3650 -key ca-key.pem > ca-cert.pem

执行这个命令时, 会需要填写一些问题, 随便填写就可以了. 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> openssl req -sha1 -new -x509 -nodes -days 3650 -key ca-key.pem > ca-cert.pem

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:Beijing
Locality Name (eg, city) []:Beijing
Organization Name (eg, company) [Internet Widgits Pty Ltd]:xys
Organizational Unit Name (eg, section) []:xys
Common Name (e.g. server FQDN or YOUR name) []:xys
Email Address []:yongshun1228@gmail.com

执行上述命令后, 我们就有了一个 CA 私钥和一个 CA 证书.

创建服务器端的 RSA 私钥和数字证书

接着, 我们需要创建服务器端的私钥和一个证书请求文件, 命令如下:

1
openssl req -sha1 -newkey rsa:2048 -days 3650 -nodes -keyout server-key.pem > server-req.pem

上面这个命令会生成一个新的私钥(server-key.pem), 同时会使用这个新私钥来生成一个证书请求文件(server-req.pem).
上面这个命令同样需要回答几个问题, 随便填写即可. 不过需要注意的是, A challenge password 这一项需要为空.
即:

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
>>> openssl req -sha1 -newkey rsa:2048 -days 3650 -nodes -keyout server-key.pem > server-req.pem

Generating a 2048 bit RSA private key
.................+++
..+++
writing new private key to 'server-key.pem'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:Beijing
Locality Name (eg, city) []:Beijing
Organization Name (eg, company) [Internet Widgits Pty Ltd]:xys
Organizational Unit Name (eg, section) []:xys
Common Name (e.g. server FQDN or YOUR name) []:xys
Email Address []:yongshun1228@gmail.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

下一步, 我们需要将生成的私钥转换为 RSA 私钥文件格式:

1
openssl rsa -in server-key.pem -out server-key.pem

最后一步, 我们需要使用原先生成的 CA 证书来生成一个服务器端的数字证书:

1
openssl x509 -sha1 -req -in server-req.pem -days 3650 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 > server-cert.pem

上面的命令会创建以服务器端的数字证书文件.

创建客户端的 RSA 私钥和数字证书

和服务器端所执行的命令类似, 我们也需要为客户端生成一个私钥和证书请求文件, 命令如下:

1
openssl req -sha1 -newkey rsa:2048 -days 3650 -nodes -keyout client-key.pem > client-req.pem

同样地, 我们需要将生成的私钥转换为 RSA 私钥文件格式:

1
openssl rsa -in client-key.pem -out client-key.pem

最后, 我们也需要为客户端创建一个数字证书:

1
openssl x509 -sha1 -req -in client-req.pem -days 3650 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 > client-cert.pem

使用工具创建证书与私钥

前面我们介绍了如何使用 OpenSSL 来创建 SSL 连接的私钥和证书文件, 现在我们来看一个更简单的方法.
在 MySQL 5.7 中, 提供了一个名为 mysql_ssl_rsa_setup 的工具, 通过它, 我们可以很方便地创建 SSL 连接所需要的各种文件:

1
2
3
mkdir ~/temp/cert
cd ~/temp/cert
mysql_ssl_rsa_setup --datadir ./

上面的命令中, --datadir 表示生成的文件的目录.

当执行了上述命令后, 也会生成八个文件:

1
2
3
4
5
6
7
8
ca-key.pem
ca.pem
client-cert.pem
client-key.pem
private_key.pem
public_key.pem
server-cert.pem
server-key.pem

这些文件和我们使用 OpenSSL 所创建的那八个文件的作用是一样的, 这里就不再详述了.

SSL 配置

在前面的步骤中, 我们已经生成了8个文件, 分别是:

  • ca-cert.pem: CA 证书, 用于生成服务器端/客户端的数字证书.

  • ca-key.pem: CA 私钥, 用于生成服务器端/客户端的数字证书.

  • server-key.pem: 服务器端的 RSA 私钥

  • server-req.pem: 服务器端的证书请求文件, 用于生成服务器端的数字证书.

  • server-cert.pem: 服务器端的数字证书.

  • client-key.pem: 客户端的 RSA 私钥

  • client-req.pem: 客户端的证书请求文件, 用于生成客户端的数字证书.

  • client-cert.pem: 客户端的数字证书.

接下来我们就需要分别配置服务器端和客户端.

服务器端配置

服务器端需要用到三个文件, 分别是: CA 证书, 服务器端的 RSA 私钥, 服务器端的数字证书, 我们需要在 [mysqld] 配置域下添加如下内容:

1
2
3
4
[mysqld]
ssl-ca=/etc/mysql/ca-cert.pem
ssl-cert=/etc/mysql/server-cert.pem
ssl-key=/etc/mysql/server-key.pem

接着我们还可以更改 bind-address, 使 MySQL 服务可以接收来自所有 ip 地址的客户端, 即:

1
bind-address = *

当配置好后, 我们需要重启 MySQL 服务, 使能配置.

最后一步, 我们添加一个需要使用 SSL 才可以登录的帐号, 来验证一下我们所配置的 SSL 是否生效:

1
2
GRANT ALL PRIVILEGES ON *.* TO 'ssl_test'@'%' IDENTIFIED BY 'ssl_test' REQUIRE SSL;
FLUSH PRIVILEGES;

当配置好后, 使用 root 登录 MySQL, 执行 show variables like '%ssl%' 语句会有如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> show variables like '%ssl%';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| have_openssl | YES |
| have_ssl | YES |
| ssl_ca | ca.pem |
| ssl_capath | |
| ssl_cert | server-cert.pem |
| ssl_cipher | |
| ssl_crl | |
| ssl_crlpath | |
| ssl_key | server-key.pem |
+---------------+-----------------+
9 rows in set (0.01 sec)

客户端配置

客户端配置相对简单一些. 首先我们需要拷贝 ca-cert.pem, client-cert.pemclient-key.pem 这三个文件到客户端主机中, 然后我们可以执行如下命令来使用 SSL 连接 MySQL 服务:

1
mysql --ssl-ca=/path/to/ca-cert.pem --ssl-cert=/path/to/client-cert.pem --ssl-key=/path/to/client-key.pem -h host_name -u ssl_test -p

除了上述的使用命令行方式配置 SSL 外, 我们也可以使用配置文件的方式. 即在 ~/.my.cnf 文件中添加如下内容即可:

1
2
3
4
[client]
ssl-ca=/path/to/ca-cert.pem
ssl-cert=/path/to/client-cert.pem
ssl-key=/path/to/client-key.pem

当连接成功后, 我们执行如下指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> \s
--------------
mysql Ver 14.14 Distrib 5.7.17, for Linux (x86_64) using EditLine wrapper

Connection id: 14
Current database:
Current user: ssl_test@172.17.0.4
SSL: Cipher in use is DHE-RSA-AES256-SHA
Current pager: stdout
Using outfile: ''
Using delimiter: ;
Server version: 5.7.17 MySQL Community Server (GPL)
Protocol version: 10
Connection: test_db via TCP/IP
Server characterset: latin1
Db characterset: latin1
Client characterset: latin1
Conn. characterset: latin1
TCP port: 3306
Uptime: 1 hour 2 min 9 sec

Threads: 1 Questions: 23 Slow queries: 0 Opens: 126 Flush tables: 3 Open tables: 0 Queries per second avg: 0.006
--------------

如果输出中有 SSL: Cipher in use is DHE-RSA-AES256-SHA 之类的信息, 则表示已经使用 SSL 来连接了.

在 Docker 中使能 MySQL SSL 连接

上面我们简单介绍了一下如果使能 MySQL SSL 连接, 那么现在我们使用 Docker 来具体的实战一把吧!

首先拉取最新的 MySQL 镜像:

1
docker pull mysql

然后需要准备一下挂载到 Docker 容器的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> cd ~/temp
>>> tree
.
├── cert
│ ├── ca-key.pem
│ ├── ca.pem
│ ├── client-cert.pem
│ ├── client-key.pem
│ ├── private_key.pem
│ ├── public_key.pem
│ ├── server-cert.pem
│ └── server-key.pem
├── config
│ └── my.cnf
└── db

3 directories, 9 files

在 temp 目录下有三个子目录:

  • cert 目录用于存放我们先前生成的证书和私钥信息;

  • config 目录用于存放 MySQL 服务的配置文件

  • db 目录是用于存放 MySQL 的数据.

下一步我们需要使用如下命令启动 MySQL 容器:

1
docker run --rm --name test_db -p 10000:3306 -e MYSQL_ROOT_PASSWORD=root -v /Users/xiongyongshun/temp/db:/var/lib/mysql -v /Users/xiongyongshun/temp/config:/etc/mysql/conf.d -v /Users/xiongyongshun/temp/cert:/etc/mysql/cert mysql:latest

我们在上面的命令中, 我们分别挂载了 cert, config, db 这三个宿主机上的目录到 MySQL 容器中.

启动了 MySQL 服务后, 可以先使用 root 帐号登录 MySQL, 来检查 MySQL 服务此时是否已经开启了 SSL 功能:

1
docker run -it --link test_db:test_db --rm  mysql sh -c 'exec mysql -u root -p -h test_db'

登录成功后, 我们在 MySQL 中执行如下指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> show variables like '%ssl%';
+---------------+---------------------------------+
| Variable_name | Value |
+---------------+---------------------------------+
| have_openssl | YES |
| have_ssl | YES |
| ssl_ca | /etc/mysql/cert/ca-cert.pem |
| ssl_capath | |
| ssl_cert | /etc/mysql/cert/server-cert.pem |
| ssl_cipher | |
| ssl_crl | |
| ssl_crlpath | |
| ssl_key | /etc/mysql/cert/server-key.pem |
+---------------+---------------------------------+
9 rows in set (0.01 sec)

有上面的输出后, 表明此时 MySQL 服务已经使用 SSL 功能了.

接着下一步, 我们按照前面所提到的, 创建一个仅仅可以使用 SSL 登录的帐号, 来检验我们的配置是否有效:

1
2
GRANT ALL PRIVILEGES ON *.* TO 'ssl_test'@'%' IDENTIFIED BY 'ssl_test' REQUIRE SSL;
FLUSH PRIVILEGES;

上面的命令创建了一个帐号名为 ssl_test, 密码为 ssl_test, 并且不限制登录主机 ip 的帐号.

这些都配置成功后, 我们再启动一个 MySQL 客户端容器:

1
docker run -it --link test_db:test_db --rm -v /Users/xiongyongshun/temp/cert:/etc/mysql/cert mysql sh -c 'exec mysql --ssl-ca=/etc/mysql/cert/ca-cert.pem --ssl-cert=/etc/mysql/cert/client-cert.pem --ssl-key=/etc/mysql/cert/client-key.pem -h test_db -u ssl_test -p'

从上面的这个命令中我们可以看到, 启动 MySQL 客户端容器时, 我们挂载了宿主机的 cert 目录到容器内的 /etc/mysql/cert 目录, 这样在容器中就可以访问到 SSL 私钥和证书文件了. 接着我们在 MySQL 客户端命令行中, 使用 --ssl-ca, --ssl-cert, --ssl-key 这三个参数来指定 SSL 连接所需要的 CA 证书, RSA 私钥和客户端证书.

登录成功后, 我们执行 s 命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> \s
--------------
mysql Ver 14.14 Distrib 5.7.17, for Linux (x86_64) using EditLine wrapper

Connection id: 5
Current database:
Current user: ssl_test@172.17.0.5
SSL: Cipher in use is DHE-RSA-AES256-SHA
Current pager: stdout
Using outfile: ''
Using delimiter: ;
Server version: 5.7.17 MySQL Community Server (GPL)
Protocol version: 10
Connection: test_db via TCP/IP
Server characterset: latin1
Db characterset: latin1
Client characterset: latin1
Conn. characterset: latin1
TCP port: 3306
Uptime: 6 min 8 sec

Threads: 2 Questions: 10 Slow queries: 0 Opens: 113 Flush tables: 1 Open tables: 106 Queries per second avg: 0.027
--------------

输出中有 SSL: Cipher in use is DHE-RSA-AES256-SHA 信息则说明我们确实是使用了 SSL 连接的 MySQL 服务器.

留言與分享

CentOs7.3 安装 MySQL 5.7.19 二进制版本

参考官网 - 使用通用二进制文件在Unix / Linux上安装MySQL

MySQL社区版 下载地址

采用二进制方式免编译安装MySQL,适合各类MySQL产品系列,不需要复杂的编译设置和编译时间等待,直接解压下载的软件包,初始化即可完成MySQL的安装和启动.

1.准备工作

依赖环境

关闭防火墙

1
$ systemctl stop firewalld.service

MySQL依赖于libaio 库

1
2
$ yum search libaio
$ yum install libaio

下载,解压,重命名

通常解压在 /usr/local/mysql

mysql-5.7.19-linux-glibc2.12-x86_64 文件夹,重命名成mysql,这样就凑成/usr/local/mysql目录了

1
2
3
4
$ cd /opt/
$ wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.19-linux-glibc2.12-x86_64.tar.gz
$ tar -zxvf /opt/mysql-5.7.19-linux-glibc2.12-x86_64.tar.gz -C /usr/local/
$ mv /usr/local/mysql-5.7.19-linux-glibc2.12-x86_64/ /usr/local/mysql

解压目录内容

bin mysqld服务器,客户端和实用程序
data 日志文件,数据库
docs MySQL手册信息格式
man Unix手册页
include 包含(标题)文件
lib
share 其他支持文件,包括错误消息,示例配置文件,用于数据库安装的SQL

2.安装MySQL

1. 新建用户组和用户

1
2
3
$ cd /usr/local/mysql/ 
$ groupadd mysql
$ useradd mysql -g mysql

2. 创建目录并授权

1
2
3
4
$ mkdir data mysql-files
$ chmod 750 mysql-files
$ chown -R mysql .
$ chgrp -R mysql .

3. 初始化MySQL

1
$ bin/mysqld --initialize --user=mysql # MySQL 5.7.6 and up

注意密码

4. mysql 临时密码

[注意]root@localhost生成临时密码:;b;s;)/rn6A3,也就是root@localhost:后的字符串

1
2
3
4
5
6
2017-08-26T03:23:35.368366Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
2017-08-26T03:23:35.748679Z 0 [Warning] InnoDB: New log files created, LSN=45790
2017-08-26T03:23:35.793190Z 0 [Warning] InnoDB: Creating foreign key constraint system tables.
2017-08-26T03:23:35.848286Z 0 [Warning] No existing UUID has been found, so we assume that this is the first time that this server has been started. Generating a new UUID: f210c54b-8a0d-11e7-abbd-000c29129bb0.
2017-08-26T03:23:35.848889Z 0 [Warning] Gtid table is not ready to be used. Table 'mysql.gtid_executed' cannot be opened.
2017-08-26T03:23:35.849421Z 1 [Note] A temporary password is generated for root@localhost: ;b;s;)/rn6A3

5. 生成RSA私钥,可以跳过此步骤

mysql_ssl_rsa_setup需要openssl支持,用于启用数据量ssl连接,需要进一步配置。

参考-MySQL 使用 SSL 连接

1
$ bin/mysql_ssl_rsa_setup 

6. 授予读写权限

1
2
$ chown -R root .
$ chown -R mysql data mysql-files

7. 添加到MySQL 启动脚本到系统服务

1
$ cp support-files/mysql.server /etc/init.d/mysql.server

3.启动MySQL服务

启动脚本有两个分别是:

/usr/local/mysql/bin/mysqld_safe
/usr/local/mysql/support-files/mysql.server(即/etc/init.d/mysqld

当启动mysqld时,mysqld_safe同时启动

mysqld_safe监控mysqld服务,记录错误日志,并在mysqld因故障停止时将其重启

启动方式一

1
$ bin/mysqld_safe --user=mysql &

启动方式二

1
$ service mysql.server start

或者

1
/usr/local/mysql/support-files/mysql.server start

如若出现报错

1
2
Starting MySQL.2017-08-26T07:31:24.312411Z mysqld_safe error: log-error set to '/var/log/mariadb/mariadb.log', however file don't exists. Create writable for user 'mysql'.
ERROR! The server quit without updating PID file (/var/lib/mysql/node1.pid).

给日志目录授予读写权限

1
2
3
$ mkdir /var/log/mariadb
$ touch /var/log/mariadb/mariadb.log
$ chown -R mysql:mysql /var/log/mariadb

4.登录MySQL

1
2
$ /usr/local/mysql/bin/mysql -uroot -p
Enter password:

如果不知道密码
密码在,安装MySQL步骤 4 ,有提到,怎么找初始化临时密码

如若出现报错

1
2
Enter password: 
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

故障分析

查看mysql实例的状态

1
2
$ netstat -ntlp  | grep 3306
tcp6 0 0 :::3306 :::* LISTEN 10794/mysqld

查看my.cnf关于socket的配置

1
2
$ more /etc/my.cnf |grep sock
socket=/var/lib/mysql/mysql.sock

解决方法,修改/etc/my.cnf

1
$ vi /etc/my.cnf

修改 [mysqld]组下的 socket 路径,我是选择注释掉,加一行为tmp/mysql.soc

1
2
3
4
[mysqld]
datadir=/var/lib/mysql
#socket=/var/lib/mysql/mysql.sock
socket=/tmp/mysql.sock

重启MySQL 服务

1
2
$ service mysql.server start
Shutting down MySQL.. SUCCESS!

再次登录

1
$ /usr/local/mysql/bin/mysql -uroot -p

如果不知道密码
密码在,安装MySQL步骤 4 ,有提到,怎么找初始化临时密码

设置MySQL密码

登陆成功后,设置MySQL密码

1
mysql> ALTER USER   'root'@'localhost' identified by 'mima';  

或者

1
mysql> set password=password("mima");

刷新权限

1
2
mysql> flush privileges;
mysql> exit;

查看mysql.user表中存在哪些帐户 以及它们的密码是否为空:

MySQL 5.7.6起,使用这个语句:

1
2
3
4
5
6
7
8
mysql> SELECT User, Host, HEX(authentication_string) FROM mysql.user;
+---------------+-----------+------------------------------------------------------------------------------------+
| User | Host | HEX(authentication_string) |
+---------------+-----------+------------------------------------------------------------------------------------+
| root | localhost | 2A39383730334637413534333934344644333831383037373636394637344436303631364442324338 |
| mysql.session | localhost | 2A5448495349534E4F544156414C494450415353574F52445448415443414E42455553454448455245 |
| mysql.sys | localhost | 2A5448495349534E4F544156414C494450415353574F52445448415443414E42455553454448455245 |
+---------------+-----------+------------------------------------------------------------------------------------+

开启远程登录

关闭防火墙

1
$ systemctl stop firewalld.service

以权限用户root登录

1
$ /usr/local/mysql/bin/mysql -uroot -p
1
2
3
mysql> use mysql;
mysql> update user set host = '%' where user ='root';
mysql> flush privileges;

第1行:选择mysql库
第2行:修改host值(以通配符%的内容增加主机/IP地址),当然也可以直接增加IP地址
第3行:刷新MySQL的系统权限相关表

或者

1
2
mysql> grant all privileges on *.*  to  'root'@'%'  identified by 'mima'  with grant option;
mysql> flush privileges;

推荐阅读

CentOs7.3 搭建 MySQL 5.7.19 主从复制,以及复制实现细节分析

留言與分享

git教程

分類 devops, git
初始化仓库:
1
git init

如果当前目录下有几个文件想要纳入版本控制,需要先用 git add 命令告诉 Git 开始对这些文件进行跟踪,然后提交:

1
2
3
$ git add *.c
$ git add README
$ git commit -m 'initial project version'

image

忽略某些文件

我们可以创建一个名为 .gitignore 的文件,列出要忽略的文件模式。
文件 .gitignore 的格式规范如下:

  • 所有空行或者以注释符号 # 开头的行都会被 Git 忽略。
  • 可以使用标准的 glob 模式匹配。
  • 匹配模式最后跟反斜杠(/)说明要忽略的是目录。
  • 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。

所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。星号(*)匹配零个或多个任意字符;[abc] 匹配任何一个列在方括号中的字符(这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c);问号(?)只匹配一个任意字符;如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。

我们再看一个 .gitignore 文件的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 此为注释 – 将被 Git 忽略
# 忽略所有 .a 结尾的文件
*.a
# 但 lib.a 除外
!lib.a
# 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
/TODO
# 忽略 build/ 目录下的所有文件
build/
# 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt
doc/*.txt
# 忽略 doc/ 目录下所有扩展名为 txt 的文件
doc/**/*.txt
跟踪新文件

使用命令 git add 开始跟踪一个新文件。所以,要跟踪 README 文件,运行:

1
$ git add README
提交更新

每次准备提交前,先用 git status 看下,是不是都已暂存起来了,然后再运行提交命令 git commit:

1
$ git commit

这种方式会启动文本编辑器以便输入本次提交的说明。(默认会启用 shell 的环境变量 $EDITOR 所指定的软件,一般都是 vim 或 emacs。当然也可以按照第一章介绍的方式,使用 ++git config --global core.editor++ 命令设定你喜欢的编辑软件。)

默认的提交消息包含最后一次运行 git status 的输出,放在注释行里,另外开头还有一空行,供你输入提交说明。你完全可以去掉这些注释行,不过留着也没关系,多少能帮你回想起这次更新的内容有哪些。(如果觉得这还不够,可以用 -v 选项将修改差异的每一行都包含到注释中来。)退出编辑器时,Git 会丢掉注释行,将说明内容和本次更新提交到仓库。

另外也可以用 +±m++ 参数后跟提交说明的方式,在一行命令中提交更新:

跳过使用暂存区域
Git 提供了一个跳过使用暂存区域的方式,只要在提交的时候,给 git commit 加上 -a 选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤

移除文件

要从 Git 中移除某个文件,就必须要从已跟踪文件清单中移除(确切地说,是从暂存区域移除),然后提交。可以用 git rm 命令完成此项工作,并连带从工作目录中删除指定的文件,这样以后就不会出现在未跟踪文件清单中了。

移动文件

要在 Git 中对文件改名,可以这么做:

1
$ git mv file_from file_to

运行 git mv 就相当于运行了下面三条命令:

1
2
3
$ mv README.txt README
$ git rm README.txt
$ git add README
查看提交历史

默认不用任何参数的话,git log 会按提交时间列出所有的更新,最近的更新排在最上面。看到了吗,每次更新都有一个 SHA-1 校验和、作者的名字和电子邮件地址、提交时间,最后缩进一个段落显示提交说明。

git log 有许多选项可以帮助你搜寻感兴趣的提交,接下来我们介绍些最常用的。

我们常用 -p 选项展开显示每次提交的内容差异,用 -2 则仅显示最近的两次更新

某些时候,单词层面的对比,比行层面的对比,更加容易观察。Git 提供了 +±-word-diff++ 选项。我们可以将其添加到 ++git log -p++ 命令的后面,从而获取单词层面上的对比。在程序代码中进行单词层面的对比常常是没什么用的。不过当你需要在书籍、论文这种很大的文本文件上进行对比的时候,这个功能就显出用武之地了。

另外,git log 还提供了许多摘要选项可以用,比如 --stat,仅显示简要的增改行数统计

用 oneline 或 format 时结合 --graph 选项,可以看到开头多出一些 ASCII 字符串表示的简单图形,形象地展示了每个提交所在的分支及其分化衍合情况。

另外还有按照时间作限制的选项,比如 --since 和 --until。

撤消操作

有时候我们提交完了才发现漏掉了几个文件没有加,或者提交信息写错了。想要撤消刚才的提交操作,可以使用 --amend 选项重新提交:

1
$ git commit --amend

撤消操作

远程仓库的使用

查看当前的远程库
要查看当前配置有哪些远程仓库,可以用 git remote 命令,它会列出每个远程库的简短名字。
也可以加上 -v 选项(译注:此为 --verbose 的简写,取首字母),显示对应的克隆地址

添加远程仓库

要添加一个新的远程仓库,可以指定一个简单的名字,以便将来引用,运行

1
git remote add [shortname] [url]:

现在可以用字符串 pb 指代对应的仓库地址了。比如说,要抓取所有 Paul 有的,但本地仓库没有的信息,可以运行 git fetch pb

从远程仓库抓取数据

正如之前所看到的,可以用下面的命令从远程仓库抓取数据到本地:

1
$ git fetch [remote-name]

推送数据到远程仓库

目进行到一个阶段,要同别人分享目前的成果,可以将本地仓库中的数据推送到远程仓库。实现这个任务的命令很简单:

1
git push [remote-name] [branch-name]。

如果要把本地的 master 分支推送到 origin 服务器上(再次说明下,克隆操作会自动使用默认的 master 和 origin 名字),可以运行下面的命令:

1
$ git push origin master

查看远程仓库信息
我们可以通过命令

1
git remote show [remote-name]

查看某个远程仓库的详细信息

远程仓库的删除和重命名
在新版 Git 中可以用 git remote rename 命令修改某个远程仓库在本地的简称,比如想把 pb 改成 paul,可以这么运行:

1
2
3
4
$ git remote rename pb paul
$ git remote
origin
paul

碰到远端仓库服务器迁移,或者原来的克隆镜像不再使用,又或者某个参与者不再贡献代码,那么需要移除对应的远端仓库,可以运行 git remote rm 命令:

1
2
3
$ git remote rm paul
$ git remote
origin
打标签

打标签

Git 命令别名

Git 并不会推断你输入的几个字符将会是哪条命令,不过如果想偷懒,少敲几个命令的字符,可以用 git config 为命令设置别名。来看看下面的例子:

1
2
3
4
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status

取消暂存文件时的输入比较繁琐,可以自己设置一下:

1
$ git config --global alias.unstage 'reset HEAD --'

这样一来,下面的两条命令完全等同:

1
2
$ git unstage fileA
$ git reset HEAD fileA

显然,使用别名的方式看起来更清楚。另外,我们还经常设置 last 命令:

1
$ git config --global alias.last 'log -1 HEAD'

然后要看最后一次的提交信息,就变得简单多了

分支的新建与切换

要新建并切换到该分支,运行 git checkout 并加上 -b 参数:

1
2
$ git checkout -b iss53
Switched to a new branch 'iss53'

回到 master 分支并把它合并进来,然后发布到生产服务器。用 git merge 命令来进行合并:

1
2
3
4
5
6
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
README | 1 -
1 file changed, 1 deletion(-)

这相当于执行下面这两条命令:

1
2
$ git branch iss53
$ git checkout iss53

如果顺着一个分支走下去可以到达另一个分支的话,那么 Git 在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)。

合并之后,master 分支和 hotfix 分支指向同一位置。
由于当前 hotfix 分支和 master 都指向相同的提交对象,所以 hotfix 已经完成了历史使命,可以删掉了。使用 git branch 的 -d 选项执行删除操作:

1
2
$ git branch -d hotfix
Deleted branch hotfix (was 3a0874c).
遇到冲突时的分支合并

有时候合并操作并不会如此顺利。如果在不同的分支中都修改了同一个文件的同一部分,Git 就无法干净地把两者合到一起(译注:逻辑上说,这种问题只能由人来裁决。)。
Git 作了合并,但没有提交,它会停下来等你解决冲突。要看看哪些文件在合并时发生冲突,可以用 git status 查阅

任何包含未解决冲突的文件都会以未合并(unmerged)的状态列出。Git 会在有冲突的文件里加入标准的冲突解决标记,可以通过它们来手工定位并解决这些冲突。可以看到此文件包含类似下面这样的部分:

1
2
3
4
5
6
7
<<<<<<< HEAD
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53

可以看到 ======= 隔开的上半部分,是 HEAD(即 master 分支,在运行 merge 命令时所切换到的分支)中的内容,下半部分是在 iss53 分支中的内容。解决冲突的办法无非是二者选其一或者由你亲自整合到一起。比如你可以通过把这段内容替换为下面这样来解决:

1
2
3
<div id="footer">
please contact us at email.support@github.com
</div>

这个解决方案各采纳了两个分支中的一部分内容,而且我还删除了 <<<<<<<,======= 和 >>>>>>> 这些行。在解决了所有文件里的所有冲突后,运行 git add 将把它们标记为已解决状态(译注:实际上就是来一次快照保存到暂存区域。)。

再运行一次 git status 来确认所有冲突都已解决:

1
2
3
4
5
6
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified: index.html

如果觉得满意了,并且确认所有冲突都已解决,也就是进入了暂存区,就可以用 git commit 来完成这次合并提交。提交的记录差不多是这样:

1
2
3
4
5
6
7
8
9
10
Merge branch 'iss53'

Conflicts:
index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
# .git/MERGE_HEAD
# and try again.
#
查看当前的远程库

要查看当前配置有哪些远程仓库,可以用 git remote 命令,它会列出每个远程库的简短名字。
可以加上 -v 选项(译注:此为 --verbose 的简写,取首字母),显示对应的克隆地址

添加远程仓库

要添加一个新的远程仓库,可以指定一个简单的名字,以便将来引用,运行

1
git remote add [shortname] [url]:
从远程仓库抓取数据

正如之前所看到的,可以用下面的命令从远程仓库抓取数据到本地:

1
$ git fetch [remote-name]

推送数据到远程仓库
项目进行到一个阶段,要同别人分享目前的成果,可以将本地仓库中的数据推送到远程仓库。实现这个任务的命令很简单: git push [remote-name] [branch-name]。如果要把本地的 master 分支推送到 origin 服务器上(再次说明下,克隆操作会自动使用默认的 master 和 origin 名字),可以运行下面的命令:

1
$ git push origin master

只有在所克隆的服务器上有写权限,或者同一时刻没有其他人在推数据,这条命令才会如期完成任务。如果在你推数据前,已经有其他人推送了若干更新,那你的推送操作就会被驳回。你必须先把他们的更新抓取到本地,合并到自己的项目中,然后才可以再次推送。

查看远程仓库信息
我们可以通过命令

1
git remote show [remote-name]

查看某个远程仓库的详细信息

远程仓库的删除和重命名
在新版 Git 中可以用 git remote rename 命令修改某个远程仓库在本地的简称,比如想把 pb 改成 paul,可以这么运行:

1
2
3
4
$ git remote rename pb paul
$ git remote
origin
paul

注意,对远程仓库的重命名,也会使对应的分支名称发生变化,原来的 pb/master 分支现在成了 paul/master。

碰到远端仓库服务器迁移,或者原来的克隆镜像不再使用,又或者某个参与者不再贡献代码,那么需要移除对应的远端仓库,可以运行 git remote rm 命令:

1
2
3
$ git remote rm paul
$ git remote
origin
远程分支

我们用 ==(远程仓库名)/(分支名)== 这样的形式表示远程分支。比如我们想看看上次同 origin 仓库通讯时 master 分支的样子,就应该查看 origin/master 分支。如果你和同伴一起修复某个问题,但他们先推送了一个 iss53 分支到远程仓库,虽然你可能也有一个本地的 iss53 分支,但指向服务器上最新更新的却应该是 origin/iss53 分支。

可以运行 git fetch origin 来同步远程服务器上的数据到本地。该命令首先找到 origin 是哪个服务器(本例为 git.ourcompany.com),从上面获取你尚未拥有的数据,更新你本地的数据库,然后把 origin/master 的指针移到它最新的位置上
image

为了演示拥有多个远程分支(在不同的远程服务器上)的项目是如何工作的,我们假设你还有另一个仅供你的敏捷开发小组使用的内部服务器 git.team1.ourcompany.com。可以用第二章中提到的 git remote add 命令把它加为当前项目的远程分支之一。我们把它命名为 teamone,以便代替完整的 Git URL 以方便使用
image
现在你可以用 git fetch teamone 来获取小组服务器上你还没有的数据了。由于当前该服务器上的内容是你 origin 服务器上的子集,Git 不会下载任何数据,而只是简单地创建一个名为 teamone/master 的远程分支,指向 teamone 服务器上 master 分支所在的提交对象 31b8e(见图 3-26)。

image

推送本地分支

要想和其他人分享某个本地分支,你需要把它推送到一个你拥有写权限的远程仓库。你创建的本地分支不会因为你的写入操作而被自动同步到你引入的远程服务器上,你需要明确地执行推送分支的操作。

如果你有个叫 serverfix 的分支需要和他人一起开发,可以运行 git push (远程仓库名) (分支名):

1
2
3
4
5
6
7
$ git push origin serverfix
Counting objects: 20, done.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (15/15), 1.74 KiB, done.
Total 15 (delta 5), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
* [new branch] serverfix -> serverfix

这里其实走了一点捷径。Git 自动把 serverfix 分支名扩展为 refs/heads/serverfix:refs/heads/serverfix,意为“取出我在本地的 serverfix 分支,推送到远程仓库的 serverfix 分支中去”。

也可以运行 ==git push origin serverfix:serverfix== 来实现相同的效果,它的意思是“上传我本地的 serverfix 分支到远程仓库中去,仍旧称它为 serverfix 分支”。通过此语法,你可以把本地分支推送到某个命名不同的远程分支:若想把远程分支叫作 awesomebranch,可以用 git push origin serverfix:awesomebranch 来推送数据。

跟踪远程分支

从远程分支 checkout 出来的本地分支,称为 跟踪分支 (tracking branch)。跟踪分支是一种和某个远程分支有直接联系的本地分支。在跟踪分支里输入 git push,Git 会自行推断应该向哪个服务器的哪个分支推送数据。同样,在这些分支里运行 git pull 会获取所有远程索引,并把它们的数据都合并到本地分支中来。

在克隆仓库时,Git 通常会自动创建一个名为 master 的分支来跟踪 origin/master。这正是 git push 和 git pull 一开始就能正常工作的原因。当然,你可以随心所欲地设定为其它跟踪分支,比如 origin 上除了 master 之外的其它分支。刚才我们已经看到了这样的一个例子:git checkout -b [分支名] [远程名]/[分支名]。如果你有 1.6.2 以上版本的 Git,还可以用 --track 选项简化:

1
2
3
$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

要为本地分支设定不同于远程分支的名字,只需在第一个版本的命令里换个名字:

1
2
3
$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch serverfix from origin.
Switched to a new branch 'sf'

现在你的本地分支 sf 会自动将推送和抓取数据的位置定位到 origin/serverfix 了

删除远程分支

如果不再需要某个远程分支了,比如搞定了某个特性并把它合并进了远程的 master 分支(或任何其他存放稳定代码的分支),可以用这个非常无厘头的语法来删除它:git push [远程名] :[分支名]。如果想在服务器上删除 serverfix 分支,运行下面的命令:

1
2
3
$ git push origin :serverfix
To git@github.com:schacon/simplegit.git
- [deleted] serverfix

咚!服务器上的分支没了。你最好特别留心这一页,因为你一定会用到那个命令,而且你很可能会忘掉它的语法。有种方便记忆这条命令的方法:==记住我们不久前见过的 git push [远程名] [本地分支]:[远程分支] 语法,如果省略 [本地分支],那就等于是在说“在这里提取空白然后把它变成[远程分支]”==。

解决远程分支和本地冲突

1.先将本地修改储存起来

1
git stash

2.pull内容

1
git pull

3.还原暂存内容

1
git stash pop stash@{0}

4.解决冲突

5.提交

1
git commit -m ''
  1. 打标签
1
git tag v1.1 -m ''

7.列标签

1
git tag -l

8.获取标签代码

1
git checkout 

9.提交标签到远程端

1
git push tag 
拉取本地没有的远程分支
1
git checkout -b 本地分支名 origin/远程分支名

后续:变基rebase

留言與分享

Scala-2.13.0 安装及配置

分類 编程语言, scala

Scala 简介

Scala 是一门多范式(multi-paradigm)的编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性。

Scala 运行在Java虚拟机上,并兼容现有的Java程序。

Scala 源代码被编译成Java字节码,所以它可以运行于JVM之上,并可以调用现有的Java类库。

准备工作

环境

1
2
JDK:1.8  
Scala:Scala-2.13.0

依赖环境

Scala 语言可以运行在Window、Linux、Unix、 Mac OS X等系统上。

Scala是基于java之上,大量使用java的类库和变量,必须使用Scala之前必须先安装 Java

安装 JDK

CentOs7.3 安装 JDK1.8

安装

确保你本地以及安装了 JDK 1.8 以上版本,并且设置了 JAVA_HOME 环境变量及 JDK 的bin目录。

下载解压

解压缩文件包,可将其移动至/lib/下:

1
2
3
4
5
su hadoop
cd /home/hadoop/
wget https://downloads.lightbend.com/scala/2.13.0-M2/scala-2.13.0-M2.tgz
sudo tar -zxvf scala-2.13.0-M2.tgz -C /lib/
sudo mv /lib/scala-2.13.0-M2/ /lib/scala

环境变量

如果是对所有的用户都生效就修改vi /etc/profile 文件
如果只针对当前用户生效就修改 vi ~/.bahsrc 文件

1
sudo vi /etc/profile
1
2
3
#scala
export SCALA_HOME=/lib/scala
export PATH=${SCALA_HOME}/bin:$PATH

使环境变量生效,运行 source /etc/profile使/etc/profile文件生效

验证

:wq!保存退出,重启终端,执行 scala 命令,输出以下信息,表示安装成功:

1
scala
1
2
3
4
5
6
Welcome to Scala 2.13.0-M2 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_144).
Type in expressions for evaluation. Or try :help.

scala> scala> println("Hello World!")
Hello World!
scala>

留言與分享

Hadoop简介

2003-2004年,Google公开了部分GFS和Mapreduce思想的细节,以此为基础Doug Cutting等人用了2年业余时间实现了DFS和Mapreduce机制,一个微缩版:Nutch

Hadoop 于 2005 年秋天作为 Lucene的子项目 Nutch的一部分正式引入Apache基金会。2006 年 3 月份,Map-Reduce 和 Nutch Distributed File System (NDFS) 分别被纳入称为 Hadoop 的项目中

Hadoop

分布式存储系统HDFS (Hadoop Distributed File System )

  • 分布式存储系统
  • 提供了 高可靠性、高扩展性和高吞吐率的数据存储服务

分布式计算框架MapReduce

  • 分布式计算框架
  • 具有 易于编程、高容错性和高扩展性等优点。

HDFS优点

高容错性

  • 数据自动保存多个副本
  • 副本丢失后,自动恢复

适合批处理

  • 移动计算而非数据
  • 数据位置暴露给计算框架

适合大数据处理

  • GB 、TB 、甚至PB 级数据
  • 百万规模以上的文件数量
  • 10K+ 节点

可构建在廉价机器上

  • 通过多副本提高可靠性
  • 提供了容错和恢复 机制

HDFS缺点

低延迟数据访问

  • 比如毫秒级
  • 低延迟与高吞吐率

小文件存取

  • 占用NameNode 大量内存
  • 寻道时间超过读取时间

并发写入、文件随机修改

  • 一个文件只能有一个写者
  • 仅支持append

准备工作

环境

1
2
3
4
5
6
7
JDK:1.8  
Hadoop Release:2.7.4
centos:7.3

node1(master) 主机: 192.168.252.121
node2(slave1) 从机: 192.168.252.122
node3(slave2) 从机: 192.168.252.123

安装 JDK

CentOs7.3 安装 JDK1.8

SSH 免秘钥

CentOs7.3 Hadoop 用户 ssh 免密登录

创建用户

建议创建一个单独的用户Hadoop以从Unix文件系统隔离Hadoop文件系统

1
2
3
4
$ useradd hadoop
$ passwd hadoop
New password:
Retype new password:

授权 root 权限,在root下面加一条hadoophadoop ALL=(ALL) ALL

1
2
3
4
5
$ chmod 777 /etc/sudoers
$ vi /etc/sudoers
root ALL=(ALL) ALL
hadoop ALL=(ALL) NOPASSWD:ALL
$ pkexec chmod 0440 /etc/sudoers

免秘钥登录

安装

下载解压

在 ndoe1 上操作

1
2
3
4
su hadoop
cd /home/hadoop/
wget https://mirrors.tuna.tsinghua.edu.cn/apache/hadoop/common/hadoop-2.7.4/hadoop-2.7.4.tar.gz
tar -zxvf hadoop-2.7.4.tar.gz

环境变量

如果是对所有的用户都生效就修改vi /etc/profile 文件
如果只针对当前用户生效就修改 vi ~/.bahsrc 文件

1
sudo vi /etc/profile
1
2
3
#hadoop
export PATH=${HADOOP_HOME}/bin:$PATH
export HADOOP_HOME=/home/hadoop/hadoop-2.7.4/

使环境变量生效,运行 source /etc/profile使/etc/profile文件生效

配置Hadoop

进入hadoop 配置文件目录

1
cd /home/hadoop/hadoop-2.7.4/etc/hadoop/

编辑 hadoop-env.sh 文件,找到 JAVA_HOME 改为 JDK 的安装目录

1
sudo vi hadoop-env.sh
1
export JAVA_HOME=/lib/jvm

修改 core-site.xml

打开 core-site.xml文件并对其进行编辑,如下图所示。

1
vi core-site.xml
1
2
3
4
5
6
7
8
9
10
<configuration>
<property>
<name>fs.defaultFS</name>
<value>hdfs://node1:9000</value>
</property>
<property>
<name>hadoop.tmp.dir</name>
<value>file:/home/hadoop/hadoop-2.7.4/tmp</value>
</property>
</configuration>

修改 hdfs-site.xml

打开hdfs-site.xml文件并对其进行编辑,如下图所示。

1
vi hdfs-site.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<configuration>
<property>
<name>dfs.namenode.secondary.http-address</name>
<value>node1:50090</value>
</property>
<property>
<name>dfs.replication</name>
<value>2</value>
</property>
<property>
<name>dfs.namenode.name.dir</name>
<value>file:/home/hadoop/hadoop-2.7.4/tmp/dfs/name</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>file:/home/hadoop/hadoop-2.7.4/tmp/dfs/data</value>
</property>
</configuration>

修改 mapred-site.xml

目录下么没有这个文件,这有一个模板,我们需要先拷贝一份

1
2
cp mapred-site.xml.template mapred-site.xml
vi mapred-site.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<configuration>
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
<property>
<name>mapreduce.jobhistory.address</name>
<value>node1:10020</value>
</property>
<property>
<name>mapreduce.jobhistory.webapp.address</name>
<value>node1:19888</value>
</property>
</configuration>

修改 yarn-site.xml

1
vi yarn-site.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
<configuration>

<!-- Site specific YARN configuration properties -->

<property>
<name>yarn.resourcemanager.hostname</name>
<value>node1</value>
</property>
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
</configuration>

配置集群

复制节点

将 hadoop-2.7.4 文件夹重打包后复制到其他子节点

1
2
3
4
5
cd /home/hadoop/

tar zcvf hadoop.tar.gz hadoop-2.7.4
scp hadoop.tar.gz hadoop@node2:/home/hadoop/
scp hadoop.tar.gz hadoop@node3:/home/hadoop/

在其他子节点 解压

1
tar -zxvf hadoop.tar.gz

配置slaves文件

修改(Master主机)node1/etc/hadoop/slaves该文件指定哪些服务器节点是datanode节点。删除locahost,添加所有datanode节点的主机名

1
2
3
4
5
cd /home/hadoop/hadoop-2.7.4/etc/hadoop/

[hadoop@node1 hadoop]$ cat slaves
node2
node3

集群操作

Format

格式化namenode和datanode并启动,(在master上执行就可以了 不需要在slave上执行)

1
2
3
4
cd /home/hadoop/hadoop-2.7.4/bin

./hadoop namenode -format
./hadoop datanode -format

启动 hadoop

关闭防火墙

1
systemctl stop firewalld.service
1
2
3
4
5
6
cd /home/hadoop/hadoop-2.7.4/sbin


./start-dfs.sh
./start-yarn.sh
./mr-jobhistory-daemon.sh start historyserver

或者

1
2
3
./start-all.sh

./mr-jobhistory-daemon.sh start historyserver

查看进程服务

查看启动进程,缺少以下任一进程都表示出错

1
2
3
4
5
$ jps
2528 NameNode
2720 SecondaryNameNode
2872 ResourceManager
3151 JobHistoryServer

查看端口占用情况

1
netstat -tnlp | grep java

访问node1

1
2
3
http://192.168.252.121:50070

http://192.168.252.121:8088

图片描述

图片描述

图片描述

停止 hadoop

1
cd /home/hadoop/hadoop-2.7.4/sbin
1
./stop-all.sh

或者 jps 查看进程kill

留言與分享

iptables

分類 服务端, linux

基本操作


查看iptable版本

1
rpm -q iptables

查看iptables 规则

1
2
3
service iptables status
# 或
service -L --line-numbers

iptables的4个表分别是:

  • filter(过滤):数据包过滤/拦截,可以包含INPUT、FORWARD、OUTPUT这3个内置chain。
  • nat(地址转换):IP地址或端口号转换,可以包含PREROUTING、OUTPUT、POSTROUTING 3个内置chain,nat table在会话建立时会记录转换的对应关系,同一会话的回包和后续报文会自动地址转换,这是因为nat使用了ip_conntrack模块。
  • mangle(包管理):用来修改IP报文,可以包含PREROUTING、OUTPUT、INPUT、FORWARD、POSTROUTING 5个内置chain。
  • raw:此表的优先级高于ip_conntrack模块和其它的table,主要用于将有会话状态的连接(比如tcp)的数据包排除在会话外。可以包含POSTROUTING、OUTPUT两个内置chain。

看到这里肯定会有这样的疑问,为什么table只能包含一些而不是全部的chain呢?我想这个构架是按需设计而不是按功能设计的,尽管table不是包含所有的chain,但是每种功能的table都包含了实现这种功能所需的chain,即使包含更多的chain也是累赘或者无用的,而且实际上用起来也的确如此,够用了。

还有就是不同table生效优先顺序问题,先后优先级是这样的:

raw > mangle > nat > filter

所以,如果有filter禁止ping目的地址2.2.2.2,而nat又有策略将目的地址1.1.1.1转换成2.2.2.2,那么ping 1.1.1.1是ping不通的。

不过一般情况下filter是不会和nat的策略打起架来,比如INPUT chain能做filter,却不能做nat,PREROUTING能做nat却不能做filter,而且PREROUTING只能做目的地址转换,不会对源地址过滤的需求造成麻烦,所以通常是不会相互干扰的。

规则链(chain)

iptables内置的5个chain:PREROUTING、INPUT、OUPUT、FORWARD、POSTROUGING,这5个chain分别与netfilter中数据转发路径上的5个不同的位置挂钩,以匹配筛选不同类型的数据流,如下图所示:

其中:

  • PREROUTING链:应用于所有进入机器的ip包,包括目的地址是本机和目的地址非本机的包。
  • INPUT链:应用于所有目的是本机的包,也就是目的IP是本机接口地址,所有发给本地socket的数据都经过它。
  • OUPUT链:应用于所有由本机产生的包,所有应用程序发出的数据都经过它。
  • FORWARD链:应用于所有经过路由决策被转发的包,也就是目的地址不是本机的数据包。
  • POSTROUGING链:应用于所有发出机器的IP包,包括本机发出的和从本机转发的数据包。

策略匹配按照重上到下的顺序进行,当测试到某策略匹配时执行target并跳出,不再向下匹配,当测试到最后一条策略仍不匹配时,则采用policy指定的动作,如下图:

除了内置chain外,还可以自定义chain,自定义chain并不能利用netfilter的hook来捕捉数据包,但是可用于策略的分类,比如有3类不同的用户访问主机上的不同服务,如果所有策略都放在INPUT chain中策略会多而难以维护,这个时候就可以定义3个自定义chain,分别配置不同的策略,同时在INPUT chain中添加策略对来访者分类并将目标指向3个自定义chain。

自定义chain大显神威的地方在于动态生成策略,例如VPN服务器上,需要对不同分组的用户区别对待管理,但是用户IP是随机分配的,不能根据IP来区分用户组,这时候可以预先定义好各组chain,利用VPN服务端软件的一些钩子,当用户登陆时自动添加策略引导到自定义chain上来匹配。如果这时候没有自定义chain,那么策略的数量将是(用户数×所属组策略数),每增加一个用户,都要把所属组的全部策略添加一遍,这样大量的时间花费在策略匹配上,性能下降很快。


各指令的含义


-APPEND -A

1
iptables -A chain firewall-rule

将规则添加到末尾。

一般情况下,iptables 中最后的规则是丢弃所有数据包。

于是使用-A参数添加的规则在丢弃规则后,将不起作用。


-DELETE -D

1
2
3
4
5
iptables -D chain firewall-rule
# 删除对应规则direwall-rule的那一条
# 或
iptables -D chain rulenum
# 删除chain中编号为rulenum的那一条.1表示第一条。

-INSERT -I

1
iptables -I chain [rulenum] firewall-rule.

将firewall-rule添加为chain中的第rulenum条规则,原先的第rulenum条规则及以后的各条顺次+1。如未指定rulenum,则默认为1.


-REPLACE -R

1
iptables -R chain [rulenum] firewall-rule.

将firewall-rule添加为chain中的第rulenum条规则,原先的第rulenum条规则及以后的各条顺次+1。如未指定rulenum,则默认为1.


-LIST -L

1
iptables -L chain [rulenum] firewall-rule.

列出chain或所有chain的第rulenum条规则或所有规则,在-L后再加上–lube-numbers,则显示序号


-LIST-RULES -S

1
iptables -S chain [rulenum] 

打印全部规则


-FLUSH -F

1
iptables -F chain

情况chain(或全部chain)中的规则


-ZERO -Z

1
iptables -Z chain [rulenum] 

清空chain或所有chain的包和字节计数器


-NEW -N

1
iptables -N chain [rulenum] 

创建名称为chain的新链


-DELETE-CHAIN -X

1
iptables -X [chain] 

删除用户自定义chain或所有用户定义的chain,该指令不影响预设规则


-POLICY -P

1
iptables -P chain target 

改变chain的策略为target


-RENAME-CHAIN -E

1
iptables -E old-chain new-chain

将old-chain名更换为new-chain,使得old-chain的规则失效


各参数的含义


-P 代表协议(PROTOCOL)

  • 指明当前规则针对的传输协议(如 TCP、UDP、ICMP 等)
  • 可能的参数值有:tcp, udp, icmp, all
  • 使用 “all” 表示适用于所有协议。而如果在规则中不指定 -p 参数,则默认使用 “all” 参数。一般不使用 “all” 这个值,要么指定某个特定的协议,要么就指定 -p 参数。
  • -p 的参数值既可以用名称(如 tcp)也可以用协议对应的数值(如 6 代表 tcp 协议)
  • /etc/protocols 文件中包含了所有允许的协议名称和相应数值
  • 也可以用长的参数名 –protocol

-S 代表源地址(SOURCE)

  • 指定数据包的源地址
  • 可以是 ip 地址,或者网络地址,或者主机名(hostname)
  • 例如:-s 192.168.1.101 表示针对特定的 ip 地址
  • 对于网络掩码,使用 /mask。例如,“-s 192.168.1.0/24″ 表示网络掩码为 255.255.255.0 的所有 192.168.1.x 地址都匹配。
  • 如果不指定 -s 参数,默认匹配所有源地址
  • 也可以用长参数名 –src 或者 –source

-D 代表目的地址(DESTINATION)

  • 指定数据包的目的地址
  • 使用方式与上面的 “-s” 一样(不同之处仅在于 -s 指源,而 -d 表示目的地址)
  • 也可以用长参数名 –dst 或者 –destination

-J 代表跳转(TARGET)

  • j 的意思是 “jump”(跳转) 到目标
  • 指定当某个数据包满足该规则的时候的就跳转到的下一个处理规则,而不再顺序执行后面的规则判断
  • 可能的值有:ACCEPT, DROP, QUEUE, RETURN,分别表示接受、丢弃、进入队列,返回(跳出,通常是从某个 chain 中跳回到调用该 chain 的上层 chain)
  • 也可以跳转到某个自定义的 chain 中,使用该 chain 的名称做为跳转目标

-I 代表 IN INTERFACE(入站接口)

  • i 表示 “input interface”(输入接口,即,指定网络数据处理的网卡,一般 eth0 即表示第一块有线网卡的外网接口,lo 表示局域网接口)
  • 可以直接理解为 “-i” 表示接口。不过,-i 和 -o 都表示接口,-i 表示输入时的接口,而 -o 特指输出用的接口。
  • 指定数据包进入 INPUT、FORWARD 和 PREROUTING 链时经由的接口。
  • 例如:-i eth0 表示该规则应该针对从 eth0 接口进来的数据包。
  • 如果不指定 -i 参数,则经由系统中所有可用的接口进入的数据包都可以匹配该规则。
  • 也可以使用长参数 –in-interface

-O 代表 OUT INTERFACE(出站接口)

  • o 表示 “output interface”(出站经由接口)
  • 指定发送出去的数据包进入 INPUT、FORWARD 和 PREROUTING 链时经由的接口。
  • 如果不指定 -o 参数,则经由系统中所有可用的接口发出的数据包都可以匹配该规则。
  • 也可以使用长参数 –out-interface

其它防火墙参数选项

上面某些防火墙参数还有属于它自己的参数选项,可以与其配合使用。下面是一些常用的选项。

要使用这些参数选项,需要指定相应的参数,例如,要使用 “–sport” 选项,应该在规则中指定 “-p tcp” (或者 “-p udp”)等参数。

注意:所有这些选项前面都是 –(2个短横线 – )。

–SPORT 表示 SOURCE PORT (源端口,用于 -P TCP, 或者 -P UDP)

  • 默认匹配所有端口(未特别指定时)
  • 可以指定端口号(数字),也可以指定端口名称。例如,默认的 SSH 端口号码为 22,名称为 ssh,可以表示为 “–sport 22″ 或者 “–sport ssh”。
  • /etc/services 文件包含了所有允许的端口名称和对应的端口号码。
  • 在规则中使用号码比使用名称要好(效率高些)
  • 要匹配一个端口范围,使用英文半角冒号(:),如 22:100 匹配从 22 到 100 的所有端口号。
  • 也可以使用长名称 –source-port

–DPORT 表示 DESTINATION PORT (目的端口,-P TCP, 或者 -P UDP)

  • 与 –sport 的用法相同,区别仅在于对象是目的端口
  • 也可以使用长名称 –destination-port

–TCP-FLAGS 表示 TCP FLAGS (用于 -P TCP)

  • 可以使用英文半角逗号(,)来指定多个 TCP 状态标识
  • 可能值有:SYN, ACK, FIN, RST, URG, PSH。可以全用,也可以不用。

–ICMP-TYPE 表示 ICMP TYPE (ICMP 类型,用于 -P ICMP)

  • 当使用 icmp 协议 “-p icmp” 的时候,可以使用 “–icmp-type” 特别指定 ICMP 类型
  • 例如,使用 “–icmp-type 0″ 表示 “Echo Reply”,“–icmp-type 8″ 表示 “Echo”。

添加VIP:

1
ifconfig eth0:1 192.168.1.22 broadcast 192.168.1.2 netmask 255.255.255.0 up

留言與分享

linux基础

分類 后端, linux

Linux 基础操作笔记

1. 文件操作

1.1 创建文件

1
2
3
4
5
# 创建单个文件
touch file

# 批量创建文件(创建love_1_linux.txt到love_10_linux.txt)
touch love_{1..10}_linux.txt

1.2 文件权限查看

1
ls -l  # 详细列表显示文件权限

输出示例:

1
-rw-r--r-- 1 user group 1024 Mar 1 10:00 example.txt

权限字段说明:

1
2
[文件类型][所有者权限][组权限][其他用户权限]
d rwx r-x r-x
  • 文件类型:-普通文件,d目录,l链接文件
  • 权限组成:r读(4) + w写(2) + x执行(1)

2. 帮助系统

1
2
man <command>  # 查看命令手册
man ls # 示例:查看ls命令手册

常用操作键:

  • 空格键:向下翻页
  • q:退出手册
  • /:搜索内容

3. 用户管理

3.1 用户查看命令

1
2
3
4
whoami        # 显示当前用户名
who am i # 显示登录信息
who -m # 同"who am i"
who mom likes # 显示特定用户信息(需实际用户存在)

3.2 用户操作

1
2
3
4
5
6
7
8
9
10
11
# 创建用户(自动创建home目录)
sudo adduser lilei

# 查看已创建用户
ls /home

# 切换用户
su -l lilei # "-l"模拟完整登录环境

# 删除用户及home目录
sudo deluser lilei --remove-home

4. 用户组管理

4.1 用户组查看

1
2
3
4
5
# 查看指定用户所属组
groups shiyanlou

# 查看所有组(按字母排序)
cat /etc/group | sort

/etc/group 文件格式:

1
group_name:password:GID:user_list

4.2 用户组操作

1
2
# 添加用户到组(需要root权限)
sudo usermod -aG sudo lilei # 示例:添加sudo组

5. 权限管理

5.1 文件权限修改

1
2
3
4
5
6
7
8
# 变更文件所有者
sudo chown shiyanlou iphone6

# 变更文件所属组
sudo chgrp group_name file

# 修改所有权限(所有者/组/其他用户)
chmod 755 filename # rwxr-xr-x

5.2 权限数字表示法

权限 数字
r– 4
-w- 2
–x 1
rwx 7 (4+2+1)

6. 文件类型说明

Linux 一切皆文件,主要类型包括:

  • -:普通文件
  • d:目录文件
  • l:符号链接(相当于快捷方式)
  • c:字符设备文件
  • b:块设备文件
  • s:套接字文件
  • p:管道文件

7. 重要补充说明

  1. 目录权限特殊含义:

    • 读权限:可查看目录内容
    • 写权限:可创建/删除文件
    • 执行权限:可进入目录
  2. 新建用户默认会:

    • 创建同名用户组
    • /home下创建用户目录
    • 基本配置文件从/etc/skel复制
  3. sudo权限配置路径:

    1
    2
    /etc/sudoers
    /etc/sudoers.d/

建议后续可补充:

  • 实际权限修改案例
  • 用户组管理实验
  • sudoers文件的配置方法
  • ACL高级权限控制

留言與分享

19.python异步IO

分類 编程语言, python

在IO编程一节中,我们已经知道,CPU的速度远远快于磁盘、网络等IO。在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。这种情况称为同步IO。

在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。

因为一个IO操作就阻塞了当前线程,导致其他代码无法执行,所以我们必须使用多线程或者多进程来并发执行代码,为多个用户服务。每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的线程不受影响。

多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降。

由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的一种方法。

另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。

可以想象如果按普通顺序写出的代码实际上是没法完成异步IO的:

1
2
3
4
5
do_some_code()
f = open('/path/to/file', 'r')
r = f.read() # <== 线程停在此处等待IO操作结果
# IO操作完成后线程才能继续执行:
do_some_code(r)

所以,同步IO模型的代码是无法实现异步IO模型的。

异步IO模型需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程:

1
2
3
4
loop = get_event_loop()
while True:
event = loop.get_event()
process_event(event)

消息模型其实早在应用在桌面应用程序中了。一个GUI程序的主线程就负责不停地读取消息并处理消息。所有的键盘、鼠标等消息都被发送到GUI程序的消息队列中,然后由GUI程序的主线程处理。

由于GUI线程处理键盘、鼠标等消息的速度非常快,所以用户感觉不到延迟。某些时候,GUI线程在一个消息处理的过程中遇到问题导致一次消息处理时间过长,此时,用户会感觉到整个GUI程序停止响应了,敲键盘、点鼠标都没有反应。这种情况说明在消息模型中,处理一个消息必须非常迅速,否则,主线程将无法及时处理消息队列中的其他消息,导致程序看上去停止响应。

消息模型是如何解决同步IO必须等待IO操作这一问题的呢?当遇到IO操作时,代码只负责发出IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮消息处理过程。当IO操作完成后,将收到一条“IO完成”的消息,处理该消息时就可以直接获取IO操作结果。

在“发出IO请求”到收到“IO完成”的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。

在学习异步IO模型前,我们先来了解协程。

协程,又称微线程,纤程。英文名Coroutine。

协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。

子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。

所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。

子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。比如子程序A、B:

1
2
3
4
5
6
7
8
9
def A():
print('1')
print('2')
print('3')

def B():
print('x')
print('y')
print('z')

假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:

1
2
3
4
5
6
1
2
x
y
3
z

但是在A中是没有调用B的,所以协程的调用比函数调用理解起来要难一些。

看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

Python对协程的支持是通过generator实现的。

在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。

但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。

来看例子:

传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。

如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'

def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()

c = consumer()
produce(c)

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

注意到consumer函数是一个generator,把一个consumer传入produce后:

  1. 首先调用c.send(None)启动生成器;
  2. 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
  3. consumer通过yield拿到消息,处理,又通过yield把结果传回;
  4. produce拿到consumer处理的结果,继续生产下一条消息;
  5. produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

整个流程无锁,由一个线程执行,produceconsumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

最后套用Donald Knuth的一句话总结协程的特点:

“子程序就是协程的一种特例。”

参考源码

coroutine.py

asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。

asyncio的编程模型就是一个消息循环。asyncio模块内部实现了EventLoop,把需要执行的协程扔到EventLoop中执行,就实现了异步IO。

asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。

为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法asyncawait,可以让coroutine的代码更简洁易读。

asyncio实现Hello world代码如下:

1
2
3
4
5
6
7
8
9
import asyncio

async def hello():
print("Hello world!")
# 异步调用asyncio.sleep(1):
await asyncio.sleep(1)
print("Hello again!")

asyncio.run(hello())

async把一个函数变成coroutine类型,然后,我们就把这个async函数扔到asyncio.run()中执行。执行结果如下:

1
2
3
Hello!
(等待约1秒)
Hello again!

hello()会首先打印出Hello world!,然后,await语法可以让我们方便地调用另一个async函数。由于asyncio.sleep()也是一个async函数,所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,就接着执行下一行语句。

asyncio.sleep(1)看成是一个耗时1秒的IO操作,在此期间,主线程并未等待,而是去执行EventLoop中其他可以执行的async函数了,因此可以实现并发执行。

上述hello()还没有看出并发执行的特点,我们改写一下,让两个hello()同时并发执行:

1
2
3
4
5
6
7
8
# 传入name参数:
async def hello(name):
# 打印name和当前线程:
print("Hello %s! (%s)" % (name, threading.current_thread))
# 异步调用asyncio.sleep(1):
await asyncio.sleep(1)
print("Hello %s again! (%s)" % (name, threading.current_thread))
return name

asyncio.gather()同时调度多个async函数:

1
2
3
4
5
async def main():
L = await asyncio.gather(hello("Bob"), hello("Alice"))
print(L)

asyncio.run(main())

执行结果如下:

1
2
3
4
5
6
Hello Bob! (<function current_thread at 0x10387d260>)
Hello Alice! (<function current_thread at 0x10387d260>)
(等待约1秒)
Hello Bob again! (<function current_thread at 0x10387d260>)
Hello Alice again! (<function current_thread at 0x10387d260>)
['Bob', 'Alice']

从结果可知,用asyncio.run()执行async函数,所有函数均由同一个线程执行。两个hello()是并发执行的,并且可以拿到async函数执行的结果(即return的返回值)。

如果把asyncio.sleep()换成真正的IO操作,则多个并发的IO操作实际上可以由一个线程并发执行。

我们用asyncio的异步网络连接来获取sina、sohu和163的网站首页:

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
import asyncio

async def wget(host):
print(f"wget {host}...")
# 连接80端口:
reader, writer = await asyncio.open_connection(host, 80)
# 发送HTTP请求:
header = f"GET / HTTP/1.0\r\nHost: {host}\r\n\r\n"
writer.write(header.encode("utf-8"))
await writer.drain()

# 读取HTTP响应:
while True:
line = await reader.readline()
if line == b"\r\n":
break
print("%s header > %s" % (host, line.decode("utf-8").rstrip()))
# Ignore the body, close the socket
writer.close()
await writer.wait_closed()
print(f"Done {host}.")

async def main():
await asyncio.gather(wget("www.sina.com.cn"), wget("www.sohu.com"), wget("www.163.com"))

asyncio.run(main())

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
wget www.sohu.com...
wget www.sina.com.cn...
wget www.163.com...
(等待一段时间)
(打印出sohu的header)
www.sohu.com header > HTTP/1.1 200 OK
www.sohu.com header > Content-Type: text/html
...
(打印出sina的header)
www.sina.com.cn header > HTTP/1.1 200 OK
www.sina.com.cn header > Date: Wed, 20 May 2015 04:56:33 GMT
...
(打印出163的header)
www.163.com header > HTTP/1.0 302 Moved Temporarily
www.163.com header > Server: Cdn Cache Server V2.0
...

可见3个连接由一个线程并发执行3个async函数完成。

小结

asyncio提供了完善的异步IO支持,用asyncio.run()调度一个coroutine

在一个async函数内部,通过await可以调用另一个async函数,这个调用看起来是串行执行的,但实际上是由asyncio内部的消息循环控制;

在一个async函数内部,通过await asyncio.gather()可以并发执行若干个async函数。

参考源码

hello.py

gather.py

wget.py

asyncio可以实现单线程并发IO操作。如果仅用在客户端,发挥的威力不大。如果把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程+async函数实现多用户的高并发支持。

asyncio实现了TCP、UDP、SSL等协议,aiohttp则是基于asyncio实现的HTTP框架。

我们先安装aiohttp

1
$ pip install aiohttp

然后编写一个HTTP服务器,分别处理以下URL:

  • / - 首页返回Index Page
  • /{name} - 根据URL参数返回文本Hello, {name}!

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# app.py
from aiohttp import web

async def index(request):
text = "<h1>Index Page</h1>"
return web.Response(text=text, content_type="text/html")

async def hello(request):
name = request.match_info.get("name", "World")
text = f"<h1>Hello, {name}</h1>"
return web.Response(text=text, content_type="text/html")

app = web.Application()

# 添加路由:
app.add_routes([web.get("/", index), web.get("/{name}", hello)])

if __name__ == "__main__":
web.run_app(app)

直接运行app.py,访问首页:

Index

访问http://localhost:8080/Bob

Hello

使用aiohttp时,定义处理不同URL的async函数,然后通过app.add_routes()添加映射,最后通过run_app()以asyncio的机制启动整个处理流程。

参考源码

app.py

留言與分享

作者的圖片

Kein Chan

這是獨立全棧工程師Kein Chan的技術博客
分享一些技術教程,命令備忘(cheat-sheet)等


全棧工程師
資深技術顧問
數據科學家
Hit廣島觀光大使


Tokyo/Macau