nginScript 系列:使用 nginScript 將客戶端重定向到新服務器

nginScript 的一個關鍵優勢在於它提供了讀取和設置 NGINX 配置變量的能力。變量可以用於自定義路由規則。也就是說,我們可以使用 JavaScript 來實現複雜的功能,這些功能可以直接對請求的處理產生影響。

將客戶端重定向到新的應用服務器

在這篇文章裡,我們將介紹如何使用 nginScript 來實現優雅的服務器間切換。我們不打算進行“一次性”的切換,而是定義了一個時間窗口,客戶端在事件窗口內循序漸進地切換到新的服務器。我們可以逐漸地自動給新服務器增加流量。

我們定義了一個兩個小時的時間窗口,我們希望切換就在這兩個小時內完成,也就是下午 5 點到 7 點。我們預期在第一個 12 分鐘內,有 10% 的客戶端被重定向到新的服務器,24 分鐘之後有 20%,並以此類推。下圖展示了切換過程。

(點擊放大圖像)

nginScript 系列:使用 nginScript 將客戶端重定向到新服務器

在兩個小時內將客戶端從舊的服務器重定向到新的服務器

這種“漸進式切換”要求已經切換到新服務器的客戶端不能又回到舊的服務器,也就是說,一旦一個客戶端被重定向到新的服務器,那麼從今以後它就一直被定向到那裡。

我們會在稍後描述完整的配置,不過簡單地說,NGINX 和 NGINX Plus 在處理已經被切換過來的請求時,會遵循如下規則。

  • 如果切換時間窗口還沒有開始,那麼請求就被重定向到舊的服務器。

  • 如果切換時間窗口已經結束,那麼請求就被重定向到新的服務器。

  • 如果切換在進行當中,那麼:

  1. 計算當前時間在切換時間窗口中的位置。

  2. 計算客戶端 IP 地址的散列值。

  3. 計算散列值在所有散列值中的位置。

  4. 如果散列值的位置比切換時間窗口的當前位置要大,那麼請求就被重定向到新的服務器,否則重定向到舊的服務器。

為 HTTP 應用配置 NGINX 和 NGINX Plus

在這個例子裡,我們將使用 NGINX 和 NGINX Plus 作為一個 Web 應用服務器的反向代理,所以所有的配置都是關於 HTTP 的。

首先,我們分別為舊應用程序和新應用程序所在的服務器定義單獨的 upstream 配置塊。雖然切換過程是漸進式的,NGINX 和 NGINX Plus 在切換期間會一直充當負載均衡器的角色。

upstream old {
 server 10.0.0.1;
 server 10.0.0.2;
}

upstream new {
 server 10.0.0.9;
 server 10.0.0.10;
}

接下來,我們定義前端的服務,NGINX 和 NGINX Plus 通過它們將呈現內容發送給客戶端。

js_include /etc/nginx/progressive_transition.js;
js_set $upstream transitionStatus; # 基於時間窗口位置返回"old|new"

server {
 listen 80;

 location / {
 set $transition_window_start "Wed, 31 Aug 2016 17:00:00 +0100";
 set $transition_window_end "Wed, 31 Aug 2016 19:00:00 +0100";

 proxy_pass http://$upstream;
 error_log /var/log/nginx/transition.log info; # 啟用 nginScript 日誌 
 }
}

我們使用 nginScript 來決定應該使用哪個 upstream 組,所以我們需要指定 nginScript 代碼的位置。在 NGINX Plus R11 及其後的版本里,所有的 nginScript 代碼必須被放置在單獨的文件裡,然後通過 js_include 指令來指定它們的位置。

js_set 指令用於設置 $upstream 變量。要注意,這個指令並不是要讓 NGINX 或 NGINX Plus 去調用 nginScript 函數 transitionStatus。NGINX 變量是按需進行計算的,也就是在處理請求期間用到變量時才會進行計算。所以,js_set 指令是要告訴 NGINX 或 NGINX Plus 在必要的時候如何計算 $upstream 變量。

server 代碼塊定義 NGINX 和 NGINX Plus 如何處理 HTTP 請求。listen 指令告訴 NGINX 和 NGINX Plus 對 80 端口(默認 HTTP 端口)進行監聽,不過生產環境一般配置成 SSL/TLS 來保護傳輸中的數據。

location 代碼塊的作用域包括了整個應用空間(/)。在這個代碼塊裡,我們使用了 set 指令和兩個新的變量 $transition_window_start 和 $transition_window_end 來定義切換時間窗口。時間可以被聲明成RFC 2822 格式(例子裡所示)或ISO 8601 格式(包含毫秒)。兩種格式必須包含它們各自的本地時區標識符。因為 JavaScript 的 Date.now 函數總是返回 UTC 日期和時間,所以只有提供本地時區才能進行準確的時間比較。

