Round-Robin DNS (DNS 輪循、循環 DNS) 是非常古老的負載平衡手法,其原理是 DNS 伺服器回應 DNS 請求時,返回結果來自一長串 IP 位址清單,並在每次回應時抽換 IP 組合跟掉換 IP 順序,一般來說,客戶端會採用回應結果的第一個 IP,如此,要連上 www.xxx.yyy 的不同客戶端便會導向不同 IP 連到不同主機,達到分散流量及工作負載的效果。

雖然這種做法無法平均分配負載(將流量導向空閒主機)、不會避開故障中的伺服器,並可能因客戶端快取減弱效果,但因為容易實現,至今仍普遍使用。(延伸閱讀:現代 CDN 會使用 BGP Anycast 實現負載平衡,Internet 跟你想的不一樣:IP 地址不是唯一,有許多相同 IP 的主機散落各地)

以微軟的 login.microsoftonline.com 為例,nslookup 可得到 8 個 IPv4 地址,反覆查詢,IP 項目及順序會改變:

好奇心起:這個 IP 清單包含多少個不同 IP?每次查詢 IP 的變化有特殊規律嗎?

做個實驗試試,我寫了以下 PowerShell 從 nslookup 輸出擷取 IPv4 地址,10 秒查一次。

for ($i = 0; $i -lt 128; $i++) {
    $capture = $false
    $ips = @()
    . nslookup.exe login.microsoftonline.com 8.8.8.8 | ForEach-Object {
        if ($_ -match 'Addresses') {
            $capture = $true
        } 
        elseif ($capture -and $_ -match '(\d+\.\d+\.\d+\.\d+)') {
            $ips += $matches[1]
        }
    } 
    Write-Output ($ips -join ',')
    Start-Sleep -Seconds 10
}

.\run-100-times-dns.ps1 | Out-File -FilePath 'dns-results.txt' 將執行結果輸出到 dns-results.txt。有趣的是,「未經授權的回答:」是透過 stderr 輸出,不會寫入檔案而是顯示在螢幕上。

然後我用 Vue 寫了個簡單查詢,方便觀察各 IP 出現次數、順序。線上展示

由結果可知,login.microsoftonline.com 背後共有 20 個 IP,每次出現 8 個 128 次總數 1024,1024 / 20 = 理論上每個 IP 會出現 51.2 次,接近實測的數字。而順序分佈也呈現隨機。點選最上方 IP [1] 可觀察該 IP 出現在哪些資料列,點選列號 [2] 可觀察相同 IP 組合重複出現的位置。依觀察結果,相同 IP 組合出現次數介於 1 到 7 次,而且偏集中,前後很少超過五分鐘(每列相隔十秒,也就是序號差小於 30)。

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>Round-Robin DNS Test</title>
    <script src="https://unpkg.com/vue@3"></script>
    <style>
        body {
            font-family: Arial, Helvetica, sans-serif; font-size: 9pt;
        }
        .iplist {
            display: flex; flex-direction: row; flex-wrap: wrap;
        }

        .iplist>span {
            margin-right: 2px; padding: 2px; border: 1px solid #ccc;
            width: 120px; text-align: center;
        }

        table {
            margin-top: 12px; border-collapse: collapse;
        }

        tr:nth-child(odd) {
            background-color: #eee;
        }

        td {
            border: 1px solid #ccc; padding: 4px;
        }

        td.sn {
            text-align: right;
        }

        .focus {
            text-shadow: 1px 1px 2px black; font-weight: bolder;
            background-color: black!important;
            color: yellow!important;
        }
        .link {
            cursor: pointer;
        }
        #app {
            width: 800px;
        }
.dc-0  { color:#e6194B; } .dbc-0  { color:#ffffff; background-color:#e6194Ba0; }
.dc-1  { color:#3cb44b; } .dbc-1  { color:#ffffff; background-color:#3cb44ba0; }
/* ... 省略 ... */
.dc-20 { color:#ffffff; } .dbc-20 { color:#000000; background-color:#ffffffa0; }
.dc-21 { color:#000000; } .dbc-21 { color:#ffffff; background-color:#000000a0; }        
    </style>
    <script type="application/x-data" id="data">
        40.126.38.19,20.190.166.68,20.190.166.66,20.190.166.132,20.190.166.67,40.126.38.22,20.190.166.131,40.126.38.20
        20.190.141.33,20.190.141.32,20.190.141.39,20.190.141.38,20.190.141.37,20.190.141.35,40.126.13.8,40.126.13.9
        ... 省略 ...
        40.126.13.9,20.190.141.38,40.126.13.8,20.190.141.33,20.190.141.35,20.190.141.39,20.190.141.32,20.190.141.37               
    </script>
</head>

<body>
    <div id="app">
        <div class="iplist">
            <span v-for="ip in list" @click="focusIp = ip" class="link" 
                :class="css(ip)">{{ip}} ({{counts[ip]}})</span>
        </div>
        <table>
            <tr v-for="(line,idx) in data"  >
                <td class="sn link" @click="focusLine = line" :class="{'focus':focusLine == line}">
                    {{idx+1}}. ({{dupLineCount[line]}}) 
                </td>
                <td v-for="ip in line.split(',')" :class="css(ip)">{{ip}}</td>
            </tr>
        </table>
    </div>
    <script>
        const data = [];
        const ipChk = {};
        const counts = {};
        const dupLineCount = {};
        let c = 0;
        document.head.querySelector('script[id=data]').innerText.split('\n').forEach(function (line) {
            line = line.trim();
            if (!line) return;
            if (line in dupLineCount) dupLineCount[line]++;
            else dupLineCount[line] = 1;
            var ips = line.split(',');
            ips.forEach(function (ip) {
                if (!(ip in ipChk)) { ipChk[ip] = c++; counts[ip] = 0; }
                else counts[ip]++;
            });
            data.push(line);
        });
        const ips = Object.keys(ipChk);
        ips.sort();
        const app = Vue.createApp({
            data: function () {
                return {
                    data: data,
                    list: ips,
                    counts: counts,
                    focusLine: '',
                    dupLineCount: dupLineCount,
                    focusIp: ''
                };
            },
            methods: {
                css: function (ip) {
                    return (this.focusIp == ip ? 'focus ' : '') +
                        'dbc-' + (ipChk[ip] % 22);
                }
            }
        });
        app.mount('#app');
    </script>
</body>


</html>

就醬,我又完成一次無聊的實驗,累積一些 Coding 經驗。

A experiment to observe how round-robin DNS provides different IP addresses.


Comments

Be the first to post a comment

Post a comment