golang 日志分析_容器日志采集利器:Filebeat深度剖析与实践

在云原生时代和容器化浪潮中,容器的日志采集是一个看起来不起眼却又无法忽视的重要议题。对于容器日志采集我们常用的工具有filebeat和fluentd,两者对比各有优劣,相比基于ruby的fluentd,考虑到可定制性,我们一般默认选择golang技术栈的filbeat作为主力的日志采集agent。
相比较传统的日志采集方式,容器化下单节点会运行更多的服务,负载也会有更短的生命周期,而这些更容易对日志采集agent造成压力,虽然filebeat足够轻量级和高性能,但如果不了解filebeat的机制,不合理的配置filebeat,实际的生产环境使用中可能也会给我们带来意想不到的麻烦和难题。

整体架构

日志采集的功能看起来不复杂,主要功能无非就是找到配置的日志文件,然后读取并处理,发送至相应的后端如elasticsearch,kafka等。
filebeat官网有张示意图,如下所示:

90a6e684bd35d3d843fb7b9cb2e7bed9.png

针对每个日志文件,filebeat都会启动一个harvester协程,即一个goroutine,在该goroutine中不停的读取日志文件,直到文件的EOF末尾。一个最简单的表示采集目录的input配置大概如下所示:

filebeat.inputs:
- type: log# Paths that should be crawled and fetched. Glob based paths.paths:- /var/log/*.log

不同的harvester goroutine采集到的日志数据都会发送至一个全局的队列queue中,queue的实现有两种:基于内存和基于磁盘的队列,目前基于磁盘的队列还是处于alpha阶段,filebeat默认启用的是基于内存的缓存队列。
每当队列中的数据缓存到一定的大小或者超过了定时的时间(默认1s),会被注册的client从队列中消费,发送至配置的后端。目前可以设置的client有kafka、elasticsearch、redis等。

虽然这一切看着挺简单,但在实际使用中,我们还是需要考虑更多的问题,例如:

  • 日志文件是如何被filbebeat发现又是如何被采集的?
  • filebeat是如何确保日志采集发送到远程的存储中,不丢失一条数据的?
  • 如果filebeat挂掉,下次采集如何确保从上次的状态开始而不会重新采集所有日志?
  • filebeat的内存或者cpu占用过多,该如何分析解决?
  • filebeat如何支持docker和kubernetes,如何配置容器化下的日志采集?
  • 想让filebeat采集的日志发送至的后端存储,如果原生不支持,怎样定制化开发?

这些均需要对filebeat有更深入的理解,下面让我们跟随filebeat的源码一起探究其中的实现机制。

一条日志是如何被采集的

filebeat源码归属于beats项目,而beats项目的设计初衷是为了采集各类的数据,所以beats抽象出了一个libbeat库,基于libbeat我们可以快速的开发实现一个采集的工具,除了filebeat,还有像metricbeat、packetbeat等官方的项目也是在beats工程中。
如果我们大致看一下代码就会发现,libbeat已经实现了内存缓存队列memqueue、几种output日志发送客户端,数据的过滤处理processor等通用功能,而filebeat只需要实现日志文件的读取等和日志相关的逻辑即可。

从代码的实现角度来看,filebeat大概可以分以下几个模块:

  • input: 找到配置的日志文件,启动harvester
  • harvester: 读取文件,发送至spooler - spooler: 缓存日志数据,直到可以发送至publisher
  • publisher: 发送日志至后端,同时通知registrar
  • registrar: 记录日志文件被采集的状态

1. 找到日志文件

对于日志文件的采集和生命周期管理,filebeat抽象出一个Crawler的结构体, 在filebeat启动后,crawler会根据配置创建,然后遍历并运行每个input:

for _, inputConfig := range c.inputConfigs {err := c.startInput(pipeline, inputConfig, r.GetStates())}

在每个input运行的逻辑里,首先会根据配置获取匹配的日志文件,需要注意的是,这里的匹配方式并非正则,而是采用linux glob的规则,和正则还是有一些区别。

