当咱们在看Loki的架构文档时,社区都会宣称Loki是一个能够支持多租户形式下运行的日志体系,但咱们再想进一步了解时,它却含蓄的表明Loki开启多租户只需求满足两个条件:

  • 装备文件中增加 auth_enabled: true
  • 恳求头内带上租户信息X-Scope-OrgID

这一切似乎都在告知你,”快来用我吧,这很简略”,事实受骗咱们真的要在kubernetes中构建一个多租户的日志体系时,咱们需求考虑的远不止于此。

通常当咱们在面临一个多租户的日志体系架构时,出于对日志存储的考虑,咱们一般会有两种形式来影响体系的架构。

1. 日志会集存储(后文以计划A代称)

和Loki原生一样,在日志进入到集群内,经过一系列校验和索引后会集的将日志统一写入后端存储上。

image.png

2. 日志分区存储(后文以计划B代称)

反中心存储架构,每个租户或项目都能够具有独立的日志服务和存储区块来保存日志。

image.png

从直觉上来看,日志分区带来的全体结构会更为杂乱,除了需求自己开发操控器来办理loki服务的生命周期外,它还需求为网关提供正确的路由战略。不过,不论多租户的体系挑选何种计划,在本文咱们也需从日志的整个流程来阐述不同计划的完成。

第一关:Loki区分

Loki是终究承载日志存储和查询的服务,在多租户的形式下,不论是大集群仍是小服务,Loki本身也存在一些装备空间需求架构者去适配。其间特别是在面临大集群场景下,保证每个租户的日志写入和查询所占资源的合理分配调度就显得尤为重要。

在原生装备中,大部分关于租户的调整能够在下面两个装备区块中完结:

  • query_frontend_config
  • limits_config

query_frontend_config

query_frontend是Loki分布式集群形式下的日子查询最前端,它承担着用户日志查询恳求的分化和聚合作业。那么显然,query_frontend关于恳求的处理资源应避免被单个用户过火抢占。

每个frontend处理的租户

[max_outstanding_per_tenant: <int> | default = 100]

limits_config

limits_config根本操控了Loki大局的一些流控参数和部分的租户资源分配,这里面能够经过Loki的-runtime-config发动参数来让服务动态定期的加载租户约束。这部分能够经过runtime_config.go中的runtimeConfigValues结构体内看到

type runtimeConfigValues struct {
TenantLimits map[string]*validation.Limits `yaml:"overrides"`
Multi kv.MultiRuntimeConfig `yaml:"multi_kv_config"`
}

能够看到关于TenantLimits内的约束装备是直接继承limits_config的,那么这部分的结构应该便是下面这样:

overrides:
tenantA:
ingestion_rate_mb: 10
max_streams_per_user: 100000
max_chunks_per_query: 100000
tenantB:
max_streams_per_user: 1000000
max_chunks_per_query: 1000000

当咱们在挑选选用计划A的日子架构时,关于租户部分的约束逻辑就应该要根据租户内的日志规模灵敏的装备。如果挑选计划B,由于每个租户占有完好的Loki资源,所以这部分逻辑就直接由原生的limits_config操控。

第二关:日志客户端

在Kubernetes环境下,最重要是让日志客户端知道被收集的容器所属的租户信息。这部分完成能够是经过日志Operator或者是解析kubernetes元数据来完成。尽管这两个完成方法不同,不过终究意图都是让客户端在收集日之后,在日志流的恳求上增加租户信息头。下面我别离以logging-operator和fluentbit/fluentd这两种完成方法来描述他们的完成逻辑

Logging Operator

Logging Operator是BanzaiCloud下开源的一个云原生场景下的日志收集计划。它能够经过创立NameSpace级别的CRD资源flow和output来操控日志的解析和输出。

image.png

经过Operator的方法能够精密的操控租户内的日志需求被收集的容器,以及操控它们的流向。以输出到loki举例,通常在只需在租户的命名空间内创立如下资源就能满足需求。

  • output.yaml,在创立资源时带入租户相关的信息
apiVersion: logging.banzaicloud.io/v1beta1
kind: Output
metadata:
name: loki-output
namespace: <tenantA-namespace>
spec:
loki:
url: http://loki:3100
username: <tenantA>
password: <tenantA>
tenant: <tenantA>
...
  • flow.yaml,在创立资源时关联租户需求被收集日志的容器,以及指定输出
apiVersion: logging.banzaicloud.io/v1beta1
kind: Flow
metadata:
name: flow
namespace:  <tenantA-namespace>
spec:
localOutputRefs:
- loki-output
match:
- select:
labels:
app: nginx
filters:
- parser:
remove_key_name_field: true
reserve_data: true
key_name: "log"

