月度归档:2014年08月

keepalived vip漂移基本原理及选举算法

keepalived可以将多个无状态的单点通过虚拟IP(以下称为VIP)漂移的方式搭建成一个高可用服务,常用组合比如keepalived+nginx,lvs,haproxy和memcached等。它的实现基础是VRRP协议,包括核心的MASTER竞选机制都是在VRRP协议所约定的。

一、配置说明:
keepalived的配置位于/etc/keepalived/keepalived.conf,配置文件格式包含多个必填/可选的配置段,部分重要配置含义如下:
global_defs: 全局定义块,定义主从切换时通知邮件的SMTP配置。
vrrp_instance: vrrp实例配置。
vrrp_script: 健康检查脚本配置。

细分下去,vrrp_instance配置段包括:
state: 实例角色。分为一个MASTER和一(多)个BACKUP。
virtual_router_id: 标识该虚拟路由器的ID,有效范围为0-255。
priority: 优先级初始值,竞选MASTER用到,有效范围为0-255。
advert_int: VRRP协议通告间隔。
interface: VIP所绑定的网卡,指定处理VRRP多播协议包的网卡。
mcast_src_ip: 指定发送VRRP协议通告的本机IP地址。
authentication: 认证方式。
virtual_ipaddress: VIP。
track_script: 健康检查脚本。

vrrp_script配置段包括:
script: 一句指令或者一个脚本文件,需返回0(成功)或非0(失败),keepalived以此为依据判断其监控的服务状态。
interval: 健康检查周期。
weight: 优先级变化幅度。
fall: 判定服务异常的检查次数。
rise: 判定服务正常的检查次数。

这里有MASTERBACKUP的参考配置。

二、选举算法
keepalived中优先级高的节点为MASTER。MASTER其中一个职责就是响应VIP的arp包,将VIP和mac地址映射关系告诉局域网内其他主机,同时,它还会以多播的形式(目的地址224.0.0.18)向局域网中发送VRRP通告,告知自己的优先级。网络中的所有BACKUP节点只负责处理MASTER发出的多播包,当发现MASTER的优先级没自己高,或者没收到MASTER的VRRP通告时,BACKUP将自己切换到MASTER状态,然后做MASTER该做的事:1.响应arp包,2.发送VRRP通告。

MASTER和BACKUP节点的优先级如何调整?
首先,每个节点有一个初始优先级,由配置文件中的priority配置项指定,MASTER节点的priority应比BAKCUP高。运行过程中keepalived根据vrrp_script的weight设定,增加或减小节点优先级。规则如下:

1. 当weight > 0时,vrrp_script script脚本执行返回0(成功)时优先级为priority + weight, 否则为priority。当BACKUP发现自己的优先级大于MASTER通告的优先级时,进行主从切换。
2. 当weight < 0时,vrrp_script script脚本执行返回非0(失败)时优先级为priority + weight, 否则为priority。当BACKUP发现自己的优先级大于MASTER通告的优先级时,进行主从切换。 3. 当两个节点的优先级相同时,以节点发送VRRP通告的IP作为比较对象,IP较大者为MASTER。 以上文中的配置为例: HOST1: 10.15.8.100, priority=91, MASTER(default) HOST2: 10.15.8.101, priority=90, BACKUP VIP: 10.15.8.102 weight = 2 抓包命令: tcpdump -nn vrrp 示例一:HOST1和HOST2上keepalived和nginx均正常。

1
2
3
4
16:33:07.697281 IP 10.15.8.100 > 224.0.0.18: VRRPv2, Advertisement, vrid 102, 
prio 93, authtype simple, intvl 1s, length 20
16:33:08.697588 IP 10.15.8.100 > 224.0.0.18: VRRPv2, Advertisement, vrid 102, 
prio 93, authtype simple, intvl 1s, length 20

此时HOST1优先级为priority + weight = 93,HOST2优先级为priority + weight = 92,HOST1仍为MASTER。

示例二:关闭HOST1上的nginx。

