Full page write in PostgreSQL

Published on:
Tags: postgresql

读了一篇文章,简单翻译总结下。

Partial Writes / Torn Pages

pg 默认是 8kB 一个 page。linux 文件系统一般是 4kB(x86 里面最大是 4kB),老设备驱动一般是 512B 一个扇区,新的设备有些支持 4kB 或者 8kB。

当 pg 写入一个 page 8kB 的时候,系统的底层会拆分小一点块,这里涉及到写入的原子性。8kB 的 pg page,会被文件系统拆分成 4kB 的块,然后拆分成 512B 扇区大小。这个时候如果系统崩溃(比如停电,内核 bug)会发生什么?

即使系统的存储有针对这种情况的设计(比如 SSD 自带电容器,RAID 控制器自带电池),内核那块也是会拆分成 4kB 的 page,所以还是有一定可能性,pg 写了 8kB,但是只有部分写入成功。

这个时候你可能意识到这就是为啥我们要有事务日志(WAL)。所以当系统崩溃重启之后,数据库会读取 WAL(从最后一次 checkpoint),然后重新写入一遍,以保证数据文件是完整的。

恢复的时候,在修改一个 page 之前,还是会读取一下。

在 checkpoint 之后第一次修改一个 page 的时候,会把整个 page 写入 WAL。这是为了保证在恢复的时候,能保证这些被修改的 page 能完全恢复到他原有的样子。

写放大

如果打开 Full page write,很显然会导致 WAL 文件增加,因为就算修改一个字节,也会导致 8kB page 的写入。因为 Full page write 只发生在 checkpoint 之后的第一次写入,所以减少 checkpoint 的发生频率是可以减少写入的。

UUID vs BIGSERIAL 主键

比较了一下使用 UUID 或者 bigserial 做主键对写入的影响。可以看原链接的图,会发现在 INSERT 语句的情况下 UUID 产生的 WAL 文件量比较多。主要原因是 Btree 索引的情况下,bigserial 是顺序的维护这个索引,UUID 是无顺序的,会导致维护索引产生的数据量不同。

如果是使用 UPDATE 随机修改,那么会发现产生的 WAL 数量就差不多了。

8kB and 4kB pages

如果减小 pg 的 page 的大小,可以减小 WAL 数量。从 8kB 减小到 4kB,上面 UUID 那个例子,可以减少大概 35% 的量。

需要 full-page write 吗?

首先,这个参数是 2005 年 pg 8.1 引入的,那么现代的文件系统是不是已经不用操心部分写入的情况了?作者尝试了一些测试没有测试出来部分写入的情况,当然这不表示不会存在。但是就算是存在,数据的一致性校验也会是有效的保护(虽然并不能修复这个问题,但是至少能让你知道有坏的 page)

其次,现在很多系统都依赖于流式同步,并不会等着有问题的服务器在有硬件问题的时候重启,并且花费很多时间恢复,一般都直接切换到热备服务器上面了。这个时候部分写就不是什么问题了。但是如果我们都推荐这么做,那么「我也不知道为啥数据损坏了,我只是设置了 full_page_writes=off」这种会是 DBA 死前最常见的言论了。(类似于「这种蛇我之前在 reddit 看见过,无毒的」)

总结

对于 full-page write 你没法直接优化。大部分情况下,full-page write 都是发生在 checkpoint 之后,直到下一次 checkpoint。所以调整 checkpoint 的发生频率不要太频繁很重要。

有些应用层的操作,可能会导致对表或者索引的随机写入的增加,例如上面的 UUID 的值就是随机的,会让简单的 INSERT 也会导致索引的随机 update。使用 Bigserial 做主键(让 UUID 做替代键)可以减少写放大。

使用 pgrepup 跨版本升级 pg

Published on:
Tags: postgres

pgrepup 其实是一个支持 pg 跨版本复制的工具。而 pg 大版本升级需要停机是个比较郁闷的事情,如果能通过这个解决就实在太好了。下面测试了一下。

安装

需要安装 pgrepuppglogical

安装 pgrepup

pgrepup 官方说是支持 python >= 2.7 的版本,我自己测试的结果,python 3.5 里面执行有点问题,需要修改几个地方。但是在 python 2.7 里面,不需要做任何修改,所以建议使用 python 2.7。安装很简单,执行 pip install pgprepup 就可以了。

安装 pglogical

需要给你的 pg 安装这个扩展。高版本的和低版本的都需要安装。

安装也很简单,下载源码,执行 PATH=/opt/pg96/bin:$PATH make USE_PGXS=1 install 就好了。如果是给 pg95 装,那就把路径改成 pg95。

可以参考这里

配置

配置 db

先给几个 db 定义一下角色。db1 假设为 9.5 版本,db2 假设为 9.6 版本。

pgrepup 允许 db1, db2 和执行 pgrepup 所在的机器分别在不同的机器,也可以在相同的机器,看机器情况。

对于 db,最小配置的 postgres.conf 修改如下,我测试的时候两个 db 在一台机器上面,只需要修改 port 不一样就可以了。

1
2
3
4
5
6
7
8
9
10
listen_addresses = '*' # what IP address(es) to listen on;
port = 5495
wal_level = logical # minimal, archive, hot_standby, or logical
max_wal_senders = 3 # max number of walsender processes
max_replication_slots = 3 # max number of replication slots
shared_preload_libraries = 'pglogical' # (change requires restart)
## 下面几个参数不是必须设置的
logging_collector = on # Enable capturing of stderr and csvlog
log_filename = 'postgresql-%Y-%m-%d.log' # log file name pattern,

pg_hba.conf 如下,修改其中的 client_ip 和 db_ip 为对应的真实 ip。

1
2
3
host all all client_ip/32 md5
host replication pgrepup_replication db_ip/32 md5
host all pgrepup_replication db_ip/32 md5

配置好之后,启动 db1 和 db2 看看是不是可以正常连接。

还需要建立用户。如果已经存在一个 super 的用户,那也可以直接用那个用户,没有的话,就建一个。db1 和 db2 都需要建立,可以是不同的用户。

hint

当然,如果我们在生产环境里面做这个事情,那肯定会是 db1 已经是一个存在的 db,只需要增加原来没有的配置就好了。db2 会是一个全新的 db,使用 initdb 初始化,之后配置上面的配置项(当然,如果是将来要给生产用,那应该是复制 db1 的配置文件过来,修改端口就可以了,其他都一样)。

配置 pgrepup

执行一下 pgrepup config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
❯❯❯ pgrepup config
Pgrepup 0.3.7
Create a new pgrepup config
Configuration filename [~/.pgrepup] ./pgrepup.config
Security
Do you want to encrypt database credentials using a password? [Y/n] n
Folder where pgrepup store temporary dumps and pgpass file [/tmp] ./tmp
Source Database configuration
Ip address or Dns name: db_ip
Port: 5495
Connect Database: [template1]
Username: wd
Password:
Destination Database configuration
Ip address or Dns name: db_ip
Port: 5496
Connect Database: [template1]
Username: wd
Password:
Configuration saved to ./pgrepup.config.
You can now use the check command to verify setup of source and destination databases

之后会产生一个配置文件 pgrepup.config,有修改的话,可以打开再次编辑。

