CoreDNS为什么会成为Kubernetes的默认选项?

标签: coredns kubernetes 默认选项 | 发表时间:2020-03-26 04:30 | 作者:Andy_Lee
出处:http://weekly.dockone.io

【编者的话】CoreDNS是一个DNS服务器,CoreDNS于2017年提交给CNCF,并于2019年1月变为“已毕业”状态。凭借着超强的灵活性和环境兼容性以及插件化的可扩展性,CoreDNS成为了Kubernetes附带的默认DNS服务器,为集群提供DNS和服务发现的功能。

本篇文章我们将讨论CoreDNS的历史和优势;为什么能在多种DNS项目中脱颖而出;它是如何设计和提供服务的以及如何和Kubernetes结合,最后讨论下它的性能和稳定性以及不足。希望能够让大家全面了解CoreDNS。

CoreDNS凭什么胜出?

Miek Gieben在2016年编写了CoreDNS的原始版本,之前他曾编写过一个名为SkyDNS的DNS服务器和一种流行的DNS函数库,该库以Go语言编写,名为Go DNS。但是Miek觉得基于Go的Web服务器Caddy的体系结构更加强大,因此他fork了Caddy创建了CoreDNS。CoreDNS也就继承了Caddy的主要优点:简单的配置语法,强大的基于插件的体系结构以及Go实现的基础。

总的来说,CoreDNS的优势:
  • 简单友好的配置:与BIND的配置文件的语法相比,CoreDNS的Corefile令人耳目一新。CoreDNS的DNS服务器的Corefile通常只有几行,而且相对而言非常易于阅读;
  • 链式的插件化设计:CoreDNS使用插件来提供DNS功能,并且每个插件都执行DNS功能,这就解决了多个插件的兼容性问题。这也使得CoreDNS更快,更安全。
  • 易于上手和定制:容易定制,这很重要。支持能力而不是功能,这是能够发展壮大的基础。官网也有数十种插件任由你选择和配置。
  • 安全的编码:Go语言是“内存安全”,这意味着它可以防止“内存访问错误”,例如缓冲区溢出和指针空等。这对于像CoreDNS这样的DNS服务器特别重要,可以想象互联网上的任何人都可以访问它。恶意行为者可能利用缓冲区溢出来使DNS服务器崩溃,甚至获得对底层操作系统(OS)的控制。实际上,在其几十年的历史中,BIND中的许多严重漏洞都是由内存访问错误引起的。使用CoreDNS,则无需担心这些。
  • 与容器和编排系统的完美结合:CoreDNS可以和Kubernetes在内的许多容器编排系统直接集成,这意味着容器化应用程序的管理员可以轻松地设置DNS服务器来协调和促进容器之间的通信。所以它也能够伴随着Kubernetes快速被用户和开发者接受,并成为主流。


设计和功能介绍

架构和配置

CoreDNS提供了简单易懂的DSL语言,我们可以通过Corefile来自定义DNS服务,例如:
coredns.io:5300 {  
file db.coredns.io
}

example.io:53 {
log
errors
file db.example.io
}

example.net:53 {
file db.example.net
}

.:53 {
kubernetes
proxy . 8.8.8.8
log
errors
cache


以上的配置表示,CoreDNS会开启两个端口5300和53 ,提供DNS解析服务。对于coredns.io相关的域名会通过5300端口进行解析,其他域名都会被解析到53端口,不同的域名可以设置不同的插件来提供服务(如下图)。

插件设计

从源码不难看出,每一个插件的实现都是一个出参和入参都为Handler的函数,而Handler只需要实现两个函数:ServeDNS(提供的DNS服务)和Name(插件的名称)。
// Plugin is a middle layer which represents the traditional  
// idea of plugin: it chains one Handler to the next by being
// passed the next Handler in the chain.
Plugin func(Handler) Handler

// Handler is like dns.Handler except ServeDNS may return an rcode
// and/or error.
//
// If ServeDNS writes to the response body, it should return a status
// code. CoreDNS assumes *no* reply has yet been written if the status
// code is one of the following:
//
// * SERVFAIL (dns.RcodeServerFailure)
//
// * REFUSED (dns.RecodeRefused)
//
// * FORMERR (dns.RcodeFormatError)
//
// * NOTIMP (dns.RcodeNotImplemented)
//
// All other response codes signal other handlers above it that the
// response message is already written, and that they should not write
// to it also.
//
// If ServeDNS encounters an error, it should return the error value
// so it can be logged by designated error-handling plugin.
//
// If writing a response after calling another ServeDNS method, the
// returned rcode SHOULD be used when writing the response.
//
// If handling errors after calling another ServeDNS method, the
// returned error value SHOULD be logged or handled accordingly.
//
// Otherwise, return values should be propagated down the plugin
// chain by returning them unchanged.
Handler interface {
ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error)
Name() string


此外,实现插件的链式执行也很简单,由一个NextOrFailure方法,在每个插件在执行完自身的逻辑之后再执行下一个插件。
// NextOrFailure calls next.ServeDNS when next is not nil, otherwise it will return, a ServerFailure and a nil error.  
func NextOrFailure(name string, next Handler, ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { // nolint: golint
if next != nil {
if span := ot.SpanFromContext(ctx); span != nil {
  child := span.Tracer().StartSpan(next.Name(), ot.ChildOf(span.Context()))
  defer child.Finish()
  ctx = ot.ContextWithSpan(ctx, child)
}
return next.ServeDNS(ctx, w, r)
}

return dns.RcodeServerFailure, Error(name, errors.New("no next plugin found"))


是的,可以看到CoreDNS的设计和实现非常简单,但也非常灵活。

CoreDNS如何为Kubernetes提供DNS服务?

首先是我们可以看到实现的kubernetes插件,也就是上面我们说的每个插件的都必须实现的函数:ServeDNS和Name。
// ServeDNS implements the plugin.Handler interface.  
func (k Kubernetes) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}

qname := state.QName()
zone := plugin.Zones(k.Zones).Matches(qname)
if zone == "" {
return plugin.NextOrFailure(k.Name(), k.Next, ctx, w, r)
}
zone = qname[len(qname)-len(zone):] // maintain case of original query
state.Zone = zone

var (
records []dns.RR
extra   []dns.RR
err     error
)

switch state.QType() {
case dns.TypeAXFR, dns.TypeIXFR:
k.Transfer(ctx, state)
case dns.TypeA:
records, err = plugin.A(ctx, &k, zone, state, nil, plugin.Options{})
case dns.TypeAAAA:
records, err = plugin.AAAA(ctx, &k, zone, state, nil, plugin.Options{})
case dns.TypeTXT:
records, err = plugin.TXT(ctx, &k, zone, state, nil, plugin.Options{})
case dns.TypeCNAME:
records, err = plugin.CNAME(ctx, &k, zone, state, plugin.Options{})
case dns.TypePTR:
records, err = plugin.PTR(ctx, &k, zone, state, plugin.Options{})
case dns.TypeMX:
records, extra, err = plugin.MX(ctx, &k, zone, state, plugin.Options{})
case dns.TypeSRV:
records, extra, err = plugin.SRV(ctx, &k, zone, state, plugin.Options{})
case dns.TypeSOA:
records, err = plugin.SOA(ctx, &k, zone, state, plugin.Options{})
case dns.TypeNS:
if state.Name() == zone {
  records, extra, err = plugin.NS(ctx, &k, zone, state, plugin.Options{})
  break
}
fallthrough
default:
// Do a fake A lookup, so we can distinguish between NODATA and NXDOMAIN
fake := state.NewWithQuestion(state.QName(), dns.TypeA)
fake.Zone = state.Zone
_, err = plugin.A(ctx, &k, zone, fake, nil, plugin.Options{})
}

if k.IsNameError(err) {
if k.Fall.Through(state.Name()) {
  return plugin.NextOrFailure(k.Name(), k.Next, ctx, w, r)
}
if !k.APIConn.HasSynced() {
  // If we haven't synchronized with the kubernetes cluster, return server failure
  return plugin.BackendError(ctx, &k, zone, dns.RcodeServerFailure, state, nil /* err */, plugin.Options{})
}
return plugin.BackendError(ctx, &k, zone, dns.RcodeNameError, state, nil /* err */, plugin.Options{})
}
if err != nil {
return dns.RcodeServerFailure, err
}

if len(records) == 0 {
return plugin.BackendError(ctx, &k, zone, dns.RcodeSuccess, state, nil, plugin.Options{})
}

m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true
m.Answer = append(m.Answer, records...)
m.Extra = append(m.Extra, extra...)

w.WriteMsg(m)
return dns.RcodeSuccess, nil
}

// Name implements the Handler interface.
func (k Kubernetes) Name() string { return "kubernetes" } 

其中最核心的即是调用NextOrFailure方法,这个我们上面已经说了。

另外,这个插件还会有一些设置:
func init() { plugin.Register("kubernetes", setup) }  

func setup(c *caddy.Controller) error {
klog.SetOutput(os.Stdout)

k, err := kubernetesParse(c)
if err != nil {
return plugin.Error("kubernetes", err)
}

err = k.InitKubeCache()
if err != nil {
return plugin.Error("kubernetes", err)
}

k.RegisterKubeCache(c)

c.OnStartup(func() error {
metrics.MustRegister(c, DnsProgrammingLatency)
return nil
})

dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
k.Next = next
return k
})

