UnknownHostException

去年9.30号写过一篇文章《AI编程神器v0.dev》,现在再看v0.dev已经改成了https://v0.app,支持的语言和功能更丰富,但是好久没有使用过了。现在主要用腾讯元宝和阿里云千问,一年AI的变化太快了,从单一的大模型已经升级为多态大模型,在单一模型上智能化越来越强,例如Google的🍌模型:

产品上也有很大变化,就拿有道词典来说,操作入口从搜索引擎变成答案引擎。

现在已经离不开AI,对AI最大的感触就是可以帮助我快速熟悉不同的技术领域、代码review、生产代码框架等,引用一句话。
现阶段的 AI,更像团队中的一个非常有干劲的初级程序员,可以快速编写代码,但需要不断的监督和纠正。你知道的越多,你就越能指导它。
遇到的问题
我们有个出海业务研发同学反馈APP上报的日志中存在下面的这种错误:
java.net.UnknownHostException: Unable to resolve host "*******co": No address associated with hostname

不是一直出现,偶发性的那种,美洲、亚洲、非洲等区域都会出现。

DNS解析排查
从ES集群查看APP上报的异常log,发现出现DNS No address associated with hostname的用户不是一直失败,一个用户使用同样的IP请求同一个接口,出现这种异常的情况占比达到3%。

在域名接入层Nginx 的Accesslog中是可以看到这个用户正常访问的请求的,如下所示。

DNS 如何工作可以参见:DNS原理、cloudflareDNS 文章

接口域名托管在阿里云,使用的阿里云免费的DNS,看不到DNS的解析日志,同时也怀疑DNS免费版因为没有海外集群可能导致海外用户本地的local dns获取这个接口域名的时候发生超时。

本着先在运维侧把能做的先做了,从免费DNS升级到付费版DNS,打开了解析日志记录和分析功能,这样优化完之后无法解析的异常变少了,但是还是存在。
联系阿里云技术同事帮进行排查,结合DNS解析日志和深度分析,以及排查阿里云海外的DNS缓存服务器日志,初步判断目前是用户localDNS到阿里云权威的网络不稳定导致的。

