Month: 十月 2018

influxdb 排坑记

相同点覆盖

Duplicate points

A point is uniquely identified by the measurement name, tag set, and timestamp. If you submit Line Protocol with the same measurement, tag set, and timestamp, but with a different field set, the field set becomes the union of the old field set and the new field set, where any conflicts favor the new field set.

相同的 measurement,tagset 和 timestamp 的数据会覆盖

解决方法:

  1. 提高时间精度
  2. 增加一个tag来标识不同的点
  3. 预处理数据 ✔️

每个点的 tag 数量

opentsdb 限制为每个点 8 个 tag

influxdb 限制不明确,最好也不要太多

在设计数据库是需要考虑的是,同一类型的字段作为 tag key 还是 tag value?

两种情况分别为:

time        key1 key2 key3
1500000000  true true true
1500000001  true false false
time        key   value
1500000000  _key1 true
1500000000  _key2 true
1500000000  _key3 true
1500000001  _key1 true
1500000001  _key2 false
1500000001  _key3 false

假设某个类型的字段可选值为 k 个,而一个用有 n 个这样的字段。如果作为 tag key,那么可能数据库的 series 复杂度是 O(k^n)。而作为 tag value,那么数据库的复杂度是 O(k * n)。也就是第二种的 series 复杂度大大降低。

但是对于第二种方法,效果就是打的点更多。第一种方法打点 m 个的时候,第二种需要 m * n 个。

另外考虑对于 down sampling 的友好性,虽然第二种造成点更多,但是在进行 down sampling 的时候有更好的压缩效率。

https://docs.influxdata.com/influxdb/v1.2/concepts/schema_and_data_layout/#don-t-have-too-many-series

max-values-per-tag 限制

每个 tag 可选值也有限制,不能超过10万,tag用于标识点,而不是用于存储数据,如果需要存储数据,应该使用 fields。

比如:

  • 域名:可选范围很大
  • 邮箱:可选范围几乎无限大
  • UUID

max-series-per-database 限制

tagset 的定义:每一个点的 tag key 和 tag value 的组合
series 的定义:使用同一个 measurement,retention policy,tagset 组合的点的集合

每一个数据库中的 series 的数量不能超过 100 万

以上连个限制的原因在于,influxdb 为每个 series 建立了索引并且常驻内存,如果过大

tag 只能存储字符串

一般来说,在 tag 中应该存储的是 enum 的值,而不是任意的字符串。

推荐配置

https://docs.influxdata.com/influxdb/v1.6/concepts/schema_and_data_layout/

如果想要使用 group by,推荐使用 tags
如果需要使用函数(mean,sum), 只有 fields 才能
如果需要使用数字,只有 fields 才能

硬件配置

https://docs.influxdata.com/influxdb/v1.6/guides/hardware_sizing/

Linux 中的 Process Group 和 Session

使用了这么多年的 Ubuntu, 自以为 Linux 下进程的概念已经很熟悉了, 然而发现进程组(Process Group)和会话(Session)两个概念日常并不会接触很多, 平时也没有注意, 导致今天遇到一个问题还想了半天才想明白.

看了一些讲进程控制的书和文章, 感觉都比较老了, 不少都还在讲 double fork 的原理及意义, 而现实是 systemd 已经接管了几乎整个 Linux 世界, double fork 这种东西真的不应该存在了, 至少在新的程序中不应该再使用了, 所以有了这篇文章.

# 引子–问题

我们知道在命令行运行的前台命令, 可以随时通过 Ctrl-C 关闭掉. 原理很简单, 当我们按下 Ctrl-C 的时候, shell 进程会向前台进程发送一个 SIGINT 信号, 进程收到 SIGINT 的默认操作就是退出. 按照这个思路出发, 在 fork 之后, 如果按下 Ctrl-C 应该只有主进程会关闭, 而子进程应该继续运行, 实际上并不是这样的, 两个进程都收到了 SIGINT 信号.

“`
import os
import sys
import time

def child():
while True:
try:
sys.stdout.write(“child process\n”)
sys.stdout.flush()
time.sleep(5)
except KeyboardInterrupt:
sys.stdout.write(“child sigint\n”)
sys.stdout.flush()
sys.exit()

def main():
while True:
try:
sys.stdout.write(“main process\n”)
sys.stdout.flush()
time.sleep(4)
except KeyboardInterrupt:
sys.stdout.write(“main sigint\n”)
sys.stdout.flush()
sys.exit()

pid = os.fork()

if pid != 0:
main()
else:
child()
“`

当我们按下 Ctrl-C 的时候

“`
main process
child process
main process
child process
^Cchild sigint
main sigint

“`

也就是说上述说法并不是完全正确的. 实际上, SIGINT 并不只会发送给前台进程, 而是发送给**前台进程组**中的每一个进程. 那么什么是进程组呢?

# 进程 — Process

要说进程组, 我们首先来回忆一下进程的概念. 进程可以理解为 “进行中的程序”, 在 Linux 上可以通过 fork 来创建新的进程, 然后可以使用 exec 来在子进程或者父进程中执行新的程序.

## 进程退出的情况

当一个进程的子进程退出的时候, 父进程有义务对子进程的状态进行回收(wait). 子进程退出的时候, 父进程会收到 SIGCHLD 信号. 如果子进程退出了, 而父进程又还没有进行回收, 那么在这段时间内, 这个子进程被称为僵尸进程(zombie process), 僵尸进程会持续占用一部分系统资源, 所以最好还是尽快回收. 如果父进程没有进行回收, 也退出了, pid=1 的 init 进程会接管僵尸状态的子进程并进行回收.

