解决django使用多进程部署时apscheduler重复运行的问题

问题

在一个django应用中需要定时执行一些任务,所以用了APScheduler这个库。在开发中直接测试运行是没有问题的,但是用gunicorn部署以后发生了重复运行的问题:每个任务在时间到的时刻会同时执行好几遍。注意了一下重复的数量,恰恰是gunicorn里配置的worker进程数量,显然是每个worker进程都启动了一份scheduler造成。

解决

可以想到的方案有几个:

  • 用--preload启动gunicorn,确保scheduler只在loader的时候创建一次
  • 另外创建一个单独的定时任务项目,单独以一个进程运行
  • 用全局锁确保scheduler只运行一次

经过实践,只有第三个方案比较好。

  1. preload的问题:
虽然这样可以使用scheduler创建代码只执行一次,但是问题也在于它只执行一次,重新部署以后如果用kill -HUP重启gunicorn,它并不会重启,甚至整个项目都不会更新。这是preload的副作用,除非重写部署脚本,完全重启应用。
  1. 单独进程的问题:
也是因为部署麻烦,需要多一套部署方案,虽然用Docker会比较方便,但仍然不喜欢,而且同时维护两个项目也多出很多不必要的事情。
  1. 全局锁是一个较好的方案,但问题在于找一个合适的锁。
python自带的多进程多线程锁方案都需要一个共享变量来维护,但是因为worker进程是被gunicorn的主进程启动的,并不方便自己维护,所以需要一个系统级的锁。在Stackoverflow上看到有人是用了一个socket端口来做锁实现这个方案,但是我也不喜欢这样浪费一个宝贵的端口资源。不过这倒给了我一个启发,可以用文件锁,于是有了这个解决方案:
import atexit
import fcntl
from apscheduler.scheduler import Scheduler

f = open("scheduler.lock", "wb")
try:
    fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
except:
    pass
else:
    sched = Scheduler()

    @sched.interval_schedule(seconds=60)
    def mytask():
    '''
    定义你的定时任务执行的内容
    '''
        pass

    sched.start()

def unlock():
    fcntl.flock(f, fcntl.LOCK_UN)
    f.close()

atexit.register(unlock)

原理

首先打开(或创建)一个scheduler.lock文件,并加上非阻塞互斥锁。成功后创建scheduler并启动。

如果加文件锁失败,说明scheduler已经创建,就略过创建scheduler的部分。

最后注册一个退出事件,如果这个django退出,则解锁并关闭scheduler.lock文件的锁。

6063899814

最近在用lua写一个nginx模块时,遇到一个需要判断一个值是否在一个table里的问题,我有一个类似如下的一个table:

local items = { "apple", "orange", "pear", "banana" }

我怎样去判断orange是否在这个table里,在python里我可以使用如下的方法:

if "orange" in items:
    # do something

在lua里如何实现类似的方法呢?

517-340-1837 中提供了一种方法:

function Set (list)
  local set = {}
  for _, l in ipairs(list) do set[l] = true end
  return set
end

你可以使用一个类似set的结构,具体使用的方法如下:

local items = Set { "apple", "orange", "pear", "banana" }

if items["orange"] then
  -- do something
end

上面的这个方法和下面的这个方法等同:

local items = { apple=true, orange=true, pear=true, banana=true }
if items.apple then
    -- do something
end

这个方法足够简单吧,并且效率上要比遍历这个table要高。

-- eof --

Mod

在11年时,将我的博客从wordpress+mysql换成了django+sqlite,具体可见 434-974-5925 这篇文章, 在折腾精神的感召下,在12年底我将博客程序又换掉了,这次换成了一个基于 (954) 326-4053 静态博客生成器 warmhouse , tinkerer 使用 reStructuredText 作为标记语言,可以生成文章的分类,tag,支持评论和代码高亮,文章评论使用 disqus , 高亮代码使用 Pygments 。

说说为什么使用 tinkerer ,从打算把博客弄成静态站点开始我尝试了很多项目,如 octpress 、 jekyll 、 pelican ,一次偶然的机会在 bitbucket 上发现了 tinkerer ,试用后感觉比较顺手,并且可以使用sphinx的扩展,我比较熟悉 sphinx 和 reStructuredText 的使用,平时就用这两个东西写文档,因此选择了基于 sphinx 的 tinkerer 。我最开始使用的是0.4 beta版还存在一些bug,然后我自己做了一些修改pull给作者,作者很快就接受了我的pull request,并表示感谢,现在1.0版本已经发布,基本上没有比较明显的bug了,只是有一些细节可能不是那么完美,还有一个就是主题很少,目前默认的几款主题,都不是很美观,目前我在移植一款主题。

tinkerer的一些使用经验:

* 自定义博客的侧边栏

这里以创建友情链接的plugin作为例子,复制boilerplate主题目录下recent.html为一个新的文件,如friendslinks.html,然后改成如下这样就可以了:

