|
9.3 UpdateServer实现机制4 w1 |- p% w0 D1 Z* R! V. s
UpdateServer用于存储增量数据,它是一个单机存储系统,由如下几个部分组
0 W) Z: b! ~! y# q6 y/ u成:& Q! W/ e# ^' t5 M! j: V
●内存存储引擎,在内存中存储修改增量,支持冻结以及转储操作;# `) g% y6 w5 ~/ K5 M
●任务处理模型,包括网络框架、任务队列、工作线程等,针对小数据包做了专3 p+ J S2 F( h: t1 a6 J0 ^0 u+ g
门的优化; I0 R* Z- w5 p% U
●主备同步模块,将更新事务以操作日志的形式同步到备UpdateServer。
, w/ I& ^; e$ c/ _+ c oUpdateServer是OceanBase性能瓶颈点,核心是高效,实现时对锁(例如,无锁* e9 a% v4 n+ V: w- }% b/ O
数据结构)、索引结构、内存占用、任务处理模型以及主备同步都需要做专门的优
) j% E7 }6 Q' Z' [' m* m5 u; A; p化。
1 K; p+ [* ?5 s1 N5 [9.3.1 存储引擎/ J3 k+ S# y0 X! K7 O7 |
UpdateServer存储引擎如图9-3所示。! D$ e3 F* C+ U/ |6 N' ^4 S8 N. h
图 9-3 UpdateServer存储引擎
& }$ m/ i$ Q/ s1 d5 d: L/ wUpdateServer存储引擎与6.1节中提到的Bigtable存储引擎看起来很相似,不同点- f5 N. Z5 E+ z, G) H
在于:
; U C7 y# D5 p●UpdateServer只存储了增量修改数据,基线数据以SSTable的形式存储在
' u6 D. U" T6 |8 w, g/ C, }ChunkServer上,而Bigtable存储引擎同时包含某个子表的基线数据和增量数据;
# k8 M' U% m2 m1 L8 x●UpdateServer内部所有表格共用MemTable以及SSTable,而Bigtable中每个子表
: Q7 a9 G$ V' Q1 j1 M的MemTable和SSTable分开存放;% N2 {9 K4 J; k; J u: T0 F; J
●UpdateServer的SSTable存储在SSD磁盘中,而Bigtable的SSTable存储在 GFS
5 L' T' C `& D/ n6 t中。
2 [& Z+ E+ W- H! w7 s% BUpdateServer存储引擎包含几个部分:操作日志、MemTable以及SSTable。更新
; X% a/ o+ Z: u5 [操作首先记录到操作日志中,接着更新内存中活跃的MemTable(Active
) o7 M0 y$ b1 o4 K- YMemTable),活跃的MemTable到达一定大小后将被冻结,称为Frozen MemTable,同0 k& Z( I [3 `7 l# O" c
时创建新的Active MemTable。Frozen MemTable将以SSTable文件的形式转储到SSD磁
' r f) i* F* }. O盘中。5 E# x- c2 ~- i4 X9 a2 U
1.操作日志
3 d4 g: Q' N) o( f1 W( ~8 r" NOceanBase中有一个专门的提交线程负责确定多个写事务的顺序(即事务id),
* ^7 ~1 K8 s$ }: z9 a将这些写事务的操作追加到日志缓冲区,并将日志缓冲区的内容写入日志文件。为6 o1 O2 @9 z* S1 V9 ^# r8 S9 U% G
了防止写操作日志污染操作系统的缓存,写操作日志文件采用Direct IO的方式实现:9 [% }' c; d% U1 A
class ObLogWriter
, Y& C d; F+ I$ T{+ {+ e* q: B& |7 S6 W# t8 O
public:) R' T! v/ I# g
//write_log函数将操作日志存入日志缓冲区
6 H# z1 e/ N2 }) F0 d1 E5 |' n8 v1 _# Lint write_log(const LogCommand cmd,const char*log_data,const int64_t( G) w' m$ r+ z0 J* G5 b
data_len);
! W+ x9 O! d, s; L2 c. K//将日志缓冲区中的日志先同步到备机再写入主机磁盘$ i% [+ @& T; N& y, A; S6 k
int flush_log(LogBuffer&tlog_buffer,const bool sync_to_slave=true,const bool5 A) k" g- V/ J' s" k
is_master=true);2 [: H, Y9 g, |9 H* s* ?* C0 s+ n' Y
};6 t6 e% Z) U+ w3 Q' V
每条日志项由四部分组成:日志头+日志序号+日志类型(LogCommand)+日志1 R- B2 H* R+ a1 p8 `
内容,其中,日志头中记录了每条日志的校验和(checksum)。ObLogWriter中的, s' U8 g, J* H3 v2 V
write_log函数负责将操作日志拷贝到日志缓冲区中,如果日志缓冲区已满,则向调 u" x/ Q* u1 I" [+ t
用者返回缓冲区不足(OB_BUF_NOT_ENOUGH)错误码。接着,调用者会通过
, h, d8 J x, M( S% e* Xflush_log将缓冲区中已有的日志内容同步到备机并写入主机磁盘。如果主机磁盘的最
9 p2 n; \6 g: O3 d# g后一个日志文件超过指定大小(默认为64MB),还会调用switch_log函数切换日志
- \2 m B7 e" _( P. F文件。为了提高写性能,UpdateServer实现了成组提交(Group Commit)技术,即首) ?4 e3 j5 n% u) X
先多次调用write_log函数将多个写操作的日志拷贝到相同的日志缓冲区,接着再调* z$ Y2 \# w$ V& u% @
用flush_log函数将日志缓冲区中的内容一次性写入到日志文件中。
5 |" I% F" M: @2.MemTable
* I) M/ d* \3 NMemTable底层是一个高性能内存B树。MemTable封装了B树,对外提供统一的读
" q) r: J) x w5 o( n8 e写接口。
8 O! f0 o0 H9 Z( O- H/ C! a9 hB树中的每个叶子节点对应MemTable中的一行数据,key为行主键,value为行操
" r# V# c1 @& d4 I- }! q; A. ?. W5 ?作链表的指针。每行的操作按照时间顺序构成一个行操作链表。9 i M+ S* s5 E
如图9-4所示,MemTable的内存结构包含两部分:索引结构以及行操作链表,索
, p1 b' l5 k8 u d3 K2 W引结构为9.1.2节中提到的B树,支持插入、删除、更新、随机读取以及范围查询操7 W m8 ]# u2 o- D1 p! p% U
作。行操作链表保存的是对某一行各个列(每个行和列确定一个单元,称为Cell)的
. V: Q( ?( F3 m6 h* Y修改操作。
B3 Q' ?7 |3 j9 Y% ]* Q7 e图 9-4 MemTable的内存结构
: W! v' z* {9 B' ?/ `' h例9-3 对主键为1的商品有3个修改操作,分别是:将商品购买人数修改为
( I3 s0 n( J: k/ F2 D+ S& n5 I100,删除该商品,将商品名称修改为“女鞋”,那么,该商品的行操作链中将保存三* r2 W3 O( l* o; T& Q% \, A
个Cell,分别为:<update,购买人数,100>、<delete,*>以及<update,商品1 W" C2 U' u! }: |+ E
名,“女鞋”>。1 h3 T4 A$ U% f7 B3 i. c2 L# @( I/ l
MemTable中存储的是对该商品的所有修改操作,而不是最终结果。另外,
8 p' I4 G% H! _; C, @( X: e1 B" zMemTable删除一行也只是往行操作链表的末尾加入一个逻辑删除标记,即<delete,& c) Q7 q& D, i& |/ @$ U: X; L# l* Y
*>,而不是实际删除索引结构或者行操作链表中的行内容。
( A9 K) U# }9 @9 R5 I! g/ }MemTable实现时做了很多优化,包括:
+ Z3 r7 ?6 X0 m" p4 b1 t4 g●哈希索引:针对主要操作为随机读取的应用,MemTable不仅支持B树索引,还
5 H# v% o; m0 F1 U, ?, V支持哈希索引,UpdateServer内部会保证两个索引之间的一致性。
* o5 x# L: R& L$ N4 }# j●内存优化:行操作链表中每个cell操作都需要存储操作列的编号
6 \0 G" _/ J9 [" ^- f(column_id)、操作类型(更新操作还是删除操作)、操作值以及指向下一个cell操
7 [$ a2 N7 \+ `1 [8 \! H) l) ?* P作的指针,如果不做优化,内存膨胀会很大。为了减少内存占用,MemTable实现时- A, O: Z3 K9 H8 w
会对整数值进行变长编码,并将多个cell操作编码后序列到同一块缓冲区中,共用一
" }2 b' O' n% y! A. w0 D; r3 s3 _, [个指向下一批cell操作缓冲区的指针:
% u- @. r! Y! B( M. astruct ObCellMeta
# p3 e/ H! ]- y6 ?{/ `( V ]; C5 H
const static int64_t TP_INT8=1;//int8整数类型3 s0 W. r, f0 ^% G& ?! r
const static int64_t TP_INT16=2;//int16整数类型
; I% u/ c: e# ?# Lconst static int64_t TP_INT32=3;//int32整数类型! _2 H# P3 f0 `6 X# O0 P: ?
const static int64_t TP_INT64=4;//int64整数类型
9 }) x8 V/ ]$ |5 `: X6 w& tconst static int64_t TP_VARCHAR=6;//变长字符串类型
! q7 `* o9 U% ~" p% J* dconst static int64_t TP_DOUBLE=13;//双精度浮点类型$ ` y' ]0 |8 {: r& {! F: j
const static int64_t TP_ESCAPE=0x1f;//扩展类型
5 m3 [- N' p+ lconst static int64_t ES_DEL_ROW=1;//删除行操作
* q' H% n$ t9 d( U2 C! |* p};) P- A# T( l% w7 a J9 R+ R7 g
class ObCompactCellWriter# Y( U) ^1 X* V- L& s3 b( x
{
+ Z- D! _, D% R# x) X5 qpublic:+ u3 x5 J% p- J6 l
//写入更新操作,存储成压缩格式1 ?4 L5 g B$ ?4 a4 t
int append(uint64_t column_id,const ObObj&value);
# P4 F2 B( x3 N2 v; y//写入删除操作,存储成压缩格式2 \4 f9 [5 ^$ ?% n. S1 N! `* O9 k& l
int row_delete();2 M0 C: W/ @; U
};5 O) l# |8 E5 H" P
MemTable通过ObCompactCellWriter来将cell操作序列化到内存缓冲区中,如果为3 z' _ q1 s1 M* r1 [
更新操作,调用append函数;如果为删除操作,调用row_delete函数。更新操作的存
! j2 o- i$ T; K, M储格式为:数据类型+值+列ID,TP_INT8/TP_INT16/TP_INT32/TP_INT64分别表示8
6 j9 A! T$ @' X位/16位/32位/64位整数类型,TP_VARCHAR表示变长字符串类型,TP_DOUBLE表示
* H, [$ t U7 P双精度浮点类型。删除操作为扩展操作,其存储格式为:
+ }% w+ J& D) }8 t$ S' MTP_ESCAPE+ES_DEL_ROW。例9-3中的三个Cell:<update,购买人数,100>、<0 @/ l3 W3 d# |6 C; k0 g
delete,*>以及<update,商品名,“女鞋”>在内存缓冲区的存储格式为:; O; b% P6 S% r t( g3 N1 O
第1~3字节表示第一个Cell,即<update,购买人数,100>;第4~5字节表示第
6 {& O% S' v7 x8 \% _2 O: ]二个cell,即<delete,*>;第6~8字节表示第三个Cell,即<update,商品名,“女
6 G _! Z0 A3 }9 N& T. Y鞋”>。
3 M- ? K- D, n- SMemTable的主要对外接口可以归结如下:
- u) t2 v! h$ {: r- Q4 c3 m& |) ?//开启一个事务
5 E4 Y# Z- f& R9 I( h! {' `//@param[in]trans_type事务类型,可能为读事务或者写事务
0 b" C" o$ C6 j' ]# U3 L+ f//@param[out]td返回的事务描述符
; W* w+ U6 m' }) Zint start_transaction(const TETransType trans_type,MemTableTransDescriptor&* S9 b3 N) r9 `; B5 L$ B. l6 P. o$ k
td);! {+ T0 R9 M4 ?, G# t9 E" c
//提交或者回滚一个事务
. V$ Q4 u8 A g) z//@param[in]td事务描述符, g1 U! l" ]; B/ `# O
//@param[in]rollback是否回滚,默认为false
) K; w$ Y! g6 s9 P- H' a/ Lint end_transaction(const MemTableTransDescriptor td,bool rollback=false);2 V% e; M* t3 L9 j |( A7 o0 F
//执行随机读取操作,返回一个迭代器
, P7 W& J' n/ X4 h$ F+ S% v//@param[in]td事务描述符
" K' p' w; f8 w//@param[in]table_id表格编号5 |- |- Q3 W3 U% _* [
//@param[in]row_key待查询的主键
' w7 O2 }1 s3 Z! e) q# `% r: |$ Q6 H//@param[out]iter返回的迭代器
& w0 d d/ L, R/ O6 Sint get(const MemTableTransDescriptor td,const uint64_t table_id,const# R. D$ f- v7 M% U3 a
ObRowkey&row_key,MemTableIterator&iter);. t# K9 D. J l- r# I
//执行范围查询操作,返回一个迭代器! I' Q. d3 t$ `) @
//@param[in]td事务描述符
% I3 n7 I& k- `+ v' Z//@param[in]range查询范围,包括起始行、结束行,开区间或者闭区间$ D, k" b$ n* `4 k2 t# |
//@param[out]iter返回的迭代器0 s1 i5 Q" n1 W4 ]9 o
int scan(const MemTableTransDescriptor td,const ObRange&
$ y/ P; W w3 Y- m. `# w" I7 b' B+ j# Wrange,MemTableIterator&iter);1 T' a1 @! V/ z6 w) E
//开始执行一次修改操作
: _! }' e& w& a6 g0 y//@param[in]td事务描述符
8 Z# j' m6 g0 wint start_mutation(const MemTableTransDescriptor td);: h% n% |" \1 H8 r6 m( f8 D+ D
//提交或者回滚一次修改操作, t: J3 x4 ^! T2 z l# x6 p
//@param[in]td事务描述符
8 N& u( q5 _: b9 q% m3 k//@param[in]rollback是否回滚
( D' g9 Z) @" ~7 `9 t! m' wint end_mutation(const MemTableTransDescriptor td,bool rollback);, p7 y( k z5 R2 z4 t; L& `
//执行修改操作4 O4 a, q4 ^1 B8 F9 \6 G, X- q
//@param[in]td事务描述符
/ h6 A. A G, u2 W8 Y0 e//@param[in]mutator修改操作,包含一个或者多个对多个表格的cell操作8 @" _ h* G* \! s/ m" b; q; d
int set(const MemTableTransDescriptor td,ObUpsMutator&mutator);
" n: a+ r- T; V对于读事务,操作步骤如下:
: x F, O1 f3 {1)调用start_transaction开始一个读事务,获得事务描述符;
4 `. I1 F/ N7 `" B. m6 ?1 I$ T* @2)执行随机读取或者扫描操作,返回一个迭代器;接着可以从迭代器不断迭代: s, ^5 ^) t8 G0 h" p$ ^3 u
数据;* x! f) I6 a4 o( W$ u/ d% X4 T
3)调用end_transaction提交或者回滚一个事务。
% [$ A' |+ D3 K* f6 Lclass MemTableIterator+ g4 A8 \2 Y* f1 M: I1 `' r
{
' c# K3 d; w% n, g& K2 n# p/ fpublic:. e2 ^" T& `5 a9 K# r! c
//迭代器移动到下一个cell
7 Q5 h3 b) r5 C2 R1 Fint next_cell();% S! w h7 u! \* I8 @
//获取当前cell的内容9 ]( r+ v$ |/ P8 P& f8 E Q
//@param[out]cell_info当前cell的内容,包括表名(table_id),行主键(row_, q0 x; [6 w9 l9 u7 B$ p3 [
key),列编号(column_id)以及列值(column_value)
( u: x6 ^/ e4 c- j& e+ {int get_cell(ObCellInfo**cell_info);
0 P" w0 Z- z$ d2 _! U t: n& m//获取当前cell的内容
1 G3 d% Q- _( s2 G# _' B* Y+ S//@param[out]cell_info当前cell的内容 g1 g/ I8 z9 N" ~" l
//@param is_row_changed是否迭代到下一行; R* g0 z7 `* R0 h
int get_cell(ObCellInfo**cell_info,bool*is_row_changed);. d0 j1 }9 b1 P9 C
};& N) ^* ]' z8 [, p* n) v
读事务返回一个迭代器MemTableIterator,通过它可以不断地获取下一个读到的
5 J! S+ d. D9 P) Bcell。在例9-3中,读取编号为1的商品可以得到一个迭代器,从这个迭代器中可以读
" ^* ]7 s: c; p出行操作链中保存的3个Cell,依次为:<update,购买人数,100>,<delete,*
: z& T4 S# _' P1 F7 o>,<update,商品名,“女鞋”>。
3 {3 g& T4 N$ t6 P: p写事务总是批量执行,步骤如下:
% J4 e$ G& u: Y7 g8 J# G+ g1)调用start_transaction开始一批写事务,获得事务描述符;
+ [& Y7 |) ]1 s. W* V2)调用start_mutation开始一次写操作;4 u. l! u( U( x' V/ E w: \
3)执行写操作,将数据写入到MemTable中;7 ?, N3 R/ h( |
4)调用end_mutation提交或者回滚一次写操作;如果还有写事务,转到步骤7 e& i7 c+ d: c
2);$ I# }/ x' ~& L1 C {
5)调用end_transaction提交写事务。
5 o5 @/ I/ j% H7 r E- L% r3.SSTable
6 P& i& O4 l$ W5 C" ?当活跃的MemTable超过一定大小或者管理员主动发起冻结命令时,活跃的
9 R- X. f! @8 C! rMemTable将被冻结,生成冻结的MemTable,并同时以SSTable的形式转储到SSD磁盘! }8 |! u4 z8 E. o" u9 a$ D2 G
中。
6 I7 F- f& }5 c$ C2 N4 sSSTable的详细格式请参考9.4节ChunkServer实现机制,与ChunkServer中的
4 s7 q* j5 W2 x- B6 T1 ~SSTable不同的是,UpdateServer中所有的表格共用一个SSTable,且SSTable为稀疏格
; f6 b7 V f1 h式,也就是说,每一行数据的每一列可能存在,也可能不存在修改操作。
. A! n; N: ?2 k7 n# H另外,OceanBase设计时也尽量避免读取UpdateServer中的SSTable,只要内存足% Z! m3 l4 }( j6 t
够,冻结的MemTable会保留在内存中,系统会尽快将冻结的数据通过定期合并或者4 ^4 a- }+ O4 s$ P' h! E" Z
数据分发的方式转移到ChunkServer中去,以后不再需要访问UpdateServer中的# y; C0 }7 G1 q" S5 K
SSTable数据。5 s7 T9 Z0 V& }8 B0 _- q
当然,如果内存不够需要丢弃冻结MemTable,大量请求只能读取SSD磁盘,# i( A# D+ F% X2 m1 B
UpdateServer性能将大幅下降。因此,希望能够在丢弃冻结MemTable之前将SSTable
. K) V! l7 E2 W9 R2 ~的缓存预热。
- V5 Y. W# w, ?4 c9 a6 x3 GUpdateServer的缓存预热机制实现如下:在丢弃冻结MemTable之前的一段时间/ `4 |0 X$ X) q; K0 v+ Z
(比如10分钟),每隔一段时间(比如30秒),将一定比率(比如5%)的请求发给' c* _# e+ _. H/ U m
SSTable,而不是冻结MemTable。这样,SSTable上的读请求将从5%到10%,再到. [; R" H4 i& r# J/ x% S6 y5 T8 E
15%,依次类推,直到100%,很自然地实现了缓存预热。
: Z& w, O1 J( t- W6 A4 b9.3.2 任务模型4 d l8 S3 z8 q' N0 r- X2 @
任务模型包括网络框架、任务队列、工作线程,UpdateServer最初的任务模型基- J6 p" z& z% {7 e
于淘宝网实现的Tbnet框架(已开源,见http://code.taobao.org/p/tb-common-
. i3 u5 N( ^7 S% xutils/src/trunk/tbnet/)。Tbnet封装得很好,使用比较方便,每秒收包个数最多可以达
0 I' u2 h1 X9 F: R. p7 E4 ^; g到接近10万,不过仍然无法完全发挥UpdateServer收发小数据包以及内存服务的特. Z+ D! n3 p" q) U% Z9 p
点。OceanBase后来采用优化过的任务模型Libeasy,小数据包处理能力得到进一步提& h4 C v: K8 I1 R
升。
! V; a6 F' r' S+ f2 P% y1.Tbnet% y# z( |( k# P: o9 l5 ]% J, J
如图9-5所示,Tbnet队列模型本质上是一个生产者—消费者队列模型,有两个线! ?' |* ~. Z1 U% H: m" o- `+ I3 u
程:网络读写线程以及超时检查线程,其中,网络读写线程执行事件循环,当服务
\/ _/ j b y器端有可读事件时,调用回调函数读取请求数据包,生成请求任务,并加入到任务& E. ?7 [2 Q( m- G% X+ `
队列中。工作线程从任务队列中获取任务,处理完成后触发可写事件,网络读写线6 k' ~% I% h2 w- G$ y0 ^/ Y6 a
程会将处理结果发送给客户端。超时检查线程用于将超时的请求移除。: Z# B* |5 l9 A$ b O- g. ^5 H
图 9-5 Tbnet队列模型6 G N9 [: O1 f+ ^4 M# j8 [0 S, U( X
Tbnet模型的问题在于多个工作线程从任务队列获取任务需要加锁互斥,这个过
' K8 D* W: }/ Y" \1 w& X6 v" e程将产生大量的上下文切换(context switch),测试发现,当UpdateServer每秒处理& s! c) |/ f/ n7 L; `
包的数量超过8万个时,UpdateServer每秒的上下文切换次数接近30万次,在测试环
: K; r% o( b' n9 y$ Q5 M境中已经达到极限(测试环境配置:Linux内核2.6.18,CPU为2*Intel Nehalem; b" f+ v4 d6 ~4 L6 W4 u/ \9 U
E5520,共8核16线程)。
4 z7 U0 ]) e6 \3 p4 M- `2.Libeasy. @, u# V; O2 d- Q6 J3 E8 j9 Z" F3 H
为了解决收发小数据包带来的上下文切换问题,OceanBase目前采用Libeasy任务
( P6 b) ?- q9 U5 L' R7 P模型。Libeasy采用多个线程收发包,增强了网络收发能力,每个线程收到网络包后
$ _2 |$ Y2 [* |* v立即处理,减少了上下文切换,如图9-6所示。1 V9 u/ U: q/ E: ~ E
图 9-6 Libeasy任务模型
" D/ g' B& B$ ZUpdateServer有多个网络读写线程,每个线程通过Linux epool监听一个套接字集8 Z' {- ^# a' t1 X! V; U! h
合上的网络读写事件,每个套接字只能同时分配给一个线程。当网络读写线程收到4 @1 m; Z4 G( n l b4 H5 d* e
网络包后,立即调用任务处理函数,如果任务处理时间很短,可以很快完成并回复
7 |2 d: g8 ~! l/ O- t, {4 U客户端,不需要加锁,避免了上下文切换。UpdateServer中大部分任务为短任务,比
@1 L; x; j i0 g' K$ k如随机读取内存表,另外还有少量任务需要等待共享资源上的锁,可以将这些任务
/ ]7 _9 r7 B* x加入到长任务队列中,交给专门的长任务处理线程处理。
4 N# [) R8 C7 l% p5 C由于每个网络读写线程处理一部分预先分配的套接字,这就可能出现某些套接
8 K6 P/ _/ C( _1 l5 s/ i; `* E字上请求特别多而导致负载不均衡的情况。例如,有两个网络读写线程thread1和/ y* N# `) M5 ?# F- p% T7 P
thread2,其中thread1处理套接字fd1、fd2,thread2处理套接字fd3、fd4,fd1和fd2上每
( G- Y! c" q# x, o/ z秒1000次请求,fd3和fd4上每秒10次请求,两个线程之间的负载很不均衡。为了处理
+ X9 ?- V* M2 I" M3 N6 L这种情况,Libeasy内部会自动在网络读写线程之间执行负载均衡操作,将套接字从5 w8 X8 w. I4 R
负载较高的线程迁移到负载较低的线程。1 [8 O6 {1 {: k7 o
9.3.3 主备同步: | J# B% z6 e4 j' v
8.4.1节已经介绍了UpdateServer的一致性选择。OceanBase选择了强一致性,主) J) U/ K9 @9 U: ?- `7 ?
UpdateServer往备UpdateServer同步操作日志,如果同步成功,主UpdateServer操作本$ G3 d% z% O* B4 i, ^" S
地后返回客户端更新成功,否则,主UpdateServer会把备UpdateServer从同步列表中5 S+ l5 m1 u# w% h$ D# j
剔除。另外,剔除备UpdateServer之前需要通知RootServer,从而防止RootServer将不
: I# b5 {0 v7 G9 V. ?, O一致的备UpdateServer选为主UpdateServer。1 X; [7 G$ S$ e
如图9-7所示,主UpdateServer往备机推送操作日志,备UpdateServer的接收线程1 q5 O8 M% [& S" a
接收日志,并写入到一块全局日志缓冲区中。备UpdateServer只要接收到日志就可以* J& \! }/ @7 {
回复主UpdateServer同步成功,主UpdateServer接着更新本地内存并将日志刷到磁盘) r5 @* t- k5 s0 U/ l1 ? O
文件中,最后回复客户端写入操作成功。这种方式实现了强一致性,如果主
' _0 T; K2 ~4 |7 {UpdateServer出现故障,备UpdateServer包含所有的修改操作,因而能够完全无缝地
5 r" h$ {! v% d- [切换为主UpdateServer继续提供服务。另外,主备同步过程中要求主机刷磁盘文件,
$ N( C7 j0 K w, z备机只需要写内存缓冲区,强同步带来的额外延时也几乎可以忽略。( O5 e' }. _- D( W
图 9-7 UpdateServer主备同步原理
: F. G1 N+ [ l+ L) s正常情况下,备UpdateServer的日志回放线程会从全局日志缓冲区中读取操作日
+ o1 x8 Y$ |4 o4 R- s志,在内存中回放并同时将操作日志刷到备机的日志文件中。如果发生异常,比如* b9 m- ~9 u/ _$ Y0 C0 M
备UpdateServer刚启动或者主备之间网络刚恢复,全局日志缓冲区中没有日志或者日
) }* {/ p: X6 A0 Z D志不连续,此时,备UpdateServer需要主动请求主UpdateServer拉取操作日志。主/ a+ T4 i4 u" y W. [4 H. N
UpdateServer首先查找日志缓冲区,如果缓冲区中没有数据,还需要读取磁盘日志文
/ U7 @6 O6 l& i2 F件,并将操作日志回复备UpdateServer。代码如下:
7 S) E" A! I6 K7 d+ `class ObReplayLogSrc
' T2 U4 Y6 N; x0 B% U! N2 t{0 |& n( a8 s9 u( S2 z j/ h
public: e& a, K8 A! h9 f( G4 n( e: y6 K
//读取一批待回放的操作日志
& i4 E, V) I0 V5 s//@param[in]start_cursor日志起始点
6 q1 T4 J$ q( p* @1 j. n" N+ v//@param[out]end_id读取到的最大日志号加1,即下一次读取的起始日志号
9 A+ X$ n( n6 H% C* v//@param[in]buf日志缓冲区
1 A! f8 b4 _5 n' Z- {//@param[in]len日志缓冲区长度
% s; N2 w. K3 A//@param[out]read_count读取到的有效字节数5 {8 N1 {3 y: L6 i7 x8 T1 g
int get_log(const ObLogCursor&start_cursor,int64_t&end_id,char*buf,const% {" j6 s6 A$ E- S* p
int64_t len,int64_t&read_count);9 G) K; q2 Y1 c: e% p2 M c" A2 I
};
' w- k# }8 {0 O' w# l, Gclass ObUpsLogMgr
4 b" P6 [, A: y U/ E{
+ j" o8 D6 t+ }! tpublic:0 a& Y) I' H: |* p4 C5 d; [6 x0 g1 o
enum WAIT_SYNC_TYPE
' [. d4 P) h9 S4 ?: A7 d$ I4 w{
5 B9 j1 [# t6 p! J( O: n3 j/ sWAIT_NONE=0,. u9 T2 ]' V: M2 @: s! M$ Q2 V
WAIT_COMMIT=1,
8 A' \* K5 z7 mWAIT_FLUSH=2,6 B9 d7 K. i c
};
T% Y3 j0 g2 N ~public:
! U8 ]: H. q) w: o" r+ |; o//备UpdateServer接收主UpdateServer发送的操作日志
. Q8 {+ E( ~; A6 Gint slave_receive_log(const char*buf,int64_t len,const int64_t
4 m9 o2 x& W$ K: B9 w( b) A0 mwait_sync_time_us,const WAIT_SYNC_TYPE wait_event_type);$ v( f0 _- M: {# |7 O: w
//备UpdateServer获取并回放操作日志0 W& A6 B. h6 e; }4 E: Z( Z9 g
int replay_log();/ |. f, T4 e" m" r( C
};! @; i, I' d9 Q" J7 J, _
备UpdateServer接收到主UpdateServer发送的操作日志后,调用ObUpsLogMgr类的1 l5 V3 t9 N3 a6 f% y
slave_receive_log将操作日志保存到日志缓冲区中。备UpdateServer可以配置成不等待8 o; M+ R6 E# R
(WAIT_NONE)、等待提交到MemTable(WAIT_COMMIT)或者等待提交到" S8 d5 \! t& B1 _& |
MemTable且写入磁盘(WAIT_FLUSH)。另外,备UpdateServer有专门的日志回放线
5 e W M9 Y+ `7 o9 w程不断地调用ObUpsLogMgr中的replay_log函数获取并回放操作日志。+ {& }8 H3 `% l7 e6 I" H: T
备UpdateServer执行replay_log函数时,首先调用ObReplayLogSrc的get_log函数读
! p3 Q1 f& ^: ^1 E/ c" U% K, }取一批待回放的操作日志,接着,将操作日志应用到MemTable中并写入日志文件持6 l, a; X8 X9 I5 i2 \
久化。Get_log函数执行时首先查看本机的日志缓冲区,如果缓冲区中不存在日志起6 ~/ N+ X1 M# U1 E+ h% @
始点(start_cursor)开始的操作日志,那么,生成一个异步任务,读取主
: R8 L( ~1 T; w* M# XUpdateServer。一般情况下,slave_receive_log接收的日志刚加入日志缓冲区就被
3 a" t, s# |1 x/ @' C B, j ^get_log读走了,不需要读取主UpdateServer。
$ }- h& ?, g# |' Y
4 f @9 S+ B0 l- t8 @9 k+ s8 H# r$ p% v9 j) B2 q* c# @2 L
|
|