Skip to main content

· One min read
jojotong

OpenTelemetry

Opentelemetry 是一个CNCF社区下一个开源的可观测性框架,或者也可以说是一组工具、API 和 SDK 的集合,来检测、生成、收集和导出可观测性数据(指标、日志和链路),以帮助我们分析软件的性能和行为。

优点

过去,检测代码的方式会有所不同,因为每个可观测性后端都有自己的检测库和代理,用于向工具发送数据。

这意味着没有用于将数据发送到可观察性后端的标准化数据格式,由于缺乏标准化,最终结果是缺乏数据可移植性和用户维护仪器库的负担。

Opentelemetry因此而生,拥有来自云提供商、 供应商和最终用户的广泛行业支持和采用,提供了:

  • 每种语言都有一个独立于供应商的instrumentation library ,支持自动和手动。

  • 可以以多种方式部署的单个供应商中立的收集器二进制文件。

  • 生成、发出、收集、处理和导出遥测数据的端到端实现。

  • 完全控制您的数据,能够通过配置将数据并行发送到多个目的地。

  • 开放标准语义约定以确保与供应商无关的数据收集

  • 能够并行支持多种 上下文传播 格式,以协助随着标准的发展进行迁移。

缺点

有别于 Istio ,它并不是一个开箱即用的工具,也是更有侵入性的,但是根据我们的经验:

越不具侵入性的工具,就越无法做出更深更广的观测

我们为了获取更深、更广的指标,势必要侵入性地进行观测,因此,采用Istio envoy提供的指标是不够的。而此时,Opentelemetry正在逐渐形成行业标准,受到许多供应商支持,是我们一个很好的选择。

OpenTelemetry 架构

OpenTelemetry Reference Architecture

如上图所示,整体的组织架构实际可以理解为两部分:

  1. 将可观测性数据(trace, metric, log)全部导出(push)到 otel collector,无论你是通过什么形式,来自什么组件,如:
  • 从项目代码通过otlp协议导出

    • 语言:go, java, python...
    • 集成方式: auto/manual instrumentation, api, sdk
# example config for otel collector's receivers
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
  • 通过基础设施(本质上还是通过应用程序导出)
    • k8s
    • aws
    • others...
  • 通过其他服务,直接将一些服务数据导出到otel collector,如
    • prometheus
    • jarger
    • others...
  1. 将不同类型的数据按需求导出(push or pull)到具体的可观测性工具,如

    • metrics 指标可以导出至监控服务(如通过prometheues)
    • trace 指标可以导出至链路追踪服务(如jaeger)
    • log 指标可以导出至日志服务(如loki)
# example config for otel collector's exporters
exporters:
jaeger:
endpoint: jaeger-operator-jaeger-collector.observability:14250
tls:
insecure: true
loki:
endpoint: http://localhost:3100/loki/api/v1/push
prometheus:
endpoint: 0.0.0.0:8889
resource_to_telemetry_conversion:
enabled: true

项目组织结构

Opentelemetry项目组织结构繁多而复杂,官方共有59个repo,但我可以大致按以下结构进行梳理:

首先,Opentelemetry提供了官方的opentelemetry-collector,作为整个项目的核心仓库,用以整和所有可观测性指标,也整合了opentelemetry-collector-contrib提供的第三方服务,这两个项目统一构成collector,但是作为开发者,我们不需要过多关心。

然后,针对不同的语言,基本每种语言都提供了三个仓库作以下用途:

  • 核心仓库(黄色): 提供该语言的基础SDK,为instrumentationcontrib仓库提供接入的统一标准,通过这个仓库,你也可以在不使用以下两个库的情况下接入opentelemetry。

  • instrumentation(绿色): 特定的语言实现,通过它,你可以在不甚了解otel的情况下,实现一体化、开箱即用地、一键地为你的工程引入opentelemetry。

    opentelemetry-java-instrumentation可以直接以

    java -javaagent:path/to/opentelemetry-javaagent.jar \
    -jar myapp.jar

    的形式接入opentelemetry。

  • contrib(蓝色): 提供一些为第三方库以相对便捷的形式接入Opentelemetry的库。

    opentelemetry-go-contrib提供了针对gin, beego框架等第三方库接入opentelemetry的便捷方法。

Golang 实践指南

Trace(stable)

初始化

我们需要构造一个全局的TraceProvider,下面的例子构造的provider 采用的 http exporter,即将traces通过http协议发送给指定的opentelemetry-collector

import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
exp, err := otlptracehttp.New(ctx)
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithBatcher(exp),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
return tp, nil
}

注意:

  1. 全局TraceProvider通过otel.SetTracerProvider()设置,获取时,也可直接调otel.GetTracerProvider()

我建议大家直接设置为全局的,而不是作为局部变量传来传去的一个好处是,当我们引用了第三方库,它通常也会默认使用全局的provider,这样就能简单的保证我们一个程序只有一个provider,也就是说,只会把数据发送到一个collector。

  1. 初始化的过程中,不需要指定 opentelemetry-collector endpoint等配置,我们统一通过环境变量注入。如:
  • otlptracehttp.WithEndpoint() => OTEL_EXPORTER_OTLP_ENDPOINT
  • otlptracehttp.WithInsecure => OTEL_EXPORTER_OTLP_INSECURE

支持的环境变量:

采样器

Go SDK 提供了几个基本的采样器:

  • AlwaysSample(): 全部采样
  • NeverSample(): 全部丢弃
  • TraceIDRatioBased(fraction float64): 设置采样率
  • ParentBased(root Sampler, samplers ...ParentBasedSamplerOption): 基于parent span 设置采样策略

除此之外,根据Sampler接口:

// Sampler decides whether a trace should be sampled and exported.
type Sampler interface {
// DO NOT CHANGE: any modification will not be backwards compatible and
// must never be done outside of a new major release.

// ShouldSample returns a SamplingResult based on a decision made from the
// passed parameters.
ShouldSample(parameters SamplingParameters) SamplingResult
// DO NOT CHANGE: any modification will not be backwards compatible and
// must never be done outside of a new major release.

// Description returns information describing the Sampler.
Description() string
// DO NOT CHANGE: any modification will not be backwards compatible and
// must never be done outside of a new major release.
}

我们可以编写自己的采样器,eg:

import (
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

// kubegems sampler, ignore samples whitch contains "kubegems.ignore" attrbute.
type kubegemsSampler struct{}

func (as kubegemsSampler) ShouldSample(p sdktrace.SamplingParameters) sdktrace.SamplingResult {
result := sdktrace.SamplingResult{
Tracestate: trace.SpanContextFromContext(p.ParentContext).TraceState(),
}
shouldSample := true
for _, att := range p.Attributes {
if att.Key == "kubegems.ignore" && att.Value.AsBool() == true {
shouldSample = false
break
}
}
if shouldSample {
result.Decision = sdktrace.RecordAndSample
} else {
result.Decision = sdktrace.Drop
}
return result
}

func (as kubegemsSampler) Description() string {
return "KubegemsSampler"
}

使用采样器时,我们需要注意以下问题:

假如有两个服务为A,B, 调用关系为 A -> B, 我们想要为其设置采样率为50%,怎么设?

  1. 直接为两个服务都设置

    sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.5))

    这样设置后,A的采样率自然是50%,但B的采样率并不会成了25%,测试发现它仍然是50%。我们可以查阅设计文档

    • The TraceIdRatioBased MUST ignore the parent SampledFlag. To respect the parent SampledFlag, the TraceIdRatioBased should be used as a delegate of the ParentBased sampler specified below.

    也就是说,它只会根据parent span来决定是否被采样

  2. 使用ParentBased采样器(最好的方法)

    sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.5))),

    ParentBased Sampler显式地配置有parent span情况下地采样策略,默认情况下使用如下策略:

    func configureSamplersForParentBased(samplers []ParentBasedSamplerOption) samplerConfig {
    c := samplerConfig{
    remoteParentSampled: AlwaysSample(),
    remoteParentNotSampled: NeverSample(),
    localParentSampled: AlwaysSample(),
    localParentNotSampled: NeverSample(),
    }

    for _, so := range samplers {
    c = so.apply(c)
    }

    return c
    }

    remoteParentSampled: AlwaysSample()为例:它是说,默认情况下,如果这个span来自远程的parent span,而且parent spane已经被采样了,那么,这个span也会被采样。

    我们也可以调整ParentBasedSamplerOption参数,eg:

    sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.5), sdktrace.WithRemoteParentSampled(sdktrace.NeverSample()))),

    它表示,当parent span被采样时,自己不采样,当然,这是不合理的。

埋点

我们可以在想要记录trace的地方,通过tracer.Start()创建一个新span来埋点。

当然,在span中,我可以主要可以添加以下几类信息:

  • SetAttributes: 设置一些属性(记录为tag)

  • AddEvent: 添加事件(记录为log), 通常用来记录一些重要操作

  • SetStatus: 设置span状态。

// get user name by user id
func getUser(ctx context.Context, id string) (string, error) {
// start a new span from context.
newCtx, span := tracer.Start(ctx, "getUser", trace.WithAttributes(attribute.String("user.id", id)))
defer span.End()
// add start event
span.AddEvent("start to get user",
trace.WithTimestamp(time.Now()),
)
var username string
// get user name from db, if you want to trace it, `WithContext` is necessary.
result := getDB().WithContext(newCtx).Raw(`select username from users where id = ?`, id).Scan(&username)
if result.Error != nil || result.RowsAffected == 0 {
err := fmt.Errorf("user %s not found", id)
span.SetStatus(codes.Error, err.Error())
return "", err
}
// set user info in span's attributes
span.SetAttributes(attribute.String("user.name", username))
// add end event
span.AddEvent("end to get user",
trace.WithTimestamp(time.Now()),
trace.WithAttributes(attribute.String("user.name", username)),
)
span.SetStatus(codes.Ok, "")
return username, nil
}

届时,span大概长这个样子:

另外,关于span的父子关系,是通过context上下文来传递的。

tracer.Start(ctx context.Context, ...)中,如果传入的ctx 中没有span,那么返回的就是root span;如果有,那返回的就是该span的子span。

因此,我们能通过context串联起清晰的链路调用,但也因此,我们需要非常关注context的使用。

跨进程传播

Openletemetry 提供 propagator在进程间交换的消息中读取和写入上下文数据的对象,详见 https://opentelemetry.io/docs/reference/specification/context/api-propagators/

Openletemetry 实现了两种propagator API:

  • TraceContext: 用以传播traceparenttracestate信息来保证一条trace的调用信息不会因为跨进程而中断
  • Baggage: 用以传播用户自定义信息

propagator实现两个方法:

  • Inject(ctx context.Context, carrier TextMapCarrier): Injects the value into a carrier. For example, into the headers of an HTTP request.
  • Extract(ctx context.Context, carrier TextMapCarrier) context.Context: Extracts the value from an incoming request. For example, from the headers of an HTTP request.
TraceContext

使用TraceContext在下游Inject和上游Extract来打通服务间调用链路, eg:

  1. 设置propagater:
    otel.SetTextMapPropagator(propagation.TraceContext{})
  1. client:
import (
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)

func DoRequest(){
...
req, err := http.NewRequestWithContext(ctx, method, addr, body)
// inject to http.Request by propagator to do distribute tracing
otel.GetTextMapPropagator().Inject(req.Context(), propagation.HeaderCarrier(req.Header))
http.DefaultClient.Do(req)
...
}
  1. server:
import (
"go.opentelemetry.io/otel/propagation"
)

func HandleRequest(){
...
// extract from http.Request by propagator to do distribute tracing
ctx := cfg.Propagators.Extract(req.Context(), propagation.HeaderCarrier(req.Header))
ctx, span := tracer.Start(ctx, spanName, opts...)
defer span.End()
req = req.WithContext(ctx)
...
}

如果你想了解更多关于TraceContext的信息,可以阅读文档:https://www.w3.org/TR/trace-context/,因为它遵从`W3C Trace Context format`标准。

Baggage

使用Baggage在进程间传递信息,在使用它之前,我们需要弄清楚两个问题:

  1. 为什么我们需要 Baggage?

    • 在整条trace中传播信息
    • 假如我们希望将应用程序中的信息附加到一个 span, 并在稍后检索该信息,然后将其用于另一个 span。由于span一经创建就不能修改,而Baggage 允许通过提供一个存储和检索信息的地方来解决这个问题。
  2. Baggage应该用来做什么?

    Baggage 应该用于我们可以向第三方公开的非敏感数据,因为它与当前上下文一起存储在 HTTP 标头中。

    建议用来传播包括帐户标识、用户 ID、产品 ID 和原始 IP 等内容。将它们向下传递之后,我们就可以将它们添加到下游服务中的 Span 中,以便在在可观察性后端中进行搜索时更轻松地进行过滤。

比如说,在kubegems中有两个服务:apiagent,以一次用户请求获取k8s资源为例:

  • api: 解析用户token,校验用户信息,再交给agent获取对应集群的k8s资源
  • agent: 不再处理用户信息,直接调用k8s api并返回

在这种情况下,假如我们想要在agent的trace信息中,知道这个请求时哪个用户发起的,就可以借助baggage来实现:

首先,初始化TextMapPropagator时,需要加上Baggage Propagator:

    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))

然后,在apiagent发起请求时,注入user namebaggage:

import (
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/baggage"
)

func DoRequest(){
...
userBaggage, err := baggage.Parse(fmt.Sprintf("user.id=%d,user.name=%s", user.ID, user.Username))
if err != nil {
otel.Handle(err)
}

req, err := http.NewRequestWithContext(baggage.ContextWithBaggage(ctx, userBaggage), clientreq.Method, addr, body)
if err != nil {
return nil, err
}
otel.GetTextMapPropagator().Inject(req.Context(), propagation.HeaderCarrier(req.Header))
http.DefaultClient.Do(req)
...
}

最后,在agent解析baggage并设置为attributes:

import (
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/baggage"
)

func HandleRequest(){
...
// extract from http.Request by propagator to do distribute tracing
ctx := cfg.Propagators.Extract(req.Context(), propagation.HeaderCarrier(req.Header))
ctx, span := tracer.Start(ctx, spanName, opts...)
defer span.End()

reqBaggage := baggage.FromContext(ctx)
span.SetAttributes(
attribute.String("user.id", reqBaggage.Member("user.id").Value()),
attribute.String("user.name", reqBaggage.Member("user.name").Value()),
)
req = req.WithContext(ctx)
...
}

如果你想了解更多关于Baggage的信息,可以阅读文档:https://www.w3.org/TR/baggage/,因为它遵从`W3C Baggage format`标准。

理解propagator

无论是TraceContext还是Baggage,在我们选用的TextMapPropagator中,都是采用TextMapCarrier来实现

// TextMapCarrier is the storage medium used by a TextMapPropagator.
type TextMapCarrier interface {
...
}

TextMapCarrier,目前的唯一实现是HeaderCarrier

// HeaderCarrier adapts http.Header to satisfy the TextMapCarrier interface.
type HeaderCarrier http.Header

也就是说,不管我们采用http还是grpc协议,只要我们采用TextMapPropagator,实现信息传播的,是http协议 header。

我们可以通过Debug来追踪这一过程,首先, 在client端的Inject方法打上断点,观察它是怎么把要传播的信息注入进去的:

可以看到,注入前 context 已经带有了user.iduser.name信息,然后下一步:

通过把ctx带的信息注入进headr, 此时请求的Header中已经带有了TraceparentBaggage信息。

然后我们在server端的Extract方法打上断点,观察它是怎么解析出传播的信息的。

很显然,它通过从client请求的header中提取Traceparent来获取traceIDspanID,来关联上下游,再提取Baggage来获取来自client的信息。

其他形式的propagator

对基于http协议的进程间通信,我们使用TextMapPropagator完全足够,但如果说要针对没有HeaderCarrier实现的通信协议,官方有计划开发binary propagator来实现, 详见 https://github.com/open-telemetry/opentelemetry-specification/issues/437

Metrics(alpha)

由于opentelemety go标准库的metric实现还是alpha,极不稳定,文档几乎没有,请谨慎使用。

初始化

import (
"context"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/metric/global"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
)

func initMeter(ctx context.Context) (*sdkmetric.MeterProvider, error) {
exp, err := otlpmetrichttp.New(ctx)
if err != nil {
return nil, err
}
mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exp, sdkmetric.WithInterval(15*time.Second))))
global.SetMeterProvider(mp)
return mp, nil
}

要注意的配置主要是NewPeriodicReader(), 它用来设置我们收集并向opentelemetry collector发送指标的时间间隔。

在kubegems上,我们的opentelemetry collector使用的是pometheus exporter来导出监控指标,并设置有30sscrape_interval,因此,我们这里的WithInterval()最好是小于30s以保证监控数据的及时性。

使用

以下的示例是kubegems为gin框架添加的metrics实现,参照了net/httpopentelemetry实现(https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/instrumentation/net/http/otelhttp),记录了两个指标:

  • http.server.request_count: 请求总量
  • http.server.duration:请求耗时(ms)
import (
"time"

"github.com/gin-gonic/gin"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric/global"
"go.opentelemetry.io/otel/metric/instrument/syncfloat64"
"go.opentelemetry.io/otel/metric/instrument/syncint64"
"go.opentelemetry.io/otel/propagation"
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
)

// Server HTTP metrics.
const (
RequestCount = "http.server.request_count" // Incoming request count total
ServerLatency = "http.server.duration" // Incoming end to end duration, microseconds
)

const (
instrumentationName = "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

var (
counters map[string]syncint64.Counter
valueRecorders map[string]syncfloat64.Histogram
)

func MeterMiddleware(service string) gin.HandlerFunc {
counters = make(map[string]syncint64.Counter)
valueRecorders = make(map[string]syncfloat64.Histogram)
meter := global.MeterProvider().Meter(instrumentationName)

requestCounter, _ := meter.SyncInt64().Counter(RequestCount)
serverLatencyMeasure, _ := meter.SyncFloat64().Histogram(ServerLatency)

counters[RequestCount] = requestCounter
valueRecorders[ServerLatency] = serverLatencyMeasure
return func(c *gin.Context) {
requestStartTime := time.Now()
attributes := semconv.HTTPServerMetricAttributesFromHTTPRequest(service, c.Request)
ctx := otel.GetTextMapPropagator().Extract(c.Request.Context(), propagation.HeaderCarrier(c.Request.Header))

c.Next()
// Use floating point division here for higher precision (instead of Millisecond method).
// 由于Bucket分辨率的问题,这里只能记录为millseconds而不是seconds
elapsedTime := float64(time.Since(requestStartTime)) / float64(time.Millisecond)
counters[RequestCount].Add(ctx, 1, attributes...)
valueRecorders[ServerLatency].Record(ctx, elapsedTime, attributes...)
}
}

Log (not implemented yet)

opentelemetry 目前还未针对go有相关的实现。

但是,假如我们的应用运行在kubegems上,其中的日志收集、查询功能本身就提供了相关的能力,所以在官方的标准推出之前,我们也可以先通过span.SpanContext().TraceID()获取trace-id,自行在日志中打印trace-id,来实现trace-log关联。

下面以gin 和beego框架为例,简单讲解一下:

gin可以添加个打印日志的middleware

func logMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
ctx := otel.GetTextMapPropagator().Extract(c.Request.Context(), propagation.HeaderCarrier(c.Request.Header))
span := trace.SpanFromContext(ctx)

c.Next()
statusCode := c.Writer.Status()
logrus.WithFields(logrus.Fields{
"method": c.Request.Method,
"path": c.Request.URL.Path,
"trace-id": span.SpanContext().TraceID(),
"code": statusCode,
"latency": time.Since(start).String(),
"sampled": span.SpanContext().IsSampled(),
}).Info(http.StatusText(statusCode))
}
}

beego可以添加个filter:

    beego.InsertFilter("*", beego.BeforeRouter, func(c *bcontext.Context) {
ctx := otel.GetTextMapPropagator().Extract(c.Request.Context(), propagation.HeaderCarrier(c.Request.Header))
newctx, span := tracer.Start(ctx, "getUserFromBaggage")
defer span.End()
logrus.WithFields(logrus.Fields{
"method": c.Request.Method,
"path": c.Request.URL.Path,
"trace-id": span.SpanContext().TraceID(),
"sampled": span.SpanContext().IsSampled(),
}).Info("handle request")

reqBaggage := baggage.FromContext(newctx)
span.SetAttributes(
attribute.String("user.id", reqBaggage.Member("user.id").Value()),
attribute.String("user.name", reqBaggage.Member("user.name").Value()),
)
c.Request = c.Request.WithContext(newctx)
})

Kubegems接入Opentelemetry

假如我们的应用程序,已经在代码层面接入了opentelemetry,我们只需要为其添加几个环境变量(为统一kubegems上应用程序的接入,不建议修改):

    - name: OTEL_K8S_NODE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.nodeName
- name: OTEL_K8S_POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: OTEL_SERVICE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.labels['app']
- name: OTEL_K8S_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
- name: OTEL_RESOURCE_ATTRIBUTES
value: service.name=$(OTEL_SERVICE_NAME),namespace=$(OTEL_K8S_NAMESPACE),node=$(OTEL_K8S_NODE_NAME),pod=$(OTEL_K8S_POD_NAME)
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://opentelemetry-collector.observability:4318 # grpc change to 4317 port
- name: OTEL_EXPORTER_OTLP_INSECURE
value: "true"

示例程序

我们通过示例程序 otel-demo来演示、使用opentelemetry基本功能,该demo功能如下:

代码演示

获取代码并部署:

$ git clone https://github.com/jojotong/otel-demo.git
$ cd otel-demo
$ make build docker-build docker-push deploy

重点:sampler, propagator, baggage使用,gorm接入

kubegems功能演示

重点:trace, metric, log 联动查询

应用性能

trace详情

trace -> log

log -> monitor

· One min read
LinkMaq

KubeGems 是一款开源的企业级多租户容器云平台。围绕云原生社区,KubeGems 提供了多 Kubernetes 集群接入能力,并具备丰富的组件管理和资源成本分析功能,能够帮助企业快速的构建和打造一个本地化、功能强大且低成本的云管理平台。