1
2
3
4
5
6
7
8
16:33:09.697928 IP 10.15.8.100 > 224.0.0.18: VRRPv2, Advertisement, vrid 102, 
prio 93, authtype simple, intvl 1s, length 20
16:33:10.698285 IP 10.15.8.100 > 224.0.0.18: VRRPv2, Advertisement, vrid 102, 
prio 91, authtype simple, intvl 1s, length 20
16:33:10.698482 IP 10.15.8.101 > 224.0.0.18: VRRPv2, Advertisement, vrid 102, 
prio 92, authtype simple, intvl 1s, length 20
16:33:11.699441 IP 10.15.8.101 > 224.0.0.18: VRRPv2, Advertisement, vrid 102, 
prio 92, authtype simple, intvl 1s, length 20

HOST1上的nginx关闭后,killall -0 nginx返回非0,HOST1通告的优先级为priority = 91,HOST2的优先级为priority + weight = 92,HOST2抢占成功,被选举为MASTER。相关日志可tail /var/log/messages。

由此可见,主从的优先级初始值priority和变化量weight设置非常关键,配错的话会导致无法进行主从切换。比如,当MASTER初始值定得太高,即使script脚本执行失败,也比BACKUP的priority + weight大,就没法进行VIP漂移了。所以priority和weight值的设定应遵循: abs(MASTER priority - BAKCUP priority) < abs(weight)。 另外,当网络中不支持多播(例如某些云环境),或者出现网络分区的情况,keepalived BACKUP节点收不到MASTER的VRRP通告,就会出现脑裂(split brain)现象,此时集群中会存在多个MASTER节点。 --EOF--

gen_server behaviour exercise

In these exercises, we’ll make a server in the module job_centre, which uses gen_server to implement a job management service. The job center keeps a queue of jobs that have to be done. The jobs are numbered. Anybody can add jobs to the queue. Workers can request jobs from the queue and tell the job center that a job has been performed. Jobs are represented by funs. To do a job F, a worker must evaluate the function F().
1. Implement the basic job center functionality, with the following interface:
job_centre:start_link() -> true.
    Start the job center server.
job_centre:add_job(F) -> JobNumber.
    Add a job F to the job queue. Return an integer job number.
job_centre:work_wanted() -> {JobNumber,F} | no.
    Request work. When a worker wants a job, it calls job_centre:work_wanted(). If there are jobs in the queue, a tuple {JobNumber, F} is returned. The worker performs the job by evaluating F(). If there are no jobs in the queue, no is returned. Make sure the same job cannot be allocated to more than one worker at a time. Make sure that the system is fair, meaning that jobs are handed out in the order they were requested.
job_centre:job_done(JobNumber)
    Signal that a job has been done. When a worker has completed a job, it must call job_centre:job_done(JobNumber).
2. Add a statistics call called job_centre:statistics() that reports the status of the jobs in the queue and of jobs that are in progress and that have been done.
3. Add code to monitor the workers. If a worker dies, make sure the jobs it was doing are returned to the pool of jobs waiting to be done.
4. Check for lazy workers; these are workers that accept jobs but don’t deliver on time. Change the work wanted function to return {JobNumber, JobTime, F} where JobTime is a time in seconds that the worker has to complete the job by. At time JobTime - 1, the server should send a hurry_up message to the worker if it has not finished the job. And at time JobTime + 1, it should kill the worker process with an exit(Pid, youre_fired) call.

Ans:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
-module(job_center).
-behaviour(gen_server).
 
