2015/4/13

應付 NullPointerException 的 java.util.Optional

因為不可預期的 NullPointerException 而造成系統 crash,這應該是所有寫程式的人都曾經遇到的情況,從資料的狀態來看,null 確實是一種特殊的存在狀態,也必須區分開來,但偏偏 Java 無法直接使用 null 物件,因為會產生 NullPointerException,接下來就討論一下 null 物件跟應對的方法。

Boundary Test

在測試理論的 boundary test 中,我們要注意資料、狀態變化與一些功能的特殊狀況,測試並檢驗程式會不會出錯。在 Java 中最常見的會引發系統 crash 的錯誤,就是 NullPointerException,系統會在遇到資料為 null 的情況下,發生不可預期的錯誤,就因為程式設計師不認為這個資料會是 null,而一旦存取到了一個 null 的物件,就會造成 NullPointerException 而中斷整個程式,讓系統 offline。

null 是不是一個正確值

程式設計師在一般的 Java 物件時,通常就會注意到這個物件有沒有被初始化,那怎麼還會遇到 NullPointerException 呢?

最重要的原因就是 String 這個最常見的物件,再加上初始化 String 物件有 syntax sugar,在撰寫程式時,通常 String 變數會存放在客製物件中,也常常不會發覺這個字串變數,到底有沒有初始化,只知道在 Data Bean 裡面 寫上 get/set method 而已。

null 對一個物件來說,也是一種正常的狀態,而這個狀態,代表的是這個物件所儲存的數值,還沒有被初始化。

null 跟 空字串 是不同的

不管是在資料庫或是在 Java 裡面,null 跟 空字串 都是不同的,也有不同的意義,null 基本上是一個標記,不暫用到任何記憶體或實體的空間。

而空字串 "" 就不同了,因為空字串是個正常的字串,長度為 0 的字串,在 Java 的字串 instance 中,就等於已經產生了一個長度為 0 的字串物件 instance,實際上是有佔用到記憶體的。

java.util.Optional

Java 8 提供一個新的 class java.util.Optional,可以封裝一個物件,並提供方法做 null 的檢測。基本上,所有可能會 return null 物件的 method 都應該改為 return Optional。

建立 Optional Object

  • Optional empty()
    產生 empty Optional

  • Optional of(T value)
    產生 T value 的 Optional object,但如果 value 是 null,就會丟出 NullPointerException

  • Optional ofNullable(T value)
    產生 T value 的 Optional object,但如果 value 是 null,就會回傳 empty Optional

使用範例

    Optional<String> empty  = Optional.empty();
    System.out.println(empty);

    Optional<String> str = Optional.of("custom string");
    System.out.println(str);

    String nullableString = null; 
    Optional<String> str2  = Optional.ofNullable(nullableString);
    System.out.println(str2);

ifPresent, isPresent

# 檢查此 Optional 物件是不是 null
public boolean isPresent()

# 這是用 Lambda 語法,當 Optional 為 empty 時,就不執行後面的 Consumer。
public void ifPresent(Consumer<? super T> consumer)

使用的範例如下

Optional<String> str = Optional.ofNullable(null);
str.ifPresent(value -> System.out.println("Optional contains " + value));

if (str.isPresent()) {
    String value = str.get();
    System.out.println("Optional contains " + value);
} else {
    System.out.println("Optional is empty.");
}

Optional<String> str2 = Optional.ofNullable("custom string");
str2.ifPresent(value -> System.out.println("Optional contains " + value));

執行結果,只會看到有2行輸出

Optional is empty.
Optional contains custom string

get, orElse, orElseGet, orElseThrow, ifElse

# 取得 Optional 裡面存放的物件,但如果 Optional 物件是 null,就 throw NoSuchElementException
public T get()

# 取得 Optional 裡面存放的物件,但如果 Optional 物件是 null,就 return 物件 other
public T orElse(T other)

# 取得 Optional 裡面存放的物件,但如果 Optional 物件是 null,就呼叫 other 這個 supplier 的 get 取得下一個物件
public T orElseGet(Supplier<? extends T> other)

# 取得 Optional 裡面存放的物件,但如果 Optional 物件是 null,就呼叫 exceptionSupplier 的 get 產生的 Throwable Exception,例如  IllegalStateException::new
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X extends Throwable

使用範例

String defaultString = "default string";
Optional<String> optionalString = Optional.empty();
String resultString = optionalString.orElse(defaultString);
System.out.println("resultString: " + resultString);

optionalString = Optional.ofNullable("custom string");
resultString = optionalString.orElse(defaultString);
System.out.println("resultString: " + resultString);

optionalString = Optional.ofNullable(null);
optionalString.orElseThrow(IllegalStateException::new);

執行結果

resultString: default string
resultString: custom string
Exception in thread "main" java.lang.IllegalStateException
    at Main$$Lambda$1/8460669.get(Unknown Source)
    at java.util.Optional.orElseThrow(Optional.java:290)
    at Main.main(Main.java:44)

filtering and mapping with Lambdas

# 如果 Optional 裡面的 value 存在,且符合 predicate 的條件限制,就回傳這個 value,否則就回傳 empty Optional
public Optional<T> filter(Predicate<? super T> predicate)

# 如果 Optional 裡面的 value 存在,就執行 mapper function,如果 value 為 null,就回傳 empty Optional
public <U> Optional<U> map(Function<? super T,? extends U> mapper)

使用範例

Optional<Integer> emptyNumber = Optional.empty();
emptyNumber.filter(x -> x == 250).ifPresent(
        x -> System.out.println(x + " is ok!"));

Optional<Integer> cardNumber = Optional.of(new Integer(300));
cardNumber.filter(x -> x == 200).ifPresent(
        x -> System.out.println(x + " is ok!"));

cardNumber = Optional.of(new Integer(200));
cardNumber.filter(x -> x == 200).ifPresent(
        x -> System.out.println(x + " is ok!"));

執行結果

200 is ok!

IntStream 的 max 與 min 回傳了 OptionalInt

