32-数据处理:如何高效处理应用程序产生的数据?

 如何更好地进行异步数据处理。

一个大型应用为了后期的排障、运营等,会将一些请求数据保存在存储系统中 。例如:应用将请求日志保存到 Elasticsearch 中,方便排障;网关将 API 请求次数、请求消息体等数据保存在数据库中,供控制台查询展示。

为了满足这些需求,我们需要进行数据采集,数据采集在大型应用中很常见,但我发现不少开发者设计的数据采集服务, 

  • 采集服务只针对某个采集需求开发,如果采集需求有变,需要修改主代码逻辑,代码改动势必会带来潜在的 Bug,增加开发测试工作量。
  • 数据采集服务会导致已有的服务请求延时变高。
  • 采集数据性能差,需要较长时间才能采集完一批数据。
  • 启停服务时,会导致采集的数据丢失。

 

数据采集方式的分类

 

目前,数据采集主要有两种方式,分别是同步采集和异步采集。 

 

数据采集系统设计

 

 如何在采集过程中不影响程序的性能?

答案就是让数据采集模型化。 

 

设计数据采集系统时需要解决的核心问题

采集系统首先需要一个数据源 Input,Input 可以是一个或者多个,Input 中的数据来自于应用程序上报。采集后的数据通常需要经过处理,比如格式化、增删字段、过滤无用的数据等,然后将处理后的数据存储到下游系统(Output)中,如下图所示:

这里,我们需要解决这 3 个核心问题:

  • 进行数据采集,就需要在正常流程中多加一个上报数据环节,这势必会影响程序的性能。那么,如何让程序的性能损失最小化?
  • 如果 Input 产生数据的速度大于 Output 的消费能力,产生数据堆积怎么办?
  • 数据采集后需要存储到下游系统。在存储之前,我们需要对数据进行不同的处理,并可能会存储到不同的下游系统,这种可变的需求如何满足?

对于让程序性能损失最小化这一点,缓存批量上报。

对于数据堆积这个问题,比较好的解决方法是,将采集的数据先上报到一些具有高吞吐量、可以存储大量数据的中间组件,比如 Kafka、Redis 中。 

对于采集需求多样化这个问题,我们可以将采集程序做成插件化、可扩展的,满足可变的需求。

 

数据上报功能设计

为了提高异步上报的吞吐量,你可以将数据缓存在内存中(Go 中可以使用有缓冲 channel),并使用多个 worker 去消费内存中的数据。使用多个 worker ,可以充分发挥 CPU 的多核能力。另外,上报给下游系统时,你也可以采用批量上报的方式。

数据采集功能设计

现代应用程序越来越讲究插件化、扩展性,在设计采集系统时,也应该考虑到未来的需求。比如,未来你可能需要将数据从上报到 MongoDB 切换到 HBase 中,或者同时将数据上报到 MongoDB 和 HBase 中。因此,上报给下游的程序逻辑要具有插件化的能力,并能通过配置选择需要的插件。

为了提高程序性能,会先把数据缓存在内存中。但是这样有个缺点:在关停程序时,内存中的数据就会丢失。所以,在程序结束之前,我们需要确保内存中的数据能够上报成功,也就是说采集程序需要实现优雅关停功能。优雅关停不仅要确保缓存中的数据被成功上报,还要确保正在处理的数据被成功上报。

当然了,既然是数据采集,还要能够配置采集的频率。最后,因为采集程序通常是非 API 类型的,所以还需要对外暴露一个特殊的 API,用来返回采集程序的健康状态。

数据采集应用模型

通过上面的分析和设计,可以绘制出下面这个采集模型:

 

数据采集系统落地项目:iam-authz-server + iam-pump

 

  • iam-authz-server:实现数据上报功能。
  • iam-pump:实现数据采集功能。

 

iam-authz-server:数据上报

 我们主要的解决思路就是异步上报数据。

 

iam-authz-server 的数据上报架构如下图所示:

iam-authz-server 服务中的数据上报功能可以选择性开启,开启代码见 internal/authzserver/server.go ,代码如下:

 if s.analyticsOptions.Enable {                                           analyticsStore := storage.RedisCluster{KeyPrefix: RedisKeyPrefix}              analyticsIns := analytics.NewAnalytics(s.analyticsOptions, &analyticsStore)    analyticsIns.Start()                                                   s.gs.AddShutdownCallback(shutdown.ShutdownFunc(func(string) error {    analyticsIns.Stop()    return nil    }))    }     

 

在上面的代码中,通过 NewAnalytics 创建一个数据上报服务,代码如下:

func NewAnalytics(options *AnalyticsOptions, store storage.AnalyticsHandler) *Analytics {                              ps := options.PoolSize                                                                                             recordsBufferSize := options.RecordsBufferSize                                                                     workerBufferSize := recordsBufferSize / uint64(ps)                                                                 log.Debug("Analytics pool worker buffer size", log.Uint64("workerBufferSize", workerBufferSize))                   recordsChan := make(chan *AnalyticsRecord, recordsBufferSize)                                                      return &Analytics{                                                                                                 store:                      store,                                                                             poolSize:                   ps,                                                                                recordsChan:                recordsChan,                                                                       workerBufferSize:           workerBufferSize,                                                                  recordsBufferFlushInterval: options.FlushInterval,                                                             }                                                                                                                  
}      

这里的代码根据传入的参数,创建 Analytics 类型的变量并返回,变量中有 5 个字段需要你关注:

  • store: storage.AnalyticsHandler 接口类型,提供了 Connect() bool和 AppendToSetPipelined(string, byte)函数,分别用来连接 storage 和上报数据给 storage。iam-authz-server 用了 redis storage。
  • recordsChan:授权日志会缓存在 recordsChan 带缓冲 channel 中,其长度可以通过 iam-authz-server.yaml 配置文件中的 analytics.records-buffer-size 配置。
  • poolSize:指定开启 worker 的个数,也就是开启多少个 Go 协程来消费 recordsChan 中的消息。
  • workerBufferSize:批量投递给下游系统的的消息数。通过批量投递,可以进一步提高消费能力、减少 CPU 消耗。
  • recordsBufferFlushInterval:设置最迟多久投递一次,也就是投递数据的超时时间。

analytics.ecords-buffer-size 和 analytics.pool-size 建议根据部署机器的 CPU 和内存来配置。在应用真正上线前,我建议你通过压力和负载测试,来配置一个合适的值。

Analytics提供了 3 种方法:

  • Start(),用来启动数据上报服务。
  • Stop(),用来关停数据上报服务。主程序在收到系统的终止命令后,调用 Stop 方法优雅关停数据上报服务,确保缓存中的数据都上报成功。
  • RecordHit(record *AnalyticsRecord) error,用来记录 AnalyticsRecord 的数据。

通过 NewXxx (NewAnalytics)返回一个 Xxx (Analytics)类型的结构体,Xxx(Analytics) 类型带有一些方法,如下:

func NewAnalytics(options) *Analytics {...
}func (r *Analytics) Start() {...
}
func (r *Analytics) Stop() {...
}
func (r *Analytics) RecordHit(record *AnalyticsRecord) error {...
}

其实,上述代码段是一种常见的 Go 代码编写方式/设计模式。你在以后的开发生涯中,会经常遇到这种设计方式。使用上述代码设计方式有下面两个好处。

  • 功能模块化:将数据上报的功能封装成一个服务模块,数据和方法都围绕着 Xxx 结构体来展开。这和 C++、Java、Python 的类有相似的地方,你可以这么理解:Xxx 相当于类,NewXxx 相当于初始化一个类实例,Start、Stop、RecordHit 是这个类提供的方法。功能模块化可以使程序逻辑更加清晰,功能更独立、更好维护,也可以供其他应用使用。
  • 方便数据传递:可以将数据存放在 Xxx 结构体字段中,供不同的方法共享使用,如果有并发,数据共享时,注意要给非并发安全的类型加锁,例如recordsChan。

 

启动服务:启动数据上报服务

在服务启动时,首先要启动数据上报功能模块。我们通过调用 analyticsIns.Start() 启动数据上报服务。Start 代码如下:

func (r *Analytics) Start() {analytics = rr.store.Connect()// start worker poolatomic.SwapUint32(&r.shouldStop, 0)for i := 0; i < r.poolSize; i++ {r.poolWg.Add(1)go r.recordWorker()}// stop analytics workersgo r.Stop()
}

这里有一点需要你注意,数据上报和数据采集都大量应用了 Go 协程来并发地执行操作,为了防止潜在的并发读写引起的Bug,建议你的测试程序编译时加上 -race,例如go build -race cmd/iam-authz-server/authzserver.go。 

Start 中会开启 poolSize 个数的 worker 协程,这些协程共同消费 recordsChan 中的消息,消费逻辑见 recordWorker() ,代码如下:

func (r *Analytics) recordWorker() {defer r.poolWg.Done()// this is buffer to send one pipelined command to redis// use r.recordsBufferSize as cap to reduce slice re-allocationsrecordsBuffer := make([][]byte, 0, r.workerBufferSize)// read records from channel and processlastSentTS := time.Now()for {readyToSend := falseselect {case record, ok := <-r.recordsChan:// check if channel was closed and it is time to exit from workerif !ok {// send what is left in bufferr.store.AppendToSetPipelined(analyticsKeyName, recordsBuffer)return}// we have new record - prepare it and add to bufferif encoded, err := msgpack.Marshal(record); err != nil {log.Errorf("Error encoding analytics data: %s", err.Error())} else {recordsBuffer = append(recordsBuffer, encoded)}// identify that buffer is ready to be sentreadyToSend = uint64(len(recordsBuffer)) == r.workerBufferSizecase <-time.After(r.recordsBufferFlushInterval):// nothing was received for that period of time// anyways send whatever we have, don't hold data too long in bufferreadyToSend = true}// send data to Redis and reset bufferif len(recordsBuffer) > 0 && (readyToSend || time.Since(lastSentTS) >= recordsBufferForcedFlushInterval) {r.store.AppendToSetPipelined(analyticsKeyName, recordsBuffer)recordsBuffer = recordsBuffer[:0]lastSentTS = time.Now()}}
}

recordWorker 函数会将接收到的授权日志保存在 recordsBuffer 数组中,当数组内元素个数为 workerBufferSize ,或者距离上一次投递时间间隔为 recordsBufferFlushInterval 时,就会将 recordsBuffer 数组中的数据上报给目标系统(Input)。
recordWorker()中有些设计技巧,很值得你参考。

  • 使用 msgpack 序列化消息:msgpack 是一个高效的二进制序列化格式。它像 JSON 一样,让你可以在各种语言之间交换数据。但是它比 JSON 更快、更小。
  • 支持 Batch Windows:当 worker 的消息数达到指定阈值时,会批量投递消息给 Redis,阈值判断代码为readyToSend = uint64(len(recordsBuffer)) == r.workerBufferSize
  • 超时投递:为了避免因为产生消息太慢,一直达不到 Batch Windows,无法投递消息这种情况,投递逻辑也支持超时投递,通过 case <-time.After(r.recordsBufferFlushInterval)代码段实现。
  • 支持优雅关停:当 recordsChan 关闭时,将 recordsBuffer 中的消息批量投递给 Redis,之后退出 worker 协程。

这里有个注意事项:投递完成后,你需要重置 recordsBuffer 和计时器,否则会重复投递数据:

recordsBuffer = recordsBuffer[:0]
lastSentTS = time.Now()

这里还设置了一个最大的超时时间 recordsBufferForcedFlushInterval,确保消息最迟被投递的时间间隔。也就是说, iam-authz-server 强制要求最大投递间隔为 recordsBufferForcedFlushInterval 秒,这是为了防止配置文件将 recordsBufferFlushInterval 设得过大。

运行服务:异步上报授权日志

开启了数据上报服务后,当有授权日志产生时,程序就会自动上报数据。接下来,我会详细介绍下如何高效上报数据。

iam-authz-server 会在授权成功时调用 LogGrantedAccessRequest 函数,在授权失败时调用 LogRejectedAccessRequest 函数。并且,在这两个函数中,调用 RecordHit 函数,记录授权日志。

iam-authz-server 通过调用 RecordHit(record *AnalyticsRecord) error 函数,异步缓存授权日志。调用 RecordHit 后,会将 AnalyticsRecord 类型的消息存放到 recordsChan 有缓冲 channel 中。

这里要注意:在缓存前,需要判断上报服务是否在优雅关停中,如果在关停中,则丢弃该消息:

if atomic.LoadUint32(&r.shouldStop) > 0 {return nil
}

通过将授权日志缓写入 recordsChan 有缓冲 channel 中,LogGrantedAccessRequest 和 LogRejectedAccessRequest 函数可以不用等待授权日志上报成功就返回,这样就使得整个授权请求的性能损耗几乎为零。

关停服务:优雅关停数据上报

完成数据上报之后的下一步,就是要优雅地将数据上报关停。为了确保在应用关停时,缓存中的数据和正在投递中的数据都能够投递到 Redis,iam-authz-server 实现了数据上报关停功能,代码如下:

gs.AddShutdownCallback(shutdown.ShutdownFunc(func(string) error {analyticsIns.Stop()return nil
}))

当收到 os.Interrupt 和 syscall.SIGTERM 系统信号后,调用 analyticsIns.Stop()函数,关停数据上报服务, Stop 函数会停止接收新的授权日志,并等待正在上报的数据上报完成。

上面我介绍了数据上报部分的功能设计,接下来,我来介绍下数据采集部分的功能设计。

iam-pump:数据采集

iam-authz-server 将数据上报到 Redis,iam-pump 消费 Redis 中的数据,并保存在 MongoDB 中做持久化存储。

iam-pump 的设计要点是:插件化、可配置地将 Redis 中的数据处理后存储到下游系统中,并且实现优雅关停功能,这些也是设计数据采集程序的要点和难点所在。下面,我们就来看下 iam-pump 是如何插件化地实现一个数据采集程序的。这个数据采集程序的设计思路,在我开发的大型企业应用中有实际的落地验证,你可以放心使用。

iam-pump 数据采集架构如下图所示:

在iam-pump服务启动时,要启动数据采集功能,启动代码见 internal/pump/server.go。

接下来,我会介绍下 iam-pump 服务中的 5 部分核心代码:

  • 数据采集插件定义。
  • 初始化数据采集插件。
  • 健康检查。
  • 启动 Loop 周期性消费 Redis 数据。
  • 优雅关停数据采集服务。

初始化服务:数据采集插件定义

数据采集组件设计的核心是插件化,这里我将需要上报的系统抽象成一个个的 pump,那么如何定义 pump 接口呢?接口定义需要参考实际的采集需求,通常来说,至少需要下面这几个函数。

  • New:创建一个 pump。
  • Init:初始化一个 pump,例如,可以在 Init 中创建下游系统的网络连接。
  • WriteData:往下游系统写入数据。为了提高性能,最好支持批量写入。
  • SetFilters:设置是否过滤某条数据,这也是一个非常常见的需求,因为不是所有的数据都是需要的。
  • SetTimeout:设置超时时间。我就在开发过程中遇到过一个坑,连接 Kafka 超时,导致整个采集程序超时。所以这里需要有超时处理,通过超时处理,可以保证整个采集框架正常运行。

我之前开发过公有云的网关服务,网关服务需要把网关的请求数据转存到 MongoDB 中。 有些用户会通过网关上传非常大的文件(百 M 级别),这些数据转存到 MongoDB 中,快速消耗了 MongoDB 的存储空间(500G 存储空间)。为了避免这个问题,在转存数据时,需要过滤掉一些比较详细的数据,所以 iam-pump 添加了 SetOmitDetailedRecording 来过滤掉详细的数据。

所以,最后 iam-pump 的插件接口定义为 internal/pump/pumps/pump.go :

type Pump interface {GetName() stringNew() PumpInit(interface{}) errorWriteData(context.Context, []interface{}) errorSetFilters(analytics.AnalyticsFilters)GetFilters() analytics.AnalyticsFiltersSetTimeout(timeout int)GetTimeout() intSetOmitDetailedRecording(bool)GetOmitDetailedRecording() bool
}

你在实际开发中,如果有更多的需求,可以在 Pump interface 定义中继续添加需要的处理函数。

初始化服务:初始化数据采集插件

定义好插件之后,需要初始化插件。在 initialize 函数中初始化 pumps:

func (s *pumpServer) initialize() {pmps = make([]pumps.Pump, len(s.pumps))i := 0for key, pmp := range s.pumps {pumpTypeName := pmp.Typeif pumpTypeName == "" {pumpTypeName = key}pmpType, err := pumps.GetPumpByName(pumpTypeName)if err != nil {log.Errorf("Pump load error (skipping): %s", err.Error())} else {pmpIns := pmpType.New()initErr := pmpIns.Init(pmp.Meta)if initErr != nil {log.Errorf("Pump init error (skipping): %s", initErr.Error())} else {log.Infof("Init Pump: %s", pmpIns.GetName())pmpIns.SetFilters(pmp.Filters)pmpIns.SetTimeout(pmp.Timeout)pmpIns.SetOmitDetailedRecording(pmp.OmitDetailedRecording)pmps[i] = pmpIns}}i++}
}

initialize 会创建、初始化,并调用 SetFilters、SetTimeout、SetOmitDetailedRecording 来设置这些 pump。Filters、Timeout、OmitDetailedRecording 等信息在 pump 的配置文件中指定。

这里有个技巧你也可以注意下:pump 配置文件支持通用的配置,也支持自定义的配置,配置结构为 PumpConfig :

type PumpConfig struct {Type                  stringFilters               analytics.AnalyticsFiltersTimeout               intOmitDetailedRecording boolMeta                  map[string]interface{}
}

pump 自定义的配置可以存放在 map 类型的变量 Meta 中。通用配置可以使配置共享,减少开发和维护工作量,自定义配置可以适配不同pump的差异化配置。

初始化服务:健康检查

因为 iam-pump 是一个非 API 服务,为了监控其运行状态,这里也设置了一个健康检查接口。iam-pump 组件通过调用 server.ServeHealthCheck 函数启动一个 HTTP 服务,ServeHealthCheck 函数代码如下:

func ServeHealthCheck(healthPath string, healthAddress string) {http.HandleFunc("/"+healthPath, func(w http.ResponseWriter, r *http.Request) {w.Header().Set("Content-type", "application/json")w.WriteHeader(http.StatusOK)_, _ = w.Write([]byte(`{"status": "ok"}`))})if err := http.ListenAndServe(healthAddress, nil); err != nil {log.Fatalf("Error serving health check endpoint: %s", err.Error())}
}

该函数启动了一个 HTTP 服务,服务监听地址通过 health-check-address 配置,健康检查路径通过 health-check-path 配置。如果请求 http://<health-check-address>/<health-check-path>返回{"status": "ok"},说明 iam-pump 可以正常工作。

这里的健康检查只是简单返回了一个字符串,实际开发中,可以封装更复杂的逻辑。比如,检查进程是否可以成功 ping 通数据库,进程内的工作进程是否处于 worker 状态等。

iam-pump 默认的健康检查请求地址为http://127.0.0.1:7070/healthz 。

运行服务:启动 Loop 周期性消费 Redis 数据

初始化 pumps 之后,就可以通过 Run 函数启动消费逻辑了。在 Run 函数中,会定期(通过配置 purge-delay 设置轮训时间)从 Redis 中获取所有数据,经过 msgpack.Unmarshal 解压后,传给 writeToPumps 处理:

func (s preparedPumpServer) Run(stopCh <-chan struct{}) error {ticker := time.NewTicker(time.Duration(s.secInterval) * time.Second)defer ticker.Stop()for {select {case <-ticker.C:analyticsValues := s.analyticsStore.GetAndDeleteSet(storage.AnalyticsKeyName)if len(analyticsValues) > 0 {// Convert to something cleankeys := make([]interface{}, len(analyticsValues))for i, v := range analyticsValues {decoded := analytics.AnalyticsRecord{}err := msgpack.Unmarshal([]byte(v.(string)), &decoded)log.Debugf("Decoded Record: %v", decoded)if err != nil {log.Errorf("Couldn't unmarshal analytics data: %s", err.Error())} else {if s.omitDetails {decoded.Policies = ""decoded.Deciders = ""}keys[i] = interface{}(decoded)}}// Send to pumpswriteToPumps(keys, s.secInterval)}// exit consumption cycle when receive SIGINT and SIGTERM signalcase <-stopCh:log.Info("stop purge loop")return nil}}
}

writeToPumps 函数通过调用 execPumpWriting 函数,异步调用 pump 的 WriteData 函数写入数据。execPumpWriting 函数中有一些设计技巧,你可以注意下这两个:

  • 将一些通用的处理,例如 Filters、Timeout、OmitDetailedRecording 放在 pump 之外处理,这样可以减少 pump 中代码的重复性。
  • 优雅关停。通过如下代码实现优雅关停功能:
select {case <-stopCh:log.Info("stop purge loop")returndefault:
}

上面的代码需要放在 writeToPumps 之后,这样可以确保所有数据都成功写入 pumps 之后,再停止采集逻辑。

关停服务:优雅关停数据采集服务

在关停服务时,为了确保正在处理的数据被成功存储,还需要提供优雅关停功能。iam-pump 通过 channel 传递 SIGINT 和 SIGTERM 信号,当消费逻辑收到这两个信号后,会退出消费循环,见 Run 函数。代码如下:

func (s preparedPumpServer) Run(stopCh <-chan struct{}) error {    ticker := time.NewTicker(time.Duration(s.secInterval) * time.Second)    defer ticker.Stop()                                                     for {    select {    case <-ticker.C:    // 消费逻辑...// exit consumption cycle when receive SIGINT and SIGTERM signalcase <-stopCh:    log.Info("stop purge loop")    return nil}}
}

总结

 上报:异步缓存,批量上报

 采集:插件,可配置,健康检查,采集时间设置,优雅关闭

课后练习

  1. 思考下,如何设计一个数据上报和数据采集应用,设计时有哪些点需要注意?
  2. 动手练习下,启动 iam-authz-server 和 iam-pump 服务,验证整个流程。

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

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

相关文章

.net 6 swagger Failed to load API definition

为什么会出现这样的问题? 因为swagger用的是restful 规则同一个路径下面&#xff0c;只有一个Get、Post、Put 如果你控制器下面有多个HttpGet、HttpPost、HttpPut 请求就会报错 正确方式&#xff0c;在控制器上添加路由[Route("api/[controller]/[action]")] 或…

算法练习第四十二天|01背包问题、416. 分割等和子集

一些背包问题 01背包问题 题目描述 小明是一位科学家&#xff0c;他需要参加一场重要的国际科学大会&#xff0c;以展示自己的最新研究成果。他需要带一些研究材料&#xff0c;但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等&#xff0c;它们各自…

蓝桥杯嵌入式学习笔记(9):RTC程序设计

目录 前言 1. RTC介绍 2. 使用CubeMx进行源工程配置 3. 代码编程 3.1 准备工作 3.2 进行bsp_rtc.h编写 3.3 进行bsp_rtc.c编写 3.4 main.c编写 3.4.1 头文件引用 3.4.2 变量声明 3.4.3 子函数声明 3.4.4 函数实现 3.4.5 main函数编写 4. 代码实验 5. 总结 前言 因本人备赛蓝…

如何使用 Python 本地客户端操作读写云服务器 Redis 缓存数据库详细教程(更新中)

Redis 基本概述 Redis&#xff08;Remote Dictionary Server&#xff09;是一个开源的使用 ANSI C 语言编写的、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库&#xff0c;并提供多种语言的 API。它通常被称为数据结构服务器&#xff0c;因为值&#xff08;value…

Kubernetes(k8s):如何进行 Kubernetes 集群健康检查?

Kubernetes&#xff08;k8s&#xff09;&#xff1a;如何进行 Kubernetes 集群健康检查&#xff1f; 一、节点健康检查1、使用 kubectl 查看节点状态2、查看节点详细信息3、检查节点资源使用情况 2、Pod 健康检查2.1、 使用 kubectl 查看 Pod 状态2.2、 查看特定 Pod 的详细信息…

2012年认证杯SPSSPRO杯数学建模D题(第一阶段)人机游戏中的数学模型全过程文档及程序

2012年认证杯SPSSPRO杯数学建模 减缓热岛效应 D题 人机游戏中的数学模型 原题再现&#xff1a; 计算机游戏在社会和生活中享有特殊地位。游戏设计者主要考虑易学性、趣味性和界面友好性。趣味性是本质吸引力&#xff0c;使玩游戏者百玩不厌。网络游戏一般考虑如何搭建安全可…

zabbix 7.0 新增功能亮点(一)——T参数

概要&#xff1a; T参数是zabbix7.0新增的一项功能&#xff0c;它支持对配置文件进行可用性验证&#xff0c;即zabbix程序(server/proxy/agent等)修改配置文件后&#xff0c;支持-T或–test-config参数验证配置参数可用性。 T参数主要包含以下三个方面的应用场景&#xff1a; …

解决Centos7无法连接网络和访问网页连接不上问题

一、网络无法连接问题 网络无法连接的问题我查到了一个很良心的操作&#xff0c;不用重装&#xff0c;因为可能是你虚拟机设置上的问题。我先写我的解决方案&#xff0c;再附上其他几种解决方案。 问题一&#xff1a; 虚拟机的问题****加粗样式 解决&#xff1a; &#xff08;…

ImportError: cannot import name ‘PILLOW_VERSION‘ from ‘PIL‘

原因&#xff1a;torchvision模块在运行时要调用PIL模块的PILLOW_VERSION函数&#xff0c;但PILLOW_VERSION在Pillow 7.0.0之后的版本被移除了&#xff0c;Pillow 7.0.0之后的版本使用__version__函数代替PILLOW_VERSION函数。 解决方法&#xff1a;降低pillow版本即可。 参考…

人工智能应用工程师怎样报名,费用多少,就业机会怎么样?

人工智能应用工程师是能够利用人工智能相关技术进行应用研发&#xff0c;并开展各类工作的从业人员统称。为贯彻落实《国务院关于印发新一代人工智能发展规划的通知》、工业和信息化部《促进新一代人工智能产业发展三年行动计划&#xff08;2018-2020年&#xff09;》等文件精神…

JUC:原子类型的使用(原子整数、原子引用、原子数组、字段更新器、累加器)

文章目录 原子类型AtomicInteger 原子整数AtomicReferenc 原子引用AtomicStampedReference 带版本号的原子引用AtomicMarkableReference 仅记录是否修改的原子引用AtomicXXXArray 原子数组AtomicXXXFieldUpdater 字段更新器LongAdder累加器 原子类型 AtomicInteger 原子整数 …

部署项目遇到的各种问题总结

文章目录 前言一、后端问题 jar包运行出现错误宝塔面板使用jdk17二、数据库问题 版本问题三、前端问题 连不上后端总结 前言 在做完项目之后&#xff0c;为了让别人访问到自己的网站&#xff0c;就需要部署前端后端以及数据库&#xff0c;但是在部署的过程中出现了各种问题和困…