Ingress-nginx工作原理和實踐

本文記錄/分享 目前項目的 K8s 部署結構和請求追蹤改造方案

這個圖算是一個通用的前後端分離的 k8s 部署結構:
Nginx Ingress 負責暴露服務(nginx前端靜態資源服務), 根據十二要素應用的原
則,將後端 api 作為 nginx 服務的附加動態資源。

Ingress vs Ingress-nginx

Ingress 是一種向 k8s 集群外部的客戶端公開服務的方法, Ingress 在網路協議棧的應用層工作,
根據請求的主機名 host 和路徑 path 決定請求轉發到的服務。

在應用 Ingress對象提供的功能之前,必須強調集群中存在 Ingress Controller, Ingress 資源才能正常工作。

我這裡的 web 項目使用的是常見的 Ingress-nginx (官方還有其他用途的 Ingress),Ingress-nginx 是使用 nginx 作為反向代理和負載均衡器的 K8s Ingress 控制器, 作為 Pod 運行在kube-system 命名空間。

了解 Ingress 工作原理,有利於我們如何與運維人員打交道。

下面通過 Ingress-nginx 暴露 Kibana 服務:

---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: kibana
  labels:
    app: kibana
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "30"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "1800"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "1800"
    nginx.ingress.kubernetes.io/proxy-body-size: "8m"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  tls:
    - hosts:
      - '//logging.internal.gridsum.com/'
      secretName: tls-cert
  rules:
    - host: '//logging.internal.gridsum.com'
      http:
        paths:
          - path: /
            backend:
              serviceName: kibana
              servicePort: 5601

Ingress-nginx 中最讓我困惑的是它的Paths分流rewrite-target註解。

  • Paths 分流
    一般用於 根據特定的 Path,將請求轉發到特定的後端服務 Pod,後端服務 Pod 能接收到 Path 這個資訊。
    一般後端服務是作為 api。
  • rewrite-target
    將請求重定向到後端服務, 那有什麼用處呢?

答: 以上面暴露的 kibana 為例, 我們已經可以在//logging.internal.gridsum.com/ 訪問完整的 Kibana, 如果我想利用這個域名暴露 ElasticSearch 站點,怎麼操作?
這時就可以利用rewrite-target

---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: elasticsearch
  labels:
    app: kibana
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "30"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "1800"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "1800"
    nginx.ingress.kubernetes.io/proxy-body-size: "8m"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/rewrite-target: "/$2"
spec:
  tls:
    - hosts:
      - 'logging.internal.gridsum.com'
      secretName: tls-cert
  rules:
    - host: 'logging.internal.gridsum.com'
      http:
        paths:
          - path: /es(/|$)(.*)
            backend:
              serviceName: elasticsearch
              servicePort: 9200

在此 Ingress 定義中,由(.*)捕獲的所有字元都將分配給佔位符$2,然後將其用作重寫目標註解中的參數。 這樣的話://logging.internal.gridsum.com/es 將會重定向到後端 elasticsearch 站點,並且忽略了 es 這個 path

Ingress-nginx 到 webapp 的日誌追蹤

熟悉我的朋友知道, 我寫了《一套標準的ASP.NET Core容器化應用日誌收集分析方案》,這裡面主要是 BackEnd App 的日誌,從我上面的結構圖看,

Ingress-nginx—-> Nginx FrontEnd App—>BackEnd App 需要一個串聯的追蹤 Id, 便於觀察運維網路和業務應用。

幸好 Ingress-nginx, Nginx 強大的配置能力幫助我們做了很多事情:

  • 客戶端請求到達 Ingress-Nginx Controllerr,Ingress-Nginx Controller 會自動添加一個X-Request-ID的請求 Header, 隨機值—- 這個配置是默認的

  • 請求達到 Nginx FrontEnd App, Nginx 有默認配置proxy_pass_request_headers on;, 自動將請求頭都傳遞到上游的 Backend App

這樣跨越整個結構圖的 request_id 思路已經清楚了,最後一步只需要我們在 Backend App 中提取請求中攜帶的X-Request-ID, 並作為日誌的關鍵輸出欄位。

這就涉及到怎麼從自定義日誌的 LayoutRender

下面為 NLog 自定義名為x_request_id的 Render,該 Render 從請求的 X-Request-ID 標頭中提取值。

① 定義 NLog Render

    /// <summary>
    /// Represent a unique identifier to represent a request from the request HTTP header X-Request-Id.
    /// </summary>
    [LayoutRenderer("x_request_id")]
    public class XRequestIdLayoutRender : HttpContextLayoutRendererBase
    {
        protected override void Append(StringBuilder builder, LogEventInfo logEvent)
        {
            var identityName = HttpContextAccessor.HttpContext?.Request?.Headers?["X-Request-Id"].FirstOrDefault();
            builder.Append(identityName);
        }
    }

    /// <summary>
    /// Represent a http context layout renderer to access the current http context.
    /// </summary>
    public abstract class HttpContextLayoutRendererBase : LayoutRenderer
    {
        private IHttpContextAccessor _httpContextAccessor;

        /// <summary>
        /// Gets the <see cref="IHttpContextAccessor"/>.
        /// </summary>
        protected IHttpContextAccessor HttpContextAccessor { get { return _httpContextAccessor ?? (_httpContextAccessor = ServiceLocator.ServiceProvider.GetService<IHttpContextAccessor>()); } }
    }

    internal sealed class ServiceLocator
    {
        public static IServiceProvider ServiceProvider { get; set; }
    }

② 從請求中獲取 X-Request-Id 依賴 IHttpContextAccessor 組件
這裡使用 依賴查找的方式獲取該組件, 故請在 Startup ConfigureService 中生成服務

 public void ConfigureServices(IServiceCollection services)
 {
     // ......
     ServiceLocator.ServiceProvider = services.BuildServiceProvider();
 }

③ 最後在 Program 中註冊這個 NLog Render:

 public static void Main(string[] args)
{
     LayoutRenderer.Register<XRequestIdLayoutRender>("x_request_id");
     CreateHostBuilder(args).Build().Run();
}

這樣從 Ingress-Nginx 產生的request_id,將會流轉到 Backend App, 並在日誌分析中起到巨大作用,也便於劃清運維/開發的故障責任。

總結

  1. 了解了Ingress在應用層工作,根據Host和Path暴露k8s服務
  2. 本文梳理了Ingress和常見的Ingress-nginx的關係
  3. 對於應用了Ingress的應用,梳理了從Ingress-Nginx到WebApp的日誌追蹤id, 便於排查網路/業務故障
Tags: