tornado 实现把 session 存储到 redis

作为SA,我们肯定会自己写各种运维系统实现自动化,而作为一个运维系统是肯定需要登陆的,so今天我想尝试一下用 tornado 实现一个需要登陆的网站,这一次我想把 session 存储在 redis 里,这样就不用在负载均衡层(比如lvs、haproxy、nginx) 做会话保持,更加通用。

 

这里忽略注册的部分,只说下登陆和session存储的部分,我设计的大概步骤如下:

1. 用户第一次访问,发现没登陆,要求用户登陆。

怎么发现用户没登陆呢,查看 cookie 没有 session_id 和 hmac_key  即表示 用户没登陆。

服务器端获取 session_id 和 verification 的代码:
    session_id = request_handler.get_secure_cookie(“session_id”)
    hmac_key = request_handler.get_secure_cookie(“verification”)

这里用 verification 的目的是验证 session_id 的正确性,verification 是用 session_id 生成的,具体代码如下:
def _generate_hmac(session_id):
    return hmac.new(session_id, session_secret, hashlib.sha256).hexdigest()

发现没登陆之后就跳转到 /login 页面。

 

2. 在 /login 页面,用户输入用户名和密码(有一个可选项: 30天内自动登陆)之后,服务器验证通过,然后给用户生成一个 session_id ,最后 把 session_id 和 用户名 保存在 redis 中。如果选择了 30天内自动登陆 ,那么过期时间设置成 30 * 24 * 3600 。

验证的代码就不说了,就是去读账号数据库。

验证通过后,给用户生成一个 session_id,生成代码:
def _generate_id():
    new_id = hashlib.sha256(session_secret + str(uuid.uuid4()))
    return new_id.hexdigest()

session_secret 我们自己设置。

把 session_id 和 用户名 保存在redis 里,用:
session_timeout = 30 * 24 * 3600
session_data = {“user_name”: user_name}
redis_client.setex(session_id, session_data, session_timeout)

在 session_timeout 之后,redis 会自动把这条记录删除。

保存之后,set_secure_cookie 一下,此时请求返回之后在客户端的 Cookie里面可以看到 session_id 和 hmac_key (加密了) 了。
request_handler.set_secure_cookie(“session_id”, session_id, expires_days=30)
request_handler.set_secure_cookie(“verification”, hmac_key, expires_days=30)

这里我们用set_secure_cookie,tornado会对Cookie进行加密,加密的key 叫做 cookie_secret ,在 tornado.web.Application 初始化的时候传入。

另外,set_secure_cookie 有个参数 expires_days,表示Cookie的过期时间,默认是30天,如果 expires_days = None,表示它是一个 a session cookie (浏览器关闭就过期)。

所以 set_secure_cookie 的 expires_days 最好 和 session_timeout 一致。

 

3. 下次用户再请求的时候,服务器会自动从 header 获取到 session_id 和 verification ,然后判断session_id 是否有效和过期。

判断 session_id 是否存在 和 session_id 和 verification 是否吻合:
session_id = request_handler.get_secure_cookie(“session_id”)
hmac_key = request_handler.get_secure_cookie(“verification”)

if session_id == None:
    session_exists = False
else:
    session_exists = True

if session_exists == True:
    check_hmac = self._generate_hmac(session_id)
if hmac_key != check_hmac:
    pass

另外,需要根据session_id 获取 user_name,此时从redis里面获取,如果为{},表示已经session过期了(跳到 /login) 。
def _fetch(self, session_id):
    try:
        session_data = redis_client.get(session_id)

    if type(session_data) == type({}):
        return session_data
    else:
        return {}
    except IOError:
        return {}

 

 

参考:

https://github.com/zs1621/tornado-redis-session

AWS 的几个”坑”

这篇文章简单说说最近搭建 aws 基础环境遇到的几个”坑”,有兴趣的可以参考下呃。

1. 默认公网IP 在机器重启之后会变化,如果想保持固定,可以申请一个 弹性IP 与实例绑定。

2. Classic 网络里 内网IP 在重启之后是有可能变化的,VPC 则不会。

3. /etc/resolv.conf 文件里面的 nameserver 重启之后会被重置(重置成aws内部的DNS IP),如果想固定,可以修改 /etc/rc.d/rc.local

4. 用aws的AMI创建出来的实例,assign 一个新的内网IP 给实例,实例里会自动生成这个IP 的网卡配置,这个是 ec2-net-utils 包 实现的;而 如果你用 redhat 或者 centos 创建的实例,默认不装 ec2-net-utils,所以你得手动配置新的地址了,当然你也可以给 redhat  或者 centos  装上  ec2-net-utils (这个包我有,可以找我要:| )