之后,可以执行一下 pgrepup check 来检查一下

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
33
34
35
36
37
38
39
40
41
42
43
❯❯❯ pgrepup -c pgrepup.config check
Pgrepup 0.3.7
Global checkings...
> Folder ./tmp exists and is writable ..........................................OK
Checking Source...
> Connection PostgreSQL connection to db_ip:5495 with user wd OK
> pglogical installation .......................................................KO
Hint: Install docs at https://2ndquadrant.com/it/resources/pglogical/pglogical-installation-instructions/
> Needed wal_level setting .....................................................OK
> Needed max_worker_processes setting ..........................................OK
> Needed max_replication_slots setting .........................................OK
> Needed max_wal_senders setting ...............................................OK
> pg_hba.conf settings .........................................................KO
Hint: Add the following lines to /home/wd/data95/pg_hba.conf:
host replication pgrepup_replication db_ip/32 md5
host all pgrepup_replication db_ip/32 md5
After adding the lines, remember to reload postgreSQL
> Local pg_dumpall version .....................................................OK
> Source cluster tables without primary keys
> template1 ................................................................OK
> testdb
> public.t1 ............................................................OK
> postgres .................................................................OK
Checking Destination...
> Connection PostgreSQL connection to db_ip:5496 with user wd OK
> pglogical installation .......................................................KO
Hint: Install docs at https://2ndquadrant.com/it/resources/pglogical/pglogical-installation-instructions/
> Needed wal_level setting .....................................................KO
Hint: Set wal_level to logical
> Needed max_worker_processes setting ..........................................OK
> Needed max_replication_slots setting .........................................KO
Hint: Increase max_replication_slots to 3
> Needed max_wal_senders setting ...............................................OK
> pg_hba.conf settings .........................................................KO
Hint: Add the following lines to /home/wd/data96/pg_hba.conf:
host replication pgrepup_replication db_ip/32 md5
host all pgrepup_replication db_ip/32 md5
After adding the lines, remember to reload postgreSQL
> Local pg_dumpall version .....................................................OK

上面是我第一次执行 check 的结果,可以看到很多红色的 KO,有些下面还有 hint 提示告诉你怎么修复,针对红色的信息进行修复就好了。

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
❯❯❯ pgrepup -c pgrepup.config check
Pgrepup 0.3.7
Global checkings...
> Folder ./tmp exists and is writable ..........................................OK
Checking Source...
> Connection PostgreSQL connection to db_ip:5495 with user wd ...OK
> pglogical installation .......................................................OK
> Needed wal_level setting .....................................................OK
> Needed max_worker_processes setting ..........................................OK
> Needed max_replication_slots setting .........................................OK
> Needed max_wal_senders setting ...............................................OK
> pg_hba.conf settings .........................................................OK
> Local pg_dumpall version .....................................................OK
> Source cluster tables without primary keys
> template1 ................................................................OK
> testdb
> public.t1 ............................................................OK
> postgres .................................................................OK
Checking Destination...
> Connection PostgreSQL connection to db_ip:5496 with user wd ...OK
> pglogical installation .......................................................OK
> Needed wal_level setting .....................................................OK
> Needed max_worker_processes setting ..........................................OK
> Needed max_replication_slots setting .........................................OK
> Needed max_wal_senders setting ...............................................OK
> pg_hba.conf settings .........................................................OK
> Local pg_dumpall version .....................................................OK

上面是我修复之后执行的结果。其中会提示会被同步的 db(上面是 template1, testdb, postgres)。之后执行 setup

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
33
❯❯❯ pgrepup -c pgrepup.config setup
Pgrepup 0.3.7
Check if there are active subscriptions in Destination nodes .....................OK
Global tasks
> Remove nodes from Destination cluster
> postgres .................................................................OK
> template1 ................................................................OK
> testdb ...................................................................OK
> Create temp pgpass file ......................................................OK
> Drop pg_logical extension in all databases of Source cluster
> template1 ................................................................OK
> postgres .................................................................OK
> testdb ...................................................................OK
> Drop pg_logical extension in all databases of Destination cluster
> postgres .................................................................OK
> template1 ................................................................OK
> testdb ...................................................................OK
Setup Source
> Create user for replication ..................................................OK
> Dump globals and schema of all databases .....................................OK
> Setup pglogical replication sets on Source node name
> template1 ................................................................OK
> postgres .................................................................OK
> testdb ...................................................................OK
Setup Destination
> Create and import source globals and schema ..................................OK
> Setup pglogical Destination node name
> postgres .................................................................OK
> testdb ...................................................................OK
> template1 ................................................................OK
Cleaning up
> Remove temporary pgpass file .................................................OK
> Remove other temporary files .................................................OK

然后执行 start

1
2
3
4
5
6
❯❯❯ pgrepup -c pgrepup.config start
Pgrepup 0.3.7
Start replication and upgrade
> postgres .................................................................OK
> template1 ................................................................OK
> testdb ...................................................................OK

可以通过 status 看同步状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
❯❯❯ pgrepup -c pgrepup.config status
Pgrepup 0.3.7
Configuration
> Source database cluster ......................................................OK
> Destination database cluster .................................................OK
Pglogical setup
> Source database cluster
> template1 ................................................................OK
> postgres .................................................................OK
> testdb ...................................................................OK
> Destination database cluster
> postgres .................................................................OK
> testdb ...................................................................OK
> template1 ................................................................OK
Replication status
> Database postgres
> Replication status ..............................................replicating
> Database testdb
> Replication status ..............................................replicating
> Database template1
> Replication status ..............................................replicating
> Xlog difference (bytes) ...................................................57816

可以看到三个 db 都在同步。这个时候在 db1 上面插入数据,能在 db2 上面看到会同步过去。

状态有三种情况

  • initializing: pglogical 正在 copy 数据
  • replication: 同步状态
  • down: 同步断开了,需要检查日志修复

需要注意的问题

db 里面的表都需要有主键

如果存在没有主键的表,执行 check 的时候会看到下面的信息

1
2
3
4
5
6
7
> Source cluster tables without primary keys
> template1 ................................................................OK
> testdb
> public.t2 ............................................................KO
Hint: Add a primary key or unique index or use the pgrepup fix command
> public.t1 ............................................................OK
> postgres .................................................................OK

如果不解决就执行 setup,会提示下面的信息

1
2
3
4
5
6
7
Setup Source ........................................Skipped, configuration problems
Setup Destination
> Create and import source globals and schema .............................Skipped
> Setup pglogical Destination node name
> postgres .................................................................OK
> template1 ................................................................OK
> testdb ...................................................................OK

可以自己创建一个主键重新 check,也可以执行 fix 来修复,然后再次执行 setup。

1
2
3
4
5
6
7
❯❯❯ pgrepup -c pgrepup.config fix
Pgrepup 0.3.7
Find Source cluster's databases with tables without primary key/unique index...
> template1 ....................................................................OK
> postgres .....................................................................OK
> testdb
> Found public.t2 without primary key ................Added __pgrepup_id field

通过 fix 加的主键,在 uninstall 的时候会被删除。

Replication status .. down

有时候会遇到有的 db 的状态是好的,有的 db 是 down 的情况

1
2
3
4
5
6
7
8
Replication status
> Database postgres
> Replication status ..............................................replicating
> Database testdb
> Replication status .....................................................down
> Database template1
> Replication status ..............................................replicating
> Xlog difference (bytes) ..................................................614096

在同步状态下面,如果给某个 db 加一个没有主键的表,就会导致同步断掉。修复方法是先 stop,然后执行 check,按照提示修复,然后执行 setup,然后 start 就可以了。

官方列出来的几个问题

  • DDL 命令。不会同步 DDL 命令,可以在 db1 试试看 pglogical.replicate_ddl_command
  • seq 序列。执行 stop 命令的时候,会在目标 db 的 seq 上面加 1000。
  • 有大量的 db。执行 start 命令之后,pglogical 会每个 db 启动一个 worker 来同步数据,要是 db 比较多会导致比较高的负载。

