java自学网VIP

Java自学网

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 2796|回复: 0

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

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

    [LV.Master]出神入化

    2096

    主题

    3754

    帖子

    6万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    66788

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

    发表于 2017-3-6 14:39:35 | 显示全部楼层 |阅读模式
    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
    回复

    使用道具 举报

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

    本版积分规则

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

    GMT+8, 2025-4-1 13:53 , Processed in 0.110364 second(s), 34 queries .

    Powered by Javazx

    Copyright © 2012-2022, Javazx Cloud.

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