|
9.3 UpdateServer实现机制3 A0 @7 A9 I. E/ x2 L5 S, L
UpdateServer用于存储增量数据,它是一个单机存储系统,由如下几个部分组
. h& A) J, d7 ]5 r8 i成:
/ ~1 l* b* F) |- o5 m; y●内存存储引擎,在内存中存储修改增量,支持冻结以及转储操作;* l2 W$ g% U `: ?2 \8 P
●任务处理模型,包括网络框架、任务队列、工作线程等,针对小数据包做了专% B; ?0 a D2 [; X9 ^# U! E4 D0 s
门的优化;1 K0 B, X, l. E4 c4 M( u
●主备同步模块,将更新事务以操作日志的形式同步到备UpdateServer。
$ Q, I9 Y4 Z: t4 z" j5 AUpdateServer是OceanBase性能瓶颈点,核心是高效,实现时对锁(例如,无锁/ I6 o: h9 H4 G* f
数据结构)、索引结构、内存占用、任务处理模型以及主备同步都需要做专门的优
0 b) N' M1 h* b; `" `化。
* b8 u$ D; H8 @( B# s, @9.3.1 存储引擎
/ P9 n9 o) z: p6 bUpdateServer存储引擎如图9-3所示。/ x% `, f3 x8 Q
图 9-3 UpdateServer存储引擎
# \8 ~2 U& k( E+ M3 PUpdateServer存储引擎与6.1节中提到的Bigtable存储引擎看起来很相似,不同点
- e5 X( x! Y# G4 v5 L4 s0 ^& P在于:
; [' |; v, }: x4 Z4 n3 s●UpdateServer只存储了增量修改数据,基线数据以SSTable的形式存储在5 U( d3 L. e1 ^8 ?2 h0 D2 W( o! q; h
ChunkServer上,而Bigtable存储引擎同时包含某个子表的基线数据和增量数据;# X* H& a( w' K" v3 {
●UpdateServer内部所有表格共用MemTable以及SSTable,而Bigtable中每个子表
1 ^- z5 W1 v" Y# P$ Q h的MemTable和SSTable分开存放;. v" z+ @; U0 ~% y! p- `' U `* L
●UpdateServer的SSTable存储在SSD磁盘中,而Bigtable的SSTable存储在 GFS
/ u. F: r$ A1 N中。
^" I6 v; v; ^* S! O) g2 IUpdateServer存储引擎包含几个部分:操作日志、MemTable以及SSTable。更新
9 O3 j' r! R, A操作首先记录到操作日志中,接着更新内存中活跃的MemTable(Active
" {) w3 ~* L4 Y, CMemTable),活跃的MemTable到达一定大小后将被冻结,称为Frozen MemTable,同
% E- ^. A0 g5 u- S时创建新的Active MemTable。Frozen MemTable将以SSTable文件的形式转储到SSD磁+ E% O, q( O5 H. B' e
盘中。: x4 Z2 W. y2 ?/ h+ J
1.操作日志
% a# N6 V% S5 t; p! g! p7 hOceanBase中有一个专门的提交线程负责确定多个写事务的顺序(即事务id),- r( H2 R+ D4 R9 A3 Q+ |( j+ D
将这些写事务的操作追加到日志缓冲区,并将日志缓冲区的内容写入日志文件。为7 R* G7 z* F9 f m3 w' E
了防止写操作日志污染操作系统的缓存,写操作日志文件采用Direct IO的方式实现:
( F- k( o- y4 ^2 Vclass ObLogWriter9 o1 a1 ?& x; Q, k" o2 t: F
{
& U- O# N& U; l1 Q/ gpublic:8 R9 k; m [& A! v9 I
//write_log函数将操作日志存入日志缓冲区
7 i! V0 f! Q1 _8 C cint write_log(const LogCommand cmd,const char*log_data,const int64_t
0 h9 O; g8 t s0 g& }6 P& [data_len);& @# u7 J2 v: o. \ p( Z/ J, n
//将日志缓冲区中的日志先同步到备机再写入主机磁盘
7 H* O$ w& l% j4 Kint flush_log(LogBuffer&tlog_buffer,const bool sync_to_slave=true,const bool7 I0 s; P1 A3 U# R
is_master=true);. [/ K- j" \! v
};
# x" i3 Z: D5 e. @0 v每条日志项由四部分组成:日志头+日志序号+日志类型(LogCommand)+日志
) H# R9 Z' C( j1 U# a1 a内容,其中,日志头中记录了每条日志的校验和(checksum)。ObLogWriter中的
( Z" e; U: l" b2 u4 }1 I% Vwrite_log函数负责将操作日志拷贝到日志缓冲区中,如果日志缓冲区已满,则向调5 {3 l9 G6 {. m e
用者返回缓冲区不足(OB_BUF_NOT_ENOUGH)错误码。接着,调用者会通过
3 P9 E5 o! m3 Tflush_log将缓冲区中已有的日志内容同步到备机并写入主机磁盘。如果主机磁盘的最/ h! f' E1 J9 c3 M) `) C$ a% z
后一个日志文件超过指定大小(默认为64MB),还会调用switch_log函数切换日志 {7 |/ i8 x7 k
文件。为了提高写性能,UpdateServer实现了成组提交(Group Commit)技术,即首+ L) V/ _4 x" o |
先多次调用write_log函数将多个写操作的日志拷贝到相同的日志缓冲区,接着再调
2 K9 M z% S$ K" E) O用flush_log函数将日志缓冲区中的内容一次性写入到日志文件中。/ {. y P. P( O
2.MemTable
0 Q9 ~% O3 A7 o2 p- K# J3 p. X7 pMemTable底层是一个高性能内存B树。MemTable封装了B树,对外提供统一的读
X* b1 k6 u2 n" M+ c% \写接口。
* ^* x1 R8 g! m" b5 UB树中的每个叶子节点对应MemTable中的一行数据,key为行主键,value为行操/ Z- d, H: \. z! K& e" d6 I0 e7 |3 y( d
作链表的指针。每行的操作按照时间顺序构成一个行操作链表。0 g2 s7 t0 K' j1 A* O
如图9-4所示,MemTable的内存结构包含两部分:索引结构以及行操作链表,索
" D: K$ t% K q2 f( ^5 O引结构为9.1.2节中提到的B树,支持插入、删除、更新、随机读取以及范围查询操
( N5 h( x/ a9 [$ C3 b$ B作。行操作链表保存的是对某一行各个列(每个行和列确定一个单元,称为Cell)的5 R f5 X# r1 i: Q
修改操作。
) Q; ?4 k' L/ c8 B图 9-4 MemTable的内存结构
. ~9 z7 B; f5 | G* ?6 l: r# U; H例9-3 对主键为1的商品有3个修改操作,分别是:将商品购买人数修改为$ g! ~9 r; ?/ q1 E) W# Y3 z
100,删除该商品,将商品名称修改为“女鞋”,那么,该商品的行操作链中将保存三
8 f4 j8 w4 }* p ^个Cell,分别为:<update,购买人数,100>、<delete,*>以及<update,商品- g* |+ L; M3 }) V# Y
名,“女鞋”>。9 W9 {) t3 }# ~, q S3 c1 Z( _5 e
MemTable中存储的是对该商品的所有修改操作,而不是最终结果。另外,( q/ O$ O. r! P, |+ u
MemTable删除一行也只是往行操作链表的末尾加入一个逻辑删除标记,即<delete,3 U9 O+ g. e+ Z: \2 p9 r# v1 c
*>,而不是实际删除索引结构或者行操作链表中的行内容。
% `( L/ U4 |& N5 S2 ]" F' L1 SMemTable实现时做了很多优化,包括:0 \4 n' U- r% v" k
●哈希索引:针对主要操作为随机读取的应用,MemTable不仅支持B树索引,还! |/ V E" X% I
支持哈希索引,UpdateServer内部会保证两个索引之间的一致性。
! U- ]4 K3 F6 u# u$ ^& k●内存优化:行操作链表中每个cell操作都需要存储操作列的编号- Y( w6 w1 D& C \% V2 l
(column_id)、操作类型(更新操作还是删除操作)、操作值以及指向下一个cell操) |7 t9 W/ e5 I: _; X
作的指针,如果不做优化,内存膨胀会很大。为了减少内存占用,MemTable实现时% U( M* n. x" g" V
会对整数值进行变长编码,并将多个cell操作编码后序列到同一块缓冲区中,共用一
; F$ l2 }# G7 n2 c6 E: T个指向下一批cell操作缓冲区的指针:& G- f7 Q. I7 i3 k+ p! d- R5 w
struct ObCellMeta6 o. r# g$ t3 }% K
{/ W7 d j( c3 V
const static int64_t TP_INT8=1;//int8整数类型9 C. C$ A! z0 P, c9 J1 V
const static int64_t TP_INT16=2;//int16整数类型2 u) `0 }( ?3 t3 d
const static int64_t TP_INT32=3;//int32整数类型
/ n% T) |- T7 L! h* U; W/ E+ `const static int64_t TP_INT64=4;//int64整数类型
! C8 J: t# V$ C& S2 F; e( Tconst static int64_t TP_VARCHAR=6;//变长字符串类型" s$ j- ?, k6 x* _+ m9 t
const static int64_t TP_DOUBLE=13;//双精度浮点类型" s- X# f: |) b/ C# {' m% w
const static int64_t TP_ESCAPE=0x1f;//扩展类型
2 j- i1 y/ @% j6 e) F9 f: jconst static int64_t ES_DEL_ROW=1;//删除行操作/ p: s+ T) Y! Q/ q* g, @, {
};
# k5 J% h' Y7 v+ U) zclass ObCompactCellWriter
2 e _# d% S/ [- L& W{7 J) T$ B& d' s, c4 a
public:
$ B- _, L. a4 A$ z" q1 D# A8 T) B0 ]//写入更新操作,存储成压缩格式
/ ~4 b2 v9 `1 G1 B1 k' I0 N$ X* jint append(uint64_t column_id,const ObObj&value);
" U7 r0 L3 D8 Y/ c//写入删除操作,存储成压缩格式$ A5 q6 l' d) W2 @* g W
int row_delete();0 C7 x: D" f) L# L
};
' g5 t( s4 j+ D$ v W/ LMemTable通过ObCompactCellWriter来将cell操作序列化到内存缓冲区中,如果为, s! @3 C& d0 ^! w% g- i: w& b
更新操作,调用append函数;如果为删除操作,调用row_delete函数。更新操作的存7 M0 R4 ^4 v* B9 O6 F% f E% c! B
储格式为:数据类型+值+列ID,TP_INT8/TP_INT16/TP_INT32/TP_INT64分别表示8
" E8 g9 r3 n8 o位/16位/32位/64位整数类型,TP_VARCHAR表示变长字符串类型,TP_DOUBLE表示
7 W, n8 t9 D9 \( V; [/ x- J( @5 }( L u双精度浮点类型。删除操作为扩展操作,其存储格式为:
$ l3 p" u- J- t/ H6 V+ e& ]TP_ESCAPE+ES_DEL_ROW。例9-3中的三个Cell:<update,购买人数,100>、<
5 g6 h- F. T' A4 N% m' {! Edelete,*>以及<update,商品名,“女鞋”>在内存缓冲区的存储格式为:$ K B- p! t* q
第1~3字节表示第一个Cell,即<update,购买人数,100>;第4~5字节表示第
/ e) m m# O1 r3 Y& \' S9 x二个cell,即<delete,*>;第6~8字节表示第三个Cell,即<update,商品名,“女# G0 Y3 k+ a+ q7 v4 z
鞋”>。
* V5 @: J4 V ?& d) m4 i+ Z4 GMemTable的主要对外接口可以归结如下:
7 e1 ^" p3 j, ~9 ?& P! `" B- j//开启一个事务
, a, u6 ~9 x4 a% E$ t2 R//@param[in]trans_type事务类型,可能为读事务或者写事务5 N6 x5 @* H9 F" D6 K- ?$ u. x
//@param[out]td返回的事务描述符
2 V9 p8 u. Q" bint start_transaction(const TETransType trans_type,MemTableTransDescriptor&0 G4 }. m* b# X1 h, p- j
td);4 y m. | w5 d+ q5 x: |
//提交或者回滚一个事务: N: D* }! a8 J* t6 C
//@param[in]td事务描述符9 [$ ]$ G; l! i) p
//@param[in]rollback是否回滚,默认为false
8 V# \/ f% w* Y* k7 I! A9 eint end_transaction(const MemTableTransDescriptor td,bool rollback=false);
, d% k! l+ E8 @* ?9 s5 a% c. U//执行随机读取操作,返回一个迭代器
$ y6 I: w3 X* U" v: r# l//@param[in]td事务描述符
5 k$ b8 v# X! p; m; B! m1 z; Z9 z//@param[in]table_id表格编号1 [6 F3 {! t1 m7 r
//@param[in]row_key待查询的主键
0 {! K, G# g( T' V1 m5 ^//@param[out]iter返回的迭代器
" P, f% @. d1 S- F! l cint get(const MemTableTransDescriptor td,const uint64_t table_id,const0 o$ }9 P7 k3 i5 ~
ObRowkey&row_key,MemTableIterator&iter);
0 v1 Q4 Z j5 X3 i( f" \6 E8 O) }//执行范围查询操作,返回一个迭代器% J! r( P- |5 w# p }9 X" x) |/ d
//@param[in]td事务描述符* L" v) ]% ]/ r q
//@param[in]range查询范围,包括起始行、结束行,开区间或者闭区间( L4 e4 {: Z# g: c1 w
//@param[out]iter返回的迭代器
* s/ Y8 T( t+ x3 a; {& Y& Fint scan(const MemTableTransDescriptor td,const ObRange&! A, H1 z3 d1 W- _8 F- f
range,MemTableIterator&iter);! N3 V: R4 a; d5 ?' f3 T6 e) ^* \
//开始执行一次修改操作, D- }# f, l$ q
//@param[in]td事务描述符) t4 `8 ^! U5 }) n& |, j/ u9 M
int start_mutation(const MemTableTransDescriptor td);; q7 |, R% d4 O0 B( }- o! E/ E1 C# k
//提交或者回滚一次修改操作8 _& ?7 p6 [( S8 i
//@param[in]td事务描述符$ I# m# `% O$ w
//@param[in]rollback是否回滚6 z3 R1 O- q5 H1 A* S! N
int end_mutation(const MemTableTransDescriptor td,bool rollback);' P$ }% @8 O; [2 S: @/ U
//执行修改操作
. }. R: q/ G: @1 b//@param[in]td事务描述符
9 P4 l, x: c8 g# i* {, D$ x//@param[in]mutator修改操作,包含一个或者多个对多个表格的cell操作' ]& a4 U" j2 S; s6 l8 M
int set(const MemTableTransDescriptor td,ObUpsMutator&mutator);
9 d3 w" V. w$ C! ]对于读事务,操作步骤如下:6 C. m7 N3 Y& i1 P' _- i1 H
1)调用start_transaction开始一个读事务,获得事务描述符;* g7 _5 j6 N! H/ }7 h& e
2)执行随机读取或者扫描操作,返回一个迭代器;接着可以从迭代器不断迭代
' x) L# ~2 \8 r, G6 x: Z数据;" d6 ?2 J. U$ j
3)调用end_transaction提交或者回滚一个事务。
! @9 \! q0 B" nclass MemTableIterator$ C. e- Y4 H& @
{
9 T8 l1 W1 [# \# D% Z7 z: l) gpublic:
" L0 q# B# @1 G0 K% r//迭代器移动到下一个cell
9 {/ u: _0 Q `; D4 w! \int next_cell();9 f7 e, |3 ]+ U$ b
//获取当前cell的内容
0 y5 ?; `, [7 U( h9 Q6 y3 e//@param[out]cell_info当前cell的内容,包括表名(table_id),行主键(row_
5 Q, \* [# ^, _* k% |key),列编号(column_id)以及列值(column_value)( A6 E) ]0 z( x% \1 K
int get_cell(ObCellInfo**cell_info);
8 b' q! d( W( Q//获取当前cell的内容
$ h, F0 F- C2 e Z+ O//@param[out]cell_info当前cell的内容
* @0 o- S, x* J: z& t: ?//@param is_row_changed是否迭代到下一行1 Q" E. J6 C8 s) ~
int get_cell(ObCellInfo**cell_info,bool*is_row_changed);
; f: I. H, C x};8 B* t2 \- p* T' h3 q
读事务返回一个迭代器MemTableIterator,通过它可以不断地获取下一个读到的
0 U, Y. d2 U/ e+ t8 Vcell。在例9-3中,读取编号为1的商品可以得到一个迭代器,从这个迭代器中可以读* L) Y9 Y- o, F: b
出行操作链中保存的3个Cell,依次为:<update,购买人数,100>,<delete,*
0 |/ p+ ~- y9 Q9 c$ b O9 _>,<update,商品名,“女鞋”>。
7 Z4 O# n/ } y6 |0 D' I; f写事务总是批量执行,步骤如下:
; Y- Y4 |7 W5 y D1)调用start_transaction开始一批写事务,获得事务描述符;1 ]# @' P+ C- j" ?( _
2)调用start_mutation开始一次写操作;/ M8 I- I( q; C4 j4 |# P
3)执行写操作,将数据写入到MemTable中;& D/ J: D# u r# N- m) e7 {: W; n8 a0 N
4)调用end_mutation提交或者回滚一次写操作;如果还有写事务,转到步骤
4 e5 z9 W0 M+ h+ m2);# f* j' h9 ?' R+ F& b
5)调用end_transaction提交写事务。
& f4 P$ T+ ]/ U& f# c' Q1 K3.SSTable
]' ~# i8 t5 |2 I* \: g3 a当活跃的MemTable超过一定大小或者管理员主动发起冻结命令时,活跃的
$ ]5 ~* }" P L& gMemTable将被冻结,生成冻结的MemTable,并同时以SSTable的形式转储到SSD磁盘# K* G* d: P7 {8 F/ u0 S% s' T
中。- r' ~% B- }7 F- V7 H. M" X7 X
SSTable的详细格式请参考9.4节ChunkServer实现机制,与ChunkServer中的
* C/ d; @7 z$ K4 F1 K5 b8 tSSTable不同的是,UpdateServer中所有的表格共用一个SSTable,且SSTable为稀疏格
; O7 C1 g) t, {( N式,也就是说,每一行数据的每一列可能存在,也可能不存在修改操作。
$ _8 g* L. `. V- ~6 H( p) t B' w另外,OceanBase设计时也尽量避免读取UpdateServer中的SSTable,只要内存足1 w8 R4 i3 b6 x
够,冻结的MemTable会保留在内存中,系统会尽快将冻结的数据通过定期合并或者
2 h5 _3 J9 c2 ^# }/ n4 t数据分发的方式转移到ChunkServer中去,以后不再需要访问UpdateServer中的
( ~" B& |2 O: m8 p; F' s* lSSTable数据。. L5 Z- L, I O1 s# Y
当然,如果内存不够需要丢弃冻结MemTable,大量请求只能读取SSD磁盘,) H4 W! a% c% @( h9 K5 \) j' b! k
UpdateServer性能将大幅下降。因此,希望能够在丢弃冻结MemTable之前将SSTable; Y: j- a7 B7 F7 E, \9 t
的缓存预热。
2 U9 {2 _% f. O, ?7 c9 Y- f7 k) ]& {UpdateServer的缓存预热机制实现如下:在丢弃冻结MemTable之前的一段时间! h% J. X& q5 v3 L4 i
(比如10分钟),每隔一段时间(比如30秒),将一定比率(比如5%)的请求发给: C- o' B+ f6 n2 G. Q( ?/ j: E1 _1 R
SSTable,而不是冻结MemTable。这样,SSTable上的读请求将从5%到10%,再到
) f6 u2 m% M$ i* R# P( Z* A, V0 l0 x15%,依次类推,直到100%,很自然地实现了缓存预热。
0 X; c- o7 k% m, k& y5 S+ N1 M9.3.2 任务模型5 a8 N. }1 n9 o% E: ^
任务模型包括网络框架、任务队列、工作线程,UpdateServer最初的任务模型基# l+ x6 \4 ?9 b. Z, z: a3 Y' D
于淘宝网实现的Tbnet框架(已开源,见http://code.taobao.org/p/tb-common-2 s6 v! u. `: m, I8 |( s, ]$ B
utils/src/trunk/tbnet/)。Tbnet封装得很好,使用比较方便,每秒收包个数最多可以达
" M4 }7 h9 I" N8 f4 b% ?# Q9 e到接近10万,不过仍然无法完全发挥UpdateServer收发小数据包以及内存服务的特3 a. B1 b) o( J: M$ t- r1 S# n ]
点。OceanBase后来采用优化过的任务模型Libeasy,小数据包处理能力得到进一步提
( k6 D# p( B5 V. ^8 ~升。0 n" H$ c5 n! m
1.Tbnet
) K, F* B% y& @/ K: X. Z8 ^如图9-5所示,Tbnet队列模型本质上是一个生产者—消费者队列模型,有两个线
* f* i" Q2 T! }/ T- ?/ U程:网络读写线程以及超时检查线程,其中,网络读写线程执行事件循环,当服务0 w5 s" u! c; Q/ B p
器端有可读事件时,调用回调函数读取请求数据包,生成请求任务,并加入到任务
2 t# l% u+ s; T; ^; M3 m" o7 w队列中。工作线程从任务队列中获取任务,处理完成后触发可写事件,网络读写线( E* Y8 R/ @& Y2 _- I
程会将处理结果发送给客户端。超时检查线程用于将超时的请求移除。
& D0 v- z) d, q图 9-5 Tbnet队列模型
( r9 Q! o. S' H/ G! v: p) z1 HTbnet模型的问题在于多个工作线程从任务队列获取任务需要加锁互斥,这个过
8 Y6 l! h0 l x7 B程将产生大量的上下文切换(context switch),测试发现,当UpdateServer每秒处理. m1 a1 r6 w+ _8 }
包的数量超过8万个时,UpdateServer每秒的上下文切换次数接近30万次,在测试环/ i7 W( F! |. C3 c
境中已经达到极限(测试环境配置:Linux内核2.6.18,CPU为2*Intel Nehalem1 ~; \) g$ g8 a$ A: D7 ?
E5520,共8核16线程)。
+ I) a4 D7 a7 p- v1 x3 v2.Libeasy: ?" ~3 I4 w( I1 Z3 w- g+ P
为了解决收发小数据包带来的上下文切换问题,OceanBase目前采用Libeasy任务4 \% e# G& s: I* x8 N
模型。Libeasy采用多个线程收发包,增强了网络收发能力,每个线程收到网络包后/ ^7 N) d: `: S. n# H) e# \' `
立即处理,减少了上下文切换,如图9-6所示。
1 i; i7 m! @& C. s图 9-6 Libeasy任务模型
! C! n5 n3 V( kUpdateServer有多个网络读写线程,每个线程通过Linux epool监听一个套接字集+ ?' Q# I- Z' z5 r$ ?' h/ w
合上的网络读写事件,每个套接字只能同时分配给一个线程。当网络读写线程收到
4 U8 W, f$ s& E7 `6 r+ K) r4 B% n网络包后,立即调用任务处理函数,如果任务处理时间很短,可以很快完成并回复% Q* d2 i8 W* g
客户端,不需要加锁,避免了上下文切换。UpdateServer中大部分任务为短任务,比
+ ^4 t+ l- @* |: D如随机读取内存表,另外还有少量任务需要等待共享资源上的锁,可以将这些任务9 h% l4 q# P8 A/ x2 ]1 s" M+ f
加入到长任务队列中,交给专门的长任务处理线程处理。3 v1 T5 r) o! V3 p& E
由于每个网络读写线程处理一部分预先分配的套接字,这就可能出现某些套接/ P; H, i" v# h
字上请求特别多而导致负载不均衡的情况。例如,有两个网络读写线程thread1和" {6 o r% w7 W4 E( ~, K. V
thread2,其中thread1处理套接字fd1、fd2,thread2处理套接字fd3、fd4,fd1和fd2上每
! Y# e p/ |2 q& P1 B秒1000次请求,fd3和fd4上每秒10次请求,两个线程之间的负载很不均衡。为了处理7 G0 z/ G6 G; ~8 x1 L$ [1 ~
这种情况,Libeasy内部会自动在网络读写线程之间执行负载均衡操作,将套接字从
G+ Z: b0 V+ ^" p5 u8 x+ C9 g负载较高的线程迁移到负载较低的线程。
% C( T9 F: l8 N0 R# _# w9.3.3 主备同步
; l2 d+ S1 K- f& n" W/ p' I$ r1 p8.4.1节已经介绍了UpdateServer的一致性选择。OceanBase选择了强一致性,主
9 S* l5 b. S+ X7 {( \UpdateServer往备UpdateServer同步操作日志,如果同步成功,主UpdateServer操作本
/ S7 s9 v7 [" J' u; ]地后返回客户端更新成功,否则,主UpdateServer会把备UpdateServer从同步列表中& K3 x8 ~4 I: I7 h) W' ~$ T3 N
剔除。另外,剔除备UpdateServer之前需要通知RootServer,从而防止RootServer将不* F9 n2 ]! R' \! l& V u
一致的备UpdateServer选为主UpdateServer。
- p% ^/ C+ a, a3 z9 J如图9-7所示,主UpdateServer往备机推送操作日志,备UpdateServer的接收线程
0 j4 w% ^3 G! v' t5 Q! V接收日志,并写入到一块全局日志缓冲区中。备UpdateServer只要接收到日志就可以1 _5 b- L' q5 Q" H, g
回复主UpdateServer同步成功,主UpdateServer接着更新本地内存并将日志刷到磁盘3 i( X8 ^2 I; }) d
文件中,最后回复客户端写入操作成功。这种方式实现了强一致性,如果主8 H. |9 i s8 ^( H
UpdateServer出现故障,备UpdateServer包含所有的修改操作,因而能够完全无缝地7 |* X6 U/ i; h" o7 h* b
切换为主UpdateServer继续提供服务。另外,主备同步过程中要求主机刷磁盘文件,
8 U" y" g$ C' f3 N" _5 [备机只需要写内存缓冲区,强同步带来的额外延时也几乎可以忽略。
7 Q$ ]9 p* C7 _3 _图 9-7 UpdateServer主备同步原理4 K0 J d6 i& z7 ?
正常情况下,备UpdateServer的日志回放线程会从全局日志缓冲区中读取操作日
& Z9 f4 ~3 I- L, W3 I志,在内存中回放并同时将操作日志刷到备机的日志文件中。如果发生异常,比如0 J, _# s" k, ~, h
备UpdateServer刚启动或者主备之间网络刚恢复,全局日志缓冲区中没有日志或者日* F" R) Q/ Y6 }" l9 g. n" |
志不连续,此时,备UpdateServer需要主动请求主UpdateServer拉取操作日志。主
9 v8 L: a: [8 m4 eUpdateServer首先查找日志缓冲区,如果缓冲区中没有数据,还需要读取磁盘日志文
" K: u1 [1 g) i. B7 |件,并将操作日志回复备UpdateServer。代码如下:
5 ?$ ~6 y# R9 wclass ObReplayLogSrc
( @3 D+ K# ~& [- e6 \+ F. X{
3 h( @* U# t. h+ ]+ i2 I+ |8 bpublic:; U8 m5 u+ [& e u. ?$ \( C
//读取一批待回放的操作日志
6 H0 f1 j$ C( t: ^//@param[in]start_cursor日志起始点$ S; _" r4 N0 ]! F
//@param[out]end_id读取到的最大日志号加1,即下一次读取的起始日志号1 d2 s j+ E* C# k d# N* D7 T
//@param[in]buf日志缓冲区6 W" `) ?' N3 D" a% j l* T+ S
//@param[in]len日志缓冲区长度; ?2 p: z! K8 }9 j% Q
//@param[out]read_count读取到的有效字节数
/ S8 e9 v- i3 [( [* I4 d9 M& Rint get_log(const ObLogCursor&start_cursor,int64_t&end_id,char*buf,const0 |- _ c S: l/ `% l/ T5 _
int64_t len,int64_t&read_count);9 Z3 r5 \1 m, B! w7 ]) S( F% A
};$ ?- `# F* [, \7 T' y
class ObUpsLogMgr
, j0 y L- [3 l# ^% e: }4 o" m{
9 J4 v" H; }- r! i {public:
! l0 B H# ?0 i, Denum WAIT_SYNC_TYPE& N z2 o4 \! a9 d- n. j1 H( d8 N
{
) M {0 W6 ~ fWAIT_NONE=0,
- I* n% C# z# F6 y% Q; C5 nWAIT_COMMIT=1,
4 m1 \7 f" ]- J( r H1 N4 vWAIT_FLUSH=2,
2 k) L3 f" e- `2 n, B; a};
' K4 `+ S: b/ O. [0 Epublic:$ j' t1 ^, T1 T8 F
//备UpdateServer接收主UpdateServer发送的操作日志
6 p& A7 d4 {' a U0 Hint slave_receive_log(const char*buf,int64_t len,const int64_t
: N0 l0 q; C4 A/ k8 nwait_sync_time_us,const WAIT_SYNC_TYPE wait_event_type);+ x s) d* `3 k/ Y' _
//备UpdateServer获取并回放操作日志0 G' q1 W4 d: i. _) M9 x
int replay_log();
/ z2 a& ?5 {$ X+ K0 t2 u};- t5 O- J2 B. M2 | [* M4 |
备UpdateServer接收到主UpdateServer发送的操作日志后,调用ObUpsLogMgr类的
" C- T& G8 T- d$ g; u8 aslave_receive_log将操作日志保存到日志缓冲区中。备UpdateServer可以配置成不等待
+ u7 i4 u+ g7 W2 f( D* Q8 T(WAIT_NONE)、等待提交到MemTable(WAIT_COMMIT)或者等待提交到
! w9 g. N1 a- D _* L/ U$ D, P, e; eMemTable且写入磁盘(WAIT_FLUSH)。另外,备UpdateServer有专门的日志回放线
$ z, J( }2 ]! B2 r |! l" c程不断地调用ObUpsLogMgr中的replay_log函数获取并回放操作日志。
# Y( l5 W" ?/ {' C备UpdateServer执行replay_log函数时,首先调用ObReplayLogSrc的get_log函数读7 P8 W- w# z* d- u# S- Y
取一批待回放的操作日志,接着,将操作日志应用到MemTable中并写入日志文件持
& I2 U3 S/ P8 W久化。Get_log函数执行时首先查看本机的日志缓冲区,如果缓冲区中不存在日志起. D$ E. a6 Y G2 R/ _' e' Z9 ^
始点(start_cursor)开始的操作日志,那么,生成一个异步任务,读取主( S9 r5 j! n) u) D0 [! p4 S
UpdateServer。一般情况下,slave_receive_log接收的日志刚加入日志缓冲区就被
7 @4 R, \4 @. X7 ?1 t4 e* @get_log读走了,不需要读取主UpdateServer。
6 n0 s/ b: f5 d" }8 n0 ]) C% b8 ^8 D8 [* b1 @# Y2 X4 q8 V+ S
7 I$ U8 b( b2 U6 w9 w1 j |
|