0%

supervisor(监控树)的使用和重启策略

1. init函数

1
2
init() ->
{ok, {SupFlags, [ChildSpec,...]}} | ignore.

[ChildSpec,…] 是在init之后默认要启动的子进程。

2. SupFlags参数

{Type, Times, Sec}

  • Type: 重启策略
    • one_for_one: 一个子进程终止,只重启该进程,在init的时候会启动参数内的子进程
    • simple_one_for_one: 同one_for_one,但是在init的时候不会启动子进程,需要动态调用启动
    • one_for_all: 一个子进程终止,将重启所有子进程
    • rest_for_one: 一个子进程终止,将按顺序重启这个子进程和之后顺序的子进程
  • Times: 次数(监控频率)
  • Sec: 秒数(监控频率),如果在Sec秒内重启次数超过Times,则终止所有进程,并终止监控树,将由父进程决定它的命运

3. ChildSpec参数如下

1
2
3
4
5
6
7
8
9
10
11
12
{Id, StartFunc, Restart, Shutdown, Type, Modules}

%% 或者

#{
id => child_id(),
start => mfaargs(),
restart => restart(),
shutdown => shutdown(),
type => work(),
modules => modules()
}
  • Id 子进程ID标识符
  • StartFunc = {M, F, A}: 子程序启动入口
  • Restart: 重启方案
    • permanent: 如果app终止了,整个系统都会停止工作(application:stop/1除外)。
    • transient: 如果app以normal的原因终止,没有影响。任何其它终止原因都谁导致整个系统关闭。
    • temporary: app可以以任何原因终止。只产生报告,没有其它任何影响。
  • Shutdown: 终止策略
    • brutal_kill: 无条件终止
    • 超时值(毫秒): 终止时,如果超时,则强制终止
    • infinity: 如果子进程是监控树,设置为无限大,等待其终止为止
  • Type:
    • worker: 普通子进程
    • supervisor: 子进程是监控树
  • Modules:
    • dynamic: 当子进程是gen_event
    • [Module]: 当子进程是监控树、gen_server或者gen_fsm,表示回调模块名称

4. 监控树操作

Sup通常可以为?MODULE

1
2
3
4
5
6
7
8
9
10
11
% 启动监控树
supervisor:start_link(Sup, []).

% 启动一个子进程
supervisor:start_child(Sup, ChildSpec).

% 停止一个子进程
supervisor:terminate(Sup, Id).

% 删除一个子进程
supervisor:delete_child(Sup, Id).

AOI主要有九宫格、灯塔和十字链表的算法实现。本文阐述十字链表的实现和尝试。

1. 基本原理

根据二维地图,将其分成x轴和y轴两个链表。如果是三维地图,则还需要维护多一个z轴的链表。将对象的坐标值按照大小相应的排列在相应的坐标轴上面。

2. 基本接口

对对象的操作主要有以下三个接口:

  • add:对象进入地图;
  • leave:对象离开地图;
  • move:对象在地图内移动。

2. 算法实现

既然是链表,很自然地想到用线性表来实现。因为存在向前和向后找的情况,所以使用双链表实现。其实实现也是非常简单,就是两个双链表(这里以二维地图举例)。那么我们的节点需要四个指针,分布为x轴的前后指针,y轴的前后指针。

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
// 双链表(对象)
class DoubleNode
{
public:
DoubleNode(string key, int x, int y)
{
this->key = key;
this->x = x;
this->y = y;
xPrev = xNext = NULL;
};

DoubleNode * xPrev;
DoubleNode * xNext;

DoubleNode * yPrev;
DoubleNode * yNext;

string key; // 只是一个关键字
int x; // 位置(x坐标)
int y; // 位置(y坐标)

private:

};

下面是地图场景信息和接口。这里的实现比较粗略,是带头尾的的双链表,暂时不考虑空间占用的问题。类Scene有分别有一个头尾指针,初始化的时候会为其赋值,主要用DoubleNode类的指针来存储x轴和y轴的头尾。初始化的时候,将_head的next指针指向尾_tail;将_tail的prev指针指向_head

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
// 地图/场景
class Scene
{
public:
Scene()
{
this->_head = new DoubleNode("[head]", 0, 0); // 带头尾的双链表(可优化去掉头尾)
this->_tail = new DoubleNode("[tail]", 0, 0);
_head->xNext = _tail;
_head->yNext = _tail;
_tail->xPrev = _head;
_tail->yPrev = _head;
};

// 对象加入(新增)
DoubleNode * Add(string name, int x, int y);

// 对象离开(删除)
void Leave(DoubleNode * node);

// 对象移动
void Move(DoubleNode * node, int x, int y);

// 获取范围内的AOI (参数为查找范围)
void PrintAOI(DoubleNode * node, int xAreaLen, int yAreaLen);

private:
DoubleNode * _head;
DoubleNode * _tail;
};

2.1. add(进入地图)

DoubleNode对象插入到十字链表中。x轴和y轴分别处理,处理方法基本一致。其实就是双链表的数据插入操作,需要从头开始遍历线性表,对比相应轴上的值的大小,插入到合适的位置。

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
void _add(DoubleNode * node)
{
// x轴处理
DoubleNode * cur = _head->xNext;
while(cur != NULL)
{
if((cur->x > node->x) || cur==_tail) // 插入数据
{
node->xNext = cur;
node->xPrev = cur->xPrev;
cur->xPrev->xNext = node;
cur->xPrev = node;
break;
}
cur = cur->xNext;
}

// y轴处理
cur = _head->yNext;
while(cur != NULL)
{
if((cur->y > node->y) || cur==_tail) // 插入数据
{
node->yNext = cur;
node->yPrev = cur->yPrev;
cur->yPrev->yNext = node;
cur->yPrev = node;
break;
}
cur = cur->yNext;
}
}

假设可视范围为x轴2以内,y轴2以内,则运行:

  1. 分别插入以下数据a(1,5)、f(6,6)、c(3,1)、b(2,2)、e(5,3),然后插入d(3,3),按照x轴和y轴打印其双链表结果;
  2. 插入d(3,3)数据,求其可视AOI范围(如图,除了f(6,6),其它对象都在d的可视范围内)。

控制台结果(前8行):

cll_005.png

步骤1结果图示:

cll_001.png

步骤2结果图示:

cll_002.png

2.2. leave(离开地图)和move(移动)