{#-
    boilerplate/friendslinks.html
    ~~~~~~~~~~~~~~~~~~~~~

    Sidebar list of all tags.

    :copyright: Copyright 2012 by Iñigo Serna
    :license: FreeBSD, see LICENSE file
-#}

<div class="widget">
    <h1>Friends links</h1>
    <ul>
        <li><a href="/www.qdyongai.cn/?from=jason">龙哥-网站设计</a></li>
        <li><a href="/www.zipeng.info/?from=jason">子鹏-kun的记事本</a></li>
        <li><a href="/www.aaronw.me/?from=jason">王炜-我的技术生活</a></li>
        <li><a href="/cuikai-wh.com/?from=jason">小轰-时光立方</a></li>
    </ul>
</div>

这里要注意的是默认的modern5主题是继承于boilerplate这个主题,因此只要修改boilerplate这个主题就可以了

然后在conf.py中加入这个文件的配置

# Add templates to be rendered in sidebar here
html_sidebars = {
    "**": ["searchbox.html", "categories.html", "recent.html", "friendslinks.html", "weibo.html"]
    }
  • 创建一个makefile,使用过 sphinx 的人应该都知道 sphinx 会生成一个 makefile,这样可以直接使用make html就可以生成文档了,tinker没有提供,那么我们可以自己写一个,让我么操作的更加自动化,如下是我 Makefile 示例,大家可以参照下:
all: build commit update

clean:
             rm -rf blog/html/

build:
             tinker -b

serve:
             cd blog/html/ && python -m SimpleHTTPServer

commit:
             hg commit ./ -m 'add new post'&&hg push
                     @echo "Done..."

update:
             ssh root@jasonwu.me 'cd /home/admin/jasonwu.me/&&hg pull&&hg update'

这个makefile主要实现了生成文档,在本地起一个http服务器来查看生成的文章效果,提交到bitbucket和更新vps上的博客内容等功能,将这些操作完全的自动化,是不是很方便,大家玩的开心。

使用NGINX+UWSGI来部署Django

关于UWSGI的介绍不多说了,想了解的可以自己去搜,uwsgi性能还蛮不错,我们之前使用的fastcgi方式来跑django,前端用F5做负载均衡,用户数增加后服务器的load很高,但换uwsgi服务器的load下降不少,废话不多说,进入正题。 编译安装nginx,用的nginx-0.8.54,目前最新的stable版本

wget /nginx.org/download/nginx-0.8.54.tar.gz
tar zxvf  nginx-0.8.54.tar.gz
./configure --user=nobody
  --group=nobody
  --prefix=/usr/local/nginx
  --with-http_ssl_module
  --http-log-path=/var/log/nginx/access.log
  --with-http_gzip_static_module
make&&make install

下面来安装uwsgi

wget /projects.unbit.it/downloads/uwsgi-0.9.6.5.tar.gz
tar zvxf uwsgi-0.9.6.5.tar.gz
cd uwsgi-0.9.6.5
make -f Makefile.Py26
cp uwsgi /usr/sbin/uwsgi    #将uwsgi放到PATH下

下面来说说怎么配置: 新建一个uwsgi.xml的文件放到django的目录下

#cat uwsgi.xml
<uwsgi>

<socket>0.0.0.0:8000</socket>

<listen>204800</listen>

<!-- 开启32个线程 -->
<processes>32</processes>

<max-requests>2048000</max-requests>

<buffer-size>8192</buffer-size>

<!-- 你的配置文件 -->
<module>django_wsgi</module>

<profiler>true</profiler>

<enable-threads>true</enable-threads>

<!-- 限制内存空间256M -->
<limit-as>256</limit-as>

<!-- 使用async模式来运行,这里要注意一下,如果你的app的是no-async-friendly 那就不要用这个模式 -->
<async>10</async>

<disable-logging/>

<daemonize>/home/app01/uwsgi.log</daemonize>

</uwsgi>
#cat django_wsgi.py
import os
import django.core.handlers.wsgi
os.environ['DJANGO_SETTINGS_MODULE'] = 'your settings'
application = django.core.handlers.wsgi.WSGIHandler()

下面是nginx的配置:

server {
        listen   80;
        server_name jasonwu.me;
        access_log /var/log/jasownu.me/access_log;
        location / {
            root   /home/app01/;
            uwsgi_pass 127.0.0.1:8000;
            include        uwsgi_params;
      }
  }

启动服务:

/usr/bin/uwsgi -x /home/app01/uwsgi.xml
/usr/local/nginx/sbin/nginx

这样部署完成了

下面来说说遇到的一个问题,不知道大家有没有遇到, 在我们启动uwsgi后在uwsgi的日志中会出现如下的信息:

– unavailable modifier requested: 1 –
– unavailable modifier requested: 1 –

表现的现象就是启动一段时间没法访问app,在查看uwsgi的源代码中我们找到打印这部份日志的段落,正常情况下应该返回的-1,目前还在查找这个出现这个错误的原因。

参考文档:

unp读书笔记(第六章I/O模型)

在看了unp后才发现自己之前的认识一直都是错误的,把阻塞I/O,非阻塞I/O,同步和异步混为一谈,之前一直觉得异步就是非阻塞io,同步就是阻塞io,主要搞清楚了,五种I/O模型的实现,以及epoll并不是aio,这两个并不是一个东西。

实际上unix有五种I/O模型:

  • 阻塞I/O
  • 非阻塞I/O
  • I/O复用(select和poll)
  • 信号驱动I/O(SIGIO)
  • 异步I/O

一个输入操作一般有两个不同的阶段: 1. 等待数据准备好 2. 从内核到进程拷贝数据 对于一个套接口上的输入操作,第一步一般是等待数据到达网络,当分组到达时,它被拷贝到内核中的某个缓冲区,第二步是将数据从内核缓冲区拷贝到应用缓冲区。

1.阻塞I/O模型

最流行的I/O模型是阻塞I/O模型,缺省时,所有套接口都是阻塞的。

/farm6.staticflickr.com/5195/7407797190_a48b6dcf80_z.jpg

2. 非阻塞I/O模型

前三次调用recvfrom时仍无数据返回,因此内核立即返回一个EWOULDBLOCK错误。第四次调用recvfrom时,数据已经准备好,被拷贝到应用缓冲区,recvfrom返回成功指示,接着就是我们处理数据。当一个应用进程像这样对一个非阻塞描述字循环调用recvfrom时,我们称此过程为轮询(polling)。应用进程连续不断地查询内核,看看某操作是否准备好,这对cpu时间是极大的浪费,但这种模型只是偶尔才遇到,一般是在只专门提供某种功能的系统中才有。

/farm6.staticflickr.com/5332/7407811366_60f22d6337_z.jpg

3. I/O复用模型

有了I/O复用,我们就可以调用select或poll,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正的I/O系统调用。如图6.3,我们阻塞于select调用,等待数据报套接口可读。当select返回套接口可读条件时,我们调用recvfrom将数据报拷贝到应用缓冲区中。使用select的好处在于我们可以等待多个描述字准备好。

/farm6.staticflickr.com/5447/7407852344_49d1a95c98_z.jpg

4. 信号驱动I/O模型

我们也可以用信号,让内核在描述字准备好时,用信号SIGIO通知我们,我们将此方法称为信号驱动I/O,如图6.4 首先我们允许套接口进行信号驱动IO,并通过系统调用sigaction安装一个信号处理程序。此系统调用立即返回,进程继续工作,它是非阻塞的。当数据报准备好被读时,就为该进程生成一个SIGIO信号。我们随即可以在信号处理程序中调用recvfrom来读数据报,并通知主循环数据已准备好被处理。也可以通知主循环,让它来读数据报。无论我们如何处理SIGIO信号,这种模型的好处是当等待数据报到达时,可以不阻塞。主循环可以继续执行,只是等待信号处理程序的通知,或者数据已准备好处理,或者数据报已准备好被读。

/farm8.staticflickr.com/7258/7407859790_b922c7fedf_z.jpg

5. 异步I/O模型

异步IO是POSIX实时扩展,我们让内核启动操作,并在整个操作完成后(包括将数据从内核拷贝到我们自己的缓冲区)通知我们。这种模型没有广泛使用。这种模型与前一节介绍的信号驱动模型的主要区别在于:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。我们调用函数aio_read(POSIX异步I/O函数以aio_或者lio_开头),给内核传递描述字,缓冲区指针,缓冲区大小(与read相同的三个参数),文件偏移(与lseek类似),并告诉内核当前整个操作完成是如何通知我们。此系统调用立即返回,我们的进程不阻塞于等待I/O操作的完成。在此例子中,我们假设要求内核在操作完成时生成一个信号,此信号直到数据已拷贝到应用缓冲区才生成,这一点是与信号驱动I/O模型不同的。

/farm8.staticflickr.com/7106/7407898142_64f4033a11_z.jpg

6. 五种不同I/O模型的比较

/farm8.staticflickr.com/7121/7407921310_b58dfa8cb5_z.jpg

7. 同步I/O与异步I/O

POSIX定义这两个术语如下: 同步I/O操作引起请求进程阻塞,直到I/O操作完成。 异步I/O操作不引起请求进程阻塞。 根据上述定义,我们的前四个模型--阻塞I/O模型,非阻塞I/O模型,I/O复用模型和信号驱动I/O模型都是同步I/O模型,因为真正的I/O操作(recvfrom)阻塞进程,只有异步I/O模型与异步I/O的定义相匹配。

Python : 什么是*args和**kwargs?

def foo(*args, **kwargs):
    print 'args = ', args
    print 'kwargs = ', kwargs
    print '---------------------------------------'
if __name__ == '__main__':
    foo(1,2,3,4)
    foo(a=1,b=2,c=3)
    foo(1,2,3,4, a=1,b=2,c=3)
    foo('a', 1, None, a=1, b='2', c=3)

输出结果如下:

| args = (1, 2, 3, 4)
| kwargs = {}
| ---------------------------------------
| args = ()
| kwargs = {'a': 1, 'c': 3, 'b': 2}
| ---------------------------------------
| args = (1, 2, 3, 4)
| kwargs = {'a': 1, 'c': 3, 'b': 2}
| ---------------------------------------
| args = ('a', 1, None)
| kwargs = {'a': 1, 'c': 3, 'b': '2'}
| ---------------------------------------

可以看到,这两个是python中的可变参数。 *args 表示任何多个无名参数,它是一个tuple; **kwargs 表示关键字参数,它是一个dict。并且同时使用 *args 和 **kwargs 时,必须 *args 参数列要在 **kwargs 前,像foo(a=1, b='2', c=3, a', 1, None, )这样调用的话,会提示语法错误“SyntaxError: non-keyword arg after keyword arg”。

呵呵,知道 *args 和 **kwargs 是什么了吧。还有一个很漂亮的用法,就是创建字典:

def kw_dict(**kwargs):
    return kwargs
    print kw_dict(a=1,b=2,c=3) == {'a':1, 'b':2, 'c':3}

其实python中就带有dict类,使用dict(a=1,b=2,c=3)即可创建一个字典了。

另:连接两个字典的方法:

第一种:

>>> a={'a':'a','b':'b'}
>>> m=dict(c='c',**a)
>>> m
{'a': 'a', 'c': 'c', 'b': 'b'}
>>>

第二种:

>>> a={'a':'a','b':'b'}
>>> m={'c':'c'}
>>> m.update(a)
>>> m
{'a': 'a', 'c': 'c', 'b': 'b'}

python的lambda函数介绍

今天在论坛上看到有人问的一个关于如何从一个python的字典中取到value中最大的那个key值,里面用到了 lambda 函数,今天那就大致介绍下 lambda 是个什么东东。 python支持创建一种匿名的函数(一种没绑定名字的函数),这种函数叫做lambda,这个和fp(函数编程)里面的lambda的含义并不是完全一致,下面这段代码将展示 lambda 和普通函数之间的区别

>>> def f (x): return x**2
...
>>> print f(8)
64
>>>
>>> g = lambda x: x**2
>>>
>>> print g(8)
64

g()是一个 lambda 函数,完成同上面普通函数相同的事情。注意这里的简短的语法:在参数列表周围没有括号,而且忽略了 return 关键字 (隐含存在,因为整个函数只有一行)。而且,该函数没有函数名称,但是可以将它赋值给一个变量进行调用。 使用 lambda 函数时甚至不需要将它赋值给一个变量。这可能不是世上最有用的东西,它只是展示了 lambda 函数只是一个内联函数。

总的来说,lambda 函数可以接收任意多个参数 (包括可选参数) 并且返回单个表达式的值。lambda 函数不能包含命令,包含的表达式不能超过一个。不要试图向 lambda 函数中塞入太多的东西;如果你需要更复杂的东西,应该定义一个普通函数,然后想让它多长就多长。 这里只是大致介绍一下,想深入研究的可以看文章后面附的文档,这里回到开头的问题,如果返回一个字典中最大的value值的key,下面为代码:

>>> dict = {'a':1,'b':2,'c':3}
>>> max(dict.iterkeys(),key=lambda k:dict[k])
'c'
>>>

下面来大致解释这段代码,先定义了一个列表,通过使用key参数改变了max比较列表元素的方法,最终达到了取得value值最大的key的目的。

接下来讲一下,python字典的 iterkeys iteritems itervalues 这三个方法,字典对象也提供keys,items,values这三个方法,那前面的三种方法和后面的三种方法有什么不一样呢,我们大致运行一下就可以知道了,前面的三个方法返回迭代器对象,而后三种方法返回的为列表对象,使用前三种方法更高效一些,后三种方法对内存占用比较大,在python 3.0中取消了iterkeys,iteritems,itervalues这三个方法,将keys,items,values这三个方法功能改为原来iter*的功能。

参考文档:

(306) 489-9946

这个是 cx_Oracle 的说明链接/cx-oracle.sourceforge.net/README.txt,我是采用源码安装 安装过程:

wget  /prdownloads.sourceforge.net/cx-oracle/cx_Oracle-5.0.3.tar.gz?download
tar zxvf   cx_Oracle-5.0.3.tar.gz
cd cx_Oracle-5.0.3
python setup.py build
python setup.py install

下面是检查它是否可以用

>>> import cx_Oracle
接着出现了两个错误,错误信息如下:
/usr/local/lib/python2.6/site-packages/cx_Oracle-5.0.3-py2.6-linux-x86_64.egg/cx_Oracle.py:3:
UserWarning: Module cx_Oracle was already imported from /usr/local/lib/python2.6/site-packages
/cx_Oracle-5.0.3-py2.6-linux-x86_64.egg/cx_Oracle.pyc,
but /usr/local/cx_Oracle-5.0.3 is being added to sys.path
ImportError: libclntsh.so.10.1: cannot open shared object file: No such file or directory

下面来说说这两个错误的解决方法, 第一个问题,提示说/usr/local/cx_Oracle-5.0.3已经加入到python的 sys.path 里面了 那么我们就在 sys.path 中去掉这个路径: 具体方法

>>> import sys
>>> sys.path
['', '/usr/local/lib/python2.6/site-packages/setuptools-0.6c11-py2.6.egg',
'/usr/local/lib/python2.6/site-packages/fudge-0.9.4-py2.6.egg',
 '/usr/local/lib/python2.6/site-packages/python_memcached-1.45-py2.6.egg',
 '/usr/local/lib/python2.6/site-packages/MySQL_python-1.2.3c1-py2.6-linux-x86_64.egg',
'/usr/local/lib/python2.6/site-packages/flup-1.0.3.dev_20100525-py2.6.egg',
'/usr/local/lib/python2.6/site-packages/regex-0.1.20100706.1-py2.6-linux-x86_64.egg',
 '/usr/local/lib/python2.6/site-packages/cx_Oracle-5.0.3-py2.6-linux-x86_64.egg',
 '/usr/local/lib/python26.zip', '/usr/local/lib/python2.6', '/usr/local/lib/python2.6/plat-linux2',
 '/usr/local/lib/python2.6/lib-tk', '/usr/local/lib/python2.6/lib-old',
'/usr/local/lib/python2.6/lib-dynload', '/usr/local/lib/python2.6/site-packages',
 '/usr/local/lib/python2.6/site-packages/PIL', '/usr/local/cx_Oracle-5.0.3']

>>> sys.path.remove(r'/usr/local/cx_Oracle-5.0.3')

第一个问题解决,接着第二个问题,这个网上有解决方案,下面说说解决方法: 这个的原因是 Oracle 的路径没有设定

locate libclntsh.so.10.1
/opt/oracle/product/10.2/db_1/lib/libclntsh.so.10.1
/opt/oracle/product/10.2/db_1/lib32/libclntsh.so.10.1
echo /opt/oracle/product/10.2/db_1/lib/ >> /etc/ld.so.conf
ldconfig

就这样第二个问题解决 再试一下:

>>> import cx_Oracle
>>>

drainman

5597830984

DR模式的工作过程:

当一个client发送一个WEB请求到VIP,LVS服务器根据VIP选择对应的real-server的Pool,根据算法,在Pool中选择一台Real-server,LVS在hash表中记录该次连接,然后将client的请求包发给选择的Real-server,最后选择的Real-server把应答包直接传给client;当client继续发包过来时,LVS根据更才记录的hash表的信息,将属于此次连接的请求直接发到刚才选择的Real-server上;当连接中止或者超时,hash表中的记录将被删除。

5738031920

LVS和Real-server必须在相同的网段

DR模式在转发client的包时,只修改了包目的MAC地址为选定的Real-server的mac地址,所以如果LVS和Real-server在不通的广播域内,那么Real-server就没办法接收到转发的包。下面是mac地址的修改过程:

/farm7.static.flickr.com/6100/6322179927_eb8e928a02_z.jpg

LVS不需要开启路由转发:

LVS的DR模式不需要开启路由转发功能,就可以正常的工作,出于安全考虑,如果不需要转发功能,最好关闭。

4252218151

通常,DR模式需要在Real-server上配置VIP,配置的方式为:

/sbin/ifconfig lo:0 inet VIP netmask 255.255.255.255

原因在于,当LVS把client的包转发给Real-server时,因为包的目的IP地址是VIP,那么如果Real-server收到这个包后,发现包的目的IP不是自己的系统IP,那么就会认为这个包不是发给自己的,就会丢弃这个包,所以需要将这个IP地址绑到网卡上;当发送应答包给client时,Real-server就会把包的源和目的地址调换,直接回复给client。

关于ARP广播:

  • 上面绑定VIP的掩码是”255.255.255.255″,说明广播地址是其本身,那么他就不会将ARP发送到实际的自己该属于的广播域了,这样防止与LVS上VIP冲突,而导致IP冲突。
  • 另外在Linux的Real-server上,需要设置ARP的sysctl选项:(下面是举例说明设置项的)

假设服务器上ip地址如下所示:

System Interface MAC Address IP Address
HN eth0 00:0c:29:b3:a2:54 192.168.18.10
HN eth3 00:0c:29:b3:a2:68 192.168.18.11
HN eth4 00:0c:29:b3:a2:5e 192.168.18.12
client eth0 00:0c:29:d2:c7:aa 192.168.18.129

当我从192.168.18.129 ping 192.168.18.10时,tcpdump抓包发现:

00:0c:29:d2:c7:aa > ff:ff:ff:ff:ff:ff, ARP, length 60: arp who-has 192.168.18.10 tell 192.168.18.129
00:0c:29:b3:a2:5e > 00:0c:29:d2:c7:aa, ARP, length 60: arp reply 192.168.18.10 is-at 00:0c:29:b3:a2:5e
00:0c:29:b3:a2:54 > 00:0c:29:d2:c7:aa, ARP, length 60: arp reply 192.168.18.10 is-at 00:0c:29:b3:a2:54
00:0c:29:b3:a2:68 > 00:0c:29:d2:c7:aa, ARP, length 60: arp reply 192.168.18.10 is-at 00:0c:29:b3:a2:68
00:0c:29:d2:c7:aa > 00:0c:29:b3:a2:5e, IPv4, length 98: 192.168.18.129 > 192.168.18.10: ICMP echo request, id 32313, seq 1, length 64
00:0c:29:b3:a2:54 > 00:0c:29:d2:c7:aa, IPv4, length 98: 192.168.18.10 > 192.168.18.129: ICMP echo reply, id 32313, seq 1, length 64
00:0c:29:d2:c7:aa > 00:0c:29:b3:a2:5e, IPv4, length 98: 192.168.18.129 > 192.168.18.10: ICMP echo request, id 32313, seq 2, length 64
00:0c:29:b3:a2:54 > 00:0c:29:d2:c7:aa, IPv4, length 98: 192.168.18.10 > 192.168.18.129: ICMP echo reply, id 32313, seq 2, length 64
00:0c:29:b3:a2:54 > 00:0c:29:d2:c7:aa, ARP, length 60: arp who-has 192.168.18.129 tell 192.168.18.10
00:0c:29:d2:c7:aa > 00:0c:29:b3:a2:54, ARP, length 60: arp reply 192.168.18.129 is-at 00:0c:29:d2:c7:aa

三个端口都发送了arp的reply包,但是192.168.18.129使用的第一个回应的eth4的mac地址作为ping请求的端口,由于192.168.18.10是icmp包中的目的地址,那么ping的应答包,会从eth0端口发出。

如果Real-server有个多个网卡,每个网卡在不同的网段,那么可以过滤掉非本网卡ARP请求的回应;但是如果多个网卡的ip在一个网段,那么就不行了。

sysctl -w net.ipv4.conf.all.arp_filter=1

对于多个接口在相同网段可以设置下面的来防止:

sysctl -w net.ipv4.conf.all.arp_ignore=1
sysctl -w net.ipv4.conf.all.arp_announce=2

还是从192.168.18.129 ping 192.168.18.10时,tcpdump抓包发现:

00:0c:29:d2:c7:aa > ff:ff:ff:ff:ff:ff, ARP, length 60: arp who-has 192.168.18.10 tell 192.168.18.129
00:0c:29:b3:a2:54 > 00:0c:29:d2:c7:aa, ARP, length 60: arp reply 192.168.18.10 is-at 00:0c:29:b3:a2:54
00:0c:29:d2:c7:aa > 00:0c:29:b3:a2:54, IPv4, length 98: 192.168.18.129 > 192.168.18.10: ICMP echo request, id 32066, seq 1, length 64
00:0c:29:b3:a2:54 > 00:0c:29:d2:c7:aa, IPv4, length 98: 192.168.18.10 > 192.168.18.129: ICMP echo reply, id 32066, seq 1, length 64
00:0c:29:d2:c7:aa > 00:0c:29:b3:a2:54, IPv4, length 98: 192.168.18.129 > 192.168.18.10: ICMP echo request, id 32066, seq 2, length 64
00:0c:29:b3:a2:54 > 00:0c:29:d2:c7:aa, IPv4, length 98: 192.168.18.10 > 192.168.18.129: ICMP echo reply, id 32066, seq 2, length 64
00:0c:29:b3:a2:54 > 00:0c:29:d2:c7:aa, ARP, length 60: arp who-has 192.168.18.129 tell 192.168.18.10
00:0c:29:d2:c7:aa > 00:0c:29:b3:a2:54, ARP, length 60: arp reply 192.168.18.129 is-at 00:0c:29:d2:c7:aa

看到了么,现在只有eth0会回应arp请求了。

arp报文格式:

/farm7.static.flickr.com/6219/6324726842_6f0aea5dab_z.jpg

请求报文:MAC地址字段是空的。 应答报文:所有字段都又内容。:

The arp_announce/arp_ignore reference:

arp_announce – INTEGER
Define different restriction levels for announcing the local
source IP address from IP packets in ARP requests sent on
interface:
0 – (default) Use any local address, configured on any interface
1 – Try to avoid local addresses that are not in the target’s
subnet for this interface. This mode is useful when target
hosts reachable via this interface require the source IP
address in ARP requests to be part of their logical network
configured on the receiving interface. When we generate the
request we will check all our subnets that include the
target IP and will preserve the source address if it is from
such subnet. If there is no such subnet we select source
address according to the rules for level 2.
2 – Always use the best local address for this target.
In this mode we ignore the source address in the IP packet
and try to select local address that we prefer for talks with
the target host. Such local address is selected by looking
for primary IP addresses on all our subnets on the outgoing
interface that include the target IP address. If no suitable
local address is found we select the first local address
we have on the outgoing interface or on all other interfaces,
with the hope we will receive reply for our request and
even sometimes no matter the source IP address we announce.

The max value from conf/{all,interface}/arp_announce is used.

Increasing the restriction level gives more chance for
receiving answer from the resolved target while decreasing
the level announces more valid sender’s information.

arp_announce 用来限制,是否使用发送的端口的ip地址来设置ARP的源地址:

  • “0″代表是用ip包的源地址来设置ARP请求的源地址。
  • “1″代表不使用ip包的源地址来设置ARP请求的源地址,如果ip包的源地址是和该端口的IP地址相同的子网,那么用ip包的源地址,来设置ARP请求的源地址,否则使用”2″的设置。
  • “2″代表不使用ip包的源地址来设置ARP请求的源地址,而由系统来选择最好的接口来发送。

当内网的机器要发送一个到外部的ip包,那么它就会请求路由器的Mac地址,发送一个arp请求,这个arp请求里面包括了自己的ip地址和Mac地址,而linux默认是使用ip的源ip地址作为arp里面的源ip地址,而不是使用发送设备上面的 ,这样在lvs这样的架构下,所有发送包都是同一个VIP地址,那么arp请求就会包括VIP地址和设备 Mac,而路由器收到这个arp请求就会更新自己的arp缓存,这样就会造成ip欺骗了,VIP被抢夺,所以就会有问题。

现在假设一个场景来解释 arp_announce :

Real-server的ip地址:202.106.1.100(public local address),
172.16.1.100(private local address),
202.106.1.254(VIP)

如果发送到client的ip包产生的arp请求的源地址是202.106.1.254(VIP),那么LVS上的VIP就会被冲掉,因为交换机上现在的arp对应关系是Real-server上的VIP对应自己的一个MAC,那么LVS上的VIP就失效了。:

arp_ignore – INTEGER
Define different modes for sending replies in response to
received ARP requests that resolve local target IP addresses:
0 – (default): reply for any local target IP address, configured
on any interface
1 – reply only if the target IP address is local address
configured on the incoming interface
2 – reply only if the target IP address is local address
configured on the incoming interface and both with the
sender’s IP address are part from same subnet on this interface
3 – do not reply for local addresses configured with scope host,
only resolutions for global and link addresses are replied
4-7 – reserved
8 – do not reply for all local addresses

The max value from conf/{all,interface}/arp_ignore is used
when ARP request is received on the {interface}

“0″,代表对于arp请求,任何配置在本地的目的ip地址都会回应,不管该arp请求的目的地址是不是接口的ip;如果有多个网卡,并且网卡的ip都是一个子网,那么从一个端口进来的arp请求,别的端口也会发送回应。 “1″,代表如果arp请求的目的地址,不是该arp请求包进入的接口的ip地址,那么不回应。 “2″,要求的更苛刻,除了”1″的条件外,还必须要求arp发送者的ip地址和arp请求进入的接口的ip地址是一个网段的。 (后面略)

2816434448

Kabyle

1> client 发送request包到LVS服务器的VIP上。

2> VIP按照算法选择后端的一个Real-server,并将记录一条消息到hash表中,然后将client的request包封装到一个新的IP包里,新IP包的目的IP是Real-server的IP,然后转发给Real-server。

3> Real-server收到包后,解封装,取出client的request包,发现他的目的地址是VIP,而Real-server发现在自己的lo:0口上有这个IP地址,于是处理client的请求,然后将relpy这个request包直接发给client。

4> 该client的后面的request包,LVS直接按照hash表中的记录直接转发给Real-server,当传输完毕或者连接超时,那么将删除hash表中的记录。

203-643-9116

LVS和Real-server不需要在一个网段:

由于通过IP Tunneling 封装后,封装后的IP包的目的地址为Real-server的IP地址,那么只要Real-server的地址能路由可达,Real-server在什么网络里都可以,这样可以减少对于公网IP地址的消耗,但是因为要处理IP Tunneling封装和解封装的开销,那么效率不如DR模式。

Real-server的系统设置:

由于需要Real-server支持IP Tunneling,所以设置与DR模式不太一样,LVS不需要设置tunl设备,LVS本身可以进行封装 i) 需要配置VIP在tunl设备上:(VIP:172.16.1.254)

shell> ifconfig tunl0 172.16.1.254 netmask 255.255.255.255
shell> ifconfig tunl0
tunl0 Link encap:IPIP Tunnel HWaddr
inet addr:172.16.1.254 Mask:255.255.255.255
UP RUNNING NOARP MTU:1480 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:0 (0.0 b) TX bytes:0 (0.0 b)

当添加tunl0设备时,自动载入需要的模块:

shell> lsmod |grep ipip
ipip 7516 0
tunnel4 2700 1 ipip

其中,ipip依赖于tunnel4,假如现在删除tunnel4的话:

shell> rmmod tunnel4
ERROR: Module tunnel4 is in use by ipip

如果添加tunl0失败,那么可能是内核没有开启tunneling功能,默认是以模块形式,加载到内核里的:

/farm7.static.flickr.com/6115/6323980779_b0369d8de9.jpg

ARP问题:

如果LVS和Real-server不在一个网络内,不需要处理ARP问题,如果在相同网络,那么处理方法和DR模式一样,但是如果一样,我就不知道选择tun模式有什么好理由了,DR似乎效率更高些吧。

6205628934

IP Tunneling模式不需要开启ip_forward功能。

715-620-7175

stick-at-it

/farm7.static.flickr.com/6117/6324734426_fc573643bf.jpg
client:202.100.1.2
VIP:202.103.106.5
Real-server:172.16.0.2 和 172.16.0.3(提供http和ftp服务)

1> client发送request到LVS的VIP上,VIP选择一个Real-server,并记录连接信息到hash表中,然后修改client的request的目的IP地址为Real-server的地址,将请求发给Real-server;

2> Real-server收到request包后,发现目的IP是自己的IP,于是处理请求,然后发送reply给LVS;

3> LVS收到reply包后,修改reply包的的源地址为VIP,发送给client;

4> 从client来的属于本次连接的包,查hash表,然后发给对应的Real-server。

5> 当client发送完毕,此次连接结束或者连接超时,那么LVS自动从hash表中删除此条记录。

下面是地址转换的过程:

/farm7.static.flickr.com/6239/6323980835_6e32e61090_b.jpg

5416226066

8339213836

  • 在Linux的2.6版本,LVS-NAT不能做防火墙,在只有一个网关的情况下,没有任何问题。
  • 防火墙不兼容:LVS的架构中,LVS的前端不能设置防火墙,修复的补丁”NFCT” patch。
  • 源路由问题

ICMP重定向问题

一. 对于路由器来说,只有当如下条件同时满足的时候,才进行重定向

  • 数据包的入接口和路由后的指定的出接口是同一个接口。
  • 数据包的源IP地址和该包应走的下一跳IP地址属于同一个网段。
  • 数据报非源路由的(这种情况应该比较少见了,源路由多见于Token Ring)。
  • 系统开启重定向功能。
例如:
两个路由器都开启了IP重定向功能。HostA 的默认网关为1.1.1.1。当HostA要和不在同一网段中的HostB通信的时候,会把数据报递交给默认网关RT1。然而RT1经过查找发现到达3.3.3.3的路径下一跳恰恰是经由自己的E0/1口的RT2接口1.1.1.2。满足上述条件,将会发生重定向。

二. LVS为什么会产生ICMP重定向问题: * 在LVS-NAT模式下,如果LVS的各个成员,client,LVS,Real-server在同一个网段(比如:192.168.1.*/24);

  • 当Real-server将Reply发送回LVS时,Reply包是 RIP -> CIP的,LVS看到RIP-> CIP实际上根本没必要经过LVS,直接到网关就行了,因为大家在一个网段,所以产生ICMP重定向发送给Real-server;
  • Real-server收到ICMP重定向包后,如果Real-server的ICMP重定向开启了,Real-server就会处理ICMP重定向包,直接将Reply包发给网关,这时Reply包头并没有被LVS重写,所以LVS负载出现了问题。

注意:这种情况只会出现在所有的LVS的成员都在一个网段的情况下。

重定向的处理办法(Real-server的配置):

1> 关闭Real-server的重定向,忽略LVS发来的重定向包

2> 删除到网段的路由:

/farm7.static.flickr.com/6105/6324754200_9780818531_z.jpg

执行:

realserver:/etc/lvs#route del -net 192.168.1.0 netmask 255.255.255.0 dev eth0

路由已经被删除了:

/farm7.static.flickr.com/6214/6324754218_a6258829e5_z.jpg

3> LVS-NAT模式支持四层的端口重写: LVS-DR,LVS-TUN不能修改client发来的请求的目的端口,但是LVS-NAT可以,参考命令:

shell> ipvsadm -a -t VIP:PORT -r RIP:NEWPORT -m -w 1

LVS的三种转发模式就先说到这吧,具体的细节可以参考LVS的HOWTO文档。

Python colored output

/farm8.staticflickr.com/7021/6509302675_ca07164663.jpg

下面是颜色的代码

print '\033[1;30mGray like Ghost\033[1;m'
print '\033[1;31mRed like Radish\033[1;m'
print '\033[1;32mGreen like Grass\033[1;m'
print '\033[1;33mYellow like Yolk\033[1;m'
print '\033[1;34mBlue like Blood\033[1;m'
print '\033[1;35mMagenta like Mimosa\033[1;m'
print '\033[1;36mCyan like Caribbean\033[1;m'
print '\033[1;37mWhite like Whipped Cream\033[1;m'
print '\033[1;38mCrimson like Chianti\033[1;m'
print '\033[1;41mHighlighted Red like Radish\033[1;m'
print '\033[1;42mHighlighted Green like Grass\033[1;m'
print '\033[1;43mHighlighted Brown like Bear\033[1;m'
print '\033[1;44mHighlighted Blue like Blood\033[1;m'
print '\033[1;45mHighlighted Magenta like Mimosa\033[1;m'
print '\033[1;46mHighlighted Cyan like Caribbean\033[1;m'
print '\033[1;47mHighlighted Gray like Ghost\033[1;m'
print '\033[1;48mHighlighted Crimson like Chianti\033[1;m'