在 IntStream 裡面,max 以及 min 的回傳資料都是 OptionalInt,這是在提取一堆 Int 裡面的最大值,而這個最大值,當然有可能會因為給予的 IntStream 本身就不存在,導致根本找不到最大值而得到不存在的結果,為了描述這個「合理」的不存在的結果,就套用了 Optional 封裝這個結果。

使用範例

OptionalInt maxOdd = IntStream.of(10, 20, 30).filter(n -> n % 2 == 1).max();
if (maxOdd.isPresent()) {
    int value = maxOdd.getAsInt();
    System.out.println("Maximum odd integer is " + value);
} else {
    System.out.println("Stream is empty.");
}

執行結果

Stream is empty.

真的會使用 Optional 嗎?

關於這個問題,我的看法是:

  • 如果 null 是邏輯上正確的狀態,那就使用 Optional。
  • 如果有個物件變數,但不確定會不會是 null,要避開產生 NullPointerException,就用 Optional.ofNullable(object).orElse(defaultValue); 確保在遇到 null 的時候,會有預設值
  • 如果不應該發生 null,那就直接把初始值放進去就好了,不需要再用 Optional 封裝起來。

或許一般的程式設計中,不需要考慮到這個問題,畢竟如果在 javadoc 裡面看到一堆 method,回傳的物件是 Optional,使用起來也會產生一些困擾,畢竟不是 CustomClass 的物件,沒辦法在收到回傳物件後,就直接使用該物件的 method。

在沒有 Optional 之前,我們也可以使用 if( object==null ) 的方式去檢查是不是 null,用 object==null ? defaultValue : realValue 的方式,為物件設定預設值。

但也不需要排斥或拒絕使用 Optional,他等於是用了一個物件的方式,包裝 null 的狀態下的處理過程,因此就把 Optional 當作是一個習慣的寫法就好了。

重要的是程式設計師要注意 null

不管語言或語法的問題,就程式的邏輯來看,在設計程式語言時,就必須明確地將 null 與 "" 區分開來,因為他們本質上就是不一樣的兩個概念。

最重要的是程式設計師對 null 的認知,寫程式的時候,必須隨時意識到這個變數會不會有 null 的狀況,必須運用程式語言既有的機制,對自己的程式做出負責任的處理。

換句話說,到底這個物件會不會有 null 的情況,必須明確地在程式中,以各種方式標示出來,甚至可以在規避 NullPointerException 的前提下,率先在 method 的 return value 中加上 null 的檢查,並在 null 時改回傳 default value。

也許一個 @NotNull 或是 @Nullable 的 annotation,更明確地告訴程式設計師要注意 null 會更有用,但就不繼續討論了。

Reference

java2s Optional

Java 8 Optional Example

Java Gossip: 使用 Optional 取代 null

Bootstrap Grid System

Bootstrap 以 grid system 提供網頁畫面版面切割的功能,官方手冊上 Grid system 這個段落,將此功能定義為 a responsive, mobile first fluid grid system,這表示 grid 是符合 Responsive Web Design 的系統,而且在行動版畫面是以比例的方式 fluid 為主,如果是一個設計良好的 fluid 頁面,那麼在手機瀏覽該頁面時,就不會看到水平方向有產生 scroll bar,該說明內容也明確地提供了使用 grid 要注意的設定。

要注意的是,bootstrap 的 grid system 在 v2 與 v3 有一些不同的地方,以下我們只討論 v3 的 grid system。

fixed/fluid/elastic layout

Fixed vs. Fluid vs. Elastic Layout: What’s The Right One For You?

fixed layout 是以固定的寬度 px 設定元件的寬度,而 fluid layout 是以總寬度的百分比例,設定元件的寬度。elastic 則是認為,要混合使用 fixed 與 fluid layout。

elastic layout 認為除了考慮螢幕寬度之外,還要考慮 resolution,低解析度的螢幕就該要配置較大尺寸的文字與圖片。

以 bootstrap grid system 來說,是使用 fluid layout 來設計頁面 layout。因為它只考慮了寬度,沒有考慮解析度。

grid system

以 Grid 的方式設計網頁,並不只有 bootstrap 才提供這樣的功能,目前在討論 Responsive Web Design 時,都已經習慣以 grid 的方式進行網頁設計,所以有很多 web framework 都支援 grid system,例如 960gs、susy與 blueprint。

當 web designer 與 programmer 都能以 grid 的方式,進行網頁設計,這樣就能對網頁 layout 能有共同的認知,而不會在 web desinger 切割的 layout 圖片在實際上放入網頁時,發生偏移無法對齊的問題。

不過同樣都是在使用 grid system,必須要注意的是,共用的 grid 規則為何,因為每個 framework 使用的 gutter 與 padding 不一樣。

  1. 頁寬共分為幾欄,bootstrap 為 12 欄
  2. 欄和欄之間的間距 gutter,bootstrap 的 column 前後的 padding 為 15 px,所以 gutter 就是 15+15 = 30 px
  3. 主要文本和邊緣的間距 grid padding

ref:
使用 grid system 來設計你的網頁
Grid 的運用:PS外掛
Using the 960 Grid System as a Design Framework

bootstrap grid system

grid system 是以 rows 與 columns 的組合,建立頁面的 layout。

  1. 假設每一頁可切割為 12 個 columns
  2. row (行) 必須放在一個有 .container (fixed-width) 或是 .container-fluid (full-width,滿版,以比例方式設定欄寬) 的 div 中

  3. 類似 col-xs-6 的 css class,用來定義 column寬度,並放在 row 裡面,網頁內容放在 column 裡面

  4. grid columns 總共有 12 個,可以指定 3個 .col-xs-4 用來填滿 12 個 columns
  5. 如果一個 .row 有超過 12個 columns,則超過的部份就會自動折行

Grid Options

-- Extra Small devices (Phones) (<768px) SMall devices (Tablets) (>=768px) Medium devices (Desktops) (>= 992px) LarGe devices (Desktops) (>=1200px)
Grid Behavior 永遠都是 horizontal 一開始是收合的,如果超過寬度的設定值,就會顯示出來 同左 同左
Container width none(auto) 750px 970px 1170px
Class prefix .col-xs- .col-sm- .col-md- .col-lg-
# of columns 12 同左 同左 同左
Column width auto ~62px ~81px ~97px
Gutter width 30px (15px on each side of a column) 同左 同左 同左
Nestable Yes 同左 同左 同左
Offset Yes 同左 同左 同左
Column ordering Yes 同左 同左 同左

