您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

APIServer源码分析之入口点解析

时间:2023-03-17 11:44:24  来源:  作者:k8s技术圈

Kube.NETes(K8s)集群中最关键的组件之一是 API Server,它是所有集群管理活动的入口点。从本文开始,我们将对 K8s API Server 的代码进行详细分析,并探讨其应用入口点、框架以及与 etcd 的通信。

应用入口点

K8s API Server 的主要入口点位于 cmd/kube-apiserver/apiserver.go 文件的。

// cmd/kube-apiserver/apiserver.go 

// apiserver is the mAIn api server and master for the cluster.
// it is responsible for serving the cluster management API.
package main

import (
 "os"

 "k8s.io/component-base/cli"
 _ "k8s.io/component-base/logs/json/register"          // 用于JSON日志格式注册
 _ "k8s.io/component-base/metrics/prometheus/clientgo" // 加载所有的 prometheus client-go 插件
 _ "k8s.io/component-base/metrics/prometheus/version"  // 用于版本指标注册
 "k8s.io/kubernetes/cmd/kube-apiserver/App"
)

func main() {
 command := app.NewAPIServerCommand()
 code := cli.Run(command)
 os.Exit(code)
}

其中的 app.NewAPIServerCommand() 是构建的一个 cobra 的命令对象,cli.Run 然后执行该命令即可,所以我们直接查看 NewAPIServerCommand 函数是如果构造 cobra.Command 对象的:

// cmd/kube-apiserver/app/server.go

// NewAPIServerCommand 使用默认参数创建一个 *cobra.Command 对象
func NewAPIServerCommand() *cobra.Command {
  // NewServerRunOptions 使用默认参数创建一个新的 ServerRunOptions 对象。
  // ServerRunOption 对象是运行 apiserver 需要的对象
 s := options.NewServerRunOptions()
 cmd := &cobra.Command{
  Use: "kube-apiserver",
  Long: `The Kubernetes API server validates and configures data
for the api objects which include pods, services, replicationcontrollers, and
others. The API Server services REST operations and provides the frontend to the
cluster's shared state through which all other components interact.`,

  // ......
  RunE: func(cmd *cobra.Command, args []string) error {
   verflag.PrintAndExitIfRequested()
   fs := cmd.Flags()

   if err := s.Logs.ValidateAndApply(); err != nil {
    return err
   }
   cliflag.PrintFlags(fs)

   err := checkNonZeroInsecurePort(fs)
   if err != nil {
    return err
   }
   // 设置默认选项
   completedOptions, err := Complete(s)
   if err != nil {
    return err
   }
   // 校验选项
   if errs := completedOptions.Validate(); len(errs) != 0 {
    return utilerrors.NewAggregate(errs)
   }
   return Run(completedOptions, genericapiserver.SetupSignalHandler())
  },
 }

 // ......

 return cmd
}

该函数最核心的功能就是使用 Complete(s) 函数来生成 apiserver 启动需要的默认参数,然后将默认参数传递给 Run 函数进行启动。

// cmd/kube-apiserver/app/server.go
// Run 运行指定的 APIServer,不能退出.
func Run(completeOptions completedServerRunOptions, stopCh <-chan struct{}) error {

  // 创建服务链(包含的3个server组件)
 server, err := CreateServerChain(completeOptions, stopCh)
 

  // 服务启动前的准备工作,包括健康检查、存活检查、OpenAPI路由注册等
 prepared, err := server.PrepareRun()

  // 正式启动运行
 return prepared.Run(stopCh)
}

在 Run 函数中首先会通过 CreateServerChain 函数通过委托创建连接的 APIServer 对象。

// cmd/kube-apiserver/app/server.go

