NYC's Blog - Docker http://niyanchun.com/tag/docker/ zh-CN Sat, 25 Feb 2017 13:15:00 +0800 Sat, 25 Feb 2017 13:15:00 +0800 Ubuntu16.04手动部署Kubernetes(3)——Dashboard和KubeDNS部署 http://niyanchun.com/deploy-kubernetes-step-by-step-on-trusty-section-3.html http://niyanchun.com/deploy-kubernetes-step-by-step-on-trusty-section-3.html Sat, 25 Feb 2017 13:15:00 +0800 NYC 今天继续接着前文:

继续来部署Kubernetes。今天主要来部署Dashboard和KubeDNS,主要是后者。因为Kubernetes在每个Pod内都会起一个很小的“系统”容器pause-amd64:3.0来实现Pod内的网络,而这个容器默认会去从Google的Registry拉,但国内如果没有梯子访问不了。所以我pull了一个,传到了国内的镜像仓库,我们改下每个Node节点上的kubelet的配置,增加--pod_infra_container_image选项,这个选项可以指定从哪里pull这个基础镜像,我修改后的配置如下:

KUBELET_ARGS="--api-servers=http://192.168.56.101:8080 --cluster-dns=169.169.0.2 --cluster-domain=cluster.local --hostname-override=192.168.56.101 --logtostderr=false --log-dir=/var/log/kubernetes --v=2 --pod_infra_container_image=hub.c.163.com/allan1991/pause-amd64:3.0"

然后重启Kubelet:systemctl restart kubelet.service.另外,

除了这个镜像外,还有很多镜像也会有这个问题,所以我都传到了国内的镜像仓库,本文介绍时都使用国内我自己传的。大家没有梯子的话,直接使用我用的这个就可以了。OK,下面开始今天的主题。

部署Dashboard

Kubernetes提供了一个基础的图形化界面,就是这个Dashboard。基本上使用kubectl可以实现的功能现在都可以通过界面去做了,本质上它俩都是调用Kubernetes API。而部署这个Dashboard也非常简答,就是创建一个Pod和一个Service。最新的创建dashboard的yaml文件可以去查看官方:https://rawgit.com/kubernetes/dashboard/master/src/deploy/kubernetes-dashboard.yaml。我的yaml文档如下:

# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Configuration to deploy release version of the Dashboard UI.
#
# Example usage: kubectl create -f <this_file>

kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  labels:
    app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kube-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kubernetes-dashboard
  template:
    metadata:
      labels:
        app: kubernetes-dashboard
      # Comment the following annotation if Dashboard must not be deployed on master
      annotations:
        scheduler.alpha.kubernetes.io/tolerations: |
          [
            {
              "key": "dedicated",
              "operator": "Equal",
              "value": "master",
              "effect": "NoSchedule"
            }
          ]
    spec:
      containers:
      - name: kubernetes-dashboard
        image: hub.c.163.com/allan1991/kubernetes-dashboard-amd64:v1.5.1
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 9090
          protocol: TCP
        args:
          # Uncomment the following line to manually specify Kubernetes API server Host
          # If not specified, Dashboard will attempt to auto discover the API server and connect
          # to it. Uncomment only if the default does not work.
          - --apiserver-host=http://192.168.56.101:8080
        livenessProbe:
          httpGet:
            path: /
            port: 9090
          initialDelaySeconds: 30
          timeoutSeconds: 30
---
kind: Service
apiVersion: v1
metadata:
  labels:
    app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kube-system
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 9090
  selector:
    app: kubernetes-dashboard

这里需要注意的是--apiserver-host默认是注释掉的,我有次部署时虽然找到了APIServer,但是连接被决绝,但是显式的去掉这个注释,写上APIServer的地址后就好了,所以我推荐还是写上比较好。然后使用kubectl create -f <this_file>就可以创建了。待这个DeploymentService创建成功后,执行kubectl --namespace=kube-system get svc命令查看映射的端口号,当然我们也可以在yaml文件里面使用nodePort指定为我们想要的端口,这里我没有指定,让系统自己选择,因为选择好以后就会固定,不会再变了。我的创建好有如下:

Master➜  kube-system kubectl --namespace=kube-system get svc
NAME                   CLUSTER-IP       EXTERNAL-IP   PORT(S)         AGE
kubernetes-dashboard   169.169.162.93   <nodes>       80:10977/TCP    3d

这里需要注意执行命令时要加上--namespace=kube-system。前面我们也已经介绍过了,Kubernetes部署好后默认有两个namespace:defaultkube-system。用户自己的默认在default下,kubectl默认也使用这个namespace;Kubernetes自己的系统模块在kube-system下面。这样我们的Dashboard已经创建好了,通过APIServer的IP加上面的端口号10977就可以访问界面了:

Kubernetes_Dashboard.png

界面还是非常的清爽的,使用起来也很方便和简单,所以这里就不详细介绍了,可以参考官方图文教程:https://kubernetes.io/docs/user-guide/ui/

部署KubeDNS

在这之前我们先要搞清楚为什么需要DNS?是为了实现服务发现。而且DNS其实是Kubernetes 1.3开始才内置的功能,在这之前是利用Linux环境的方式来做的服务发现。这里我们举例说明一下利用环境变量做服务发现的过程:比如我们部署了几个服务,这些服务需要找到彼此。利用环境变量的方式就是在创建Pod的时候将Service的一些信息(主要是IP+端口)以环境变量的方式(环境变量按照一定的规则命名)注入到Pod上的容器内,这样就做到了服务发现。这种方式虽然简单,但显然有两个不足:

  1. 随着服务越来越多,环境变量会越来越多。
  2. Pod必须在Service之后创建。

所以后来就出现了DNS,利用这种方式去做服务发现就不会有上面所说的限制了。那DNS又是怎么做服务发现的呢,再举个例子:比如我们在Kubernetes的叫bar的namespace下面创建了一个Service叫foo。那么在bar下运行的Pod只需要做一个foo DNS查询便可获取到这个服务的IP和Port。如果是运行在其他namespace下的Pod想要查询foo这个服务,则只需要查询foo.bar即可。当然一般还会有一个域名(domain)。更详细的信息请参阅https://kubernetes.io/docs/admin/dns/

起初,Kubernetes的DNS是使用SkyDNS来实现的,网上现在的教程也基本是基于此的。但是新版本的Kubernetes已经不需要SkyDNS了,它有了自己的DNS模块——KubeDNS。这也是本文要介绍的。SkyDNS依赖于etcd,而KubeDNS不需要etcd。KubeDNS的部署也很简单,官方已经提供了基础的yaml文件:https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/dns。下面是我的yaml文件:

kubedns-deployment.yaml:

# Copyright 2016 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# TODO - At some point, we need to rename all skydns-*.yaml.* files to kubedns-*.yaml.*
# Should keep target in cluster/addons/dns-horizontal-autoscaler/dns-horizontal-autoscaler.yaml
# in sync with this file.

# Warning: This is a file generated from the base underscore template file: skydns-rc.yaml.base

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: kube-dns
  namespace: kube-system
  labels:
    k8s-app: kube-dns
    kubernetes.io/cluster-service: "true"
spec:
  # replicas: not specified here:
  # 1. In order to make Addon Manager do not reconcile this replicas parameter.
  # 2. Default is 1.
  # 3. Will be tuned in real time if DNS horizontal auto-scaling is turned on.
  strategy:
    rollingUpdate:
      maxSurge: 10%
      maxUnavailable: 0
  selector:
    matchLabels:
      k8s-app: kube-dns
  template:
    metadata:
      labels:
        k8s-app: kube-dns
      annotations:
        scheduler.alpha.kubernetes.io/critical-pod: ''
        scheduler.alpha.kubernetes.io/tolerations: '[{"key":"CriticalAddonsOnly", "operator":"Exists"}]'
    spec:
      containers:
      - name: kubedns
        image: hub.c.163.com/allan1991/kubedns-amd64:1.9
        resources:
          # TODO: Set memory limits when we've profiled the container for large
          # clusters, then set request = limit to keep this container in
          # guaranteed class. Currently, this container falls into the
          # "burstable" category so the kubelet doesn't backoff from restarting it.
          limits:
            memory: 170Mi
          requests:
            cpu: 100m
            memory: 70Mi
        livenessProbe:
          httpGet:
            path: /healthz-kubedns
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 60
          timeoutSeconds: 5
          successThreshold: 1
          failureThreshold: 5
        readinessProbe:
          httpGet:
            path: /readiness
            port: 8081
            scheme: HTTP
          # we poll on pod startup for the Kubernetes master service and
          # only setup the /readiness HTTP server once that's available.
          initialDelaySeconds: 3
          timeoutSeconds: 5
        args:
        - --kube-master-url=http://192.168.56.101:8080
        - --domain=cluster.local.
        - --dns-port=10053
        - --config-map=kube-dns
        # This should be set to v=2 only after the new image (cut from 1.5) has
        # been released, otherwise we will flood the logs.
        - --v=0
        # {{ pillar['federations_domain_map'] }}
        env:
        - name: PROMETHEUS_PORT
          value: "10055"
        ports:
        - containerPort: 10053
          name: dns-local
          protocol: UDP
        - containerPort: 10053
          name: dns-tcp-local
          protocol: TCP
        - containerPort: 10055
          name: metrics
          protocol: TCP
      - name: dnsmasq
        image: hub.c.163.com/allan1991/kube-dnsmasq-amd64:1.4
        livenessProbe:
          httpGet:
            path: /healthz-dnsmasq
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 60
          timeoutSeconds: 5
          successThreshold: 1
          failureThreshold: 5
        args:
        - --cache-size=1000
        - --no-resolv
        - --server=127.0.0.1#10053
        - --log-facility=-
        ports:
        - containerPort: 53
          name: dns
          protocol: UDP
        - containerPort: 53
          name: dns-tcp
          protocol: TCP
        # see: https://github.com/kubernetes/kubernetes/issues/29055 for details
        resources:
          requests:
            cpu: 150m
            memory: 10Mi
      # - name: dnsmasq-metrics
      #   image: gcr.io/google_containers/dnsmasq-metrics-amd64:1.0
      #   livenessProbe:
      #     httpGet:
      #       path: /metrics
      #       port: 10054
      #       scheme: HTTP
      #     initialDelaySeconds: 60
      #     timeoutSeconds: 5
      #     successThreshold: 1
      #     failureThreshold: 5
      #   args:
      #   - --v=2
      #   - --logtostderr
      #   ports:
      #   - containerPort: 10054
      #     name: metrics
      #     protocol: TCP
      #   resources:
      #     requests:
      #       memory: 10Mi
      - name: healthz
        image: hub.c.163.com/allan1991/exechealthz-amd64:1.2
        resources:
          limits:
            memory: 50Mi
          requests:
            cpu: 10m
            # Note that this container shouldn't really need 50Mi of memory. The
            # limits are set higher than expected pending investigation on #29688.
            # The extra memory was stolen from the kubedns container to keep the
            # net memory requested by the pod constant.
            memory: 50Mi
        args:
        - --cmd=nslookup kubernetes.default.svc.cluster.local 127.0.0.1 >/dev/null
        - --url=/healthz-dnsmasq
        - --cmd=nslookup kubernetes.default.svc.cluster.local 127.0.0.1:10053 >/dev/null
        - --url=/healthz-kubedns
        - --port=8080
        - --quiet
        ports:
        - containerPort: 8080
          protocol: TCP
      dnsPolicy: Default  # Don't use cluster DNS.

这里我把dnsmasq-metrics这个container注释掉了,因为我没把这个镜像下载下来...不过没了这个也没什么影响,这个也是后来才新增的,原来是没有的。

kubedns-svc.yaml:

# Copyright 2016 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This file should be kept in sync with cluster/images/hyperkube/dns-svc.yaml

# TODO - At some point, we need to rename all skydns-*.yaml.* files to kubedns-*.yaml.*

# Warning: This is a file generated from the base underscore template file: skydns-svc.yaml.base

apiVersion: v1
kind: Service
metadata:
  name: kube-dns
  namespace: kube-system
  labels:
    k8s-app: kube-dns
    kubernetes.io/cluster-service: "true"
    kubernetes.io/name: "KubeDNS"
spec:
  selector:
    k8s-app: kube-dns
  clusterIP: 169.169.0.2
  ports:
  - name: dns
    port: 53
    protocol: UDP
  - name: dns-tcp
    port: 53
    protocol: TCP

这里需要注意下clusterIP,你可以不设置,让系统自己去选择。如果显式的设置的话,指定的IP必须在APIServer的--service-cluster-ip-range参数指定的网段内。我们可以用以下命令去检查是否创建成功:

Master➜  ~ kubectl --namespace=kube-system get deployment
NAME                   DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
kube-dns               1         1         1            1           1d
kubernetes-dashboard   1         1         1            1           3d
Master➜  ~ kubectl --namespace=kube-system get pod
NAME                                    READY     STATUS    RESTARTS   AGE
kube-dns-2528035289-r7vrq               3/3       Running   0          19h
kubernetes-dashboard-1077846755-lj7sc   1/1       Running   0          19h
Master➜  ~ kubectl --namespace=kube-system get svc
NAME                   CLUSTER-IP       EXTERNAL-IP   PORT(S)         AGE
kube-dns               169.169.0.2      <none>        53/UDP,53/TCP   1d
kubernetes-dashboard   169.169.162.93   <nodes>       80:10977/TCP    3d

Service、Deployment都OK后,我们需要在每个Node的Kubelet配置中加入DNS的信息,主要是--cluster-dns--cluster-domain,比如我的配置如下:

KUBELET_ARGS="--api-servers=http://192.168.56.101:8080 --cluster-dns=169.169.0.2 --cluster-domain=cluster.local --hostname-override=192.168.56.101 --logtostderr=false --log-dir=/var/log/kubernetes --v=2 --pod_infra_container_image=hub.c.163.com/allan1991/pause-amd64:3.0"

这里我们使用的是默认的Domain cluster.local,当然你可以在kubedns-deployment.yaml将所有的cluster.local换为你想要的,比如time-track.cn

最后我们来验证一下,首先创建一个Pod,里面运行一个busybox容器,主要是为了使用里面的nslookup命令:

busybox.yaml

apiVersion: v1
kind: Pod
metadata:
  name: busybox
  namespace: default
spec:
  containers:
  - image: hub.c.163.com/library/busybox
    command:
      - sleep
      - "3600"
    imagePullPolicy: IfNotPresent
    name: busybox
  restartPolicy: Always

然后我们再创建一个服务,比如mysql吧:

mysql.yaml

apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  ports:
    - port: 3306
  selector:
    app: mysql

然后我们在先创建的busybox里面通过DNS查询后创建的mysql服务,看是否可以查到:

Master➜  ~ kubectl exec busybox -c busybox -- nslookup mysql
Server:    169.169.0.2
Address 1: 169.169.0.2 kube-dns.kube-system.svc.cluster.local

Name:      mysql
Address 1: 169.169.96.10 mysql.default.svc.cluster.local


Master➜  ~ kubectl describe svc mysql
Name:            mysql
Namespace:        default
Labels:            <none>
Selector:        app=mysql
Type:            ClusterIP
IP:            169.169.96.10
Port:            <unset>    3306/TCP
Endpoints:        <none>
Session Affinity:    None
No events.