能够看到经过operator来办理多租户的日志是一个十分简略且优雅的方法,同时经过CRD的方法创立资源对开发者集成到项目也十分友爱。这也是我比较引荐的日志客户端计划。

FluentBit/FluentD

FluentBit和FluentD的Loki插件相同支持对多租户的装备。关于它们而言最重要的是让其感知到日志的租户信息。与Operator在CRD中直接声明租户信息不同,直接选用客户端计划就需求经过Kubernetes Metadata的方法来主动抓取租户信息。对租户信息的界说,咱们会声明在资源的label中。不过关于不同的客户端,label界说的途径仍是比较有考究的。它们总体处理流程如下:

image.png

  • FluentD

fluentd的kubernetes-metadata-filter能够抓取到namespaces_label,所以我比较引荐将租户信息界说在命名空间内。

apiVersion: v1
kind: Namespace
metadata:
labels:
tenant: <tenantA>
name: <taenant-namespace>

这样在就能够loki的插件中直接提取namespace中的租户标签内容,完成逻辑如下

<match loki.**>
@type loki
@id loki.output
url "http://loki:3100"
# 直接提取命名空间内的租户信息
tenant ${$.kubernetes.namespace_labels.tenant}
username <username>
password <password>
<label>
tenant ${$.kubernetes.namespace_labels.tenant}
</label>
  • FluentBit

fluentbit的metadata是从pod中抓取,那么咱们就需求将租户信息界说在workload的template.metadata.labels当中,如下:

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app:  nginx
spec:
template:
metadata:
labels:
app: nginx
tenant: <tanant-A>

之后就需求利用rewrite_tag将容器的租户信息提取出来进行日志管道切分。并在output阶段针对不同日志管道进行输出。它的完成逻辑如下:

[FILTER]
Name           kubernetes
Match          kube.*
Kube_URL       https://kubernetes.default.svc:443
Merge_Log      On
[FILTER]
Name           rewrite_tag
Match          kube.*
#提取pod中的租户信息,并进行日志管道切分
Rule           $kubernetes['labels']['tenant'] ^(.*)$ tenant.$kubernetes['labels']['tenant'].$TAG false
Emitter_Name   re_emitted
[Output]
Name           grafana-loki
Match          tenant.tenantA.*
Url            http://loki:3100/api/prom/push
TenantID       "tenantA"
[Output]
Name           grafana-loki
Match          tenant.tenantB.*
Url            http://loki:3100/api/prom/push
TenantID       "tenantB"

能够看到不论是用FluentBit仍是Fluentd的方法进行多租户的装备,它们不但对标签有一定的要求,对日志的输出途径装备也不是十分灵敏。所以fluentd它比较做合适计划A的日志客户端,而fluentbit比较合适做计划B的日志客户端

第三层:日志网关

日志网关精确的说是Loki服务的网关,关于计划A来说,一个大Loki集群前面的网关,只需求简略满足能够横向扩展即可,租户的头信息直接传递给后方的Loki服务处理。这类计划相对简略,并无特别阐明。只需注意针对查询接口的装备需调试优化,例如网关服务与upstream之间的连接超时时间网关服务response数据包大小等。

本文想阐明的日志网关是针对计划B场景下,处理针对不同租户的日志路由问题。从上文能够看到,在计划B中,咱们引入了一个操控器来处理租户Loki实例的办理问题。可是这样就带来一个新的问题需求处理,那便是Loki的服务需求注册到网关,并完成路由规矩的生成。这部分能够由集群的操控器CRD资源作为网关的upsteam源装备。操控器的逻辑如下:

image.png

网关服务在处理租户头信息时,路由部分的逻辑为判断Header中X-Scope-OrgID带租户信息的日志恳求,并将其转发到对应的Loki服务。咱们以nginx作为网关举个例,它的核心逻辑如下:

#upstream内地址由sidecar从CRD中获取loki实例后渲染生成
upstream tenantA {
server x.x.x.x:3100;
}
upstream tenantB {
server y.y.y.y:3100;
}
server {
location / {
set tenant $http_x_scope_orgid;
proxy_pass http://$tenant;
include proxy_params;

总结

本文介绍了基于Loki在多租户形式下的两种日志架构,别离为日志会集存储日志分区存储。他们别离具备如下的特点:

计划 Loki架构 客户端架构 网关架构 开发难度 运维难度 自动化程度
日志会集存储 集群、杂乱 fluentd / fluentbit 简略 简略 中等
日志分区存储 简略 Logging Opeator 较杂乱 较杂乱(操控器部分) 中等

关于团队内具备kubernetes operator相关开发经历的同学能够选用日志分区存储计划,如果团队内倾向运维方向,能够挑选日志会集存储计划。