2019年1月1日 星期二

淺談 Dockerfile

前陣子在玩 Dockerfile 想做成自動化打包 + 測試,在這過程中深刻體認到... Docker 需要注意的地方真不少,三天時間每當打包好後的 10 到 20分鐘,就可以找到數個問題,後續的我就浮沉在尋找問題跟解決問題之間。整理這篇文章作為經驗分享期許能幫助到更多人。

什麼是 Dockerfile

在開始講會碰到的一些問題前,要先知道 Dockerfile 其實是一個檔案用於撰寫 Docker Image 的建置腳本,編寫好後使用 Docker build -t {repository name} . 指令,這時 Docker 就會為我們打造一個專屬於自己的 Docker Image。
接著可以再執行 docker run -it {repository name},運行起一個被稱之為 Container 的環境,在環境中有一個與本機隔絕獨立的環境,至於這樣有什麼好處小編不在此多講,試想一個輕量化可通過指令控制的 VM 能帶來哪些好處,那大致也不會差得太遠了。

認識 Dockerfile

既然知道是一種腳本那就會有語法,讓我們用一個範例來認識 Dockerfile

範例

這是一個期望用於測試環境用的腳本,所以主要包含 Java, HBase, Maven 和 Proguard 等。(除此之外還包含 SSh 是由於 HBase 會通過通過此工具連線自身喚起服務)
Dockerfile
FROM centos:centos6.10

MAINTAINER CookieTsai

# Add JDK, HBase and Maven tarball file, using ADD will auto unzip the file.
ADD jdk1.7.0_151.tar.gz /opt/
ADD apache-maven-3.5.4-bin.tar.gz proguard5.3.3.tar.gz /opt/
ADD hbase-1.2.9-local.tar.gz /opt/

# 1. Install sshd and vim and making ssh non-ask
# 2. Setting ssh login without password and close firewall
# 3. Install JDK, HBase and Maven
RUN yum -y install openssh* vim-enhanced \
    && echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config \
    && ssh-keygen -t rsa -f ~/.ssh/id_rsa -P "" \
    && cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys \
    && chmod 600 ~/.ssh/authorized_keys \
    && service iptables stop && chkconfig iptables off \
    && ln -s /opt/jdk1.7.0_151 /opt/java \
    && ln -s /opt/apache-maven-3.5.4 /opt/maven \
    && ln -s /opt/proguard5.3.3 /opt/proguard \
    && ln -s /opt/hbase-1.2.9 /opt/hbase

ENV USER=root \
    JAVA_HOME=/opt/java \
    MAVEN_HOME=/opt/maven \
    HBASE_HOME=/opt/hbase \
    PROGUARD_HOME=/opt/proguard \
    PATH=/opt/proguard/bin:/opt/hbase/bin:/opt/maven/bin:/opt/java/bin:$PATH

# 1. Install nodejs's nvm
# 2. Using npm install newman and newman-reporter-html
RUN curl -s -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.2/install.sh | bash \
    && source /root/.nvm/nvm.sh \
    && nvm install v10.14.2 \
    && nvm alias default v10.14.2 \
    && nvm use default \
    && npm install -g newman \
    && npm install -g newman-reporter-html

EXPOSE 22 2181 8090 60000 60010 60020 60030

# Set root's setting
ADD startup.sh /startup.sh
RUN cat /startup.sh >> /root/.bashrc && echo "root:root" | chpasswd

CMD ["/bin/bash"]
startup.sh
if [ -d "$HBASE_HOME" ]; then
  # Start SSH Server
  service sshd start

  # Set Reginservers
  HOST_NAME=`hostname`
  sed -i "s/localhost/$HOST_NAME/" /opt/hbase/conf/hbase-site.xml
  echo $HOST_NAME > /opt/hbase/conf/regionservers

  # Start HBase
  start-hbase.sh
fi

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && . "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

語法解析

  • FROM:指要以哪一個 Image 作為基底
  • MAINTAINER:註明主要的維護者
  • ADD:將指定路徑的目錄或檔案放入 Image 的指定目錄中
  • RUN:建置時會執行此指令後的語法
  • ENV:設定系統內的環境變數
  • EXPOSE:設定給使用者知道哪些 PORT 有開放
  • CMD:設定當啟動時預設執行的命令

註解

  1. 多數語法指令是相互獨立,建議使用絕對路徑或直接宣告
    ADD /opt/sample.tar.gz /opt
    RUN cd /opt
    RUN tar -zxvf sample.tar.gz # 與前一指令相互獨立,此執行結果為找不到檔案
  2. 每一個指令 (如:RUN、ADD) 執行後會有一次 Commit
  3. FROM 指令的預設來源為 Docker hub
  4. ADD 指令在指定壓縮檔案加入時,如果有支援的情況下會自動解壓縮
  5. 除非確定要解壓縮其他時候都可以利用 COPY 指令取代來 ADD

其他

  1. Docker Image 最多只能有 127 個 Parents
  2. 使用 docker run 時並非是真的使用 root 身份完成登入必須當心
  3. 使用 docker run -it {image name} 時是可以利用 .bashrc 執行預設指令

2018年11月29日 星期四

Quick-Start Apache HBase at Local Site

注意事項:
    1. 安裝前請先自行安裝 Java 1.7 JDK 及設定 JAVA_HOME
    2. 此方式啟動時會抓取本機的 ip 變動時會無法連線
       可砍掉 /tmp/zookeeper 後重新啟動
    3. 文章以 1.2.X 版本為範例
  • Download Apache HBase hbase-1.2.9-bin.tar.gz
  • Unzip hbase-1.2.9-bin.tar.gz
    $ mkdir ~/opt && tar -zxvf hbase-1.2.9-bin.tar.gz -C $_
    $ ln -s ~/opt/hbase-1.2.9 ~/opt/hbase
  • Edit ~/.bash_profile add HBASE_HOME and PATH
    HBASE_HOME=~/opt/hbase
    export HBASE_HOME
    
    export PATH=$HBASE_HOME/bin:$PATH
    • After editing using command source ~/.bash_profile
  • Edit ~/opt/hbase/conf/hbase-site.xml Update Content
    <?xml version="1.0"?>
    <?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
    <configuration>
        <property>
            <name>hbase.rootdir</name>
            <value>file:///tmp/hbase</value>
        </property>
        <property>
            <name>hbase.zookeeper.property.dataDir</name>
            <value>/tmp/zookeeper</value>
        </property>
        <property>
            <name>hbase.zookeeper.quorum</name>
            <value>localhost</value>
        </property>
    </configuration>
  • Start The HBase
    $ start-hbase.sh

2018年10月9日 星期二

HBase Split Table And Base64 Pre-Split Shell

Split Table 的三種方法

第一種:Pre-Splitting

當 Table 被建立時,預設會配置一個 Region 給 Table,這時所有讀寫請求都會訪問到同一個 RegionServer 的同一個 Region 中,此時是沒有負載平衡的效果的,集群中可能存在空閒的 RegionServer。為了解決這個問題可以利用 Pre-Splitting,在建立 Table 的時候配置多個 Region 到同一個 Table 以期許達到分散的效果。
在 Table 初始化時,HBase 是不知道如何去 Split Region 的,因為 HBase 不知道RowKey 會以怎樣的方式被配置及產生。HBase 自帶了數種 Pre-Split 的算法,如 HexStringSplit 和 UniformSplit,我們可以根據 RowKey 的前綴來決定使用哪種 Split 方法。
範例
Bash
$ hbase org.apache.hadoop.hbase.util.RegionSplitter pre_split_table HexStringSplit -c 10 -f f1
P.S. -c 10 為 Region 數目;-f f1 為 Table's Column Family.

Base64 Pre-Split Table Shell

由於小編用的是 Base64 字元當前綴,所以另外寫了一個小工具可以幫助切割 Table,產出的結果可在 HBase Shell 中執行。
preSplit.sh
Bash
#!/bin/bash

CHARS="1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM+/"
TABLE=$1
PARTS=$2