Nestable 就是在 row, column 裡面,可再加入一層 rows, columns,就像是在一個 table 的 column 裡面再放進一個 table
Offset 就是位移,當設定了 .col-md-offset-4 ,這個區塊就會向右位移 4 格 columns,兩個 columns 中間多了一塊空白區域
Column ordering 就是可以改變 column 的前後順序,當設定了 .col-md-push-3 ,則會在 .col-md 有作用時,將該區塊往右邊 push 3 格

grid system 的範例

有關 grid system 的範例可查閱這兩個網址
bootstrap grid
bootstrap grid examples

接下來,列出一些簡短的範例紀錄使用方法

當 md 有作用時,分為 8:4 兩塊

<div class="container">
    <div class="row">
        <div class="col-md-8">.col-md-8</div>
        <div class="col-md-4">.col-md-4</div>
    </div>
</div>

同時支援 Mobile, Desktop 的樣式,Mobile 時為 12:6,Desktop 時為 8:4

<div class="row">
    <div class="col-xs-12 col-md-8">.col-xs-12 .col-md-8</div>
    <div class="col-xs-6 col-md-4">.col-xs-6 .col-md-4</div>
</div>

同時支援 Mobile, Tablet, Desktop 的樣式,Mobile 時為 12:6,也就是兩行,Tablet 時為 6:6,Desktop 時為 8:4

<div class="row">
    <div class="col-xs-12 col-sm-6 col-md-8">.col-xs-12 .col-sm-6 .col-md-8</div>
    <div class="col-xs-6 col-md-4">.col-xs-6 .col-md-4</div>
</div>

同時支援 Mobile, Tablet 的樣式,Mobile 時為 6:6,因為加上了 clearfix 與 visible-xs-block,所以 xs 的部份重設了,就不會折行,Tablet 時為 4:4:4

<div class="row">
    <div class="col-xs-6 col-sm-4">.col-xs-6 .col-sm-4</div>
    <div class="col-xs-6 col-sm-4">.col-xs-6 .col-sm-4</div>
    <!-- Optional: clear the XS cols if their content doesn't match in height -->
    <div class="clearfix visible-xs-block"></div>
    <div class="col-xs-6 col-sm-4">.col-xs-6 .col-sm-4</div>
</div>

支援 Tablet,第二區塊,會往右位移 4 格

<div class="row">
    <div class="col-md-4">.col-md-4</div>
    <div class="col-md-4 col-md-offset-4">.col-md-4 .col-md-offset-4</div>
</div>

Nesting columns,把 row 放入 col 裡面

<div class="row">
  <div class="col-sm-9">
    Level 1: .col-sm-9
    <div class="row">
      <div class="col-xs-8 col-sm-6">
        Level 2: .col-xs-8 .col-sm-6
      </div>
      <div class="col-xs-4 col-sm-6">
        Level 2: .col-xs-4 .col-sm-6
      </div>
    </div>
  </div>
</div>

Column Ordering: 利用 .col-md-push- 與 .col-md-pull- 改變區塊的順序,第一塊大小為 9,往右 push 3 格,第二塊大小為 3 ,往左 pull 9 格

<div class="row">
    <div class="col-md-9 col-md-push-3">.col-md-9 .col-md-push-3</div>
    <div class="col-md-3 col-md-pull-9">.col-md-3 .col-md-pull-9</div>
</div>

Responsive utilities: visible hidden

Responsive utilities 為了快速建立 mobile-friendly 的網頁,div 元件還可以根據 device 的狀態,來決定要 顯示 或是 隱藏。

css class devices
.visible-xs-* Extra small (less than 768px) visible
.visible-sm-* Small (up to 768 px) visible
.visible-md-* Medium (768 px to 991 px) visible
.visible-lg-* Larger (992 px and above) visible
.hidden-xs Extra small (less than 768px) hidden
.hidden-sm Small (up to 768 px) hidden
.hidden-md Medium (768 px to 991 px) hidden
.hidden-lg Larger (992 px and above) hidden
.visible-print-* Visible when print
.hidden-print Visible only to browser not to print

所有 .visible-device- 後面都可以加上 css 的 display property value: inline, block, inline-block,換句話說,.visible-xs-* 的設定就可以是 .visible-xs-inline 或是 .visible-xs-block 或是 .visible-xs-inline-block

References

Bootstrap Grid System in w3schools
Bootstrap 2.x 與 3 的重要差異(二):grid系統的變革
15 款相當不錯的 Twitter Bootstrap 開發工具

使用 Bootstrap 建置雛型網站
簡單的Bootstrap效果(v 3.0.0 / v 3.2.0版)與入門教學 (#2 範例修正與下載)

淺談CSS Grid system
《LayoutIt!》視覺化Bootstrap線上編輯器

Twitter Bootstrap 框架簡化網頁開發、兼容不同裝置,讓網頁工程師兼設計師!

2015/3/23

RWD Responsive Web Design

RWD Responsive Web Design 中文翻譯為響應式網頁設計或是適應性網頁設計,由於行動裝置盛行,為了能夠同時讓手機、平板電腦、一般PC電腦的瀏覽器能夠在不同的螢幕大小的條件下,都能夠在不縮放以及橫向捲動畫面的要求下,以最佳瀏覽體驗瀏覽一個網頁,同樣的網頁內容必須再藉由 RWD 要求要注意的幾項設計重點。

但我們倒可以直接將 RWD 翻譯為「負責任」的網頁設計,因為如果使用者同時會有不同螢幕大小的設備瀏覽網頁,網站設計者就必須「承擔起責任」,讓使用者能以最佳瀏覽體驗瀏覽網頁。

Ethan Marcotte

RWD 最早是在 2010 年由 Ethan Marcotte: A List Apart 提出的一項概念。

在文章的結論中提到 Fluid grids, flexible images, and media queries 是進行 RWD 時最重要的三項技術。

什麼時候要注意 RWD

在進行網頁設計前,有幾項設計需求是要先確認的,否則就可以決定不需要多花時間與人力進行 RWD 開發。

  1. 網站有沒有行動版的需求?
    如果在開發時就已經確定這個網站面對的使用者會使用手機、平板或是PC這三種類形的設備的某一種的瀏覽器,那麼我們就沒有必要花時間進行RWD

  2. 行動版跟PC版的網頁內容重複很多嗎?
    在一個網頁上進行 RWD,就表示我們在處理一個網頁的內容,但不同設備的呈現方式的問題。如果行動版的網頁內容在設計初期就決定要給使用者完全不同的呈現方式,那倒不如在使用者進入網頁後,就根據 media query 的結果,直接將使用者導向到不同的兩個網頁,一個是進入 PC 的網頁,一個是進入行動版的網頁。

如何實踐 RWD

以下內容是從 九個適應性設計小撇步,把你的網站打造成變形金剛!(上篇) 以及 九個適應性設計小撇步,把你的網站打造成變形金剛!(下篇) 兩篇文章中精簡得來的筆記。

讓圖片維持比例自動縮放

  1. 使用 img 標籤放置圖片

    當外層元素寬度縮到小於圖片時,圖片會自動縮小,不設定 width 屬性(或設定為 auto ),讓圖片維持不超過原始大小的寬高。

     <img id="imageid" src="xxx.png"></img>
    
     #imageid {
         height: auto;
         max-width: 100%;
     }
    
  2. 使用 CSS background 屬性

    可彌補 img 無法針對螢幕大小指定合適圖片的缺點。background-size 內兩個值分別代表背景圖的寬高。background-position 的兩個數值則分別指定背景圖片的水平及垂直位置,50% 表示置中。

     <div id="divid"></div>
    
     #divid {
         background-image: url(xxx.png);
         background-position: 50% 50%;
         background-repeat: no-repeat;
         background-size: 100% auto;
         display: block;
         height: 100px;
         width: 100%;
     }
    

讓 div 區塊內容自動往下排列

用 float 讓 div 區塊由右至左排列




用 inline-block 讓 div 區塊由左至右排列




讓 div 區塊置中

  1. 使用 display: block 時-固定寬度加上 margin: auto 屬性
    適用單一區塊水平置中,區塊的寬度設定為某個「小於上層元素」的數值,加上 margin: auto

  2. 使用 display: inline-block 時-在外層設定 text-align: center
    同一行內的多個 inline-block 可以一起置中

  3. 使用絕對定位時-margin: auto 加上左右定位
    用 position: absolute 做絕對定位,固定寬度,再加上 margin: auto,再設定左右位置兩個數值- left: 0; right: 0;




文字對齊

text-align: center 可以置中文字
text-align: justify 可左右分散對齊一整段文字

只要將上層元素設定為 display: table ,並在文字區塊元素上設定 display: table-cell; vertical-align: center; 即可將多行文字垂直置中。

<div id="content">
    <div id="vertical-text">
        大量的文字。
    </div>
</div>
#content > #vertical-text {
    display: table-cell;
    vertical-align: middle;
}

#content {
    border: 1px dotted gray;
    display: table;
    height: 240px;
    width: 100%;
}

排版避免設定高度及絕對位置,寬度設定儘量集中

利用 media query 修改寬度的 css class,不設定高度可讓區塊的內容自動將元件高度撐開。

當螢幕寬度小於 540px 時,文字區塊的寬度就會縮成 400px

.container {
    width: 500px;
}

@media only screen and (max-width: 540px) {
    .container {
        width: 400px;
    }
}

@media only screen and (max-width: 440px) {
    .container {
        width: 300px;
    }
}

隱藏暫不需要的選單、點擊後展開隱藏的頁籤

寬度拉到小於 400px 時,原本左邊的綠色選單就不見了,縮成一個綠色選單鈕,滑鼠放上去時選單就會自動展開




展開頁籤的內容

因為手機上畫面空間不足,通常不適合使用頁籤,這時不如把內容直接展開來,寬度縮到 400px 以下時三個頁籤的內容就會展開來。




使用 Firefox 特有開發工具

Firefox 15.0 以後,按下快速鍵 Ctrl+Shift+M (Windows 及 Linux 平台) 就可以使用適應性設計檢視 (Responsive Design View)。

References

響應式網頁設計 wiki

RWD 流言終結者

2015/3/16

HAProxy and sticky session

在處理 HTTP Load Balance 的時候,很多網路服務都必須考量需不需要使用者登入後才能使用,以往不是 connection oriented 的 HTTP 也因此發展出 HTTP Session Tracking 的技術,在導入 HAProxy 時,也必須考量到,要如何持續支援 Session Tracking 的技術。

HTTP session

以往的網站在處理 session 時,會將 user 相關的資料存放在 client 端的 cookie 裡面,但 cookie 會在每一次 http request 都放在 header 裡面,因此產生了資訊安全的疑慮。

後來就發展了 Server Side 的 Session,以 J2EE jsessionid 為例,就是將 user 相關資料存放於 Server 的記憶體中,由 browser client 端 將一個 session id 紀錄起來,藉由這個 session id,來讓每一次同一個 browser 的所有 request 都可認定為同一個 user。

session 在 J2EE Server 可設定其時效,通常是設定為 10-20分鐘,當 server side session 的資料逾時,session 就會被清除,user 也必須重新登入一次,才能再執行接下來的工作。

通常 browser client 都有 enable cookie 的功能,可以將 session id 存放在 cookie 裡面,比較古老的 browser 沒有支援 cookie,或是 user 刻意將 cookie 功能 disabled 了,這時候 J2EE Server 會自動改用 URL Rewrite 的方式,將 jesssionid 存在 url 上面。

sticky session

當 server 要進行 load balance 的時候,通常會導入硬體或軟體的 load balancer,因為 J2EE Server 有存放了 session 資料,我們可選擇將 server 藉由 cluster 的方式,將 session 互相複製,但這種作法會消耗掉過多的 server side 資源,server 之間互相 replicate session 的次數越多,只會讓 dirty data 與 traffic 更頻繁,發生 data 錯誤的機會也會變高。

sticky session 通常是指 load balancer 支援了 session id 的處理能力,可以認出 http request 裡面的 jsessionid 的資料,並將同一個 jsessionid 的多個 request 都導向到同一個後端的 server 上。

load balancer 可選擇使用 J2EE 的 jessionid,或是使用自訂的 SERVERID 作為 sticky session 的判斷基準。

sticky session in HAProxy

HAProxy 要能夠支援 session id ,可以在 backend 的設定中增加 Cookie 的設定值。

設定說明如下:

cookie <name> [ rewrite | insert | prefix ] [ indirect ] [ nocache ] [ postonly ] [ preserve ] [ httponly ] [ secure ] [ domain <domain> ]* [ maxidle <idle> ] [ maxlife <life> ]
  • name
    就是 Cookie 的名稱,會透過 HTTP response 的 "Set-Cookie" header 將 Cookie 設定於 browser client。

  • rewrite | insert | prefix
    rewrite 表示 backend server 會提供此 cookie,但 HAproxy 必須修改此值,並將 server 的 id 填寫進去。因為必須監控所有 response 封包,所以只能在 HTTP close mode 運作。

    insert 表示此 cookie 必須由 HAPRoxy 填寫進去。如果沒有設定 preserve,而 server 也設定了此 cookie,則會先被移除,然後再重新新增進去。通常建議要搭配設定 "nocache" 與 "postonly"

    prefix 表示透過既有的一個 cookie,server id 會填寫到該 cookie 的 prefix 中,當 client 端發送 requst,其夾帶的 cookie value 會有 haproxy 填寫的 prefix,但會被 haproxy 去掉,只保留原本 server 設定的 cookie value,然後再 forward 給 server。

  • indirect
    server 設定的 cookie 都不會送到 client,這可讓 server 完全不知道 haproxy 的 sticky session 機制。

    建議不要同時使用 prefix 與 indirect

  • nocache
    搭配 insert 使用,可讓 cachable respone 貼上 non-cacheable 選項。

  • postonly
    確保 cookie insertion 只會作用在 responses to POST requests,這是 nocache 的替代方案,因為 POST response 都是 not cachable 的,就可以確保 persistence cookie 永遠不會被 cache。

  • preserve
    只能搭配 insert 與 indirect 使用。在這樣的狀況下,haproxy 會保留不更動 response cookie。

    可用在 logout request 之後,要 end persistence的地方,通常 server 會將 cookie 設定為空值。

  • httponly
    當 haproxy 增加新 cookie 時,同時設定了 HttpOnly 的屬性。

  • secure
    當 haproxy 增加新 cookie 時,同時設定了 Secure 的屬性。

  • domain
    設定 cookie 的 domain

  • maxidle
    idle 時間逾時後,cookie 就會沒有作用,只能用在 insert-mode cookies

  • maxlife
    life time 逾時後,cookie 就會沒有作用,只能用在 insert-mode cookies

範例:

cookie JSESSIONID prefix
cookie SRV insert indirect nocache
cookie SRV insert postonly indirect
cookie SRV insert indirect nocache maxidle 30m maxlife 8h

實際上比較完整的範例為

backend web_server_cluster
    mode http
    balance roundrobin
    cookie SERVERID insert indirect nocache
    # Web Server Cluster
    server web1 192.168.1.10:80 check cookie s1
    server web2 192.168.1.20:80 check cookie s2

直接在 JSESSIONID 上加上 server id 的 prefix

frontend ft_web
  bind 0.0.0.0:80
  default_backend bk_web

backend bk_web
  balance roundrobin
  cookie JSESSIONID prefix indirect nocache
  server s1 192.168.1.10:80 check cookie s1
  server s2 192.168.1.20:80 check cookie s2

appsession

設定說明

appsession <cookie> len <length> timeout <holdtime> [request-learn] [prefix] [mode <path-parameters|query-string>]
  • cookie
    application 使用的 cookie name,可讓 haproxy 了解到該 application 處理 session 的 cookie。

  • length
    該 cookie value 儲存的最大字元長度。

  • holdtime
    該 cookie 值不被使用時,保留在 haproxy 記憶體中的時間

  • request-learn
    設定後,haproxy 會由 request 中發現新的 session

  • prefix
    設定 haproxy 將會認定的 cookie prefix

    範例

      appsession ASPSESSIONID len 64 timeout 3h prefix
    

    如果 cookie 是 ASPSESSIONIDXXXX=XXXXX,appsession 的 value 將會是 XXXX=XXXXX

  • mode
    修改 URL parser mode,目前有兩種

    • path-parameters
      這是預設模式,在 path parameter 的裡面尋找 appsession。適用於 JSESSIONID,通常會是在分號的後面。

      當 browser client 不支援 cookie 時,jsessionid 就會放在 url 上面

        a.jsp;jsessionid=5AC6256DD8D4D5D1FDF5D41E9F2FD900?param1=value1&param2=value2
      
    • query-string
      在 query string 裡面尋找 appsession

範例

# Load balancing PHP sessions:

appsession PHPSESSID len 64 timeout 3h request-learn prefix

# Load balancing ASP.Net sessions:

appsession ASP.NET_SessionId len 64 timeout 3h request-learn prefix

# Load balancing ASP sessions:

appsession ASPSESSIONID len 64 timeout 3h request-learn prefix

# Load balancing Java Server sessions:

appsession JSESSIONID len 52 timeout 3h request-learn prefix

比較完整的範例:

global
    maxconn 10000
    daemon

defaults
    log      global
    mode      http
    contimeout      5000
    clitimeout      50000
    srvtimeout      50000

listen webfarm 0.0.0.0:80
    mode http
    balance roundrobin
    option httpchk HEAD /index.html HTTP/1.0
    appsession JSESSIONID len 128 timeout 1m
    server serverA 192.168.1.10:80 check
    server serverB 192.168.1.20:80 check