可以看到,成功的查到了。这样通过DNS变做到了服务发现,而且没有先后顺序的限制了。

]]>
0 http://niyanchun.com/deploy-kubernetes-step-by-step-on-trusty-section-3.html#comments http://niyanchun.com/feed/tag/docker/
Ubuntu16.04手动部署Kubernetes(2)——Flannel网络部署 http://niyanchun.com/deploy-kubernetes-step-by-step-on-trusty-section-2.html http://niyanchun.com/deploy-kubernetes-step-by-step-on-trusty-section-2.html Fri, 24 Feb 2017 17:30:00 +0800 NYC 在《Ubuntu16.04手动部署Kubernetes(1)——Master和Node部署》一文中,我们已经介绍了Kubernetes的Master和Node的手动部署,而且最后执行kubectl get node也成功看到了一个Node节点。本文接着上文继续部署,这次要部署的是网络。回想一下,Kubernetes有一个非常重要的特性就是任意Pod间都可以通过网络进行彼此访问,不管这些Pod是否在同一个Node上面。

Flannel介绍

我们知道docker默认的网络模型是创建一个虚拟网桥docker0,然后每创建一个容器,就创建一个虚拟的veth pair,其中一端关联到docker0这个网桥上,另一端使用Linux的网络命名空间技术映射到容器内的eth0设备,然后从网桥的地址段内给eth0接口分配一个IP地址。如下图(图片来自Google图片搜索,懒得画图了...):

docker-network.png

我们自己可以在机器上面创建几个容器,然后用ip a或者ifconfig命令看一下。比如我的:

Master➜  others ip a | grep veth
28: veth9b8f2f6@if27: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue master docker0 state UP group default
30: veth70447ef@if29: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue master docker0 state UP group default
32: veth9f627b0@if31: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue master docker0 state UP group default
34: veth79f6b76@if33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue master docker0 state UP group default
36: vethd91d567@if35: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue master docker0 state UP group default

这样的网络模型非常的简单,但不好处就是处于不同主机的容器之间无法直接通信。这里之所以说“无法直接通信”是因为通过一些手段比如端口映射等可以勉强实现不同主机上的容器通信,但非常不方便。所以Kubernetes在自己的网络模型设计方面做了一些硬性规定:

  • 所有容器间可以相互通信,且不是通过NAT方式。
  • 所有Node可以和任意容器通信(反过来也可以),且不是通过NAT方式。
  • 容器看到自己的IP和别人看到自己的IP是一样的。

这个是Kubernetes集群网络的最基本要求,现在有很多软件都已经实现了这种网络模型,可参阅官方文档https://kubernetes.io/docs/admin/networking/。本文今天介绍的就是其中一种,而且非常的简单——Flannel,这是CoreOS专门为Kubernetes开发的一个网络工具,用于实现其网络模型。其模型图如下所示:

flannel.png

flannel在每个host上面运行了一个代理程序flanneld,它可以为所在的host维护一个网段,并且为运行在这个host上的Kubernetes pods从这个网段里分配IP地址。多个hosts上的flanneld进程可以利用etcd机群互相协调以确保各自拥有不交叠的网段,这样一个集群上的pods各自拥有互不重复的IP地址。这种构筑在一个网络(hosts网络)之上的另一个网络(flanneld维护的pods网络),被称为 overlay network。上面的模型图也非常的清楚,容器里的网络流量先到docker0,然后会转到flannel0,再通过flanneld发到主机的某个网卡上(图中是eth0),然后再转发出去。流量到另外一台主机时,执行相反的过程。

下面我们在上文已经搭建的Kubernetes上面部署flannel网络。

Flannel部署

具体部署之前先说明两个点:

  • 要测试网络至少需要两台主机,上文部署的Master和Node在同一台主机上面,所以我们需要按照上文部署Node的方法,再部署一台Node,部署好以后,会自动向Master注册。具体部署过程我就不赘述了(就是部署Kubelet、kube-proxy、docker即可),我这里已经再部署了一台。而且这两台的网络必须是通的。
  • Flannel支持很多种模式:udp、vxlan、host-gw、aws-vpc、gce等。我们这里部署采用默认的模式即UDP。这种模式就是在应用层再对数据包做了一次封装,然后通过UDP发出去,这种方式下默认使用的端口是8258。这种应用层的再次封装转发必然会对性能有影响,而且因为是UDP,所以还可能丢包,但这些都不是本文要讨论的。

etcd设置

OK,现在我们来开始部署。从前面的介绍我们知道Flannel依赖于etcd,前文中我们已经部署好了,这里只需要再增加一条配置即可:

etcdctl set /coreos.com/network/config '{ "Network": "10.1.0.0/16" }'

这条指令我们指定了一个网段,后面Flannel在host上面构建子网的时候就会从这个网段中选取一个子网。注意,如果这个网段和你已有的网段冲突,那就更换成其他的。而这里/coreos.com/network是Flannel默认从etcd读取的前缀,可以改成其他的,然后启动Flannel时通过--etcd-prefix指定新的前缀即可,这里为了方便,直接使用默认的。这个动作只在Master上面执行。

另外,etcd默认是只监听本地地址的,我们需要改为也监听其他IP,至少要监听其他Node主机可以访问到IP,这里我改为在本机所有IP上面监听,在/etc/default/etcd中增加以下两行:

ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"
ETCD_ADVERTISE_CLIENT_URLS="http://0.0.0.0:2379"

然后重启etcd:systemctl restart etcd.service。这样etcd部分就配置完了。

flanneld配置

然后从https://github.com/coreos/flannel/releases下载最新的Flannel,将解压得到的flanneldmk-docker-opts.sh放到你的PATH里面,我继续放到/opt/bin目录下。然后和上文一样我们为flanneld创建service文件/lib/systemd/system/flanneld.service

[Unit]
Description=flanneld overlay address etcd agent
After=network.target
Before=docker.service

[Service]
Type=notify
ExecStart=/opt/bin/flanneld -etcd-endpoints=http://192.168.56.101:2379 --iface=enp0s8 --log_dir=/var/log/kubernetes -logtostderr=false -v=1

[Install]
RequiredBy=docker.service
WantedBy=multi-user.target

这里有三点注意:

  1. -etcd-endpoints:这个是etcd的地址。可以在本节点上面执行curl http://192.168.56.101:2379/version来验证看是否可以访问到etcd。
  2. Flannel复用了docker的docker0网桥,所以必须在docker启动前就启动Flannel。
  3. 最后要说的就是这个--iface=enp0s8。我使用Virtualbox创建了两个Ubuntu 16.04的虚拟机,默认采用的是NAT网络,一般对应enp0s3这个网卡。但为了让两个虚拟机有在同一网段的不同IP,我又使用host-only的模式增加了一个网卡enp0s8,这样一个节点的地址是192.168.56.101,另外一个是192.168.56.102,且这两个都是静态IP。默认flanneld会使用enp0s3,但两个节点通信其实是通过enp0s8,所以这里我需要显式的指定一下使用哪个网卡。

如果你的docker在运行,必须先停止systemctl stop docker.service,因为flanneld会覆盖docker的docker0网桥。然后启动flanneld服务systemctl restart flanneld.service.此时执行ip a已经可以看到虚拟的flannel0了

然后执行/opt/bin/mk-docker-opts.sh -i获取flanneld使用的网段,查看cat /run/flannel/subnet.env,比如我的是:

FLANNEL_NETWORK=10.1.0.0/16
FLANNEL_SUBNET=10.1.35.1/24
FLANNEL_MTU=1472
FLANNEL_IPMASQ=false

可见flanneld给本机分配的网段是10.1.35.1/24,我们需要将让docker在自己的docker0上面也使用这个网段,创建/etc/systemd/system/docker.service.d目录,并在里面创建任意conf后缀结尾的文件,如果已经有了,直接修改其内容,比如我的这个文件内容如下:

Node➜  ~ cat /etc/systemd/system/docker.service.d/docker.conf
[Service]
ExecStart=
ExecStart=/usr/bin/docker daemon -H fd:// --bip=10.1.35.1/24 --mtu=1472

然后启动docker:systemctl start docker.service。这样docker0和flannel0就都配置好了,比如我的网络如下(如果起了容器的话,还会看到veth网络,这里未列出):

Node➜  ~ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:6e:c8:25 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global enp0s3
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fe6e:c825/64 scope link
       valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:ca:35:d1 brd ff:ff:ff:ff:ff:ff
    inet 192.168.56.102/24 brd 192.168.56.255 scope global enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:feca:35d1/64 scope link
       valid_lft forever preferred_lft forever
4: flannel0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1472 qdisc pfifo_fast state UNKNOWN group default qlen 500
    link/none
    inet 10.1.35.0/16 scope global flannel0
       valid_lft forever preferred_lft forever
5: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue state UP group default
    link/ether 02:42:6e:e0:81:0a brd ff:ff:ff:ff:ff:ff
    inet 10.1.35.1/24 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:6eff:fee0:810a/64 scope link
       valid_lft forever preferred_lft forever

然后把这一节的操作在另外一台上面也执行一遍。我的另外一台部署好以后的网络如下:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:af:51:42 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global enp0s3
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:feaf:5142/64 scope link
       valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:91:b8:d0 brd ff:ff:ff:ff:ff:ff
    inet 192.168.56.101/24 brd 192.168.56.255 scope global enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fe91:b8d0/64 scope link
       valid_lft forever preferred_lft forever
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:96:fd:14:ef brd ff:ff:ff:ff:ff:ff
    inet 10.1.85.1/24 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:96ff:fefd:14ef/64 scope link
       valid_lft forever preferred_lft forever
49: flannel0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1472 qdisc pfifo_fast state UNKNOWN group default qlen 500
    link/none
    inet 10.1.85.0/16 scope global flannel0
       valid_lft forever preferred_lft forever

flanneld使用了的子网在Master上面的etcd也会有记录:

Master➜  ~ etcdctl ls /coreos.com/network/subnets  --recursive
/coreos.com/network/subnets/10.1.85.0-24
/coreos.com/network/subnets/10.1.35.0-24
Master➜  ~ etcdctl get /coreos.com/network/subnets/10.1.85.0-24
{"PublicIP":"192.168.56.101"}
Master➜  ~ etcdctl get /coreos.com/network/subnets/10.1.35.0-24
{"PublicIP":"192.168.56.102"}

验证

最后我们验证一下:

  1. 分别在每台上面ping一下另外一台的docker0,看是否可以ping通。如果通了再看2.
  2. 我们在Kubernetes上面部署几个Pod,在一个Node上面ping另外一个Node上面的Pod的IP,如果可以ping通那几成功了。

最后,如果没有通的话,看下各个服务是否启动正常,有没有报错之类的。再看下ufw防火墙和iptable是不是有问题。还是定位不出问题的话,根据Flannel的网络模型使用tcpdump分别在flannel0和docker0上面抓包,看是哪里的问题。

]]>
2 http://niyanchun.com/deploy-kubernetes-step-by-step-on-trusty-section-2.html#comments http://niyanchun.com/feed/tag/docker/
如何让docker daemon默认支持http的docker registry http://niyanchun.com/let-docker-daemon-support-non-tls-registry.html http://niyanchun.com/let-docker-daemon-support-non-tls-registry.html Thu, 19 Jan 2017 12:29:00 +0800 NYC 搭建过docker registry的人都知道,docker默认不支持http的registry,如果一定要支持,就需要配置--insecure-registry选项才可以,而且配置完以后需要重启docker daemon。本文从源代码角度分析docker daemon是如何限制的,以及如何去掉这个限制。

我们搭建了一个私有的镜像仓库,地址是http://222.222.222.222,没有配置https。因为要支持http,需要配置docker daemon,所以基本可以判断是docker daemon做的限制。我们通过docker login登录http://222.222.222.222时docker daemon的日志会报如下错误:

time="2017-01-16T09:30:33.349460061Z" level=debug msg="Calling POST /v1.24/auth"
time="2017-01-16T09:30:33.350468050Z" level=debug msg="form data: {\"password\":\"*****\",\"serveraddress\":\"http://222.222.222.222/\",\"username\":\"test\"}"
time="2017-01-16T09:30:33.351098322Z" level=debug msg="hostDir: /etc/docker/certs.d/222.222.222.222"
time="2017-01-16T09:30:33.351607498Z" level=debug msg="hostDir: /etc/docker/certs.d/222.222.222.222"
time="2017-01-16T09:30:33.353994569Z" level=debug msg="attempting v2 login to registry endpoint https://222.222.222.222/v2/"
time="2017-01-16T09:30:33.383185881Z" level=info msg="Error logging in to v2 endpoint, trying next endpoint: Get https://222.222.222.222/v2/: dial tcp 222.222.222.222:443: getsockopt: connection refused"
time="2017-01-16T09:30:33.383694112Z" level=debug msg="attempting v1 login to registry endpoint https://222.222.222.222/v1/"
time="2017-01-16T09:30:33.411921859Z" level=info msg="Error logging in to v1 endpoint, trying next endpoint: Get https://222.222.222.222/v1/users/: dial tcp 222.222.222.222:443: getsockopt: connection refused"
time="2017-01-16T09:30:33.412569676Z" level=error msg="Handler for POST /v1.24/auth returned error: Get https://222.222.222.222/v1/users/: dial tcp 222.222.222.222:443: getsockopt: connection refused"

然后我们给docker daemon配置了--insecure-registry选项后登录,观察日志:

time="2017-01-17T09:56:52.267456792Z" level=debug msg="Calling POST /v1.24/auth"
time="2017-01-17T09:56:52.268047734Z" level=debug msg="form data: {\"password\":\"*****\",\"serveraddress\":\"http://222.222.222.222\",\"username\":\"admin\"}"
time="2017-01-17T09:56:52.268603531Z" level=debug msg="attempting v2 login to registry endpoint https://222.222.222.222/v2/"
time="2017-01-17T09:56:52.300374963Z" level=info msg="Error logging in to v2 endpoint, trying next endpoint: Get https://222.222.222.222/v2/: dial tcp 222.222.222.222:443: getsockopt: connection refused"
time="2017-01-17T09:56:52.301470933Z" level=debug msg="attempting v2 login to registry endpoint http://54.222.229.223/v2/"222.222.222.222

我们发现没有配置--insecure-registry选项前只会尝试https://xxx,但是配置了以后,还会去尝试http://xxx。因为我们只有http服务,如果不去尝试http的话,肯定是登录不了的。然后我们去看下docker的代码。

因为是跟认证相关的,所以我们很容易就可以锁定docker/daemon/auth.go这个文件,文件内容很少:

package daemon

import (
    "golang.org/x/net/context"

    "github.com/docker/docker/api/types"
    "github.com/docker/docker/dockerversion"
)

// AuthenticateToRegistry checks the validity of credentials in authConfig
func (daemon *Daemon) AuthenticateToRegistry(ctx context.Context, authConfig *types.AuthConfig) (string, string, error) {
    return daemon.RegistryService.Auth(ctx, authConfig, dockerversion.DockerUserAgent(ctx))
}

RegistryService的类型是registry.Service,或者根据代码跳转我们可以很容易的找到docker/registry/service.go这个文件,因为这个文件对于分析比较重要,这里列出全部内容:

package registry

import (
    "crypto/tls"
    "fmt"
    "net/http"
    "net/url"
    "strings"
    "sync"

    "golang.org/x/net/context"

    "github.com/Sirupsen/logrus"
    "github.com/docker/distribution/registry/client/auth"
    "github.com/docker/docker/api/types"
    registrytypes "github.com/docker/docker/api/types/registry"
    "github.com/docker/docker/reference"
)

const (
    // DefaultSearchLimit is the default value for maximum number of returned search results.
    DefaultSearchLimit = 25
)

// Service is the interface defining what a registry service should implement.
type Service interface {
    Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error)
    LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error)
    LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error)
    ResolveRepository(name reference.Named) (*RepositoryInfo, error)
    Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error)
    ServiceConfig() *registrytypes.ServiceConfig
    TLSConfig(hostname string) (*tls.Config, error)
    LoadMirrors([]string) error
    LoadInsecureRegistries([]string) error
}

// DefaultService is a registry service. It tracks configuration data such as a list
// of mirrors.
type DefaultService struct {
    config *serviceConfig
    mu     sync.Mutex
}

// NewService returns a new instance of DefaultService ready to be
// installed into an engine.
func NewService(options ServiceOptions) *DefaultService {
    return &DefaultService{
        config: newServiceConfig(options),
    }
}

// ServiceConfig returns the public registry service configuration.
func (s *DefaultService) ServiceConfig() *registrytypes.ServiceConfig {
    s.mu.Lock()
    defer s.mu.Unlock()

    servConfig := registrytypes.ServiceConfig{
        InsecureRegistryCIDRs: make([]*(registrytypes.NetIPNet), 0),
        IndexConfigs:          make(map[string]*(registrytypes.IndexInfo)),
        Mirrors:               make([]string, 0),
    }

    // construct a new ServiceConfig which will not retrieve s.Config directly,
    // and look up items in s.config with mu locked
    servConfig.InsecureRegistryCIDRs = append(servConfig.InsecureRegistryCIDRs, s.config.ServiceConfig.InsecureRegistryCIDRs...)

    for key, value := range s.config.ServiceConfig.IndexConfigs {
        servConfig.IndexConfigs[key] = value
    }

    servConfig.Mirrors = append(servConfig.Mirrors, s.config.ServiceConfig.Mirrors...)

    return &servConfig
}

// LoadMirrors loads registry mirrors for Service
func (s *DefaultService) LoadMirrors(mirrors []string) error {
    s.mu.Lock()
    defer s.mu.Unlock()

    return s.config.LoadMirrors(mirrors)
}

// LoadInsecureRegistries loads insecure registries for Service
func (s *DefaultService) LoadInsecureRegistries(registries []string) error {
    s.mu.Lock()
    defer s.mu.Unlock()

    return s.config.LoadInsecureRegistries(registries)
}

// Auth contacts the public registry with the provided credentials,
// and returns OK if authentication was successful.
// It can be used to verify the validity of a client's credentials.
func (s *DefaultService) Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error) {
    // TODO Use ctx when searching for repositories
    serverAddress := authConfig.ServerAddress
    if serverAddress == "" {
        serverAddress = IndexServer
    }
    if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
        serverAddress = "https://" + serverAddress
    }
    u, err := url.Parse(serverAddress)
    if err != nil {
        return "", "", fmt.Errorf("unable to parse server address: %v", err)
    }

    endpoints, err := s.LookupPushEndpoints(u.Host)
    if err != nil {
        return "", "", err
    }

    for _, endpoint := range endpoints {
        login := loginV2
        if endpoint.Version == APIVersion1 {
            login = loginV1
        }

        status, token, err = login(authConfig, endpoint, userAgent)
        if err == nil {
            return
        }
        if fErr, ok := err.(fallbackError); ok {
            err = fErr.err
            logrus.Infof("Error logging in to %s endpoint, trying next endpoint: %v", endpoint.Version, err)
            continue
        }
        return "", "", err
    }

    return "", "", err
}

// splitReposSearchTerm breaks a search term into an index name and remote name
func splitReposSearchTerm(reposName string) (string, string) {
    nameParts := strings.SplitN(reposName, "/", 2)
    var indexName, remoteName string
    if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") &&
        !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
        // This is a Docker Index repos (ex: samalba/hipache or ubuntu)
        // 'docker.io'
        indexName = IndexName
        remoteName = reposName
    } else {
        indexName = nameParts[0]
        remoteName = nameParts[1]
    }
    return indexName, remoteName
}

// Search queries the public registry for images matching the specified
// search terms, and returns the results.
func (s *DefaultService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
    // TODO Use ctx when searching for repositories
    if err := validateNoScheme(term); err != nil {
        return nil, err
    }

    indexName, remoteName := splitReposSearchTerm(term)

    // Search is a long-running operation, just lock s.config to avoid block others.
    s.mu.Lock()
    index, err := newIndexInfo(s.config, indexName)
    s.mu.Unlock()

    if err != nil {
        return nil, err
    }

    // *TODO: Search multiple indexes.
    endpoint, err := NewV1Endpoint(index, userAgent, http.Header(headers))
    if err != nil {
        return nil, err
    }

    var client *http.Client
    if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" {
        creds := NewStaticCredentialStore(authConfig)
        scopes := []auth.Scope{
            auth.RegistryScope{
                Name:    "catalog",
                Actions: []string{"search"},
            },
        }

        modifiers := DockerHeaders(userAgent, nil)
        v2Client, foundV2, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, scopes)
        if err != nil {
            if fErr, ok := err.(fallbackError); ok {
                logrus.Errorf("Cannot use identity token for search, v2 auth not supported: %v", fErr.err)
            } else {
                return nil, err
            }
        } else if foundV2 {
            // Copy non transport http client features
            v2Client.Timeout = endpoint.client.Timeout
            v2Client.CheckRedirect = endpoint.client.CheckRedirect
            v2Client.Jar = endpoint.client.Jar

            logrus.Debugf("using v2 client for search to %s", endpoint.URL)
            client = v2Client
        }
    }

    if client == nil {
        client = endpoint.client
        if err := authorizeClient(client, authConfig, endpoint); err != nil {
            return nil, err
        }
    }

    r := newSession(client, authConfig, endpoint)

    if index.Official {
        localName := remoteName
        if strings.HasPrefix(localName, "library/") {
            // If pull "library/foo", it's stored locally under "foo"
            localName = strings.SplitN(localName, "/", 2)[1]
        }

        return r.SearchRepositories(localName, limit)
    }
    return r.SearchRepositories(remoteName, limit)
}

// ResolveRepository splits a repository name into its components
// and configuration of the associated registry.
func (s *DefaultService) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    return newRepositoryInfo(s.config, name)
}

// APIEndpoint represents a remote API endpoint
type APIEndpoint struct {
    Mirror       bool
    URL          *url.URL
    Version      APIVersion
    Official     bool
    TrimHostname bool
    TLSConfig    *tls.Config
}

// ToV1Endpoint returns a V1 API endpoint based on the APIEndpoint
func (e APIEndpoint) ToV1Endpoint(userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
    return newV1Endpoint(*e.URL, e.TLSConfig, userAgent, metaHeaders)
}

// TLSConfig constructs a client TLS configuration based on server defaults
func (s *DefaultService) TLSConfig(hostname string) (*tls.Config, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    return newTLSConfig(hostname, isSecureIndex(s.config, hostname))
}

// tlsConfig constructs a client TLS configuration based on server defaults
func (s *DefaultService) tlsConfig(hostname string) (*tls.Config, error) {
    return newTLSConfig(hostname, isSecureIndex(s.config, hostname))
}

func (s *DefaultService) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, error) {
    return s.tlsConfig(mirrorURL.Host)
}

// LookupPullEndpoints creates a list of endpoints to try to pull from, in order of preference.
// It gives preference to v2 endpoints over v1, mirrors over the actual
// registry, and HTTPS over plain HTTP.
func (s *DefaultService) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    return s.lookupEndpoints(hostname)
}

// LookupPushEndpoints creates a list of endpoints to try to push to, in order of preference.
// It gives preference to v2 endpoints over v1, and HTTPS over plain HTTP.
// Mirrors are not included.
func (s *DefaultService) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    allEndpoints, err := s.lookupEndpoints(hostname)
    if err == nil {
        for _, endpoint := range allEndpoints {
            if !endpoint.Mirror {
                endpoints = append(endpoints, endpoint)
            }
        }
    }
    return endpoints, err
}

func (s *DefaultService) lookupEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
    endpoints, err = s.lookupV2Endpoints(hostname)
    if err != nil {
        return nil, err
    }

    if s.config.V2Only {
        return endpoints, nil
    }

    legacyEndpoints, err := s.lookupV1Endpoints(hostname)
    if err != nil {
        return nil, err
    }
    endpoints = append(endpoints, legacyEndpoints...)

    return endpoints, nil
}

代码比较简单,里面定义了一个Service接口,并且定义了一个DefaultService结构体实现了这个接口。我们主要关注一下Auth这个函数。细节我就不说了,我说一下和我们要分析的点比较相关的流程。重点在LookupPushEndpoints这个函数,从代码可以看到是这个函数构造了一个endpoints,里面是会去尝试登陆的url。那么我们的http那个应该也是在这个里面构造的。

我们进到从LookupPushEndpoints跟踪到lookupEndpoints,再到lookupV2Endpoints。需要说明的是还有一个lookupV1Endpoints,这个主要是为了兼容以前V1版本的registry,现在已经很少了。而且V1和V2的代码逻辑基本是一样的,我们以V2为例分析。lookupV2Endpointsdocker/registry/service_v2.go中:

package registry

import (
    "net/url"
    "strings"

    "github.com/docker/go-connections/tlsconfig"
)

func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
    tlsConfig := tlsconfig.ServerDefault()
    if hostname == DefaultNamespace || hostname == IndexHostname {
        // v2 mirrors
        for _, mirror := range s.config.Mirrors {
            if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
                mirror = "https://" + mirror
            }
            mirrorURL, err := url.Parse(mirror)
            if err != nil {
                return nil, err
            }
            mirrorTLSConfig, err := s.tlsConfigForMirror(mirrorURL)
            if err != nil {
                return nil, err
            }
            endpoints = append(endpoints, APIEndpoint{
                URL: mirrorURL,
                // guess mirrors are v2
                Version:      APIVersion2,
                Mirror:       true,
                TrimHostname: true,
                TLSConfig:    mirrorTLSConfig,
            })
        }
        // v2 registry
        endpoints = append(endpoints, APIEndpoint{
            URL:          DefaultV2Registry,
            Version:      APIVersion2,
            Official:     true,
            TrimHostname: true,
            TLSConfig:    tlsConfig,
        })

        return endpoints, nil
    }

    tlsConfig, err = s.tlsConfig(hostname)
    if err != nil {
        return nil, err
    }

    endpoints = []APIEndpoint{
        {
            URL: &url.URL{
                Scheme: "https",
                Host:   hostname,
            },
            Version:      APIVersion2,
            TrimHostname: true,
            TLSConfig:    tlsConfig,
        },
    }

    if tlsConfig.InsecureSkipVerify {
        endpoints = append(endpoints, APIEndpoint{
            URL: &url.URL{
                Scheme: "http",
                Host:   hostname,
            },
            Version:      APIVersion2,
            TrimHostname: true,
            // used to check if supposed to be secure via InsecureSkipVerify
            TLSConfig: tlsConfig,
        })
    }

    return endpoints, nil
}

到这里,我们已经已经看到真想了。可以看到要想生成http的endpoint,需要tlsConfig.InsecureSkipVerify这个为true才可以。那这个又是在哪里设置的?这段代码里面有两处涉及tlsConfig的地方:tlsConfig := tlsconfig.ServerDefault()tlsConfig, err = s.tlsConfig(hostname)。但因为那个配置的选项是和hostname相关的,所以肯定是后者。依次进到tlsConfigisSecureIndex,就看到了,在isSecureIndex里面,如果从配置项里面找到了,就返回该条的Secure值。这个Secure字段默认是true,但如果我们配置了--insecure-registry选项,这个字段就会被置为false,具体实现在LoadInsecureRegistries函数里面,有兴趣的可以看一下。

我们已经明白了其中的原理,想让docker daemon默认就支持http的registry,改起来就非常简单了,最粗暴的做法就是将tlsConfig.InsecureSkipVerify这个恒置为true或者直接去掉那里的if,无条件执行下面的代码。当然,我觉得比较优雅的做法是增加一个配置项,比如--accept-insecure-registry,默认是false,然后再if那里和tlsConfig.InsecureSkipVerify进行或操作。这样的好处是和原来的相比这个是一种通用的配置,不会增加一项就要配置一次,重启一次docker daemon。另外,如果置为false,那就和docker原来的机制是完全一样的了,灵活性比较大。

2017.1.24更新

再看了下代码发现isSecureIndex函数里面支持解析CIDR,也就是说我们可以在配置--insecure-registry时配置子网,那么我们可以将这个选项配置为0.0.0.0/0就可以支持所有http的registry了。当然,还是那句话:http是不安全的,慎用。就算是私网内,也是可以自签名用https的。

]]>
2 http://niyanchun.com/let-docker-daemon-support-non-tls-registry.html#comments http://niyanchun.com/feed/tag/docker/
从源码编译docker http://niyanchun.com/compile-docker-from-source.html http://niyanchun.com/compile-docker-from-source.html Wed, 18 Jan 2017 13:49:00 +0800 NYC 如果你改docker的源码,那就必然需要自己从源码去编译docker,本文介绍如何从源码编译docker。

编译docker的过程很复杂,但是庆幸的是docker官方已经将这个复杂的过程简单化了,它提供了一个Makefile和Dockerfile,将复杂的操作都封装起来了,对于我们可以不去太关注里面的细节。本文主要介绍一下编译流程以及对于中国用户来说如何解决一些源下载慢甚至下载不了的问题。

  1. 硬件环境。编译docker的过程是挺耗费内存的,如果你使用虚拟机的话,建议至少分2GB的内存。我这里使用的OS是Ubuntu 14.04,硬件是8核16GB的服务器。
  2. 安装docker。因为docker是在容器里面去编译的,所以我们需要先安装docker(如果下面的命令安装失败,请参考我之前的文章《Ubuntu安装Docker》)。

    apt-get install linux-image-extra-$(uname -r) linux-image-extra-virtual
    curl -sSL https://get.daocloud.io/docker | sh
  3. 下载源码。

    git clone https://github.com/docker/docker.git
    cd docker
  4. 构建编译docker的环境。docker的编译是在容器中进行的,构建这个容器的命令很简单,在docker目录执行make build即可。我们看Makefile文件可以发现,执行make build的时候其实值执行了如下命令:

    docker build ${BUILD_APT_MIRROR} ${DOCKER_BUILD_ARGS} -t "$(DOCKER_IMAGE)" -f "$(DOCKERFILE)" .`

    其实默认就是用Makefile同级目录下的Dockerfile构建出一个编译docker的镜像,这个镜像具备了编译docker的所有东西。内容如下:

    # This file describes the standard way to build Docker, using docker
    # Usage:
    #
    # # Assemble the full dev environment. This is slow the first time.
    # docker build -t docker .
    #
    # # Mount your source in an interactive container for quick testing:
    # docker run -v `pwd`:/go/src/github.com/docker/docker --privileged -i -t docker bash
    #
    # # Run the test suite:
    # docker run --privileged docker hack/make.sh test-unit test-integration-cli test-docker-py
    #
    # # Publish a release:
    # docker run --privileged \
    #  -e AWS_S3_BUCKET=baz \
    #  -e AWS_ACCESS_KEY=foo \
    #  -e AWS_SECRET_KEY=bar \
    #  -e GPG_PASSPHRASE=gloubiboulga \
    #  docker hack/release.sh
    #
    # Note: AppArmor used to mess with privileged mode, but this is no longer
    # the case. Therefore, you don't have to disable it anymore.
    #
    
    FROM debian:jessie
    
    # allow replacing httpredir or deb mirror
    ARG APT_MIRROR=deb.debian.org
    RUN sed -ri "s/(httpredir|deb).debian.org/$APT_MIRROR/g" /etc/apt/sources.list
    
    # Add zfs ppa
    COPY keys/launchpad-ppa-zfs.asc /go/src/github.com/docker/docker/keys/
    RUN apt-key add /go/src/github.com/docker/docker/keys/launchpad-ppa-zfs.asc
    RUN echo deb http://ppa.launchpad.net/zfs-native/stable/ubuntu trusty main > /etc/apt/sources.list.d/zfs.list
    
    # Packaged dependencies
    RUN apt-get update && apt-get install -y \
        apparmor \
        apt-utils \
        aufs-tools \
        automake \
        bash-completion \
        binutils-mingw-w64 \
        bsdmainutils \
        btrfs-tools \
        build-essential \
        clang \
        cmake \
        createrepo \
        curl \
        dpkg-sig \
        gcc-mingw-w64 \
        git \
        iptables \
        jq \
        less \
        libapparmor-dev \
        libcap-dev \
        libltdl-dev \
        libnl-3-dev \
        libprotobuf-c0-dev \
        libprotobuf-dev \
        libsqlite3-dev \
        libsystemd-journal-dev \
        libtool \
        libzfs-dev \
        mercurial \
        net-tools \
        pkg-config \
        protobuf-compiler \
        protobuf-c-compiler \
        python-dev \
        python-mock \
        python-pip \
        python-websocket \
        tar \
        ubuntu-zfs \
        vim \
        vim-common \
        xfsprogs \
        zip \
        --no-install-recommends \
        && pip install awscli==1.10.15
    # Get lvm2 source for compiling statically
    ENV LVM2_VERSION 2.02.103
    RUN mkdir -p /usr/local/lvm2 \
        && curl -fsSL "https://mirrors.kernel.org/sourceware/lvm2/LVM2.${LVM2_VERSION}.tgz" \
            | tar -xzC /usr/local/lvm2 --strip-components=1
    # See https://git.fedorahosted.org/cgit/lvm2.git/refs/tags for release tags
    
    # Compile and install lvm2
    RUN cd /usr/local/lvm2 \
        && ./configure \
            --build="$(gcc -print-multiarch)" \
            --enable-static_link \
        && make device-mapper \
        && make install_device-mapper
    # See https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL
    
    # Configure the container for OSX cross compilation
    ENV OSX_SDK MacOSX10.11.sdk
    ENV OSX_CROSS_COMMIT a9317c18a3a457ca0a657f08cc4d0d43c6cf8953
    RUN set -x \
        && export OSXCROSS_PATH="/osxcross" \
        && git clone https://github.com/tpoechtrager/osxcross.git $OSXCROSS_PATH \
        && ( cd $OSXCROSS_PATH && git checkout -q $OSX_CROSS_COMMIT) \
        && curl -sSL https://s3.dockerproject.org/darwin/v2/${OSX_SDK}.tar.xz -o "${OSXCROSS_PATH}/tarballs/${OSX_SDK}.tar.xz" \
        && UNATTENDED=yes OSX_VERSION_MIN=10.6 ${OSXCROSS_PATH}/build.sh
    ENV PATH /osxcross/target/bin:$PATH
    
    # Install seccomp: the version shipped in trusty is too old
    ENV SECCOMP_VERSION 2.3.1
    RUN set -x \
        && export SECCOMP_PATH="$(mktemp -d)" \
        && curl -fsSL "https://github.com/seccomp/libseccomp/releases/download/v${SECCOMP_VERSION}/libseccomp-${SECCOMP_VERSION}.tar.gz" \
            | tar -xzC "$SECCOMP_PATH" --strip-components=1 \
        && ( \
            cd "$SECCOMP_PATH" \
            && ./configure --prefix=/usr/local \
            && make \
            && make install \
            && ldconfig \
        ) \
        && rm -rf "$SECCOMP_PATH"
    
    # Install Go
    # IMPORTANT: If the version of Go is updated, the Windows to Linux CI machines
    #            will need updating, to avoid errors. Ping #docker-maintainers on IRC
    #            with a heads-up.
    ENV GO_VERSION 1.7.4
    RUN curl -fsSL "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz" \
        | tar -xzC /usr/local
    
    ENV PATH /go/bin:/usr/local/go/bin:$PATH
    ENV GOPATH /go
    
    # Compile Go for cross compilation
    ENV DOCKER_CROSSPLATFORMS \
        linux/386 linux/arm \
        darwin/amd64 \
        freebsd/amd64 freebsd/386 freebsd/arm \
        windows/amd64 windows/386 \
        solaris/amd64
    
    # Dependency for golint
    ENV GO_TOOLS_COMMIT 823804e1ae08dbb14eb807afc7db9993bc9e3cc3
    RUN git clone https://github.com/golang/tools.git /go/src/golang.org/x/tools \
        && (cd /go/src/golang.org/x/tools && git checkout -q $GO_TOOLS_COMMIT)
    
    # Grab Go's lint tool
    ENV GO_LINT_COMMIT 32a87160691b3c96046c0c678fe57c5bef761456
    RUN git clone https://github.com/golang/lint.git /go/src/github.com/golang/lint \
        && (cd /go/src/github.com/golang/lint && git checkout -q $GO_LINT_COMMIT) \
        && go install -v github.com/golang/lint/golint
    
    # Install CRIU for checkpoint/restore support
    ENV CRIU_VERSION 2.9
    RUN mkdir -p /usr/src/criu \
        && curl -sSL https://github.com/xemul/criu/archive/v${CRIU_VERSION}.tar.gz | tar -v -C /usr/src/criu/ -xz --strip-components=1 \
        && cd /usr/src/criu \
        && make \
        && make install-criu
    
    # Install two versions of the registry. The first is an older version that
    # only supports schema1 manifests. The second is a newer version that supports
    # both. This allows integration-cli tests to cover push/pull with both schema1
    # and schema2 manifests.
    ENV REGISTRY_COMMIT_SCHEMA1 ec87e9b6971d831f0eff752ddb54fb64693e51cd
    ENV REGISTRY_COMMIT 47a064d4195a9b56133891bbb13620c3ac83a827
    RUN set -x \
        && export GOPATH="$(mktemp -d)" \
        && git clone https://github.com/docker/distribution.git "$GOPATH/src/github.com/docker/distribution" \
        && (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT") \
        && GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \
            go build -o /usr/local/bin/registry-v2 github.com/docker/distribution/cmd/registry \
        && (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT_SCHEMA1") \
        && GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \
            go build -o /usr/local/bin/registry-v2-schema1 github.com/docker/distribution/cmd/registry \
        && rm -rf "$GOPATH"
    
    # Install notary and notary-server
    ENV NOTARY_VERSION v0.5.0
    RUN set -x \
        && export GOPATH="$(mktemp -d)" \
        && git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \
        && (cd "$GOPATH/src/github.com/docker/notary" && git checkout -q "$NOTARY_VERSION") \
        && GOPATH="$GOPATH/src/github.com/docker/notary/vendor:$GOPATH" \
            go build -o /usr/local/bin/notary-server github.com/docker/notary/cmd/notary-server \
        && GOPATH="$GOPATH/src/github.com/docker/notary/vendor:$GOPATH" \
            go build -o /usr/local/bin/notary github.com/docker/notary/cmd/notary \
        && rm -rf "$GOPATH"
    
    # Get the "docker-py" source so we can run their integration tests
    ENV DOCKER_PY_COMMIT e2655f658408f9ad1f62abdef3eb6ed43c0cf324
    RUN git clone https://github.com/docker/docker-py.git /docker-py \
        && cd /docker-py \
        && git checkout -q $DOCKER_PY_COMMIT \
        && pip install -r test-requirements.txt
    
    # Install yamllint for validating swagger.yaml
    RUN pip install yamllint==1.5.0
    
    # Install go-swagger for validating swagger.yaml
    ENV GO_SWAGGER_COMMIT c28258affb0b6251755d92489ef685af8d4ff3eb
    RUN git clone https://github.com/go-swagger/go-swagger.git /go/src/github.com/go-swagger/go-swagger \
        && (cd /go/src/github.com/go-swagger/go-swagger && git checkout -q $GO_SWAGGER_COMMIT) \
        && go install -v github.com/go-swagger/go-swagger/cmd/swagger
    
    # Set user.email so crosbymichael's in-container merge commits go smoothly
    RUN git config --global user.email 'docker-dummy@example.com'
    
    # Add an unprivileged user to be used for tests which need it
    RUN groupadd -r docker
    RUN useradd --create-home --gid docker unprivilegeduser
    
    VOLUME /var/lib/docker
    WORKDIR /go/src/github.com/docker/docker
    ENV DOCKER_BUILDTAGS apparmor pkcs11 seccomp selinux
    
    # Let us use a .bashrc file
    RUN ln -sfv $PWD/.bashrc ~/.bashrc
    # Add integration helps to bashrc
    RUN echo "source $PWD/hack/make/.integration-test-helpers" >> /etc/bash.bashrc
    
    # Register Docker's bash completion.
    RUN ln -sv $PWD/contrib/completion/bash/docker /etc/bash_completion.d/docker
    
    # Get useful and necessary Hub images so we can "docker load" locally instead of pulling
    COPY contrib/download-frozen-image-v2.sh /go/src/github.com/docker/docker/contrib/
    RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \
        buildpack-deps:jessie@sha256:25785f89240fbcdd8a74bdaf30dd5599a9523882c6dfc567f2e9ef7cf6f79db6 \
        busybox:latest@sha256:e4f93f6ed15a0cdd342f5aae387886fba0ab98af0a102da6276eaf24d6e6ade0 \
        debian:jessie@sha256:f968f10b4b523737e253a97eac59b0d1420b5c19b69928d35801a6373ffe330e \
        hello-world:latest@sha256:8be990ef2aeb16dbcb9271ddfe2610fa6658d13f6dfb8bc72074cc1ca36966a7
    # See also "hack/make/.ensure-frozen-images" (which needs to be updated any time this list is)
    
    # Install tomlv, vndr, runc, containerd, tini, docker-proxy
    # Please edit hack/dockerfile/install-binaries.sh to update them.
    COPY hack/dockerfile/binaries-commits /tmp/binaries-commits
    COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh
    RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy bindata
    
    # Wrap all commands in the "docker-in-docker" script to allow nested containers
    ENTRYPOINT ["hack/dind"]
    
    # Upload docker source
    COPY . /go/src/github.com/docker/docker
    

    对于中国用户来说,如果没有科学上网工具,直接使用这个Dockerfile,基本上不会成功,因为有些下载不了。所以我们需要修改几个地方,特别是装软件的地方,需要下载很多东西。我的修改点如下:

    • 修改替换Debian源为中国的源(大约第30行)

      # 修改前
      ARG APT_MIRROR=deb.debian.org
      
      # 修改后
      ARG APT_MIRROR=ftp.cn.debian.org
    • 修改pip源为国内的源(大约第85行)

      # 修改前
      && pip install awscli==1.10.15
      
      # 修改后
      && pip install awscli==1.10.15 -i http://mirrors.aliyun.com/pypi/simple
    • 修改下载go安装包的地址(大约133行)。这里需要注意GO_VERSION的版本,不同版本的地址可以去http://golangtc.com/download获取。

      # 修改前
      ENV GO_VERSION 1.7.4
      RUN curl -fsSL "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz" \
       | tar -xzC /usr/local
      
      # 修改后
      ENV GO_VERSION 1.7.4
      RUN curl -fsSL "http://golangtc.com/static/go/1.7.4/go1.7.4.linux-amd64.tar.gz" \
       | tar -xzC /usr/local
      

    其他的基本都是从docker hub和github下载东西,对于docker hub可以配置加速器,github没有被墙,但可能不稳定或者速度比较慢。整个容器的过程还是比较慢的,不过比起编译Linux内核和Gcc还是快了不少的。如果不出什么错的话,编译完之后我们就会有一个叫docker-dev的容器,我编译出来有2.439GB。这样我们就有编译docker源码的环境了。

  5. 编译docker。这个也很简单,在刚才的目录执行make binary即可。如果没出什么错,我们可以看到如下信息:

    ---> Making bundle: binary (in bundles/1.14.0-dev/binary)
    Building: bundles/1.14.0-dev/binary-client/docker-1.14.0-dev
    Created binary: bundles/1.14.0-dev/binary-client/docker-1.14.0-dev
    Building: bundles/1.14.0-dev/binary-daemon/dockerd-1.14.0-dev
    Created binary: bundles/1.14.0-dev/binary-daemon/dockerd-1.14.0-dev
    Copying nested executables into bundles/1.14.0-dev/binary-daemon

    可以看到编译出来的东西在bundles/1.14.0-dev/binary目录,有client(bundles/1.14.0-dev/binary-client/docker)和daemon(bundles/1.14.0-dev/binary-daemon/dockerd):

至此编译结束了。

]]>
6 http://niyanchun.com/compile-docker-from-source.html#comments http://niyanchun.com/feed/tag/docker/
Kubernetes初体验 http://niyanchun.com/kubernetes-trial.html http://niyanchun.com/kubernetes-trial.html Thu, 12 Jan 2017 15:06:00 +0800 NYC 最近了解了一下Kubernetes,发现对于一个新手想要先简单体验一下Kubernetes,还是会遇到非常多的问题,所以我结合官方的Tutorials以及自己的摸索,写了本篇博客,一方面加深对Kubernetes的理解,另一方面也希望给别人带来一些帮助。另外:

  1. 我使用的环境是MacOS Sierra 10.12.2+Virtualbox+minikube v0.15+kubectl v1.5.1,不同的环境可能遇到不同的问题,希望多学习交流。
  2. 本文的定位主要是初学者体验Kubernetes,所以对于Kubernetes的Pod、Service、Label等核心概念我只是结合官方介绍与自己的了解做一个简单的介绍,能了解大概原理即可。
  3. 作为初学者,理解难免有误,希望不吝指教。

Kubernetes Cluster

Kubernetes is a production-grade, open-source platform that orchestrates the placement (scheduling) and execution of application containers within and across computer clusters.

Kubernetes将底层的计算资源连接在一起对外体现为一个计算集群,并将资源高度抽象化。部署应用时Kubernetes会以更高效的方式自动的将应用分发到集群内的机器上面,并调度运行。几个Kubernetes集群包含两种类型的资源:

  • Master节点:协调控制整个集群。
  • Nodes节点:运行应用的工作节点。

如下图:

Kubernetes Cluster Diagram

Masters manage the cluster and the nodes are used to host the running applications.

Master负责管理整个集群,协调集群内的所有行为。比如调度应用,监控应用的状态等。

Node节点负责运行应用,一般是一台物理机或者虚机。每个Node节点上面都有一个Kubelet,它是一个代理程序,用来管理该节点以及和Master节点通信。除此以外,Node节点上还会有一些管理容器的工具,比如Docker或者rkt等。生产环境中一个Kubernetes集群至少应该包含三个Nodes节点。

当部署应用的时候,我们通知Master节点启动应用容器。然后Master会调度这些应用将它们运行在Node节点上面。Node节点和Master节点通过Master节点暴露的Kubernetes API通信。当然我们也可以直接通过这些API和集群交互。

Kubernetes提供了一个轻量级的Minikube应用,利用它我们可以很容器的创建一个只包含一个Node节点的Kubernetes Cluster用于日常的开发测试。

安装Minikube

Minikube的Github:https://github.com/kubernetes/minikube

Minikube提供OSX、Linux、Windows版本,本文测试环境为OSX,且以当时最新的版本为例。实际操作时,尽量到官网查看最新的版本及安装命令。

MacOS安装:

curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.15.0/minikube-darwin-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/

Linux安装:

curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.15.0/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/

Minikube要正常使用,还必须安装kubectl,并且放在PATH里面。kubectl是一个通过Kubernetes API和Kubernetes集群交互的命令行工具。安装方法如下:

MACOS安装:

wget https://storage.googleapis.com/kubernetes-release/release/v1.5.1/bin/darwin/amd64/kubectl
chmod +x kubectl
mv kubectl /usr/local/bin/kubectl

Linux安装:

wget https://storage.googleapis.com/kubernetes-release/release/v1.5.1/bin/linux/amd64/kubectl
chmod +x kubectl
mv kubectl /usr/local/bin/kubectl

因为Minikube使用Docker Machine管理Kubernetes的虚机,所以我们还需要安装一些虚机的driver,这里推荐使用Virtualbox,因为安装简单(从官网下载安装即可),而且后面可以简单的登录到VM里面去进行一些操作。其它driver的安装请参考官方文档:https://github.com/kubernetes/minikube/blob/master/DRIVERS.md

安装完以后,我们可以使用一下命令进行一些信息查看:

minikube version    # 查看版本
minikube start --vm-driver=virtualbox     # 启动Kubernetes Cluster,这里使用的driver是Virtualbox

kubectl version    # 查看版本
kubectl cluster-info # 查看集群信息
kubectl get nodes # 查看当前可用的Node,使用Minikube创建的cluster只有一个Node节点

至此,我们已经用Minikube创建了一个Kubernetes Cluster,下面我们在这个集群上面部署一个应用。

Only For Chinese

在进行下一节部署具体应用前我们先要做一件事情。Kubernetes在部署容器应用的时候会先拉一个pause镜像,这个是一个基础容器,主要是负责网络部分的功能的,具体这里不展开讨论。最关键的是Kubernetes里面镜像默认都是从Google的镜像仓库拉的(就跟docker默认从docker hub拉的一样),但是因为GFW的原因,中国用户是访问不了Google的镜像仓库gcr.io的(如果你可以ping通,那恭喜你)。庆幸的是这个镜像被传到了docker hub上面,虽然中国用户访问后者也非常艰难,但通过一些加速器之类的还是可以pull下来的。如果没有VPN等科学上网的工具的话,请先做如下操作:

minikube ssh    # 登录到我们的Kubernetes VM里面去
docker pull hub.c.163.com/allan1991/pause-amd64:3.0
docker tag hub.c.163.com/allan1991/pause-amd64:3.0 gcr.io/google_containers/pause-amd64:3.0  

我们先从其他镜像仓库(这里我使用的是HNA CloudOS容器云平台提供的镜像仓库)下载Kubernetes需要的基础网络容器pause,Mac OSX上面kubectl 1.5.1版本需要的是pause-amd64:3.0,然后我将其打成gcr.io/google_containers/pause-amd64:3.0。这样Kubernetes VM就不会从gcr.io拉镜像了,而是会直接使用本地的镜像。

关于这个问题,Kubernetes上面还专门有个issue讨论,有兴趣的可以看看:https://github.com/kubernetes/kubernetes/issues/6888

OK,接着我们可以进行下一步了。

Deploy an App

在Kubernetes Cluster上面部署应用,我们需要先创建一个Kubernetes Deployment。这个Deployment负责创建和更新我们的应用实例。当这个Deployment创建之后,Kubernetes master就会将这个Deployment创建出来的应用实例部署到集群内某个Node节点上。而且自应用实例创建后,Deployment controller还会持续监控应用,直到应用被删除或者部署应用的Node节点不存在。

A Deployment is responsible for creating and updating instances of your application

Deploying your first app on Kubernetes

这里我们依旧使用kubectl来创建Deployment,创建的时候需要制定容器镜像以及我们要启动的个数(replicas),当然这些信息后面可以再更新。这里我用Go写了一个简单的Webserver,返回“Hello World”,监听端口是8090.我们就来启动这个应用(镜像地址:registry.hnaresearch.com/public/hello-world:v1.0 备用镜像地址:hub.c.163.com/allan1991/hello-world:v1.0)

kubectl run helloworld --image=registry.hnaresearch.com/public/hello-world:v1.0 --port=8090

这条命令执行后master寻找一个合适的node来部署我们的应用实例(我们只有一个node)。我们可以使用kubectl get deployment来查看我们创建的Deployment:

➜  ~ kubectl get deployment
NAME         DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
helloworld   1         1         1            1           53m

默认应用部署好之后是只在Kubernetes Cluster内部可见的,有多种方法可以让我们的应用暴露到外部,这里先介绍一种简单的:我们可以通过kubectl proxy命令在我们的终端和Kubernetes Cluster直接创建一个代理。然后,打开一个新的终端,通过Pod名(Pod后面会有讲到,可以通过kubectl get pod查看Pod名字)就可以访问了:

➜  ~ kubectl proxy
Starting to serve on 127.0.0.1:8001

# 打开新的终端
➜  ~ kubectl get pod
NAME                          READY     STATUS    RESTARTS   AGE
helloworld-2080166056-4q88d   1/1       Running   0          1h
➜  ~ curl http://localhost:8001/api/v1/proxy/namespaces/default/pods/helloworld-2080166056-4q88d/
Hello world !

OK,我们的第一个应用已经部署成功了。如果你的应用有问题(比如kubectl get deployment执行后看到AVAILABL是0),可以通过kubectl describe pod pod名字来查看信息(最后面的Events部分是该Pod的信息),看哪里出错了。

好吧,这里我们已经提到了Pod,它是Kubernetes中一个非常重要的概念,也是区别于其他编排系统的一个设计,这里我们简单介绍一下。

Pods和Nodes

其实我们创建了Deployment之后,它并不是直接创建了容器实例,而是先在Node上面创建了Pod,然后再在Pod里面创建容器。那Pod到底是什么?Pod是Kubernetes里面抽象出来的一个概念,它是能够被创建、调度和管理的最小单元;每个Pod都有一个独立的IP;一个Pod由若干个容器构成。一个Pod之内的容器共享Pod的所有资源,这些资源主要包括:共享存储(以Volumes的形式)、共享网络、共享端口等。Kubernetes虽然也是一个容器编排系统,但不同于其他系统,它的最小操作单元不是单个容器,而是Pod。这个特性给Kubernetes带来了很多优势,比如最显而易见的是同一个Pod内的容器可以非常方便的互相访问(通过localhost就可以访问)和共享数据。

A Pod is a group of one or more application containers (such as Docker or rkt) and includes shared storage (volumes), IP address and information about how to run them.

Pods overview

Containers should only be scheduled together in a single Pod if they are tightly coupled and need to share resources such as disk.

Nodes overview

Services和Labels

上一步我们已经创建了一个应用,并且通过proxy实现了集群外部可以访问,但这种Proxy的方式是不太适用于实际生产环境的。本节我们再介绍一个Kubernetes里面另外一个非常重要的概念———Service。Service是Kubernetes里面抽象出来的一层,它定义了由多个Pods组成的逻辑组(logical set),可以对组内的Pod做一些事情:

  • 对外暴露流量
  • 做负载均衡(load balancing)
  • 服务发现(service-discovery)

而且每个Service都有一个集群内唯一的私有IP和对外的端口,用于接收流量。如果我们想将一个Service暴露到集群外,有两种方法:

  • LoadBalancer - 提供一个公网的IP
  • NodePort - 使用NAT将Service的端口暴露出去。Minikube只支持这种方式。
A Kubernetes Service is an abstraction layer which defines a logical set of Pods and enables external traffic exposure, load balancing and service discovery for those Pods.

Services overview

我们再来介绍一下Kubernetes里面的第三个比较重要的概念——Label。Service就是靠Label选择器(Label Selectors)来匹配组内的Pod的,而且很多命令都可以操作Label。Label是绑定在对象上(比如Pod)的键值对,主要用来把一些相关的对象组织在一起,并且对于用户来说label是有含义的,比如:

  • Production environment (production, test, dev)
  • Application version (beta, v1.3)
  • Type of service/server (frontend, backend, database)

当然,Label是随时可以更改的。

Labels are key/value pairs that are attached to objects

Labels

接着上的例子,我们使用Service的方式将我们之前部署的helloworld应用暴露到集群外部。因为Minikube只支持NodePort方式,所以这里我们使用NodePort方式。使用kubectl get service可以查看目前已有的service,Minikube默认创建了一个kubernetes Service。我们使用expose命令再创建一个Service:

# 查看已有Service
➜  ~ kubectl get service
NAME          CLUSTER-IP   EXTERNAL-IP   PORT(S)          AGE
hello-nginx   10.0.0.163   <pending>     80:31943/TCP     1d
helloworld    10.0.0.114   <nodes>       8090:30350/TCP   1h
kubernetes    10.0.0.1     <none>        443/TCP          1d

# 创建新的Service
➜ ~ kubectl expose deployment/helloworld --type="NodePort" --port 8090
service "helloworld" exposed

# 查看某个Service的详细信息
➜ ~ kubectl describe service/helloworld
Name:            helloworld
Namespace:        default
Labels:            run=helloworld
Selector:        run=helloworld
Type:            NodePort
IP:            10.0.0.114
Port:            <unset>    8090/TCP
NodePort:        <unset>    30350/TCP
Endpoints:        172.17.0.3:8090
Session Affinity:    None
No events.

这样内部应用helloworld的8090端口映射到了外部的30350端口。此时我们就可以通过外部访问里面的应用了:

➜  ~ curl 192.168.99.100:30350
Hello world !

这里的IP是minikube的Docker host的IP,可以通过minikube docker-env命令查看。再看Label的例子,创建Pod的时候默认会生产一个Label和Label Selector,我们可以使用kubectl label命令创建新的Label。

➜  ~ kubectl describe pod helloworld-2080166056-4q88d
Name:        helloworld-2080166056-4q88d
Namespace:    default
Node:        minikube/192.168.99.100
Start Time:    Tue, 10 Jan 2017 16:36:22 +0800
Labels:        pod-template-hash=2080166056    # 默认的Label
        run=helloworld
Status:        Running
IP:        172.17.0.3
Controllers:    ReplicaSet/helloworld-2080166056
Containers:
  helloworld:
    Container ID:    docker://a45e88e7cb9fdb193a2ed62539918e293aa381d5c8fedb57ed78e54df9ea502a
    Image:        registry.hnaresearch.com/public/hello-world:v1.0
    Image ID:        docker://sha256:b3737bd8b3af68017bdf932671212be7211f4cdc47cd63b2300fbd71057ecf83
    Port:        8090/TCP
    State:        Running
      Started:        Tue, 10 Jan 2017 16:36:32 +0800
    Ready:        True
    Restart Count:    0
    Volume Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-zfl0m (ro)
    Environment Variables:    <none>
Conditions:
  Type        Status
  Initialized     True
  Ready     True
  PodScheduled     True
Volumes:
  default-token-zfl0m:
    Type:    Secret (a volume populated by a Secret)
    SecretName:    default-token-zfl0m
QoS Class:    BestEffort
Tolerations:    <none>
No events.


# 新增一个Label
➜  ~ kubectl label pod helloworld-2080166056-4q88d app=v1
pod "helloworld-2080166056-4q88d" labeled


➜  ~ kubectl describe pod helloworld-2080166056-4q88d
Name:        helloworld-2080166056-4q88d
Namespace:    default
Node:        minikube/192.168.99.100
Start Time:    Tue, 10 Jan 2017 16:36:22 +0800
Labels:        app=v1        # 新增的Label
        pod-template-hash=2080166056
        run=helloworld
Status:        Running
IP:        172.17.0.3
Controllers:    ReplicaSet/helloworld-2080166056
Containers:
  helloworld:
    Container ID:    docker://a45e88e7cb9fdb193a2ed62539918e293aa381d5c8fedb57ed78e54df9ea502a
    Image:        registry.hnaresearch.com/public/hello-world:v1.0
    Image ID:        docker://sha256:b3737bd8b3af68017bdf932671212be7211f4cdc47cd63b2300fbd71057ecf83
    Port:        8090/TCP
    State:        Running
      Started:        Tue, 10 Jan 2017 16:36:32 +0800
    Ready:        True
    Restart Count:    0
    Volume Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-zfl0m (ro)
    Environment Variables:    <none>
Conditions:
  Type        Status
  Initialized     True
  Ready     True
  PodScheduled     True
Volumes:
  default-token-zfl0m:
    Type:    Secret (a volume populated by a Secret)
    SecretName:    default-token-zfl0m
QoS Class:    BestEffort
Tolerations:    <none>
No events.

# 使用Label的例子
➜  ~ kubectl get service -l run=helloworld
NAME         CLUSTER-IP   EXTERNAL-IP   PORT(S)          AGE
helloworld   10.0.0.114   <nodes>       8090:30350/TCP   5m
➜  ~ kubectl get pod -l app=v1
NAME                          READY     STATUS    RESTARTS   AGE
helloworld-2080166056-4q88d   1/1       Running   0          1d

删掉一个Service使用kubectl delete service命令:

➜  ~ kubectl delete service helloworld

删除Service后,我们就没法通过外部访问到内部应用了,但是Pod内的应用依旧是在正常运行的。

Scale an App

PS:这个Scale不知道咋翻译才好,就用scale吧。

Scaling is accomplished by changing the number of replicas in a Deployment.

刚才我们部署了一个应用,并且增加了Service。但是这个应用只运行在一个Pod上面。随着流量的增加,我们可能需要增加我们应用的规模来满足用户的需求。Kubernetes的Scale功能就可以实现这个需求。

You can create from the start a Deployment with multiple instances using the --replicas parameter for the kubectl run command

Scaling overview 1

Scaling overview 2

扩大应用的规模时,Kubernetes将会在Nodes上面使用可用的资源来创建新的Pod,并运行新增加的应用,缩小规模时做相反的操作。Kubernetes也支持自动规模化Pod。当然我们也可以将应用的数量变为0,这样就会终止所有部署该应用的Pods。应用数量增加后,Service内的负载均衡就会变得非常有用了,为了表现出这个特性,我修改了一下程序,除了打印“Hello world”以外,还会打印主机名。我们先看一下现有的一些输出字段的含义:

➜  ~ kubectl get deployment
NAME         DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
helloworld   1         1         1            1           2m

可以看到,现在我们只有一个Pod,

  • DESIRED字段表示我们配置的replicas的个数,即实例的个数。
  • CURRENT字段表示目前处于running状态的replicas的个数。
  • UP-TO-DATE字段表示表示和预先配置的期望状态相符的replicas的个数。
  • AVAILABLE字段表示目前实际对用户可用的replicas的个数。

下面我们使用kubectl scale命令将启动4个复制品,语法规则是kubectl scale deployment-type name replicas-number:‘

➜  ~ kubectl scale deployment/helloworld --replicas=4
deployment "helloworld" scaled

# 查看应用实例个数
➜  ~ kubectl get deployment
NAME         DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
helloworld   4         4         4            4           9m

# 查看Pod个数
➜  ~ kubectl get pod -o wide
NAME                          READY     STATUS    RESTARTS   AGE       IP           NODE
helloworld-2080166056-fn03c   1/1       Running   0          9m        172.17.0.2   minikube
helloworld-2080166056-jlwz1   1/1       Running   0          32s       172.17.0.4   minikube
helloworld-2080166056-mmpn4   1/1       Running   0          32s       172.17.0.3   minikube
helloworld-2080166056-wh696   1/1       Running   0          32s       172.17.0.5   minikube

可以看到,我们已经有4个应用实例了,而且Pod个数也变成4个了,每个都有自己的IP。当然,日志里面也有相关信息:

➜  ~ kubectl describe deployment/helloworld
Name:            helloworld
Namespace:        default
CreationTimestamp:    Thu, 12 Jan 2017 13:37:47 +0800
Labels:            run=helloworld
Selector:        run=helloworld
Replicas:        4 updated | 4 total | 4 available | 0 unavailable
StrategyType:        RollingUpdate
MinReadySeconds:    0
RollingUpdateStrategy:    1 max unavailable, 1 max surge
Conditions:
  Type        Status    Reason
  ----        ------    ------
  Available     True    MinimumReplicasAvailable
OldReplicaSets:    <none>
NewReplicaSet:    helloworld-2080166056 (4/4 replicas created)
Events:
  FirstSeen    LastSeen    Count    From                SubObjectPath    Type        Reason            Message
  ---------    --------    -----    ----                -------------    --------    ------            -------
  11m        11m        1    {deployment-controller }            Normal        ScalingReplicaSet    Scaled up replica set helloworld-2080166056 to 1
  2m        2m        1    {deployment-controller }            Normal        ScalingReplicaSet    Scaled up replica set helloworld-2080166056 to 4

然后我们再看下之前我们创建的Service信息:

➜  ~ kubectl describe service/helloworld
Name:            helloworld
Namespace:        default
Labels:            run=helloworld
Selector:        run=helloworld
Type:            NodePort
IP:            10.0.0.170
Port:            <unset>    8090/TCP
NodePort:        <unset>    31030/TCP
Endpoints:        172.17.0.2:8090,172.17.0.3:8090,172.17.0.4:8090 + 1 more...
Session Affinity:    None
No events.

可以看到Service的信息也已经更新了。让我们验证一下这个Service是有负载均衡的:

➜  ~ curl 192.168.99.100:31030
Hello world !
hostname:helloworld-2080166056-jlwz1
➜  ~ curl 192.168.99.100:31030
Hello world !
hostname:helloworld-2080166056-mmpn4
➜  ~ curl 192.168.99.100:31030
Hello world !
hostname:helloworld-2080166056-wh696
➜  ~ curl 192.168.99.100:31030
Hello world !
hostname:helloworld-2080166056-mmpn4
➜  ~ curl 192.168.99.100:31030
Hello world !
hostname:helloworld-2080166056-wh696

接着我们将实例缩减为2个:

➜  ~ kubectl scale deployment/helloworld --replicas=2
deployment "helloworld" scaled
➜  ~ kubectl get deployment
NAME         DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
helloworld   2         2         2            2           18m
➜  ~ kubectl get pod -o wide
NAME                          READY     STATUS    RESTARTS   AGE       IP           NODE
helloworld-2080166056-fn03c   1/1       Running   0          18m       172.17.0.2   minikube
helloworld-2080166056-wh696   1/1       Running   0          9m        172.17.0.5   minikube

Update an App

Kubernetes还提供了一个非常有用的特性——滚动更新(Rolling update),这个特性的好处就是我们不用停止服务就可以实现应用更新。默认更新的时候是一个Pod一个Pod更新的,所以整个过程服务不会中断。当然你也可以设置一次更新的Pod的百分比。而且更新过程中,Service只会将流量转发到可用的节点上面。更加重要的是,我们可以随时回退到旧版本。

Rolling updates allow Deployments' update to take place with zero downtime by incrementally updating Pods instances with new ones.
If a Deployment is exposed publicly, the Service will load-balance the traffic only to available Pods during the update.

Rolling updates overview 1

Rolling updates overview 2

Rolling updates overview 3

Rolling updates overview 4

OK,我们来实践一下。我们在原来程序的基础上,多输出一个v2作为新版本,使用set image命令指定新版本镜像。

# 使用set image命令执行新版本镜像
➜  ~ kubectl set image deployments/helloworld helloworld=registry.hnaresearch.com/public/hello-world:v2.0
deployment "helloworld" image updated
➜  ~ kubectl get pod
NAME                          READY     STATUS              RESTARTS   AGE
helloworld-2080166056-fn03c   1/1       Running             0          28m
helloworld-2161692841-8psgp   0/1       ContainerCreating   0          5s
helloworld-2161692841-cmzg0   0/1       ContainerCreating   0          5s

# 更新中
➜  ~ kubectl get pods
NAME                          READY     STATUS              RESTARTS   AGE
helloworld-2080166056-fn03c   1/1       Running             0          28m
helloworld-2161692841-8psgp   0/1       ContainerCreating   0          20s
helloworld-2161692841-cmzg0   0/1       ContainerCreating   0          20s

# 已经更新成功
➜  ~ kubectl get pods
NAME                          READY     STATUS    RESTARTS   AGE
helloworld-2161692841-8psgp   1/1       Running   0          2m
helloworld-2161692841-cmzg0   1/1       Running   0          2m
➜  ~ curl 192.168.99.100:31030
Hello world v2!        # 输出以变为v2
hostname:helloworld-2161692841-cmzg0

然后我们继续更新到不存在的v3版本:

➜  ~ kubectl set image deployments/helloworld helloworld=registry.hnaresearch.com/public/hello-world:v3.0
deployment "helloworld" image updated

# 更新中
➜  ~ kubectl get pods
NAME                          READY     STATUS              RESTARTS   AGE
helloworld-2161692841-cmzg0   1/1       Running             0          16m
helloworld-2243219626-7qhzh   0/1       ContainerCreating   0          4s
helloworld-2243219626-rfw64   0/1       ContainerCreating   0          4s

# 更新时,Service只会讲请求转发到可用节点
➜  ~ curl 192.168.99.100:31030
Hello world v2!
hostname:helloworld-2161692841-cmzg0
➜  ~ curl 192.168.99.100:31030
Hello world v2!
hostname:helloworld-2161692841-cmzg0

# 因为镜像不存在,所以更新失败了。但仍然有一个Pod是可用的
➜  ~ kubectl get pod
NAME                          READY     STATUS         RESTARTS   AGE
helloworld-2161692841-cmzg0   1/1       Running        0          16m
helloworld-2243219626-7qhzh   0/1       ErrImagePull   0          21s
helloworld-2243219626-rfw64   0/1       ErrImagePull   0          21s

因为我们指定了一个不存在的镜像,所以更新失败了。现在我们使用kubectl rollout undo命令回滚到之前v2的版本:

➜  ~ kubectl rollout undo deployment/helloworld
deployment "helloworld" rolled back
➜  ~ kubectl get pod
NAME                          READY     STATUS    RESTARTS   AGE
helloworld-2161692841-cmzg0   1/1       Running   0          19m
helloworld-2161692841-mw9g2   1/1       Running   0          5s
➜  ~ curl 192.168.99.100:31030
Hello world v2!
hostname:helloworld-2161692841-cmzg0
➜  ~ curl 192.168.99.100:31030
Hello world v2!
hostname:helloworld-2161692841-mw9g2

结束语

Kubernetes是Google根据他们多年的生产经验以及已有系统Brog等开发的一套全新系统,虽然目前我还只是个初学者,但是能够感觉到Kubernetes在一些结构设计方面相比于其他容器编排系统有着独特的见解。至于Kubernetes是什么,官方也有比较详细的说明:https://kubernetes.io/docs/whatisk8s。后面我抽空会将这篇文章翻译一下,以加深理解。

哦,对,Kubernetes一词源于希腊语,是舵手(helmsman)、船员(pilot)的意思。因为首字母和最后一个字母中间有8个字母,所以又简称K8s。

参考:http://kubernetes.io/docs/tutorials

]]>
5 http://niyanchun.com/kubernetes-trial.html#comments http://niyanchun.com/feed/tag/docker/
构建最小的Go程序镜像 http://niyanchun.com/build-minimal-go-image.html http://niyanchun.com/build-minimal-go-image.html Mon, 02 Jan 2017 14:22:00 +0800 NYC 我们知道构建一个Docker镜像的时候往往需要引入一些程序依赖的东西,最常见的就是引入一个基础操作系统镜像,但这样往往会使得编译出来的镜像特别大。但是对于go语言,我们可以使用静态编译的方式构建出超小的镜像。有人会问Go本身不就是静态编译吗?请接着往下看。

示例程序

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
)

func main() {
    resp, err := http.Get("http://baidu.com")
    check(err)
    body, err := ioutil.ReadAll(resp.Body)
    check(err)
    fmt.Println(len(body))
}

func check(err error) {
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

这个程序很简单,就是访问一个网页,然后打印body的大小。

构建docker镜像

使用golang:onbuild镜像

我们先简单介绍一下golang:onbuild镜像以及如何使用它。这个镜像是golang官方提供的一个用于编译运行go程序的镜像,它包含了很多ONBUILD触发器,可以涵盖大多数Go程序。其实它就是预置了一些自动执行的命令:

COPY . /go/src/app
RUN go get -d -v
RNU go install -v

它使用起来很简单:创建一个目录,把你的go文件放到该目录。然后增加Dockerfile文件,内容就一行:

FROM golang:onbuild

然后执行docker build命令构建镜像。这个golang:onbuild镜像就会自动将这个目录下的所有文件拷贝过去,并编译安装。比如对于本文的例子,我创建了一个app-onbuild目录,里面是Dockerfile和app.go文件:

➜  app-onbuild ll
total 8.0K
-rw-r--r-- 1 Allan  20 Jan  2 12:21 Dockerfile
-rw-r--r-- 1 Allan 291 Jan  2 12:20 app.go
➜  app-onbuild cat Dockerfile
FROM golang:onbuild

然后我执行编译镜像的命令docker build -t app-onbuild .,整个过程如下:

➜  app docker build -t app-onbuild .
Sending build context to Docker daemon 3.072 kB
Step 1 : FROM golang:onbuild
onbuild: Pulling from library/golang

75a822cd7888: Already exists
57de64c72267: Pull complete
4306be1e8943: Pull complete
4ba540d49835: Pull complete
5b23b80eb526: Pull complete
981c210a3af4: Pull complete
73f7f7662eed: Pull complete
520a90f1995e: Pull complete
Digest: sha256:d3cbc855152e8672412fc32d7f19371816d686b0dfddedb8fce86245910b31ac
Status: Downloaded newer image for golang:onbuild
# Executing 3 build triggers...
Step 1 : COPY . /go/src/app
Step 1 : RUN go-wrapper download
 ---> Running in 4d183d7e1e8a
+ exec go get -v -d
Step 1 : RUN go-wrapper install
 ---> Running in 31b6371f1a4f
+ exec go install -v
app
 ---> 94cc8fb334ea
Removing intermediate container f13df1977590
Removing intermediate container 4d183d7e1e8a
Removing intermediate container 31b6371f1a4f
Successfully built 94cc8fb334ea

我们可以看到整个过程如前面所述。编译完以后,golang:onbuild镜像默认还包含CMD ["app"]执行来运行编译出来的镜像。当然如果你的程序有参数,我们可以在启动的时候加命令行参数。

最后让我们来看看golang:onbuild的Dockerfile吧(这里以目前最新的1.8为例):

FROM golang:1.8
RUN mkdir -p /go/src/app
WORKDIR /go/src/app
# this will ideally be built by the ONBUILD below ;)
CMD ["go-wrapper", "run"]
ONBUILD COPY . /go/src/app
ONBUILD RUN go-wrapper download
ONBUILD RUN go-wrapper install

我们可以看到其实golang:onbuild镜像其实引用的还是golang标准镜像,只不过封装了一些自动执行的动作,使用使用起来更加方便而已。接下里我们看看如何直接基于标准的golang镜像来构建我们自己的镜像。

使用golang:latest镜像

相比于使用golang:onbuild的便利性,golang:latest给了我们更多的灵活性。我们以构建app镜像为例:创建app-golang目录,目录内容如下所示:

➜  app-golang ll
total 8.0K
-rw-r--r-- 1 Allan 101 Jan  2 12:32 Dockerfile
-rw-r--r-- 1 Allan 291 Jan  2 12:32 app.go
➜  app-golang cat Dockerfile
FROM golang:latest

RUN mkdir /app
ADD . /app/
WORKDIR /app
RUN go build -o main .
CMD ["/app/main"]

执行构建命令docker build -t app-golang .

➜  app-golang docker build -t app-golang .
Sending build context to Docker daemon 3.072 kB
Step 1 : FROM golang:latest
latest: Pulling from library/golang
75a822cd7888: Already exists
57de64c72267: Already exists
4306be1e8943: Already exists
4ba540d49835: Already exists
5b23b80eb526: Already exists
981c210a3af4: Already exists
73f7f7662eed: Already exists
Digest: sha256:5787421a0314390ca8da11b26885502b58837ebdffda0f557521790c13ddb55f
Status: Downloaded newer image for golang:latest
 ---> 6639f812dbc7
Step 2 : RUN mkdir /app
 ---> Running in a6f105ecc042
 ---> f73030d40507
Removing intermediate container a6f105ecc042
Step 3 : ADD . /app/
 ---> 3fcc194ce29d
Removing intermediate container 013c2192f90e
Step 4 : WORKDIR /app
 ---> Running in b8a2ca8d7ae0
 ---> 853dfe15c6cd
Removing intermediate container b8a2ca8d7ae0
Step 5 : RUN go build -o main .
 ---> Running in e0de5c273d7b
 ---> 28ef112e8c23
Removing intermediate container e0de5c273d7b
Step 6 : CMD /app/main
 ---> Running in 82c67389d9ab
 ---> 139ad10f61dc
Removing intermediate container 82c67389d9ab
Successfully built 139ad10f61dc

其实golang标准镜像还有一个非常方便的用途。比如我们需要开发go应用,但是又不想安装go环境或者还没有安装,那么我们可以直接使用golang标准镜像来在容器里面编译go程序。假设当前目录是我们的工程目录,那么我们可以使用如下命令来编译我们的go工程:

$ docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp golang:latest go build -v

这条命令的含义是把当前目录作为一个卷挂载到golang:latest镜像的/usr/src/myapp目录下,并将该目录设置为工作目录,然后在该目录下执行go build -v,这样就会在该目录下编译出一个名为myapp的可执行文件。当然默认编译出的是linux/amd64架构的二进制文件,如果我们需要编译其他系统架构的文件,可以加上相应的参数,比如我们要编译Windows下的32位的二进制文件,可执行如下命令:

$ docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp -e GOOS=windows -e GOARCH=386 golang:latest go build -v

当然,我们也可以shell脚本一次编译出多种OS下的文件:

$ docker run --rm -it -v "$PWD":/usr/src/myapp -w /usr/src/myapp golang:latest bash
$ for GOOS in darwin linux windows; do
> for GOARCH in 386 amd64; do
> go build -v -o myapp-$GOOS-$GOARCH
> done
> done

OK,言归正传,我们继续来进行本文要讨论的话题。我们看一下上述两种方式编译出来的镜像的大小:

➜  app-golang docker images | grep -e golang -e app
app-golang                                      latest              139ad10f61dc        36 minutes ago      679.3 MB
app-onbuild                                     latest              94cc8fb334ea        39 minutes ago      679.3 MB
golang                                          onbuild             a422f764b58c        2 weeks ago         674 MB
golang                                          latest              6639f812dbc7        2 weeks ago         674 MB

可以看到,golang-onbuildgolang-latest两个基础镜像大小都为647MB,而我们编译出来的自己的镜像大小为679.3MB,也就是说我们自己的程序其实只有5.3MB。这是为什么呢?因为我们使用的两个基础镜像是通用镜像,他们包含了go依赖的所有东西。比如我们看一下golang-1.8的Dockerfile:

FROM buildpack-deps:jessie-scm
# gcc for cgo
RUN apt-get update && apt-get install -y --no-install-recommends \
        g++ \
        gcc \
        libc6-dev \
        make \
        pkg-config \
    && rm -rf /var/lib/apt/lists/*
ENV GOLANG_VERSION 1.8beta2
ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz
ENV GOLANG_DOWNLOAD_SHA256 4cb9bfb0e82d665871b84070929d6eeb4d51af6bedbc8fdd3df5766e937ef84c
RUN curl -fsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz \
    && echo "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - \
    && tar -C /usr/local -xzf golang.tar.gz \
    && rm golang.tar.gz
ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
WORKDIR $GOPATH
COPY go-wrapper /usr/local/bin/

这样我们就不奇怪了,因为基础镜像里面包含了太多的东西,但其实我们的程序只使用了极少的一部分东西。下面我们就介绍一种方法可以只编译我们程序依赖的东西,这样编译出来的镜像非常的小。

静态编译

其实在生产环境中,对于Go应用我们往往是现在本地编译出一个可执行文件,然后将这个文件打进容器里面,而不是在容器里面进行编译,这样可以做到容器里面只有我们需要的东西,而不会引入一些只在编译过程中需要的文件。这种方式一般也有两种操作方法:第一种就是利用go build编译出二进制文件,然后在Dockerfile里面引入一个操作系统镜像作为程序运行环境。这样打出的镜像也比较大,但是却不会有编译过程中依赖的一些文件,所以相比较之前的方式,这种方式打出来的镜像也会小很多。在镜像大小不是特别重要的场景下,我比较喜欢这种方式,因为基础的操作系统往往带了一些基础的命令,如果我们的程序有些异常什么的,我们可以登录到这些容器里面去查看。比如可以使用psnetstat等命令。但如果没有操作系统的话,这些命令就都没法使用了。当然,本文讨论的就是如何构建最小的镜像,所以接下来我们介绍第二种操作方法:利用scratch镜像构建最小的Go程序镜像。

scratch镜像其实是一个特殊的镜像,为什么特殊呢?因为它是一个空镜像。但是它却是非常重要的。我们知道Dockerfile文件必须以FROM开头,但如果我们的镜像真的是不依赖任何其他东西的时候,我们就可以FROM scratch。在Docker 1.5.0之后,FROM scratch已经变成一个空操作(no-op),也就是说它不会再单独占一层了。

OK,下面我们来进行具体的操作。我们先创建一个go-scratch目录,然后将先go build编译出来一个二进制文件拷贝到这个目录,并增加Dockerfile文件:

➜  app-scratch ll
total 5.1M
-rw-r--r-- 1 Allan   36 Jan  2 13:44 Dockerfile
-rwxr-xr-x 1 Allan 5.1M Jan  2 13:42 app
➜  app-scratch cat Dockerfile
FROM scratch
ADD app /
CMD ["/app"]

然后打镜像:

➜  app-scratch docker build -t app-scratch .
Sending build context to Docker daemon 5.281 MB
Step 1 : FROM scratch
 --->
Step 2 : ADD app /
 ---> 65d4b96cf3a3
Removing intermediate container 2a6498e02c75
Step 3 : CMD /app
 ---> Running in c8f2958f09e2
 ---> dcd05e331135
Removing intermediate container c8f2958f09e2
Successfully built dcd05e331135

可以看到,FROM scratch并没有单独占一层。然后我们运行构建出来的镜像:

➜  app-scratch docker images app-scratch
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
app-scratch         latest              7ef9c5620b4f        5 minutes ago       5.259 MB

只有5.259MB,和之前相比是不是超级小呢?

NB(不是牛逼,是nota bene):

我们知道Go的编译是静态的,但是如果你的Go版本是1.5之前的,那你编译你的程序时最好使用如下命令去编译:

CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

因为Go 1.5之前Go依赖了一些C的一些库,Go编译的时候是动态链接这些库的。这样你使用scratch的方式打出来的镜像在运行时会因为找不到这些库而报错。CGO_ENABLED=0表示静态编译cgo,里面的GOOS改为你程序运行的目标机器的系统,-a的意思是重新编译所有包。

参考:

  1. https://blog.codeship.com/building-minimal-docker-containers-for-go-applications/
  2. https://hub.docker.com/_/golang/
]]>
9 http://niyanchun.com/build-minimal-go-image.html#comments http://niyanchun.com/feed/tag/docker/
搭建registry mirror http://niyanchun.com/deploy-registry-mirror.html http://niyanchun.com/deploy-registry-mirror.html Mon, 19 Dec 2016 22:51:00 +0800 NYC Docker registry是专门用于存放docker镜像的,docker官方提供了docker hub,是全球最大的docker镜像存储中心。但是在中国既没有服务器也没有CDN,所以导致pull镜像特别的慢,而且很不稳定。解决这个问题的方式一般有两种:

  1. 搭建自己私有的docker registry,存储镜像,并定期同步官方常用的镜像。
  2. 搭建docker mirror。

其实,选用哪一种或者both主要取决于自己的使用场景。如果你想要一个类似docker hub的私有registry,那肯定是第一种。如果你只是想解决从docker hub拉取镜像慢的问题,那就选择第二种,因为第一种的维护成本比较高。当然,不管是哪一种,我们都可以使用docker官方开源的registry镜像去实现。如果你想部署私有的registry,可以关注一下VMware开源的Harbor项目,其在开源registry的基础上增加了一些实际应用场景中需要的一些特性,比如项目权限、角色管理等。本文介绍如何使用部署registry mirror。

在这之前,需要强调一下目前如果registry部署为mirror方式,将只能pull镜像而不能push镜像

registry mirror原理

Docker Hub的镜像数据分为两部分:index数据和registry数据。前者保存了镜像的一些元数据信息,数据量很小;后者保存了镜像的实际数据,数据量比较大。平时我们使用docker pull命令拉取一个镜像时的过程是:先去index获取镜像的一些元数据,然后再去registry获取镜像数据。

所谓registry mirror就是搭建一个registry,然后将docker hub的registry数据缓存到自己本地的registry。整个过程是:当我们使用docker pull去拉镜像的时候,会先从我们本地的registry mirror去获取镜像数据,如果不存在,registry mirror会先从docker hub的registry拉取数据进行缓存,再传给我们。而且整个过程是流式的,registry mirror并不会等全部缓存完再给我们传,而且边缓存边给客户端传。

对于缓存,我们都知道一致性非常重要。registry mirror与docker官方保持一致的方法是:registry mirror只是缓存了docker hub的registry数据,并不缓存index数据。所以我们pull镜像的时候会先连docker hub的index获取镜像的元数据,如果我们registry mirror里面有该镜像的缓存,且数据与从index处获取到的元数据一致,则从registry mirror拉取;如果我们的registry mirror有该镜像的缓存,但数据与index处获取的元数据不一致,或者根本就没有该镜像的缓存,则先从docker hub的registry缓存或者更新数据。

registry mirror的工作原理我们就介绍完了,下面介绍如何利用docker开源的registry镜像部署自己的registry mirror。

registry mirror部署

NB:假设我们将缓存的数据存放到/data目录。

  1. 从官方拉取registry的镜像,目前最新的registry镜像是2.5版本(我使用的是2.5.0)。
  2. 获取registry的默认配置:

    docker run -it --rm --entrypoint cat registry:2.5.0  /etc/docker/registry/config.yml > config.yml

    文件的内容大概是下面这样:

    version: 0.1
    log:
      fields:
        service: registry
    storage:
      cache:
        blobdescriptor: inmemory
      filesystem:
        rootdirectory: /var/lib/registry
    http:
      addr: :5000
      headers:
        X-Content-Type-Options: [nosniff]
    health:
      storagedriver:
        enabled: true
        interval: 10s
        threshold: 3

    我们在最后面加上如下配置:

    proxy:
      remoteurl: https://registry-1.docker.io
      username: [username]
      password: [password]

    usernamepassword是可选的,如果配置了的话,那registry mirror除了可以缓存所有的公共镜像外,也可以访问这个用户所有的私有镜像。

  3. 启动registry容器:

    docker run  --restart=always -p 5000:5000 --name v2-mirror -v /data:/var/lib/registry -v  $PWD/config.yml:/etc/registry/config.yml registry:2.5.0 /etc/registry/config.yml

    当然我们也可以使用docker-compose启动:

    version: '2'
    services:
      registry:
        image: library/registry:2.5.0
        container_name: registry_mirror
        restart: always
        volumes:
          - /data:/var/lib/registry
          - ./config.yml:/etc/registry/config.yml
        ports:
          - 5000:5000
        command:
          ["serve", "/etc/registry/config.yml"]

    当我们看到如下日志输出的时候就说明已经启动成功了:

    time="2016-12-19T14:22:35Z" level=warning msg="No HTTP secret provided - generated random secret. This may cause problems with uploads if multiple registries are behind a load-balancer. To provide a shared secret, fill in http.secret in the configuration file or set the REGISTRY_HTTP_SECRET environment variable." go.version=go1.6.3 instance.id=da5468c4-1ee1-4df2-95cf-1336127c87bb version=v2.5.0
    time="2016-12-19T14:22:35Z" level=info msg="redis not configured" go.version=go1.6.3 instance.id=da5468c4-1ee1-4df2-95cf-1336127c87bb version=v2.5.0
    time="2016-12-19T14:22:35Z" level=info msg="Starting upload purge in 39m0s" go.version=go1.6.3 instance.id=da5468c4-1ee1-4df2-95cf-1336127c87bb version=v2.5.0
    time="2016-12-19T14:22:35Z" level=info msg="using inmemory blob descriptor cache" go.version=go1.6.3 instance.id=da5468c4-1ee1-4df2-95cf-1336127c87bb version=v2.5.0
    time="2016-12-19T14:22:35Z" level=info msg="Starting cached object TTL expiration scheduler..." go.version=go1.6.3 instance.id=da5468c4-1ee1-4df2-95cf-1336127c87bb version=v2.5.0
    time="2016-12-19T14:22:35Z" level=info msg="Registry configured as a proxy cache to https://registry-1.docker.io" go.version=go1.6.3 instance.id=da5468c4-1ee1-4df2-95cf-1336127c87bb version=v2.5.0
    time="2016-12-19T14:22:35Z" level=info msg="listening on [::]:5000" go.version=go1.6.3 instance.id=da5468c4-1ee1-4df2-95cf-1336127c87bb version=v2.5.0

至此,registrymirror就算部署完了。我们也可以用curl验证一下服务是否启动OK:

curl -I http://registrycache.example.com:5000/v2/
HTTP/1.1 200 OK
Content-Length: 2
Content-Type: application/json; charset=utf-8
Docker-Distribution-Api-Version: registry/2.0
Date: Thu, 17 Sep 2015 21:42:02 GMT

registry mirror使用

要使用registry mirror,我们需要配置一下自己的docker daemon。

对于Mac:在docker的客户端的Preferences——>Advanced——>Registry mirrors里面添加你的地址,然后重启。

对于Ubuntu 14.04:在/etc/default/docker文件中添加DOCKER_OPTS="$DOCKER_OPTS --registry-mirror=http://registrycache.example.com:5000”,然后重启docker(services docker restart)。

其他系统和发行版的配置方法请Google之。

然后我们pull一个本地不存在的镜像,这时去查看registry mirror服务器的data目录下面已经有了数据。执行如下命令也可以看到效果:

curl https://mycache.example.com:5000/v2/library/busybox/tags/list

需要说明的是缓存的镜像的有效期默认是一周(168hour),而且如果registry被配置成mirror模式,这个时间是不能通过maintenance部分来改变的:

  maintenance:
    uploadpurging:
      enabled: true
      age: 168h
      interval: 24h
      dryrun: false
    readonly:
      enabled: false

我研究了好久发现怎么改都不能生效,最后发现mirror模式下这个时间竟然在registry的代码里面写死了:

// todo(richardscothern): from cache control header or config
const repositoryTTL = time.Duration(24 * 7 * time.Hour)

囧o(╯□╰)o~不过看这TODO,应该是后面会改的~

当然如果你想你的registry mirror是https的话,在config.yml的http部分增加tls配置即可:

   http:
      addr: :5000
      headers:
        X-Content-Type-Options: [nosniff]
      tls:
        certificate: /etc/registry/domain.crt
        key: /etc/registry/domain.key

OK,至此registry mirror就介绍完了。

]]>
5 http://niyanchun.com/deploy-registry-mirror.html#comments http://niyanchun.com/feed/tag/docker/
Harbor多节点部署实践 http://niyanchun.com/harbor-multi-nodes-deployment-practices.html http://niyanchun.com/harbor-multi-nodes-deployment-practices.html Tue, 13 Dec 2016 17:22:00 +0800 NYC Harbor是VMware开源的一套企业级Registry解决方案,功能比较丰富,特别是增加了角色管理、镜像复制等在实际场景中非常有用的功能。项目地址见:https://github.com/vmware/harbor。本文介绍一种多节点配置方法,主要是实现HA。

1. 部署架构

Harbor多机部署方案.png

这个架构所有Harbor共享存储。这个存储包含两个部分:(1)数据库存储(主要包括用户信息、工程信息、日志信息等)。(2)镜像信息。这里我使用的共享存储是AWS的RDS和S3,当然其他任何共享的存储都应该是可以的。

2. 部署过程

2.1 部署Harbor

Harbor的部署方式与单机部署一样,不过需要修改存储相关的部分为共享存储。修改点如下:

  1. 修改数据库相关的存储:(1)修改harbor/make/common/templates/uiharbor/make/common/templates/jobservice下面的env文件,将其中的MYSQL_HOSTMYSQL_PORTMYSQL_USR等信息改为我的AWS RDS的信息。(2)修改harbor/make/harbor.cfg文件,将其中的db_password密码配置为RDS中用户的密码。
  2. 修改镜像相关存储:修改harbor/make/common/templates/registry下的config.yml,将默认的本地存储改为S3.比如我的改完之后信息如下:

    version: 0.1
    log:
      level: debug
      fields:
        service: registry
    storage:
        cache:
            layerinfo: inmemory
        s3:
            accesskey:******
            secretkey:*****
            region: cn-north-1
            bucket:*****
            rootdirectory: /s3/harbor/registry
        maintenance:
            uploadpurging:
                enabled: false
        delete:
            enabled: true
    http:
        addr: :5000
        secret: placeholder
        debug:
            addr: localhost:5001
    auth:
      token:
        issuer: registry-token-issuer
        realm: $ui_url:8080/service/token
        rootcertbundle: /etc/registry/root.crt
        service: token-service
    
    notifications:
      endpoints:
          - name: harbor
            disabled: false
            url: http://ui/service/notifications
            timeout: 3000ms
            threshold: 5
            backoff: 1s
  3. 重要:修改harbor/make/common/templates/nginx/nginx.http.conf文件,注释掉其中的location /v2/中的proxy_set_header X-Forwarded-Proto $$scheme;,不然到时候docker pull和push镜像的时候会有问题。

集群内的Harbor都按照这种方式部署。

2.2 部署LB

我是用Nginx做了一个LB,其配置文件如下:

worker_processes auto;

events {
  worker_connections 1024;
  use epoll;
  multi_accept on;
}

http {
  tcp_nodelay on;

  # this is necessary for us to be able to disable request buffering in all cases
  proxy_http_version 1.1;

  upstream harbor {
    ip_hash;
    server xxx.xxx.xxx.xxx:8080;
    server xxx.xxx.xxx.yyy:8080;
  }


  server {
    listen 443; server_name xxxx.com;
    ssl on;
    #root html;
    #index index.html index.htm;
    ssl_certificate   /***/***.pem;
    ssl_certificate_key  /***/***.key;
    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers AESGCM:ALL:!DH:!EXPORT:!RC4:+HIGH:!MEDIUM:!LOW:!aNULL:!eNULL;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;

    # disable any limits to avoid HTTP 413 for large image uploads
    client_max_body_size 0;

    # required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486)
    chunked_transfer_encoding on;

    location / {
      proxy_pass http://harbor/;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
      proxy_set_header X-Forwarded-Proto $scheme;

      proxy_buffering off;
      proxy_request_buffering off;
    }
  }
}

我部署了两台harbor,监听端口都是8080,而且都是http的,而这个LB是https的。对于LB,有一个注意点就是upstream中需要使用ip_hash方式轮询,否则会有session问题。这样整个部署就完成了。此时你可以使用https://xxx.com去访问harbor了。

3. 其他

  1. 之前我的LB使用的也是http,不修改harbor/make/common/templates/nginx/nginx.http.conf文件也不会有什么问题,但换成https之后,不修改pull和push就会一直Retrying,还不知道是什么问题。
  2. 如果后端存储使用S3的话,需要使用#1244之后的Harbor版本,不然pull和push的时候也会有问题。
  3. 当然这种部署方式主要是为了HA:只要集群内有一台Harbor正常工作,系统就是可用的。但是因为后端使用的是共享存储,所以实际使用中可能在存储这块会有性能瓶颈。另外,这种多实例使用共享存储是否会有问题,也没有经过生产验证。本文的部署方式只是对Harbor多节点部署方式的一种探索与实践,暂时还不保证生产环境使用会有什么问题。

4. 补充(更新于2017.1.24)

最近发现之前方案部署时一些可能出现的问题,这里再补充一下。

token问题

  1. 之前的部署方案如果后台的Harbor没有公网IP的话,就会有问题。而多节点部署时,后台的Harbor都是在私网内,只有前端的LB暴露在公网。而registry得配置文件里面需要配置一个获取token的地址,这个地址必须是公网可以访问的。因为registry会把这个返回给访问registry的用户,让用户去获取token,如果公网没法访问,用户就没法docker login了。所以,如果后台的Harbor是在私网内,那在原来配置的基础上需要将harbor/make/common/templates/nginx/nginx.http.conf文件里面的realm配置为LB的公网IP(http(s)://LB公网IP或域名/service/token)。
  2. 多节点部署的时候多个Harbor的private_key.pem文件要一样,否则可能出现在一台上面生成的token到另外一台验证不通过。需要注意的是执行./prepare命令默认会重新生成registry用来生成token的密钥文件,所以要么加参数让不重新生成pem文件,要么就将一台生成的拷贝到其他节点上去。
  3. 多个服务器的时间也要完全一致,不然可能出现token过期或者时间未到的问题。
]]>
0 http://niyanchun.com/harbor-multi-nodes-deployment-practices.html#comments http://niyanchun.com/feed/tag/docker/
理解Docker的镜像和容器 http://niyanchun.com/images-and-containers.html http://niyanchun.com/images-and-containers.html Wed, 02 Nov 2016 22:03:00 +0800 NYC 本文参阅自Docker官方的一篇文档:《Understand images, containers, and storage drivers》。原文有些长,有些啰嗦,所以我翻译精简了一下。如果你的英语还可以,强烈建议读原文。

Docker的口号是“Build, Ship and Run Any App, Anywhere.”,大概意思就是编译好一个应用后,可以在任何地方运行,不会像传统的程序一样,一旦换了运行环境,往往就会出现缺这个库,少那个包的问题。那么Docker是怎么做到这点的呢?简单说就是它在编译应用的时候把这个应用依赖的所有东西都编译到了镜像里面(有点像程序的静态编译——只是像而已)。我们把这个编译好的静态的东西叫docker镜像(Image),然后当docker deamon(docker的守护进程/服务进程)运行这个镜像的时候,我们称其为docker容器(Container)。好吧,你可以简单理解docker镜像和docker容器的关系就像是程序和进程的关系一样(当然实质是不一样的)。等读完此文,你会对它们的细节有一个更深入的了解,也会对docker有一个非常深入的了解(然而,如果你想深入学习docker,这还是远远不够滴...^_^)。


1. Images和Layers

每个Docker镜像(Image)都引用了一些只读的(read-only)层(layer),不同的文件系统layer也不同。这些layer堆叠在一起构成了容器(Container)的根文件系统(root filesystem)。下图是Ubuntu 15.04的镜像,共由4个镜像层(image layer)组成:
image-layers.jpg

Docker的storage driver负责堆叠这些layer,并对外提供一个统一的视图。

当你基于Ubuntu 15.04创建一个新的容器的时候,你其实只是在它的上层又增加了一个新的、薄的、可写层。这个新增的可写层称为容器层(container layer)。当这个新的容器运行时,所有的改动(比如创建新文件、修改已有文件、删除文件等)都会写到这一层。下图展示了一个新容器的结构:

container-layers.jpg

新旧版本Content addressable storage的变化:“Content addressable storage”是什么意思呢?其实主要是说layer的在磁盘存储时地址/名字选取的策略。Docker 1.10之前会给每个layer随机生成一个UUID作为其标识,这个UUID也是存储该layer层数据的目录名字。Docker1.10版本引入了一种新的方式——内容哈希(content hash),这种方式更加安全,而且通过内部实现避免了ID冲突的问题,还可以保证pull、push、load、save等操作之后数据的完整性(integrity)。除此以外,它可以让不同Image之间更好的共享一些公共的layer,哪怕他们不是从同一个Dockerfile构建来的(layer共享的问题后面会讲)。

下图展示了新版本的这个变化:

container-layers-cas.jpg

我们可以看到所有的镜像层的ID都是由密码散列而来,而容器层的ID依旧是随机生成的UUID。这个变化看似只是名字的变化,但其实二者是不兼容的。所以旧版本升级到新版本后,数据也必须做迁移才可以使用。当然官方提供了迁移工具,原文也给了例子,这里就不翻译了,有兴趣的去看原文。

2. Container和Layers

容器和镜像的主要区别就是顶部的那个可写层(即之前说的那个“container layer”)。容器运行时做的所有操作都会写到这个可写层里面,当容器删除的时候,这个可写层也会被删掉,但底层的镜像依旧保持不变。所以,不同的容器都有自己的可写层,但可以共享同一个底层镜像。下图展示了多个容器共享同一个Ubuntu 15.04镜像。

sharing-layers.jpg

Docker的storage driver负责管理只读的镜像层和可写的容器层,当然不同的driver实现的方式也不同,但其后都有两项关键技术:可堆叠的镜像层(stackable image layer)和写时拷贝技术(copy-on-write, CoW)。

3. 写时拷贝策略

系统内需要同一份数据的进程共享同一份数据实例,只有当某个进程需要对这些数据做修改时,它才会把数据拷贝一份做修改,而这个修改也只对它自己可见,其它的进程依旧使用的是原来的数据。Docker使用这个技术极大的减小了镜像对于磁盘空间的占用以及容器的启动时间(如果某个容器的镜像层已经被其他容器启动了,那它就只需启动自己的容器层就可以了)。比如对于AUFS和OverlayFS这两个storage driver,他们的CoW流程大致如下:

  • 从最新的layer自上而下,一层一层寻找需要更新的镜像层。
  • 当找到需要更新的文件的时候,就执行“copy-up”操作。这个操作其实就是把这个文件拷贝到自己的可写层/容器层去。
  • 修改自己可写层的这个拷贝。

当然,当文件比较大,或者镜像的层数比较多等情况下,这个“copy-up”操作成为性能负担。不过好在对于同一个文件的多次修改只会拷贝一次。

4. 数据持久化

刚开始的时候,docker一般只适用于无状态的计算场景使用。但随着发展,docker通过data volume技术也可以做到数据持久化了。data volume就是我们将主机的某个目录挂载到容器里面,这个data volume不受storage driver的控制,所有对这个data volume的操作会绕过storage driver直接其操作,其性能也只受本地主机的限制。而且我们可以挂载任意多个data volume到容器中,不同容器也可以共享同一个data volume。

下图展示了一个Docker主机上面运行着两个容器.每一个容器在主机上面都有着自己的地址空间(/var/lib/docker/...),除此以外,它们还共享着主机上面的同一个/data目录。

shared-volume.jpg

更多关于data volume的信息,请参阅 Manage data in containers.

]]>
0 http://niyanchun.com/images-and-containers.html#comments http://niyanchun.com/feed/tag/docker/
Ubuntu安装Docker http://niyanchun.com/install-docker-on-ubuntu.html http://niyanchun.com/install-docker-on-ubuntu.html Fri, 09 Sep 2016 19:06:00 +0800 NYC 1. 先决条件

(1)Docker现在支持以下版本的Ubuntu:

  • Ubuntu Xenial 16.04 LTS
  • Ubuntu Wily 15.10
  • Ubuntu Trusty 14.04 LTS
  • Ubuntu Precise 12.04 LTS

PS:虽然Ubuntu 14.10和15.04在Docker的APT源里面也有,但是官方已经不再提供支持了。

(2)Docker只支持64位系统,且内核版本不低于3.10.如果该要求不满足,请更换64位Ubuntu系统或者升级内核版本。

(3)对于Ubuntu 14.04、15.10、16.04版本,推荐安装linux-image-extra-*内核包,这样就可以使用aufs存储器。安装方法如下:

$ sudo apt-get update
$ sudo apt-get install linux-image-extra-$(uname -r) linux-image-extra-virtual

2. 增加Docker的源

(1)使apt支持https以及增加根证书。

$ sudo apt-get update
$ sudo apt-get install apt-transport-https ca-certificates

(2)增加GPG key。

$ sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D

(3)打开/etc/apt/sources.list.d/docker.list文件(不存在则创建)并清空。然后增加下面的内容:

  • Ubuntu 12.04
deb https://apt.dockerproject.org/repo ubuntu-precise main
  • Ubuntu 14.04
deb https://apt.dockerproject.org/repo ubuntu-trusty main
  • Ubuntu 15.10
deb https://apt.dockerproject.org/repo ubuntu-wily main
  • Ubuntu 16.04
deb https://apt.dockerproject.org/repo ubuntu-xenial main

保存关闭。

(4)然后执行下面的命令。

$ sudo apt-get update     # 更新源
$ sudo apt-get purge lxc-docker    # 清除旧的仓库
$ apt-cache policy docker-engine   # 查看获取docker的源是否正确

3. 安装

分别执行下面的命令:

$ sudo apt-get update                    # 更新源
$ sudo apt-get install docker-engine     # 安装Docker
$ sudo service docker start              # 启动docker守护进程

检查docker是否安装正确:

$ sudo docker run hello-world                  

这个命令会下载最新的测试镜像并在容器里面运行,运行时会打印一条信息,然后退出。

4. 可选配置

4.1 用户组配置

因为docker守护进程使用的是Unix套接字,默认情况下unix套接字属于root,其他用户只能通过sudo访问。所以docker守护进程总是以root用户运行。为了避免我们每次使用docker命令都要加sudo,我们可以单独创建一个docker组,然后把使用docker的用户加到这个组里面。当docker守护进程启动的时候,它让该组对Unix套接字具有读写权限(当然,这会带来一定的安全风险)。命令如下:

$ sudo groupadd docker    # 组可能已经存在
$ sudo usermod -aG docker $USER  # 把当前用户加入到docker组

然后注销该用户重新登录,我们以该用户执行docker命令就不需要再加sudo了。

4.2 配置Docker开机自启

Ubuntu 15.04及之后的版本使用systemctl控制自启。所以执行如下命令即可:

$ sudo systemctl enable docker

Ubuntu 14.10及之前版本安装时docker会自动配置upstart开机自启。

4.3 升级Docker

$ sudo apt-get upgrade docker-engine

4.4 卸载Docker

$ sudo apt-get purge docker-engine                            # 卸载Docker包
$ sudo apt-get autoremove --purge docker-engine     # 卸载Docker包依赖的一些不再使用的包
$ sudo rm -rf /var/lib/docker                                         # 删除所有的镜像及容器等

原文网址:https://docs.docker.com/engine/installation/linux/ubuntulinux/

5. 其它

因为墙的原因,很可能从docker官方安装docker特别缓慢,甚至下载不下来。这时可以从国内的一些源安装,比如可以使用下面命令安装docker-engine:

curl -sSL https://get.daocloud.io/docker | sh

docker安装好以后,默认下载docker镜像是从Docker Hub下载的,对于国内用户,极其不稳定,而且速度堪忧。所以国内一些公司提供了镜像(Docker Mirror),其原理就是如果你需要下载的镜像它那里已经存在,就会直接从它那里下载,如果它那里不存在,它就先替你下载,然后你再从它那里下载。目前国内提供该服务的公司很多,比如阿里云加速器、DaoCloud加速器、灵雀云加速器等。你注册以后,会给你生成一个url,将这个url加入你的docker配置文件中,重启docker守护进程即可生效:

对于upstart系统(比如Ubuntu 14.04),编辑/etc/default/docker文件(将其中的url替换为你的url):

DOCKER_OPTS="--registry-mirror=https://xxxx.mirror.aliyuncs.com"

对于其他系统请自行Google修改方法。

]]>
0 http://niyanchun.com/install-docker-on-ubuntu.html#comments http://niyanchun.com/feed/tag/docker/