Kuberneters源码分析 - Ingress nginx 配置更新

在Ingress Nginx主流程一文中,已经介绍了Ingress Nginx的驱动流程,系统的主要驱动逻辑是监控API Server中相关资源的变化,并通过事件回调去驱动系统的运行。本篇主要介绍关键的部分,如何准实时的更新Nginx的配置,与Ingress资源保持同步。

数据驱动回顾

基于SharedIndexInformer,我们可以实时感知到Ingress、CongMap、Secret、Endpoint、Service等资源的变化,并通过事件回调处理,通知到同步任务队列去定期更新Nginx的配置文件。

资源变化,同步事件的生成代码为例子为:

            updateCh.In() <- Event{
                Type: CreateEvent,
                Obj:  obj,
            }

目前支持的事件类型有:

const (
    // CreateEvent event associated with new objects in an informer
    CreateEvent EventType = "CREATE"
    // UpdateEvent event associated with an object update in an informer
    UpdateEvent EventType = "UPDATE"
    // DeleteEvent event associated when an object is removed from an informer
    DeleteEvent EventType = "DELETE"
    // ConfigurationEvent event associated when a controller configuration object is created or updated
    ConfigurationEvent EventType = "CONFIGURATION"
)

Event的Obj成员一般就是发生变化的资源数据。

同步任务队列

同步任务队列负责接收同步事件,并保持Nginx的配置与资源的同步。

n.syncQueue = task.NewTaskQueue(n.syncIngress)
func NewTaskQueue(syncFn func(interface{}) error) *Queue {
    return NewCustomTaskQueue(syncFn, nil)
}

// NewCustomTaskQueue ...
func NewCustomTaskQueue(syncFn func(interface{}) error, fn func(interface{}) (interface{}, error)) *Queue {
    q := &Queue{
        queue:      workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
        sync:       syncFn,
        workerDone: make(chan bool),
        fn:         fn,
    }

    if fn == nil {
        q.fn = q.defaultKeyFunc
    }

    return q
}

同步任务队列负责把为每个入队的元素调用syncFn,而这里的syncFn实际调用NGINXController.syncIngress方法。

这里有一个问题要考虑的就是如果资源变化非常频繁,那会不会造成我们要频繁的去更新Nginx的配置文件,频发的发起Nginx的Reload操作,这个操作对于业务量大的服务来说,还是影响比较大的。

上面这个问题,其实就跟这个任务队列的实现有关了,任务队列实现了流控,并且还会针对变更资源的时间进行一些处理,忽略掉一些变更事件比同步事件旧的变更事件。这里暂时不做分析。

同步处理

同步处理是syncIngress方法,虽然他带了一个参数,但是我们可以看到这个参数基本是忽略的,syncIngress对每类资源的变更都是执行相同的操作。

// syncIngress collects all the pieces required to assemble the NGINX
// configuration file and passes the resulting data structures to the backend
// (OnUpdate) when a reload is deemed necessary.
func (n *NGINXController) syncIngress(interface{}) error {
    n.syncRateLimiter.Accept() // 流控处理,防止更新太频繁

    if n.syncQueue.IsShuttingDown() {
        return nil
    }

    // sort Ingresses using the ResourceVersion field
    ings := n.store.ListIngresses() // 从缓存中获取所有的Ingress服务
    sort.SliceStable(ings, func(i, j int) bool {
        ir := ings[i].ResourceVersion
        jr := ings[j].ResourceVersion
        return ir < jr
    })

    upstreams, servers := n.getBackendServers(ings) // 从ings中提取所有的upstreams和servers
// 这样做的主要原因在于:相同的upstream可能被多个servers共享(条件:namespace、service、port相同即可)
    var passUpstreams []*ingress.SSLPassthroughBackend

    for _, server := range servers {
        if !server.SSLPassthrough {
            continue
        }

        for _, loc := range server.Locations {
            if loc.Path != rootLocation {
                glog.Warningf("Ignoring SSL Passthrough for location %q in server %q", loc.Path, server.Hostname)
                continue
            }
            passUpstreams = append(passUpstreams, &ingress.SSLPassthroughBackend{
                Backend:  loc.Backend,
                Hostname: server.Hostname,
                Service:  loc.Service,
                Port:     loc.Port,
            })
            break
        }
    }

    pcfg := &ingress.Configuration{ // 生成新的ingress配置
        Backends:              upstreams,
        Servers:               servers,
        TCPEndpoints:          n.getStreamServices(n.cfg.TCPConfigMapName, apiv1.ProtocolTCP),
        UDPEndpoints:          n.getStreamServices(n.cfg.UDPConfigMapName, apiv1.ProtocolUDP),
        PassthroughBackends:   passUpstreams,
        BackendConfigChecksum: n.store.GetBackendConfiguration().Checksum,
    }

    if n.runningConfig.Equal(pcfg) { // 判断新旧配置是否相同,相同则不用继续处理
        glog.V(3).Infof("No configuration change detected, skipping backend reload.")
        return nil
    }

        // 是否启动了动态配置支持并且目前的变更能够支持动态配置
    if n.cfg.DynamicConfigurationEnabled && n.IsDynamicConfigurationEnough(pcfg) {
        glog.Infof("Changes handled by the dynamic configuration, skipping backend reload.") // 符合动态条件,就不用变更配置文件
    } else { // 不符合,就需要变更配置文件
        glog.Infof("Configuration changes detected, backend reload required.")

        hash, _ := hashstructure.Hash(pcfg, &hashstructure.HashOptions{
            TagName: "json",
        }) // 生成配置文件的hash值,看起来主要是做metric监控收集使用

        pcfg.ConfigurationChecksum = fmt.Sprintf("%v", hash) 

        err := n.OnUpdate(*pcfg) // 变更Nginx配置文件,后面详细分析
        if err != nil {
            n.metricCollector.IncReloadErrorCount()
            n.metricCollector.ConfigSuccess(hash, false)
            glog.Errorf("Unexpected failure reloading the backend:\n%v", err)
            return err
        }

        glog.Infof("Backend successfully reloaded.")
        n.metricCollector.ConfigSuccess(hash, true)
        n.metricCollector.IncReloadCount()
        n.metricCollector.SetSSLExpireTime(servers)
    }

    if n.cfg.DynamicConfigurationEnabled { // 这里走到动态配置变更功能
        isFirstSync := n.runningConfig.Equal(&ingress.Configuration{})
        go func(isFirstSync bool) {
            if isFirstSync {
                glog.Infof("Initial synchronization of the NGINX configuration.")

                // it takes time for NGINX to start listening on the configured ports
                time.Sleep(1 * time.Second)
            }
            err := configureDynamically(pcfg, n.cfg.ListenPorts.Status) // 动态配置处理,这块也会在后面分析
            if err == nil {
                glog.Infof("Dynamic reconfiguration succeeded.")
            } else {
                glog.Warningf("Dynamic reconfiguration failed: %v", err)
            }
        }(isFirstSync)
    }

    ri := getRemovedIngresses(n.runningConfig, pcfg)
    re := getRemovedHosts(n.runningConfig, pcfg)
    n.metricCollector.RemoveMetrics(ri, re)

    n.runningConfig = pcfg // 修改更新后的配置

    return nil
}

syncIngress方法的逻辑比较清晰,主要的功能都写在注释中了。下面详细讲述OnUpdate方法,该方法实现了Nginx配置文件的更新和reload。

OnUpdate

OnUpdate是在同步操作时发现需要变更Nginx配置文件时,就会被调用。后端配置与ConfigMap的配置会合并之后再创建最终的配置文件。

  • PassthroughBackends
    TODO:
