java自学网VIP

Java自学网

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 2555|回复: 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实现机制! h- D( H1 J$ t( }9 e7 h: _3 H) h. u) v
    ChunkServer用于存储基线数据,它由如下基本部分组成:/ x7 U' d( [% x) u2 j
    ●管理子表,主动实现子表分裂,配合RootServer实现子表迁移、删除、合并;
    / }' {3 `/ w$ R0 M' l●SSTable,根据主键有序存储每个子表的基线数据;( ~& Y: K: @) \
    ●基于LRU实现块缓存(Block cache)以及行缓存(Row cache);& H2 b  C( z& Y1 [8 i& x
    ●实现Direct IO,磁盘IO与CPU计算并行化;% t. L0 p: J6 Y  U8 s+ w8 k
    ●通过定期合并&数据分发获取UpdateServer的冻结数据,从而分散到整个集. k/ k1 D# t" C' _+ Q
    群。
    , u2 D: A) C6 h5 v* m# `: ]3 m每台ChunkServer服务着几千到几万个子表的基线数据,每个子表由若干个+ G$ Z+ j' `3 x* T* h* X1 F4 R5 Z
    SSTable组成(一般为1个)。下面从SSTable开始介绍ChunkServer的内部实现。
    , u/ I* G" A" a' t6 L) g( q9.4.1 子表管理3 Y3 [5 c4 i5 V2 j6 Q; [' Z
    每台ChunkServer服务于多个子表,子表的个数一般在10000~100000之间。
    / V1 c5 v8 t* I  [0 V; J1 U( |Chunk-Server内部通过ObMultiVersionTabletImage来存储每个子表的索引信息,包括数- A  b+ W2 V" i5 g
    据行数(row_count),数据量(occupy_size),校验和(check_sum),包含的
    7 [$ p  ~% S5 jSSTable列表,所在磁盘编号(disk_no)等,代码如下:; r) M- @6 `$ m- a; _
    class ObMultiVersionTabletImage+ f6 Q* n0 z3 X% V- V/ Q
    {
    - E- h3 z% B" ]8 `. e, tpublic:
    & {' n! L3 t; j& C3 p, Q( L# s//获取第一个包含指定数据范围的子表
    % U& f. n( k) {/ Q- {9 Y//@param[in]range数据范围0 @. P  @+ O. p% V
    //@param[in]scan_direction正向扫描(默认)还是逆向扫描- h4 d" P. n4 L' d" Q3 J
    //@param[in]version子表的版本号
    5 g# n: n: V& {6 @1 S* v//@param[out]tablet获取的子表索引结构+ O/ }% r& S0 ?. U
    int acquire_tablet(const ObNewRange&range,const ScanDirection
    & U) b3 w1 q- w5 J5 V2 i# Jscan_direction,const int64_t version,ObTablet*&tablet)const;' o* E* s0 ]+ {9 G. U4 s
    //释放一个子表4 R2 n4 B! g$ k3 s/ _6 }+ I
    int release_tablet(ObTablet*tablet);
    8 n7 l/ a( o. d7 H: ]//新增一个子表,load_sstable表示是否立即加载其中的SSTable文件
    7 P6 z% i0 W3 l$ N0 l' O, l. ~int add_tablet(ObTablet*tablet,const bool load_sstable=false);& k0 ]2 `4 y7 q3 N: C) s5 B
    //每日合并后升级子表到新版本,load_sstable表示是否立即加载新版本的SSTable文件2 O1 h" t5 h# g
    int upgrade_tablet(ObTablet*old_tablet,ObTablet*new_tablet,const bool+ X2 O& v7 K: i8 H# J4 v
    load_sstable=false);
    6 O! ]+ K* |$ m* e4 r5 _, A//每日合并后升级子表到新版本,且子表发生分裂,有一个变成多个。load_sstable表示是否立即加载: k3 E3 t  N# R2 n/ x
    分裂后的SSTable文件
    3 |5 @0 v: r; f% V( _: oint upgrade_tablet(ObTablet*old_tablet,ObTablet*new_tablets[],const int32_t* g* N" ^/ ]& f4 P2 ]
    split_size,const bool load_sstable=false);
    . N" f! w! i; V: B& Q//删除一个指定数据范围和版本的子表
    " M9 `% Q. h6 d& n4 \int remove_tablet(const ObNewRange&range,const int64_t version);
    ! p; C$ |6 w- j. Y7 y; }$ L//删除一个表格对应的所有子表* O# v7 D$ e' c* A
    int delete_table(const uint64_t table_id);
    * X+ E: h- Z# w% o6 O: S//获取下一批需要进行每日合并的子表6 N8 |; g; g0 Q0 _
    //@param[in]version子表的版本号
    # |- k. s; ]  W  Z. x: h- U//@param[out]size下一批需要进行每日合并的子表个数2 p, J2 u2 I9 }
    //@param[out]tablets下一批需要进行每日合并的子表索引结构
    - G; i8 d) H3 S5 u- }int get_tablets_for_merge(const int64_t version,int64_t&size,ObTablet*&& r6 h' S: i5 f+ r& P
    tablets[])const;  k- u- p8 T2 R4 M
    };
    8 }# @' T9 C7 ?  w1 B- x2 I( eChunkServer维护了多个版本的子表数据,每日合并后升级子表的版本号。如果
      Y& `6 ?$ [* `子表发生分裂,每日合并后将由一个子表变成多个子表。子表相关的操作方法包
    3 Y. x3 S8 N4 i括:
    6 I/ b, p! m5 e3 \9 d# c; c1)add_tablet:新增一个子表。如果load_sstable参数为true,那么,立即加载其
    " N& M" \  y0 p! I- [4 Z& v中的SSTable文件。否则,使用延迟加载策略,即读取子表时再加载其中的SSTable。
    ) i' p, j0 J, z1 C* a2 h2)remove_tablet:删除一个子表。RootServer发现某个子表的副本数过多,则会# G( [0 A/ d3 e) j0 Y7 b
    通知其中某台ChunkServer删除指定的子表。
    ! E8 {8 f" a# X1 [3 E3)delete_table:删除表格。用户执行删除表格命令时,RootServer会通知每台
    : h6 h3 [" m2 c# T* SChunkServer删除表格包含的所有子表。
    . j% x6 Z. F; O+ ~4)upgrade_tablet:每日合并后升级子表的版本号。如果没有发生分裂,只需要' m" X5 ?6 O  m- h8 C0 _
    将老子表的版本号加1;否则,将老子表替换为多个范围连续的新子表,每个新子表
    5 `  ?6 W5 q1 ~) X* m0 _. t4 F2 \的版本号均为老子表的版本号加1。
    , i0 r3 r6 z2 {2 I' b( c( m8 Z# x5)acquire_tablet/release_tablet:读取时首先调用acquire_tablet获取一个子表,增$ L" }) g) A% U! I2 L$ v3 Y
    加该子表的引用计数从而防止它在读取过程中被释放掉,接着读取其中的SSTable,! L1 p7 i* d1 N8 K# Q, }
    最后调用release_tablet释放子表。
    7 Z' G3 M( h0 r2 Q6)get_tablets_for_merge:每日合并时通过调用该函数获取下一批需要进行每日
    / Y- @3 T) G' F9 h) Y合并的子表。: P+ R2 |" O& l2 T  w; q* \
    9.4.2 SSTable
    5 g2 R1 i4 f3 F/ p- x8 t如图9-8所示,SSTable中的数据按主键排序后存放在连续的数据块(Block)
    " i2 S1 G5 B" B* W+ p0 Z4 P$ M中,Block之间也有序。接着,存放数据块索引(Block Index),由每个Block最后一
    & S% _' n. i; q, b, o0 Q9 E行的主键(End Key)组成,用于数据查询中的Block定位。接着,存放布隆过滤器' `- K6 J: I0 ^4 M' Z
    (Bloom Filter)和表格的Schema信息。最后,存放固定大小的Trailer以及Trailer的偏$ e0 B7 ^0 N) z: R' b" u1 x
    移位置。) q0 ?0 p  f& ]7 O
    图 9-8 SSTable格式, c2 z8 }7 e7 F" ^2 a6 C! I
    查找SSTable时,首先从子表的索引信息中读取SSTable Trailer的偏移位置,接着5 i8 o! R: L- D$ j2 T
    获取Trailer信息。根据Trailer中记录的信息,可以获取块索引的大小和偏移,从而将
    7 _' N, V! W0 o; w, G7 A整个块索引加载到内存中。根据块索引记录的每个Block的最后一行的主键,可以通9 r1 |- O9 }1 n% M7 F! R% [0 d
    过二分查找定位到查找的Block。最后将Block加载到内存中,通过二分查找Block中
    / _. L  @; X8 ~& ~, L, `+ b# ^记录的行索引(Row Index)查找到具体某一行。本质上看,SSTable是一个两级索引
    ' I: i: D* V  t+ p( l结构:块索引以及行索引;而整个ChunkServer是一个三级索引结构:子表索引、块
    * o" p  Q5 G# h; f$ q索引以及行索引。
    . M! f- Y- ^7 [: a8 FSSTable分为两种格式:稀疏格式以及稠密格式。对于稀疏格式,某些列可能存/ r% _. V3 n5 m: M! ?: H8 S
    在,也可能不存在,因此,每一行只存储包含实际值的列,每一列存储的内容为:. A9 R; D1 v6 N  D' Z
    <列ID,列值>(<Column ID,Column Value>);而稠密格式中每一行都需要存储
    6 @1 E- a. |' H/ K" K1 A所有列,每一列只需要存储列值,不需要存储列ID,这是因为列ID可以从表格9 J$ X: p5 h6 s  ?
    Schema中获取。
    9 B2 j3 z# k; h例9-4 假设有一张表格包含10列,列ID为1~10,表格中有一行的数据内容) I( z# V' C6 r: e% D
    为:0 ^2 E1 G7 K$ i3 R% Y- y! V
    那么,如果采用稀疏格式存储,内容为:<2,20>,<3,30>,<5,50>,
    5 P- J' n1 q/ A& i2 }( j<7,70>,<8,80>;如果采用稠密格式存储,内容为:null,20,30,null,4 y7 t4 f% Y& `# V% ]- W" V2 @
    50,null,70,80,null,null。2 L5 n: S5 l, `7 [
    ChunkServer中的SSTable为稠密格式,而UpdateServer中的SSTable为稀疏格式,- I" B3 Z3 b, J0 g( Y1 y
    且存储了多张表格的数据。另外,SSTable支持列组(Column Group),将同一个列
    9 G3 ?- r6 V! _7 t, ?, \" G/ P组下的多个列的内容存储在一块。列组是一种行列混合存储模式,将每一行的所有- x3 u, x9 f4 B  M+ F  e: j& x
    列分成多个组(称为列组),每个列组内部按行存储。- Y: q! s0 V( m
    如图9-9所示,当一个SSTable中包含多个表格/列组时,数据按照[表格ID,列组6 \4 f! b; Q* M) F7 q! k
    ID,行主键]([table_id,column group id,row_key])的形式有序存储。% d: }' L# z0 ~4 }+ G
    图 9-9 SSTable包含多个表格/列组" n$ V9 p9 v" A" U8 g
    另外,SSTable支持压缩功能,压缩以Block为单位。每个Block写入磁盘之前调
    7 H5 K( Z; @3 }8 [用压缩算法执行压缩,读取时需要解压缩。用户可以自定义SSTable的压缩算法,目8 |7 L0 \2 n9 T8 U! f. [
    前支持的算法包括LZO以及Snappy。
    3 s2 a' u3 L; p1 X+ }6 r3 X& JSSTable的操作接口分为写入和读取两个部分,其中,写入类为
    2 h( J% |+ o; s+ P/ kObSSTableWriter,读取类为ObSSTableGetter(随机读取)和ObSSTableScanner(范围
    7 x( B2 X3 j1 D! t; P查询)。代码如下:
    7 G" K6 X9 N. g+ @% I5 I8 Fclass ObSSTableWriter) y* b+ y- x9 W/ t& W% Z/ B7 b( V
    {
    2 K) {2 X! x, R  n" E$ d; ^public:: e/ [1 M" a; ~7 D
    //创建SSTable
    0 G3 D) H" \4 K* S$ a& P& c//@param[in]schema表格schema信息
    + ^. d7 L. v. L' D4 O* k//@param[in]path SSTable在磁盘中的路径名" ?3 H3 `) N3 g: @; o1 }& v
    //@param[in]compressor_name压缩算法名
    1 o: q  X8 @; g9 V. d% G5 J//@param[in]store_type SSTable格式,稀疏格式或者稠密格式" Y' D+ ]  i5 z+ S4 E- o. d: @- q# F
    //@param[in]block_size块大小,默认64KB
    : j/ ~8 o/ ?" v  Y% z, @int create_sstable(const ObSSTableSchema&schema,const ObString&path,const' L4 k; |& B! S! X; C0 V- K  A
    ObString&compressor_name,const int store_type,const int64_t block_size);
    " n' d& G) |7 W  l: I# r0 J- d) v//往SSTable中追加一行数据
    8 [7 q6 x4 n' ?; D" w3 A2 j9 |//@param[in]row一行SSTable数据  Z  G; E8 `8 q8 |9 k. ]
    //@param[out]space_usage追加完这一行后SSTable大致占用的磁盘空间" F  I& Q! h5 i( w' x" s
    int append_row(const ObSSTableRow&row,int64_t&space_usage);4 K, }7 ~# z* b
    //关闭SSTable,将往磁盘中写入Block Index,Bloom Filter,Schema,Trailer等信息/ H+ M; }" ?/ z' A+ B9 }8 c
    //@param[out]trailer_offset返回SSTable的Trailer偏移量
    5 t" Q  L- R( E' {, x4 ?int close_sstable(int64_t&trailer_offset);
    ' Q6 |; B0 H1 D# I};
    8 n  D# J, y. N: Y  E6 z1 p定期合并&数据分发过程将产生新的SSTable,步骤如下:$ B& n# M. o% b9 g: }2 E  |7 q. @7 v
    1)调用create_sstable函数创建一个新的SSTable;
    2 |8 Z- @6 N- C! }$ ]( F1 n2)不断调用append_row函数往SSTable中追加一行行数据;: D: G3 Y& E& o2 Y9 i" m
    3)调用close_sstable完成SSTable写入。4 d( {8 |* g1 g! Y" c: y
    与9.2.1节中的MemTableIterator一样,ObSSTableGetter和ObSSTableScanner实现了
    4 P: z% T8 M, ?; e" t迭代器接口,通过它可以不断地获取SSTable的下一个cell。8 L3 ^: c4 x* H4 ?
    class ObIterator) M- p8 a) l, R# l7 Z# O1 [
    {1 [# _* t1 y# m" o
    public:
    ' x- i+ A% o. L& f//迭代器移动到下一个cell
    3 h  h6 Y( n6 i6 g. Fint next_cell();
    ' b& m, Z" r$ w//获取当前cell的内容
    / u5 m, B1 ?! T4 R6 J//@param[out]cell_info当前cell的内容,包括表名(table_id),行主键(row_key),列编号
    & V: a0 S% L7 L4 ^(column_id)以及列值(column_value)# q  B. ~9 G: K
    int get_cell(ObCellInfo**cell_info);* X0 o2 E( Q6 H2 A$ i3 g
    //获取当前cell的内容
    " i6 f; h5 a7 {3 z- u//@param[out]cell_info当前cell的内容5 k' Y3 L$ `$ n) e/ B' a3 v4 e. v
    //@param is_row_changed是否迭代到下一行1 I7 ^. u& E! V7 F0 S6 _* p
    int get_cell(ObCellInfo**cell_info,bool*is_row_changed);
    ! v1 t' O7 u8 j! f$ p" S, Q};) ]! b( W0 q  o: c3 l" U
    OceanBase读取的数据可能来源于MemTable,也可能来源于SSTable,或者是合
    " j- R3 L3 f! i5 ?+ t9 R. m并多个MemTable和多个SSTable生成的结果。无论底层数据来源如何变化,上层的读
    & K( ~6 H* W- m6 V2 F取接口总是ObIterator。
    : Q+ M1 ~2 i* h& L3 m# \3 |7 `* E9.4.3 缓存实现) i0 W, M- h" x+ P+ _
    ChunkServer中包含三种缓存:块缓存(Block Cache)、行缓存(Row Cache)以
    % ^* _+ q- H3 O3 I及块索引缓存(Block Index Cache)。其中,块缓存中存储了SSTable中访问较热的数
    $ j$ A* B$ y' L% v( H7 l据块(Block),行缓存中存储了SSTable中访问较热的数据行(Row),而块索引缓
    ! d5 l- _, S; U) M5 z& j存中存储了最近访问过的SSTable的块索引(Block Index)。一般来说,块索引不会3 {9 j( Q" k% B' e6 g. p1 m
    太大,ChunkServer中所有SSTable的块索引都是常驻内存的。不同缓存的底层采用相
    ; f8 h+ G2 {, R) U. u5 ~! c- V+ R同的实现方式。
    ! K7 ~3 f1 W: Y/ V0 C1.底层实现/ `# V0 D! }. y1 w5 N
    经典的LRU缓存实现包含两个部分:哈希表和LRU链表,其中,哈希表用于查找/ c) S  X8 T) k4 B* p) B+ W
    缓存中的元素,LRU链表用于淘汰。每次访问LRU缓存时,需要将被访问的元素移动
      [) o: I% f% \; J/ D7 p到LRU链表的头部,从而避免被很快淘汰,这个过程需要锁住LRU链表。
    : q' P8 m6 k/ q* b如图9-10所示,块缓存和行缓存底层都是一个Key-Value Cache,实现步骤如下:# X- O' |4 o# `
    图 9-10 Key-Value Cache的实现! b0 h0 _* B  [
    1)OceanBase一次分配1MB的连续内存块(称为memblock),每个memblock包
    / _) g5 [' I7 ^+ C" L: |) _: M含若干缓存项(item)。添加item时,只需要简单地将item追加到memblock的尾部;
    " v- h  W( `- P8 q! ^5 z( I1 }( q; Q( N另外,缓存淘汰以memblock为单位,而不是以item为单位。0 U9 Z$ A) g, g# F/ S7 y
    2)OceanBase没有维护LRU链表,而是对每个memblock都维护了访问次数和最
    2 [* T  ]2 y! j5 Q0 q7 F近频繁访问时间。访问memblock中的item时将增加memblock的访问次数,如果最近一4 {: }1 }$ v5 ]6 }7 [: R7 x
    段时间之内的访问次数超过一定值,那么,更新最近频繁访问时间;淘汰memblock
    1 l( b' X- a8 ~* R9 o8 g时,对所有的memblock按照最近频繁访问时间排序,淘汰最近一段时间访问较少的: m  P1 r! d6 H8 v4 u3 m
    memblock。可以看出,读取时只需要更新memblock的访问次数和最近频繁访问时$ J2 {% P( N: `' \4 G& s6 I
    间,不需要移动LRU链表。这种实现方式通过牺牲LRU算法的精确性,来规避LRU链& I1 V9 ]' l1 X. `& @3 }& O
    表的全局锁冲突。
    7 }) m* Q) I. V3)每个memblock维护了引用计数,读取缓存项时所在memblock的引用计数加! [& d" z& w9 x) i" F- [
    1,淘汰memblock时引用计数减1,引用计数为0时memblock可以回收重用。通过引用
      T4 w" z8 V7 c! }计数,实现读取memblock中的缓存项不加锁。$ m& [- Y* n6 p4 q* J6 [7 |' J& K
    2.惊群效应
    / r; L  }  g5 v0 o以行缓存为例,假设ChunkServer中有一个热点行,ChunkServer中的N个工作线
    ( f! j' ]# a6 e" G. R程(假设为N=50)同时发现这一行的缓存失效,于是,所有工作线程同时读取这行7 P9 M# K- l; m- q2 R7 B
    数据并更新行缓存。可以看出,N-1共49个线程不仅做了无用功,还增加了锁冲突。7 ]! a$ x2 @% }7 u2 s, M/ ~5 N: I
    这种现象称为“惊群效应”。为了解决这个问题,第一个线程发现行缓存失效时会往" K9 ]- d& E' q. k' i, S* C& p8 `# ]
    缓存中加入一个fake标记,其他线程发现这个标记后会等待一段时间,直到第一个线
    - c- L6 o) C1 `1 v" J# x程从SSTable中读到这行数据并加入到行缓存后,再从行缓存中读取。
    2 F- s+ x2 X4 Q3 ?: }* A算法描述如下:( D) c4 k0 k$ M# Q! q) ?* S2 R/ a
    调用internal_get读取一行数据;7 W& q5 z. n' H- D4 r0 Z0 s
    if(行不存在){$ E3 G" i! F3 U& Z# `4 I
    调用internal_set往缓存中加入一个fake标记;3 T- E) x; O2 P! h1 W- U
    从SSTable中读取数据行;3 i  ]! t' B9 H
    将SSTable中读到的行内容加入缓存,清除fake标记,唤醒等待线程;
    & H; t; \# G$ F8 |/ s0 {/ D返回读到的数据行;
    % g" u% z/ k: ?- }" ]7 H}else if(行存在且为fake标记)% k- v- `, N9 T# m- ]9 _0 y' m
    {+ R# ?, f7 `1 R! _) V
    线程等待,直到清除fake标记;% r* R  |! g* x
    if(等待成功)返回行缓存中的数据;
    9 ]4 u% U: C% Z: b0 S8 |  k0 Jif(等待超时)返回读取超时;
    , s6 ^. q* h, E  E6 V4 H}$ L1 n+ g' W5 [* Z0 R, E1 I2 @
    else
    7 x( A# \# |( }% b7 {{
    ( a. o" c. T) D; d' m返回行缓存中的数据;
    : g( S, J/ a7 j, G) ^: w( X}
    + [6 ], H3 o. p5 Y5 t( d5 A3.缓存预热
    * o8 w/ Y4 d/ p" d7 S" v- ~ChunkServer定期合并后需要使用生成的新的SSTable提供服务,如果大量请求同
    * C7 W7 s0 Y) w2 i* o时读取新的SSTable文件,将使得ChunkServer的服务能力在切换SSTable瞬间大幅下
    & R% }5 Z6 C! A% G% \8 T8 _降。因此,这里需要一个缓存预热的过程。OceanBase最初的版本实现了主动缓存预
    ; D* ]' G# U" [7 |! K7 N6 t热,即:扫描原来的缓存,根据每个缓存项的key读取新的SSTable并将结果加入到新4 j# R" A4 U2 I
    的缓存中。例如,原来缓存数据项的主键分别为100、200、500,那么只需要从新的6 W8 `; x8 n3 C2 K
    SSTable中读取主键为100、200、500的数据并加入新的缓存。扫描完成后,原来的缓/ ]0 B7 J: M4 \" a# W" o, r
    存可以丢弃。3 o, T, f3 \9 A/ s9 [5 \# m
    线上运行一段时间后发现,定期合并基本上都安排在凌晨业务低峰期,合并完7 c6 _8 q/ Y) j3 i8 l
    成后OceanBase集群收到的用户请求总是由少到多(早上7点之前请求很少,9点以后1 F. M* ]% N- P& R2 ]% m: \- j
    请求逐步增多),能够很自然地实现被动缓存预热。由于ChunkServer在主动缓存预+ ~. {" A& B# T) i$ p' f. _
    热期间需要占用两倍的内存,因此,目前的线上版本放弃了这种方式,转而采用被
    0 ~3 V+ h$ v# O9 {) @动缓存预热。+ d2 v! C7 z4 g
    9.4.4 IO实现
    0 _$ C! O9 H1 POceanBase没有使用操作系统本身的页面缓存(page cache)机制,而是自己实现
      W" S9 W/ c2 H! b/ w缓存。相应地,IO也采用Direct IO实现,并且支持磁盘IO与CPU计算并行化。
    & C, V8 _  R; z1 QChunkServer采用Linux的Libaio [1] 实现异步IO,并通过双缓冲区机制实现磁盘预读
    & d7 w1 f$ b, V5 ?. B  n  D9 J与CPU处理并行化,实现步骤如下:
    " q& U8 O8 D% O6 k- p- F2 S( n1)分配当前(current)以及预读(ahead)两个缓冲区;
    9 @& @$ T; Z5 s+ s- s- W( l2)使用当前缓冲区读取数据,当前缓冲区通过Libaio发起异步读取请求,接着
    9 C( u! V4 [- D5 a: p( z: }. M  }等待异步读取完成;
    ! l- Y0 B  p; f+ t7 T! z& t3)异步读取完成后,将当前缓冲区返回上层执行CPU计算,同时,原来的预读
    0 k3 B  M+ n. D0 S. j/ ]% g+ Y缓冲区变为新的当前缓冲区,发送异步读取请求将数据读取到新的当前缓冲区。* b4 P% s0 @. F; }- r1 T
    CPU计算完成后,原来的当前缓冲区变为空闲,成为新的预读缓冲区,用于下一次2 l: f6 L1 w* T5 j2 `, @
    预读。3 X! r& f8 m: S' {% W; Y7 O' O
    4)重复步骤3),直到所有数据全部读完。
    1 Z) n4 Q9 E; m5 x5 Z$ f例9-5 假设需要读取的数据范围为(1,150],分三次读取:(1,50],(50,. P# P/ A4 I7 ~
    100],(100,150],当前和预读缓冲区分别记为A和B。实现步骤如下:
    5 C+ N! U6 A  p1)发送异步请求将(1,50]读取到缓冲区A,等待读取完成;
    " g. P0 r# Z8 J4 D9 _: H2)对缓冲区A执行CPU计算,发送异步请求,将(50,100]读取到缓冲区B;
    3 m) v1 N! G9 G8 B2 S3)如果CPU计算先于磁盘读取完成,那么,缓冲区A变为空闲,等到(50,5 T& ^6 ~+ Q' G' I  h% a: v
    100]读取完成后将缓冲区B返回上层执行CPU计算,同时,发送异步请求,将8 {0 w, K# K2 }! t: g2 w
    (100,150]读取到缓冲区A;
      M. v$ n/ k* `9 F" Z4)如果磁盘读取先于CPU计算完成,那么,首先等待缓冲区A上的CPU计算完
    9 y; W4 y: I2 M+ x5 R' t6 a- `$ k成,接着,将缓冲区B返回上层执行CPU计算,同时,发送异步请求,将(100,
    # ~( o* {) i% `5 f) O3 Q$ b) H150]读取到缓冲区A;! @1 g3 v& C' R9 U+ i
    5)等待(100,150]读取完成后,将缓冲区A返回给上层执行CPU计算。
    / D/ x8 y! S" o: I; c! k双缓冲区广泛用于生产者/消费者模型,ChunkServer中使用了双缓冲区异步预读
    7 O2 C- y& K0 N, V% k; q* S的技术,生产者为磁盘,消费者为CPU,磁盘中生产的原始数据需要给CPU计算消费
    - \' ^" g3 ]  _$ J( @9 Q掉。
    + G, @( f8 ?( e7 a所谓“双缓冲区”,顾名思义就是两个缓冲区(简称A和B)。这两个缓冲区,总% l+ o' J. c9 C  L0 E
    是一个用于生产者,另一个用于消费者。当两个缓冲区都操作完,再进行一次切; G; i8 x$ W1 t) \
    换,先前被生产者写入的被消费者读取,先前消费者读取的转为生产者写入。为了
    . N/ K2 C- y2 }" F! D0 r; d8 h3 q做到不冲突,给每个缓冲区分配一把互斥锁(简称La和Lb)。生产者或者消费者如  [5 t1 Q: p9 S: y" @
    果要操作某个缓冲区,必须先拥有对应的互斥锁。
    & Z' \8 I+ B2 B' G1 q; X& [双缓冲区包括如下几种状态:& x+ c4 E2 u1 n6 d$ Q! g
    ●双缓冲区都在使用的状态(并发读写)。大多数情况下,生产者和消费者都处& H) }* ~8 p) O! X" M
    于并发读写状态。不妨设生产者写入A,消费者读取B。在这种状态下,生产者拥有) D7 \" i/ W( g% ]
    锁La;同样地,消费者拥有锁Lb。由于两个缓冲区都是处于独占状态,因此每次读% X) P4 ?& V8 Z+ }
    写缓冲区中的元素都不需要再进行加锁、解锁操作。这是节约开销的主要来源。3 L" y" Y/ |. \: }" b9 _
    ●单个缓冲区空闲状态。由于两个并发实体的速度会有差异,必然会出现一个缓
      Y4 `4 H9 M! v0 K+ W* e- s9 R9 B冲区已经操作完,而另一个尚未操作完。不妨假设生产者快于消费者。在这种情况
    ; v; U  O% O# y( r1 ?8 v下,当生产者把A写满的时候,生产者要先释放La(表示它已经不再操作A),然后
    3 @% l' `- ]& }# z" M尝试获取Lb。由于B还没有被读空,Lb还被消费者持有,所以生产者进入等待
    - k! P$ O1 h( d( Y2 l(wait)状态。0 g% b8 m/ S6 h$ U
    ●缓冲区的切换。过了若干时间,消费者终于把B读完。这时候,消费者也要先5 _: T% S9 K/ l" h
    释放Lb,然后尝试获取La。由于La刚才已经被生产者释放,所以消费者能立即拥有9 X, z6 c0 i$ q" n1 @: ?. P: L
    La并开始读取A的数据。而由于Lb被消费者释放,所以刚才等待的生产者会苏醒过来
    ' m- k( |( m7 u+ S5 B/ E(wakeup)并拥有Lb,然后生产者继续往B写入数据。+ d) S& v' p; _- K
    [1]Oracle公司实现的Linux异步IO库,开源地址:https://oss.oracle.com/projects/libaio-
    * ]* R* u0 _* Z4 l% U2 s+ Ioracle/# W! U) ?9 Q' Y. ~9 Z7 y
    9.4.5 定期合并&数据分发
    , q" J% y( G8 G+ ^; v1 ?RootServer将UpdateServer上的版本变化信息通知ChunkServer后,ChunkServer将
    7 q: \: Y; s# v2 _3 Y% q7 I6 _执行定期合并或者数据分发。
    $ E2 S& Y: ^& p  w$ ^# r8 z如果UpdateServer执行了大版本冻结,ChunkServer将执行定期合并。ChunkServer- h# H3 h. N2 P; D3 }
    唤醒若干个定期合并线程(比如10个),每个线程执行如下流程:; r) f+ C+ T" I1 w1 i, P7 ?% P
    1)加锁获取下一个需要定期合并的子表;
    " c0 w  X9 P& R, v. y2)根据子表的主键范围读取UpdateServer中的修改操作;& g% R6 _& w2 a) k
    3)将每行数据的基线数据和增量数据合并后,产生新的基线数据,并写入到新
    : m7 _# \# ~, j" O3 D, |的SSTable中;3 C! [; r3 _. Y2 w  ~7 ], \8 _* S
    4)更改子表索引信息,指向新的SSTable。- b; \9 y8 u+ b! p& [
    等到ChunkServer上所有的子表定期合并都执行完成后,ChunkServer会向
    ; p0 H; m& ?8 ]/ s/ c/ U/ A4 N# |RootServer汇报,RootServer会更新RootTable中记录的子表版本信息。定期合并一般5 V. l4 k# b, q' k( i  Q9 x
    安排在每天凌晨业务低峰期(凌晨1:00开始)执行一次,因此也称为每日合并。另9 w; `. a5 U" w
    外,定期合并过程中ChunkServer的压力比较大,需要控制合并速度,否则可能影响/ e5 j" J  u" j  b
    正常的读取服务。# V' O, f5 x5 P- s! O+ k
    如果UpdateServer执行了小版本冻结,ChunkServer将执行数据分发。与定期合并
    ! [" X; a9 j5 K0 ?不同的是,数据分发只是将UpdateServer冻结的数据缓存到ChunkServer,并不会生成
    - b3 j9 o, O/ }- X新的SSTable文件。因此,数据分发对ChunkServer造成的压力不大。6 E' G( Q* ?. O2 ^) `
    数据分发由外部读取请求驱动,当请求ChunkServer上的某个子表时,除了返回
    5 g, G4 Y7 ]" S3 W; }8 {使用者需要的数据外,还会在后台生成这个子表的数据分发任务,这个任务会获取
    / Z  ^. p% c: }, yUpdateServer中冻结的小版本数据,并缓存在ChunkServer的内存中。如果内存用完,- V& t* J5 r; j4 z
    数据分发任务将不再进行。当然,这里可以做一些改进,比如除了将UpdateServer分
    6 `* Y' z0 g6 x2 P5 {, y3 H发的数据存放到ChunkServer的内存中,还可以存储到SSD磁盘中。
    * m; `; f$ n% P0 _+ u8 g  }例9-6 假设某台ChunkServer上有一个子表t1,t1的主键范围为(1,10],只有一
    ( q6 E; _3 r5 D6 t行数据:rowkey=8=>(<2,update,20>,<3,update,30>,<4,update,40
    9 T' _! n+ U1 i2 l+ c( S>)。UpdateServer的冻结版本有两行更新操作:rowkey=8=>(<2,update,30
    , w* R) s0 U+ |5 O8 @- g5 V9 D>,<3,up-date,38>)和rowkey=20=>(<4,update,50>)。
    : t2 e. u" l9 S●如果是大版本冻结,那么,ChunkServer上的子表t1执行定期合并后结果为:3 Y# M) T% q: h! P& w- S' O! u
    ro-wkey=8=>(<2,update,30>,<3,update,38>,<4,update,40>);, |9 y  }1 \, h8 p0 L
    ●如果是小版本冻结,那么,ChunkServer上的子表t1执行数据分发后的结果为:# q# C% N0 B( j# I% D5 d/ g% f
    rowkey=8=>(<2,update,20>,<3,update,30>,<4,update,40>,<2,3 {0 }/ f. d7 D! N$ i
    update,30>,<3,update,38>)。
    % C0 d3 `1 h9 z9 V9.4.6 定期合并限速
    0 d( ?+ n$ x( F: A" V+ s& B  }/ d定期合并期间系统的压力较大,需要控制定期合并的速度,避免影响正常服
    & c' k: N4 H0 @7 L务。定期合并限速的措施包括如下步骤:
    8 J5 ?7 p5 p6 T+ F; S2 ?1)ChunkServer:ChunkServer定期合并过程中,每合并完成若干行(默认2000; T  Q( g3 q4 Y7 G# x+ K
    行)数据,就查看本机的负载(查看Linux系统的Load值)。如果负载过高,一部分$ n6 l4 x, f  ~/ c0 S3 i
    定期合并线程转入休眠状态;如果负载过低,唤醒更多的定期合并线程。另外,
    6 m( r6 @% i1 x: P: S' K" w/ _RootServer将UpdateServer冻结的大版本通知所有的ChunkServer,每台ChunkServer会" F7 V+ h- S9 x$ I3 e
    随机等待一段时间再开始执行定期合并,防止所有的ChunkServer同时将大量的请求& C" }; e9 |/ q7 F1 k% t
    发给UpdateServer。
    : y0 B  H9 C) ~+ t. ^$ n2)UpdateServer:定期合并过程中ChunkServer需要从UpdateServer读取大量的数
    8 a* W4 |; l5 v3 I5 s* N据,为了防止定期合并任务用满带宽而阻塞用户的正常请求,UpdateServer将任务区
    2 L4 n# ~% _! @( U分为高优先级(用户正常请求)和低优先级(定期合并任务),并单独统计每种任
    + j/ U; h  E% y: T* w务的输出带宽。如果低优先级任务的输出带宽超过上限,降低低优先级任务的处理# y' V: o! ]! ?
    速度;反之,适当提高低优先级任务的处理速度。
    3 ~! O3 A2 A$ p. f如果OceanBase部署了两个集群,还能够支持主备集群在不同时间段进行“错峰
    - h. ^/ x) ?" q" G( }合并”:一个集群执行定期合并时,把全部或大部分读写流量切到另一个集群,该集
    - P/ D; I* Z. s& T+ Z2 \群合并完成后,把全部或大部分流量切回,以便另一个集群接着进行定期合并。两
      f2 k* c& P5 T7 y. K个集群都合并完成后,恢复正常的流量分配。
    5 s) ?' K1 _/ e0 O0 y% ?9 D: h
    1 c* o8 P" X  A  @9 I3 Q) |5 k+ o+ @  W& E/ y
    回复

    使用道具 举报

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

    本版积分规则

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

    GMT+8, 2024-11-23 17:29 , Processed in 0.135710 second(s), 31 queries .

    Powered by Javazx

    Copyright © 2012-2022, Javazx Cloud.

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