5. 如果想在aws ec2 里面实现 可以在实例之间移动的虚拟IP,比如 两台实例A 和 B,可以先给A assign 一个新的内网IP,然后A 和 B 互相检测,假如B 发现A 挂了,可以把 之前 assign 给A 的内网IP reassign 给 B 。

    命令类似:

     /opt/aws/bin/ec2-assign-private-ip-addresses -n $ENI_ID –secondary-private-ip-address $VIP –allow-reassignment –region $REGION –aws-access-key ${aws_access_key} –aws-secret-key ${aws_secret_key}

    如果需要公网的虚拟IP,那么再创建一个弹性IP 和 这个内网IP 绑定。

6. 实例不要创建在Classic 网络里,要自己建立VPC,实例都放在VPC里,VPC 比 Classic 有几个好处(不止):

   1). 支持实例有多个私有IP

   2). 实例停止后,弹性IP会依然保持与实例的关联

   3). 实例创建的时候可以不创建公网IP

7. 如果 VPC 的子网是私有子网(创建机器不自动分配公网IP),在这个子网创建的实例即使手动分配了弹性IP 也是无效的(从外面也连不上),所以想创建能从外部连上的实例,需要在公网子网(自动分配外网IP)。

8. 建立 LB 选取子网的时候要选择「公有子网」,LB 的实例会建在 「公网子网」内,所以如果选择了私有子网会连不上。

36751CB1-44B8-42BF-803D-5C241132B71A

9. 对于建立在 VPC 的 RDS,如果想从外网访问,除了选择「公开访问」 ,对于子网组,要选择「公网子网」,因为分配的 RDS 实例在选择的子网中,如果是私有子网,从外网连不上,和 EC2 LB 的子网选择类似。

F0D0795D-59A1-4B84-86C0-14BF892E2D75

长连接下LVS和NGINX对HTTP请求的处理是否使用负载均衡

今天突然想对长连接下LVS和NGINX是怎么做负载均衡的,会不会转发到同一个后端机器,so 测试一下。

Client —> LVS —> NGINX —> APP

测试点

在持久连接(长连接)的情况下,http的请求会保持一条路径么(同一个LVS、NGINX、APP)?

准备,分别对 NGINX 和 LVS 测试,测试路径如下:

1. NGINX —> APP

2. LVS —> APP

测试一

测试代码(http1.1默认支持keepalive,不用指定即是长连接)

[python]#!/usr/bin/python
#-*- coding: utf-8 -*-

import urllib
import httplib
import time

def keepalive(t):
url = "60.28.208.8"

conn = httplib.HTTPConnection(url)

conn.request("GET", "/test")
ret = conn.getresponse()
print ret.status, ret.reason
data = ret.read()

time.sleep(10)

conn.request("GET", "/test")
ret = conn.getresponse()
print ret.status, ret.reason
data = ret.read()

conn.close()
[/python]

NGINX 配置文件

upstream test-nodes {
    server pxe0.hy01:8888;
    server pxe2.hy01:8888;
}

server {
    listen 80;
    server_name 60.28.208.8;
    access_log /home/work/nginx/logs/test_wandoujia.access.log main;
    error_log /home/work/nginx/logs/test_wandoujia.error.log;

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://test-nodes;
    }
}

其中,NGINX的 keepalive_timeout 设置为30,保证 在我们sleep 的时间内 连接不断。

跑脚本发现,第一次http请求落在 test0 上,第二次落在 test1 上,说明在 http持久连接下 NGINX 依然会 选择不同的后端进行 负载均衡。

测试二

1. persistence_timeout 设置为0,syn_proxy 关闭的情况下,两次请求 落到同一个后端。

2. persistence_timeout 设置为0,syn_proxy 打开的情况下,两次请求 落到同一个后端。

3. persistence_timeout 设置不为0,syn_proxy 关闭的情况下,两次请求 落到同一个后端。

4. persistence_timeout 设置不为0,syn_proxy 打开的情况下,两次请求 落到同一个后端。

看起来和 syn_proxy 没关系,可以忽略syn_proxy 了。

可以看出,在http持久连接下,LVS 会把 请求转给同一个后端。

另外,我也验证了,如果是多个单独的HTTP请求(请求完就关闭连接),LVS 会负载均衡到 两个后端。

总结

持久连接下,LVS会把请求转发到同一个后端,而NGINX 会转发到不同的后端。

那么即使在长连接下,NGINX也会对请求做负载均衡,这种情况会造成SESSION失效,所以对和账号相关的服务,需要做会话保持,还好 像LVS、NGINX 和HAPROXY 这些负载均衡器都有保持会话的方法。

