1. 程式人生 > >Android DNS之懲罰機制

Android DNS之懲罰機制

資料結構

統計資訊同樣是基於網絡卡的,所以理所當然的,這些資訊儲存在了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, &params, stats);
        //下面的函式會決定各個DNS伺服器地址是否可用,是否可用都設定到usable_servers[]中
	    bool usable_servers[MAXNS];
	    android_net_res_stats_get_usable_servers(&params, 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伺服器地址進行域名查詢。