Service Mesh深度学习系列|istio源码分析之pilot-agent组件分析

Service Mesh深度学习系列(一)| istio源码分析之pilot-agent模块分析

本文分析的istio代码版本为0.8.0,commit为0cd8d67,commit时间为2018年6月18日。

**pilot总体架构

istio architecture 上面是[官方关于pilot的架构图][2],因为是old_pilot_repo目录下,可能与最新架构有出入,仅供参考。所谓的pilot包含两个组件:pilot-agent和pilot-discovery。图里的agent对应pilot-agent二进制,proxy对应envoy二进制,它们两个在同一个容器中,discovery service对应pilot-discovery二进制,在另外一个跟应用分开部署的单独的deployment中。

  1. discovery service:从Kubernetes apiserver list/watch serviceendpointpodnode等资源信息,监听istio控制平面配置信息(Kubernetes CRD),翻译为envoy可以直接理解的配置格式。
  2. proxy:也就是envoy,直接连接discovery service,间接地从Kubernetes apiserver等服务注册中心获取集群中微服务的注册情况
  3. agent:本文分析对象pilot-agent,生成envoy配置文件,管理envoy生命周期
  4. service A/B:使用了istio的应用,如Service A/B,的进出网络流量会被proxy接管

对于模块的命名方法,本文采用模块对应源码main.go所在包名称命名法。其他istio分析文章有其他命名方法。比如pilot-agent也被称为istio pilot,因为它在Kubernetes上的部署形式为一个叫istio-pilot的deployment。

pilot-agent的部署存在形式

pilot-agent在pilot/cmd包下面,是个单独的二进制。
pilot-agent跟envoy打包在同一个docker镜像里,镜像由Dockerfile.proxy定义。Makefile(include了tools/istio-docker.mk)把这个dockerfile build成了proxy镜像,也就是Kubernetes里跟应用放在同一个pod下的sidecar。非Kubernetes情况下需要把pilot-agent、envoy跟应用部署在一起,这个就有点“污染”应用的意思了。

支持v2 api的sidecar镜像为proxyv2,镜像中包含的pilot-agent和envoy二进制文件和proxy镜像中的完全相同,只是使用不同的envoy bootstrap配置(envoy_bootstrap_tmpl.json vs. envoy_bootstrap_v2.json)。但是当前仅完成部分开发工作,makefile中build proxyv2镜像的target默认也不会自动执行。

pilot-agent功能简述

在proxy镜像中,pilot-agent负责的工作包括:

  1. 生成envoy的配置
  2. 启动envoy
  3. 监控并管理envoy的运行状况,比如envoy出错时pilot-agent负责重启envoy,或者envoy配置变更后reload envoy

而envoy负责接受所有发往该pod的网络流量,分发所有从pod中发出的网络流量。

根据代码中的sidecar-injector-configmap.yaml(用来配置如何自动化地inject istio sidecar),inject过程中,除了proxy镜像作为sidecar之外,每个pod还会带上initcontainer(Kubernetes中的概念),具体镜像为proxy_init。proxy_init通过注入iptables规则改写流入流出pod的网络流量规则,使得流入流出pod的网络流量重定向到proxy的监听端口,而应用对此无感。

pilot-agent主要功能分析之一:生成envoy配置

envoy的配置主要在pilot-agent的init方法与proxy命令处理流程的前半部分生成。其中init方法为pilot-agent二进制的命令行配置大量的flag与flag默认值,而proxy命令处理流程的前半部分负责将这些flag组装成为envoy的配置ProxyConfig对象。下面分析几个相对重要的配置。

role

pilot-agent的role类型为model包下的Proxy,决定了pilot-agent的“角色”,role包括以下属性:

  1. Type
    pilot-agent有三种运行模式。根据role.Type变量定义,类型为model.Proxy,定义在context.go文件中,允许的3个取值范围为:

    1. “sidecar”
      默认值,可以在启动pilot-agent,调用proxy命令时覆盖。Sidecar type is used for sidecar proxies in the application containers
    2. “ingress”
      Ingress type is used for cluster ingress proxies
    3. “router”
      Router type is used for standalone proxies acting as L7/L4 routers
  2. IPAddress, ID, Domain
    它们都可以通过pilot-agent的proxy命令的对应flag来提供用户自定义值。如果用户不提供,则会在proxy命令执行时,根据istio连接的服务注册中心(service registry)类型的不同,会采用不同的配置方式。agent当前使用的服务注册中心类型保存在pilot-agent的registry变量里,在init函数中初始化为默认值Kubernetes。当前只处理以下三种情况:

    1. Kubernetes
    2. Consul
    3. Other

    |:–|:–|:–|:–| | registry值 | role.IPAddress | rule.ID |role.Domain | |Kubernetes | 环境变量INSTANCE_IP | 环境变量POD_NAME.环境变量POD_NAMESPACE | 环境变量POD_NAMESPACE.svc.cluster.local | |Consul | private IP,默认127.0.0.1 | IPAddress.service.consul | service.consul| |Other |private IP,默认127.0.0.1 | IPAddress | “” |

其中的private ip通过WaitForPrivateNetwork函数获得。

istio需要从服务注册中心(service registry)获取微服务注册的情况。当前版本中istio可以对接的服务注册中心类型包括:

  1. “Mock”
    MockRegistry is a service registry that contains 2 hard-coded test services
  2. “Config”
    ConfigRegistry is a service registry that listens for service entries in a backing ConfigStore
  3. “Kubernetes”
    KubernetesRegistry is a service registry backed by k8s API server
  4. “Consul”
    ConsulRegistry is a service registry backed by Consul
  5. “Eureka”
    EurekaRegistry is a service registry backed by Eureka
  6. “CloudFoundry”
    CloudFoundryRegistry is a service registry backed by Cloud Foundry.

官方about文档说当前支持Kubernetes, Nomad with Consul,未来准备支持 Cloud Foundry,Apache Mesos。另外根据官方的feature成熟度文档,当前只有Kubernetes的集成达到stable程度,Consul,Eureka和Cloud Foundry都还是alpha水平

envoy配置文件及命令行参数

agent.waitForExit会调用envoy.Run方法启动envoy进程,为此需要获取envoy二进制所在文件系统路径和flag两部分信息:

  1. envoy二进制所在文件系统路径:evony.Run通过proxy.config.BinaryPath变量得知envoy二进制所在的文件系统位置,proxy就是envoy对象,config就是pilot-agent的main方法在一开始初始化的proxyConfig对象。里面的BinaryPath在pilot-agent的init方法中被初始化,初始值来自pilot/pkg/model/context.go的DefaultProxyConfig函数,值是/usr/local/bin/envoy
  2. envoy的启动flag形式为下面的startupArgs,包含一个-c指定的配置文件,还有一些flag。除了下面代码片段中展示的这些flag,还可以根据启动agent时的flag,再加上--concurrency, --service-zone等flag。

startupArgs := []string{"-c", fname,
"--restart-epoch", fmt.Sprint(epoch),
"--drain-time-s", fmt.Sprint(int(convertDuration(proxy.config.DrainDuration) / time.Second)),
"--parent-shutdown-time-s", fmt.Sprint(int(convertDuration(proxy.config.ParentShutdownDuration) / time.Second)),
"--service-cluster", proxy.config.ServiceCluster,
"--service-node", proxy.node,
"--max-obj-name-len", fmt.Sprint(MaxClusterNameLength),
}
关于以上启动envoy的flag及其值的解释:

  1. –restart-epoch:epoch决定了envoy hot restart的顺序,在后面会有详细描述,第一个envoy进程对应的epoch为0,后面新建的envoy进程对应epoch顺序递增1
  2. –drain-time-s:在pilot-agent init函数中指定默认值为2秒,可通过pilot-agent proxy命令的drainDuration flag指定
  3. –parent-shutdown-time-s:在pilot-agent init函数中指定默认值为3秒,可通过pilot-agent proxy命令的parentShutdownDuration flag指定
  4. –service-cluster:在pilot-agent init函数中指定默认值为”istio-proxy”,可通过pilot-agent proxy命令的serviceCluster flag指定
  5. –service-node:将agent.role的Type,IPAddress,ID和Domain用”~”连接起来

而上面的-c指定的envoy配置文件有几种生成的方式:

  1. 运行pilot-agent时,用户不指定customConfigFile参数(agent init时默认为空),但是制定了templateFile参数(agent init时默认为空),这时agent的main方法会根据templateFile帮用户生成一个customConfigFile,后面就视作用户制定了customConfigFile。这个流程在agent的main方法里
  2. 如果用户制定了customConfigFile,那么就用customConfigFile
  3. 如果用户customConfigFile和templateFile都没指定,则调用pilot/pkg包下的bootstrap_config.go中的WriteBootstrap自动生成一个配置文件,默认将生成的配置文件放在/etc/istio/proxy/envoy-rev%d.json,这里的%d会用epoch序列号代替。WriteBootstrap在envoy.Run方法中被调用

举个例子的话,根据参考文献中某人实验,第一个envoy进程启动参数为:

-c /etc/istio/proxy/envoy-rev0.json --restart-epoch 0
--drain-time-s 45 --parent-shutdown-time-s 60
--service-cluster sleep
--service-node sidecar~172.00.00.000~sleep-55b5877479-rwcct.default~default.svc.cluster.local
--max-obj-name-len 189 -l info --v2-config-only

如果使用第三种方式自动生成默认的envoy配置文件,如上面例子中的envoy-rev0.json,那么pilot-agent的proxy命令处理流程中前半部分整理的大量envoy参数中的一部分会被写入这个配置文件中,比如DiscoveryAddressDiscoveryRefreshDelayZipkinAddressStatsdUdpAddress

证书文件

agent会监控chainfile,keyfile和rootcert三个证书文件的变化,如果是Ingress工作模式,则还会加入ingresscert,ingress key这2个证书文件

pilot-agent主要功能分析之二:envoy监控与管理

为envoy生成好配置文件之后,pilot-agent还要负责envoy进程的监控与管理工作,包括:

  1. 创建envoy对象,结构体包含proxyConfig(前面步骤中为envoy生成的配置信息),role.serviceNode(似乎是agent唯一标识符),loglevel和pilotsan(service account name)
  2. 创建agent对象,包含前面创建的envoy结构体,一个epochs的map,3个channel:configCh, statusCh和abortCh
  3. 创建watcher并启动协程执行watcher.Run
    watcher.Run首先启动协程执行agent.Run(agent的主循环),然后调用watcher.Reload(kickstart the proxy with partial state (in case there are no notifications coming)),Reload会调用agent.ScheduleConfigUpdate,并最终导致第一个envoy进程启动,见后面分析。然后监控各种证书,如果证书文件发生变化,则调用ScheduleConfigUpdate来reload envoy,然后watcher.retrieveAZ(TODO)
  4. 创建context,调用cmd.WaitSignal以等待进程接收到SIGINT, SIGTERM信号,接受到信号之后通过context通知agent,agent接到通知后调用terminate来kill所有envoy进程,并退出agent进程

上面的pilot/pkg/proxy包下的agent中采用Proxy接口管理pilot/pkg/proxy/envoy包下的envoy对象,从理论上来说也可以把envoy换成其他proxy实现管理。不过此事还牵扯discovery service等其他组件。

上面第三步启动协程执行的agent.Run是agent的主循环,会一直通过监听以下几个channel来监控envoy进程:

  1. agent的configCh:如果配置文件,主要是那些证书文件发生变化,则调用agent.reconcile来reload envoy
  2. statusCh:这里的status其实就是exitStatus,处理envoy进程退出状态,处理流程如下:
    1. 把刚刚退出的epoch从agent维护的两个map里删了,后面会讲到这两个map。把agent.currentConfig置为agent.latestEpoch对应的config,因为agent在reconcile的过程中只有在desired config和current config不同的时候才会创建新的epoch,所以这里把currentConfig设置为上一个config之后,必然会造成下一次reconcile的时候current与desired不等,从而创建新的envoy
    2. 如果exitStatus.err是errAbort,表示是agent让envoy退出的(这个error是调用agent.abortAll时发出的),这时只要log记录epoch序列号为xxx的envoy进程退出了
    3. 如果exitStatus.err并非errAbort,则log记录epoch异常退出,并给所有当前正在运行的其他epoch进程对应的abortCh发出errAbort,所以后续其他envoy进程也都会被kill掉,并全都往agent.statusCh写入exitStatus,当前的流程会全部再为每个epoch进程走一遍
    4. 如果是其他exitStatus(什么时候会进入这个否则情况?比如exitStatus.err是wait epoch进程得到的正常退出信息,即nil),则log记录envoy正常退出
    5. 调用envoy.Cleanup,删除刚刚退出的envoy进程对应的配置文件,文件路径由ConfigPath和epoch序列号串起来得到
    6. 如果envoy进程为非正常退出,也就是除了“否则”描述的case之外的2中情况,则试图恢复刚刚退出的envoy进程(可见前面向所有其他进程发出errAbort消息的意思,并非永远停止envoy,pilot-agent接下来马上就会重启被abort的envoy)。恢复方式并不是当场启动新的envoy,而是schedule一次reconcile。如果启动不成功,可以在得到exitStatus之后再次schedule(每次间隔时间为 $2^n*200$ 毫秒 ),最多重试10次(budget),如果10次都失败,则退出整个golang的进程(os.Exit),由容器环境决定如何恢复pilot-agent。所谓的schedule,就是往agent.retry.restart写入一个预定的未来的某个时刻,并扣掉一次budget(budget在每次reconcile之前都会被重置为10),然后就结束当前循环。在下一个开始的时候,会检测agent.retry.restart,如果非空,则计算距离reconcile的时间delay
  3. time.After(delay):监听是否到时间执行schedule的reconcile了,到了则执行agent.reconcile
  4. ctx.Done:执行agent.terminate
    terminate方法比较简单,向所有的envoy进程的abortCh发出errAbort消息,造成他们全体被kill(Cmd.Kill),然后agent自己return,退出当前的循环,这样就不会有人再去重启envoy

