searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

基于Android Q的DNS优化思路及方案

2023-07-20 03:09:51
68
0

此次优化的目标主要是减少网页渲染的首屏时延,也就是从用户点击到页面完全加载的耗时。

加载一个页面主要分为几个步骤(以输入URL加载网页的方式为例):

1、首先,在浏览器地址栏中输入url

2、初始化webview client

3、网页缓存查询(浏览器缓存-系统缓存-路由器缓存),未命中则发起网页加载。

4、DNS解析(也分为浏览器缓存->java缓存->native缓存)。

5、发起TCP/HTTP/TLS握手。

6、请求基本HTML+CSS内容,加载js脚本。

7、生成Dom树,解析css样式,js交互,js动态请求网页内容。

8、渲染引擎工作,blink引擎+v8引擎

 

作为平台侧,可以优化的是webview和网络请求实现。

这里首先优化平台侧DNS的逻辑,以提高网络请求的稳定性(防劫持/崩溃),提高网络体验下限。

 

那么要提高dns效率,有几个方面可以进行:

1、增加/延长dns缓存

提升:增加缓存命中率,减少某些重复的dns查询

风险:dns映射失效时缓存命中,会无法联通

2、超时优化(已实现)

提升:在有nameserver失效,但未全部失效的情况下,降低整体dns时延

风险:弱网场景可能出现解析失败

3、httpDns

提升:防劫持,稳定快速

风险:成本高,需自行搭建

4、串行改并行(已实现)

提升:类似超时优化,搭配多个name server,可实现min(ns1, ns2, ...)延迟

风险:逻辑改动较大

 

围绕串行改并行讲下:

首先通过流程图再回顾下当前的dns主要流程

可以看出,从APP进程(java层->native层->dnsproxyd写命令)到netd进程(监听命令→执行res_nsend→send_dg),

整个流程对于单个name server来说,都是串行逻辑,虽然经历一次跨进程,但是对于java层来说,都是单线程阻塞的逻辑。

如上述,并行的逻辑粒度要细化到每个name server,就需要看哪里逻辑牵涉到了对应iface的name server数组。

 

java层流程图

 

 

native层流程图

 

 

netd流程图

 

 

纵观流程对应的函数,上层一直都不关心name server,直到libc中res_state的初始化才出现对当前netid对应nameserver的查询。

也就是res_init.c中,存放在nsaddr_list[MAXNS]中

		if (MATCH(buf, "nameserver") && nserv < MAXNS) {
		    struct addrinfo hints, *ai;
            ...
			if (getaddrinfo(cp, sbuf, &hints, &ai) == 0 &&
			    ai->ai_addrlen <= minsiz) {
			    if (statp->_u._ext.ext != NULL) {
				memcpy(&statp->_u._ext.ext->nsaddrs[nserv],
				    ai->ai_addr, ai->ai_addrlen);
			    }
			    if (ai->ai_addrlen <=
			        sizeof(statp->nsaddr_list[nserv])) {
				memcpy(&statp->nsaddr_list[nserv],
				    ai->ai_addr, ai->ai_addrlen);
			    } else
				statp->nsaddr_list[nserv].sin_family = 0;
			    freeaddrinfo(ai);
			    nserv++;
			}
		    }
		    continue;
		}

 

继续向下到res_nsend中,就可以看到aosp默认的串行逻辑实现,根据res_state中设置的retry和name server,轮询阻塞发包。

循环内单次逻辑分为socket random_bind,connect,sendto和retrying_select。

如果将串行改并行,需要找到一个异步操作逻辑才可以实现。

random_bind,connect,sendto都是阻塞逻辑,是没有机会做异步的,只有select操作是异步的。

但是回到代码,select操作是封装在send_dg中的,而retrying_select的参数只有一个name server建立的socket的对应fd

static int
retrying_select(const int sock, fd_set *readset, fd_set *writeset, const struct timespec *finish)
{
	struct timespec now, timeout;
	int n, error;
	socklen_t len;
...
}

那么就需要:

1、把nameserver数组下放到select

2、循环建立socket并把socket fd数组下放

3、在select处增加循环

4、一旦有可读且有效数据,停止其他socket的select

 

把name server数组传到select逻辑处:

+retrying_select_array(const int socks[], const bool usable_servers[], int available_sock, fd_set *readset, fd_set *writeset, const struct timespec *finish)
+{
+       struct timespec now, timeout;
+       int n, error;
+       socklen_t len;
+       int ns;
+
+retry:
+       for (ns = 0; ns < MAXNS; ns++) {
+               if (!usable_servers[ns]) continue;
+               int sock = socks[ns];
		...
+               n = pselect(sock + 1, readset, writeset, NULL, &timeout, NULL);
+               if (n == 0) {
+                       syslog(LOG_WARNING, "  %d retrying_select_array timeout. Abort all sockets query.\n", sock);
+                       errno = ETIMEDOUT;
+                       return 0;
+               }
		...
+       return n;
+}

然后将send_dg中建立socket的循环建立,并将fd数组下发

+       int ns;
+       for (ns = 0; ns < statp->nscount; ns++) {
+               nsap = get_nsaddr(statp, (size_t)ns);
+               nsaplen = get_salen(nsap);
+               char abuf[NI_MAXHOST];
+               getnameinfo(nsap, (socklen_t)nsaplen, abuf, sizeof(abuf),
+                       if (statp->_mark != MARK_UNSET) {
+                               if (setsockopt(EXT(statp).nssocks[ns], SOL_SOCKET,
+                                               SO_MARK, &(statp->_mark), sizeof(statp->_mark)) < 0) {
+                                       res_nclose(statp);
+                                       return -1;
+                               }
+                       }
+                       if (random_bind(EXT(statp).nssocks[ns], nsap->sa_family) < 0) {
+                               Aerror(statp, stderr, "bind(dg)", errno, nsap,
+                                       nsaplen);
                                res_nclose(statp);
-                               return -1;
+                               return (0);
                        }
+                       if (__connect(EXT(statp).nssocks[ns], nsap, (socklen_t)nsaplen) < 0) {
+                               Aerror(statp, stderr, "connect(dg)", errno, nsap,
+                                       nsaplen);
+                               res_nclose(statp);
+                               return (0);
+                       }

+               if (send(s, (const char*)buf, (size_t)buflen, 0) != buflen) {
+                       Perror(statp, stderr, "send", errno);
                        res_nclose(statp);
                        return (0);
                }
-               if (__connect(EXT(statp).nssocks[ns], nsap, (socklen_t)nsaplen) < 0) {
-                       Aerror(statp, stderr, "connect(dg)", errno, nsap,
-                           nsaplen);

+               if (sendto(s, (const char*)buf, buflen, 0, nsap, nsaplen) != buflen)
+               {
+                       Aerror(statp, stderr, "sendto", errno, nsap, nsaplen);

			n = retrying_select_array(EXT(statp).nssocks, usable_servers, s, &dsmask, NULL, &finish);
		...

}

 

还有部分边缘逻辑,比如控制整体超时,以及判断数据有效的逻辑暂时不讲述。

下图是修改后的dns行为,三个ns会一起发包,为避免某些网关不支持同时处理多个包,还需要加入大概2ms的延时间隔(此处例子未加入延时)

只要上层收到任何一个ns的有效包即可停止select

 

0条评论
0 / 1000
马****壮
2文章数
0粉丝数
马****壮
2 文章 | 0 粉丝
马****壮
2文章数
0粉丝数
马****壮
2 文章 | 0 粉丝
原创

基于Android Q的DNS优化思路及方案

2023-07-20 03:09:51
68
0

此次优化的目标主要是减少网页渲染的首屏时延,也就是从用户点击到页面完全加载的耗时。

加载一个页面主要分为几个步骤(以输入URL加载网页的方式为例):

1、首先,在浏览器地址栏中输入url

2、初始化webview client

3、网页缓存查询(浏览器缓存-系统缓存-路由器缓存),未命中则发起网页加载。

4、DNS解析(也分为浏览器缓存->java缓存->native缓存)。

5、发起TCP/HTTP/TLS握手。

6、请求基本HTML+CSS内容,加载js脚本。

7、生成Dom树,解析css样式,js交互,js动态请求网页内容。

8、渲染引擎工作,blink引擎+v8引擎

 

作为平台侧,可以优化的是webview和网络请求实现。

这里首先优化平台侧DNS的逻辑,以提高网络请求的稳定性(防劫持/崩溃),提高网络体验下限。

 

那么要提高dns效率,有几个方面可以进行:

1、增加/延长dns缓存

提升:增加缓存命中率,减少某些重复的dns查询

风险:dns映射失效时缓存命中,会无法联通

2、超时优化(已实现)

提升:在有nameserver失效,但未全部失效的情况下,降低整体dns时延

风险:弱网场景可能出现解析失败

3、httpDns

提升:防劫持,稳定快速

风险:成本高,需自行搭建

4、串行改并行(已实现)

提升:类似超时优化,搭配多个name server,可实现min(ns1, ns2, ...)延迟

风险:逻辑改动较大

 

围绕串行改并行讲下:

首先通过流程图再回顾下当前的dns主要流程

可以看出,从APP进程(java层->native层->dnsproxyd写命令)到netd进程(监听命令→执行res_nsend→send_dg),

整个流程对于单个name server来说,都是串行逻辑,虽然经历一次跨进程,但是对于java层来说,都是单线程阻塞的逻辑。

如上述,并行的逻辑粒度要细化到每个name server,就需要看哪里逻辑牵涉到了对应iface的name server数组。

 

java层流程图

 

 

native层流程图

 

 

netd流程图

 

 

纵观流程对应的函数,上层一直都不关心name server,直到libc中res_state的初始化才出现对当前netid对应nameserver的查询。

也就是res_init.c中,存放在nsaddr_list[MAXNS]中

		if (MATCH(buf, "nameserver") && nserv < MAXNS) {
		    struct addrinfo hints, *ai;
            ...
			if (getaddrinfo(cp, sbuf, &hints, &ai) == 0 &&
			    ai->ai_addrlen <= minsiz) {
			    if (statp->_u._ext.ext != NULL) {
				memcpy(&statp->_u._ext.ext->nsaddrs[nserv],
				    ai->ai_addr, ai->ai_addrlen);
			    }
			    if (ai->ai_addrlen <=
			        sizeof(statp->nsaddr_list[nserv])) {
				memcpy(&statp->nsaddr_list[nserv],
				    ai->ai_addr, ai->ai_addrlen);
			    } else
				statp->nsaddr_list[nserv].sin_family = 0;
			    freeaddrinfo(ai);
			    nserv++;
			}
		    }
		    continue;
		}

 

继续向下到res_nsend中,就可以看到aosp默认的串行逻辑实现,根据res_state中设置的retry和name server,轮询阻塞发包。

循环内单次逻辑分为socket random_bind,connect,sendto和retrying_select。

如果将串行改并行,需要找到一个异步操作逻辑才可以实现。

random_bind,connect,sendto都是阻塞逻辑,是没有机会做异步的,只有select操作是异步的。

但是回到代码,select操作是封装在send_dg中的,而retrying_select的参数只有一个name server建立的socket的对应fd

static int
retrying_select(const int sock, fd_set *readset, fd_set *writeset, const struct timespec *finish)
{
	struct timespec now, timeout;
	int n, error;
	socklen_t len;
...
}

那么就需要:

1、把nameserver数组下放到select

2、循环建立socket并把socket fd数组下放

3、在select处增加循环

4、一旦有可读且有效数据,停止其他socket的select

 

把name server数组传到select逻辑处:

+retrying_select_array(const int socks[], const bool usable_servers[], int available_sock, fd_set *readset, fd_set *writeset, const struct timespec *finish)
+{
+       struct timespec now, timeout;
+       int n, error;
+       socklen_t len;
+       int ns;
+
+retry:
+       for (ns = 0; ns < MAXNS; ns++) {
+               if (!usable_servers[ns]) continue;
+               int sock = socks[ns];
		...
+               n = pselect(sock + 1, readset, writeset, NULL, &timeout, NULL);
+               if (n == 0) {
+                       syslog(LOG_WARNING, "  %d retrying_select_array timeout. Abort all sockets query.\n", sock);
+                       errno = ETIMEDOUT;
+                       return 0;
+               }
		...
+       return n;
+}

然后将send_dg中建立socket的循环建立,并将fd数组下发

+       int ns;
+       for (ns = 0; ns < statp->nscount; ns++) {
+               nsap = get_nsaddr(statp, (size_t)ns);
+               nsaplen = get_salen(nsap);
+               char abuf[NI_MAXHOST];
+               getnameinfo(nsap, (socklen_t)nsaplen, abuf, sizeof(abuf),
+                       if (statp->_mark != MARK_UNSET) {
+                               if (setsockopt(EXT(statp).nssocks[ns], SOL_SOCKET,
+                                               SO_MARK, &(statp->_mark), sizeof(statp->_mark)) < 0) {
+                                       res_nclose(statp);
+                                       return -1;
+                               }
+                       }
+                       if (random_bind(EXT(statp).nssocks[ns], nsap->sa_family) < 0) {
+                               Aerror(statp, stderr, "bind(dg)", errno, nsap,
+                                       nsaplen);
                                res_nclose(statp);
-                               return -1;
+                               return (0);
                        }
+                       if (__connect(EXT(statp).nssocks[ns], nsap, (socklen_t)nsaplen) < 0) {
+                               Aerror(statp, stderr, "connect(dg)", errno, nsap,
+                                       nsaplen);
+                               res_nclose(statp);
+                               return (0);
+                       }

+               if (send(s, (const char*)buf, (size_t)buflen, 0) != buflen) {
+                       Perror(statp, stderr, "send", errno);
                        res_nclose(statp);
                        return (0);
                }
-               if (__connect(EXT(statp).nssocks[ns], nsap, (socklen_t)nsaplen) < 0) {
-                       Aerror(statp, stderr, "connect(dg)", errno, nsap,
-                           nsaplen);

+               if (sendto(s, (const char*)buf, buflen, 0, nsap, nsaplen) != buflen)
+               {
+                       Aerror(statp, stderr, "sendto", errno, nsap, nsaplen);

			n = retrying_select_array(EXT(statp).nssocks, usable_servers, s, &dsmask, NULL, &finish);
		...

}

 

还有部分边缘逻辑,比如控制整体超时,以及判断数据有效的逻辑暂时不讲述。

下图是修改后的dns行为,三个ns会一起发包,为避免某些网关不支持同时处理多个包,还需要加入大概2ms的延时间隔(此处例子未加入延时)

只要上层收到任何一个ns的有效包即可停止select

 

文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0