因为这个是基于 pglogical 的,所以还需要关注 pglogical 列出来的一些限制 第 4 部分 Limitations and Restrictions。

  • 4.1 Superuser is required
  • 4.2 UNLOGGED and TEMPORARY not replicated
  • 4.3 One database at a time
  • 4.4 PRIMARY KEY or REPLICA IDENTITY required
  • 4.5 Only one unique index/constraint/PK
  • 4.6 DDL
  • 4.7 No replication queue flush
  • 4.8 FOREIGN KEYS
  • 4.9 TRUNCATE
  • 4.10 Sequences
  • 4.11 Triggers
  • 4.12 PostgreSQL Version differences
  • 4.13 Doesn’t replicate DDL

pgrepup uninstall

uninstall 会清理 pgrepup 创建的一些信息,比如安装的 pglogical 扩展,创建用来同步的用户,和通过 fix 命令添加的 seq。

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
❯❯❯ pgrepup -c pgrepup.config uninstall
Pgrepup 0.3.7
Check active subscriptions in Destination nodes
> template1 ...............................................................Stopped
> testdb ..................................................................Stopped
> postgres ................................................................Stopped
Uninstall operations
> Remove nodes from Destination cluster
> postgres .................................................................OK
> testdb ...................................................................OK
> template1 ................................................................OK
> Drop pg_logical extension in all databases
> Source
> template1 ............................................................OK
> postgres .............................................................OK
> testdb ...............................................................OK
> Destination
> postgres .............................................................OK
> testdb ...............................................................OK
> template1 ............................................................OK
> Drop user for replication ....................................................OK
> Drop unique fields added by fix command
> template1
> postgres
> testdb
> public.t1 ........................................................OK
> public.t2 ........................................................OK

升级

如果前面配置好了同步状态,那剩下的事情就简单了。

  • 停止应用链接 db1
  • 确保 db1 已经没有任何链接
  • 使用 pgrepup stop 停止 replication
  • 修改应用链接到 db2
  • 启动应用
  • 剩下的就是处理掉停止的 db1

参考文档

http://qiita.com/yteraoka/items/e82e4d28f6a23915d190

Built in sharding in PostgreSQL

Published on:
Tags: postgresql

PostgreSQL 内建 sharding 支持,粗略翻译自 https://wiki.postgresql.org/wiki/Built-in_Sharding

Introduction

内建支持 sharding 最大的挑战是,如何用最小的代码修改实现。大部分社区的 sharding 修改支持都修改了很多 PostgreSQL 的代码,这也导致这些不能被 Postgres 社区那些不需要 sharding 的人接受。有了 FDW 之后,就有了在有限代码修改情况下实现内建 sharding 支持的可能。

基于 FDW 的这种 sharding 设计,是基于 NTT 开发的 Postgres-XC,大概已经有 10 年了。Postgres-XL 是基于这个设计的一种更加灵活的实现。

Enhance Existing Features

  • 已完成?提升 FDW 的基础设计和 postgres_fdw。特别的,好的性能要求合理的把一些操作推送到子节点(foreign shards)。在 Postgres 9.6 中,join, sort, update, delete 都可以推送到字节点了。聚合的 pushdown 将在 Postgres 10 中支持。FDW 表已经可以作为继承表出现。
  • 提升分区支持有效提升 existence of shards。幸运的是,单节点的分区支持也需要重构才能提升性能和更多优化。例如,executor-based partition pruning.
  • 给 FDW 请求增加并行支持。这样能允许节点并行执行,这个可能会通过多个异步的链接来实现。

New Subsystems

还需要开发一些子系统:

  • 允许表可以复制到所有节点,以允许更多的 join pushdown。这个可以通过 trigger 或者逻辑复制来完成。
  • 实现一个子模块,以使用新的分区系统表来提交符合提交的查询的 FDW 查询。
  • 实现一个子模块收集 FDW 查询的结果返回给用户。
  • 实现全局事务管理器以便更加高效的允许子节点原子的提交事务。这个可能会通过 prepared 的事务来实现,还有某种在 crash 之后清理那些 preapared 的事务的事务管理器。例如 XA。
  • 实现全局快照管理器,以允许子节点可以看到一致性的快照。(是不是 serialisable 事务模式会避免跨节点快照冲突?pg_export_snapshot() 或者 hot_standby_feedback 是不是会有帮助?) 多节点的备份的一致性也需要这个支持。
  • 实现支持 create, manage, report on shards 这些用户 API。

Use Cases

有四种可能的用户案例和不同的需求:

  • 跨节点在只读节点上面执行只读聚合查询,例如数据仓库
    这种是最简单的场景,不需要全局事务管理,全局快照管理,并且因为聚合,所以子节点返回的数据量也是最小的。
  • 跨节点在只读节点上面执行只读非聚合查询
    这种会给调度节点压力,需要收集和处理很多子节点返回的数据。这种也能看到 FDW 传输机制等级。
  • 跨节点在可读写节点执行只读查询
    这个需要全局快照管理来保证子节点返回数据的一致性
  • 跨节点执行读写查询
    这个需要全局快照管理器和全局事务管理器

申请 google voice

Published on:
Tags: google

昨天晚上突然想申请一个 google voice 帐号。有了之后就可以作为国外的号码使用了,可以打电话收短信,想想好像还有点用。比如我就可以用他注册一个微博帐号用来关联一些无聊的服务了。。。

申请的过程网上很多,不细说了。主要注意下面几点。

  1. 需要美国 ip 打开 https://www.google.com/voice ,否则会跳转到一个帮助页面。这个从网上搜一些免费的代理就可以了。我是搜索到了一些 ss 帐号,然后配合 surge 来做的。
  2. 需要一个能接电话的美国号码来接受 google voice 的验证电话。这个我是通过 http://textnow.com 来做的。登录 textnow.com 注册一个免费的帐号,其中有一步是需要输入一个美国区号,这个要注意,我第一次注册的时候,输入的是 213 (洛杉矶地区的),然后就不行,后来又申请了一个 517 的就可以。所以最好搜索一下别人申请成功的区号有哪些,另外还好就是只要有不同的邮箱,就可以多次注册来换号码,如果遇到不行的,可以换个邮箱重新注册一下。
  3. 在 google voice 里面填入电话之后。google voice 会打电话给那个号码。我用 textnow 的 ios 客户端接的电话(建议不要用他们的 web,他们的 web 必须要你的浏览器支持 flash 才行,很恶心),我遇到的情况下,前几次接到的电话没有对方的声音,自己尝试直接输入号码也不行。后来多试几次就好了。
  4. 后面就是选号了。选好号码之后,点提交一般都会遇到一个错误,「There was an error with your request. Please try again」, 遇到这个是正常的。需要你多次点击那个按钮提交,有人用按键精灵点了几个小时搞定了。我这是打开 chrome 的 dev tools,然后看 network 里面发的请求,每次点击都会有一个 post 请求,在上面按右键,选择 「copy as curl」,然后在命令行写一个简单的程序,while true; do 这里把复制的内容贴过来 ;sleep 1.5s; done,复制到命令行,不停的重试就可以了,直到收到邮件说你的号码开通了。要注意的是命令行也得设置代理,一般是通过 export。

新与旧

Published on:
Tags: heart

北京有个古北水镇,运营上据说是杭州乌镇的那拨人。乌镇没去过,去过古北水镇。在一片山里面,长城下面,一群建筑。里面还开了一些模仿古代的商店,比如酒家,染坊这些可以参观。

