网站竞价推广哪个好电脑在哪网站接做扇子单

diannao/2026/1/21 19:59:22/文章来源:
网站竞价推广哪个好,电脑在哪网站接做扇子单,app开发价格要多少钱,php网站程序安装我们再来看下 iam-apiserver 中的核心功能实现。 这些关键代码设计分为 3 类#xff0c;分别是应用框架相关的特性、编程规范相关的特性和其他特性。 应用框架相关的特性 应用框架相关的特性包括三个#xff0c;分别是优雅关停、健康检查和插件化加载中间件。 优雅关停 … 我们再来看下 iam-apiserver 中的核心功能实现。  这些关键代码设计分为 3 类分别是应用框架相关的特性、编程规范相关的特性和其他特性。  应用框架相关的特性 应用框架相关的特性包括三个分别是优雅关停、健康检查和插件化加载中间件。 优雅关停 当我们需要重启服务时首先需要停止服务这时可以通过两种方式来停止我们的服务 在 Linux 终端键入 Ctrl C其实是发送 SIGINT 信号。发送 SIGTERM 信号例如 kill -9 或者 systemctl stop 等。 当我们使用以上两种方式停止服务时都会产生下面两个问题 有些请求正在处理如果服务端直接退出会造成客户端连接中断请求失败。我们的程序可能需要做一些清理工作比如等待进程内任务队列的任务执行完成或者拒绝接受新的消息等。 在 Go 开发中通常通过拦截 SIGINT 和 SIGTERM 信号来实现优雅关停。  先来看一个简单的优雅关停的示例代码 package mainimport (contextlognet/httposos/signaltimegithub.com/gin-gonic/gin )func main() {router : gin.Default()router.GET(/, func(c *gin.Context) {time.Sleep(5 * time.Second)c.String(http.StatusOK, Welcome Gin Server)})srv : http.Server{Addr: :8080,Handler: router,}go func() {// 将服务在 goroutine 中启动if err : srv.ListenAndServe(); err ! nil err ! http.ErrServerClosed {log.Fatalf(listen: %s\n, err)}}()quit : make(chan os.Signal)signal.Notify(quit, os.Interrupt)-quit // 阻塞等待接收 channel 数据log.Println(Shutdown Server ...)ctx, cancel : context.WithTimeout(context.Background(), 5*time.Second) // 5s 缓冲时间处理已有请求defer cancel()if err : srv.Shutdown(ctx); err ! nil { // 调用 net/http 包提供的优雅关闭函数Shutdownlog.Fatal(Server Shutdown:, err)}log.Println(Server exiting) }上面的代码实现优雅关停的思路如下 将 HTTP 服务放在 goroutine 中运行程序不阻塞继续执行。创建一个无缓冲的 channel quit调用 signal.Notify(quit, os.Interrupt)。通过 signal.Notify 函数调用可以将进程收到的 os.InterruptSIGINT信号发送给 channel quit。-quit 阻塞当前 goroutine也就是 main 函数所在的 goroutine等待从 channel quit 接收关停信号。通过以上步骤我们成功启动了 HTTP 服务并且 main 函数阻塞防止启动 HTTP 服务的 goroutine 退出。当我们键入 Ctrl C时进程会收到 SIGINT 信号并将该信号发送到 channel quit 中这时候-quit收到了 channel 另一端传来的数据结束阻塞状态程序继续执行。这里-quit唯一目的是阻塞当前的 goroutine所以对收到的数据直接丢弃。打印退出消息提示准备退出当前服务。调用 net/http 包提供的 Shutdown 方法Shutdown 方法会在指定的时间内处理完现有请求并返回。最后程序执行完 log.Println(Server exiting) 代码后退出 main 函数。 iam-apiserver 也实现了优雅关停优雅关停思路跟上面的代码类似。  第一步创建 channel 用来接收 os.InterruptSIGINT和 syscall.SIGTERMSIGKILL信号。 代码见 internal/pkg/server/signal.go 。 var onlyOneSignalHandler make(chan struct{})var shutdownHandler chan os.Signalfunc SetupSignalHandler() -chan struct{} {close(onlyOneSignalHandler) // panics when called twiceshutdownHandler make(chan os.Signal, 2)stop : make(chan struct{})signal.Notify(shutdownHandler, shutdownSignals...)go func() {-shutdownHandlerclose(stop)-shutdownHandleros.Exit(1) // second signal. Exit directly.}()return stop }SetupSignalHandler 函数中通过 close(onlyOneSignalHandler)来确保 iam-apiserver 组件的代码只调用一次 SetupSignalHandler 函数。  SetupSignalHandler 函数还实现了一个功能收到一次 SIGINT/ SIGTERM 信号程序优雅关闭。收到两次 SIGINT/ SIGTERM 信号程序强制关闭。实现代码如下 go func() {-shutdownHandlerclose(stop)-shutdownHandleros.Exit(1) // second signal. Exit directly. }()这里要注意signal.Notify(c chan- os.Signal, sig ...os.Signal)函数不会为了向 c 发送信息而阻塞。也就是说如果发送时 c 阻塞了signal 包会直接丢弃信号。为了不丢失信号我们创建了有缓冲的 channel shutdownHandler。 最后SetupSignalHandler 函数会返回 stop后面的代码可以通过关闭 stop 来结束代码的阻塞状态。 第二步将 channel stop 传递给启动 HTTPS、gRPC 服务的函数在函数中以 goroutine 的方式启动 HTTPS、gRPC 服务然后执行 -stop 阻塞 goroutine。 第三步当 iam-apiserver 进程收到 SIGINT/SIGTERM 信号后关闭 stop channel继续执行 -stop 后的代码在后面的代码中我们可以执行一些清理逻辑或者调用 google.golang.org/grpc和 net/http包提供的优雅关停函数 GracefulStop 和 Shutdown。例如下面这个代码位于 internal/apiserver/grpc.go 文件中 func (s *grpcAPIServer) Run(stopCh -chan struct{}) {listen, err : net.Listen(tcp, s.address)if err ! nil {log.Fatalf(failed to listen: %s, err.Error())}log.Infof(Start grpc server at %s, s.address)go func() {if err : s.Serve(listen); err ! nil {log.Fatalf(failed to start grpc server: %s, err.Error())}}()-stopChlog.Infof(Grpc server on %s stopped, s.address)s.GracefulStop() }除了上面说的方法iam-apiserver 还通过 github.com/marmotedu/iam/pkg/shutdown 包实现了另外一种优雅关停方法这个方法更加友好、更加灵活。实现代码见 PrepareRun 函数。 github.com/marmotedu/iam/pkg/shutdown 包的使用方法如下 package main import (fmttimegithub.com/marmotedu/iam/pkg/shutdowngithub.com/marmotedu/iam/pkg/shutdown/shutdownmanagers/posixsignal ) func main() {// initialize shutdowngs : shutdown.New()// add posix shutdown managergs.AddShutdownManager(posixsignal.NewPosixSignalManager())// add your tasks that implement ShutdownCallbackgs.AddShutdownCallback(shutdown.ShutdownFunc(func(string) error {fmt.Println(Shutdown callback start)time.Sleep(time.Second)fmt.Println(Shutdown callback finished)return nil}))// start shutdown managersif err : gs.Start(); err ! nil {fmt.Println(Start:, err)return}// do other stufftime.Sleep(time.Hour) }上面的代码中通过 gs : shutdown.New() 创建 shutdown 实例通过 AddShutdownManager 方法添加监听的信号通过 AddShutdownCallback 方法设置监听到指定信号时需要执行的回调函数。这些回调函数可以执行一些清理工作。最后通过 Start 方法启动 shutdown 实例。 健康检查 通常我们会根据进程是否存在来判定 iam-apiserver 是否健康例如执行 ps -ef|grep iam-apiserver。  我们可以在启动 iam-apiserver 进程后手动调用 iam-apiserver 健康检查接口进行检查。但还有更方便的方法启动服务后自动调用健康检查接口。  url : fmt.Sprintf(http://%s/healthz, s.InsecureServingInfo.Address) if strings.Contains(s.InsecureServingInfo.Address, 0.0.0.0) {url fmt.Sprintf(http://127.0.0.1:%s/healthz, strings.Split(s.InsecureServingInfo.Address, :)[1]) }插件化加载中间件 iam-apiserver 支持插件化地加载 Gin 中间件  那么为什么要将中间件做成一种插件化的机制呢 一方面每个中间件都完成某种功能这些功能不是所有情况下都需要的 另一方面中间件是追加在 HTTP 请求链路上的一个处理函数会影响 API 接口的性能。  例如在测试环境中为了方便 Debug可以选择加载 dump 中间件。dump 中间件可以打印请求包和返回包信息这些信息可以协助我们 Debug。  iam-apiserver 通过 InstallMiddlewares 函数来安装 Gin 中间件函数代码如下 func (s *GenericAPIServer) InstallMiddlewares() {// necessary middlewaress.Use(middleware.RequestID())s.Use(middleware.Context())// install custom middlewaresfor _, m : range s.middlewares {mw, ok : middleware.Middlewares[m]if !ok {log.Warnf(can not find middleware: %s, m)continue}log.Infof(install middleware: %s, m)s.Use(mw)} }可以看到安装中间件时我们不仅安装了一些必备的中间件还安装了一些可配置的中间件。 上述代码安装了两个默认的中间件 RequestID 和 Context 。 RequestID 中间件主要用来在 HTTP 请求头和返回头中设置 X-Request-ID Header。如果 HTTP 请求头中没有 X-Request-ID HTTP 头则创建 64 位的 UUID如果有就复用。UUID 是调用 github.com/satori/go.uuid包提供的 NewV4().String()方法来生成的 rid uuid.NewV4().String()另外这里有个 Go 常量的设计规范需要你注意常量要跟该常量相关的功能包放在一起不要将一个项目的常量都集中放在 const 这类包中。例如 requestid.go 文件中我们定义了 XRequestIDKey X-Request-ID常量其他地方如果需要使用 XRequestIDKey只需要引入 XRequestIDKey所在的包并使用即可。 Context 中间件用来在 gin.Context 中设置 requestID和 username键在打印日志时将 gin.Context 类型的变量传递给 log.L() 函数log.L() 函数会在日志输出中输出 requestID和 username域 2021-07-09 13:33:21.362 DEBUG apiserver v1/user.go:106 get 2 users from backend storage. {requestID: f8477cf5-4592-4e47-bdcf-82f7bde2e2d0, username: admin}requestID和 username字段可以方便我们后期过滤并查看日志。 除了默认的中间件iam-apiserver 还支持一些可配置的中间件我们可以通过配置 iam-apiserver 配置文件中的 server.middlewares 配置项来配置这些这些中间件。 可配置以下中间件 recovery捕获任何 panic并恢复。secure添加一些安全和资源访问相关的 HTTP 头。nocache禁止客户端缓存 HTTP 请求的返回结果。corsHTTP 请求跨域中间件。dump打印出 HTTP 请求包和返回包的内容方便 debug。注意生产环境禁止加载该中间件。 当然你还可以根据需要添加更多的中间件。方法很简单只需要编写中间件并将中间件添加到一个 map[string]gin.HandlerFunc 类型的变量中即可 func defaultMiddlewares() map[string]gin.HandlerFunc {      return map[string]gin.HandlerFunc{                          recovery:  gin.Recovery(),                            secure:    Secure,                                options:   Options,             nocache:   NoCache,             cors:      Cors(),                        requestid: RequestID(),                       logger:    Logger(),                                                           dump:      gindump.Dump(),                                                               }                                                                                               }  上述代码位于 internal/pkg/middleware/middleware.go 文件中。 编程规范相关的特性 API 版本 RESTful API 为了方便以后扩展都需要支持 API 版本。在 12 讲 中我们介绍了 API 版本号的 3 种标识方法iam-apiserver 选择了将 API 版本号放在 URL 中例如/v1/secrets。放在 URL 中的好处是很直观看 API 路径就知道版本号。另外API 的路径也可以很好地跟控制层、业务层、模型层的代码路径相映射。例如密钥资源相关的代码存放位置如下 internal/apiserver/controller/v1/secret/ # 控制几层代码存放位置 internal/apiserver/service/v1/secret.go # 业务层代码存放位置 github.com/marmotedu/api/apiserver/v1/secret.go # 模型层代码存放位置关于代码存放路径我还有一些地方想跟你分享。对于 Secret 资源通常我们需要提供 CRUD 接口。 CCreate创建 Secret。RGet获取详情、List获取 Secret 资源列表。UUpdate更新 Secret。DDelete删除指定的 Secret、DeleteCollection批量删除 Secret。 $ ls internal/apiserver/controller/v1/secret/ create.go delete_collection.go delete.go doc.go get.go list.go secret.go update.go业务层和模型层的代码也可以这么组织。iam-apiserver 中因为 Secret 的业务层和模型层代码比较少所以我放在了 internal/apiserver/service/v1/secret.go和 github.com/marmotedu/api/apiserver/v1/secret.go文件中。如果后期 Secret 业务代码增多我们也可以修改成下面这种方式 $ ls internal/apiserver/service/v1/secret/create.go delete_collection.go delete.go doc.go get.go list.go secret.go update.go这里再说个题外话/v1/secret/和/secret/v1/这两种目录组织方式都可以你选择一个自己喜欢的就行。 当我们需要升级 API 版本时相关代码可以直接放在 v2 目录下例如 internal/apiserver/controller/v2/secret/ # v2 版本控制几层代码存放位置 internal/apiserver/service/v2/secret.go # v2 版本业务层代码存放位置 github.com/marmotedu/api/apiserver/v2/secret.go # v2 版本模型层代码存放位置这样既能够跟 v1 版本的代码物理隔离开互不影响又方便查找 v2 版本的代码。 统一的资源元数据 iam-apiserver 设计的一大亮点是像Kubernetes REST 资源一样支持统一的资源元数据。 iam-apiserver 中所有的资源都是 REST 资源iam-apiserver 将 REST 资源的属性也进一步规范化了这里的规范化是指所有的 REST 资源均支持两种属性 公共属性。资源自有的属性。 例如Secret 资源的定义方式如下 type Secret struct {// May add TypeMeta in the future.// metav1.TypeMeta json:,inline// Standard objects metadata.metav1.ObjectMeta json:metadata,omitemptyUsername string json:username gorm:column:username validate:omitemptySecretID string json:secretID gorm:column:secretID validate:omitemptySecretKey string json:secretKey gorm:column:secretKey validate:omitempty// Required: trueExpires int64 json:expires gorm:column:expires validate:omitemptyDescription string json:description gorm:column:description validate:description }资源自有的属性会因资源不同而不同。这里我们来重点看一下公共属性 ObjectMeta 它的定义如下 type ObjectMeta struct {ID uint64 json:id,omitempty gorm:primary_key;AUTO_INCREMENT;column:idInstanceID string json:instanceID,omitempty gorm:unique;column:instanceID;type:varchar(32);not nullName string json:name,omitempty gorm:column:name;type:varchar(64);not null validate:nameExtend Extend json:extend,omitempty gorm:- validate:omitemptyExtendShadow string json:- gorm:column:extendShadow validate:omitemptyCreatedAt time.Time json:createdAt,omitempty gorm:column:createdAtUpdatedAt time.Time json:updatedAt,omitempty gorm:column:updatedAt }接下来我来详细介绍公共属性中每个字段的含义及作用。 ID 这里的 ID映射为 MariaDB 数据库中的 id 字段。id 字段在一些应用中会作为资源的唯一标识。但 iam-apiserver 中没有使用 ID 作为资源的唯一标识而是使用了 InstanceID。iam-apiserver 中 ID 唯一的作用是跟数据库 id 字段进行映射代码中并没有使用到 ID。 InstanceID InstanceID 是资源的唯一标识格式为resource identifier-xxxxxx。其中resource identifier是资源的英文标识符号xxxxxx是随机字符串。字符集合为 abcdefghijklmnopqrstuvwxyz1234567890长度6例如 secret-yj8m30、user-j4lz3g、policy-3v18jq。 腾讯云、阿里云、华为云也都是采用这种格式的字符串作为资源唯一标识的。 InstanceID 的生成和更新都是自动化的通过 gorm 提供的 AfterCreate Hooks 在记录插入数据库之后生成并更新到数据库的 instanceID字段 func (s *Secret) AfterCreate(tx *gorm.DB) (err error) {s.InstanceID idutil.GetInstanceID(s.ID, secret-)return tx.Save(s).Error }上面的代码在 Secret 记录插入到 iam 数据库的 secret 表之后调用 idutil.GetInstanceID生成 InstanceID并通过 tx.Save(s)更新到数据库 secret 表的 instanceID字段。 因为通常情况下应用中的 REST 资源只会保存到数据库中的一张表里这样就能保证应用中每个资源的数据库 ID 是唯一的。所以 GetInstanceID(uid uint64, prefix string) string函数使用 github.com/speps/go-hashids包提供的方法对这个数据库 ID 进行哈希最终得到一个数据库级别的唯一的字符串例如3v18jq并根据传入的 prefix得到资源的 InstanceID。 使用这种方式生成资源的唯一标识有下面这两个优点 数据库级别唯一。InstanceID 是长度可控的字符串长度最小是 6 个字符但会根据表中的记录个数动态变长。根据我的测试2176782336 条记录以内生成的 InstanceID 长度都在 6 个字符以内。长度可控的另外一个好处是方便记忆和传播。 这里需要你注意如果同一个资源分别存放在不同的表中那在使用这种方式时生成的 InstanceID 可能相同不过概率很小几乎为零。这时候我们就需要使用分布式 ID 生成技术。这又是另外一个话题了这里不再扩展讲解。 在实际的开发中不少开发者会使用数据库数字段 ID例如 121和 36/64 位的 UUID例如 20cd59d4-08c6-4e86-a9d4-a0e51c420a04 来作为资源的唯一标识。相较于这两种资源标识方式使用resource identifier-xxxxxx这种标识方式具有以下优点 看标识名就知道是什么类型的资源例如secret-yj8m30说明该资源是 secret 类型的资源。在实际的排障过程中能够有效减少误操作。长度可控占用数据库空间小。iam-apiserver 的资源标识长度基本可以认为是 12 个字符secret/policy 是 6 个字符再加 6 位随机字符。如果使用 121 这类数值作为资源唯一标识相当于间接向友商透漏系统的规模是一定要禁止的。 另外还有一些系统如 Kubernetes 中使用资源名作为资源唯一标识。这种方式有个弊端就是当系统中同类资源太多时创建资源很容易重名你自己想要的名字往往填不了所以 iam-apiserver 不采用这种设计方式。 我们使用 instanceID 来作为资源的唯一标识在代码中就经常需要根据 instanceID 来查询资源。所以在数据库中要设置该字段为唯一索引一方面可以防止 instanceID 不唯一另一方面也能加快查询速度。 Name Name 即资源的名字我们可以通过名字很容易地辨别一个资源。 Extend、ExtendShadow Extend 和 ExtendShadow 是 iam-apiserver 设计的又一大亮点。 在实际开发中我们经常会遇到这个问题随着业务发展某个资源需要增加一些属性这时我们可能会选择在数据库中新增一个数据库字段。但是随着业务系统的演进数据库中的字段越来越多我们的 Code 也要做适配最后就会越来越难维护。 我们还可能遇到这种情况我们将上面说的字段保存在数据库中叫 meta的字段中数据库中 meta字段的数据格式是{disable:true,tag:colin}。但是我们如果想在代码中使用这些字段需要 Unmarshal 到一个结构体中例如 metaData : {disable:true,tag:colin} meta : make(map[string]interface{}) if err : json.Unmarshal([]byte(metaData), meta); err ! nil {return err }再存入数据中时又要 Marshal 成 JSON 格式的字符串例如 meta : map[string]interface{}{disable: true, tag: colin} data, err : json.Marshal(meta) if err ! nil {return err }你可以看到这种 Unmarshal 和 Marshal 操作有点繁琐。 因为每个资源都可能需要用到扩展字段那么有没有一种通用的解决方案呢iam-apiserver 就通过 Extend 和 ExtendShadow 解决了这个问题。 Extend 是 Extend 类型的字段Extend 类型其实是 map[string]interface{}的类型别名。在程序中我们可以很方便地引用 Extend 包含的属性也就是 map 的 key。Extend 字段在保存到数据库中时会自动 Marshal 成字符串保存在 ExtendShadow 字段中。 ExtendShadow 是 Extend 在数据库中的影子。同样当从数据库查询数据时ExtendShadow 的值会自动 Unmarshal 到 Extend 类型的变量中供程序使用。 具体实现方式如下 借助 gorm 提供的 BeforeCreate、BeforeUpdate Hooks在插入记录、更新记录时将 Extend 的值转换成字符串保存在 ExtendShadow 字段中并最终保存在数据库的 ExtendShadow 字段中。借助 gorm 提供的 AfterFind Hooks在查询数据后将 ExtendShadow 的值 Unmarshal 到 Extend 字段中之后程序就可以通过 Extend 字段来使用其中的属性。 CreatedAt 资源的创建时间。每个资源在创建时我们都应该记录资源的创建时间可以帮助后期进行排障、分析等。 UpdatedAt 资源的更新时间。每个资源在更新时我们都应该记录资源的更新时间。资源更新时该字段由 gorm 自动更新。 可以看到ObjectMeta 结构体包含了很多字段每个字段都完成了很酷的功能。那么如果把 ObjectMeta 作为所有资源的公共属性这些资源就会自带这些能力。 当然有些开发者可能会说User 资源其实是不需要 user-xxxxxx这种资源标识的所以 InstanceID 这个字段其实是无用的字段。但是在我看来和功能冗余相比功能规范化、不重复造轮子以及 ObjectMeta 的其他功能更加重要。所以也建议所有的 REST 资源都使用统一的资源元数据。 统一的返回 在18 讲 中我们介绍过 API 的接口返回格式应该是统一的。要想返回一个固定格式的消息最好的方式就是使用同一个返回函数。因为 API 接口都是通过同一个函数来返回的其返回格式自然是统一的。 IAM 项目通过 github.com/marmotedu/component-base/pkg/core 包提供的 WriteResponse 函数来返回结果。WriteResponse 函数定义如下 func WriteResponse(c *gin.Context, err error, data interface{}) {if err ! nil {log.Errorf(%#v, err)coder : errors.ParseCoder(err)c.JSON(coder.HTTPStatus(), ErrResponse{Code: coder.Code(),Message: coder.String(),Reference: coder.Reference(),})return}c.JSON(http.StatusOK, data) }可以看到WriteResponse 函数会判断 err 是否为 nil。如果不为 nil则将 err 解析为 github.com/marmotedu/errors包中定义的 Coder 类型的错误并调用 Coder 接口提供的 Code() 、String() 、Reference() 方法获取该错误的业务码、对外展示的错误信息和排障文档。如果 err 为 nil则调用 c.JSON返回 JSON 格式的数据。 并发处理模板 在 Go 项目开发中经常会遇到这样一种场景查询列表接口时查询出了多条记录但是需要针对每一条记录做一些其他逻辑处理。因为是多条记录比如 100 条处理每条记录延时如果为 X 毫秒串行处理完 100 条记录整体延时就是 100 * X 毫秒。如果 X 比较大那整体处理完的延时是非常高的会严重影响 API 接口的性能。 这时候我们自然就会想到利用 CPU 的多核能力并发来处理这 100 条记录。这种场景我们在实际开发中经常遇到有必要抽象成一个并发处理模板这样以后在查询时就可以使用这个模板了。 例如iam-apiserver 中查询用户列表接口 List 还需要返回每个用户所拥有的策略个数。这就用到了并发处理。这里我试着将其抽象成一个模板模板如下 func (u *userService) List(ctx context.Context, opts metav1.ListOptions) (*v1.UserList, error) {users, err : u.store.Users().List(ctx, opts)if err ! nil {log.L(ctx).Errorf(list users from storage failed: %s, err.Error())return nil, errors.WithCode(code.ErrDatabase, err.Error())}wg : sync.WaitGroup{}errChan : make(chan error, 1)finished : make(chan bool, 1)var m sync.Map// Improve query efficiency in parallelfor _, user : range users.Items {wg.Add(1)go func(user *v1.User) {defer wg.Done()// some cost time processpolicies, err : u.store.Policies().List(ctx, user.Name, metav1.ListOptions{})if err ! nil {errChan - errors.WithCode(code.ErrDatabase, err.Error())return}m.Store(user.ID, v1.User{...Phone: user.Phone,TotalPolicy: policies.TotalCount,})}(user)}go func() {wg.Wait()close(finished)}()select {case -finished:case err : -errChan:return nil, err}// infos : make([]*v1.User, 0)infos : make([]*v1.User, 0, len(users.Items))for _, user : range users.Items {info, _ : m.Load(user.ID)infos append(infos, info.(*v1.User))}log.L(ctx).Debugf(get %d users from backend storage., len(infos))return v1.UserList{ListMeta: users.ListMeta, Items: infos}, nil }在上面的并发模板中我实现了并发处理查询结果中的三个功能 第一个功能goroutine 报错即返回。goroutine 中代码段报错时会将错误信息写入 errChan中。我们通过 List 函数中的 select 语句实现只要有一个 goroutine 发生错误即返回 select { case -finished: case err : -errChan:return nil, err }第二个功能保持查询顺序。我们从数据库查询出的列表是有顺序的比如默认按数据库 ID 字段升序排列或者我们指定的其他排序方法。在并发处理中这些顺序会被打断。但为了确保最终返回的结果跟我们预期的排序效果一样在并发模板中  上面的模板中我们将处理后的记录保存在 map 中map 的 key 为数据库 ID。并且在最后按照查询的 ID 顺序依次从 map 中取出 ID 的记录例如 var m sync.Mapfor _, user : range users.Items {...go func(user *v1.User) {...m.Store(user.ID, v1.User{})}(user)}...infos : make([]*v1.User, 0, len(users.Items))for _, user : range users.Items {info, _ : m.Load(user.ID)infos append(infos, info.(*v1.User))}通过上面这种方式可以确保最终返回的结果跟从数据库中查询的结果保持一致的排序。 第三个功能并发安全。Go 语言中的 map 不是并发安全的要想实现并发安全需要自己实现如加锁或者使用 sync.Map。上面的模板使用了 sync.Map。 当然了如果期望 List 接口能在期望时间内返回还可以添加超时机制例如 select {case -finished:case err : -errChan:return nil, errcase -time.After(time.Duration(30 * time.Second)):return nil, fmt.Errorf(list users timeout after 30 seconds)}有很多优秀的协程包可供我们直接使用比如 ants 、 tunny 等。 其他特性 插件化选择 JSON 库 Golang 提供的标准 JSON 解析库 encoding/json在开发高性能、高并发的网络服务时会产生性能问题。所以很多开发者在实际的开发中往往会选用第三方的高性能 JSON 解析库例如 jsoniter 、 easyjson 、 jsonparser 等。 我见过的很多开发者选择了 jsoniter也有一些开发者使用了 easyjson。jsoniter 的性能略高于 encoding/json。但随着 go 版本的迭代encoding/json 库的性能也越来越高jsoniter 的性能优势也越来越有限。所以IAM 项目使用了 jsoniter 库并准备随时切回 encoding/json 库。 为了方便切换不同的 JSON 包iam-apiserver 采用了一种插件化的机制来使用不同的 JSON 包。具体是通过使用 go 的标签编译选择运行的解析库来实现的。 标签编译就是在源代码里添加标注通常称之为编译标签build tag。编译标签通过注释的方式在靠近源代码文件顶部的地方添加。go build 在构建一个包的时候会读取这个包里的每个源文件并且分析编译便签这些标签决定了这个源文件是否参与本次编译。例如 // build jsoniterpackage jsonimport jsoniter github.com/json-iterator/gobuild jsoniter就是编译标签。这里要注意一个源文件可以有多个编译标签多个编译标签之间是逻辑“与”的关系一个编译标签可以包括由空格分割的多个标签这些标签是逻辑“或”的关系。例如 // build linux darwin // build 386那具体来说我们是如何实现插件化选择 JSON 库的呢 首先我自定义了一个 github.com/marmotedu/component-base/pkg/json json 包来适配 encoding/json 和 json-iterator。github.com/marmotedu/component-base/pkg/json 包中有两个文件 json.go映射了 encoding/json 包的 Marshal、Unmarshal、MarshalIndent、NewDecoder、NewEncoder 方法。jsoniter.go映射了 github.com/json-iterator/go 包的 Marshal、Unmarshal、MarshalIndent、NewDecoder、NewEncoder。 json.go 和 jsoniter.go 通过编译标签让 Go 编译器在构建代码时选择使用哪一个 json 文件。 接着通过在执行 go build时指定 -tags 参数来选择编译哪个 json 文件。 json/json.go、json/jsoniter.go 这两个 Go 文件的顶部都有一行注释 // build !jsoniter// build jsoniter// build !jsoniter表示tags 不是 jsoniter 的时候编译这个 Go 文件。// build jsoniter表示tags 是 jsoniter 的时候编译这个 Go 文件。也就是说这两种条件是互斥的只有当 tagsjsoniter 的时候才会使用 json-iterator其他情况使用 encoding/json。 例如如果我们想使用包可以这么编译项目 $ go build -tagsjsoniter在实际开发中我们需要根据场景来选择合适的JSON 库。这里我给你一些建议。 场景一结构体序列化和反序列化场景 在这个场景中我个人首推的是官方的 JSON 库。可能你会比较意外那我就来说说我的理由 首先虽然 easyjson 的性能压倒了其他所有开源项目但它有一个最大的缺陷那就是需要额外使用工具来生成这段代码而对额外工具的版本控制就增加了运维成本。当然如果你的团队已经能够很好地处理 protobuf 了也是可以用同样的思路来管理 easyjson 的。 其次虽然 Go 1.8 之前官方 JSON 库的性能总是被大家吐槽但现在1.16.3官方 JSON 库的性能已不可同日而语。此外作为使用最为广泛而且没有之一的 JSON 库官方库的 bug 是最少的兼容性也是最好的 最后jsoniter 的性能虽然依然优于官方但没有达到逆天的程度。如果你追求的是极致的性能那么你应该选择 easyjson 而不是 jsoniter。jsoniter 近年已经不活跃了比如说我前段时间提了一个 issue 没人回复于是就上去看了下 issue 列表发现居然还遗留着一些 2018 年的 issue。 场景二非结构化数据的序列化和反序列化场景 这个场景下我们要分高数据利用率和低数据利用率两种情况来看。你可能对数据利用率的高低没啥概念那我举个例子JSON 数据的正文中如果说超过四分之一的数据都是业务需要关注和处理的那就算是高数据利用率。 在高数据利用率的情况下我推荐使用 jsonvalue。 至于低数据利用率的情况还可以根据 JSON 数据是否需要重新序列化分成两种情况。 如果无需重新序列化这个时候选择 jsonparser 就行了因为它的性能实在是耀眼。 如果需要重新序列化这种情况下你有两种选择如果对性能要求相对较低可以使用 jsonvalue如果对性能的要求高并且只需要往二进制序列中插入一条数据那么可以采用 jsoniter 的 Set 方法。 实际操作中超大 JSON 数据量并且同时需要重新序列化的情况非常少往往是在代理服务器、网关、overlay 中继服务等同时又需要往原数据中注入额外信息的时候。换句话说jsoniter 的适用场景比较有限。 下面是从 10%到 60%数据覆盖率下不同库的操作效率对比纵坐标单位μs/op 可以看到当 jsoniter 的数据利用率达到 25% 时和 jsonvalue、jsonparser 相比就已经没有任何优势至于 jsonvalue由于对数据做了一次性的全解析因此解析后的数据存取耗时极少因此在不同数据覆盖率下的耗时都很稳定。 调用链实现 调用链对查日志、排障帮助非常大。所以在 iam-apiserver 中也实现了调用链通过 requestID来串联整个调用链。 具体是通过以下两步来实现的 第一步将 ctx context.Context 类型的变量作为函数的第一个参数在函数调用时传递。 第二步不同函数中通过 log.L(ctx context.Context)来记录日志。 在请求到来时请求会通过 Context 中间件处理 func Context() gin.HandlerFunc {return func(c *gin.Context) {c.Set(log.KeyRequestID, c.GetString(XRequestIDKey))c.Set(log.KeyUsername, c.GetString(UsernameKey))c.Next()} }在 Context 中间件中会在 gin.Context 类型的变量中设置 log.KeyRequestID键其值为 36 位的 UUID。UUID 通过 RequestID 中间件来生成并设置在 gin 请求的 Context 中。 RequestID 中间件在 Context 中间件之前被加载所以在 Context 中间件被执行时能够获取到 RequestID 生成的 UUID。 log.L(ctx context.Context)函数在记录日志时会从头 ctx 中获取到 log.KeyRequestID并作为一个附加字段随日志打印。 通过以上方式我们最终可以形成 iam-apiserver 的请求调用链日志示例如下 2021-07-19 19:41:33.472 INFO    apiserver       apiserver/auth.go:205   user admin is authenticated.  {requestID: b6c56cd3-d095-4fd5-a928-291a2e33077f, username: admin} 2021-07-19 19:41:33.472 INFO    apiserver       policy/create.go:22     create policy function called.  {requestID: b6c56cd3-d095-4fd5-a928-291a2e33077f, username: admin} ...另外ctx context.Context作为函数/方法的第一个参数还有一个好处是方便后期扩展。例如如果我们有以下调用关系 package mainimport fmtfunc B(name, address string) string {return fmt.Sprintf(name: %s, address: %s, name, address) }func A() string {return B(colin, sz) }func main() {fmt.Println(A()) }上面的代码最终调用 B函数打印出用户名及其地址。如果随着业务的发展希望 A 调用 B 时传入用户的电话B 中打印出用户的电话号码。这时候我们可能会考虑给 B 函数增加一个电话号参数例如 func B(name, address, phone string) string {return fmt.Sprintf(name: %s, address: %s, phone: %s, name, address) }如果我们后面还要增加年龄、性别等属性呢按这种方式不断增加 B 函数的参数不仅麻烦而且还要改动所有调用 B 的函数工作量也很大。这时候可以考虑通过 ctx context.Context 来传递这些扩展参数实现如下 package mainimport (contextfmt )func B(ctx context.Context, name, address string) string {return fmt.Sprintf(name: %s, address: %s, phone: %v, name, address, ctx.Value(phone)) }func A() string {ctx : context.WithValue(context.TODO(), phone, 1812884xxxx)return B(ctx, colin, sz) }func main() {fmt.Println(A()) }这样我们下次需要新增参数的话只需要调用 context 的 WithValue 方法 ctx context.WithValue(ctx, sex, male)在 B 函数中通过 context.Context 类型的变量提供的 Value 方法从 context 中获取 sex key 即可 return fmt.Sprintf(name: %s, address: %s, phone: %v, sex: %v, name, address, ctx.Value(phone), ctx.Value(sex))数据一致性 为了提高 iam-authz-server 的响应性能我将密钥和授权策略信息缓存在 iam-authz-server 部署机器的内存中。同时为了实现高可用我们需要保证 iam-authz-server 启动的实例个数至少为两个。这时候我们会面临数据一致性的问题所有 iam-authz-server 缓存的数据要一致并且跟 iam-apiserver 数据库中保存的一致。iam-apiserver 通过如下方式来实现数据一致性 具体流程如下 第一步iam-authz-server 启动时会通过 grpc 调用 iam-apiserver 的 GetSecrets 和 GetPolicies 接口获取所有的密钥和授权策略信息。 第二步当我们通过控制台调用 iam-apiserver 密钥/授权策略的写接口POST、PUT、DELETE时会向 Redis 的 iam.cluster.notifications通道发送 SecretChanged/PolicyChanged 消息。 第三步iam-authz-server 会订阅 iam.cluster.notifications通道当监听到有 SecretChanged/PolicyChanged 消息时会请求 iam-apiserver 拉取所有的密钥/授权策略。 通过 Redis 的 Sub/Pub 机制保证每个 iam-authz-server 节点的缓存数据跟 iam-apiserver 数据库中保存的数据一致。所有节点都调用 iam-apiserver 的同一个接口来拉取数据通过这种方式保证所有 iam-authz-server 节点的数据是一致的。 总结 今天我和你分享了 iam-apiserver 的一些关键功能实现并介绍了我的设计思路。这里我再简要梳理下。 为了保证进程关停时HTTP 请求执行完后再断开连接进程中的任务正常完成iam-apiserver 实现了优雅关停功能。为了避免进程存在但服务没成功启动的异常场景iam-apiserver 实现了健康检查机制。Gin 中间件可通过配置文件配置从而实现按需加载的特性。为了能够直接辨别出 API 的版本iam-apiserver 将 API 的版本标识放在 URL 路径中例如 /v1/secrets。为了能够最大化地共享功能代码iam-apiserver 抽象出了统一的元数据每个 REST 资源都具有这些元数据。因为 API 接口都是通过同一个函数来返回的其返回格式自然是统一的。因为程序中经常需要处理并发逻辑iam-apiserver 抽象出了一个通用的并发模板。为了方便根据需要切换 JSON 库我们实现了插件化选择 JSON 库的功能。为了实现调用链功能iam-apiserver 不同函数之间通过 ctx context.Context 来传递 RequestID。iam-apiserver 通过 Redis 的 Sub/Pub 机制来保证数据一致性。 课后练习 思考一下在你的项目开发中使用过哪些更好的并发处理方式欢迎你在留言区分享。试着给 iam-apiserver 增加一个新的、可配置的 Gin 中间件用来实现 API 限流的效果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/89524.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

哪些是asp网站iis网页提示网站建设中

什么是函数指针?如何定义和使用函数指针? 函数指针是指向函数的指针,它存储了函数的地址,通过这个地址,程序可以间接地调用并执行这个函数。函数指针在C语言中常用于实现回调函数、函数表等高级功能,提高了…

高端网站建设机构坪地网站建设信息

掘金输出的时间数据处理方法掘金在为使用者提供数据时,有一类数据处理起来有些麻烦,这类数据就是时间数据。它们长这样:或者这样:查看一下它们的类型,发现有datetime,datetime64,Timestamp等等。这么多各种各样的类型&…

计算机应用技术专业网站开发方向wordpress网站排行榜

最近,我们想对MongoDB在大量连接中的行为进行基准测试。这使我不得不重新讨论该主题,并刷新关于如何在Linux服务器上创建大量连接和线程的记忆。在此过程中,我发现了一些没有使用过的新可调参数。MongoDB配置甚至MongoDB本身也可以选择限制传…

百度创建网站吗网站的设计

环境信息 minio版本 :最新 k8s 版本1.22 使用nfs作为共享存储 一.单节点安装包部署 脚本部署,一键部署,单节点应用于数据量小,一些缓存存储,比如gitlab-runner的产物数据,maven的打包依赖数据 #!/bin/bash# 步骤…

网站开发技术方案与实施手机钓鱼网站免费制作

LinkedHashSet是Set集合的一个实现,具有set集合不重复的特点,同时具有可预测的迭代顺序,也就是我们插入的顺序。并且linkedHashSet是一个非线程安全的集合。如果有多个线程同时访问当前linkedhashset集合容器,并且有一个线程对当前…

建站用什么搭建比较好电子商务网站建设技术解决方案

转载自 Java对象的引用类型 Java对象的引用类型有强引用,软引用,弱引用,虚引用和FinalReference,提供这几种引用类型的主要目的: 1.程序员可以通过不同的引用方式决定某些对象的生命周期; 2.利用JVM的垃圾回收机制&a…

求职网站怎么做关于网站建设的网站有哪些

在C#中,什么是委托(Delegate)?请简要说明委托的概念,并提供一个简单的示例说明如何使用委托。 答案: 委托的概念: 委托是一种类型,它允许将方法作为参数传递,使得可以…

电商设计网站哪个好哪些网站的简历做的比较好

蜂鸣器常用分类从两方面 声源类型:压电蜂鸣器( Piezoceramic Element Buzzers )、电磁蜂鸣器( Magnetic Buzzers ) 驱动类型:有源蜂鸣器( Indicators )、无源蜂鸣器( Transducers ) 一、电磁式蜂鸣器 无源电磁式设计电路 电磁蜂鸣器的线圈类似于电感&am…

东南亚cod建站工具住房和城乡建设部网站标准下载

本文介绍了OpenHub框架 -基于Apache Camel的新的开源集成解决方案。 本文回答了一些问题,为什么您应该关心另一个集成框架,强弱属性以及如何使用OpenHub启动新项目。 OpenHub框架是Apache Camel,但经过改进…… 当然,您只能使用A…

wordpress怎么设置跳站外链接网站型销售怎么做的

1.map是什么? map函数是Python中的一个内置函数,用于将一个函数应用到一个或多个可迭代对象的每个元素上,生成一个新的可迭代对象。它的一般形式是: map(function, iterable1, iterable2, ...)其中,function是一个函…

北京网站建设建设公司上海网站建设平台

Linear Decoders Deep Learning and Unsupervised Feature Learning Tutorial Solutions 以三层的稀疏编码神经网络而言,在sparse autoencoder中的输出层满足下面的公式 从公式中可以看出,a3的输出值是f函数的输出,而在普通的sparse autoenc…

广州商城建站搭建公司内部网站

1.概要这一系列将进行PrismWPF技术的实战讲解。实战项目内容选型为Email邮件收发的客户端(WeMail),项目结构简单方便大家理解。相关技术:C#、WPF、Prism软件开发环境:VS2019 、 .NET5 、 windows11需掌握技能&#xf…

利用业务时间做的网站与公司有关吗在百度上怎么搜到自己的网站

文章目录 openssl3.2 - xx_fetch函数参数名称字符串有效值列表概述笔记xx_fetch函数所在的头文件目录xx_fetch函数所在的头文件列表xx_fetch函数列表每个xx_fetch()API的字符串名称的有效值列表OSSL_DECODER *OSSL_DECODER_fetch();OSSL_ENCODER *OSSL_ENCODER_fetch();EVP_CIP…

dw中怎样做网站二级页面wordpress迁移后无法登录

ChatGPT与文心一言:两大AI助手智能回复、语言准确性、知识库丰富度比较 在现代科技飞速发展的时代,人工智能已经成为了我们生活中不可或缺的一部分。特别是在对话AI领域,两大巨头ChatGPT和文心一言以其出色的性能和广泛的应用引起了大家的广…

网站建设重要意义用自己的电脑做主机建网站

一、题目 新年第一题,避免老年痴呆! 题源:牛客 一个数组A中存有 n 个整数,在不允许使用另外数组的前提下,将每个整数循环向右移 M( M >0)个位置,即将A中的数据由(A0…

网站建设应遵循的原则小程序开发公司如何寻找客户

这是一款简单的数据库文档生成工具,主要实现了SQlServer生成说明文档的小工具,目前不够完善,主要可以把数据库的表以及表的详细字段信息,导出到Word中,可以方便开发人员了解数据库的信息或写技术说明文档。技术上主要采…

怎样做企业网站备案wordpress 菜单跳转

Itoh首先给出了传统解包裹算法的数学描述!。传统的相位解包裹操作是通过对空间相邻点相位值的比较来完成的。根据抽样定理,如果相邻采样点的相位差不超过z,则对应的相位解包裹处理是非常简单的,理论上以某点为起始点沿某一路径对包裹相位的差…

windows部署网站php电商平台seo

注意:本文下载的资源,与以下文章的思路有相同点,也有不同点,最终目标只是让读者从多维度去熟练掌握本知识点。 使用Python进行数据挖掘项目开发,采用深度学习方法为图像中的物体进行分类,可以按照以下步骤进…

连云港网站建设案例策划公司起名

本次系列使用的所需部署包版本都使用的目前最新的或最新稳定版,安装包地址请到公众号内回复【K8s实战】获取 介绍 Helm 是 Deis 开发的一个用于 Kubernetes 应用的包管理工具,主要用来管理 Charts。有点类似于 Ubuntu 中的 APT 或 CentOS 中的 YUM。Helm…

采购网站有哪些wordpress 前台密码

Boost::Asio为同步和异步操作提供了并行支持,异步支持基于前摄器模式,这种模式的优点和缺点可能比只同步或反应器方法要低。让我们检查一下Boost::Asio是如何实现前摄器模式的,没有引用基于平台的细节。前摄器设计模式,改编自POSA…