Keep learning, keep living...

0%

我们的NGINX的IP封禁功能基于Redis实现。当只支持单IP封禁时,直接以IP作为KEY,调用”GET”命令,根据Value判断是否需要封禁该IP。若要支持网段封禁,需要取出所有的CIDR段,然后判断IP是否在CIDR范围内。随着CIDR越来越多,从Redis中取出的数据则越来越多,性能消耗越来越大。为了减少数据传输量,则可以将判断逻辑改由Redis来完成。

Redis本身支持Lua脚本的执行,可以由Lua来实现相应逻辑。不过Lua语言本身不支持位运算(5.2之后支持),需要第三方库支持。所以,我们直接通过修改Redis代码扩展Redis命令来实现该功能。

阅读全文 »

在LVS的FULLNAT转发模式下, LVS对数据包同时做SNAT和DNAT,将数据包的源IP、源端口更换为LVS本地的IP和端口,将数据包的目的IP和目的端口修改为RS的IP和端口,从而不再依赖特定网络拓朴转发数据包。

这种方式存在一个问题: RealServer中接收到数据包中源IP和源端口为LVS机器的IP和端口,这样应用层程序获取到的TCP连接的客户端地址为LVS的IP地址,很多依赖客户端地址的功能就不能正常工作了。

为了解决这问题,FULLNAT模式在转发包的时候,在TCP包中添加一个OPTION,来传递客户端的真实地址。RealServer中通过内核模块toa令应用层程序获取真实的客户端地址。

阅读全文 »

Varnish使用状态机机制处理请求,在各个状态中,用户通过使用Varnish自己实现的VCL(Varnish Configuration Language)定制处理逻辑。

比如,Varnish接收并解析完请求就进入vcl_recv状态。在这个阶段,我们可以使用VCL来决定是否要服务该请求,怎么服务,以及使用哪个Backend来服务等。

VCL实现了状态的流程控制,请求信息的读取和修改等,但很多业务层面需要的逻辑没办法由VCL简单完成。比如,对某些信息进行BASE64编码等等。由于Varnish支持外部模块,用户可以使用C语言开发自己的模块,由VCL来调用这些模块来完成这些处理。但每种功能都由C模块来开发成本较大。

Lua是一种优秀的脚本语言,可以非常轻松地嵌入C语言中,而Lua语言本身有大量的库来实现各种各样的功能。因而我开发了VMOD_LUA这个Varnish模块,使用它来执行Lua脚本,由Lua代码来定制各种处理逻辑。

阅读全文 »

ngx_http_limit_conn_module模块用来限制某个KEY的并发连接数。它的实现与ngx_http_limit_req_module模块类似,整体逻辑和实现更为简单。LimitConn模块也将某个KEY的信息存储在共享内存RBTREE中的节点中, 但不需要QUEUE结构。LimitConn模块只需要在节点中记录当前的连接数信息:

1
2
3
4
5
6
typedef struct {
u_char color;
u_char len;
u_short conn;
u_char data[1];
} ngx_http_limit_conn_node_t;
阅读全文 »

LVS包转发功能由内核模块IPVS实现。Keepalived的Check进程周期性地对后端RealServer进行健康检测,根据检测结果摘除或恢复。摘除和恢复RealServer等操作本质上为Keepalived这个用户态进程与IPVS内核模块的通信操作。

libipvs封装了用户态程序对内核模块IPVS可以进行的操作,如:

  • 创建LVS服务
  • 删除LVS服务
  • 添加RealServer
  • 删除RealServer
  • 获取相关信息
阅读全文 »

ngx_http_limit_req_module用于依据指定的KEY来限制请求处理速度。比如,用来限制单个IP的访问频率。

文档地址: http://nginx.org/en/docs/http/ngx_http_limit_req_module.html

下面来看具体实现:

NGINX模块从逻辑中主要有两部分:

  • 配置解析: 将配置文件的内容解析到各配置结构中
  • 请求处理: 在当前请求上完成模块逻辑处理
