织网

身体和灵魂,总有一个在路上

Celery的Crontab实践

| Comments

有时候我们需要处理耗时的操作,同时又要保持较快的响应速度,就需要借助异步队列的帮助。Celery 作为异步队列服务,想必是很多人和我一样的选择。用法在官方文档也详细介绍,不再赘述。

这次想记录的是用 Celery 来实现定时任务。这里也有一点点坑。

main.py 的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from celery import Celery
from lib import distribute
from celery.schedules import crontab

app = distribute.app
app.conf.update(
    CELERYBEAT_SCHEDULE = {
        'every-minute': {
            'task': 'test_cron',
            'schedule': crontab(minute="*"),
            'args': (16, 13),
        }
    },
    CELERY_INCLUDE=("apps.tasks",)
)
if __name__ == '__main__':
    app.start()

实际工作单元,我放在 apps 目录下的 tasks.py 文件中

1
2
3
4
from lib.distribute import app
@app.task(name="test_cron")
def mul(x, y):
    return x * y

上述是一个简单的 Crontab 应用,它仅需要以下命令就能执行, 其中 --beat 表示 crontab 的应用

1
python main.py worker --beat -l info

起初我想把异步队列和定时任务放在一起,就加上了一句 CELERY_QUEUES 的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.conf.update(
    // 添加的部分
    CELERY_QUEUES=(
        Queue(
          'test', Exchange('test_exchange'),
           routing_key='test_queue'
        ),
    ),
    CELERYBEAT_SCHEDULE = {
        'every-minute': {
            'task': 'test_cron',
            'schedule': crontab(minute="*"),
            'args': (16, 13),
        }
    },
    CELERY_INCLUDE=("apps.tasks",)
)

同样用上述命令开启worker,发现这个时候 Crontab 不能工作了,后来看到官方的文档:

celery beat and celery worker as separate services instead.

也就是说 Celery 的 Beat 需要和其他异步worker 分开,单独执行。

相关代码链接

环境变量的那些事

| Comments

四种模式下的环境变量加载

名词解析

  1. login shell: 指用户以非图形化界面 ssh登陆到机器上时获得的第一个 shell。
  2. interactive: 交互式,有输入提示符,它的标准输入输出和错误输出都会显示在控制台上。
  • interactive + login shell

比如登陆机器后的第一个 shell 就是这种场景。它首先加载 /etc/profile,然后再依次去加载下列三个配置文件之一,一旦找到其中一个便不再接着寻找

1
2
3
~/.bash_profile
~/.bash_login
~/.profile

设计如此多的配置是为了兼容 bourne shell 和 C shell,尽量杜绝使用 .bash_login,如果已创建,需要创建 .bash_profile 覆盖

  • noninteractive + login shell

bash -l script.sh 就是这种场景。-l 参数是将shell作为一个login shell启动,配置文件的加载与第一种完全一样。

  • interactive + non-login shell

在一个已有shell中运行bash,此时会打开一个交互式的shell,因为不再需要登陆,所以不是login shell。启动 shell 时会去查找并加载

1
2
/etc/bash.bashrc
~/.bashrc 
  • non-interactive + non-login shell

比如执行脚本 bash script.sh 或者 ssh user@remote command。这两种都是创建一个shell,执行完脚本之后便退出,不再需要与用户交互。它会去寻找环境变量BASH_ENV,将变量的值作为文件名进行查找,如果找到便加载它。

从网上看到一个清晰的图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+----------------+--------+-----------+---------------+
|                | login  |interactive|non-interactive|
|                |        |non-login  |non-login      |
+----------------+--------+-----------+---------------+
|/etc/profile    |   A    |           |               |
+----------------+--------+-----------+---------------+
|/etc/bash.bashrc|        |    A      |               |
+----------------+--------+-----------+---------------+
|~/.bashrc       |        |    B      |               |
+----------------+--------+-----------+---------------+
|~/.bash_profile |   B1   |           |               |
+----------------+--------+-----------+---------------+
|~/.bash_login   |   B2   |           |               |
+----------------+--------+-----------+---------------+
|~/.profile      |   B3   |           |               |
+----------------+--------+-----------+---------------+
|BASH_ENV        |        |           |       A       |
+----------------+--------+-----------+---------------+

