java自学网VIP

Java自学网

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 2272|回复: 0

《大规模分布式存储系统》第9章 分布式存储引擎【9.3】

[复制链接]
  • TA的每日心情
    开心
    2021-5-25 00:00
  • 签到天数: 1917 天

    [LV.Master]出神入化

    2025

    主题

    3683

    帖子

    6万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    66101

    宣传达人突出贡献优秀版主荣誉管理论坛元老

    发表于 2017-3-6 14:39:35 | 显示全部楼层 |阅读模式
    9.3 UpdateServer实现机制
    / i# P) _. \3 p5 U& X* o( {1 tUpdateServer用于存储增量数据,它是一个单机存储系统,由如下几个部分组/ H$ {9 }# N9 [6 E) S! @
    成:& d& w9 T/ M8 l9 M( v
    ●内存存储引擎,在内存中存储修改增量,支持冻结以及转储操作;
      C  ]) V' k+ `# W●任务处理模型,包括网络框架、任务队列、工作线程等,针对小数据包做了专, \. A2 v( `: [2 a" e. c4 [
    门的优化;
    2 X  d  M* ]/ V( ~4 ^$ O" F●主备同步模块,将更新事务以操作日志的形式同步到备UpdateServer。
    1 s7 T+ c& {( E& |6 iUpdateServer是OceanBase性能瓶颈点,核心是高效,实现时对锁(例如,无锁
    1 d0 l: E8 r" C4 ~+ L数据结构)、索引结构、内存占用、任务处理模型以及主备同步都需要做专门的优8 u& S! r4 t3 W1 k2 ~& X0 w
    化。: K/ d' K$ o( q1 r1 W0 x$ `
    9.3.1 存储引擎
      d9 v( R5 p& `- T: ]UpdateServer存储引擎如图9-3所示。
    7 K8 b8 Z( o9 b* M# U* M8 A图 9-3 UpdateServer存储引擎
    5 v# U7 B  B1 Q" b- O" XUpdateServer存储引擎与6.1节中提到的Bigtable存储引擎看起来很相似,不同点  g; c+ j* `4 l  Q' g* R
    在于:
      K" W4 v: K$ D. n9 @+ n! k●UpdateServer只存储了增量修改数据,基线数据以SSTable的形式存储在
    1 q: N$ b) O. w* a$ {6 e! Y  iChunkServer上,而Bigtable存储引擎同时包含某个子表的基线数据和增量数据;
    ; ]) f9 g6 l9 D) s" e●UpdateServer内部所有表格共用MemTable以及SSTable,而Bigtable中每个子表2 g! }8 Q+ u# m+ `( v8 D: S
    的MemTable和SSTable分开存放;, d- j- g- E+ i2 _* w/ d/ [
    ●UpdateServer的SSTable存储在SSD磁盘中,而Bigtable的SSTable存储在 GFS
    7 I( a+ `7 p1 ~# Q) \4 T* [5 m) y中。
      i( O$ t! a$ B- @5 qUpdateServer存储引擎包含几个部分:操作日志、MemTable以及SSTable。更新& }, G( @, O% K0 `
    操作首先记录到操作日志中,接着更新内存中活跃的MemTable(Active, S$ u. t! S: j3 F6 Q' S$ h
    MemTable),活跃的MemTable到达一定大小后将被冻结,称为Frozen MemTable,同
    9 ?3 P8 s; k/ G% D& h5 [时创建新的Active MemTable。Frozen MemTable将以SSTable文件的形式转储到SSD磁
    & _: m, s) m$ s8 R4 X, P, c盘中。: b, F$ s5 x* @( V4 S
    1.操作日志
    + P! j' H: \8 K6 ?OceanBase中有一个专门的提交线程负责确定多个写事务的顺序(即事务id),
    : V4 k$ `9 g) x$ I4 v2 B将这些写事务的操作追加到日志缓冲区,并将日志缓冲区的内容写入日志文件。为
    9 U) J& a  f0 {了防止写操作日志污染操作系统的缓存,写操作日志文件采用Direct IO的方式实现:" z  d7 l. C( V1 o
    class ObLogWriter6 {8 s( J* A# w  ?2 K7 h) a4 h- T
    {- X0 j) o/ _- N
    public:
    7 r9 c3 {" ?" q. e5 d//write_log函数将操作日志存入日志缓冲区
    4 O4 e) _- W" Y6 pint write_log(const LogCommand cmd,const char*log_data,const int64_t3 j) E6 L2 o- p% Q/ J' T# C7 I
    data_len);3 l5 _( b: O+ w3 R0 ~3 G# Z& G
    //将日志缓冲区中的日志先同步到备机再写入主机磁盘
    ; O( I0 w& z/ ?; r1 t1 b9 Oint flush_log(LogBuffer&tlog_buffer,const bool sync_to_slave=true,const bool
    / Y$ E9 ~1 j; N" p$ A4 uis_master=true);
    , i: p. q# h+ m+ n% I. Z};/ l3 V: c2 \" a; i
    每条日志项由四部分组成:日志头+日志序号+日志类型(LogCommand)+日志
    7 w8 a7 }6 V7 {内容,其中,日志头中记录了每条日志的校验和(checksum)。ObLogWriter中的
    ; ?( c- M* o3 c/ Y( Vwrite_log函数负责将操作日志拷贝到日志缓冲区中,如果日志缓冲区已满,则向调. L$ _7 J- t1 C  Q* {
    用者返回缓冲区不足(OB_BUF_NOT_ENOUGH)错误码。接着,调用者会通过. s2 t  N. G& `  s0 L
    flush_log将缓冲区中已有的日志内容同步到备机并写入主机磁盘。如果主机磁盘的最
    % O+ S1 A7 h3 H% ?5 u后一个日志文件超过指定大小(默认为64MB),还会调用switch_log函数切换日志9 @& r% C2 j% @5 G  l) ]5 Z; c# M
    文件。为了提高写性能,UpdateServer实现了成组提交(Group Commit)技术,即首  N" {( q* O( Q4 h$ S8 S; @2 s0 ~
    先多次调用write_log函数将多个写操作的日志拷贝到相同的日志缓冲区,接着再调
    / ~7 q' d% A( x% t6 q; w0 G用flush_log函数将日志缓冲区中的内容一次性写入到日志文件中。1 E( I9 w7 p& M& U
    2.MemTable
    : u$ ~% t& @( hMemTable底层是一个高性能内存B树。MemTable封装了B树,对外提供统一的读
    2 D5 s4 I) R' q写接口。# t: E( f* g% H0 n- L% k2 S/ h) h
    B树中的每个叶子节点对应MemTable中的一行数据,key为行主键,value为行操  e8 Y# w3 @, t0 Q! Z5 H
    作链表的指针。每行的操作按照时间顺序构成一个行操作链表。
    + g1 e* t7 {5 Z4 i如图9-4所示,MemTable的内存结构包含两部分:索引结构以及行操作链表,索
    5 h' K! m) O2 p8 P引结构为9.1.2节中提到的B树,支持插入、删除、更新、随机读取以及范围查询操
    7 ^7 }' {  ^' \' T: o% S作。行操作链表保存的是对某一行各个列(每个行和列确定一个单元,称为Cell)的0 _9 b& Y4 N  [) \: \
    修改操作。
    . v0 C9 ?) L8 E/ G% w) m图 9-4 MemTable的内存结构
    ; ]  @' ~( o  D3 I例9-3 对主键为1的商品有3个修改操作,分别是:将商品购买人数修改为
    8 L4 ]5 O' H1 u5 `2 k& t  W4 r100,删除该商品,将商品名称修改为“女鞋”,那么,该商品的行操作链中将保存三
    0 ]0 m( l/ Z1 h5 ^9 W: ?个Cell,分别为:<update,购买人数,100>、<delete,*>以及<update,商品" b, V# y# |7 z6 x3 ]$ Q* c
    名,“女鞋”>。% ?" P  ]+ q6 O! U* f
    MemTable中存储的是对该商品的所有修改操作,而不是最终结果。另外,5 [* e7 ?* E9 ^9 [2 {
    MemTable删除一行也只是往行操作链表的末尾加入一个逻辑删除标记,即<delete,) T; \8 `7 m' [
    *>,而不是实际删除索引结构或者行操作链表中的行内容。2 A6 j, G: Z2 v5 p+ m
    MemTable实现时做了很多优化,包括:9 c8 {/ A% H% p/ f; z/ @4 j( r
    ●哈希索引:针对主要操作为随机读取的应用,MemTable不仅支持B树索引,还$ _/ @  C+ P' w/ m
    支持哈希索引,UpdateServer内部会保证两个索引之间的一致性。8 ^8 L  @( K8 m& [3 W$ A
    ●内存优化:行操作链表中每个cell操作都需要存储操作列的编号
    & d& i: ]4 D" |; U1 a$ N(column_id)、操作类型(更新操作还是删除操作)、操作值以及指向下一个cell操
    ( N3 ], c  B* X" I/ Y. j作的指针,如果不做优化,内存膨胀会很大。为了减少内存占用,MemTable实现时8 F+ c( |' n6 K& L: p; L# q- i& G
    会对整数值进行变长编码,并将多个cell操作编码后序列到同一块缓冲区中,共用一
    6 Q% ?5 p9 R3 |- P9 k个指向下一批cell操作缓冲区的指针:
    5 a0 Q) d0 Q: `2 Q" j& i% \struct ObCellMeta4 r! t% H6 b, k/ b
    {# D4 b6 `3 K  s: ^  Q
    const static int64_t TP_INT8=1;//int8整数类型1 t8 j( {  i( v$ \  L, K
    const static int64_t TP_INT16=2;//int16整数类型! ]; E  G+ x0 h5 V8 P# D
    const static int64_t TP_INT32=3;//int32整数类型* o' @0 ?+ U6 S6 C/ V* v
    const static int64_t TP_INT64=4;//int64整数类型
      h4 ^0 _( x) Tconst static int64_t TP_VARCHAR=6;//变长字符串类型
    & }& c7 q: V% W: ?, D1 Q& b  n" @const static int64_t TP_DOUBLE=13;//双精度浮点类型/ [9 O0 D( c/ `+ \& b
    const static int64_t TP_ESCAPE=0x1f;//扩展类型/ l$ P7 V# T8 d) j
    const static int64_t ES_DEL_ROW=1;//删除行操作& i& ~% ?- o- D) w) G0 o/ @
    };
    & F7 ]0 o4 N" Q8 a8 r$ Z- sclass ObCompactCellWriter
    : j) A5 l2 [+ g7 P+ X! }{: X- i  r/ Q6 q8 T
    public:
    ) z2 m4 H. V! l' G# r: a( _//写入更新操作,存储成压缩格式" x! \1 p% g" j5 @. o
    int append(uint64_t column_id,const ObObj&value);) }$ V5 V; P) s! P% M9 u3 U
    //写入删除操作,存储成压缩格式
    ( ^1 ?+ Z: h0 `) q% zint row_delete();
    % V% Y( g# x2 r};$ M4 w8 B  _2 w% v; R' F
    MemTable通过ObCompactCellWriter来将cell操作序列化到内存缓冲区中,如果为; f" K4 u9 A8 l- v' [$ q% j( O
    更新操作,调用append函数;如果为删除操作,调用row_delete函数。更新操作的存5 Z3 q3 Q" c8 J$ K" g
    储格式为:数据类型+值+列ID,TP_INT8/TP_INT16/TP_INT32/TP_INT64分别表示8
    * k1 C6 r( w6 K  ?" s+ |位/16位/32位/64位整数类型,TP_VARCHAR表示变长字符串类型,TP_DOUBLE表示' N# K: H# r, Q
    双精度浮点类型。删除操作为扩展操作,其存储格式为:! @* P8 p1 q- y2 c
    TP_ESCAPE+ES_DEL_ROW。例9-3中的三个Cell:<update,购买人数,100>、<! q! O/ f3 _5 w# k+ \+ F
    delete,*>以及<update,商品名,“女鞋”>在内存缓冲区的存储格式为:4 D8 p. _7 s0 Z, c( j$ p( |
    第1~3字节表示第一个Cell,即<update,购买人数,100>;第4~5字节表示第
    ; N$ G( t, T+ `  @* ?二个cell,即<delete,*>;第6~8字节表示第三个Cell,即<update,商品名,“女- ^+ M, Q. Y/ g; ]9 K2 W
    鞋”>。/ ?9 E6 T( {( _, q3 ^$ m
    MemTable的主要对外接口可以归结如下:
    ) p+ b. k& C4 m' s4 u; Q. O//开启一个事务% D" Y0 N% p  u$ }6 [) \/ l
    //@param[in]trans_type事务类型,可能为读事务或者写事务
    5 Y/ j- m( [% b# J  \! E//@param[out]td返回的事务描述符/ D% ~9 x: f! j% h6 D
    int start_transaction(const TETransType trans_type,MemTableTransDescriptor&) o+ ?# C9 }% D2 N
    td);+ |7 ?  C  l  B, J7 K
    //提交或者回滚一个事务$ [$ u* {5 y1 Z5 G: W, ]  E; [
    //@param[in]td事务描述符' W* P* ^6 S! g* z( n* c& S
    //@param[in]rollback是否回滚,默认为false
    + L4 f$ a) E  g) U5 V0 W3 L* Tint end_transaction(const MemTableTransDescriptor td,bool rollback=false);
    " U+ M# R% ~! O' M) \/ }# _//执行随机读取操作,返回一个迭代器
    1 Y# Q3 J0 q3 i//@param[in]td事务描述符2 p4 \" S4 H! j5 M/ U' X
    //@param[in]table_id表格编号. n( ^2 o, G2 R7 d! ?7 l) y
    //@param[in]row_key待查询的主键( }3 [' A9 C6 e9 c6 Y; n" X1 O: d' l
    //@param[out]iter返回的迭代器' q+ c8 S% v% d. ]8 N8 \" u
    int get(const MemTableTransDescriptor td,const uint64_t table_id,const- c, g# |( V& F0 b6 w
    ObRowkey&row_key,MemTableIterator&iter);' u1 J9 G* I0 v' P3 n  i; D' K
    //执行范围查询操作,返回一个迭代器  s) ^0 ~. x( [
    //@param[in]td事务描述符
    0 f; P, h' A) d- X//@param[in]range查询范围,包括起始行、结束行,开区间或者闭区间
    . C! V$ u+ X- N6 w1 e( ]5 W2 f//@param[out]iter返回的迭代器6 T1 B3 w0 `* G1 D0 z
    int scan(const MemTableTransDescriptor td,const ObRange&4 A: k5 E2 v; `. n8 B
    range,MemTableIterator&iter);
    . `& ]4 I2 Q4 l8 y) [6 |- o3 _0 ]# g1 U//开始执行一次修改操作" Q' E+ r1 B0 g2 a
    //@param[in]td事务描述符% V' p, ~' @8 ]2 A, R+ f
    int start_mutation(const MemTableTransDescriptor td);2 w- t9 I$ @# w) J' ?5 u0 ?6 e
    //提交或者回滚一次修改操作
    7 @* |4 i( a- \2 \$ B2 s$ W6 t//@param[in]td事务描述符
    ) X( N0 i$ p$ v1 R//@param[in]rollback是否回滚
    8 ^/ [. K( z! V$ Fint end_mutation(const MemTableTransDescriptor td,bool rollback);
    + m: K# G" }5 X7 G6 p; E  G//执行修改操作
    9 i, H, L; R8 j: y/ S1 A//@param[in]td事务描述符$ B3 r6 g; y) w5 o: v3 P
    //@param[in]mutator修改操作,包含一个或者多个对多个表格的cell操作
    6 r9 ]% T1 {1 w" @int set(const MemTableTransDescriptor td,ObUpsMutator&mutator);. Y" Y) m5 C5 c3 N# Q
    对于读事务,操作步骤如下:/ q6 g" ~& v: X
    1)调用start_transaction开始一个读事务,获得事务描述符;
    8 w7 N) y3 N0 S7 s3 }  R$ |% P$ z2)执行随机读取或者扫描操作,返回一个迭代器;接着可以从迭代器不断迭代& d5 O5 v6 G7 T/ A! b8 p% \, ]
    数据;) D; Z$ c( T+ Q8 y: F9 ~8 Z; g5 \
    3)调用end_transaction提交或者回滚一个事务。' g- G% H4 N! i1 e& w8 ?2 f
    class MemTableIterator# ^) z: D/ W% m
    {
    2 a! ^$ j7 z% ~9 }public:
    " k! T" E/ A9 V//迭代器移动到下一个cell4 O9 W7 w% b- r- T$ C- M
    int next_cell();" i4 R; @% @5 A) v
    //获取当前cell的内容! L5 S* b. B8 i2 [
    //@param[out]cell_info当前cell的内容,包括表名(table_id),行主键(row_+ ~# S" N3 i( |# b. N% R5 C4 i% J
    key),列编号(column_id)以及列值(column_value)
    ! x& S- K- q. i9 E/ hint get_cell(ObCellInfo**cell_info);  {6 @' M* {( b! o; T: ~8 J
    //获取当前cell的内容
    - i. ^/ G; h& \//@param[out]cell_info当前cell的内容& |' |4 ]; j- w6 m
    //@param is_row_changed是否迭代到下一行2 f! s' K0 U* _! t4 H
    int get_cell(ObCellInfo**cell_info,bool*is_row_changed);  J0 ]4 Q1 n) L5 J" C- O7 n" x
    };6 M3 B  B# f* z5 _
    读事务返回一个迭代器MemTableIterator,通过它可以不断地获取下一个读到的6 ^; M. @$ G* K2 s/ n  n4 g
    cell。在例9-3中,读取编号为1的商品可以得到一个迭代器,从这个迭代器中可以读
    5 z8 V( {7 ~7 j$ Z4 Y. X: k1 w出行操作链中保存的3个Cell,依次为:<update,购买人数,100>,<delete,*
    ( I9 \. A/ I, G9 j>,<update,商品名,“女鞋”>。
    $ k9 q1 q3 E# ~8 D% n写事务总是批量执行,步骤如下:" T5 X: e, U: A; Q+ w( [- h
    1)调用start_transaction开始一批写事务,获得事务描述符;4 b( f1 z( `* d+ w& n: z4 f. m& g8 i
    2)调用start_mutation开始一次写操作;" M- |" {% D% }( s7 o: ?
    3)执行写操作,将数据写入到MemTable中;, W% ~7 v0 }$ d9 i0 O( \: `
    4)调用end_mutation提交或者回滚一次写操作;如果还有写事务,转到步骤2 @+ A) K2 `8 m% t# l& r
    2);
    9 U2 S# ~. ]: r. W/ i! u0 t5 S5)调用end_transaction提交写事务。
    * J4 {" Z6 o$ ~' `7 e! q3.SSTable
    8 [6 y9 h, u7 Y$ O3 C1 I8 R当活跃的MemTable超过一定大小或者管理员主动发起冻结命令时,活跃的  }1 _) H0 a+ c* {( L( ^0 ]( m
    MemTable将被冻结,生成冻结的MemTable,并同时以SSTable的形式转储到SSD磁盘3 N" u$ U, K6 }7 ^. A! k) F* y  ]( _
    中。
    ; T5 ]. q; t9 b8 O1 F% nSSTable的详细格式请参考9.4节ChunkServer实现机制,与ChunkServer中的5 h+ ]/ I7 ]* H4 G& v
    SSTable不同的是,UpdateServer中所有的表格共用一个SSTable,且SSTable为稀疏格
    & T& ?9 I8 u1 y式,也就是说,每一行数据的每一列可能存在,也可能不存在修改操作。' n& N! E& K; `( i5 t
    另外,OceanBase设计时也尽量避免读取UpdateServer中的SSTable,只要内存足
    + Y9 E9 o) Q4 ]够,冻结的MemTable会保留在内存中,系统会尽快将冻结的数据通过定期合并或者, r$ G. O2 z4 ]5 m! a
    数据分发的方式转移到ChunkServer中去,以后不再需要访问UpdateServer中的- o- T1 p! e" n9 q; {6 a, O
    SSTable数据。
    + C0 u. h' E+ Z" o3 k$ |8 \& r. Q当然,如果内存不够需要丢弃冻结MemTable,大量请求只能读取SSD磁盘,
    4 P7 T( f* i) @& Y- XUpdateServer性能将大幅下降。因此,希望能够在丢弃冻结MemTable之前将SSTable) L+ r/ S$ e' A8 T
    的缓存预热。+ Y1 U/ K# T7 E+ y( I; Z' M; W
    UpdateServer的缓存预热机制实现如下:在丢弃冻结MemTable之前的一段时间
    / s+ S( O. Z& N. K" t(比如10分钟),每隔一段时间(比如30秒),将一定比率(比如5%)的请求发给
    # r) v7 h6 p! y9 w) J" PSSTable,而不是冻结MemTable。这样,SSTable上的读请求将从5%到10%,再到: O4 o- [/ V3 X& \
    15%,依次类推,直到100%,很自然地实现了缓存预热。
    , x. M/ w; p6 k- d9.3.2 任务模型, c) l6 L3 b3 U0 L" l+ @! C- [
    任务模型包括网络框架、任务队列、工作线程,UpdateServer最初的任务模型基
    ' ?& e% D& ?+ R; O/ j于淘宝网实现的Tbnet框架(已开源,见http://code.taobao.org/p/tb-common-. |  T* O* D. ^' J
    utils/src/trunk/tbnet/)。Tbnet封装得很好,使用比较方便,每秒收包个数最多可以达
    9 ]( d; ]" x) k8 r到接近10万,不过仍然无法完全发挥UpdateServer收发小数据包以及内存服务的特- R) U4 g. N: y- s0 V
    点。OceanBase后来采用优化过的任务模型Libeasy,小数据包处理能力得到进一步提
    & [& @6 g  j# v6 g升。
    $ B% t* o; U! u/ |/ D5 e1.Tbnet
    ( f# u  b/ q' {# N$ A4 D如图9-5所示,Tbnet队列模型本质上是一个生产者—消费者队列模型,有两个线
    8 l# X: Z$ @& C* {程:网络读写线程以及超时检查线程,其中,网络读写线程执行事件循环,当服务; c( @6 Z" W& @1 `
    器端有可读事件时,调用回调函数读取请求数据包,生成请求任务,并加入到任务5 l9 x1 w& a% t
    队列中。工作线程从任务队列中获取任务,处理完成后触发可写事件,网络读写线
    8 O$ c; S/ A3 l$ O6 P$ h程会将处理结果发送给客户端。超时检查线程用于将超时的请求移除。
    6 {; T: i& _* C6 Z  T" A0 V8 Z+ d% e, n6 A图 9-5 Tbnet队列模型
    * d: \. K$ S. O; VTbnet模型的问题在于多个工作线程从任务队列获取任务需要加锁互斥,这个过/ z; f% M+ Z5 t( _
    程将产生大量的上下文切换(context switch),测试发现,当UpdateServer每秒处理5 P. g1 y, Z/ ?+ I# k% M
    包的数量超过8万个时,UpdateServer每秒的上下文切换次数接近30万次,在测试环
    3 [' X* R3 O% h% W1 M境中已经达到极限(测试环境配置:Linux内核2.6.18,CPU为2*Intel Nehalem9 h- s9 {+ n/ s
    E5520,共8核16线程)。
    2 Z" ]6 v4 L% C2.Libeasy
    5 ~. `/ a! H2 _4 ]5 I为了解决收发小数据包带来的上下文切换问题,OceanBase目前采用Libeasy任务
    & Z2 {2 @9 i( V模型。Libeasy采用多个线程收发包,增强了网络收发能力,每个线程收到网络包后! z& Q6 h* I$ ?+ [9 I
    立即处理,减少了上下文切换,如图9-6所示。
    , s( e- t% }: g& W% P+ S图 9-6 Libeasy任务模型& N6 C* V2 M' _; Y2 l( C: ?1 T
    UpdateServer有多个网络读写线程,每个线程通过Linux epool监听一个套接字集
    4 `6 b; R& N' G; F, z7 ^: q& v合上的网络读写事件,每个套接字只能同时分配给一个线程。当网络读写线程收到
    # |( d0 B, }+ x, M- L- n网络包后,立即调用任务处理函数,如果任务处理时间很短,可以很快完成并回复  i$ k) x: B! o" {+ E% ?
    客户端,不需要加锁,避免了上下文切换。UpdateServer中大部分任务为短任务,比6 g, ]! i/ B. G' B# B3 n8 X
    如随机读取内存表,另外还有少量任务需要等待共享资源上的锁,可以将这些任务
    , J" e! U& z! A* F. Q6 L; V) Z加入到长任务队列中,交给专门的长任务处理线程处理。
    & |2 H/ t. c* S3 v- {由于每个网络读写线程处理一部分预先分配的套接字,这就可能出现某些套接
    & X$ [1 u, R: }" l字上请求特别多而导致负载不均衡的情况。例如,有两个网络读写线程thread1和: d- d/ A& e/ I+ E+ `8 a) n6 l3 @
    thread2,其中thread1处理套接字fd1、fd2,thread2处理套接字fd3、fd4,fd1和fd2上每
      C4 }4 X( w! A/ o秒1000次请求,fd3和fd4上每秒10次请求,两个线程之间的负载很不均衡。为了处理# x$ g, {) D% j: ^; s
    这种情况,Libeasy内部会自动在网络读写线程之间执行负载均衡操作,将套接字从$ [1 C6 `7 E. o
    负载较高的线程迁移到负载较低的线程。
    5 y" V% C( d8 b/ U4 Z) Q8 j9.3.3 主备同步  Y$ Y% N- v0 g
    8.4.1节已经介绍了UpdateServer的一致性选择。OceanBase选择了强一致性,主9 k, v% |& s" l( K% p# m
    UpdateServer往备UpdateServer同步操作日志,如果同步成功,主UpdateServer操作本7 Z: ^) r  R; ]
    地后返回客户端更新成功,否则,主UpdateServer会把备UpdateServer从同步列表中0 m, t; l8 M8 ^: I3 X$ b) T
    剔除。另外,剔除备UpdateServer之前需要通知RootServer,从而防止RootServer将不" \6 q: p. l  n8 @7 N
    一致的备UpdateServer选为主UpdateServer。7 w0 L  x) e0 {+ S. p$ D
    如图9-7所示,主UpdateServer往备机推送操作日志,备UpdateServer的接收线程
    7 O- w  l1 _2 E7 l/ o接收日志,并写入到一块全局日志缓冲区中。备UpdateServer只要接收到日志就可以9 t$ X( w8 z- U9 o
    回复主UpdateServer同步成功,主UpdateServer接着更新本地内存并将日志刷到磁盘
    9 ^6 R0 P) z9 K) r1 R1 h, r文件中,最后回复客户端写入操作成功。这种方式实现了强一致性,如果主
    , _2 Z. R/ L* K+ p, [; @! G+ @UpdateServer出现故障,备UpdateServer包含所有的修改操作,因而能够完全无缝地
    ' X1 T: Q( Q6 x  j8 M0 I切换为主UpdateServer继续提供服务。另外,主备同步过程中要求主机刷磁盘文件,
    6 U% a% ?, q  J+ @7 B备机只需要写内存缓冲区,强同步带来的额外延时也几乎可以忽略。  y5 u9 ~- q! e1 b" `
    图 9-7 UpdateServer主备同步原理
    , f) E( f. I) y, n* s0 F% c正常情况下,备UpdateServer的日志回放线程会从全局日志缓冲区中读取操作日9 f6 a  w9 |" c" l
    志,在内存中回放并同时将操作日志刷到备机的日志文件中。如果发生异常,比如
    $ {1 R! _2 x- M& b备UpdateServer刚启动或者主备之间网络刚恢复,全局日志缓冲区中没有日志或者日
    7 h1 Q" z: v: [! b) u1 W4 T志不连续,此时,备UpdateServer需要主动请求主UpdateServer拉取操作日志。主2 o0 O9 o, r2 Y, c7 f3 T9 S
    UpdateServer首先查找日志缓冲区,如果缓冲区中没有数据,还需要读取磁盘日志文
    ( f3 M5 E2 F' E* x# f3 G/ ^- i9 a件,并将操作日志回复备UpdateServer。代码如下:
    8 S/ F! v# ?" hclass ObReplayLogSrc" p, {. V- Z6 X6 D; U4 I: s) v
    {
    5 ?; Y; L  Q" ]! _  \public:
    # M+ |4 D+ z2 l8 P& A//读取一批待回放的操作日志
    ( i% M  l: T+ z, `3 a//@param[in]start_cursor日志起始点
    9 V) w6 C' g  a, a5 ]//@param[out]end_id读取到的最大日志号加1,即下一次读取的起始日志号0 L2 I+ a5 E: ^2 I
    //@param[in]buf日志缓冲区0 c, ?0 N% A% i+ x4 b0 O/ q
    //@param[in]len日志缓冲区长度& s! C! n# C+ m: s/ n
    //@param[out]read_count读取到的有效字节数2 n/ k+ b/ f2 s" S& P
    int get_log(const ObLogCursor&start_cursor,int64_t&end_id,char*buf,const+ O  [- e" W5 Q8 g& |
    int64_t len,int64_t&read_count);$ U5 R7 J  ]" F
    };$ z3 y7 q0 M0 i  [1 o
    class ObUpsLogMgr
      F% F! z- }: I; o{& q3 c! N, S7 z0 h
    public:" n! p( P: a. f1 G+ m; V1 t
    enum WAIT_SYNC_TYPE+ E: o& w7 p+ R, J" {- K" K
    {' F1 j+ a* I! |, e% {4 u5 p
    WAIT_NONE=0,
      e  M6 P6 Q) C7 IWAIT_COMMIT=1,  a% N5 ?; |7 a+ N6 U0 X6 p7 f
    WAIT_FLUSH=2,
    * _7 i. N: U- b};. v! d) h2 h' [' r
    public:" P( T& T, w) u3 V
    //备UpdateServer接收主UpdateServer发送的操作日志
    3 x/ O4 A5 e6 Z% m; H9 B" k3 Rint slave_receive_log(const char*buf,int64_t len,const int64_t. a; s, w! b  \5 J
    wait_sync_time_us,const WAIT_SYNC_TYPE wait_event_type);
    2 B2 I) E9 _% O3 Y  J//备UpdateServer获取并回放操作日志$ b6 i1 \' a2 G% j; D6 ?
    int replay_log();
    / c* s. i8 C9 B, u- ^};# a7 z; v/ N. q  a5 y, h. E
    备UpdateServer接收到主UpdateServer发送的操作日志后,调用ObUpsLogMgr类的
    $ h# ]- p  k4 y- }2 g- |$ N6 Oslave_receive_log将操作日志保存到日志缓冲区中。备UpdateServer可以配置成不等待
    1 d" m6 y# n  o; O% u: X(WAIT_NONE)、等待提交到MemTable(WAIT_COMMIT)或者等待提交到" v2 H, P6 f7 J2 H( w9 B0 D
    MemTable且写入磁盘(WAIT_FLUSH)。另外,备UpdateServer有专门的日志回放线# n4 I, W. D- j9 J
    程不断地调用ObUpsLogMgr中的replay_log函数获取并回放操作日志。
    ' ~  \, o# L' a- j/ v备UpdateServer执行replay_log函数时,首先调用ObReplayLogSrc的get_log函数读( w0 \5 x7 `* s. n2 P
    取一批待回放的操作日志,接着,将操作日志应用到MemTable中并写入日志文件持
    , y$ o9 E# v0 z" d# a' g久化。Get_log函数执行时首先查看本机的日志缓冲区,如果缓冲区中不存在日志起
    , C; ]" o6 k4 |5 a9 x2 i始点(start_cursor)开始的操作日志,那么,生成一个异步任务,读取主
    1 L! Z; v7 O; S4 |; d4 h. ^UpdateServer。一般情况下,slave_receive_log接收的日志刚加入日志缓冲区就被+ F& T2 l+ X+ l1 D% y1 b* k+ n! N3 k* v
    get_log读走了,不需要读取主UpdateServer。5 r( n2 a/ F& ]2 V4 J3 r) {' S2 y
    8 a; u; g4 O7 B& D

    & \8 X6 d2 B' L0 ?) R$ t! I
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|Archiver|手机版|小黑屋|Java自学网

    GMT+8, 2024-4-29 13:10 , Processed in 0.084203 second(s), 35 queries .

    Powered by Javazx

    Copyright © 2012-2022, Javazx Cloud.

    快速回复 返回顶部 返回列表