java自学网VIP

Java自学网

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 2553|回复: 0

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

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

    [LV.Master]出神入化

    2025

    主题

    3683

    帖子

    6万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    66345

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

    发表于 2017-3-6 14:40:59 | 显示全部楼层 |阅读模式
    9.4 ChunkServer实现机制# x% D$ s( [9 }# o8 a# h
    ChunkServer用于存储基线数据,它由如下基本部分组成:
    5 I! W- [- U# [0 o+ I  }) t% M●管理子表,主动实现子表分裂,配合RootServer实现子表迁移、删除、合并;
    . e4 O" e' Q. P+ ]- q●SSTable,根据主键有序存储每个子表的基线数据;  W/ m5 z1 i7 Z5 ^8 A0 }8 o
    ●基于LRU实现块缓存(Block cache)以及行缓存(Row cache);
    7 e6 ?# P1 ]4 S3 i●实现Direct IO,磁盘IO与CPU计算并行化;
    9 y) W% {# V" m●通过定期合并&数据分发获取UpdateServer的冻结数据,从而分散到整个集
    ) e# r  X+ K7 L: v6 q# G! u群。
    3 y# H/ v# ?8 g2 _每台ChunkServer服务着几千到几万个子表的基线数据,每个子表由若干个
      m) Q; W9 y2 R8 c/ `SSTable组成(一般为1个)。下面从SSTable开始介绍ChunkServer的内部实现。
    3 k7 t! O; S1 `2 B7 R' m, _9.4.1 子表管理, x: a, C! J- e+ s; U2 Q
    每台ChunkServer服务于多个子表,子表的个数一般在10000~100000之间。
    . a4 y8 J4 e5 b3 J4 u. D/ cChunk-Server内部通过ObMultiVersionTabletImage来存储每个子表的索引信息,包括数" O# P& z, W% K1 _0 P# ~/ L
    据行数(row_count),数据量(occupy_size),校验和(check_sum),包含的1 p! E1 a1 T5 n9 F1 M' ]; x
    SSTable列表,所在磁盘编号(disk_no)等,代码如下:/ _9 _; i+ W5 G/ s5 R& }( N0 ]
    class ObMultiVersionTabletImage- V: a& W, m* |& X: V
    {
    2 S" H# _2 I- O$ R4 ypublic:0 g! V7 h) _3 x; _- ~
    //获取第一个包含指定数据范围的子表5 ~: b. S% F; r1 {1 Q7 b9 |0 k
    //@param[in]range数据范围  s6 `) d% H$ ]) e; z% P# w+ F
    //@param[in]scan_direction正向扫描(默认)还是逆向扫描$ Z6 o2 T6 {" O
    //@param[in]version子表的版本号% D0 y2 V% X) f9 _: u9 o& o5 {
    //@param[out]tablet获取的子表索引结构
    6 J$ \1 @% N% Xint acquire_tablet(const ObNewRange&range,const ScanDirection. u% [) C/ @" R
    scan_direction,const int64_t version,ObTablet*&tablet)const;
    % U# S; g) h) o//释放一个子表; e6 y2 X' x3 |5 O
    int release_tablet(ObTablet*tablet);' `! l% _4 g; J9 u* d& Y/ X* P" ?
    //新增一个子表,load_sstable表示是否立即加载其中的SSTable文件. R" Q; g9 g* ?/ s' h$ @$ E
    int add_tablet(ObTablet*tablet,const bool load_sstable=false);' Q; v4 W3 d( B: F& N
    //每日合并后升级子表到新版本,load_sstable表示是否立即加载新版本的SSTable文件0 h" N; o  ?4 v6 n
    int upgrade_tablet(ObTablet*old_tablet,ObTablet*new_tablet,const bool; q9 [: ~  K, a
    load_sstable=false);: J) Y% \4 k+ U& [% p
    //每日合并后升级子表到新版本,且子表发生分裂,有一个变成多个。load_sstable表示是否立即加载. X( Q. e  a+ F, Q; }, e' h+ H  w8 d5 V
    分裂后的SSTable文件5 X8 g0 ]9 F. L0 z0 u" T
    int upgrade_tablet(ObTablet*old_tablet,ObTablet*new_tablets[],const int32_t6 L( u" N* A7 o
    split_size,const bool load_sstable=false);
    / m5 G( l# Y* Z  V//删除一个指定数据范围和版本的子表  ]4 {0 F9 M) J) T2 ?% `
    int remove_tablet(const ObNewRange&range,const int64_t version);* F  L( J, ~( O; r5 V
    //删除一个表格对应的所有子表
    ! X2 ]  N, B& J5 ~5 a% H) O. ~int delete_table(const uint64_t table_id);6 B  D# d- R1 Y$ g# ]0 l' ?1 F
    //获取下一批需要进行每日合并的子表
    , }1 `4 a4 A) O% [; h6 ^. m" `//@param[in]version子表的版本号% n% o. @4 H3 `+ o! Q
    //@param[out]size下一批需要进行每日合并的子表个数
    8 b  Z) s: _. e# F* T9 X( L: V//@param[out]tablets下一批需要进行每日合并的子表索引结构; K$ P" q( Z4 Y4 d' D
    int get_tablets_for_merge(const int64_t version,int64_t&size,ObTablet*&
    5 j) d) {+ F: \tablets[])const;+ U- C6 ~# H: T4 K' c9 V! W
    };
    1 z3 |) y" }2 a$ |$ x+ sChunkServer维护了多个版本的子表数据,每日合并后升级子表的版本号。如果
    % A. f% S" a! `8 x4 k子表发生分裂,每日合并后将由一个子表变成多个子表。子表相关的操作方法包
    , `* R3 u0 }' d( ?$ s8 k括:
    8 N- @; r1 i5 ~& L5 }7 e6 k1)add_tablet:新增一个子表。如果load_sstable参数为true,那么,立即加载其' F. ~2 k, @; q
    中的SSTable文件。否则,使用延迟加载策略,即读取子表时再加载其中的SSTable。0 l0 v( y6 t% w. ]' E/ I" N" y
    2)remove_tablet:删除一个子表。RootServer发现某个子表的副本数过多,则会
    % W) |8 V* ~# l/ c4 @! Q通知其中某台ChunkServer删除指定的子表。
      }4 E+ j. N, ~* k3)delete_table:删除表格。用户执行删除表格命令时,RootServer会通知每台& c5 z  H* w) N' T, M9 t
    ChunkServer删除表格包含的所有子表。
    1 g' |. f% `1 ~: K: m( x' t4)upgrade_tablet:每日合并后升级子表的版本号。如果没有发生分裂,只需要
    ' ]& y6 p& F4 }- ?2 q3 g) L将老子表的版本号加1;否则,将老子表替换为多个范围连续的新子表,每个新子表! m8 W2 Q" g4 _8 `
    的版本号均为老子表的版本号加1。! w3 A+ h3 K, |) K% i! z! I
    5)acquire_tablet/release_tablet:读取时首先调用acquire_tablet获取一个子表,增8 i8 G* g: B2 C* ~0 S5 i( E
    加该子表的引用计数从而防止它在读取过程中被释放掉,接着读取其中的SSTable,
    7 W9 ?6 d' j8 ~1 e# l9 \最后调用release_tablet释放子表。
    & h0 {7 E0 s, k5 k6)get_tablets_for_merge:每日合并时通过调用该函数获取下一批需要进行每日" p% i/ L) {% v0 j5 f
    合并的子表。
    $ Y; l- h' F7 U+ c" F. x9.4.2 SSTable
    9 l5 p' w2 Y7 |% m; y9 @5 k如图9-8所示,SSTable中的数据按主键排序后存放在连续的数据块(Block)
    / O$ e9 z, Q# \8 c6 g( s! B7 m中,Block之间也有序。接着,存放数据块索引(Block Index),由每个Block最后一  ~8 }: z% Q( g8 s
    行的主键(End Key)组成,用于数据查询中的Block定位。接着,存放布隆过滤器- w& t; H9 W# q1 ?3 a$ V
    (Bloom Filter)和表格的Schema信息。最后,存放固定大小的Trailer以及Trailer的偏* P+ @4 I( Y$ O  _6 r8 z/ s  B
    移位置。
    * J" u  h+ z2 L0 @图 9-8 SSTable格式1 y( _; O4 a+ b' m( u
    查找SSTable时,首先从子表的索引信息中读取SSTable Trailer的偏移位置,接着
    5 \( y" l0 I8 i: J! |) b1 l获取Trailer信息。根据Trailer中记录的信息,可以获取块索引的大小和偏移,从而将7 m8 G( M. Q3 F: o  a
    整个块索引加载到内存中。根据块索引记录的每个Block的最后一行的主键,可以通. C1 D$ r" L5 F# }$ j4 Q
    过二分查找定位到查找的Block。最后将Block加载到内存中,通过二分查找Block中% e7 S+ Z3 s; _" _# j
    记录的行索引(Row Index)查找到具体某一行。本质上看,SSTable是一个两级索引
    1 q3 y0 v$ n" ?: O结构:块索引以及行索引;而整个ChunkServer是一个三级索引结构:子表索引、块
    $ u; p( A5 O; B+ q" G索引以及行索引。
    2 Y% |  g1 J5 U  F3 [& c& CSSTable分为两种格式:稀疏格式以及稠密格式。对于稀疏格式,某些列可能存
    - f2 ~' ~  l* l# M+ e在,也可能不存在,因此,每一行只存储包含实际值的列,每一列存储的内容为:! n  C0 A5 \4 P: e3 Z1 O% @
    <列ID,列值>(<Column ID,Column Value>);而稠密格式中每一行都需要存储  W/ W7 ~! h4 Q1 y
    所有列,每一列只需要存储列值,不需要存储列ID,这是因为列ID可以从表格& v' w0 ^# n0 i) G2 a
    Schema中获取。
    3 q" H4 L0 q4 f例9-4 假设有一张表格包含10列,列ID为1~10,表格中有一行的数据内容
    - R7 B& R; k+ z" D2 T7 F为:8 Z3 |" ~9 M- ^, |& h
    那么,如果采用稀疏格式存储,内容为:<2,20>,<3,30>,<5,50>,5 S% E- q9 V0 c5 A5 u- U0 B% \
    <7,70>,<8,80>;如果采用稠密格式存储,内容为:null,20,30,null,
    / q$ u: H- S$ |( H! D6 f50,null,70,80,null,null。
    5 P3 G) z8 }, j3 WChunkServer中的SSTable为稠密格式,而UpdateServer中的SSTable为稀疏格式,
    5 O" @9 ]/ F' [8 j* C且存储了多张表格的数据。另外,SSTable支持列组(Column Group),将同一个列
    + _: H9 O3 p* E" L7 W' t  |组下的多个列的内容存储在一块。列组是一种行列混合存储模式,将每一行的所有
    - `0 A3 t# V# f* [4 I# z4 K% u列分成多个组(称为列组),每个列组内部按行存储。1 W, w; i8 T. u0 w
    如图9-9所示,当一个SSTable中包含多个表格/列组时,数据按照[表格ID,列组
    ' \: H3 n2 L: z$ G) Z, KID,行主键]([table_id,column group id,row_key])的形式有序存储。
    & `" Q. f$ Z$ t1 z8 ^( C图 9-9 SSTable包含多个表格/列组
    % Q+ [, j1 I3 n  y# d另外,SSTable支持压缩功能,压缩以Block为单位。每个Block写入磁盘之前调
    + b( k' I. e6 d$ N% f用压缩算法执行压缩,读取时需要解压缩。用户可以自定义SSTable的压缩算法,目( }9 q$ \6 ^) ?  `- S% E
    前支持的算法包括LZO以及Snappy。9 x( j, Y8 V: M9 |
    SSTable的操作接口分为写入和读取两个部分,其中,写入类为
    & ], I3 D/ t; x0 s% YObSSTableWriter,读取类为ObSSTableGetter(随机读取)和ObSSTableScanner(范围5 A' ]* P' [9 X# k/ v& o
    查询)。代码如下:! A9 o: d( b8 Y. g5 h. ^/ c
    class ObSSTableWriter
    : }% y. f; b$ C( s{, F) @% [/ [6 C+ k
    public:! y% q9 \& P, @# c- l, U
    //创建SSTable
    / U* {) j! D: S. m6 q//@param[in]schema表格schema信息$ s2 v% G6 S3 k2 s  x. D% T" G
    //@param[in]path SSTable在磁盘中的路径名
      r  ^' F2 P( J3 T$ Y: a//@param[in]compressor_name压缩算法名+ ^, q7 X# y, Z( g' a6 v
    //@param[in]store_type SSTable格式,稀疏格式或者稠密格式
    ; _3 j+ G: D% j# i5 S  v//@param[in]block_size块大小,默认64KB! p/ F/ ^1 g8 A8 T* \9 f
    int create_sstable(const ObSSTableSchema&schema,const ObString&path,const
    + p5 K1 D* C8 x( VObString&compressor_name,const int store_type,const int64_t block_size);
    5 m) Z! Q( H! C8 y( w//往SSTable中追加一行数据# A# Z2 f& ^% K
    //@param[in]row一行SSTable数据
    ; [6 f$ T1 x5 x6 ]9 d0 I//@param[out]space_usage追加完这一行后SSTable大致占用的磁盘空间7 e! y! k6 m" ]9 i2 m8 E* t
    int append_row(const ObSSTableRow&row,int64_t&space_usage);
    $ Q7 ]- Y; N5 _, m: p6 V//关闭SSTable,将往磁盘中写入Block Index,Bloom Filter,Schema,Trailer等信息# h7 l% s/ H; e0 F, I4 o- k
    //@param[out]trailer_offset返回SSTable的Trailer偏移量' [9 @, `8 B, R3 h+ F
    int close_sstable(int64_t&trailer_offset);) o0 b6 H- i; H2 R
    };
    9 ^. f" E( t% U8 S0 s7 ~定期合并&数据分发过程将产生新的SSTable,步骤如下:0 m+ l0 V) O/ \6 y2 W3 y6 h6 j
    1)调用create_sstable函数创建一个新的SSTable;) |3 V6 g; L6 F# c- J8 D! O
    2)不断调用append_row函数往SSTable中追加一行行数据;
    ' Z9 m6 `5 _0 x& E* J6 Q3)调用close_sstable完成SSTable写入。& B  G" ?2 l; Q  z3 E6 L
    与9.2.1节中的MemTableIterator一样,ObSSTableGetter和ObSSTableScanner实现了
    5 B" N  Q8 v- H" Q. {( S迭代器接口,通过它可以不断地获取SSTable的下一个cell。/ u2 |* P0 d% c9 x# K0 e9 @$ ^  a" K& e
    class ObIterator1 B9 \# j0 [8 e' W# o
    {
    . {5 U4 Z" E0 T/ b6 @2 ?public:
    , w$ l: W7 x  r1 f& {9 Z: K//迭代器移动到下一个cell8 M) a% T; W/ M6 C
    int next_cell();0 c# r1 R6 k6 Y* G% _
    //获取当前cell的内容: m6 [2 Q3 V; N0 D+ o
    //@param[out]cell_info当前cell的内容,包括表名(table_id),行主键(row_key),列编号+ A. I7 j( R: [, Z* Y
    (column_id)以及列值(column_value)
    * G7 N$ O' N) s( [# nint get_cell(ObCellInfo**cell_info);
    0 {2 h) l/ }% _: _; N% Y% |//获取当前cell的内容
    0 Y6 w7 l5 a+ O5 Y//@param[out]cell_info当前cell的内容
    6 X: x- O; T7 _//@param is_row_changed是否迭代到下一行
    / B+ s0 W( N& a: [4 K) g8 i2 dint get_cell(ObCellInfo**cell_info,bool*is_row_changed);
    ! W+ ?! f  v; D: {% w- T+ T};0 q& ^8 X3 F) F# ~# B1 W
    OceanBase读取的数据可能来源于MemTable,也可能来源于SSTable,或者是合
    / i$ [1 R' i4 y/ C: M并多个MemTable和多个SSTable生成的结果。无论底层数据来源如何变化,上层的读1 W+ u8 u  F6 W9 }. G
    取接口总是ObIterator。
    . f* _( \7 f0 H3 v+ e9.4.3 缓存实现
    : D: J# L3 t9 i! e1 M2 sChunkServer中包含三种缓存:块缓存(Block Cache)、行缓存(Row Cache)以# E0 ^. s. I1 x/ G5 d1 }
    及块索引缓存(Block Index Cache)。其中,块缓存中存储了SSTable中访问较热的数7 W7 T1 I8 W% R% Y& O# N0 n
    据块(Block),行缓存中存储了SSTable中访问较热的数据行(Row),而块索引缓& x1 P! q5 G: m6 `3 E
    存中存储了最近访问过的SSTable的块索引(Block Index)。一般来说,块索引不会
    ) Z4 n  G8 }% A5 ]太大,ChunkServer中所有SSTable的块索引都是常驻内存的。不同缓存的底层采用相
    6 ?% n9 f" \, M1 |% N* _同的实现方式。* Q! a5 k) `+ \, ~
    1.底层实现* b, d/ M3 {9 f& a% }# ~+ _
    经典的LRU缓存实现包含两个部分:哈希表和LRU链表,其中,哈希表用于查找- _, }4 f" s0 \8 }7 p9 V
    缓存中的元素,LRU链表用于淘汰。每次访问LRU缓存时,需要将被访问的元素移动' |3 b, O% y1 k' @3 A
    到LRU链表的头部,从而避免被很快淘汰,这个过程需要锁住LRU链表。* O$ E2 C6 w4 O6 H
    如图9-10所示,块缓存和行缓存底层都是一个Key-Value Cache,实现步骤如下:
    * c( g6 v1 _8 e  c% P图 9-10 Key-Value Cache的实现  _( v0 e8 K: C! X% ?
    1)OceanBase一次分配1MB的连续内存块(称为memblock),每个memblock包
      L' H" c4 _; b. u& c5 V含若干缓存项(item)。添加item时,只需要简单地将item追加到memblock的尾部;
    3 u- O& `# s. _另外,缓存淘汰以memblock为单位,而不是以item为单位。; p- |$ A" O% d. p4 r! `9 c
    2)OceanBase没有维护LRU链表,而是对每个memblock都维护了访问次数和最0 t( |5 B3 V5 v1 c
    近频繁访问时间。访问memblock中的item时将增加memblock的访问次数,如果最近一: x$ B: g7 q& K5 \0 y0 [
    段时间之内的访问次数超过一定值,那么,更新最近频繁访问时间;淘汰memblock
    / D$ T  U6 e( i: h  @时,对所有的memblock按照最近频繁访问时间排序,淘汰最近一段时间访问较少的2 Q3 Z" T& ?  C1 s0 E* A
    memblock。可以看出,读取时只需要更新memblock的访问次数和最近频繁访问时
    * q3 L1 c( }$ g" {# F4 A3 n间,不需要移动LRU链表。这种实现方式通过牺牲LRU算法的精确性,来规避LRU链/ [3 M" }* L. h5 |0 x# T$ W
    表的全局锁冲突。+ [0 P/ g; g: K# {  m
    3)每个memblock维护了引用计数,读取缓存项时所在memblock的引用计数加9 y# M4 [, }* h# |% F, r- b3 A
    1,淘汰memblock时引用计数减1,引用计数为0时memblock可以回收重用。通过引用. \2 k+ Z; C& l3 `
    计数,实现读取memblock中的缓存项不加锁。
    ) H7 G( E* O: u2.惊群效应! b# [( P0 t6 X) L* I6 i5 W
    以行缓存为例,假设ChunkServer中有一个热点行,ChunkServer中的N个工作线
    3 b& t" ]( ^) [# Q7 A6 O程(假设为N=50)同时发现这一行的缓存失效,于是,所有工作线程同时读取这行
    0 a) x% H9 N$ Q数据并更新行缓存。可以看出,N-1共49个线程不仅做了无用功,还增加了锁冲突。/ M8 ~) ], L0 T. K4 i3 P
    这种现象称为“惊群效应”。为了解决这个问题,第一个线程发现行缓存失效时会往+ i# S  `% a- e! Q; T6 y
    缓存中加入一个fake标记,其他线程发现这个标记后会等待一段时间,直到第一个线
    " f6 _6 I. ]6 V5 P! c  u1 |程从SSTable中读到这行数据并加入到行缓存后,再从行缓存中读取。
    0 ^& ^& y9 c0 b5 f/ M4 g算法描述如下:
    , I8 I9 d5 c# s4 d- j# z' x6 P调用internal_get读取一行数据;
    " `- ]) e) g+ u! t/ N1 z! Eif(行不存在){4 W, \/ l  p& k& F  ]
    调用internal_set往缓存中加入一个fake标记;& Y+ }* ]+ B& u- g9 R6 |0 g
    从SSTable中读取数据行;' q: N( P9 P% K0 z: N6 {2 t
    将SSTable中读到的行内容加入缓存,清除fake标记,唤醒等待线程;; S' V4 h2 ?- N! T" t8 t" O; g
    返回读到的数据行;
    + [2 n5 F  H& c. v}else if(行存在且为fake标记). _, K/ Z! d0 n! M7 E& a
    {1 D* ^0 z; A# @2 }- _
    线程等待,直到清除fake标记;. T7 K3 H% D4 o, l/ v( |- ~. l
    if(等待成功)返回行缓存中的数据;
    1 S4 J- {' G2 t' v' R. S% b4 bif(等待超时)返回读取超时;
    6 P0 V- s% @; o* O}' a, X) l+ Z5 c
    else/ x. @) [+ D4 c6 U2 q
    {
    + L3 G  A9 r: X( W9 c+ e返回行缓存中的数据;
    9 A) H; S: S4 z9 o1 }/ t$ B% {, J}; e; I: e3 ^/ z3 U. K* A% j
    3.缓存预热& ?5 U2 h% k/ m/ d0 T1 a) ?
    ChunkServer定期合并后需要使用生成的新的SSTable提供服务,如果大量请求同
    6 f9 }4 I) h  O! s5 t9 U" d4 w! e时读取新的SSTable文件,将使得ChunkServer的服务能力在切换SSTable瞬间大幅下
    " d1 D5 i! l3 j3 i3 \* Q" `% U5 p降。因此,这里需要一个缓存预热的过程。OceanBase最初的版本实现了主动缓存预- W$ Y8 |1 C. E# v/ [) ?
    热,即:扫描原来的缓存,根据每个缓存项的key读取新的SSTable并将结果加入到新5 S2 V5 h3 R7 `1 e0 e" V. |: G
    的缓存中。例如,原来缓存数据项的主键分别为100、200、500,那么只需要从新的
    - u# ]9 T9 A0 ]SSTable中读取主键为100、200、500的数据并加入新的缓存。扫描完成后,原来的缓
    4 f. c$ _4 A' K: v存可以丢弃。; A+ K9 T2 g& Q* \$ w, }0 O
    线上运行一段时间后发现,定期合并基本上都安排在凌晨业务低峰期,合并完
    5 }7 N8 O/ n. m4 w# n0 n成后OceanBase集群收到的用户请求总是由少到多(早上7点之前请求很少,9点以后
    2 t+ [0 ]# S. s. [8 a1 x9 F) s请求逐步增多),能够很自然地实现被动缓存预热。由于ChunkServer在主动缓存预
    ) e9 U, G% O* j6 f% m热期间需要占用两倍的内存,因此,目前的线上版本放弃了这种方式,转而采用被) c( u  a6 T# B6 v) D
    动缓存预热。) `7 |) g' ^0 [* z
    9.4.4 IO实现  [/ q' Q2 E- X5 u% A
    OceanBase没有使用操作系统本身的页面缓存(page cache)机制,而是自己实现- D. G: x9 H% P& x
    缓存。相应地,IO也采用Direct IO实现,并且支持磁盘IO与CPU计算并行化。4 g/ [, S1 ^, P8 O" a
    ChunkServer采用Linux的Libaio [1] 实现异步IO,并通过双缓冲区机制实现磁盘预读& s4 k! B$ A* G1 L. ~0 X
    与CPU处理并行化,实现步骤如下:7 R$ Z$ \' W4 E4 j' b2 O
    1)分配当前(current)以及预读(ahead)两个缓冲区;! B, y4 c: A, f
    2)使用当前缓冲区读取数据,当前缓冲区通过Libaio发起异步读取请求,接着
    # q, e( f* C2 M4 U1 |& S等待异步读取完成;
    9 F- f: |6 |% }$ J3)异步读取完成后,将当前缓冲区返回上层执行CPU计算,同时,原来的预读
    ; a1 O( W4 C- m  h2 c% F缓冲区变为新的当前缓冲区,发送异步读取请求将数据读取到新的当前缓冲区。) H) A, w: _+ E+ N4 {6 `$ i$ y
    CPU计算完成后,原来的当前缓冲区变为空闲,成为新的预读缓冲区,用于下一次+ `, r! ^' O1 W8 g) |
    预读。
    , p; L: ~! E5 B! L& d% H+ P- [7 V! B8 {4)重复步骤3),直到所有数据全部读完。* I# n/ X# n  a4 ~% E* c# U0 {
    例9-5 假设需要读取的数据范围为(1,150],分三次读取:(1,50],(50,
    8 p* K. i0 s4 F- w7 m100],(100,150],当前和预读缓冲区分别记为A和B。实现步骤如下:+ l0 R1 C$ M8 {$ c
    1)发送异步请求将(1,50]读取到缓冲区A,等待读取完成;
    ' V; n& ]& J9 i7 j, @7 y2)对缓冲区A执行CPU计算,发送异步请求,将(50,100]读取到缓冲区B;' H% Z9 @7 J4 q% q
    3)如果CPU计算先于磁盘读取完成,那么,缓冲区A变为空闲,等到(50," i  e5 k+ t/ _3 O, n* }; }
    100]读取完成后将缓冲区B返回上层执行CPU计算,同时,发送异步请求,将: s0 I# Y' u2 ^" M# [
    (100,150]读取到缓冲区A;
    4 _/ p* Q1 Y6 L9 _/ X4)如果磁盘读取先于CPU计算完成,那么,首先等待缓冲区A上的CPU计算完' g$ L7 @3 a% M5 M5 K' _) ?
    成,接着,将缓冲区B返回上层执行CPU计算,同时,发送异步请求,将(100,/ ^% D: G# Z9 k( R
    150]读取到缓冲区A;7 |- N  N/ B: H1 X, G2 f
    5)等待(100,150]读取完成后,将缓冲区A返回给上层执行CPU计算。2 o1 F! k5 T" F5 \) u% A5 b
    双缓冲区广泛用于生产者/消费者模型,ChunkServer中使用了双缓冲区异步预读/ ~2 _4 h" O/ a) T
    的技术,生产者为磁盘,消费者为CPU,磁盘中生产的原始数据需要给CPU计算消费
    5 b* x# a& a, W' Q% g掉。
    $ s' l/ i% A. F* ?& V  r所谓“双缓冲区”,顾名思义就是两个缓冲区(简称A和B)。这两个缓冲区,总
    ) U5 [& Y6 K5 k3 F" u. a) a9 J是一个用于生产者,另一个用于消费者。当两个缓冲区都操作完,再进行一次切( A  Z9 k& H' U. K7 [8 y% t; D
    换,先前被生产者写入的被消费者读取,先前消费者读取的转为生产者写入。为了, b/ v4 m# R* g$ |$ r% F
    做到不冲突,给每个缓冲区分配一把互斥锁(简称La和Lb)。生产者或者消费者如2 [4 C0 U4 U: ~# U$ F6 E
    果要操作某个缓冲区,必须先拥有对应的互斥锁。& `8 N  S" e1 V, o% R; h: {* _
    双缓冲区包括如下几种状态:% D3 Y0 o. }4 v, E) c2 f# v5 ]0 t+ I3 J
    ●双缓冲区都在使用的状态(并发读写)。大多数情况下,生产者和消费者都处; s" E; \0 a% E8 C6 q; L. ?1 p
    于并发读写状态。不妨设生产者写入A,消费者读取B。在这种状态下,生产者拥有
    & u, K8 D9 x( C5 F锁La;同样地,消费者拥有锁Lb。由于两个缓冲区都是处于独占状态,因此每次读
    $ \+ _6 a2 b5 y/ L$ p写缓冲区中的元素都不需要再进行加锁、解锁操作。这是节约开销的主要来源。, T) P* ^4 T& c2 l" X5 D
    ●单个缓冲区空闲状态。由于两个并发实体的速度会有差异,必然会出现一个缓
    % Z; A0 D" j1 Y: v* K: o8 |% f+ Y冲区已经操作完,而另一个尚未操作完。不妨假设生产者快于消费者。在这种情况
    : _$ h$ H, ^2 U& e$ Y" I下,当生产者把A写满的时候,生产者要先释放La(表示它已经不再操作A),然后& \& |+ x- X, v& t3 R7 x
    尝试获取Lb。由于B还没有被读空,Lb还被消费者持有,所以生产者进入等待
    * v/ j& ^  Z! c2 ]1 r# Y4 s( U(wait)状态。0 n4 }$ ^2 V: M; @) r8 `
    ●缓冲区的切换。过了若干时间,消费者终于把B读完。这时候,消费者也要先
    + h9 d0 V! k# C* h' f释放Lb,然后尝试获取La。由于La刚才已经被生产者释放,所以消费者能立即拥有
    3 s8 m' w/ E7 _3 @La并开始读取A的数据。而由于Lb被消费者释放,所以刚才等待的生产者会苏醒过来
    1 B- o/ I7 k* ^; F(wakeup)并拥有Lb,然后生产者继续往B写入数据。! }5 g# w% x: d. ^
    [1]Oracle公司实现的Linux异步IO库,开源地址:https://oss.oracle.com/projects/libaio-3 C, Y+ M  f6 _# r* e% `0 Z/ d' ^) Z
    oracle/: S* i0 V6 e4 ?# W- C- t
    9.4.5 定期合并&数据分发4 ]8 c. i' Q! j' ^* Z( B6 u  k
    RootServer将UpdateServer上的版本变化信息通知ChunkServer后,ChunkServer将1 Y2 ^; k9 g* M% e# @/ n0 i
    执行定期合并或者数据分发。7 J& O, W( U2 h0 c, `
    如果UpdateServer执行了大版本冻结,ChunkServer将执行定期合并。ChunkServer5 T& N, D0 l6 b% K8 C% s6 z
    唤醒若干个定期合并线程(比如10个),每个线程执行如下流程:) |( H6 \$ z9 m/ ?" J% @0 A
    1)加锁获取下一个需要定期合并的子表;
    $ g/ ?9 r+ G$ A; Z2)根据子表的主键范围读取UpdateServer中的修改操作;
    - z/ s4 J4 Z# H4 H# n3)将每行数据的基线数据和增量数据合并后,产生新的基线数据,并写入到新2 K5 ~" I/ J% k
    的SSTable中;
    0 L+ H* s5 K3 h6 b4)更改子表索引信息,指向新的SSTable。
    - Y  D  W! h& {等到ChunkServer上所有的子表定期合并都执行完成后,ChunkServer会向+ N0 e& o: T. n
    RootServer汇报,RootServer会更新RootTable中记录的子表版本信息。定期合并一般
    % ?- a4 k5 w4 Q8 X1 |: X: h8 ^安排在每天凌晨业务低峰期(凌晨1:00开始)执行一次,因此也称为每日合并。另
    " D& F" w, u# t. ?$ N0 C$ w外,定期合并过程中ChunkServer的压力比较大,需要控制合并速度,否则可能影响
    # B/ j. u& ~# q8 A: N# x  F, F正常的读取服务。7 G3 B% f* H+ E3 d# O
    如果UpdateServer执行了小版本冻结,ChunkServer将执行数据分发。与定期合并
    2 c4 L1 l! P2 s  f. p: t7 q不同的是,数据分发只是将UpdateServer冻结的数据缓存到ChunkServer,并不会生成
    / e9 B! ^5 |' s  [新的SSTable文件。因此,数据分发对ChunkServer造成的压力不大。6 s8 V' L# \% p$ Y  W+ r; k% ]0 R( z
    数据分发由外部读取请求驱动,当请求ChunkServer上的某个子表时,除了返回. h0 r& m9 @5 t8 R& G/ @
    使用者需要的数据外,还会在后台生成这个子表的数据分发任务,这个任务会获取2 n" V# Z+ N6 Z5 J% K7 c, J! Q! z
    UpdateServer中冻结的小版本数据,并缓存在ChunkServer的内存中。如果内存用完,3 K9 _# \1 A: C6 S+ ~
    数据分发任务将不再进行。当然,这里可以做一些改进,比如除了将UpdateServer分& y) n% O# {1 E# }1 v5 X, V8 E- B
    发的数据存放到ChunkServer的内存中,还可以存储到SSD磁盘中。
    + D( k/ e$ ]- t2 w例9-6 假设某台ChunkServer上有一个子表t1,t1的主键范围为(1,10],只有一
    $ e' ?) q! T& |$ F; o行数据:rowkey=8=>(<2,update,20>,<3,update,30>,<4,update,40
    ; q% x  I! T+ l( T>)。UpdateServer的冻结版本有两行更新操作:rowkey=8=>(<2,update,30
    5 g8 n* Y& U2 b. M% q4 \8 ]$ j>,<3,up-date,38>)和rowkey=20=>(<4,update,50>)。2 w7 X1 y: w5 }% I0 n3 q
    ●如果是大版本冻结,那么,ChunkServer上的子表t1执行定期合并后结果为:
    ! X, q7 n! K1 j/ B  b- L: M3 n- A* Dro-wkey=8=>(<2,update,30>,<3,update,38>,<4,update,40>);
    7 a/ ]2 f: Q- u" d●如果是小版本冻结,那么,ChunkServer上的子表t1执行数据分发后的结果为:
    % K. i: }, |* i6 Y4 g" mrowkey=8=>(<2,update,20>,<3,update,30>,<4,update,40>,<2,
    # `; H8 E" L* yupdate,30>,<3,update,38>)。
      v* F& v7 ~) h) I, {& M. L: O9.4.6 定期合并限速7 ]$ _9 y6 L3 l" K
    定期合并期间系统的压力较大,需要控制定期合并的速度,避免影响正常服
    : q" {+ s( {, x" b7 G, h2 v4 p/ _务。定期合并限速的措施包括如下步骤:
    5 z" F  ?; P" v4 Y; Z  Q1)ChunkServer:ChunkServer定期合并过程中,每合并完成若干行(默认2000, l- z* U0 A4 m- b
    行)数据,就查看本机的负载(查看Linux系统的Load值)。如果负载过高,一部分
    2 C3 _4 [# g+ g7 a定期合并线程转入休眠状态;如果负载过低,唤醒更多的定期合并线程。另外,
    + n; a) C2 y! e+ G. z, J: I  GRootServer将UpdateServer冻结的大版本通知所有的ChunkServer,每台ChunkServer会1 h0 ^5 k3 B. e
    随机等待一段时间再开始执行定期合并,防止所有的ChunkServer同时将大量的请求4 S# i! M) ~+ A$ n/ ^5 e
    发给UpdateServer。8 p0 P, q: q  W8 h7 r
    2)UpdateServer:定期合并过程中ChunkServer需要从UpdateServer读取大量的数' X8 B# Z3 X- @' \$ p
    据,为了防止定期合并任务用满带宽而阻塞用户的正常请求,UpdateServer将任务区
    & n; M, g) j' d+ ]/ k分为高优先级(用户正常请求)和低优先级(定期合并任务),并单独统计每种任
    . F; w/ O% S1 s0 a/ X务的输出带宽。如果低优先级任务的输出带宽超过上限,降低低优先级任务的处理5 r) i. k) [' ?! K! [
    速度;反之,适当提高低优先级任务的处理速度。
    3 E' h, c6 P" Y' q3 l) x! f如果OceanBase部署了两个集群,还能够支持主备集群在不同时间段进行“错峰
    % {1 t  r0 ~7 ^合并”:一个集群执行定期合并时,把全部或大部分读写流量切到另一个集群,该集1 W. \: r1 ?$ N
    群合并完成后,把全部或大部分流量切回,以便另一个集群接着进行定期合并。两
    ) l! H+ M( u# H8 x% D* U个集群都合并完成后,恢复正常的流量分配。
    3 f. i* `9 d2 Z- I5 e# `) `4 ^
    & t6 d2 C' d9 W" N6 C3 _% y
    1 s. h5 g$ M" @7 W3 }
    回复

    使用道具 举报

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

    本版积分规则

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

    GMT+8, 2024-11-21 20:43 , Processed in 0.151585 second(s), 33 queries .

    Powered by Javazx

    Copyright © 2012-2022, Javazx Cloud.

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