結語

綜合兩個設定方式的說明,如果認定 browser 一定會支援 cookie,就可以利用 cookie 設定來處理 session id,但如果要同時支援 cookie 以及 url 形式的 session tracking ,就要使用 appsession。

網路上找到的實際範例,多數都是使用 cookie,而不是 appsession。目前除非使用者刻意 disable browser cookie 的機制,否則要遇到不支援 cookie 的 browser 也很困難了。

如果要用 haproxy 專用的 server id 機制,當然就選擇利用 cookie,設定自訂的 cookie name。但如果是要沿用標準的 ASPSESSIONID/PHPSESSID/JSESSIONID,就直接用 appsession 來設定就可以了。

References

富人用 L4 Switch,窮人用 Linux HAProxy!

Configuring haproxy to load balance multiple engine.io servers

Trick My Proxy: Front Apache Tomcat with HAProxy instead of Apache

Sticky Session Load Balancing with HAProxy

load balancing, affinity, persistence, sticky sessions: what you need to know

Haproxy 安裝手記,版本升級並追加 log 機制

HAProxy configuration

2015/3/9

HAProxy

Server提供網路服務時,面臨到一台 Server 的 capacity 瓶頸,這時候,就可以用 L4 Load Balancer 或是 HAProxy 來進行網路分流與 load balance。

HAProxy 目前主要支援兩種 traffic,http 以及一般的 tcp,http traffic 是最主要支援的部份,HAProxy 可深入到 HTTP Traffic 的封包內容,並根據設定的條件修改封包資料。

安裝

我們可以選擇直接用 yum 安裝,目前的版本為 haproxy-1.5.2-2.el6.i686

yum install haproxy

安裝完成後,執行檔會放在 /usr/sbin 目錄中。

也可以選擇直接從 source code 編譯。

tar -zxvf haproxy-1.5.11.tar.gz
cd haproxy-1.5.11

make clean

#  linux 核心為 2.6.x 可以用使用 TARGET=linux26 參數進行最佳化,如果真的不知道可以用 TARGET=generic 即可
make TARGET=linux26 USE_OPENSSL=1 ADDLIB=-lz
make install

安裝完成後,執行檔會裝在 /usr/local/sbin 目錄中。

自行編譯後,還需要製作 service 啟動檔,這也是從 yum 安裝的 haproxy 抄來的,改一下執行檔的路徑就可以用了。

vi /etc/init.d/haproxy

#!/bin/sh
#
# haproxy
#
# chkconfig:   - 85 15
# description:  HAProxy is a free, very fast and reliable solution \
#               offering high availability, load balancing, and \
#               proxying for TCP and  HTTP-based applications
# processname: haproxy
# config:      /etc/haproxy/haproxy.cfg
# pidfile:     /var/run/haproxy.pid

# Source function library.
. /etc/rc.d/init.d/functions

# Source networking configuration.
. /etc/sysconfig/network

# Check that networking is up.
[ "$NETWORKING" = "no" ] && exit 0

exec="/usr/local/sbin/haproxy"
prog=$(basename $exec)

[ -e /etc/sysconfig/$prog ] && . /etc/sysconfig/$prog

lockfile=/var/lock/subsys/haproxy

check() {
    $exec -c -V -f /etc/$prog/$prog.cfg
}

start() {
    $exec -c -q -f /etc/$prog/$prog.cfg
    if [ $? -ne 0 ]; then
        echo "Errors in configuration file, check with $prog check."
        return 1
    fi

    echo -n $"Starting $prog: "
    # start it up here, usually something like "daemon $exec"
    daemon $exec -D -f /etc/$prog/$prog.cfg -p /var/run/$prog.pid
    retval=$?
    echo
    [ $retval -eq 0 ] && touch $lockfile
    return $retval
}

stop() {
    echo -n $"Stopping $prog: "
    # stop it here, often "killproc $prog"
    killproc $prog 
    retval=$?
    echo
    [ $retval -eq 0 ] && rm -f $lockfile
    return $retval
}

restart() {
    $exec -c -q -f /etc/$prog/$prog.cfg
    if [ $? -ne 0 ]; then
        echo "Errors in configuration file, check with $prog check."
        return 1
    fi
    stop
    start
}

reload() {
    $exec -c -q -f /etc/$prog/$prog.cfg
    if [ $? -ne 0 ]; then
        echo "Errors in configuration file, check with $prog check."
        return 1
    fi
    echo -n $"Reloading $prog: "
    $exec -D -f /etc/$prog/$prog.cfg -p /var/run/$prog.pid -sf $(cat /var/run/$prog.pid)
    retval=$?
    echo
    return $retval
}

force_reload() {
    restart
}

fdr_status() {
    status $prog
}

case "$1" in
    start|stop|restart|reload)
        $1
        ;;
    force-reload)
        force_reload
        ;;
    check)
        check
        ;;
    status)
        fdr_status
        ;;
    condrestart|try-restart)
      [ ! -f $lockfile ] || restart
    ;;
    *)
        echo $"Usage: $0 {start|stop|status|restart|try-restart|reload|force-reload}"
        exit 2
esac

設定

設定檔會放在 /etc/haproxy/haproxy.cfg,設定內容有兩大部份,global 以及 proxy,而 proxy 中,有四個主要項目:defaults, listen, frontend 以及 backend 的部份。

global

範例

global
    log         127.0.0.1 local2

    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     4000
    user        haproxy
    group       haproxy
    daemon
    nbproc        1

    # turn on stats unix socket
    stats socket /var/lib/haproxy/stats

此區塊可使用的設定項目

  • chroot
  • daemon
    以 daemon 的方式運行
  • nbproc
    daemon mode 才能使用,設定 process 的數量
  • user/uid
  • group/gid
  • log
  • pidfile
  • stats

  • maxconn
    可接受最大的連線數量

  • noepoll
    不使用 linux 的 epoll
  • nokqueue
    不使用 BSD 的 kqueue polling
  • debug
    將所有訊息顯示到 console
  • quiet
    不顯示任何訊息

獨立的 haproxy logfile