LVS fullnat模式下 syn_proxy机制对 建立连接过程 和 TCP option 的影响

我们的前端负载均衡已经切换到了LVS FULLNAT模式,它比LVS DR 模式 的好处是 LVS和后端机器不用在一个二层网络里面,非常好扩展,不好的地方是后端机器要装一台TOA模块来识别 HTTP请求的源IP。

想了解LVS FULLNAT的话可以参考这个链接:

http://kb.linuxvirtualserver.org/wiki/IPVS_FULLNAT_and_SYNPROXY

用LVS fullnat的时候其实需要注意一些TCP option的设置,比如 timestamps 和 windows scaling,而且 TCP option 和 synproxy 还有关系,在这篇文章我们手动测试,得出 怎么在LVS fullnat机器上 设置TCP option 。

 

环境:

LVS VIP :  60.28.208.10
LVS fullnat localip 段:10.0.19.0/24
后端IP :  10.0.11.12

 

探测的抓包

LVS 检测后端时LVS的抓包记录:

18:45:36.023248 IP 10.0.19.226.63923 > 10.0.11.12.http: Flags [S], seq 1537556744, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0 18:45:36.023270 IP 10.0.19.226.19042 > 10.0.11.12.https: Flags [S], seq 520304659, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0 18:45:36.023544 IP 10.0.11.12.http > 10.0.19.226.63923: Flags [S.], seq 3628316982, ack 1537556745, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0 18:45:36.023560 IP 10.0.19.226.63923 > 10.0.11.12.http: Flags [.], ack 1, win 115, length 0 18:45:36.023565 IP 10.0.11.12.https > 10.0.19.226.19042: Flags [S.], seq 458068257, ack 520304660, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0 18:45:36.023572 IP 10.0.19.226.19042 > 10.0.11.12.https: Flags [.], ack 1, win 115, length 0 18:45:36.023589 IP 10.0.19.226.63923 > 10.0.11.12.http: Flags [R.], seq 1, ack 1, win 115, length 0 18:45:36.023602 IP 10.0.19.226.19042 > 10.0.11.12.https: Flags [R.], seq 1, ack 1, win 115, length 0

LVS 检测后端时后端的抓包记录:

18:46:26.041901 IP 10.0.19.226.61028 > 10.0.11.12.http: Flags [S], seq 3032456781, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0

18:46:26.041937 IP 10.0.11.12.http > 10.0.19.226.61028: Flags [S.], seq 1104080475, ack 3032456782, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
18:46:26.041951 IP 10.0.19.226.40349 > 10.0.11.12.https: Flags [S], seq 849356025, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
18:46:26.041960 IP 10.0.11.12.https > 10.0.19.226.40349: Flags [S.], seq 1050279505, ack 849356026, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
18:46:26.042113 IP 10.0.19.226.61028 > 10.0.11.12.http: Flags [.], ack 1, win 115, length 0
18:46:26.042147 IP 10.0.19.226.40349 > 10.0.11.12.https: Flags [.], ack 1, win 115, length 0
18:46:26.042310 IP 10.0.19.226.61028 > 10.0.11.12.http: Flags [R.], seq 1, ack 1, win 115, length 0
18:46:26.042373 IP 10.0.19.226.40349 > 10.0.11.12.https: Flags [R.], seq 1, ack 1, win 115, length 0

上面可以看到,LVS 从localip 段里面选一个IP,起两个端口分别探测 80 和 443,建立连接之后 里面发送 RST 给后端,这样探测也成功了。

 

请求的抓包

在一台机器上执行下面命令:
curl -H “Host:10.0.11.12” http://60.28.208.10/test

LVS 上面抓VIP 的包如下:
tcpdump -n -i em1 host 60.28.208.10 and port 80
17:08:00.122008 IP 111.206.15.146.15405 > 60.28.208.10.http: Flags [S], seq 1679917190, win 14600, options [mss 1460,sackOK,TS val 1412716450 ecr 0,nop,wscale 7], length 0
17:08:00.122018 IP 60.28.208.10.http > 111.206.15.146.15405: Flags [S.], seq 2689133917, ack 1679917191, win 14600, options [mss 1452,sackOK,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,wscale 1], length 0
17:08:00.132352 IP 111.206.15.146.15405 > 60.28.208.10.http: Flags [.], ack 1, win 115, length 0
17:08:00.132439 IP 111.206.15.146.15405 > 60.28.208.10.http: Flags [P.], seq 1:169, ack 1, win 115, length 168
17:08:00.132877 IP 60.28.208.10.http > 111.206.15.146.15405: Flags [.], ack 169, win 123, length 0
17:08:01.138207 IP 60.28.208.10.http > 111.206.15.146.15405: Flags [P.], seq 1:215, ack 169, win 123, length 214
17:08:01.148585 IP 111.206.15.146.15405 > 60.28.208.10.http: Flags [.], ack 215, win 123, length 0
17:08:01.148741 IP 111.206.15.146.15405 > 60.28.208.10.http: Flags [F.], seq 169, ack 215, win 123, length 0
17:08:01.149060 IP 60.28.208.10.http > 111.206.15.146.15405: Flags [F.], seq 215, ack 170, win 123, length 0
17:08:01.159344 IP 111.206.15.146.15405 > 60.28.208.10.http: Flags [.], ack 216, win 123, length 0

