面试官:如何将多个容器暴露到一个端口上?问倒一大片。。。

2023-11-07 11:27:16 浏览数 (1)

大家好,我是民工哥!

富 Web 时代,应用变得越来越强大,与此同时也越来越复杂。集群部署、隔离环境、灰度发布以及动态扩容缺一不可,而容器化则成为中间的必要桥梁。

IT 软件中所说的 “Docker” ,是指容器化技术,用于支持创建和使用 Linux 容器。而 Docker 技术就是这样一种神奇的存在:懂,万物皆可容器化;不懂,则重复“搬砖”,繁忙而不自知。

我们的容器需要对外提供访问的话,就是必须使用端口暴露。

Docker 容器暴露端口的形式有四种:

代码语言:javascript复制
-p    
#将指定的容器端口映射到宿主机所有地址的一个随机端口

-p : 
#将容器端口映射到指定的主机端口

-p ::
#将容器端口映射到主机指定ip的随机端口

-p ::
#将容器端口映射到指定主机ip的指定端口

在日常工作环境中,我们会部署多个相同的服务来对外提供服务,这样可以有效保证集群的高可用性,从而使用户得到很好的体验。

那么,如果多个容器提供一个服务,对外只暴露一个端口,怎么做呢?

通常有以下三种主流方法。

反向代理

当请求达到后,通过反向代理比如nginx、haproxy等,负载均衡的方式将流量转发到后端不同的容器里面。对外就可以暴露一个端口了。

步骤一:创建一个网络

首先,我们需要创建一个网络,使得多个容器能够相互通信。我们可以使用Docker命令docker network create来创建网络。下面是创建一个名为my-network的网络的代码示例:

代码语言:javascript复制
docker network create my-network

这将创建一个名为my-network的网络,供后续的容器使用。

启动多个容器

接下来,我们需要启动多个容器,并将它们连接到之前创建的网络上。同时,我们需要将容器的端口映射到宿主机的端口上,以便外部可以访问。以下是启动三个容器并进行端口映射的代码示例:

代码语言:javascript复制
docker run -d --network my-network --name container1 -p 8080:80 image1
docker run -d --network my-network --name container2 -p 8080:80 image2
docker run -d --network my-network --name container3 -p 8080:80 image3

上述代码中,我们使用docker run命令分别启动了三个容器,并指定了容器的网络为my-network。--name参数用于指定容器的名称,-p参数用于进行端口映射,将容器的80端口映射到宿主机的8080端口上。

步骤三:配置负载均衡

最后,我们需要配置一个负载均衡容器,将外部对于宿主机的访问请求分发到多个容器上。在本示例中,我们使用了Nginx作为负载均衡容器。以下是配置负载均衡容器的代码示例:

代码语言:javascript复制
docker run -d --network my-network -p 8080:80 --name load-balancer nginx

上述代码中,我们使用docker run命令启动了一个Nginx容器,并将容器的网络设置为my-network。-p参数用于进行端口映射,将容器的80端口映射到宿主机的8080端口上。

DNAT

熟悉k8s Nodeport 实现的话就会发现,k8s里面service iptables 实现就是基于DNAT。关于k8s Nodeport的实现参见之前k8s的文章。我们可以先通过docker 命令启动两个容器:

代码语言:javascript复制
$ CONT_PORT=9090

$ docker run -d --rm 
  --name http_server_foo 
  -e INSTANCE=foo 
  -e PORT=$CONT_PORT 
  http_server

$ docker run -d --rm 
  --name http_server_bar 
  -e INSTANCE=bar 
  -e PORT=$CONT_PORT 
  http_server

获取这个两个容器的IP

代码语言:javascript复制
$ CONT_FOO_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' http_server_foo)
$ echo $CONT_FOO_IP
172.17.0.2

$ CONT_BAR_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' http_server_bar)
$ echo $CONT_BAR_IP
172.17.0.3

然后创建 DNAT 规则

代码语言:javascript复制
$ FRONT_PORT=80

# Not secure! Use global ACCEPT only for tests.
$ sudo iptables -I FORWARD 1 -j ACCEPT

# DNAT 本地流量,宿主机访问
$ sudo iptables -t nat -I OUTPUT 1 -p tcp --dport $FRONT_PORT 
    -m statistic --mode random --probability 1.0  
    -j DNAT --to-destination $CONT_FOO_IP:$CONT_PORT

$ sudo iptables -t nat -I OUTPUT 1 -p tcp --dport $FRONT_PORT 
    -m statistic --mode random --probability 0.5  
    -j DNAT --to-destination $CONT_BAR_IP:$CONT_PORT

# DNAT 外部流量,其他机器访问
$ sudo iptables -t nat -I PREROUTING 1 -p tcp --dport $FRONT_PORT 
    -m statistic --mode random --probability 1.0  
    -j DNAT --to-destination $CONT_FOO_IP:$CONT_PORT

$ sudo iptables -t nat -I PREROUTING 1 -p tcp --dport $FRONT_PORT 
    -m statistic --mode random --probability 0.5  
    -j DNAT --to-destination $CONT_BAR_IP:$CONT_PORT

通过上面模仿k8s Nodeport的实现,就是可以轻松实现一个端口对应多个容器了。

多服务监听

这个方法稍微hack 一点,其实 socket 在listen 的时候,支持 SO_REUSEPORT ,它的效果是运行多个程序监听同一个端口。

代码语言:javascript复制
func main() {
    lc := net.ListenConfig{
        Control: func(network, address string, conn syscall.RawConn) error {
            var operr error
            if err := conn.Control(func(fd uintptr) {
                operr = syscall.SetsockoptInt(
                    int(fd),
                    unix.SOL_SOCKET,
                    unix.SO_REUSEPORT,
                    1,
                )
            }); err != nil {
                return err
            }
            return operr
        },
    }

    ln, err := lc.Listen(
        context.Background(),
        "tcp",
        os.Getenv("HOST") ":" os.Getenv("PORT"),
    )
    if err != nil {
        panic(err)
    }

    http.HandleFunc("/", func(w http.ResponseWriter, _req *http.Request) {
        w.Write([]byte(fmt.Sprintf("Hello from %sn", os.Getenv("INSTANCE"))))
    })

    if err := http.Serve(ln, nil); err != nil {
        panic(err)
    }
}

这个我们就启动多个容器,共享 network namespace,同时监听这个端口了。

参考链接:https://www.toutiao.com/article/7055968933 785944583/ https://blog.51cto.com/u_16213361/7531581

0 人点赞