我們必須調整一些 rsyslog 的設定,才能將 haproxy 的 log 放到獨立的 logfile 裡面。

vi /etc/rsyslog.conf

# 把這兩行的註解刪除
# Provides UDP syslog reception
$ModLoad imudp
$UDPServerRun 514

# 限制 rsyslog UDP 只會用在 127.0.0.1
$UDPServerAddress 127.0.0.1

新增一個檔案 /etc/rsyslog.d/haproxy.conf

vi /etc/rsyslog.d/haproxy.conf

local2.*    /var/log/haproxy/haproxy.log

也可以用這個方式,將 log file 區分不同的 log level,要不然就放在同一個 log file 裡面就好了。

local2.=info     /var/log/haproxy-info.log
local2.notice    /var/log/haproxy-allbutinfo.log

重新啟動系統 log 記錄器

service rsyslog restart

編輯 haproxy.logrotate,讓 haproxy.log 檔案每天產生一個,然後保留 120 天的資料。

vi /etc/logrotate.d/haproxy.logrotate

/var/log/haproxy/haproxy.log {
    missingok
    notifempty
    sharedscripts
    rotate 120
    daily
    compress
    postrotate
        reload rsyslog >/dev/null 2>&1 || true
    endscript
}

proxy

proxy 設定是由 defaults, listen, frontend, backend 組成。

  • defaults name
    設定所有其他區塊的預設參數,如果設定檔中,後面又多了一塊 defaults 設定,後面的設定會覆蓋掉前面的設定。

  • frontend name
    描述接受 client 端 request 的 listening sockets。

  • backend name
    描述 proxy 會將 frontend 接收的 request 連線 forward 到後端的一組 servers。

  • listen name
    定義完整的 proxy,包含了 frontend 與 backend,這個部份的定義,通常用在設定一般的 TCP-only traffic。

proxy 定義的名稱 name ,必須要使用這些字元 upper and lower case letters, digits,
'-' (dash), '_' (underscore) , '.' (dot) and ':' (colon)。

HTTP traffic

HAProxy 針對 http traffic 提供了五種 mode

  • KAL: keep alive ("option http-keep-alive")
    這是預設值,所有處理中的 request, response 以及 connection 都會持續 idle 並保持連線。

  • TUN: tunnel ("option http-tunnel")
    只會處理第一個 request 與 response,其他封包都會直接 forward,不進行任何分析與處理,目前建議不使用這個模式,因為會對 logging 以及 HTTP processing 造成問題。

  • PCL: passive close ("option httpclose")
    就跟 tunnel mode 一樣,在處理完第一個封包後,會雙向加上 "conneciton: close",讓連線結束。

  • SCL: server close ("option http-server-close")
    在收到最後的 response 時,就會 close server-facing connection,但仍然持續保留 client-facing connection。

  • FCL: forced close ("option forceclose")
    在收到最後的 response 之後,就會關閉連線。

在一個 http connection 經過 frontend 與 backend 設定之後,可能會有兩段 proxy mode 的設定,最終這個 connection 的 mode 是由下表的規則決定,keep-alive 是最弱的設定值,而 force close 是最強的設定值。KAL < TUN < PCL < SCL < FCL

                          Backend mode

                | KAL | TUN | PCL | SCL | FCL
            ----+-----+-----+-----+-----+----
            KAL | KAL | TUN | PCL | SCL | FCL
            ----+-----+-----+-----+-----+----
            TUN | TUN | TUN | PCL | SCL | FCL
 Frontend   ----+-----+-----+-----+-----+----
   mode     PCL | PCL | PCL | PCL | FCL | FCL
            ----+-----+-----+-----+-----+----
            SCL | SCL | SCL | FCL | SCL | FCL
            ----+-----+-----+-----+-----+----
            FCL | FCL | FCL | FCL | FCL | FCL
defaults

這個區塊的參數,是設定檔後面所有參數的預設設定值,下面的設定值會蓋掉上面的。

範例

defaults
    mode                    http
    log                     global
    option                  httplog
    option                  dontlognull
    option http-server-close
    option forwardfor       except 127.0.0.0/8
    option                  redispatch
    retries                 3
    timeout http-request    10s
    timeout queue           1m
    timeout connect         10s
    timeout client          1m
    timeout server          1m
    timeout http-keep-alive 10s
    timeout check           10s
    maxconn                 3000

此區塊可使用的設定項目

  • balance
    選擇使用那一種 algorithm 來執行 load balance。
  • mode { tcp|http|health } (defaults 為 http)
  • log global
  • option httplog
    Enable logging of HTTP request, session state and timers
  • option http-server-close
    Enable or disable HTTP connection closing on the server side
  • option forwardfor except 127.0.0.0/8
    因為 X-Forwarded-For header 裡面會有 client 的 IP,HAProxy 會利用此 header,所以增加此設定,可讓 X-Forwarded-For header 都轉送到 後端的 server
  • option redispatch
    當 client 連到掛掉的機器時, 可重新再分配
  • retries 3
    失敗重試次數
  • timeout http-request 10s
    Set the maximum allowed time to wait for a complete HTTP request,等待 HTTP Request 完成的 timeout 時間
  • timeout queue 1m
    Set the maximum time to wait in the queue for a connection slot to be free,等待 queue 釋放空間的 timeout 時間
  • timeout connect 10s
    Set the maximum time to wait for a connection attempt to a server to succeed.
  • timeout client 1m
    Set the maximum inactivity time on the client side. 當有 long polling 時,這個參數在 frontend 要調長到 long polling 最大的等待時間
  • timeout server 1m
    Set the maximum inactivity time on the server side. 當有 long polling 時,這個參數在 backend 要調長到 long polling 最大的等待時間
  • timeout http-keep-alive 10s
    Set the maximum allowed time to wait for a new HTTP request to appear
  • timeout check 10s
    Set additional check timeout, but only after a connection has been already established.
  • maxconn 3000
    連線到某個 server 的最大連線數
backend balance mode