LVS 上面抓连接后端机器的包:
tcpdump -n -i em2 net 10.0.19.0/24 and host 10.0.11.12
17:08:00.132468 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [S], seq 3159822860, win 5000, options [Unknown Option 2003c2d6fce0f92,mss 1440,nop,nop,sackOK,nop,wscale 7], length 0
17:08:00.132656 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [S.], seq 2205177579, ack 3159822861, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
17:08:00.132686 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [P.], seq 1:169, ack 1, win 115, options [Unknown Option 2003c2d6fce0f92], length 168
17:08:00.132855 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [.], ack 169, win 123, length 0
17:08:01.138194 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [P.], seq 1:215, ack 169, win 123, length 214
17:08:01.148598 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [.], ack 215, win 123, length 0
17:08:01.148749 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [F.], seq 169, ack 215, win 123, length 0
17:08:01.149046 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [F.], seq 215, ack 170, win 123, length 0
17:08:01.159357 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [.], ack 216, win 123, length 0

上面的包是在开启syn_proxy的时候抓的,可以看出(注意看时间点),Client 请求来的时候,LVS 会先和Client 建好连接,然后在 Client 请求数据的时候 LVS 选择一个localip端口 和 后端 建立连接,建立成功后LVS 直接 向后端请求数据,获取数据后 再通过 VIP 传给 Client。

 

下面我们把 syn_proxy 关掉之后重新抓一下看看。

tcpdump -n -i em1 host 60.28.208.10 and port 80
17:10:54.142439 IP 111.206.15.146.15408 > 60.28.208.10.http: Flags [S], seq 50780707, win 14600, options [mss 1460,sackOK,TS val 1412890473 ecr 0,nop,wscale 7], length 0
17:10:54.142802 IP 60.28.208.10.http > 111.206.15.146.15408: Flags [S.], seq 629114004, ack 50780708, win 14600, options [mss 1452,nop,nop,sackOK,nop,wscale 7], length 0
17:10:54.153112 IP 111.206.15.146.15408 > 60.28.208.10.http: Flags [.], ack 1, win 115, length 0
17:10:54.153180 IP 111.206.15.146.15408 > 60.28.208.10.http: Flags [P.], seq 1:169, ack 1, win 115, length 168
17:10:54.153360 IP 60.28.208.10.http > 111.206.15.146.15408: Flags [.], ack 169, win 123, length 0
17:10:55.158651 IP 60.28.208.10.http > 111.206.15.146.15408: Flags [P.], seq 1:215, ack 169, win 123, length 214
17:10:55.168912 IP 111.206.15.146.15408 > 60.28.208.10.http: Flags [.], ack 215, win 123, length 0
17:10:55.168940 IP 111.206.15.146.15408 > 60.28.208.10.http: Flags [F.], seq 169, ack 215, win 123, length 0
17:10:55.169284 IP 60.28.208.10.http > 111.206.15.146.15408: Flags [F.], seq 215, ack 170, win 123, length 0
17:10:55.179620 IP 111.206.15.146.15408 > 60.28.208.10.http: Flags [.], ack 216, win 123, length 0

tcpdump -n -i em2 net 10.0.19.0/24 and host 10.0.11.12
17:10:54.142475 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [S], seq 1583761902, win 14600, options [Unknown Option 2003c306fce0f92,mss 1460,sackOK,TS val 1412890473 ecr 0,nop,wscale 7], length 0
17:10:54.142766 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [S.], seq 629114004, ack 1583761903, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
17:10:54.153128 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [.], ack 1, win 115, options [Unknown Option 2003c306fce0f92], length 0
17:10:54.153192 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [P.], seq 1:169, ack 1, win 115, options [Unknown Option 2003c306fce0f92], length 168
17:10:54.153348 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [.], ack 169, win 123, length 0
17:10:55.158639 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [P.], seq 1:215, ack 169, win 123, length 214
17:10:55.168925 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [.], ack 215, win 123, length 0
17:10:55.168944 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [F.], seq 169, ack 215, win 123, length 0
17:10:55.169272 IP 10.0.11.12.http > 10.0.19.227.commplex-link: Flags [F.], seq 215, ack 170, win 123, length 0
17:10:55.179630 IP 10.0.19.227.commplex-link > 10.0.11.12.http: Flags [.], ack 216, win 123, length 0