pilot-agent主要功能分析之三:envoy启动流程

  1. 前面pilot-agent proxy命令处理流程中,watcher.Run会调用agent.ScheduleConfigUpdate,这个方法只是简单地往configCh里写一个新的配置,所谓的配置是所有certificate算出的sha256哈希值
  2. configCh的这个事件会被agent.Run监控到,然后调用agent.reconcile。
  3. reconcile方法会启动协程执行agent.waitForExit从而启动envoy
    看reconcile方法名就知道是用来保证desired config和current config保持一致的。reconcile首先会检查desired config和current config是否一致,如果是的话,就不用启动新的envoy进程。否则就启动新的envoy。在启动过程中,agent维护两个map来管理一堆envoy进程,在调用waitForExit之前会将desiredConfig赋值给currentConfig,表示reconcile工作完成:

    1. 第一个map是agent.epochs,它将整数epoch序列号映射到agent.desiredConfig。这个序列号从0开始计数,也就是第一个envoy进程对应epoch 0,后面递增1。但是如果有envoy进程异常退出,它对应的序列号并非是最大的情况下,这个空出来的序列号不会在计算下一个新的epoch序列号时(agent.latestEpoch方法负责计算当前最大的epoch序列号)被优先使用。所以从理论上来说序列号是会被用光的
    2. 第二个map是agent.abortCh,它将epoch序列号映射到与envoy进程一一对应的abortCh。abortCh使得pilot-agent可以在必要时通知对应的envoy进程推出。这个channel初始化buffer大小为常量10,至于为什么需要10个buffer,代码中的注释说buffer aborts to prevent blocking on failing proxy,也就是万一想要abort某个envoy进程,但是envoy卡住了abort不了,有buffer的话,就不会使得管理进程也卡住。
  4. waitForExit会调用agent.proxy.Run,也就是envoy的Run方法这里会启动envoy。envoy的Run方法流程如下:
    1. 调用exec.Cmd.Start方法(启动了一个新进程),并将envoy的标准输出和标准错误置为os.Stdout和Stderr。
    2. 持续监听前面说到由agent创建并管理的,并与envoy进程一一对应的abortCh,如果收到abort事件通知,则会调用Cmd.Process.Kill方法杀掉envoy,如果杀进程的过程中发生错误,也会把错误信息log一下,然后把从abortCh读到的事件返回给waitForExit。waitForExit会把该错误再封装一下,加入epoch序列号,然后作为envoy的exitStatus,并写入到agent.statusCh里
    3. 启动一个新的协程来wait刚刚启动的envoy进程,并把得到的结果写到done channel里,envoy结构体的Run方法也会监听done channel,并把得到的结果返回给waitForExit
      这里我们总结启动envoy过程中的协程关系:agent是全局唯一一个agent协程,它在启动每个envoy的时候,会再启动一个waitForExit协程,waitForExit会调用Command.Start启动另外一个进程运行envoy,然后waitForExit负责监听abortCh和envoy进程执行结果。

Cmd.Wait只能用于等待由Cmd.Start启动的进程,如果进程结束并范围值为0,则返回nil,如果返回其他值则返回ExitError,也可能在其他情况下返回IO错误等,Wait会释放Cmd所占用的所有资源

每次配置发生变化,都会调用agent.reconcile,也就会启动新的envoy,这样envoy越来越多,老的envoy进程怎么办?pilot-agent代码的注释里已经解释了这问题,原来pilot-agent不用关闭老的envoy,同一台机器上的多个envoy进程会通过unix domain socket互相通讯,即使不同envoy进程运行在不同容器里,也一样能够通讯。而借助这种通讯机制,可以自动实现新envoy进程替换之前的老进程,也就是所谓的envoy hot restart。

代码注释原文:Hot restarts are performed by launching a new proxy process with a strictly incremented restart epoch. It is up to the proxy to ensure that older epochs gracefully shutdown and carry over all the necessary state to the latest epoch. The agent does not terminate older epochs.

而为了触发这种hot restart的机制,让新envoy进程替换之前所有的envoy进程,新启动的envoy进程的epoch序列号必须比之前所有envoy进程的最大epoch序列号大1

代码注释原文:The restart protocol matches Envoy semantics for restart epochs: to successfully launch a new Envoy process that will replace the running Envoy processes, the restart epoch of the new process must be exactly 1 greater than the highest restart epoch of the currently running Envoy processes.

技术干货|深入理解flannel

根据官网的描述,flannel是一个专为kubernetes定制的三层网络解决方案,主要用于解决容器的跨主机通信问题。

1.概况


首先,flannel利用Kubernetes API或者etcd用于存储整个集群的网络配置,其中最主要的内容为设置集群的网络地址空间。例如,设定整个集群内所有容器的IP都取自网段“10.1.0.0/16”。

接着,flannel在每个主机中运行flanneld作为agent,它会为所在主机从集群的网络地址空间中,获取一个小的网段subnet,本主机内所有容器的IP地址都将从中分配。

然后,flanneld再将本主机获取的subnet以及用于主机间通信的Public IP,同样通过kubernetes API或者etcd存储起来。

最后,flannel利用各种backend mechanism,例如udp,vxlan等等,跨主机转发容器间的网络流量,完成容器间的跨主机通信。

1.1 容器间的跨主机通信如何运行

如下图所示,集群范围内的网络地址空间为10.1.0.0/16,Machine A获取的subnet为10.1.15.0/24,且其中的两个容器IP分别为10.1.15.2/24和10.1.15.3/24,两者都在10.1.15.0/24这一子网范围内,对于下方的Machine B同理。 enter image description here

如果上方Machine A中IP地址为10.1.15.2/24的容器要与下方Machine B中IP地址为10.1.16.2/24的容器进行通信,封包是如何进行转发的。从上文可知,每个主机的flanneld会将自己与所获取subnet的关联信息存入etcd中,例如,subnet 10.1.15.0/24所在主机可通过IP 192.168.0.100访问,subnet 10.1.16.0/24可通过IP 192.168.0.200访问。反之,每台主机上的flanneld通过监听etcd,也能够知道其他的subnet与哪些主机相关联。如上图,Machine A上的flanneld通过监听etcd已经知道subnet 10.1.16.0/24所在的主机可以通过Public 192.168.0.200访问,而且熟悉docker桥接模式的同学肯定知道,目的地址为10.1.16.2/24的封包一旦到达Machine B,就能通过cni0网桥转发到相应的pod,从而达到跨宿主机通信的目的。

因此,flanneld只要想办法将封包从Machine A转发到Machine B就OK了,而上文中的backend就是用于完成这一任务。不过,达到这个目的的方法是多种多样的,所以我们也就有了很多种backend. 在这里我们举例介绍的是最简单的一种方式hostgw : 因为Machine A和Machine B处于同一个子网内,它们原本就能直接互相访问。因此最简单的方法是:在Machine A中的容器要访问Machine B的容器时,我们可以将Machine B看成是网关,当有封包的目的地址在subnet 10.1.16.0/24范围内时,就将其直接转发至B即可。而这通过图中那条红色标记的路由就能完成,对于Machine B同理可得。由此,在满足仍有subnet可以分配的条件下,我们可以将上述方法扩展到任意数目位于同一子网内的主机。而任意主机如果想要访问主机X中subnet为S的容器,只要在本主机上添加一条目的地址为R,网关为X的路由即可。

下面,我们以问题驱动的方式来详细分析flannel是如何运作的。

2.节点初始化


首先,我们最感兴趣的是,当一个新的节点加入集群时,它是如何初始化的。对此,我们可能会有以下几个疑问:

若主机有多张网卡和多个IP,如何选择其中的一张网卡和一个IP用于集群主机间的通信?主机如何获取属于自己的subnet并维护?

我们如何在集群中有新的节点加入时,获取对应的subnet和Public IP,并通过配置backend进行访问?

2.1 网卡及对外IP选择

对于第一个问题,事实上我们可以在flanneld的启动参数中通过”–iface”或者”–iface-regex”进行指定。其中”–iface”的内容可以是完整的网卡名或IP地址,而”–iface-regex”则是用正则表达式表示的网卡名或IP地址,并且两个参数都能指定多个实例。flannel将以如下的优先级顺序来选取:

1) 如果”–iface”和”—-iface-regex”都未指定时,则直接选取默认路由所使用的输出网卡

2) 如果”–iface”参数不为空,则依次遍历其中的各个实例,直到找到和该网卡名或IP匹配的实例为止

3) 如果”–iface-regex”参数不为空,操作方式和2)相同,唯一不同的是使用正则表达式去匹配

最后,对于集群间交互的Public IP,我们同样可以通过启动参数”–public-ip”进行指定。否则,将使用上文中获取的网卡的IP作为Public IP。

2.2 获取subnet

在获取subnet之前,我们首先要创建一个SubnetManager,它在具体的代码实现中,表现为一个接口,如下所示:

enter image description here

从接口中各个函数的名字,我们大概就能猜出SubnetManager的作用是什么了。但是,为什么获取subnet的函数叫AcquireLease,而不叫AcquireSubnet呢?实际上,每台主机都是租借了一个subnet,如果到了一定时间不进行更新,那么该subnet就会过期从而重新分配给其他的主机,即主机和subnet的关联信息会从etcd中消失(在本文中我们将默认选择etcd作为SubnetManager的后端存储)。因此,lease就是一条subnet和所属主机的关联信息,并且具有时效性,需要定期更新。

下面我们来看看,每台主机都是如何获取lease的:

1) 首先,我们调用GetNetworkConfig(),它会访问etcd获取集群网络配置并封装在结构Config中返回,Config结构如下所示。其中的Network字段对应的集群网络地址空间是在flannel启动前,必须写入etcd中的,例如”10.1.0.0/16″。

enter image description here

对于其他字段的含义及默认值如下:

①SubnetLen表示每个主机分配的subnet大小,我们可以在初始化时对其指定,否则使用默认配置。在默认配置的情况下,如果集群的网络地址空间大于/24,则SubnetLen配置为24,否则它比集群网络地址空间小1,例如集群的大小为/25,则SubnetLen的大小为/26

②SubnetMin是集群网络地址空间中最小的可分配的subnet,可以手动指定,否则默认配置为集群网络地址空间中第一个可分配的subnet。例如对于”10.1.0.0/16″,当SubnetLen为24时,第一个可分配的subnet为”10.1.1.0/24″。

③ SubnetMax表示最大可分配的subnet,对于”10.1.0.0/16″,当subnetLen为24时,SubnetMax为”10.1.255.0/24″

④BackendType为使用的backend的类型,如未指定,则默认为“udp”

⑤ Backend中会包含backend的附加信息,例如backend为vxlan时,其中会存储vtep设备的mac地址

2) 在获取了集群的网络配置之后,接下来我们就调用SubnetManager中的AcquireLease()获取本主机的subnet。其中的参数类型LeaseAttrs如下所示:

enter image description here

显然,其中最重要的字段就是Public IP,它实质上是标识了一台主机。在获取subnet之前,我们先要从etcd中获取当前所有已经存在的lease信息—-leases,以备后用。下面我们将对不同情况下lease的获取进行讨论:

① 事实上,这可能并不是我们第一次在这台机器上启动flannel,因此,很有可能,在此之前,这台机器已经获取了lease。已知一台主机其实是由它的Public IP标识的,所以我们可以用Public IP作为关键字匹配leases中所有lease的Public IP。若匹配成功,则检查相应的lease是否和当前的集群网络配置兼容:检查的内容包括IP是否落在SubnetMin和SubnetMax内,以及subnet大小是否和SubnetLen相等。若兼容,则用新的LeaseAttrs和ttl更新该lease,表示成功获取本机的lease,否则只能将该lease删除。

② 当初始化SubnetManager时,会先试图解析之前flannel获取了lease后留下的配置文件(该文件的创建,会在下文描述),从中读取出之前获取的subnet。如果读取到的subnet不为空,则同样利用该subnet去匹配leases中所有lease的subnet。若匹配成功,则同样检查lease是否和当前的集群网络配置兼容。若兼容则更新lease,表示成功获取本机的lease,否则将其删除。如果该subnet并不能从leases中找到,但是它和当前的集群网络配置兼容的话,可以直接将它和LeaseAttrs封装为lease,写入etcd。

③ 若此时还未获取到lease,那么我们有必要自己创建一个新的了。创建的方法很简单,从SubnetMin遍历到SubnetMax,将其中和leases中已有的subnet都不重合者加入一个集合中。再从该集合随机选择一个,作为本主机的subnet即可。最后,将subnet和LeaseAttrs封装为一个lease写入etcd。由此,该主机获取了自己的subnet。

最后,我们将有关的集群网络和subnet的配置信息写入文件/run/flannel/subnet.env(可通过命令行参数”–subnet-file”手动指定)中,写入的信息如下所示,包括:集群网络地址空间FLANNEL_NETWORK,获取的子网信息FLANNEL_SUBNET等等

enter image description here

2.3 维护subnet

当SubnetManager的后端存储使用的是etcd时,各个主机还需要对自己的lease进行维护,在租期即将到期时,需要对etcd中的lease进行更新,调用SubnetManager中的RenewLease()方法,防止它到期后被自动删除。另外,我们可以在flanneld的命令行启动参数中用”–subnet-lease-renew-margin”指定在租期到期前多久进行更新。默认值为1小时,即每23小时更新一次lease,重新获取一次24小时的租期。

2.4 发现新节点

现在,初始化已经完成了,我们需要面对如下两个问题:

1、当本主机的flanneld启动时,如果集群中已经存在了其他主机,我们如何通过backend进行配置,使得封包能够到达它们

2、如果之后集群中又添加了新的主机,我们如何获取这一事件,并通过backend对配置进行调整,对于删除主机这类事件同理

当然上述两个问题,都是通过etcd解决的。backend会一边通过上文中的WatchLeases()方法对etcd进行监听,从中获取各类事件,另一边会启动一个事件处理引擎,不断地对监听到的事件进行处理。

对于问题1,我们首先要从etcd中获取当前所有的lease信息,并将其转化为一系列的event,将它交于事件处理引擎进行处理,从而让封包能够到达这些主机。

对于问题2,直接对etcd中的事件进行监听,将获取的事件转换为事件处理引擎能够处理的形式,并进行处理即可。

事件的类型也很简单,总共就只有EventAdded和EventRemoved两种,分别表示新增了lease以及一个lease过期。因为不同backend的配置方式是完全不同的,下面我们就对各种backend的基本原理进行解析,并说明它们如何处理EventAdded和EventRemoved这两类事件。

3. backend原理解析


在本节中,我们将对hostgw,udp和vxlan三种backend进行解析。

3.1 hostgw

hostgw是最简单的backend,它的原理非常简单,直接添加路由,将目的主机当做网关,直接路由原始封包。

例如,我们从etcd中监听到一个EventAdded事件subnet为10.1.15.0/24被分配给主机Public IP 192.168.0.100,hostgw要做的工作就是在本主机上添加一条目的地址为10.1.15.0/24,网关地址为192.168.0.100,输出设备为上文中选择的集群间交互的网卡即可。对于EventRemoved事件,只需删除对应的路由。

3.2 udp

我们知道当backend为hostgw时,主机之间传输的就是原始的容器网络封包,封包中的源IP地址和目的IP地址都为容器所有。这种方法有一定的限制,就是要求所有的主机都在一个子网内,即二层可达,否则就无法将目的主机当做网关,直接路由。

而udp类型backend的基本思想是:既然主机之间是可以相互通信的(并不要求主机在一个子网中),那么我们为什么不能将容器的网络封包作为负载数据在集群的主机之间进行传输呢?这就是所谓的overlay。具体过程如下所示:

enter image description here

当容器10.1.15.2/24要和容器10.1.20.2/24通信时,因为该封包的目的地不在本主机subnet内,因此封包会首先通过网桥转发到主机中。最终在主机上经过路由匹配,进入网卡flannel0。需要注意的是flannel0是一个tun设备,它是一种工作在三层的虚拟网络设备,而flanneld是一个proxy,它会监听flannel0并转发流量。当封包进入flannel0时,flanneld就可以从flannel0中将封包读出,由于flannel0是三层设备,所以读出的封包仅仅包含IP层的报头及其负载。最后flanneld会将获取的封包作为负载数据,通过udp socket发往目的主机。同时,在目的主机的flanneld会监听Public IP所在的设备,从中读取udp封包的负载,并将其放入flannel0设备内。由此,容器网络封包到达目的主机,之后就可以通过网桥转发到目的容器了。

最后和hostgw不同的是,udp backend并不会将从etcd中监听到的事件里所包含的lease信息作为路由写入主机中。每当收到一个EventAdded事件,flanneld都会将其中的subnet和Public IP保存在一个数组中,用于转发封包时进行查询,找到目的主机的Public IP作为udp封包的目的地址。

3.3 vxlan

首先,我们对vxlan的基本原理进行简单的叙述。从下图所示的封包结构来看,vxlan和上文提到的udp backend的封包结构是非常类似的,不同之处是多了一个vxlan header,以及原始报文中多了个二层的报头。

enter image description here

下面让我们来看看,当有一个EventAdded到来时,flanneld如何进行配置,以及封包是如何在flannel网络中流动的。

enter image description here

如上图所示,当主机B加入flannel网络时,和其他所有backend一样,它会将自己的subnet 10.1.16.0/24和Public IP 192.168.0.101写入etcd中,和其他backend不一样的是,它还会将vtep设备flannel.1的mac地址也写入etcd中。

之后,主机A会得到EventAdded事件,并从中获取上文中B添加至etcd的各种信息。这个时候,它会在本机上添加三条信息:

1) 路由信息:所有通往目的地址10.1.16.0/24的封包都通过vtep设备flannel.1设备发出,发往的网关地址为10.1.16.0,即主机B中的flannel.1设备。

2) fdb信息:MAC地址为MAC B的封包,都将通过vxlan首先发往目的地址192.168.0.101,即主机B

3)arp信息:网关地址10.1.16.0的地址为MAC B

现在有一个容器网络封包要从A发往容器B,和其他backend中的场景一样,封包首先通过网桥转发到主机A中。此时通过,查找路由表,该封包应当通过设备flannel.1发往网关10.1.16.0。通过进一步查找arp表,我们知道目的地址10.1.16.0的mac地址为MAC B。到现在为止,vxlan负载部分的数据已经封装完成。由于flannel.1是vtep设备,会对通过它发出的数据进行vxlan封装(这一步是由内核完成的,相当于udp backend中的proxy),那么该vxlan封包外层的目的地址IP地址该如何获取呢?事实上,对于目的mac地址为MAC B的封包,通过查询fdb,我们就能知道目的主机的IP地址为192.168.0.101。

最后,封包到达主机B的eth0,通过内核的vxlan模块解包,容器数据封包将到达vxlan设备flannel.1,封包的目的以太网地址和flannel.1的以太网地址相等,三层封包最终将进入主机B并通过路由转发达到目的容器。

事实上,flannel只使用了vxlan的部分功能,由于VNI被固定为1,本质上工作方式和udp backend是类似的,区别无非是将udp的proxy换成了内核中的vxlan处理模块。而原始负载由三层扩展到了二层,但是这对三层网络方案flannel是没有意义的,这么做也仅仅只是为了适配vxlan的模型。vxlan详细的原理参见文后的参考文献,其中的分析更为具体,也更易理解。

4. 总结


总的来说,flannel更像是经典的桥接模式的扩展。我们知道,在桥接模式中,每台主机的容器都将使用一个默认的网段,容器与容器之间,主机与容器之间都能互相通信。要是,我们能手动配置每台主机的网段,使它们互不冲突。接着再想点办法,将目的地址为非本机容器的流量送到相应主机:如果集群的主机都在一个子网内,就搞一条路由转发过去;若是不在一个子网内,就搞一条隧道转发过去。这样以来,容器的跨网络通信问题就解决了。而flannel做的,其实就是将这些工作自动化了而已。

参考文献: 1. flannel源码:https://github.com/coreos/flannel

2.《vxlan协议原理解析》:http://cizixs.com/2017/09/25/vxlan-protocol-introduction

3.《linux上实现vxlan网络》:http://cizixs.com/2017/09/28/linux-vxlan

4.《VxLAN和VTEP》:http://maoxiaomeng.com/2017/07/31/vxlan%E5%92%8Cvtep/

K8s代码贡献浙江大学排名第六

全球企业Kubernetes开源社区代码贡献排名:浙江大学SEL实验室排名第六。请看来自Stackalytics|Kubernetes community contribution官方网站截图。 1   从全球企业的贡献排名来看,排名状况基本变化不大,仍然是Google独占鳌头。令人欣喜的是,浙江大学SEL实验室排名又上升了一位,代码贡献量近24万行,总排名超越CoreOS达到世界第六位。这无疑是振奋人心的事情,这意味着越来越多的年轻学术人员投入Kubernetes开源社区建设,这将近一步提高中国在容器云社区的地位。   Kubernetes是当前容器集群编排3大开源方案中社区活跃度最高的。虽然现在正值入冬,但仍然挡不住 Kubernetes掀起全球关注的热潮。Kubernetes体现了Google对海量容器集群管理的经验和精华,这让Kubernetes在容器云计算框架横行天下的年代游刃有余,已经逐步成为了容器云框架中的佼佼者。从Kubernetes开放、自由、持续发展生态圈兼容的态度能看出,Kubernetes正变得越来越自信。   下图为Kubernetes模块代码贡献分析图: 2

从图中可知:Kubernetes运维和生态圈组件模块代码贡献直追核心模块kubernetes,排名2-4位的模块分别是Kubernetes生态圈、操作和界面。Kubernetes使用者越来越关心容器云框架落地而非仅仅将其作为空中楼阁远观而止。  Kubernetes在国内发展已经超过了3年的时间,但是遗憾的一点,尽管我们拥有世界上最多的开发人员,但是我们常常对社区没有话语权,国内的用户的需求无法对社区上游形 成影响,导致很多本地化定制的需求无法真正的在社区版本代码得到体现。所以如何让中国的声音出现在社区,是我们所有容器云计算人需要思考的问题。更希望越来越多的国内传统IT巨头能够意识到这个问题,投身于开源的事业中。

《Docker容器与容器云》第2版推荐

自Docker容器与容器云第1版出版以来,销量达到10000多本,得到了广大技术人员的认可,并且翻译成繁体,进入台湾市场。本书对Docker和Kubernetes的源码解析深入细致,是国内Docker界的良心之作。

经过作者们多年的实践经验积累及近一年的精心准备,浙江大学SEL实验室出版的《Docker容器与容器云》第2版,终于与我们见面了。

本书根据Docker 1.10版和Kubernetes 1.2版对第1版进行了全面更新,从实践者的角度出发,以Docker和Kubernetes为重点,沿着“基本用法介绍”到“核心原理解读”到“高级实践技巧”的思路,一本书讲透当前主流的容器和容器云技术,有助于读者在实际场景中利用Docker容器和容器云解决问题并启发新的思考。全书包括两部分,第一部分深入解读Docker容器技术,包括Docker架构与设计、核心源码解读和高级实践技巧;第二部分归纳和比较了三类基于Docker的主流容器云项目,包括专注Docker容器编排与部署的容器云、专注应用支撑的容器云以及一切皆容器的Kubernetes,进而详细解读了Kubernetes核心源码的设计与实现,最后介绍了几种典型场景下的Kubernetes最佳实践。

自本书第1版出版以来,容器生态圈已经发生了翻天覆地的变化。新的开源项目层出不穷,各个开源项目都在快速迭代演进。Docker已经从本书第1版里的1.6.2发展为当前的1.10。Kubernetes也从本书第1版里的0.16发展到了现在的1.2,并且在1.0.1版本时宣布其已经正式进入可投入生产环境(production ready)的状态。 