其实都是双链表的基本操作,断掉其相应的指针就好了。按理,是需要

move和leave操作如图,move是将d(3,3)移动到(4,4),然后再打印其AOI范围。

控制台结果:

cll_006.png

移动后AOI范围图示:

cll_003.png

3. 完整代码实例

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
#include "stdafx.h"
#include "stdio.h"
#include <iostream>
#include <string>

using namespace std;

// 双链表(对象)
class DoubleNode
{
public:
DoubleNode(string key, int x, int y)
{
this->key = key;
this->x = x;
this->y = y;
xPrev = xNext = NULL;
};

DoubleNode * xPrev;
DoubleNode * xNext;

DoubleNode * yPrev;
DoubleNode * yNext;

string key;
int x; // 位置(x坐标)
int y; // 位置(y坐标)

private:

};




// 地图/场景
class Scene
{
public:

Scene()
{
this->_head = new DoubleNode("[head]", 0, 0); // 带头尾的双链表(可优化去掉头尾)
this->_tail = new DoubleNode("[tail]", 0, 0);
_head->xNext = _tail;
_head->yNext = _tail;
_tail->xPrev = _head;
_tail->yPrev = _head;
};

// 对象加入(新增)
DoubleNode * Add(string name, int x, int y)
{

DoubleNode * node = new DoubleNode(name, x, y);
_add(node);
return node;
};

// 对象离开(删除)
void Leave(DoubleNode * node)
{
node->xPrev->xNext = node->xNext;
node->xNext->xPrev = node->xPrev;
node->yPrev->yNext = node->yNext;
node->yNext->yPrev = node->yPrev;

node->xPrev = NULL;
node->xNext = NULL;
node->yPrev = NULL;
node->yNext = NULL;
};

// 对象移动
void Move(DoubleNode * node, int x, int y)
{
Leave(node);
node->x = x;
node->y = y;
_add(node);
};

// 获取范围内的AOI (参数为查找范围)
void PrintAOI(DoubleNode * node, int xAreaLen, int yAreaLen)
{
cout << "Cur is: " << node->key << "(" << node ->x << "," << node ->y << ")" << endl;
cout << "Print AOI:" << endl;

// 往后找
DoubleNode * cur = node->xNext;
while(cur!=_tail)
{
if((cur->x - node->x) > xAreaLen)
{
break;
}
else
{
int inteval = 0;
inteval = node->y - cur->y;
if(inteval >= -yAreaLen && inteval <= yAreaLen)
{
cout << "\t" << cur->key << "(" << cur ->x << "," << cur ->y << ")" << endl;
}
}
cur = cur->xNext;
}

// 往前找
cur = node->xPrev;
while(cur!=_head)
{
if((node->x - cur->x) > xAreaLen)
{
break;
}
else
{
int inteval = 0;
inteval = node->y - cur->y;
if(inteval >= -yAreaLen && inteval <= yAreaLen)
{
cout << "\t" << cur->key << "(" << cur ->x << "," << cur ->y << ")" << endl;
}
}
cur = cur->xPrev;
}
};

// 调试代码
void PrintLink() // 打印链表(从头开始)
{
// 打印x轴链表
DoubleNode * cur = _head->xNext;
while (cur != _tail)
{
cout << (cur->key) << "(" << (cur->x) <<"," << (cur->y) << ") -> " ;
cur = cur->xNext;
}
cout << "end" << endl;

// 打印y轴链表
cur = _head->yNext;
while (cur != _tail)
{
cout << (cur->key) << "(" << (cur->x) <<"," << (cur->y) << ") -> " ;
cur = cur->yNext;
}
cout << "end" << endl;
};

private:
DoubleNode * _head;
DoubleNode * _tail;

void _add(DoubleNode * node)
{
// x轴处理
DoubleNode * cur = _head->xNext;
while(cur != NULL)
{
if((cur->x > node->x) || cur==_tail) // 插入数据
{
node->xNext = cur;
node->xPrev = cur->xPrev;
cur->xPrev->xNext = node;
cur->xPrev = node;
break;
}
cur = cur->xNext;
}

// y轴处理
cur = _head->yNext;
while(cur != NULL)
{
if((cur->y > node->y) || cur==_tail) // 插入数据
{
node->yNext = cur;
node->yPrev = cur->yPrev;
cur->yPrev->yNext = node;
cur->yPrev = node;
break;
}
cur = cur->yNext;
}
}
};

// --------------------------------------------
void main()
{
Scene scene = Scene();
// 增加
scene.Add("a", 1, 5);
scene.Add("f", 6, 6);
scene.Add("c", 3, 1);
scene.Add("b", 2, 2);
scene.Add("e", 5, 3);
DoubleNode * node = scene.Add("d", 3, 3);

scene.PrintLink();
scene.PrintAOI(node, 2, 2);

// 移动
cout << endl << "[MOVE]" << endl;
scene.Move(node, 4, 4);
scene.PrintLink();
scene.PrintAOI(node, 2, 2);

// 删除
cout << endl << "[LEAVE]" << endl;
scene.Leave(node);
scene.PrintLink();
}

关于Cowboy

Cowboy是基于Erlang实现的一个轻量级、快速、模块化的http web服务器。

Handlers

用于处理HTTP请求的程序处理模块。

Plain HTTP Handlers(常规Handlers)

Cowboy里面的handler最基础的事情就是实现 init/2 回调函数,处理请求,发送客户端响应(可选),最后返回。
Cowboy根据 router configuration (路由配置)接收请求并初始化State。
下面是一个不做任何处理的handler:

init(Req, State) ->
    {ok, Req, State}

Cowboy为了保证每一个相应都能有客户端响应,尽管上面例子没有发送客户端返回,客户端仍然会收到一个 204 No Content 的响应。

下面是一个有返回响应的例子:

init(Req0, State) ->
    Req = cowboy_req:reply(200, [
        {<<"content-type">>, <<"text/plain">>}
    ], <<"Hello, World!">>, Req0),
    {ok, Req, State}.

当调用了 cowboy:req/4, Cowboy会马上返回一个客户端响应。

最后我们返回一个三元组。ok 表示handler允许成功,然后返回处理过后的 Req 给Cowboy。
三元组的最后一个元素是一个贯穿在handler所有回调一个state。常规的HTTP handlers一般只附加一个回调函数,terminate/2是一个很少使用的可选的回调函数。