// CreateServerChain 通过委托创建连接的APIServer
func CreateServerChain(completedOptions completedServerRunOptions, stopCh <-chan struct{}) (*aggregatorapiserver.APIAggregator, error) {
 // CreateKubeAPIServerConfig 创建用于运行 APIServer 的所有配置资源,但不运行任何资源
  kubeAPIServerConfig, serviceResolver, pluginInitializer, err := CreateKubeAPIServerConfig(completedOptions)
 
 // // 创建 APIExtensionsServer 配置
 apiExtensionsConfig, err := createAPIExtensionsConfig(*kubeAPIServerConfig.GenericConfig, kubeAPIServerConfig.ExtraConfig.VersionedInformers, pluginInitializer, completedOptions.ServerRunOptions, completedOptions.MasterCount,
  serviceResolver, webhook.NewDefaultAuthenticationInfoResolverWrapper(kubeAPIServerConfig.ExtraConfig.ProxyTransport, kubeAPIServerConfig.GenericConfig.EgressSelector, kubeAPIServerConfig.GenericConfig.LoopbackClientConfig, kubeAPIServerConfig.GenericConfig.TracerProvider))
 
  // 创建APIExtensionsServer并注册路由
  apiExtensionsServer, err := createAPIExtensionsServer(apiExtensionsConfig, genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler))

  // 创建KubeAPIServer并注册路由
 kubeAPIServer, err := CreateKubeAPIServer(kubeAPIServerConfig, apiExtensionsServer.GenericAPIServer)

 // // 创建 aggregatorServer 配置
 aggregatorConfig, err := createAggregatorConfig(*kubeAPIServerConfig.GenericConfig, completedOptions.ServerRunOptions, kubeAPIServerConfig.ExtraConfig.VersionedInformers, serviceResolver, kubeAPIServerConfig.ExtraConfig.ProxyTransport, pluginInitializer)

  // 创建aggregatorServer并注册路由
 aggregatorServer, err := createAggregatorServer(aggregatorConfig, kubeAPIServer.GenericAPIServer, apiExtensionsServer.Informers)

 return aggregatorServer, nil
}

上面的函数中可以看到 CreateServerChain 会创建3个 server:APIExtensionServer、KubeAPIServer、AggregratorServer,APIServer 就是依靠这3个组件来对不同类型的请求进行处理的:

  • APIExtensionServer: 主要负责处理 CustomResourceDefinition(CRD)方面的请求。
  • KubeAPIServer: 主要负责处理 K8s 内置资源的请求,此外还会包括通用处理、认证、鉴权等。
  • AggregratorServer: 主要负责聚合器方面的处理,它充当一个代理服务器,将请求转发到聚合进来的 K8s service 中。

图片

创建每个 server 都有对应的 config,可以看出上面函数中的 apiExtensionServer 和 aggregatorServer 的 Config 需要依赖 kubeAPIServerConfig,而这几个 ServerConfig 都需要依赖 GenericConfig,CreateKubeAPIServerConfig 函数创建 kubeAPIServerConfig ,在该函数中通过调用 buildGenericConfig 来创建 GenericConfig 对象,如下代码所示。

// cmd/kube-apiserver/app/server.go
// CreateKubeAPIServerConfig 创建用于运行 APIServer 的所有配置资源
func CreateKubeAPIServerConfig(s completedServerRunOptions) (
 *controlplane.Config,
 aggregatorapiserver.ServiceResolver,
 []admission.PluginInitializer,
 error,
) {
 proxyTransport := CreateProxyTransport()
  // 构建通用配置
 genericConfig, versionedInformers, serviceResolver, pluginInitializers, admissionPostStartHook, storageFactory, err := buildGenericConfig(s.ServerRunOptions, proxyTransport)
 
  // ......

 config := &controlplane.Config{
  GenericConfig: genericConfig,
  ExtraConfig: controlplane.ExtraConfig{
   APIResourceConfigSource: storageFactory.APIResourceConfigSource,
   StorageFactory:          storageFactory,
   EventTTL:                s.EventTTL,
   KubeletClientConfig:     s.KubeletConfig,
   EnableLogsSupport:       s.EnableLogsHandler,
   ProxyTransport:          proxyTransport,

   ServiceIPRange:          s.PrimaryServiceClusterIPRange,
   APIServerServiceIP:      s.APIServerServiceIP,
   SecondaryServiceIPRange: s.SecondaryServiceClusterIPRange,

   APIServerServicePort: 443,

   ServiceNodePortRange:      s.ServiceNodePortRange,
   KubernetesServiceNodePort: s.KubernetesServiceNodePort,

   EndpointReconcilerType: reconcilers.Type(s.EndpointReconcilerType),
   MasterCount:            s.MasterCount,

   ServiceAccountIssuer:        s.ServiceAccountIssuer,
   ServiceAccountMaxExpiration: s.ServiceAccountTokenMaxExpiration,
   ExtendExpiration:            s.Authentication.ServiceAccounts.ExtendExpiration,

   VersionedInformers: versionedInformers,

   IdentityLeaseDurationSeconds:      s.IdentityLeaseDurationSeconds,
   IdentityLeaseRenewIntervalSeconds: s.IdentityLeaseRenewIntervalSeconds,
  },
 }

 // ......

 return config, serviceResolver, pluginInitializers, nil
}

