|
9.3 UpdateServer实现机制
. J$ s% m% r/ T) v wUpdateServer用于存储增量数据,它是一个单机存储系统,由如下几个部分组$ u4 N9 {( I$ I3 M
成:
% ]7 N3 C- y v; e1 t/ ~●内存存储引擎,在内存中存储修改增量,支持冻结以及转储操作;; _0 h( W1 O2 E6 o/ O' U7 ~4 K1 U
●任务处理模型,包括网络框架、任务队列、工作线程等,针对小数据包做了专3 i( y# G$ M5 g) L5 l2 M6 K
门的优化;
# V% j9 i" W$ P& X+ d5 Y) I●主备同步模块,将更新事务以操作日志的形式同步到备UpdateServer。
; O8 w4 X) W# t: YUpdateServer是OceanBase性能瓶颈点,核心是高效,实现时对锁(例如,无锁
, d$ w9 a% M; J5 L( X9 F数据结构)、索引结构、内存占用、任务处理模型以及主备同步都需要做专门的优
9 s; k6 S) X! c' B$ J' P4 @化。
4 F, f4 z6 c4 h7 E7 T7 {, b9.3.1 存储引擎6 h; r% ` A1 H& M
UpdateServer存储引擎如图9-3所示。
, S" W; \( W8 ?, G7 m5 Z0 \' u图 9-3 UpdateServer存储引擎
7 b# o& f9 c1 w& bUpdateServer存储引擎与6.1节中提到的Bigtable存储引擎看起来很相似,不同点/ Q7 _0 C% g& Z
在于:/ `+ x. A# [+ Z/ }* z3 [ l7 D
●UpdateServer只存储了增量修改数据,基线数据以SSTable的形式存储在
# ?; |/ e, N* a( T9 _3 c) W6 eChunkServer上,而Bigtable存储引擎同时包含某个子表的基线数据和增量数据;! P6 {' H+ f) D9 r
●UpdateServer内部所有表格共用MemTable以及SSTable,而Bigtable中每个子表/ }8 A; J. h- V3 Y
的MemTable和SSTable分开存放;! a+ Z9 \, f0 s2 V9 L% \0 `
●UpdateServer的SSTable存储在SSD磁盘中,而Bigtable的SSTable存储在 GFS: e8 c6 Y4 I {
中。4 ?7 ]3 G' n( a" R
UpdateServer存储引擎包含几个部分:操作日志、MemTable以及SSTable。更新; c8 X7 f$ }/ u* j7 c
操作首先记录到操作日志中,接着更新内存中活跃的MemTable(Active
5 q4 i$ g) w- o) l' gMemTable),活跃的MemTable到达一定大小后将被冻结,称为Frozen MemTable,同' `" U# T3 ~; N& \: L) F
时创建新的Active MemTable。Frozen MemTable将以SSTable文件的形式转储到SSD磁- \5 l1 c% H8 [7 e! l. t: q
盘中。
5 b$ b3 b9 D7 A7 ~" Y2 k7 n! R8 t1.操作日志! ^$ `* k7 `6 Q/ M
OceanBase中有一个专门的提交线程负责确定多个写事务的顺序(即事务id),
. J3 Y) K: E: y0 l5 c9 l9 a将这些写事务的操作追加到日志缓冲区,并将日志缓冲区的内容写入日志文件。为. T( f- ^! v' A- M" k
了防止写操作日志污染操作系统的缓存,写操作日志文件采用Direct IO的方式实现:
) \$ c9 ^* y: y" @! \* xclass ObLogWriter
7 D$ F1 _ m4 F/ u{7 S6 d+ [' L! K. C0 M: M2 s: j$ u* e
public:
8 O9 U- @. J6 s6 F( r0 `" w: `2 i1 h//write_log函数将操作日志存入日志缓冲区 [8 s" s" x: r- f2 G
int write_log(const LogCommand cmd,const char*log_data,const int64_t
; o4 T% `: @2 ]% P Edata_len);
' V! O+ p! e5 X% M4 Z//将日志缓冲区中的日志先同步到备机再写入主机磁盘! v2 H+ M6 A( n3 m3 q) |
int flush_log(LogBuffer&tlog_buffer,const bool sync_to_slave=true,const bool' O8 _5 l" ^7 X) ?
is_master=true);
- v7 J. ~5 |1 T4 H* ?) E& o};
" K- d! M9 F( N每条日志项由四部分组成:日志头+日志序号+日志类型(LogCommand)+日志
2 V" b7 _. Y$ o内容,其中,日志头中记录了每条日志的校验和(checksum)。ObLogWriter中的2 P- l: f/ S$ S0 e$ l+ W
write_log函数负责将操作日志拷贝到日志缓冲区中,如果日志缓冲区已满,则向调
2 i: S6 l5 Z# K用者返回缓冲区不足(OB_BUF_NOT_ENOUGH)错误码。接着,调用者会通过# Q5 q2 X% J+ r+ j
flush_log将缓冲区中已有的日志内容同步到备机并写入主机磁盘。如果主机磁盘的最
& {, g( w5 j8 p+ L, I: j后一个日志文件超过指定大小(默认为64MB),还会调用switch_log函数切换日志% B/ u* q- n4 M @% `4 A, v6 `" ?6 w
文件。为了提高写性能,UpdateServer实现了成组提交(Group Commit)技术,即首
5 p" O b3 g* x# E, n先多次调用write_log函数将多个写操作的日志拷贝到相同的日志缓冲区,接着再调
7 E* \$ s Z7 R用flush_log函数将日志缓冲区中的内容一次性写入到日志文件中。 D! A3 ?& S3 [) B
2.MemTable
( g2 Y1 K0 p/ S* I, JMemTable底层是一个高性能内存B树。MemTable封装了B树,对外提供统一的读
. `. l! {# I- r# S0 N& ?写接口。/ L, Z I# c! f. I
B树中的每个叶子节点对应MemTable中的一行数据,key为行主键,value为行操7 w, j) N% J8 s9 F& n
作链表的指针。每行的操作按照时间顺序构成一个行操作链表。
) W6 t; o6 u+ ]+ ~如图9-4所示,MemTable的内存结构包含两部分:索引结构以及行操作链表,索# x9 ^1 v+ |5 \9 {* R$ W1 m8 I% Z
引结构为9.1.2节中提到的B树,支持插入、删除、更新、随机读取以及范围查询操
9 e! X7 ?8 P3 s! k$ z/ Z作。行操作链表保存的是对某一行各个列(每个行和列确定一个单元,称为Cell)的
9 S; u6 w" D G) F# k* S& s1 s修改操作。- n0 K9 q4 i f# [) }0 y$ ?3 m
图 9-4 MemTable的内存结构3 v* c( q$ I$ q2 Q
例9-3 对主键为1的商品有3个修改操作,分别是:将商品购买人数修改为
8 A) m6 ?4 ]5 ]3 G7 \100,删除该商品,将商品名称修改为“女鞋”,那么,该商品的行操作链中将保存三
6 B) d# Z+ d0 K3 x3 t) N" i个Cell,分别为:<update,购买人数,100>、<delete,*>以及<update,商品2 l+ @, ?3 B7 L
名,“女鞋”>。$ e$ r6 j* [' @7 z! W% f
MemTable中存储的是对该商品的所有修改操作,而不是最终结果。另外,* a) B8 _3 p8 K+ u8 z" ]6 H
MemTable删除一行也只是往行操作链表的末尾加入一个逻辑删除标记,即<delete,$ r0 c. Z1 A" F1 w' o, ]! p& |
*>,而不是实际删除索引结构或者行操作链表中的行内容。3 S: Q) {5 G* b" b/ B2 e; O9 _
MemTable实现时做了很多优化,包括:4 M) S, m$ A; H/ y/ l+ r8 l3 i
●哈希索引:针对主要操作为随机读取的应用,MemTable不仅支持B树索引,还
& k% _( \/ z. L+ t支持哈希索引,UpdateServer内部会保证两个索引之间的一致性。
# n* P& x1 W" U/ w0 f& P●内存优化:行操作链表中每个cell操作都需要存储操作列的编号1 B, ^& g+ |) L$ s$ Y* A7 b, s
(column_id)、操作类型(更新操作还是删除操作)、操作值以及指向下一个cell操: L6 S7 z. }# z: B9 S! ?9 d
作的指针,如果不做优化,内存膨胀会很大。为了减少内存占用,MemTable实现时
7 a1 N5 [0 i; h会对整数值进行变长编码,并将多个cell操作编码后序列到同一块缓冲区中,共用一
# x/ P: m9 {2 D. ~4 @! D X个指向下一批cell操作缓冲区的指针:
4 C$ s1 H Z. Z3 i4 Gstruct ObCellMeta( [' t/ z, @1 \/ |/ y
{' z7 L% |; Q; i* ?% d
const static int64_t TP_INT8=1;//int8整数类型
: Z9 }- `) W/ Dconst static int64_t TP_INT16=2;//int16整数类型2 F- E+ G6 C$ e6 Y# E) q$ t
const static int64_t TP_INT32=3;//int32整数类型. f. z- ^) U3 X
const static int64_t TP_INT64=4;//int64整数类型9 q+ W/ u( k; l3 Q0 a( C! T
const static int64_t TP_VARCHAR=6;//变长字符串类型
" w0 D( s! S% B1 \const static int64_t TP_DOUBLE=13;//双精度浮点类型3 T7 P1 c( Q- @7 a
const static int64_t TP_ESCAPE=0x1f;//扩展类型
+ L/ X$ H0 u, U2 Dconst static int64_t ES_DEL_ROW=1;//删除行操作) v4 A0 s' y, A+ A- v
};
. z' q4 ^( v" {class ObCompactCellWriter
X& h% E+ A/ I) E{
7 K. X) o: L4 t7 \) e1 e& ^- mpublic:
5 @% H) i! R; e$ ~//写入更新操作,存储成压缩格式# Z3 c% G+ `9 [7 [' f8 a6 L
int append(uint64_t column_id,const ObObj&value);4 Y4 b6 F% o$ D1 G7 H# n3 R
//写入删除操作,存储成压缩格式+ R) @1 m0 k9 i' p- s
int row_delete();+ z# d# R8 F0 F
};
6 c2 |5 m& I2 E% IMemTable通过ObCompactCellWriter来将cell操作序列化到内存缓冲区中,如果为; r: F8 E+ D a& q
更新操作,调用append函数;如果为删除操作,调用row_delete函数。更新操作的存
- s4 @# N( O- E: ]储格式为:数据类型+值+列ID,TP_INT8/TP_INT16/TP_INT32/TP_INT64分别表示80 H' A# Q/ O. t; E
位/16位/32位/64位整数类型,TP_VARCHAR表示变长字符串类型,TP_DOUBLE表示' ? t6 |, w4 L# t, I, `
双精度浮点类型。删除操作为扩展操作,其存储格式为:
6 z, A+ g) f9 g, k" Q6 vTP_ESCAPE+ES_DEL_ROW。例9-3中的三个Cell:<update,购买人数,100>、<" Z8 q& [/ p' A# R8 ^) s1 u3 ~
delete,*>以及<update,商品名,“女鞋”>在内存缓冲区的存储格式为:1 U V3 z9 H6 _( I: [0 ^7 w
第1~3字节表示第一个Cell,即<update,购买人数,100>;第4~5字节表示第/ C/ T8 [6 B6 R* O/ X
二个cell,即<delete,*>;第6~8字节表示第三个Cell,即<update,商品名,“女
9 @' i. N Q0 Y& S& u鞋”>。5 H7 X: [: E, l: G& P
MemTable的主要对外接口可以归结如下:
2 R( g2 R% t" K% {& t2 q2 I6 D! g//开启一个事务
: N2 ^$ X$ k5 C+ U& d. j//@param[in]trans_type事务类型,可能为读事务或者写事务& Y' [8 u- p: X" ?* H
//@param[out]td返回的事务描述符
R9 O0 O2 Z% f. R/ }int start_transaction(const TETransType trans_type,MemTableTransDescriptor&
7 f, R- g* y2 o7 c% P7 I$ Qtd);0 n. o( o4 d9 o; z4 w8 @9 ]" f
//提交或者回滚一个事务. h& ^( J8 ?3 d6 o4 s
//@param[in]td事务描述符
* P5 A: J) k* E! O) p//@param[in]rollback是否回滚,默认为false
2 Q2 D6 n9 Q4 j; [0 r* ^2 U. f {9 vint end_transaction(const MemTableTransDescriptor td,bool rollback=false);
6 Q" x# I( y$ W. K//执行随机读取操作,返回一个迭代器9 M' J$ g6 b, _! r+ |/ F
//@param[in]td事务描述符
+ c" ~6 b5 L* p0 r' A3 T//@param[in]table_id表格编号& r& m# H3 R' |8 z
//@param[in]row_key待查询的主键7 A6 p! j' z/ d' _
//@param[out]iter返回的迭代器
4 d6 x6 j1 i. Lint get(const MemTableTransDescriptor td,const uint64_t table_id,const0 Q6 c {0 a2 y+ Y+ x
ObRowkey&row_key,MemTableIterator&iter);% `) M$ P- T$ R0 Q7 u
//执行范围查询操作,返回一个迭代器
4 |4 E5 \7 j+ n+ g$ V! @//@param[in]td事务描述符
0 z/ `" t4 ^5 g5 p/ t//@param[in]range查询范围,包括起始行、结束行,开区间或者闭区间
$ |, h& G$ N0 Y8 @//@param[out]iter返回的迭代器7 q+ M% b, W. K! Y* R% A( {
int scan(const MemTableTransDescriptor td,const ObRange&
) A8 i: f! \7 S' R. e, R% Drange,MemTableIterator&iter);
/ J: X- x2 p9 J4 b9 B//开始执行一次修改操作. y [4 O9 W7 j2 T' u3 |+ d
//@param[in]td事务描述符# I, _9 H3 R7 ]% k+ j) t
int start_mutation(const MemTableTransDescriptor td);6 a& ]; J6 I/ [/ S4 K$ [/ h
//提交或者回滚一次修改操作
8 S/ u1 o: ?& N" ~+ T4 V9 G/ L9 O//@param[in]td事务描述符6 ?: A, R) r1 S2 H$ {
//@param[in]rollback是否回滚
6 q2 @5 Y, c1 c! m: A( p f) C! rint end_mutation(const MemTableTransDescriptor td,bool rollback);
; S' \; h/ l( u0 x7 r/ E$ p//执行修改操作: i* L0 h% d5 |1 K
//@param[in]td事务描述符
: e- D1 C3 n7 |; S- I//@param[in]mutator修改操作,包含一个或者多个对多个表格的cell操作1 S) Z( ]- l* D: w6 F: V
int set(const MemTableTransDescriptor td,ObUpsMutator&mutator);7 M n, }5 M( U& v
对于读事务,操作步骤如下:5 m: B7 Z& `7 _7 C0 @
1)调用start_transaction开始一个读事务,获得事务描述符;
$ A ~* W9 V+ y! ]2 |; @7 i2)执行随机读取或者扫描操作,返回一个迭代器;接着可以从迭代器不断迭代5 S: B! A6 H/ k0 l, j7 @' e1 q
数据;' @$ ]+ n. L s( {' W
3)调用end_transaction提交或者回滚一个事务。+ P; W) Z( a2 q2 c
class MemTableIterator; p/ M ?5 S) X4 K& u0 X
{! i+ `* N5 j8 s4 A7 D% R9 N- {
public:
. }" r& F0 r# U2 [" E- ^//迭代器移动到下一个cell
: Z. ]- d. y. p1 B, Pint next_cell();
- }& D) i. \# v; t; M6 i' ]//获取当前cell的内容
/ t) i" q; t, Q$ {6 S B//@param[out]cell_info当前cell的内容,包括表名(table_id),行主键(row_
. T1 V1 |3 B3 K* Y# @key),列编号(column_id)以及列值(column_value)
9 E. t8 x6 y! f/ @2 `int get_cell(ObCellInfo**cell_info);
) t- ?# n0 ~8 @//获取当前cell的内容
4 \+ R& b7 ^- {; u//@param[out]cell_info当前cell的内容
: `. `; u: d* |' w+ x/ D6 J" ~+ K//@param is_row_changed是否迭代到下一行* {4 X; a' S' _7 f4 N7 E
int get_cell(ObCellInfo**cell_info,bool*is_row_changed);
( {$ {' @0 B$ R& v};
/ i9 N& @. y0 f9 C读事务返回一个迭代器MemTableIterator,通过它可以不断地获取下一个读到的! h5 m8 [9 ?# K/ Y' G$ G% t
cell。在例9-3中,读取编号为1的商品可以得到一个迭代器,从这个迭代器中可以读
, Q0 Q6 J8 I. D+ ` i) l% y9 `/ N出行操作链中保存的3个Cell,依次为:<update,购买人数,100>,<delete,*6 v4 s% }8 A6 v5 v
>,<update,商品名,“女鞋”>。
) |2 V q2 m8 ?' {- p% i+ Z写事务总是批量执行,步骤如下:
L4 r3 N. O1 |8 h, N, W& M1)调用start_transaction开始一批写事务,获得事务描述符;: C/ K m2 O7 Q! S3 x
2)调用start_mutation开始一次写操作;
+ `! U& A8 H7 H( g3)执行写操作,将数据写入到MemTable中;& q, d- \; `" W- {
4)调用end_mutation提交或者回滚一次写操作;如果还有写事务,转到步骤
O8 l- H5 p, J# S2);/ n+ S9 ~7 o N8 v5 |
5)调用end_transaction提交写事务。
' l. Z- i7 n/ s9 W& d3.SSTable
. s6 G0 V9 O: N6 j" l当活跃的MemTable超过一定大小或者管理员主动发起冻结命令时,活跃的
8 n5 s" D Q# VMemTable将被冻结,生成冻结的MemTable,并同时以SSTable的形式转储到SSD磁盘$ K% j: X9 [9 c& `
中。
8 q6 C$ X+ C( J8 q: y2 Q! ~SSTable的详细格式请参考9.4节ChunkServer实现机制,与ChunkServer中的4 O9 g. w2 G; E
SSTable不同的是,UpdateServer中所有的表格共用一个SSTable,且SSTable为稀疏格3 v9 o$ ^# g6 A; U C+ Q
式,也就是说,每一行数据的每一列可能存在,也可能不存在修改操作。$ n) `. A" ]7 E. J5 d8 o, {) M( A+ Q8 v
另外,OceanBase设计时也尽量避免读取UpdateServer中的SSTable,只要内存足
" p& w& p0 w6 K2 U3 [够,冻结的MemTable会保留在内存中,系统会尽快将冻结的数据通过定期合并或者 a: W* n; t6 L9 T
数据分发的方式转移到ChunkServer中去,以后不再需要访问UpdateServer中的$ r. h2 {5 \/ h+ A! z. w; k
SSTable数据。
: B4 O! {/ W$ p3 M当然,如果内存不够需要丢弃冻结MemTable,大量请求只能读取SSD磁盘,6 E% B+ ?# b: r9 \
UpdateServer性能将大幅下降。因此,希望能够在丢弃冻结MemTable之前将SSTable% D$ d, m2 N' Z' x. [' o
的缓存预热。- l0 G- I; ]9 W9 a# [& k
UpdateServer的缓存预热机制实现如下:在丢弃冻结MemTable之前的一段时间3 h) I Q/ d" k3 [* G
(比如10分钟),每隔一段时间(比如30秒),将一定比率(比如5%)的请求发给
0 P6 P% e! r- j6 x9 f+ N2 wSSTable,而不是冻结MemTable。这样,SSTable上的读请求将从5%到10%,再到
# x5 Z7 j6 k: g$ R9 K9 _8 q; f15%,依次类推,直到100%,很自然地实现了缓存预热。
- y- @! ^# P; M: U! {9.3.2 任务模型" G$ ~5 y. s0 F P5 h; E b
任务模型包括网络框架、任务队列、工作线程,UpdateServer最初的任务模型基
' q9 U2 a' {( F6 M1 N于淘宝网实现的Tbnet框架(已开源,见http://code.taobao.org/p/tb-common-' j: { K: f0 n$ H7 X- N6 u
utils/src/trunk/tbnet/)。Tbnet封装得很好,使用比较方便,每秒收包个数最多可以达
1 S: D) V8 d, m9 N9 \$ [到接近10万,不过仍然无法完全发挥UpdateServer收发小数据包以及内存服务的特
. o! O6 ~; c+ c9 w! y. [5 }, d' ?点。OceanBase后来采用优化过的任务模型Libeasy,小数据包处理能力得到进一步提# h5 k+ ^* F, B& ~0 t x; s6 i
升。4 V% U: O" p v4 [7 J
1.Tbnet) e$ J( @& e% z7 X: ]
如图9-5所示,Tbnet队列模型本质上是一个生产者—消费者队列模型,有两个线
' W2 I+ j1 B1 Q/ I5 D; e( h程:网络读写线程以及超时检查线程,其中,网络读写线程执行事件循环,当服务
$ H% b* f3 Q# d4 }器端有可读事件时,调用回调函数读取请求数据包,生成请求任务,并加入到任务$ u8 K4 G# g$ u5 W
队列中。工作线程从任务队列中获取任务,处理完成后触发可写事件,网络读写线
! I* C l/ C5 N/ O h: t程会将处理结果发送给客户端。超时检查线程用于将超时的请求移除。( C% v5 _ K% @6 t, P# `% t
图 9-5 Tbnet队列模型
0 J0 Z) M# T. v7 tTbnet模型的问题在于多个工作线程从任务队列获取任务需要加锁互斥,这个过( j" A2 n2 c2 A8 g
程将产生大量的上下文切换(context switch),测试发现,当UpdateServer每秒处理3 U& I5 t1 S: F5 I6 j
包的数量超过8万个时,UpdateServer每秒的上下文切换次数接近30万次,在测试环" J6 k& H$ f# u& I2 [
境中已经达到极限(测试环境配置:Linux内核2.6.18,CPU为2*Intel Nehalem5 u5 H# F; I& P
E5520,共8核16线程)。/ T9 h* K" Z+ d1 u; T( i# o$ b
2.Libeasy
/ h% k4 f) Z* D3 z4 G/ p- D为了解决收发小数据包带来的上下文切换问题,OceanBase目前采用Libeasy任务9 f) H# A8 V0 i
模型。Libeasy采用多个线程收发包,增强了网络收发能力,每个线程收到网络包后
- v$ f0 T5 z7 I0 V: R立即处理,减少了上下文切换,如图9-6所示。; C6 P( A2 @/ [+ ?) ?) W1 u
图 9-6 Libeasy任务模型/ i3 ]6 R* C& Q' b7 f8 |$ {
UpdateServer有多个网络读写线程,每个线程通过Linux epool监听一个套接字集
! ]+ M+ j2 n5 ]( @6 g合上的网络读写事件,每个套接字只能同时分配给一个线程。当网络读写线程收到$ G. o2 r2 v1 K8 D+ Y
网络包后,立即调用任务处理函数,如果任务处理时间很短,可以很快完成并回复
, h& C2 Q# W1 K$ n, [8 y; h客户端,不需要加锁,避免了上下文切换。UpdateServer中大部分任务为短任务,比# l! B2 N5 Q7 |
如随机读取内存表,另外还有少量任务需要等待共享资源上的锁,可以将这些任务
8 }& M% g# j' E加入到长任务队列中,交给专门的长任务处理线程处理。
* z/ _4 T. r: o由于每个网络读写线程处理一部分预先分配的套接字,这就可能出现某些套接
$ Y- @6 h* Q# r* X, N* H字上请求特别多而导致负载不均衡的情况。例如,有两个网络读写线程thread1和4 }' q7 {+ H( y4 T' m, K
thread2,其中thread1处理套接字fd1、fd2,thread2处理套接字fd3、fd4,fd1和fd2上每; J$ H; K5 J) s7 S5 T
秒1000次请求,fd3和fd4上每秒10次请求,两个线程之间的负载很不均衡。为了处理4 M# _: `6 P& t+ G) f/ |
这种情况,Libeasy内部会自动在网络读写线程之间执行负载均衡操作,将套接字从
6 Y0 i9 t5 `, N; v负载较高的线程迁移到负载较低的线程。
9 \5 q' M3 ^: d7 q" E0 A5 T9.3.3 主备同步& C% e! p" q, w) r
8.4.1节已经介绍了UpdateServer的一致性选择。OceanBase选择了强一致性,主+ j, @9 ?/ \' y* i0 }4 h
UpdateServer往备UpdateServer同步操作日志,如果同步成功,主UpdateServer操作本
6 b0 d' ]( u/ I2 j4 F" o! @地后返回客户端更新成功,否则,主UpdateServer会把备UpdateServer从同步列表中8 V: t- \- `; f! o; q" q
剔除。另外,剔除备UpdateServer之前需要通知RootServer,从而防止RootServer将不$ Z' C, C' }8 D G: O# B
一致的备UpdateServer选为主UpdateServer。 F+ [5 [! C, Q$ Z
如图9-7所示,主UpdateServer往备机推送操作日志,备UpdateServer的接收线程
. N( C' F6 i6 N0 h5 I2 v( H接收日志,并写入到一块全局日志缓冲区中。备UpdateServer只要接收到日志就可以
+ x3 M8 |( S& z* J' N回复主UpdateServer同步成功,主UpdateServer接着更新本地内存并将日志刷到磁盘 j; Y! Y- S0 M, T+ l5 ]1 X
文件中,最后回复客户端写入操作成功。这种方式实现了强一致性,如果主8 K; p; f' ^# P9 A; X
UpdateServer出现故障,备UpdateServer包含所有的修改操作,因而能够完全无缝地
5 x5 A x1 v+ J) z5 i9 M切换为主UpdateServer继续提供服务。另外,主备同步过程中要求主机刷磁盘文件,
* h" i; _' ]) F+ Y2 t备机只需要写内存缓冲区,强同步带来的额外延时也几乎可以忽略。
7 P: M- m: ?3 D图 9-7 UpdateServer主备同步原理
7 v3 D4 h* p+ q# }1 ^正常情况下,备UpdateServer的日志回放线程会从全局日志缓冲区中读取操作日
3 d4 v4 O9 v% \+ F% P志,在内存中回放并同时将操作日志刷到备机的日志文件中。如果发生异常,比如7 j* @1 h5 u( X1 \- Q' J+ u9 O
备UpdateServer刚启动或者主备之间网络刚恢复,全局日志缓冲区中没有日志或者日
, W2 N* c: F" K: F志不连续,此时,备UpdateServer需要主动请求主UpdateServer拉取操作日志。主: r% z( P t; k" e: e+ e+ o
UpdateServer首先查找日志缓冲区,如果缓冲区中没有数据,还需要读取磁盘日志文
* \+ Z! _: `( r件,并将操作日志回复备UpdateServer。代码如下:# h L- g& f$ \$ X
class ObReplayLogSrc4 X# H1 z' l9 _* a) x' k
{# O$ S, o' X9 }) F
public:
7 [$ f/ M5 r3 f$ Z//读取一批待回放的操作日志
5 s/ u1 ]3 g; G* S8 a//@param[in]start_cursor日志起始点' }/ i1 L' `2 f# d8 S
//@param[out]end_id读取到的最大日志号加1,即下一次读取的起始日志号8 E! v3 r- L" M3 W& P
//@param[in]buf日志缓冲区$ x, y+ l1 s! ]/ @
//@param[in]len日志缓冲区长度
/ Y" N( x, v! Y//@param[out]read_count读取到的有效字节数3 U0 B& o8 G4 z6 \- k
int get_log(const ObLogCursor&start_cursor,int64_t&end_id,char*buf,const
' ~5 M- m" n' ~+ e S4 x0 R1 Mint64_t len,int64_t&read_count);; F. @- s# ?( x e9 s
};- m( I% v% R' T; C% u- @9 b
class ObUpsLogMgr4 i7 F0 C# d2 J7 }, u6 r3 G: b! ~- `
{& I6 J8 I$ Y* D V, l4 `' o" x0 Z
public:
) ^9 Q4 e1 p2 e; k y! benum WAIT_SYNC_TYPE
7 Q' K+ q; z8 ^& ]/ G, m7 P{7 R+ V, p8 j+ \8 b: x5 e, ]' K
WAIT_NONE=0,* m4 f9 r: Z; L, ^! \" l
WAIT_COMMIT=1,
% }% U! O: ^5 r3 Q8 D2 o* f5 ?WAIT_FLUSH=2,
7 S- q: `2 W( h7 t' x" K/ V};
2 Z. n$ F5 b/ Jpublic:
4 a# {# m9 U x1 E) O- {1 k//备UpdateServer接收主UpdateServer发送的操作日志4 w# c4 t- `; d9 g1 p1 Q0 `
int slave_receive_log(const char*buf,int64_t len,const int64_t
4 G1 _" w- q) O( R5 \0 nwait_sync_time_us,const WAIT_SYNC_TYPE wait_event_type);* a3 M+ |, u* G( R1 {7 [) h! p
//备UpdateServer获取并回放操作日志
; |+ ~5 M6 k. n+ gint replay_log();
$ a! O$ o, z) H% B2 E, |- N};' M/ E% i" G" z/ H
备UpdateServer接收到主UpdateServer发送的操作日志后,调用ObUpsLogMgr类的5 O- _+ k! i! J' u6 R
slave_receive_log将操作日志保存到日志缓冲区中。备UpdateServer可以配置成不等待. T& S; b; C7 g9 X4 @3 K0 h/ [* T( P
(WAIT_NONE)、等待提交到MemTable(WAIT_COMMIT)或者等待提交到
) K6 I/ H1 a7 |MemTable且写入磁盘(WAIT_FLUSH)。另外,备UpdateServer有专门的日志回放线
9 L0 [- q4 N8 f, `) T, ^程不断地调用ObUpsLogMgr中的replay_log函数获取并回放操作日志。 j% F8 T- P2 @8 d' @, O0 W
备UpdateServer执行replay_log函数时,首先调用ObReplayLogSrc的get_log函数读/ b* z/ P% q, y1 m: R( ~4 F1 o
取一批待回放的操作日志,接着,将操作日志应用到MemTable中并写入日志文件持! O5 { b, L! K+ w: Y3 L, q
久化。Get_log函数执行时首先查看本机的日志缓冲区,如果缓冲区中不存在日志起
# ~* I8 ?- [& O始点(start_cursor)开始的操作日志,那么,生成一个异步任务,读取主+ N! U- [6 J- Q2 Z
UpdateServer。一般情况下,slave_receive_log接收的日志刚加入日志缓冲区就被4 j4 D/ E$ h% l" p: K8 F/ b
get_log读走了,不需要读取主UpdateServer。
* z+ O* I0 c$ L
9 \& V3 e5 n2 B* j8 ~) y4 C* ~5 N$ R6 ^4 w4 j ^
|
|