java自学网VIP

Java自学网

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 2664|回复: 0

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

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

    [LV.Master]出神入化

    2096

    主题

    3754

    帖子

    6万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    66788

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

    发表于 2017-3-6 14:40:59 | 显示全部楼层 |阅读模式
    9.4 ChunkServer实现机制
    * M1 Z. |8 `- v$ E6 ?8 uChunkServer用于存储基线数据,它由如下基本部分组成:
    . g+ X$ b6 O& r+ g●管理子表,主动实现子表分裂,配合RootServer实现子表迁移、删除、合并;
    ' g0 [: w8 \  N0 E●SSTable,根据主键有序存储每个子表的基线数据;
    . R+ T) k8 n+ C4 c- `●基于LRU实现块缓存(Block cache)以及行缓存(Row cache);
    ) S/ ^- g7 c" a+ Z3 h/ F●实现Direct IO,磁盘IO与CPU计算并行化;
    $ W8 d( O* }8 y3 |' g& |0 ^●通过定期合并&数据分发获取UpdateServer的冻结数据,从而分散到整个集- E9 D( ~. @. o: m7 p
    群。
    - A) I2 N: m1 V- C2 \( m. Q每台ChunkServer服务着几千到几万个子表的基线数据,每个子表由若干个# Z: g2 |& L2 ?9 b% L/ S
    SSTable组成(一般为1个)。下面从SSTable开始介绍ChunkServer的内部实现。
    - ?+ W8 X/ t, K) z4 [9.4.1 子表管理
    5 ?5 F( c4 _9 w8 d7 U每台ChunkServer服务于多个子表,子表的个数一般在10000~100000之间。. F/ G; c5 x( U9 C6 j0 q4 H
    Chunk-Server内部通过ObMultiVersionTabletImage来存储每个子表的索引信息,包括数8 X: F* c4 a' k) v& S5 \3 @3 a
    据行数(row_count),数据量(occupy_size),校验和(check_sum),包含的
    * N$ f/ R! d+ eSSTable列表,所在磁盘编号(disk_no)等,代码如下:% n. H6 m! s9 h, B  N, j: A1 E
    class ObMultiVersionTabletImage8 a2 E( q0 x2 }5 h
    {  J3 `/ n3 F9 R2 t7 ~! {& o9 ]
    public:
    . h& Z: l/ G( x5 t6 M" [' F! Q" x//获取第一个包含指定数据范围的子表0 @3 U+ M$ _6 m+ o
    //@param[in]range数据范围
    8 g4 g, S) `$ j, y% E  S//@param[in]scan_direction正向扫描(默认)还是逆向扫描
    6 n' ~( t! J( B2 c, T//@param[in]version子表的版本号- U* c6 i$ N3 Z0 {9 M6 M# F
    //@param[out]tablet获取的子表索引结构5 X$ v. l/ j7 {& G0 u' a
    int acquire_tablet(const ObNewRange&range,const ScanDirection+ t- l9 N& P6 t) u9 a3 t2 _. p
    scan_direction,const int64_t version,ObTablet*&tablet)const;
    ' F& A0 ^' F# u+ X4 l//释放一个子表
    , o% |4 Q/ I7 C0 d9 Cint release_tablet(ObTablet*tablet);
    3 v9 W7 ~3 x( P3 P2 T//新增一个子表,load_sstable表示是否立即加载其中的SSTable文件% W6 v# ]6 d' f0 H  w- Q: k% z
    int add_tablet(ObTablet*tablet,const bool load_sstable=false);
    9 L/ e6 |" I0 ]9 J" M//每日合并后升级子表到新版本,load_sstable表示是否立即加载新版本的SSTable文件9 ?' E6 ^# c* s' ^" I/ S
    int upgrade_tablet(ObTablet*old_tablet,ObTablet*new_tablet,const bool) O0 k4 u; w3 y; z( m
    load_sstable=false);/ @+ B& k+ U$ T4 w
    //每日合并后升级子表到新版本,且子表发生分裂,有一个变成多个。load_sstable表示是否立即加载6 A( Z6 y% d  M1 _/ V: q; {
    分裂后的SSTable文件; E: n! F! ^; a, M
    int upgrade_tablet(ObTablet*old_tablet,ObTablet*new_tablets[],const int32_t
    0 u' r# m# d) f# rsplit_size,const bool load_sstable=false);
    * J+ f  |2 Z& C# ^9 j: \//删除一个指定数据范围和版本的子表1 B7 l- Z  u+ m/ w
    int remove_tablet(const ObNewRange&range,const int64_t version);
    2 S  g# T2 [/ }- L//删除一个表格对应的所有子表7 z3 O9 ?% C7 |
    int delete_table(const uint64_t table_id);
    , z4 z/ h' ?* V2 i' U3 c5 k//获取下一批需要进行每日合并的子表7 M: Y" s2 Z, Q& E, s9 _6 b
    //@param[in]version子表的版本号
    # ]# z; d1 T/ `6 g& n( W//@param[out]size下一批需要进行每日合并的子表个数/ r' v& Z0 N  B
    //@param[out]tablets下一批需要进行每日合并的子表索引结构: H, I$ e, G' y* `
    int get_tablets_for_merge(const int64_t version,int64_t&size,ObTablet*&
    3 d" F: m) U- _& v3 j$ B; y6 Etablets[])const;
      f. }; b+ m; E8 c};2 K" i5 J0 N$ S5 ^# Q3 |$ K% m
    ChunkServer维护了多个版本的子表数据,每日合并后升级子表的版本号。如果; n- U9 s0 ~% F, k: K% z7 n& b$ j
    子表发生分裂,每日合并后将由一个子表变成多个子表。子表相关的操作方法包0 c6 C/ M8 q; q
    括:
    8 A' e4 Q6 ^; ^( j2 k% \8 c+ ]1)add_tablet:新增一个子表。如果load_sstable参数为true,那么,立即加载其
    2 p6 ^7 a( C+ m8 @; |) F中的SSTable文件。否则,使用延迟加载策略,即读取子表时再加载其中的SSTable。2 s/ n5 q3 e" o4 }
    2)remove_tablet:删除一个子表。RootServer发现某个子表的副本数过多,则会
    , s" X% P; [, R" h) ?通知其中某台ChunkServer删除指定的子表。! \) j4 E1 b- }9 V. G
    3)delete_table:删除表格。用户执行删除表格命令时,RootServer会通知每台
    1 d% R6 T. N! K5 V5 [! n9 y7 xChunkServer删除表格包含的所有子表。
    , K# U6 |1 z( \) P4)upgrade_tablet:每日合并后升级子表的版本号。如果没有发生分裂,只需要# e1 P5 E$ q6 E3 W1 g* j) I
    将老子表的版本号加1;否则,将老子表替换为多个范围连续的新子表,每个新子表
    % I& d! j) G2 f0 ]) S6 [的版本号均为老子表的版本号加1。
    " i# W3 ^1 a% T( g  n# v5 c/ H5)acquire_tablet/release_tablet:读取时首先调用acquire_tablet获取一个子表,增
    . l9 i0 ]! e( A/ w$ B  A加该子表的引用计数从而防止它在读取过程中被释放掉,接着读取其中的SSTable,
    4 e* q  d4 f5 r最后调用release_tablet释放子表。
    $ [. [& ]# u* @! c# D0 Z; {6)get_tablets_for_merge:每日合并时通过调用该函数获取下一批需要进行每日
      [9 O; }9 X- [) y0 q9 [$ a合并的子表。
    ' g' `7 B- Y* L2 O9.4.2 SSTable
    2 w- x% e  ~# Z  r8 n. ~9 p如图9-8所示,SSTable中的数据按主键排序后存放在连续的数据块(Block)
    " `/ D. v, c6 \1 ?- S, Q" }中,Block之间也有序。接着,存放数据块索引(Block Index),由每个Block最后一5 U2 S0 F) C2 q- o% O' Z
    行的主键(End Key)组成,用于数据查询中的Block定位。接着,存放布隆过滤器7 A4 ]! o6 u5 N) D2 E8 }5 V
    (Bloom Filter)和表格的Schema信息。最后,存放固定大小的Trailer以及Trailer的偏: @% K1 ]4 @4 e. p. g1 ]$ u3 k# U
    移位置。
    $ q) w* O# |" a9 I. n" X图 9-8 SSTable格式
    6 C( p2 `0 J* ]9 T查找SSTable时,首先从子表的索引信息中读取SSTable Trailer的偏移位置,接着" O' F$ u- ~1 J4 Q$ }  [4 W
    获取Trailer信息。根据Trailer中记录的信息,可以获取块索引的大小和偏移,从而将
    # X, O8 J. }. u3 b# j0 |整个块索引加载到内存中。根据块索引记录的每个Block的最后一行的主键,可以通
    # D. S$ M1 z: c6 \1 Y过二分查找定位到查找的Block。最后将Block加载到内存中,通过二分查找Block中9 |& Y, U0 `  _
    记录的行索引(Row Index)查找到具体某一行。本质上看,SSTable是一个两级索引
    , @4 d* v, B+ n# Z结构:块索引以及行索引;而整个ChunkServer是一个三级索引结构:子表索引、块
    7 r( L$ {1 }+ ]) r3 G, U  b$ n( F索引以及行索引。* r1 B7 O2 g. L1 b
    SSTable分为两种格式:稀疏格式以及稠密格式。对于稀疏格式,某些列可能存7 j, Y6 |  L+ p- N* X
    在,也可能不存在,因此,每一行只存储包含实际值的列,每一列存储的内容为:
    . T7 o- @' a% M$ o2 H& H: b<列ID,列值>(<Column ID,Column Value>);而稠密格式中每一行都需要存储1 {9 X, I1 o# H) T5 {3 Z
    所有列,每一列只需要存储列值,不需要存储列ID,这是因为列ID可以从表格8 U( }; G/ A  v$ y1 e: |$ n1 Q+ V
    Schema中获取。. O7 `: ^" y% q0 f  K
    例9-4 假设有一张表格包含10列,列ID为1~10,表格中有一行的数据内容
    $ l+ K, {, D. F为:9 `( H% W3 |( o  @7 D
    那么,如果采用稀疏格式存储,内容为:<2,20>,<3,30>,<5,50>,
    1 S6 l/ V7 C" N" Q: e<7,70>,<8,80>;如果采用稠密格式存储,内容为:null,20,30,null,
    0 S9 |" |1 y7 z$ z" V$ A9 K50,null,70,80,null,null。
    $ n# Z5 f9 m; E; Q$ d! ?- KChunkServer中的SSTable为稠密格式,而UpdateServer中的SSTable为稀疏格式,
    ( x8 `3 a( C" l. ?, N' _: `+ H且存储了多张表格的数据。另外,SSTable支持列组(Column Group),将同一个列; d$ o( A1 N2 K# x4 W; ?  @
    组下的多个列的内容存储在一块。列组是一种行列混合存储模式,将每一行的所有  g& ^+ |" |/ x9 p* R, z# I+ x
    列分成多个组(称为列组),每个列组内部按行存储。
    1 x% i# V2 Z6 D如图9-9所示,当一个SSTable中包含多个表格/列组时,数据按照[表格ID,列组
    ; ~5 A  N  F9 `% x  {% }% CID,行主键]([table_id,column group id,row_key])的形式有序存储。$ P5 @8 o0 R" }: c+ a( p2 N
    图 9-9 SSTable包含多个表格/列组
    1 ]" }7 G. |- ~9 m) P0 Q! T  R另外,SSTable支持压缩功能,压缩以Block为单位。每个Block写入磁盘之前调
    % z% _" u" H. P$ u用压缩算法执行压缩,读取时需要解压缩。用户可以自定义SSTable的压缩算法,目: g9 E5 X9 x0 k, y- I+ ]" ], F9 c% }- T
    前支持的算法包括LZO以及Snappy。
    1 f* k7 L' y( O/ hSSTable的操作接口分为写入和读取两个部分,其中,写入类为+ k  X* Y# X  n. ]7 t
    ObSSTableWriter,读取类为ObSSTableGetter(随机读取)和ObSSTableScanner(范围# w7 d& A" x0 ^
    查询)。代码如下:! s# z# o2 T, |, ]5 f  K$ D3 b
    class ObSSTableWriter
    + U8 f* e; ]; a8 E" a7 M{6 a" ~* `5 ]; v* L% x
    public:! t1 y- `; f' C* c+ _
    //创建SSTable
    - L- [  _( [; {  S- ^//@param[in]schema表格schema信息# k, e. f+ s' F1 J3 g4 M0 _
    //@param[in]path SSTable在磁盘中的路径名
    " l0 x' P) Y3 C, h0 z* ~+ ^& a//@param[in]compressor_name压缩算法名5 a3 f0 f7 O3 o+ n3 n5 h
    //@param[in]store_type SSTable格式,稀疏格式或者稠密格式
    / g+ I: R% K$ g6 u8 J  W  A//@param[in]block_size块大小,默认64KB
    & Y+ X4 k4 {% N1 `5 Aint create_sstable(const ObSSTableSchema&schema,const ObString&path,const. s. f' i) p. t1 Q$ t9 {3 h& C" [
    ObString&compressor_name,const int store_type,const int64_t block_size);
    9 F( t6 a) p7 r  M! m//往SSTable中追加一行数据& g( f# [" a+ O3 K% o# u; k9 X
    //@param[in]row一行SSTable数据
    # v2 B* o; G6 O//@param[out]space_usage追加完这一行后SSTable大致占用的磁盘空间
    9 x0 k' A/ L4 Mint append_row(const ObSSTableRow&row,int64_t&space_usage);
    3 W% \( Y$ U6 z//关闭SSTable,将往磁盘中写入Block Index,Bloom Filter,Schema,Trailer等信息2 N# P5 A3 C# B1 ^* |5 F2 O
    //@param[out]trailer_offset返回SSTable的Trailer偏移量
    : ?5 h+ g3 c8 S7 J/ wint close_sstable(int64_t&trailer_offset);
    ; _, J2 M' R7 _2 N9 M( T7 [" Z};
    - N+ V3 K, P6 q, S! D' X/ l定期合并&数据分发过程将产生新的SSTable,步骤如下:8 s/ H4 E% _! P7 a/ y3 ~' _
    1)调用create_sstable函数创建一个新的SSTable;
    # ~" T1 ^0 I% G0 `3 `2)不断调用append_row函数往SSTable中追加一行行数据;
      ?) s( }( _- n1 p; p! s3)调用close_sstable完成SSTable写入。7 P7 S" L( `! h+ f  K
    与9.2.1节中的MemTableIterator一样,ObSSTableGetter和ObSSTableScanner实现了0 w8 q4 c; n7 X
    迭代器接口,通过它可以不断地获取SSTable的下一个cell。( q0 R" c; Z& I# m) q4 M. s( |
    class ObIterator2 d8 _% q8 r" j' @1 z  @
    {+ }3 L6 D- {& g* h$ B
    public:
    $ _% C; |" q; ~8 |//迭代器移动到下一个cell! f2 c  I) N* b% `; L
    int next_cell();
    " r: A! x- F, l/ N0 b0 E//获取当前cell的内容7 I1 w& E4 m9 C& o
    //@param[out]cell_info当前cell的内容,包括表名(table_id),行主键(row_key),列编号' G" f# s0 \' ?; i: M
    (column_id)以及列值(column_value)
    ! ^( q  O" {: gint get_cell(ObCellInfo**cell_info);5 u7 x+ P2 _; V, w5 p2 \5 ?
    //获取当前cell的内容- d4 \# D4 N7 ~, S7 K" ~' [, E
    //@param[out]cell_info当前cell的内容
    1 r# ?$ z$ E/ z  U//@param is_row_changed是否迭代到下一行0 H5 g. h8 Y3 t8 o5 y4 h5 c9 `) U
    int get_cell(ObCellInfo**cell_info,bool*is_row_changed);
    1 h; Y, h- [: \/ `};7 \7 q: l( S5 X
    OceanBase读取的数据可能来源于MemTable,也可能来源于SSTable,或者是合  l& Z* [0 ~, C' g
    并多个MemTable和多个SSTable生成的结果。无论底层数据来源如何变化,上层的读
    ) K7 R6 p+ Y+ N) V, f8 K取接口总是ObIterator。
    7 ?: K0 z& e8 b" k9.4.3 缓存实现9 P, U( B5 I/ f
    ChunkServer中包含三种缓存:块缓存(Block Cache)、行缓存(Row Cache)以
    / {' ^# w3 v$ }  l2 V0 ~. Y3 O& Q及块索引缓存(Block Index Cache)。其中,块缓存中存储了SSTable中访问较热的数
    $ @9 R4 W1 i. Z: m据块(Block),行缓存中存储了SSTable中访问较热的数据行(Row),而块索引缓) ~- h. G7 [' }: u+ V# v
    存中存储了最近访问过的SSTable的块索引(Block Index)。一般来说,块索引不会
    7 R3 `* _& A2 ?. D% e9 g3 }太大,ChunkServer中所有SSTable的块索引都是常驻内存的。不同缓存的底层采用相
    " H$ p) {: i7 r6 t3 b) e# ?9 L同的实现方式。* v1 V* n8 f( w. g( W; j
    1.底层实现
    2 o0 i+ q5 C- S  ^2 r- y% Y, g经典的LRU缓存实现包含两个部分:哈希表和LRU链表,其中,哈希表用于查找
    # a; R+ m2 S1 o+ \缓存中的元素,LRU链表用于淘汰。每次访问LRU缓存时,需要将被访问的元素移动, g  S8 T0 `; _! Y! X" g
    到LRU链表的头部,从而避免被很快淘汰,这个过程需要锁住LRU链表。
    + w6 }" F% A) a如图9-10所示,块缓存和行缓存底层都是一个Key-Value Cache,实现步骤如下:2 s& _3 Y6 D7 B3 _  R; f% v
    图 9-10 Key-Value Cache的实现
      w+ i6 g& C6 N1)OceanBase一次分配1MB的连续内存块(称为memblock),每个memblock包
    ) d  e; ~, [0 N8 v含若干缓存项(item)。添加item时,只需要简单地将item追加到memblock的尾部;( x* s% |9 M2 _! k
    另外,缓存淘汰以memblock为单位,而不是以item为单位。
    1 O% L2 J0 M/ g2)OceanBase没有维护LRU链表,而是对每个memblock都维护了访问次数和最
    2 R( u- O6 U4 |; R, t! r+ L4 A近频繁访问时间。访问memblock中的item时将增加memblock的访问次数,如果最近一* ?4 s; Z" G  M) P6 X
    段时间之内的访问次数超过一定值,那么,更新最近频繁访问时间;淘汰memblock
    ! b7 X0 R; E9 l; t) \& p6 g时,对所有的memblock按照最近频繁访问时间排序,淘汰最近一段时间访问较少的
    " F2 D/ W: }" N; d7 q3 Bmemblock。可以看出,读取时只需要更新memblock的访问次数和最近频繁访问时6 }1 N1 `; G% M: {* v) O
    间,不需要移动LRU链表。这种实现方式通过牺牲LRU算法的精确性,来规避LRU链. @4 H6 f$ F% ^) B- s/ J
    表的全局锁冲突。
    6 U' L9 i  d' R) x3)每个memblock维护了引用计数,读取缓存项时所在memblock的引用计数加
    2 |5 ]" |8 r3 F4 F1 Y1,淘汰memblock时引用计数减1,引用计数为0时memblock可以回收重用。通过引用
    3 H9 `% c5 y1 O8 ?! `/ G计数,实现读取memblock中的缓存项不加锁。
    . m9 Z8 H9 |% N% F* |$ w2.惊群效应
    . O' Q  E' ~' B以行缓存为例,假设ChunkServer中有一个热点行,ChunkServer中的N个工作线
    : i$ {6 A" ^4 ^! d" h/ W/ L/ R程(假设为N=50)同时发现这一行的缓存失效,于是,所有工作线程同时读取这行
    # F% O" h- c, k$ R数据并更新行缓存。可以看出,N-1共49个线程不仅做了无用功,还增加了锁冲突。- z( o3 c  D9 Q/ i  a( p
    这种现象称为“惊群效应”。为了解决这个问题,第一个线程发现行缓存失效时会往1 `: N& T: O$ j: b+ L/ F. V
    缓存中加入一个fake标记,其他线程发现这个标记后会等待一段时间,直到第一个线
    ! {3 k* I* S: T" E程从SSTable中读到这行数据并加入到行缓存后,再从行缓存中读取。
    8 B/ T6 \" I9 k* q- ?1 l/ r! c算法描述如下:
    * k$ e0 \; ~( s% V; D7 \调用internal_get读取一行数据;  ^2 M% a8 H# V: e8 g4 ~
    if(行不存在){2 a, T, i: @% _
    调用internal_set往缓存中加入一个fake标记;% v: |3 x3 T/ q: S
    从SSTable中读取数据行;# B/ D- |. ]! Q: \4 \" F
    将SSTable中读到的行内容加入缓存,清除fake标记,唤醒等待线程;' k) I: R6 n  W1 l* G: V( |+ w
    返回读到的数据行;" p, C8 |, f! |, G  U
    }else if(行存在且为fake标记)0 N) I6 m/ k) X( p( L
    {
    ) Z3 f& X4 R* X6 h2 a线程等待,直到清除fake标记;
    # w) }& z* T4 W  e5 f8 p4 x  gif(等待成功)返回行缓存中的数据;
    7 k  j# q- _9 U/ rif(等待超时)返回读取超时;, V* [1 V  x4 J) D  |" n# y; b% Q
    }
    ! U( M* P$ u* Melse
    " g* ?) T0 ?0 h9 ^1 R; ?{8 \$ z8 l8 E  j4 A- L
    返回行缓存中的数据;1 Y  {; z9 m- r: @1 M8 k% E4 |# V
    }
    + j) A+ k( d8 e/ {: d* m9 Y) A8 r3.缓存预热
    ! C# Z5 K. c/ B; TChunkServer定期合并后需要使用生成的新的SSTable提供服务,如果大量请求同
    7 Y' _, D' g  F9 ^" w时读取新的SSTable文件,将使得ChunkServer的服务能力在切换SSTable瞬间大幅下
    ) ^5 Z- R0 ~2 ]降。因此,这里需要一个缓存预热的过程。OceanBase最初的版本实现了主动缓存预
    ) I% f: O+ m& k. J5 B  {热,即:扫描原来的缓存,根据每个缓存项的key读取新的SSTable并将结果加入到新
    9 z4 k8 q. F; d# C的缓存中。例如,原来缓存数据项的主键分别为100、200、500,那么只需要从新的
    8 u. T8 R- H: ?; X* ZSSTable中读取主键为100、200、500的数据并加入新的缓存。扫描完成后,原来的缓
    0 _2 ]) T* k6 K! E% x! c6 p存可以丢弃。
    # {8 _* |" U2 [线上运行一段时间后发现,定期合并基本上都安排在凌晨业务低峰期,合并完
    / R( G; A1 A, m+ G0 `成后OceanBase集群收到的用户请求总是由少到多(早上7点之前请求很少,9点以后
    / Y+ R8 `& w, z) Z. _" N请求逐步增多),能够很自然地实现被动缓存预热。由于ChunkServer在主动缓存预) z7 z/ N  o! E  P' W
    热期间需要占用两倍的内存,因此,目前的线上版本放弃了这种方式,转而采用被
    5 b) [3 n" i. y8 w8 k8 A, Z动缓存预热。
    * N! E) Z0 J2 l9.4.4 IO实现
    9 B9 _: X/ y5 Z0 tOceanBase没有使用操作系统本身的页面缓存(page cache)机制,而是自己实现
    % N) n" ?6 d& R( v缓存。相应地,IO也采用Direct IO实现,并且支持磁盘IO与CPU计算并行化。
    / o, l( E* \1 [( D3 p* g$ u; HChunkServer采用Linux的Libaio [1] 实现异步IO,并通过双缓冲区机制实现磁盘预读
    + g6 Y! Z0 s- }% N2 }) L与CPU处理并行化,实现步骤如下:/ \8 ^3 h) _4 a2 f1 e
    1)分配当前(current)以及预读(ahead)两个缓冲区;9 Q" T+ u7 _6 R5 ]3 i+ M
    2)使用当前缓冲区读取数据,当前缓冲区通过Libaio发起异步读取请求,接着
    + q9 ?/ Z! p2 v7 Z+ z& N等待异步读取完成;
    7 w# f  f- v4 {; J, I+ K, k3)异步读取完成后,将当前缓冲区返回上层执行CPU计算,同时,原来的预读& p8 w0 N  K, ~4 R3 y8 b
    缓冲区变为新的当前缓冲区,发送异步读取请求将数据读取到新的当前缓冲区。
    ; A8 N' W3 ]/ i; j7 s) V6 n) nCPU计算完成后,原来的当前缓冲区变为空闲,成为新的预读缓冲区,用于下一次. R5 t' [8 \& S4 A8 [3 F: g
    预读。* }. g7 d- v- a& R7 b  L
    4)重复步骤3),直到所有数据全部读完。% g1 \( Y, x5 D9 T: \2 y
    例9-5 假设需要读取的数据范围为(1,150],分三次读取:(1,50],(50,
    * {' s) i5 ?! Q" j! A; v, U100],(100,150],当前和预读缓冲区分别记为A和B。实现步骤如下:
    ! T4 L8 o( f  l1)发送异步请求将(1,50]读取到缓冲区A,等待读取完成;' t5 _1 E; Q  n! U! D  ^* j( }
    2)对缓冲区A执行CPU计算,发送异步请求,将(50,100]读取到缓冲区B;- f0 m( ]* t- g8 M* s# M
    3)如果CPU计算先于磁盘读取完成,那么,缓冲区A变为空闲,等到(50,# J4 n2 m& f& U, p, s+ P, V( v+ _
    100]读取完成后将缓冲区B返回上层执行CPU计算,同时,发送异步请求,将7 T- p' T" E5 S; e1 b0 z' i
    (100,150]读取到缓冲区A;
    3 a# s; U7 `) W8 r7 ^4)如果磁盘读取先于CPU计算完成,那么,首先等待缓冲区A上的CPU计算完! [  p7 M4 D  w
    成,接着,将缓冲区B返回上层执行CPU计算,同时,发送异步请求,将(100,
    ) F9 T; P6 |% W. l9 |  e150]读取到缓冲区A;! ~3 T% k0 V8 V8 q: S
    5)等待(100,150]读取完成后,将缓冲区A返回给上层执行CPU计算。
    0 S4 k' y  I- H' T, ?双缓冲区广泛用于生产者/消费者模型,ChunkServer中使用了双缓冲区异步预读
    8 u: s- K3 y8 N0 F: U( d7 X, z的技术,生产者为磁盘,消费者为CPU,磁盘中生产的原始数据需要给CPU计算消费
    ( q3 ?( j: i  r+ b4 m1 D2 Z. ^掉。
    ( I" s1 B; {* d) @# f/ Y- L所谓“双缓冲区”,顾名思义就是两个缓冲区(简称A和B)。这两个缓冲区,总" |" Q% |0 r( d  R' j" Q* k
    是一个用于生产者,另一个用于消费者。当两个缓冲区都操作完,再进行一次切2 ^9 P/ x' {% Z) E$ D5 A0 ~8 f
    换,先前被生产者写入的被消费者读取,先前消费者读取的转为生产者写入。为了7 K) }! W% ?9 ?( Q2 N
    做到不冲突,给每个缓冲区分配一把互斥锁(简称La和Lb)。生产者或者消费者如: w) U$ ^. k: n5 Q$ ?/ c
    果要操作某个缓冲区,必须先拥有对应的互斥锁。
    ; B( c8 b" j( J6 ~6 F3 w0 P双缓冲区包括如下几种状态:
    ' Q: _+ `5 c! u0 @  N1 n. s●双缓冲区都在使用的状态(并发读写)。大多数情况下,生产者和消费者都处9 t4 g5 g/ d) R8 w  O
    于并发读写状态。不妨设生产者写入A,消费者读取B。在这种状态下,生产者拥有
    ! K! t, {& X% S7 c5 I锁La;同样地,消费者拥有锁Lb。由于两个缓冲区都是处于独占状态,因此每次读# A+ j8 V' ~& J" v
    写缓冲区中的元素都不需要再进行加锁、解锁操作。这是节约开销的主要来源。5 V' Z, B0 G& ~
    ●单个缓冲区空闲状态。由于两个并发实体的速度会有差异,必然会出现一个缓
    , m9 i% {! w% ?, D0 Z0 Y5 O) s冲区已经操作完,而另一个尚未操作完。不妨假设生产者快于消费者。在这种情况
    6 H9 N+ }  S% p" `" J下,当生产者把A写满的时候,生产者要先释放La(表示它已经不再操作A),然后
    : ^& m7 V3 x! w0 j1 T" [尝试获取Lb。由于B还没有被读空,Lb还被消费者持有,所以生产者进入等待
    . E! B7 M) y. @# D$ ]( L(wait)状态。; J1 _2 f) P4 z4 M7 Y% R* H
    ●缓冲区的切换。过了若干时间,消费者终于把B读完。这时候,消费者也要先
    9 r: Z9 w. O6 q6 P5 N2 C3 b3 T释放Lb,然后尝试获取La。由于La刚才已经被生产者释放,所以消费者能立即拥有
    8 a- V6 R  z( s; S- U- o1 E$ oLa并开始读取A的数据。而由于Lb被消费者释放,所以刚才等待的生产者会苏醒过来
    8 K1 x1 ?( @, w$ Z(wakeup)并拥有Lb,然后生产者继续往B写入数据。
    3 Q$ c- N9 a. y1 u6 `$ s[1]Oracle公司实现的Linux异步IO库,开源地址:https://oss.oracle.com/projects/libaio-
    , K2 \$ f* i8 W4 P( qoracle/6 Z1 K- b9 }# U! j; x* d
    9.4.5 定期合并&数据分发7 I2 B$ W) v4 W- }7 S8 z
    RootServer将UpdateServer上的版本变化信息通知ChunkServer后,ChunkServer将
    1 q. j  S  G; ?1 L" L! h执行定期合并或者数据分发。- E! g" V3 L4 [
    如果UpdateServer执行了大版本冻结,ChunkServer将执行定期合并。ChunkServer
    & t5 E7 u* u' n  _' {唤醒若干个定期合并线程(比如10个),每个线程执行如下流程:, `. W" G5 D! H0 d9 b
    1)加锁获取下一个需要定期合并的子表;
    ! y( _+ |1 x* I" f4 [2)根据子表的主键范围读取UpdateServer中的修改操作;$ {0 {) I% V2 x
    3)将每行数据的基线数据和增量数据合并后,产生新的基线数据,并写入到新! N; u3 ~7 L4 A; b1 A; I
    的SSTable中;2 d( L2 L% a0 m! E5 J# |, B
    4)更改子表索引信息,指向新的SSTable。0 S8 ?3 ~+ K8 j5 u4 M: ^
    等到ChunkServer上所有的子表定期合并都执行完成后,ChunkServer会向5 ]8 f% w- M9 N
    RootServer汇报,RootServer会更新RootTable中记录的子表版本信息。定期合并一般3 F! V& p, q) ?; {( O
    安排在每天凌晨业务低峰期(凌晨1:00开始)执行一次,因此也称为每日合并。另- [' T6 D' k) j! O+ I
    外,定期合并过程中ChunkServer的压力比较大,需要控制合并速度,否则可能影响; p" X9 ^- d4 Q" B. U. |# s& r
    正常的读取服务。
    / u# V7 g0 w: R) t9 R; _- E9 `& L* I如果UpdateServer执行了小版本冻结,ChunkServer将执行数据分发。与定期合并1 h% i; Z, J0 o" {  S
    不同的是,数据分发只是将UpdateServer冻结的数据缓存到ChunkServer,并不会生成
    & e2 C( X) D! G' e% |; @新的SSTable文件。因此,数据分发对ChunkServer造成的压力不大。
    . k  H: J' e3 Q6 B8 z数据分发由外部读取请求驱动,当请求ChunkServer上的某个子表时,除了返回" o, ]' E# O6 `& I2 S
    使用者需要的数据外,还会在后台生成这个子表的数据分发任务,这个任务会获取
    0 B% I* W$ M0 z3 uUpdateServer中冻结的小版本数据,并缓存在ChunkServer的内存中。如果内存用完,
    * g8 X: E2 i! X5 a9 ]; `% o: K1 [数据分发任务将不再进行。当然,这里可以做一些改进,比如除了将UpdateServer分
    " z* S- S1 i6 b发的数据存放到ChunkServer的内存中,还可以存储到SSD磁盘中。% K' Q  E& B; g" f) A
    例9-6 假设某台ChunkServer上有一个子表t1,t1的主键范围为(1,10],只有一
    , B  ~" N! _) J1 @$ [行数据:rowkey=8=>(<2,update,20>,<3,update,30>,<4,update,40
    1 l5 }* V( c) {9 ?' Q>)。UpdateServer的冻结版本有两行更新操作:rowkey=8=>(<2,update,30! T  q9 f' O$ C3 L! Y
    >,<3,up-date,38>)和rowkey=20=>(<4,update,50>)。+ O3 J/ A( x$ p0 R) V. E
    ●如果是大版本冻结,那么,ChunkServer上的子表t1执行定期合并后结果为:- M- D' N' ~* j/ C, u, ~* K/ c
    ro-wkey=8=>(<2,update,30>,<3,update,38>,<4,update,40>);2 \$ B" j# H( U7 p* e$ ^
    ●如果是小版本冻结,那么,ChunkServer上的子表t1执行数据分发后的结果为:$ S) F' j" [: A1 G
    rowkey=8=>(<2,update,20>,<3,update,30>,<4,update,40>,<2,
    7 e- b; X! l8 Tupdate,30>,<3,update,38>)。& U' v! v; X; \. _  F& O3 g
    9.4.6 定期合并限速, p9 Y# f; [3 S8 d2 m
    定期合并期间系统的压力较大,需要控制定期合并的速度,避免影响正常服, L1 g& p" y9 b  V, T
    务。定期合并限速的措施包括如下步骤:
    # K# o& p9 R, E3 A1)ChunkServer:ChunkServer定期合并过程中,每合并完成若干行(默认2000) ^* Y/ \; A$ y. k& d7 P5 y. ]
    行)数据,就查看本机的负载(查看Linux系统的Load值)。如果负载过高,一部分+ s9 |. [+ Z( {, F
    定期合并线程转入休眠状态;如果负载过低,唤醒更多的定期合并线程。另外,
    ' F3 C) k9 F+ URootServer将UpdateServer冻结的大版本通知所有的ChunkServer,每台ChunkServer会
    2 `$ T# I  N/ y  z5 T# {/ s2 L随机等待一段时间再开始执行定期合并,防止所有的ChunkServer同时将大量的请求+ L  r) d6 U7 B* Z6 l7 R, n# P
    发给UpdateServer。3 _* L9 d; B$ o; k& B% `
    2)UpdateServer:定期合并过程中ChunkServer需要从UpdateServer读取大量的数7 O; Y. E- V# {# b
    据,为了防止定期合并任务用满带宽而阻塞用户的正常请求,UpdateServer将任务区  r2 s5 q: q0 W0 a
    分为高优先级(用户正常请求)和低优先级(定期合并任务),并单独统计每种任& i* m4 F2 H4 o3 E' i
    务的输出带宽。如果低优先级任务的输出带宽超过上限,降低低优先级任务的处理. L( P5 I2 T" U/ _6 T# v& q
    速度;反之,适当提高低优先级任务的处理速度。
      \1 d2 m8 G2 q8 K* @# j如果OceanBase部署了两个集群,还能够支持主备集群在不同时间段进行“错峰) d" E: v( F- @4 j, X
    合并”:一个集群执行定期合并时,把全部或大部分读写流量切到另一个集群,该集
    & v! i+ ^& g: T" H" N* a群合并完成后,把全部或大部分流量切回,以便另一个集群接着进行定期合并。两
    & r% I' _: n0 r3 j7 K个集群都合并完成后,恢复正常的流量分配。
    - z2 T7 P/ E" F/ C# m
    ( w8 f4 p* i7 y/ g* x% ]# U$ w1 s3 o2 t' |4 I
    回复

    使用道具 举报

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

    本版积分规则

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

    GMT+8, 2025-4-1 14:40 , Processed in 0.248249 second(s), 32 queries .

    Powered by Javazx

    Copyright © 2012-2022, Javazx Cloud.

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