java自学网VIP

Java自学网

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 2569|回复: 0

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

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

    [LV.Master]出神入化

    2025

    主题

    3683

    帖子

    6万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    66375

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

    发表于 2017-3-6 14:40:59 | 显示全部楼层 |阅读模式
    9.4 ChunkServer实现机制6 `8 G6 N. p# Q0 q! G: t
    ChunkServer用于存储基线数据,它由如下基本部分组成:- s7 J) N, S, U! F4 L# X: q0 C
    ●管理子表,主动实现子表分裂,配合RootServer实现子表迁移、删除、合并;' I& o1 {( M) w$ s
    ●SSTable,根据主键有序存储每个子表的基线数据;: ]5 H: `7 I4 I! @$ i. L2 N$ c
    ●基于LRU实现块缓存(Block cache)以及行缓存(Row cache);) Y; K" F6 G9 g' J- ^8 A
    ●实现Direct IO,磁盘IO与CPU计算并行化;( ]2 H1 V# a5 ]# N. t5 c- a
    ●通过定期合并&数据分发获取UpdateServer的冻结数据,从而分散到整个集( x/ D  V8 T) P2 ?
    群。
    / {3 q/ s  ^  B/ N2 j7 t: R& b9 h每台ChunkServer服务着几千到几万个子表的基线数据,每个子表由若干个
    : S; n5 j) h. x3 @' `3 I6 U2 }SSTable组成(一般为1个)。下面从SSTable开始介绍ChunkServer的内部实现。
    , ^0 p& [  E, |& B9.4.1 子表管理
    3 F9 P* M# ?1 q8 \  n6 z每台ChunkServer服务于多个子表,子表的个数一般在10000~100000之间。/ b5 k7 I/ V+ d/ F
    Chunk-Server内部通过ObMultiVersionTabletImage来存储每个子表的索引信息,包括数
    3 T& v" y9 H& A4 z1 H- k2 b7 Y据行数(row_count),数据量(occupy_size),校验和(check_sum),包含的
    ' U8 [8 H% H. [2 d: B3 T0 q/ ISSTable列表,所在磁盘编号(disk_no)等,代码如下:
    4 b9 i8 [: `3 Mclass ObMultiVersionTabletImage
    ' d6 j4 Q- U. [1 H$ y$ B{
    9 ~2 o  w9 s) i  W: V$ J  E: _' Vpublic:
    ; x- d0 Y) }" M% }0 |9 Q3 U//获取第一个包含指定数据范围的子表
    2 \0 ^2 p4 M) d6 N! K  D# ~//@param[in]range数据范围
    + v; b; ]( S! j: u//@param[in]scan_direction正向扫描(默认)还是逆向扫描4 @& B2 {( |& a5 B
    //@param[in]version子表的版本号# l1 Q. C( H* ~9 i/ _
    //@param[out]tablet获取的子表索引结构" K/ G, k' k0 K8 v4 n% S  |
    int acquire_tablet(const ObNewRange&range,const ScanDirection
    + _- X& Y. Z8 Y8 Y2 k( t; W. }, Kscan_direction,const int64_t version,ObTablet*&tablet)const;7 Z$ t; `# ]6 F  `
    //释放一个子表) S. I4 h. J+ r8 I6 p
    int release_tablet(ObTablet*tablet);
    - ^, K( {8 |, q" y/ D" d  t//新增一个子表,load_sstable表示是否立即加载其中的SSTable文件0 \$ H4 j! V2 H  V! [
    int add_tablet(ObTablet*tablet,const bool load_sstable=false);9 E7 \8 x7 ?: [
    //每日合并后升级子表到新版本,load_sstable表示是否立即加载新版本的SSTable文件
    4 r9 N, U; W$ S) Vint upgrade_tablet(ObTablet*old_tablet,ObTablet*new_tablet,const bool( b2 C% w) F8 b) z/ X/ s
    load_sstable=false);
    % }' \" a( Y& n  s2 i//每日合并后升级子表到新版本,且子表发生分裂,有一个变成多个。load_sstable表示是否立即加载
    $ H7 B& b4 S  r7 I  v分裂后的SSTable文件
    / _6 `% X  @0 H9 rint upgrade_tablet(ObTablet*old_tablet,ObTablet*new_tablets[],const int32_t' |7 y6 z. S8 @: k) q4 l* O
    split_size,const bool load_sstable=false);( S" P* l& Z. u3 o) ]
    //删除一个指定数据范围和版本的子表. ~4 g+ T# Y4 _- I1 e/ t4 [
    int remove_tablet(const ObNewRange&range,const int64_t version);. s7 A1 T1 d: L/ x, ~
    //删除一个表格对应的所有子表7 b1 u6 k, Q! ^4 U  h
    int delete_table(const uint64_t table_id);
    7 J7 H- \5 t% _' `//获取下一批需要进行每日合并的子表* ~3 B$ o* m6 `( ]3 R* v; q: H
    //@param[in]version子表的版本号9 P, _" ^: E1 o7 V2 A2 H- C
    //@param[out]size下一批需要进行每日合并的子表个数. B+ d7 P. E# t  I
    //@param[out]tablets下一批需要进行每日合并的子表索引结构
    - t( z2 h# F( {& D9 vint get_tablets_for_merge(const int64_t version,int64_t&size,ObTablet*&8 V: Z/ E% F1 ~4 x3 `
    tablets[])const;
    $ e+ @; K0 B$ n3 x1 @! a+ u};5 s0 B% X' g2 ~) R: _- B# ~
    ChunkServer维护了多个版本的子表数据,每日合并后升级子表的版本号。如果
    ! W: m) R( i9 j/ f' _1 |1 X子表发生分裂,每日合并后将由一个子表变成多个子表。子表相关的操作方法包
    $ @& X8 B6 \6 z" a  T1 g' J括:
    " ^: x- X" ?! c, ^6 k1 ~1)add_tablet:新增一个子表。如果load_sstable参数为true,那么,立即加载其
    * R: S# h4 d, T% @) y/ c6 ?% k中的SSTable文件。否则,使用延迟加载策略,即读取子表时再加载其中的SSTable。$ I* P" S( l: A# k- o% [- ~
    2)remove_tablet:删除一个子表。RootServer发现某个子表的副本数过多,则会$ z3 q* o$ v; B" a( O# Q+ f, G7 J
    通知其中某台ChunkServer删除指定的子表。# `% ]/ q" |. K# ~! U
    3)delete_table:删除表格。用户执行删除表格命令时,RootServer会通知每台
    # Q3 a( V+ x/ M: i$ xChunkServer删除表格包含的所有子表。% Z7 H9 D& c: A  b" U* b
    4)upgrade_tablet:每日合并后升级子表的版本号。如果没有发生分裂,只需要& k3 _) s- y2 m2 X3 B
    将老子表的版本号加1;否则,将老子表替换为多个范围连续的新子表,每个新子表7 @/ c! q+ c- d2 P: ~
    的版本号均为老子表的版本号加1。
    " D! y0 x$ _$ J# T* [5)acquire_tablet/release_tablet:读取时首先调用acquire_tablet获取一个子表,增$ T, V' K1 U/ c2 u
    加该子表的引用计数从而防止它在读取过程中被释放掉,接着读取其中的SSTable,% c* j- \% D! i7 f/ V
    最后调用release_tablet释放子表。3 P8 {) d5 }- q9 p6 A4 C% S
    6)get_tablets_for_merge:每日合并时通过调用该函数获取下一批需要进行每日
    " J( n& Y( i5 ?. e0 A; N合并的子表。
    , @4 Y. y9 a2 [8 w+ v1 r0 e' K9.4.2 SSTable4 H/ F2 q! d" n
    如图9-8所示,SSTable中的数据按主键排序后存放在连续的数据块(Block)
    * f* L, v* W. L. Q. P4 {9 r中,Block之间也有序。接着,存放数据块索引(Block Index),由每个Block最后一
    8 g* Q, f' B! g行的主键(End Key)组成,用于数据查询中的Block定位。接着,存放布隆过滤器  x' d  q" l# I% P/ S
    (Bloom Filter)和表格的Schema信息。最后,存放固定大小的Trailer以及Trailer的偏! A6 z7 \; e5 k/ G" i- s. G8 }% b4 z
    移位置。
    5 ^" \3 `. I1 S) ^( N' u5 m( N图 9-8 SSTable格式9 M6 ~- m" r& f# T! H# ~; h' {5 m
    查找SSTable时,首先从子表的索引信息中读取SSTable Trailer的偏移位置,接着, Q3 ~! [; z3 n- G
    获取Trailer信息。根据Trailer中记录的信息,可以获取块索引的大小和偏移,从而将* P, L+ }9 ?6 X1 J+ x, p; D& U
    整个块索引加载到内存中。根据块索引记录的每个Block的最后一行的主键,可以通
    5 F3 g4 \# b5 {" N) g4 @, ^过二分查找定位到查找的Block。最后将Block加载到内存中,通过二分查找Block中
    7 K4 N+ d9 o5 }$ t记录的行索引(Row Index)查找到具体某一行。本质上看,SSTable是一个两级索引6 z5 ]) S! f) s3 p
    结构:块索引以及行索引;而整个ChunkServer是一个三级索引结构:子表索引、块7 l3 ?5 u! y+ i6 m+ U
    索引以及行索引。" M, g" i6 Y4 f, {  A
    SSTable分为两种格式:稀疏格式以及稠密格式。对于稀疏格式,某些列可能存9 e$ |# r( a, \/ Y7 i
    在,也可能不存在,因此,每一行只存储包含实际值的列,每一列存储的内容为:4 F# j0 z2 ]  U- _& N3 S4 n0 ?" H
    <列ID,列值>(<Column ID,Column Value>);而稠密格式中每一行都需要存储7 a7 {8 l( o: f+ F& K! T2 {4 @9 h4 P1 B
    所有列,每一列只需要存储列值,不需要存储列ID,这是因为列ID可以从表格+ g+ r. l' ^& ]" f8 w& E
    Schema中获取。4 R2 `' ?! Y% a4 B8 f' _' `; |
    例9-4 假设有一张表格包含10列,列ID为1~10,表格中有一行的数据内容5 f7 J+ y' `' u6 q5 C: C
    为:
    5 B5 M& J& T8 p0 N那么,如果采用稀疏格式存储,内容为:<2,20>,<3,30>,<5,50>,
    ' r6 z. S6 N8 p8 j<7,70>,<8,80>;如果采用稠密格式存储,内容为:null,20,30,null,6 f/ P# S' \+ n+ v5 X
    50,null,70,80,null,null。
    % H2 a" N0 M1 W  Z7 o: [ChunkServer中的SSTable为稠密格式,而UpdateServer中的SSTable为稀疏格式,+ K' k% r. u: K' W' b2 X0 Z
    且存储了多张表格的数据。另外,SSTable支持列组(Column Group),将同一个列* M9 y5 a9 Z5 d
    组下的多个列的内容存储在一块。列组是一种行列混合存储模式,将每一行的所有
    4 C5 J  l2 ~6 V/ m+ s列分成多个组(称为列组),每个列组内部按行存储。
    * |9 i, T" @. B( {1 C0 R如图9-9所示,当一个SSTable中包含多个表格/列组时,数据按照[表格ID,列组$ ]0 z, G8 w7 p7 `  O7 ]
    ID,行主键]([table_id,column group id,row_key])的形式有序存储。
    9 Z3 o5 ~# T+ Y5 R  K图 9-9 SSTable包含多个表格/列组
    ! S& a& T' g, C8 B3 p另外,SSTable支持压缩功能,压缩以Block为单位。每个Block写入磁盘之前调4 N1 d) F5 d' X
    用压缩算法执行压缩,读取时需要解压缩。用户可以自定义SSTable的压缩算法,目
    * _( h) U& @2 n! v- p前支持的算法包括LZO以及Snappy。9 j  L: C: d3 L# h5 f, n% u+ J) m
    SSTable的操作接口分为写入和读取两个部分,其中,写入类为
    ( w% _1 q# u; g+ |# c% H% D1 aObSSTableWriter,读取类为ObSSTableGetter(随机读取)和ObSSTableScanner(范围& @% u# q( ^; T( o+ R& J" u9 z& Y
    查询)。代码如下:! N; L' i6 f, M7 \
    class ObSSTableWriter/ m# @* g# b2 d* q7 W
    {
    * e; N3 ]) Z/ |public:( k8 y: _, F+ T% H: e) [  }  d
    //创建SSTable1 g5 Y+ ?* h5 b- `6 s
    //@param[in]schema表格schema信息
    / w" P# m- N/ _' Q& ^//@param[in]path SSTable在磁盘中的路径名
    ; y+ W$ n& Q& h1 G//@param[in]compressor_name压缩算法名
    , ~8 o+ ]3 A' K- T1 k//@param[in]store_type SSTable格式,稀疏格式或者稠密格式6 z4 G6 m0 u  x7 N6 G5 l! W% D$ k
    //@param[in]block_size块大小,默认64KB
    ) \& ], |4 x0 R3 j2 F1 i4 \int create_sstable(const ObSSTableSchema&schema,const ObString&path,const/ {6 [$ E  [! C% e+ n. d
    ObString&compressor_name,const int store_type,const int64_t block_size);$ ~5 V  x8 ~+ u' @9 C6 i* @. V' [
    //往SSTable中追加一行数据( S* g: j4 A, y/ T( c; `+ u$ E' k
    //@param[in]row一行SSTable数据, M( d+ z. `* f" P' ]% O' s# I+ l  F
    //@param[out]space_usage追加完这一行后SSTable大致占用的磁盘空间, `5 x. w2 S4 L! G
    int append_row(const ObSSTableRow&row,int64_t&space_usage);2 X" e' O, l# p6 w
    //关闭SSTable,将往磁盘中写入Block Index,Bloom Filter,Schema,Trailer等信息. \* g, n9 Z) g6 U3 P+ W6 z
    //@param[out]trailer_offset返回SSTable的Trailer偏移量9 {  y" e9 m; f& S' l, P- X
    int close_sstable(int64_t&trailer_offset);
    1 W$ s$ \; I# J. C; _# H) H2 z2 H};7 }. l$ L5 O9 K4 b0 k
    定期合并&数据分发过程将产生新的SSTable,步骤如下:
    , ?" s/ w( B: V- V7 S) G' a1)调用create_sstable函数创建一个新的SSTable;
    / r# b: O  [% z+ k; H2)不断调用append_row函数往SSTable中追加一行行数据;
    9 I9 y1 M; z& O% T3 _3)调用close_sstable完成SSTable写入。
    0 d7 ?! q; w$ X+ Y与9.2.1节中的MemTableIterator一样,ObSSTableGetter和ObSSTableScanner实现了
    0 J$ f2 ^! [# k; W迭代器接口,通过它可以不断地获取SSTable的下一个cell。
    : C- c5 [6 K, g1 y$ p0 _class ObIterator
    * q) A/ e9 ?( B$ D1 j{+ o2 Y3 n$ L; s2 b1 L- d2 Z- H6 R) e
    public:
    1 M% G! r9 Q! U2 X, i//迭代器移动到下一个cell
    7 {7 Z8 V  o5 p8 Dint next_cell();6 V, L5 T6 m; M$ t% ?' v4 x/ b* Y
    //获取当前cell的内容, p6 c! K* \; t2 t  Q% n/ `/ t% w
    //@param[out]cell_info当前cell的内容,包括表名(table_id),行主键(row_key),列编号
    , Y5 _9 H; C5 k0 q2 K8 `/ ?! F, l" X8 R: w(column_id)以及列值(column_value)- c% ?4 E: U- X
    int get_cell(ObCellInfo**cell_info);8 y# @  m5 Q3 u7 L
    //获取当前cell的内容9 C0 T2 t* m* @! B! [
    //@param[out]cell_info当前cell的内容
    : V' Y+ I' N2 J' b//@param is_row_changed是否迭代到下一行; R+ d: y2 S( S  p" i
    int get_cell(ObCellInfo**cell_info,bool*is_row_changed);% h  U) G' L" e  S# p! `) w6 i
    };% }# r  Q4 T  Z& h. v( u& F2 [
    OceanBase读取的数据可能来源于MemTable,也可能来源于SSTable,或者是合
    . d8 S7 q9 J6 V5 L5 f+ h$ s$ v并多个MemTable和多个SSTable生成的结果。无论底层数据来源如何变化,上层的读$ V/ {1 D2 m9 ?+ }- _& i7 q
    取接口总是ObIterator。
    ) o* c& n& g2 `0 q% |- \! O9.4.3 缓存实现
    ! H: J. e& [& r! @4 s* wChunkServer中包含三种缓存:块缓存(Block Cache)、行缓存(Row Cache)以
    7 u- P6 y9 s9 Y  u# m9 Y3 M0 f及块索引缓存(Block Index Cache)。其中,块缓存中存储了SSTable中访问较热的数) d; U+ P; D1 B0 l
    据块(Block),行缓存中存储了SSTable中访问较热的数据行(Row),而块索引缓. a9 e4 ?9 C3 A' s& S, N0 q
    存中存储了最近访问过的SSTable的块索引(Block Index)。一般来说,块索引不会0 y3 W/ U9 E; A0 q  ^- s, T6 ]
    太大,ChunkServer中所有SSTable的块索引都是常驻内存的。不同缓存的底层采用相
    7 F- h- i  ^" ^同的实现方式。
    # s- e- {6 j! b7 Y- i! x6 P1.底层实现' R% C* j; k' A* o( i0 \% H
    经典的LRU缓存实现包含两个部分:哈希表和LRU链表,其中,哈希表用于查找
    ' W: U4 g; U% b$ V缓存中的元素,LRU链表用于淘汰。每次访问LRU缓存时,需要将被访问的元素移动# q$ J. v3 Z1 c, A1 n7 |% S4 b
    到LRU链表的头部,从而避免被很快淘汰,这个过程需要锁住LRU链表。) d7 D) K! N+ H- U
    如图9-10所示,块缓存和行缓存底层都是一个Key-Value Cache,实现步骤如下:/ |4 }) I0 ~8 k; {
    图 9-10 Key-Value Cache的实现1 A  L; Q7 C3 ?9 L2 P% ^! [2 Y# S: [
    1)OceanBase一次分配1MB的连续内存块(称为memblock),每个memblock包
    % K5 N$ s- g: C0 r$ P) I0 k8 P含若干缓存项(item)。添加item时,只需要简单地将item追加到memblock的尾部;
    $ k  V2 e' N0 G1 V0 c# T另外,缓存淘汰以memblock为单位,而不是以item为单位。0 f2 H4 W8 d6 q" p# ~
    2)OceanBase没有维护LRU链表,而是对每个memblock都维护了访问次数和最
    ! }0 _' u6 F. i1 {( ~近频繁访问时间。访问memblock中的item时将增加memblock的访问次数,如果最近一
    . X" B$ D/ e$ Q* n1 f段时间之内的访问次数超过一定值,那么,更新最近频繁访问时间;淘汰memblock
    6 D; S* M1 J' j" x2 m, i! L时,对所有的memblock按照最近频繁访问时间排序,淘汰最近一段时间访问较少的1 y* \$ w( M; a! P5 _
    memblock。可以看出,读取时只需要更新memblock的访问次数和最近频繁访问时% q3 ^  Q" P9 T; D  E9 c
    间,不需要移动LRU链表。这种实现方式通过牺牲LRU算法的精确性,来规避LRU链6 O# }3 W* \4 H3 G- t: B
    表的全局锁冲突。7 v! o' M; C6 c7 ~; T6 @' _
    3)每个memblock维护了引用计数,读取缓存项时所在memblock的引用计数加2 q  G  `" o9 \  T$ D5 T
    1,淘汰memblock时引用计数减1,引用计数为0时memblock可以回收重用。通过引用
    1 w5 a% x. R- C4 j# O+ {; x计数,实现读取memblock中的缓存项不加锁。
    4 U* u0 Q* w4 U: G, N2.惊群效应
    0 K6 r3 S9 a/ I6 w以行缓存为例,假设ChunkServer中有一个热点行,ChunkServer中的N个工作线
    / G3 {" J) c$ H程(假设为N=50)同时发现这一行的缓存失效,于是,所有工作线程同时读取这行
    7 K0 Y; M3 {6 h/ v8 a* ^6 e# R数据并更新行缓存。可以看出,N-1共49个线程不仅做了无用功,还增加了锁冲突。1 d* x* E+ C* b( ?; D
    这种现象称为“惊群效应”。为了解决这个问题,第一个线程发现行缓存失效时会往  w# n0 s0 o+ P
    缓存中加入一个fake标记,其他线程发现这个标记后会等待一段时间,直到第一个线6 m# E. {9 I6 y4 G/ B1 C+ k
    程从SSTable中读到这行数据并加入到行缓存后,再从行缓存中读取。# `5 B% f8 E8 q3 i& Z, h
    算法描述如下:
    8 c3 J- M" p2 S: B: x2 c" f调用internal_get读取一行数据;& l. D% _( h7 H3 [9 ?  u2 `
    if(行不存在){! C6 c: U6 |" Y
    调用internal_set往缓存中加入一个fake标记;& b6 F, w' u% I5 T4 W, m! }
    从SSTable中读取数据行;8 P) c1 m) P0 w5 R
    将SSTable中读到的行内容加入缓存,清除fake标记,唤醒等待线程;) K2 @, `. `! K) _
    返回读到的数据行;
    4 H. x: X+ [) E& M3 |}else if(行存在且为fake标记)
    9 w# T- ?1 h+ E4 w; e5 E{& B: `/ v3 f8 C
    线程等待,直到清除fake标记;5 b2 `, E% m" s
    if(等待成功)返回行缓存中的数据;# U! D$ [& B) g1 R, j
    if(等待超时)返回读取超时;
    9 Z& A' l+ [- [3 |, g$ R5 ^0 A. u}
    5 z0 p6 P, m. F2 K0 v) A8 jelse. g1 m3 h5 P/ _
    {
    2 X" y, O7 E* t0 i# H! A返回行缓存中的数据;
    : R/ e: L1 ]& V4 Y- q$ o) ?}
    / {: e3 j* m$ [; d/ T& }* Z+ w; G3.缓存预热" t, ?* d: V, f
    ChunkServer定期合并后需要使用生成的新的SSTable提供服务,如果大量请求同, M4 C( t; p' r" Z& h
    时读取新的SSTable文件,将使得ChunkServer的服务能力在切换SSTable瞬间大幅下
    5 y6 j* P# x! R+ G降。因此,这里需要一个缓存预热的过程。OceanBase最初的版本实现了主动缓存预9 u1 g1 v" T8 s4 b2 W: y
    热,即:扫描原来的缓存,根据每个缓存项的key读取新的SSTable并将结果加入到新$ @+ F) [6 a$ S! K9 J0 p
    的缓存中。例如,原来缓存数据项的主键分别为100、200、500,那么只需要从新的7 w: B/ }  y- d+ r+ ~& ^+ d
    SSTable中读取主键为100、200、500的数据并加入新的缓存。扫描完成后,原来的缓
    / c+ N. e; k; E2 i" I% x- N存可以丢弃。
    2 e- _' h$ `. h* x线上运行一段时间后发现,定期合并基本上都安排在凌晨业务低峰期,合并完
    5 d+ g6 ]  O' X$ T成后OceanBase集群收到的用户请求总是由少到多(早上7点之前请求很少,9点以后9 c1 _- u3 ]( C& o
    请求逐步增多),能够很自然地实现被动缓存预热。由于ChunkServer在主动缓存预
    ( A, ~6 Z% o+ K- u# V" N* Q* h2 Y热期间需要占用两倍的内存,因此,目前的线上版本放弃了这种方式,转而采用被. F6 P. ^$ U* s: P
    动缓存预热。5 U/ ~! |8 e, S- K9 {7 \0 R
    9.4.4 IO实现6 r7 L6 g4 l9 b8 C7 ^/ N
    OceanBase没有使用操作系统本身的页面缓存(page cache)机制,而是自己实现. N" F. S) Q1 F4 Z, X
    缓存。相应地,IO也采用Direct IO实现,并且支持磁盘IO与CPU计算并行化。( W& `0 x; ~9 s2 L8 x3 _) k/ G7 t
    ChunkServer采用Linux的Libaio [1] 实现异步IO,并通过双缓冲区机制实现磁盘预读0 M5 C, E) h3 r5 [; @
    与CPU处理并行化,实现步骤如下:3 e+ t! V  y9 o6 ^2 L2 Q3 m( B0 v
    1)分配当前(current)以及预读(ahead)两个缓冲区;
    + B0 w" m: y# ~2 F2)使用当前缓冲区读取数据,当前缓冲区通过Libaio发起异步读取请求,接着- s7 _+ i7 q8 V2 C# Q' r
    等待异步读取完成;1 y. v( K5 A9 j0 e6 h
    3)异步读取完成后,将当前缓冲区返回上层执行CPU计算,同时,原来的预读% w. m) j: b0 T; ^% n
    缓冲区变为新的当前缓冲区,发送异步读取请求将数据读取到新的当前缓冲区。
    6 Q# L* ?# x$ }4 e: `, v1 tCPU计算完成后,原来的当前缓冲区变为空闲,成为新的预读缓冲区,用于下一次" c4 a7 q5 N; a& A5 j7 w6 o
    预读。2 ^, z+ Z4 I. t8 K7 W$ K
    4)重复步骤3),直到所有数据全部读完。
    . K4 b: \7 v6 i9 l/ |例9-5 假设需要读取的数据范围为(1,150],分三次读取:(1,50],(50,. U: |2 f. R8 b" q8 C; p
    100],(100,150],当前和预读缓冲区分别记为A和B。实现步骤如下:, _/ u% g( Z  G
    1)发送异步请求将(1,50]读取到缓冲区A,等待读取完成;6 u; Z- L2 Q0 N- q: g
    2)对缓冲区A执行CPU计算,发送异步请求,将(50,100]读取到缓冲区B;
    2 m3 \7 E2 f7 J8 D8 x) F3 @$ m3)如果CPU计算先于磁盘读取完成,那么,缓冲区A变为空闲,等到(50,1 a$ s  Q  R% d* K0 w1 U8 Q
    100]读取完成后将缓冲区B返回上层执行CPU计算,同时,发送异步请求,将
    / v  X0 U5 V9 q4 `2 i0 w(100,150]读取到缓冲区A;2 O1 k3 R  t8 A
    4)如果磁盘读取先于CPU计算完成,那么,首先等待缓冲区A上的CPU计算完) p) O- w$ S5 N7 B
    成,接着,将缓冲区B返回上层执行CPU计算,同时,发送异步请求,将(100,
    5 [9 H8 K  s6 z1 c% r6 e, Z" |6 s% M9 `# {150]读取到缓冲区A;$ R/ }1 }6 O' ?% n
    5)等待(100,150]读取完成后,将缓冲区A返回给上层执行CPU计算。
    - v' |7 I2 a  n$ A) @0 Q- ?双缓冲区广泛用于生产者/消费者模型,ChunkServer中使用了双缓冲区异步预读
    * M& }; `/ @9 e的技术,生产者为磁盘,消费者为CPU,磁盘中生产的原始数据需要给CPU计算消费
    " M  K- K4 \6 P7 {$ S; L  C# Q掉。1 g: P: \. p* p' h, w" B/ D; A
    所谓“双缓冲区”,顾名思义就是两个缓冲区(简称A和B)。这两个缓冲区,总
      k  W; w5 V: R. X4 j( B( \" p是一个用于生产者,另一个用于消费者。当两个缓冲区都操作完,再进行一次切
    . t1 K- h4 c0 F# P' \% W换,先前被生产者写入的被消费者读取,先前消费者读取的转为生产者写入。为了
    ' n3 F7 Q" B8 F4 u做到不冲突,给每个缓冲区分配一把互斥锁(简称La和Lb)。生产者或者消费者如
    $ Y' M. f8 P* [8 O% ]果要操作某个缓冲区,必须先拥有对应的互斥锁。
    # O" d& r. F. L. Y; j0 S2 w双缓冲区包括如下几种状态:
      ^2 N6 d" ]) W8 Q$ k1 n7 W+ R! S●双缓冲区都在使用的状态(并发读写)。大多数情况下,生产者和消费者都处7 P8 J$ m0 \2 Q& e
    于并发读写状态。不妨设生产者写入A,消费者读取B。在这种状态下,生产者拥有! S8 j( W0 V& V
    锁La;同样地,消费者拥有锁Lb。由于两个缓冲区都是处于独占状态,因此每次读
    9 T$ H5 j( ^5 n% z$ f+ D+ L写缓冲区中的元素都不需要再进行加锁、解锁操作。这是节约开销的主要来源。
    7 D; Q3 n1 C: H  j  p2 H) h0 z" V: c●单个缓冲区空闲状态。由于两个并发实体的速度会有差异,必然会出现一个缓
      Q6 k( C) F) |6 b8 {冲区已经操作完,而另一个尚未操作完。不妨假设生产者快于消费者。在这种情况6 h+ C6 y1 ?: n
    下,当生产者把A写满的时候,生产者要先释放La(表示它已经不再操作A),然后
    0 J* g2 L! ~( q6 U; T尝试获取Lb。由于B还没有被读空,Lb还被消费者持有,所以生产者进入等待- q8 k# F8 s4 w' e' ]' T) J4 K* x3 ^0 i
    (wait)状态。
    ! Q- w/ ^- ]: ?4 _/ l●缓冲区的切换。过了若干时间,消费者终于把B读完。这时候,消费者也要先$ g) W  S+ r- o; r1 c+ o0 X
    释放Lb,然后尝试获取La。由于La刚才已经被生产者释放,所以消费者能立即拥有
    0 Z+ w) j" w% [! K; W- ELa并开始读取A的数据。而由于Lb被消费者释放,所以刚才等待的生产者会苏醒过来6 Z1 w' ?0 L3 H
    (wakeup)并拥有Lb,然后生产者继续往B写入数据。
    ' Y5 M) @' r" y+ k7 O$ r) ?[1]Oracle公司实现的Linux异步IO库,开源地址:https://oss.oracle.com/projects/libaio-$ w9 j/ n- l: S9 x5 Z
    oracle/
    0 N6 }; |4 A# \; I7 o9.4.5 定期合并&数据分发8 F! g: n5 O$ \! @3 \: [9 Z
    RootServer将UpdateServer上的版本变化信息通知ChunkServer后,ChunkServer将$ {. C8 O5 B9 n. |! H
    执行定期合并或者数据分发。" K- w( K, a% K( k8 t% M
    如果UpdateServer执行了大版本冻结,ChunkServer将执行定期合并。ChunkServer
    5 J. q' p" u7 w5 k/ ~唤醒若干个定期合并线程(比如10个),每个线程执行如下流程:
    % a9 e5 s! \! {1 u9 G: B1)加锁获取下一个需要定期合并的子表;' Q: I; s' v6 x$ h  S) a
    2)根据子表的主键范围读取UpdateServer中的修改操作;+ ^2 [: B* E: f* p- \' H" |
    3)将每行数据的基线数据和增量数据合并后,产生新的基线数据,并写入到新
      S/ X0 V, ~4 u( U的SSTable中;
    % R$ ~# e  ~5 W4)更改子表索引信息,指向新的SSTable。
    1 s: f6 k3 b/ }) T$ l' v& i5 v等到ChunkServer上所有的子表定期合并都执行完成后,ChunkServer会向* y, z1 u5 {% k  E6 ?/ q. ^- J
    RootServer汇报,RootServer会更新RootTable中记录的子表版本信息。定期合并一般( _4 L  I+ u! y. }
    安排在每天凌晨业务低峰期(凌晨1:00开始)执行一次,因此也称为每日合并。另
    ; w  K3 u5 M) Q! G外,定期合并过程中ChunkServer的压力比较大,需要控制合并速度,否则可能影响
    - G' S) a" b7 z- P: B正常的读取服务。
    4 w6 k* a; O, w( x, z如果UpdateServer执行了小版本冻结,ChunkServer将执行数据分发。与定期合并
    # S% l# M, G5 i, [2 C5 Y( I. o( b不同的是,数据分发只是将UpdateServer冻结的数据缓存到ChunkServer,并不会生成) t  d% k  ^4 X8 m8 q
    新的SSTable文件。因此,数据分发对ChunkServer造成的压力不大。5 |& ]  n7 p) X" }7 U6 O' D
    数据分发由外部读取请求驱动,当请求ChunkServer上的某个子表时,除了返回3 H! w/ Y! c% |8 y- V- Y1 o
    使用者需要的数据外,还会在后台生成这个子表的数据分发任务,这个任务会获取
    ' {2 H/ N) K4 _UpdateServer中冻结的小版本数据,并缓存在ChunkServer的内存中。如果内存用完,
    % b9 J$ x# K" L数据分发任务将不再进行。当然,这里可以做一些改进,比如除了将UpdateServer分
    ) D' ]) O, |& S8 u6 Z3 K发的数据存放到ChunkServer的内存中,还可以存储到SSD磁盘中。
    6 x) k' A) C' P) N6 y/ C5 Z0 s2 j# b例9-6 假设某台ChunkServer上有一个子表t1,t1的主键范围为(1,10],只有一9 t3 i% `( B6 B1 O' F( _
    行数据:rowkey=8=>(<2,update,20>,<3,update,30>,<4,update,40
    . L  O, W: Z, i. N>)。UpdateServer的冻结版本有两行更新操作:rowkey=8=>(<2,update,30
    % d7 B- t& Y/ E; `6 M8 U) z>,<3,up-date,38>)和rowkey=20=>(<4,update,50>)。& O7 l! @7 y; x2 e+ O$ {
    ●如果是大版本冻结,那么,ChunkServer上的子表t1执行定期合并后结果为:( u8 E; |  e) P  q: O3 N! ~9 e
    ro-wkey=8=>(<2,update,30>,<3,update,38>,<4,update,40>);0 [/ Q8 \8 b+ x( w+ e/ X/ q
    ●如果是小版本冻结,那么,ChunkServer上的子表t1执行数据分发后的结果为:
    % b# I& u2 u" G3 Rrowkey=8=>(<2,update,20>,<3,update,30>,<4,update,40>,<2,
    % r  a% g; D' B' Q7 x& w$ a0 Uupdate,30>,<3,update,38>)。
    % c* Y' g" m( G% W" ^9.4.6 定期合并限速
    $ v% d! J5 V8 v8 N* H定期合并期间系统的压力较大,需要控制定期合并的速度,避免影响正常服0 S4 g2 Y" V0 S+ J0 U$ d
    务。定期合并限速的措施包括如下步骤:# e: C7 ~4 N4 U: C
    1)ChunkServer:ChunkServer定期合并过程中,每合并完成若干行(默认2000
    3 w; t( f, L5 S! q" {行)数据,就查看本机的负载(查看Linux系统的Load值)。如果负载过高,一部分% T. x/ |; @9 I) [) V  ~' o
    定期合并线程转入休眠状态;如果负载过低,唤醒更多的定期合并线程。另外,
    & z# C% L" m" p# S& v- hRootServer将UpdateServer冻结的大版本通知所有的ChunkServer,每台ChunkServer会- k0 A: h* l  h: p" g" s
    随机等待一段时间再开始执行定期合并,防止所有的ChunkServer同时将大量的请求" ^. Q: X, K4 d! b% t
    发给UpdateServer。
    0 I5 d& u6 F  g, V/ m! p  ]% q2)UpdateServer:定期合并过程中ChunkServer需要从UpdateServer读取大量的数
    8 Z9 q% H) R2 e9 B! D2 H据,为了防止定期合并任务用满带宽而阻塞用户的正常请求,UpdateServer将任务区' k* h' }, |6 S, T* c1 ~
    分为高优先级(用户正常请求)和低优先级(定期合并任务),并单独统计每种任) d3 g' O# _. s- Y3 e% ~1 G
    务的输出带宽。如果低优先级任务的输出带宽超过上限,降低低优先级任务的处理: [$ q# P* `5 j8 Z: x2 @1 w
    速度;反之,适当提高低优先级任务的处理速度。; _2 u/ z1 [0 E
    如果OceanBase部署了两个集群,还能够支持主备集群在不同时间段进行“错峰2 J1 ?8 M$ v( X( ?; {( G7 C
    合并”:一个集群执行定期合并时,把全部或大部分读写流量切到另一个集群,该集" m" n# N5 g4 Y# g- M
    群合并完成后,把全部或大部分流量切回,以便另一个集群接着进行定期合并。两
    % H  y; ]" C+ o" w2 u" C( F个集群都合并完成后,恢复正常的流量分配。* g/ T3 Q  x; C% X7 H; R# ~
    8 v  O5 [, r1 t% q$ K" ?

    * G" c4 c5 S( _' d8 P  c
    回复

    使用道具 举报

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

    本版积分规则

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

    GMT+8, 2024-12-22 14:18 , Processed in 0.134290 second(s), 30 queries .

    Powered by Javazx

    Copyright © 2012-2022, Javazx Cloud.

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