这个地方离北京大概是 2 个多小时的车程(全程高速不怎么堵车的情况下)。周末过去之后会发现停车场停满了车,还有好多旅游车,水镇上面也人山人海。

这里面的建筑都是后期开发商人工开辟的。景区里面的酒店基本都是1k起,并且还得提前预约。据说夜景很漂亮,不过我没看过。

北京是个古老的城市,如果城区里面的历史建筑都留着,现在可够你转几个月的,光就绕着北京城墙走一圈,估计一天都不一定可以。曾经在西安的城墙上面走过一圈,感觉还不错,我记得还收了门票的。

北京有个南锣鼓巷,过去转你会发现并没有什么能让你理解回味京味的东西,卖的也是羊肉串奶茶这些,排好长的队买一串拿着边吃边走完了,其实可能也挺没意思的。

都说台湾是中华传统保持比较好的地方。去了台北第一印象就是,破房子挺多的。那边房子是私产,所以拆迁很难。然后个人又不一定有能力翻盖,所以就有破烂的房子。给我们开车的台湾人还说去过北京,说羡慕那边的高楼大厦,到处都很新,很时髦的样子。

台湾有很多夜市,去过几个,有的比较商业化的,你会发现周边也都是高楼了。有的就是普通的,真的是一条路白天行车,晚上就堵起来开始摆摊。我记得某个夜市里面有个摊位,说是开了好多年了,现在物价高,不得已只好比早年涨了几毛钱。看着都震惊了,涨几毛钱还废什么话,况且本身人家卖的也不贵,都是良心价。

地价涨没那么快,可能各种基础花费都会比较稳定,否则地价涨了房租涨了,那物价必定会涨。所以10年前我在北京长椿街那上班的时候,一份盖饭,大概是 7,8 块钱。到了现在,估计是翻一倍。另外这种店还越来越少,因为卫生条件,房租这些要求导致价格底了不好赚钱。

台湾夜市里面也经常能碰见那种几十年的老店,那真的是几十年一直在做那个生意。几十年价格也没有变化太多。想起来前门的那个面馆前段时间关门了,没有办法,涨价没法涨,收入基本不变的情况下,地价变化太大,只好关门了。

就目前这个房地产的情况,北京还能有多少真正的老字号,有多少真正的老街,估计基本不会有了。后面估计会有更加多人工的景区了,费用估计还不能便宜了。

想起来凤凰古城了,大家不愿意拆,那就某天一把火烧了,这下都同意了吧,这可是天意。

python 的 decorator 学习

Published on:

最近学习了一下 python 的 decorator(装饰器),看的是这篇,Python修饰器的函数式编程, 觉得挺有意思的,写点东西记录一下。

装饰器简单讲就是返回一个函数的函数/类。看个简单的例子。

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
#!/usr/bin/python
# -*- coding: utf-8 -*-
def dec1(fn):
print('inside dec1')
def wrapper():
print('inside wrapper')
return fn()
return wrapper
@dec1
def f1():
print('inside f1')
if __name__ == '__main__':
print('begin exec')
f1()
print('end exec')
# 执行结果:
# inside dec1
# begin exec
# inside wrapper
# inside f1
# end exec

看上面例子能看到,装饰器生效有 2 个步骤,第一个是装饰,第二个是执行。上面装饰器的效果,和下面的代码的效果是一样。

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
#!/usr/bin/python
# -*- coding: utf-8 -*-
def dec1(fn):
print('inside dec1')
def wrapper():
print('inside wrapper')
return fn()
return wrapper
# @dec1
def f1():
print('inside f1')
if __name__ == '__main__':
print('begin exec')
dec1(f1)()
print('end exec')
# 执行结果:
# begin exec
# inside dec1
# inside wrapper
# inside f1
# end exec

可以看到除了 「begin/end exec」,其他部分执行结果是一样的。所以理解装饰器,就把 @dec1 换成 dec1(fn)() 这么理解就可以了。

有时候会看到类也可以作为装饰器使用。其实理解起来也类似。举个例子。

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
#!/usr/bin/python
# -*- coding: utf-8 -*-
class dec1(object):
def __init__(self, fn):
print('inside dec1')
self.fn = fn
def __call__(self):
print('inside wrapper')
return self.fn()
@dec1
def f1():
print('inside f1')
if __name__ == '__main__':
print('begin exec')
f1()
print('end exec')
# 执行结果:
# inside dec1
# begin exec
# inside wrapper
# inside f1
# end exec

这里和上面类似,把 @dec1 理解成 dec1(fn)(),不过是这里的 dec1 是个类,那么 dec1(fn) 其实是调用的 dec1.__init__(fn),那么后续的 dec1(fn)() 就是调用产生的对象的 dec1.__call__() 了。

有时候还能看到加了参数的装饰器。加了参数的是怎么回事呢。再看下面的例子。

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
#!/usr/bin/python
# -*- coding: utf-8 -*-
def dec1(name):
print('inside dec1')
def real_dec1(fn):
def wrapper():
print('inside wrapper')
return fn()
return wrapper
return real_dec1
@dec1(name='1')
def f1():
print('inside f1')
if __name__ == '__main__':
print('begin exec')
f1()
print('end exec')
# 执行结果:
# inside dec1
# begin exec
# inside wrapper
# inside f1
# end exec

看懂了没有,就是多了个嵌套而已。遇到加了参数的,那就是把之前的没有参数的部分返回回来就可以了。等价的例子就不贴了,这个等价于 dec1(name='1')(fn)()

如果是类装饰器,并且有参数,那等价于 dec1(name='1')(fn)(),其中 __init__(self, name) 先处理第一层参数,然后 __call__(fn) 处理第二层,然后需要在 __call__ 里面再定义一个 wrapper 返回。

说明白没有?呵呵。

cookie 的一点研究

Published on:
Tags: cookie http

这几天搞了一下 python 里面 cookie 相关的东西。我的目的是想要尝试用 python 登录某个网站,并且保持登录状态直到过期。因为 http 协议是无状态的,所以一般来讲,网站想要用户保持登录,那么网站在用户登录之后,必须要和用户端协商好怎么来证明这个用户已经登录过了。

用户端如果使用浏览器,那么网站就可以利用浏览器对 cookie 的支持来让用户在不知情的情况下,让网站在用户登录后发的一个 token 在用户后续的请求里面都包含上。

用户端如果不是浏览器,比如是个 python 程序,那么网站可以和用户协商每次请求里面都包含某个下发的 token(当然,甚至要求客户端每次请求都带着用户名密码也是可行的)。

但是如果网站本身只是给浏览器用户准备的,那么通过用程序来「模拟」浏览器行为,把必要的 token 保存并在后续的请求里面都带上,也是可行的。

python 里面,发送 http 请求可以简单的使用 urllib.request.urlopen(url),但是如果想要定制一下请求,比如修改一些 header 信息,那么就得使用 urllib.request.Request 这个 class 先构造一个 Request 对象,然后传递给 urlopen 了。

如果要处理 cookie,那就需要使用 http.cookiejar.CookieJar 了,有了 Cookiejar 对象,就可以把网站下发的 cookie 保存到这个变量里面,然后在必要的时候,可以返回给服务器端了。如果想要保存到文件,那么可以使用 http.cookiejar.LWPCookieJar 或者 http.cookiejar.MozillaCookieJar,也可以基于 http.cookiejar.FileCookieJar 自己实现一个子类,来用自己的办法保存和加载 cookie,比如保存到数据库什么的,这样就可以多台机器之间共享 cookie 了。