跨机器传递环境变量

假设要传递的变量叫做 $VARNAME

客户端机器的 /etc/ssh_config 添加

1
SendEnv VARNAME

服务端机器的 /etc/sshd_config 添加

1
AcceptEnv VARNAME

客户端机器的 $VARNAME 就可以通过 ssh 传递到服务端机器,继续使用.


参考

ssh连接远程主机执行脚本的环境变量问题

准确监控 MySQL 复制延迟

| Comments

MySQL 建立主从复制后,在 Slave_IO_Running,Slave_SQL_Runing 都是 Yes 的前提下,通过监控 Second_Behind_Master 的数值来判断主从延迟时间,该值为0时是否意味着主从同步是无延迟的呢?

1
2
3
4
5
6
7
8
mysql> show slave status\G;
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
....
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 0
...

很遗憾,我们并不能这样去判断,因为你看到的有可能是假象。

MySQL的同步是异步完成的,其中

  • IO thread 接收从主库的 binlog,然后在从库生成 relay log
  • SQL thead 解析 relay log 后在从库上进行重放

Second_Behind_Master(以下简称SBM) 是 SQL thread 在执行IO thread 生成的relay log的时间差。relay log中event的时间戳是主库上的时间戳,而SQL thread的时间戳是从库上的,SBM 代表的是从库延后主库的时间差。

主库上执行了一个大的操作,这个操作在主库上没执行完毕的时候,从库的 SBM 会显示为0,而当主库执行完毕传到从库上开始执行的时候,SBM 就会显示很大,在网络状况不好的情况下,更是容易出现 SBM 在零和一个巨大的数值反复飘忽的现象。

pt-heartbeat 帮我们准确地检测

pt-heartbeat 是 percona-toolkit 中用来检测主从延迟的工具,需要在主库和从库同时配合才能完成

  • 首先在主库创建监控的表,并定时更新
1
2
3
4
5
6
7
8
9
//创建 heartbeat 
pt-heartbeat --user=root --ask-pass \
            --host=localhost -D <YourDatabase> \
            --create-table --update 

//每隔60s,定时更新状态,以守护进程的方式执行
pt-heartbeat --user=root --ask-pass \
           --host=localhost -D <YourDatabase>\
           --interval=60 --update --replace --daemonize

它会在指定的数据库里生产一张名为 heartbeat 的表,每隔60秒定时更新binlog 文件和位置,以及时间戳。

1
2
3
4
5
+----------------------------+-----------+------------------+-----------+-----------------------+---------------------+
| ts                         | server_id | file             | position  | relay_master_log_file | exec_master_log_pos |
+----------------------------+-----------+------------------+-----------+-----------------------+---------------------+
| 2016-06-03T22:26:29.000720 |         6 | mysql-bin.004| 716| mysql-bin.002|           291330290 |
+----------------------------+-----------+------------------+-----------+-----------------------+---------------------+
  • 接着在从库以守护进程执行定期检测,并将结果重定向到文本
1
2
3
pt-heartbeat --user=root --ask-pass \
     --host=localhost -D <YourDatabase> --interval=60 \
     --file=/tmp/output.txt --monitor --daemonize

文本的内容只有一行,每隔指定的时间就会被覆盖

1
29.00s [ 30.20s,  6.04s,  2.01s ]

29s 表示的是瞬间的延迟时间,30.20s 表示1分钟的延迟时间,6.04秒表示5分钟的延迟时间,2.01秒表示以及15分钟的延迟时间,在主从机器时间校准的前提下,这个数据才是客观准确的主从延迟。

Ansible Dynamic Inventory

| Comments

Ansible 在使用的过程中,如果机器数量比较固定,且变更不多的情况下,可在 /etc/ansible/hosts 文件里面配置固定的组合机器IP, 并给他起组的别名,执行 Ansible 脚本便可以通过别名找到相应的机器。

1
2
[webservers]
111.222.333.444 ansible_ssh_port=888

假如你有很多台机器,且机器经常变更导致IP时常变换,你还想把IP逐个写入 /etc/ansible/hosts 就不现实了。你也许会问,若不把 IP 写进 /etc/ansible/hosts,那不是没法用 Ansible 指挥这些机器? 感谢 Ansible Dynamic Inventory, 如果我们能通过编程等手段获取变更机器的IP,我们还是有办法实现的。