阅读全文 »

ejbberd中多个模块或组件都允许管理员在配置文件中基于用户JID进行访问限制, 如在ejabberd_c2s模块中禁用某用户.

ejabberd实现了一套通用的ACL机制来满足各模块的需求.

配置方法如下:

  • 添加acl规则, 如:

    1
    {acl, blocked, {user, "test"}}.

    acl元组第2个元素为该条acl规则的名称, 第3个元素为JID的过滤规则. 示例中的过滤规则表示用户JID的User部分为”test”.

  • 添加access规则, 如:

    1
    {access, c2s, [{deny, blocked}, {allow, all}]}.

    access元组第2个元素为access规则的名称, 第3个元素中的每个元素为一个指定值和一个acl规则名字.
    当JID满足某条acl规则时, 该条access规则的值则为该acl规则的对应值. 示例中, 当用户JID中的User部分为”test”时, access规则c2s值为deny, 否则为allow.

  • 为模块或组件指定access规则(不同的模块或组件所用的配置指令可能不同) 如:

    1
    2
    3
    4
    5
    {5222, ejabberd_c2s, [
    {access, c2s},
    {shaper, c2s_shaper},
    {max_stanza_size, 65536}
    ]}.

    按示例配置后, ejabberd_c2s会根据access规则c2s的值禁用相应用户, 如JID中User为”test”的用户全部被禁用.

阅读全文 »

PowerDNS中DNS解析由各类Backend模块处理。如果解析相关的数据存储于MySQL, Postgres等数据库,Backend需要在这些数据库中查询相应的记录是否存在。查询数据库的性能很低。因而PowerDNS中实现了PacketCache来提高性能。PowerDNS接收到请求后,先在PacketCache中查询是否已经有相应的DNS响应。如果有则直接返回该缓存。否则交由backend处理,处理后再添加到PacketCache中。

PacketCache实现主要位于packetcache.hh和packetcache.cc中。

阅读全文 »

flashcache是Facebook开源的用SSD来缓存机械磁盘数据的Linux内核模块。

简单来说,Linux的I/O栈层次结构可以表示为:

token data
1
2
3
4
5
6
7
+----------------------------+
| VFS(Virtual File System) |
+----------------------------+
| Block I/O Layer |
+----------------------------+
| Devices |
+----------------------------+

文件系统I/O操作传递给块设备,抽象块设备最终传递给真实的设备驱动,完成I/O操作。Linux在块设备层实现了Device Mapper(DM)的映射机制。DM机制可以将一个或多个块设备再映射成一个虚拟块设备。具体的映射规则由mapping table来实现。到达虚拟块设备的I/O请求,DM机制会根据映射表找到正确的目标设备,将请求放到目标设备的请求队列中,目标设备根据目标类型进行处理。结构图如下:

token data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
         +---------------+
| Mapped Device |
+-------+-------+
|
v
+---------------+
| mapping table |
+--+---------+--+
| |
| |
v v
+---------------+ +-----------------+
|Physical Device| | Physical Device |
+---------------+ +-----------------+

DM机制的映射目标类型可以通过内核模块进行扩展,flashcache就是通过定义一种名称为”flashcache”的映射目标类型来实现SSD做为机械磁盘的缓存。结构示意图如下:

token data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+---------------+
| Mapped Device |
+-------+-------+
|
v
+---------------+
| mapper table |
+-------+-------+
|
| flashcache
|
+---+---+
| |
v v
+-----+ +------+
| SSD | | DISK |
+-----+ +------+

flashcache做为缓存层,提供了三种写入模式:

  • writeback
  • writethrough
  • writearound

三种模式的区别如下图所示:

1
2
3
4
5
6
7
writethrough  writearound  writeback
| | |
-----|------------|-----------|------------
v | v SSD
-----|------------|------------------------
v v DISK
-------------------------------------------
  • writethrough:进行写操作时,会同时写入SSD和磁盘
  • writearound: 进行写操作时,只对磁盘进行写操作
  • writeback: 进行写操作时,只写入SSD,flashcache异步地将内容写入磁盘