// get locally bound addresses
c.OnStartup(func() error {
k.localIPs = boundIPs(c)
return nil
})

return nil


其中InitKubeCache会对Service、Pod、Endpoint三种资源对象进行watch操作,从而能够及时感知到这些资源的变化,并注册了一些回调,这也是CoreDNS提供DNS和服务发现的核心。
// newDNSController creates a controller for CoreDNS.  
func newdnsController(kubeClient kubernetes.Interface, opts dnsControlOpts) *dnsControl {
dns := dnsControl{
client:            kubeClient,
selector:          opts.selector,
namespaceSelector: opts.namespaceSelector,
stopCh:            make(chan struct{}),
zones:             opts.zones,
endpointNameMode:  opts.endpointNameMode,
}

dns.svcLister, dns.svcController = object.NewIndexerInformer(
...
object.DefaultProcessor(object.ToService(opts.skipAPIObjectsCleanup)),
)

if opts.initPodCache {
dns.podLister, dns.podController = object.NewIndexerInformer(
  ...
  object.DefaultProcessor(object.ToPod(opts.skipAPIObjectsCleanup)),
)
}

if opts.initEndpointsCache {
dns.epLister, dns.epController = object.NewIndexerInformer(
  &cache.ListWatch{
    ListFunc:  endpointsListFunc(dns.client, api.NamespaceAll, dns.selector),
    WatchFunc: endpointsWatchFunc(dns.client, api.NamespaceAll, dns.selector),
  },
 ...


并且会实现findPods和findServices两个函数来匹配集群中的资源,从而创建响应的资源和DNS信息。

Kubernetes 在解析 Service DNS 时会根据相应的Service进行匹配,遍历Service List直到找到匹配的 Service,然后再根据不同类型,决定返回的结果。
func (k *Kubernetes) findPods(r recordRequest, zone string) (pods []msg.Service, err error) {  
if k.podMode == podModeDisabled {
return nil, errNoItems
}

namespace := r.namespace
if !wildcard(namespace) && !k.namespaceExposed(namespace) {
return nil, errNoItems
}

podname := r.service

// 处理pod name为空的pod
if podname == "" {
if k.namespaceExposed(namespace) || wildcard(namespace) {
  // NODATA
  return nil, nil
}
// NXDOMAIN
return nil, errNoItems
}

// zone路径格式转换
zonePath := msg.Path(zone, coredns)
ip := ""
if strings.Count(podname, "-") == 3 && !strings.Contains(podname, "--") {
ip = strings.Replace(podname, "-", ".", -1)
} else {
ip = strings.Replace(podname, "-", ":", -1)
}

if k.podMode == podModeInsecure {
if !wildcard(namespace) && !k.namespaceExposed(namespace) { // no wildcard, but namespace does not exist
  return nil, errNoItems
}

// 如果ip不能解析为IP地址,则返回错误,否则假定为CNAME并尝试在backend_lookup.go中解析它
if net.ParseIP(ip) == nil {
  return nil, errNoItems
}

return []msg.Service{{Key: strings.Join([]string{zonePath, Pod, namespace, podname}, "/"), Host: ip, TTL: k.ttl}}, err
}

// PodModeVerified
err = errNoItems
if wildcard(podname) && !wildcard(namespace) {
// If namespace exists, err should be nil, so that we return NODATA instead of NXDOMAIN
if k.namespaceExposed(namespace) {
  err = nil
}
}

for _, p := range k.APIConn.PodIndex(ip) {
// If namespace has a wildcard, filter results against Corefile namespace list.
if wildcard(namespace) && !k.namespaceExposed(p.Namespace) {
  continue
}

// 匹配检查IP和命名空间
if ip == p.PodIP && match(namespace, p.Namespace) {
  s := msg.Service{Key: strings.Join([]string{zonePath, Pod, namespace, podname}, "/"), Host: ip, TTL: k.ttl}
  pods = append(pods, s)

  err = nil
}
}
return pods, err
}

// 查找匹配的service
func (k *Kubernetes) findServices(r recordRequest, zone string) (services []msg.Service, err error) {
if !wildcard(r.namespace) && !k.namespaceExposed(r.namespace) {
return nil, errNoItems
}

// handle empty service name
if r.service == "" {
if k.namespaceExposed(r.namespace) || wildcard(r.namespace) {
  // NODATA
  return nil, nil
}
// NXDOMAIN
return nil, errNoItems
}

err = errNoItems
if wildcard(r.service) && !wildcard(r.namespace) {
// If namespace exists, err should be nil, so that we return NODATA instead of NXDOMAIN
if k.namespaceExposed(r.namespace) {
  err = nil
}
}

var (
endpointsListFunc func() []*object.Endpoints
endpointsList     []*object.Endpoints
serviceList       []*object.Service
)

if wildcard(r.service) || wildcard(r.namespace) {
serviceList = k.APIConn.ServiceList()
endpointsListFunc = func() []*object.Endpoints { return k.APIConn.EndpointsList() }
} else {
idx := object.ServiceKey(r.service, r.namespace)
serviceList = k.APIConn.SvcIndex(idx)
endpointsListFunc = func() []*object.Endpoints { return k.APIConn.EpIndex(idx) }
}

zonePath := msg.Path(zone, coredns)
for _, svc := range serviceList {
if !(match(r.namespace, svc.Namespace) && match(r.service, svc.Name)) {
  continue
}

// If request namespace is a wildcard, filter results against Corefile namespace list.
// (Namespaces without a wildcard were filtered before the call to this function.)
if wildcard(r.namespace) && !k.namespaceExposed(svc.Namespace) {
  continue
}

// If "ignore empty_service" option is set and no endpoints exist, return NXDOMAIN unless
// it's a headless or externalName service (covered below).
if k.opts.ignoreEmptyService && svc.ClusterIP != api.ClusterIPNone && svc.Type != api.ServiceTypeExternalName {
  // serve NXDOMAIN if no endpoint is able to answer
  podsCount := 0
  for _, ep := range endpointsListFunc() {
    for _, eps := range ep.Subsets {
      podsCount = podsCount + len(eps.Addresses)
    }
  }

  if podsCount == 0 {
    continue
  }
}

// Endpoint query or headless service
if svc.ClusterIP == api.ClusterIPNone || r.endpoint != "" {
  if endpointsList == nil {
    endpointsList = endpointsListFunc()
  }
  for _, ep := range endpointsList {
    if ep.Name != svc.Name || ep.Namespace != svc.Namespace {
      continue
    }

    for _, eps := range ep.Subsets {
      for _, addr := range eps.Addresses {

        // See comments in parse.go parseRequest about the endpoint handling.
        if r.endpoint != "" {
          if !match(r.endpoint, endpointHostname(addr, k.endpointNameMode)) {
            continue
          }
        }

        for _, p := range eps.Ports {
          if !(match(r.port, p.Name) && match(r.protocol, string(p.Protocol))) {
            continue
          }
          s := msg.Service{Host: addr.IP, Port: int(p.Port), TTL: k.ttl}
          s.Key = strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name, endpointHostname(addr, k.endpointNameMode)}, "/")

          err = nil

          services = append(services, s)
        }
      }
    }
  }
  continue
}

// External service
if svc.Type == api.ServiceTypeExternalName {
  s := msg.Service{Key: strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name}, "/"), Host: svc.ExternalName, TTL: k.ttl}
  if t, _ := s.HostType(); t == dns.TypeCNAME {
    s.Key = strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name}, "/")
    services = append(services, s)

    err = nil
  }
  continue
}

