Ⅰ 面试问题redis有哪些集群方案
P2P模式,无中心化
把key分成16384个slot
每个实例负责一部分slot
客户端请求若不在连接的实例,该实例会转发给对应的实例。
通过Gossip协议同步节点信息
优点:
- 组件all-in-box,部署简单,节约机器资源
- 性能比proxy模式好
- 自动故障转移、Slot迁移中数据可用
- 官方原生集群方案,更新与支持有保障
缺点:
- 架构比较新,最佳实践较少
- 多键操作支持有限(驱动可以曲线救国)
- 为了性能提升,客户端需要缓存路由表信息
- 节点发现、reshard操作不够自动化
Ⅱ redis架构模式(4) Codis
codis是在redis官方集群方案redis-cluster发布之前便已被业界广泛使用的redis集群解决方案。
codis server :这是进行了二次开发的 Redis 实例,其中增加了额外的数据结构,支持
数据迁移操作,主要负责处理具体的数据读写请求。
codis proxy :接收客户端请求,并把请求转发给 codis server。
Zookeeper 集群 :保存集群元数据,例如数据位置信息和 codis proxy 信息。
codis dashboard 和 codis fe :共同组成了集群管理工具。其中,codis dashboard 负
责执行集群管理工作,包括增删 codis server、codis proxy 和进行数据迁移。
而 codis fe 负责提供dashboard的Web 操作界面,便于我们直接在 Web 界面上进行集群管理
codis proxy本身支持 Redis 的 RESP 交互协议,所以只需和proxy建立连接,就相当于和
整个codis集群建立连接,codis proxy 接收到请求,就会查询请求数据和 codis server 的
映射关系,并把请求转发给相应的 codis server 进行处理。当 codis server 处理完请求后,
会把结果返回给codis proxy,proxy 再把数据返回给客户端。
codis集群共有1024个slot,编号从0-1023,可以自定义或者自动均匀分配到各个server上,
使用 CRC32 算法计算数据 key 的哈希值然后对1024取模,得到slot编号,然后通过slot
和server的映射关系请求到具体实例。
Slot 和 codis server 的映射关系称为数据路由表,我们在 codis dashboard 上分配好路由表后,
dashboard 会把路由表发送给 codis proxy,同时,dashboard 也会把路由表保存在
Zookeeper 中。codis-proxy 会把路由表缓存在本地,当它接收到客户端请求后,直接查询
本地的路由表进而完成请求的转发。
跟redis cluster的区别是,codis的把映射关系存在zookeeper和proxy中,redis cluster
是通过各个实例相互通信进而存在各个实例本地,当实例较多时,可能会受到网络状况
影响而且整个集群的网络资源消耗相较之下也比较大。
codis按照slot粒度进行数据迁移,比如从serverA迁移至serverB,会从A要迁移的slot中
随机选择一个数据,发送给B,A得到确认消息后,删除本地数据。重复这个过程直到
迁移完毕。
迁移方式有两种,一种是同步迁移,一种是异步迁移。
同步迁移时,源server是阻塞的,由于迁移过程中需要数据的序列化、网络传输、删除
等等操作,如果是bigkey则会阻塞较长时间。
所以通常采用异步迁移。异步迁移模式下,源server发送完迁移数据之后,就可以继续接受
处理请求,等到目标server返回ack之后,再执行删除操作,这个过程中,为了防止数据
不一致的情况,被迁移的数据是只读模式。
对于bigkey的数据,异步迁移采用拆分指令的方式,对bigkey中的每个元素用一条指令
进行迁移,而不是等待整个数据序列化完毕再迁移,进而避免了需要序列化大量数据而
阻塞的风险,因为整个bigkey的数据是被拆分的,如果迁移期间server宕机,便会破坏
迁移的原子性,所以codis对bigkey的数据设置了一个过期时间,如果原子性遭到破坏,
目标server的数据就会过期后删除以保证迁移的原子性。可以通过异步迁移命令
SLOTSMGRTTAGSLOT-ASYNC 的参数numkeys 设置每次迁移的 key 数量,以提升
bigkey异步迁移的速度。
最后分享一下我的一些架构设计心得,通过redis cluster和codis的slot设计,把原本
数百上千万的key和实例的映射抽象成slot和实例的映射,大大减少了映射量,进而把
slot和key的映射分担到各个实例中,这种粒度的抽象,值得借鉴,另外codis与redis cluser
不同的是,codis通过zk作为第三方储存系统,保存了集群实例、状态和slot分配等信息,
这样避免了实例间的大量心跳来同步集群状态进而减少实例间的网络通信量,对于实现
大规模的集群是有益的设计,网络带宽可以集中在客户端请求上面,需要注意的是需要保证
zk的通信带宽,毕竟集群的信息同步都交给了zk
Ⅲ 何时部署要启动lua nginx
Lua是一个可以嵌入到Nginx配置文件中的动态脚本语言,从而可以在Nginx请求处理的任何阶段执行各种Lua代码。刚开始我们只是用Lua 把请求路由到后端服务器,但是它对我们架构的作用超出了我们的预期。下面就讲讲我们所做的工作。
强制搜索引擎只索引mixlr.com
Google把子域名当作完全独立的网站,我们不希望爬虫抓取子域名的页面,降低我们的Page rank
location /robots.txt {
rewrite_by_lua '
if ngx.var.http_host ~= "mixlr.com" then
return ngx.exec("/robots_disallow.txt");
end
';
}
如果对robots.txt的请求不是mixlr.com域名的话,则内部重写到robots_diallow.txt,虽然标准的重写指令也可以实现这个需求,但是 Lua的实现更容易理解和维护。
根据程序逻辑设置响应头
Lua提供了比Nginx默认配置规则更加灵活的设置方式。 在下面的例子中,我们要保证正确设置响应头,这样浏览器如果发送了指定请求头后,就可以 无限期缓存静态文件,是的用户只需下载一次即可。
这个重写规则使得任何静态文件,如果请求参数中包含时间戳值,那么就设置相应的Expires和Cache-Control响应头。
location / {
header_filter_by_lua '
if ngx.var.query_string and ngx.re.match( ngx.var.query_string, "^([0-9]{10})$" ) then
ngx.header["Expires"] = ngx.http_time( ngx.time() + 31536000 );
ngx.header["Cache-Control"] = "max-age=31536000";
end
';
try_files $uri @dynamic;}
删除jQuery JSONP请求的时间戳参数
很多外部客户端请求JSONP接口时,都会包含一个时间戳类似的参数,从而导致Nginx proxy缓存无法命中(因为无法忽略指定的HTTP参数)。下面的 规则删除了时间戳参数,使得Nginx可以缓存upstream server的响应内容,减轻后端服务器的负载。
location / {
rewrite_by_lua '
if ngx.var.args ~= nil then
-- /some_request?_=1346491660 becomes /some_request
local fixed_args, count = ngx.re.sub( ngx.var.args, "&?_=[0-9]+", "" );
if count > 0 then
return ngx.exec(ngx.var.uri, fixed_args);
end
end
';}
把后端的慢请求日志记录到Nginx的错误日志
如果后端请求响应很慢,可以把它记录到Nginx的错误日志,以备后续追查。
location / {
log_by_lua '
if tonumber(ngx.var.upstream_response_time) >= 1 then
ngx.log(ngx.WARN, "[SLOW] Ngx upstream response time: " .. ngx.var.upstream_response_time .. "s from " .. ngx.var.upstream_addr);
end
';}
基于Redis的实时IP封禁
某些情况下,需要阻止流氓爬虫的抓取,这可以通过专门的封禁设备去做,但是通过Lua,也可以实现简单版本的封禁。
lua_shared_dict banned_ips 1m;
location / {
access_by_lua '
local banned_ips = ngx.shared.banned_ips;
local updated_at = banned_ips:get("updated_at");
-- only update banned_ips from Redis once every ten seconds:
if updated_at == nil or updated_at < ( ngx.now() - 10 ) then
local redis = require "resty.redis";
local red = redis:new();
red:set_timeout(200);
local ok, err = red:connect("your-redis-hostname", 6379);
if not ok then
ngx.log(ngx.WARN, "Redis connection error retrieving banned_ips: " .. err);
else
local updated_banned_ips, err = red:smembers("banned_ips");
if err then
ngx.log(ngx.WARN, "Redis read error retrieving banned_ips: " .. err);
else
-- replace the locally stored banned_ips with the updated values:
banned_ips:flush_all();
for index, banned_ip in ipairs(updated_banned_ips) do
banned_ips:set(banned_ip, true);
end
banned_ips:set("updated_at", ngx.now());
end
end
end
if banned_ips:get(ngx.var.remote_addr) then
ngx.log(ngx.WARN, "Banned IP detected and refused access: " .. ngx.var.remote_addr);
return ngx.exit(ngx.HTTP_FORBIDDEN);
end
';}
现在就可以阻止特定IP的访问:
1
ruby> $redis.sadd("banned_ips", "200.1.35.4")
Nginx进程每隔10秒从Redis获取一次最新的禁止IP名单。需要注意的是,如果架构中使用了Haproxy这样类似的负载均衡服务器时, 需要把$remote_addr设置为正确的远端IP地址。
这个方法还可以用于HTTP User-Agent字段的检查,要求满足指定条件。
使用Nginx输出CSRF(form_authenticity_token)
Mixlr大量使用页面缓存,由此引入的一个问题是如何给每个页面输出会话级别的CSRF token。我们通过Nginx的子请求,从upstream web server 获取token,然后利用Nginx的SSI(server-side include)功能输出到页面中。这样既解决了CSRF攻击问题,也保证了cache能被正常利用。
location /csrf_token_endpoint {
internal;
include /opt/nginx/conf/proxy.conf;
proxy_pass "http://upstream";}
location @dynamic {
ssi on;
set $csrf_token '';
rewrite_by_lua '
-- Using a subrequest, we our upstream servers for the CSRF token for this session:
local csrf_capture = ngx.location.capture("/csrf_token_endpoint");
if csrf_capture.status == 200 then
ngx.var.csrf_token = csrf_capture.body;
-- if this is a new session, ensure it sticks by passing through the new session_id
-- to both the subsequent upstream request, and the response:
if not ngx.var.cookie_session then
local match = ngx.re.match(csrf_capture.header["Set-Cookie"], "session=([a-zA-Z0-9_+=/+]+);");
if match then
ngx.req.set_header("Cookie", "session=" .. match[1]);
ngx.header["Set-Cookie"] = csrf_capture.header["Set-Cookie"];
end
end
else
ngx.log(ngx.WARN, "No CSRF token returned from upstream, ignoring.");
end
';
try_files /maintenance.html /rails_cache$uri @thin;}
CSRF token生成 app/metal/csrf_token_endpoint.rb:
class CsrfTokenEndpoint
def self.call(env)
if env["PATH_INFO"] =~ /^\/csrf_token_endpoint/
session = env["rack.session"] || {}
token = session[:_csrf_token]
if token.nil?
token = SecureRandom.base64(32)
session[:_csrf_token] = token
end
[ 200, { "Content-Type" => "text/plain" }, [ token ] ]
else
[404, {"Content-Type" => "text/html"}, ["Not Found"]]
end
endend
我们的模版文件示例:
<meta name="csrf-param" value="authenticity_token"/>
<meta name="csrf-token" value="<!--# echo var="csrf_token" default="" encoding="none" -->"/>
Again you could make use of lua_shared_dict to store in memory the CSRF token for a particular session. This minimises the number of trips made to /csrf_token_endpoint.
Ⅳ 京东活动系统--亿级流量架构应对之术
京东活动系统 是一个可在线编辑、实时编辑更新和发布新活动,并对外提供页面访问服务的系统。其高时效性、灵活性等特征,极受青睐,已发展成京东几个重要流量入口之一。近几次大促,系统所承载的pv已经达到数亿级。随着京东业务的高速发展,京东活动系统的压力会越来越大。急需要一个更高效,稳定的系统架构,来支持业务的高速发展。本文主要对活动页面浏览方面的性能,进行探讨。
活动页面浏览性能提升的难点:
1. 活动与活动之间差异很大,不像商品页有固定的模式。每个页面能抽取的公共部分有限,可复用性差。
2. 活动页面内容多样,业务繁多。依赖大量外部业务接口,数据很难做到闭环。外部接口的性能,以及稳定性,严重制约了活动页的渲染速度、稳定性。
经过多年在该系统下的开发实践,提出“页面渲染、浏览异步化”的思想,并以此为指导,对该系统进行架构升级改造。通过近几个月的运行,各方面性能都有显着提升。在分享"新架构"之前,先看看我们现有web系统的架构现状。
以京东活动系统架构的演变为例,这里没有画出具体的业务逻辑,只是简单的描述下架构:
2.第二步,一般是在消耗性能的地方加缓存,这里对部分查库操作加redis缓存
3.对页面进行整页redis缓存:由于活动页面内容繁多,渲染一次页面的成本是很高。这里可以考虑把渲染好的活动内容整页缓存起来,下次请求到来时,如果缓存中有值,直接获取缓存返回。
以上是系统应用服务层面架构演进的,简单示意。为了减少应用服务器的压力,可以在应用服务器前面,加cdn和nginx的proxy_caxhe,降低回源率。
4.整体架构(老)
除了前3步讲的“浏览服务”,老架构还做了其他两个大的优化:“接口服务”、静态服务
1.访问请求,首先到达浏览服务,把整个页面框架返回给浏览器(有cdn、nginx、redis等各级缓存)。
2.对于实时数据(如秒杀)、个性化数据(如登陆、个人坐标),采用前端实时接口调用,前端接口服务。
3.静态服务:静态资源分离,所有静态js、css访问静态服务。
要点:浏览服务、接口服务分离。页面固定不变部分走浏览服务,实时变化、个性化采用前端接口服务实现。
接口服务:分两类,直接读redis缓存、调用外部接口。这里可以对直接读redis的接口采用nginx+lua进行优化( openresty ),不做详细讲解。 本次分享主要对“浏览服务”架构
在讲新架构之前先看看新老架构下的新能对比
击穿cdn缓存、nginx缓存,回源到应用服务器的流量大约为20%-40%之间,这里的性能对比,只针对回源到应用服务器的部分。
2015双十一, 浏览方法tp99如下:(物理机)
Tp99 1000ms左右,且抖动幅度很大,内存使用近70%,cpu 45%左右。
1000ms内没有缓存,有阻塞甚至挂掉的风险。
2.新架构浏览服务新能
本次2016 618采用新架构支持,浏览tp99如下(分app端活动和pc端活动):
移动活动浏览tp99稳定在8ms, pc活动浏览tp99 稳定在15ms左右。全天几乎一条直线,没有性能抖动。
新架构支持,服务器(docker)cpu性能如下
cpu消耗一直平稳在1%,几乎没有抖动。
对比结果:新架构tp99从1000ms降低到 15ms,cpu消耗从45%降低到1%,新架构性能得到质的提升。
why!!!
下面我们就来揭开新架构的面纱。
1. 页面浏览,页面渲染 异步化
再来看之前的浏览服务架构,20%-40%的页面请求会重新渲染页面,渲染需要重新计算、查询、创建对象等导致 cpu、内存消耗增加,tp99性能下降。
如果能保证每次请求都能获取到redis整页缓存,这些性能问题就都不存在了。
即:页面浏览,与页面渲染 异步。
理想情况下,如果页面数据变动可以通过 手动触发渲染(页面发布新内容)、外部数据变化通过监听mq 自动触发渲染。
但是有些外部接口不支持mq、或者无法使用mq,比如活动页面置入的某个商品,这个商品名称变化。
为了解决这个问题,view工程每隔指定时间,向engine发起重新渲染请求-最新内容放入redis。下一次请求到来时即可获取到新内容。由于活动很多,也不能确定哪些活动在被访问,所以不建议使用timer。通过加一个缓存key来实现,处理逻辑如下:
好处就是,只对有访问的活动定时重新发起渲染。
整理架构(不包含业务):
view工程职责 :
a.直接从缓存或者硬盘中获取静态html返回,如果没有返回错误页面。(文件系统的存取性能比较低,超过 100ms级别,这里没有使用)
b.根据缓存key2是否过期,判断是否向engine重新发起渲染。(如果,你的项目外面接口都支持mq,这个 功能就不需要了)
engine工程职责 :渲染活动页面,把结果放到 硬盘、redis。
publish工程、mq 职责 :页面发生变化,向engine重新发起渲染。 具体的页面逻辑,这里不做讲解
Engine工程的工作 就是当页面内容发生变化时,重新渲染页面,并将整页内容放到redis,或者推送到硬盘。
View工程的工作,就是根据链接从redis中获取页面内容返回。
3.view 工程架构 ( 硬盘 版)
两个版本对比
a.Redis版
优点:接入简单、 性能好,尤其是在大量页面情况下,没有性能抖动 。单个docker tps达到 700。
缺点:严重依赖京东redis服务,如果redis服务出现问题,所有页面都无法访问。
b.硬盘版
优点:不依赖任何其他外部服务,只要应用服务不挂、网络正常 就可以对外稳定服务。
在页面数量不大的情况下,性能优越。单个docker tps达到 2000。
缺点:在页面数据量大的情况下(系统的所有活动页有xx个G左右),磁盘io消耗增加(这里采用的java io,如果采用nginx+lua,io消耗应该会控制在10%以内)。
解决方案:
a. 对所有页面访问和存储 采用url hash方式,所有页面均匀分配到各个应用服务器上。
b. 采用nginx+lua 利用nginx的异步io,代替java io。
现在通过nginx+lua做应用服务,所具有的高并发处理能力、高性能、高稳定性已经越来越受青睐。通过上述讲解,view工程没有任何业务逻辑。可以很轻易的就可以用lua实现,从redis或者硬盘获取页面,实现更高效的web服务。如果想学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶qun:694549689,里面有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。
1.具有1-5工作经验的,面对目前流行的技术不知从何下手,需要突破技术瓶颈的可以加。
2.在公司待久了,过得很安逸,但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的可以加。
3.如果没有工作经验,但基础非常扎实,对java工作机制,常用设计思想,常用java开发框架掌握熟练的可以加。
通过测试对比,view工程读本地硬盘的速度,比读redis还要快(同一个页面,读redis是15ms,硬盘是8ms)。所以终极版架构我选择用硬盘,redis做备份,硬盘读不到时在读redis。
这里前置机的url hash是自己实现的逻辑,engine工程采用同样的规则推送到view服务器硬盘即可,具体逻辑这里不细讲。后面有时间再单独做一次分享。
优点:具备硬盘版的全部优点,同时去掉tomcat,直接利用nginx高并发能力,以及io处理能力。各项性能、以及稳定性达到最优。
缺点:1、硬盘坏掉,影响访问。2.方法监控,以及日志打印,需使用lua脚本重写。
无论是redis版、硬盘版、openresty+硬盘版,基础都是页面浏览与页面渲染异步化。
优势:
1、所有业务逻辑都剥离到engine工程,新view工程理论上永远无需上线。
2、灾备多样化(redis、硬盘、文件系统),且更加简单,外部接口或者服务出现问题后,切断engine工程渲染,不再更新redis和硬盘即可。
3、新view工程,与业务逻辑完全隔离,不依赖外部接口和服务,大促期间,即便外部接口出现新能问题,或者有外部服务挂掉,丝毫不影响view工程正常访问。
4、性能提升上百倍,从1000ms提升到10ms左右。详见前面的性能截图。
5、稳定性:只要view服务器的网络还正常,可以做到理论上用不挂机。
6、大幅度节省服务器资源,按此架构,4+20+30=54个docker足以支持10亿级pv。(4个nginx proxy_cache、20个view,30个engine)
从事开发已有近10载,一直就像寄生虫一样吸取着网络上的资源。前段时间受“张开涛”大神所托,对活动系统新架构做了一次简单整理分享给大家,希望能给大家带来一丝帮助。第一次在网上做分享,难免有些没有考虑周全的地方,以后会慢慢的多分享一些自己的心得,大家一起成长。最后再来点心灵鸡汤。。。
Ⅳ 架构高可用高并发系统的设计原则
通过学习《亿级流量网站架构核心技术》及《linux就该这么学》学习笔记及自己的感悟:架构设计之高可用高并发系统设计原则,架构设计包括墨菲定律、康威定律和二八定律三大定律,而系统设计包括高并发原则、高可用和业务设计原则等。
架构设计三大定律
墨菲定律 – 任何事没有表面看起来那么简单 – 所有的事都会比预计的时间长 – 可能出错的事情总会出错 – 担心某种事情发生,那么它就更有可能发生
康威定律 – 系统架构师公司组织架构的反映 – 按照业务闭环进行系统拆分/组织架构划分,实现闭环、高内聚、低耦合,减少沟通成本 – 如果沟通出现问题,应该考虑进行系统和组织架构的调整 – 适合时机进行系统拆分,不要一开始就吧系统、服务拆分拆的非常细,虽然闭环,但是每个人维护的系统多,维护成本高 – 微服务架构的理论基础 – 康威定律https://yq.aliyun.com/articles/8611– 每个架构师都应该研究下康威定律http://36kr.com/p/5042735.html
二八定律 – 80%的结果取决于20%的原因
系统设计遵循的原则
1.高并发原则
无状态
无状态应用,便于水平扩展
有状态配置可通过配置中心实现无状态
实践: Disconf、Yaconf、Zookpeer、Consul、Confd、Diamond、Xdiamond等
拆分
系统维度:按照系统功能、业务拆分,如购物车,结算,订单等
功能维度:对系统功能在做细粒度拆分
读写维度:根据读写比例特征拆分;读多,可考虑多级缓存;写多,可考虑分库分表
AOP维度: 根据访问特征,按照AOP进行拆分,比如商品详情页可分为CDN、页面渲染系统,CDN就是一个AOP系统
模块维度:对整体代码结构划分Web、Service、DAO
服务化
服务化演进: 进程内服务-单机远程服务-集群手动注册服务-自动注册和发现服务-服务的分组、隔离、路由-服务治理
考虑服务分组、隔离、限流、黑白名单、超时、重试机制、路由、故障补偿等
实践:利用Nginx、HaProxy、LVS等实现负载均衡,ZooKeeper、Consul等实现自动注册和发现服
消息队列
目的: 服务解耦(一对多消费)、异步处理、流量削峰缓冲等
大流量缓冲: 牺牲强一致性,保证最终一致性(案例:库存扣减,现在Redis中做扣减,记录扣减日志,通过后台进程将扣减日志应用到DB)
数据校对: 解决异步消息机制下消息丢失问题
数据异构
数据异构: 通过消息队列机制接收数据变更,原子化存储
数据闭环: 屏蔽多从数据来源,将数据异构存储,形成闭环
缓存银弹
用户层:
DNS缓存
浏览器DNS缓存
操作系统DNS缓存
本地DNS服务商缓存
DNS服务器缓存
客户端缓存
浏览器缓存(Expires、Cache-Control、Last-Modified、Etag)
App客户缓存(js/css/image…)
代理层:
CDN缓存(一般基于ATS、Varnish、Nginx、Squid等构建,边缘节点-二级节点-中心节点-源站)
接入层:
Opcache: 缓存PHP的Opcodes
Proxy_cache: 代理缓存,可以存储到/dev/shm或者SSD
FastCGI Cache
Nginx+Lua+Redis: 业务数据缓存
Nginx为例:
PHP为例:
应用层:
页面静态化
业务数据缓存(Redis/Memcached/本地文件等)
消息队列
数据层:
NoSQL: Redis、Memcache、SSDB等
MySQL: Innodb/MyISAM等Query Cache、Key Cache、Innodb Buffer Size等
系统层:
CPU : L1/L2/L3 Cache/NUMA
内存
磁盘:磁盘本身缓存、dirtyratio/dirtybackground_ratio、阵列卡本身缓存
并发化
2.高可用原则
降级
降级开关集中化管理:将开关配置信息推送到各个应用
可降级的多级读服务:如服务调用降级为只读本地缓存
开关前置化:如Nginx+lua(OpenResty)配置降级策略,引流流量;可基于此做灰度策略
业务降级:高并发下,保证核心功能,次要功能可由同步改为异步策略或屏蔽功能
限流
目的: 防止恶意请求攻击或超出系统峰值
实践:
恶意请求流量只访问到Cache
穿透后端应用的流量使用Nginx的limit处理
恶意IP使用Nginx Deny策略或者iptables拒绝
切流量
目的:屏蔽故障机器
实践:
DNS: 更改域名解析入口,如DNSPOD可以添加备用IP,正常IP故障时,会自主切换到备用地址;生效实践较慢
HttpDNS: 为了绕过运营商LocalDNS实现的精准流量调度
LVS/HaProxy/Nginx: 摘除故障节点
可回滚
发布版本失败时可随时快速回退到上一个稳定版本
3.业务设计原则
防重设计
幂等设计
流程定义
状态与状态机
后台系统操作可反馈
后台系统审批化
文档注释
备份
4.总结
先行规划和设计时有必要的,要对现有问题有方案,对未来有预案;欠下的技术债,迟早都是要还的。
本文作者为网易高级运维工程师
Ⅵ 细说分布式redis
IT培训>数据库教程
细说分布式Redis架构设计和踩过的那些坑
作者:课课家教育2015-12-14 10:15:25
摘要:本文章主要分成五个步骤内容讲解
Redis、RedisCluster和Codis;
我们更爱一致性;
Codis在生产环境中的使用的经验和坑们;
对于分布式数据库和分布式架构的一些看法;
Q & A环节。
Codis是一个分布式Redis解决方案,与官方的纯P2P的模式不同,Codis采用的是Proxy-based的方案。今天我们介绍一下Codis及下一个大版本RebornDB的设计,同时会介绍一些Codis在实际应用场景中的tips。最后抛砖引玉,会介绍一下我对分布式存储的一些观点和看法,望各位首席们雅正。
细说分布式Redis架构设计和踩过的那些坑_redis 分布式_ redis 分布式锁_分布式缓存redis
一、 Redis,RedisCluster和Codis
Redis:想必大家的架构中,Redis已经是一个必不可少的部件,丰富的数据结构和超高的性能以及简单的协议,让Redis能够很好的作为数据库的上游缓存层。但是我们会比较担心Redis的单点问题,单点Redis容量大小总受限于内存,在业务对性能要求比较高的情况下,理想情况下我们希望所有的数据都能在内存里面,不要打到数据库上,所以很自然的就会寻求其他方案。 比如,SSD将内存换成了磁盘,以换取更大的容量。更自然的想法是将Redis变成一个可以水平扩展的分布式缓存服务,在Codis之前,业界只有Twemproxy,但是Twemproxy本身是一个静态的分布式Redis方案,进行扩容/缩容时候对运维要求非常高,而且很难做到平滑的扩缩容。Codis的目标其实就是尽量兼容Twemproxy的基础上,加上数据迁移的功能以实现扩容和缩容,最终替换Twemproxy。从豌豆荚最后上线的结果来看,最后完全替换了Twem,大概2T左右的内存集群。
Redis Cluster :与Codis同期发布正式版的官方cl
Ⅶ 如何实现高可用的 redis 集群
Redis 因具有丰富的数据结构和超高的性能以及简单的协议,使其能够很好的作为数据库的上游缓存层。但在大规模的 Redis 使用过程中,会受限于多个方面:单机内存有限、带宽压力、单点问题、不能动态扩容等。
基于以上, Redis 集群方案显得尤为重要。通常有 3 个途径:官方 Redis Cluster ;通过 Proxy 分片;客户端分片 (Smart Client) 。以上三种方案各有利弊。
Redis Cluster( 官方 ) :虽然正式版发布已经有一年多的时间,但还缺乏最佳实践;对协议进行了较大修改,导致主流客户端也并非都已支持,部分支持的客户端也没有经过大规模生产环境的验证;无中心化设计使整个系统高度耦合,导致很难对业务进行无痛的升级。
Proxy :现在很多主流的 Redis 集群都会使用 Proxy 方式,例如早已开源的 Codis 。这种方案有很多优点,因为支持原声 redis 协议,所以客户端不需要升级,对业务比较友好。并且升级相对平滑,可以起多个 Proxy 后,逐个进行升级。但是缺点是,因为会多一次跳转,平均会有 30% 左右的性能开销。而且因为原生客户端是无法一次绑定多个 Proxy ,连接的 Proxy 如果挂了还是需要人工参与。除非类似 Smart Client 一样封装原有客户端,支持重连到其他 Proxy ,但这也就带来了客户端分片方式的一些缺点。并且虽然 Proxy 可以使用多个,并且可以动态增加 proxy 增加性能,但是所有客户端都是共用所有 proxy ,那么一些异常的服务有可能影响到其他服务。为每个服务独立搭建 proxy ,也会给部署带来额外的工作。
而我们选择了第三种方案,客户端分片 (Smart Client) 。客户端分片相比 Proxy 拥有更好的性能,及更低的延迟。当然也有缺点,就是升级需要重启客户端,而且我们需要维护多个语言的版本,但我们更爱高性能。
下面我们来介绍一下我们的Redis集群:
概貌:
如图0所示,
我们的 Redis 集群一共由四个角色组成:
Zookeeper :保存所有 redis 集群的实例地址, redis 实例按照约定在特定路径写入自身地址,客户端根据这个约定查找 redis 实例地址,进行读写。
Redis 实例:我们修改了 redis 源码,当 redis 启动或主从切换时,按照约定自动把地址写到 zookeeper 特定路径上。
Sentinel : redis 自带的主从切换工具,我们通过 sentinel 实现集群高可用。
客户端( Smart Client ):客户端通过约定查找 redis 实例在 ZooKeeper 中写入的地址。并且根据集群的 group 数,进行一致性哈希计算,确定 key 唯一落入的 group ,随后对这个 group 的主库进行操作。客户端会在Z ooKeeper 设置监视,当某个 group 的主库发生变化时,Z ooKeeper 会主动通知客户端,客户端会更新对应 group 的最新主库。
我们的Redis 集群是以业务为单位进行划分的,不同业务使用不同集群(即业务和集群是一对一关系)。一个 Redis 集群会由多个 group 组成 ( 一个 group 由一个主从对 redis 实例组成 ) 。即 group 越多,可以部署在更多的机器上,可利用的内存、带宽也会更多。在图0中,这个业务使用的 redis 集群由 2 个 group 组成,每个 group 由一对主从实例组成。
Failover
如图1所示,
当 redis 启动时,会 把自己的 IP:Port 写入到 ZooKeeper 中。其中的 主实例模式启动时会在 /redis/ 业务名 / 组名 永久节点写入自己的 IP:Port (如果节点不存在则创建)。由 主模式 变成 从模式 时,会创建 /redis/ 业务名 / 组名 /slaves/ip:port 临时节 点,并写入自己的 IP:Port (如果相同节点已经存在,则先删除,再创建)。而从实例 模式 启动时会创建 /redis/ 业务名 / 组名 /slaves/ip:port 临时节点,并写入自己的 ip:port (如果相同节点已经存在,则先删除,再创建)。由 从模式 变成 主模式 时,先删除 /redis/ 业务名 / 组名 /slaves/ip:port 临时节点,并在 /redis/ 业务名 / 组名 永久节点写入自己的 IP:Port 。
ZooKeeper 会一直保存当前有效的 主从实例 IP:Port 信息。至于主从自动切换过程,使用 redis 自带的 sentinel 实现,现设置为超过 30s 主 server 无响应,则由 sentinel 进行主从实例的切换,切换后就会触发以主、从实例通过以上提到的一系列动作,从而完成最终的切换。
而客户端侧通过给定业务名下的所有 groupName 进行一致性哈希计算,确定 key 落入哪个组。 客户端启动时,会从 ZooKeeper 获取指定业务名下所有 group 的 主从 IP:Port ,并在 ZooKeeper 中设置监视(监视的作用是当 ZooKeeper 的节点发生变化时,会主动通知客户端)。若客户端从 Zookeeper 收到节点变化通知,会重新获取最新的 主从 I:Port ,并重新设置监视( ZooKeeper 监视是一次性的)。通过此方法,客户端可以实时获知当前可访问最新的 主从 IP:Port 信息。
因为我们的所有 redis 实例信息都按照约定保存在 ZooKeeper 上,所以不需要针对每个实例部署监控,我们编写了一个可以自动通过 ZooKeeper 获取所有 redis 实例信息,并且监控 cpu 、 qps 、内存、主从延迟、主从切换、连接数等的工具。
发展:
现在 redis 集群在某些业务内存需求超过预期很多后,无法通过动态扩容进行扩展。所以我们正在做动态扩容的支持。原先的客户端我们是通过一致性哈希进行 key 的
路由策略,但这种方式在动态扩容时会略显复杂,所以我们决定采用实现起来相对简单的预分片方式。一致性哈希的好处是可以无限扩容,而预分片则不是。预分片
时我们会在初始化阶段指定一个集群的所有分片数量,这个数量一旦指定就不能再做改变,这个预分片数量就是后续可以扩容到最大的 redis 实例数。假设预分片 128 个 slot ,每个实例 10G 也可以达到 TB 级别的集群,对于未来数据增长很大的集群我们可以预分片 1024 ,基本可以满足所有大容量内存需求了。
原先我们的 redis 集群有四种角色, Smart Client, redis , sentinel , ZooKeeper 。为了支持动态扩容,我们增加了一个角色, redis_cluster_manager (以下简称 manager ),用于管理 redis 集群。主要工作是初始化集群(即预分片),增加实例后负责修改Z ooKeeper 状态,待客户端做好准备后迁移数据到新增实例上。为了尽量减少数据迁移期间对现性能带来的影响,我们每次只会迁移一个分片的数据,待迁移完成,再进行下一个分片的迁移。
如图2所示
相比原先的方案,多了 slots 、M anager Lock 、 clients 、M igrating Clients 节点。
Slots: 所有分片会把自身信息写入到 slots 节点下面。 Manager 在初始化集群时,根据设置的分片数,以及集群下的 group 数,进行预分片操作,把所有分片均匀分配给已有 group 。分片的信息由一个 json 串组成,记录有分片的状态 (stats) ,当前拥有此分片的 group(src) ,需要迁移到的 group(dst) 。分片的状态一共有三种: online 、 pre_migrate 、 migrating 。
Online 指这个分片处于正常状态,这时 dst 是空值,客户端根据 src 的 group 进行读写。
Pre_migrate 是指这个分片被 manager 标记为需要迁移,此时 dst 仍然为空, manager 在等所有 client 都已经准备就绪,因为 ZooKeeper 回掉所有客户端有时间差,所以如果某些 client 没有准备就绪的时候 manager 进行了数据迁移,那么就会有数据丢失。
Migrating 是 manager 确认了所有客户端都已经做好迁移准备后,在 dst 写入此分片需要迁移的目标 group 。待迁移完成,会在 src 写入目标 group_name , dst 设为空, stats 设为 online 。
Manager Lock: 因为我们是每次只允许迁移一个 slot ,所以不允许超过一个 manager 操作一个集群。所以 manager 在操作集群前,会在M anager Lock 下注册临时节点,代表这个集群已经有 manager 在操作了,这样其他 manager 想要操作这个集群时就会自动退出。
Clients 和M igrating Clients 是为了让 manager 知道客户端是否已经准备就绪的节点。客户端通过 uid 代表自己,格式是 客户端语言 _ 主机名 _pid 。当集群没有进行迁移,即所有分片都是 online 的时候,客户端会在 clients 下创建 uid 的临时节点。
当某个 slot 从 online 变成 pre_migrate 后,客户端会删除 clients 下的 uid 临时节点,然后在M igrating Clients 创建 uid 临时节点。注意,因为需要保证数据不丢失,从 pre_migrate 到 migrating 期间,这个 slot 是被锁定的,即所有对这个 slot 的读写都会被阻塞。所以 mananger 会最多等待 10s ,确认所有客户端都已经切换到准备就绪状态,如果发现某个客户端一直未准备就绪,那么 mananger 会放弃此次迁移,把 slot 状态由 pre_migrate 改为 online 。如果客户端发现 slot 状态由 pre_migrate 变成 online 了,那么会删除 migrating_clients 下的 uid 节点,在 clients 下重新创建 uid 节点。还需要注意的一点是,有可能一个客户刚启动,并且正在往 clients 下创建 uid 节点,但是因为网络延迟还没创建完成,导致 manager 未确认到这个 client 是否准备就绪,所以 mananger 把 slot 改为 pre_migrate 后会等待 1s 再确认所有客户端是否准备就绪。
如果 Manager 看到 clients 下已经没有客户端的话(都已经准备就绪),会把 slot 状态改为 migrating 。 Slot 变成 migrating 后,锁定也随之解除, manager 会遍历 src group 的数据,把对应 slot 的数据迁移到 dst group 里。客户端在 migrating 期间如果有读写 migrating slot 的 key ,那么客户端会先把这个 key 从 src group 迁移到 dst group ,然后再做读写操作。即这期间客户端性能会有所下降。这也是为什么每次只迁移一个 slot 的原因。这样即使只有 128 个分片的集群,在迁移期间受到性能影响的 key 也只有 1/128 ,是可以接受的。
Manager 发现已经把 slot 已经迁移完毕了,会在 src 写入目标 group_name , dst 设为空, stats 设为 online 。客户端也删除 migrating_clients 下的 uid ,在 clients 下创建 uid 节点。