我们在本地测试或者本地通讯的时候经常使用 localhost 域名,但是访问 localhost 的对应的一定就是我们的本机地址么?

背景

在一个风和日丽下午,突然收到了运维同学的反馈,说我们的一个服务调用突然报错了,关键是这个服务已经半年没有更新发版过了,询问后得知最近基础架构也没有什么变更,这就很迷了

我们排查日志后发现这个服务去调用了一个不知名的 ip 地址,这个地址还能 ping 通,但是我们明明是配置的 localhost,为什么会出现这个地址?localhost 不应该指向的是 127.0.0.1 么?我们使用 dignslookup 之后发现 localhost 的确是 127.0.0.1

我们修改了应用的配置,让这个调用直接调用 127.0.0.1 结果发现这个时候服务就正常了,然后我们在机器上抓包之后发现 localhost 竟然走了域名解析! 并且 localhost 这个域名在我们内网还被注册了,解析出来的地址就是最开始发现的这个不知名的地址

小结

所以我们下意识认为的域名解析流程应该是这样的,先去找 /etc/hosts 文件,localhost 找到了(默认是 127.0.0.1)就返回了

排查之后发现,实际上的流程是这样的,先做了 DNS 查询 DNS 没查到然后去查了 /etc/hosts 文件

直到有一天,我们的内网域名解析中添加了一个 localhost 的域名解析,就直接查询成功返回了

复现

我们先使用一段简单的代码复现一下,简单请求一下 localhost 就行了

package mainimport ("fmt""net/http"
)func main() {client := &http.Client{}_, err := client.Get("http://localhost:8080")fmt.Println(err)
}

然后我们使用 GODEBUG="netdns=go+2" 环境变量执行程序,带上这个环境变量之后程序运行时就会输出是先执行 dns 查询还是先从 /etc/hosts 文件进行查询

GODEBUG="netdns=go+2" go run main.go
go package net: GODEBUG setting forcing use of Go's resolver
go package net: hostLookupOrder(localhost) = files,dns
Get "http://localhost:8080": dial tcp [::1]:8080: connect: connection refused

上面显示的 files,dns 的意思就是先从 /etc/hosts 文件中查询,再去查询 dns 结果,但是我们当时服务的运行结果是 dns,files 这个问题出现在哪里呢?和 Go 的版本以及本地环境有关系

我们使用 Docker 模拟了线上环境,我们线上也是用的 Docker

FROM golang:1.15 as builderWORKDIR /appCOPY main.go main.go
COPY run.sh run.shENV CGO_ENABLED=0
ENV GOOS=linuxRUN go build main.goFROM alpine:3WORKDIR /appCOPY --from=builder /app /app
COPY run.sh run.shRUN chmod +x run.shENV GODEBUG="netdns=go+2"
ENV CGO_ENABLED=0
ENV GOOS=linuxCMD /app/run.sh

使用这个容器运行的结果如下,可以看到已经变成了 dns,files 为什么会这样呢?

go package net: built with netgo build tag; using Go's DNS resolver
go package net: hostLookupOrder(localhost) = dns,files
Get "http://localhost:8080": dial tcp 127.0.0.1:8080: connect: connection refused

排查

src/net/dnsclient_unix.go

Go 中定义了下面几种 DNS 解析顺序,其中 files 表示查询 /etc/hosts 文件,dns 表示执行 dns 查询

// hostLookupOrder specifies the order of LookupHost lookup strategies.
// It is basically a simplified representation of nsswitch.conf.
// "files" means /etc/hosts.
type hostLookupOrder intconst (// hostLookupCgo means defer to cgo.hostLookupCgo      hostLookupOrder = iotahostLookupFilesDNS                 // files firsthostLookupDNSFiles                 // dns firsthostLookupFiles                    // only fileshostLookupDNS                      // only DNS
)

在 src/net/conf.go 中可以看到

Go 会先根据一些初始条件判断查询的顺序,然后就查找 /etc/nsswitch.conf 文件中的 hosts 配置项,如果不存在就会走一些回退逻辑。这次的问题出现在这个回退逻辑上