当一个进程的父进程退出时, 这个进程被称为孤儿进程(orphaned process), 子进程会被 init 进程接管, 也就是说, 子进程的 ppid 会变成 1. 但是, 默认情况下, 子进程并不会收到任何信号. 不过, 可以使用 prctl 系统调用来设置在父进程退出的时候, 子进程收到什么信号.

# 进程组 — Process Group

顾名思义就是一组进程. 进程组的 id (pgid) 就是进程组组长(group leader)的 pid. 当一个进程 fork 的时候, 子进程默认是和父进程在同一个进程组的. 从 shell 中启动一个进程的时候, shell 会给这个进程设置为一个新的进程组. 如果使用了 pipe, 那么 shell 会将这些进程放入同一个进程组, 比如 `cat hello | less`

需要注意的是, 当进程组的 leader 退出的时候, 进程组的其他进程并不会受影响, 系统不会给孤儿进程发送任何信号. 一个进程组在最后一个进程退出时消失.

## 相关函数

“`
getpgid(pid) – 获得指定 pid 对应的 pgid
setpgid(pid, pgid) – 设定指定进程的 pgid
“`

其中可以用 0 来表示当前进程, 如果设置当前进程的 pgid 为自己的 pid, 也就是钦点自己为 group leader, 那么就相当于创建了一个新的进程组.

## 相关命令

kill 命令用来给 pid 发送信号, 一般命令形式是 `kill -SIG PID`, 可以在PID参数前面加上 `-` 表示一个 Process Group, 而不是 Process. 比如:

“`
kill -TERM -6379 # 向 6379 进程组发送 TERM 信号
“`

# 回到问题

那么我们现在可以再思考一下刚开始的问题, 为什么按 Ctrl-C 的时候, 父进程和子进程都会收到 SIGINT 信号呢? 答案之前说了:实际上, SIGINT 并不只会发送给前台进程, 而是发送给前台进程组中的每一个进程. 而父进程和子进程当前所在的组正是前台进程组.

前台进程组是一个 session 中在前台运行的那一组进程, 那么什么又是 session 呢?

# 会话 — session

session 是一个更大的概念, 一个 session 中可以包含多个 process group.

他们的关系是这样的:

“`
+————————————————————–+
| |
| pg1 pg2 pg3 pg4 |
| +——+ +——-+ +—–+ +——+ |
| | bash | | sleep | | cat | | jobs | |
| +——+ +——-+ +—–+ +——+ |
| session leader | wc | |
| +—–+ |
| |
+————————————————————–+
session
“`

和 process group 一样, 每个 session 也有一个 leader, session leader 就是 这个进程的 pid. session 的本意是用来作业控制, 每个用户登录的时候都会创建自己的 session. 一般来说在 shell 中, session leader 就是 shell 本身.

## 相关函数

“`
getsid(pid) – 获得指定 pid 对应的 sid
setsid() – 创建新的session
“`

其中需要注意的是, setsid 不能由 group leader 进程来调用, 因为这样会导致同一个 group 中的进程属于不同的 session, 所以 POSIX 标准直接禁止了这么做.

## session 退出

当一个session leader 退出时, 其他进程不会受到任何影响, 但是因为 session leader 退出可能造成 orphaned process group, 因此在shell中, **一般情况下会造成进程退出的情况**

## Orphaned Process Group

当一个 group leader 退出的时候, 本身并不会对进程组造成任何影响, 也不会收到任何信号. 但是, 当一个进程组变成孤儿进程组(orphaned process group)的时候, 可能会收到一些信号.

> 孤儿进程组
>
> A process group is called orphaned when the parent of every member is either in the process group or outside the session. In particular, the process group of the session leader is always orphaned.

如果一个进程组中的所有进程的父进程都在组内或者都是其他 session 的进程(比如 init)的时候, 这个进程组被称为孤儿进程组. 显然, 每个进程的退出或者移出进程组都可能造成进程组变成孤儿进程组.

**如果这时候进程组中的某个进程的状态是 STOP, 那么内核会向该进程组的所有进程发送 SIGHUP, 并紧接着发送 SIGCONT 信号.**

值得注意的是, session leader 本身就是一个孤儿进程组了, 所以退出的时候不会给本组的进程发信号, 下面要用到.

为什么内核要这么做呢?

一般情况下, shell 进程是当前 session 的 leader, 当我们运行每个命令的时候都会创建一个新的 Process Group, 如果这时候某个孤儿进程组中有进程是 STOP 状态的, 那么可能就再也没有机会运行了, 所以系统首先发送 SIGHUP 信号退出, 如果有进程对 SIGINT 做了处理, 那么在收到 SIGCONT 信号之后又可以继续运行了.

也就是说当我们退出 shell 的时候, 内核会向 session 中的

1. 前台进程组
2. 孤儿进程组

发送 SIGHUP 信号, 从而退出他们. 那么问题来了, 后台进程组呢?

答案是: shell 会向session的所有进程组发送 SIGHUP 信号, 所以运行中的后台进程组也会退出.

## daemonize

在 Unix 的上古时期, 没有 Process Manager 这个概念, 所以每个守护进程(比如说 apache)都需要自己变成守护进程, 一般来说是通过 double fork 的形式:

1. fork 第一次, 确保自己不是 group leader
2. setsid, 创建新的 session
3. fork 第二次, 确保自己不是 session leader, 避免获取 tty