Dynamic Inventory 的原理

  • 通过编程的方式,也就是动态获取机器的 json 信息;
  • Ansible 通过解析这串 json 字符串;
1
ansible -i yourprogram.py -m raw  -a 'cd /home'

Ansible Dynamic Inventory 对程序返回的 json 的转义是这样的:

1
{"devtest-asg": {"hosts": ["172.31.21.164"], "vars": {"ansible_ssh_port": 12306}}}

翻译一下就是 /etc/ansible/hosts 中的:

1
2
[devtest-asg]
172.31.21.164 ansible_ssh_port=12306

一个实战的例子

官方文档对 Inventory 仅作概念性描述,阅读完后仍是一头雾水,不知如何下手。 让我们用一个例子来豁然开朗吧。 我们使用 AWS 的 AutoScaling Group,以下简称 ASG,ASG 会在某种自定义的条件下会自动开启和关闭机器,这给我们在辨别IP,定位机器的时候造成困扰。因此我们需要 Ansible Dynamic Inventory

我们使用 AWS 的 boto 库来获取 ASG 的实例信息.以下程序(get_host.py)中要实现的方法就是列出返回机器信息的 json 串。

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
import boto
import boto.ec2
import boto.ec2.autoscale

AWS_REGION = 'BBB'
AWS_ACCESS_KEY = 'xxxx'
AWS_SECRET_KEY = 'yyy'

result = {}
def getData():
    conn_as = boto.ec2.autoscale.connect_to_region(
            'cn-north-1',
            aws_access_key_id=AWS_ACCESS_KEY,
            aws_secret_access_key=AWS_SECRET_KEY)
    group = conn_as.get_all_groups(names=['devtest-asg'])[0]
    conn_ec2 = boto.ec2.connect_to_region(
            AWS_REGION,
            aws_access_key_id=AWS_ACCESS_KEY,
            aws_secret_access_key=AWS_SECRET_KEY)

    instance_ids = [i.instance_id for i in group.instances]
    reservations = conn_ec2.get_all_instances(instance_ids)
    instances = [i for r in reservations for i in r.instances]

    result['devtest-asg'] = {}
    result['devtest-asg']['hosts'] = []
    for r in reservations:
        for i in r.instances:
            result['devtest-asg']['hosts'].append('%s' % i.private_ip_address)
            result['devtest-asg']['vars'] = {'ansible_ssh_port': 36000}

def getlists():
    getData()
    print json.dumps(result)

getlists()

执行以下命令就可以愉快地使用 Ansible 了,其中 devtest-asg 是 ASG 的别名:

1
ansible -i get_host.py  devtest-asg -m raw -a 'ls /'

Flume 实时收集 Nginx 日志

| Comments

在分布式系统中,各个机器都有程序运行的本地日志,有时为了分析需求,不得不这些分散的日志汇总需求,相信很多人会选择 Rsync,Scp 之类, 但它们的实时性不强,而且也会带来名字冲突的问题。扩展性差强人意,一点也不优雅。

现实中,我们就碰到了这样的需求:实时汇总线上多台服务器的 Nginx 日志。Flume 立功了。

Flume 简介

Flume 是一个分布式,可靠高效的日志收集系统,它允许用户自定义数据传输模型,因此可扩展性也强。也有较强的容错和恢复机制. 以下是几个重要的概念

  • Event:Event 是 Flume 数据传输的基本单元。flume 以事件的形式将数据从源头传送到最终的目的。
  • Agent:Agent包含 Sources, Channels, Sinks 和其他组件,它利用这些组件将events从一个节点传输到另一个节点或最终目的。
  • Source:Source负责接收events,并将events批量的放到一个或多个Channels。
  • Channel:Channel位于 Source 和 Sink 之间,用于缓存进来的events,当Sink成功的将events发送到下一跳的channel或最终目的,events从Channel移除。
  • Sink:Sink 负责将 events 传输到下一跳或最终目的,成功完成后将events从channel移除。

  • Source 就有 Syslog Source, Kafka Source,HTTP Source, Exec Source Avro Source 等。
  • Sink 有 Kafka Sink, Avro Sink, File Roll Sink, HDFS Sink 等。
  • Channel 有 Memory Channel,File Channel 等