CHARS_LEN=$(echo $CHARS | perl -pe 's/(\S)/\1\n/g' | xargs -I {} | wc -l)
RANGE=$(($CHARS_LEN/($PARTS)))
SPLIT=$(($PARTS-1))

function split_table() {
    echo $CHARS | perl -pe 's/(\S)/\1\n/g' | LC_ALL=C sort | xargs -n $2 \
    | cut -s -d ' ' -f $2 | head -n $SPLIT | xargs | sed "s/ /','/g" \
    | sed "s/^/create '$1', 'cf', SPLITS => ['/g" | sed "s/$/']/g"
}

split_table $TABLE $RANGE
Demo
Bash
# 執行方式
$ ./preSplit.sh {Table Name} {Region Count}

# Sample 1
$ ./preSplit.sh UserTable 3
create 'UserTable', 'cf', SPLITS => ['I','d']

# Sample 2
$ seq 2 10 | xargs -I {} ./preSplit.sh UserTable {}
create 'UserTable', 'cf', SPLITS => ['T']
create 'UserTable', 'cf', SPLITS => ['I','d']
create 'UserTable', 'cf', SPLITS => ['D','T','j']
create 'UserTable', 'cf', SPLITS => ['9','L','X','j']
create 'UserTable', 'cf', SPLITS => ['7','H','R','b','l']
create 'UserTable', 'cf', SPLITS => ['6','F','O','X','g','p']
create 'UserTable', 'cf', SPLITS => ['5','D','L','T','b','j','r']
create 'UserTable', 'cf', SPLITS => ['4','B','I','P','W','d','k','r']
create 'UserTable', 'cf', SPLITS => ['3','9','F','L','R','X','d','j','p']

第二種:Auto Splitting

觸發時機

  1. After Memstore Flush
  2. After Compact

Split 策略

  • ConstantSizeRegionSplitPolicy:0.94 版本前默認的切割策略。
程式註解
vim
A RegionSplitPolicy implementation which splits a region as soon as any of its store 
files exceeds a maximum configurable size. 

This is the default split policy. From 0.94.0 on the default split policy has changed 
to IncreasingToUpperBoundRegionSplitPolicy
關鍵程式碼
Java
@Override
protected boolean shouldSplit() {
  boolean force = region.shouldForceSplit();
  boolean foundABigStore = false;

  for (HStore store : region.getStores()) {
    if ((!store.canSplit())) {
       return false;
    }
    if (store.getSize() > desiredMaxFileSize) {
      foundABigStore = true;
    }
  }
  return foundABigStore || force;
}
切分規則
使用 ConstantSizeRegionSplitPolicy 策略,即 storefile 固定大小策略,當任一個 Storefile 的大小大於 hbase.hregion.max.filesize(Default: 10G) 的時候 Region 就會自動切割。

  • IncreasingToUpperBoundRegionSplitPolicy:0.94 ~ 2.0版本間默認的切割策略。
程式註解
vim
Split size is the number of regions that are on this server that all are of the same table, 
cubed, times 2x the region flush size OR the maximum region split size, whichever is smaller.

For example, if the flush size is 128MB, then after two flushes (256MB) we will split which 
will make two regions that will split when their size is 2^3 * 128MB*2 = 2048MB.

If one of these regions splits, then there are three regions and now the split size is 
3^3 * 128MB*2 = 6912MB, and so on until we reach the configured maximum file size and then 
from there on out, we'll use that.
關鍵程式碼
Java
protected long initialSize;

@Override
protected void configureForRegion(HRegion region) {
  super.configureForRegion(region);
  Configuration conf = getConf();
  initialSize = conf.getLong("hbase.increasing.policy.initial.size", -1);
  if (initialSize > 0) {
    return;
  }
  TableDescriptor desc = region.getTableDescriptor();
  if (desc != null) {
    initialSize = 2 * desc.getMemStoreFlushSize();
  }
  if (initialSize <= 0) {
    initialSize = 2 * conf.getLong(HConstants.HREGION_MEMSTORE_FLUSH_SIZE,
                                   TableDescriptorBuilder.DEFAULT_MEMSTORE_FLUSH_SIZE);
  }
}