// ClusterIP service
for _, p := range svc.Ports {
  if !(match(r.port, p.Name) && match(r.protocol, string(p.Protocol))) {
    continue
  }

  err = nil

  s := msg.Service{Host: svc.ClusterIP, Port: int(p.Port), TTL: k.ttl}
  s.Key = strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name}, "/")

  services = append(services, s)
}
}
return services, err


至此,我们基本了解了CoreDNS是如何在Kubernetes中发挥作用的。

稳定性和性能

由于笔者没有实际的环境,这里引用官方的测试数据

在大规模的Kubernetes集群中,CoreDNS的内存使用量主要受集群中Pod和Services数量的影响。其他因素包括填充的DNS缓存的大小以及每个CoreDNS实例的查询接收率(QPS)。这里给出在默认和自动路径插件两种配置下的性能数据。

内存使用量

默认配置

在默认的设置下,要估计CoreDNS实例所需的内存量(使用默认设置),可以使用以下公式:

所需的MB(默认设置)=(Pods+Service)/1000 + 54

此公式包含以下内容:
  • 30MB用于缓存。默认的高速缓存大小为10000个条目,完全填满时将使用大约30MB。
  • 5 MB的操作缓冲区,用于处理查询。在测试中,这是单个CoreDNS副本在大约30K QPS负载下使用的数量。