urlopen 本身不支持自定义 cookiejar 逻辑,得使用 opener = build_opener(HTTPCookieProcessor(cookiejar=Cookiejar对象)) 来先构造一个自定义的 openner, 然后使用 opener(Request对象) 来发送请求。

如果不定义自己的 cookie policy,那么会使用默认的 http.cookiejar.DefaultCookiePolicy,也可以自己基于 http.cookiejar.CookiePolicy 实现自己的逻辑。只需要 override set_okreturn_ok 这两个方法就可以。

http cookie 其实有很多属性,比如 domain, expire, path 等常见属性,也有 httponly, secure 等几个不常见的。这些属性都是浏览器处理的。就是说,浏览器把 cookie 返回给服务器端的时候,如果 domain 不匹配,或者已经过了 expire 时间等等一些不符合浏览器制定的 cookie 逻辑的时候,浏览器就不会把 cookie 发送给服务器端。就比如,服务器产生 cookie 的时候,声明了 domain=a.com,那么如果是来自于 b.com 的请求,浏览器是根本不会给他发送这个 cookie。再比如,服务器端产生 cookie 的时候,声明了 1 天后过期,那么 1 天之后,浏览器也不会再给服务器端发这个 cookie 了。

但是如果是我们自己实现客户端模拟浏览器的时候,其实我们是可以耍流氓的,可以制定自己的 cookie 逻辑,也就是上面提到的 cookie policy。比如我可以简单的在 return_ok 这个方法里面 return True,在任何情况下都把所有的 cookie 返回给服务器,这样服务器端如果不提前想明白,它是一点都不知道的。

所谓提前想明白就是想明白是不是需要针对这种情况做处理。如果本身我们系统也没有那么严格要求,那么不处理也可以。但是如果是某个比如金融系统,那么是必须要考虑的。否则如果完全依赖 cookie 的话,如果我通过某些手段弄到了用户的 cookie,那么我就可以骗过服务器端,让他认为我就是那个用户。

我想了一下,貌似被盗窃 cookie 这种事情服务器端不太好防范,但是可以做的是防止浏览器耍流氓。比如我们把 cookie 加密,并里面增加一个发送 cookie 的时间。收到客户端发过来的 cookie 之后,我们解密看看时间有没有过期,这样就可以在服务器端让 cookie 失效了。

另外,也可以考虑使用 session。session 是把一些用户的状态保存在服务器端。但是 session 实际上也是依赖 cookie 的,因为前面说了 http 协议无状态,就算可以把用户状态保存在服务器端,但是总还是得识别用户才可以。那个识别的 cookie 就是所谓的 session cookie,其实就是某个用户的唯一标识。

对于 session cookie 被窃,好像也没有太好的办法,无非也是想办法比对之前用户的一些状态信息,比如 ip 和现在的信息是不是一致,不一致可以认为有被窃的怀疑,这个时候让用户再次验证用户信息,这都不能 100% 保证,但是至少会增加窃贼的成本。

上面说到这些,都可以自己测试一下,测试也并不一定需要搭一个服务器端配合,以及使用复杂的抓包专鉴,其实使用 nc 就可以。

使用 nc -l 9999 就可以启动一个监听在 9999 端口的 socket 服务器。之后使用 python 或者 curl 之类的程序请求,就能立刻看到请求发送过来的 http 信息,这个对于学习 http 协议其实也很方便。

1
2
3
4
5
6
7
$ nc -l 9999
GET / HTTP/1.1
Accept-Encoding: identity
Connection: close
Cookie: QN2=test; QN1=ClbaCVfZF5lfszBALzTIAg==
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
Host: localhost:9999

收到的上面这个请求,可以看到发送过来了 2 个 cookie。

如果还想测试数据返回的情况,那么可以写一个 test.resp 文件,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cat test.resp
HTTP/1.1 200 OK
Date: Sun, 18 Oct 2009 08:56:53 GMT
Server: Apache/2.2.14 (Win32)
Last-Modified: Sat, 20 Nov 2004 07:16:26 GMT
ETag: "10000000565a5-2c-3e94b66c2e680"
Set-Cookie: QN1=ClbaCVfZF5lfszBALzTIAg==; expires=Thu, 31-Dec-37 23:55:55 GMT; path=/
Set-Cookie: QN2=test; expires=Thu, 31-Dec-37 23:55:55 GMT; path=/; secure; httponly
Accept-Ranges: bytes
Content-Length: 44
Connection: close
Content-Type: text/html
X-Pad: avoid browser bug
<html><body><h1>It works!</h1></body></html>

然后使用 nc -l 9999 < test.resp 命令启动服务,客户端来请求的时候,就会返回上面 test.resp 里面的内容。

从双拼讲输入法

Published on:
Tags: input

上高中的时候,就流行五笔打字,不过那会家里都没有电脑,都是去网吧(或者叫打字社)去学习的,不过那会学习这个貌似唯一的用途就是录入用。

当时也背过五笔的码表,王旁青头兼五一,估计很多人都很熟悉。上了大学在有更多机会接触电脑的时候,上网什么的,自然就开始使用拼音输入法,因为基本不需要练习,会盲打知道键盘按键键位就可以使用。当时用的比较多的是智能 ABC,后来 2000 年左右写书的时候,用的是紫光拼音,紫光的词库比智能 ABC 好一点,所以用起来也好用很多。此后一直使用拼音输入法。

直到应该是大概 2008 年左右,ssslang 在使用二笔,就开始想尝试一下码表输入法。因为本身使用了这么多年的电脑输入和拼音输入法之后,对字形其实已经失去了概念,如果学五笔的话,肯定会是很痛苦的。

而二笔输入法一个字 4 个码,第一个码还是拼音的第一个字母,但是大部分常用字都是 2 个码就可以出,所以这样等于每个字都至少知道了一半的编码,这个还是很赞的。

二笔输入法也有很多编码方案,当时选的是哲豆二笔,这个方案只用了 26 个字母键,所以也可以在手机上面使用(其他方案用了一些符号键,比如 [ ],当时手机还都是流行硬键盘,一般都没有单独的符号键,而且貌似现在的软键盘也没有单独的符号键)。我当时用的 palm treo 650,输入还是蛮爽的。后来自己把一些词库导入了进去,用起来就更加舒服了。

但是后来的问题是慢慢的一些流行的智能机没有那么好的输入法自定义支持,就没法用了,慢慢切回来了拼音输入法。

二笔是码表输入法,就是每个字有自己的编码,联系的过程就是熟悉码表的过程,如果某个字不熟悉,那么其实你是需要轻微的思考的,想一下那个字怎么写,是哪个码。所以输入的速度取决于对这些字的熟悉程度,遇到不熟悉的会有卡顿。

最近看到了不少双拼的讨论,就想尝试一下双拼。因为如果反正都是用拼音输入法,那么对于 ing, eng 这些,如果可以按一个按键就输入进去,那么岂不是可以节省很多时间?并且这个还是基于拼音的,和那些需要记住字形的输入法不是一个套路,看着还不错。

我是这周一开始尝试的。mac 上面用的是鼠须管,ios 用的 touchpal。双拼也有很多方案,我选的方案是小鹤双拼,也没啥特别的原因,就是看用的人挺多的。

尝试的结果就是,输入了 4 天之后,周五我又换回了拼音输入法。。。。