-export([start_link/0, add_job/2, work_wanted/0, job_done/1, statistics/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-define(TIME, 1000).
-record(state, {undo, doing, done, index}).
%%%===================================================================
%%% API
%%%===================================================================
start_link() ->
  gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
add_job(F, Time) ->
  gen_server:call(?MODULE, {add_job, F, Time}).
work_wanted() ->
  gen_server:call(?MODULE, get_job).
job_done(JobNumber) ->
  gen_server:cast(?MODULE, {job_done, JobNumber}).
statistics() ->
  gen_server:call(?MODULE, statistics).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([]) ->
  process_flag(trap_exit, true),
  timer:send_interval(?TIME, self(), check_worker),
  {ok, #state{undo = queue:new(), doing = [], done = [], index = 1}}.
 
handle_call({add_job, F, Time}, _From, #state{undo = UndoPool, index = Idx} = State) ->
  State1 = State#state{undo = queue:in({Idx, Time, F}, UndoPool), index = Idx + 1},
  {reply, Idx, State1};
handle_call(get_job, {From, _}, #state{undo = UndoPool, doing = DoingPool} = State) ->
  {Reply, State1} = case queue:out(UndoPool) of
                      {{value, {Idx, Time, Fun}}, UndoPool2} ->
                        {{Idx, Time, Fun}, State#state{undo = UndoPool2, doing = [{Idx, Time, Fun, From, now_in_secs()} | DoingPool]}};
                      {empty, _} ->
                        {no, State}
                    end,
  erlang:monitor(process, From),
  {reply, Reply, State1};
handle_call(statistics, _From, #state{undo = UndoPool, doing = DoingPool, done = DonePool} = State) ->
  io:format("undo:~p~n", [UndoPool]),
  io:format("doing:~p~n", [DoingPool]),
  io:format("done:~p~n", [DonePool]),
  {reply, [{undo, queue:len(UndoPool)}, {doing, length(DoingPool)}, {done, length(DonePool)}], State};
handle_call(_Request, _From, State) ->
  {reply, ok, State}.
 
handle_cast({job_done, JobNumber}, #state{doing = DoingPool, done = DonePool} = State) ->
  Item = lists:keyfind(JobNumber, 1, DoingPool),
  DoingPool2 = lists:delete(Item, DoingPool),
  {Idx, Time, Fun, From, _} = Item,
  DonePool2 = [{Idx, Time, Fun, From} | DonePool],
  {noreply, State#state{doing = DoingPool2, done = DonePool2}};
handle_cast(_Request, State) ->
  {noreply, State}.
 
handle_info({'DOWN', _Ref, process, From, normal}, #state{undo = UndoPool, doing = DoingPool} = State) ->
  State1 = case lists:keyfind(From, 4, DoingPool) of
             Item when is_tuple(Item) ->
               {Idx, Time, Fun, _From, _} = Item,
               DoingPool2 = lists:delete(Item, DoingPool),
               State#state{undo = queue:in({Idx, Time, Fun}, UndoPool), doing = DoingPool2};
             false -> State
           end,
  {noreply, State1};
handle_info(check_worker, #state{undo = UndoPool, doing = DoingPool} = State) ->
  Now = now_in_secs(),
  F = fun(Doing) ->
    {Idx, Time, Fun, From, StartTime} = Doing,
    if
      Now =:= StartTime + Time - 1 -> From ! hurry_up;
      Now =:= StartTime + Time + 1 ->
        exit(From, youre_fired),
        State#state{undo = queue:in({Idx, Time, Fun}, UndoPool)};
      true -> ok
    end
  end,
  lists:foreach(F, DoingPool),
  {noreply, State};
handle_info(Info, State) ->
  io:format("extra info:~p~n", [Info]),
  {noreply, State}.
 
terminate(_Reason, _State) ->
  ok.
code_change(_OldVsn, State, _Extra) ->
  {ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
now_in_secs() ->
  {A, B, _} = os:timestamp(),
  A * 1000000 + B.

Test:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
$ erl -sname aa
Erlang/OTP 17 [erts-6.1] [source] [64-bit] [smp:4:4] [async-threads:10]
 
Eshell V6.1  (abort with ^G)
1> c(job_center).
{ok,job_center}
2> F1 = fun() -> 1 end.
#Fun<erl_eval.20.90072148>
3> F2 = fun() -> 2 end.
#Fun<erl_eval.20.90072148>
4> job_center:add_job(F1, 10).
1
5> job_center:add_job(F2, 10).
2
6> job_center:add_job(F1, 20).
3
7> job_center:add_job(F2, 20).
4
8> job_center:statistics().
undo:{[{4,20,#Fun<erl_eval.20.90072148>},
       {3,20,#Fun<erl_eval.20.90072148>},
       {2,10,#Fun<erl_eval.20.90072148>}],
      [{1,10,#Fun<erl_eval.20.90072148>}]}
doing:[]
done:[]
[{undo,4},{doing,0},{done,0}]
9> job_center:work_wanted().
{1,10,#Fun<erl_eval.20.90072148>}
10> job_center:job_done(1).
ok
11> job_center:statistics().
undo:{[{4,20,#Fun<erl_eval.20.90072148>},{3,20,#Fun<erl_eval.20.90072148>}],
      [{2,10,#Fun<erl_eval.20.90072148>}]}
doing:[]
done:[{1,10,#Fun<erl_eval.20.90072148>,<0.83.0>}]
[{undo,3},{doing,0},{done,1}]
12> job_center:work_wanted().
{2,10,#Fun<erl_eval.20.90072148>}
13> receive V -> V end.
hurry_up
 
=ERROR REPORT==== 24-Aug-2014::14:37:03 ===
** Generic server job_center terminating
** Last message in was {'EXIT',<0.83.0>,youre_fired}
** When Server state == {state,{[{4,20,#Fun<erl_eval.20.90072148>}],
                                [{3,20,#Fun<erl_eval.20.90072148>}]},
                               [{2,10,#Fun<erl_eval.20.90072148>,<0.83.0>,
                                 1408862212}],
                               [{1,10,#Fun<erl_eval.20.90072148>,<0.83.0>}],
                               5}
** Reason for termination ==
** youre_fired
** exception error: youre_fired

Exercises from 『Programming Erlang (2Edtion)』.

--EOF--

理解Oauth2.0的设计

oauth2
上图是从网上找来的Web Server Flow类型的Oauth2.0流程,授权流程较为清晰。相比Oauth1.0,它已经精简了很多,但是仍旧是一个复杂的授权协议。

1. 为什么有authorization_code?

authorization_code在Oauth2.0中是用来跟Authorization Server换取access_token的。如果没有省略它,直接将上图中的(3)、(4)步骤合并,那么会存在两个问题:
1). access_token容易泄露:如果access_token由Authorization Server通过HTTP 302重定向的方式返回,容易被人通过嗅探、扫日志、扫浏览器记录等方式窃取。别人拿到这个access_token就可以根据授权scope任意存取Resource Server上的资源。要避免被窃取,势必要求第三方应用提供的redirect_uri是可信信道,例如https + POST请求,那就对第三方应用要求太高了。
2). 第三方应用(如上图中的facebook.com)身份难以确认:Authorization Server要确认第三方应用的身份,必然要求应用对步骤(1)的请求进行签名,这会加大协议复杂性。

加了authorization_code后,只要保证步骤(4)、(5)的信道是安全的即可,即便authorization_code在步骤(3)中泄露,别人拿去也没用,因为没法换取access_token。通过authorization_code换取access_token最简单的方式是使用https,由Authorization Server提供https支持,这样第三方应用直接将secret传过去即可,secret作为口令的方式使用。如果没有https链路,secret可以用来对请求进行签名。就不存在上面说的两个问题了。

注:User-Agent flow (Implicit Grant)类型的Oauth授权流程中没有authorization_code,access_token是直接通过步骤(3)的redirect_uri返回,所以安全性没有Web Server Flow类型的高。User-Agent flow一般用在没有服务器端的手机APP,浏览器插件(JS)中,这类应用难以向Authorization Server证明自己身份,因为secret会暴露在外,所以干脆取消authorization_code换access_token的流程。

2. 为什么在获取authorization_code时加入state参数可以避免CSRF攻击?

一般第三方应用在引导用户跳转到Authorization Server之前,先将生成的state参数(随机数)放入session中,然后通过步骤(1)传给Authorization Server,Authorization Server会原样返回,第三方应用得到Authorization Server返回的state参数后,跟session中的比对即可。

假设没有state参数,试想以下场景:有一个第三方应用给大家发红包,会发到google账户里,用户用google账户登录第三方应用后即可提现。恶意用户M用自己的google账户完成Oauth授权的步骤(1)、(2)、(3),拿到步骤(3)中的authorization_code,然后在自己的个人网站上伪造个跨站请求(如用iframe,src填上第三方应用的redirect_uri?code=${authorization_code}),某个小白用户A访问M的个人网站后,实际上就触发了第三方应用的授权流程,第三方应用收到一个authorization_code,然后拿这个authorization_code去交换access_token,用access_token去获取google账户信息(实际上是恶意用户M的),然后给他发红包。

现在有了state参数,每次第三方应用在redirect_uri上收到一个请求,首先判断请求中的state参数和当前会话中的state参数是否一致。如果一致,表示此请求是正常的授权流程;如果不一致,则表示是CSRF攻击。

3. 为什么会有refresh_token?

refresh_token是一个长期有效的token,被设计用来在当前access_token过期后重新获取一个新的access_token,它由Authorization Server返回(需事先在获取authorization_code时指定access_type=offline)。不同于access_token有个短暂的过期时间,refresh_token只有用户主动取消授权才会失效。

给access_token一个过期时间,同时通过一个长期有效的refresh_token来换取access_token,这么设计的出发点在于性能层面。假如没有refresh_token,并且access_token不设置过期时间,那么为了支持用户取消授权,必须要求Resoure Server在收到每一次API请求,都将access_token送到Authorization Server进行验证,看token是否仍旧有效。如果用户取消授权,token失效;如果未取消授权,token有效。Authorization Server验证access_token的过程无疑是耗时的(有读数据库或者缓存的操作)。有了access_token过期时间,access_token就可以被设计为自编码(self-encoded),通过一些CPU计算进行解码既可验证此token是否有效有权,每次API请求均可减少一次读数据库或者缓存的时间。当然每次用refresh_token获取新access_token还是避免不了一次读数据库或者缓存的操作,但是这个消耗可以忽略不计了。这个设计的缺陷就是用户取消授权会有一定延时。除非access_token过期,否则第三方应用可以拿着access_token继续调用Resource Server上的资源,即便用户已经取消了授权。

--EOF--

『设计中的设计』

design-of-design我在想,曾经的某个时刻,我离设计这条路其实很近的,只是后来渐行渐远了。这句话的涵义就是我现在不是从事这个行业的,我不生产观点,尽量只当原研哉观点的搬运工。

创意是设计的灵魂,创意不止是让人惊异它崭新的形式和素材,而应该让人惊异与它居然来自于看似平凡的日常生活。这让我瞬间想到唐少那个拿过红点奖的缺口式透明胶带设计,小小的改动,带来的是极大的生活便利,这样的创意才是令人惊艳的。这种来自生活的创意驱动原研哉策划了“RE-DESIGN”展览,展览邀请了各路设计大腕出手,取材自日常生活,比如卷成方形的卫生纸,区分方向的出入境章,源于生态的捕蟑盒与火柴,还有尿不湿等等。展览是成功的,它的思路很清晰:立足于生态,对大自然怀有敬畏之心。这一代日本设计师,早在九十年代曾以“自然的睿智”的主旨为爱知世博会设计方案,体现了这个群体渴望重新定义设计师职能和作用的决心,然而方案的最终流产也恰恰印证了在世界经济形势、商业文明以及环境问题面前,这一代的设计师仍旧任重道远。

无印良品在日本红火了几十年,这几年这阵风也烧到了中国。相对它在日本的平价,进驻中国后价格提升不少,但是即便如此,大家还是被其简约与朴素的设计风格吸引,甚至有为数不少的死忠。偏素的色彩,讲究的用料,舒适的触感,这是一般人对其商品的概括。原研哉作为无印良品的艺术总监,在他看来,无印良品所包含的是“这样就好”,“World MUJI”,“虚无”的设计哲学,是简约的审美观的体现。如果再往深处挖,这种简约的设计哲学必须与日本文化关联起来看待,日本就是个文化的熔炉,一片混沌,唯有“空”的容器才能承载这片混沌,空意味着丰富的可能性,需要细心体会这份禅意。无印良品的商品,不讲究个性,不推崇过于强烈的自我意识,而是蕴含着一种“让步”,“抑制”,“退一步海阔天空”的处世哲学与自由价值。越是个性的服饰,在大街上与人撞衫时越尴尬,而无印良品这种低调,朴素,内敛的设计会避免这种尴尬,甚至,人们根本感觉不出来两人撞衫。

低调的作品出自低调人之手,这样才不显得违和。从职业素养来说原研哉是一个低调的设计师,甚至有些谦卑,他把自己定位为“设计者”,将这个词汇解读为替“设计”概念效劳的人,正如园丁的含义就是打扫庭院的人。

原研哉说“一个真正的设计师,应该能够丰富设计这一概念”,就好像说,一个真正的程序员,应该能够丰富程序设计这一概念。任重道远啊。

--EOF--