|
9.3 UpdateServer实现机制/ `3 Y2 ~& E$ ]' t* v) G9 C2 N
UpdateServer用于存储增量数据,它是一个单机存储系统,由如下几个部分组8 V0 {1 b4 n& T7 {+ v
成:
" v; ]% J7 X9 c* z1 P9 \+ ~0 S●内存存储引擎,在内存中存储修改增量,支持冻结以及转储操作;
# j7 X; q; Q4 v3 {' K3 l●任务处理模型,包括网络框架、任务队列、工作线程等,针对小数据包做了专
3 H, o( ?9 D$ W& o% a3 s门的优化;1 j0 h1 `# G/ [6 j6 ?
●主备同步模块,将更新事务以操作日志的形式同步到备UpdateServer。6 @. f# f! f! F+ c
UpdateServer是OceanBase性能瓶颈点,核心是高效,实现时对锁(例如,无锁
( N+ I8 C( R1 B# l数据结构)、索引结构、内存占用、任务处理模型以及主备同步都需要做专门的优" x/ V& P- D9 w2 u6 D6 N* n
化。
1 U$ }# M7 c% s- Y9.3.1 存储引擎
" P" \) N2 K4 XUpdateServer存储引擎如图9-3所示。
9 j" {' t3 z* W5 l图 9-3 UpdateServer存储引擎3 F$ U# @- ?4 K6 q
UpdateServer存储引擎与6.1节中提到的Bigtable存储引擎看起来很相似,不同点: W' m' g! ?9 {% n' x1 ~
在于:; U6 }6 _/ y$ w3 `+ n+ g
●UpdateServer只存储了增量修改数据,基线数据以SSTable的形式存储在) T6 D0 B+ D& J5 W6 ~% y; m( F2 l
ChunkServer上,而Bigtable存储引擎同时包含某个子表的基线数据和增量数据;
% [9 N. {8 l8 Y! g' e n8 S3 r9 T●UpdateServer内部所有表格共用MemTable以及SSTable,而Bigtable中每个子表
, \- J5 \ m5 M5 F5 F q7 C1 [的MemTable和SSTable分开存放;5 `& J2 m6 t* o+ B9 P
●UpdateServer的SSTable存储在SSD磁盘中,而Bigtable的SSTable存储在 GFS
0 G! E- v6 M& a( Y/ y中。2 h* t$ R7 l9 N4 w+ v
UpdateServer存储引擎包含几个部分:操作日志、MemTable以及SSTable。更新
' v. w/ Q0 ^% ~) g' w' V# n6 Z/ x操作首先记录到操作日志中,接着更新内存中活跃的MemTable(Active% B) O! ]( i" z1 M, L
MemTable),活跃的MemTable到达一定大小后将被冻结,称为Frozen MemTable,同
$ D' m4 r" Q, J. J4 |- r5 N时创建新的Active MemTable。Frozen MemTable将以SSTable文件的形式转储到SSD磁
5 Q3 u: f4 m4 @0 E) c8 f& h# @) X盘中。
0 [- @3 y& r, s. ~5 u2 M1.操作日志1 c7 V* Y. d# A, {7 U( h/ ^
OceanBase中有一个专门的提交线程负责确定多个写事务的顺序(即事务id),5 Y0 w$ ]- s6 s, Y
将这些写事务的操作追加到日志缓冲区,并将日志缓冲区的内容写入日志文件。为( w$ S3 ]! P: v/ w. s
了防止写操作日志污染操作系统的缓存,写操作日志文件采用Direct IO的方式实现:
. X9 l' d d+ ^class ObLogWriter
1 H6 B: T. z8 y4 B# I3 `{
. ]2 f& h: {1 H6 M3 s; Y; S6 tpublic:# y% I( j' A7 R
//write_log函数将操作日志存入日志缓冲区
8 C2 r/ u1 E1 R4 gint write_log(const LogCommand cmd,const char*log_data,const int64_t
( e- j, q7 W( V; Hdata_len);/ K+ o2 k* ?0 u3 `
//将日志缓冲区中的日志先同步到备机再写入主机磁盘
' a% `+ j# v- j% G0 p1 R/ Gint flush_log(LogBuffer&tlog_buffer,const bool sync_to_slave=true,const bool( V' p- K- X& Y2 e) T9 W
is_master=true);
8 T( F4 d* z+ Y/ X};
4 K1 B5 E; u7 D" \/ @! `5 t每条日志项由四部分组成:日志头+日志序号+日志类型(LogCommand)+日志5 ?! V# p3 N8 |
内容,其中,日志头中记录了每条日志的校验和(checksum)。ObLogWriter中的
) ?; S7 }: W; h+ O0 f& W5 W, G! bwrite_log函数负责将操作日志拷贝到日志缓冲区中,如果日志缓冲区已满,则向调( W0 n" l/ S# W$ p0 T, \
用者返回缓冲区不足(OB_BUF_NOT_ENOUGH)错误码。接着,调用者会通过
( q# j$ ] a( D' w- I7 P! z5 o1 P" Fflush_log将缓冲区中已有的日志内容同步到备机并写入主机磁盘。如果主机磁盘的最$ R% Q+ a& Q, M6 W
后一个日志文件超过指定大小(默认为64MB),还会调用switch_log函数切换日志
" ? Q1 o8 U: ]8 W9 U2 h% m0 \文件。为了提高写性能,UpdateServer实现了成组提交(Group Commit)技术,即首6 C4 Z- M' [3 |9 M
先多次调用write_log函数将多个写操作的日志拷贝到相同的日志缓冲区,接着再调
/ x; d1 o3 j' s0 _9 n8 d7 i用flush_log函数将日志缓冲区中的内容一次性写入到日志文件中。1 o" u l9 R2 l# k2 @
2.MemTable V6 x# s- T3 [
MemTable底层是一个高性能内存B树。MemTable封装了B树,对外提供统一的读
. Q' `, h: ~2 \5 l% ~) K写接口。( q7 i* m: i8 ]3 g) z. R
B树中的每个叶子节点对应MemTable中的一行数据,key为行主键,value为行操
$ b, g2 J' b: A8 W" s* {4 L4 _ s作链表的指针。每行的操作按照时间顺序构成一个行操作链表。- T1 o9 K. m7 r4 f( E$ K9 h5 |* m/ x
如图9-4所示,MemTable的内存结构包含两部分:索引结构以及行操作链表,索
% v2 j( m$ M1 {. D* [引结构为9.1.2节中提到的B树,支持插入、删除、更新、随机读取以及范围查询操
; G9 N1 A: C3 F3 ?5 q( m5 | S' ^作。行操作链表保存的是对某一行各个列(每个行和列确定一个单元,称为Cell)的
6 m3 ~% K7 p8 H- L# |修改操作。
. f2 D% U6 J: L图 9-4 MemTable的内存结构
1 D( A0 y) J0 n J例9-3 对主键为1的商品有3个修改操作,分别是:将商品购买人数修改为: E2 K( l% X4 D: _
100,删除该商品,将商品名称修改为“女鞋”,那么,该商品的行操作链中将保存三0 d$ g0 a% m: s
个Cell,分别为:<update,购买人数,100>、<delete,*>以及<update,商品
7 `" e( V6 @1 _; P% b7 w4 a名,“女鞋”>。
# D. h9 w' ~0 a ZMemTable中存储的是对该商品的所有修改操作,而不是最终结果。另外,
- I# P( q) ?8 ~* J" S- H7 dMemTable删除一行也只是往行操作链表的末尾加入一个逻辑删除标记,即<delete,4 B3 w7 p- i l z. d& m
*>,而不是实际删除索引结构或者行操作链表中的行内容。9 z/ a- F' ?6 A" N9 z" H/ H
MemTable实现时做了很多优化,包括:' ?0 g! i7 D* T( d
●哈希索引:针对主要操作为随机读取的应用,MemTable不仅支持B树索引,还
1 V1 M( ?( h* }' H& r支持哈希索引,UpdateServer内部会保证两个索引之间的一致性。! [% E4 s# r3 O5 d0 f' K
●内存优化:行操作链表中每个cell操作都需要存储操作列的编号
; o3 O& Q# N2 m1 a. A(column_id)、操作类型(更新操作还是删除操作)、操作值以及指向下一个cell操2 W( L0 ~1 k8 ^' c+ x- a+ q! r- J
作的指针,如果不做优化,内存膨胀会很大。为了减少内存占用,MemTable实现时* n& b$ @/ ]% [
会对整数值进行变长编码,并将多个cell操作编码后序列到同一块缓冲区中,共用一6 `) _/ y9 b. ^' v4 ?$ d
个指向下一批cell操作缓冲区的指针:
1 a* I5 ]' Z- astruct ObCellMeta" f) p3 F- x E% a! ]! e7 l
{; m! C9 W% t* z. Y- q5 Q; b3 }" m
const static int64_t TP_INT8=1;//int8整数类型
$ B0 F* P* D; o! Pconst static int64_t TP_INT16=2;//int16整数类型- A+ W+ p1 R8 ?4 i4 K$ ?( S0 D
const static int64_t TP_INT32=3;//int32整数类型
M3 _: x6 ~( k0 {const static int64_t TP_INT64=4;//int64整数类型
# C* O7 V+ A( a1 mconst static int64_t TP_VARCHAR=6;//变长字符串类型9 N) B }& |0 z4 ?+ Z$ |4 D9 d' a! x2 [$ b
const static int64_t TP_DOUBLE=13;//双精度浮点类型
3 A& N0 ~( r, \const static int64_t TP_ESCAPE=0x1f;//扩展类型8 J; n. z# F6 J6 z
const static int64_t ES_DEL_ROW=1;//删除行操作$ g9 w' \0 y' f' Y: I) l i
};6 I9 \1 c- o) |' B( p, q! ?. ?5 Q. h
class ObCompactCellWriter) p( _2 ^ g$ L' j' n/ A) I
{
. @/ W* B' r J2 T' Rpublic:, J; Y9 A9 L+ E" A3 U
//写入更新操作,存储成压缩格式, X: m$ T, B- |" }9 @2 h- d: {- g
int append(uint64_t column_id,const ObObj&value);2 \! a% t; `9 a6 d$ k, G( g, U
//写入删除操作,存储成压缩格式+ D! k9 |0 J8 H( U7 J4 k: N
int row_delete();
" ~) ~. \; m/ k};
, P! T) \* ]( ~: |$ V% b4 mMemTable通过ObCompactCellWriter来将cell操作序列化到内存缓冲区中,如果为% t, h) ?- u5 F" ?
更新操作,调用append函数;如果为删除操作,调用row_delete函数。更新操作的存* I4 h. c: A8 n9 J9 Q! T& K: k8 P
储格式为:数据类型+值+列ID,TP_INT8/TP_INT16/TP_INT32/TP_INT64分别表示8/ r/ |! y' L5 v" X: c9 s* e6 J
位/16位/32位/64位整数类型,TP_VARCHAR表示变长字符串类型,TP_DOUBLE表示
, l' D- P. l6 I: x$ V0 O双精度浮点类型。删除操作为扩展操作,其存储格式为:
5 w" p, G c! [( c* ATP_ESCAPE+ES_DEL_ROW。例9-3中的三个Cell:<update,购买人数,100>、<, A" Q$ w; V; z3 }
delete,*>以及<update,商品名,“女鞋”>在内存缓冲区的存储格式为:3 x! T2 u& U/ K# N
第1~3字节表示第一个Cell,即<update,购买人数,100>;第4~5字节表示第
0 k( W8 u0 Y8 E& a9 G2 `二个cell,即<delete,*>;第6~8字节表示第三个Cell,即<update,商品名,“女- r3 L( I$ C0 ~5 @3 J
鞋”>。
& Q0 N1 _9 c& @# ~7 xMemTable的主要对外接口可以归结如下:
- i8 ^ ~, e1 c//开启一个事务( i8 c8 `0 I& y
//@param[in]trans_type事务类型,可能为读事务或者写事务
& k& a! U: K. I$ {6 B m- t O//@param[out]td返回的事务描述符# z+ A2 D- A2 u' g1 R) S
int start_transaction(const TETransType trans_type,MemTableTransDescriptor&! E" O/ D3 |0 R- x
td);
0 Q+ m1 @ e: h& k' y5 |//提交或者回滚一个事务
" N8 g8 B; j3 a//@param[in]td事务描述符
$ G5 r( F5 @3 X//@param[in]rollback是否回滚,默认为false7 ~9 z3 r# I$ e) ^& D; R+ @
int end_transaction(const MemTableTransDescriptor td,bool rollback=false);. I5 S! @7 @' m( \$ [
//执行随机读取操作,返回一个迭代器% r) }" U% ]/ z6 j E) K) h
//@param[in]td事务描述符4 _7 t& G% {/ S' U! S* d
//@param[in]table_id表格编号' N! [5 e$ X/ `
//@param[in]row_key待查询的主键; @+ \! f, @1 v. j P3 H q
//@param[out]iter返回的迭代器
0 z% L5 g4 ?3 A4 n- H% o2 hint get(const MemTableTransDescriptor td,const uint64_t table_id,const
; G, O+ N. g0 b( U/ q1 AObRowkey&row_key,MemTableIterator&iter);. Y; r t. Q& V% _: r
//执行范围查询操作,返回一个迭代器
* C* J' b: K# g4 c, f. f" c9 x//@param[in]td事务描述符- ?* S1 V$ K/ ], Q- d" h, F2 t/ C
//@param[in]range查询范围,包括起始行、结束行,开区间或者闭区间
: N+ W5 A4 W I' i; ~//@param[out]iter返回的迭代器
$ S! ?: i+ D- _9 d- |: Z$ aint scan(const MemTableTransDescriptor td,const ObRange&0 m3 ^* _* y; C B3 u
range,MemTableIterator&iter);- K. B) m9 w& g7 q' `; }
//开始执行一次修改操作, s0 f+ J& D g% [; D: W e) @
//@param[in]td事务描述符3 C% a7 _# F, o4 T: X
int start_mutation(const MemTableTransDescriptor td);
. p: a& l8 ]6 r//提交或者回滚一次修改操作+ U% f {, V0 z6 E# H
//@param[in]td事务描述符
# U) O2 W' F: m O//@param[in]rollback是否回滚& k5 [5 V- v" Z9 n
int end_mutation(const MemTableTransDescriptor td,bool rollback);* v3 ~0 _4 `- G. l! |. x
//执行修改操作" B: O W: T1 B! G" `
//@param[in]td事务描述符
8 F% g& q0 j; q8 w1 K//@param[in]mutator修改操作,包含一个或者多个对多个表格的cell操作) p7 Q! k# S- m+ ?8 M
int set(const MemTableTransDescriptor td,ObUpsMutator&mutator);+ {/ {* n5 S1 R
对于读事务,操作步骤如下:
8 V/ K2 X+ _2 l# b; }1)调用start_transaction开始一个读事务,获得事务描述符;- r& X" m$ V& x+ o" |7 X- _) x# v E
2)执行随机读取或者扫描操作,返回一个迭代器;接着可以从迭代器不断迭代
: e# q4 c" L9 x" K数据;
+ u0 G: |4 _- Q+ E% ~% z3)调用end_transaction提交或者回滚一个事务。, G) }: ?1 u3 a' g
class MemTableIterator4 \+ R5 @ ^/ u0 l* d) ?
{5 d; ^( G0 ]. k5 H; e. { r
public:
. a5 F" b( W! P2 x/ @//迭代器移动到下一个cell
+ G3 t: r9 a1 g# cint next_cell();8 Y8 ?: n2 n( _3 y
//获取当前cell的内容, w) v8 B! t- a# `& `! k
//@param[out]cell_info当前cell的内容,包括表名(table_id),行主键(row_
2 R5 G @+ v; g/ e5 V* o' Fkey),列编号(column_id)以及列值(column_value)' O* |+ }- a# [% u. y2 ^% C! C8 ?. L
int get_cell(ObCellInfo**cell_info);
) j! l; G0 c, L; A//获取当前cell的内容
7 D! g: U; | \9 t4 r//@param[out]cell_info当前cell的内容" g0 n1 K% n8 Z% Q, s$ }
//@param is_row_changed是否迭代到下一行& _4 B7 C& c: J6 `$ c* Q1 f8 O3 Y
int get_cell(ObCellInfo**cell_info,bool*is_row_changed);9 A0 U: ]" e6 o4 ?
};
1 h7 V3 {9 Z* U! d+ V* j读事务返回一个迭代器MemTableIterator,通过它可以不断地获取下一个读到的5 F7 P/ z; E& ~/ I
cell。在例9-3中,读取编号为1的商品可以得到一个迭代器,从这个迭代器中可以读
: e1 C7 t' t: d3 e! o* u出行操作链中保存的3个Cell,依次为:<update,购买人数,100>,<delete,*
0 ~9 ]) i& f( H6 [>,<update,商品名,“女鞋”>。) {8 b$ G# \* @! Q% {' _$ w
写事务总是批量执行,步骤如下:! c; r( L3 `% Q, ?6 ?2 }1 e" W' Z
1)调用start_transaction开始一批写事务,获得事务描述符;
$ C' }9 M- x9 L' V2 u$ k/ p2)调用start_mutation开始一次写操作;
7 t7 G; J( k4 V" K3)执行写操作,将数据写入到MemTable中;
* k3 t) h* Z: V4)调用end_mutation提交或者回滚一次写操作;如果还有写事务,转到步骤
& K% {" e; k' ~2);7 p$ T' {9 o- O/ g
5)调用end_transaction提交写事务。
4 L* n4 G o' x2 D+ X1 H- _3.SSTable
7 M, }/ _4 l& b- v( |当活跃的MemTable超过一定大小或者管理员主动发起冻结命令时,活跃的. \, `/ I( W; w- d0 d3 B
MemTable将被冻结,生成冻结的MemTable,并同时以SSTable的形式转储到SSD磁盘3 `, z1 p7 X$ ]
中。4 V( x* b A: X
SSTable的详细格式请参考9.4节ChunkServer实现机制,与ChunkServer中的
+ w% x9 B; I) A6 m/ |( xSSTable不同的是,UpdateServer中所有的表格共用一个SSTable,且SSTable为稀疏格 w' |+ }( d& @, H) d/ p
式,也就是说,每一行数据的每一列可能存在,也可能不存在修改操作。- Z% F1 O4 w+ E0 L
另外,OceanBase设计时也尽量避免读取UpdateServer中的SSTable,只要内存足, j- V3 I, Y7 V4 K
够,冻结的MemTable会保留在内存中,系统会尽快将冻结的数据通过定期合并或者
$ I0 @3 K8 d* e. q数据分发的方式转移到ChunkServer中去,以后不再需要访问UpdateServer中的0 [) M' l. J( L9 M! K- e
SSTable数据。" s3 F- h% h2 [5 e( C5 [
当然,如果内存不够需要丢弃冻结MemTable,大量请求只能读取SSD磁盘,( ?9 I3 h f5 J e9 U7 W
UpdateServer性能将大幅下降。因此,希望能够在丢弃冻结MemTable之前将SSTable
+ w6 P) F9 I5 }' M5 ~5 v$ U: Z- L) b* C的缓存预热。) N+ C! i) l1 A+ k v
UpdateServer的缓存预热机制实现如下:在丢弃冻结MemTable之前的一段时间
, K) F) ^% O8 E J& o! x, |(比如10分钟),每隔一段时间(比如30秒),将一定比率(比如5%)的请求发给
+ U! d6 `3 S, h/ G% a: N" SSSTable,而不是冻结MemTable。这样,SSTable上的读请求将从5%到10%,再到( L$ y, y& z3 T
15%,依次类推,直到100%,很自然地实现了缓存预热。
0 F) F, p# Z- j: \9.3.2 任务模型" f# L& V0 Z/ z1 Y% V4 T
任务模型包括网络框架、任务队列、工作线程,UpdateServer最初的任务模型基5 {1 K6 Z9 Y; c" F% x5 @
于淘宝网实现的Tbnet框架(已开源,见http://code.taobao.org/p/tb-common-( z6 A$ @6 m6 h' @, o
utils/src/trunk/tbnet/)。Tbnet封装得很好,使用比较方便,每秒收包个数最多可以达; z0 d% n+ q# j( I) J8 ]
到接近10万,不过仍然无法完全发挥UpdateServer收发小数据包以及内存服务的特
' d0 N+ h' u( v5 d" Z; Z" E点。OceanBase后来采用优化过的任务模型Libeasy,小数据包处理能力得到进一步提3 ^9 R- d8 Y. C$ W
升。
9 J+ {$ J! H; Z+ }! e5 c5 T4 f/ w1 V1.Tbnet5 g% I: P0 k2 t
如图9-5所示,Tbnet队列模型本质上是一个生产者—消费者队列模型,有两个线
. V/ [" y: c1 {, h$ s程:网络读写线程以及超时检查线程,其中,网络读写线程执行事件循环,当服务
4 g5 `% J- q( s* H4 @7 R- u器端有可读事件时,调用回调函数读取请求数据包,生成请求任务,并加入到任务
$ |: I* h) e! Q5 D+ u队列中。工作线程从任务队列中获取任务,处理完成后触发可写事件,网络读写线; |$ s% w2 @5 r( S0 M8 c/ R' I
程会将处理结果发送给客户端。超时检查线程用于将超时的请求移除。. V% @1 O* P! R- n/ H3 t
图 9-5 Tbnet队列模型5 }5 R) d0 {4 D2 ]: N% X, y
Tbnet模型的问题在于多个工作线程从任务队列获取任务需要加锁互斥,这个过
" U# U6 X6 K# _- j2 h程将产生大量的上下文切换(context switch),测试发现,当UpdateServer每秒处理2 o9 h& C6 H- Y8 y" Y* ~ |% e
包的数量超过8万个时,UpdateServer每秒的上下文切换次数接近30万次,在测试环
5 z' f% k. Q$ m- Z+ h" ]) E境中已经达到极限(测试环境配置:Linux内核2.6.18,CPU为2*Intel Nehalem7 x+ u! J" v( \1 t( y
E5520,共8核16线程)。
/ [# I& w( d$ `3 v; E+ m/ U2.Libeasy
4 B* W% {/ } A* n6 j* `) c2 {为了解决收发小数据包带来的上下文切换问题,OceanBase目前采用Libeasy任务
4 b5 t P; s2 w模型。Libeasy采用多个线程收发包,增强了网络收发能力,每个线程收到网络包后* R( R/ f. ]3 q2 N* N7 f
立即处理,减少了上下文切换,如图9-6所示。
* a, x- f5 u$ d9 c4 X! B1 d图 9-6 Libeasy任务模型+ T' R" W7 a! W4 T# L9 e+ A
UpdateServer有多个网络读写线程,每个线程通过Linux epool监听一个套接字集: }5 ~% e1 @% F \- J9 o
合上的网络读写事件,每个套接字只能同时分配给一个线程。当网络读写线程收到
- E7 U. m; z4 m$ k+ y& k网络包后,立即调用任务处理函数,如果任务处理时间很短,可以很快完成并回复
J( t5 S* L n+ l+ l客户端,不需要加锁,避免了上下文切换。UpdateServer中大部分任务为短任务,比
$ ?# Q) O7 o' ~- k; Z; N9 A如随机读取内存表,另外还有少量任务需要等待共享资源上的锁,可以将这些任务: T# g( g5 h8 Z4 Z4 S+ y" O% ?
加入到长任务队列中,交给专门的长任务处理线程处理。
- A* ]" n" x9 G A7 Z [由于每个网络读写线程处理一部分预先分配的套接字,这就可能出现某些套接% i ? Z2 J+ `4 X/ r
字上请求特别多而导致负载不均衡的情况。例如,有两个网络读写线程thread1和. T$ ^# F( U2 e$ E& M. p; S1 y
thread2,其中thread1处理套接字fd1、fd2,thread2处理套接字fd3、fd4,fd1和fd2上每
3 D& G; a. a, c/ f; W秒1000次请求,fd3和fd4上每秒10次请求,两个线程之间的负载很不均衡。为了处理) A8 }1 P# l9 ^
这种情况,Libeasy内部会自动在网络读写线程之间执行负载均衡操作,将套接字从
$ W p ?9 t6 r5 p$ z负载较高的线程迁移到负载较低的线程。, q3 c1 j8 c( R: d* Q% K* A
9.3.3 主备同步
) K+ k. { m7 I) D3 ?, B: V8.4.1节已经介绍了UpdateServer的一致性选择。OceanBase选择了强一致性,主
- H- Y) X+ G( bUpdateServer往备UpdateServer同步操作日志,如果同步成功,主UpdateServer操作本/ E4 Z' N/ M, l l* S! v
地后返回客户端更新成功,否则,主UpdateServer会把备UpdateServer从同步列表中
! V9 P# b# O( ~3 u! O# ^- h剔除。另外,剔除备UpdateServer之前需要通知RootServer,从而防止RootServer将不2 o0 h# E& Z8 r3 w' E3 B
一致的备UpdateServer选为主UpdateServer。
& K7 V$ l1 P6 Y/ y" b2 C! c4 a如图9-7所示,主UpdateServer往备机推送操作日志,备UpdateServer的接收线程
2 n; B' o6 G- j" k* P接收日志,并写入到一块全局日志缓冲区中。备UpdateServer只要接收到日志就可以& s# P* ]' a6 y5 Q* P, N
回复主UpdateServer同步成功,主UpdateServer接着更新本地内存并将日志刷到磁盘% j" D" ~9 p* [) ~
文件中,最后回复客户端写入操作成功。这种方式实现了强一致性,如果主
* O+ b: g- I' l5 j- @UpdateServer出现故障,备UpdateServer包含所有的修改操作,因而能够完全无缝地
$ v9 P$ }# m1 s! E切换为主UpdateServer继续提供服务。另外,主备同步过程中要求主机刷磁盘文件,% U `# X! ?/ K5 h# ^8 T6 {
备机只需要写内存缓冲区,强同步带来的额外延时也几乎可以忽略。 t& m! D3 ~$ U5 L: D& X
图 9-7 UpdateServer主备同步原理
" F" ~' P9 q( K5 ?' T6 z- p正常情况下,备UpdateServer的日志回放线程会从全局日志缓冲区中读取操作日# t8 Q8 c- A3 T( i4 e8 S9 F( p
志,在内存中回放并同时将操作日志刷到备机的日志文件中。如果发生异常,比如9 B! j! \5 z. v+ \0 W
备UpdateServer刚启动或者主备之间网络刚恢复,全局日志缓冲区中没有日志或者日. ?; n/ ?4 k& a7 P. |1 Z$ t( D
志不连续,此时,备UpdateServer需要主动请求主UpdateServer拉取操作日志。主3 K6 \. L% E4 F+ n2 C0 s
UpdateServer首先查找日志缓冲区,如果缓冲区中没有数据,还需要读取磁盘日志文
) |* N) ]9 z+ q& O. a" e# r7 j! ^件,并将操作日志回复备UpdateServer。代码如下:! M# G& W1 T9 i8 n/ ~3 S
class ObReplayLogSrc
5 `& v. n" j& x5 N9 ?{8 K5 U, F- g, x2 b
public:
1 c. w4 d( ^# g4 R//读取一批待回放的操作日志
1 E% h& B+ M: B+ [0 b9 _3 i//@param[in]start_cursor日志起始点
4 A: n" y7 Y9 P/ U! ]//@param[out]end_id读取到的最大日志号加1,即下一次读取的起始日志号
C3 c/ M o2 I/ I//@param[in]buf日志缓冲区
8 Y' C, I9 j6 j7 l6 N//@param[in]len日志缓冲区长度
; `. c" F2 P+ |( M. o//@param[out]read_count读取到的有效字节数
9 m0 Q* m- Y* k3 ~4 J$ F7 Nint get_log(const ObLogCursor&start_cursor,int64_t&end_id,char*buf,const
! J' K1 H- P% P) hint64_t len,int64_t&read_count);. S* N0 }" N5 h) O
};7 d( ~$ [$ A2 }9 V, q* F0 R5 S
class ObUpsLogMgr
0 c. O t. _* c; ^/ }9 ?3 Y{/ C2 C+ Q# d8 B
public:: A6 i8 `5 ?4 F- X/ F+ [2 z$ X
enum WAIT_SYNC_TYPE5 F3 S: e1 j' t+ [2 q
{. F. e+ _" z$ X1 @+ r& n- ^' K
WAIT_NONE=0,
F1 F; M. ~% CWAIT_COMMIT=1,$ i+ p8 \+ `( Y( P8 s8 J4 j' c
WAIT_FLUSH=2,
- a7 G+ D4 T1 l" t/ r};
5 @+ K+ ?8 c1 H, k+ ?) T% d& g7 [public: O8 q! V9 r2 ?' F2 O4 f) F
//备UpdateServer接收主UpdateServer发送的操作日志" E/ u% K, B5 }0 X/ }. m
int slave_receive_log(const char*buf,int64_t len,const int64_t2 |6 `9 N# g/ \
wait_sync_time_us,const WAIT_SYNC_TYPE wait_event_type);9 k* |4 L0 L+ s' _* i2 E/ v$ h+ A
//备UpdateServer获取并回放操作日志 f5 J1 v6 ]' `
int replay_log();8 O8 W0 s5 ]( Q5 F5 L% J/ U2 `; X
};
0 K; s3 t% K* q' p# C. Z, X% \7 [1 W备UpdateServer接收到主UpdateServer发送的操作日志后,调用ObUpsLogMgr类的
4 W$ X5 s8 U; Y) Sslave_receive_log将操作日志保存到日志缓冲区中。备UpdateServer可以配置成不等待
1 s. n3 m# O" g+ g/ C# A3 |(WAIT_NONE)、等待提交到MemTable(WAIT_COMMIT)或者等待提交到# G+ [) d8 Q, u/ O) h
MemTable且写入磁盘(WAIT_FLUSH)。另外,备UpdateServer有专门的日志回放线3 E; [7 B0 c$ h9 |( z
程不断地调用ObUpsLogMgr中的replay_log函数获取并回放操作日志。1 s' G, I9 N& A4 v( f0 @8 I
备UpdateServer执行replay_log函数时,首先调用ObReplayLogSrc的get_log函数读8 ~2 X% S- o8 o; B; `
取一批待回放的操作日志,接着,将操作日志应用到MemTable中并写入日志文件持
* b$ v) s$ X5 r2 r久化。Get_log函数执行时首先查看本机的日志缓冲区,如果缓冲区中不存在日志起
3 R+ i8 D. B, M* x始点(start_cursor)开始的操作日志,那么,生成一个异步任务,读取主, B/ ?( ~8 }& w5 d# @' o
UpdateServer。一般情况下,slave_receive_log接收的日志刚加入日志缓冲区就被" C' c6 W; H/ I- ?. C7 T# A
get_log读走了,不需要读取主UpdateServer。. l+ a) |6 I; ^2 i4 j2 Y$ A3 X0 [
1 O; C& _+ R1 D2 Y4 P
! z+ a2 H, n- ]# u4 q |
|