@Override
protected boolean shouldSplit() {
  boolean force = region.shouldForceSplit();
  boolean foundABigStore = false;
  int tableRegionsCount = getCountOfCommonTableRegions();
  long sizeToCheck = getSizeToCheck(tableRegionsCount);
  
  for (HStore store : region.getStores()) {
    if (!store.canSplit()) {
      return false;
    }
    long size = store.getSize();
    if (size > sizeToCheck) {
      foundABigStore = true;
    }
  }
  return foundABigStore || force;
}

protected long getSizeToCheck(final int tableRegionsCount) {
  // safety check for 100 to avoid numerical overflow in extreme cases
  return tableRegionsCount == 0 || tableRegionsCount > 100
             ? getDesiredMaxFileSize()
             : Math.min(getDesiredMaxFileSize(),
                        initialSize * tableRegionsCount * tableRegionsCount * tableRegionsCount);
}
切分規則
使用 IncreasingToUpperBoundRegionSplitPolicy 策略,0.94 (包含)之後默認使用此策略,當 Storefile 大於 getSizeToCheck() 所回傳的值時會進行切割。此策略與 RegionServer 上的 Region 個數有關(假定其個數為 RS)
getSizeToCheck() 的規則如下:
  1. 當 RS 等於 0 或大於 100 時,與 ConstantSizeRegionSplitPolicy 策略相同
  2. 當 RS 介於 0 ~ 100 之間時,公式如下:
    • Min(hbase.hregion.max.filesize, hbase.increasing.policy.initial.size * RS ^ 3)
註解
  1. hbase.increasing.policy.initial.size 在未設定的情況下為 hbase.hregion.memstore.flush.size * 2
  2. hbase.hregion.memstore.flush.size 預設值為 128M

  • SteppingSplitPolicy:從 2.0版本開始默認的切割策略。
關鍵程式碼
Java
public class SteppingSplitPolicy extends IncreasingToUpperBoundRegionSplitPolicy {
  @Override
  protected long getSizeToCheck(final int tableRegionsCount) {
    return tableRegionsCount == 1 ? this.initialSize : getDesiredMaxFileSize();
  }
}
切分規則
繼承 IncreasingToUpperBoundRegionSplitPolicy 策略,當 Storefile 大於 getSizeToCheck() 所回傳的值時會進行切割。
getSizeToCheck() 的規則如下:
  1. 當 Regions 數量為 1 時,回傳 hbase.increasing.policy.initial.size
  2. 當 Regions 數量不為 1 時,與 ConstantSizeRegionSplitPolicy 策略相同

第三種:forced splits

使用 HBase Shell 可以強制切分 Table,方法如下:
HBase Shell
Bash
hbase(main):001:0> split

ERROR: wrong number of arguments (0 for 1)

Here is some help for this command:
Split entire table or pass a region to split individual region.  With the
second parameter, you can specify an explicit split key for the region.
Examples:
    split 'tableName'
    split 'namespace:tableName'
    split 'regionName' # format: 'tableName,startKey,id'
    split 'tableName', 'splitKey'
    split 'regionName', 'splitKey'

相關的配置參數

  1. hbase.regionserver.regionSplitLimit 預設值為 Integer.MAX(Cloudera)
控制 RegionServer 的最大 Region 數量,超過則不會進行 Auto Splitting。如果不禁止 Auto Splitting 在每次 flush 或著 compact 之後,RegionServer 都會檢查是否需要 Split,Split 過程老 Region 會先下線在上線 Split 後的 Region,在此期間 Client 訪問會失敗,Retry 過程中會成功但是如果是提供實時服務的系統則想印時長會增加,同時 Split 後的 Compact 是一個比較消耗資源的行為。
  1. hbase.hregion.max.filesize 預設值為 10G
設定 Region's Store Size 的 Max 值,此參數是主要觸發 Split 的條件之一,建議勿將此值設定過低,否則可能會有過多的零碎的 Region,一般的建議是 5G ~ 10G。