java自学网VIP

Java自学网

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 2594|回复: 0

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

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

    [LV.Master]出神入化

    2025

    主题

    3683

    帖子

    6万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    66345

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

    发表于 2017-3-6 14:39:35 | 显示全部楼层 |阅读模式
    9.3 UpdateServer实现机制
    . g& |6 g3 y' C: Z- O, m9 TUpdateServer用于存储增量数据,它是一个单机存储系统,由如下几个部分组
    - I* \" ^3 J8 b6 N0 u+ `成:
    $ f( @! E# X+ D, h1 T! U3 Z$ B1 W●内存存储引擎,在内存中存储修改增量,支持冻结以及转储操作;1 N4 w& e6 t$ m  g6 a/ `
    ●任务处理模型,包括网络框架、任务队列、工作线程等,针对小数据包做了专: j7 `3 u1 |0 H* F
    门的优化;
    : I, ~. @) M; P! \$ x. q●主备同步模块,将更新事务以操作日志的形式同步到备UpdateServer。
    8 o# [3 e* \4 IUpdateServer是OceanBase性能瓶颈点,核心是高效,实现时对锁(例如,无锁* i3 \. O* Q9 c; m
    数据结构)、索引结构、内存占用、任务处理模型以及主备同步都需要做专门的优
    " L3 x( d1 e0 w7 J; E化。  Y) l$ u# a2 r
    9.3.1 存储引擎  Q/ A! x2 y0 s2 @, \
    UpdateServer存储引擎如图9-3所示。
    8 v8 z1 ~' n2 S8 \1 b图 9-3 UpdateServer存储引擎/ R) d4 X4 t  k& `7 k
    UpdateServer存储引擎与6.1节中提到的Bigtable存储引擎看起来很相似,不同点
    % |' C7 k4 Y. ^" u在于:
    - x; M  i# k1 J4 r●UpdateServer只存储了增量修改数据,基线数据以SSTable的形式存储在- b7 [. e/ z( s: u
    ChunkServer上,而Bigtable存储引擎同时包含某个子表的基线数据和增量数据;
    7 A( m- P3 \- K' a9 w●UpdateServer内部所有表格共用MemTable以及SSTable,而Bigtable中每个子表# G0 _" P% d- q
    的MemTable和SSTable分开存放;
    8 I0 m8 D, X3 S; \0 ^7 _% m! H1 g●UpdateServer的SSTable存储在SSD磁盘中,而Bigtable的SSTable存储在 GFS: b! A  K1 p/ n9 _' f
    中。3 h6 `, h4 S+ S
    UpdateServer存储引擎包含几个部分:操作日志、MemTable以及SSTable。更新
    $ K+ E8 l0 S+ i0 d* ]" h' C% o操作首先记录到操作日志中,接着更新内存中活跃的MemTable(Active$ Q, }0 z2 A  m' f, N# r, `
    MemTable),活跃的MemTable到达一定大小后将被冻结,称为Frozen MemTable,同6 ]; _, A' ?/ p, B+ K6 d( s3 I7 o
    时创建新的Active MemTable。Frozen MemTable将以SSTable文件的形式转储到SSD磁
    ( Y) c+ E% t& I1 N% h3 T盘中。
    " q( F' @. W3 l1 L8 j; O  x1 ~0 y1.操作日志
    % ?1 h1 n+ n1 z% E7 Y9 aOceanBase中有一个专门的提交线程负责确定多个写事务的顺序(即事务id),. p# l$ y; B  d" d0 [: s+ R, |( c
    将这些写事务的操作追加到日志缓冲区,并将日志缓冲区的内容写入日志文件。为
    1 c1 {* [$ A$ [, W; C9 w3 b1 x了防止写操作日志污染操作系统的缓存,写操作日志文件采用Direct IO的方式实现:
    . a: X/ C7 b# x+ g' Q, }class ObLogWriter
    ( Y2 _3 u- K% a4 k* Z  h% v' T{
    5 N8 L9 f$ D1 U# Z5 tpublic:2 B" q+ t6 ~1 y, b% \3 ~- O
    //write_log函数将操作日志存入日志缓冲区7 B  J) [- W9 ?6 t' }+ H
    int write_log(const LogCommand cmd,const char*log_data,const int64_t/ Z( d+ S1 H" M! b* o
    data_len);
      ]& |: c& c7 R9 y//将日志缓冲区中的日志先同步到备机再写入主机磁盘6 r8 K  g- q& h. W) q
    int flush_log(LogBuffer&tlog_buffer,const bool sync_to_slave=true,const bool
    ) H: |' N1 Q2 D4 r7 w  k- wis_master=true);
    6 H! ~6 i& x9 ?' d# _};
    % F* _  \$ n3 r- O6 w3 j每条日志项由四部分组成:日志头+日志序号+日志类型(LogCommand)+日志5 H4 m0 ?" [2 X
    内容,其中,日志头中记录了每条日志的校验和(checksum)。ObLogWriter中的9 z) d! ~" N- O( A/ C  N  H- W- J
    write_log函数负责将操作日志拷贝到日志缓冲区中,如果日志缓冲区已满,则向调
    - a7 }# E* s' J用者返回缓冲区不足(OB_BUF_NOT_ENOUGH)错误码。接着,调用者会通过
    ) m8 g8 b7 K/ x& B  |+ tflush_log将缓冲区中已有的日志内容同步到备机并写入主机磁盘。如果主机磁盘的最
    6 B7 X8 t* Q1 A% T. O后一个日志文件超过指定大小(默认为64MB),还会调用switch_log函数切换日志
    + X/ o3 o' L' ?; V6 H* t' d文件。为了提高写性能,UpdateServer实现了成组提交(Group Commit)技术,即首5 j$ @1 z0 y# w& m
    先多次调用write_log函数将多个写操作的日志拷贝到相同的日志缓冲区,接着再调
    " D; w. M$ f" r! H用flush_log函数将日志缓冲区中的内容一次性写入到日志文件中。
    " p9 {2 I$ [% L. E2.MemTable7 T: A" g! f: M/ q
    MemTable底层是一个高性能内存B树。MemTable封装了B树,对外提供统一的读
    * E+ E0 r: E: N7 j. H4 t* W  f写接口。6 d7 ?- x/ Z' Z. H
    B树中的每个叶子节点对应MemTable中的一行数据,key为行主键,value为行操5 G2 q7 @8 f* x1 I: @7 X
    作链表的指针。每行的操作按照时间顺序构成一个行操作链表。
    " {5 x, d2 q) v& j% I% H: i如图9-4所示,MemTable的内存结构包含两部分:索引结构以及行操作链表,索
    9 p( i. K+ M" G0 B; c引结构为9.1.2节中提到的B树,支持插入、删除、更新、随机读取以及范围查询操
    0 _& V3 g* x7 O8 |作。行操作链表保存的是对某一行各个列(每个行和列确定一个单元,称为Cell)的
    % x% ~( L5 ~  N) H0 w; N( }7 Q7 H修改操作。6 L8 x: w0 h1 ?' Z0 H. S# E" D1 M: O
    图 9-4 MemTable的内存结构" L3 p; I+ C1 _/ j6 J  x/ P
    例9-3 对主键为1的商品有3个修改操作,分别是:将商品购买人数修改为7 u0 Q7 [3 }1 `: t* o
    100,删除该商品,将商品名称修改为“女鞋”,那么,该商品的行操作链中将保存三$ Y. W. d5 ?3 S
    个Cell,分别为:<update,购买人数,100>、<delete,*>以及<update,商品& a0 q- C( B5 Z: o" B7 Z, v
    名,“女鞋”>。
    7 r$ U  B" u% CMemTable中存储的是对该商品的所有修改操作,而不是最终结果。另外,
    ! I( P+ @- l6 J$ H  Z+ K' s/ jMemTable删除一行也只是往行操作链表的末尾加入一个逻辑删除标记,即<delete,! s; u; T. ?8 S1 X0 W( `( R
    *>,而不是实际删除索引结构或者行操作链表中的行内容。3 t1 j+ b( Z% K. ]
    MemTable实现时做了很多优化,包括:. i; q' Y( J* t' T
    ●哈希索引:针对主要操作为随机读取的应用,MemTable不仅支持B树索引,还
    ! J; G1 _& {6 h2 l: t) n3 T支持哈希索引,UpdateServer内部会保证两个索引之间的一致性。
    # R2 s2 c3 j' S; U●内存优化:行操作链表中每个cell操作都需要存储操作列的编号- D# H; `/ o: D# ~! ]( R
    (column_id)、操作类型(更新操作还是删除操作)、操作值以及指向下一个cell操; Y! h8 Q0 X/ {  L+ n$ U
    作的指针,如果不做优化,内存膨胀会很大。为了减少内存占用,MemTable实现时
      D% f& I0 A8 q/ E9 i- ]$ z" f会对整数值进行变长编码,并将多个cell操作编码后序列到同一块缓冲区中,共用一
    # e1 b0 p5 \, L' ^2 g个指向下一批cell操作缓冲区的指针:
    $ k+ R6 N$ B; G& a+ c  H. Fstruct ObCellMeta5 q- [4 J4 v& |( c5 O
    {
    2 e7 r# }# F* a% `const static int64_t TP_INT8=1;//int8整数类型1 {) `. j. B4 P+ I" O8 i1 T2 J/ J
    const static int64_t TP_INT16=2;//int16整数类型
    - O( P  G8 t" Y7 Jconst static int64_t TP_INT32=3;//int32整数类型# O! `6 _! Y. E
    const static int64_t TP_INT64=4;//int64整数类型& N) `9 d% i, _5 P! W1 v
    const static int64_t TP_VARCHAR=6;//变长字符串类型
    6 z+ S# n; j9 `const static int64_t TP_DOUBLE=13;//双精度浮点类型2 p, [5 c4 j9 h9 j. s& E0 W* Y
    const static int64_t TP_ESCAPE=0x1f;//扩展类型1 h0 J, `. N7 G8 P- r3 C
    const static int64_t ES_DEL_ROW=1;//删除行操作' s7 G: a2 i/ u; E
    };
    ' m" ]- I4 C. N2 L; t' Cclass ObCompactCellWriter
    2 R# r( B3 Y- t3 ^{
    7 `4 }0 G) I" Ypublic:
    3 p! {8 G' [! n' S; Q//写入更新操作,存储成压缩格式2 u) k) m0 w" D4 n6 R. H7 M6 d
    int append(uint64_t column_id,const ObObj&value);
    & [7 p" e* F9 E, `//写入删除操作,存储成压缩格式
    9 L9 j$ K) Y* ?; u! }$ h% Mint row_delete();8 ~/ B+ Q8 o# ]' F
    };
    ' S# J6 E  g5 B2 t) @0 MMemTable通过ObCompactCellWriter来将cell操作序列化到内存缓冲区中,如果为5 w2 [" }8 d  q& p% X$ J  d. C" b# [
    更新操作,调用append函数;如果为删除操作,调用row_delete函数。更新操作的存
    % c( E5 ]6 j+ u4 c; @! c储格式为:数据类型+值+列ID,TP_INT8/TP_INT16/TP_INT32/TP_INT64分别表示8$ K  k3 \2 }, l* V5 d
    位/16位/32位/64位整数类型,TP_VARCHAR表示变长字符串类型,TP_DOUBLE表示
    ' n  d' M" f8 w" T  c4 b双精度浮点类型。删除操作为扩展操作,其存储格式为:4 o, y! g5 ?5 u+ m7 M; v" l% x  {
    TP_ESCAPE+ES_DEL_ROW。例9-3中的三个Cell:<update,购买人数,100>、<
    * f! X$ Q' J0 M- Jdelete,*>以及<update,商品名,“女鞋”>在内存缓冲区的存储格式为:& M, h! Y+ L/ M7 M& q
    第1~3字节表示第一个Cell,即<update,购买人数,100>;第4~5字节表示第
    + \3 j* _" N  W2 k# d6 S( C二个cell,即<delete,*>;第6~8字节表示第三个Cell,即<update,商品名,“女
    , ^( F7 M: U2 r& m' D0 C  f" v. `4 {鞋”>。
    - Q, X2 k) Y& ~8 AMemTable的主要对外接口可以归结如下:
    ) Q) v3 y! M- p2 R//开启一个事务0 K  N' \% o( {1 B8 ^( g
    //@param[in]trans_type事务类型,可能为读事务或者写事务
    " M  t6 y+ I% v+ n6 Q$ h+ g4 {//@param[out]td返回的事务描述符
    5 Z4 J1 v# K3 f" x+ R$ n& ]- k2 qint start_transaction(const TETransType trans_type,MemTableTransDescriptor&
      @, F% W% c9 _! G2 Etd);
    / j  c% C; j& I0 p( h2 j//提交或者回滚一个事务
      ?: V; }9 ?4 Q% j//@param[in]td事务描述符2 G" j5 s6 w3 e- `
    //@param[in]rollback是否回滚,默认为false+ C+ E- y) P4 [
    int end_transaction(const MemTableTransDescriptor td,bool rollback=false);4 g6 h4 ?& k' x' g1 P/ R& v
    //执行随机读取操作,返回一个迭代器
    4 j  S7 {# i: l# D- H//@param[in]td事务描述符9 ~# w) _; A* B: ]0 ]( Y0 m: i' ]* y$ c
    //@param[in]table_id表格编号
    + J" b4 ]5 i. G% b* ]* L: p3 ?//@param[in]row_key待查询的主键
    0 t+ ^9 k2 c+ T" j8 f//@param[out]iter返回的迭代器
    7 r! c5 t$ L: n6 _int get(const MemTableTransDescriptor td,const uint64_t table_id,const# d' v3 R4 O/ F( z
    ObRowkey&row_key,MemTableIterator&iter);: Z/ X- J4 F+ c9 i) T$ a
    //执行范围查询操作,返回一个迭代器
    , W- g  r: }7 H- r5 f0 s& e/ t' P" q//@param[in]td事务描述符
    4 D" ]8 ?7 q) X  T+ Y//@param[in]range查询范围,包括起始行、结束行,开区间或者闭区间
    0 Y  B/ D# R2 S- g! ?* V//@param[out]iter返回的迭代器" a( C* q: F; ]$ a
    int scan(const MemTableTransDescriptor td,const ObRange&" F' c$ A4 }9 ^, A4 x0 K6 l% J
    range,MemTableIterator&iter);
    / q3 _9 t$ u# z5 L) W( h, w//开始执行一次修改操作
    ! U, I6 g/ K$ j* h//@param[in]td事务描述符) d7 c; A2 e& t* {9 V
    int start_mutation(const MemTableTransDescriptor td);
    ( B/ Z  e+ o, h& L0 u* F6 j//提交或者回滚一次修改操作
    . K5 j6 A( V1 j! g) J- G( x//@param[in]td事务描述符
    / y4 o- ~9 D0 P7 e5 l  q//@param[in]rollback是否回滚
    1 j1 w) x# k7 @int end_mutation(const MemTableTransDescriptor td,bool rollback);
    : G$ [$ N) D. O9 P. k" B7 F% G2 M//执行修改操作# R+ B% ], w- a. z
    //@param[in]td事务描述符, c2 K9 B6 {* }: g5 v( T
    //@param[in]mutator修改操作,包含一个或者多个对多个表格的cell操作$ y5 u2 @2 q& w$ e
    int set(const MemTableTransDescriptor td,ObUpsMutator&mutator);
    ! c9 h# P# ?3 K! J- n对于读事务,操作步骤如下:
    . ^: }1 H% S) X& C+ d2 n0 ^1)调用start_transaction开始一个读事务,获得事务描述符;& R% t' ^% }, N5 D6 A: ^! u$ t
    2)执行随机读取或者扫描操作,返回一个迭代器;接着可以从迭代器不断迭代
    % A) N# S/ t. B0 A! }6 }5 [数据;6 V/ p1 U- c7 k2 v& D+ X
    3)调用end_transaction提交或者回滚一个事务。% m9 U. X/ j, j0 M/ e/ K
    class MemTableIterator
      n+ t& w  M6 d6 G. c{
    ! d& \% Q; Z2 Z* C. `6 k6 s' `public:( C' w& _: E' p) H9 L" l
    //迭代器移动到下一个cell6 P1 D; F+ W# G: C+ Z
    int next_cell();1 j" j  h  l3 {2 O+ g
    //获取当前cell的内容
    , `) g$ L; x8 a2 X//@param[out]cell_info当前cell的内容,包括表名(table_id),行主键(row_6 Q. h$ @' e! Q2 e
    key),列编号(column_id)以及列值(column_value): |; j# @; v$ Z2 p% g
    int get_cell(ObCellInfo**cell_info);
    # S7 d1 P! W, D9 t  V8 ]- B' ^//获取当前cell的内容
    : R, n' K) H6 C" I$ W0 C//@param[out]cell_info当前cell的内容) j; Z5 _) _5 k4 N7 {$ D5 ~
    //@param is_row_changed是否迭代到下一行
    , M/ m9 P2 C4 ?) bint get_cell(ObCellInfo**cell_info,bool*is_row_changed);
    2 K2 b5 [' f" @; _};  z- ]  t5 c5 x( h
    读事务返回一个迭代器MemTableIterator,通过它可以不断地获取下一个读到的# O7 ^$ ?7 D& M4 b# U- O1 S
    cell。在例9-3中,读取编号为1的商品可以得到一个迭代器,从这个迭代器中可以读
    " E: j$ T  b% R5 z; j0 ~" R6 \出行操作链中保存的3个Cell,依次为:<update,购买人数,100>,<delete,*
    ' \/ i3 a% F7 J* V>,<update,商品名,“女鞋”>。$ ]' Y+ m; \3 n" ?: J  W- b' J
    写事务总是批量执行,步骤如下:  ~7 a' Q  G2 R
    1)调用start_transaction开始一批写事务,获得事务描述符;
    - P' Q; [0 O$ l5 ^2)调用start_mutation开始一次写操作;
    : ~& k0 E; c7 w0 v) \  |3)执行写操作,将数据写入到MemTable中;% x7 L# e! h3 w( C; w2 }
    4)调用end_mutation提交或者回滚一次写操作;如果还有写事务,转到步骤
    + ]4 g( l7 {' r! c/ Z2);0 f- f1 A# `7 Y
    5)调用end_transaction提交写事务。, B- z& J3 U4 e, O# N% w& R/ w3 H
    3.SSTable
    , N# J1 r9 A/ ]! k- b8 m# p当活跃的MemTable超过一定大小或者管理员主动发起冻结命令时,活跃的$ Z/ q4 E+ A6 X' D3 \$ T' |
    MemTable将被冻结,生成冻结的MemTable,并同时以SSTable的形式转储到SSD磁盘( Y' ~. a7 O+ T9 e# j; I
    中。* ^* _( I- w& b6 r
    SSTable的详细格式请参考9.4节ChunkServer实现机制,与ChunkServer中的
    9 g# |. x; T  e& `+ @SSTable不同的是,UpdateServer中所有的表格共用一个SSTable,且SSTable为稀疏格
    6 s7 Y# F7 F  O9 X; _: \. U0 s式,也就是说,每一行数据的每一列可能存在,也可能不存在修改操作。
    . i0 C4 Y; [6 i  f9 n6 n5 P  P8 ~另外,OceanBase设计时也尽量避免读取UpdateServer中的SSTable,只要内存足4 ]% w( M5 b5 U1 O
    够,冻结的MemTable会保留在内存中,系统会尽快将冻结的数据通过定期合并或者
    9 r& x+ [2 H! A. e, [( E' d$ k数据分发的方式转移到ChunkServer中去,以后不再需要访问UpdateServer中的, n0 ?/ I6 E. `
    SSTable数据。2 O$ |4 t# b# y& ]0 u' c! h
    当然,如果内存不够需要丢弃冻结MemTable,大量请求只能读取SSD磁盘,
    0 |5 ?" S+ O: f6 J! X4 LUpdateServer性能将大幅下降。因此,希望能够在丢弃冻结MemTable之前将SSTable
    ' g/ S4 Y# U: Y. Q的缓存预热。
    4 V& q/ d3 q5 z  h* M* D! \UpdateServer的缓存预热机制实现如下:在丢弃冻结MemTable之前的一段时间8 Z& P+ w' G$ M* S0 C
    (比如10分钟),每隔一段时间(比如30秒),将一定比率(比如5%)的请求发给- o/ N" e0 K+ H3 E
    SSTable,而不是冻结MemTable。这样,SSTable上的读请求将从5%到10%,再到
    4 @6 m! Z( {: g" T15%,依次类推,直到100%,很自然地实现了缓存预热。+ w+ ~, o& C# Y$ S
    9.3.2 任务模型$ J: N2 U  F* E' r" s! w
    任务模型包括网络框架、任务队列、工作线程,UpdateServer最初的任务模型基
    * H, P6 o' T- I3 I; b! @& @于淘宝网实现的Tbnet框架(已开源,见http://code.taobao.org/p/tb-common-& O, M2 g. a. f
    utils/src/trunk/tbnet/)。Tbnet封装得很好,使用比较方便,每秒收包个数最多可以达7 _, I; Q. r. K
    到接近10万,不过仍然无法完全发挥UpdateServer收发小数据包以及内存服务的特, f+ Y% G8 P! ]
    点。OceanBase后来采用优化过的任务模型Libeasy,小数据包处理能力得到进一步提# i  \5 i7 u: D$ l7 g
    升。
    + P+ [% f3 H: B1.Tbnet7 N& d( \* J* x
    如图9-5所示,Tbnet队列模型本质上是一个生产者—消费者队列模型,有两个线
    * V/ E* X! }% e+ `程:网络读写线程以及超时检查线程,其中,网络读写线程执行事件循环,当服务
    / p% v3 f$ z3 }! F! o器端有可读事件时,调用回调函数读取请求数据包,生成请求任务,并加入到任务
    0 X$ g1 n' a( I: _' Y队列中。工作线程从任务队列中获取任务,处理完成后触发可写事件,网络读写线
    : x( i0 S( B9 M% S4 ?程会将处理结果发送给客户端。超时检查线程用于将超时的请求移除。
    ; i, U. J" ?) X, R  ]图 9-5 Tbnet队列模型
    ) g8 ^: S& G- P7 E; \Tbnet模型的问题在于多个工作线程从任务队列获取任务需要加锁互斥,这个过+ s- O1 F0 W, c: o" t! i
    程将产生大量的上下文切换(context switch),测试发现,当UpdateServer每秒处理1 m4 j& `. _0 `, e" I) R' e
    包的数量超过8万个时,UpdateServer每秒的上下文切换次数接近30万次,在测试环2 ^/ \9 E9 [, C& }
    境中已经达到极限(测试环境配置:Linux内核2.6.18,CPU为2*Intel Nehalem' \* n3 \$ H/ C, U4 E1 {
    E5520,共8核16线程)。
    4 d0 t" J9 E5 Z9 i$ b1 M2.Libeasy% o. ]! \# @& ?: X
    为了解决收发小数据包带来的上下文切换问题,OceanBase目前采用Libeasy任务
    - S( C1 K, E1 q0 _  a% C: z模型。Libeasy采用多个线程收发包,增强了网络收发能力,每个线程收到网络包后; B& b8 z) x" u4 v$ \
    立即处理,减少了上下文切换,如图9-6所示。$ e3 q6 `* v0 x
    图 9-6 Libeasy任务模型
    1 Z2 B6 v6 d$ _/ k7 M- g" [UpdateServer有多个网络读写线程,每个线程通过Linux epool监听一个套接字集3 c# _6 j8 U* b+ h) N4 X0 j. o( h
    合上的网络读写事件,每个套接字只能同时分配给一个线程。当网络读写线程收到
    % T. p& x1 k; C6 Z) S- q& H  k网络包后,立即调用任务处理函数,如果任务处理时间很短,可以很快完成并回复
    ' H1 Q' E/ }* L4 O客户端,不需要加锁,避免了上下文切换。UpdateServer中大部分任务为短任务,比
    - x. i7 R0 X! ~9 v# O/ y8 O如随机读取内存表,另外还有少量任务需要等待共享资源上的锁,可以将这些任务# ]: R6 W9 E3 N0 X9 d
    加入到长任务队列中,交给专门的长任务处理线程处理。% O6 O; S1 c2 S. @8 `
    由于每个网络读写线程处理一部分预先分配的套接字,这就可能出现某些套接0 Y4 }( n' }; Q) n% L4 }
    字上请求特别多而导致负载不均衡的情况。例如,有两个网络读写线程thread1和
    . ~; [& S6 A$ O/ ]thread2,其中thread1处理套接字fd1、fd2,thread2处理套接字fd3、fd4,fd1和fd2上每
    8 L  Y5 e: f8 e6 m/ e/ b) r1 x秒1000次请求,fd3和fd4上每秒10次请求,两个线程之间的负载很不均衡。为了处理
    ( R. j; @8 m2 p1 K( `8 a" P2 {* x0 J这种情况,Libeasy内部会自动在网络读写线程之间执行负载均衡操作,将套接字从
      n: s9 T7 z9 r3 I' n负载较高的线程迁移到负载较低的线程。5 x5 F& a9 ]! N% [
    9.3.3 主备同步. \1 L7 v# V& n$ w$ e0 h' u
    8.4.1节已经介绍了UpdateServer的一致性选择。OceanBase选择了强一致性,主
    & G; G" X4 G" r) VUpdateServer往备UpdateServer同步操作日志,如果同步成功,主UpdateServer操作本
    + p% K  G" H* `- g地后返回客户端更新成功,否则,主UpdateServer会把备UpdateServer从同步列表中
    / f0 _! {# C0 d7 t/ p8 R% C' D1 J剔除。另外,剔除备UpdateServer之前需要通知RootServer,从而防止RootServer将不. j4 M/ I' z7 f/ A4 w
    一致的备UpdateServer选为主UpdateServer。7 x1 A# `5 K3 q* q9 U- D- }( `! j
    如图9-7所示,主UpdateServer往备机推送操作日志,备UpdateServer的接收线程* `) D* E$ V8 z& C
    接收日志,并写入到一块全局日志缓冲区中。备UpdateServer只要接收到日志就可以
    & @3 V/ T0 j6 ^; U; ^/ y回复主UpdateServer同步成功,主UpdateServer接着更新本地内存并将日志刷到磁盘; ]9 l2 O% _2 @" v3 K! X3 e
    文件中,最后回复客户端写入操作成功。这种方式实现了强一致性,如果主
    2 e# M# t5 `: P  C% o% yUpdateServer出现故障,备UpdateServer包含所有的修改操作,因而能够完全无缝地
    ' X% s9 [' P" Z切换为主UpdateServer继续提供服务。另外,主备同步过程中要求主机刷磁盘文件,# s2 K+ {( P2 o' s6 q( b
    备机只需要写内存缓冲区,强同步带来的额外延时也几乎可以忽略。1 ]0 B% L' T0 ~0 h
    图 9-7 UpdateServer主备同步原理
    # W: H: e& p) B- q) i, h* n正常情况下,备UpdateServer的日志回放线程会从全局日志缓冲区中读取操作日
    2 [; Y% w0 P) k, T6 u) X% I志,在内存中回放并同时将操作日志刷到备机的日志文件中。如果发生异常,比如
    & t. K0 G9 O2 e备UpdateServer刚启动或者主备之间网络刚恢复,全局日志缓冲区中没有日志或者日
    . g& e+ T8 }$ R9 ^$ e  M  s: ?志不连续,此时,备UpdateServer需要主动请求主UpdateServer拉取操作日志。主. C8 ^" G$ F# M, V- N% z
    UpdateServer首先查找日志缓冲区,如果缓冲区中没有数据,还需要读取磁盘日志文
    & N$ \" _4 W9 z+ j- x件,并将操作日志回复备UpdateServer。代码如下:& u7 g5 S2 R$ R
    class ObReplayLogSrc- S% Z+ Z' R# @) e0 O
    {/ v+ ~/ P0 N% i' f
    public:
    8 p. M8 B' Y6 L: V* y( d7 U* x# F- \, e//读取一批待回放的操作日志
    / O6 Z: t8 ]. C3 N. D: H//@param[in]start_cursor日志起始点/ }5 _8 i( k, A, A$ q+ W
    //@param[out]end_id读取到的最大日志号加1,即下一次读取的起始日志号6 e4 X( V  a5 G9 Q4 s3 C; k( X
    //@param[in]buf日志缓冲区8 n/ {1 [2 \. L6 G- ]# y. ]8 m6 F
    //@param[in]len日志缓冲区长度% s! p8 f. M* {' H! \7 z# p: ^- E( h
    //@param[out]read_count读取到的有效字节数9 S# b: k$ B2 i. B7 g! ~# D
    int get_log(const ObLogCursor&start_cursor,int64_t&end_id,char*buf,const
    & U$ `1 V+ q* M7 n& I4 ?int64_t len,int64_t&read_count);; k4 Y* V6 y+ j9 a! t! j
    };) |+ |( x' V. d# ?! s7 J% e
    class ObUpsLogMgr
    ( J. ?; p3 [- s& r7 w{; B0 d( l7 ]+ d. p: q# V( @
    public:0 d; |- _" S( z* M* m8 c' }/ u
    enum WAIT_SYNC_TYPE
    , {( d7 n) S+ z: {& a) h9 w) q" `' D/ p{
    - N0 P4 V) z) T8 I  {) lWAIT_NONE=0,) G9 k6 M: P7 ?5 Y
    WAIT_COMMIT=1,! C. f* a) [8 z0 Y& L! u2 y
    WAIT_FLUSH=2,
    . t. w) E4 v( {% v: N7 t};: {2 I( s" v& ^) g$ v% f
    public:
    $ B4 E: X0 {5 D5 [//备UpdateServer接收主UpdateServer发送的操作日志7 j# \, Z$ n" u
    int slave_receive_log(const char*buf,int64_t len,const int64_t7 f: x$ N. q, v  B; A
    wait_sync_time_us,const WAIT_SYNC_TYPE wait_event_type);
    " v0 R- E3 _" R//备UpdateServer获取并回放操作日志
    5 p; u3 S+ [6 w! V$ Jint replay_log();
    4 l4 n+ z; J8 B, A: J# r};. o" J2 |0 X/ _7 y( P$ Y
    备UpdateServer接收到主UpdateServer发送的操作日志后,调用ObUpsLogMgr类的- I( T1 b+ k0 h* o
    slave_receive_log将操作日志保存到日志缓冲区中。备UpdateServer可以配置成不等待- w6 L* T, P/ _  X& r+ g
    (WAIT_NONE)、等待提交到MemTable(WAIT_COMMIT)或者等待提交到
    . P* U/ v7 @& b; i3 @, I3 RMemTable且写入磁盘(WAIT_FLUSH)。另外,备UpdateServer有专门的日志回放线2 U8 k9 M4 h* q# O2 g
    程不断地调用ObUpsLogMgr中的replay_log函数获取并回放操作日志。
    + i. }5 U7 b; n8 i备UpdateServer执行replay_log函数时,首先调用ObReplayLogSrc的get_log函数读5 t* k4 F' q" Y) ^+ p
    取一批待回放的操作日志,接着,将操作日志应用到MemTable中并写入日志文件持
    8 |9 v$ ~0 `4 O+ L9 }: X/ T久化。Get_log函数执行时首先查看本机的日志缓冲区,如果缓冲区中不存在日志起, b2 }; s0 E7 ^% ?7 {4 T
    始点(start_cursor)开始的操作日志,那么,生成一个异步任务,读取主: C) K" j1 w' b- K1 {0 w
    UpdateServer。一般情况下,slave_receive_log接收的日志刚加入日志缓冲区就被
    3 M; f) E4 ?' V8 U3 }) A5 Kget_log读走了,不需要读取主UpdateServer。
    - m, W7 Q& t$ g1 z) J% l9 w& M0 U: ]4 J: w$ _. s- z! f7 h" n8 H' ]
    . F1 K" }8 r. S5 G3 L7 [9 j/ {8 s& v' z
    回复

    使用道具 举报

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

    本版积分规则

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

    GMT+8, 2024-11-21 20:28 , Processed in 0.149764 second(s), 30 queries .

    Powered by Javazx

    Copyright © 2012-2022, Javazx Cloud.

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