实际上整个步骤需要 15 步之多, 可以查看 `man 7 daemon` 命令.

整个过程非常复杂, 在 GNU C lib 中提供了 daemon() 函数来实现这些步骤, 然而讽刺的是, 由于步骤实在太多了, 系统提供的 daemon 函数竟然忘了其中几步, 所以不推荐使用…

在我看来, 由进程自我守护实际上完全背离的 Unix philosophy — Write programs that do one thing and do it well, 每个进程应该只做一件事, 变成守护进程显然是让一个进程做了两件事, 而且是一个重复性的工作, 由一个统一的 init 进程来管理 daemon 才是真正符合 Unix 哲学的.

# systemd

在现代的 Linux 上, 系统层面, 我们通过 systemd 来管理守护进程, 每个进程只需要实现最简单的单进程程序就好了, 然后通过编写 systemd 的 unit 文件来实现 daemonize. 用户层面, 我们可以使用 supervisord 或者 pm2 来管理进程, 他们和 systemd 的功能和理念都是类似的.

但是, 如上文所述, 一个进程完全可以通过 setsid 和 fork 等操作而完全脱离创建进程的控制, 而且不少进程在创建的时候也是具有 root 权限的, 那么 systemd 是怎样确保进程不会偷偷跑掉的呢?

答案是 cgroups, 且听下回分解…

# 参考资料

1. https://www.win.tue.nl/~aeb/linux/lk/lk-10.html
2. https://notes.shichao.io/apue/ch9/#sessions
3. http://blog.jorgenschaefer.de/2014/07/why-systemd.html
4. https://unix.stackexchange.com/questions/149741/why-is-sigint-not-propagated-to-child-process-when-sent-to-its-parent-process
5. https://segmentfault.com/a/1190000009152815
6. https://stackoverflow.com/questions/24346126/where-do-zombie-processes-go-after-their-parent-dies
7. https://stackoverflow.com/questions/881388/what-is-the-reason-for-performing-a-double-fork-when-creating-a-daemon
8. https://unix.stackexchange.com/questions/404054/how-is-a-process-group-id-set
9. https://stackoverflow.com/a/39109685/1061155
10. https://stackoverflow.com/questions/32780706/does-linux-kill-background-processes-if-we-close-the-terminal-from-which-it-has
11. https://superuser.com/questions/403200/what-is-a-stopped-process-in-linux
12. http://www.informit.com/articles/article.aspx?p=397655&seqNum=6
13. https://stackoverflow.com/questions/13069634/python-daemon-and-systemd-service
14. https://unix.stackexchange.com/questions/447275/does-systemd-not-protect-processes-against-acquiring-a-controlling-terminal
15. https://linuxaria.com/article/how-to-manage-processes-with-cgroup-on-systemd

Python + Influxdb + Grafana 的监控系统

# influxdb

为什么我们要把监控数据存到 influxdb 呢? 存到 MySQL 或者 MongoDB 甚至 Elasticsearch 不好么?

数据模型上, 监控数据是和时间相关的, 脱离点产生的时间, 那么数据将毫无意义, 传统数据库中并没又强调这一点.

性能上, influxdb 是时间序列数据库, 这类数据库面临的问题是写入非常多, 而读取比较少. 而普通的数据库一般都是读比较多, 而写入较少, 并且要保证写入的正确性.

监控打点显然是一个面向时间序列的过程, 并且写入非常多, 而一般只有在触发告警, 排查问题的时候可能读取才比较多. 所以从性能和功能考虑上来说, 传统数据库都是不适用的.

influxdb 中常见的存储数据格式:

“`
cpu_usage value=49 1502043216
cpu_usage value=50 1502193042
cpu_usage value=5 1502196258
“`

## influxdb 数据模型

我们以一个 measurement(测量) 作为一个表, tag-value, field-value 都是记录数据的键值对, 区别是 tag 是由索引的, 而 field 没有, timestamp 是时间戳. tag set 自然指的是一组 tag 的组合.

“`
measurement,tag=value,tag1=value1 field=value,field1=value1 timestamp
“`

`measurement + tag set` 被称为一个序列(series). 每一个 series 都可以指定不同的 retention policy.

## influxdb 查询

使用类似 SQL 的语言, 执行 `influx` 进入shell

“`
> CREATE DATABASE mydb
> SHOW DATABASES
> USE mydb
“`
插入数据, 和 SQL 差别还是挺大的, 其中 cpu 是 measurement, 也就是 “表名”, 没指定时间的话, influxdb 会自己加上.

“`
INSERT cpu,host=serverA,region=us_west value=0.64
“`

查询数据, 注意多出来的 timestamp 一栏

“`
> SELECT “host”, “region”, “value” FROM “cpu”
name: cpu
———
time host region value
2015-10-21T19:28:07.580664347Z serverA us_west 0.64
“`

## 回收策略

默认情况下, influxdb 会永久保留数据, 一般来说这样是没有意义的, 我们可以设置短一点.

“`
CREATE RETENTION POLICY ON DURATION REPLICATION [DEFAULT]
“`

其中 replication 只能设置为 1, 因为开源版只有 1. 可以设置成 30d, 1w

# Python 客户端的编写

看到这里有人可能要问了, 不是有 python-influxdb 这个库么, 好好地客户端你为什么要封装一层呢? 答案很简单: 性能.

