java自学网VIP

Java自学网

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 2344|回复: 0

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

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

    [LV.Master]出神入化

    2025

    主题

    3683

    帖子

    6万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    66101

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

    发表于 2017-3-6 14:40:59 | 显示全部楼层 |阅读模式
    9.4 ChunkServer实现机制
    " `! K5 ?% c% T& I7 ?ChunkServer用于存储基线数据,它由如下基本部分组成:  A+ A( Y# h# S& @. }8 g
    ●管理子表,主动实现子表分裂,配合RootServer实现子表迁移、删除、合并;/ O4 o. @& X0 y! d/ C
    ●SSTable,根据主键有序存储每个子表的基线数据;
    & c$ R4 j$ W/ W" H●基于LRU实现块缓存(Block cache)以及行缓存(Row cache);
    . ]1 z+ B. S- }●实现Direct IO,磁盘IO与CPU计算并行化;
    . M7 J" c# |/ g" v●通过定期合并&数据分发获取UpdateServer的冻结数据,从而分散到整个集+ n0 i; X1 c! c; T! N" J. ]0 ^
    群。2 ]4 w/ f5 @. T7 g
    每台ChunkServer服务着几千到几万个子表的基线数据,每个子表由若干个  A" [5 U2 n- r1 l9 g& w
    SSTable组成(一般为1个)。下面从SSTable开始介绍ChunkServer的内部实现。" I8 w7 B) A1 m/ C/ K$ \
    9.4.1 子表管理# X; G( r. `, |9 }
    每台ChunkServer服务于多个子表,子表的个数一般在10000~100000之间。
    4 W7 u( m* i, D9 l! {( I3 \6 ]1 ^: QChunk-Server内部通过ObMultiVersionTabletImage来存储每个子表的索引信息,包括数
    . C# a1 f$ h9 @$ X& A0 g+ X( ]据行数(row_count),数据量(occupy_size),校验和(check_sum),包含的
    6 z0 B% b1 s5 }& t5 g- x" DSSTable列表,所在磁盘编号(disk_no)等,代码如下:
    2 v0 ?* U8 V$ d: sclass ObMultiVersionTabletImage& r8 [$ ?" p, Q. V8 _
    {, {2 O8 [: x. ^7 K' H
    public:
    & o+ N! d6 x% u$ m% O% d. ]//获取第一个包含指定数据范围的子表
    & }8 l1 f) X2 r//@param[in]range数据范围. b; j. {- ]8 Y$ H
    //@param[in]scan_direction正向扫描(默认)还是逆向扫描
    9 ^- d! {7 h9 ]$ R& P& E4 u//@param[in]version子表的版本号
      e5 i; |5 A2 h* Q  P% n2 ?//@param[out]tablet获取的子表索引结构
    ! f" U8 ?5 Q2 R3 \int acquire_tablet(const ObNewRange&range,const ScanDirection
    . b5 B" h% ^! r. Oscan_direction,const int64_t version,ObTablet*&tablet)const;- F) i. G- W: }) u+ G( n
    //释放一个子表
    ) x) N% C' ~) v& m# E% uint release_tablet(ObTablet*tablet);
    8 Z0 \+ J0 `) ]5 x5 E, w! j//新增一个子表,load_sstable表示是否立即加载其中的SSTable文件
    # z3 \4 y/ _7 e9 ?* rint add_tablet(ObTablet*tablet,const bool load_sstable=false);* x. f4 {/ k: u/ W2 V3 v
    //每日合并后升级子表到新版本,load_sstable表示是否立即加载新版本的SSTable文件4 O6 W6 Z3 {2 T1 C+ K2 F6 B
    int upgrade_tablet(ObTablet*old_tablet,ObTablet*new_tablet,const bool( e9 s( `+ Z7 A. R0 s7 z
    load_sstable=false);( A9 `/ F7 ^$ H; x7 G
    //每日合并后升级子表到新版本,且子表发生分裂,有一个变成多个。load_sstable表示是否立即加载
    : Z+ C1 l6 A  D8 X8 d分裂后的SSTable文件
    2 `- \' c* z' Y' A% m  q/ hint upgrade_tablet(ObTablet*old_tablet,ObTablet*new_tablets[],const int32_t; Z, _0 @8 H+ U
    split_size,const bool load_sstable=false);
    ; d- ~0 Z& K( e% z: U" y//删除一个指定数据范围和版本的子表: E7 q" k6 A9 n* J5 ~
    int remove_tablet(const ObNewRange&range,const int64_t version);
    ; M8 z8 f/ c' G//删除一个表格对应的所有子表- E9 m6 R' C; G9 m. |. Z
    int delete_table(const uint64_t table_id);" \$ V7 B0 c, E! }
    //获取下一批需要进行每日合并的子表6 B& W6 l" |: S0 G4 g( H
    //@param[in]version子表的版本号
    " r7 U& a* @4 p: i0 A//@param[out]size下一批需要进行每日合并的子表个数
    8 B  t( A& _( q3 F: W//@param[out]tablets下一批需要进行每日合并的子表索引结构2 o5 K. ?) i  L' D' A
    int get_tablets_for_merge(const int64_t version,int64_t&size,ObTablet*&
    2 S$ d: ~2 u" P- I: G  ytablets[])const;1 d3 T( q$ C1 F: [+ Q
    };
    3 [2 w$ ~0 m9 B2 [& L2 M, B* K3 y/ {ChunkServer维护了多个版本的子表数据,每日合并后升级子表的版本号。如果
    $ d$ Z4 c' g7 W' D% q子表发生分裂,每日合并后将由一个子表变成多个子表。子表相关的操作方法包
    ' _) Q: \( u) u- F  M2 |; A) P- h括:+ `5 C" D. x$ m& g
    1)add_tablet:新增一个子表。如果load_sstable参数为true,那么,立即加载其+ j& ^5 n  G) S: S1 n: B) ?
    中的SSTable文件。否则,使用延迟加载策略,即读取子表时再加载其中的SSTable。9 k, w: P6 [  {. t2 P1 y
    2)remove_tablet:删除一个子表。RootServer发现某个子表的副本数过多,则会$ X, U, Z# U) `
    通知其中某台ChunkServer删除指定的子表。- m" P$ \0 }$ M. @# v; D
    3)delete_table:删除表格。用户执行删除表格命令时,RootServer会通知每台
    + |" Y$ \" P& X, M2 o1 @ChunkServer删除表格包含的所有子表。
    1 O- B3 L7 n& M, n% H4 e4)upgrade_tablet:每日合并后升级子表的版本号。如果没有发生分裂,只需要. F$ T+ ?+ Q; [  `! I& S$ Y
    将老子表的版本号加1;否则,将老子表替换为多个范围连续的新子表,每个新子表# E% k: ~0 n0 q3 d8 j
    的版本号均为老子表的版本号加1。: C  V/ {4 r' r# Q* s1 ?4 d+ S. ^
    5)acquire_tablet/release_tablet:读取时首先调用acquire_tablet获取一个子表,增8 l' B/ ^1 j/ S7 @- c2 |; l
    加该子表的引用计数从而防止它在读取过程中被释放掉,接着读取其中的SSTable,
    9 I4 u9 [: s+ C最后调用release_tablet释放子表。
      D' w. H1 ]" t2 n! p. r6)get_tablets_for_merge:每日合并时通过调用该函数获取下一批需要进行每日& l9 H% ]5 d- ^: @# Q' d$ \1 e7 v
    合并的子表。
    3 p- l8 Q1 u. Z! {9.4.2 SSTable
    9 |/ N* J$ @. G& I如图9-8所示,SSTable中的数据按主键排序后存放在连续的数据块(Block)+ G% ~4 n% Q7 d, z: D
    中,Block之间也有序。接着,存放数据块索引(Block Index),由每个Block最后一/ [- h% M/ @$ E9 I/ U& d
    行的主键(End Key)组成,用于数据查询中的Block定位。接着,存放布隆过滤器
    ! k# Z1 {0 G$ Y" }$ Y( F9 m(Bloom Filter)和表格的Schema信息。最后,存放固定大小的Trailer以及Trailer的偏: X# }8 Z+ \  [" ]
    移位置。# O! J3 n5 \6 i' o9 H2 D
    图 9-8 SSTable格式
    ' m9 U' G. W- N3 X9 c) o8 F7 q查找SSTable时,首先从子表的索引信息中读取SSTable Trailer的偏移位置,接着
    2 X! q6 M4 ~" [4 q! u" u获取Trailer信息。根据Trailer中记录的信息,可以获取块索引的大小和偏移,从而将
    3 k# j3 y: E* ?3 S" c# @整个块索引加载到内存中。根据块索引记录的每个Block的最后一行的主键,可以通7 x5 t0 h' G0 q9 [) w
    过二分查找定位到查找的Block。最后将Block加载到内存中,通过二分查找Block中6 j7 f& B* h7 H7 G; U
    记录的行索引(Row Index)查找到具体某一行。本质上看,SSTable是一个两级索引6 }7 L1 l1 Q/ T5 r1 r+ a
    结构:块索引以及行索引;而整个ChunkServer是一个三级索引结构:子表索引、块2 h8 g7 H5 j& u
    索引以及行索引。
    9 r3 T5 f. u% L$ ~7 c2 q+ ~& s( USSTable分为两种格式:稀疏格式以及稠密格式。对于稀疏格式,某些列可能存
    ' u1 _. Q, \1 K$ T1 D! Q3 E; q在,也可能不存在,因此,每一行只存储包含实际值的列,每一列存储的内容为:
    5 i, k/ F2 d3 c; r9 D/ `/ ]1 N<列ID,列值>(<Column ID,Column Value>);而稠密格式中每一行都需要存储0 c. F* U/ f! g# T: y
    所有列,每一列只需要存储列值,不需要存储列ID,这是因为列ID可以从表格
    / x" W' f- X; |0 e( ESchema中获取。
    1 p3 H* h+ |: J5 R$ p0 y例9-4 假设有一张表格包含10列,列ID为1~10,表格中有一行的数据内容/ W/ a& ^6 M, _9 Y9 ~: D/ \
    为:
      X( `# {1 v; l那么,如果采用稀疏格式存储,内容为:<2,20>,<3,30>,<5,50>,
    % Z7 S& G% Q* E, D<7,70>,<8,80>;如果采用稠密格式存储,内容为:null,20,30,null,
    & X) W# `5 M& d2 {1 F& A  A50,null,70,80,null,null。7 r0 f% t  \  V# D
    ChunkServer中的SSTable为稠密格式,而UpdateServer中的SSTable为稀疏格式,
    5 |  f; u" \3 B/ s且存储了多张表格的数据。另外,SSTable支持列组(Column Group),将同一个列6 f: r; S( t- Y1 g" p9 F
    组下的多个列的内容存储在一块。列组是一种行列混合存储模式,将每一行的所有. O# ?6 J3 j) e) C! k( X
    列分成多个组(称为列组),每个列组内部按行存储。- }) O  k) ?$ c& k' y% O- ?
    如图9-9所示,当一个SSTable中包含多个表格/列组时,数据按照[表格ID,列组' U: A0 M) Y7 s- a, Y' R
    ID,行主键]([table_id,column group id,row_key])的形式有序存储。
    3 @/ y5 k7 r2 A' N) `图 9-9 SSTable包含多个表格/列组
    5 {+ J: p/ F6 ~6 k另外,SSTable支持压缩功能,压缩以Block为单位。每个Block写入磁盘之前调6 U- V) P# @7 l  [" Y3 ^
    用压缩算法执行压缩,读取时需要解压缩。用户可以自定义SSTable的压缩算法,目1 f, S& Z7 O/ B; Y% P# `5 R
    前支持的算法包括LZO以及Snappy。5 P* c* f0 }# ~7 H& ~/ a1 q
    SSTable的操作接口分为写入和读取两个部分,其中,写入类为
    ( N; M+ F; X: [' M8 i& L6 bObSSTableWriter,读取类为ObSSTableGetter(随机读取)和ObSSTableScanner(范围
      o9 [2 i2 N2 Q; A, {% F; W查询)。代码如下:
    ( `6 J. t4 _9 f* }. q- wclass ObSSTableWriter
    , b$ _" e9 J+ ?% D1 l. B& V{
    - D$ B" `+ i/ S- H0 c' ppublic:
    ) U5 G' f/ c+ B& ]# j. N7 T//创建SSTable
    5 x1 [$ Z3 X; @0 n//@param[in]schema表格schema信息
    7 ?/ X; W" W6 E3 @+ A' N//@param[in]path SSTable在磁盘中的路径名
    5 w' E. b  |. K//@param[in]compressor_name压缩算法名4 `) {: I& o# ?. W% E
    //@param[in]store_type SSTable格式,稀疏格式或者稠密格式
      A. v. A! G) [+ H! `//@param[in]block_size块大小,默认64KB5 k$ i4 L! ~8 h
    int create_sstable(const ObSSTableSchema&schema,const ObString&path,const
    $ t1 Y0 Z8 ^) |# g. p1 ?! u5 }ObString&compressor_name,const int store_type,const int64_t block_size);8 {, B0 A" U- q
    //往SSTable中追加一行数据0 G' E- c- n) P( M
    //@param[in]row一行SSTable数据
    - ~6 k3 |; I3 `3 O5 Q5 @//@param[out]space_usage追加完这一行后SSTable大致占用的磁盘空间
    # t! a; R% k- p$ [: q) dint append_row(const ObSSTableRow&row,int64_t&space_usage);
    - O, Y2 g; P, o- x6 v% S//关闭SSTable,将往磁盘中写入Block Index,Bloom Filter,Schema,Trailer等信息# O9 r+ ?) \( M6 Z
    //@param[out]trailer_offset返回SSTable的Trailer偏移量0 A# _1 E$ V4 Z7 [' B8 l
    int close_sstable(int64_t&trailer_offset);* E, R& H- [6 w) `
    };2 l, [6 Y' P: C/ h1 W! K6 x. {8 s1 c
    定期合并&数据分发过程将产生新的SSTable,步骤如下:( u* E* @/ l& Z7 ^9 x" V; \5 k
    1)调用create_sstable函数创建一个新的SSTable;  f1 n# \4 y. b& {% r8 w2 l7 O
    2)不断调用append_row函数往SSTable中追加一行行数据;
    0 G) b. m7 j5 ~7 m3 M* D! s3)调用close_sstable完成SSTable写入。
    $ h8 j& t; A" |5 Y- v8 u与9.2.1节中的MemTableIterator一样,ObSSTableGetter和ObSSTableScanner实现了2 U$ V% k3 k7 {& a
    迭代器接口,通过它可以不断地获取SSTable的下一个cell。# h) |3 F+ r! v+ [# |, x
    class ObIterator
    ; T' S, E0 w1 F5 s: _2 d6 @% E{: @, C- k$ S0 \8 a7 C
    public:
    ' r9 l& Y; @5 }, P0 p+ U//迭代器移动到下一个cell- ~; o8 b# n* f6 O
    int next_cell();
    # J" j2 d1 d0 P% O/ O//获取当前cell的内容* ^$ V3 U" s2 x/ ]. s# D2 H  P5 Q
    //@param[out]cell_info当前cell的内容,包括表名(table_id),行主键(row_key),列编号$ U# o' N7 g  \" r/ [2 i
    (column_id)以及列值(column_value)
    5 E$ ]* e" m4 ]. V$ |$ Tint get_cell(ObCellInfo**cell_info);
    " h  ]9 w) \/ c6 Y//获取当前cell的内容% b+ K; R. i) s  y+ _0 T
    //@param[out]cell_info当前cell的内容
    - N* g+ ^' `; e- f//@param is_row_changed是否迭代到下一行7 g5 l6 ~% c- V9 g" |* j  c+ W% s
    int get_cell(ObCellInfo**cell_info,bool*is_row_changed);
    : W# h2 j9 z4 t5 A9 n};5 l, e4 b: D! G
    OceanBase读取的数据可能来源于MemTable,也可能来源于SSTable,或者是合! r% K7 f8 i1 p8 |
    并多个MemTable和多个SSTable生成的结果。无论底层数据来源如何变化,上层的读. k# b" c: t# B1 H
    取接口总是ObIterator。0 \* q/ ]' D4 N5 l$ r
    9.4.3 缓存实现/ J& K& G/ B* u; c7 Z6 z
    ChunkServer中包含三种缓存:块缓存(Block Cache)、行缓存(Row Cache)以$ V4 F/ p# C" _& R6 e! F5 N: ~
    及块索引缓存(Block Index Cache)。其中,块缓存中存储了SSTable中访问较热的数
    4 ?$ ^( V8 G+ r3 ]1 L8 J据块(Block),行缓存中存储了SSTable中访问较热的数据行(Row),而块索引缓
    / v+ K2 |. I; q0 t% \存中存储了最近访问过的SSTable的块索引(Block Index)。一般来说,块索引不会3 E" x: o  b) @" k$ D
    太大,ChunkServer中所有SSTable的块索引都是常驻内存的。不同缓存的底层采用相$ o  I* N" g! K5 }2 `9 X
    同的实现方式。
    ! z1 j* ?" H" V" K& U% ]/ \6 t1.底层实现! X  B2 D  O7 z, w1 Y( Z% V( ]
    经典的LRU缓存实现包含两个部分:哈希表和LRU链表,其中,哈希表用于查找
    ) \0 ^; Y" {6 [  O缓存中的元素,LRU链表用于淘汰。每次访问LRU缓存时,需要将被访问的元素移动
    . I/ A7 t" N' n) ^& W/ s到LRU链表的头部,从而避免被很快淘汰,这个过程需要锁住LRU链表。
    % r' U9 q2 G' H. J* m如图9-10所示,块缓存和行缓存底层都是一个Key-Value Cache,实现步骤如下:) Y" |$ H  S) P6 n1 _8 k& B
    图 9-10 Key-Value Cache的实现
    $ H# d$ e- U/ j( t1 r% P/ F  @1)OceanBase一次分配1MB的连续内存块(称为memblock),每个memblock包% O/ q  B6 h9 O) p
    含若干缓存项(item)。添加item时,只需要简单地将item追加到memblock的尾部;
    3 ~7 V) H4 R, u. x9 T: ?" b另外,缓存淘汰以memblock为单位,而不是以item为单位。
    ; @" f( q8 {3 c- l. j2)OceanBase没有维护LRU链表,而是对每个memblock都维护了访问次数和最
    7 p0 l) }$ _/ w' M近频繁访问时间。访问memblock中的item时将增加memblock的访问次数,如果最近一
    * t* D% ?% _+ ~/ i3 P5 b5 F段时间之内的访问次数超过一定值,那么,更新最近频繁访问时间;淘汰memblock# _2 H- n- y# P, u) p5 v& x' S
    时,对所有的memblock按照最近频繁访问时间排序,淘汰最近一段时间访问较少的
    5 T8 A. u% D) S  v) s, v+ Mmemblock。可以看出,读取时只需要更新memblock的访问次数和最近频繁访问时$ J8 @' O6 k0 @, |, _
    间,不需要移动LRU链表。这种实现方式通过牺牲LRU算法的精确性,来规避LRU链
    , U  B8 u4 S" V- w( }0 ^表的全局锁冲突。9 |& r" h8 @3 _. l( ~. F# [4 r) a
    3)每个memblock维护了引用计数,读取缓存项时所在memblock的引用计数加
    7 h/ d% Z" a4 i7 x1,淘汰memblock时引用计数减1,引用计数为0时memblock可以回收重用。通过引用' n- v* g9 r9 f
    计数,实现读取memblock中的缓存项不加锁。
    ! K( A4 T! u* Y9 v; f! O5 `( d2.惊群效应6 D% w% C. B' n
    以行缓存为例,假设ChunkServer中有一个热点行,ChunkServer中的N个工作线  p/ W8 T! L$ {* R- _
    程(假设为N=50)同时发现这一行的缓存失效,于是,所有工作线程同时读取这行9 o; D! G6 G4 `( s9 l3 ?3 j
    数据并更新行缓存。可以看出,N-1共49个线程不仅做了无用功,还增加了锁冲突。8 n& ~9 a1 Z# L* W5 J5 e, D
    这种现象称为“惊群效应”。为了解决这个问题,第一个线程发现行缓存失效时会往
    $ U9 T+ l4 x$ F& _5 c) X! O; K缓存中加入一个fake标记,其他线程发现这个标记后会等待一段时间,直到第一个线) _1 K6 g6 S% W) z- Q3 K+ d
    程从SSTable中读到这行数据并加入到行缓存后,再从行缓存中读取。
    ; Y' `* |. S7 V; a) b9 \! [算法描述如下:( H  f2 Q+ `& e6 O
    调用internal_get读取一行数据;9 j) p4 {8 `, c, |& m% @' m; T: `
    if(行不存在){& j$ D  p1 e- V: v" Y7 ^
    调用internal_set往缓存中加入一个fake标记;
    $ ]. k4 n( D9 X% m2 i& O8 M: u- ~从SSTable中读取数据行;
    9 p, V: B$ Q! Z: _; M; J将SSTable中读到的行内容加入缓存,清除fake标记,唤醒等待线程;0 A5 V8 x7 d8 e6 ]; h- }" y9 P
    返回读到的数据行;
    : ^) W6 B2 q" c) V% B3 m}else if(行存在且为fake标记)2 W, C! U; ?2 N7 @% }/ b) c
    {
    1 [: D2 C4 r# D' h/ d( L线程等待,直到清除fake标记;4 K  S- T5 G4 |- {
    if(等待成功)返回行缓存中的数据;
    : [: J# ~8 `1 H. e) cif(等待超时)返回读取超时;
    ' E' V& }8 Z% y! E% \}$ Z- N# W9 M+ R( P! @
    else7 @0 ^2 g; y8 ?. a% G! H
    {4 V6 j) T2 R, C# u8 r, s
    返回行缓存中的数据;2 ?/ |' R* c& v) n
    }. \! V9 ?, o' h* N7 e% z
    3.缓存预热
    8 X4 ]6 i! B9 Y" p4 V4 Q' G) G( PChunkServer定期合并后需要使用生成的新的SSTable提供服务,如果大量请求同
    ; }- F! u" r7 B- b时读取新的SSTable文件,将使得ChunkServer的服务能力在切换SSTable瞬间大幅下
    9 q) [9 O4 f& n* V降。因此,这里需要一个缓存预热的过程。OceanBase最初的版本实现了主动缓存预% {& Q! V* _% o5 G+ i
    热,即:扫描原来的缓存,根据每个缓存项的key读取新的SSTable并将结果加入到新
    & d7 P$ {! G/ Q  \0 M8 u的缓存中。例如,原来缓存数据项的主键分别为100、200、500,那么只需要从新的" @* a: n. \5 \  l* T! e
    SSTable中读取主键为100、200、500的数据并加入新的缓存。扫描完成后,原来的缓' N$ d5 l3 @8 u  ^
    存可以丢弃。
    ) t' ^9 C5 Y0 }: Y7 M0 v线上运行一段时间后发现,定期合并基本上都安排在凌晨业务低峰期,合并完0 }( {# _' c/ n5 o
    成后OceanBase集群收到的用户请求总是由少到多(早上7点之前请求很少,9点以后" g! u% l* m2 `$ B9 P4 U
    请求逐步增多),能够很自然地实现被动缓存预热。由于ChunkServer在主动缓存预0 Z, H3 u$ J! F' B! q
    热期间需要占用两倍的内存,因此,目前的线上版本放弃了这种方式,转而采用被
    6 R3 T1 C! H7 X% a  B2 @动缓存预热。, d' y8 }2 ~, B+ h1 p9 c( C6 T
    9.4.4 IO实现
    6 v+ R+ A. e/ D4 u# v1 x; DOceanBase没有使用操作系统本身的页面缓存(page cache)机制,而是自己实现
    4 Q' U* Z5 T! N/ E( }% S缓存。相应地,IO也采用Direct IO实现,并且支持磁盘IO与CPU计算并行化。
    7 N; N1 X. [3 p8 QChunkServer采用Linux的Libaio [1] 实现异步IO,并通过双缓冲区机制实现磁盘预读! p! `7 P# c7 @0 Q
    与CPU处理并行化,实现步骤如下:
    $ @9 I/ V" j: o: u% i( r1)分配当前(current)以及预读(ahead)两个缓冲区;2 h' f1 n" p4 |
    2)使用当前缓冲区读取数据,当前缓冲区通过Libaio发起异步读取请求,接着
    7 S1 d: b$ Z: o等待异步读取完成;
    : w5 m, O! ]9 b+ |3)异步读取完成后,将当前缓冲区返回上层执行CPU计算,同时,原来的预读; @6 q& t" M+ R* ~' s' g/ w
    缓冲区变为新的当前缓冲区,发送异步读取请求将数据读取到新的当前缓冲区。, u' ?1 Y" Z8 {5 |; ~8 Q8 g& r
    CPU计算完成后,原来的当前缓冲区变为空闲,成为新的预读缓冲区,用于下一次9 L  x4 t; Q- G- l4 v4 Q* `' c
    预读。
    0 s, y! W, J8 i! O( Q1 T1 N; c4)重复步骤3),直到所有数据全部读完。. {( d5 S2 I7 c6 }
    例9-5 假设需要读取的数据范围为(1,150],分三次读取:(1,50],(50,$ X) P5 ?! \. B7 B, u4 K
    100],(100,150],当前和预读缓冲区分别记为A和B。实现步骤如下:  _5 h% p/ L7 h: z9 R8 u* ]
    1)发送异步请求将(1,50]读取到缓冲区A,等待读取完成;
    * Q6 e  n! [# d2)对缓冲区A执行CPU计算,发送异步请求,将(50,100]读取到缓冲区B;
    8 ]7 W8 g# R7 E9 B2 [- n3)如果CPU计算先于磁盘读取完成,那么,缓冲区A变为空闲,等到(50,
    3 }9 F' ^% x, U! @" n  W3 U, [( G100]读取完成后将缓冲区B返回上层执行CPU计算,同时,发送异步请求,将
    ! ~4 P9 {. K# j0 }; p4 w(100,150]读取到缓冲区A;
    + |' `" B' ^# R5 f; @4 q0 [7 u. I4)如果磁盘读取先于CPU计算完成,那么,首先等待缓冲区A上的CPU计算完
    4 S" o% @6 V3 H, f  b5 j8 \成,接着,将缓冲区B返回上层执行CPU计算,同时,发送异步请求,将(100,$ J" h& ?( U5 v7 d
    150]读取到缓冲区A;
    0 V; ]! q3 t/ _/ o7 {4 w; h5)等待(100,150]读取完成后,将缓冲区A返回给上层执行CPU计算。
    . E: w, O, t% Q; q3 A. h双缓冲区广泛用于生产者/消费者模型,ChunkServer中使用了双缓冲区异步预读
    % b& z2 }$ x. Y! |8 F的技术,生产者为磁盘,消费者为CPU,磁盘中生产的原始数据需要给CPU计算消费
    $ |7 W* k+ I6 ~8 @7 W. p+ `8 I) L掉。
    3 U: c& F8 o4 f- Z  Y2 l& o/ G8 |所谓“双缓冲区”,顾名思义就是两个缓冲区(简称A和B)。这两个缓冲区,总
    % r! C4 a" Z- v* ]. a3 Y7 s是一个用于生产者,另一个用于消费者。当两个缓冲区都操作完,再进行一次切
    . I0 Q- K* H9 m  l- \7 E7 W2 J换,先前被生产者写入的被消费者读取,先前消费者读取的转为生产者写入。为了
    9 A5 I. M8 p4 V( \) s; D做到不冲突,给每个缓冲区分配一把互斥锁(简称La和Lb)。生产者或者消费者如& y& \7 Q" h! ?2 ^# e3 l. \, [
    果要操作某个缓冲区,必须先拥有对应的互斥锁。) `0 E2 y! {4 @$ n) p
    双缓冲区包括如下几种状态:
    ; X& c+ C( z7 ?4 f+ q) X: s8 ~●双缓冲区都在使用的状态(并发读写)。大多数情况下,生产者和消费者都处
    0 `2 N% ]) K* T7 V& M, `于并发读写状态。不妨设生产者写入A,消费者读取B。在这种状态下,生产者拥有
    ( C( ^  d& u% z) `* k% ~! }% d锁La;同样地,消费者拥有锁Lb。由于两个缓冲区都是处于独占状态,因此每次读& I) z% q$ [. V, K1 v: L* \& M1 E6 ]
    写缓冲区中的元素都不需要再进行加锁、解锁操作。这是节约开销的主要来源。0 c* `+ ~  I7 E- F+ p4 @5 \0 i
    ●单个缓冲区空闲状态。由于两个并发实体的速度会有差异,必然会出现一个缓% ^: _* \( r* F+ v& q* Z. @" h
    冲区已经操作完,而另一个尚未操作完。不妨假设生产者快于消费者。在这种情况
    - [7 F1 G2 `- H: [3 B  ~下,当生产者把A写满的时候,生产者要先释放La(表示它已经不再操作A),然后
    6 O8 P6 F! z: e1 E6 p& `. j8 U尝试获取Lb。由于B还没有被读空,Lb还被消费者持有,所以生产者进入等待2 W6 {7 o: c+ ^
    (wait)状态。
    ( t! {7 y3 Z$ d0 h8 P7 v●缓冲区的切换。过了若干时间,消费者终于把B读完。这时候,消费者也要先. c$ j3 z1 a& w1 l
    释放Lb,然后尝试获取La。由于La刚才已经被生产者释放,所以消费者能立即拥有6 w5 @% Y: I# P1 ^5 b! D  }' ^
    La并开始读取A的数据。而由于Lb被消费者释放,所以刚才等待的生产者会苏醒过来1 u9 F& W/ s1 Y) {2 T( T
    (wakeup)并拥有Lb,然后生产者继续往B写入数据。
    8 Q" h/ p: H1 b8 G  r9 e& |6 r[1]Oracle公司实现的Linux异步IO库,开源地址:https://oss.oracle.com/projects/libaio-. [; h7 Q. M# ~# m
    oracle/' {* O' M" x- d/ o( ~2 m. B. k* e
    9.4.5 定期合并&数据分发
    1 u7 L' P. Y  ^RootServer将UpdateServer上的版本变化信息通知ChunkServer后,ChunkServer将; |8 J6 i4 g1 @9 t8 c
    执行定期合并或者数据分发。2 ?( p5 o& R! L$ \0 Q: U
    如果UpdateServer执行了大版本冻结,ChunkServer将执行定期合并。ChunkServer5 O; y- D3 O& N: x3 X
    唤醒若干个定期合并线程(比如10个),每个线程执行如下流程:+ a) n) i& `) O  j1 h
    1)加锁获取下一个需要定期合并的子表;
    % p# @* T/ Q; K- X& ]3 ]2)根据子表的主键范围读取UpdateServer中的修改操作;3 d% [0 }8 V9 F% R! h
    3)将每行数据的基线数据和增量数据合并后,产生新的基线数据,并写入到新* p* g) s: d# u/ f* {7 e. ?
    的SSTable中;
    / _; p# [$ \% y* A4)更改子表索引信息,指向新的SSTable。) j1 S# g' j/ t+ ?" k
    等到ChunkServer上所有的子表定期合并都执行完成后,ChunkServer会向, \1 V& D; D) y7 j5 [
    RootServer汇报,RootServer会更新RootTable中记录的子表版本信息。定期合并一般6 C, c& r  }# d7 T; r+ T
    安排在每天凌晨业务低峰期(凌晨1:00开始)执行一次,因此也称为每日合并。另
    / ^' L' D8 F, V外,定期合并过程中ChunkServer的压力比较大,需要控制合并速度,否则可能影响
    5 x2 z/ }/ e4 g正常的读取服务。- Y. Z) l" D, S1 \! l$ q
    如果UpdateServer执行了小版本冻结,ChunkServer将执行数据分发。与定期合并
    * q4 _9 Q$ i( H. W不同的是,数据分发只是将UpdateServer冻结的数据缓存到ChunkServer,并不会生成
    $ d& B: h0 S7 ]4 N+ \: v+ `新的SSTable文件。因此,数据分发对ChunkServer造成的压力不大。
    + x( C0 h5 i) ?! B7 m/ e, K3 Z0 |数据分发由外部读取请求驱动,当请求ChunkServer上的某个子表时,除了返回
    6 C0 W6 g6 [1 R0 @* {  M( E使用者需要的数据外,还会在后台生成这个子表的数据分发任务,这个任务会获取8 p! Q, |. J( x$ B6 Z+ |8 |+ v
    UpdateServer中冻结的小版本数据,并缓存在ChunkServer的内存中。如果内存用完,; W! @* |/ q' k5 [
    数据分发任务将不再进行。当然,这里可以做一些改进,比如除了将UpdateServer分
    2 M" p! C# H' p* x4 @发的数据存放到ChunkServer的内存中,还可以存储到SSD磁盘中。
    2 P' R) e) F2 X/ E9 j. j例9-6 假设某台ChunkServer上有一个子表t1,t1的主键范围为(1,10],只有一
    . M$ g" s# c; \$ Z7 C' P. _- `5 {行数据:rowkey=8=>(<2,update,20>,<3,update,30>,<4,update,40
    1 F6 o# o/ V# t$ _1 Z6 |>)。UpdateServer的冻结版本有两行更新操作:rowkey=8=>(<2,update,30
    # {) ^. P: n" z: H" _* @>,<3,up-date,38>)和rowkey=20=>(<4,update,50>)。" M* x/ D" o; s& h& @
    ●如果是大版本冻结,那么,ChunkServer上的子表t1执行定期合并后结果为:
    0 \; _3 `( a( L4 r  t; @, rro-wkey=8=>(<2,update,30>,<3,update,38>,<4,update,40>);
    1 E% S. H' ?& L* t" A●如果是小版本冻结,那么,ChunkServer上的子表t1执行数据分发后的结果为:1 U# w  m  b: l% e% f) A2 }
    rowkey=8=>(<2,update,20>,<3,update,30>,<4,update,40>,<2,
    & c% j  K+ ~6 l  ^+ j+ J! ]0 {( |update,30>,<3,update,38>)。2 a) e/ Q& }( w! ^, }+ {, g
    9.4.6 定期合并限速2 a- D/ b$ }  C6 F. u
    定期合并期间系统的压力较大,需要控制定期合并的速度,避免影响正常服- X$ F! K+ A6 T2 p8 C7 a# p: I  U
    务。定期合并限速的措施包括如下步骤:3 k5 y" Z/ p) A
    1)ChunkServer:ChunkServer定期合并过程中,每合并完成若干行(默认2000
    4 p% Z% S3 c+ W2 w$ ]行)数据,就查看本机的负载(查看Linux系统的Load值)。如果负载过高,一部分2 a( i" x% K4 D7 i% M9 q
    定期合并线程转入休眠状态;如果负载过低,唤醒更多的定期合并线程。另外,6 C/ N- [% @& Q( Y1 L
    RootServer将UpdateServer冻结的大版本通知所有的ChunkServer,每台ChunkServer会1 O. ~! O; }; v6 g& F: {
    随机等待一段时间再开始执行定期合并,防止所有的ChunkServer同时将大量的请求8 V& e3 L* S* r
    发给UpdateServer。+ S( {* Z, q0 ]9 v9 _7 v
    2)UpdateServer:定期合并过程中ChunkServer需要从UpdateServer读取大量的数! ?0 m) J1 z) w9 N7 o  O* v
    据,为了防止定期合并任务用满带宽而阻塞用户的正常请求,UpdateServer将任务区3 p7 s6 L2 S6 ^; @+ K6 `
    分为高优先级(用户正常请求)和低优先级(定期合并任务),并单独统计每种任1 M% T) ~! K. v' X( h2 n
    务的输出带宽。如果低优先级任务的输出带宽超过上限,降低低优先级任务的处理3 i  W6 u) o: C+ {
    速度;反之,适当提高低优先级任务的处理速度。4 q  y  m1 j: B
    如果OceanBase部署了两个集群,还能够支持主备集群在不同时间段进行“错峰
    - p; J0 P* U% X; ^合并”:一个集群执行定期合并时,把全部或大部分读写流量切到另一个集群,该集
    " R! G3 h" Q$ t! P群合并完成后,把全部或大部分流量切回,以便另一个集群接着进行定期合并。两
    8 i. y& h7 _- _个集群都合并完成后,恢复正常的流量分配。
    ! V/ L7 h; u& E' I( }# E/ {) D8 {5 j# h9 R1 r8 J2 {5 Y% I' S

    / ?3 |# B* q0 |+ K% E  P
    回复

    使用道具 举报

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

    本版积分规则

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

    GMT+8, 2024-4-29 05:23 , Processed in 0.067393 second(s), 27 queries .

    Powered by Javazx

    Copyright © 2012-2022, Javazx Cloud.

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