第3章是本书第一部分的重点。Docker 1.10版相对于本书第1版中的1.6.2版,主要的更新包括如下几个方面:

1、Docker在架构方面不断将自身解耦,逐步发展成容器运行时(runtime)、镜像构建(builder)、镜像分发(distribution)、网络(networking)、数据卷(volume)等独立的功能组件,提供daemon来管理,并通过Engine暴露一组标准的API来操作这些组件(详见本书3.2节);

2、将网络和数据卷提升为“一等公民”,提供了独立子命令进行操作,网络和数据卷具备独立的生命周期,不再依赖容器的生命周期(详见本书3.7节、3.8节);

3、网络实现方面,Docker将网络相关的实现解耦为独立的组件libnetwork,抽象出一个通用的容器网络模型(CNM),功能上也终于原生支持了跨主机通信(详见本书3.8节);

4、在扩展性方面,在1.7.0版本后就开始支持网络、volume和存储驱动(仍处于实验阶段)的插件化,开发者可以通过实现Docker提供的插件标准来定制自己的插件(详见本书3.6节、3.7节、3.8节);

5、在Docker安全方面,Docker支持了user namespace和seccomp来提高容器运行时的安全,在全新的镜像分发组件中引入可信赖的分发和基于内容存储的机制,从而提高镜像的安全性(详见本书3.5节、3.6节、3.9节)。

需要特别指出的一点是,随着容器如火如荼的发展,为了推动容器生态的健康发展,促进生态系统内各组织间的协同合作,容器的标准化也显得越来越重要。Linux基金会于2015年6月成立OCI(Open Container Initiative)组织,并针对容器格式和运行时制定了一个开放的工业化标准,即OCI标准。Docker公司率先贡献出满足OCI标准的容器运行时runC,HyperHQ公司也开源了自己的OCI容器运行时runV,相信业界会有越来越多的公司加入这个标准化浪潮中。Docker公司虽然没有在Docker 1.10版本中直接使用runC作为容器的运行时,但是已经将“修改Docker engine来直接调用runC的二进制文件为Docker提供容器引擎”写入到了1.10版本的roadmap中。本书在3.4.3节中对runC的构建和使用进行了介绍。   第8章是本书第二部分的重点。由于Kubernetes的代码始终处于积极更新之中,自本书第1版截稿以来,Kubernetes又相继发布了0.17、0.18、0.19、0.20、0.21、1.0、1.1与1.2等几个版本。主要的更新包括如下几个方面:

1、大大丰富了支撑的应用运行场景。从全面重构的long-running service的replicaSet,到呼声渐高的支持batch job的Job、可类比为守护进程的DaemonSet、负责进行应用更新的Deployment、具备自动扩展能力的HPA(Horizontal Pod Autoscaler),乃至于有状态服务的petSet,都已经或者即将涵盖在Kubernetes的支撑场景中(详见本书8.2节)。

2、加强各个组件的功能扩展或者性能调优。apiserver和controller manager为应对全新的resource和API有显著的扩展;scheduler也在丰富调度策略和多调度器协同调度上有积极的动作;kubelet在性能上也有长足的进步,使得目前单个节点上支持的pod从原来的30个增长到了110个,集群工作节点的规模也从100个跃升为1000个;多为人诟病的kube-proxy如今也鸟枪换炮,默认升级为iptables模式,在吞吐量上也更为乐观;在可以预期的未来,rescheduler将成为Kubernetes家庭中的新成员,使得重调度成为可能(详见本书8.3节);

3、兼容更多的容器后端模型、网络及存储方案。从Docker到rkt,Kubernetes展示了对容器后端开放姿态,同时它还准备以C/S模式实现对其他容器的支撑。在网络方面,Kubernetes引入了网络插件,其中最为瞩目的当属CNI;存储上的解决方案更是层出不穷,flocker、Cinder、CephFS不一而足,还增加了许多特殊用途的volume,如secret、configmap等(详见本书8.4节、8.5节);

4、增加了OpenID、Keystone等认证机制、Webhook等授权机制,以及更为丰富的多维资源管理机制admissioncontroller(详见本书8.6节);

5、另外,作为Kubernetes社区的积极参与者,我们还专门增加了8.8节,讨论当前社区正在酝酿中的一些新特性,如Ubernetes、petSet、rescheduler。我们还讨论了Kubernetes性能优化,以及Kubernetes与OCI的关系等话题。 

除了全面更新这两个重点章节之外,我们还在第1章中更新了Docker近期的“大事记”并重新整理了容器生态圈,加入了许多重要的容器云技术开源项目,以及OCI、CNCF等国际标准化组织;在第2章中,我们将Docker命令行工具的基础用法更新到了Docker 1.10版;在第4章中完善了对时下火热的“容器化思维”和“微服务”的讨论;在第6章中更新了对Docker“三剑客”——Compose、Swarm和Machine的讨论;在附录中以Docker 1.10版为标准更新了附录A的Docker安装指南,以Kubernetes 1.2为标准,更新了附录F中Kubernetes的安装指南。

如果你是初级程序员,本书可以帮助你熟悉Docker与kubernetes的基本使用;如果你正在IT领域进行高级进阶修炼,那本书也可以与你一起探索Docker与kubernetes的工作原理。无论是架构师、开发者、运维人员,还是对Docker比较好奇的读者,本书都是一本不可多得的带你从入门向高级进阶的精品书,值得大家选择!

docker2

 

最后,摘录一些读者的推荐如下——

 —— 许式伟,七牛云存储CEO : “虽然在此之前已经有了由Docker团队出的第一本Docker书,但是这是国内第一本深入解读Docker与Kubernetes原理的原创图书,这一点意义重大。本书比较完整地介绍了Docker与Kubernetes的工作原理和生态,非常有借鉴意义。” 

——肖德时,数人科技CTO: “Docker容器技术已经在国内如火如荼地流行起来,浙江大学SEL实验室目前是国内掌握Docker技术最熟练的技术团队之一,他们在国内Docker技术界一直产生着重要影响。这次他们把Docker的实战经验汇编成书,可以帮助更多的Docker爱好者学习到一手的实战经验。”

——程显峰,火币网CTO : “本书非常细致地讲解了Docker技术的来龙去脉和技术细节,更为难得是还加入了Docker生态当中的其他技术。Docker这项技术本身就是将多种思想和技术融合的产物,从生态的视角去解读技术的来龙去脉将极大地促进读者对云计算和容器技术的重新思考。”

—— 刘俊,百度运维部高级架构师,两次百度最高奖获得者 : “本书宏观上描绘了容器和容器云技术发展的浪潮和生态系统,微观上以Docker和Kubernetes为典型进行了深度分析。无论是Docker技术爱好者,还是系统架构师、云端开发者、系统管理和运维人员,都能在本书中找到适合自己阅读的要点。浙江大学SEL实验室云计算团队是一支非常优秀的云计算研究团队,很多85后、90后人才活跃在顶级社区前沿,感谢他们能将多年的知识和智慧积累分享出来!”

——郝林,微影时代架构师,《Go并发编程实战》作者 : “本书是浙江大学SEL实验室云计算团队多年深耕Docker及背后的容器技术的结晶。最大的特点就是深入,并且有各种实用案例和细致讲解。另外,这本书在怎样真正地把Docker及周边产品落地以构建灵活多变的云平台方面也进行了生动的阐释。”   ——网友 Monster-Z: “自本书第一版出版以来,Docker社区又经过了如火如荼的发展,特别是网络部分的实现已经发生了翻天覆地的变化,而本书在第一版的基础之上及时地对网络部分的内容进行了更新。本书对于docker网络最新内容深入透彻的分析,让我们这些急于想要对Docker网络部分实现原理有所了解的开发人员如醍醐灌顶。相信对Docker技术感兴趣的读者在读完本书之后会与我有相同的感受。”

——网友 XiaoYu: “之前对于浙江大学的SEL实验室早有耳闻,也深知他们在云计算领域有着深厚的积淀。这次对他们著作的第二版进行了深入的研读,可以看得出,在保持了第一版架构的同时,对各个章节的内容又进行了大量的扩充,紧紧跟随着Docker以及k8s社区的发展步伐。相信不论是对于初涉容器领域的新人,还是希望深入理解Docker容器实现原理的开发人员,这都是一本不可多得的好书。” 

——网友 Mongo: “在学习了本书的第一版之后,就一直期待着本书第二版的出版。于是当得到第二版出版的消息之后,就迫不及待入手了一本。捧读之后,发现在内容上与第一版有了较大的变化,这应该与Docker本身快速的发展有关,当然也从侧面反应了容器技术在时下的热度。相信对于像我这样Docker技术的一线使用者来说,本书固有的深度以及第二版的时效性都将让我们对Docker的使用和理解都更上一层楼。”

——网友 YaoDD: “之前在学习k8s的过程中,一直苦于没有深入到代码层面的书籍可供参考。国内出版的大多数图书更多的是对官方文档的”翻译”,而并没有进一步深入的解读。直到发现了浙江大学SEL实验室出版的《Docker容器与容器云》一书,其中对k8s的各个组件做到了深入源码级的原理分析,让人直呼过瘾。所以在该书第二版出版的第一时间就购买并阅读了,发现在保持第一版分析深度的同时,又加入了许多社区最新的研发成果。如果你想对k8s有着更加深入的理解,那么本书将是你的不二选择。”    ——网友 DockerLover: “作为一名Kubernetes企业级应用的一线实践者,早就对浙大的SEL实验室慕名已久。该实验室的研究人员为Kubernetes社区共享了大量的代码,数量上堪比华为等巨头企业,尤其是其中的张磊博士,更是Kubernetes的member,技术实力可见一斑。因此本书不论是深度广度,还是对于学习容器技术的指导意义都是毋庸置疑的。” 

——网友 GaryRong: “自Docker出现以来,一股容器热潮已经席卷了整个互联网,如火如荼,方兴未艾。而浙大SEL实验室在云计算和PaaS领域深耕多年,作为docker技术在国内第一批的学习者和实践者,积累了丰富的经验。而出版的《Docker 容器与容器云》从第一版到第二版,对低到底层容器技术的实现,高到整个容器生态圈的发展都有着独到而深刻的理解。相信本书一定不会让你失望。 ” 

《Docker容器与容器云》推荐

《Docker容器与容器云》是Kubernetes社区核心开发者张磊博士及其技术团队近年来PaaS方面积累,全书不仅描述了容器与容器云技术,还融入了实验室四年来对云计算各个层面的理解。该书是国内第一本对Docker原理进行深度解析的书籍,也是第一本结合PaaS对各类容器云进行深度剖析,并着重深入分析Kubernetes原理的书籍。

该书从2014年12月开始写作到2015年9月正式出版发行,期间数易其稿,从最初的对源码进行逐字逐句的分析,转变为带着问题去思考,最后再回到源码去理解问题背后的本质,书的每一章每一节都是实验室智慧与汗水的结晶。

出版发行以来,我们几乎没有对本书进行特别的宣传,凭着少数几次活动以及朋友圈里朋友们的口口相传,逐渐被广大云计算爱好者们发现,并受到了业内外读者的一致好评。从九月初至今,不到两个月的时间,第一次印刷的三千五百册就已接近全部售罄,准备进行第二次印刷。我们也会尽可能多的根据读者的反馈,对第一次印刷中存在的问题进行勘误。

我们自己没有太多权利评判书本本身的好坏,但是我们看到了业内外读者的评价,让我们感受到了深深的肯定,非常感谢大家的支持!同时也向更多朋友推荐我们这本《Docker容器与容器云》,这绝对是您在容器技术方面最值得一读的书!

Docker 容器与容器云-

最后,摘录一些读者的推荐如下:

  • “虽然在此之前已经有了由Docker团队出的第一本Docker书,但是这是国内第一本深入解读Docker与Kubernetes原理的原创图书,这一点意义重大。本书比较完整地介绍了Docker与Kubernetes的工作原理和生态,非常有借鉴意义。” ——许式伟,七牛云存储CEO

  • “Docker容器技术已经在国内如火如荼地流行起来,浙江大学SEL实验室目前是国内掌握Docker技术最熟练的技术团队之一,他们在国内Docker技术界一直产生着重要影响。这次他们把Docker的实战经验汇编成书,可以帮助更多的Docker爱好者学习到一手的实战经验。”——肖德时,数人科技CTO

  • “本书非常细致地讲解了Docker技术的来龙去脉和技术细节,更为难得是还加入了Docker生态当中的其他技术。Docker这项技术本身就是将多种思想和技术融合的产物,从生态的视角去解读技术的来龙去脉将极大地促进读者对云计算和容器技术的重新思考。”——程显峰,OneAPM首席运营官

  • “本书宏观上描绘了容器和容器云技术发展的浪潮和生态系统,微观上以Docker和Kubernetes为典型进行了深度分析。无论是Docker技术爱好者,还是系统架构师、云端开发者、系统管理和运维人员,都能在本书中找到适合自己阅读的要点。浙江大学SEL实验室云计算团队是一支非常优秀的云计算研究团队,很多85后、90后人才活跃在顶级社区前沿,感谢他们能将多年的知识和智慧积累分享出来!”——刘俊,百度运维部高级架构师,百度最高奖获得者

  • “本书是浙江大学SEL实验室云计算团队多年深耕Docker及背后的容器技术的结晶。最大的特点就是深入,并且有各种实用案例和细致讲解。另外,这本书在怎样真正地把Docker及周边产品落地以构建灵活多变的云平台方面也进行了生动的阐释。”——郝林,微影时代架构师,《Go并发编程实战》作者

  • “Docker颠覆了容器技术,也将容器技术带到了新的高度。InfoQ从2014年初就开始密切关注容器技术,见证并切身参与了容器技术的发展。作为我们的优秀作者,浙江大学SEL实验室在InfoQ撰写了很多与Docker、Kubernetes相关的技术文章,得到了广大读者的肯定。希望这本书能推动容器技术在中国的落地。”——郭蕾,InfoQ主编

  • “浙江大学SEL实验室属于国内较早接触并研究开源PaaS技术的团队之一,从传统PaaS的开源代表CloudFoundry、OpenShift,到新一代基于Docker的PaaS平台如DEIS、Flynn等,他们均有深入的研究和实践经验。更为难得的是,他们不仅参与开源贡献,而且笔耕不辍,通过博客、论坛等方式积极分享有深度、有内涵的技术文章,并广泛参与国内PaaS届各种技术交流会议。华为PaaS团队也在与之交流中汲取了不少营养。此次,他们将近年来对Docker容器和Kubernetes、DEIS、Flynn等PaaS开源平台的研究成果结集成册,内容详尽且深入浅出。我相信,无论是入门者还是老手,都能够从中获益。”——刘赫伟,华为中央软件院

  • “容器技术在大型互联网企业中已广泛应用,而Docker是容器技术中的杰出代表。本书不仅介绍了Docker基础知识,而且进行了代码级的深入分析,并通过对Kubernetes等技术的讲解延伸至集群操作系统以及对Docker生态领域的思考,同时结合了大量实践,内容丰富,值得拥有。”——王炜煜,百度运维部高级架构师,JPaaS项目负责人

  • “Docker作为操作系统层面轻量级的虚拟化技术,凭借简易的使用、快速的部署以及灵活敏捷的集成支持等优势,奠定了Docker如今在PaaS领域的江湖地位。 浙江大学SEL实验室在云计算和 PaaS领域耕耘多年, 积累了丰富的经验。本书既有对Docker源代码层面的深度解读,也有实战经验的分享,希望这本书能够帮助Docker开发者在技术上更上一层楼。”——李三红,蚂蚁金服基础技术部JVM Architect

  • 本书覆盖面非常广,从docker的使用、核心原理、高级实践再到编排工具以及著名的集群调度工具kubernetes均有涉及,很好的把握了技术人员的痛点。而且架构原理方面均是用docker1.7和部分1.6作为依据,非常有时效性。《Docker源码分析》的作者也是出于该团队,容器云方面功底很深厚啊。 ——网友Crazykev

  • 比较专业的书,书中的内容远远高于这本书的物理容量,希望好好研究Docker和Linux某些虚拟化机制的同学,可以以这本书入门去不断扩充自己的知识。不过这边毕竟是一Docker为主线的书,主要还是为了深入帮助大家理解Docker,以及Docker生态圈内的应用和服务。一句话:国内不可多得的一本Docker相关知识的书,此书讲解深入,排版合理,内容紧凑,值得好好看。 ——网友RHMAN

  • 其实之前不太了解SEL实验室的背景是什么,不过从此书的内容来看,团队的研究能力和文字能力都相当不错。好久没有读过让人“爽快”的技术书籍了。在Docker应用中各领域的知识要点都能提纲挈领的进行讲解,而且对于生产环境的挑战也有深刻的理解,算得上是既有广度又有深度的佳作。 ——网友jerryshang

Docker背后的标准化容器执行引擎——runC

文 孙健波

随着容器技术发展的愈发火热,Linux基金会于2015年6月成立OCI(Open Container Initiative)组织,旨在围绕容器格式和运行时制定一个开放的工业化标准。该组织一成立便得到了包括谷歌、微软、亚马逊、华为等一系列云计算厂商的支持。而runC就是Docker贡献出来的,按照该开放容器格式标准(OCF, Open Container Format)制定的一种具体实现。

1. 容器格式标准是什么?

制定容器格式标准的宗旨概括来说就是不受上层结构的绑定,如特定的客户端、编排栈等,同时也不受特定的供应商或项目的绑定,即不限于某种特定操作系统、硬件、CPU架构、公有云等。

该标准目前由libcontainerappc的项目负责人(maintainer)进行维护和制定,其规范文档就作为一个项目在Github上维护,地址为https://github.com/opencontainers/specs

1.1 容器标准化宗旨

标准化容器的宗旨具体分为如下五条。

  • 操作标准化:容器的标准化操作包括使用标准容器感觉创建、启动、停止容器,使用标准文件系统工具复制和创建容器快照,使用标准化网络工具进行下载和上传。

  • 内容无关:内容无关指不管针对的具体容器内容是什么,容器标准操作执行后都能产生同样的效果。如容器可以用同样的方式上传、启动,不管是php应用还是mysql数据库服务。

  • 基础设施无关:无论是个人的笔记本电脑还是AWS S3,亦或是Openstack,或者其他基础设施,都应该对支持容器的各项操作。

  • 为自动化量身定制:制定容器统一标准,是的操作内容无关化、平台无关化的根本目的之一,就是为了可以使容器操作全平台自动化。

  • 工业级交付:制定容器标准一大目标,就是使软件分发可以达到工业级交付成为现实。

1.2 容器标准包(bundle)和配置

一个标准的容器包具体应该至少包含三块部分:

  • config.json: 基本配置文件,包括与宿主机独立的和应用相关的特定信息,如安全权限、环境变量和参数等。具体如下:
    • 容器格式版本
    • rootfs路径及是否只读
    • 各类文件挂载点及相应容器内挂载目录(此配置信息必须与runtime.json配置中保持一致)
    • 初始进程配置信息,包括是否绑定终端、运行可执行文件的工作目录、环境变量配置、可执行文件及执行参数、uid、gid以及额外需要加入的gid、hostname、低层操作系统及cpu架构信息。
  • runtime.json: 运行时配置文件,包含运行时与主机相关的信息,如内存限制、本地设备访问权限、挂载点等。除了上述配置信息以外,运行时配置文件还提供了“钩子(hooks)”的特性,这样可以在容器运行前和停止后各执行一些自定义脚本。hooks的配置包含执行脚本路径、参数、环境变量等。
  • rootfs/:根文件系统目录,包含了容器执行所需的必要环境依赖,如/bin/var/lib/dev/usr等目录及相应文件。rootfs目录必须与包含配置信息的config.json文件同时存在容器目录最顶层。

1.3 容器运行时和生命周期

容器标准格式也要求容器把自身运行时的状态持久化到磁盘中,这样便于外部的其他工具对此信息使用和演绎。该运行时状态以JSON格式编码存储。推荐把运行时状态的json文件存储在临时文件系统中以便系统重启后会自动移除。

基于Linux内核的操作系统,该信息应该统一地存储在/run/opencontainer/containers目录,该目录结构下以容器ID命名的文件夹(/run/opencontainer/containers/<containerID>/state.json)中存放容器的状态信息并实时更新。有了这样默认的容器状态信息存储位置以后,外部的应用程序就可以在系统上简便地找到所有运行着的容器了。

state.json文件中包含的具体信息需要有:

  • 版本信息:存放OCI标准的具体版本号。
  • 容器ID:通常是一个哈希值,也可以是一个易读的字符串。在state.json文件中加入容器ID是为了便于之前提到的运行时hooks只需载入state.json就可以定位到容器,然后检测state.json,发现文件不见了就认为容器关停,再执行相应预定义的脚本操作。
  • PID:容器中运行的首个进程在宿主机上的进程号。
  • 容器文件目录:存放容器rootfs及相应配置的目录。外部程序只需读取state.json就可以定位到宿主机上的容器文件目录。

标准的容器生命周期应该包含三个基本过程。

  • 容器创建:创建包括文件系统、namespaces、cgroups、用户权限在内的各项内容。
  • 容器进程的启动:运行容器进程,进程的可执行文件定义在的config.json中,args项。
  • 容器暂停:容器实际上作为进程可以被外部程序关停(kill),然后容器标准规范应该包含对容器暂停信号的捕获,并做相应资源回收的处理,避免孤儿进程的出现。

1.4 基于开放容器格式(OCF)标准的具体实现

从上述几点中总结来看,开放容器规范的格式要求非常宽松,它并不限定具体的实现技术也不限定相应框架,目前已经有基于OCF的具体实现,相信不久后会有越来越多的项目出现。

  • 容器运行时opencontainers/runc,即本文所讲的runc项目,是后来者的参照标准。
  • 虚拟机运行时hyperhq/runv,基于Hypervisor技术的开放容器规范实现。
  • 测试huawei-openlab/oct基于开放容器规范的测试框架。

2. runC工作原理与实现方式

runC的前身实际上是Docker的libcontainer项目,笔者曾经写过一篇文章《Docker背后的容器管理——Libcontainer深度解析》专门对libcontainer进行源码分析和解读,感兴趣的读者可以先阅读一下,目前runC也是对libcontainer包的调用,libcontainer包变化并不大。所以此文将不再花费太多笔墨分析其源码,我们将着重讲解其中的变化。

2.1 runC从libcontainer的变迁

从本质上来说,容器是提供一个与宿主机系统共享内核但与系统中的其他进程资源相隔离的执行环境。Docker通过调用libcontainer包对namespaces、cgroups、capabilities以及文件系统的管理和分配来“隔离”出一个上述执行环境。同样的,runC也是对libcontainer包进行调用,去除了Docker包含的诸如镜像、Volume等高级特性,以最朴素简洁的方式达到符合OCF标准的容器管理实现。

总体而言,从libcontainer项目转变为runC项目至今,其功能和特性并没有太多变化,具体有如下几点。

  1. 把原先的nsinit移除,放到外面,命令名称改为runc,同样使用cli.go实现,一目了然。
  2. 按照开放容器标准把原先所有信息混在一起的一个配置文件拆分成config.jsonruntime.json两个。
  3. 增加了按照开放容器标准设定的容器运行前和停止后执行的hook脚本功能。
  4. 相比原先的nsinit时期的指令,增加了runc kill命令,用于发送一个SIG_KILL信号给指定容器ID的init进程。

总体而言,runC希望包含的特征有:

  • 支持所有的Linux namespaces,包括user namespaces。目前user namespaces尚未包含。
  • 支持Linux系统上原有的所有安全相关的功能,包括Selinux、 Apparmor、seccomp、cgroups、capability drop、pivot_root、 uid/gid dropping等等。目前已完成上述功能的支持。
  • 支持容器热迁移,通过CRIU技术实现。目前功能已经实现,但是使用起来还会产生问题。
  • 支持Windows 10 平台上的容器运行,由微软的工程师开发中。目前只支持Linux平台。
  • 支持Arm、Power、Sparc硬件架构,将由Arm、Intel、Qualcomm、IBM及整个硬件制造商生态圈提供支持。
  • 计划支持尖端的硬件功能,如DPDK、sr-iov、tpm、secure enclave等等。
  • 生产环境下的高性能适配优化,由Google工程师基于他们在生产环境下的容器部署经验而贡献。
  • 作为一个正式真实而全面具体的标准存在!

2.2 runC是如何启动容器的?

从开放容器标准中我们已经定义了关于容器的两份配置文件和一个依赖包,runc就是通过这些来启动一个容器的。首先我们按照官方的步骤来操作一下。

runc运行时需要有rootfs,最简单的就是你本地已经安装好了Docker,通过

docker pull busybox

下载一个基本的镜像,然后通过

docker export $(docker create busybox) > busybox.tar

导出容器镜像的rootfs文件压缩包,命名为busybox.tar。然后解压缩为rootfs目录。

mkdir rootfs
tar -C rootfs -xf busybox.tar

这时我们就有了OCF标准的rootfs目录,需要说明的是,我们使用Docker只是为了获取rootfs目录的方便,runc的运行本身不依赖Docker。

接下来你还需要config.jsonruntime.json,使用

runc spec 可以生成一份标准的config.jsonruntime.json配置文件,当然你也可以按照格式自己编写。

如果你还没有安装runc,那就需要按照如下步骤安装一下,目前runc暂时只支持Linux平台。

`\

create a ‘github.com/opencontainers’ in your GOPATH/src

cd github.com/opencontainers git clone https://github.com/opencontainers/runc cd runc make sudo make install `\

最后执行

runc start

你就启动了一个容器了。

可以看到,我们对容器的所有定义,均包含在两份配置文件中,一份简略的config.json配置文件类似如下,已用省略号省去部分信息,完整的可以查看官方github

{
"version": "0.1.0",
"platform": {
"os": "linux",
"arch": "amd64"
},
"process": {
"terminal": true,
"user": {
"uid": 0,
"gid": 0,
"additionalGids": null
},
"args": [
"sh"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],
"cwd": ""
},
"root": {
"path": "rootfs",
"readonly": true
},
"hostname": "shell",
"mounts": [
{
"name": "proc",
"path": "/proc"
},
……
{
"name": "cgroup",
"path": "/sys/fs/cgroup"
}
],
"linux": {
"capabilities": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
]
}
}
各部分表示的意思在1.2节中已经讲解,针对具体的内容我们可以看到,版本是0.10,该配置文件针对的是AMD64架构下的Linux系统,启动容器后执行的命令就是sh,配置的环境变量有两个,是PATHTERM,启动后user的uid和gid都为0,表示进入后是root用户。cwd项为空表示工作目录为当前目录。capabilities能力方面则使用白名单的形式,从配置上可以看到只允许三个能力,功能分别为允许写入审计日志、允许发送信号、允许绑定socket到网络端口。

一份简略的runtime.json配置则如下,同样用省略号省去了部分内容:

{
"mounts": {
"proc": {
"type": "proc",
"source": "proc",
"options": null
},
……
"cgroup": {
"type": "cgroup",
"source": "cgroup",
"options": [
"nosuid",
"noexec",
"nodev",
"relatime",
"ro"
]
}
},
"hooks": {
"prestart": null,
"poststop": null
},
"linux": {
"uidMappings": null,
"gidMappings": null,
"rlimits": [
{
"type": "RLIMIT_NOFILE",
"hard": 1024,
"soft": 1024
}
],
"sysctl": null,
"resources": {
"disableOOMKiller": false,
"memory": {
"limit": 0,
"reservation": 0,
"swap": 0,
"kernel": 0,
"swappiness": -1
},
"cpu": {
"shares": 0,
"quota": 0,
"period": 0,
"realtimeRuntime": 0,
"realtimePeriod": 0,
"cpus": "",
"mems": ""
},
"pids": {
"limit": 0
},
"blockIO": {
"blkioWeight": 0,
"blkioWeightDevice": "",
"blkioThrottleReadBpsDevice": "",
"blkioThrottleWriteBpsDevice": "",
"blkioThrottleReadIopsDevice": "",
"blkioThrottleWriteIopsDevice": ""
},
"hugepageLimits": null,
"network": {
"classId": "",
"priorities": null
}
},
"cgroupsPath": "",
"namespaces": [
{
"type": "pid",
"path": ""
},
{
"type": "network",
"path": ""
},
{
"type": "ipc",
"path": ""
},
{
"type": "uts",
"path": ""
},
{
"type": "mount",
"path": ""
}
],
"devices": [
{
"path": "/dev/null",
"type": 99,
"major": 1,
"minor": 3,
"permissions": "rwm",
"fileMode": 438,
"uid": 0,
"gid": 0
},
……
{
"path": "/dev/urandom",
"type": 99,
"major": 1,
"minor": 9,
"permissions": "rwm",
"fileMode": 438,
"uid": 0,
"gid": 0
}
],
"apparmorProfile": "",
"selinuxProcessLabel": "",
"seccomp": {
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": []
},
"rootfsPropagation": ""
}
}

可以看到基本的几项配置分别为挂载点信息、启动前与停止后hooks脚本、然后就是针对Linux的特性支持的诸如用户uid/gid绑定,rlimit配置、namespace设置、cgroups资源限额、设备权限配置、apparmor配置项目录、selinux标记以及seccomp配置。其中,namespacescgroups笔者均有写文章详细介绍过。

再下面的工作便都由libcontainer完成了,大家可以阅读这个系列前一篇文章《Docker背后的容器管理——Libcontainer深度解析》或者购买书籍《Docker容器与容器云》,里面均有详细介绍。

简单来讲,有了配置文件以后,runC就开始借助libcontainer处理以下事情:

  • 创建libcontainer构建容器需要使用的进程,称为Process;
  • 设置容器的输出管道,这里使用的就是daemon提供的pipes;
  • 使用名为Factory的工厂类,通过factory.Create(<容器ID>, <填充好的容器模板container>)创建一个逻辑上的容器,称为Container;
  • 执行Container.Start(Process)启动物理的容器;
  • runC等待Process的所有工作都完成。

可以看到,具体的执行者是libcontainer,它是对容器的一层抽象,它定义了Process和Container来对应Linux中“进程”与“容器”的关系。一旦上述物理的容器创建成功,其他调用者就可以通过ID获取这个容器,接着使用Container.Stats得到容器的资源使用信息,或者执行Container.Destory来销毁这个容器。

综上,runC实际上是把配置交给libcontainer,然后由libcontainer完成容器的启动,而libcontainer中最主要的内容是Process、Container以及Factory这3个逻辑实体的实现原理。runC或者其他调用者只要依次执行“使用Factory创建逻辑容器Container”、“用Process启动逻辑容器Container”即可。

3. 总结

本文从OCI组织的成立开始讲起,描述了开放容器格式的标准及其宗旨,这其实就是runC的由来。继而针对具体的runC特性及其启动进行了详细介绍。笔者在后续的文章中还将针对runC中诸如CRIU热迁移、selinux、apparmor及seccomp配置等特性进行具体的介绍。可以看到OCI的成立表明了社区及各大厂商对容器技术的肯定以及加快容器技术发展进步的强烈决心,相信在不久的将来,符合OCI标准的开放容器项目会越来越多,容器技术将更加欣欣向荣地不断前进。

4. 作者简介

孙健波,浙江大学SEL实验室硕士研究生,《Docker容器与容器云》主要作者之一,目前在云平台团队从事科研和开发工作。浙大团队对PaaS、Docker、大数据和主流开源云计算技术有深入的研究和二次开发经验,团队现将部分技术文章贡献出来,希望能对读者有所帮助。

kubernetes apiserver源码分析——api请求的认证过程

文 王哲

解决什么问题

笔者之前希望全面分析一下k8apiserver的源码,后来发现这样并不十分有效,其一没有针对性,其二由于代码本身比较复杂,涉及到的功能较多,面面俱到也不太现实。

于是我们就回到最初的需求,到底需要解决什么问题,第一个问题就是,apiserver启动的时候,使用secure模式,参数应该如何设置,相关的机制又是怎样?

这一部分的issue很多,如果不从源码来分析的话,就只能黑盒化的去尝试各种参数搭配,费时费力,也不确定是否正确,之前就是这样,这几个issue可以供参考:

https://github.com/GoogleCloudPlatform/kubernetes/issues/10159#issuecomment-113955582

https://github.com/GoogleCloudPlatform/kubernetes/issues/11000

Continue reading

kubernetes node components – kubelet

kubelet作为k8s集群node上的重要组件,一直饱受关注。下面请随笔者一起walk through the code.

文 何思玫

1. Brief introduction of kubelet

管中窥豹,可见一斑。我们首先从k8s的官方文档中对kubelet的描述中一探究竟。

The kubelet is the primary “node agent” that runs on each node. The kubelet works in terms of a PodSpec. A PodSpec is a YAML or JSON object that describes a pod. The kubelet takes a set of PodSpecs that are provided through various echanisms (primarily through the apiserver) and ensures that the containers described in those PodSpecs are running and healthy. Other than from an PodSpec from the apiserver, there are three ways that a container manifest can be provided to the Kubelet.

File: Path passed as a flag on the command line. This file is rechecked every 20 seconds (configurable with a flag).

HTTP endpoint: HTTP endpoint passed as a parameter on the command line. This endpoint is checked every 20 seconds (also configurable with a flag).

HTTP server: The kubelet can also listen for HTTP and respond to a simple API (underspec’d currently) to submit a new manifest.

从上述描述中我们可以看到,kubelet是在集群中每个node上都必须存在的组件,负责维护管理pod。

它的工作主要基于四种source—— apiserver的PodSpec,File,HTTP endpoint及HTTP server——来更新pod。其中,File通过--file-check-frequency参数传入, HTTP通过--http-check-frequency参数。

2. Walk through the src code

文章分析的代码版本>v1.0.1, HEAD 6129d3d4。

kubelet相关代码的主入口在cmd/kublet下,调用方法的实现可能会在pkg/kubelet下。文中为了便利读者,也会就具体函数再进行标明。 下面我们就根据代码逻辑继续一次源码解读的DFS。

kubelet.go#main函数的主要流程包括如下三个步骤:

  • 创建一个KubeletServer实例
  • 根据命令行参数加载flag
  • 启动该kubeletServer。

如下图1所示。

kubelet-go-main

图1 kubelet.go#main

注意到,Run函数负责执行具体的kubelet的构建过程,在正常情况下是永远不会exit的。下面我们就来对该函数进行进一步的分析。

2.1 server.go#Run

如上图所示,KubeletServer.Run函数主要包括以下三个主要流程:

  • 配置并构建KubeletConfig实例。
  • 调用RunKubelet(&KubeletConfig,nil)函数真正地运行kubelet,该函数接受一个KubeletConfig类型的实例作为参数,包含了kubelet的配置信息。
  • 根据KubeletServer.HealthzPort的配置(即命令行参数--healthz-port),为/healthz url注册一个默认的handler(http.DefaultServeMux),启动health server。

可见,Run函数的核心步骤在于RunKubelet函数。

2.2 server.go#RunKubelet

我们首先通过略显复杂的图2来了解RunKubelet函数。它的使用场景非常明确,在node上创建并运行kubelet。 Runkubelet 图2 RunKubelet函数可以被大致划分为三大步骤。

Step1: 配置KubeletConfig信息,包括Hostname,NodeName,Recorder等。KubeletConfig这个结构包含了运行kubelet需要的所有参数,在可见的将来可能会和KubeletServer结构进行merge,不再单独存在。

  • 处理Hostname/NodeName信息
  • 创建了一个EventBroadcaster,创建一条包含kubelet及其NodeName配置的api.EventSource,将其注册到该EventBroadcaster中,用以记录kubelet产生的event并发送给apiserver。
  • 配置capabilities:系--allow-privileged--HostNetworkSources参数传入的值。
  • 配置使用docker拉取镜像时的credentialprovider的路径。
  • 配置OSInterface信息

Step2: createAndInitKubelet,顾名思义,创建并初始化kubelet。

builder在这里定义为createAndInitKubelet。

注意到这里返回了三个变量值,其中k为KubeletBootstrap(kubelet的interface),podCfg是config.PodConfig,最后一个是error变量。 podCfg变量是通过pc = makePodSourceConfig(kc)返回的,其中kc为传入的kubeletConfig变量。k则由NewMainKubelet函数返回。

Step3: 通过startKubelet启动kubelet,传入参数包括上一个函数的返回值及kcfg。

下面我们将就Step2和Step3进行更为详细的阐述。

2.2.1 server.go#createAndInitKubelet

这节内容主要包括两部分,首先来看makePodSourceConfig函数。

Section 1: makePodSourceConfig

该函数的返回值是一个config.PodConfig变量,用于将不同source的pod configuration合并到一个结构中,结构如下所示。

下面,我们具体来看makePodSourceConfig完成了什么工作。源码如下:

  • 使用NewPodConfig函数创建一个PodConfig变量cfg,用到了config.PodConfigNotificationSnapshotAndUpdateskc.RecorderPodConfigNotificationSnapshotAndUpdates的更新规则是,在增删pod的时候发送SET消息,其它变更发送UPDATE消息。其中,cfg.Updates是一个缓存大小为50的channel,收发数据类型为kubelet.PodUpdate(即对于某个pod的某种更新操作)。
  • 向podConfig变量cfg加入config source,包括 file/hhtp(url)/apiserver source。如同在开篇综述中所说,File类型和HTTP类型的pod configuration的变更,是通过定时查询对应source完成的。
  • 此处以更为常见的apiserver为例,我们使用NewSourceApiserver监听apiserver中podSpec的变更。该函数接收三个参数,第一个参数为用于向apiserver发起请求的Client,第二个参数为NodeName,第三个参数是由cfg.Channel返回的一个只读channel,类型是chan <- interface{}

也许读者会好奇,这个Channel函数具体做了哪些工作呢? 它会创建并返回一个interface{}类型的channel,并且启动go routine持续监听该Channel,并且将监听到的update进行merge。

merge是在NewPodConfig阶段决定的,即此前newPodStorage(updates, mode, recorder)返回的podStorage的Merge函数。该Merge函数定义在pkg/kubelet/config/config.go中,在该过程中会将监听的channel中更新的信息更新到cfg.updates中。限于篇幅,此处只展示PodConfigNotificationSnapshotAndUpdates对应的case,其它仅以”…”代替。

也就是说,cfg.Channel返回的是一个未知具体类型的被持续监听的channel(且该channel在merge之后会影响到原cfg),那么写channel的工作由谁完成呢?我们相信这是由config source(此处即apiserver)来完成的,也会在NewSourceApiserver函数分析中进行阐释。

  • 通过NewListWatchFromClient函数构造了一个ListWatch
  • 调用newSourceApiserverFromLW来watch并pull(拉取)apiserver的更新。

    // newSourceApiserverFromLW holds creates a config source that watches and pulls from the apiserver. func newSourceApiserverFromLW(lw cache.ListerWatcher, updates chan<- interface{}) { send := func(objs []interface{}) { var pods []api.Pod for _, o := range objs { pods = append(pods, o.(api.Pod)) } updates <- kubelet.PodUpdate{pods, kubelet.SET, kubelet.ApiserverSource} } cache.NewReflector(lw, &api.Pod{}, cache.NewUndeltaStore(send, cache.MetaNamespaceKeyFunc), 0).Run() }

可以看到,newSourceApiserverFromLW的执行核心只有一行代码,将创建出来的Reflector Run起来。

NewReflector函数创建一个Reflector对象,用于保证传入的第三个参数Store里的内容是update to date的。第三个参数以send函数为参数构建一个UndeltaStore,其PushFunc为send函数。 UndeltaStore listens to incremental updates and sends complete state on every change.也就是说,UndeltaStore不仅接受从Reflector来的update,并且还会使用UndeltaStore.PushFunc来进行change的deliver。这里就是写入到Cfg.Updates里,即完成了update的写操作。

Note:之前在集群运行过程中遇到过一个error,(可能在将来会upgrage成warning),即watcher超时,导致kubelet报错,位于reflector.go line#225。更新的内容来自listWatch,即apiserver。这里相当于是写updates。

Section 2: NewMainKubelet

我们通过pkg/kubelet/kubelet.go#NewMainKubelet函数构造了一个Kubelet。在该函数中创建各个kubelet需要的组件,部分参见如下列表

  • ContainerGC manages garbage collection of dead containers。负责进行对死掉的container进行垃圾回收。
  • ImageManager manages lifecycle of all images. 负责管理image。
  • DiskSpaceManager manages policy for diskspace management for disks holding docker images and root fs. 负责进行磁盘管理。
  • StatusManager updates pod statuses in apiserver. Writes only when new status has changed.该结构体中的podStatusChannel负责buffer podStatusSyncRequest,是一个缓冲区大小为1000的channel。 从apiserver中更新pod状态。
  • ReadinessManager maintains the readiness information(probe results) of containers over time to allow for implementation of health thresholds.负责收集container的readiness information(readiness info具体指什么并不明晰,是一个bool型,可能代表该container是否准备好?)。
  • RefManager manages the references for the containers. The references are used for reporting events such as creation, failure, etc.
  • VolumeManager manages the volumes for the pods running on the kubelet. Currently it only does book keeping, but it can be expanded to take care of the volumePlugins.
  • OOMWatcher watches cadvisor for system oom’s and records an event for every system oom encountered.
  • 初始化network plugin。这是个alpha feature。
  • ContainerRuntime 可以是DockerManager或者是rkt,默认为docker。
  • ContainerManager manages the containers running on a machine. -(周期性)配置网络参数(iptables rules)。 -(周期性)向master节点发送其它各个node的状态。synchronizes node status to master, registering the kubelet first if necessary.
  • 启动ContainerRuntime。ContainerRuntime是Runtime(interface)的一个实现,定义在pkg/kubelet/container/runtime.go中,包含了SyncPod/GetPodStatus等重要方法。以”docker”为例,它将是一个DockerManager,定义在pkg/kubelet/dockertools/manager.go中,并且实现了Runtime的所有方法。
  • PodManager stores and manages access to the pods. Kubelet discovers pod updates from 3 sources: file, http, and apiserver.三个update pod的来源,均在上例中的makePodSourceConfig 中进行了注册。 特别地,对于非apiserver来源的pod,apiserver是不知道其变更的。为了对其进行更新,我们创建了static pod。A mirror pod has the same pod full name (name and namespace) as its static counterpart (albeit different metadata such as UID, etc). By leveraging the fact that kubelet reports the pod status using the pod full name, the status of the mirror pod always reflects the actual status of the static pod. When a static pod gets deleted, the associated orphaned mirror pod will also be removed. 根据GetPodByFullName的原则,mirror pod会映射原pod的状态。
  • 为containerRuntime设置RuntimeCache。RuntimeCache caches a list of pods.该Cache中pod的状态会通过它的两个方法GetPodsForceUpdateIfOlder来更新。
  • 创建PodWorkersklet.podWorkers = newPodWorkers(runtimeCache, klet.syncPod, recorder) 第二个参数将作为podWorker.syncPodFn,即用以sync the desired stated of pod.实现为‵func (kl *Kubelet) syncPod`(定义在pkg/kubelet/kubelet.go)。
  • 在metrics中注册RuntimeCache。 创建目录,默认根目录在/var/lib/kubelet下,并在根目录下创建pods目录和plugins目录。
  • 初始化所有注册的(即可以访问kubelet的)plugin。
  • 建立记录容器的log目录(/var/log/containers)。

至此,所有相关组件或目录文件都已经被创建完毕,该函数返回一个Kubelet实例。

2.2.2 startKubelet

如果读者还记得我们在2.2节中的下段描述,“Step 3: 通过startKubelet启动kubelet,传入参数包括上一个函数的返回值及kcfg。” 那将是一个莫大的幸事,因为我们终于要从一个非常深的源码栈中跳出进入另一个更深的栈了:) 创建完毕kubelet的,下边无疑要进行的是运行操作。

根据运行类别分为两种,其一为RunOnce(只运行一次),其二为startKubelet(持续运行不退出);两种情况本质的处理逻辑是类似的,下面谨以第二种为例进行讲述。

该函数的核心——启动kubelet——由如下一行代码完成,

podCfg.Updates()返回podCfg.updates,类型为<-chan kubelet.PodUpdate,即前述提及的缓冲区为50的用以存储Pod更新信息的channel,让我们来一览其结构。

Kubelet.Run()定义在pkg/kubelet/kubelet.go中,它完成的工作包括

  • 定义kubelet.logServer
  • 如果kl.resourceContainer不为空(命令行参数--resource-container,”Absolute name of the resource-only container to create and run the Kubelet in (Default: “/kubelet.”) ),则将kubelet移入该container中。
  • 启动imageManager。运行时该imageManager负责持续list image和container,并在其Record中更新。
  • 启动cadvisor。
  • 启动containerManager。
  • 启动oomWatcher。
  • 启动周期性更新uptime。
  • 启动statusManager。
  • syncLoop(updates, kl)。

statusManager.StartsyncLoop具体做了什么工作,可能会引起读者的兴趣。下面我们就具体进行分析。为了让结构显得更为明晰,我们还是新起一个小章节:)

2.2.2.1 statusManager的启动

我们知道statusManager负责监听从apiserver对于pod的更新,更新的读写通过podStatusSyncRequest类型的channelpodStatusChannel

statusManager.Start执行核心工作的代码如下

即持续运行s.syncBatch(),这个函数负责同apiserver交互来获得pod status的更新(located @pkg/kubelet/status_manager.go),在没有更新时会block。这是因为在该函数中会等待s.podStatusChannel的返回值。从另一个角度而言,这是在读channel,写channel的操作应该由apiserver完成。

2.2.2.2 syncLoop

syncLoop这个函数使用到了传入的channel参数,即podCfg.updates,第二个参数为kubelet实例本身。它的函数体异常简单,但是却为我们执行了非常重要的sync pod的核心任务。

syncLoopIteration方法在检查各种配置之后,启动select监听updates(channel)上的数据,一旦有更新的数据,则执行podManager.UpdatePods方法,负责update the internal pods with those provided by the update,实际上就是在podManager的列表里进行相应的更新,并进行相应的标记——把每个pod(以types.UID标记)及其SyncPodType记录好(该函数执行完毕后,所有现有的pod的状态都是”sync”)。

然后,则是最为重要的sync pod

handler.SyncPods方法实际为pkg/kubelet/kubelet.go#SyncPods,进行的工作为SyncPods synchronizes the configured list of pods (desired state) with the host current state。具体包括如下步骤:

  1. 根据传入的现存pod列表将statusManager中的不存在的pod entry删除。
  2. 通过admitPods函数过滤terminated& notfitting & outofdisk pods,返回的pod列表即为desired pod。
  3. 对照desired pod列表,进行sync工作,具体是由Kubelet.podWorkers.UpdatePod来完成的。
  4. 把不存在的pod对应的podWorkers释放掉。这里就体现了维护podWorkers.podUpdates这个field的重要性。
  5. 杀死不需要的pod。
  6. 杀死孤儿volume。
  7. 删除不存在的pod对应的dir
  8. 删除孤儿mirror pod

下面我们来具体分析一下第3条Kubelet.podWorkers.UpdatePod进行的工作。

如果该pod在podWorkers.podUpdates里不存在,则需要进行创建,此时调用podWorkers.managePodLoop。 然后,根据podWorkers.isWorking[pod.UID]的值更新对应podUpdates的值或者传入podWorkers.lastUndeliveredWorkUpdate。这是对于对应pod状态的一个记录。

下面我们具体来看一下创建新pod的过程经历了什么。

从代码中可以看到,核心步骤调用了podWorkers.syncPodFn,这是在创建podWorker时构建的,具体指向了func (kl *Kubelet) syncPod(定义在pkg/kubelet/kubelet.go,完整的传参列表

这个函数在做什么呢? Let’s figure it out.

  • canRunPod:确认是否有足够的权限运行该pod。
  • 创建pod data存储的目录,包括root目录(以pod的UID命名)/volumes/plugins(默认在/var/lib/kubelet/pods下)
  • 为pod创建reference
  • mount volume,并且在volumeManager中设置相应的entry。volume是pod的manifest里边的Spec.Volumes下的信息。
  • 更新statusManager中的pod信息。根据传入参数updateType的不同,a)对于create:kl.statusManager.SetPodStatus(pod, podStatus),在statusManager里为新的pod设置StartTime,并且将新的podStatusSyncRequest传给statusManager.podStatusChannel。b)否则,通过kl.containerRuntime.GetPodStatus(pod)获得pod信息(返回值是api.PodStatus )。但是,在现在这个context中,应该为a).
  • 获得inspect pod的manifest中pod.Spec.ImagePullSecrets的信息。(这个secret是用来pull image的)
  • sync pod(在下文中再进行详细论述)
  • 根据该pod是否为static pod,在podManager中进行了相应的设置。

sync pod的过程,以DockerManager.SyncPod为例。有鉴于函数体比较长,在此就不再贴出源码。其功能非常明晰——目的在于使得running pod能够match对应的desired pod。

  • 计算running pod和desired pod之前的差距,返回的信息里包括StartInfraContainer(是否需要创建infra container),InfraContainerId(infra container ID),ContainersToStart(需要start的container),ContainersToKeep(需要keep的container)。
  • 根据返回的信息决定是否重启infra container((即gcr.io/google_containers/pause:0.8.0),配置network及其它container

至此,Kubelet.syncLoopIteration算是基本完成了。这也就意味着kubelet已经在node上运行起来,充当其pod的维护者的角色了。

3. Summary

总之,从宏观角度上而言,kubelet进行的工作就是从apiserver/file/http中获取pod更新的信息,并且定期进行sync pod。其中有大量的工作都涉及了channel的使用,也希望读者在阅读的过程中能够加以注意。

4S: Services Account, Secret, Security Context and Security in Kubernetes

Service Account, Secrets和Security Contexts作为保证kubernetes集群Security的策略被引入,相关代码一直处于快速变更与迭代中。本文谨从design和初级实践的视角对其进行概略性的分析,以飨读者。

文 何思玫

1. 集群安全(Security in Kubernetes)

众所周知,集群安全的首要关注点无疑是隔离性。进程之间的相互隔离,进程与集群基础设施的严格界限,用户与管理员之前的天然角色区分,都应该被考虑到隔离性的范畴内。 统而言之,集群安全性必须考虑如下几个目标: (1) 保证容器与其运行的宿主机的隔离。 (2) 限制容器对于基础设施及其它容器的影响权限,运行拥有特权模式的容器是不被推荐的行为。 (3) 最小权限原则——对所有组件权限的合理限制。 (4) 通过清晰地划分组件的边界来减少需要加固和加以保护的系统组件数量。 (5) 普通用户和管理员的角色区分,同时允许在必要的时候将管理员权限委派给普通用户。 (6) 允许集群上运行的应用拥有secret data。 涉及安全,authentication和authorization是不能绕过的两个话题。下面我们就先来了解一下k8s在这两个issue所提供的支持。

2. Authentication

k8s目前支持三种认证方式,包括certificates/tokens/http basic auth。 client certificate authentication是双向认证的方式,可以经由easyrsa等证书生成工具生成服务器端并客户端证书。 Token authentication:单向认证方式,为kube-apiserver提供- -token_ auth_file,格式为一个有3columns的csv file:token,user name,user id。此处为使用该认证方法的常见的elasticsearch case。 Basic authentication:传入明文用户名密码作为apiserver的启动参数,不支持在不重启apiserver的前提下进行用户名/密码修改。

更多细节详见官方相关文档

3. Authorization

--authorization-mode,apiserver的参数,用于定义对secure port设置何种authorization policy,包括三种AlwaysAllow/AlwayDeny/ABAC,第一种policy允许所有对apiserver的API request,与之相反,第二种则会block所有的API request,第三种则为Attribute-Based Access Control,即对于不同request attribute,有不同的access control。 下面我们着重讨论ABAC mode。

3.1 Request Attributes

在考虑authorization时,一个request有5种attribute需要考虑: – user – group – readOnly – resource(如只访问API endpoint,如/api/v1/namesapces/default/pods,或者其它杂项endpoint,如/version,此时的resource是空字符串) – namespace

3.2 Policy File Format

对于ABAC mode,还需要specify --authorization-policy-file参数,例子参见此处

除此之外,apiserver还可以通过调用Authorizer(interface)来决定是否允许某个API操作。

4. UserAccount

这个概念适用于所有希望与k8s集群交互的个体(通常可以认为是human),因此用户名通常是human-readable,目前并不作为一个代码中的类型单独出现,一般通过config file来读取和感知。

5. Service Account

Service Account概念的引入是基于这样的使用场景:运行在pod里的进程需要调用k8s API(如scheduler/replication controller/minitor system等)以及非k8s API的其它服务(如image repository/被mount到pod上的NFS volumes中的file等)。我们使用Service Account来为pod提供id。 Service Account和第4节中提及的User account可能会带来一定程度上的混淆,下面我们就先从概念的层面将其sort it out。

  • user account通常是为human设计的,而service account则是为跑在pod里的process。
  • user account是global的,即跨namespace使用;而service account是namespaced的,即仅在belonging的namespace下使用。
  • 创建一个新的user account通常需要较高的特权并且需要经过比较复杂的business process(即对于集群的访问权限的创建),而service account则不然。
  • 不同的auditing consideration。
5.1 Design Overview

Service Account包括如下几个元素:

  • name,用以作为id
  • principal,用以authenticated及authorized
  • security context,定义linux capabilities等与系统相关的参数。
  • secrets
5.2 Use Cases

通常我们在使用kubectlbinary来与集群交互时,我们(User Account)都会通过apiserver的认证(如果该集群设置了认证方式的话),可以被看作某个特定的User Account。类似地,当pod希望与apiserver进行交互时,也需要使用特定的认证机制,即Service Account。

当创建pod且没有经过特殊设置时,它将会被默认地分发到该namespace下default的service account,可以通过kubectl get pods/podname -o yaml的命令行查看spec.serviceAccount的field)。

5.2.1 default service account

每个namespace在创建时都会有一个default的service Account,如下所示

5.2.2 Create your own service Account

用户可以方便地通过yaml文件等形式创建自己的serviceAccount,如

通过`kubectl get < resource > -o yaml获取详细信息

可以看到,token(secret)被自动生成并ref了。如果在之后创建pod的时候ref这个自建的serviceAccount,在spec.serviceAccount中指定即可。

通过如下命令可以删除serviceAccount。

5.2.3Adding Secrets to a service account

注意到,我们可以不使用serviceAccount自建的secrets,而是选用我们自己生成的secrets,这是一种consume secret的方式。 首先创建secret,kubectl create -f secret.yaml

然后创建一个comsume该secret,kubectl create -f serviceaccount.yaml

最后在pod的manifest中指定其serviceaccount,kubectl create -f busybox4.yaml

已了解如何创建一个引用自建secret的serviceaccount,但是如果在exsiting serviceaccount中添加引用,目前官方文档mark it as TODO。笔者认为此用法应该与上例类似,但笔者采用这种方式consume secret时,serviceaccount似乎并不对该secret有维护作用。 Update:此用法确与上例类似,参见issue#12012,笔者后续会考虑对相关doc进行更新。

6. Secrets

secret是用于存放sensitive information的数据结构,可以通过manual或者automatic两种方式创建。 secret的use case主要有两种,或者作为volume mount到pod上,或者用于kubelet在为pod里的container拉取镜像时。 使用serviceAccount自建的secret是一种保证secure的推荐方式,当然可以由用户disable或者overridden,如果用户愿意承担额外的风险的话。

6.1 Secrets类型一览

secret的类型目前有三种,如下所示

Opaque类型是默认的用户自建类型,即任意string(实际上也并不是十分任意,data label的key/value必须满足特定的要求:Each key must be a valid DNS_SUBDOMAIN or leading dot followed by valid DNS_SUBDOMAIN. Each value must be a base64 encoded string as described in https://tools.ietf.org/html/rfc4648#section-4)。

第二种类型的例子如下所示

第三种类型一般用于向private registry拉取镜像,在6.3节中再做阐述。

6.2 manually specify a secret

如上述所示,使用kubectl create命令即可创建一个secret。创建完毕之后,可以使用serviceAccount来consume,也可以手动添加到pod的manifest中,如下例所示:

More example here。

6.3 Manually specifying an imagePullSecret

这个方法仅在GKE/GCE或其它cloud-provider场景下推荐使用,创建一个类型为kubernetes.io/dockercfg的secret,并且在pod manifest的imagePullSecrets label下使用。因为缺乏实验平台,在此不作赘述。

总而言之,目前k8s对于secret的support仍处于不十分完善的阶段,secret可能以多种方式暴露给外部用户甚至是attacker。

7. Security context

Security context是用以对容器进行限制,使得不同的运行容器之前能够实现较为明晰的隔离,以及降低其影响宿主机和其它容器的可能性。通俗而言,容器中的security context用于表征在创建及运行容器时,它能够使用及访问的资源参数。 该概念将会被用到如下两处:

  • kubelet用于创建及运行container。
  • 作为serviceAccount的一部分定义在pod中。

这个概念具体如何使用在k8s场景中基本上可以认为在servicAccount中的securityContext label下,目前代码只实现了capabilities和privileged,在此不展开说明。

tutum-agent原理浅析

孙健波

tutum-aget是tutum提供的一个开源代理引擎。当你把tutum-agent安装在你本地机器上以后,你就可以把本地的机器节点添加到云端,让tutum来帮你统一进行管理。你随时随地都可以登陆tutum网站获得如下功能。

  1. 启停Docker容器。
  2. 动态伸缩容器实例数量。
  3. 监控容器状态、容器CPU\Memory\Disk\Bandwidth使用量
  4. 查看容器日志、操作记录
  5. 与tutum其他服务结合的衍生功能(部署应用、绑定服务等)

可见,安装了tutum-agent以后相当于把本地的机器加入到了tutum的数据中心进行统一管理。本文将分析tutum-agent的工作原理。

安装

tutum-agent通过shell进行安装,安装脚本可以在执行官方的安装指令时下载curl -Ls https://get.tutum.co/,也可以在github源码中找到。

这个shell脚本很简单,根据不同linux发行版安装tutum-agent,如果是ubuntu则是执行apt-get install -yq tutum-agent,然后生成配置文件/etc/tutum/agent/tutum-agent.conf,内容如下。

这些配置文件的作用在随后的代码解析中讲解。生成完配置文件后启动该服务。

service tutum-agent restart

至此shell脚本执行完了,但是安装过程并没有结束,在安装tutum-agent时,会因为冲突卸载原先已经安装好的Docker。

启动后的第一件事就是在TutumHost上注册并验证用户的TutumToken和TutumUUID,获得相应的证书文件,然后从TutumHost上下载Docker的二进制文件,目前Tutum官方提供的是1.5版本的Docker。随后启动Docker daemon,之前下载的证书也主要用在这里。

启动完Docker以后,就开始下载Ngrok客户端。Ngrok是一个网络代理,它可以让你在防火墙或者NAT映射的私有网络内的服务,让公网可以直接访问。Ngrok的实现方式是,在公网架设一个Ngrok 服务器,私有网络的Ngrok客户端与公网的Ngrok Server保持tcp长连接,用户通过访问Ngrok Server进行代理,从而访问到Ngrok客户端所在机器的服务。使用Ngrok客户端,只需要配置你想要提供服务的应用程序对外服务的端口即可。把这个端neng l口告诉Ngrok客户端,就相当于把本地私有网络下的服务提供给外部。

至此,所有的服务也就安装完毕了。总结一下,我们实际上安装了tutum-agent、Docker、Ngrok三个应用程序。通过Ngrok把本地的Docker作为服务让公网的其他用户可以使用,tutum-agent在随后会处理包括日志、监控、Docker指令处理等相关内容。

连接

安装完Ngrok以后,实际上就相当于建立了穿越NAT网络的隧道,使得公网所在的程序可以直接控制私有网络的Docker服务。Tutum-agent在启动Docker时使用了用户相关的证书,保证了Docker只接受有证书认证的请求,一定程度上保证了本地服务的安全。

获得了本地Docker服务的控制能力,容器相关的功能的实现自然迎刃而解。Ngrok还会为用户提供命令操作日志以便于审计,所以你可以方便的查看容器的操作记录。

建立了连接后我们还需要做什么?

通过Ngrok,这一切都变得很简单,但是别忘了,Docker daemon的运行状态需要时刻监控。tutum-agent会对Docker daemon的运行状态周期性的检查,一旦daemon意外推出,可以保证快速检测到并重启。

性能

ngrok是一个非常强大的tunnel工具,使用它以后本身带来的代理转发性能损失非常低。

但是实际上通过ngrok搭建起来的服务,都是应用用户先去访问ngrok服务端,然后再有服务端转发给ngrok客户端,最后处理用户请求并返回。所以,真正让人感知到性能损失的可能是你的ngrok服务端搭建的位置较远,而ngrok客户端与应用用户本身网络较近,这样就容易导致较高的应用访问延迟。

延伸

看完了上述内容,可能你会想到,如何构建一个类似Tutum的“Bring your own node”服务,并把它应用到Docker以为的其他项目上。

第一步,搭建自己的ngrok服务,使得用户可以通过ngrok客户端连接并在任意网络访问本地的服务,可参考博客“搭建自己的ngrok服务”

第二步,区分用户。当你的ngrok对外提供服务的时候,会有许多客户端来连接,不同的客户端可能是不同的用户连接的,也有可能是同一个用户的不同应用或不同主机节点。所以你需要在服务端编写自己的判断逻辑,方法很简单。ngrok客户端与服务端建立连接时会生成子域名(或者自定义域名),这个域名一旦建立了连接,就是唯一的,别的用户无法占用,通过这个方法就可以进行最简单的区分。当然,根据你对外提供的服务,你可能还需要通过生成证书来保证服务的安全性。

第三步,通过API进行操控。当ngrok客户端连接成功后,实际上服务端已经可以连接用户私有网络下服务的端口了,通过端口自然可以访问到服务的API,就基本上可以让用户全局操控自己的服务了。

第四步,日志、监控、报警与可视化。简单的日志收集分为两部分,一部分自然是ngrok的操作日志,另一部分则是应用相关的日志,需要通过应用API收取。简单来说,监控也分为两部分,一部分是用户主机资源、服务可用性相关的数据监控,另一部分是业务相关的监控。

基本上,有了这些,一个最基本的服务就完成了。