它提供了一个骨架,以及多种 Source, Sink, Channel, 让你设计合适的数据模型。事实上也可以多个 Flume 联动完成,就像地铁的车厢一样。

定义数据流模型

回到我们开头的场景,我们要将多台服务器的 Nginx 日志进行汇总分析,

分成两个 flume 来实现

  • Flume1 数据流是 Exec Source -> Memory Channel -> Avro Sink,部署在业务机器上
  • Flume2 数据流是 Avro Source -> Memory Channel -> FileRoll Sink

需要的准备

你需要安装

  • 下载 Flume
  • 安装 JavaSDk,并在下载解压之后的 conf/flume-env.sh,配置
1
2
# 我用的是oracle-java-8
export JAVA_HOME=/usr/lib/jvm/java-8-oracle/jre/
  • 思考你的数据流动模型,编写配置,如上文所说的Flume1, tail2avro.conf :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
agent.sources = s1
agent.channels = c1
agent.sinks = k1

agent.sources.s1.type=exec
agent.sources.s1.command=tail -F <Your File Path>
agent.sources.s1.channels=c1

agent.channels.c1.type=memory
agent.channels.c1.capacity=10000
agent.channels.c1.transactionCapacity=10000

agent.sinks.k1.type = avro
agent.sinks.k1.hostname = <Your Target Address>
agent.sinks.k1.port = <Your Target Port>
agent.sinks.k1.channel=c1

Flume2 中的 avro2file.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
agent.sources = s1
agent.channels = c1
agent.sinks = k1

agent.sources.s1.type = avro
agent.sources.s1.bind = <Your Address>
agent.sources.s1.port = <Your Port>
agent.sources.s1.channels = c1

agent.sinks.k1.type = file_roll
agent.sinks.k1.sink.directory = /data/log/ngxlog
# 滚动间隔
agent.sinks.k1.sink.rollInterval = 86400
agent.sinks.k1.channel = c1

agent.channels.c1.type = memory
# 队列里 Event 的容量
agent.channels.c1.capacity = 10000
agent.channels.c1.transactionCapacity = 10000
agent.channels.c1.keep-alive = 60
  • 启动运行
1
2
3
4
5
6
7
# 启动flume1
bin/flume-ng agent -n agent -c conf -f conf/tail2avro.conf \
-Dflume.root.logger=WARN

# 启动flume2
in/flume-ng agent -n agent -c conf -f conf/avro2file.conf \
-Dflume.root.logger=INFO

参考

Redis 该选择哪种持久化配置

| Comments

这个标题或许会让你想起《黑客帝国》里经典的台词,你要选择蓝色药丸,还是红色药丸?

Redis 是我们重度使用的一个开源软件,对它的持久化配置做一番相对深入的总结,是值得的。目前它有两种主流的持久化存储方式 SnapShot 以及 AOF 。

什么是 Snapshot

Snapshot 将内存中数据以结构化的方式序列化到 rdb 文件中,是默认的持久化方式,便于解析引擎快速解析和内存实施。快照得由间隔时间,变更次数同时符合才会触发, 该过程中并不阻塞客户端请求,copy-on-write 方式也意味着极端情况下可能会导致实际数据2倍内存的使用量。它首先将数据写入临时文件,结束后,将临时文件重名为 dump.rdb。可以使用 redis-check-dump 用来检测完整性

只有快照结束后才会将旧的文件替换成新的,因此任何时候 RDB 文件都是完整的。如果在触发 snapshot 之前,server 失效。会导致上一个时间点之后的数据未能序列化到 rdb 文件,安全性上稍弱。

我们可手动执行 save 或 bgsave 命令让 redis 执行快照。两个命令的区别在于:

  • save 是由主进程进行快照操作,会阻塞其它请求;
  • bgsave 会通过 fork 子进程进行快照操作;

RDB 文件默认是经过压缩的二进制文件,占用的空间会小于内存中的数据,更加利于传输。设置如下,可以关闭快照功能

1
save ""

相关配置

1
2
3
4
5
6
7
8
# snapshot触发的时机,save <seconds> <changes>, 比如600秒有2个操作
save 600 2
# 当snapshot 时出现错误无法继续时,是否阻塞客户端变更操作 
stop-writes-on-bgsave-error yes 
# 是否启用rdb文件压缩,默认为 yes cpu消耗,快速传输  
rdbcompression yes  
# rdb文件名称  
dbfilename dump.rdb  

