Codis 运维应该做到的几点

我司目前使用 Codis,突然对如何 Codis 运维有点思考,记录一下。

 

1、 扩容和缩容对开发无感

这一点 Codis 可以做到。

 

2、 迁移对开发无感

如果使用 Codis,迁移分为三个部分:

1). 迁移 Codis Server,和上一点相同,Codis 可以做到;

2). 迁移 Codis Proxy,先新建 Proxy,然后启动新建的 Proxy,启动之后会向 zk 注册,然后停止旧的 Proxy,zk 中就只有新的 Proxy 了,程序会自动获取新的 Proxy 并连接;

3). 迁移 zk。由于 Codis Proxy 和 程序都依赖 zk,迁移 zk 看起来麻烦点。

迁移 zk 机器,可以对 zk 封装一个代理,比如 LVS,通过 LVS 转发到多个 zk,这样 zk 机器的迁移可以实现无感;

用了 LVS 就会涉及到 LVS 的迁移,我们的做法是把 LVS 的 IP 封装成域名,Codis Proxy 和程序中都写这个域名,当需要迁移 LVS 时,可以在新的 LVS 上建立新 IP 转发到 zk 机器,然后修改域名的 DNS 指向新 LVS IP。注:这里可能涉及到 DNS 缓存的问题。

 

3、申请和部署尽量简单

申请和部署和用不用 Codis 都没有关系,我的思路是:

1). 对于申请,开发人员首先通过提交配置,包括 内存、是否需要 Slave 等,提交方式可以通过网页提交单子或者通过 git,然后运维审核配置,通过后调用「部署」的 API 「自动」创建。

2). 对于部署,Redis (Codis) 的集群资源需要一个管理工具来统一管理,需要有 API 来新建、删除和修改,也需要有很好的调度功能(什么样的申请在什么机器创建)。

 

redis 连接一直是 ESTABLISHED 的问题排查

昨天想删一台机器,发现上面还有 redis 连接:

# netstat -nat |grep ESTABLISHED |grep 6379
tcp 0 0 10.0.27.92:6379 10.0.27.157:24044 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.96.27:28975 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.69.47:58511 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.16.29.9:44571 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.29.49:48137 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.69.46:8854 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.70.67:42271 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.70.67:42269 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.24.30:17776 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.22.91:17823 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.23.79:59200 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.24.30:46296 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.23.98:31277 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.22.118:40458 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.16.29.9:44548 ESTABLISHED

这些连接一直都在,更奇怪的是,10.0.70.67 这个 IP 已经 ping 不通了,连接却不会断,正常情况下 tcp keepalive 会隔段时间检查一次,发现不通之后会发送 reset 。

 

为了看看到底有没有发送 tcp keepalive ack 包,抓个包看看,命令是 tcpdump -i em2 port 6379,下面是抓了一夜的包:redis.cpap

用 wireshark 分析,发现基本都是 keepalive ack 的包,取 10.0.96.27 这个IP,截个图看看:

4888351E-2D1D-4E6F-8062-F2DA259CF9C7

可以看到,10.0.96.27 主动 发送 ACK(SEQ: M、ACK: N)给 redis,redis 回复 ACK(SEQ: N、ACK:M+1),且 Len 都是0。

这能够解释大部分 IP 一直在 ESTABLISHED,因为一直有 tcp keepalive,但是 10.0.70.67 解释不通了,而且上面根本没抓到 10.0.70.67 的包,这只有一种可能: redis 不主动发送 keepalive。

 

找了下文档,发现 redis 确实默认关闭 tcp keepalive,所以对于已经建立的连接,不会发送 tcp keepalive ack 来确认对方存活,而如果对方突然死机或者关电源导致对方不主动关闭连接,那么 redis 就一直认为对方是活的,就不会去关闭连接了。

redis 提供了配置文件来更改这一默认行为:

# TCP keepalive.
#
# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence
# of communication. This is useful for two reasons:
#
# 1) Detect dead peers.
# 2) Take the connection alive from the point of view of network
# equipment in the middle.
#
# On Linux, the specified value (in seconds) is the period used to send ACKs.
# Note that to close the connection the double of the time is needed.
# On other kernels the period depends on the kernel configuration.
#
# A reasonable value for this option is 60 seconds.
tcp-keepalive 0

 

事实上,如果 redis 对 idle 有时间限制,我遇到的情况也不会存在,但是 redis 也确实默认对 idle 的连接不加时间限制, 只是提供了 timeout 参数来更改。

 

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