自动路径插件配置

自动路径是一项可选的优化功能,可提高查询集群外部名称(例如infoblox.com)的性能。但此时CoreDNS使用大量内存来存储有关Pod的信息,同时也会给Kubernetes API带来额外的流量负担,因为它必须watch Pod的所有变更。

要估计CoreDNS实例所需的内存量(使用自动路径插件),可以使用以下公式:

所需的MB(带有自动路径)=(Pods+Services)/ 250 + 56

此公式包含以下内容:
  • 30MB用于缓存。默认的高速缓存大小为10000个条目,完全填满时将使用大约30MB。
  • 5MB的操作缓冲区,用于处理查询。在测试中,这是单个CoreDNS副本在大约30K QPS负载下使用的数量。


CPU和QPS

通过使用该kubernetes/perf-tests/dns工具,在使用CoreDNS的群集上对Max QPS进行了测试。所使用的两种查询类型是内部查询(例如kubernetes)和外部查询(例如infoblox.com)。遵循ClusterFirst域搜索列表(加上一个本地域),采取了一些步骤来综合标准Pod行为。这些测试中使用的有效域搜索列表为default.svc.cluster.local svc.cluster.local cluster.local mydomain.com)。QPS和延迟在这里是客户端的角度。这对于外部查询特别重要,其中单个客户端查询实际上会生成5个对DNS服务器的后端查询,仅计为一个查询。

默认配置

GCE n1-standard-2节点上CoreDNS的单个实例(默认设置):

自动路径插件配置

自动路径插件选项,减轻了ClusterFirst搜索列表处罚。启用后,它将以一次往返而不是五次来回答Pod。这样可以将后端的DNS查询数量减少到一个。回想一下,启用自动路径插件需要CoreDNS使用显着更多的内存,并增加Kubernetes API的负载。

GCE n1-standard-2节点上CoreDNS的单个实例(启用了自动路径插件):

CoreDNS的局限性

CoreDNS确实很不错,但目前确实也存在一些重大限制,并且它并不适合所有的DNS服务器场景。其中最主要的一点是,CoreDNS不支持完全递归。换句话说,CoreDNS无法处理查询,方法是从DNS名称空间的根目录开始,查询根DNS服务器并遵循引用,直到从权威DNS服务器之一获得答案为止。相反,它依赖于其他DNS服务器(通常称为转发器)。


原文链接: https://mp.weixin.qq.com/s/39mOLGU-2CGVhfzMxtWETg

相关 [coredns kubernetes 默认选项] 推荐:

CoreDNS为什么会成为Kubernetes的默认选项?

