Docker技術原理のLinux Namespace(容器隔離)

11427 ワード

0.はじめに
まず、実行されるコンテナが、独立性とリソースの制限を受けたLinuxプロセスであることを知っておく必要があります.はい、プロセスです.ここでは主にDocker容器の隔離を実現するために用いられる技術Linux Namespaceについて検討する.
1.Linux Namespaceについて
Linuxでは、次のNamespaceが提供されています.
Namespace   Constant          Isolates
Cgroup      CLONE_NEWCGROUP   Cgroup root directory
IPC         CLONE_NEWIPC      System V IPC, POSIX message queues
Network     CLONE_NEWNET      Network devices, stacks, ports, etc.
Mount       CLONE_NEWNS       Mount points
PID         CLONE_NEWPID      Process IDs
User        CLONE_NEWUSER     User and group IDs
UTS         CLONE_NEWUTS      Hostname and NIS domain name

以上のNamespaceでは、プロセスのCgroup root、プロセス間通信、ネットワーク、ファイルシステムマウントポイント、プロセスID、ユーザーとグループ、ホスト名ドメイン名などを分離しています.
コンテナの作成(プロセス)には、主に3つのシステム呼び出しが使用されます.
  • clone()–スレッドのシステム呼び出しを実装し、新しいプロセスを作成し、上記のパラメータによって分離
  • に達することができる.
  • unshare()–プロセスをnamespace
  • から解放
  • setns()–プロセスをnamespace
  • に追加
    2.例を挙げる(PID namespace)
    1)コンテナの起動
    $ docker run -it busybox /bin/sh
    / #
    

    2)コンテナ内のプロセスidを表示する(/bin/shのpid=1が表示される)
    / # ps
    PID   USER     TIME  COMMAND
        1 root      0:00 /bin/sh
        5 root      0:00 ps
    

    3)ホスト内のこの/bin/shのプロセスidを表示する
    # ps -ef |grep busy
    root      3702  3680  0 15:53 pts/0    00:00:00 docker run -it busybox /bin/sh
    

    Dockerで最初に実行した/bin/shは、このコンテナ内部の1番目のプロセス(PID=1)であり、シンクホスト上でPID=3702が見られることがわかります.これは,前に実行した/bin/shが,Dockerによってホストとは全く異なる世界に隔離されていることを意味する.
    これはDockerがコンテナを起動するときにPID namespaceを使用したことです
    int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
    

    このとき、DockerはこのPID=3702のプロセスが開始されたときに「目障り法」を施し、このnamespaceに属さないプロセスを永遠に見ることができないようにします.このメカニズムは,隔離されたアプリケーションのプロセス空間を手足で操作し,PID=1のように再計算されたプロセス番号しか見えないようにする.しかし、実際には、ホストのオペレーティングシステムでは、3702番目のプロセスだった.
    その後、PID namespaceだけで上記のclone()を使用してプロセスを作成し、psやtopなどのコマンドを表示すると、すべてのプロセスが表示されます.説明は完全に隔離されていません.これは、ps、topのようなコマンドが/procファイルシステムを読み、pidを隔離するプロセスとホストが同じ/procファイルシステムを使用しているため、これらのコマンドが表示されるものは同じです.したがって、ファイルシステムなどの他のnamespaceを分離する必要があります.
    3.Dockerソースと照合
    dockerコンテナを起動すると、dockerdが提供する/containers/{name:.*}/startインタフェースが呼び出され、コンテナが起動されます.dockerサービスは要求を受信すると、呼び出し関係は次のようになります.
    //  http handler
    router.NewPostRoute("/containers/{name:.*}/start", r.postContainersStart)
    //
    func (s *containerRouter) postContainersStart(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error 
    //
    func (daemon *Daemon) ContainerStart(name string, hostConfig *containertypes.HostConfig, checkpoint string, checkpointDir string) error 
    //
    func (daemon *Daemon) containerStart(container *container.Container, checkpoint string, checkpointDir string, resetRestartManager bool) (err error) {
        //...
        spec, err := daemon.createSpec(container)
        //...
        err = daemon.containerd.Create(context.Background(), container.ID, spec, createOptions)
        //...
        pid, err := daemon.containerd.Start(context.Background(), container.ID, checkpointDir,
            container.StreamConfig.Stdin() != nil || container.Config.Tty,
            container.InitializeStdio)
        //...
        container.SetRunning(pid, true)
        //...
    }
    
    Daemon.containerStartインタフェースでコンテナが作成され、起動されていることがわかります.コンテナを作成するときに入力されるspecパラメータにはnamespaceが含まれています.daemon.createSpec(container)インタフェースで返されるspecとは何かを見てみましょう.
    func (daemon *Daemon) createSpec(c *container.Container) (retSpec *specs.Spec, err error) {
        s := oci.DefaultSpec()
        //...
        if err := setUser(&s, c); err != nil {
            return nil, fmt.Errorf("linux spec user: %v", err)
        }
        if err := setNamespaces(daemon, &s, c); err != nil {
            return nil, fmt.Errorf("linux spec namespaces: %v", err)
        }
        //...
        return &s
    }
    
    //oci.DefaultSpec()   DefaultLinuxSpec,       spec    namespace
    func DefaultLinuxSpec() specs.Spec {
        s := specs.Spec{
            Version: specs.Version,
            Process: &specs.Process{
                Capabilities: &specs.LinuxCapabilities{
                    Bounding:    defaultCapabilities(),
                    Permitted:   defaultCapabilities(),
                    Inheritable: defaultCapabilities(),
                    Effective:   defaultCapabilities(),
                },
            },
            Root: &specs.Root{},
        }
        s.Mounts = []specs.Mount{
            {
                Destination: "/proc",
                Type:        "proc",
                Source:      "proc",
                Options:     []string{"nosuid", "noexec", "nodev"},
            },
            {
                Destination: "/sys/fs/cgroup",
                Type:        "cgroup",
                Source:      "cgroup",
                Options:     []string{"ro", "nosuid", "noexec", "nodev"},
            },
            //...
        }
    
        s.Linux = &specs.Linux{
            //...
            Namespaces: []specs.LinuxNamespace{
                {Type: "mount"},
                {Type: "network"},
                {Type: "uts"},
                {Type: "pid"},
                {Type: "ipc"},
            },
            //...
        //...
        return s
    }
    
    //  setNamespaces          namespace    
    func setNamespaces(daemon *Daemon, s *specs.Spec, c *container.Container) error {
        userNS := false
        // user
        if c.HostConfig.UsernsMode.IsPrivate() {
            uidMap := daemon.idMapping.UIDs()
            if uidMap != nil {
                userNS = true
                ns := specs.LinuxNamespace{Type: "user"}
                setNamespace(s, ns)
                s.Linux.UIDMappings = specMapping(uidMap)
                s.Linux.GIDMappings = specMapping(daemon.idMapping.GIDs())
            }
        }
        // network
        if !c.Config.NetworkDisabled {
            ns := specs.LinuxNamespace{Type: "network"}
            parts := strings.SplitN(string(c.HostConfig.NetworkMode), ":", 2)
            if parts[0] == "container" {
                nc, err := daemon.getNetworkedContainer(c.ID, c.HostConfig.NetworkMode.ConnectedContainer())
                if err != nil {
                    return err
                }
                ns.Path = fmt.Sprintf("/proc/%d/ns/net", nc.State.GetPID())
                if userNS {
                    // to share a net namespace, they must also share a user namespace
                    nsUser := specs.LinuxNamespace{Type: "user"}
                    nsUser.Path = fmt.Sprintf("/proc/%d/ns/user", nc.State.GetPID())
                    setNamespace(s, nsUser)
                }
            } else if c.HostConfig.NetworkMode.IsHost() {
                ns.Path = c.NetworkSettings.SandboxKey
            }
            setNamespace(s, ns)
        }
    
        // ipc
        ipcMode := c.HostConfig.IpcMode
        switch {
        case ipcMode.IsContainer():
            ns := specs.LinuxNamespace{Type: "ipc"}
            ic, err := daemon.getIpcContainer(ipcMode.Container())
            if err != nil {
                return err
            }
            ns.Path = fmt.Sprintf("/proc/%d/ns/ipc", ic.State.GetPID())
            setNamespace(s, ns)
            if userNS {
                // to share an IPC namespace, they must also share a user namespace
                nsUser := specs.LinuxNamespace{Type: "user"}
                nsUser.Path = fmt.Sprintf("/proc/%d/ns/user", ic.State.GetPID())
                setNamespace(s, nsUser)
            }
        case ipcMode.IsHost():
            oci.RemoveNamespace(s, specs.LinuxNamespaceType("ipc"))
        case ipcMode.IsEmpty():
            // A container was created by an older version of the daemon.
            // The default behavior used to be what is now called "shareable".
            fallthrough
        case ipcMode.IsPrivate(), ipcMode.IsShareable(), ipcMode.IsNone():
            ns := specs.LinuxNamespace{Type: "ipc"}
            setNamespace(s, ns)
        default:
            return fmt.Errorf("Invalid IPC mode: %v", ipcMode)
        }
    
        // pid
        if c.HostConfig.PidMode.IsContainer() {
            ns := specs.LinuxNamespace{Type: "pid"}
            pc, err := daemon.getPidContainer(c)
            if err != nil {
                return err
            }
            ns.Path = fmt.Sprintf("/proc/%d/ns/pid", pc.State.GetPID())
            setNamespace(s, ns)
            if userNS {
                // to share a PID namespace, they must also share a user namespace
                nsUser := specs.LinuxNamespace{Type: "user"}
                nsUser.Path = fmt.Sprintf("/proc/%d/ns/user", pc.State.GetPID())
                setNamespace(s, nsUser)
            }
        } else if c.HostConfig.PidMode.IsHost() {
            oci.RemoveNamespace(s, specs.LinuxNamespaceType("pid"))
        } else {
            ns := specs.LinuxNamespace{Type: "pid"}
            setNamespace(s, ns)
        }
        // uts
        if c.HostConfig.UTSMode.IsHost() {
            oci.RemoveNamespace(s, specs.LinuxNamespaceType("uts"))
            s.Hostname = ""
        }
    
        return nil
    }
    func setNamespace(s *specs.Spec, ns specs.LinuxNamespace) {
        for i, n := range s.Linux.Namespaces {
            if n.Type == ns.Type {
                s.Linux.Namespaces[i] = ns
                return
            }
        }
        s.Linux.Namespaces = append(s.Linux.Namespaces, ns)
    }
    

    実は以前Dockerがコンテナを作成して、namespaceを取得するのはCloneFlags関数で、後にオープンコンテナ計画(OCI)規範があった後、上のコードでコンテナを作成するように変更しました.OCIの前のコードは次のとおりです.
    var namespaceInfo = map[NamespaceType]int{
        NEWNET:  unix.CLONE_NEWNET,
        NEWNS:   unix.CLONE_NEWNS,
        NEWUSER: unix.CLONE_NEWUSER,
        NEWIPC:  unix.CLONE_NEWIPC,
        NEWUTS:  unix.CLONE_NEWUTS,
        NEWPID:  unix.CLONE_NEWPID,
    }
    
    // CloneFlags parses the container's Namespaces options to set the correct
    // flags on clone, unshare. This function returns flags only for new namespaces.
    func (n *Namespaces) CloneFlags() uintptr {
        var flag int
        for _, v := range *n {
            if v.Path != "" {
                continue
            }
            flag |= namespaceInfo[v.Type]
        }
        return uintptr(flag)
    }
    func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, childPipe *os.File) (*initProcess, error) {     t := "_LIBCONTAINER_INITTYPE=standard"
        //
        //  ,    ~
        //
        cloneFlags := c.config.Namespaces.CloneFlags()
        if cloneFlags&syscall.CLONE_NEWUSER != 0 {
            if err := c.addUidGidMappings(cmd.SysProcAttr); err != nil {
                // user mappings are not supported
                return nil, err
            }
            enableSetgroups(cmd.SysProcAttr)
            // Default to root user when user namespaces are enabled.
            if cmd.SysProcAttr.Credential == nil {
                cmd.SysProcAttr.Credential = &syscall.Credential{}
            }
        }
        cmd.Env = append(cmd.Env, t)
        cmd.SysProcAttr.Cloneflags = cloneFlags
        return &initProcess{
            cmd:        cmd,
            childPipe:  childPipe,
            parentPipe: parentPipe,
            manager:    c.cgroupManager,
            config:     c.newInitConfig(p),
        }, nil
    }
    

    現在、コンテナ実行時には、OCIというコンテナ実行時仕様により、下位レベルのLinuxオペレーティングシステムとインタラクティブになります.すなわち、コンテナ操作要求をLinuxオペレーティングシステムへの呼び出し(Linux NamespaceやCgroupsなどの操作)に翻訳します.
    リファレンス
  • https://coolshell.cn/articles/17010.html
  • https://github.com/moby/moby
  • http://lwn.net/Articles/531114/
  • http://man7.org/linux/man-pages/man7/namespaces.7.html