什么是 AOF

Append-only file,将 操作 + 数据 以格式化指令的方式追加到操作日志文件的尾部,在 append 操作返回后, 已经写入到文件或者即将写入,才进行实际的数据变更,日志文件保存了历史的操作过程;当 server 需要数据恢复时,可以直接回放此日志文件,即可还原所有的操作过程。 如果你期望数据更少的丢失,那么可以采用 AOF 模式。可以用 redis-check-aof 检测文件是否完整。

AOF 就是日志会记录变更操(例如:set/del等),会导致AOF文件非常的庞大,意味着server失效后,数据恢复的过程将会很长;事实上,一条数据经过多次变更,将会产生多条AOF记录,其实只要保存当前的状态,历史的操作记录是可以抛弃的, 由此催生了 AOF ReWrite。

什么是 AOF Rewrite

其实是压缩 AOF 文件的过程,Redis 采取了类似 Snapshot 的方式:基于 copy-on-write,全量遍历内存中数据,然后逐个序列到 aof 文件中。因此 AOF Rewrite 能够正确反应当前内存数据的状态, Rewrite 过程中,新的变更操作将仍然被写入到原 AOF 文件中,同时这些新的变更操作也会被收集起来, 并不阻塞客户端请求。

相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
##只有在yes下,aof重写/文件同步等特性才会生效  
appendonly no  
  
##指定aof文件名称  
appendfilename appendonly.aof  
  
##指定aof操作中文件同步策略,有三个合法值:always everysec no,默认为everysec  
appendfsync everysec  

##在aof-rewrite期间,appendfsync 是否暂缓文件同步,no 表示不暂缓,yes 表示暂缓,默认为no  
no-appendfsync-on-rewrite no  
  
##aof文件rewrite触发的最小文件尺寸 只有大于此aof文件大于此尺寸是才会触发rewrite,默认64mb,建议512mb  
auto-aof-rewrite-min-size 64mb  
  
##相对于上一次rewrite,本次rewrite触发时aof文件应该增长的百分比
auto-aof-rewrite-percentage 100  

appendfsync 方式:

  • always:每一条 aof 记录都立即同步到文件,这是最安全的方式,但是更多的磁盘操作和阻塞延迟,IO 开支较大。
  • everysec:每秒同步一次,性能和安全也是redis推荐的方式。如果服务器故障,有可能导致最近一秒内aof记录丢失。
  • no:redis并不直接调用文件同步,而是交给操作系统来处理,操作系统可以根据buffer填充情况等择机触发同步;性能较好,在物理服务器故障时,数据丢失量会因OS配置有关。

选择哪种药丸

  • AOF更安全,可将数据及时同步到文件中,但需要较多的磁盘IO,AOF文件尺寸较大,文件内容恢复相对较慢, 也更完整。
  • Snapshot,安全性较差,它是正常时期数据备份及 master-slave 数据同步的最佳手段,文件尺寸较小,恢复数度较快。

主从架构的环境下的选择

  • 通常 master 使用AOF,slave 使用 Snapshot,master 需要确保数据完整性,slave 提供只读服务.
  • 如果你的网络稳定性差, 物理环境糟糕情况下,那么 master, slave均采取 AOF,这个在 master, slave角色切换时,可以减少时间成本;

实时监控 Nginx Qps 的拓展

| Comments

用下班时间写了一个 ngx lua 的拓展, GitHub。可以:

  • [x] 实时监控域名的 qps
  • [x] 实时监控域名的 5xx 个数
  • [x] 实时监控域名的 响应时长
  • [x] 并附带 Ganglia 监控插件

使用

  • 配置 nginx.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
http {
    ...
    ...

    lua_shared_dict statics_dict    1M; # 初始化变量
    lua_package_path "/etc/nginx/ngx_lua_reqstatus/?.lua";  #路径
    log_by_lua_file "/etc/nginx/ngx_lua_reqstatus/hook.lua"; # 添加此句

    server {
        listen 80;
        server_name  justforfun.com; 

        location /{
            ...
        }
    }
    # 监控服务
    server {
        listen 127.0.0.1:6080;
        location /{
            access_by_lua_file "/etc/nginx/ngx_lua_reqstatus/status.lua";
        }
    }
}
  • 效果