因为我越用越发现,双拼也是个码表输入法。开始想得是节省拼音的输入时间,但是打字的时候,比如输入 hao 的时候,想到的不是输入 h 然后输入 ao 对应的字母 c,而是熟悉之后自然而然的按照 hc 来输入,下次需要输入 lao 的时候,还是需要想一下 ao 在字母 c,需要输入 lc。

这么看的话,和前面说的码表输入法有啥区别呢?所以我感觉双拼也是个码表输入法。既然是码表输入法,双拼的区分度那么差,何必折腾他呢,不如去用二笔呢。而且我用的时候最郁闷的是,二笔训练出来的键位记忆居然总会出来捣乱,这其实也更加说明这个就是码表输入法。。。

而且还有就是,普通拼音输入法里面,尤其是手机上面,容易按错键,比如 hao 输入成了 hso 的时候,手机上面会纠错,把「好」这个选项放在第一个。但是如果是双拼就没那么好搞了,如果也增加纠错,那会增加很多重码,体验不好。

另外还有个问题就是对于 sh, s, en, eng 这些不太清楚的人来讲,更加是个挑战,当然拼音可以用模糊音,按说双拼也可以想办法自定义模糊音。但是可能就失去了双拼的一些优势。

进化论

Published on:
Tags: tech

回忆一些没用的

我是 1998 年上的大学。小的时候就对各种电子产品比较感兴趣,当然其实没有什么机会的,当时主要就是游戏机,但是我家里是没有的。高中的时候曾经借同学的回家玩过,和我弟弟一起玩的挺嗨,但是对于增长知识应该是没啥用的。

高考完毕之后,其实最想要报的还是计算机系,记得暑假的时候在电视里面看电脑相关的讲座,讲的最多的好像就是五笔,那会 windows 用户也不多,都是用的 dos。ucdos 是最常用的。那会电脑最大的用途感觉就是打字输入,然后打印。

当时没有报计算机,因为闭塞的农村人认为计算机会和财经一样(当时财经类专业曾经火过一些时间,但是介于毕业的人都没啥靠谱工作,所以就认为一般了),过段时间就不行了,所以没有报这块,报的是应用物理专业(当然了,这个专业更屎)。

那个假期去我舅舅的办公室去玩玩电脑。我舅舅是微机专业毕业的,毕业后回我们那边做类似一个打字的工作(其实我也不知道具体做什么….)。我当时去那边就是熟悉各种 dos 命令,学习一下 foxbase。

当时我记得有一本 foxbase 的书,看里面东西看不懂,没有现在的论坛和方便的网络,没有人可以交流。我记得印象最深的是有一道题需要算 1+2+3+…+100 等于多少,不懂编程完全不知道怎么做这个事情,但是看了答案看不懂,因为不懂 i=i+1 是什么意思,那会不明白为什么 i 还可以在右边。现在看来,生在那个年代那个地方真的很悲哀。

进入大学之后,选修了一门 word 之类的课,开始接触 windows。然后宿舍同学熟悉之后,就计划一起买了一台电脑,每个人 1200,我们总共 6 个人,7200 一台电脑,包括音箱电脑桌。当时要上网只能是拨号,56k 猫,开始是用不起的,大概到了大2,3 才开始拨号。然后 6 个人轮流用。那是我们班的地一台电脑,当时辅导员要求大家写保证书,保证不玩游戏才允许买。当然,你们懂得,保证书有个卵用,所以后来某天被发现玩游戏之后,给我们把电脑格式化了。但是之后大家还是会玩。

上了大学之后,就有网络中心,第一次去网络中心的时候,有点懵逼,和 xh 两个人对着电脑不知道该干啥。旁边的人都在玩 mud,当时不懂,就是看着一个绿窗口觉得很神奇。当然后来我们玩 mud 到昏天黑地就不多说了。玩 mud 玩的多了,慢慢了解到了 mud 的机制,有时候自己搭一个开始研究里面的编程,不过还比较粗浅。当时学的也就是 fortrun,c 语言这些。

那会的搜索引擎是目录式的,就和黄页一样,除了去看一些门户,就是翻这些东西,希望发现一些有意思的东西。那会学习也基本是靠买书和看自带的文档(不如 msdn),和通过网络靠一些小论坛里面的一些交流。那会写的比较多的是 vc vb 这些,php 之类学的人不多,java 我记得好像也不多。我的毕业论文是一个 vb 的程序,现在看写的比较烂,也就是能用。

开始讲网站开发

前面废话太多,又不太舍得删掉,就分一下块吧,下面讲讲网站开发技术的进化。

dreamwaver & frontpage 时代

那会做网页比较多的应该是 asp,用 frontpage 写,用 iis 发布,都是图形界面的,linux 没有什么人会用。linux 下面那会各种问题,驱动输入法这些都得花费很大的精力去解决。我装的第一个 linux 应该是 xterm linux,11 张光盘,因为他把一些软件也刻录到光盘了。第一次装 linux 就把宿舍的电脑搞挂了,没选对直接重新分区了。不过也就当年折腾这些弄明白了分区表 mbr 这些东西。

那会还攒了一些书,有一本就是讲怎么用 dreamwaver 的,当时觉得这个东西真牛逼,抛弃了 frontpage。我还记得我用 frontpage 做的第一个自我感觉良好的网页给别人看的时候,人家的那个表情,基本就是想吐的那种。我现在想,典型的那些花背景底色,大字体,图片按钮,来回滚动的一些内容等等,都是很烂的。

基于对 dreamwaver 的熟悉,当时还去联想实习了一段时间,给他们解决了一些问题。

那会做网页一般也就是收个表单,存到数据库什么的,不会有太复杂的东西。做个论坛是最常见的了。那个年代 javascript 是忌讳的东西,一般也就是用来滚动个页面标题什么的,没人拿他做特牛逼的东西。

毕业之后,就进了出版社。那个时代桌面程序很流行,因为大部分电脑都不能上网的,我到了出版社凭着自己的兴趣,用 vb 做了一个库存管理的软件,其实就是入库多少,出库多少登记一下,简单的很。

后来也是自己的兴趣,找了一台没人用的电脑,windows 上面跑起了 java web 服务,那会还流行搞一些 java applet 跑。出版社最早的网站就是这么跑起来的。

在后来装了一个 linux,开始在上面跑 qmail,跑 apache,开始熟悉 linux。同时也有了 php 的网站,不是我开发的,跑在我那个 linux 机器上面。

ajax & mvc

后来大概有几年没有接触这些了,大概 08 年左右接触到了 ajax,javascript 算是就那几年走上神坛。当时老何演示了一个跑在本地浏览器里面的留言本程序,服务器端不用提供任何 html 相关的服务,只需要保存数据返回数据就好,逻辑都跑在本地浏览器里面。

不过那个时候我写的比较多的还是那种古老的表单方式,自己没去仔细了解这种新技术。后来大概 09 年左右有机会开始写 javascript 的时候了解了一些机制,感觉这种方式很有意思,渲染逻辑跑在浏览器,这样节省了一些服务器端的压力,并不是什么操作都需要提交到服务器让服务器去判断下一步怎么办。那会用到了 jquery,感觉真的好方便,并且他还提供一些 css 的效果,让不太熟悉的人也可以很方便的搞起来。而服务器端那会用 php 比较多。

php 当时也接触了一些框架,名字是不记得的。我这种野路子程序员,根本不明白人家的一些设计思路,代码和模板分离,mvc 这些都不懂,不过代码看多了也能明白一些。

