|
9.3 UpdateServer实现机制, O8 d3 E1 i" L
UpdateServer用于存储增量数据,它是一个单机存储系统,由如下几个部分组
# J! |+ k4 z0 Y成:
6 J$ [4 l' M% H' v●内存存储引擎,在内存中存储修改增量,支持冻结以及转储操作;
. p' ^) \: ^( {●任务处理模型,包括网络框架、任务队列、工作线程等,针对小数据包做了专% B: _5 d1 w5 O. R" r8 p
门的优化;: A* I" r" K. W5 o _
●主备同步模块,将更新事务以操作日志的形式同步到备UpdateServer。6 ^) c+ x' F) z' |+ @. n4 ?
UpdateServer是OceanBase性能瓶颈点,核心是高效,实现时对锁(例如,无锁
; J0 Z! h7 J0 J ?/ J' T: `数据结构)、索引结构、内存占用、任务处理模型以及主备同步都需要做专门的优* |3 G4 K! |" L# F: [. w( f& @
化。
. f& a+ {7 V9 x/ `# A9.3.1 存储引擎
! Y8 l D1 }0 ^5 p) ~% r0 r' rUpdateServer存储引擎如图9-3所示。
! y( ]3 ?& Z3 p( k& e5 @+ c3 j7 g图 9-3 UpdateServer存储引擎3 @. s$ X' x9 Q8 | Y) c! z0 a
UpdateServer存储引擎与6.1节中提到的Bigtable存储引擎看起来很相似,不同点% n9 _, E, X0 O$ J- n
在于:
0 O" ~ _: ?& m/ f: X; `- G●UpdateServer只存储了增量修改数据,基线数据以SSTable的形式存储在5 X x. E% Q2 s I" @) \- r
ChunkServer上,而Bigtable存储引擎同时包含某个子表的基线数据和增量数据;
d0 m0 o; E- z6 M$ `* O●UpdateServer内部所有表格共用MemTable以及SSTable,而Bigtable中每个子表
9 p" \3 o# y! i ?的MemTable和SSTable分开存放;
5 A* q1 j1 b- b+ s●UpdateServer的SSTable存储在SSD磁盘中,而Bigtable的SSTable存储在 GFS6 G3 J# U% F6 P! z* C; }3 a4 m
中。: \& n+ ?5 V% Z7 k) i4 l/ V9 D" ?$ u
UpdateServer存储引擎包含几个部分:操作日志、MemTable以及SSTable。更新& y0 j, v) [7 k j: |% p" k* l6 l
操作首先记录到操作日志中,接着更新内存中活跃的MemTable(Active5 Y' o" i! w% }; `* s3 f2 R
MemTable),活跃的MemTable到达一定大小后将被冻结,称为Frozen MemTable,同$ H$ M" q; R: ?. S9 Y/ I
时创建新的Active MemTable。Frozen MemTable将以SSTable文件的形式转储到SSD磁+ J4 V( o E0 x/ T$ o8 t+ U; y
盘中。
5 }! y3 {2 ]+ _6 R8 `: ^: t1.操作日志0 M" O' f9 }( e% i7 {8 y
OceanBase中有一个专门的提交线程负责确定多个写事务的顺序(即事务id),
5 ^( \ F6 _1 v& D( b# W将这些写事务的操作追加到日志缓冲区,并将日志缓冲区的内容写入日志文件。为2 ?5 s7 \! \1 U; N; B
了防止写操作日志污染操作系统的缓存,写操作日志文件采用Direct IO的方式实现:# R( h Z3 \; z G( e
class ObLogWriter
, m* S' V% R0 h( T1 R1 m4 f{
* s" `! \7 h* J5 [public:
7 T/ \) J% b; l5 O- V//write_log函数将操作日志存入日志缓冲区: P. C- j3 E9 |0 u/ d
int write_log(const LogCommand cmd,const char*log_data,const int64_t. ?( g2 F' u8 F f
data_len);% f* E0 Z$ D7 R3 a+ W1 C
//将日志缓冲区中的日志先同步到备机再写入主机磁盘 S( P; G5 a& s2 H( X
int flush_log(LogBuffer&tlog_buffer,const bool sync_to_slave=true,const bool
3 I' L3 O- @" A0 P7 e O4 |: _is_master=true);
; a2 r; i% ?! Q& G7 j; S# y! I};3 }$ m' q2 l7 t8 ?' G
每条日志项由四部分组成:日志头+日志序号+日志类型(LogCommand)+日志
{0 i5 z+ U& g/ [* s内容,其中,日志头中记录了每条日志的校验和(checksum)。ObLogWriter中的
( ^" F7 N Q6 z, J( ~& cwrite_log函数负责将操作日志拷贝到日志缓冲区中,如果日志缓冲区已满,则向调
7 s; [5 N+ Q! ?- o( L用者返回缓冲区不足(OB_BUF_NOT_ENOUGH)错误码。接着,调用者会通过
; _- H P' z* w6 n$ ~$ Gflush_log将缓冲区中已有的日志内容同步到备机并写入主机磁盘。如果主机磁盘的最: [$ ]. T& P1 M! T; _. k& I% s
后一个日志文件超过指定大小(默认为64MB),还会调用switch_log函数切换日志
7 p Q4 K% P: k( v! ~% J: q文件。为了提高写性能,UpdateServer实现了成组提交(Group Commit)技术,即首- [9 U. Z. T8 R' ^: ^3 r
先多次调用write_log函数将多个写操作的日志拷贝到相同的日志缓冲区,接着再调
6 A: W( E; u4 b3 g* z4 f用flush_log函数将日志缓冲区中的内容一次性写入到日志文件中。$ ~0 V+ _/ l, a2 r( W
2.MemTable
! a8 }1 o" M0 O6 @( }: MMemTable底层是一个高性能内存B树。MemTable封装了B树,对外提供统一的读
( r' Z/ P7 E5 @! v9 f' t写接口。
/ e( v5 P! d* P1 v$ B h6 W# j. z2 FB树中的每个叶子节点对应MemTable中的一行数据,key为行主键,value为行操- O+ n( b4 _5 Y1 K* o. M" H! v% e
作链表的指针。每行的操作按照时间顺序构成一个行操作链表。: ^& m6 T+ f, t" v- a
如图9-4所示,MemTable的内存结构包含两部分:索引结构以及行操作链表,索7 I% Q- K$ x) g4 x9 e3 W
引结构为9.1.2节中提到的B树,支持插入、删除、更新、随机读取以及范围查询操
. M9 x' I E4 r, ^( L$ l4 r; `$ U; h作。行操作链表保存的是对某一行各个列(每个行和列确定一个单元,称为Cell)的0 w. h2 t# ?/ U+ h. T
修改操作。3 l4 U1 i0 ^. ^/ Z6 \* c" K
图 9-4 MemTable的内存结构
- y U) o* x- G例9-3 对主键为1的商品有3个修改操作,分别是:将商品购买人数修改为2 l9 F+ y6 m" q( W
100,删除该商品,将商品名称修改为“女鞋”,那么,该商品的行操作链中将保存三1 q9 P x3 i' g2 k% A/ y9 I
个Cell,分别为:<update,购买人数,100>、<delete,*>以及<update,商品: F' y1 H' {& u( S) F9 p0 J# H
名,“女鞋”>。# l$ k8 S* N& z9 T( H; w4 r2 m
MemTable中存储的是对该商品的所有修改操作,而不是最终结果。另外,. H# ]7 A& e6 M P& t' Z4 [2 K
MemTable删除一行也只是往行操作链表的末尾加入一个逻辑删除标记,即<delete,
6 |9 y0 A) m2 m*>,而不是实际删除索引结构或者行操作链表中的行内容。
o3 v$ @9 Y* R) ~( M! XMemTable实现时做了很多优化,包括:& ~8 n6 ?# ~4 f, ?
●哈希索引:针对主要操作为随机读取的应用,MemTable不仅支持B树索引,还
! O# }" J9 l9 [" Z7 T* q/ M0 q支持哈希索引,UpdateServer内部会保证两个索引之间的一致性。+ A9 t7 J* I$ B3 M( V/ {
●内存优化:行操作链表中每个cell操作都需要存储操作列的编号
) Q( Z! C/ F i. Y(column_id)、操作类型(更新操作还是删除操作)、操作值以及指向下一个cell操
+ o' ^% C6 M- v% ^3 @0 j( B作的指针,如果不做优化,内存膨胀会很大。为了减少内存占用,MemTable实现时: }, |+ Z; S1 p, H) Z* M- P+ R6 X' t! B
会对整数值进行变长编码,并将多个cell操作编码后序列到同一块缓冲区中,共用一
5 R4 e/ }2 P# \. a6 v7 Z( {个指向下一批cell操作缓冲区的指针:* |& {5 d& W" p
struct ObCellMeta
& K% k9 T" f3 W# n) c. J{
. x; V' X' Q( jconst static int64_t TP_INT8=1;//int8整数类型
3 m& y. a( X4 rconst static int64_t TP_INT16=2;//int16整数类型" L: q0 x" q' [0 c8 u F9 W
const static int64_t TP_INT32=3;//int32整数类型
" p; g' ^8 I p" lconst static int64_t TP_INT64=4;//int64整数类型
$ e3 U/ P- U, u, F6 O( ^9 x% Z) w. @! Aconst static int64_t TP_VARCHAR=6;//变长字符串类型% s, V% e! \/ v; T
const static int64_t TP_DOUBLE=13;//双精度浮点类型- H# _8 k6 `5 y. m# Q
const static int64_t TP_ESCAPE=0x1f;//扩展类型
* n; v9 ]2 [0 b, Xconst static int64_t ES_DEL_ROW=1;//删除行操作
4 C; Z' B9 ` h8 @5 Y7 B};
, \4 Q( g3 \+ eclass ObCompactCellWriter2 L4 y2 C& U. X0 m! z, T/ B' J
{
, Q* `# H, V4 W7 {public:# H2 G( ^. a6 |, \3 R+ ?! P
//写入更新操作,存储成压缩格式
: L3 [. u( V4 z4 S7 z8 }% R. Vint append(uint64_t column_id,const ObObj&value);
$ V: t: S2 n3 m, j$ j. y- r2 ~2 }" R//写入删除操作,存储成压缩格式
" C1 w; i$ G& [4 j! K6 Dint row_delete();/ A; `$ Y# w6 w9 a, G$ I
};2 j: y/ A1 N e; \# A
MemTable通过ObCompactCellWriter来将cell操作序列化到内存缓冲区中,如果为9 Z" n, R P/ d: r8 U( {
更新操作,调用append函数;如果为删除操作,调用row_delete函数。更新操作的存* P6 w4 M' N& X' s
储格式为:数据类型+值+列ID,TP_INT8/TP_INT16/TP_INT32/TP_INT64分别表示8
+ x3 [: V0 H$ T9 ?位/16位/32位/64位整数类型,TP_VARCHAR表示变长字符串类型,TP_DOUBLE表示
' R7 k6 b; _+ ~! q; P8 k* S8 W双精度浮点类型。删除操作为扩展操作,其存储格式为:
# V- h E! m' G) g+ n2 v1 f5 s/ G, vTP_ESCAPE+ES_DEL_ROW。例9-3中的三个Cell:<update,购买人数,100>、<9 G( A) h, D% P9 S
delete,*>以及<update,商品名,“女鞋”>在内存缓冲区的存储格式为:3 o! O8 M. k2 g* f1 D
第1~3字节表示第一个Cell,即<update,购买人数,100>;第4~5字节表示第% O$ R2 g6 g* _% M
二个cell,即<delete,*>;第6~8字节表示第三个Cell,即<update,商品名,“女. m+ t+ e, j9 U
鞋”>。
! ^) h: ~$ z9 y( H% e; s0 J$ IMemTable的主要对外接口可以归结如下:# S6 @; F. F/ `4 Q
//开启一个事务7 B' G, [$ z. O& w
//@param[in]trans_type事务类型,可能为读事务或者写事务
0 @) {) o/ |( P3 ^//@param[out]td返回的事务描述符9 n4 }/ J6 j: \7 ~- j) ?
int start_transaction(const TETransType trans_type,MemTableTransDescriptor&9 v, _- J3 e, @+ O, x: D3 {
td);
9 x [7 c- w% m0 l//提交或者回滚一个事务1 m/ w1 d3 t$ r1 Y3 m4 H/ P; k
//@param[in]td事务描述符. _- D* z/ j8 ^5 C* M; j
//@param[in]rollback是否回滚,默认为false
t/ K! U& u9 y, S0 w- X' [) a" e. hint end_transaction(const MemTableTransDescriptor td,bool rollback=false);
7 j5 @% t/ ]; O. m# ]: @//执行随机读取操作,返回一个迭代器. m) ?9 e1 |2 l+ x
//@param[in]td事务描述符
8 E9 v9 |* L$ K- v* K3 v) I* r& w& L//@param[in]table_id表格编号
# J c+ K h! |: y, `% v. n* o//@param[in]row_key待查询的主键
' c4 ?9 x8 @2 s, y6 G0 ^1 }//@param[out]iter返回的迭代器, H3 m0 {! y- }# u& Y
int get(const MemTableTransDescriptor td,const uint64_t table_id,const
6 x) {2 \0 {* e* U( e- v* ]ObRowkey&row_key,MemTableIterator&iter);
# U" D7 S- @( B9 @//执行范围查询操作,返回一个迭代器1 T" @3 @# F3 U
//@param[in]td事务描述符3 {: l/ j% O5 p
//@param[in]range查询范围,包括起始行、结束行,开区间或者闭区间, o- a6 |! K6 [/ G- `$ c% Q% g; R9 v) w
//@param[out]iter返回的迭代器" H; b' @) S2 w: X- [# s6 r
int scan(const MemTableTransDescriptor td,const ObRange&) D) S: S- G& x3 j1 H& X
range,MemTableIterator&iter);) t) N6 R* z ^* t
//开始执行一次修改操作
, d8 Y" L* L, O5 K2 }5 E) A//@param[in]td事务描述符
3 C5 n! |' J2 h( k. vint start_mutation(const MemTableTransDescriptor td);
" R* b/ R" F n) v# A, I- `8 x//提交或者回滚一次修改操作
. z$ l0 \- W$ Y7 L3 T//@param[in]td事务描述符
' F r3 h! [! T0 A& b//@param[in]rollback是否回滚; e2 e: A% F; s6 Q2 h. x V
int end_mutation(const MemTableTransDescriptor td,bool rollback);
' ~& m: l0 ]8 |//执行修改操作4 Z; K+ ~. v3 D" R
//@param[in]td事务描述符
2 I8 h O" H- F# i; P; ^. ^0 N//@param[in]mutator修改操作,包含一个或者多个对多个表格的cell操作
1 c2 q9 L. N0 i( mint set(const MemTableTransDescriptor td,ObUpsMutator&mutator);
! W* M( G$ @! S4 Y+ I) r对于读事务,操作步骤如下:+ b; U% Q3 l# \3 N" ~( c( y9 b, W0 D
1)调用start_transaction开始一个读事务,获得事务描述符;2 H. T" Y% A' _6 b. N
2)执行随机读取或者扫描操作,返回一个迭代器;接着可以从迭代器不断迭代" q$ D7 D$ @- X! M; b
数据;; |# s6 `3 \8 X' ]* S* ?; t! J
3)调用end_transaction提交或者回滚一个事务。# w; v: O" \! Q I4 Q B+ Q
class MemTableIterator3 @1 f0 S) ^& |. M
{
0 o0 t9 y3 X5 u$ Cpublic:
6 m& ^7 s8 P4 B( z# A/ d9 g8 ^//迭代器移动到下一个cell# v) v5 b5 k: [6 Q
int next_cell();) m1 l4 ^! z+ S) U1 V9 P
//获取当前cell的内容
6 m% M5 W7 E6 ~0 i2 \6 B//@param[out]cell_info当前cell的内容,包括表名(table_id),行主键(row_& L0 ~1 S$ u( H" r* Y! X' H6 f2 t6 U
key),列编号(column_id)以及列值(column_value)- b1 z% N: ]4 r4 ]5 @/ o" X
int get_cell(ObCellInfo**cell_info);& ^9 b+ ~" \( t4 W
//获取当前cell的内容& E- y& r- ?6 T
//@param[out]cell_info当前cell的内容
- ^- [! D( K" L9 P0 @//@param is_row_changed是否迭代到下一行
# M; i4 `* l3 s/ t5 A$ g4 w" A% Uint get_cell(ObCellInfo**cell_info,bool*is_row_changed);
; k& c& s- I5 B& {8 ~% |};6 m2 H) |; s4 G1 i( k
读事务返回一个迭代器MemTableIterator,通过它可以不断地获取下一个读到的
/ r/ G, V, J& C0 O6 x$ i1 H; L! D) {cell。在例9-3中,读取编号为1的商品可以得到一个迭代器,从这个迭代器中可以读
" c2 Q- a) _* ]0 c9 J4 F& ]出行操作链中保存的3个Cell,依次为:<update,购买人数,100>,<delete,*
. L5 ], H, T- Z* B>,<update,商品名,“女鞋”>。
6 |0 |7 ]2 h+ \* ~$ f写事务总是批量执行,步骤如下:) _) M) k5 E) \
1)调用start_transaction开始一批写事务,获得事务描述符;4 u& m9 [$ `6 z' d" Y1 f
2)调用start_mutation开始一次写操作;
/ N) p5 h9 i. m( N& K& O. P& j: ?3)执行写操作,将数据写入到MemTable中;# u& |' P/ Q: }! V& z
4)调用end_mutation提交或者回滚一次写操作;如果还有写事务,转到步骤& R( z- s8 m* Q% P
2);
" g! V$ Y3 U5 r O2 A8 Y3 G0 k5)调用end_transaction提交写事务。
# w- b6 M; L% _- q5 e5 ?3.SSTable
5 ^7 h8 K! t, h0 b/ j8 {& J3 g当活跃的MemTable超过一定大小或者管理员主动发起冻结命令时,活跃的
8 m. U! i- E' c. j; SMemTable将被冻结,生成冻结的MemTable,并同时以SSTable的形式转储到SSD磁盘: X- Y o+ Y0 G& |$ }+ j- x
中。# e \8 u0 m0 x) G4 o1 O
SSTable的详细格式请参考9.4节ChunkServer实现机制,与ChunkServer中的
* x% Y/ N2 L! h( NSSTable不同的是,UpdateServer中所有的表格共用一个SSTable,且SSTable为稀疏格
. G4 f6 ?4 U' b" O& A式,也就是说,每一行数据的每一列可能存在,也可能不存在修改操作。( e7 L W2 w* v
另外,OceanBase设计时也尽量避免读取UpdateServer中的SSTable,只要内存足, ^/ l; s% j% j# y% ^
够,冻结的MemTable会保留在内存中,系统会尽快将冻结的数据通过定期合并或者% S) H& w3 Q; W# u
数据分发的方式转移到ChunkServer中去,以后不再需要访问UpdateServer中的
' Z/ E i% o+ D" FSSTable数据。8 f4 \* h) E+ W C4 \4 x
当然,如果内存不够需要丢弃冻结MemTable,大量请求只能读取SSD磁盘,
8 D3 _6 i( L' |+ KUpdateServer性能将大幅下降。因此,希望能够在丢弃冻结MemTable之前将SSTable
) T s7 z1 E: i7 t& P+ ^9 U% U的缓存预热。
, G3 t0 E5 S7 `/ o! l+ VUpdateServer的缓存预热机制实现如下:在丢弃冻结MemTable之前的一段时间! t) p( M0 q4 a7 i r. F
(比如10分钟),每隔一段时间(比如30秒),将一定比率(比如5%)的请求发给4 g/ A# L4 @- x* I% ~
SSTable,而不是冻结MemTable。这样,SSTable上的读请求将从5%到10%,再到
/ z H7 C) W, v8 e; \15%,依次类推,直到100%,很自然地实现了缓存预热。
6 b: _* S6 |1 x% _* K9.3.2 任务模型+ q! V: M" B2 k! v
任务模型包括网络框架、任务队列、工作线程,UpdateServer最初的任务模型基
1 i; \% g5 `: K, K! ~于淘宝网实现的Tbnet框架(已开源,见http://code.taobao.org/p/tb-common-
( x& N+ |, A; K; @utils/src/trunk/tbnet/)。Tbnet封装得很好,使用比较方便,每秒收包个数最多可以达# F* n* c2 K) t$ {9 ]4 R
到接近10万,不过仍然无法完全发挥UpdateServer收发小数据包以及内存服务的特' L/ R2 B4 z7 ?3 S( J' N( D
点。OceanBase后来采用优化过的任务模型Libeasy,小数据包处理能力得到进一步提
; B* k9 J7 e/ r; R8 e5 s, j* }升。
# ^$ c+ c( U4 F+ D! a2 M4 j* N1.Tbnet' n- x: |* N$ F
如图9-5所示,Tbnet队列模型本质上是一个生产者—消费者队列模型,有两个线
; C0 x8 x3 O, F+ _" W1 g9 T程:网络读写线程以及超时检查线程,其中,网络读写线程执行事件循环,当服务, d/ a, Z) a* F; F; Y) P c4 _
器端有可读事件时,调用回调函数读取请求数据包,生成请求任务,并加入到任务
" i" v& J: r1 ?% K5 R! g* W队列中。工作线程从任务队列中获取任务,处理完成后触发可写事件,网络读写线
+ Z' Y4 C/ M! g" ~, x8 [5 d程会将处理结果发送给客户端。超时检查线程用于将超时的请求移除。: R- U6 ~! q# A! ?
图 9-5 Tbnet队列模型
8 v0 M% W( i) N4 f( T ]- KTbnet模型的问题在于多个工作线程从任务队列获取任务需要加锁互斥,这个过3 i# u7 G7 P b' r8 Z5 B
程将产生大量的上下文切换(context switch),测试发现,当UpdateServer每秒处理" `, D7 R+ r+ `1 I1 L, G! t
包的数量超过8万个时,UpdateServer每秒的上下文切换次数接近30万次,在测试环7 Z: }/ d& m" ~3 N* J
境中已经达到极限(测试环境配置:Linux内核2.6.18,CPU为2*Intel Nehalem4 R; ~% g( g e8 @
E5520,共8核16线程)。 p5 C( C; z6 f+ Q w
2.Libeasy
- G M9 k0 G4 S3 N' n: t1 S为了解决收发小数据包带来的上下文切换问题,OceanBase目前采用Libeasy任务! } G- \* J" o9 i4 M. m1 }
模型。Libeasy采用多个线程收发包,增强了网络收发能力,每个线程收到网络包后
2 Q8 a# g- [2 D; c6 q' K6 A" v立即处理,减少了上下文切换,如图9-6所示。
5 n* H$ c* K, h% h# V; f( J图 9-6 Libeasy任务模型! ^" |; e3 X% b
UpdateServer有多个网络读写线程,每个线程通过Linux epool监听一个套接字集% }/ q3 `7 ~+ G* T' E
合上的网络读写事件,每个套接字只能同时分配给一个线程。当网络读写线程收到( Y6 q. T. K0 o7 f8 O
网络包后,立即调用任务处理函数,如果任务处理时间很短,可以很快完成并回复
( J( R } |7 i1 p客户端,不需要加锁,避免了上下文切换。UpdateServer中大部分任务为短任务,比
5 l8 X3 A7 \0 L; u+ ]如随机读取内存表,另外还有少量任务需要等待共享资源上的锁,可以将这些任务9 q; q' P5 _: e; P$ O* `6 S8 \3 Q5 Z- [8 v
加入到长任务队列中,交给专门的长任务处理线程处理。: K/ {) x& d% n7 B
由于每个网络读写线程处理一部分预先分配的套接字,这就可能出现某些套接* O7 E/ F- t: c, k8 ?1 ~+ L
字上请求特别多而导致负载不均衡的情况。例如,有两个网络读写线程thread1和4 X: [ g- O2 t* c$ ^
thread2,其中thread1处理套接字fd1、fd2,thread2处理套接字fd3、fd4,fd1和fd2上每
: r9 @7 v7 t- {7 B秒1000次请求,fd3和fd4上每秒10次请求,两个线程之间的负载很不均衡。为了处理 ~4 w" @8 d% X3 U" H% [+ |
这种情况,Libeasy内部会自动在网络读写线程之间执行负载均衡操作,将套接字从, L& N6 h% ^( y* S3 u
负载较高的线程迁移到负载较低的线程。
; ^. {" j, J2 s- i1 Y' W9.3.3 主备同步: \0 F( x7 T7 i9 I4 I6 a
8.4.1节已经介绍了UpdateServer的一致性选择。OceanBase选择了强一致性,主$ q Y6 x1 S, a% |+ i7 s
UpdateServer往备UpdateServer同步操作日志,如果同步成功,主UpdateServer操作本5 ^7 U; y3 _5 P2 Q! J
地后返回客户端更新成功,否则,主UpdateServer会把备UpdateServer从同步列表中
$ X1 ]2 l+ F2 E7 H8 y6 m剔除。另外,剔除备UpdateServer之前需要通知RootServer,从而防止RootServer将不
/ i' x' I7 \" S5 ~- P& q一致的备UpdateServer选为主UpdateServer。
+ j% t; m, ]& b5 b V, w如图9-7所示,主UpdateServer往备机推送操作日志,备UpdateServer的接收线程# w, a8 M7 G' S+ f
接收日志,并写入到一块全局日志缓冲区中。备UpdateServer只要接收到日志就可以4 ? n2 M* Y# T6 L; o
回复主UpdateServer同步成功,主UpdateServer接着更新本地内存并将日志刷到磁盘
8 Z7 Y8 h6 `( j6 q) B: n, l8 o文件中,最后回复客户端写入操作成功。这种方式实现了强一致性,如果主" [, }3 [4 \* i3 G2 Z: ]4 I( S0 U* o
UpdateServer出现故障,备UpdateServer包含所有的修改操作,因而能够完全无缝地6 D ~& i: y) ~4 B
切换为主UpdateServer继续提供服务。另外,主备同步过程中要求主机刷磁盘文件," ^2 d2 E; Y$ u$ c
备机只需要写内存缓冲区,强同步带来的额外延时也几乎可以忽略。) }7 s' G( {: d6 |) T$ j
图 9-7 UpdateServer主备同步原理
, f- C3 d6 \# I9 b9 K/ l正常情况下,备UpdateServer的日志回放线程会从全局日志缓冲区中读取操作日
8 o( ]7 w0 C8 z3 {$ X志,在内存中回放并同时将操作日志刷到备机的日志文件中。如果发生异常,比如- f( d g+ g5 w% |, D6 I4 D( i3 |
备UpdateServer刚启动或者主备之间网络刚恢复,全局日志缓冲区中没有日志或者日. V. G" o2 c/ U m) i M9 A7 a! C
志不连续,此时,备UpdateServer需要主动请求主UpdateServer拉取操作日志。主
$ Z- p4 p. ~# |+ D; ~- F8 ^" }UpdateServer首先查找日志缓冲区,如果缓冲区中没有数据,还需要读取磁盘日志文
; d0 J5 @) A2 y, l! o) B/ X件,并将操作日志回复备UpdateServer。代码如下:
9 ^8 I2 g9 J) d* M5 W* iclass ObReplayLogSrc
' E1 \$ {3 X) z2 I' ^4 i$ ~{
; l! }" Z2 U, n |! n& Ppublic:
2 m4 g. p. w! }* |4 Y; P. E//读取一批待回放的操作日志- t8 |+ z7 Z. r8 X3 l
//@param[in]start_cursor日志起始点2 e; p+ F1 T2 P6 z% f9 O
//@param[out]end_id读取到的最大日志号加1,即下一次读取的起始日志号* w* O( @5 a- T Y% j, ]
//@param[in]buf日志缓冲区
* V* Y4 ?% D9 B4 w( p//@param[in]len日志缓冲区长度( X& q9 X' I" }6 R2 H* L$ U/ G
//@param[out]read_count读取到的有效字节数
x! _* q$ q- M* dint get_log(const ObLogCursor&start_cursor,int64_t&end_id,char*buf,const
" y) S' r3 R" i8 p- ^6 w% A% U: zint64_t len,int64_t&read_count);1 I9 R2 p# @ [! G) V3 I( @; k* t
};
9 n' z3 f) N U2 n yclass ObUpsLogMgr- @1 h! c# X: B5 M
{1 F, V' }# o/ _: W% c$ y, |
public:
9 E; M" l8 E! S& W7 h4 s2 genum WAIT_SYNC_TYPE" y8 a3 F+ K2 y1 F' ~4 l, \7 @
{
: |8 A, E, n5 t9 f5 N3 u+ sWAIT_NONE=0,. q: c" ]1 |5 R
WAIT_COMMIT=1," @8 p( X3 F5 Z4 t
WAIT_FLUSH=2,5 k8 c% Y$ t0 n# ?# N4 C! X' ?
};# S7 v! ?& b$ \5 C+ F m
public:7 f. }4 j' ~/ r# X
//备UpdateServer接收主UpdateServer发送的操作日志9 T% N/ [& U" F0 S! Z/ A
int slave_receive_log(const char*buf,int64_t len,const int64_t
5 g6 A, r# W u/ s* J7 ~wait_sync_time_us,const WAIT_SYNC_TYPE wait_event_type);
$ i, G, \$ p8 ]: ~. E//备UpdateServer获取并回放操作日志4 s' z3 R* i+ v G) V
int replay_log();
1 x4 g2 N0 V# W0 e# M b: ~};0 m' b6 n& R2 g2 Z. m6 ^. z/ k' ~5 s
备UpdateServer接收到主UpdateServer发送的操作日志后,调用ObUpsLogMgr类的
% |6 _( G0 e% D( pslave_receive_log将操作日志保存到日志缓冲区中。备UpdateServer可以配置成不等待$ z) X% p: ]* m1 v, }7 F' N! S
(WAIT_NONE)、等待提交到MemTable(WAIT_COMMIT)或者等待提交到 y3 i$ `" z$ M' k$ x
MemTable且写入磁盘(WAIT_FLUSH)。另外,备UpdateServer有专门的日志回放线/ h/ x( }3 s* L2 Q* |" j
程不断地调用ObUpsLogMgr中的replay_log函数获取并回放操作日志。/ k2 |4 m; _: L. }: W$ `
备UpdateServer执行replay_log函数时,首先调用ObReplayLogSrc的get_log函数读
u$ u! z8 o r% Q6 j取一批待回放的操作日志,接着,将操作日志应用到MemTable中并写入日志文件持% R9 ]) j. p7 j
久化。Get_log函数执行时首先查看本机的日志缓冲区,如果缓冲区中不存在日志起
$ H- S9 m2 \# T6 {( g W始点(start_cursor)开始的操作日志,那么,生成一个异步任务,读取主% k* J& v1 i; N
UpdateServer。一般情况下,slave_receive_log接收的日志刚加入日志缓冲区就被
' X1 J' n4 [( Q4 b bget_log读走了,不需要读取主UpdateServer。
% w A1 |9 N7 d( J) y& z- g5 }5 r- {
: M6 j% Z9 p! v, w M3 T; T3 Z
|
|