1
curl localhost:6080/?domain=justforfun.com
  • 输出
1
2
3
4
5
6
Server Name:    justforfun.com
Seconds SinceLast:   1.4399998188019 secs
Request Count:      1
Average Req Time:   0 secs
Requests Per Secs:  0.69444453182781
5xx num:    0

Nginx 日志利器 GoAccess

| Comments

我们经常需要从 Nginx 日志分析得出很多有价值的东西,分析的方法是各种 shell, awk, python, 现在 GoAccess 给你另外一种选择, 值得拥有。

  • 安装 用以下的方式安装,才能得到新版的 GoAccess, 功能更健全
1
2
3
4
$ echo "deb http://deb.goaccess.io $(lsb_release -cs) main"|sudo tee -a /etc/apt/sources.list
$ wget -O - http://deb.goaccess.io/gnugpg.key | sudo apt-key add -
$ sudo apt-get update
$ sudo apt-get install goaccess
  • 推荐配置

安装完成之后,会生成一份 /etc/goaccess.conf 稍作编辑,这就是默认的配置,免去了后续每次都要定义格式

1
2
3
time-format %T   # 只有这种方式才能解决 0.0us 的显示问题
date-format %d/%b/%Y
log-format %h %^[%d:%t %^] "%r" %s %b "%R" "%u" %T
  • 使用

输出报表,报表中,我们可以看到最常访问的 IP, 接口,以及每个接口使用带宽,平均响应时长,状态码等,对业务分析有较好的便利性

1
2
3
4
5
6
7
8
9
10
11
终端显示
goaccess -f access.log 
 
输出报表,报表中,我们可以看到top ip, 接口,以及接口使用带宽,平均响应时长,状态码等
goaccess -f access.log > report.html
 
具体某段时间的输出
sed -n '/5\/Dec\/2015/,/10\/Dec\/2010/ p' access.log | goaccess -a
 
处理已经压缩的日志
zcat access.log.*.gz | goaccess

Gre 隧道与 Keepalived

| Comments

这一篇文章是做了不少功课的。

什么是 Gre 隧道

GRE 隧道是一种 IP-2-IP 的隧道,是通用路由封装协议,可以对某些网路层协议的数据报进行封装,使这些被封装的数据报能够在 IPv4/IPv6 网络中传输。Tunnel 是一个虚拟的点对点的连接,提供了一条通路使封装的数据报文能够在这个通路上传输,并且在一个Tunnel 的两端分别对数据报进行封装及解封装。Linux 上创建 GRE 隧道,需要 ip_gre 内核模块,它是Pv4 隧道的驱动程序。

假设我有2台服务器,想做这两台之间创建 GRE 隧道

  • $server_A_ip 表示服务器A的IP
  • $server_B_ip 服务器B 的内网IP
1
2
3
4
5
6
7
8
9
10
11
# 在 A 机器上执行
# 创建 GRE 协议的 虚拟网络设备, 指定本地和对端 IP
sudo ip link add gretap1 type gretap local $server_a_ip remote $server_b_ip 
sudo ip link set dev gretap1 up  # 启动该设备
sudo ip addr add dev gretap1 10.1.1.2/24 # 给该设备一个虚拟网络地址

# 在 B 机器上执行
# 创建 GRE 协议的 虚拟网络设备, 指定本地和对端 IP
sudo ip link add gretap1 type gretap local $server_b_ip remote $server_a_ip 
sudo ip link set dev gretap1 up  # 启动该设备
sudo ip addr add dev gretap1 10.1.1.3/24 # 给该设备一个虚拟网络地址

如果想停止或者删除上述网卡

1
2
ip link set gretap1 down
ip tunnel del gretap1

至此点到点得隧道建立。

什么是 vrrp 协议

VRRP(Virtual Router Redundancy Protocol), 即虚拟路由冗余协议。是实现路由器高可用的容错协议。