- - DockOne.io
【编者的话】CoreDNS是一个DNS服务器,CoreDNS于2017年提交给CNCF,并于2019年1月变为“已毕业”状态. 凭借着超强的灵活性和环境兼容性以及插件化的可扩展性,CoreDNS成为了Kubernetes附带的默认DNS服务器,为集群提供DNS和服务发现的功能. 本篇文章我们将讨论CoreDNS的历史和优势;为什么能在多种DNS项目中脱颖而出;它是如何设计和提供服务的以及如何和Kubernetes结合,最后讨论下它的性能和稳定性以及不足.

Kubernetes & Microservice

- - 午夜咖啡
这是前一段时间在一个微服务的 meetup 上的分享,整理成文章发布出来. 谈微服务之前,先澄清一下概念. 微服务这个词的准确定义很难,不同的人有不同的人的看法. 比如一个朋友是『微服务原教旨主义者』,坚持微服务一定是无状态的 http API 服务,其他的都是『邪魔歪道』,它和 SOA,RPC,分布式系统之间有明显的分界.

Kubernetes学习(Kubernetes踩坑记)

- - Z.S.K.'s Records
记录在使用Kubernetes中遇到的各种问题及解决方案, 好记性不如烂笔头. prometheus提示 /metrics/resource/v1alpha1 404. 原因: 这是因为[/metrics/resource/v1alpha1]是在v1.14中才新增的特性,而当前kubelet版本为1.13.

kubernetes移除Docker?

- -
两周前,Kubernetes在其最新的Changelog中宣布1.20之后将要弃用dockershime,也就说Kubernetes将不再使用Docker做为其容器运行时. 这一消息持续发酵,掀起了不小的波澜,毕竟Kubernetes+Docker的经典组合是被市场所认可的,大量企业都在使用. 看上去这个“弃用”的决定有点无厘头,那么为什么Kubernetes会做出这样的决定.

Kubernetes 完全教程

- - 午夜咖啡
经过一个阶段的准备,视频版本的 《Kubernetes 完全教程》出炉了. 课程一共分为七节,另外有一节 Docker 预备课,每节课大约一个多小时. 目标是让从没接触过 Kubernetes 的同学也能通过这个课程掌握 Kubernetes. 为什么要学习 Kubernetes. 在介绍课程之前,先说说为什么要学习 Kubernetes 以及什么人需要学习 Kubernetes.

Kubernetes 监控详解

- - DockOne.io
【编者的话】监控 Kubernetes 并不是件容易的事. 本文介绍了监控 Kubernetes 的难点、用例以及有关工具,希望可以帮助大家进一步了解监控 Kubernetes. 如果想要监控 Kubernetes,包括基础架构平台和正在运行的工作负载,传统的监控工具和流程可能还不够用. 就目前而言,监控 Kubernetes 并不是件容易的事.

Kubernetes 切换到 Containerd

- - bleem
由于 Kubernetes 新版本 Service 实现切换到 IPVS,所以需要确保内核加载了 IPVS modules;以下命令将设置系统启动自动加载 IPVS 相关模块,执行完成后需要重启. 重启完成后务必检查相关 module 加载以及内核参数设置:. 1.2、安装 Containerd. Containerd 在 Ubuntu 20 中已经在默认官方仓库中包含,所以只需要 apt 安装即可:.

Spring Cloud Kubernetes指南

- -
当我们构建微服务解决方案时,SpringCloud和Kubernetes都是最佳解决方案,因为它们为解决最常见的挑战提供组件. 但是,如果我们决定选择Kubernetes作为我们的解决方案的主要容器管理器和部署平台,我们仍然可以主要通过SpringCloudKubernetes项目使用SpringCloud的有趣特性.

喜大普奔:Spark on kubernetes

- - Zlatan Eevee
两个星期前(08/15/2017),spark社区提了一个新的SPIP(Spark Project Improvement Proposals): Spark on Kubernetes: Kubernetes as A Native Cluster Manager,即用k8s管理spark集群. 经过社区2个星期的投票,看上去很快要能合入了.

Kubernetes 日志收集方案

- - IT瘾-dev
Kubernetes 中的基本日志. Kubernetes 日志收集. 以 sidecar 容器收集日志. 用 sidecar 容器重新输出日志. 使用 sidecar 运行日志采集 agent. 前面的课程中和大家一起学习了 Kubernetes 集群中监控系统的搭建,除了对集群的监控报警之外,还有一项运维工作是非常重要的,那就是日志的收集.