一 背景
? 最近,開發(fā)部門有一個訪問需求,被訪問方給了我們兩個https的域名訪問接口,這里假設(shè)為:
https://aaa.target.com/my_target/login/
https://bbb.target.com/my_target/login/
? 這兩個域名解析出來的地址和接口信息都是一樣的,但是根據(jù)要求,需要將兩個域名訪問接口作為主備的方式進(jìn)行配置,在https://aaa.target.com/mytarget/login/出現(xiàn)異常不能使用的時候,能夠動態(tài)切換到https://bbb.target.com/mytarget/login/訪問域名接口。
? 那么通過nginx來進(jìn)行代理配置,首先想到的就是使用其負(fù)載均衡均衡的功能(upstream)對兩個域名進(jìn)行主備配置:
upstream mytarget
{
server aaa.target.com:443 max_fails=30 fail_timeout=300s;
server bbb.target.com:443 backup;
}
? 以上配置,通過upstream創(chuàng)建了一個mytarget的訪問池,訪問該池的時候,首先會訪問aaa.target.com:443,當(dāng)訪問30次失敗后,該服務(wù)會停用300s,在300s之后重新嘗試訪問該地址;而當(dāng)aaa.target.com:443訪問失敗到達(dá)30次后,服務(wù)停用,則會啟用bbb.target.com地址用于訪問。
? 在server中,最初的配置如下:
server {
listen 8901;
server_name target.server;
location /login/ {
proxy_pass https://mytarget/my_target/login/;
}
}
? proxy_pass中只需要訪問upstream訪問池即可。
? 但是通過實際情況對該網(wǎng)址進(jìn)行訪問(curl http://localhost:8901/login/),卻返回了406 Not Acceptable錯誤。
? 而當(dāng)我們不使用upstream的方式進(jìn)行請求的時候:
server {
listen 8901;
server_name target.server;
location /login/ {
proxy_pass https://aaa.target.com/my_target/login/;
#proxy_pass https://bbb.target.com/my_target/login/;
}
}
? 請求(curl http://localhost:8901/login/))則可以順利完成,HTTP1.1返回200代碼。
? 經(jīng)過很長時間的分析和測試,最終還是無法解決該問題,故想到了通過“二級跳”的方式變向?qū)崿F(xiàn)(經(jīng)過測試,server里面如果是ip,也可以訪問的通):
upstream target {
server 127.0.0.1:18901;
server 127.0.0.1:18902 backup;
}
server {
listen 8900;
server_name mytarget.server;
location /login/ {
proxy_pass http://target/;
proxy_next_upstream error timeout http_404 http_403;
}
}
server {
listen 18901;
location / {
proxy_pass https://aaa.target.com/my_target/login/;
}
}
server {
listen 18902;
location / {
proxy_pass https://bbb.target.com/my_target/login/;
}
}
? 以上過程也很好理解,即分別對我們需要的https://aaa.target.com/my_target/login/和https://bbb.target.com/my_target/login/地址進(jìn)行代理,通過本機的18901和18902端口提供服務(wù);而upstream再對本機的18901端口和18902端口進(jìn)行負(fù)載均衡(主備配置),然后再通過本機的8900端口代理訪問127.0.0.1的18901和18902端口,最終實現(xiàn)訪問https://aaa.target.com/my_target/login/或https://bbb.target.com/my_target/login/
? 但二級跳的訪問方式也具有一些缺陷,這個缺陷主要反映在我的日志可讀性上,我們的當(dāng)前http訪問的日志格式如下:
log_format main '$remote_addr - $http_referer - $upstream_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" "$http_cookie" "$upstream_connect_time" "$upstream_response_time" "$request_time"';
access_log logs/access.log main;
? 也就是說我們在日志中會顯示出 u p s t r e a m a d d r ,即被訪問端解析出的地址,從而方便我們做一些問題排查依據(jù)和手段,但采取“二級跳”的方式,則導(dǎo)致我的 upstream_addr,即被訪問端解析出的地址,從而方便我們做一些問題排查依據(jù)和手段,但采取“二級跳”的方式,則導(dǎo)致我的 upstreama?ddr,即被訪問端解析出的地址,從而方便我們做一些問題排查依據(jù)和手段,但采取“二級跳”的方式,則導(dǎo)致我的upstrema_addr顯示的均為127.0.0.1:18901/18902,反而給我們的日志觀察造成不便。
因此,最好的方式還是解決直接通過域名做負(fù)載均衡的訪問問題,才能最好的達(dá)到我們的要求。但是看似合理的操作,為什么會產(chǎn)生406的問題?406的問題又該如何解決?
二 分析思路
? 對于406這個問題的分析,我們首先要知道HTTP1.1返回406 Not Acceptable代碼代表什么意思?根據(jù)資料解釋:
http返回406錯誤的時候,往往說明是客戶端錯誤 , 即客戶端無法解析服務(wù)端返回的內(nèi)容,一般是說在客戶端發(fā)送的accept頭里 , 設(shè)置了允許接受的類型 , 但是服務(wù)端沒有按該格式返回,如果accept指定的類型和response返回的content-type類型不一致,會出現(xiàn)406 not acceptable錯誤。
? 而針對該問題的解決方式有兩種:
1.修改服務(wù)端按指定格式返回
2.修改客戶端接受服務(wù)端的格式
? 此時,我們可以通過curl -vvv的方式詳細(xì)顯示訪問請求及返回代碼:
? 返回成功時侯的content-type:
? 返回失敗時候的content-type:
? 此時可以看出,當(dāng)返回406的時候,content-type返回的是text/html格式,而不是正確的application/json格式(其實,該格式是指返回后的內(nèi)容的格式)。
? 那么此時其實可以理解為當(dāng)訪問返回406的時候,我們在客戶端想向?qū)Ψ降姆?wù)器端請求包頭中的content-type為json格式,但是最終服務(wù)器端并沒有找到我們想要的請求內(nèi)容所以反饋了一個406 Not Acceptable的html。
起初,認(rèn)為是由于客戶端向服務(wù)器發(fā)送的包頭請求類型不對,所以導(dǎo)致服務(wù)器返回的content-type也不對,最終產(chǎn)生406錯誤。因此,著重研究了如何讓nginx強制向服務(wù)端請求我們想要的content-type,詳細(xì)配置如下:
upstream mytarget
{
server aaa.target.com:443 max_fails=30 fail_timeout=300s;
server bbb.target.com:443 backup;
}
server {
listen 8901;
server_name mytarget.server;
location /login/ {
type {} default_type application/json;
add_header Content-Type 'application/json; charset=utf-8';
proxy_pass https://mytarget/my_target/login/;
}
? 該配置中,實現(xiàn)強制指定nginx請求content-type的部分主要為:
type {} default_type application/json;
add_header Content-Type 'application/json; charset=utf-8';
? 在nginx中,http層面的配置默認(rèn)的content-type是application/octet-stream,charset=utf-8,所以在location中type{ }表示會先將默認(rèn)的content-type置空,然后通過defalut_type將content-type改為application/json,而add_header Content-Type則表示直接在請求頭中直接指定Content-type為 ‘a(chǎn)pplication/json; charset=utf-8’;(這里還需要注意,想要強制生效,我們必須還要修改http引入的mime.types文件,需要在mime.type文件中加入application/json json;的配置,否則可能不生效,經(jīng)過檢查在我們配置中,之前就已經(jīng)加入了)
? 在這樣設(shè)置后,再次進(jìn)行模擬訪問嘗試(curl -vvv http://localhost:8901/login/), 發(fā)現(xiàn)http返回依然是406,而客戶端返回的content-type還是text/html。最開始我一直認(rèn)為是配置未生效,直到我在請求中加入了一段配置:
server {
listen 8901;
server_name mytarget.server;
location /login/ {
type {} default_type application/json;
add_header Content-Type 'application/json; charset=utf-8';
return 200 '{"status":"success","result":"nginx json"}'
proxy_pass https://mytarget/my_target/login/;
}
? 再次模擬訪問嘗試(curl -vvv http://localhost:8901/login/),發(fā)現(xiàn)nginx返回了200,且返回的內(nèi)容也是我們定義的’{“status”:“success”,“result”:“nginx json”}',而這也說明我們的配置生效了。但是為什么直接訪問地址還是不行呢?
? 通過資料查詢,我知道了,nginx在做代理轉(zhuǎn)發(fā)的時候,會自行將前段請求請求頭進(jìn)行處理,并根據(jù)我們處理結(jié)果,向服務(wù)端發(fā)送請求。那么當(dāng)我們請求頭處理異常時,則會導(dǎo)致nginx轉(zhuǎn)發(fā)到后段的請求由于服務(wù)器無法正確相應(yīng),返回406、400、404等錯誤。
? 所以,我認(rèn)為處理該問題最好的辦法就是讓nginx按照我們想要的方式處理前段發(fā)來的請求頭,那么處理請求頭該如何設(shè)置呢?在nginx中有一個參數(shù),即proxy_set_header。
? 該參數(shù)可以根據(jù)我們的需求設(shè)置請求頭,而這里最終要的一個即為proxy_set_header HOST。在nginx官方指導(dǎo)文檔中,proxy_set_header HOST有幾種寫法:
proxy_set_header HOST $host
proxy_set_header HOST $proxy_host
proxy_set_header HOST $host:$proxy_port
proxy_set_header HOST $http_host
這里,對幾種方法的解釋如下:
1.不設(shè)置proxy_set_header Host時,瀏覽器直接訪問nginx,獲取到的Host是proxy_pass后面的值,即 $proxy_host的值;
2.設(shè)置proxy_set_header Host $host時,瀏覽器直接訪問nginx,獲取到的Host是$host的值,沒有端口信息,此時代碼中如果有重定向路由,那么重定向時就會丟失端口信息,導(dǎo)致 404;
3.設(shè)置proxy_set_header Host $host:$proxy_port時,瀏覽器直接訪問nginx,獲取到的Host是 $host:$proxy_port的值;
4.proxy_set_header Host $http_host時,瀏覽器直接訪問nginx,獲取到的Host包含瀏覽器請求的IP和端口;
? 此時,則可以知道,我們在對HOST設(shè)置不同變量的時候,則會封裝不同的請求頭,當(dāng)請求頭在遠(yuǎn)端服務(wù)器找不到的時候,則無法訪問。
? 由于在不設(shè)置proxy_set_header HOST的時候,默認(rèn)時取proxy_pass后面的值,那此時其實向服務(wù)器端請求的時候,我們認(rèn)為是向mystarget的upstream池中的地址發(fā)出請求,實際上則是向服務(wù)器真實請求了mytarget,而在服務(wù)器端根本不存在mytarget這個請求內(nèi)容,所以會返回406這種無法處理客戶端請求的消息。
? 搞清楚原理后,我們則只需設(shè)置對應(yīng)的HOST到請求頭中就可以解決該問題了,但是發(fā)現(xiàn)我們設(shè)置了官方給的方式,都不能達(dá)到效果。
那么,這里就需要介紹nginx中proxy_set_header的隱藏用法(這個經(jīng)過網(wǎng)上鮮有的資料和多次嘗試以后,得出的結(jié)論):
? 1.在proxy_set_header HOST后,我們可以直接加upstream中明確的域名地址:aaa.target.com,即
upstream mytarget
{
server aaa.target.com:443 max_fails=30 fail_timeout=300s;
server bbb.target.com:443 backup;
}
server {
listen 8901;
server_name mytarget.server;
location /login/ {
proxy_set_header Host aaa.target.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass https://mytarget/my_target/login/;
? 該方式找了很久,找到兩篇帖子這樣設(shè)置,跟著設(shè)置后,發(fā)現(xiàn)返回成功。
? 繼續(xù)查閱資料發(fā)現(xiàn),另外一篇帖子上雖然也是用上述方法,但是進(jìn)行了更加詳細(xì)的解釋。大概意思是,proxy_set_header Host就是向服務(wù)器請求vhost的server_name,我們不能將該參數(shù)寫成$http_host,否則請求的則是我們代理的server_name,即mytarget.server。同樣,在服務(wù)器端并沒有mytarget.server的vhost,對方服務(wù)器的vhost是aaa.target.com/bbb.target.com。
根據(jù)上述資料理解,其實可以理解:
1.當(dāng)我們不配置HOST,則客戶端會向服務(wù)端請求mytarget;
2.當(dāng)我們配置HOST為$host的時候,則會向?qū)Ψ秸埱蟊緳C的ip或者域名,且不帶端口;
3.當(dāng)我們配置為 h o s t : host: host:proxy_prot的時候,則會向?qū)Ψ秸埱蟊緳C的ip或者域名,且?guī)Ф丝冢?/p>
4.當(dāng)我們配置為$http_host的時候,則會向?qū)Ψ秸埱笪覀冊O(shè)置的server_name,即target.server;
5.當(dāng)我們配置為指定的aaa.target.com/bbb.target.com時,則會向?qū)Ψ秸埱箜憫?yīng)的vhost;
? 綜上,只有第五種的配置可以實現(xiàn)我們向服務(wù)器發(fā)出正確的請求,而其他四種配置都無法在服務(wù)端找到正確的vhost,從而導(dǎo)致返回出現(xiàn)406或者404錯誤。
? 可是,這種方式也存在問題。在我們的場景下,需要有兩個域名,而這里我們只能使用一個域名,那么當(dāng)aaa.target.com不可用的時候,需要請求bbb.target.com,但是proxy_set_header HOST依然回去請求aaa.target.com,兩者不一樣,則無法返回正確的值,則依然返回406,此時我們則需要手動進(jìn)行調(diào)整,非常麻煩。
? 于是我想通過第四種方式,將我們的server_name設(shè)置為我需要的aaa.target.com/bbb.target.com,然后使用$http_host,但發(fā)現(xiàn)還是不行(這里與查詢到的帖子寫的有所不符,不知道為什么)。
? 最后,經(jīng)過多次嘗試,發(fā)現(xiàn)了proxy_set_header的隱藏用法:
? 2.在proxy_set_header HOST后,我們可以直接加upstream中將server_name以變量的形式帶入,而在server_name中,我們則可以寫入我們需要請求的vhost(而且可以寫多個),即server_name aaa.target.com bbb.target.com。具體寫法如下:
upstream mytarget
{
server aaa.target.com:443 max_fails=30 fail_timeout=300s;
server bbb.target.com:443 backup;
}
server {
listen 8901;
server_name aaa.target.com bbb.target.com;
location /login/ {
proxy_set_header Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass https://mytarget/my_target/login/;
? 經(jīng)過模擬訪問嘗試,發(fā)現(xiàn)這種寫法可以成功通過upstream的方式訪問http/https。
三 問題解決方案
? 在經(jīng)過長時間的查閱資料、學(xué)習(xí)、理解和測試中,最終我們找到了nginx主備方式訪問域名解決方案,為了更好的配合nginx代理作用,避免域名對應(yīng)的ip改變,由于nginx解析緩存,導(dǎo)致nginx無法訪問,我又加入了動態(tài)解析dns的相關(guān)配置,最終形成完整的最佳解決方案,具體完整訪問配置最佳解決方案如下:文章來源:http://www.zghlxwxcb.cn/news/detail-806023.html
upstream mytarget {
server aaa.target.com:443 max_fails=10 fail_timeout=300s;
server bbb.target.com:443 backup;
}
server {
listen 8901;
server_name aaa.target.com bbb.target.com;
resolver 61.139.2.69 valid=10s ipv6=off;
location /login/ {
#default_type application/json;
#add_header Content-Type 'application/json; charset=utf-8';
proxy_set_header Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
set $cmpassport_addr https://mytarget/my_target/login/;
proxy_pass $cmpassport_addr;
}
}
? 想要動態(tài)解析,我們必須要將proxy_pass訪問地址設(shè)置為變量,在變量中指定我們的具體訪問地址,然后再加上resolver配置即可。這里的resolver表示通過61.139.2.69(四川電信)的dns,每10s中動態(tài)解析一次server中的域名,且關(guān)閉ipv6的解析。文章來源地址http://www.zghlxwxcb.cn/news/detail-806023.html
到了這里,關(guān)于通過nginx的upstream配置域名進(jìn)行http/htts的訪問最佳實踐方案(406/404問題解決)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!