1. 调用 `influxdb.write_points()` 是一个涉及到网络的阻塞操作, 极有可能对于程序造成性能影响.
2. 如果我们在程序中散落着各种打点的代码, 那么就会造成没打一个点都去调用一些 IO, 不如放在一个队列里面可以每次多打几个, 减少 IO 次数, 这样对程序和 influxdb 的性能都有好处.

## UDP vs http

influxdb 支持使用 UDP 和 HTTP 两种协议访问. 显然对于打点这种操纵来说, 我们不关心响应结果, 哪怕一个点没打上, 没打上就没打上吧……所以采用 UDP 就好了. 根据测试 udp 的性能在 http 的几十倍.

按照 influxdb 官网的建议, 我们需要调整 UDP buffer 的 size 到 25MB(26214400B) 为宜.

查看系统的 udp buffer 大小:

“`
$ sysctl net.core.rmem_max
$ sysctl net.core.rmem_default
“`

修改 `/etc/sysctl.conf` 文件:

“`
net.core.rmem_max=26214400
net.core.rmem_default=26214400
“`

但是这个设置只有到下次重启才能生效, 继续使用 sysctl 设置立即生效:

“`
$ sysctl -w net.core.rmem_max=26214400
$ sysctl -w net.core.rmem_default=26214400
“`

另外注意, UDP 有一个大坑, 只吃吃精度为 s 的打点, 所以在配置和客户端中都必须使用这个时间精度.

P.S. 中文互联网上的好多教程都在使用 http 打点, 误人子弟, 毁人不倦啊……

参考:

1. https://docs.influxdata.com/influxdb/v1.6/supported_protocols/udp/
2. https://github.com/MikaelGRA/InfluxDB.Client/issues/31
3. https://blog.codeship.com/a-deep-dive-into-influxdb/
4. http://docs.grafana.org/features/datasources/influxdb/

使用 prctl 在父进程退出的时候安全退出子进程

在 Linux 中, 当子进程退出的时候, 父进程可以收到信号, 但是当父进程退出的时候, 子进程并不会受到信号. 这样就造成了在父进程崩溃的时候, 子进程并不能同时退出, 而是一直会在后台运行, 比如下面的例子:

“`
import os
import time

def loop_print():
import time
while True:
print(‘child alive, %s’ % time.time())
time.sleep(1)

try:
pid = os.fork()
except OSError:
pass

if pid != 0: # parent
print(‘parent sleep for 2’)
time.sleep(2)
print(‘parent quit’)
else:
loop_print()
“`

当父进程退出的时候, 子进程一直在不断地 print, 而没有退出.

# naive 的方法, 使用 multiprocessing 库

昨天我已经吐槽过标准库的 multiprocessing 有很多坑, 不出所望, 在这个问题上 multiprocessing 依然提供了半个解法, 只解决了一半问题……

在使用 multiprocessing 库创建进程的时候, 可以设置 `Process.daemon = True`, 这个属性又是模仿 threading 库的 API 来的.

正常情况下, 当一个程序收到 SIGTERM 或者 SIGHUP 等信号的时候, multiprocessing 会调用每个子进程的 terminate 方法, 这样会给每个子进程发送 SIGTERM 信号, 子进程就可以优雅退出. 然而, 当异常发生的时候, 父进程挂了, 比如说收到了 SIGKILL 信号, 那么子进程就得不到收割, 也就变成了孤儿进程.

所以说, multiprocessing 库只解决了半个问题, 真遇到问题的时候就会坑你一把.

# 正确解决方法

Linux 提供了 prctl 系统调用, 可以由子进程向内核注册父进程退出时候收到什么信号, 我们只要注册一个 SIGTERM 信号就好了.

在 Python 中可以使用 python-prctl 这个包.

## 安装

“`
# apt install libcap-dev && pip install python-prctl
“`

## 使用

以上面的程序为例:

“`
import os
import time

def loop_print():
import time
import prctl
import signal
prctl.set_pdeathsig(signal.SIGTERM)
while True:
print(‘child alive, %s’ % time.time())
time.sleep(1)

try:
pid = os.fork()
except OSError:
pass

if pid != 0: # parent
print(‘parent sleep for 2’)
time.sleep(2)
print(‘parent quit’)
else:
loop_print()
“`

这次我们看到, 在父进程退出的同时, 子进程也推出了.

“`
parent sleep for 2
child alive, 1539676057.5094635
child alive, 1539676058.5105338
parent quit
“`

吐槽一下 Python 混乱的 threading 和 multiprocessing

最近要写一个库往 influxdb 中打点, 因为要被很多程序使用, 而又要创建新的进程, 为了避免引起使用方的异常, 简单深入了解了下 Python 的并发控制, 这才发现标准库真是坑. 之前没过多考虑过, 只是凭感觉在 CPU 密集的时候使用 multiprocessing, 而默认使用 threading, 其实两个还是有很多不一样的, 除了都是并发执行以外还有很大的不同. Python 中试图用 threading 和 multiprocessing 实现类似的接口来统一两方面, 结果导致更混乱了. 本文探讨几个坑.

# 在多线程环境中 fork

首先不谈 Python, 我们思考一下, 在多线程环境下如果执行 fork 会怎样? 在新的进程中, 会不会所有线程都在运行? 答案是否定的, **在 fork 之后, 只有执行 fork 的线程在运行**, 而其他线程都不会运行. 这是 POSIX 标准规定的:

> A process shall be created with a single thread. If a multi-threaded process calls fork(), the new process shall contain a replica of the calling thread and its entire address space, possibly including the states of mutexes and other resources. Consequently, to avoid errors, the child process may only execute async-signal-safe operations until such time as one of the exec functions is called. Fork handlers may be established by means of the pthread_atfork() function in order to maintain application invariants across fork() calls.

但是这时候其他线程持有的锁并不会自动转化到当前线程, 所以可能造成死锁. 关于在多线程程序中执行 fork 会造成的问题, 有好多文章有详细的讨论:

1. http://www.linuxprogrammingblog.com/threads-and-fork-think-twice-before-using-them
2. https://stackoverflow.com/questions/1073954/fork-and-existing-threads/1074663#1074663

# 在 python 的 daemon thread 中 fork 又会怎样

在 Python 中可以把线程设置为 daemon 状态, 如果一个进程中只有 daemon thread, 这个进程就会自动退出. 那么问题来了, 如果我们 daemon thread 中执行 fork 会怎样呢?

理论上来说, 既然 fork 之后只有一个线程, 而这个线程又是 daemon 线程, 那么显然这个进程应该直接退出的, 然而并不会这样, 这个进程会一直运行, 直到该线程退出. 这是因为 fork 之后, 唯一的线程自动成为了 main thread, 而 Python 中硬编码了 main thread 不是 daemon thread, 所以这个线程不会退出.

参考:

1. https://stackoverflow.com/questions/31055960/is-it-a-python-bug-that-the-main-thread-of-a-process-created-in-a-daemon-thread

# 在新创建的进程中创建线程又会怎样