即将N台提供相同功能的路由器组成一个路由器组,这个组里面有一个 master 和多个 backup, 但在外界看来就像一台一样,构成虚拟路由器,拥有一个虚拟IP(vip),占有这个IP 的 master 实际负责 ARP 相应和转发 IP 数据包, 组中的其它路由器作为备份的角色处于待命状态。 master 会发组播消息,当 backup 在超时时间内收不到 vrrp 包时就认为 master 宕掉了,这时就需要根据VRRP的优先级来选举一个backup当master,保证路由器的高可用。

Keepalived 是什么

Keepalived 是一个基于 VRRP 协议来实现的服务高可用方案,可以利用其来避免IP单点故障。

  • 一个经典的案例

一个WEB服务至少会有2台服务器运行 Keepalived,一台为主服务器,一台为备份服务器, 但是对外表现为一个虚拟IP,主服务器会发送特定的消息给备份服务器,当备份服务器收不到这个消息的时候,即主服务器宕机的时候,备份服务器就会接管虚拟IP,继续提供服务,从而保证了高可用性。

怎么玩 Keepalived

  • 安装
1
sudo apt-get install keepalived

之前提到的,我们在 A, B 两台服务器建立起了 GRE 隧道了。 现在我们有一个虚拟的内网IP, 姑且叫做 $virtual_third_ip 接着在 A 服务器上

  • 配置

编辑服务器 A, B 的 /etc/keepalived/keepalived.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

global_defs {
    router_id LVS_DEVEL
}

vrrp_instance VI_1 {
    state MASTER
    interface gretap1 # 绑在建立起来的隧道上
    virtual_router_id 51
    # 优先级别,越高的设备会被弄成主设备, A,B 服务器要有所差别,其他都一样
    priority 100          advert_int 1      # 广播时间间隔
    authentication {  #  验证相关
        auth_type PASS
        auth_pass 1111
    }
    virtual_ipaddress {
        $virtual_third_ip dev eth0
    }
}

我们将服务器 A 作为 Master, 服务器 B 当做 Backup, 当服务器 A 需要停机的时候,$virtual_third_ip 就会漂移到服务器 B 上面。 我们两台机器都有相同配置的 Nginx 服务,就可以保障机器出现故障的时候,Nginx 服务丝毫不受影响。

附录

给 Tengine 加上 Lua 拓展

| Comments

Tengine 能动态加载第三方模块,成为我们青睐的选择,我们可以编译动态链接文件,而不需要重新安装 Nginx, 这对在线增强 webservice 很有帮助. 感谢 agentzh, lua-nginx-module, 可以让我们使用 lua 增强nginx的功能, 不言而喻,我们必须现有 Lua 的环境,才能运行 ngx_lua;

编译 nginx_lua

官方推荐使用LuaJit,虽然也可以使用Lua,但是即时编译(Just-In-Time Compiler)混合了动态编译和静态编译,一句一句编译源代码,但是会将翻译过的代码缓存起来以降低性能损耗。

  • 下载安装
1
2
3
4
5
wget http://luajit.org/download/LuaJIT-2.0.4.tar.gz
tar zxvf LuaJIT-2.0.4.tar.gz
cd LuaJIT-2.0.4
make
make install
  • 设置环境变量
1
2
export LUAJIT_LIB=/usr/local/lib
export LUAJIT_INC=/usr/local/include/luajit-2.0
  • 然后编译ngx-lua-module.so
1
/usr/local/share/dso_tool/ --path=/Path/To/Lua-Nginx-module
  • 设置动态库
1
2
> echo "/usr/local/lib" > /etc/ld.so.conf.d/usr_local_lib.conf
> ldconfig

在 Tengine 中启用

nginx.conf 中先加载动态库

1
2
3
dso {
    load ngx_load_module;
}

在 nginx.conf 中添加

1
lua_code_cache on/off;

来开启是否将代码缓存,所以每次变更 “*.lua” 文件时,必须重启 nginx 才可生效.

使用 ngx_lua_waf

有了基础环境,我们要开始发挥 ngx lua 的优点了, 我用他安装了 waf (web application firework) ngx_lua_waf,这是一个通过 ngx_lua 编写的 web 应用防火墙, 在使用过程中也发现了 ngx_lua_waf 一个bug,给他提了一个Pull Request, 码农生涯第一个 PR.


注: 静态编译的程序在执行前全部被翻译为机器码,而动态直译执行的则是一句一句边运行边翻译。