我记得 09 年写这些的时候,一个遗留的 php 网站,需要增加一点功能,发现需要我一个人在 n 个文件里面修改,楞要把一个功能分散个 n 个文件让我很不理解,不是蛋疼么。当然后来明白这么做是为了分工合作为了灵活扩展,那么如果就一个人写的时候,我们是不是也需要这么做呢?

emberjs & angular & react & django & spring

后来大概 10 年左右的时候,我写代码还是用 jquery 的方式来做,因为当时觉得很好用。再后来接触了一些 emberjs angular 的一些知识。学习到了 emberjs 里面的命名惯例,比如一个路由叫 posts 那么就有对应的 PostsController,不用你显式的去让他们产生关系。学习 angular 学到了双向绑定的概念,你在定义了一个模型之后,在模板里面用到变量的地方会自动跟着这个模型的数据变化而变化,这就比用 jquery 的时候需要你自己去替换 dom 里面的内容方便多了。然后到了现在的 react 又提出了 virtual dom 的概念,让你不需要关心 dom 结构,你只需要操作你的虚拟 view 就好了。

当然前端其实还有好多的进步,比如 coffee script 来解决 javascript 这个语言本身的一些问题,哪里不严谨,一些最佳实践什么的就不用你操心了,只需要写 coffee script 就好了,编译成 javascript 之后,会自动处理好。

还有比如 css 不能 include,import,很多方案来解决这个问题。

后端这块,最近接触了 django,spring 这些。程序员要分 2 类的话,那就是 java 的和非 java 的,要分三类的话,就是 java ,javascript 和其他,呵呵。看过之后感觉 spring 真是不错的东西。难怪 java 程序员不去了解其他的,入了 java 坑就不用搞其他的了,库类很方便。并且类似 spring 这种东西,好多年前就有了,可以极大的方便 web 开发(不止 web 开发可以用)。如果能很好的驾驭 java,驾驭 tomcat jvm 这些东西,那么使用 java 其实是很不错的方案。通过 java 的 oo 特性,比如继承,重载 interface abstract 这些,可以很方便的定义好一些规范,也可以很好的把大家的工作划分开,架构,定义模型定义接口的人大家协同工作。

以前文件路径就是网页路径,现在有了 route 的概念(这个概念在 angular, emberjs 里面也有),做了 route mapping 之后,可以很方便的把不同的 route 映射到相同的方法上面,可以很方便复用代码逻辑。

总结下

上面讲了很多,整个就是个进化史。以前弄个网站出来需要花不少时间,还很难看。现在基于 bootstrap 还有这些开发框架,做一个出来很快外观还好看。这就是进步。

计算机技术的颠覆是很快的,如果打算一直从事这块工作,建议还是得保持一颗持续学习的心态,也得扩展自己学习的广度,这样可以吸收到各种技术的长处。

现在学习的手段也多了很多,我看视频直播网站都有开始讲课的了(不一定靠谱),还有各种学习视频,更别说各种可参考的学习文档了。实在是太方便了,只要愿意花时间,进步速度绝对很快。

LLD in zabbix

Published on:

如果需要监控的内容比较多的时候,手动管理报警信息就已经不使用了,加一批机器就需要忙活一阵子。也不能体现我们充满智慧的大脑的作用。

zabbix 支持 LLD(low level discovery) 方式来自动产生监控项目,包括 item, trigger 这些都可以自动添加。大概讲解一下可以利用这个东西做什么事情。

zabbix 收集数据的方式

zabbix 有很多收集数据的方法,这里重点讲 2 个,一个是 zabbix agent,一个是 zabbix traper。这两个方式可以和 nagios 里面的 active 和 passive 方式做类比。traaper 方式对应的就是 passive,就是 client 主动发送数据给 server。

对于 zabbix agent 方式,我们可以自己定义一些 userParameter 来添加自定义监控,这些网上很多例子。如果使用 trapper 方式,那么原则上面可以不用做任何自定义,就可以通过 zabbix-sender 或者自己模拟 sender 的协议,通过比如 python,java 等发送自己的监控信息。通过 python 发送的例子网上也有。

LLD

参考这里,LLD 主要的思路就是给服务器端发送一个 json 数据格式。例如下面这个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"data":[
{ "{#FSNAME}":"/", "{#FSTYPE}":"rootfs" },
{ "{#FSNAME}":"/sys", "{#FSTYPE}":"sysfs" },
{ "{#FSNAME}":"/proc", "{#FSTYPE}":"proc" },
{ "{#FSNAME}":"/dev", "{#FSTYPE}":"devtmpfs" },
{ "{#FSNAME}":"/dev/pts", "{#FSTYPE}":"devpts" },
{ "{#FSNAME}":"/lib/init/rw", "{#FSTYPE}":"tmpfs" },
{ "{#FSNAME}":"/dev/shm", "{#FSTYPE}":"tmpfs" },
{ "{#FSNAME}":"/home", "{#FSTYPE}":"ext3" },
{ "{#FSNAME}":"/tmp", "{#FSTYPE}":"ext3" },
{ "{#FSNAME}":"/usr", "{#FSTYPE}":"ext3" },
{ "{#FSNAME}":"/var", "{#FSTYPE}":"ext3" },
{ "{#FSNAME}":"/sys/fs/fuse/connections", "{#FSTYPE}":"fusectl" }
]
}

这个数据里面,data 是必须的,里面包含里面发现的可监控数据,这可以是任何数据。例子里面是发现了可以用来监控的磁盘分区。data 是个数组,每个可监控项是一个数组元素。还有类似下面这样的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"data": [
{
"{#HOST}": "Japan 1",
"{#COUNT}": "5"
},
{
"{#HOST}": "Japan 2",
"{#COUNT}": "12"
},
{
"{#HOST}": "Latvia",
"{#COUNT}": "3"
}
]
}

这个是发现了一些可监控的 host。

理解没有?发现是发现可监控的服务,并不是发现监控项。比如我们可以通过发现这机器上面有没有启动 ssh,发现有启动之后,我们就可以通过服务器端配置 discovery 自动添加一些监控规则。

1
2
3
4
5
6
{
"data": [
{ "{#SSH_PORT}": "22" },
{ "{#SSH_PORT}": "8022" }
]
}

比如上面这个,我们发现了 2 个 ssh 进程,一个是 22 端口,一个是 8022 端口。

所以重点是发现有什么可监控的服务,并不是发现监控项。

BUT,其实并不是不能发现监控项,也是可以的。不过是,这种被发现的监控项,除非对应的 trigger 也都是一样的,否则你会发现无法分别添加不同的 trigger 规则。

发现监控项

有了发现服务之后,就肯定需要对相应的服务的一些监控项做监控了。这个给 discovery 规则配置 item prototype 就可以了,不过这个里面有点坑需要填,后面会说,这里先不讲。

那么比如对于 ssh 服务,可以监控

  • 当前链接人数,conn.cnt
  • 配置文件的 md5,conf.md5(配合 zabbix trigger 可以用来监控文件是不是被修改了)

那监控数据就如下面

1
2
3
4
5
6
{
"22.conn.cnt": 4,
"22.conf.md5": "18492113fb263c9d0a33c9fea403eea1",
"8022.conn.cnt": 9,
"8022.conf.md5": "6cab272daa07202ccb57c4064c0dcfb8"
}