KubeGems 发行版本的主要愿景如下:

  • 产品化开箱即用。确保 KubeGems 安装部署可以快速上手,并支持在界面中自我更新

  • 充分依托 CNCF 生态。 插件中心全面转向在线仓库,确保 KubeGems 始终使用云原生生态体系,形成围绕以 Kubernetes 为核心的云原生操作系统

  • 引入OpenTelemetry 可观测性。确保平台应用的各项 Metrics、Trace、Log 数据能够相互联通,提高用户查询效率

  • 机器学习 MLOps 自动化。支持海量在线(HuggingFace 、 OpenMMlab)、离线(ModelX)AI 算法的自动化部署与预览,推动云原生 AI 发展

  • 边缘计算与设备管理。KubeGems Edge 与 Rancher k3s 联合完成对边缘 K3S 集群的云端统一管理,确保云边协同任务开展。

KubeGems 是 2022 年 3 月底在 GitHub(https://github.com/kubegems) 开源一款云端 PaaS 产品。历经1年3个主版本的迭代,我们正式推出 v1.23 发行版的 GA 版本,欢迎试用并在 KubeGems 社区进行反馈。目前我们提供了在线 Demo 环境(https://demo.kubegems.io) ,后续我们也会长期跟踪 Kubernetes 和 CNCF社区的上游版本演进。

快速上手

安装部署

基于 Kubernetes 1.23.14 版本部署

  1. 确定部署版本
export KUBEGEMS_VERSION=<kubegems version>
  1. 部署 kubegems installer
kubectl create namespace kubegems-installer
kubectl apply -f https://github.com/kubegems/kubegems/raw/${KUBEGEMS_VERSION}/deploy/installer.yaml
  1. (可选)安装本地盘存储
kubectl create namespace local-path-storage
kubectl apply -f https://github.com/kubegems/kubegems/raw/${KUBEGEMS_VERSION}/deploy/addon-local-path-provisioner.yaml
  1. 部署 kubegems 核心组件
kubectl create namespace kubegems

export STORAGE_CLASS=local-path # 改为您使用的 storageClass

curl -sL https://github.com/kubegems/kubegems/raw/${KUBEGEMS_VERSION}/deploy/kubegems.yaml \
| sed -e "s/local-path/${STORAGE_CLASS}/g" \
> kubegems.yaml

kubectl apply -f kubegems.yaml

kubegems 所有服务部署并启动完成后会有如下 pod:

$ kubectl -n kubegems get pod

NAME READY STATUS RESTARTS AGE
kubegems-api-6d45f656f8-lfk7j 1/1 Running 0 21h
kubegems-argo-cd-app-controller-5b849bfb49-ltvdz 1/1 Running 0 21h
kubegems-argo-cd-repo-server-7dddd8f57d-ldj5k 1/1 Running 0 21h
kubegems-argo-cd-server-76745cc657-v8dx9 1/1 Running 0 21h
kubegems-chartmuseum-6c546b4d-qxfjj 1/1 Running 0 21h
kubegems-charts-init-main-lmtwt 0/1 Completed 0 21h
kubegems-dashboard-6bcd7f65f-89gsk 1/1 Running 0 21h
kubegems-gitea-0 1/1 Running 0 21h
kubegems-init-main-vjxnq 0/1 Completed 3 21h
kubegems-msgbus-7c58548497-pqwht 1/1 Running 5 (21h ago) 21h
kubegems-mysql-0 1/1 Running 0 21h
kubegems-redis-master-0 1/1 Running 0 21h
kubegems-worker-7d67974f4c-cj65l 1/1 Running 5 (21h ago) 21h

访问仪表盘

编辑 kubegems 插件,为 dashbnoard 组件开启 nodeport

kubectl -n kubegems edit plugins.plugins.kubegems.io kubegems

示例:

apiVersion: plugins.kubegems.io/v1beta1
kind: Plugin
metadata:
annotations:
plugins.kubegems.io/category: core/KubeGems
plugins.kubegems.io/description: KubeGems core service and dashboard.
plugins.kubegems.io/required: "true"
name: kubegems
namespace: kubegems-installer
spec:
chart: kubegems
installNamespace: kubegems
kind: helm
url: https://charts.kubegems.io/kubegems
values:
dashboard:
service:
type: NodePort

您可以通过如下用户名与密码登录控制台:

user: admin password: demo!@#admin

1.23 功能特性

应用可观测性

KubeGems 从 v1.23 开始引入 OpenTelemetry 作为其平台内部采集、分析应用信息的核心组件。并在平台内完成在多集群多租户多环境的场景下独立管理用户的监控大屏、日志采集和应用性能等数据。并通过云端统一的元数据进行跨数据维度的查询。

日志与链路跟踪

otel-java监控面板

应用性能

应用在使用OpenTelemety SDK上报 Tracing 数据同时也由平台内置组件进行分析后形成应用统性能统计,便于用户实时掌控平台内应用运行状态

应用性能分析

trace 数据

云拨测

云拨测为 KubeGems 用户提供开箱即用的主动拨测式应用监测解决方案,利用集群内(即将支持集群外)的 blackbox-exporter,对目标应用进行性能管理(HTTP)和网络性能监控(ICMP、TCP),先于终端用户挖掘故障隐患,助力KubeGems用户提升自身应用产品的用户体验。

云拨测 HTTP

海量指标和告警渠道

监控指标依托 Prometheus 实现对平台内容器、中间件、Otel 等服务指标的快速查询与过滤,通过内置模版节省用户大量时间。

监控查询模版

通过创建告警通道,平台内用户可以制定针对特定监控对象的报警规则。当规则被触发时,系统会以您指定的报警方式向告警渠道中指定的接受者,以提醒您采取必要的问题解决措施。

告警渠道

Model Zoo

随着近年来人工智能产业的兴起,越来越多的 AI 算法服务部署在 Kubernetes 之上。KubeGems 自 1.22版本引入了 ModelX(https://github.com/kubegems/modelx)后,同时支持了在线和离线 Model Zoo 的接入

modelx 离线模型仓库

ModelX 一个轻量、高性能、可扩展的 AI 模型 ML/DL服务

KubeGems 1.23 版本迎来重大的功能更新,支持算法的实时预览。

算法应用中心

AI算法预览

边缘计算

KubeGems 自 v1.23 版本开始,产品联合了 Rancher k3s 开启对边缘集群的管理。通过 Grpc Tunnel 技术实现了边缘 k3s 集群自主连接和上报心跳到云端。

kubegems 边缘架构

  • Edge Agent 由 Golang 开发,支持在 x86 和 arm 架构下运行。主要代理K3S API 服务以及与云端 Hub 服务建立 GRPC 隧道,并定时上报隧道和设备心跳数据

  • Edge Hub 管理边缘设备上连云端的隧道管理,并提供设备认证支持(计划中)

  • Edge Server KubeGems Edge 资源管理(CRD) 与 Edge API 服务

  • Edge Task (计划中) KubeGems Task 边缘设备任务调度服务,包含应用发布,(发送设备指令)等功能

边缘集群列表

边缘集群

后续发展

KubeGems 当前仍然在高速的持续迭代,后续我们的规划也主要围绕 可观测性云原生生态AIOps途径发展。您可以访问我们托管在 GitHub 上的 Project (https://github.com/orgs/kubegems/projects/9)来了解KubeGems最新动态,同时也欢迎任何人为项目提供有价值的议题!

加入社区

· One min read
yud

随着众多model zoo的出现,对于我们这样不懂得高深的数学基础知识的小白来说,能体验众多业界大牛开发的模型也不再是一个遥不可及的事情了。现在唯一的成本可能就是要熟悉各种开发框架,如 Transformers,OpenMMLab 等。KubeGems 在1.23版本中加入了模型商店的功能,其主要目的就是为了让开发者快速部署和体验这些优秀的模型,当前KubeGems主要对接Huggingface 和 OpenMMLab 两个model zoo,后续我们还将不断集成其他优秀的model zoo。本文将以HuggingFace为例,简单介绍如何在KubeGems上快速体验一个视觉问答的模型任务,以及一些实现背后的技术细节。

KubeGems模型商店

KubeGems 模型商店目前的设计目的是基于它来托管和集成第三方模型和自有模型;对于自有模型,我们通过modelx项目来存储其模型数据。同时在某些私有化场景下,我们也可以基于modex来导入私有化部署所需的模型。对于第三方的模型,通常我们仅仅存储其模型元数据(模型名字,模型数据的url地址等),但不会储存其模型数据本身,KubeGems 模型商店提供了一个“模型同步器",它实际上是一个简单的 spider,会将HuggingFace的模型列表和其他任务相关信息记录下来,以便KubeGems用户可以在KubeGems中筛选和检索。当然modelx 也是可以存储第三方的模型的,例如我们要将一个优秀的开源模型部署到私有化环境下的时候,也可以将第三方的模型数据导入到modex中。

modelx 是一个基于 OCI 的简单、高性能、可扩展的 ML/DL 模型存储库。

Seldon 项目

出于快速部署任意模型的目的,我们需要一套方案来快速集成主流的模型开发框架,同时还能为模型部署提供一些额外的监控数据,经过一些筛选,我们采用的是SeldonIO这个项目。seldon-core 是一个用于打包、部署、监控和管理数千个生产机器学习模型的 MLOps 框架,它主要支持两个类型的推理组件 TritonMLServer。Triton 推理服务器是NVIDIA AI平台的一部分,是一款开源推理服务软件,可帮助标准化模型部署和执行,并在生产中提供快速且可扩展的AI服务。MLServer 是机器学习模型的推理服务器,包括对多个框架、多模型服务等的支持,同时也很容易基于它来开发一个自定义的推理运行时,它由Seldon开发,当前KubeGems集成HuggingFace 就是基于 MLServer 项目。有关更多SeldonIO 和 MLServer的信息可以在其官网找到,所以这儿就不再赘述了。

一点使用心得,seldon-core 的 operator 处理 SeldonDeployment 的时候,是通过硬编码编排内容的方式来提交Resource到 Kubernetes API Server,灵活度还是有点欠缺,例如我们想调整Deployment的updateStrategy的时候,就无从下手。单从部署的角度来看,KubeVela 使用模版的办法才是一个更优雅的办法。瑕不掩瑜,这点小问题不影响它是个不错的项目。

MLServer 项目

MLServer 旨在提供一种简单的方法来通过 REST 和 gRPC 接口开始为机器学习模型提供服务,它实现了 KFServing 的 Predict Protocol V2 协议规范。

本文中,Predict Protocol V2 我们简称其为“V2协议"。 V2 推理协议的目的是提供一种标准化协议来与不同的推理服务器(例如 MLServer、Triton 等)和编排框架(例如 Seldon Core、KServe 等)进行通信。 V2 推理协议的规范定义了 REST 和 gRPC 接口和请求负载数据的格式。

MLServer 核心由两个部分组成,他们分别是编解码器(codecs)运行时(runtime)

编解码器

MLServer 的编解码器负责将用户的输入转换成兼容 Predict Protocol V2 协议的数据格式,同时也将模型的推理输出按照V2协议进行编码返回。MLServer 实现了对V2协议的数据编码的支持,同时允许用户开发和注册自定义的编解码器。

由于V2协议是一个面向推理服务的协议,并没有对媒体类型进行支持,所以对于要求直接将图片或者音频作为输入的任务来说,就需要开发自定义的编解码器。

其中编解码器又分成两个类型,分别是RequestCodecInputCodec

RequestCodec 会作为整个请求的默认编码器,当payload的input字段中没有提供Content-Type的时候,就会使用默认编码器编解码,InputCodec 则是真正执行编解码的编解码,每个RequestCodec应该有一个默认的InputCodec,当payload的input字段中提供了Content-Type的时候,则会使用指定Content-Type的编解码器对数据进行编解码。

例子:

{
"parameters": {
"content_type": "pd"
},
"inputs": [
{
"name": "First Name",
"datatype": "BYTES",
"parameters": {
"content_type": "str"
},
"shape": [2],
"data": ["Joanne", "Michael"]
},
{
"name": "Age",
"datatype": "INT32",
"shape": [2],
"data": [34, 22]
},
]
}



以MLServer在解码这个请求输入的时候,默认使用PandasCodec对数据decode,返回一个pandas.DataFrame,但是 inputs[1] 指定了Content-Type 为str,那么这个字段将被编码成字符串。以上数据会decode为一个python字典

{
"First Name": ["Joanne", "Michale"],
"Age": pandas.DataFrame([34, 22]),
}

运行时

运行时是MLServer实现推理的核心模块,当前MLServer内置了八个推理运行时,每个推理运行时可以注册自己专有的Content-type对应的InputCodec和RequestCodec。目前已经支持的推理框架如下:

FrameworkPackage NameImplementation ClassExampleDocumentation
Scikit-Learnmlserver-sklearnmlserver_sklearn.SKLearnModelScikit-Learn exampleMLServer SKLearn
XGBoostmlserver-xgboostmlserver_xgboost.XGBoostModelXGBoost exampleMLServer XGBoost
HuggingFacemlserver-huggingfacemlserver_huggingface.HuggingFaceRuntimeHuggingFace exampleMLServer HuggingFace
Spark MLlibmlserver-mllibmlserver_mllib.MLlibModelComing SoonMLServer MLlib
LightGBMmlserver-lightgbmmlserver_lightgbm.LightGBMModelLightGBM exampleMLServer LightGBM
Tempotempotempo.mlserver.InferenceRuntimeTempo examplegithub.com/SeldonIO/tempo
MLflowmlserver-mlflowmlserver_mlflow.MLflowRuntimeMLflow exampleMLServer MLflow
Alibi-Detectmlserver-alibi-detectmlserver_alibi_detect.AlibiDetectRuntimeAlibi-detect exampleMLServer Alibi-Detect
Alibi-Explainmlserver-alibi-explainmlserver_alibi_explain.AlibiExplainRuntimeComing SoonMLServer Alibi-Explain

此外,想要开发一个自定义的推理运行时也十分容易,仅需要继承 mlserver.MLModel, 然后实现 load predict方法即可,常见的编码器MLServer已经提供,大部分可以直接复用,如果有特殊的媒体类型,开发一个自己的Codec也十分简单。这儿以transformers库为例,其推理运行时核心代码可以简化如下:

class MyCustomRuntime(MLModel):
# 加载模型
def load(self) -> bool:
settings = get_settings_from_env()
pp = pipeline(**settings)
self._model = pp
# 执行推理
def predict(self, *args, **kwargs):
prediction = self._model(*args, **kwargs)
return self.serialize(prediction)

在load方法中通过transformers库的pipeline来加载模型,在predict方法中使用模型来执行推理,返回被编码器encode之后的推理结果。

KubeGems 的 OpenMMLab 模型推理运行时就是一个自定义的推理运行时,但是尚未支持完所有任务类型。

部署体验

我们经将HuggingFace的相关元数据存放在了KubeGems模型商店中,快速部署一个模型已经十分方便。用户可以在KubeGems模型商店内根据任务类型找到感兴趣的模型,快速部署到自己的环境中。一图胜千言,可以看接下来这两个例子。

运行 HuggingFace 图片语义分割任务(Image Segmentation)

1673932616990

运行HuggingFace 视觉问答任务 (Visual Question Answering)

1673933034799

一些限制和问题

  1. HuggingFace 并非所有模型都能直接下载,部分模型是需要授权的,这类模型在部署的时候需要提供一个被授权用户的Token,KubeGems仅帮助快速部署和体验模型,使用相关模型的时候还是休要遵守HuggingFace的一些协议,许可,策略等。
  2. HuggingFace 的模型文件虽然放在了CDN上,但是中国大陆访问的时候,还是会出现下载非常缓慢的情况,特别是十几G以上的大模型。当然,在真实部署的时候,可以通过NFS共享模型卷的方式实现缓存加速,或者实现自己的缓存加速方案,这取决于部署的基础设施情况了,KubeGems 研发团队内部已经完成了一套缓存加速管理方案(这部分并未开源)。
  3. MLServer 当前并没有提供像openapi schema这样的东西来直接生成接口描述文件,由于其主要支持 Predict Protocol V2 协议,用户只能通过v2协议的metadata来了解输入和输出,这点对于非算法相关背景的同学来说不是很友好。所以我们也在调研其他支持媒体类型的适配器,(如 讯飞的 https://github.com/iflytek/aiges),以尽可能降低使用这些优秀的模型的学习成本和接入的开发成本。

· One min read
cnfatal

为什么要自己设计模型仓库

最近需要寻找一种更友好的方式来存储我们的模型。 我们曾经在使用 ormb 时遇见了问题,由于我们的模型有的非常大(数十 GB),在使用 ormb 时将会面临:

  1. ormb push 时,harbor 报错。原因是 harbor 内存超出限制以及 harbor 接入的 s3 有单文件上传大小限制。
  2. 每当模型有变动时(即使变动很小),都会重新生成全量的镜像层,在部署时都需要重新拉取数十 GB 的文件。
  3. 其他相关问题。

此外,我们还正在开发新的KubeGems算法商店,需要一个能够和算法商店对接的模型仓库服务,以便于仓库中的模型能够在算法商店中更好的展示。

寻找替代

为此,需要寻找新的替代方案。需要能够满足:

  • 支持模型readme预览,能查看模型文件列表。
  • 可以版本化管理并支持增量。

使用 GIT LFS:

Huggingface 使用了 git + lfs 模型进行模型托管,将小文件以及代码使用 git 进行版本管理,将模型或其他大文件存放至 git lfs。

这种方式增加了一个 git 服务,对于大流量时,git 服务会成为瓶颈。 对于私有化部署,需要引入一个 git server 和对 git servre 的多租户配置,对于我们来说确实不够精简。 而且使用 git 方式,模型数据会在两个地方存放,增加维护成本。

使用对象存储:

一众云厂商的解决方案是将模型存储到了对象存储,华为 ModelArts ,百度 BML,阿里 PAI,腾讯 TI-ONE,Amazon SageMaker 等。 对于公有云来说,提供 ML 解决方案同时将数据都放在对象存储中是最好的方式。

但在私有云中,虽然也用对象存储,但我们没有 ML 的配套方案。若让用户将模型直接存储在对象存储中,将难以进行版本控制。 我们需要提供一套管理机制,放在用户和对象存储之间。

使用OCI registry:

借鉴 ormb 的方式,可以将模型存储在 OCI registry 中。为了解决之前遇到的问题,需要对现有 ormb 的打包流程进行优化,将 big layer 进行拆分,增加文件复用。 这又带来新问题,对于使用者来说,必须要使用修改后的 ormb 才能正常下载模型。

改都改了不如彻底一点,基于 OCI 编写一个新的程序来帮助我们解决这个问题。对,就这么干!

从零开始

以 OCI 作为服务端,OCI 服务端后端存储对接到 S3。OCI 仓库不使用 harbor,选择了更轻量的 docker registry v2。 将模型使用合适的方法分层然后 push 到 OCI 仓库,下载时再将模型拉下来合并还原。

非常好,我们的数据经过了 本地->OCI->S3 并存储起来了。但是,如果流量再大一点呢,OCI 或许是新的瓶颈。 那能不能 本地->S3 呢?这样岂不是又快又好了。

上面说到在直接使用对象存储时我们面临的问题为难以进行版本控制,且 s3 的 key 需要分发到客户端,更难以进行权限控制。

这里借鉴 git lfs 提供的思路,将文件直接从 git 直接上传到 git lfs server,而 git server 仅做了协调。

于是一个新的结构产生了:

这个协调者负责沟通用户和 S3,并包含了鉴权等,核心流程为:

  1. 用户本地将模型合理打包成多个文件,并计算文件的 hash 准备上传。
  2. 检查该 hash 的文件是否存在,若存在即结束,不做操作。
  3. 若不存在则 modelx 返回一个临时 url,客户端向该 url 上传。
  4. 上传完成后通告 modelx。

这和 git lfs 非常像, git lfs 也有基于 S3 的实现, 但是我们不需要引入一个完整的 git server(带来了额外的复杂度)。

好了,总体思路确定后,开始完善每个流程的细节,这个新的东西称为 modelx

modelx

数据存储

先解决如何存储数据,先看存储部分 server 端接口:

参考 OCI 我们 server 端仅包含三种核心对象:

namedescription
index全局索引,用于寻找所有 manifest
manifest版本描述文件,记录版本包含的 blob 文件
blob数据文件,实际存储数据的类型

所以一个 manifest 示例为:

schemaVersion:
mediaType:
config:
- name: modelx.yaml
digest: sha256:xxxxxxxx...
mediaType: "?"
size: 45
blobs:
- name: file-a.bin
digest: sha256:xxxxxxxx...
mediaType: ""
size: 18723
- name: file-b.bin
digest: sha256:xxxxxxxx...
mediaType: "?"
size: 10086
annotaitons:
description: "some description"

服务端接口:

methodpathdescription
GET/获取全局索引
GET/{repository}/{name}/index获取索引
GET/{repository}/{name}/manifests/{tag}获取特定版本描述文件
DELETE/{repository}/{name}/manifests/{tag}删除特定版本描述文件
HEAD/{repository}/{name}/blobs/{digest}判断数据文件是否存在
GET/{repository}/{name}/blobs/{digest}获取特定版本数据文件
PUT/{repository}/{name}/blobs/{digest}上传特定版本数据文件

似曾相识,对,这和 OCI registry 的接口非常像,但是更简单了。

上传流程:

  1. 客户端准备本地文件,对每个需要上传的 blob 文件,计算 sha256。生成 manifest。
  2. 客户端对每个 blob 文件执行:
    1. 检查服务端是否存在对应 hash 的 blob 文件,如果存在,则跳过。
    2. 否则开始上传,服务端存储 blob 文件。服务端可能存在重定向时遵循重定向。
  3. 在每个 blob 均上传完成后,客户端上传 manifest 文件
  4. 服务端解析 manifest 文件,更新 index。

这里有一个隐形约定:客户端在上传 manifest 之前,确保已经上传了所有 blob。 上传 manifest 意味着客户端承诺上传了 manifest 中所有的部分,以便于其他客户端可以发现并能够下载完整的数据。

下载流程:

  1. 客户端向服务端查询 index 文件,获取 manifest 文件的地址。
  2. 客户端向服务端获取 manifest 文件,并解析 manifest 文件,获取每个 blob 文件的地址。
  3. 客户端对每个 blob 文件执行:
    1. 检查本地文件是否存在,如果存在,判断 hash 是否相等,若相等则认为本地文件于远端相同无需更新。
    2. 若不存在或者 hash 不同,则下载该文件覆盖本地文件。

我们实现了一个简单的文件服务器,这对我们来说已经可以用了。

负载分离

这就是一个简单的文件服务器,数据还是流过了 modelx, 那如何实现直接本地直接上传到 S3 流程呢?

这里借助了 302 状态码,当客户端上传 blob 时,可能收到 302 响应, 此时 Location Header 会包含重定向的 URI,客户端需要重新将 blob 上传至该地址。下载时也使用相同逻辑。

在使用S3作为存储后端时,我们使用到了s3 presign urls,能够对特定object生成临时 url 来上传和下载,这非常关键。

为了支持非 http 协议存储,客户端需要在收到 302 响应后根据具体地址使用不同的方式处理上传。

  • 对于 http ,会收到以 http(s):// 开头的重定向地址,此时客户端继续使用 http 协议上传至该地址。
  • 对于 S3,可能收到以 s3:// 开头的 presign 的 S3 地址,则此时则需要客户端转为使用 s3 client 上传 blob 到该地址。
  • 此外,服务端还可以响应其他协议的地址,客户端可以自行实现并扩展到其他存储协议。

这基本上是一个简单高效的,可索引的,版本化的文件存储服务。不仅可以用于存储模型,甚至可以推广到存储镜像,charts 等。

为什么不用OCI?

我们在研究了OCI destribution 的协议后,发现OCI协议在上传接口上无法做到能够让客户端直接与存储服务器交互。 总是需要在最终的存储服务器前增加一个适应层。 详细的可以参看 OCI Distribution Specification

OCI 中无法获得模型文件列表,从而无法仅下载指定文件。

模型存储

在已有的服务端实现中,可以看到 modelx 服务端仅负责文件存储,对于 manifest 中实际包含哪些 blob,还是由客户端决定。

我们的最终目的是用于存储模型,面临的模型可能有超大单文件以及海量小文件的场景。 除了解决如何将模型存储起来,还需要解决如何管理多个模型版本,模型下载(增量下载)。

在上一节的 manifest 中,每一个 blob 都包含了 mediaType 字段,以表示该文件的类型。可以从这里进行扩展。

  • 对于单个大文件,可以不用特殊处理,客户端会在上传和下载时使用 s3 client 分块处理。
  • 对于海量小文件,选择在客户端将小文件打包压缩为单文件,设置特别的 mediaType 进行上传;在下载时,对特别的 mediaType 进行解包还原。
  • 对于增量,类似于OCI image,客户端会在本地计算更改的文件,客户端仅用上传改变的文件。在下载时,客户端也会仅下载与远程对比 hash 不同的文件。
  • 对于部署,部署时可能仅需要下载某一个文件,则可以借助 modelx.yaml,在其中指定仅需要在部署时下载的模型文件。 同时 modelx.yaml 还包含了 model-serving 时所需要的一些信息。

一个 modelx.yaml 示例(非最终版):

config:
inputs: {}
outputs: {}
description: Awesome text generator
framework: pytorch
maintainers:
- maintainer
tags:
- modelx
- demo
task: text-generation
modelFiles:
- torch.bin
- tf_output.h5

认证授权

有了上面的实现,认证可以插入到服务端几个接口中,对请求进行拦截。

目前modelx实现了基于 OIDC 的认证方案,modelx 仅需要填写一个外部 OIDC Identity Provider 地址即可使用。

总结

最终来说,我们实现了一个简单的、 高性能的、可扩展的模型存储服务。 结合了 OCI、git-lfs 和 对象存储的优势,并解决了我们在模型管理遇到的问题。 未来 modelx 依旧还有许多事情要做,欢迎大家参与到 modelx 以及 kubegems 社区中来。

如果你想快速体验一下 modelx 可以看看Setup.

· One min read
LinkMaq

Nacos 介绍

Nacos 是阿里云开源一款在微服务场景下用于处理应用配置发布管理和服务注册管理的服务平台。其主要提供了如下几个特性:

  • 服务发现和服务健康监测

基于 DNS 和 RPC 的服务发现。服务提供者使用 原生SDKOpenAPI、或一个独立的Agent TODO注册 Service 后,服务消费者可以使用DNS TODOHTTP&API查找和发现服务。

  • 动态配置服务

动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。

  • 动态 DNS 服务

动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务。

  • 服务及其元数据管理

Nacos 从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。

KubeGems 中的 Nacos

KubeGems 自v1.21版本之后开启了对 Nacos 配置中心的支持,并利用了内置 Plugins CRD 实现了对 Nacos 的快速启动。

KubeGems 中的 Nacos 安装源来至官方社区提供https://github.com/nacos-group/nacos-k8s,并在 plugin crd 中来管理部署的版本。用过 Nacos 的同学可能知道,其内部的数据模型主要围绕dataidgroupnamespace这 3 个进行操作。由于 KubeGems 的设计是一个支持多租户的平台,所以在应用 nacos 数据模型时,按照了 tenant + project来区分内部的命名空间。

启用和配置插件

KubeGems 启用 Nacos 需要具备系统管理员的权限进行操作。管理员进入管理后台的“插件管理”,点击“启用“按钮“即可开启Nacos。

直到出现如下状态,代表插件运行正常

此时,我们就可以在租户的环境中开始使用 Nacos 服务

个性化配置

Nacos插件的配置以 CRD 的形式存放在 nacos命名空间中,我们可以通过命令kubectl edit plugin nacos -n nacos对插件进行个性化配置。

apiVersion: plugins.kubegems.io/v1beta1
kind: Plugin
metadata:
finalizers:
- plugins.kubegems.io/finalizer
generation: 1
name: nacos
namespace: nacos
spec:
kind: helm
path: helm
url: https://github.com/nacos-group/nacos-k8s.git
values:
namespace: nacos
global:
mode: cluster
nacos:
replicaCount: 1
image:
repository: registry.cn-beijing.aliyuncs.com/kubegems/nacos-server
tag: v2.1.1
plugin:
image:
repository: registry.cn-beijing.aliyuncs.com/kubegems/nacos-peer-finder-plugin
persistence:
data:
storageClassName: local-path
enabled: true
service:
type: ClusterIP
version: master

提示:KubeGems的插件 CRD 由 https://github.com/kubegems/bundle-controller提供支持,我们也可以直接使用 bundle-controller 在非 kubegems 集群中管理插件。

集群部署

Nacos集群由社区提供支持部署,kubegems 默认将 nacos 的全局运行模式设置为“cluster”,如果您需要扩展成多集群,只需修改replicaCount的副本数为 3 即可。

开放集群外访问

Nacos 插件默认运行在 Kubernetes 内部,如果需要在集群外访问 Nacos 需借助网关实现。管理员可以在后台创建一条基于默认网关的 ingress 来代理 nacos api。过程如下:

第一步: 进入路由功能页面,选择 nacos 命名空间

第二步:创建并提交一条路由规则,用于 nacos 的代理

第三步:获取访问地址

提示:Nacos2.0版本相比1.X新增了gRPC的通信方式,因此需要增加2个端口。新增端口是在配置的主端口(server.port)基础上,进行一定偏移量自动生成

使用配置中心

进入应用环境下的“应用配置”,可以点击右上角的“获取访问信息”查看当前环境下的 nacos sdk 所需的配置信息

配置管理

点击“创建配置项”就可以创建配置

配置历史与回滚

监听列表

运行测试

我们用 nacos-sdk-go/v1来做一个简单的认证

package main

import (
"fmt"
"time"

"github.com/nacos-group/nacos-sdk-go/clients"
"github.com/nacos-group/nacos-sdk-go/common/constant"
"github.com/nacos-group/nacos-sdk-go/vo"
)

func main() {
sc := []constant.ServerConfig{
{
IpAddr: "nacos.kubegems.io",
Port: 31956,
},
}

cc := constant.ClientConfig{
NamespaceId: "69f7325702bc396a8773f9a0a94eea310b21ec39", //namespace id
TimeoutMs: 5000,
NotLoadCacheAtStart: true,
LogDir: "/tmp/nacos/log",
CacheDir: "/tmp/nacos/cache",
LogLevel: "debug",
}
client, err := clients.NewConfigClient(
vo.NacosClientParam{
ClientConfig: &cc,
ServerConfigs: sc,
},
)

if err != nil {
panic(err)
}

content, err := client.GetConfig(vo.ConfigParam{
DataId: "test",
Group: "e3",
})
fmt.Println("GetConfig,config :" + content)

err = client.ListenConfig(vo.ConfigParam{
DataId: "test",
Group: "e3",
OnChange: func(namespace, group, dataId, data string) {
fmt.Println("config changed group:" + group + ", dataId:" + dataId + ", content:" + data)
},
})
time.Sleep(300 * time.Second)
}

以下是运行情况

总结

本文主要介绍了在 KubeGems 中启用并使用Nacos插件作为应用的配置中心的基本管理功能。Nacos 是一个非常棒的应用配置管理平台,KubeGems 团队将持续关注此项目,并为用户在 Kubernetes 集群提供更友好的支持。

· One min read
LinkMaq

Kind是Kubernetes In Docker的缩写,通过使用 Docker ,它能快速的拉起一套 Kubernetes 服务。因此它在Kubernetes功能测试和二开等领域被广泛使用。

KubeGems是一款以围绕 Kubernetes 通过自研和集成云原生项目而构建的通用性开源 PaaS 云管理平台。经过我们内部近一年的持续迭代,当前 KubeGems 的核心功能已经初步具备多云多租户场景下的统一管理。并通过插件化的方式,在用户界面中灵活控制包括 监控系统日志系统微服务治理 等众多插件的启用和关闭。

本文将指导用户使用 Kind 快速部署一个 KubeGems v1.21的版本用于本地测试。

安装 Kind

在 Linux 上

curl -Lo ./kind https://github.com/kubegems/kind/releases/download/v0.15.0-alpha-kubegems/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind

在 MacOS 上

# for Intel Macs
[ $(uname -m) = x86_64 ]&& curl -Lo ./kind https://github.com/kubegems/kind/releases/download/v0.15.0-alpha-kubegems/kind-darwin-amd64
# for M1 / ARM Macs
[ $(uname -m) = arm64 ] && curl -Lo ./kind https://github.com/kubegems/kind/releases/download/v0.15.0-alpha-kubegems/kind-darwin-arm64
chmod +x ./kind
mv ./kind /some-dir-in-your-PATH/kind

在 Windows 上

curl.exe -Lo kind-windows-amd64.exe ./kind https://github.com/kubegems/kind/releases/download/v0.15.0-alpha-kubegems/kind-windows-amd64
Move-Item .\kind-windows-amd64.exe c:\some-dir-in-your-PATH\kind.exe

创建服务

Single Cluster

和创建 Kubernetes 集群一样,使用命令kind create cluster就能快速拉起一个 Kubernetes 服务并部署 KubeGems image-20220805141017484.png

由于不需要定制kindest/node镜像,所以 KubeGems安装全程需要连接公网下载所需的镜像。在启动完成之前会有许多 Pod 的状态为 CrashLoopBackOff,这是由于其依赖的服务(mysql、redis、gitea、argocd 等)还在启动中,这是正常的,请耐心等待。

kubegems 所有服务部署并启动完成后会有如下 pod

image-20220805141327441.png

当容器状态全部Running后,使用 port-forward 将 KubeGems Dashboard 服务映射到本地

kubectl port-forward svc/kubegems-dashboard :80 -n kubegems                           

Forwarding from 127.0.0.1:52302 -> 8000
Forwarding from [::1]:52302 -> 8000

此时,我们打开浏览器访问 http://localhost:52302即可访问 KubeGems,默认用户admin 默认密码demo!@#admin

使用 Kind 生成的 KubeConfig文件导入集群是,注意修改集群 Server 地址为内部地址http://kubernetes.default:443

image-20220805144837887.png

Mutil Cluster

如果您需要使用 Kind 部署一个 Kubernetes 集群,那么可以按照如下配置

cat ./kind.yaml

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: control-plane
- role: control-plane
- role: worker
- role: worker
- role: worker

并通过命令 kind create cluster --config kind.yaml

image-20220805143833694.png

打开 KubeGems 后台并导入集群后,我们便可以在机器列表中查看集群内主机数量

image-20220805145229274.png

指定 Kubernetes版本创建 KubeGems

如果您要在指定的 Kubernetes 版本中创建 KubeGems,只需要kind 在创建过程中指定kindest/node镜像版本即可

kind create cluster  --image kindest/node:v1.23.6

启用插件

默认情况下 KubeGems 只做了最小化安装,如果您要启用更多功能,可在管理员后台的组件管理中启用相关插件

image-20220805145350662.png


· One min read
liutao

Apache Skywalking 专门为微服务架构和云原生架构系统而设计并且支持分布式链路追踪的APM系统。Apache Skywalking 通过加载探针的方式收集应用调用链路信息,并对采集的调用链路信息进行分析,生成应用间关系和服务间关系以及服务指标。Apache Skywalking 目前支持多种语言,其中包括 Java.Net CoreNode.jsGo 语言。本文将从以 KubeGems 应用商店出发,来快速搭建一套Skywalking,希望能够帮助到大家。

安装SkyWalking OAP

KubeGems应用商店(HelmChart)是一个描述Kubernetes相关资源的文件集合,单个应用可以用来部署某些复杂的HTTP服务器以及Web全栈应用、数据库、缓存等

  1. Elasticsearch安装

在KubeGems应用商店中找到Elasticsearch

选择部署7.13.2版本,填写必要的【项目】、【环境】等信息

img

为方便演示,Master、ES副本数都配置为1,可根据实际需要配置参数,还可以修改 Values 中的配置

SkyWalking 初始化 ElasticSearch index 的是默认规则是 1 副本 1 分片,实际在使用中ElasticSearch 的实例数最好大于 2 个

img

点击部署,ES服务搭建完成。

img

  1. SkyWalking安装

同样在应用商店找到skywalking应用,填写基本信息,进入详细配置页,将数据设置为ES应用名称与端口

img

点击部署,可以看到skywalking-oap、skywalking-ui服务已经部署完成

img

在KubeGems控制台,找到容器服务-->运行时-->路由,创建路由,将skywalking-ui服务地址进行域名映射。我这里直接采用随机域名,用户可以根据自己公司内的域名手动配置。

img

在浏览器中打开路由访问地址,已经能正常看skywalking-ui的页面了

img

img

skywalking服务搭建完成啦,是不是非常的快速方便,哈哈哈哈哈😄

SkyWalking Agent

所谓Agent是指SkyWalking从各个平台(Java Python等)收集监控数据的代理,此处我们以为Java应用为例,收集Java应用产生的各种监控数据

SkyWalking的数据采集主要是通过业务探针(Agent)来实现的,针对不同的编程语言SkyWalking提供了对应的Agent实现。Java微服务接入SkyWalking可以使用“SkyWalking Java Agent”来上报监控数据。

这就需要Java微服务在部署启动的过程中需要获取 SkyWalking Java Agent 探针包,并在启动参数中通过--javaagent:xxx进行参数指定。而具体的集成方式大致有以下四种:

  • 使用官方提供的基础镜像;

  • 将agent包构建到已存在的基础镜像中;

  • 将agent包放到共享volume中;

  • 通过sidecar 模式挂载agent;

其中前两种方式主要是通过在构建Docker镜像的过程中将Agent依赖打包集成到Java服务的Docker镜像中,而sidecar模式则是利用k8s的相关特性来实现在容器启动时挂载Agent相关依赖。

为什么选择sidecar

Sidecard主要原理是通过Kubernetes的初始化容器initContainers来实现的,initContainers是一种专用容器,它应用容器启动之前运行,可以用于完成应用启动前的必要初始化工作。如果微服务是直接部署在 Kubernetes 集群,那么采用 sidecar 模式来使用 SkyWalking Agent会更加方便,因为这种方式不需要修改原来的基础镜像,也不需要重新构建新的服务镜像,而是会以sidecar模式,通过共享的volume将agent所需的相关文件直接挂载到已经存在的服务镜像中。

初始化容器InitContainers

InitContainers 就是用来做初始化工作的容器,可以是一个或者多个,如果有多个的话,这些容器会按定义的顺序依次执行,只有所有的initContainers执行完后,主容器才会被启动。我们知道一个Pod里面的所有容器是共享数据卷和网络命名空间的,所以initContainers里面产生的数据可以被主容器使用到的

自定义SkyWalking Agent镜像

  • 下载SkyWalking官方发行包,并解压到指定目录
#下载skywalking-8.6.0 for es7版本的发布包,与部署的skywalking后端版本一致
$ wget https://archive.apache.org/dist/skywalking/8.6.0/apache-skywalking-apm-8.6.0.tar.gz

#将下载的发布包解压到当前目录
$ tar -zxvf apache-skywalking-apm-es7-8.6.0.tar.gz
  • 修改配置,编辑config/agent.config文件,以下只列出部分配置项
# The agent namespace
# agent.namespace=${SW_AGENT_NAMESPACE:default-namespace}
# 表示提供相同功能/逻辑的逻辑组的服务名称
agent.service_name=${SW_AGENT_NAME:Your_ApplicationName}

# OAP服务地址,修改默认地址为skywalking-oap服务名
collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:skywalking-oap:11800}
# 日志文件名
logging.file_name=${SW_LOGGING_FILE_NAME:skywalking-api.log}
# 日志级别:TRACE、DEBUG、INFO、WARN、ERROR、OFF。
logging.level=${SW_LOGGING_LEVEL:INFO}
# 最大历史日志文件。发生翻转时,如果日志文件超过此数量,则最旧的文件将被删除。默认情况下,负数或零表示关闭。
logging.max_history_files=${SW_LOGGING_MAX_HISTORY_FILES:10}
# 挂载插件的特定文件夹。安装文件夹中的插件可以工作。
plugin.mount=${SW_MOUNT_FOLDERS:plugins,activations,bootstrap-plugins}
# 排除插件目录中定义的一些插件
# plugin.exclude_plugins=${SW_EXCLUDE_PLUGINS:}
  • 构建skywalking-agent sidecar 镜像并push至hub私有镜像仓库

在前面步骤中解压的skywalking发行包,进入agent目录编写Dockerfile文件

FROM busybox:latest
RUN set -eux && mkdir -p /usr/skywalking/agent/
COPY . /usr/skywalking/agent/
WORKDIR /
  • 完成Docker文件编写后,执行镜像构建命令:
# 构建镜像,注意最后一个.
docker build -t <your-registry>/skywalking-agent-sidecar:8.6.0 .
# 镜像推送至私有Harbor仓库
docker push <your-registry>/skywalking-agent-sidecar:8.6.0

sidecar挂载

在KubeGems中找到 【工作负载】,编辑更新工作负载进入到容器镜像页面

设置SW环境变量,编辑工作容器

  • SW_AGENT_NAME=\<YourApplicationName>
  • JAVA_TOOL_OPTIONS=-javaagent:/usr/skywalking/agent/skywalking-agent.jar

添加容器镜像,选择初始化容器将skywalking-agent-sidecard镜像进行挂载,并添加启动命令

sh -c /usr/skywalking/agent/* /skywalking/agent

img

挂载emptyDir卷

img

点击确定保存工作负载信息,自动重启后进入应用容器,可以看到agent目标已经加载到容器中了

root@pod-7bc77468ff-7b4xt:/# cd /usr/skywalking/agent/
root@pod-7bc77468ff-7b4xt:/usr/skywalking/agent# ll

total 17716
drwxrwxrwx 9 root root 194 May 19 08:14 ./
drwxr-xr-x 3 root root 27 May 19 08:14 ../
-rw-r--r-- 1 root root 114 May 19 08:14 Dockerfile
drwxr-xr-x 2 root root 4096 May 19 08:14 activations/
drwxr-xr-x 2 root root 85 May 19 08:14 bootstrap-plugins/
drwxr-xr-x 2 root root 64 May 19 08:14 config/
drwxr-xr-x 2 root root 32 May 19 08:14 logs/
drwxr-xr-x 2 root root 4096 May 19 08:14 optional-plugins/
drwxr-xr-x 2 root root 45 May 19 08:14 optional-reporter-plugins/
drwxr-xr-x 2 root root 4096 May 19 08:14 plugins/
-rw-r--r-- 1 root root 18121582 May 19 08:14 skywalking-agent.jar

此时打开skywalking-ui,已经可以看到监控数据了

img

SkyWalking动态配置

SkyWalking配置主要是通过application.yml操作系统环境变量设置的,其中一些还支持来自上游管理系统的动态设置。上游服务支持包括Zookeeper、Etcd、Consul、Apollo、Nacos、K8s-configmap等。

目前SkyWalking支持以下动态配置

配置说明
agent-analyzer.default.slowDBAccessThresholdreceiver-trace/default/slowDBAccessThreshold慢数据库语句的阈值,覆盖application.yml.
agent-analyzer.default.uninstrumentedGateways未检测的网关覆盖gateways.yml.
alarm.default.alarm-settings警报设置将覆盖alarm-settings.yml
core.default.apdexThresholdapdex 阈值设置,将覆盖service-apdex-threshold.yml.
core.default.endpoint-name-grouping端点名称分组设置,将覆盖endpoint-name-grouping.yml.
agent-analyzer.default.sampleRate跟踪采样,覆盖receiver-trace/default/sampleRate.application.yml
agent-analyzer.default.slowTraceSegmentThreshold设置这个关于延迟的阈值将使慢跟踪段在花费更多时间时被采样,即使采样机制被激活。默认值为-1,这意味着不会对慢速跟踪进行采样。单位,毫秒。的覆盖receiver-trace/default/slowTraceSegmentThresholdapplication.yml
configuration-discovery.default.agentConfigurationsConfigurationDiscovery 设置

k8s-configmap

很多应用在其初始化或运行期间要依赖一些配置信息。大多数时候, 存在要调整配置参数所设置的数值的需求。 ConfigMap 是 Kubernetes 用来向应用 Pod 中注入配置数据的方法。

本文介绍如何通过KubGems平台动态配置SkyWalking参数

  • KubGems工作台 --> 配置中心 --> 配置 --> 创建配置
  • 添加参数及告警规则配置

img

  • 编辑yaml,添加修改app、compoent标签值
kind: ConfigMap
apiVersion: v1
metadata:
name: skywalking-dynamic-config
namespace: default
creationTimestamp: '2022-05-16T11:14:23Z'
labels:
component: oap
data:
agent-analyzer.default.sampleRate: '7000'
agent-analyzer.default.slowDBAccessThreshold: default:250,mongodb:100
alarm.default.alarm-settings: >-
rules:
# Rule unique name, must be ended with `_rule`.
service_resp_time_rule:
metrics-name: service_resp_time
op: ">"
threshold: 2000
period: 10
count: 3
silence-period: 5
message: 最近3分钟内服务 {name} 的平均响应时间超过2秒
  • 修改skywalking-oap环境变量信息,使用k8s-configmap配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: skywalking-oap
labels:
app: skywalking
app.kubernetes.io/instance: skywalking
.........

env:

.....

- name: SW_CLUSTER_K8S_LABEL

value: app=skywalking,component=oap

- name: SW_CONFIGURATION

value: k8s-configmap

SW_CONFIGURATION= k8s-configmap,动态配置采用k8s-configmap SW_CLUSTER_K8S_LABEL= app=collector,release=skywalking,根据这个值自动选择合适的configmap

  • 重启skywalking-oap,配置生效

SkyWalking配置优化

OAP优化

skywalking写入ES的操作是使用了ES的批量写入接口,我们要做的是调整相关参数尽量降低ES索引的写入频率。 参数调整主要是针对skywalking的配置文件`application.yml,相关参数如下:

storage:
elasticsearch:
bulkActions: ${SW_STORAGE_ES_BULK_ACTIONS:4000} # Execute the bulk every 2000 requests
bulkSize: ${SW_STORAGE_ES_BULK_SIZE:40} # flush the bulk every 20mb
flushInterval: ${SW_STORAGE_ES_FLUSH_INTERVAL:30} # flush the bulk every 10 seconds whatever the number of requests
concurrentRequests: ${SW_STORAGE_ES_CONCURRENT_REQUESTS:4} # the number of concurrent requests
metadataQueryMaxSize: ${SW_STORAGE_ES_QUERY_MAX_SIZE:8000}

调整 bulkActions 默认2000次请求批量写入一次改到4000次;

  • bulkSize批量刷新从20M一次到40M一次;

  • flushInterval每10秒刷新一次堆改为每30秒刷新;

  • concurrentRequests查询的最大数量由5000改为8000。

过滤不需要监控的接口

  • 制作agent镜像时,将apm-trace-ignore-plugin-8.6.0.jar复制到\plugins下面

  • config目录下新建一个配置文件 apm-trace-ignore-plugin.config,文件内容为:

trace.ignore_path=${SW_AGENT_TRACE_IGNORE_PATH:/actuator,/actuator/**,/admin/**,/nacos/**}

设置采样率

在默认情况下,SkyWalking会采集所有追踪的数据。但是如果系统比较复杂,采集的端点比较多的时候,可能存储压力比较大,这个时候我们可以修改配置,只存储部分的调用链路信息。比如:50%。 设置采样率的时候并不会影响相关指标的计算。包括服务,服务实例,端点,拓扑图等相关指标的计算还是使用完整的数据计算的。

在k8s-configmap中,添加配置项,设置采样率为70%:

agent-analyzer.default.sampleRate: '7000'

总结

本文用于指导用于在 KubeGems 中快速部署并运行 SkyWalking服务,用户可在研发环境中快速启用此功能来验证 APM 相关功能。更多关于KubeGems 与 SkyWalking 的配置优化,我们会持续更新,敬请关注。

· One min read
LinkMaq

KubeGems Logging 服务主要面向平台内部以及平台内租户提供日志采集、解析、传输和存储等相关的能力。依靠 Logging Operator 对日志的配置和路由管理,实现平台的终端用户可以对应用运行期间的日志进行实时查询和分析。KubeGems 日志持久化采用 Grafana Loki 实现。

核心需求

多租户

KubeGems 是一个多租户平台,基于此场景。平台内部对于租户应用产生的日志应该具备独立的解析配置以及路由规则

系统鲁棒性

  • 高性能

    • 日志采集和转发性能至少需处理 10K line/sec
    • 支持采取日志限流策略
    • 日志延迟不得低于 5min
  • 可扩展

    • 架构支持灵活的水平扩展以提升整体日志吞吐量
    • 组件因满足无状态属性

可运维性

  • 可配置

    • 日志规则和路由的配置应 CRD 化,由 Operator 统一管理,并尽量做到配置简化。
    • 需支持常见的 json 解析字段增删改 等插件配置。
    • 应用日志应满足发送多种常见的数据管道或收集系统,诸如 kafka、elasticSearch、MongoDB 等。
  • 可视化

    • 日志规则应在 UI 中由用户组合装配置日志的解析与输出规则。
  • 监控与告警

    • 日志采集的状态统计,包含组件运行状态以及日志采集统计。
    • 需支持用户根据自定义日志片段进行设置告警规则。

需求边界

  • 对于应用日志没有输出到控制台(stdout)的场景,暂不纳入采集需求

可采取其他方式重定向内部日志到控制台,诸如s6-log

日志设计

Logging Operator

Logging Operator 是 BanzaiCloud 下开源的一个云原生场景下的日志采集方案。它在 2020 年 3 月的时候经过重构后的 v3 版本,底层凭借高效的 fluentbit 和插件丰富的 flunetd,Logging Operator几乎已经完美的适配了 kubernetes 模式下的日志采集场景。

在 KubeGems 1.20 的版本中,我们选择采用 Logging Operator 作为内部日志流传的核心框架。其主要原因如下:

  • 原生 Flow 和 Output 类资源作用域为 kubernetes 命名空间,这与 KubeGems 租户环境的资源独立性相谋和

  • 采用高性能的 fluentbit 作为日志采集客户端,fluentd 为日志聚合端。flunetd 在 logging 中通过 replicas 控制副本数,可根据吞吐量水平扩容

  • flunetd 支持的插件较为丰富,满足当前基本需求

Logging Operator 不足:

  • 核心资源 Flow 和 Output 交于用户配置较为困难,需要 KubeGems 将资源封装(也许兼容源对象)
  • 可观测性功能较弱
  • 日志 Match 部分功能较弱,无法通过直接匹配 workload 进行关联

KubeGems 日志整体架构

由 Logging Operator 负责日志组件的运行管理和配置管理,租户侧资源以 CR 的方式在所属的环境空间中管理。Operator 将 CR 渲染为 Fluentd 的配置文件,用于处理日志的过滤和转发规则。可观测部分,由 KubeGems Plugins 服务初始化 ServiceMonitor,抓取组件运行期间的状态。

KubeGems Logging

KubeGems 对 Logging Operator 的封装仅满足简单的两种模式的场景:

  • 精简模式

    开箱即用的日志采集模式,对于用户环境空间内的所有容器开启采集,并输出到 KubeGems 平台内置的 Loki 组件用于日志分析和告警等场景

  • 局部自定义模式

    面向希望通过配置局部容器采集,并需要对接外部日志分析系统的场景。则采用此方式,不过此时

除此之外,对于希望能够完全掌握平台内的日志路由的高端用户,KubeGems 只需兼容对 Logging Operator 的原始 CR 资源即可。

精简模式

对于通用场景下的容器控制台日志采集,KubeGems 采用精简模式配置规则,仅需在用户界面中支持 一键配置开启日志采集 功能。一键启用功能的实现主要分为两部分。

  1. KubeGems Installer 服务在对 kubernetes 集群启用 logging 插件时,将对 logging operator 以及关联的 clusteroutputs/containers-console资源进行初始化。

    默认的clusteroutput 资源定义了容器日志的输出路径是 Loki

  2. 用户创建默认的容器采集规则时,LabelSelector 为空,即匹配当前命名空间下的所有 Pod。

  3. Flow 中只启用 Prometheus 插件用于统计采集状态。

  4. Flow 中关联系统默认的 clusteroutputs/containers-console

即在精简模式下,KubeGems 只在租户空间的接口中传入如下参数:

POST  observe/log/<tenant_name>/flowlite?enabled=true&namespace=tenant
参数释意requiredType
enabled启用环境空间的日志采集功能TrueBoolean
namespace采集日志的目标命名空间TrueString

KubeGems 将 Flows 渲染为如下内容:

apiVersion: logging.banzaicloud.io/v1beta1
kind: Flow
metadata:
name: default
namespace: tenant
spec:
match:
- select: {}
filters:
- prometheus:
labels:
container: $.kubernetes.container_name
namespace: $.kubernetes.namespace_name
node: $.kubernetes.host
pod: $.kubernetes.pod_name
metrics:
- desc: Total number of log entries generated by either application containers
or system components
name: logging_entry_count
type: counter
globalOutputRefs:
- containers-console

局部自定义模式

用户如果需要按照应用日志需求,局部对环境空内应用进行日志的规则和路由时,KubeGems 需要对 Logging Operator 的 CR 资源进行优化,以方面在用户界面中实现跟友好的交互。其中首先需要处理平台 应用元数据 相关的事务。默认情况下 Flow 的规则采用 labelSelector 对命名空间内资源做匹配,如下:

apiVersion: logging.banzaicloud.io/v1beta1
kind: Flow
metadata:
name: default
namespace: tenant
spec:
localOutputRefs:
- defalt
match:
- select:
labels:
app: nginx

虽然通过 labelSelector可以灵活控制日志采集规则,但经过实际验证,这个逻辑仍然存在 反直觉的场景,用户大多需要的是在 Selector 阶段与应用资源直接关联 ,当然我们不能直接把label 与 workload 做等同映射。我们需要通过外部方式来对 Label 做通用性匹配。

KubeGems CommonLabels

KubeGems 通用标签 是根据用户上层操作而对 Kubernetes Workload 做自动注入的一组元数据。它是一组常量,被定义到common.go当中。 当用户在 Kubernetes 中做资源对象的操作时,它会以 mutatingwebhook的方式自动注入的被管理的资源对象当中。

CommonLabel 中的 kubegems.io/applications 或者 Kubernetes 中的 app.kubernetes.io/nameapp共同声明了该应用的 workerload 标签。基于此,用户在创建日志规则是,可以通过 LabelSelector 定位到环境下的唯一资源。对于用户提交的 Flow ,同一种日志解析、路由规则类型的资源可以集中管理配置,如下:

apiVersion: logging.banzaicloud.io/v1beta1
kind: Flow
metadata:
name: default
namespace: tenant
spec:
localOutputRefs:
- default
match:
- select:
labels:
kubegems.io/applications: nginx
- select:
labels:
kubegems.io/applications: mysql
- select:
labels:
app.kubernetes.io/name: tomcat

局部模式下的用户流程

局部自定义模式下,开放普通用户配置有限功能的 Flow 以及 Outputs 资源。KubeGems 仍然需要对 CR 做简单接口封装。它的调用流程如下:

  1. 创建日志规则时,请求KubeGems listWorkload 返回当前环境空间下具备采集条件( CommonLabel)的资源列表,由用户在前端选择加入。
  2. 用户界面内提供插件列表,有用户自定义插件是否启用
  3. 通过请求 KubeGems listOutput 返回当前环境下可用的日志路由。普通用户同时也具备列出 ClusterOutput 资源(它由KubeGems 平台管理员创建)。
  4. 日志规则关联 localOutputRefs或者 globalOutputRefs后提交给 KubeGems 后台渲染 Flow 文件。
  5. Flow/Output 资源由 Logging Operator 处理,并返回资源validate结果和状态。

即在 局部自定义模式 下,KubeGems 在租户空间的接口中传入如下参数:

POST  observe/log/<tenant_name>/flow?name=tenant&namespace=tenant&monitor=true&throttle=4000&geoip_keys=remote_addr&outputs=my-elasticsearch,my-kafka&clusteroutputs=loki
参数释意requiredType
name日志采集规则名称TrueString
namespace采集日志的目标命名空间TrueString
monitor启用日志采集状态监控,default: trueFalseBoolean
throttle启用容器级日志条目限速,Lines / 10sFalseInt16
geoip_keys启用 GEO IPFalseString
outputs普通日志输出通道,多个通道用 ,逗号分割At laeast oneString
clusteroutputs日志输出通道,多个通道用 ,逗号分割At laeast oneString

outputs 和 clusteroutputs 参数至少满足一个

KubeGems 将 Flow 渲染如下:

apiVersion: logging.banzaicloud.io/v1beta1
kind: Flow
metadata:
name: tenant
namespace: tenant
spec:
filters:
- geoip:
geoip_lookup_keys: remote_addr
records:
- city: ${city.names.en["remote_addr"]}
location_array: '''[${location.longitude["remote"]},${location.latitude["remote"]}]'''
country: ${country.iso_code["remote_addr"]}
country_name: ${country.names.en["remote_addr"]}
postal_code: ${postal.code["remote_addr"]}
- record_modifier:
records:
- throttle_group_key: ${record['kubernetes']['namespace_name']+record['kubernetes']['pod_name']}
- prometheus:
labels:
container: $.kubernetes.container_name
namespace: $.kubernetes.namespace_name
node: $.kubernetes.host
pod: $.kubernetes.pod_name
metrics:
- desc: Total number of log entries generated by either application containers
or system components
name: logging_entry_count
type: counter
- throttle:
group_bucket_limit: 4000
group_bucket_period_s: 10
group_key: throttle_group_key
localOutputRefs:
- my-elasticsearch
- my-kafka
globalOutputRefs:
- loki

原始模式

对于租户需要使用 Logging Operator 完整特性来做自定义日志解析场景,KubeGems 只需在页面中满足对 Flow 原始格式 的校验和提交即可。

POST  observe/log/<tenant_name>/flow?raw=true

Body:

apiVersion: logging.banzaicloud.io/v1beta1
kind: Flow
metadata:
name: kafka
spec:
filters:
- tag_normaliser: {}
- parser:
remove_key_name_field: true
reserve_data: true
parse:
type: multi_format
patterns:
- format: nginx
- format: regexp
expression: /foo/
- format: none
match:
- select:
labels:
app.kubernetes.io/name: log-generator
localOutputRefs:
- kafka-output

KubeGems Log Observability

KubeGems 的日志可观测性主要满足以下几点需求

  • 用户环境空间内的日志采集速率分析
  • 用户环境空间内的错误日志统计
  • 用户自定义的日志告警规则

默认情况下 KubeGems Logging 插件集成了 Loki 实例用于持久化平台内容器日志。借有 Loki Ruler,可实现日志告警和错误日志分析相关功能。

日志可观测性流程

  1. KubeGems Installer 在 Kubernetes 集群初始化阶段负责将 Logging 插件下的 Loki 和 Recording Rules 配置。
  2. 普通用户在用户界面中创建日志告警规则,由 KubeGems 将告警规则以 Loki API 方式提交。
  3. 当产生Loki 产生日志告警时,经由 AlertManager 将告警事件推送给用户,并在 KubeGems Webhook 记录。

在上述流程中,KubeGems 日志告警中仅需提供 logrules 接口,用于管理用户告警内容。

Log Alerting Template

Loki 的 Rules 的语法规则和 Prometheus 一样,区别只在expr中体现。当前 KubeGems 中的 Metrics 告警采用的是预制模板 的方式,以支持用户更快的创建规则。在日志告警规则也可参考此方式,预制常见的 LogQL 模板。

普通模板

普通模板即用户只需要设置日志关键字符以管道的方式过滤字符。KubeGems 在后端组装语句 expr 并请求 Loki API 完成规则提交 。查询语句如下:

sum by (pod,namespace,application) (count_over_time({pod="<pod>",namespace="<namespace>",applications="<applications>"}  |~ `<your_log_string>`  |~ `<your_log_string>`[1m]))

格式化模板(json/logfmt)

采用 LogQL 的格式化解析器提取日志,通过查找 key-values 的方式过滤结果。

  • json 解释器
sum by  (pod,namespace,application) (count_over_time({pod="<pod>",namespace="<namespace>",applications="<applications>"}  | json |  <your_key>=<your_string>  |   __error__=""[1m]))
  • logfmt
 sum by  (pod,namespace,application) (count_over_time({pod="<pod>",namespace="<namespace>",applications="<applications>"}  | logfmt |  <your_key>=<your_string>  |   __error__=""[1m]))

高级模式

采用 LogQL 原生语句直接提交 Rules。

上述 3 种 LogQL 预制模板,最终提交的格式化 alertrules 结构如下:

  - name: should_fire
rules:
- alert: <your_log_string>-alert
expr: sum by (pod,namespace,application) (count_over_time({pod="<pod>",namespace="<namespace>",applications="<applications>"} |~ `<$your_log_string>` |~ `<$your_log_string>`[1m])) >= <$your_thresholds>
for: 1m
labels:
severity: <$your_severity>
pod: {{$labels.pod}}
namespace: {{$labels.namespace}}
application: {{labels.applicastions}}
annotations:
summary: message <your_log_string> alerting ,now has {{$labels.value}}.

Log Recording Rules

Recording Rules 允许用户预先将需要进行大量计算的表达式的结果转化保存为一组新的时间序列,并将其通过 remote_write的方式写入 Prometheus。在 KubeGems 中,平台将接入 Logging Observability 的应用预制了通用性的 Error Log Rules。

与 Alerting Rules 一样,Recoring Rules 如要 Loki Ruler 的支持,这部分将在 KubeGems Installer 初始化中部署到您的集群。

关于 Loki Ruler 对 RemoteWrite 的配置,可查考loki/remote-write

Log Metrics

Log Metrics 在 KubeGems 中,由用户提交的日志采集器中声明,这部分采用 fluent-plugin-prometheus,核心部分即为每个进入管道的日志流创建一个 计数器(Counter)并记录其条目和元数据。

  - prrometheus:
labels:
container: $.kubernetes.container_name
namespace: $.kubernetes.namespace_name
node: $.kubernetes.host
pod: $.kubernetes.pod_name
metrics:
- desc: Total number of log entries generated by either application containers
or system components
name: logging_entry_count
type: counter

最终由 Prometheus 将指标logging_entry_count持久化到本地。

总结

KubeGems 中基于租户的日志采集方案整体设计采用 Logging Operator + Loki 架构,用户可根据企业自身组织结构对其进行管理和适配。对于在 Kubernetes 集群中操作原生的 CRD 资源复杂的场景下,KubeGems 尽量让用户在接入日志采集、监控和告警的三个方面做到开箱即用的功能,极大简化系统管理者或研发人员的是学习和接入成本。

· One min read
yud

主要数据模型

pic

数据模型的主要层级关系为 租户 -> 项目 -> 环境 -> 应用;

对应到集群中的以下资源

资源简写group/version是否是namespaced资源Crd
environmentstenvgems.kubegems.io/v1beta1falseEnvironment
tenantgatewaystgwgems.kubegems.io/v1beta1falseTenantGateway
tenantnetworkpoliciestnetpolgems.kubegems.io/v1beta1falseTenantNetworkPolicy
tenantresourcequotastquotagems.kubegems.io/v1beta1falseTenantResourceQuota
tenantstengems.kubegems.io/v1beta1falseTenant
  • 系统内顶级资源为租户和集群, 租户和集群都由系统管理员添加;租户与集群通过TenantResourceQuota关联,一个租户在一个集群下只能存在一个TenantResourceQuota; 租户映射到集群中的CRD为Tenant, 租户CRD下存在网络隔离策略(TenantNetworkPolicy)资源限制(TenantResourceQuota)以及租户网关(TenantGateway), 这些子资源都将在租户crd创建的时候默认创建;
  • 用户(Users)与租户,项目,环境都存在着关联关系,这些关联关系将为以后的用户权限提供数据支持;
  • 项目仅仅是平台侧的概念,它表示一组应用的集合
  • 环境与集群的namespace关联,实现环境隔离,资源限制,网络隔离等,环境则更多的是运维相关属性;
  • 应用表示真实的应用

用户权限

系统的用户权限主要通过角色实现, 角色又分为系统级角色,租户级角色,项目级角色环境级角色;

系统级角色

  • 系统管理员的职责是管理系统资源,集群,集群插件,租户等; 系统管理员拥有一切资源的操作权限和读权限
  • 普通用户代表 KubeGems 中的普通成员,用普通用户角色的账号仅能登陆系统,其他租户,项目等权限将根据租户和项目下的角色判断

租户级角色

  • 租户管理员的主要职责是负责租户的成员管理和项目管理,负责项目添加和删除,租户成员的添加和修改; 租户管理员拥有租户下的一切资源操作权限和读权限

  • 租户成员默认仅可以读租户下的项目信息; 在添加项目成员环境成员的时候,用户必须是租户成员才能作为项目成员和环境成员的备选项;

项目级角色

  • 项目管理员的职责是负责项目的成员管理,项目的环境管理和项目下的应用管理; 项目管理员拥有项目下的一切资源的操作权限和读权限;

  • 项目成员拥有三个角色,分别是开发 测试 运维

    • 项目开发成员可以读所有环境,只能操作开发类型的环境
    • 项目测试成员可以读所有环境,只能操作测试类型的环境
    • 项目运维成员可以读所有环境,可以操作开发 测试 生产类型的环境

环境级角色

  • 环境reader在默认情况下,项目成员是所有环境的reader,即只要是项目成员,就能读取所有的环境数据

  • 环境operator通常不需要配置这个角色,但是有特殊的情况,例如开发需要操作生产环境的资源,默认情况下开发人员只能操作开发环境,这时候授权开发人员在生产环境是operator的角色,就可以操作生产环境了;

登陆模块

需求

支持多源登陆(ldap, oauth2)

  • 本地认证,支持账号+密码登陆

  • 外部认证,支持ldap和oauth2的认证

登陆设计

插件式设计,允许不同类型的登陆源实现登陆插件即可,插件目前分为两类,分别是OAUTHLDAP

插件需要实现接口aaa.AuthenticateIface接口

type AuthenticateIface interface {
// 返回登陆插件的名字
GetName() string
// 返回登陆地址
LoginAddr() string
// , 获取用户信息
// 验证凭据,获取根据用户提供的凭据获取用户信息
GetUserInfo(ctx context.Context, cred *Credential) (*UserInfo, error)
}

登陆流程:

  1. LDAP类型和默认账号密码登陆,直接提供登陆的用户和密码以及登陆源即可,登陆后将获得token

  2. OAUTH类型,先获取登陆地址,重定向到登陆地址,通常这个登陆地址为第三方平台的认证授权界面,授权后第三方将会重定向到平台配置的一个地址,并且携带着第三方平台的一个授权code,平台通过这个code获取access_token,再带着这个access_token访问用户信息,通过第三方平台中的用户名作为kubegems中的用户,登陆成功后获得token

认证设计

插件式设计,目前仅实现了基于JWT的认证方式; 需要实现接口aaa.UserGetterIface

type UserGetterIface interface {
GetUser(req *http.Request) (u user.CommonUserIface, exist bool)
}

认证流程

不同的认证插件, 从请求头中获取需要的信息,例如通过Authorization头获取Bearer token,通过获取到的信息载入用户,如果没有找到用户,则表示未登陆