matches, err := filepath.Glob(path)

获取到了所有匹配的日志文件之后,会经过一些复杂的过滤,例如如果配置了exclude_files则会忽略这类文件,同时还会查询文件的状态,如果文件的最近一次修改时间大于ignore_older的配置,也会不去采集该文件。

2. 读取日志文件

匹配到最终需要采集的日志文件之后,filebeat会对每个文件启动harvester goroutine,在该goroutine中不停的读取日志,并发送给内存缓存队列memqueue。
(h *Harvester) Run()方法中,我们可以看到这么一个无限循环,省略了一些逻辑的代码如下所示:

for {message, err := h.reader.Next()if err != nil {switch err {case ErrFileTruncate:logp.Info("File was truncated. Begin reading file from offset 0: %s", h.state.Source)h.state.Offset = 0filesTruncated.Add(1)case ErrRemoved:logp.Info("File was removed: %s. Closing because close_removed is enabled.", h.state.Source)case ErrRenamed:logp.Info("File was renamed: %s. Closing because close_renamed is enabled.", h.state.Source)case ErrClosed:logp.Info("Reader was closed: %s. Closing.", h.state.Source)case io.EOF:logp.Info("End of file reached: %s. Closing because close_eof is enabled.", h.state.Source)case ErrInactive:logp.Info("File is inactive: %s. Closing because close_inactive of %v reached.", h.state.Source, h.config.CloseInactive)default:logp.Err("Read line error: %v; File: %v", err, h.state.Source)}return nil}...if !h.sendEvent(data, forwarder) {return nil}
}

可以看到,reader.Next()方法会不停的读取日志,如果没有返回异常,则发送日志数据到缓存队列中。
返回的异常有几种类型,除了读取到EOF外,还会有例如文件一段时间不活跃等情况发生会使harvester goroutine退出,不再采集该文件,并关闭文件句柄。
filebeat为了防止占据过多的采集日志文件的文件句柄,默认的close_inactive参数为5min,如果日志文件5min内没有被修改,上面代码会进入ErrInactive的case,之后该harvester goroutine会被关闭。
这种场景下还需要注意的是,如果某个文件日志采集中被移除了,但是由于此时被filebeat保持着文件句柄,文件占据的磁盘空间会被保留直到harvester goroutine结束。

3. 缓存队列

在memqueue被初始化时,filebeat会根据配置min_event是否大于1创建BufferingEventLoop或者DirectEventLoop,一般默认都是BufferingEventLoop,即带缓冲的队列。

type bufferingEventLoop struct {broker *Brokerbuf        *batchBufferflushList  flushListeventCount intminEvents    intmaxEvents    intflushTimeout time.Duration// active broker API channelsevents    chan pushRequestget       chan getRequestpubCancel chan producerCancelRequest// ack handlingacks        chan int      // ackloop -> eventloop : total number of events ACKed by outputsschedACKS   chan chanList // eventloop -> ackloop : active list of batches to be ackedpendingACKs chanList      // ordered list of active batches to be send to the ackloopackSeq      uint          // ack batch sequence number to validate ordering// buffer flush timer statetimer *time.TimeridleC <-chan time.Time
}

BufferingEventLoop是一个实现了Broker、带有各种channel的结构,主要用于将日志发送至consumer消费。 BufferingEventLoop的run方法中,同样是一个无限循环,这里可以认为是一个日志事件的调度中心。

for {select {case <-broker.done:returncase req := <-l.events: // producer pushing new eventl.handleInsert(&req)case req := <-l.get: // consumer asking for next batchl.handleConsumer(&req)case count := <-l.acks:l.handleACK(count)case <-l.idleC:l.idleC = nill.timer.Stop()if l.buf.length() > 0 {l.flushBuffer()}}}

上文中harvester goroutine每次读取到日志数据之后,最终会被发送至bufferingEventLoop中的events chan pushRequest channel,然后触发上面req := <-l.events的case,handleInsert方法会把数据添加至bufferingEventLoop的buf中,buf即memqueue实际缓存日志数据的队列,如果buf长度超过配置的最大值或者bufferingEventLoop中的timer定时器触发了case <-l.idleC,均会调用flushBuffer()方法。
flushBuffer()又会触发req := <-l.get的case,然后运行handleConsumer方法,该方法中最重要的是这一句代码:

req.resp <- getResponse{ackChan, events}

这里获取到了consumer消费者的response channel,然后发送数据给这个channel。真正到这,才会触发consumer对memqueue的消费。所以,其实memqueue并非一直不停的在被consumer消费,而是在memqueue通知consumer的时候才被消费,我们可以理解为一种脉冲式的发送。

4. 消费队列

实际上,早在filebeat初始化的时候,就已经创建了一个eventConsumer并在loop无限循环方法里试图从Broker中获取日志数据。

for {if !paused && c.out != nil && consumer != nil && batch == nil {out = c.out.workQueuequeueBatch, err := consumer.Get(c.out.batchSize)...batch = newBatch(c.ctx, queueBatch, c.out.timeToLive)}...select {case <-c.done:returncase sig := <-c.sig:handleSignal(sig)case out <- batch:batch = nil}}

上面consumer.Get就是消费者consumer从Broker中获取日志数据,然后发送至out的channel中被output client发送,我们看一下Get方法里的核心代码:

select {case c.broker.requests <- getRequest{sz: sz, resp: c.resp}:case <-c.done:return nil, io.EOF}// if request has been send, we do have to wait for a responseresp := <-c.respreturn &batch{consumer: c,events:   resp.buf,ack:      resp.ack,state:    batchActive,}, nil

getRequest的结构如下:

type getRequest struct {sz   int              // request sz events from the brokerresp chan getResponse // channel to send response to
}

getResponse的结构:

type getResponse struct {ack *ackChanbuf []publisher.Event
}

getResponse里包含了日志的数据,而getRequest包含了一个发送至消费者的channel。
在上文bufferingEventLoop缓冲队列的handleConsumer方法里接收到的参数为getRequest,里面包含了consumer请求的getResponse channel。
如果handleConsumer不发送数据,consumer.Get方法会一直阻塞在select中,直到flushBuffer,consumer的getResponse channel才会接收到日志数据。

5. 发送日志

在创建beats时,会创建一个clientWorker,clientWorker的run方法中,会不停的从consumer发送的channel里读取日志数据,然后调用client.Publish批量发送日志。

func (w *clientWorker) run() {for !w.closed.Load() {for batch := range w.qu {if err := w.client.Publish(batch); err != nil {return}}}
}

libbeats库中包含了kafka、elasticsearch、logstash等几种client,它们均实现了client接口:

type Client interface {Close() errorPublish(publisher.Batch) errorString() string
}

当然最重要的是实现Publish接口,然后将日志发送出去。

实际上,filebeat中日志数据在各种channel里流转的设计还是比较复杂和繁琐的,笔者也是研究了好久、画了很长的架构图才理清楚其中的逻辑。 这里抽出了一个简化后的图以供参考:

d9dc22740ae00a63b1aa053cd350c4d6.png

如何保证at least once

filebeat维护了一个registry文件在本地的磁盘,该registry文件维护了所有已经采集的日志文件的状态。 实际上,每当日志数据发送至后端成功后,会返回ack事件。filebeat启动了一个独立的registry协程负责监听该事件,接收到ack事件后会将日志文件的State状态更新至registry文件中,State中的Offset表示读取到的文件偏移量,所以filebeat会保证Offset记录之前的日志数据肯定被后端的日志存储接收到。
State结构如下所示:

type State struct {Id          string            `json:"-"` // local unique id to make comparison more efficientFinished    bool              `json:"-"` // harvester stateFileinfo    os.FileInfo       `json:"-"` // the file infoSource      string            `json:"source"`Offset      int64             `json:"offset"`Timestamp   time.Time         `json:"timestamp"`TTL         time.Duration     `json:"ttl"`Type        string            `json:"type"`Meta        map[string]string `json:"meta"`FileStateOS file.StateOS
}

记录在registry文件中的数据大致如下所示:

[{"source":"/tmp/aa.log","offset":48,"timestamp":"2019-07-03T13:54:01.298995+08:00","ttl":-1,"type":"log","meta":null,"FileStateOS":{"inode":7048952,"device":16777220}}]

由于文件可能会被改名或移动,filebeat会根据inode和设备号来标志每个日志文件。
如果filebeat异常重启,每次采集harvester启动的时候都会读取registry文件,从上次记录的状态继续采集,确保不会从头开始重复发送所有的日志文件。
当然,如果日志发送过程中,还没来得及返回ack,filebeat就挂掉,registry文件肯定不会更新至最新的状态,那么下次采集的时候,这部分的日志就会重复发送,所以这意味着filebeat只能保证at least once,无法保证不重复发送。
还有一个比较异常的情况是,linux下如果老文件被移除,新文件马上创建,很有可能它们有相同的inode,而由于filebeat根据inode来标志文件记录采集的偏移,会导致registry里记录的其实是被移除的文件State状态,这样新的文件采集却从老的文件Offset开始,从而会遗漏日志数据。
为了尽量避免inode被复用的情况,同时防止registry文件随着时间增长越来越大,建议使用clean_inactive和clean_remove配置将长时间未更新或者被删除的文件State从registry中移除。

同时我们可以发现在harvester读取日志中,会更新registry的状态处理一些异常场景。例如,如果一个日志文件被清空,filebeat会在下一次Reader.Next方法中返回ErrFileTruncate异常,将inode标志文件的Offset置为0,结束这次harvester,重新启动新的harvester,虽然文件不变,但是registry中的Offset为0,采集会从头开始。

特别注意的是,如果使用容器部署filebeat,需要将registry文件挂载到宿主机上,否则容器重启后registry文件丢失,会使filebeat从头开始重复采集日志文件。

filebeat自动reload更新

目前filebeat支持reload input配置,module配置,但reload的机制只有定时更新。
在配置中打开reload.enable之后,还可以配置reload.period表示自动reload配置的时间间隔。
filebeat在启动时,会创建一个专门用于reload的协程。对于每个正在运行的harvester,filebeat会将其加入一个全局的Runner列表,每次到了定时的间隔后,会触发一次配置文件的diff判断,如果是需要停止的加入stopRunner列表,然后逐个关闭,新的则加入startRunner列表,启动新的Runner。

filebeat对kubernetes的支持

filebeat官方文档提供了在kubernetes下基于daemonset的部署方式,最主要的一个配置如下所示:

- type: dockercontainers.ids:- "*"processors:- add_kubernetes_metadata:in_cluster: true

即设置输入input为docker类型。由于所有的容器的标准输出日志默认都在节点的/var/lib/docker/containers/<containerId>/*-json.log路径,所以本质上采集的是这类日志文件。
和传统的部署方式有所区别的是,如果服务部署在kubernetes上,我们查看和检索日志的维度不能仅仅局限于节点和服务,还需要有podName,containerName等,所以每条日志我们都需要打标增加kubernetes的元信息才发送至后端。
filebeat会在配置中增加了add_kubernetes_metadata的processor的情况下,启动监听kubernetes的watch服务,监听所有kubernetes pod的变更,然后将归属本节点的pod最新的事件同步至本地的缓存中。
节点上一旦发生容器的销毁创建,/var/lib/docker/containers/下会有目录的变动,filebeat根据路径提取出containerId,再根据containerId从本地的缓存中找到pod信息,从而可以获取到podName、label等数据,并加到日志的元信息fields中。
filebeat还有一个beta版的功能autodiscover,autodiscover的目的是把分散到不同节点上的filebeat配置文件集中管理。目前也支持kubernetes作为provider,本质上还是监听kubernetes事件然后采集docker的标准输出文件。
大致架构如下所示:

29f81237f5371f523d3742e58b274838.png

但是在实际生产环境使用中,仅采集容器的标准输出日志还是远远不够,我们往往还需要采集容器挂载出来的自定义日志目录,还需要控制每个服务的日志采集方式以及更多的定制化功能。

在轻舟容器云上,我们自研了一个监听kubernetes事件自动生成filebeat配置的agent,通过CRD的方式,支持自定义容器内部日志目录、支持自定义fields、支持多行读取等功能。同时可在kubernetes上统一管理各种日志配置,而且无需用户感知pod的创建销毁和迁移,自动完成各种场景下的日志配置生成和更新。

性能分析与调优

虽然beats系列主打轻量级,虽然用golang写的filebeat的内存占用确实比较基于jvm的logstash等好太多,但是事实告诉我们其实没那么简单。
正常启动filebeat,一般确实只会占用3、40MB内存,但是在轻舟容器云上偶发性的我们也会发现某些节点上的filebeat容器内存占用超过配置的pod limit限制(一般设置为200MB),并且不停的触发的OOM。
究其原因,一般容器化环境中,特别是裸机上运行的容器个数可能会比较多,导致创建大量的harvester去采集日志。如果没有很好的配置filebeat,会有较大概率导致内存急剧上升。
当然,filebeat内存占据较大的部分还是memqueue,所有采集到的日志都会先发送至memqueue聚集,再通过output发送出去。每条日志的数据在filebeat中都被组装为event结构,filebeat默认配置的memqueue缓存的event个数为4096,可通过queue.mem.events设置。默认最大的一条日志的event大小限制为10MB,可通过max_bytes设置。4096 * 10MB = 40GB,可以想象,极端场景下,filebeat至少占据40GB的内存。特别是配置了multiline多行模式的情况下,如果multiline配置有误,单个event误采集为上千条日志的数据,很可能导致memqueue占据了大量内存,致使内存爆炸。
所以,合理的配置日志文件的匹配规则,限制单行日志大小,根据实际情况配置memqueue缓存的个数,才能在实际使用中规避filebeat的内存占用过大的问题。

如何对filebeat进行扩展开发

一般情况下filebeat可满足大部分的日志采集需求,但是仍然避免不了一些特殊的场景需要我们对filebeat进行定制化开发,当然filebeat本身的设计也提供了良好的扩展性。
beats目前只提供了像elasticsearch、kafka、logstash等几类output客户端,如果我们想要filebeat直接发送至其他后端,需要定制化开发自己的output。同样,如果需要对日志做过滤处理或者增加元信息,也可以自制processor插件。
无论是增加output还是写个processor,filebeat提供的大体思路基本相同。一般来讲有3种方式:

  1. 直接fork filebeat,在现有的源码上开发。output或者processor都提供了类似Run、Stop等的接口,只需要实现该类接口,然后在init方法中注册相应的插件初始化方法即可。当然,由于golang中init方法是在import包时才被调用,所以需要在初始化filebeat的代码中手动import。
  2. 复制一份filebeat的main.go,import我们自研的插件库,然后重新编译。本质上和方式1区别不大。
  3. filebeat还提供了基于golang plugin的插件机制,需要把自研的插件编译成.so共享链接库,然后在filebeat启动参数中通过-plugin指定库所在路径。不过实际上一方面golang plugin还不够成熟稳定,一方面自研的插件依然需要依赖相同版本的libbeat库,而且还需要相同的golang版本编译,坑可能更多,不太推荐。

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

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

相关文章

java ldap userpassword 解密_Spring Boot中使用LDAP来统一管理用户信息

LDAP简介LDAP(轻量级目录访问协议&#xff0c;Lightweight Directory Access Protocol)是实现提供被称为目录服务的信息服务。目录服务是一种特殊的数据库系统&#xff0c;其专门针对读取&#xff0c;浏览和搜索操作进行了特定的优化。目录一般用来包含描述性的&#xff0c;基于…

unity中怎么做河流_【干货】工作中怎么做工业设计的?(一)

最近在找工作&#xff0c;一直在看招聘信息。看到工业设计工资还是蛮高的。应届毕业生一般是4-6K&#xff0c;1-3年工作经验是6-8K&#xff0c;3年以后的差不多是8K以上了。我没有嫉妒羡慕恨&#xff0c;发誓&#xff0c;真的没有。工业设计已经被重视&#xff0c;未来的道路会…

[易学易懂系列|golang语言|零基础|快速入门|(一)]

golang编程语言&#xff0c;是google推出的一门语言。 主要应用在系统编程和高性能服务器编程&#xff0c;有广大的市场前景&#xff0c;目前整个生态也越来越强大&#xff0c;未来可能在企业应用和人工智能等领域占有越来越重要的地位。 本文章是【易学易懂系列|编程语言入门】…

spi收发时的寄存器sr不变_「正点原子Linux连载」第二十七章SPI实验(二)

1)实验平台&#xff1a;正点原子Linux开发板2)摘自《正点原子I.MX6U嵌入式Linux驱动开发指南》关注官方微信号公众号&#xff0c;获取更多资料&#xff1a;正点原子文件bsp_spi.c中有两个函数&#xff1a;spi_init和spich0_readwrite_byte&#xff0c;函数spi_init是SPI初始化函…

[JSOI2018]潜入行动

题解 一道思路不难但是写起来很麻烦的树形背包 我们发现每个节点有很多信息需要保留 所以就暴力的设\(f[u][j][0/1][0/1]\)表示点u的子树分配了j个监察器,点u有没有被控制,点u放没放监察器 然后就分四种情况暴力讨论就好了 注意背包的时候要卡常数 代码 #include<cstdio>…

css。元素样式、边框样式

1.外边距  margin 缩写形式&#xff1a; margin&#xff1a;上边距  右边距  下边距  左边距 margin&#xff1a;上下边距  左右边距 margin&#xff1a;上边距  左右边距  下边距 2.内边距  padding 缩写形式&#xff1a; padding&#xff1a;上边距  右边距…

C# 动态创建数据库三(MySQL)

前面有说明使用EF动态新建数据库与表&#xff0c;数据库使用的是SQL SERVER2008的&#xff0c;在使用MYSQL的时候还是有所不同 一、添加 EntityFramework.dll &#xff0c;System.Data.Entity.dll &#xff0c;MySql.Data, MySql.Data.Entity.EF6 注意&#xff1a;Entity Frame…

如何用计算机管理员权限,教你电脑使用代码添加管理员权限的详细教程

我们在使用电脑运行某些软件的时候&#xff0c;可能需要用到管理员权限才能运行&#xff0c;通常来说直接点击右键就会有管理员权限&#xff0c;但最近有用户向小编反馈&#xff0c;在需要管理员权限的软件上点击右键没有看到管理员取得所有权&#xff0c;那么究竟该如何才能获…

activiti 5.22的demo运行

activiti 5.22的demo运行 从github上clon下来的activiti项目,运行demo项目activiti-webapp-explorer2时&#xff0c;在使用到流程设计工作区&#xff0c;选取activiti modeler作为设计器的时候报错。 从下面的报错信息中发现&#xff0c;请求路径http://localhost:8080/activit…

内是不是半包围结构_轻钢别墅的体系结构

一、轻钢别墅介绍1、轻钢别墅的屋面系统轻钢别墅屋面系统是由屋架、结构OSB面板、防水层、轻型屋面瓦&#xff08;金属或沥青瓦&#xff09;组成的。轻钢结构的屋面&#xff0c;外观可以有多种组合。材料也有多种。在保障了防水这一技术的前提下&#xff0c;外观有了许多的选择…

hive 去重 字符串_hive函数

Hive是建立在 Hadoop 上的数据仓库基础架构,定义了简单的类 SQL 查询语言(HQL)函数分类&#xff1a;简单内置函数&#xff1a;数学函数&#xff0c;字符函数&#xff0c;日期函数&#xff0c;条件函数&#xff0c;聚合函数。高级内置函数&#xff1a;行列转换函数&#xff0c;分…

【Python3爬虫】为什么你的博客没人看呢?

我相信对于很多爱好和习惯写博客的人来说&#xff0c;如果自己的博客有很多人阅读和评论的话&#xff0c;自己会非常开心&#xff0c;但是你发现自己用心写的博客却没什么人看&#xff0c;多多少少会觉得有些伤心吧&#xff1f;我们今天就来看一下为什么你的博客没人看呢&#…

泰安高考2021成绩查询,泰安高考成绩查询入口2021

高考结束之后&#xff0c;为了方便大家进行高考成绩的查询&#xff0c;下面跟着出国留学网小编来一起看看“泰安高考成绩查询入口2021”&#xff0c;仅供参考&#xff0c;希望对大家有帮助。2021山东高考成绩查询时间及志愿填报时间根据山东2021年夏季高考须知&#xff0c;2021…

用GitHub Issue取代多说,是不是很厉害?

2019独角兽企业重金招聘Python工程师标准>>> 摘要: 别了&#xff0c;多说&#xff0c;拥抱Gitment。 2017年6月1日&#xff0c;多说正式下线&#xff0c;这多少让人感觉有些遗憾。在比较了多个博客评论系统&#xff0c;我最终选择了Gitment作为本站的博客评论系统&a…

【动态规划】Vijos P1313 金明的预算方案(NOIP2006提高组第二题)

题目链接&#xff1a; https://vijos.org/p/1313 题目大意&#xff1a; m(m<32000)金钱&#xff0c;n&#xff08;n<60&#xff09;个物品&#xff0c;花费vi&#xff0c;价值vi*ci,每个物品可能有不超过2个附件&#xff0c;附件没有附件。 题目思路&#xff1a; 【动态规…

计算机网络应用答题卡,2013-2014学年第2学期11级计算机网络技术毕业考试试卷

2013-2014学年第2学期11级《计算机网络技术》课程毕业考试试卷得分&#xff1a;一、单项选择题&#xff1a;(每题1分&#xff0c;共30分&#xff0c;答案必须写在后面的选择题答题卡内&#xff0c;否则不得分)1、计算机网络可以按网络的覆盖范围来划分&#xff0c;以下()不是按…

为什么那些每三年跳一次槽的人越跳越好? - 震撼

现在&#xff0c;人们已经放下了对跳槽的偏见。这是一件好事。之前。假设你每几年换一次工作&#xff0c;人们会认为你的简历上有 “污点”。面试官会认为你无法胜任一份工作。与同事相处不好。或者你对公司不忠诚&#xff0c;不能承担任务&#xff0c;等等。 这样的想法非常快…

矢量合成和分解的法则_专题14 运动的合成与分解

运动的合成与分解【基础回顾】 考点内容:运动的合成与分解 考纲解读: 1.掌握曲线运动的概念、特点及条件. 2.掌握运动的合成与分解法则&#xff0e; 考点一 物体做曲线运动的条件及轨迹分析 1&#xff0e;条件  (1)因为速度时刻在变&#xff0c;所以一定存在加速度&#xff1…

Java Persistence with MyBatis 小结2

MyBatis 最关键的组成部分是 SqlSessionFactory&#xff0c;我们可以从中获取 SqlSession&#xff0c;并执行映射的 SQL 语句。SqlSessionFactory 对象可以通过基于 XML 的配置信息或者 Java API 创建。 1 mybatis环境&#xff0c;environments 配置默认的数据库环境 MyBatis 支…

c#时分秒毫秒微妙_你真的清楚DateTime in C#吗?

DateTime&#xff0c;就是一个世界的大融合。日期和时间&#xff0c;在我们开发中非常重要。DateTime在C#中&#xff0c;专门用来表达和处理日期和时间。本文算是多年使用DateTime的一个总结&#xff0c;包括DateTime对象的整体应用&#xff0c;以及如何处理不同的区域、时区、…