func (c *conf) hostLookupOrder(r *Resolver, hostname string) (ret hostLookupOrder) {// ... 省略nss := c.nsssrcs := nss.sources["hosts"]// If /etc/nsswitch.conf doesn't exist or doesn't specify any// sources for "hosts", assume Go's DNS will work fine.if os.IsNotExist(nss.err) || (nss.err == nil && len(srcs) == 0) {if c.goos == "solaris" {// illumos defaults to "nis [NOTFOUND=return] files"return fallbackOrder}if c.goos == "linux" {// glibc says the default is "dns [!UNAVAIL=return] files"// https://www.gnu.org/software/libc/manual/html_node/Notes-on-NSS-Configuration-File.html.return hostLookupDNSFiles}return hostLookupFilesDNS}if nss.err != nil {// We failed to parse or open nsswitch.conf, so// conservatively assume we should use cgo if it's// available.return fallbackOrder}
}

通过上面的代码我们可以发现,当前系统如果是 linux 并且不存在 /etc/nsswitch.conf 文件的时候,会直接返回 dns,files 的顺序,这个是参考了 glibc 的实现[^2]

这个问题其实一般在虚拟机上没有问题,因为一般操作系统都会默认有这个配置文件,但是容器化之后我们一般喜欢使用 alpine linux 这种比较小的基础镜像,alpine 中就不存在的 /etc/nsswitch.conf 这个文件,所以就有可能会出现问题

上面这段逻辑不能再 1.16 中进行复现,是因为 1.16 已经修改了这个逻辑,主要就是把 linux 的这个判断分支删除掉了,感兴趣可以看这个修改记录[^3] 和这个 issue[^4]

总结

最大的感受就是经验主义害死人,很多时候由于我们知识点的原因所以可能会出现一些和我们认为的常识相违背的地方,这个时候就需要大胆假设小心求证了

针对这次这个问题的修复方案,我们是直接先删除了 localhost 的解析,复盘之后给出我不成熟的几点小建议

  • 公司内网就不要搞注册 localhost 域名这种骚操作了

  • 基础镜像的维护很重要,建议大家最好能够统一一个基础镜像这样不仅仅可以减少一些磁盘空间,同时还可以做一些统一的变更,例如这次这种就可以直接在基础镜像加上 /etc/nsswitch.conf 文件,避免其他业务也进坑里

  • 如果没有什么特别的版本依赖(绝大部分应用其实都没有)Go 版本建议升级 1.16 可以省很多事

  • dns 解析并不一定会先查询 hosts 文件,除了这种默认的情况外,还可以手动修改  /etc/nsswitch.conf 文件,调整解析的顺序,这个感兴趣的话可以试试

这篇文章还试着用 figma 做了几个小动画,感觉还是不错,后续有空写文章可以再搞搞(曹大不要再卷了,快学不动了)

参考文献

[^1]: Go 1.14 标准库源码: https://github.com/golang/go/blob/go1.14/src/net/conf.go

[^2]: glibc 实现 https://www.gnu.org/software/libc/manual/html_node/Notes-on-NSS-Configuration-File.html

[^3]: 修改记录: https://github.com/golang/go/commit/c80022204e8fc36ec487888d471de27a5ea47e17#diff-a7c29e18c1a96d08fed3e81f367d079d14c53ea85d739e7460b21fb29a063128

[^4]: https://github.com/golang/go/issues/35305

- END -

扫码关注公众号「网管叨bi叨」

给网管个星标,第一时间吸我的知识 

