nginx動態代理方案
當我們面對一個技術問題毫無頭緒時,技術方案的不同選擇,即將付出的技術代價也將差別很大,有時不妨從源碼入手,嘗一次破案癮的感覺。
0、需求:動態調整轉發策略
數據庫存放著大量的用戶數據,需要制定一個策略,負載均衡服務器可以根據用戶信息,動態轉發請求。
比如A用戶(001)的請求轉發到A服務器(192.168.1.101),B用戶(002)的請求轉發到B服務器(192.168.1.102),C用戶(003)的請求轉發到A服務器(192.168.1.103),等等。
1、服務器上下文
前端nginx服務器 + N臺后端應用服務器。準備用單臺服務器模擬。
前端:192.168.1.101:80
后端:192.168.1.101:81
2、技術頭腦風暴
程序員們腦袋里開始有好幾個方案了,有的是直覺,有的是經驗,如下:
a、寫nginx模塊,模塊里實現讀數據庫或nosql,根據數據值做轉發。
b、找現成的模塊,看能不能直接改根據或腳本就可以解決。據說ngx_lua很強大,可以考慮。
c、服務器必須保證不能有任何阻塞,模塊實現時得用nginx的subrequest機制。
d、blalala...
3、程序員的腦袋里裝的什么呢? 簡單
任務1:web程序員A,寫個http api接口,返回具體的服務器信息(ip+port)。
任務2:系統工程師B,做個nginx配置,根據A的api讓nginx根據返回信息實現動態轉發。
4、開始實現:
任務1:分分鐘搞定,寫個php腳本唄。
任務2:繼續拆解
任務2.1:實現最簡單轉發
server {
listen 80;
location / {
proxy_pass 192.168.1.101:81;
}
}
server {
listen 81;
location / {
root html;
}
} 任務2.2:實現動態轉發
看下源碼proxy_pass能不能使用變量
static char *
ngx_http_proxy_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
...
value = cf->args->elts;
url = &value[1];
n = ngx_http_script_variables_count(url); // 找到這里了,可以使用變量
if (n) {
ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));
sc.cf = cf;
sc.source = url;
...
}
...
} 改下配置,測試ok,繼續往下走 server {
listen 80;
location / {
set $url 192.168.1.101:81;
proxy_pass $url;
}
} 任務2.3:根據api設置變量$url的值
好像沒有現成的模塊,腦袋里過濾了一遍,有的話必須是跟subrequest有關的模塊,想起了 auth_request 模塊,它可以配置一個http請求,根據http請求的返回結果決定給客戶端是否正常訪問,去看下源碼先。
static ngx_int_t
ngx_http_auth_request_handler(ngx_http_request_t *r)
{
...
if (ctx != NULL) {
...
if (ngx_http_auth_request_set_variables(r, arcf, ctx) != NGX_OK) { // 看函數名感覺這里有干貨
return NGX_ERROR;
}
/* return appropriate status */
if (ctx->status == NGX_HTTP_FORBIDDEN) { // 403
return ctx->status;
}
// 200 and ...
if (ctx->status >= NGX_HTTP_OK
&& ctx->status < NGX_HTTP_SPECIAL_RESPONSE)
{
return NGX_OK;
}
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"auth request unexpected status: %d", ctx->status);
return NGX_HTTP_INTERNAL_SERVER_ERROR;
} ...
return NGX_AGAIN;
} 從源碼我們掌握兩個信息:1、api返回狀態碼要為200 2、ngx_http_auth_request_set_variables 可能有我們要的信息,繼續查看:
static ngx_int_t
ngx_http_auth_request_set_variables(ngx_http_request_t *r,
ngx_http_auth_request_conf_t *arcf, ngx_http_auth_request_ctx_t *ctx)
{
...
cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
v = cmcf->variables.elts;
av = arcf->vars->elts;
last = av + arcf->vars->nelts;
// 遍歷 arcf->vars 這個東東
while (av < last) {
/*
* explicitly set new value to make sure it will be available after
* internal redirects
*/
vv = &r->variables[av->index];
if (ngx_http_complex_value(ctx->subrequest, &av->value, &val)
!= NGX_OK)
{
return NGX_ERROR;
}
vv->valid = 1;
vv->not_found = 0;
vv->data = val.data;
vv->len = val.len;
if (av->set_handler) {
/*
* set_handler only available in cmcf->variables_keys, so we store
* it explicitly
*/
av->set_handler(r, vv, v[av->index].data); // 設置變量
}
av++;
}
return NGX_OK;
} 代碼邏輯很簡單,遍歷這個模塊的配置的某個成員,肯定是跟變量有關的了,找配置信息了 static ngx_command_t ngx_http_auth_request_commands[] = {
{ ngx_string("auth_request"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_auth_request,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
{ ngx_string("auth_request_set"), // 名稱很像設置變量,就是它了
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE2,
ngx_http_auth_request_set, // 看一下這函數實現,印證了猜測是正確的
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
}; 整理一下,就是當接收到api的返回信息后,模塊處理了設置變量。so配置為如下,測試ok,繼續 server {
listen 80;
location / {
auth_request /api.php; #用php模擬
auth_request_set $url 192.168.1.101:81; #保持簡單,先用正確值模擬
proxy_pass $url;
}
location ~ \.php$ { # 為了處理上面的 /api.php
root html;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
include fastcgi_params;
}
} 任務2.4:讓$url的值從api返回信息里獲取 (請不要在2.3里一步搞定整個模擬,保持每步正確和簡單,是不是很像代碼重構的原則)
我們要解決這個:auth_request_set $url 192.168.1.101:81;
nginx有什么變量可以讓我們獲取請求的返回信息呢,頭部信息也可以(其實這里心里已經判斷肯定只能從頭部信息里獲取,以對nginx的代碼熟悉了解程度)。去看下獲取變量值的函數吧。
ngx_http_variable_value_t *
ngx_http_get_variable(ngx_http_request_t *r, ngx_str_t *name, ngx_uint_t key)
{
...
v = ngx_hash_find(&cmcf->variables_hash, key, name->data, name->len);
if (v) {
if (v->flags & NGX_HTTP_VAR_INDEXED) {
return ngx_http_get_flushed_variable(r, v->index);
} else {
vv = ngx_palloc(r->pool, sizeof(ngx_http_variable_value_t));
if (vv && v->get_handler(r, vv, v->data) == NGX_OK) {
return vv;
}
return NULL;
}
}
vv = ngx_palloc(r->pool, sizeof(ngx_http_variable_value_t));
if (vv == NULL) {
return NULL;
}
// 我清楚nginx可以通過 http_xxx 獲取請求的頭部信息 xxx: ...
if (ngx_strncmp(name->data, "http_", 5) == 0) {
if (ngx_http_variable_unknown_header_in(r, vv, (uintptr_t) name)
== NGX_OK)
{
return vv;
}
return NULL;
}
// 這個沒看過,不知道哪個版本加上的,感覺很意外,看名字應該就是我要找的
if (ngx_strncmp(name->data, "sent_http_", 10) == 0) {
// 這個函數怎么實現的,看了下類似上面的,上面用于獲取header_in即請求,
它用于獲取header_out,就是我們要的響應信息
if (ngx_http_variable_unknown_header_out(r, vv, (uintptr_t) name)
== NGX_OK)
{
return vv;
}
return NULL;
}
...
vv->not_found = 1;
return vv;
} 到這里心里已經很有數了,寫php代碼,并且直接訪問測試正常 api.php
<?php
$url = "192.168.1.101:81";
header("url: $url"); 改nginx配置: server {
listen 80;
location / {
auth_request /api.php; #用php模擬
auth_request_set $url $sent_http_url; #保持簡單,先用正確值模擬
proxy_pass $url;
}
location ~ \.php$ { # 為了處理上面的 /api.php
root html;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
include fastcgi_params;
}
} 搞定,心情特好 ,預期半個小時驗證方案,提前5分鐘完成。
5、整個方案整理:
a、> ./configure --with-http_auth_request_module && make && ./objs/nginx
b、nginx.conf
server {
listen 80;
location / {
auth_request /api.php; #用php模擬
auth_request_set $url $sent_http_url; #保持簡單,先用正確值模擬
proxy_pass $url;
}
location ~ \.php$ { # 為了處理上面的 /api.php
root html;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
include fastcgi_params;
}
}
server {
listen 81;
location / {
root html;
}
} c、api.php <?php
$url = "192.168.1.101:81";
header("url: $url");
6、小結
很開心的一次歷程,不喜請輕噴 -_-
來自:http://my.oschina.net/fqing/blog/347365