下面分析flashcache模块的初始化。

flashcache内核模块的入口函数为flashcache_init。

1
module_init(flashcache_init);

flashcache_init首先会对job相关的变量及结构进行初始化:

1
2
3
4
5
r = flashcache_jobs_init();
if (r)
return r;
atomic_set(&nr_cache_jobs, 0);
atomic_set(&nr_pending_jobs, 0);

flashcache_jobs_init会分配job操作所需的内存池,简化逻辑如下:

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
static int
flashcache_jobs_init(void)
{
_job_cache = kmem_cache_create("kcached-jobs",
sizeof(struct kcached_job),
__alignof__(struct kcached_job),
0, NULL);
...

_job_pool = mempool_create(MIN_JOBS, mempool_alloc_slab,
mempool_free_slab, _job_cache);

_pending_job_cache = kmem_cache_create("pending-jobs",
sizeof(struct pending_job),
__alignof__(struct pending_job),
0, NULL);

...

_pending_job_pool = mempool_create(MIN_JOBS, mempool_alloc_slab,
mempool_free_slab, _pending_job_cache);
...

return 0;
}

然后,flashcache_init调用dm_io_client_create创建一个dm_io_client结构,它为块设备I/O请求执行过程提供内存池。

1
flashcache_io_client = dm_io_client_create();

接着,flashcache_init调用dm_kcopyd_client_create创建了一个dm_kcopyd_client结构。kcopyd是一个内核进程,它用于异步地copy一个块设备的区域到一个或多个其他的块设备。dm_kcopyd_client用于向kcopyd提交任务。

1
flashcache_kcp_client = dm_kcopyd_client_create(NULL);

然后,初始化了一个内核工作队列,执行do_work去处理各种job, 本文略过。

1
INIT_WORK(&_kcached_wq, do_work);

接着,注册flashcache的DM机制目标类型, 目标类型中具体回调本文略过。

1
r = dm_register_target(&flashcache_target);

再来看flashcache_init的最后部分:

1
2
3
4
5
6
flashcache_module_procfs_init();
flashcache_control = (struct flashcache_control_s *)
kmalloc(sizeof(struct flashcache_control_s), GFP_KERNEL);
flashcache_control->synch_flags = 0;
register_reboot_notifier(&flashcache_notifier);
return 0;

首先,调用flashcache_module_procfs_init创建”/proc/flashcache”目录及文件”/proc/flashcache/flashcache_version”。
flachcache_module_procfs_init简化逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void
flashcache_module_procfs_init(void)
{
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *entry;

if (proc_mkdir("flashcache", NULL)) {
entry = create_proc_entry("flashcache/flashcache_version", 0, NULL);
if (entry)
entry->proc_fops = &flashcache_version_operations;
}
#endif /* CONFIG_PROC_FS */
}

最后,通过register_reboot_notifier注册函数flashcache_notify_reboot。这函数会在机器重启或关闭时被调用。

1
2
3
4
5
static struct notifier_block flashcache_notifier = {
.notifier_call = flashcache_notify_reboot,
.next = NULL,
.priority = INT_MAX, /* should be > ssd pri's and disk dev pri's */
};

至此,flashcache内核模块初始化完成,可以使用dmsetup指定flashcache目标类型创建虚拟块设备了。

ejabberd_config模块负责加载ejabberd配置文件,存储相应的配置选项,并提供添加和获取配置选项的API。

比如, ejabberd_app:start_modules函数会使用ejabber_config:get_local_option获取配置文件中的modules选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
%% Start all the modules in all the hosts
start_modules() ->
lists:foreach(
fun(Host) ->
case ejabberd_config:get_local_option({modules, Host}) of
undefined ->
ok;
Modules ->
lists:foreach(
fun({Module, Args}) ->
gen_mod:start_module(Host, Module, Args)
end, Modules)
end
end, ?MYHOSTS).

下面分析ejbberd_config模块实现。

ejabberd启动时,ejabberd_app:start/0会调用ejabberd_config:start/0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
start() ->
mnesia:create_table(config,
[{disc_copies, [node()]},
{attributes, record_info(fields, config)}]),
mnesia:add_table_copy(config, node(), ram_copies),
mnesia:create_table(local_config,
[{disc_copies, [node()]},
{local_content, true},
{attributes, record_info(fields, local_config)}]),
mnesia:add_table_copy(local_config, node(), ram_copies),
Config = get_ejabberd_config_path(),
load_file(Config),
%% This start time is used by mod_last:
add_local_option(node_start, now()),
ok.

start函数首先创建config和local_config两个mnesia表,接着调用get_ejabberd_config_path获取配置文件路径。

1
2
3
4
5
6
7
8
9
10
11
get_ejabberd_config_path() ->
case application:get_env(config) of
{ok, Path} -> Path;
undefined ->
case os:getenv("EJABBERD_CONFIG_PATH") of
false ->
?CONFIG_PATH;
Path ->
Path
end
end.

get_ejabberd_config_path首先使用application:get_env从ejabberd.app的env或erlang命令行中的config选项中获取值:
如:

1
{env, [{config, "/etc/ejabberd/ejabberd.cfg"]}

或者:

1
erl -config "/path/to/ejabberd.cfg"

如果没有设置这两个选项,则尝试从系统环境变量”EJABBERD_CONFIG_PATH”读取文件路径。ejabberdctl会设置该环境变量。若也没有设置该环境变量,get_ejabberd_config_path则返回?CONFIG_PATH, 这个宏被定义为”ejabberd.cfg”。

1
-define(CONFIG_PATH, "ejabberd.cfg").

接下来, start函数调用load_file来加载配置文件:

1
2
3
4
5
6
load_file(File) ->
Terms = get_plain_terms_file(File),
State = lists:foldl(fun search_hosts/2, #state{}, Terms),
Terms_macros = replace_macros(Terms),
Res = lists:foldl(fun process_term/2, State, Terms_macros),
set_opts(Res).

load_file首先调用get_plain_terms_file来获取所有配置选项的列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
get_plain_terms_file(File1) ->
File = get_absolute_path(File1),
case file:consult(File) of
{ok, Terms} ->
include_config_files(Terms);
{error, {LineNumber, erl_parse, _ParseMessage} = Reason} ->
ExitText = describe_config_problem(File, Reason, LineNumber),
?ERROR_MSG(ExitText, []),
exit_or_halt(ExitText);
{error, Reason} ->
ExitText = describe_config_problem(File, Reason),
?ERROR_MSG(ExitText, []),
exit_or_halt(ExitText)
end.

我们来看get_plain_terms_file实现。首先,调用get_absolute_path,期望得到配置文件的绝对路径。不过,get_absolute_path实现上存在BUG:

1
2
3
4
5
6
7
8
9
get_absolute_path(File) ->
case filename:pathtype(File) of
absolute ->
File;
relative ->
Config_path = get_ejabberd_config_path(),
Config_dir = filename:dirname(Config_path),
filename:absname_join(Config_dir, File)
end.

当File为相对路径时,使用filename:absname_join不能得到绝对路径。我提了一个PATCH,使用当前目录来转成绝对路径。官方已接受。
PATCH地址:
https://github.com/processone/ejabberd/commit/62ccf1cf0e13954ee5207bc6288afbc669247d14

接着,get_plain_terms_file调用file:consult读取配置文件中的所有Erlang Terms到列表中,再调用include_config_files函数来处理include_config_file选项。

1
2
3
4
5
6
7
include_config_files([{include_config_file, Filename, Options} | Terms], Res) ->
Included_terms = get_plain_terms_file(Filename),
Disallow = proplists:get_value(disallow, Options, []),
Included_terms2 = delete_disallowed(Disallow, Included_terms),
Allow_only = proplists:get_value(allow_only, Options, all),
Included_terms3 = keep_only_allowed(Allow_only, Included_terms2),
include_config_files(Terms, Res ++ Included_terms3);

include_config_file选项格式为:

1
{include_config_file, [{disallow, foo}, {allow_only, bar}], "/path/to/included_config"}.

include_config_files递归调用get_plain_terms_file获取被引用的配置文件中所有配置,接着检查include_config_file选项中是否有disallow选项。如果有,调用delete_disallowed将disallow指定的配置选项从被引用文件的配置列表中删除。接着检查其中是否存在allow_only选项,如果有,则调用keep_only_allowed只保留下allow_only中指定的配置,将其和外部配置合并,再递归调用include_config_files/2处理剩余的选项,最终返回所有配置文件中所有选项列表。

1
2
include_config_files([], Res) ->
Res;

load_file接着遍历配置列表调用search_host, 最终调用add_option来添加hosts选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
add_option(Opt, Val, State) ->
Table = case Opt of
hosts ->
config;
language ->
config;
_ ->
local_config
end,
case Table of
config ->
State#state{opts = [#config{key = Opt, value = Val} |
State#state.opts]};
local_config ->
case Opt of
{{add, OptName}, Host} ->
State#state{opts = compact({OptName, Host}, Val,
State#state.opts, [])};
_ ->
State#state{opts = [#local_config{key = Opt, value = Val} |
State#state.opts]}
end
end.

add_option将指定选项以Key/Value形式添加进状态结构的opts域中。其中,hosts和language使用记录config, 其他选项使用local_config。这与最初创建的MNESIA表相对应。
状态结构如下:

1
2
3
4
5
-record(state, {opts = [],
hosts = [],
override_local = false,
override_global = false,
override_acls = false}).

接下来,load_file调用replace_macros来替换配置中的宏为相应的值。我们来看replace_macros实现。

1
2
3
replace_macros(Terms) ->
{TermsOthers, Macros} = split_terms_macros(Terms),
replace(TermsOthers, Macros).

首先, 调用split_terms_macros将宏选项和其他选项分开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
split_terms_macros(Terms) ->
lists:foldl(
fun(Term, {TOs, Ms}) ->
case Term of
{define_macro, Key, Value} ->
case is_atom(Key) and is_all_uppercase(Key) of
true ->
{TOs, Ms++[{Key, Value}]};
false ->
exit({macro_not_properly_defined, Term})
end;
Term ->
{TOs ++ [Term], Ms}
end
end,
{[], []},
Terms).

宏定义选项格式为:

1
{define_macro, 'KEY', bar}.

其中key必须为atom类型且必须全部为大写字母,得到的宏选项列表为{key, value}格式的列表。
接着, replace_macros调用replace/2。

1
2
3
4
replace([], _) ->
[];
replace([Term|Terms], Macros) ->
[replace_term(Term, Macros) | replace(Terms, Macros)].

replace通过递归对每个选项调用replace_term/2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
replace_term(Key, Macros) when is_atom(Key) ->
case is_all_uppercase(Key) of
true ->
case proplists:get_value(Key, Macros) of
undefined -> exit({undefined_macro, Key});
Value -> Value
end;
false ->
Key
end;
replace_term({use_macro, Key, Value}, Macros) ->
proplists:get_value(Key, Macros, Value);
replace_term(Term, Macros) when is_list(Term) ->
replace(Term, Macros);
replace_term(Term, Macros) when is_tuple(Term) ->
List = tuple_to_list(Term),
List2 = replace(List, Macros),
list_to_tuple(List2);
replace_term(Term, _) ->
Term.

replace_term遍历选项中的所有子项,如果在宏列表中查找到相应的值,则替换该子项为找到的值。
另外,如果配置中某子项指定了{use_macro, Key, Value}这种格式的配置,在替换时优先从宏列表中查找相应的值,找不到再使用use_macro指定的Value来替换。

至此,获得了所有配置选项的列表,接下来对每个选项调用process_term。

选项存储主要分为3种类型, process_term分别进行不同处理:

  • 在状态结构中以独立域进行存储,如override_global选项:
    1
    2
    override_global ->
    State#state{override_global = true};
  • 以选项名做为key进行存储, 如max_fsm_queue选项:
    1
    2
    {max_fsm_queue, N} ->
    add_option(max_fsm_queue, N, State);
  • 以选项名和Host一起做为key进行存储,如domain_certfile选项:
    1
    2
    3
    4
    5
    6
    7
    8
    {domain_certfile, Domain, CertFile} ->
    case ejabberd_config:is_file_readable(CertFile) of
    true -> add_option({domain_certfile, Domain}, CertFile, State);
    false ->
    ErrorText = "There is a problem in the configuration: "
    "the specified file is not readable: ",
    throw({error, ErrorText ++ CertFile})
    end;
    其中由host_config指定的绝大多数配置选项都以这种方式存储:
    1
    2
    3
    {host_config, Host, Terms} ->
    lists:foldl(fun(T, S) -> process_host_term(T, Host, S) end,
    State, Terms);
    process_terms对于没有明确列出的选项,给配置的每个HOST都调用process_host_term添加了一个以选项名和HOST一起做为Key的配置选项。
    1
    2
    3
    {_Opt, _Val} ->
    lists:foldl(fun(Host, S) -> process_host_term(Term, Host, S) end,
    State, State#state.hosts)
    这样,如果我们自定义配置选项dummy_config:
    1
    {dummy_config, [foo, bar]}.
    查询时应使用如下提供Host参数的语句:
    1
    ejabberd_config:get_local_option({dummy_config, Host})

此外,acl, accessshaper三个选项比较特殊。
acl存储acl记录结构在状态结构的opts域中, 在set_opts中这些记录会被写入acl表中。
accessshaper选项的KEY中除了选项名,HOST,还包括了规则名称。

至此,load_file将所有选项保存到了状态结构的opts域中,最后调用set_opts进行存储:

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
set_opts(State) ->
Opts = lists:reverse(State#state.opts),
F = fun() ->
if
State#state.override_global ->
Ksg = mnesia:all_keys(config),
lists:foreach(fun(K) ->
mnesia:delete({config, K})
end, Ksg);
true ->
ok
end,
if
State#state.override_local ->
Ksl = mnesia:all_keys(local_config),
lists:foreach(fun(K) ->
mnesia:delete({local_config, K})
end, Ksl);
true ->
ok
end,
if
State#state.override_acls ->
Ksa = mnesia:all_keys(acl),
lists:foreach(fun(K) ->
mnesia:delete({acl, K})
end, Ksa);
true ->
ok
end,
lists:foreach(fun(R) ->
mnesia:write(R)
end, Opts)
end,
case mnesia:transaction(F) of
...
end.

如果配置了override_global, override_local, override_acls选项,set_opts首先会分别删除表config, local_config和acl中的所有内容。接着分别将状态结构opts域中的配置写入config, local_config和acl三个表中。

配置加载过程结束。

ejabberd_config模块提供了添加和查询选项的API:

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
add_global_option(Opt, Val) ->
mnesia:transaction(fun() ->
mnesia:write(#config{key = Opt,
value = Val})
end).

add_local_option(Opt, Val) ->
mnesia:transaction(fun() ->
mnesia:write(#local_config{key = Opt,
value = Val})
end).

get_global_option(Opt) ->
case ets:lookup(config, Opt) of
[#config{value = Val}] ->
Val;
_ ->
undefined
end.

get_local_option(Opt) ->
case ets:lookup(local_config, Opt) of
[#local_config{value = Val}] ->
Val;
_ ->
undefined
end.

add_global_option和add_local_option分别向config和local_config表中添加选项记录。get_global_option和get_local_option直接使用ets:lookup查找相应配置。这是由于mnesia底层由ETS实现,直接使用ets:lookup性能会更高。不过,我个人不太欣赏这种写法。