通过上面的抓包可以看到(同样看时间点),关闭syn_proxy 的时候,LVS 接受到请求,此时开启一个localip的一个端口 给后端发送 SYN,收到 后端的SYN+ACK之后,LVS通过VIP 给Client 返回 SYN+ACK,然后 Client 会再发送 ACK,VIP 会接收到这个ACK,然后再通过localip 传给 后端,后端收到后 建立连接。看这个流程,LVS 纯粹扮演 Proxy 的角色。

syn_proxy 机制会影响 Client 到 Server 的TCP 连接的Option 选项,下面细看。

 

关于timestamps 的测试

Client —> LVS —> Server

LVS 上面有三个和 timestamps 相关的参数 :
net.ipv4.tcp_timestamps
net.ipv4.vs.fullnat_timestamp_remove_entry
net.ipv4.vs.synproxy_timestamp

(LVS 默认 net.ipv4.vs.fullnat_timestamp_remove_entry = 1 ,net.ipv4.vs.synproxy_timestamp = 0)

在开启 syn_proxy 情况下的测试:

1. 即使 Client 和 Server 的 timestamps 都设置为1 ,只要 LVS net.ipv4.vs.synproxy_timestamp 不设置为1 ,timestamps 就不会启用。此时  LVS 的 net.ipv4.tcp_timestamps 和 net.ipv4.vs.fullnat_timestamp_remove_entry 完全没用。

2. 如果Client 关闭了 net.ipv4.tcp_timestamps ,timestamps 选项不会启用。

3. 比较诧异的是,无论 Server 的 net.ipv4.tcp_timestamps 是否设置为1,只要LVS 开启 net.ipv4.vs.synproxy_timestamp 和 Client 开启 net.ipv4.tcp_timestamps,建立的TCP连接 就会启用 timestamps (此时在Server 上抓包 会看到 TS val 和 ecr )

在关闭 syn_proxy 情况下的测试:

1. 此时 net.ipv4.vs.fullnat_timestamp_remove_entry  参数起作用,即使 Client 和 Server 的 timestamps 都设置为1 ,只要 LVS net.ipv4.vs.fullnat_timestamp_remove_entry 不设置为0 ,timestamps 就不会启用。

2. 如果Client 关闭了 net.ipv4.tcp_timestamps ,timestamps 选项不会启用。

3. 如果Server关闭了 net.ipv4.tcp_timestamps ,timestamps 选项不会启用。

总结:

1. syn_proxy 开启,net.ipv4.vs.synproxy_timestamp 决定LVS 是否启用timestamps。

2. syn_proxy 关闭,net.ipv4.vs.fullnat_timestamp_remove_entry 决定LVS 是否启用timestamps。

3. LVS FULLNAT 模式 的 net.ipv4.tcp_timestamps 参数 完全没用,设置与不设置都不会影响timestamps

4. 简单点,如果想开启LVS的 timestamps,把 net.ipv4.vs.synproxy_timestamp 设置成1,同时把  net.ipv4.vs.fullnat_timestamp_remove_entry 设置成 0

 

关于windows  scaling 的测试

Client —> LVS —> Server

在开启 syn_proxy 情况下的测试:

1. net.ipv4.vs.synproxy_wscale 决定连接 是否 wscale,即使 Client 和 Server 都开启了wscale,只要LVS 不开启 net.ipv4.vs.synproxy_wscale,连接就不会开启wscale。

2. 如果Client 不开启 net.ipv4.tcp_window_scaling ,连接就不会开启 wscale。

3. Server 的 net.ipv4.tcp_window_scaling 变的没用,即使没打开,只要 Client 打开了net.ipv4.tcp_window_scaling 而且LVS 打开了 net.ipv4.vs.synproxy_wscale ,连接就会打开 wscale

在关闭 syn_proxy 情况下的测试:

1. 只要 Client 和 Server 同时开启 wscale ,连接就会开启 wscale,不管 LVS 如何设置。

总结:

1. LVS 的 net.ipv4.tcp_window_scaling 完全没用,可忽略这个参数。

2. 想开启LVS 的wscale, 把 net.ipv4.vs.synproxy_wscale  设置成1