這是 balance 設定值可選填的 algorithm 項目。

  • roundrobin
    每個 server 依據權值 weight 依序選擇 servers,這是最快的 algorithm,server weight 會根據 instance slow start 的狀況而隨時變動。這個 algorithm 在每個 backend 最多可處理 4095 個 active server nodes。如果有個 server node 在 down 之後馬上又變成 active,可能會需要數百個 requests 之後,才會再 join 這個 server farm。

  • static-rr
    每個 server 根據 weight 依序選擇 server,類似 roundrobin,但權值 weight 是固定的。沒有 active server node 的處理數量上限。

  • leastconn
    目前處理最少連線數量的 server,作為下一個 connection 轉向的 server。建議在較長 tcp session 的服務中使用這個演算法,例如 LDAP, SQL, etc...,不適合用在類似 HTTP 這種短 sesion time 的 protocol。

  • first
    server nodes 之中,選擇第一個可使用的 server,當這個 server 連線處理數量達到 maxconn 時,就會選擇下一個 server,所以設定檔中一定要有 maxconn 設定值。

  • source
    source IP address 會 hashed 並除以 total weight of running serer,用這個結果來決定要選擇那一個 server,這可確保同一個 client IP 的 traffic 會連到同一個 server。如果 server nodes 數量改變了,就會因為重新計算 hash,造成 clients 連線到不同的 servers。

  • uri
    可用 uri 的 ? 前面的部份,或是整個 uri 進行 hash,可確保同一個 uri 會轉給同一個 server。這個設定通常用在 proxy caches 與 anti-virus proxyies,用以提高 cache hit rate。

    有兩個 optinal parameter: len 與 depth。len 就是使用 uri 裡面幾個字元,但因為 uri 通常是 / 開頭,所以不要將 len 設定為 1。

    depth 表示計算 hash 的 maxmimum directory depth,出現一個 / 代表多了一層 depth。

  • url_param
    使用 HTTP Get request URL 裡面的參數,如果用到了 "check_post",就會使用 POST request 裡面的參數。

  • hdr(name)
    會尋找名稱為 name 的 HTTP header 欄位。

    optonal 參數 "use_domain_only" 可用在指定 header 為 Host 的時候,如果 Host 為 "haproxy.1wt.eu" 就只會考慮 "1wt"

  • rdp-cookie, rdp-cookie(name)
    尋找設定的 cookie,通常就是 session ID,如果沒有 session ID 就會使用 roundrobin algorithm。

一些設定的參考範例

範例 1:雖然這個是 listen 區塊,但實際上是設定一個網頁 service,提供一個 HAProxy 網頁監控界面。

# HAProxy 網頁管理界面
listen stats *:2000
    mode http
    stats enable
    stats hide-version
    stats realm Haproxy\ Statistics
    stats uri /
    # 帳號密碼
    stats auth username:password
    stats refresh 10s

範例 2:

defaults
    log     global
    mode    http
    option  httplog
    option  dontlognull
    timeout connect 5000
    timeout client  50000
    timeout server  50000
    errorfile 400 /etc/haproxy/errors/400.http
    errorfile 403 /etc/haproxy/errors/403.http
    errorfile 408 /etc/haproxy/errors/408.http
    errorfile 500 /etc/haproxy/errors/500.http
     errorfile 502 /etc/haproxy/errors/502.http
    errorfile 503 /etc/haproxy/errors/503.http
    errorfile 504 /etc/haproxy/errors/504.http

frontend localnodes
    bind *:80
    mode http
    default_backend nodes

backend nodes
    mode http
    balance roundrobin
    option forwardfor
    http-request set-header X-Forwarded-Port %[dst_port]
    http-request add-header X-Forwarded-Proto https if { ssl_fc }
    option httpchk HEAD / HTTP/1.1\r\nHost:localhost
    server web01 127.0.0.1:9000 check
    server web02 127.0.0.1:9001 check
    server web03 127.0.0.1:9002 check

範例 3:

defaults
    log    global
    mode    http
    option    httplog
    option    dontlognull
    retries    3
    option redispatch
    option httpclose
    maxconn    32768
    contimeout    5000
    clitimeout    50000
    srvtimeout    50000

listen www-balancer
    bind 0.0.0.0:80
    balance         roundrobin
    server          w1 192.168.1.10:80 weight 3 check
    server          w2 192.168.1.20:80 weight 3 check
    option          httpchk GET /robots.txt
    option          forwardfor

範例 4: DB1 與 DB2 設定為 Master-Master Replication

defaults
    mode tcp
    option redispatch   #連前當前配置失敗時,允許重新配發到他台機器
    option abortonclose
    timeout connect 5000ms
    timeout client 30000ms
    timeout server 30000ms
    log 127.0.0.1 local0
    balance roundrobin   #使用輪替演算法,分散主機流量

listen proxy
    bind *:3306  #綁定192.168.11.180的3306 Port為運行主機端口
    mode tcp
    server db1 192.168.1.10:3306  weight 1 check  

    #後端主機.178,179,權重值配置

    server db2 192.168.1.20:3306  weight 2 check  

    #check 參數檢查改主機是否Live

範例 5:

defaults
    timeout server 86400000
    timeout connect 86400000
    timeout client 86400000
    timeout queue   1000s

# [HTTP Site Configuration]
listen  http_web 192.168.10.10:80
    mode http
    balance roundrobin  # Load Balancing algorithm
    option httpchk
    option forwardfor
    server server1 192.168.1.10:80 weight 1 maxconn 512 check
    server server2 192.168.1.20:80 weight 1 maxconn 512 check

# [HTTPS Site Configuration]
listen  https_web 192.168.10.10:443
    mode tcp
    balance source# Load Balancing algorithm
    reqadd X-Forwarded-Proto:\ http
    server server1 192.168.1.10:443 weight 1 maxconn 512 check
    server server2 192.168.1.20:443 weight 1 maxconn 512 check

References

富人用 L4 Switch,窮人用 Linux HAProxy!

使用 HAProxy 完成 網站分流, 平衡負載

[FreeBSD & Linux]網站分流:簡易架設 HAProxy 伺服器

使用 Haproxy 搭建高可用與負載平衡叢集