func (n *NGINXController) OnUpdate(ingressCfg ingress.Configuration) error {
    cfg := n.store.GetBackendConfiguration()
    cfg.Resolver = n.resolver

    if n.cfg.EnableSSLPassthrough {
        servers := []*TCPServer{}
        for _, pb := range ingressCfg.PassthroughBackends {
            svc := pb.Service
            if svc == nil {
                glog.Warningf("Missing Service for SSL Passthrough backend %q", pb.Backend)
                continue
            }
            port, err := strconv.Atoi(pb.Port.String())
            if err != nil {
                for _, sp := range svc.Spec.Ports {
                    if sp.Name == pb.Port.String() {
                        port = int(sp.Port)
                        break
                    }
                }
            } else {
                for _, sp := range svc.Spec.Ports {
                    if sp.Port == int32(port) {
                        port = int(sp.Port)
                        break
                    }
                }
            }

            // TODO: Allow PassthroughBackends to specify they support proxy-protocol
            servers = append(servers, &TCPServer{
                Hostname:      pb.Hostname,
                IP:            svc.Spec.ClusterIP,
                Port:          port,
                ProxyProtocol: false,
            })
        }

        n.Proxy.ServerList = servers
    }
    .......
}
  • ServerNameHash
    if cfg.ServerNameHashBucketSize == 0 {
        nameHashBucketSize := nginxHashBucketSize(longestName)
        glog.V(3).Infof("Adjusting ServerNameHashBucketSize variable to %q", nameHashBucketSize)
        cfg.ServerNameHashBucketSize = nameHashBucketSize
    }
    serverNameHashMaxSize := nextPowerOf2(serverNameBytes)
    if cfg.ServerNameHashMaxSize < serverNameHashMaxSize {
        glog.V(3).Infof("Adjusting ServerNameHashMaxSize variable to %q", serverNameHashMaxSize)
        cfg.ServerNameHashMaxSize = serverNameHashMaxSize
    }
  • Headers
    Headers包括setHeaders和addHeaders两种,他们的都是放在configMap中。
    这里再提一下configMap是通过参数--configmap=ingress-nginx/nginx-configuration带入的,举例说明:
#  cat configmap.yaml 
apiVersion: v1
data:
  proxy-set-headers: "ingress-nginx/custom-headers"
  add-headers: "ingress-nginx/custom-headers"
kind: ConfigMap
metadata:
  name: nginx-configuration
  namespace: ingress-nginx
  labels:
    app: ingress-nginx

我们在上面的configmap中,指定了proxy-set-headers和add-headers的值都是ingrss-nginx/custom-headers,这里的ingress-nginx/custom-headers也是一个configmap,创建customer-headers的脚本如下:

curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/docs/examples/customization/custom-headers/custom-headers.yaml \
    | kubectl apply -f -

custom-headres.yaml的实际内容为:

apiVersion: v1
data:
  X-Different-Name: "true"
  X-Request-Start: t=${msec}
  X-Using-Nginx-Controller: "true"
kind: ConfigMap
metadata:
  name: custom-headers
  namespace: ingress-nginx

下面是处理setHeaders和addHeaders的代码:

        setHeaders := map[string]string{}
    if cfg.ProxySetHeaders != "" {
        cmap, err := n.store.GetConfigMap(cfg.ProxySetHeaders)
        if err != nil {
            glog.Warningf("Error reading ConfigMap %q from local store: %v", cfg.ProxySetHeaders, err)
        }

        setHeaders = cmap.Data
    }

    addHeaders := map[string]string{}
    if cfg.AddHeaders != "" {
        cmap, err := n.store.GetConfigMap(cfg.AddHeaders)
        if err != nil {
            glog.Warningf("Error reading ConfigMap %q from local store: %v", cfg.AddHeaders, err)
        }

        addHeaders = cmap.Data
    }

通过上面的代码,基于我们的例子配置来说,cfg.ProxySetHeaders值为“ingress-nginx/custom-headers”,通过n.store.GetConfigMap调用,最终得到setHeaders值为