在普通进程中, 进程在所有非daemon 的线程退出之后才会推出, 但是在新创建的进程中, 不论创建的线程是 daemon thread 还是不是 daemon thread 都会在主线程退出后退出. 这是 Python 的一个 [bug](https://bugs.python.org/issue18966), 这个 bug 最早在 2013-09-08 01:20 报告出来, 而直到 2017-08-16 18:54 的 Python 3.7 才修复…

如何复现这个 bug

“`
#!/usr/bin/env python

from multiprocessing import Process
from threading import Thread
from time import sleep

def proc():
mythread = Thread(target=run)
mythread.start()
print(‘Thread.daemon = %s’ % mythread.daemon)
sleep(4)
#mythread.join()

def run():
for i in range(10):
sleep(1)
print(‘Tick: %s’ % i)

if __name__ == ‘__main__’:
p = Process(target=proc)
p.start()
print(‘Process.daemon = %s’ % p.daemon)
“`

下面大概说下这个 bug 的原因:

1. 普通进程会调用 `sys.exit()` 退出, 在这个函数中会调用 `thread.join()` 也就是会等待其他线程运行结束
2. 在 Python 3.4 之前, 默认只会使用 fork 创建线程, 而对于 fork 创建的线程, 会使用 `os._exit()` 退出, 也就是不会调用 `thread.join()`. 所以也就不会等待其他线程退出
3. 在 Python 3.4 中引入了对 `spawn` 系统调用的支持, 可以通过 `multiprocessing.set_start_method` 来设定创建进程使用的系统调用. 而使用 `spawn` 调用创建的进程会通过 `sys.exit()` 退出, 也就避免了这个 bug 的影响. 而使用 `fork` 创建的进程依然受到这个 bug 的影响.
4. 在 Python 3.7 中终于在添加了 `thread._shutdown` 的调用, 也就是会 join 其他的 thread.

# fork vs spawn 造成的 OS 平台差异性

我们知道, 在 `*nix` 系统中创建一个一个新的进程可以使用系统调用 `fork`, 父进程的所有资源都会被复制到子进程中, 当然是 Copy On Write 的. 如果要执行一个新的程序, 必须在 `fork` 之后调用 `exec*` 家族的系统调用, 后来 Linux 中添加了 `spawn` 系统调用, `spawn` 和 `fork` 的不同是, 他是从头创建了一个新的子程序, 而不是像 `fork` 一样复制了一份父进程.

而在 Windows 上, 从来没有类似 `fork` 的系统调用, 只有类似 `spawn` 的系统调用, 也就是从头创建一个新的程序.

对于 Python 的影响. 在 `*nix` 操作系统上, 当使用 multiprocessing 的时候, 默认调用的是 fork, 在新的进程中所有导入的包都已经在了, 所以不会再 import 一次. 而在 Windows 系统上, 使用 multiprocessing 创建新的进程时, 所有包都会被在新进程中重新 import 一遍, 如果 import 操作是对外部系统有副作用的, 就会造成不同.

当然如上文所述, 在 Python 3.4 之后可以选择创建进程时使用的系统调用, 如果选择了 `spawn`, 那么在各个平台上行为就是统一的了.

参考:

1. 为什么要区别 fork 和 exec: https://www.zhihu.com/question/66902460
2. fork 和 spawn 造成的有趣影响: https://zhuanlan.zhihu.com/p/39542342
2. https://stackoverflow.com/questions/38236211/why-multiprocessing-process-behave-differently-on-windows-and-linux-for-global-o

# fork 和 asyncio

多进程和 Event Loop 也可能引起一些问题, [这篇文章](http://4fish.xyz/posts/asyncio-concurrency/) 给了一个很好的例子:

假设现在有一个场景,主进程运行着一个event loop,在某个时候会fork出一个子进程,子进程再去运行一个新建的event loop:

“`
async def coro(loop):
pid = os.fork()
if pid != 0: # parent
pass
else: # child
cloop = asyncio.new_event_loop()
cloop.run_forever()

loop = asyncio.get_event_loop()
asyncio.ensure_future(coro(loop), loop=loop)
loop.run_forever()
loop.close()
“`

这段代码看起来没有什么问题, 在子进程中开了一个新的 Event Loop, 然而在 Python 3.5 和以下, 在真正运行时会报错:

“`

cloop.run_forever()
File “/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/base_events.py”, line 411, in run_forever
‘Cannot run the event loop while another loop is running’)
RuntimeError: Cannot run the event loop while another loop is running
“`

原因就在于标准库的 Event Loop 没有考虑多进程环境, 只是使用一个 thread local 来表示当前的 loop, 在多线程条件下, 这样当然是可以的, 但是在 fork 之后, 数据结构全部都得到了复制, 因此子进程就会检查到已经有 event loop 在运行了.

在 Python 3.6 中, 这个问题得到了简单粗暴的修复, 在每个 loop 上都标记一个 pid, 检查的时候再加上 pid 验证是不是当前进程就好了.

总而言之, 尽量不要同时使用多进程和多线程, 如果非要用的话, 首先尽早创建好需要的进程, 然后在进程中再开始创建线程或者开启 Event Loop.

还有一篇文章没看, 用空了再看下吧, 是讲 multiprocessing.Pool 的坑:

1. https://codewithoutrules.com/2018/09/04/python-multiprocessing/

Python 高性能请求库 aiohttp 的基本用法

aiohttp 是 Python 异步编程最常用的一个 web 请求库了, 依托于 asyncio, 性能非常吓人. 下面列举几个常见的用法:

# 最基础: 并发下载网页

“`
import aiohttp
import asyncio

async def fetch(session, url):
async with session.get(url) as response:
return await response.text()

async def main():
urls = [
‘http://python.org’,
‘https://google.com’,
‘http://yifei.me’
]
tasks = []
async with aiohttp.ClientSession() as session:
for url in urls:
tasks.append(fetch(session, url))
htmls = await asyncio.gather(*tasks)
for html in htmls:
print(html[:100])

if __name__ == ‘__main__’:
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
“`

kubernetes 初探——使用 helm 部署服务

前几篇写了一写 k8s 部署方面的东西, 后来发现阿里云的 k8s 集群还很好用…何苦自己研究几天部署呢…?

——分隔线——

在 k8s 上部署一个应用还是有些复杂的, 自己的应用当然需要自己一步一步部署, 但是对于一些通用的应用, 比如说 mysql/grafana 这种就没必要自己手工一步一步部署了. 这时候就有了 helm, 通俗的来说他就是 kubernetes 上的 AppStore, 常见的应用都已经在了, 而且你也可以按照他的格式打包自己的应用部署.

安装

在 helm 的 release 页面下载, 然后拷贝到本地的 /usr/local/bin/ 目录就好了. helm 运行的时候会使用 ~/.kube/config 文件, 所以本地 kubectl 可以使用就好了.

helm 分为两部分, helm 是本地命令, tiller 是服务端, 作为一个 pod 运行在, 两者的版本需要保持一致. 理论上来说, 安装好 helm 之后, 只需要执行 helm init 就可以自动在 k8s 集群中安装 tiller 了, 但是由于一些众所周知的原因, 在中国大陆地区并不能安装成功, 需要使用阿里云或者中科大的镜像:

helm init --stable-repo-url http://mirror.azure.cn/kubernetes/charts/

如果已经安装好了,

使用

概念

  • Chart, 大概相当于 package 的意思
  • Repository, Helm 的中心仓库
  • Release, 每次运行一个 Chart 都会生成一个 Release, 每个 release 之间是独立的. Chart/Release 的关系就好比 Docker 的 Image/Container 一样.
  • Revision, 每次更新 Release 都会产生一个新的版本, 可以回滚

基础命令

helm search 查找相关的软件包(chart), 现在 stable 仓库中大概有 200 多个包可以安装.

helm install –name NAME PACKAGE 会安装对应的 chart. 如果不指定 name, 会自动生成一个.

helm status NAME 可以查看对应的包的信息, 一般包括了如何连接使用这个包等信息, 可以当做帮助来用.

在安装包之前更改配置

每个 helm chart 都定义了一些默认的配置, 可以在安装之前查看并修改这些值.

helm inspect values stable/mysql 查看 mysql 的默认值. 或者去 GitHub 上看这个仓库的 readme.

把需要覆盖的变量写到 OVERRIDE.yml 文件中, helm install -f OVERRIDE.yml stable/mysql 就可以使用自己的配置了

更新 release

如果需要更新一个 release, 可以使用 helm upgrade -f OVERRIDE.yml RELEASE_NAME 命令更新相关的配置. 这时就会创建一个新的版本.

使用 helm list 可以查看当前部署的 release, 这时候我们可以看到部署的版本变成了 2 (最初是1).

-> % helm ls
NAME    REVISION        UPDATED                         STATUS          CHART           NAMESPACE
mysql   1               Sat Oct  6 15:44:25 2018        DEPLOYED        mysql-0.3.0     default

如果当前的更新有误, 可以回退到之前的版本, 语法是 helm rollback [RELEASE] [REVISION]

管理 repo

helm repo list 列出当前添加的 repo

$ helm repo list
NAME            URL
stable          https://kubernetes-charts.storage.googleapis.com
local           http://localhost:8879/charts
mumoshu         https://mumoshu.github.io/charts

添加新的 repo

$ helm repo add dev https://example.com/dev-charts

参考文献

  1. https://docs.helm.sh/
  2. https://ezmo.me/2017/09/24/helm-quick-toturial/
  3. https://help.aliyun.com/document_detail/58587.html
  4. https://johng.cn/helm-brief/

kubernetes 初探——部署有状态服务

为了部署我们自己的应用, 首先需要把基础设施部署上去, 其中数据库就占了大头. 有人说数据库等应用不是和容器化部署, 但是也有人认为所有的应用都适合容器化部署. 在这里我们不讨论这些观点,仅以部署 MySQL 为例说明一下如何在 K8S 上部署有状态服务。

# 相关概念

– PersistentVolume(PV) 是集群之中的一块网络存储。跟 Node 一样,也是集群的资源。PV 跟 Volume 类似,不过会有独立于 Pod 的生命周期。这一 API 对象包含了存储的实现细节,例如 NFS、iSCSI 或者其他的云提供商的存储系统。
– PersistentVolumeClaim (PVC) 是用户的一个请求。他跟 Pod 类似。Pod 消费 Node 的资源,PVC 消费 PV 的资源。Pod 能够申请特定的资源(CPU 和 内存);Claim 能够请求特定的尺寸和访问模式(例如可以加载一个读写,以及多个只读实例)
– Stateful Set. Deployment 是无状态的服务,而 StatefulSets 旨在与有状态的应用及分布式系统一起使用。
– ConfigMap 用来保存非密码的配置. configmap 可以以配置文件或者环境变量等方式挂在到 pod 中
– Secret 用来保存密码等私密数据
– Init Container 用于初始化的容器. 有点类似于 docker build 的过程

# 动态 PV vs 静态 PV

# 使用 Deployment PVC 还是 Stateful Set

可以看出我们即可以使用普通的 Deployment + PVC 来部署 MySQL, 也可以使用 Stateful Set 来部署, 那么哪种方式更好呢?

个人理解:

– 对于需要使用挂载一定资源的,使用 PVC 就好了,甚至只需要只读挂载就好。
– 对于强状态依赖的服务,比如数据库,肯定要使用 PVC

Stack Overflow 上的一个问题[2]也很值得参考.

# MySQL 主从集群

本文中我们要部署一个一主多从的 MySQL 集群. 关于一主多从的优点不是本文的重点, 这里就不多说了, 可以参考下面:

> 1. 扩容解决方案:在多个slave之间扩展负载以提高性能。在这种模式下,所有的写入和更新操作都必须在主服务器上进行。然而,读取操作通过slave镜像。该模型可以提高写入操作的性能,同时,也能够通过增加slave的节点数量,从而显著地提升读取速度。
> 2. 数据安全:数据从master被复制到slave,并且slave可以暂停复制过程。因此,可以在不损坏master的情况下,在slave上运行备份服务。
> 3. 分析:现场数据可以在master上创建,而对信息的分析可以在slave进行,而不影响master的性能。
> 4. 远程数据分发:可以使用复制为远程站点创建本地数据的副本,而不必一直通过访问master。

# 参考

1. [使用 PVC 和 Deployment 部署单实例 MySQL 集群](https://blog.csdn.net/sweatOtt/article/details/81092484)
2. https://stackoverflow.com/questions/41732819/why-statefulsets-cant-a-stateless-pod-use-persistent-volumes
3. [使用普通 Deployment 部署单节点 mysql](https://kubernetes.io/docs/tasks/run-application/run-single-instance-stateful-application/)
4. https://kubernetes.io/cn/docs/tutorials/stateful-application/basic-stateful-set/
5. [如何使用 ConfigMap](https://www.cnblogs.com/zhenyuyaodidiao/p/6594410.html)
6. [Secret 文档](https://kubernetes.io/cn/docs/concepts/configuration/secret/)
7. https://stackoverflow.com/questions/41583672/kubernetes-deployments-vs-statefulsets
8. [阿里云挂载 OSS Volume](https://yq.aliyun.com/articles/640212)
9. https://jimmysong.io/posts/kubernetes-persistent-volume/
10. https://kubernetes.io/docs/concepts/storage/volumes/

kubernetes 初探——部署无状态应用

kubernetes 架构图

![](https://ws2.sinaimg.cn/large/006tNbRwly1fvxa7izqb8j30mb0gcmzk.jpg)

* Master. 用于控制整个集群部署的机器, 为了高可用, 可以使用多台,一般至少三台为宜。
* Node. 工作节点, 用于部署服务. 一台机器可以既是 Master 也是 Worker,当然最好 Master 不要做 Worker。
* Pod. k8s 部署的最小单元, 一个 Pod 中可能包含多个 container. Pod 随时可能挂掉,也可能被替换。
* Label. Pod 的标签, 可以通过这些标签(组合)来选择对应的 Pod。
* Replica Set. 作为一个高可用的系统, 每个服务一般来说可能有多个 Pod. Replication Set 用来创建并保证有足够的 Pod 副本。RS 的名字总是 格式的。
* Deployment. 用户一般来说不会直接创建 Pod, 而是创建一个 Deployment 来部署服务. (在老版本中是创建 RC)
* Namespace. 命名空间, 默认情况下会有 kube-system 和 default 两个命名空间, kube-system 存放的是 k8s 系统的 pod 等资源, 而用户部署的资源会在 default 命名空间中.
* PersistendVolume. 如果把 Node 理解为抽象的 CPU 和内存资源,那么 PV 就可以理解为抽象的硬盘资源。我们通过 Pod 来使用 Node,因此我们也不直接使用 PV,而是通过 PersistentVolumeClaim 来使用 PV。
* PersistentVolumeClaim. 存储声明,用来声明需要使用的存储资源。
* Stateful Set. Deployment 对应的部署语义上没有状态的,而StatefulSet会充分运用 PV 和 PVC 实现 Pod 重启之间的状态保持。典型应用场景是数据库。
* Label 和 Selector. K8S 中的资源全部都通过 Label 来标识的选择。

# deployment

deployment 是使用 k8s 部署服务直接操作的概念。其他的概念往往都是**通过 deployment 来间接使用**的,因此理解 deployment 至关重要。

一个典型的 deployment 配置文件如下:

“`
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
spec:
selector:
matchLabels:
run: my-nginx
replicas: 2
template:
metadata:
labels:
run: my-nginx
spec:
containers:
– name: my-nginx
image: nginx
ports:
– containerPort: 80

“`

deployment 的配置可以发生改变,如果只是 replica 的数目发生了改变,那么这只是一个简单的扩容或者缩容操作,k8s只会简单的打开或者杀死新的 Pod。如果镜像、命令等参数发生了改变,那么 K8S 会把这次操作视为升级,也就是开始一个 RollOut 操作,创建新的 ReplicaSet。在这个过程中,如果 deployment 中的 spec 指定了保留历史 revision 的次数大于零,那么原有的 ReplicaSet 不会被删除,只是会被 Scale 到 0 而已,方便回滚。

文档:

> Note: a Deployment’s rollout is triggered if and only if the Deployment’s pod template (i.e. .spec.template) is changed, e.g. updating labels or container images of the template. Other updates, such as scaling the Deployment, will not trigger a rollout.

> https://stackoverflow.com/questions/42561791/right-way-to-update-deployments-on-kubernetes

# 服务

## 相关概念

* Service. 如果一个 Deployment 对外(pod 外)提供服务的话,可以暴露为 Service。它是服务的抽象, 通过 kube-proxy 和 DNS 等提供负载均衡给后端 RC 定义的 Pod。
* clusterIP. 服务暴露在集群内部的一个虚拟 IP,声明周期和服务相同
* nodePort. 暴露在 Node 上的服务端口,不建议在生产环境使用。
* Ingress Controller. Service 只是对集群内部暴露了服务,ingress controller 用于把 Service 再对外暴露。

如果一个 deployment 需要对集群内部或者是外部提供服务的话,可以使用 service。

这时将创建一个 clusterIP,需要特别注意的是,这个 clusterIP 也是虚拟的,并没有和任何 pod 绑定,而是绑定到了服务上,可以理解为绑定到了这个服务对应的内部负载均衡上,并且是不变的。即使你的 RC 中指定了多个副本,那么也只有这一个 clusterIP,pod 的创建和销毁也都不会影响到 clusterIP。

# kubectl 使用

## kubectl get

常用参数 `-o wide` 用来现实更详细信息. 用来获取集群的各种信息:

* `kubectl get pod` 显示所有 pod 信息
* `kubectl get deployment` 显示所有部署信息
* `kubectl get node` 显示所有节点信息

## kubectl create & apply

用来创建 pod, 部署等. 一般情况下都是使用 `-f` 参数来制定配置文件

“`
kubectl create -f file.yml
“`

和 kubectl create 还有一个类似的概念,kubectl apply 也可以用于创建资源。这两个的区别有以下几点:

– kubectl create 是过程性的,重点在于“创建”这个操作;而 kubectl apply 是声明性的,重点在于达成“应用”这个结果
– kubectl create 在创建重复资源的时候会报错,而 kubectl apply 可以用于更新。

## kubectl run

类似于 docker run, 但是由 kubernetes 接管, 直接运行在集群上. 比如运行 hello world

## kubectl delete

用来删除节点上的 pod, deployment 等信息

## kubectl logs

类似于 docker logs, 用来显示打印到 stdout 的日志

1. [deployment 和 pod 的区别](https://stackoverflow.com/questions/41325087/in-kubernetes-what-is-the-difference-between-a-pod-and-a-deployment)
2. [Kubernetes 基础概念](http://dockone.io/article/932)
3. [客户端和服务端服务发现](https://www.jianshu.com/p/1bf9a46efe7a)
4. [kubernetes 命令表](http://docs.kubernetes.org.cn/683.html)
5. [Kubernetes之kubectl常用命令使用指南:1:创建和删除](https://blog.csdn.net/liumiaocn/article/details/73913597)
6. [Kubernetes之kubectl常用命令](https://blog.csdn.net/xingwangc2014/article/details/51204224)
7. [Kubernetes基本概念以及术语](https://blog.csdn.net/u010209217/article/details/78782353)
8. [kubectl create 和 apply 的区别](https://stackoverflow.com/questions/47369351/kubectl-apply-vs-kubectl-create)
9. https://stackoverflow.com/questions/42561791/right-way-to-update-deployments-on-kubernetes
10. https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
11. [K8S YAML 文件基础](https://blog.csdn.net/phantom_111/article/details/79427144)
12. [NodePort/LB/Ingress 三种方式的对比](https://medium.com/google-cloud/kubernetes-nodeport-vs-loadbalancer-vs-ingress-when-should-i-use-what-922f010849e0)
13. https://kubernetes.io/docs/tasks/access-application-cluster/service-access-application-cluster/