func buildGenericConfig(
 s *options.ServerRunOptions,
 proxyTransport *http.Transport,
)(...){
 //创建一个通用配置对象
 genericConfig = genericapiserver.NewConfig(legacyscheme.Codecs)

 // ......

 //创建认证实例
 if lastErr = s.Authentication.ApplyTo(&genericConfig.Authentication, genericConfig.SecureServing, genericConfig.EgressSelector, genericConfig.OpenAPIConfig, clientgoExternalClient, versionedInformers); lastErr != nil {
  return
 }

  // ...
  // openapi/swagger配置,OpenAPIConfig 用于生成 OpenAPI 规范
 getOpenAPIDefinitions := openapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(generatedopenapi.GetOpenAPIDefinitions)
 genericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(getOpenAPIDefinitions, openapinamer.NewDefinitionNamer(legacyscheme.Scheme, extensionsapiserver.Scheme, aggregatorscheme.Scheme))
 genericConfig.OpenAPIConfig.Info.Title = "Kubernetes"
 genericConfig.LongRunningFunc = filters.BasicLongRunningRequestCheck(
  sets.NewString("watch", "proxy"),
  sets.NewString("attach", "exec", "proxy", "log", "portforward"),
 )
  
  // storageFactoryConfig 对象定义了 kube-apiserver 与 etcd 的交互方式,如:etcd认证、地址、存储前缀等
  // 该对象也定义了资源存储方式,如:资源信息、资源编码信息、资源状态等
  storageFactoryConfig := kubeapiserver.NewStorageFactoryConfig()
 storageFactoryConfig.APIResourceConfig = genericConfig.MergedResourceConfig
 completedStorageFactoryConfig, err := storageFactoryConfig.Complete(s.Etcd)
 
 storageFactory, lastErr = completedStorageFactoryConfig.New()
 
 if lastErr = s.Etcd.ApplyWithStorageFactoryTo(storageFactory, genericConfig); lastErr != nil {
  return
 }
  
  // ......
  
  // 初始化 SharedInformerFactory
  kubeClientConfig := genericConfig.LoopbackClientConfig
 clientgoExternalClient, err := clientgoclientset.NewForConfig(kubeClientConfig)
 versionedInformers = clientgoinformers.NewSharedInformerFactory(clientgoExternalClient, 10*time.Minute)
  
  // 认证配置,内部调用 authenticatorConfig.New()
  // K8s提供了9种认证机制,每种认证机制被实例化后都成为认证器
 if lastErr = s.Authentication.ApplyTo(&genericConfig.Authentication, genericConfig.SecureServing, genericConfig.EgressSelector, genericConfig.OpenAPIConfig, clientgoExternalClient, versionedInformers); lastErr != nil {
  return
 }

 // 创建鉴权实例,K8s也提供了6种授权机制,每种授权机制被实例化后都成为授权器
 genericConfig.Authorization.Authorizer, genericConfig.RuleResolver, err = BuildAuthorizer(s, genericConfig.EgressSelector, versionedInformers)
 
  // ...
  // 审计
  lastErr = s.Audit.ApplyTo(genericConfig)

  // 准入控制器
  // k8s资源在认证和授权通过,被持久化到etcd之前进入准入控制逻辑
  // 准入控制包括:对请求的资源进行自定义操作(校验、修改、拒绝)
  // 准入控制器通过 Plugins 数据结构统一注册、存放、管理
  admissionConfig := &kubeapiserveradmission.Config{
  ExternalInformers:    versionedInformers,
  LoopbackClientConfig: genericConfig.LoopbackClientConfig,
  CloudConfigFile:      s.CloudProvider.CloudConfigFile,
 }
 serviceResolver = buildServiceResolver(s.EnableAggregatorRouting, genericConfig.LoopbackClientConfig.Host, versionedInformers)
 pluginInitializers, admissionPostStartHook, err = admissionConfig.New(proxyTransport, genericConfig.EgressSelector, serviceResolver, genericConfig.TracerProvider)
 
 err = s.Admission.ApplyTo(
  genericConfig,
  versionedInformers,
  kubeClientConfig,
  feature.DefaultFeatureGate,
  pluginInitializers...)
  
  // ...

}

然后我们再来分别看看这3个 Server 是如何构建的。

go-restful框架

这里我们就不得不先了解下 go-restful 这个框架了,因为 APIServer 就使用的这个框架。下面的代码是 go-restful 官方的一个示例,这个 demo 了解后基本上就知道 go-restful 框架是如何使用的了:

package main

import (
 "log"
 "net/http"

 restfulspec "Github.com/emicklei/go-restful-openapi/v2"
 restful "github.com/emicklei/go-restful/v3"
 "github.com/go-openapi/spec"
)

// UserResource is the REST layer to the User domain
type UserResource struct {
 // normally one would use DAO (data access object)
 users map[string]User
}

// WebService creates a new service that can handle REST requests for User resources.
func (u UserResource) WebService() *restful.WebService {
 ws := new(restful.WebService)
 ws.
  Path("/users").
  Consumes(restful.MIME_XML, restful.MIME_JSON).
  Produces(restful.MIME_JSON, restful.MIME_XML) // you can specify this per route as well

 tags := []string{"users"}

 ws.Route(ws.GET("/").To(u.findAllUsers).
  // docs
  Doc("get all users").
  Metadata(restfulspec.KeyOpenAPITags, tags).
  Writes([]User{}).
  Returns(200, "OK", []User{}))

 ws.Route(ws.GET("/{user-id}").To(u.findUser).
  // docs
  Doc("get a user").
  Param(ws.PathParameter("user-id", "identifier of the user").DataType("integer").DefaultValue("1")).
  Metadata(restfulspec.KeyOpenAPITags, tags).
  Writes(User{}). // on the response
  Returns(200, "OK", User{}).
  Returns(404, "Not Found", nil))

 ws.Route(ws.PUT("/{user-id}").To(u.updateUser).
  // docs
  Doc("update a user").
  Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")).
  Metadata(restfulspec.KeyOpenAPITags, tags).
  Reads(User{})) // from the request

 ws.Route(ws.PUT("").To(u.createUser).
  // docs
  Doc("create a user").
  Metadata(restfulspec.KeyOpenAPITags, tags).
  Reads(User{})) // from the request

 ws.Route(ws.DELETE("/{user-id}").To(u.removeUser).
  // docs
  Doc("delete a user").
  Metadata(restfulspec.KeyOpenAPITags, tags).
  Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")))

 return ws
}

// GET http://localhost:8080/users
//
func (u UserResource) findAllUsers(request *restful.Request, response *restful.Response) {
 list := []User{}
 for _, each := range u.users {
  list = append(list, each)
 }
 response.WriteEntity(list)
}

// GET http://localhost:8080/users/1
//
func (u UserResource) findUser(request *restful.Request, response *restful.Response) {
 id := request.PathParameter("user-id")
 usr := u.users[id]
 if len(usr.ID) == 0 {
  response.WriteErrorString(http.StatusNotFound, "User could not be found.")
 } else {
  response.WriteEntity(usr)
 }
}

// PUT http://localhost:8080/users/1
// <User><Id>1</Id><Name>Melissa Raspberry</Name></User>
//
func (u *UserResource) updateUser(request *restful.Request, response *restful.Response) {
 usr := new(User)
 err := request.ReadEntity(&usr)
 if err == nil {
  u.users[usr.ID] = *usr
  response.WriteEntity(usr)
 } else {
  response.WriteError(http.StatusInternalServerError, err)
 }
}

// PUT http://localhost:8080/users/1
// <User><Id>1</Id><Name>Melissa</Name></User>
//
func (u *UserResource) createUser(request *restful.Request, response *restful.Response) {
 usr := User{ID: request.PathParameter("user-id")}
 err := request.ReadEntity(&usr)
 if err == nil {
  u.users[usr.ID] = usr
  response.WriteHeaderAndEntity(http.StatusCreated, usr)
 } else {
  response.WriteError(http.StatusInternalServerError, err)
 }
}

// DELETE http://localhost:8080/users/1
//
func (u *UserResource) removeUser(request *restful.Request, response *restful.Response) {
 id := request.PathParameter("user-id")
 delete(u.users, id)
}

func main() {
 u := UserResource{map[string]User{}}
 restful.DefaultContainer.Add(u.WebService())

 config := restfulspec.Config{
  WebServices:                   restful.RegisteredWebServices(), // you control what services are visible
  APIPath:                       "/apidocs.json",
  PostBuildSwaggerObjectHandler: enrichSwaggerObject}
 restful.DefaultContainer.Add(restfulspec.NewOpenAPIService(config))

 // Optionally, you can install the Swagger Service which provides a nice Web UI on your REST API
 // You need to download the Swagger html5 assets and change the FilePath location in the config below.
 // Open http://localhost:8080/apidocs/?url=http://localhost:8080/apidocs.json
 http.Handle("/apidocs/", http.StripPrefix("/apidocs/", http.FileServer(http.Dir("/Users/emicklei/Projects/swagger-ui/dist"))))

 log.Printf("start listening on localhost:8080")
 log.Fatal(http.ListenAndServe(":8080", nil))
}

func enrichSwaggerObject(swo *spec.Swagger) {
 swo.Info = &spec.Info{
  InfoProps: spec.InfoProps{
   Title:       "UserService",
   Description: "Resource for managing Users",
   Contact: &spec.ContactInfo{
    ContactInfoProps: spec.ContactInfoProps{
     Name:  "john",
     Email: "john@doe.rp",
     URL:   "http://johndoe.org",
    },
   },
   License: &spec.License{
    LicenseProps: spec.LicenseProps{
     Name: "MIT",
     URL:  "http://mit.org",
    },
   },
   Version: "1.0.0",
  },
 }
 swo.Tags = []spec.Tag{spec.Tag{TagProps: spec.TagProps{
  Name:        "users",
  Description: "Managing users"}}}
}

// User is just a sample type
type User struct {
 ID   string `json:"id" description:"identifier of the user"`
 Name string `json:"name" description:"name of the user" default:"john"`
 Age  int    `json:"age" description:"age of the user" default:"21"`
}

这个示例代码,就是使用 go-restful 的核心功能实现了一个简单的 RESTful 的 API,实现了对 User 的增删查改,其中有这么几个核心概念:Container、WebService、Route。

  • Container:服务器容器,包含多个 WebService 和一个 http.ServerMux。
  • WebService:服务,由多个 Route 组成,一个 WebService 其实代表某一个对象相关的服务,如上例中的 /users,针对该 /users 要实现RESTful API,那么需要向其添加增删查改的路由,即 Route,它是 Route 的集合。
  • Route:路由,包含了 url,http 方法,接收和响应的媒体类型以及处理函数。每一个 Route,根据 Method 和 Path,映射到对应的方法中,即是 Method/Path 到 Function 映射关系的抽象,如上例中的 ws.Route(ws.GET("/{user-id}").To(u.findUser)),就是针对 /users/{user-id}该路径的GET请求,则被路由到 findUser 方法中进行处理。
  • Container 是 WebService 的集合,可以向 Container 中添加多个 WebService,而 Container 因为实现了 ServeHTTP() 方法,其本质上还是一个http Handler,可以直接用在 http Server 中。

Kubernetes 中对 go-restful 的使用比较基础,就使用到了其最基础的路由功能,由于 K8s 有很多内置的资源对象,也包括 CRD 这种自定义资源对象,所以一开始并不是直接将这些资源对应对应的接口硬编码的,而是通过一系列代码动态注册的,所以接下来我们分析的其实就是想办法让 APIServer 能够提供如下所示的路由处理出来:

GET   /apis/apps/v1/namespaces/{namespace}/deployments/{name}
POST  /apis/apps/v1/namespaces/{namespace}/deployments

GET   /apis/apps/v1/namespaces/{namespace}/daemonsets/{name}
POST  /apis/apps/v1/namespaces/{namespace}/daemonsets

对 go-restful 有一个基础了解后,后面就可以去了解下这3个 Server 具体是如何实例化的了。



Tags:APIServer   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
APIServer源码分析之入口点解析
Kubernetes(K8s)集群中最关键的组件之一是 API Server,它是所有集群管理活动的入口点。从本文开始,我们将对 K8s API Server 的代码进行详细分析,并探讨其应用入口点、框架以及与...【详细内容】
2023-03-17  Search: APIServer  点击:(148)  评论:(0)  加入收藏
▌简易百科推荐
Netflix 是如何管理 2.38 亿会员的
作者 | Surabhi Diwan译者 | 明知山策划 | TinaNetflix 高级软件工程师 Surabhi Diwan 在 2023 年旧金山 QCon 大会上发表了题为管理 Netflix 的 2.38 亿会员 的演讲。她在...【详细内容】
2024-04-08    InfoQ  Tags:Netflix   点击:(0)  评论:(0)  加入收藏
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(6)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(13)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(9)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(11)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(9)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
相关文章
    无相关信息
站内最新
站内热门
站内头条