map[string]string{
  "X-Different-Name": "true",
  "X-Request-Start": "t=${msec}",
  "X-Using-Nginx-Controller": "true"
}
  • 生成配置文件内容

代码如下所示,这块主要使用了text/template模块的功能来生成配置文件。主要功能在于模板的内容,后面再具体分析这块内容。

        // 构建模板配置参数
    tc := ngx_config.TemplateConfig{
        ProxySetHeaders:             setHeaders,
        AddHeaders:                  addHeaders,
        MaxOpenFiles:                maxOpenFiles,
        BacklogSize:                 sysctlSomaxconn(),
        Backends:                    ingressCfg.Backends,
        PassthroughBackends:         ingressCfg.PassthroughBackends,
        Servers:                     ingressCfg.Servers,
        TCPBackends:                 ingressCfg.TCPEndpoints,
        UDPBackends:                 ingressCfg.UDPEndpoints,
        HealthzURI:                  ngxHealthPath,
        CustomErrors:                len(cfg.CustomHTTPErrors) > 0,
        Cfg:                         cfg,
        IsIPV6Enabled:               n.isIPV6Enabled && !cfg.DisableIpv6,
        NginxStatusIpv4Whitelist:    cfg.NginxStatusIpv4Whitelist,
        NginxStatusIpv6Whitelist:    cfg.NginxStatusIpv6Whitelist,
        RedirectServers:             redirectServers,
        IsSSLPassthroughEnabled:     n.cfg.EnableSSLPassthrough,
        ListenPorts:                 n.cfg.ListenPorts,
        PublishService:              n.GetPublishService(),
        DynamicConfigurationEnabled: n.cfg.DynamicConfigurationEnabled,
        DisableLua:                  n.cfg.DisableLua,
    }

    tc.Cfg.Checksum = ingressCfg.ConfigurationChecksum
        // 调用模板,生成配置文本内容
    content, err := n.t.Write(tc)
    if err != nil {
        return err
    }
  • 生成opentracing配置文件
    如果启动了opentracing,就需要生成opentracing配置文件,opentracing的配置文件为/etc/nginx/opentracing.json,它支持两种zipkin、jaeger两种opentracing系统。它们的模板文件如下所示:
const zipkinTmpl = `{
  "service_name": "{{ .ZipkinServiceName }}",
  "collector_host": "{{ .ZipkinCollectorHost }}",
  "collector_port": {{ .ZipkinCollectorPort }},
  "sample_rate": {{ .ZipkinSampleRate }}
}`

const jaegerTmpl = `{
  "service_name": "{{ .JaegerServiceName }}",
  "sampler": {
    "type": "{{ .JaegerSamplerType }}",
    "param": {{ .JaegerSamplerParam }}
  },
  "reporter": {
    "localAgentHostPort": "{{ .JaegerCollectorHost }}:{{ .JaegerCollectorPort }}"
  }
}`
  • 测试配置文件的正确性
    由NGINXController.testTemplate方法实现,主要功能为:
    1)生成测试配置文件
    2)调用nginx -c ${Config} -t来测试配置文件的正确性
  • 生成配置文件并重载配置文件
    生成/etc/nginx/nginx.conf文件,调用nginx -s reload触发配置加载。

Ingress的更新分析完了,到这里我们还遗留了两个问题:
1)动态配置功能
2)配置文件模板

动态配置功能

动态配置把一个Backend封装成JSON格式,并把内容POST到内部的被Lua处理的HTTP服务。

Backend描述了关联到service的一个或者多个远程服务(endpoints)。
譬如,下面是一个ingress的yaml文件,对应的backend的概念就是对应的rules->host:foo.bar.com->paths->path->backend

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    ingress.kubernetes.io/rewrite-target: /
  namespace: ingress-nginx