用户的localDNS 没有在APP异常log上报中记录,需要再单独实现一下。
上报日志修改
上报异常的代码这块比较简单,catch异常后直接写入到failMessage中进行上报。
} catch (e: Exception) {
if (e is SocketTimeoutException) {
logForApiMonitor(
api = api,
startTime = startTime,
result = ApiResult.Timeout,
failMessage = "${response?.code}_${url}_" + (response?.body?.string() ?: ""),
requestForm = buildFormParams(request),
extra = extra
)
}
需要增加一下上报的log内容,加上获取本地localDNS的逻辑。
平常主要用Python进行开发,还没有了解过安卓APP的开发,使用Deepseek帮修改一下log上报代码,推荐使用DnsUtil来获取:
try{
dnsInfo = DnsUtil.getDnsServers(context).joinToString(",")
} catch (e: Exception) {
dnsInfo = "failed_to_get_dns"
}
改完的代码又没有办法直接在vscode里面直接调试。在本地 VS Code 中调试需要安装Kotlin插件,鼓捣半天,遇到jdk、gradle问题,Kotlin Gradle 插件与 Gradle 版本之间还存在兼容性。
阿里云千问推荐 Android 开发环境(Android Studio)IDE,下载安装了一下。让千问生成这个测试demo,按照目录结构生成对应的文件,如下图:

发现使用DnsUtil.getDnsServers(context)
返回了 dns_unknown
,🔍 为什么拿不到 Local DNS?
从 Android 7.0 (API level 24) 开始,系统对访问底层网络配置增加了更严格的限制。普通应用可能无法直接读取 /system/etc/resolv.conf
或相关系统属性,导致此方法在高版本系统上失效。
之前正好写过用户反馈提交信息的服务,使用网易开源的https://nstool.netease.com来获取本地localDNS就可以,需要注意的是访问https://nstool.netease.com拿到的是iframe这个域名返回的信息。
curl "https://nstool.netease.com"
<html><head><title>����DNS����</title></head><body><iframe src='https://only-90807-52-220-230-70.nstool.netease.com/' frameborder=0 scrolling=no height='100%' width='100%'></iframe></body></html>
AI修改后的代码如下:
object DnsUtil {
private const val DNS_LOOKUP_URL = "https://nstool.netease.com"
private const val TIMEOUT_SECONDS = 5L
fun getDnsServers(context: Context): List<String> {
return try {
// 创建临时OkHttpClient
val client = OkHttpClient.Builder()
.connectTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.readTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.followRedirects(true)
.followSslRedirects(true)
.build()
val request = Request.Builder()
.url(DNS_LOOKUP_URL)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.build()
Log.d("DnsUtil", "Attempting to fetch DNS info from: $DNS_LOOKUP_URL")
val response = client.newCall(request).execute()
val finalUrl = response.request.url.toString()
Log.d("DnsUtil", "Final URL after redirects: $finalUrl")
if (!response.isSuccessful) {
return listOf("dns_query_failed_${response.code}")
}
val responseBody = response.body?.string() ?: ""
Log.d("DnsUtil", "Initial response body: ${responseBody.take(200)}")
// 提取iframe的src并请求真实内容
val iframeSrc = extractIframeSrc(responseBody)
if (iframeSrc.isNotEmpty()) {
Log.d("DnsUtil", "Found iframe src: $iframeSrc")
return fetchAndParseDnsFromIframe(client, iframeSrc)
}
// 如果没有iframe,直接解析当前响应
parseDnsFromResponse(responseBody)
} catch (e: Exception) {
Log.e("DnsUtil", "Failed to query DNS info: ${e.message}")
listOf("dns_query_error")
}
}
private fun fetchAndParseDnsFromIframe(client: OkHttpClient, iframeSrc: String): List<String> {
return try {
val iframeRequest = Request.Builder()
.url(iframeSrc)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.build()
val iframeResponse = client.newCall(iframeRequest).execute()
if (!iframeResponse.isSuccessful) {
return listOf("iframe_request_failed_${iframeResponse.code}")
}
val iframeBody = iframeResponse.body?.string() ?: ""
Log.d("DnsUtil", "Iframe response body: $iframeBody")
parseDnsFromResponse(iframeBody)
} catch (e: Exception) {
Log.e("DnsUtil", "Failed to fetch iframe content: ${e.message}")
listOf("iframe_fetch_error")
}
}
private fun extractIframeSrc(html: String): String {
val iframePattern = Regex("""<iframe[^>]*src=['"]([^'"]+)['"][^>]*>""")
val match = iframePattern.find(html)
return match?.groupValues?.get(1) ?: ""
}
private fun parseDnsFromResponse(html: String): List<String> {
val dnsServers = mutableListOf<String>()
// 方案1: 精确匹配DNS地址信息行
val dnsPatterns = listOf(
Regex("""您的DNS地址信息[::]\s*([\d.]+)"""), // 匹配"您的DNS地址信息: 220.181.102.176"
Regex("""DNS地址信息[::]\s*([\d.]+)"""),
Regex("""DNS[::]\s*([\d.]+)""")
)
for (pattern in dnsPatterns) {
val match = pattern.find(html)
if (match != null) {
val dns = match.groupValues[1]
if (dns.isNotEmpty() && isValidIpAddress(dns)) {
dnsServers.add(dns)
Log.d("DnsUtil", "Found DNS using pattern: ${pattern.pattern} -> $dns")
break
}
}
}
// 方案2: 如果没有找到特定模式,提取所有有效的IP地址
if (dnsServers.isEmpty()) {
val ipPattern = Regex("""\b(?:\d{1,3}\.){3}\d{1,3}\b""")
val matches = ipPattern.findAll(html)
val allIps = matches.map { it.value }.distinct()
allIps.forEach { ip ->
if (isValidIpAddress(ip) && !isPrivateIp(ip)) {
dnsServers.add(ip)
}
}
Log.d("DnsUtil", "Found all IPs: $allIps, filtered DNS: $dnsServers")
}
return if (dnsServers.isEmpty()) listOf("no_dns_found") else dnsServers
}
private fun isValidIpAddress(ip: String): Boolean {
return try {
val parts = ip.split(".")
parts.size == 4 && parts.all { part ->
part.toInt() in 0..255
}
} catch (e: Exception) {
false
}
}
private fun isPrivateIp(ip: String): Boolean {
return ip.startsWith("10.") ||
ip.startsWith("192.168.") ||
ip.startsWith("172.") && ip.substring(4, 5).toInt() in 16..31 ||
ip == "127.0.0.1" ||
ip == "0.0.0.0" ||
ip == "255.255.255.255"
}
}
实际运行效果,dns_servers可以获取到localDNS IP:

未完待续
UnknownHostException等节后开发同学把log上报增加一些日志发版后获取用户localDNS 再找阿里云技术一起进一步查看下,后续有进展再补充到文章中。

祝大家国庆快乐!~