localhost 就一定是 localhost 么?相关推荐

  1. VUE_APP_BASE_API = 'http://localhost:8082' 不能写localhost

    VUE_APP_BASE_API = http://localhost:8082 不能写localhost 是因为在真实的生产环境中,应用程序运行的服务器的主机名并不是"localhost& ...

  2. Proxy error: Could not proxy request /getInfo from localhost:81 to http://localhost:8080/.

    Proxy error: Could not proxy request /getInfo from localhost:81 to http://localhost:8080/. 项目启动时,我的登 ...

  3. Proxy error: Could not proxy request /api/ from localhost:8080 to http://localhost:80

    很多扯淡的,找了半天,终于在stackoverflow找到答案. 1.先保证格式正确, 2.请求地址正确且可访问, 3.加上'http://' ` // 错误示范 devServer: {proxy: ...

  4. 网站无法用localhost打开,修复localhost一切常见错误,一次性去除病根

    作者:Lccee 转载自:Lccee博客 网上那些很多解决方案都是没用的,有些答案根本还没验证就发出来了,网上搜出的答案根本没用. 如果遇到此问题直接使用PHPCUSTOM功能大全里的localhos ...

  5. Proxy error: Could not proxy request /students from localhost:8080 to http://localhost:5000/.See ht

    正在练习怎么跨区代理,按步骤一样的写,npm run serve 之后就出现错误 浏览器运行报错: 终端报错: 原因: 这个里面给的文件没有打开,打开之后就正常了

  6. 启动HDFS时报错localhost: Warning: Permanently added ‘localhost‘ (ECDSA)Permission denied

    由于本人是新手,网上很多帖子也没看懂. 我按照厦门大学林子雨老师的教程重新装了几遍后发现: 如果配置ssh时没有配置无密码登录,便会出现如上报错,而配成无密码登陆后,便成功了,如下 解决方法如下:(图 ...

  7. [Err] 1449 - The user specified as a definer ('admin_isbox'@'localhost') does not exist

    晚上加班调用一个远程拷贝的本地Mysql的储存过程,报错:[Err] 1449 - The user specified as a definer ('admin_isbox'@'localhost' ...

  8. 【SSL】java keytool工具创建自己的(localhost)(JKS)证书库

    前言 java : jdk1.8 证书库:用于在本地测试的证书库,域名为:locahost. 证书库密码:密码为"localhost". 证书库位置:当前路径 证书库文件名:loc ...

  9. 本地连接时,通过localhost不能登陆到指定的端口

    本地连接时,通过localhost不能登陆到指定的端口 朋友说他的一台服务器上,装了多个mysql,用了不同的端口,通过localhost的方式指定端口时,连上的还是3306的端口 mysql -ur ...

最新文章

  1. J0ker的CISSP之路:复习Access Control(10)
  2. Sqlserver数据库的恢复
  3. matlab中结束脚本运行_疫情当前,生活向前 | MATLAB 一直 Online 等你
  4. .NET Framework 工具
  5. Spark History Server配置及其启动
  6. (转)Hibernate的一级缓存
  7. python按键按下改变数值_「正点原子NANO STM32开发板资料连载」第十六章电容触摸按键实验...
  8. ollydbg调试使用
  9. 查看win10的产品密钥过期时间
  10. Spring的事务传播特性
  11. 汽车冬季养护的四个重点
  12. 通过定义函数,来实现判断1-100之间奇数的目的
  13. java日期 国际化_java中国际化的时间处理
  14. FPGA—蜂鸣器播放《两只老虎》
  15. Excel表格之道 学习笔记(四)
  16. ARP 地址解析协议 IP地址到MAC地址的转换过程
  17. PARAMETERS、SELECTION OPTIONS和SELECTION SCREEN的全部用法
  18. Word embeddings in 2017: Trends and future directions (2017年里的词嵌入:趋势和未来方向)
  19. java 解析 manifest_详解Manifest
  20. Jenkins+Jmeter+Ant 生成接口测试报告并发送邮件

热门文章

  1. 排序学习之---插入排序
  2. 越努力越幸运--动态数组vector
  3. 图片数字型的九九乘法表
  4. mysql之TIMESTAMP(时间戳)用法详解
  5. Windows系统安全从定制IP策略开始
  6. 开源漏洞扫描工具(OWASP-Dependency-Check)探索
  7. Linux服务器运维安全策略
  8. C++程序设计基础(7)位运算
  9. 理解JavaScript里this关键字
  10. java23中设计模式——行为模式——Chain of Responsibility(职责链)