Android DNS之懲罰機制
阿新 • • 發佈:2018-12-11
資料結構
統計資訊同樣是基於網絡卡的,所以理所當然的,這些資訊儲存在了resolv_cache_info中,該結構中與統計有關的資訊如下:
struct resolv_cache_info {
struct __res_params params;
//每個DNS伺服器地址都有一個自己的統計資訊
struct __res_stats nsstats[MAXNS];
};
統計引數的定義如下,這幾個引數的用法及其含義見下文分析:
/* per-netid configuration parameters passed from netd to the resolver */
struct __res_params {
uint16_t sample_validity; // sample lifetime in s
// threshold of success / total samples below which a server is considered broken
uint8_t success_threshold; // 0: disable, value / 100 otherwise
uint8_t min_samples; // min # samples needed for statistics to be considered meaningful
//__res_stats.samples中最多可以儲存多少個樣本,該值應該不能超過MAXNSSAMPLES,否則就陣列越界了
uint8_t max_samples; // max # samples taken into account for statistics
};
統計資訊的定義如下:
/*
* Resolver reachability statistics and run-time parameters.
*/
//稱之為統計樣本吧
struct __res_sample {
//該值是DNS請求報文被髮送的時間,牆上時鐘表示
time_t at; // time in s at which the sample was recorded
//如果收到了響應,那麼為請求耗時,單位為毫秒;如果沒有收到響應,那麼該值為0
uint16_t rtt; // round-trip time in ms
//響應報文中的返回碼
uint8_t rcode; // the DNS rcode or RCODE_XXX defined above
};
#define MAXNSSAMPLES 64 /* max # samples to store per server */
struct __res_stats {
// 儲存樣本,作為環形陣列使用
struct __res_sample samples[MAXNSSAMPLES];
// 當前環形陣列中儲存的贗本數
uint8_t sample_count;
// 下一個樣本應該儲存到samples[]的哪個位置
uint8_t sample_next;
};
要強調的是,對於每個網絡卡,統計引數只有一套,但是統計資訊是根據DNS伺服器地址分別記錄的。
基本操作
初始化
初始化是在設定DNS伺服器地址的時候完成的,其中相關程式碼如下:
int
_resolv_set_nameservers_for_net(unsigned netid, const char** servers, unsigned numservers,
const char *domains, const struct __res_params* params)
{
pthread_once(&_res_cache_once, _res_cache_init);
pthread_mutex_lock(&_res_cache_list_lock);
//分配resolv_cache_info結構,當然包括統計資訊和統計引數
_get_res_cache_for_net_locked(netid);
if (cache_info != NULL) {
uint8_t old_max_samples = cache_info->params.max_samples;
if (params != NULL) {
//如果FWK有設定統計引數,那麼使用FWK指定的
cache_info->params = *params;
} else {
//FWK沒有指定,使用預設的
_resolv_set_default_params(&cache_info->params);
}
//設定DNS地址或者修改了統計引數,那麼清除統計資訊
if (!_resolv_is_nameservers_equal_locked(cache_info, servers, numservers)) {
// Clear the NS statistics because the mapping to nameservers might have changed.
_res_cache_clear_stats_locked(cache_info);
} else if (cache_info->params.max_samples != old_max_samples) {
// If the maximum number of samples changes, the overhead of keeping the most recent
// samples around is not considered worth the effort, so they are cleared instead. All
// other parameters do not affect shared state: Changing these parameters does not
// invalidate the samples, as they only affect aggregation and the conditions under
// which servers are considered usable.
_res_cache_clear_stats_locked(cache_info);
}
}
pthread_mutex_unlock(&_res_cache_list_lock);
return 0;
}
清空統計資訊_resolv_set_default_params()
static void _res_cache_clear_stats_locked(struct resolv_cache_info* cache_info) {
if (cache_info) {
//將每個server對應的統計全部去清零
for (int i = 0 ; i < MAXNS ; ++i) {
cache_info->nsstats->sample_count = cache_info->nsstats->sample_next = 0;
}
}
}
新增樣本
在res_nsend()中,如果查詢結束,會呼叫_resolv_cache_add_resolver_stats_sample()將樣本加入到cache中,程式碼如下:
int res_nsend(res_state statp,
const u_char *buf, int buflen, u_char *ans, int anssiz)
//注意,這裡只統計第一輪的查詢結果
/* Only record stats the first time we try a query. See above. */
if (try == 0) {
struct __res_sample sample;
//用引數now、rcode、delay設定樣本sample
_res_stats_set_sample(&sample, now, rcode, delay);
//將樣本加入到快取中
_resolv_cache_add_resolver_stats_sample(statp->netid, revision_id,
ns, &sample, params.max_samples);
}
}
void _resolv_cache_add_resolver_stats_sample( unsigned netid, int revision_id, int ns,
const struct __res_sample* sample, int max_samples) {
if (max_samples <= 0) return;
pthread_mutex_lock(&_res_cache_list_lock);
struct resolv_cache_info* info = _find_cache_info_locked(netid);
//找到對應的cache,並且二者的revision_id是一致,這種比較是防止在一個DNS請求過程中DNS資訊被修改過
if (info && info->revision_id == revision_id) {
_res_cache_add_stats_sample_locked(&info->nsstats[ns], sample, max_samples);
}
pthread_mutex_unlock(&_res_cache_list_lock);
}
static void
_res_cache_add_stats_sample_locked(struct __res_stats* stats, const struct __res_sample* sample,
int max_samples) {
// Note: This function expects max_samples > 0, otherwise a (harmless) modification of the
// allocated but supposedly unused memory for samples[0] will happen
XLOG("%s: adding sample to stats, next = %d, count = %d", __FUNCTION__,
stats->sample_next, stats->sample_count);
//儲存當前樣本
stats->samples[stats->sample_next] = *sample;
//樣本數不能超過配置引數中指定的最大樣本數
if (stats->sample_count < max_samples) {
++stats->sample_count;
}
//從這裡可以看出,stats->samples[]是作為環形陣列使用的,並且stats->sample_next指向的就是下一個要
//賦值的樣本的索引
if (++stats->sample_next >= max_samples) {
stats->sample_next = 0;
}
}
懲罰機制
前面介紹的都是統計資訊的資料結構以及它們是如何儲存的,但是還沒有看儲存這些資訊到底要幹什麼?這些資訊實際上會在res_nsend()中使用,下面先看程式碼實現,然後再來總結這種機制。
res_nsend()
res_nsend()中有如下程式碼片段:
int res_nsend(res_state statp, const u_char *buf, int buflen, u_char *ans, int anssiz)
{
/*
* Send request, RETRY times, or until successful.
*/
for (try = 0; try < statp->retry; try++) {
struct __res_stats stats[MAXNS];
struct __res_params params;
//獲取當前resolv_cache中的統計引數、統計資訊以及revision_id
int revision_id = _resolv_cache_get_resolver_stats(statp->netid, ¶ms, stats);
//下面的函式會決定各個DNS伺服器地址是否可用,是否可用都設定到usable_servers[]中
bool usable_servers[MAXNS];
android_net_res_stats_get_usable_servers(¶ms, stats, statp->nscount,
usable_servers);
//在遍歷各個DNS伺服器地址時,如果已經標記該伺服器地址不可用,則直接跳過,
//所以我們稱這種機制為懲罰機制(不喜勿噴)
for (ns = 0; ns < statp->nscount; ns++) {
if (!usable_servers[ns])
continue;
}
}
}
}
先來看看當前統計引數和統計資訊的獲取程式碼:
int
_resolv_cache_get_resolver_stats( unsigned netid, struct __res_params* params,
struct __res_stats stats[MAXNS]) {
int revision_id = -1;
pthread_mutex_lock(&_res_cache_list_lock);
struct resolv_cache_info* info = _find_cache_info_locked(netid);
if (info) {
//完全正確,要獲取的資訊全部來自於resolv_cache_info
memcpy(stats, info->nsstats, sizeof(info->nsstats));
*params = info->params;
revision_id = info->revision_id;
}
pthread_mutex_unlock(&_res_cache_list_lock);
return revision_id;
}
下面重點來看到底是如何判斷DNS伺服器地址是否可用的。
DNS伺服器地址的可用性判定
void
android_net_res_stats_get_usable_servers(const struct __res_params* params,
struct __res_stats stats[], int nscount, bool usable_servers[]) {
//統計總共有多少個地址是可用的
unsigned usable_servers_found = 0;
for (int ns = 0; ns < nscount; ns++) {
//具體的一個伺服器地址是否可用有下面的函式決定
bool usable = _res_stats_usable_server(params, &stats[ns]);
if (usable) {
++usable_servers_found;
}
usable_servers[ns] = usable;
}
// If there are no usable servers, consider all of them usable.
// TODO: Explore other possibilities, such as enabling only the best N servers, etc.
//如註釋所述,如果上面的邏輯判斷所有的DNS地址都不可用,那麼為了保證至少有DNS伺服器地址可用,
//這種情況下會將所有的地址都置為可用。顯然這是一種防止懲罰過度的手段
if (usable_servers_found == 0) {
for (int ns = 0; ns < nscount; ns++) {
usable_servers[ns] = true;
}
}
}
單個DNS伺服器地址的可用性判定
bool _res_stats_usable_server(const struct __res_params* params, struct __res_stats* stats)
{
int successes = -1;
int errors = -1;
int timeouts = -1;
int internal_errors = -1;
int rtt_avg = -1;
time_t last_sample_time = 0;
//該函式實際上是非常簡單的,就是統計stats中:DNS查詢成功的次數、查詢失敗次數、查詢超時次數、查詢過程中
//發生了內部錯誤(快取區太小等)的次數、查詢成功時的平均RTT時延、最後一次新增統計樣本的時間戳
android_net_res_stats_aggregate(stats, &successes, &errors, &timeouts, &internal_errors,
&rtt_avg, &last_sample_time);
//進行門限判斷
if (successes >= 0 && errors >= 0 && timeouts >= 0) {
//總的DNS查詢次數,注意不包含內部錯誤,因為這種情況根本就不會發起DNS請求
int total = successes + errors + timeouts;
//1. 總的查詢次數超過了統計引數中配置的min_samples門限-----樣本要達到一定數量
//2. 有查詢失敗的情況發生-----如果全部正確也確實沒有什麼要繼續判定的必要
if (total >= params->min_samples && (errors > 0 || timeouts > 0)) {
//計算DNS查詢成功率,百分比
int success_rate = successes * 100 / total;
//如果成功率低於統計引數中設定的成功率門限,那麼需要懲罰該DNS伺服器地址
if (success_rate < params->success_threshold) {
// evNowTime() is used here instead of time() to stay consistent with the rest of
// the code base
time_t now = evNowTime().tv_sec;
//如果從上次新增樣本到當前時間已經超過了要懲罰的時間,那麼就不需要懲罰了
if (now - last_sample_time > params->sample_validity) {
// Note: It might be worth considering to expire old servers after their expiry
// date has been reached, however the code for returning the ring buffer to its
// previous non-circular state would induce additional complexity.
//雖然不懲罰了,但是該DNS服務其之前的統計資訊要清除
_res_stats_clear_samples(stats);
} else {
//需要懲罰並且還沒有超過懲罰時間,那麼禁用該DNS伺服器地址
return 0;
}
}
}
}
//其它所有的情況,該DNS伺服器地址都是可用的
return 1;
}
從上面的程式碼中,可以清楚的看到統計引數的含義分別如下:
min_samples: 懲罰一個伺服器地址所需的最小樣本數; success_threshold: 如果一個伺服器地址的查詢成功率低於該閾值,那麼該地址將會被懲罰; sample_validity:如果一個地址要被懲罰,那麼應該懲罰多長時間,單位為秒。
max_samples: 該引數和懲罰機制無關,它控制一個伺服器地址最多可以儲存多少個樣本。
綜上,不難理解何為懲罰機制,其實其設計思想非常簡單,就是如果使用一個DNS伺服器地址查詢的成功率過低了,那麼就禁用該地址一段時間,這段懲罰時間內,不會在使用該DNS伺服器地址進行域名查詢。