spec:
  rules:
  -  host: foo.bar.com
     http:
      paths:
      - path: /
        backend:
          serviceName: my-service
          servicePort: 1005

动态配置代码如下:

func configureDynamically(pcfg *ingress.Configuration, port int) error {
        // 生成新的backends,TODO:为什么这里要生成新的,不直接用pcfg.Backends???
        // 看起来是去掉了service字段信息
    backends := make([]*ingress.Backend, len(pcfg.Backends))

    for i, backend := range pcfg.Backends {
        luaBackend := &ingress.Backend{
            Name:            backend.Name,   // --
            Port:            backend.Port,
            Secure:          backend.Secure,
            SSLPassthrough:  backend.SSLPassthrough,
            SessionAffinity: backend.SessionAffinity,
            UpstreamHashBy:  backend.UpstreamHashBy,
            LoadBalancing:   backend.LoadBalancing,
        }

        var endpoints []ingress.Endpoint
        for _, endpoint := range backend.Endpoints {
            endpoints = append(endpoints, ingress.Endpoint{
                Address:     endpoint.Address,
                FailTimeout: endpoint.FailTimeout,
                MaxFails:    endpoint.MaxFails,
                Port:        endpoint.Port,
            })
        }

        luaBackend.Endpoints = endpoints
        backends[i] = luaBackend
    }

        // 把所有的backends打包成json
    buf, err := json.Marshal(backends)
    if err != nil {
        return err
    }

    glog.V(2).Infof("Posting backends configuration: %s", buf)
        // post backends数据到nginx的http服务,让lua去处理
    url := fmt.Sprintf("http://localhost:%d/configuration/backends", port)
    resp, err := http.Post(url, "application/json", bytes.NewReader(buf))
    if err != nil {
        return err
    }
        ......
    return nil
}

对于configuration/backends的endpoint的处理逻辑,我们可以看一下nginx.tmpl文件中的片段:

        {{ if $all.DynamicConfigurationEnabled }}
        location /configuration {
            {{ if $cfg.EnableOpentracing }}
            opentracing off;
            {{ end }}

            allow 127.0.0.1;
            {{ if $IsIPV6Enabled }}
            allow ::1;
            {{ end }}
            deny all;

            # this should be equals to configuration_data dict
            client_max_body_size                    "10m";
            proxy_buffering                         off;

            content_by_lua_block {
              configuration.call()
            }
        }
        {{ end }}

最终调用了lua代码configuration.call()。

配置文件模板

nginx的配置文件模板是基于text/template模块实现的,这里就不详细介绍text/template了。

前面我们知道,模板中使用的变量是TemplateConfig结构,TemplateConfig的结构定义如下:

type TemplateConfig struct {
    ProxySetHeaders             map[string]string // SetHeaders中的内容
    AddHeaders                  map[string]string    // AddHeaders中的内容
    MaxOpenFiles                int              // 最大打开文件数
    BacklogSize                 int
    Backends                    []*ingress.Backend  // 所以的backends
    PassthroughBackends         []*ingress.SSLPassthroughBackend
    Servers                     []*ingress.Server  // servers信息
    TCPBackends                 []ingress.L4Service  // TCP反向代理
    UDPBackends                 []ingress.L4Service  // UDP反向代理
    HealthzURI                  string
    CustomErrors                bool
    Cfg                         Configuration
    IsIPV6Enabled               bool
    IsSSLPassthroughEnabled     bool
    NginxStatusIpv4Whitelist    []string
    NginxStatusIpv6Whitelist    []string
    RedirectServers             map[string]string
    ListenPorts                 *ListenPorts
    PublishService              *apiv1.Service
    DynamicConfigurationEnabled bool
    DisableLua                  bool
}

nginx的配置文件模板为/etc/nginx/template/nginx.tmpl,在前面动态配置分析中,我们列举了一部分内容。

你可能感兴趣的:(Kuberneters源码分析 - Ingress nginx 配置更新)