上面就是一个 discovery 项目,filter 是 {#SSH_PORT},和 2 个 item prototype,分别是 {#SSH_PORT}.cnn.cnt 和 {#SSH_PORT}.conf.md5。

复杂一点的 LLD

一个 LLD 还可以发现多个服务。比如下面这种。

1
2
3
4
5
6
7
8
{
"data": [
{ "{#SSH_PORT}": "22" },
{ "{#SSH_PORT}": "8022" },
{ "{#PG_PORT}": 5432 },
{ "{#PG_PORT}": 6432 }
]
}

这个除了我们前面讲的 ssh 服务,还发现了两个 pg 的服务。在服务器端,只需要添加两个 discovery 规则就可以了,分别使用 {#SSH_PORT} 和 {#PG_PORT} 这两个宏来过滤数据。

1
2
3
4
5
6
7
8
9
10
{
"data": [
{ "{#SSH_PORT}": "22" },
{ "{#SSH_PORT}": "8022" },
{ "{#PG_PORT}": 5432 },
{ "{#PG_PORT}": 6432 }
{ "{#MASTER_DB_PORT}": 5432, "{#SLAVE_DB}": "host1" },
{ "{#MASTER_DB_PORT}": 5432, "{#SLAVE_DB}": "host2" },
]
}

上面这个,除了有 2 个 db 之外,还有一个 db 是个 master,能看到他对应的 slave 有哪些。要注意,我们在新增加的这个发现项里面,不能再使用 {#PG_PORT} 这个宏了,因为如果使用了这个宏,就会和第3,4个项目无法区分了。所以我们改了一下名字。

到此为止,只是我们的构思,想要告诉 zabbix 我们想要监控什么。真正使用还需要走一些路。

如何发送数据

不管是 discovery 数据,还是 item 的监控数据,都可以通过 agent 和 trapper 方式发送。

对于 discovery 数据,使用 agent 发送就是上面讲的格式。

1
2
3
4
5
{
"data": [
{ "{#PG.OTHER}": "0" },
]
}

如果使用 trapper 方式发送,格式如下

1
2
3
4
5
6
7
8
9
10
{
"data": [
{
"host": "HOST1",
"value": "{\"data\": [{\"{#PG.OTHER}\": \"0\"}]}",
"key": "pg.discover"
}
],
"request": "sender data"
}

上面这个数据里面,data 和 request 是 zabbix sender 的固定格式。data 里面,包含了 host, value, key 三个字段。host 是被监控的 host,和将来服务器端的 host 对应。value 是发送的监控内容,可以看到也就是我们使用 agent 发送的内容。key 就是对应的监控项,这个监控项也就是 agent 方式发送对应的那个 userParameter。

使用 trapper 方式发送里面,是可以伪造被监控的 host 的,所以 trapper 方式并不要求一定要在被监控机器上面执行。

对于 item 监控数据,使用 agent 发送是下面这种格式。

1
2
3
4
{
"key1": 2,
"key2": "ok"
}

使用 trapper 方式发送,是下面的这种格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"data": [
{
"host": "HOST1",
"value": 1,
"key": "key1"
},
{
"host": "HOST1",
"value": "ok",
"key": "key2"
}
],
"request": "sender data"
}

zabbix 里面的限制

上面的例子很完美,但实际上 zabbix 是有一些限制的。比如 item 定义。

假如对于发现的 pg 服务,有一个监控项是连接数,比如 {#PG_PORT}.conn.cnt,此时你会发现在 zabbix 新建 item 的 Key 那个设置里面,这么写无法提交。需要使用假装类似 userParameter 的方式来写,比如 pg.[{#PG.PORT}.conn.cnt],假装那个 pg. 是个 userParameter 命令,[{#PG.PORT}.conn.cnt] 里面的内容是他的参数。当然,这个 pg. 可以基本可以是任何字符串,比如 abc,你自己觉得有意义就好了。

那么这个时候对于发现那块,我们基本不用动,需要动的是被发送的服务的监控项的命名上面。

比如以那个 ssh 的监控为例,原来发送的数据如下

1
2
3
4
5
6
{
"22.conn.cnt": 4,
"22.conf.md5": "18492113fb263c9d0a33c9fea403eea1",
"8022.conn.cnt": 9,
"8022.conf.md5": "6cab272daa07202ccb57c4064c0dcfb8"
}

我们只需要修改成这样

1
2
3
4
5
6
{
"ssh[22.conn.cnt]": 4,
"ssh[22.conf.md5]": "18492113fb263c9d0a33c9fea403eea1",
"ssh[8022.conn.cnt]": 9,
"ssh[8022.conf.md5]": "6cab272daa07202ccb57c4064c0dcfb8"
}

对应的 2 个 item prototype,key 分别修改为 ssh[{#SSH_PORT}.cnn.cnt] 和 ssh[{#SSH_PORT}.conf.md5]。那个 ssh 可以随意起。并且其实并不一定就得是这种模式,比如叫做 ssh.conf.md5[{#SSH_PORT}] 应该也可以,当然需要你发送的数据也做对应修改。

如何发送监控数据

咦?好像说过一次了?这次和上面不一样,呵呵。

设计好并写好监控之后,选择什么方式发送监控数据呢。我选择的是 discovery 数据通过 agent 方式获取,也就是在各服务器上面定义相同的一个 key,然后执行这个 key 的时候发送发现的服务信息。

而对于监控项数据则通过 trapper 方式发送。通过 trapper 方式发送,需要定时执行,可以通过 crontab 发送。我选择的是建立了一个 agent 类型的 item,执行这个 item 的时候发送监控数据。这样一方面可以针对这个发送动作建立一个监控,另外一方面调整很方便,zabbix 界面修改就可以。并且我把这个 item 建立到了模板上面,只要修改应用模板就可以了。

监控数据也可以用 agent 方式发送,如果用 agent 方式发送,对于上面的 ssh 服务,就需要真的建立那个 ssh 的 userParameter 了,然后接受比如 22.conf.md5 这样的参数,去返回对应的监控数据。我没有用这种方式,是因为这样做等于有多少个 item 就需要在监控周期内执行多少次那个命令,给服务器增加负担(虽然没多少)。而使用 trapper 方式的话,就可以一次把所有的监控数据都发过去了,命令只需要执行一次。

如何应对不同的部分

到此为止,应该可以很完美的发现服务,并且监控了。但是会发现其实并不是所有服务器的服务都是一样的,比如对于 pgsql,slow query 的界定对于不同的业务可能不一样。而因为 trigger 也是自动发现添加的,这样也有可能需要不同的机器上面的服务有不同的阈值,怎么解决呢?

先说监控项的阈值。因为我的监控数据其实是通过建立一个 agent 类型的 item 定期发送 trapper 数据来实现的,所以只需要在调用那个 item 的时候传送不同的阈值就可以了。实际上面我的 itme key 定义是这样的 pg.sendtrap[{$PG.DISCOVER.SETTINGS}] 。那个 pg.sendtrap 是对应到一个 userParameter 的 UserParameter=pg.sendtrap[*],/etc/zabbix/bin/zabbix_pg.py --check --sendtrap --settings $1,在 zabbix_pg.py 里面,会处理 settings 参数。如果有阈值,那就定义好 {$PG.DISCOVER.SETTINGS} 这个宏就可以了。template 上面可以定义默认的阈值,当然默认阈值在程序里面定义也可以。然后不同 host 可以定义 host 的阈值,会覆盖模板的配置。

其实 trigger 的阈值和这个思路类似,也是 template 里面定义一个宏,trigger 里面使用这个宏就可以了。如果 host 有不同的阈值,那就定义一个 host 的宏覆盖他就可以了。

目前的情况

配合 zabbix 的 auto registration 这个 action,可以做到新机器只需要执行一个 saltstack state,安装好我们的 zabbix agent,就可以自动注册 host,自动添加监控报警。

相当完美。