Other Handlers(其它Handlers)

init/2 回调函数也可以用来告诉cowboy,这是一个不同类型的handler,Cowboy应该做一些其他处理。为了方便使用,如果返回handler类型的模块名称,就可以切换handler处理模块。

Cowboy提供了三种可选handler类型:cowboy_reset, Cowboy_websocke和cowboy_loop。另外也可以自己定义handler类型。

切换非常简单,用handler类型替换掉返回的 ok 就可以了。下面是一个切换为 Websocket handler 的代码片段。

init(Req, State) ->
    {cowboy_websocket, Req, State}.

也可以切换到一个自定义的handler模块:

init(Req, State) ->
    {my_handler_type, Req, State}.

如何使用自定义的handler类型可以查看Sub protocols 章节(https://ninenines.eu/docs/en/cowboy/2.0/guide/sub_protocols)。

Cleaning up

除了Websocket handlers,其它所有类型都提供可选回调函数terminate/3

terminate(_Reason, _Req, _State) ->
    ok.

这个回调函数是为了cleanup保留下来的。该函数不能发送响应给客户端。也没有其他返回值(只能返回ok)。

terminate/3之所以是可选是因为其极少会用到。Cleanup应该在各自的进程中直接处理。(通过监控handler进程来知道其何时退出)

Cowboy不会在不同的请求重复使用进程(应该是http短链接设计引起的)。进程在返回之后很快就会被销毁。

Others

英文官方原文:

https://ninenines.eu/docs/en/cowboy/2.0/guide/handlers/#_plain_http_handlers

关于Cowboy

Cowboy是基于Erlang实现的一个轻量级、快速、模块化的http web服务器。

Routing(路由)

本文官方原文:http://ninenines.eu/docs/en/cowboy/1.0/guide/routing/

默认情况下,Cowboy不会做什么事情。
为了使Cowboy可用,需要映射URL和处理请求的Erlang模型(Module),这个过程,我们称之为路由(routing)。
当Cowboy接收到一个请求,通过路由,Cowboy就会尝试去匹配到相应请求的主机和资源路径。如果匹配成功,那么相关的Erlang代码就会被执行。
每个主机会给出相应的路由规则。Cowboy首先会匹配主机,然后尝试寻找匹配的路径。
在使用Cowboy之前需要先编译路由。

Structure(结构)

路由一般定义成以下结构

Routes = [Host1, Host2, ... HostN].

每个主机包含匹配规则(HostMatch)、限制规则(非必须)(Constraints)和一个由路径组成的路由列表(PathsList)。

Host1 = {HostMatch, PathsList}.
Host2 = {HostMatch, Constraints, PathsList}.

由路径组成的路由列表与主机列表类似。

PathsList = [Path1, Path2, ... PathN].

而路径(path)包含匹配路径规则、限制规则(非必须)、处理逻辑的module和会被初始化的选项参数。

Path1 = {PathMatch, Handler, Opts}.
Path2 = {PathMatch, Constraints, Handler, Opts}.

下面内容为匹配语法和限制选项。

Match syntax(匹配语法)

匹配语法用来关联主机名字和相应的handler路径。
主机的匹配语法和路径的匹配语法类似,只有轻微的区别。譬如,他们分隔符是不一样的。而且主机是从最后开始匹配的,而路径是不是。
(其实说了老半天,这不就是一个普通的URL嘛。URL的前半部分为主机IP或域名,这里称之为HOST,即主机。而后半部分为路径)
除了特殊情况,最简单的匹配就是只有主机或者只有路径的匹配。他的值可以为string() 或binary() 类型。

PathMatch1 = "/".
PathMatch2 = "/path/to/resource".
 
HostMatch1 = "cowboy.example.org".

正如你所见,所有的路径都是由斜杠开始的。注意,下面两条路径对于路由而言是一样的。

PathMatch2 = "/path/to/resource".
PathMatch3 = "/path/to/resource/".

而对于主机名,最后有点和没有点对于路由来说也是一样的。同样,在前面多一个点和少一个点也是一样的。

HostMatch1 = "cowboy.example.org".
HostMatch2 = "cowboy.example.org.".
HostMatch3 = ".cowboy.example.org".

因此能够提取主机和路径的数据段并且存储在Req 对象供后面使用。我们称之为值绑定。
绑定语法非常简单。由冒号字符(:)开头,一直到数据段的结尾的这个数据段是我们的绑定名称,会被保存。

PathMatch = "/hats/:name/prices".
HostMatch = ":subdomain.example.org".

如果这两个最终匹配,那么就会有两个绑定定义,分布是:subdomain 和:name ,每个包含被定义的数据段。例如,这个URL地址 http://test.example.org/hats/wild_cowboy_legendary/prices 会将 test绑定到subdomain ,并将wild_cowboy_legendary 绑定到 name 。他们通过cowboy_req:binding/{2,3} 函数检索出来的。绑定名字必须是原子(atom)类型。

还有一种特殊的绑定名字,它模仿erlang的下划线变量。任意内容都能与下划线(_)相匹配,但是数据会被丢弃。最有用的场景就是去匹配多个域名。

HostMatch = "ninenines.:_".

类似地,也可以添加可选内容。中括号内的内容都是可选的。

PathMatch = "/hats/[page/:number]".
HostMatch = "[www.]ninenines.eu".

并且可选内容可以内嵌

PathMatch = "/hats/[page/[:number]]".

还可以使用[…] 来获取主机名或路径剩余的部分。匹配主机的时候,需要放在最前面;匹配路径的时候是放在最后面。分别使用cowboy_req:host_info/1 和 cowboy_req:path_info/1 函数可以找到他们。

PathMatch = "/hats/[...]".
HostMatch = "[...]ninenines.eu".

如果一个绑定变量出现了两次,那么只有这两个位置的值相同的时候才会匹配成功。

PathMatch = "/hats/:name/:name".

在可选变量里面也是一样的,在下面这个例子中,如果可选变量有值,必须两个绑定变量的值都一样才可匹配到。

PathMatch = "/hats/:name/[:name]".

如果一个绑定变量出现在主机名和路径当中,他们需要是相同的才能匹配。

PathMatch = "/:user/[...]".
HostMatch = ":user.github.com".

当然也有两种特殊情况,第一种使用下划线变量(_)可以匹配任意的主机名和路径。

PathMatch = '_'.
HostMatch = '_'.

第二种,使用通配符星号(*)来匹配。

HostMatch = "*".

Constraints(约束)

关于这段没看懂,下面是英文原文:

After the matching has completed, the resulting bindings can be tested against a set of constraints. Constraints are only tested when the binding is defined. They run in the order you defined them. The match will succeed only if they all succeed.

They are always given as a two or three elements tuple, where the first element is the name of the binding, the second element is the constraint’s name, and the optional third element is the constraint’s arguments.

The following constraints are currently defined:

  • {Name, int}
  • {Name, function, fun ((Value) -> true | {true, NewValue} | false)}

The int constraint will check if the binding is a binary string representing an integer, and if it is, will convert the value to integer.

The function constraint will pass the binding value to a user specified function that receives the binary value as its only argument and must return whether it fulfills the constraint, optionally modifying the value. The value thus returned can be of any type.

Note that constraint functions SHOULD be pure and MUST NOT crash.

Compilation(编译/收集)

在传递给Cowboy之前,定义的结构首先要先编译。才能是Cowboy有效查找到正确的handler,并执行,而不必重复地解析路由。
编译通过调用cow_router:compile/1 函数进行。

Dispatch = cowboy_router:compile([
    %% {HostMatch, list({PathMatch, Handler, Opts})}
    {'_', [{'_', my_handler, []}]}
]),
%% Name, NbAcceptors, TransOpts, ProtoOpts
cowboy:start_http(my_http_listener, 100,
    [{port, 8080}],
    [{env, [{dispatch, Dispatch}]}]
).

注意,如果结构不正确,函数会返回{error, badarg}。

Live update(热更新)

使用cowboy:set_env/3 函数可以更新当前的路由列表。这会应用到所有的监听器中。

cowboy:set_env(my_http_listener, dispatch,
    cowboy_router:compile(Dispatch)).

注意,设置之前还是需要编译的哦。

0. 学习的一些疑问

  • 如何热更新镜像(images)?(你可以快速启动或者销毁容器。这种时间几乎是实时的)
  • 如何热更新游戏服?
  • 好处在于各个应用之间环境相互独立,即使某一个容器崩溃也不会影响到其它容器;
  • 每个容器使用端口如何维护?(方法1写在Dockerfile里面,不灵活;方法2在run的时候-p指定);
  • 那这样的话,会存在好多linux用户,相当于每一个容器就要维护一个物理机(虚拟);
  • 需要一套工具来管理维护镜像、容器的操作和状态;
  • 目前主流使用docker都是应用到哪些场景中?

1. docker的二个软件

  • Docker: 开源的容器虚拟化平台;
  • Docker Hub: Software-as-a-Service平台,用来共享和管理docker容器。

2. docker的三大模块

  • Docker images.(镜像)
  • Docker registries.(仓库)
  • Docker container.(容器)

3. 常用命令

3.1. 常用镜像命令

  • docker image(查看镜像信息)
  • docker build(创建镜像)
    • Dockerfile
      • ‘#注释’
      • FROM 基于哪个镜像为基础
      • MAINTAINER 维护者信息
      • RUN 运行指令
      • ADD 复制本地文件到镜像
      • EXPOSE 设置开放端口
      • CMD 容器启动后允许的程序
      • WORKDIR 切换工作目录
    • -t 添加tag
    • build后面需要接路径

3.2. 少用镜像命令

  • docker pull(获取镜像)
  • docker push(上传镜像)
  • docker search(搜索镜像)
    • -s N 只搜索指定星级以上的镜像
  • docker rmi(删除镜像)
  • docker tag [id] [new name:tag] (修改tag)
  • docker save(保存镜像)
  • docker load(加载镜像)
    • docker load –input xxx.tar
    • docker load < xxx.tar
    • load与import的区别,镜像是完整的与快照是丢弃历史记录和元数据信息的
  • docker rmi $(docker images -q -f “dangling=true”)(清理所有未打过标签的本地镜像)

3.3. 常用容器命令

  • docker run([下载镜像并]启动容器)
    • -t 分配一个伪终端
    • -i 打开标准输入
    • -d 后台运行
    • -v 创建并挂载数据卷(可有多个)
    • –volumes-from 挂载数据卷(可有多个)
    • -p 指定映射端口 (ip:Port:containerPort/udp|ip::containerPort|port:containerPort)
    • -P 随机映射端口
    • –name 自定义容器名字
    • –rm 终止后立即删除容器
    • –link : 容器互联
  • docker start(启动已终止容器)
  • docker stop(终止容器)
  • nsenter(进入容器)(推荐)
1
2
PID=$(docker inspect --format "{{ .State.Pid }}" <container ID>)
nsenter --target $PID --mount --uts --ipc --net --pid

3.4. 少用容器命令

  • docker commit(提交容器)
    • -m –massage=”” 提交信息
    • -a –author=”” 作者信息
    • -p –pause=true 提交时暂停容器运行
  • docker attach(进入容器)
  • docker ps(查看正在运行的容器)
    • -a 查看已终止
  • docker logs [container ID or NAMES] 查看(后台)运行日志
  • docker export(导出容器为文件)
    • docker export > xxx.tar
  • docker import(文件快照导入镜像)
    • cat xxx.tar | docker import - test/name:v1.0
    • docker import http://xxx.tgz test/name
  • docker rm(删除容器)
    • 默认不会删除运行中的容器
    • docker rm $(docker ps -a -q) 清理所有处于终止状态的容器
    • -v 同时删除数据卷

4. 安装

4.1. 在CentOS7中安装

curl -sSL https://get.docker.com/ | sh        //下载官服脚本按照
chkconfig docker on                           //设置开机自动启动

4.2. 在CentOS6中安装

4.2.1. 添加yum软件源

tee /etc/yum.repo.d/docker.repo << 'EOF'
[dockerrepo]
name=Docker Repository
baseurl=https://yum.dockerproject.org/repo/main/centos/$releasever/
enabled=1
gpgcheck=1
gpgkey=https://yum.dockerproject.org/gpg
EOF

4.2.2. 安装docker

yum update
yum install -y docker-engine

4.2.3. No module named yum

如果在执行yum update的时候出现了No module named yum错误,可能是存在与yum不对应的python版本引起。可以通过修改yum和yum-updatest的执行脚本(/usr/bin/yum/usr/bin/yum-updatest)的注释来指定python版本。譬如:

#!/usr/bin/python
修改为
#!/usr/bin/python2.6

5. 基础环境

可以下载bashrc_docker文件,加载到环境.bashrc中,其可以提供一些方便的命令用于做一些比较复杂的过程。

.bashrc_docker(https://raw.githubusercontent.com/yeasy/docker_practice/master/_local/.bashrc_docker) 定义了以下命令
- docker-pid(获取容器pid)
- docker-enter(进入容器)

下载和加载到linux环境中:

wget -P ~ https://raw.githubusercontent.com/yeasy/docker_practice/master/_local/.bashrc_docker
echo "[ -f ~/.bashrc_docker ] && . ~/.bashrc_docker" >> ~/.bashrc;source ~/.bashrc

6. 仓库

6.1. 私有仓库

官服提供了一个docker-registry镜像来供私有仓库的搭建。

docker run -d -p 80:5000 registry

vi /etc/docker/daemon.json
{"insecure-registries":["myregistry.example.com:5000"]}

cul http://x.x.x.x:2010/v2/linerl/tags/list

API文档:https://github.com/docker/distribution/blob/master/docs/spec/api.md

7. 学习后的一些结论

  • 本身是虚拟机技术实现的服务器大多数带有随时可扩展升级的性质,没有资源分配的需求,没有必要用到docker;
  • docker适合在做负载均衡的短链接的web服务上面,应用场景都是以镜像、容器为操作单位的最佳;
  • 如果有业务可以做到镜像、容器来维护就可以的,说明这个业务就很合适使用docker。

一、背景

我们项目开发人员写的文档都是markdown文件。对于其它组的同学要进行阅读不是很方便。每次编辑完markdown文件,我都是用软件将md文件转成html文件。刚开始转的时候,还没啥,转得次数多了,就觉得不能继续这样下去了。作为一名开发人员,还是让机器去做这些琐碎的事情吧。故写了两个脚本将md文件转成html文件,并将其放置在web服务器下,方便其他人员阅读。

主要有两个脚本和一个定时任务:

  • 一个python脚本,主要将md文件转成html文件;
  • 一个shell脚本,主要用于管理逻辑;
  • 一个linux定时任务,主要是定时执行shell脚本。

二、用python将markdown转成html

2.1 python依赖库

使用python的markdown库来转换md文件到html依赖两个库:

  • pip install markdown
  • pip install importlib

2.2 核心代码

核心代码其实只有一句,执行 markdown.markdown(text)就可以获得生成的html的原文。

1
2
3
input_file = codecs.open(in_file, mode="r", encoding="utf-8")
text = input_file.read()
html = markdown.markdown(text)

2.3 html编码和html样式

直接markdown.markdown(text)生成的html文本,非常粗略,只是单纯的html内容。而且在浏览器内查看的时候中文乱码(在chrome中),没有好看的css样式,太丑了。

乱码无样式

解决办法也很简单,在保存文件的时候,将<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />和css样式添加上。就这么简单解决了。

带css样式

2.4 完整python内容

  • 读取md文件;
  • 将md文件转成html文本;
  • 添加css样式和保存html文本。

python代码内容:

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 使用方法 python markdown_convert.py filename

import sys
import markdown
import codecs


css = '''
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css">
<!-- 此处省略掉markdown的css样式,因为太长了 -->
</style>
'''

def main(argv):
name = argv[0]
in_file = '%s.md' % (name)
out_file = '%s.html' % (name)

input_file = codecs.open(in_file, mode="r", encoding="utf-8")
text = input_file.read()
html = markdown.markdown(text)

output_file = codecs.open(out_file, "w",encoding="utf-8",errors="xmlcharrefreplace")
output_file.write(css+html)

if __name__ == "__main__":
main(sys.argv[1:])

三、shell逻辑

3.1 逻辑说明

建立一个shell文件,用于进行逻辑处理,主要操作如下:

  • 更新svn文件,将最新的md文件更新下来(此处假设md文件是测试文档.md);
  • 执行python markdown_convert.py $NAME将md文件转成html文件(生成测试文档.html);
  • 将转好的html迁移到web路径下(移动到html/测试文档.html);
  • 启动一个web服务(此处用的是python的SimpleHTTPServer的web服务器).

3.2 完整shell逻辑

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
#!/bin/bash

NAME='测试文档'

## 更新代码
svn update

## 删除html文件
if [ -f "$NAME.html" ];then
rm "$NAME.html"
fi

## 生成html
if [ -f "$NAME.md" ];then
python markdown_convert.py $NAME
fi

## 生成html目录
if [ ! -d "html" ];then
mkdir "html"
fi

## 拷贝html文件
if [ -f "$NAME.html" ];then
mv -f "$NAME.html" "html/"
fi

## 开启web服务器
PID=`ps aux | grep 'python -m SimpleHTTPServer 8080' | grep -v 'grep' | awk '{print $2}'`

if [ "$PID" = "" ];then
cd html
nohup python -m SimpleHTTPServer 8080 &
echo 'start web server'
else
echo 'already start'
fi

四、linux定时任务

在shell命令下输入crontab -e进入linux定时任务编辑界面。在里面设置markdown2web.sh脚本的定时任务:

1
2
## 更新文档
*/10 * * * * cd /home/xxx/doc; sh markdown2web.sh > /dev/null 2>&1

设置每10分钟执行一次markdown2web.sh脚本,当然也可以根据需求修改频率。

事故背景

由于误操作在erlcron设置了一个超过3个月后的定时任务。然后第二天之后发现每天的daily reset没有被执行,一些定时任务也没有被执行。瞬间感觉整个人都不好了,怎么无端端就不执行了呢。

通过排查日志,发现了以下报错:

2016-03-22 16:54:32.014 [error] gen_server ecrn_control terminated with reason: no case clause matching {ok,[<0.14123.1577>,<0.13079.1576>,<0.25254.1569>,<0.13402.1577>,...]} in ecrn_control:internal_cancel/1 line 111
2016-03-22 16:54:32.015 [error] CRASH REPORT Process ecrn_control with 0 neighbours exited with reason: no case clause matching {ok,[<0.14123.1577>,<0.13079.1576>,<0.25254.1569>,<0.13402.1577>,...]} in ecrn_control:internal_cancel/1 line 111 in gen_server:terminate/6 line 744

我擦,ecrn_control都崩了,怎么回事。

找到具体出错的代码:

internal_cancel(AlarmRef) ->
    case ecrn_reg:get(AlarmRef) of
        undefined ->
            undefined;
        {ok, [Pid]} ->
            ecrn_agent:cancel(Pid)
    end.

发现调用ecrn_reg:get(AlarmRef)被返回了{ok, List},而且这个List的数据远不止一个。明显在设置那个超过3个月的定时任务的时候,ecrn_reg被注册进了脏数据。

事故重现

先设置几个正常的定时任务

> erlcron:cron({{once, 1000}, {io, fwrite, ["Hello, world!~n"]}}).
> erlcron:cron({{once, 1000}, {io, fwrite, ["Hello, world!~n"]}}).
> erlcron:cron({{once, 1000}, {io, fwrite, ["Hello, world!~n"]}}).

查看observer:start() 可以看到进程树如下:

再设置一个4294968秒之后的定时任务

> erlcron:cron({{once, 4294968}, {io, fwrite, ["Hello, world!~n"]}}).

结果就gg了,好多崩溃信息是不是:

22:49:16.818 [error] CRASH REPORT Process <0.5822.64> with 0 neighbours crashed with reason: timeout_value in gen_server:loop/6 line 358
22:49:16.818 [error] Supervisor ecrn_cron_sup had child ecrn_agent started with ecrn_agent:start_link(#Ref<0.0.11.11209>, {{once,4294968},{io,fwrite,["Hello, world!~n"]}}) at <0.5822.64> exit with reason timeout_value in context child_terminated
22:49:16.819 [error] CRASH REPORT Process <0.5701.64> with 0 neighbours crashed with reason: timeout_value in gen_server:loop/6 line 358
22:49:16.821 [error] Supervisor ecrn_cron_sup had child ecrn_agent started with ecrn_agent:start_link(#Ref<0.0.11.11209>, {{once,4294968},{io,fwrite,["Hello, world!~n"]}}) at <0.5701.64> exit with reason timeout_value in context child_terminated
22:49:16.821 [error] CRASH REPORT Process <0.6237.64> with 0 neighbours crashed with reason: timeout_value in gen_server:loop/6 line 358
22:49:16.821 [error] Supervisor ecrn_cron_sup had child ecrn_agent started with ecrn_agent:start_link(#Ref<0.0.11.11209>, {{once,4294968},{io,fwrite,["Hello, world!~n"]}}) at <0.6237.64> exit with reason timeout_value in context child_terminated
22:49:16.821 [error] CRASH REPORT Process <0.5862.64> with 0 neighbours crashed with reason: timeout_value in gen_server:loop/6 line 358
22:49:16.821 [error] Supervisor ecrn_cron_sup had child ecrn_agent started with ecrn_agent:start_link(#Ref<0.0.11.11209>, {{once,4294968},{io,fwrite,["Hello, world!~n"]}}) at <0.5862.64> exit with reason timeout_value in context child_terminated

...(总共有25条)

再看一下进程数:

我擦,为毛原来的 scrn_agent 进程也没有了。

可以发现,erlcron 在尝试了25次设置 这个定时任务之后,也就是 scrn_agent 崩溃了25次之后,原来设置的三个正常的定时任务的scrn_agent 进程也没有掉了。
也就是说,不但我新设置的定时任务没有成功,而且我原来正常的定时任务也没有掉了。

再看一下崩溃日志里面的崩掉的进程号,每一个都是不一样的。可以推算其实原来的报错ecrn_reg:get(AlarmRef)获取到了多个Pid,其实就是这里插入失败的定时任务产生的25个Pid。也就是说,虽然ecrn_agent进程崩溃了,但是ecrn_reg还是保存了这些Pid。所以在取消这些定时任务的时候,ecrn_reg:get(AlarmRef)返回的内容在internal_cancel(AlarmRef)没有被匹配到。

为什么是4294968,其实是2^32

为什么设置了4294968秒后的定时任务就崩溃了。这个数估计很多人很熟悉,2^32=4294967296,而4294968000也就是刚好大于2^32。即,如果设置的定时任务超过了2^32毫秒,在erlcron里面就不支持了。

查看gen_server:loop的源码,找到引起崩溃的代码:

loop(Parent, Name, State, Mod, hibernate, Debug) ->
    proc_lib:hibernate(?MODULE,wake_hib,[Parent, Name, State, Mod, Debug]);
loop(Parent, Name, State, Mod, Time, Debug) ->
    Msg = receive
          Input ->
            Input
      after Time ->
          timeout
      end,
    decode_msg(Msg, Parent, Name, State, Mod, Time, Debug, false).

可以发现引起崩溃的,358行是一段receive代码。也就是说receive是不支持超过2^32大小的。

自测了一下,的确如果receiveafter后面如果是大于等于2^32的数值就会出现bad receive timeout value的报错。查看官方解释,已经明确说明不能大于32位大小。

ExprT is to evaluate to an integer. The highest allowed value is 16#FFFFFFFF, that is, the value must fit in 32 bits. receive..after works exactly as receive, except that if no matching message has arrived within ExprT milliseconds, then BodyT is evaluated instead. The return value of BodyT then becomes the return value of the receive..after expression.

引用自:http://erlang.org/doc/reference_manual/expressions.html

再回到erlcron, 在 ecrn_agent:start_link的时候,ecrn_agent:init执行完ecrn_reg:register(JobRef, self())返回{ok, NewState, Millis}gen_server之后,Millis如果超过2^32gen_server:loop就会引起gen_servertimeout_value异常退出。

%% @private
init([JobRef, Job]) ->
    State = #state{job=Job,
                   alarm_ref=JobRef},
    {DateTime, Actual} = ecrn_control:datetime(),
    NewState = set_internal_time(State, DateTime, Actual),
    case until_next_milliseconds(NewState, Job) of
        {ok, Millis} when is_integer(Millis) ->
            ecrn_reg:register(JobRef, self()),
            {ok, NewState, Millis};
        {error, _}  ->
            {stop, normal}
    end.

最后

这坑踩的,有点郁闷。其实这跟erlcron也没关系,也不是gen_server的问题。而是erlang自身receive不支持2^32引起的。继续往下查其实可以发现,再往下是其它语言写的了。

-module(prim_eval).

%% This module is simply a stub which abstract code gets included in the result
%% of compilation of prim_eval.S, to keep Dialyzer happy.

-export(['receive'/2]).

-spec 'receive'(fun((term()) -> nomatch | T), timeout()) -> T.
'receive'(_, _) ->
    erlang:nif_error(stub).

与君共勉

背景

由于敏捷开发,快速迭代,我们项目一天会有三个版本,也就意味着我一天要去获取三次软件包。我负责服务端开发,所以我经常需要去拿最新的客户端。我们的客户端放置在一个公共的ftp上面。每天频繁登陆ftp下载,或者使用ftp工具,每次都要点击同步,都不太方便。如果在linux下就好了,然而在windows也是可以运行脚本的,何不尝试下呢。

完整代码

@echo off
rem for download file
rem ftp config 
rem ip login_name password remote_dir

set "ftp_ip=192.168.0.1"
set "ftp_user=admin"
set "ftp_pass=123456"
set "ftp_path=/"

set "f_tmp=tmp"
set "f_info=tmp\tmp_info.dat"
set "f_list=tmp\tmp_list.dat"

rd /s /q pack
mkdir %f_tmp%

echo open %ftp_ip% > %f_info%
echo user %ftp_user% >> %f_info%
echo %ftp_pass%>> %f_info%
echo prompt >> %f_info%
echo binary >> %f_info%
echo cd %ftp_path% >> %f_info%
echo ls . %f_list% >> %f_info%
echo lcd %f_tmp% >> %f_info%
echo disconnect >> %f_info%
echo bye >> %f_info%

ftp -v -n -s:%f_info%


for /f "delims=" %%i in ('type "%f_list%"') do (
    set "target_7z=%%i"
)

echo open %ftp_ip% > %f_info%
echo user %ftp_user% >> %f_info%
echo %ftp_pass%>> %f_info%
echo prompt >> %f_info%
echo binary >> %f_info%
echo cd %ftp_path% >> %f_info%
echo lcd %f_tmp% >> %f_info%
echo get %target_7z%>> %f_info%
echo disconnect >> %f_info%
echo bye >> %f_info%

ftp -v -n -s:%f_info%

call tools\7z\x64\7za.exe x %f_tmp%\%target_7z%

rd /s /q %f_tmp%

exit

运行脚本

逐步解释

获取文件列表

echo open %ftp_ip% > %f_info%
echo user %ftp_user% >> %f_info%
echo %ftp_pass%>> %f_info%
echo prompt >> %f_info%
echo binary >> %f_info%
echo cd %ftp_path% >> %f_info%
echo ls . %f_list% >> %f_info%
echo lcd %f_tmp% >> %f_info%
echo disconnect >> %f_info%
echo bye >> %f_info%

ftp -v -n -s:%f_info%

这部分代码主要有以下几个作用:

  1. 将ftp的命令写入到文件;
  2. 在ftp上获取对应目录的文件列表,并写到本地文件下。

获取最新的一个文件

for /f "delims=" %%i in ('type "%f_list%"') do (
    set "target_7z=%%i"
)

然后循环遍历文件列表,最终获取到最后一个列表(也就是最新的文件名)。

下载最新文件

echo open %ftp_ip% > %f_info%
echo user %ftp_user% >> %f_info%
echo %ftp_pass%>> %f_info%
echo prompt >> %f_info%
echo binary >> %f_info%
echo cd %ftp_path% >> %f_info%
echo lcd %f_tmp% >> %f_info%
echo get %target_7z%>> %f_info%
echo disconnect >> %f_info%
echo bye >> %f_info%

ftp -v -n -s:%f_info%

有了文件名,我们就可以再执行一次ftp命令,下载我们最新的文件了。以上就实现了动态下载最新文件了。

解压

这边我们使用的软件包是7z打包的。所以也要下载7z解压工具。
官方地址:http://www.7-zip.org/
然后下载到命令行版,放置到任意可读取目录就可以了。

call tools\7z\x64\7za.exe x %f_tmp%\%target_7z%

后话

平常习惯了在linux下倒腾。可以写些脚本做些繁琐的事情,但是在windows经常就傻眼了。可视化的东西是有很多好处,但是也有些弊端。批处理脚本虽然不好用,但也并不是不可用。很多时候也可以带来很大的方便。当然会python、ruby这些脚本语言其实也是完全可以满足的。毕竟现在这年头批处理这种东西用的越来越少了。windows shell也可以,但是感觉也不太好用。

参考资料

树莓派

背景

一直想捣鼓点什么东西。当看到树莓派的时候,就是它了。

树莓派可以安装Linux系统,而我在工作当中,可以说Linux是一半工作环境。树莓派真是个好东西,这个东西应该在我学习linxu/Unix的时候就该接触了。想想大学的时候,在windows下安装虚拟机,安装Linux是件多么痛苦的事情。而且那时的电脑配置也不算高,才2G内存,还要开虚拟机。玩个蛋蛋。

Linux也算比较熟吧,入手一个树莓派应该可以玩很多好玩的事情。

购买硬件

直接在某宝搜索入手。必须内容:

  • 树莓派一个(Raspberry Pi 2
  • 小usb口电源(5V2A的充电器随便找一个)
  • 4G或者更大存储空间的SD卡一张(树莓派本身不带存储空间)

以下非必需:

  • 散热器三片(风扇什么的觉得也太夸张了)
  • 无线网卡(本身有网卡入口,所以不是必须的)
  • SD卡读卡器(安装系统的时候会用到)

树莓派连接外置硬件

安装系统

树莓派得到了各种Linux发行版本的支持,甚至微软在自己的windows 10上也发行了一个支持树莓派的版本。最常见的,还是在树莓派上面安装RASPBIANUbuntuRASPBIAN是树莓派官方出品基于DebianLinux系统。也有喜欢在树莓派上面玩windows 10的。相关的系统官方都有提供下载。(https://www.raspberrypi.org/downloads/)。

我本人安装的是官方提供的RASPBIAN系统,基于Debian实现。可以说对Debian比较了解,所以RASPBIAN对我来说是一个比较好的选择。以安装RASPBIAN为例,有多种安装方式。

树莓派官方推荐的是使用其官方工具NOOBS安装工具。

  1. 下载NOOBS工具(https://www.raspberrypi.org/downloads/noobs/);
  2. 下载SD卡格式化工具(https://www.sdcard.org/downloads/formatter_4/eula_windows/);
    1. 安装SD卡格式工具;
    2. 在选项Option里面设置“FORMAT SIZE ADJUSTMENT”为开启ON状态;
    3. 检查SD卡是否插入电脑;
    4. 点击格式化工具的【格式化(Format)】按钮格式化SD卡。
  3. 解压NOOBS.zip文件;
  4. 将解压的文件复制到SD卡上面;
  5. 将SD卡插入到树莓派里面;
  6. 接上鼠标、键盘、显示器(这一部非必需);
  7. 接上网线(无线网卡也可以)、电源,然后就自动开机启动了。

开机启动后,树莓派会自行安装系统,看sd卡的写的速度时间会不一样,10~60分钟估计就好了。然后就会进入了树莓派的系统界面。至此,算是大功告成了。
安装系统

关于系统

树莓派官方系统RASPBIAN是基于Debian修改而来的。所以熟悉DebianUbuntu的话,对RASPBIAN是完全没有任何入门门槛的。RASPBIAN使用的是树莓派自己的镜像。其服务器在国外,访问起来可能有速度慢的情况,建议修改成网易的Debian镜像(http://mirrors.163.com/.help/debian.html)。
编辑/etc/apt/sources.list文件, 在文件最前面添加以下条目(操作前请做好相应备份)

deb http://mirrors.163.com/debian/ wheezy main non-free contrib
deb http://mirrors.163.com/debian/ wheezy-updates main non-free contrib
deb http://mirrors.163.com/debian/ wheezy-backports main non-free contrib
deb-src http://mirrors.163.com/debian/ wheezy main non-free contrib
deb-src http://mirrors.163.com/debian/ wheezy-updates main non-free contrib
deb-src http://mirrors.163.com/debian/ wheezy-backports main non-free contrib
deb http://mirrors.163.com/debian-security/ wheezy/updates main non-free contrib
deb-src http://mirrors.163.com/debian-security/ wheezy/updates main non-free contrib

执行sudo apt-get update更新软件包列表。详细可以查看网易Debian镜像的使用帮助(http://mirrors.163.com/.help/debian.html)。

结束语

树莓派最大的优势在于便宜,而且资料方面也算比较充足。个人觉得最大的价值还是拿来学习Linux的知识。独立的Linux机器,比起虚拟机,给人带来的学习积极性和成就感感觉是完全不一样的。当然,在可玩性方面,树莓派也可以做很多有趣的事情。倒腾飞行器、遥控玩具车、控制家庭电器、控制门禁、制作超级电脑等等。最主要的还是要有兴趣。而我,是想让树莓派来实现一些没有必要使用PC、需要长时间、或者定期任务的执行。

参考资料

背景

工作室也经历过好几个游戏了。服务端的架构跟实际业务需求出现过不少的冲突。导致后来花了挺多时间去擦屁股的。以最近的一个游戏举例,原本的世界观设想是一个大服的世界观。也就是只有一个服,撑下百万用户,数万同时在线的设计。而后随着业务变化和线上表现,原本大服的设计并不能满足,最终变成了滚服玩法。由于大服变滚服,在原来的服务器架构约束下,对于后续增加的跨服玩法和合服实现都带来了比较大的麻烦和不少的工作量。

物理分服和逻辑分服

物理分服

原来的架构是按照大服设计的,所以在数据库上面的设计一个服对应一个数据库。假设我们滚了500个服,就需要建500个数据库,部署500个游戏服。无论后续跨服、合服的业务扩展,还是运维的维护方面,都变得比较复杂和困难。特别是合服的需求上面,需要将两个数据库甚至多个数据库合并成一个数据库。在量上来的时候,这一切都变得无比繁琐和复杂。开发人员也需要花费较多的人力和时间去写相应的工具。而且操作相对复杂,也比较容易出bug。而且后续新增的业务如果出现了持久化数据就需要增加相应的合服处理。

逻辑分服

如果说我们一开始就已经将数据库合并了呢,是不是后续根本就不需要去合并数据库了。所以如果在当初框架设计的时候就已经按照逻辑来分服的话,后续的事情处理起来就简单多了。问过同行业的一些游戏架构,他们也是这么处理的。

对于合服

因为数据其实还是在同一个库里面,而且也是在同一个服务器里面。只要简单处理,或者甚至不需要任何处理,就可以将两个或多个服合并。只需要在后台设置一下入口配置、可见配置就可以解决合服的问题了。

对于跨服

跨服原本的问题就是需要从不同库读取数据和与不同服进行交互。如果本身就不存在多服的问题,也不存在跨服的问题。

虽然逻辑分服可以比较完美解决合服的问题,但是对于跨服还是需要单独处理。毕竟如果一个逻辑分服的服务器真的扛不住的时候,就会出现真的物理分服。对于跨服的需求来说,可能都是需要跨的。

维护成本

相对于物理分服,逻辑分服可以极大地降低运维成本。数据库数量级可以极大减少,服务器数量也可以减少。对于备份、更新等运维操作都相对变得简单。甚至可以不依赖于运维工具,就可以简单地维护机器了。一台机器部署一个服(多个逻辑服)对比一台机器部署多个游戏服(一个逻辑服),需要初始化的内存一般来说会变小(不排除不一样的情况),机器的资源占用一般来说会小很多。所以对物理机的利用效率可以提高很多。

用户数量级的问题

逻辑分服必然会出现性能瓶颈,不可避免地出现了物理分服、分库的情况。而对于合服来说,合服本身就是发生在用户数量或者同时在线数量不足的情况下出现的。如果用户数量过大,基本上不太可能出现合服的需求。如果前期量级大,已经物理分服了。后期量级小了,其实重新叠回去也不是什么大的问题。只需要跟运营沟通好了,还是可以使用逻辑分服的事情去解决合服的事情。当然如果运营需要真的在不同物理服上面进行合服,我也没有想到比较好的办法,只能又苦逼地去处理的样子。

开发成本

由于逻辑分服,的确是增加了一些内容,譬如玩家所在的服务器ID。但是这个处理起来并没有多大的难度,而且对key值也并没有多大的影响。

逻辑分服的架构对于大世界和滚服都是支持的,只是对于大世界的话,就浪费了一个存储空间和一点点内存。但是这样的框架可以自如应对大世界到滚服之间的变化。如果一开始就按照大世界来设计,万一某一天滚服了,就要麻烦地多。

所以逻辑分服并不会提升多大的开发成本。