用go从零构建写一个RPC(4)--gonet网络框架重构+聚集发包
在追求高性能的分布式系统中,RPC 框架的底层网络能力和数据传输效率起着决定性作用。经过几轮迭代优化,我完成了第四版本的 RPC 框架。相比以往版本,这一版本的最大亮点在于 重写了底层网络框架 和 实现了发送端的数据聚集机制,这使得框架在高并发、高吞吐场景下表现更稳定、更高效。本文将重点介绍这两个新功能的设计动机、技术选型与实现细节。
代码仓库:https://github.com/karatttt/MyRPC
版本四新增功能
重写 Go 原生 net 库
背景:
先说说go原生net的处理逻辑是:
每个 fd 对应⼀个 goroutine,业务⽅对 conn 发起主动的读写,底层使⽤⾮阻塞 IO,当事件未就绪,将 fd 注册(epoll_ctl)进 epoll fd,通过把 goroutine 设置(park)成 GWaiting 状态。当有就绪事件后,唤醒(ready) 对应 goroutine 成 GRunnable 状态------go会在调度goroutine时候执行epoll_wait系统调用,检查是否有状态发生改变的fd,有的话就把他取出,唤醒对应的goroutine去处理
在前三个版本中,我使用了 Go 原生的 net 库作为 RPC 的通信基础。虽然 Go 的网络抽象简单易用,但在构建高性能、低延迟的服务端系统时,它逐渐暴露出如下限制:
- 每一个连接必须需要一个协程,需要在协程中完成编解码和序列化反序列化的操作,连接关闭或者网络错误无法即时感知销毁协程(go的调度模型使得连接和协程是一一对应的,因为非阻塞的Read实际上交由用户调用,而调用的时机也同样在该协程中发生)
- gonet原生网络库是ET模式,这意味着当可读事件发生时,需要一次性的从缓冲区中读出所有的数据,因为如果没有读完,事件不会在下一次的epollwait中唤醒(除非新数据到达该缓冲区),无法再次读取。而这个循环读取同样也需要在用户协程中处理
受 netpoll 和 tnet 等优秀项目的启发,我决定基于 epoll(Linux)实现一套更底层、更灵活的网络事件驱动模型,实际上以上两个项目,并结合目前的RPC实现完整功能
实现思路:
对于第一个问题,可以借鉴netty的做法,分为Reactor线程和subReactor线程,他们都是poller线程,通过epoll_wait来监听事件循环,但是reactor线程只负责监听新连接,subReactor负责IO读写,并将业务处理交由线程池管理。
我们可以采集类似的做法,设置多个poller协程,并且让IO读写(编解码和序列化流程)交由poller线程处理,实际上的业务逻辑交由协程池处理,这样的总的协程数量就是poller数量 + 协程池的协程数量
对于第二个问题,实际上前面的版本采取了长连接的做法来避免连接的频繁建立和关闭,也就是服务端对每一个连接的readFrame是循环进行的(ET模式需要循环读完数据),直到一定时间未收到数据关闭这个连接。但是对于多客户端的情况,我们仍然会出现大量的连接,且每一个连接都需要阻塞直到到达最大空闲时间才主动关闭,就会导致连接过多(协程过多),我们希望使用LT模式,在读取完一帧之后并通过业务协程池异步处理业务逻辑后,主动释放协程,执行其他的协程
实际上目前的netpoll和tnet实现了类似的机制,但是他们都是提供了一个零拷贝接口由业务方调用,当融入RPC系统(往往需要反序列化的场景)后,零拷贝后的在缓冲区的数据,还会因为反序列化而进行到用户态的拷贝,所以上面的零拷贝实际上适合的场景时proxy / 转发场景,或者只关心字节数据的场景。所以我去除了零拷贝的设计,直接融入当前的RPC系统
PollerManager
type manager struct {polls []PollnumLoops int32pickIdx int32
}// Init 初始化并创建 poll 数组
func (m *manager) InitManager(numPolls int) error {fmt.Printf("Initializing poll manager with %d pollers\n", numPolls)if numPolls < 1 {numPolls = 1}atomic.StoreInt32(&m.numLoops, int32(numPolls))m.polls = make([]Poll, numPolls)for i := 0; i < numPolls; i++ {poll, err := NewDefaultPoll()if err != nil {fmt.Printf("Failed to create poller %d: %v\n", i, err)return err}m.polls[i] = pollgo poll.Wait()}return nil}
- 首先初始化一个pollerManager,来初始化多个可能的poller协程(最少一个),并且调用poll.wait开启事件循环
poller相关操作
// Control implements Poll.
func (p *defaultPoll) Control(operator *FDOperator, event PollEvent) error {fd := operator.FDvar op intvar evt syscall.EpollEventp.setOperator(unsafe.Pointer(&evt.Fd), operator)switch event {case PollReadable: // server accept a new connection and wait readop, evt.Events = syscall.EPOLL_CTL_ADD, syscall.EPOLLIN|syscall.EPOLLRDHUP|syscall.EPOLLERRcase PollWritable: // client create a new connection and wait connect finishedop, evt.Events = syscall.EPOLL_CTL_ADD, EPOLLET|syscall.EPOLLOUT|syscall.EPOLLRDHUP|syscall.EPOLLERRcase PollDetach: // deregisterp.delOperator(operator)op, evt.Events = syscall.EPOLL_CTL_DEL, syscall.EPOLLIN|syscall.EPOLLOUT|syscall.EPOLLRDHUP|syscall.EPOLLERRcase PollR2RW: // connection wait read/writeop, evt.Events = syscall.EPOLL_CTL_MOD, syscall.EPOLLIN|syscall.EPOLLOUT|syscall.EPOLLRDHUP|syscall.EPOLLERRcase PollRW2R: // connection wait readop, evt.Events = syscall.EPOLL_CTL_MOD, syscall.EPOLLIN|syscall.EPOLLRDHUP|syscall.EPOLLERR}evt.Fd = int32(fd)return EpollCtl(p.fd, op, fd, &evt)
}func (p *defaultPoll) Wait() error {events := make([]syscall.EpollEvent, 128)for {n, err := syscall.EpollWait(p.fd, events, -1)if err != nil {if err == syscall.EINTR {continue}return err}for i := 0; i < n; i++ {fd := int(events[i].Fd)op := p.operators[fd]if op == nil {continue}evt := events[i].Eventsif evt&(syscall.EPOLLIN|syscall.EPOLLPRI) != 0 && op.OnRead != nil {_ = op.OnRead(op.Conn)if op.Type == ConnectionType {// 关闭该事件,避免LT模式持续onRead_ = p.Control(op, PollDetach)}}if evt&(syscall.EPOLLOUT) != 0 && op.OnWrite != nil {_ = op.OnWrite(op)}}}
}
- 为了方便后面理解,这里先放出poller的相关操作,control就是注册事件,wait就是进行事件循环,这里的wait,对于可读事件,直接调用传入的OnRead,如果是已存在连接的数据可读,进行事件的关闭(不然这个实际上已经读完的连接就会一直被唤醒。。。)
eventLoop
// Serve implements EventLoop.
func (evl *eventLoop) Serve(ln net.Listener) error {evl.Lock()evl.ln = lnfd, err := getListenerFD(ln)if err != nil {return err}operator := FDOperator{FD: int(fd),OnRead: evl.ListenerOnRead,Type: ListenerType, // 标记为监听器类型}operator.poll = pollmanager.Pick()err = operator.Control(PollReadable)evl.Unlock()return err
}// 每一个事件循环中一定有listen连接的事件,当事件就绪的时候就调用这个函数
func (evl *eventLoop) ListenerOnRead(conn net.Conn) error {conn, err := evl.ln.Accept()if err != nil {// 非阻塞下 accept 没有新连接时返回if ne, ok := err.(net.Error); ok && ne.Temporary() {// 临时错误,继续等待return nil}fmt.Println("Accept error:", err)return err}fmt.Printf("Accepted new connection: %s\n", conn.RemoteAddr())// 选择 pollerpoller := pollmanager.Pick()if poller == nil {fmt.Println("No available poller")conn.Close()}// 获取FDrawConn, ok := conn.(syscall.Conn)if !ok {// 不是 syscall.Conn,不能获取 fd}var fd intsysRawConn, err := rawConn.SyscallConn()if err != nil {fmt.Println("Error getting syscall connection:", err)} else {err = sysRawConn.Control(func(f uintptr) {fd = int(f)})if err != nil {fmt.Println("Error getting file descriptor:", err)}}// 初始化连接OpConn := connection.InitConn(conn)fmt.Printf("Initialized connection with FD: %d\n", fd)// 创建 FDOperator 并注册到 pollernewOp := &FDOperator{poll : poller,Conn: OpConn,FD: fd,OnRead: evl.opts.onRequest, // 这里传入业务处理函数Type: ConnectionType, // 标记为连接类型}if err := poller.Control(newOp, PollReadable); err != nil {fmt.Println("Error registering connection:", err)conn.Close()}fmt.Printf("Registered new connection with FD: %d\n", fd)return nil
}
- 开启了poller的wait,就要为其分配事件,也就是初始化这个eventLoop,这个server只需要执行一次,注册一个listener监听连接,并且定制一个OnRead()
- 这个OnRead实际上就是accept一个连接,然后为这个连接注册一个可读事件(Control)
ServerTransport
启动server时,也需要一点改动,融入这个新的网络框架
// serveTCP 处理 TCP 连接
func (t *serverTransport) serveTCP(ctx context.Context, ln net.Listener) error {//初始化事件循环eventLoop, err := poller.NewEventLoop(t.OnRequest)if err != nil {return fmt.Errorf("failed to create event loop: %w", err)}err = eventLoop.Serve(ln)if err != nil {return fmt.Errorf("failed to serve: %w", err)}return nil
}// handleConnection 处理单个连接
func (t *serverTransport) OnRequest(conn net.Conn) error {// 设置连接超时idleTimeout := 30 * time.Secondif t.opts != nil && t.opts.IdleTimeout > 0 {idleTimeout = t.opts.IdleTimeout}// 设置读取超时conn.SetReadDeadline(time.Now().Add(idleTimeout))// 处理连接fmt.Printf("New connection from %s\n", conn.RemoteAddr())frame, err := codec.ReadFrame(conn)if err != nil {// 2. 如果读取帧失败,如客户端断开连接,则关闭连接if err == io.EOF {fmt.Printf("Client %s disconnected normally\n", conn.RemoteAddr())return err}// 3. 如果连接超时,超过设置的idletime,关闭连接if e, ok := err.(net.Error); ok && e.Timeout() {fmt.Printf("Connection from %s timed out after %v\n", conn.RemoteAddr(), idleTimeout)return err}// 4. 处理强制关闭的情况if strings.Contains(err.Error(), "forcibly closed") {fmt.Printf("Client %s forcibly closed the connection\n", conn.RemoteAddr())return err}fmt.Printf("Read error from %s: %v\n", conn.RemoteAddr(), err)return err}// 重置读取超时conn.SetReadDeadline(time.Now().Add(idleTimeout))// 使用协程池处理请求,适用于多路复用模式frameCopy := frame // 创建副本避免闭包问题err = t.pool.Submit(func() {// 处理请求response, err := t.ConnHandler.Handle(context.Background(), frameCopy)if err != nil {fmt.Printf("Handle error for %s: %v\n", conn.RemoteAddr(), err)return}// 发送响应conn = conn.(netxConn.Connection) // 确保conn实现了Connection接口,调用聚集发包的接口if _, err := conn.Write(response); err != nil {fmt.Printf("Write response error for %s: %v\n", conn.RemoteAddr(), err)}})if err != nil {fmt.Printf("Submit task to pool error for %s: %v\n", conn.RemoteAddr(), err)// 协程池提交失败,直接处理response, err := t.ConnHandler.Handle(context.Background(), frame)if err != nil {fmt.Printf("Handle error for %s: %v\n", conn.RemoteAddr(), err)}if _, err := conn.Write(response); err != nil {fmt.Printf("Write response error for %s: %v\n", conn.RemoteAddr(), err)return err}}return nil
}
- 可以看到serveTCP的适合启动一个事件循环,并传入一个OnRequest(作为事件就绪的时候的OnRead),当连接可读的时候调用这个方法
- 这个OnRequest在一开始通过codec.ReadFrame(conn)读取一个帧,这里只需要关心一个帧的原因是采取了LT模式,后续的没有读完的帧自然会再次唤醒,并且如果这里循环获取了,一个是循环停止的界限不好控制(什么时候才算数据读完?实际上的go的ioRead对于用户层面是阻塞,但底层通过 运行时调度器 + 多线程(GMP) 实现了“伪非阻塞”,也就是可能当Read() 一个永远没有数据的连接,那么这个 goroutine 会一直阻塞挂起(休眠状态),不会主动退出、不会被销毁,),还有一个是会阻塞该poller协程,影响到其他事件的处理。
- 需要注意的是,业务处理必须要用协程池处理,避免阻塞poller协程
- 这样就实现了让poller线程处理IO,并且通过LT模式减少连接的优化
批量发包
背景
其实在io读写中,还有一个消耗性能的就是频繁的系统调用,涉及到两态数据间的拷贝。比如服务端回包的时候,每一次的回包都是一次系统调用,这里就是可以优化的地方。
所以可以通过批量的形式,来减少系统调用,也就是用一个缓冲区来实现发包的聚集效应,当实际发生系统调用时,将缓冲区的所有数据一并发出,而不是每一次有数据就发生系统调用。
实现思路:
为什么收包的时候不批量呢?前面的OnRequest中的IoRead实际上也是一次系统调用,如果这里要实现聚集效应批量收包,也就是每一次epoll唤醒后,先将数据存到缓冲区中(这里可以用零拷贝),然后这里OnRead来挖缓冲区(只涉及到一次系统调用),但是这样带来的问题是,需要在OnRead中解决半包粘包问题,且要为每一个连接单独提供一个这样的缓冲区(实际上这个形式的缓冲区是有的,也就是linkBuffer,大家感兴趣可以去看看它的实现,但是它的主要功能还是为了提供零拷贝接口,只是为了批量收包而引入这个数据结构有点多余了。。。而且这个带来的收益只是单个连接维度下的收包聚集,从而系统调用次数的减少,假如一个连接只有一次的数据传输,实际上还是每一次事件就绪就需要一次系统拷贝)
对于发包的时候的聚集,我们就可以在整个系统维度下,多个连接将包放到一个并发安全的队列中,交由poller线程的写事件来决定什么时候写出,所以需要实现一个线程安全的队列,以及批量发包的接口
func (r *Ring[T]) commit(seq uint32, val T) {item := &r.data[seq&r.mask]for {getSeq := atomic.LoadUint32(&item.getSeq)putSeq := atomic.LoadUint32(&item.putSeq)// Waiting for data to be ready for writing. Due to the separation of// obtaining the right to use the sequence number and reading and writing// data operations, there is a short period of time that the old data has// not been read, wait for the read operation to complete and set getSeq.if seq == putSeq && getSeq == putSeq {break}runtime.Gosched()}// Complete the write operation and set putSeq to the next expected write sequence number.item.value = valatomic.AddUint32(&item.putSeq, r.capacity)
}func (r *Ring[T]) consume(seq uint32) T {item := &r.data[seq&r.mask]for {getSeq := atomic.LoadUint32(&item.getSeq)putSeq := atomic.LoadUint32(&item.putSeq)// Waiting for data to be ready to read. Due to the separation of// obtaining the right to use the sequence number and reading and writing// data operations, there is a short period of time that the writing data has// not been written yet, wait for the writing operation to complete and set putSeq.if seq == getSeq && getSeq == (putSeq-r.capacity) {break}runtime.Gosched()}// Complete the read operation and set getSeq to the next expected read sequence number.val := item.valuevar zero Titem.value = zeroatomic.AddUint32(&item.getSeq, r.capacity)return val
}
- 以上的这个ringBuffer的借鉴了tent的实现,但是实际上它和LMAX Disruptor的思想是一致的,都是实现了无锁化的并发安全队列,主要是以上的两个put和get的逻辑
- 举一个例子:
每个槽位的 putSeq 和 getSeq 都初始化为槽位的下标:
slot[0]: putSeq=0, getSeq=0
slot[1]: putSeq=1, getSeq=1
slot[2]: putSeq=2, getSeq=2
slot[3]: putSeq=3, getSeq=3
第一次 Put(写入):
写入线程获得 seq=1,即它准备写入 slot[1]:
- 写入 slot[1].value = val
然后执行:slot[1].putSeq += capacity → slot[1].putSeq = 1 + 4 = 5
现在:
slot[1]: putSeq=5, getSeq=1
表示这个槽位已经写入完成,等待消费者读取。
第一次 Get(读取):
读取线程获得 seq=1,即从 slot[1] 读数据:
- 消费成功后,执行:slot[1].getSeq += capacity → slot[1].getSeq = 1 + 4 = 5
现在:
slot[1]: putSeq=5, getSeq=5
说明这一轮(第1轮)读写都结束了,可以被下一轮复用。
第二轮 Put:
写入线程再次获得 seq=5(因为 tail 不断递增),这时还是映射到 slot[1],因为:
slotIndex = seq & (capacity - 1) = 5 & 3 = 1
此时:
- 它要判断:seq == putSeq && getSeq == putSeq,才能继续写
- 此时 putSeq=5,getSeq=5,满足条件
说明这个槽位已经被消费完了,可以再次复用来写入!也就是说,这个序号的作用是为了分配到该槽位时,保证数据不被覆盖,读和写都是安全的。
Buffer批量发包
func (b *Buffer) start() {initBufs := make(net.Buffers, 0, maxWritevBuffers)vals := make([][]byte, 0, maxWritevBuffers)bufs := initBufsdefer b.opts.handler(b)for {if err := b.getOrWait(&vals); err != nil {b.err = errbreak}for _, v := range vals {bufs = append(bufs, v)}vals = vals[:0]if _, err := bufs.WriteTo(b.w); err != nil {b.err = errbreak}// Reset bufs to the initial position to prevent `append` from generating new memory allocations.bufs = initBufs}
}func (b *Buffer) writeOrWait(p []byte) (int, error) {for {// The buffer queue stops receiving packets and returns directly.if b.isQueueStopped {return 0, b.err}// Write the buffer queue successfully, wake up the sending goroutine.if err := b.queue.Put(p); err == nil {b.wakeUp()return len(p), nil}// The queue is full, send the package directly.if err := b.writeDirectly(); err != nil {return 0, err}}
}
func (b *Buffer) getOrWait(values *[][]byte) error {for {// Check whether to be notified to close the outgoing goroutine.select {case <-b.done:return ErrAskQuitcase err := <-b.errCh:return errdefault:}// Bulk receive packets from the cache queue.size, _ := b.queue.Gets(values)if size > 0 {return nil}// Fast Path: Due to the poor performance of using select// to wake up the goroutine, it is preferred here to use Gosched()// to delay checking the queue, improving the hit rate and// the efficiency of obtaining packets in batches, thereby reducing// the probability of using select to wake up the goroutine.runtime.Gosched()if !b.queue.IsEmpty() {continue}// Slow Path: There are still no packets after the delayed check queue,// indicating that the system is relatively idle. goroutine uses// the select mechanism to wait for wakeup. The advantage of hibernation// is to reduce CPU idling loss when the system is idle.select {case <-b.done:return ErrAskQuitcase err := <-b.errCh:return errcase <-b.wakeupCh:}}
}
- 实现批量发包,只需要一开始对于这块全局的buffer进行一个start,循环看队列有没有数据,有的话全量取出并write
- 写的时候,调用writeOrWait这个接口,数据进ringBuffer就可以了
测试
server:
client:
总结
目前RPC先做到这了,以后还有什么优化或者有意思的再补充版本吧
相关文章:

用go从零构建写一个RPC(4)--gonet网络框架重构+聚集发包
在追求高性能的分布式系统中,RPC 框架的底层网络能力和数据传输效率起着决定性作用。经过几轮迭代优化,我完成了第四版本的 RPC 框架。相比以往版本,这一版本的最大亮点在于 重写了底层网络框架 和 实现了发送端的数据聚集机制,这…...

OpenBayes 一周速览|TransPixeler 实现透明化文本到视频生成;统一图像定制框架 DreamO 上线,一键处理多种图像生成任务
公共资源速递 2 个公共数据集: * s1K-1.1 数学推理数据集 * HPA 人类蛋白质图谱数据集 3 个公共模型: * MedGemma-4B-IT * Devstral-Small-2505 * DeepSeek-Prover-V2-7B 12 个公共教程: 视频生成 * 2 语音交互 * 3 代码生成 * 3 …...
视频的分片上传,断点上传
上传功能的实现,点击上传按钮,判断添加的文件是否符合要求,如果符合把他放入文件列表中,并把他的状态设置为等待中,对于每个文件,把他们切分为chunksize大小的文件片段,再检查他的状态是否为…...
CSS 性能优化
目录 CSS 性能优化CSS 提高性能的方法1. 选择器优化1.1 选择器性能原则1.2 选择器优化示例 2. 重排(Reflow)和重绘(Repaint)优化2.1 重排和重绘的概念2.2 触发重排的操作2.3 触发重绘的操作2.4 优化重排和重绘的方法 3. 资源优化3…...
华为×小鹏战略合作:破局智能驾驶深水区的商业逻辑深度解析
当中国智能电动车竞争进入下半场,头部玩家的合纵连横正在重构产业格局。华为与小鹏汽车近日官宣的“战略合作”,表面看是技术互补的常规操作,实则暗藏改写行业游戏规则的深层商业逻辑。 一、技术破壁:从“单点突破”到“全栈协同”…...

4D毫米波雷达产品推荐
供应商链接 :https://mp.weixin.qq.com/s/GYarrc9VEZS0FafxRUeG9w 大陆 ARS548 采埃孚 博世 安波福 -------- Waymo MobileEye 华为(未找到官网资料) ------- 森思泰克 http://www.whst.com/contact.html 芜湖经济技术开发区东区…...

yolo 训练 中间可视化
yolo训练前几个batch,会可视化target: if plots and ni < 33:f save_dir / ftrain_batch{ni}.jpg # filenameplot_images(imgs, targets, paths, f, kpt_labelkpt_label)...

Rust 学习笔记:关于 Cargo 的练习题
Rust 学习笔记:关于 Cargo 的练习题 Rust 学习笔记:关于 Cargo 的练习题问题一问题二问题三问题四问题五问题六问题七 Rust 学习笔记:关于 Cargo 的练习题 参考视频: https://www.bilibili.com/video/BV1xjAaeAEUzhttps://www.b…...

光伏功率预测 | BiLSTM多变量单步光伏功率预测(Matlab完整源码和数据)
光伏功率预测 | BiLSTM多变量单步光伏功率预测(Matlab完整源码和数据) 目录 光伏功率预测 | BiLSTM多变量单步光伏功率预测(Matlab完整源码和数据)效果一览基本介绍程序设计参考资料 效果一览 基本介绍 光伏功率预测 | BiLSTM多变…...

20250606-C#知识:委托和事件
C#知识:委托和事件 使用委托可以很方便地调用多个方法,也方便将方法作为参数进行传递 1、委托 委托是方法的容器委托可以看作一种特殊的类先定义委托类,再用委托类声明委托变量,委托变量可以存储方法 delegate int Calculate(in…...

AI数字人技术革新进行时:井云数字人如何重塑人机交互未来?
老板们注意了!不用反复真人出镜拍摄,AI数字人来帮你做口播,只需3分钟克隆你的形象和声音,输入文案24小时随时都能生成视频! 在元宇宙概念持续升温、虚拟与现实加速融合的当下,AI数字人正以惊人的速度从科幻…...

ruoyi-plus-could 负载均衡 通过 Gateway模块配置负载均衡
这个很简单的,其实都不用配置。 在nacos中ruoyi-gateway.yml配置文件里面: 其实他已经给我们配置好了,只要uri:lb有【lb】就表示负载均衡配置 我们只需要在启动服务的时候改下端口就可以。 然后通过小工具测试下: 结…...
江科大读写内部flash到hal库实现
hal库相关代码 进程结构体 typedef struct {__IO FLASH_ProcedureTypeDef ProcedureOnGoing; /*表示闪存操作过程中的不同状态或过程类型*/__IO uint32_t DataRemaining; /*记录尚未完成的页数或者半字数*/__IO uint32_t Address; /…...

Matlab回归预测大合集又更新啦!新增2种高斯过程回归预测模型,已更新41个模型!性价比拉满!
Matlab回归预测大合集又更新啦!新增2种高斯过程回归预测模型,已更新41个模型!性价比拉满! 目录 Matlab回归预测大合集又更新啦!新增2种高斯过程回归预测模型,已更新41个模型!性价比拉满…...

主流 AI IDE 之一的 Cursor 介绍
一、什么是 Cursor Cursor 是由 Anysphere 公司开发的 AI 驱动的代码编辑器(IDE);Anysphere 成立于 2022 年,创始团队包括来自麻省理工学院(MIT)的毕业生,如联合创始人 Aman Sanger 和 Michael …...

0x-1 记一次SGA PGA设置失败,重新开库
0、生产侧定时平台上传数据库11g hang,修改无法startup 厂商统一发放的虚拟机作为前置机导入平台后,直接开机使用。主机在虚拟化平台中,实例卡死后,按照虚拟机系统64G,原SGA2g,不知哪个大聪明给默认设置的。保守计划修…...

【科研绘图系列】R语言绘制和弦图(Chord diagram plot)
禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包数据下载导入数据数据预处理相关性计算和弦图系统信息介绍 本文介绍了一个基于R语言的数据分析和可视化流程,主要用于生成和弦图(Chord Diagram)。和弦图是一种用于展示…...

PPT转图片拼贴工具 v3.0
软件介绍 这个软件就是将PPT文件转换为图片并且拼接起来。 这个代码支持导入单个文件也支持导入文件夹 但是目前还没有解决可视化界面问题。 效果展示 软件源码 import os import re import win32com.client from PIL import Image from typing import List, Uniondef con…...

关于安科瑞APD局部放电监测装置解决方案的应用分析
1 什么是局部放电? 局部放电(Partial Discharge, PD)是指发生在电气设备绝缘系统局部区域的、未贯穿整个电极的微小放电现象。它通常发生在高压电气设备(如变压器、开关柜、电缆、GIS等)内部存在绝缘缺陷、电场集中或…...

设计模式-2 结构型模式
一、代理模式 1、举例 海外代购 2、代理基本结构图 3、静态代理 1、真实类实现一个接口,代理类也实现这个接口。 2、代理类通过真实对象调用真实类的方法。 4、静态代理和动态代理的区别 1、静态代理在编译时就已经实现了,编译完成后代理类是一个实际…...

大量企业系统超龄服役!R²AIN SUITE 一体化企业提效解决方案重构零售数智化基因
《中国百货商业协会2024零售IT及数字化系统需求调查报告》为我们呈现了零售企业在数字化转型中的复杂图景。数据显示,82%的企业高管对AI改变行业未来充满信心 source:中国百货商业协会 ,零售IT及数字化系统需求调查报告 ,2024年 但…...

Cesium使用glb模型、图片标记来实现实时轨迹
目录 1、使用glb模型进行实时轨迹 2、使用图片进行实时轨迹 基于上一篇加载基础地图的代码上继续开发 vue中加载Cesium地图(天地图、高德地图)-CSDN博客文章浏览阅读164次。vue中加载Cesium三维地球https://blog.csdn.net/ssy001128/article/details…...
【拓扑剪枝+深搜剪枝/计数】2024睿抗-章鱼图的判断
题目描述 对于无向图 G ( V , E ) G(V,E) G(V,E),我们将有且只有一个环的、大于 2 2 2 个顶点的无向连通图称之为章鱼图,因为其形状像是一个环(身体)带着若干个树(触手),故得名。 给定一个…...

Android基础回顾】六:安卓显示机制Surface 、 SurfaceFlinger、Choreographer
在 Android 系统中,Surface 和 SurfaceFlinger 是图形渲染系统的核心组件,负责屏幕显示内容的合成与管理。它们协同工作,使各种 App 和系统界面能够高效地显示在屏幕上。 1 Surface 是什么? Surface 是一个抽象的图形缓冲区接口…...
SpringBoot核心注解详解及3.0与2.0版本深度对比
SpringBoot核心注解详解及3.0与2.0版本深度对比 本文全面解析SpringBoot核心注解原理,深入对比3.0与2.0版本差异,助你掌握新一代SpringBoot开发精髓 一、SpringBoot核心注解全景解析 1.1 什么是SpringBoot核心注解 SpringBoot核心注解是构建SpringBoot…...

敏捷开发中如何避免过度加班
在敏捷开发过程中避免过度加班,需要明确敏捷原则、合理规划迭代任务、加强团队沟通、优化流程效率、设定合理的工作负荷、注重团队士气和成员健康。明确敏捷原则,即保证可持续发展的步调,避免频繁地变更需求、过度承诺任务量。合理规划迭代任…...
深入浅出多路归并:原理、实现与实战案例解析
文章目录 二路归并多路归并方法一:指针遍历(多指针比较法)方法二:小根堆法(最小堆归并) 实际场景外部排序 经典题目丑数Ⅱ方法一:三指针法方法二:优先队列法(K路归并&…...
Java八股文——集合「Map篇」
Map 面试官您好,关于 Java 中常见的 Map 集合,我可以从非线程安全和线程安全两个方面来介绍: 首先,我们来看一下非线程安全的 Map 实现,这些在单线程环境下性能通常更好,但在并发场景下需要外部同步&…...

第1章_数据分析认知_知识点笔记
来自:数据分析自学课程-戴戴戴师兄 逐字稿:【课程4.0】第1章_分析认知_知识点笔记 【课程4.0】第1章 分析认知 知识点总结 数据分析的核心价值不是工具,而是用数据驱动业务增长。 一、数据分析的本质认知 数据分析是什么? 不是酷…...

111页可编辑精品PPT | 华为业务变革框架及战略级项目管理华为变革管理华为企业变革华为的管理模式案例培训
这份文档是关于华为公司业务变革管理框架(BTMS)V2.0的详细介绍,涵盖从年度规划到项目执行的全流程管理。BTMS框架通过变革战略规划、年度规划流程、解决方案开发(PMOP流程)、运作管理流程等多个模块,系统地…...