proxy_pass 指令將請求重定向到 upstream 組,transitionStatus 函數會對它進行計算。

最後,error_log 指令啟用了 nginScript 事件日誌,級別為 info 及以上(默認情況下,只有 warn 及以上級別的事件會被記錄下來)。將這個指令放在 location 代碼塊裡,並指定單獨的日誌文件,這樣就可以避免將主要的錯誤日誌與其他 info 日誌消息混雜在一起。

HTTP 應用的 nginScript 代碼

我們假設你已經啟用了 nginScript 模塊。

我們將 nginScript 代碼放在
/etc/nginx/progressive_transition.js
文件裡,正如 js_include 指令所指定的那樣。所有的函數都包含在這個文件裡。

被調用的函數必須在函數調用者之前出現,於是我們定義了一個函數用於返回客戶端 IP 地址的散列值。如果應用服務器的主要用戶處於相同的局域網內,那麼我們的客戶端就會有相似的 IP 地址,所以我們的散列函數需要為小區間的輸入值返回平均分佈的散列值。

在這個例子裡,我們使用了FNV-1a 散列算法,這個算法短小精悍,很快,而且具有良好的平均分佈能力。它的另一個優勢在於,它返回的是一個 32 位的正整數,這樣可以很方便地計算每一個客戶端 IP 地址在輸出區間的位置。下面的代碼是 FNV-1a 算法的 JavaScript 實現。

function fnv32a(str) {
 var hval = 2166136261;
 for (var i = 0; i < str.length; ++i ) {
 hval ^= str.charCodeAt(i);
 hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
 }
 return hval >>> 0;
}

接下來,我們定義 transitionStatus 函數,這個函數將 js_set 指令裡的 $upstream 變量設置到 NGINX 配置裡。

function transitionStatus(req) {
 var vars, window_start, window_end, time_now, timepos, numhash, hashpos;

 // 從 NGINX 配置裡獲取切換時間窗口 
 vars = req.variables;
 window_start = new Date(vars.transition_window_start);
 window_end = new Date(vars.transition_window_end);

 // 是否處於切換時間窗口內?
 time_now = Date.now();
 if ( time_now < window_start ) {
 return "old";
 } else if ( time_now > window_end ) {
 return "new";
 } else { // 處於切換時間窗口內 
 // 計算切換時間窗口內的位置 (0-1)
 timepos = (time_now - window_start) / (window_end - window_start);

 // 獲取客戶端 IP 地址的散列值 
 numhash = fnv32a(req.remoteAddress);

 // 計算散列值在輸出區間裡的位置 (0-1)
 hashpos = numhash / 4294967295; // Upper bound is 32 bits
 req.log("timepos = " + timepos + ", hashpos = " + hashpos); //error_log [info]

 // 需要切換這個客戶端嗎?
 if ( timepos > hashpos ) {
 return "new";
 } else {
 return "old";
 }
 }
}

transitionStatus 函數只有一個參數 req,這個參數代表的是一個 HTTP request 對象。request 對象的 variables 屬性包含了 NGINX 的所有配置變量,包括用於設置切換時間窗口的 $transition_window_start 和 $transition_window_end。

外面的 if/else 代碼塊檢查切換時間窗口是否啟動、結束或者正在進行當中。如果在進行當中,我們通過向 fnv32a 函數傳遞 req.remoteAddress 來獲取客戶端 IP 地址的散列值。

然後我們計算散列值在區間中的位置。因為 FNV-1a 算法返回的是一個 32 位的正整數,我們可以直接將散列值除以 4,294,967,295(32 位整數的十進制表示)。

這個時候,我們調用 req.log() 來記錄散列位置和切換時間窗口的當前位置。我們使用 info 級別將這些信息記錄到之前在 NGINX 和 NGINX Plus 裡配置的 error_log 文件裡,並生成如下所示的日誌條目。其中 js: 前綴表示從 JavaScript 代碼中獲得的日誌條目。

2016/09/08 17:44:48 [info] 41325#41325: *84 js: timepos = 0.373333, hashpos = 0.840858

最後,我們比較散列值在輸出區間中的位置和切換時間窗口的當前位置,並返回相應的 upstream 組的名字。

總結

在這篇文章裡,我們介紹瞭如何使用 nginScript 複雜的編程式配置循序漸進地切換客戶端到新的服務器。通過部署自定義邏輯來實現可控地選擇合適的上游組,這只是 nginScript 提供的眾多解決方案裡一個特性。


分享到:


相關文章: