TiProxy 原理和实现

说明

在上篇《TiProxy 尝鲜》 中做了一些实验,比如加减tidb节点后tiproxy可以做到自动负载均衡,如果遇到会话有未提交的事务则等待事务结束才迁移。

本次主要研究这样的功能在tiproxy中是如何实现的,本次分享内容主要为以下几部分:

  • tiproxy是怎么发现tidb?

  • tiproxy是在tidb节点间自动负载均衡的逻辑?

  • 在自动负载均衡时tiproxy是怎么做到优雅的session迁移、session上下文恢复?

  • tiproxy在自动负载均衡期间遇到处于未提交事务的session是怎么等待结束的?

Tiproxy 介绍

tiproxy 在 2022年12月2日被operator支持

相关的设计文档可以从官方 README  和 goole doc  中查看

这个有个重要特性需要说明下:

  • tiproxy组件不会保存账号的密码,因为这是不安全的行为,所以当进行会话迁移的时候使用的是 session token 认证方式(下文会提到这种方式的实现原理)。

声明

目前tiproxy还处于实验阶段、功能还在持续开发中,本文讲述的内容跟日后GA版本可能存在差异,届时请各位看官留意。

另外本人能力有限,在阅读源码中难免有理解不到位的地方,如有发现欢迎在评论区指正,感谢。

开始发车

原理分析

1、tiproxy是怎么发现tidb?

获取tidb拓扑最核心、简化后的代码如下,其实就是使用etcdCli.Get获取信息

// 从 etcd 获取 tidb 拓扑 路径 /topology/tidb/<ip:port>/info /topology/tidb/<ip:port>/ttl
func (is *InfoSyncer) GetTiDBTopology(ctx context.Context) (map[string]*TiDBInfo, error) {res, err := is.etcdCli.Get(ctx, tidbinfo.TopologyInformationPath, clientv3.WithPrefix())infos := make(map[string]*TiDBInfo, len(res.Kvs)/2)for _, kv := range res.Kvs {var ttl, addr stringvar topology *tidbinfo.TopologyInfokey := hack.String(kv.Key)switch {case strings.HasSuffix(key, ttlSuffix):addr = key[len(tidbinfo.TopologyInformationPath)+1 : len(key)-len(ttlSuffix)-1]ttl = hack.String(kv.Value)case strings.HasSuffix(key, infoSuffix):addr = key[len(tidbinfo.TopologyInformationPath)+1 : len(key)-len(infoSuffix)-1]json.Unmarshal(kv.Value, &topology)default:continue}info := infos[addr]if len(ttl) > 0 {info.TTL = hack.String(kv.Value)} else {info.TopologyInfo = topology}}return infos, nil
}

这个函数是怎么被tiproxy用起来的呢?

其实在每个proxy启动时后都会开启一个BackendObserver协程,这个协程会做三件事:

func (bo *BackendObserver) observe(ctx context.Context) {for ctx.Err() == nil {// 获取backendInfo, err := bo.fetcher.GetBackendList(ctx)// 检查bhMap := bo.checkHealth(ctx, backendInfo)// 通知bo.notifyIfChanged(bhMap)select {case <-time.After(bo.healthCheckConfig.Interval):  // 间隔3秒case <-bo.refreshChan:case <-ctx.Done():return}}
}

第一步获取:

从etcd获取tidb拓扑;代码见上;

第二步检查:

判断获取到tidb节点是否可以连通、访问,给每个节点设置StatusHealthy或者StatusCannotConnect状态

func (bo *BackendObserver) checkHealth(ctx context.Context, backends map[string]*BackendInfo) map[string]*backendHealth {curBackendHealth := make(map[string]*backendHealth, len(backends))for addr, info := range backends {bh := &backendHealth{status: StatusHealthy,}curBackendHealth[addr] = bh// http 服务检查if info != nil && len(info.IP) > 0 {schema := "http"httpCli := *bo.httpClihttpCli.Timeout = bo.healthCheckConfig.DialTimeouturl := fmt.Sprintf("%s://%s:%d%s", schema, info.IP, info.StatusPort, statusPathSuffix)resp, err := httpCli.Get(url)if err != nil {bh.status = StatusCannotConnectbh.pingErr = errors.Wrapf(err, "connect status port failed")continue}}// tcp 服务检查conn, err := net.DialTimeout("tcp", addr, bo.healthCheckConfig.DialTimeout)if err != nil {bh.status = StatusCannotConnectbh.pingErr = errors.Wrapf(err, "connect sql port failed")}        }return curBackendHealth
}

第三步通知:

将检查后的 backends 列表跟内存中缓存的 backends 进行比较,将变动的 updatedBackends 进行通知

// notifyIfChanged 根据最新的 tidb 拓扑 bhMap 与之前的 tidb 拓扑 bo.curBackendInfo 进行比较
// - 在 bo.curBackendInfo 中但是不在 bhMap 中:说明 tidb 节点失联,需要记录下
// - 在 bo.curBackendInfo 中也在 bhMap 中,但是最新的状态不是 StatusHealthy:也需要记录下
// - 在 bhMap 中但是不在 bo.curBackendInfo 中:说明是新增 tidb 节点,需要记录下
func (bo *BackendObserver) notifyIfChanged(bhMap map[string]*backendHealth) {updatedBackends := make(map[string]*backendHealth)for addr, lastHealth := range bo.curBackendInfo {if lastHealth.status == StatusHealthy {if newHealth, ok := bhMap[addr]; !ok {updatedBackends[addr] = &backendHealth{status:  StatusCannotConnect,pingErr: errors.New("removed from backend list"),}updateBackendStatusMetrics(addr, lastHealth.status, StatusCannotConnect)} else if newHealth.status != StatusHealthy {updatedBackends[addr] = newHealthupdateBackendStatusMetrics(addr, lastHealth.status, newHealth.status)}}}for addr, newHealth := range bhMap {if newHealth.status == StatusHealthy {lastHealth, ok := bo.curBackendInfo[addr]if !ok {lastHealth = &backendHealth{status: StatusCannotConnect,}}if lastHealth.status != StatusHealthy {updatedBackends[addr] = newHealthupdateBackendStatusMetrics(addr, lastHealth.status, newHealth.status)} else if lastHealth.serverVersion != newHealth.serverVersion {// Not possible here: the backend finishes upgrading between two health checks.updatedBackends[addr] = newHealth}}}// Notify it even when the updatedBackends is empty, in order to clear the last error.bo.eventReceiver.OnBackendChanged(updatedBackends, nil)bo.curBackendInfo = bhMap
}

通过上面的步骤就获取到了变动的backends,将这些变动从 BackendObserver 模块同步给 ScoreBasedRouter 模块。

2、tiproxy是在tidb节点间自动负载均衡的逻辑?

此处自动负载的语义是:将哪个 backend 的哪个 connect 迁移到哪个 backend 上。这就要解决 backend 挑选和 connect 挑选问题。

这个问题的解决办法是在 ScoreBasedRouter 模块完成。这个模块有3个 func 和上述解释相关:

type ScoreBasedRouter struct {sync.Mutex// A list of *backendWrapper. The backends are in descending order of scores.backends     *glist.List[*backendWrapper]// ...
}// 被 BackendObserver 调用,传来的 backends 会合并到 ScoreBasedRouter::backends 中
func (router *ScoreBasedRouter) OnBackendChanged(backends map[string]*backendHealth, err error) {}// 通过比较 backend 分数方式调整 ScoreBasedRouter::backends 中的位置
func (router *ScoreBasedRouter) adjustBackendList(be *glist.Element[*backendWrapper]) {}// 协程方式运行,做负载均衡处理
func (router *ScoreBasedRouter) rebalanceLoop(ctx context.Context) {}

OnBackendChanged 是暴露给 BackendObserver 模块的一个接口, 用来同步从 etcd 发现的 tidb 信息,这个逻辑不复杂,详细可自行阅读源码。这个方法是问题一种提到的“通知”接收处。

adjustBackendList 本质就是调整 item 在双向链表中的位置,这个也不复杂。

下面重点说下 rebalanceLoop 的逻辑,这里涉及到"将哪个 backend 的哪个 connect 迁移到哪个 backend 上"的问题。

// rebalanceLoop 计算间隔是 10 ms,每次最多处理 10 个连接(防止后端出现抖动)
// - backends 的变化是通过 OnBackendChanged 修改的,连接平衡是 rebalanceLoop 函数做的,两者为了保证并发使用了 sync.Mutex
func (router *ScoreBasedRouter) rebalanceLoop(ctx context.Context) {for {router.rebalance(rebalanceConnsPerLoop)select {case <-ctx.Done():returncase <-time.After(rebalanceInterval):}}
}// rebalance
func (router *ScoreBasedRouter) rebalance(maxNum int) {curTime := time.Now()router.Lock()defer router.Unlock()for i := 0; i < maxNum; i++ {var busiestEle *glist.Element[*backendWrapper]for be := router.backends.Front(); be != nil; be = be.Next() {backend := be.Valueif backend.connList.Len() > 0 {busiestEle = bebreak}}if busiestEle == nil {break}busiestBackend := busiestEle.ValueidlestEle := router.backends.Back()idlestBackend := idlestEle.Valueif float64(busiestBackend.score())/float64(idlestBackend.score()+1) < rebalanceMaxScoreRatio {break}var ce *glist.Element[*connWrapper]for ele := busiestBackend.connList.Front(); ele != nil; ele = ele.Next() {conn := ele.Valueswitch conn.phase {case phaseRedirectNotify:continuecase phaseRedirectFail:if conn.lastRedirect.Add(redirectFailMinInterval).After(curTime) {continue}}ce = elebreak}if ce == nil {break}conn := ce.ValuebusiestBackend.connScore--router.adjustBackendList(busiestEle)idlestBackend.connScore++router.adjustBackendList(idlestEle)conn.phase = phaseRedirectNotifyconn.lastRedirect = curTimeconn.Redirect(idlestBackend.addr)}
}

rebalance 的逻辑

  • 从前往后访问 backends list,找到 busiestBackend

  • 在 backends list 最后找到 idlestBackend

  • 比较两者 score, 如果差距在 20% 以内就不用处理了

  • 否则在 busiestBackend 中取出一个 conn 给 idlestBackend

    • 取出的逻辑很简单,就是从前到后遍历当前 backend 的 connList

    • 因为session迁移要保证事务完成,所以迁移不是立刻执行的,这就得加个 phase 来跟进

      • 处于 phaseRedirectNotify 阶段的不要再取出;

      • 处于 phaseRedirectFail 但还没到超时时间的,也不要取出;

    • 其他状态的 conn 可以被取出

  • 因为有 conn 变动所以要调整下 busiestBackend 和 idlestBackend 在 backends list 中的位置

  • 最后通过 channel 通知 BackendConnManager 做去session迁移,此时 conn 状态是 phaseRedirectNotify

给每个backend的打分逻辑如下,分数越大说明负载越大

func (b *backendWrapper) score() int {return b.status.ToScore() + b.connScore
}// var statusScores = map[BackendStatus]int{
//     StatusHealthy:        0,
//     StatusCannotConnect:  10000000,
//     StatusMemoryHigh:     5000,
//     StatusRunSlow:        5000,
//     StatusSchemaOutdated: 10000000,
// }// connScore = connList.Len() + incoming connections - outgoing connections.

3、在自动负载均衡时tiproxy是怎么做到优雅的session迁移、session上下文恢复?

这个问题可以继续细分:

  • 迁移消息接收

    • ScoreBasedRouter 模块计算出哪个 conn 从哪个 backend 迁移到哪个 backend 后,怎么通知给对应的 conn ?

  • 迁移任务执行

    • conn 接收到消息后要进行session迁移,那么如何解决迁移期间 client 可能存在访问的问题 ?

    • 因为tiproxy没有保存密码,那么基于session token的验证方式是怎么实现的?

    • 新的tidb节点登录成功后,session上下问题信息是怎么恢复的?

以上的问题都可以在 BackendConnManager 模块找到答案:

type BackendConnManager struct {// processLock makes redirecting and command processing exclusive.processLock sync.MutexclientIO   *pnet.PacketIObackendIO        atomic.Pointer[pnet.PacketIO]authenticator  *Authenticator
}
func (mgr *BackendConnManager) Redirect(newAddr string) bool {}
func (mgr *BackendConnManager) processSignals(ctx context.Context) {}
func (mgr *BackendConnManager) tryRedirect(ctx context.Context) {}
func (mgr *BackendConnManager) querySessionStates(backendIO *pnet.PacketIO) (sessionStates, sessionToken string, err error) {}
func (mgr *BackendConnManager) ExecuteCmd(ctx context.Context, request []byte) (err error) {}

迁移消息接收

在前文的 rebalance 方法最后,有行这样的逻辑

conn.Redirect(idlestBackend.addr)

这就是 ScoreBasedRouter 的通知给对应 conn 的地方。

这里调用的是 BackendConnManager::Redirect, 具体执行逻辑 - 将目标 backend 存储到 redirectInfo - 给 signalReceived channel 发 signalTypeRedirect 消息

func (mgr *BackendConnManager) Redirect(newAddr string) bool {// NOTE: BackendConnManager may be closing concurrently because of no lock.switch mgr.closeStatus.Load() {case statusNotifyClose, statusClosing, statusClosed:return false}mgr.redirectInfo.Store(&signalRedirect{newAddr: newAddr})// Generally, it won't wait because the caller won't send another signal before the previous one finishes.mgr.signalReceived <- signalTypeRedirectreturn true
}

该消息被 BackendConnManager::processSignals 协程接收

func (mgr *BackendConnManager) processSignals(ctx context.Context) {for {select {case s := <-mgr.signalReceived:// Redirect the session immediately just in case the session is finishedTxn.mgr.processLock.Lock()switch s {case signalTypeGracefulClose:mgr.tryGracefulClose(ctx)case signalTypeRedirect:   // <<<<<<<<<<<<<<<<<<mgr.tryRedirect(ctx)   }mgr.processLock.Unlock()case rs := <-mgr.redirectResCh:mgr.notifyRedirectResult(ctx, rs)case <-mgr.checkBackendTicker.C:mgr.checkBackendActive()case <-ctx.Done():return}}
}

这里补充下 processSignals 是怎么来的。正常情况下,client每发起一个连接,proxy就会起两个协程:

  • 连接、转发 tcp 消息协程:

    • 连接:SQLServer::Run 方法启动,也就是每连接每协程的意思。

    • 转发:ClientConnection 模块调用 BackendConnManager::ExecuteCmd 实现消息转发

  • 监听和执行 redirect 任务协程:

    • BackendConnManager 模块启动 processSignals 协程处理:

所以上文监听 signalTypeRedirect 消息的 processSignals 协程,在连接建立时就启动了,当收到消息后执行 tryRedirect 方法尝试执行迁移。

迁移任务执行

tryRedirect 处理逻辑比较复杂,我们选取核心流程进行简述:

func (mgr *BackendConnManager) tryRedirect(ctx context.Context) {// 获取目标 backendsignal := mgr.redirectInfo.Load()// 处于事务中,先不做迁移if !mgr.cmdProcessor.finishedTxn() {return}// 组装执行结果rs := &redirectResult{from: mgr.ServerAddr(),to:   signal.newAddr,}defer func() {// 不论执行成功与否都清空 redirectInfo, 并将 rs 结果发到 redirectResCh, redirectResCh 的处理逻辑还是在 processSignals 中处理mgr.redirectInfo.Store(nil)mgr.redirectResCh <- rs}()// 从源 backend 获取 sessionStates, sessionTokenbackendIO := mgr.backendIO.Load()sessionStates, sessionToken, rs.err := mgr.querySessionStates(backendIO)// 跟目标 backend 建立tcp连接cn, rs.err := net.DialTimeout("tcp", rs.to, DialTimeout)// 将 conn 包裹为 PacketIOnewBackendIO := pnet.NewPacketIO(cn, mgr.logger, pnet.WithRemoteAddr(rs.to, cn.RemoteAddr()), pnet.WithWrapError(ErrBackendConn))// 使用 session token方式跟目标 backend 进行鉴握手鉴权mgr.authenticator.handshakeSecondTime(mgr.logger, mgr.clientIO, newBackendIO, mgr.backendTLS, sessionToken)// 登录目标 backend 进行鉴权rs.err = mgr.initSessionStates(newBackendIO, sessionStates)// 将新的 PacketIO 存储到 BackendConnManager 的成员变量中,后续再有请求都是用此变量mgr.backendIO.Store(newBackendIO)
}

上面展示了 session token 的认证方式和上下文恢复的逻辑,对应 querySessionStates 、handshakeSecondTime 、initSessionStates 三个方法:

  • querySessionStates: tiproxy 在 tidb a 上执行 SHOW SESSION_STATES 获取到 session_token session_state

  • handshakeSecondTime: tiproxy 使用 session_token 认证方式登录到 tidb b

  • initSessionStates: tiproxy 登录成功后执行 SET SESSION_STATES '%s' 设置 tidb b 的 session_state

补充:

  • tiproxy 使用的 session token 的方式可以理解为 tidb 丰富了 mysql 协议,在 client 登录 server 的时候,除了账号密码这种mysql_native_password方式,还支持了账号token方式。

  • 使用 session token 认证方式,要求整个tidb集群证书是一样的,这样tidb a签名,tidb b才可以验签通过。

为了方式迁移期间,client还有新的会话,在执行 tryRedirect 前后使用 sync.Mutex 进行保护

func (mgr *BackendConnManager) processSignals(ctx context.Context) {for {// ...mgr.processLock.Lock()switch s {case signalTypeRedirect:mgr.tryRedirect(ctx)}mgr.processLock.Unlock()// ...}}
}func (mgr *BackendConnManager) ExecuteCmd(ctx context.Context, request []byte) (err error) {// ...mgr.processLock.Lock()defer mgr.processLock.Unlock()// ...waitingRedirect := mgr.redirectInfo.Load() != nil// ...if waitingRedirect {mgr.tryRedirect(ctx)}// ...
}

4、tiproxy在自动负载均衡期间遇到处于未提交事务的session是怎么等待结束的?

对于 tryRedirect 方法有两个地方被调用,即前文提到的 BackendConnManager::processSignals 和 BackendConnManager::ExecuteCmd

BackendConnManager::processSignals 只有在收到channe消息后立即出发一次,如果有未完成的事务就不再执行了。

所以为了保证迁移任务可继续,在 BackendConnManager::ExecuteCmd 中每次执行完 executeCmd 后尝试迁移,这样就能保证事务结束后立刻迁移。

func (mgr *BackendConnManager) ExecuteCmd(ctx context.Context, request []byte) (err error) {// ...waitingRedirect := mgr.redirectInfo.Load() != nil// ...holdRequest, err = mgr.cmdProcessor.executeCmd(request, mgr.clientIO, mgr.backendIO.Load(), waitingRedirect)// ...if mgr.cmdProcessor.finishedTxn() {if waitingRedirect {mgr.tryRedirect(ctx)}// ...}// ...
}

判断事务是否结束的 finishedTxn 方法逻辑:解析 client 的请求类型、解析 backend 的响应状态综合判断事务是否完成,此逻辑过于硬核,等以后研究明白后再分享吧。

有兴趣的读者可以分析下这段逻辑:

func (cp *CmdProcessor) finishedTxn() bool {if cp.serverStatus&(StatusInTrans|StatusQuit) > 0 {return false}// If any result of the prepared statements is not fetched, we should wait.return !cp.hasPendingPreparedStmts()
}func (cp *CmdProcessor) updatePrepStmtStatus(request []byte, serverStatus uint16) {var (stmtID         intprepStmtStatus uint32)cmd := pnet.Command(request[0])switch cmd {case pnet.ComStmtSendLongData, pnet.ComStmtExecute, pnet.ComStmtFetch, pnet.ComStmtReset, pnet.ComStmtClose:stmtID = int(binary.LittleEndian.Uint32(request[1:5]))case pnet.ComResetConnection, pnet.ComChangeUser:cp.preparedStmtStatus = make(map[int]uint32)returndefault:return}switch cmd {case pnet.ComStmtSendLongData:prepStmtStatus = StatusPrepareWaitExecutecase pnet.ComStmtExecute:if serverStatus&mysql.ServerStatusCursorExists > 0 {prepStmtStatus = StatusPrepareWaitFetch}case pnet.ComStmtFetch:if serverStatus&mysql.ServerStatusLastRowSend == 0 {prepStmtStatus = StatusPrepareWaitFetch}}if prepStmtStatus > 0 {cp.preparedStmtStatus[stmtID] = prepStmtStatus} else {delete(cp.preparedStmtStatus, stmtID)}
}

总结

本文从4个疑惑入手,阅读了下tiproxy的代码实现,都找到了对应的处理逻辑。

对比于tidb、tikv、pd等组件代码,tiproxy实简单很多,推荐大家学习下。

彩蛋

在梳理上面4个问题的时,理清思路后,绘制了如下的内部交互图,有兴趣的可以自己研究下,下篇文章我们将对其进行说明。

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

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

相关文章

【 Python 全栈开发 - 人工智能篇 - 45 】决策树与随机森林

文章目录 一、概念与原理1.1 决策树1.1.1 概念1.1.2 原理特征选择分割方法 1.1.3 优点与缺点1.1.4 Python常用决策树算法 1.2 随机森林1.2.1 概念1.2.2 原理1.2.3 优点与缺点1.2.4 Python常用随机森林算法 1.3 决策树与随机森林的比较1.3.1 相同之处1.3.2 不同之处 二、决策树算…

嵌入式开发:单片机嵌入式Linux学习路径

SOC&#xff08;System on a Chip&#xff09;的本质区别在于架构和功能。低端SOC如基于Cortex-M架构的芯片&#xff0c;如STM32和NXP LPC1xxx系列&#xff0c;不具备MMU&#xff08;Memory Management Unit&#xff09;&#xff0c;适用于轻量级实时操作系统如uCOS和FreeRTOS。…

Vue基础-综合案例(基于vue2)

一、目标 能够知道如何使用vue-cli创建vue项目能够知道如何在项目中安装与配置element-ui能够知道element-ui中常见组件的用法能够知道如何使用axios中的拦截器能够知道如何配置proxy接口代理 二、目录 vue-cli组件库axios拦截器proxy跨域代理用户列表案例 vue-cli 1.什么…

利用mysqldump实现分库分表备份的shell脚本

一、信息摘要 linux版本&#xff1a;CentOS 7.9 mysql版本&#xff1a;MySQL 5.7.36 脚本实现功能&#xff1a;利用mysqldump工具实现对mysql中的数据库分库备份&#xff0c;和对所备份数据库中的表分表备份 二、shell脚本 #!/bin/bash ######################### #File n…

[Linux]进程控制详解!!(创建、终止、等待、替换)

hello&#xff0c;大家好&#xff0c;这里是bang___bang_&#xff0c;在上两篇中我们讲解了进程的概念、状态和进程地址空间&#xff0c;本篇讲解进程的控制&#xff01;&#xff01;包含内容有进程创建、进程等待、进程替换、进程终止&#xff01;&#xff01; 附上前2篇文章…

使用Beego和MySQL实现帖子和评论的应用,并进行接口测试(附源码和代码深度剖析)

文章目录 小项目介绍源码分析main.gorouter.gomodels/user.gomodels/Post.gomodels/comment.gocontrollers/post.gocontrollers/comment.go 接口测试测试增加帖子测试查看帖子测试增加评论测试查看评论 小项目介绍 经过对需求的分析&#xff0c;我增加了一些额外的东西&#x…

Open3D (C++) ISS特征点提取

目录 一、算法原理1、原理概述2、参考文献二、代码实现三、结果展示本文由CSDN点云侠原创,原文链接。爬虫网站自重,把自己当个人 一、算法原理 1、原理概述 内部形状描述子(ISS)是一种表示立体几何形状的方法,该算法含有丰富的几何特征信息,可以完成高质量的点云配准。设…

禁用右键菜单AMD Software: Adrenalin Edition

本文参考链接&#xff1a; 删除win11右键一级菜单的AMD驱动栏 - 哔哩哔哩 windows11 求助删除右键菜单方法_windows11吧_百度贴吧 Windows安装最新的AMD显卡驱动后&#xff0c;右键菜单会多出AMD Software: Adrenalin Edition。使用一些右键菜单管理工具也没能屏蔽禁用掉该功…

mybatis-config.xml-配置文件详解

文章目录 mybatis-config.xml-配置文件详解说明文档地址:配置文件属性解析properties 属性应用实例 settings 全局参数定义应用实例 typeAliases 别名处理器举例说明 typeHandlers 类型处理器environments 环境environment 属性应用实例 mappers配置 mybatis-config.xml-配置文…

运维高级--shell脚本完成分库分表

为什么要进行分库分表 随着系统的运行&#xff0c;存储的数据量会越来越大&#xff0c;系统的访问的压力也会随之增大&#xff0c;如果一个库中的表数据超过了一定的数量&#xff0c;比如说MySQL中的表数据达到千万级别&#xff0c;就需要考虑进行分库分表&#xff1b; 其…

iOS-持久化

目的 1.快速展示&#xff0c;提升体验 已经加载过的数据&#xff0c;用户下次查看时&#xff0c;不需要再次从网络&#xff08;磁盘&#xff09;加载&#xff0c;直接展示给用户 2.节省用户流量&#xff08;节省服务器资源&#xff09; 对于较大的资源数据进行缓存&#xf…

ClickHouse(六):Clickhouse数据类型-1

进入正文前&#xff0c;感谢宝子们订阅专题、点赞、评论、收藏&#xff01;关注IT贫道&#xff0c;获取高质量博客内容&#xff01; &#x1f3e1;个人主页&#xff1a;含各种IT体系技术&#xff0c;IT贫道_Apache Doris,Kerberos安全认证,大数据OLAP体系技术栈-CSDN博客 &…

从电容到晶体管的基本介绍

​随着科技的不断进步&#xff0c;元器件在现代电子学中扮演着至关重要的角色。从电容器到晶体管&#xff0c;各种元器件都具有不同的特性和用途。本文将从基础知识出发&#xff0c;介绍电子学中常见的元器件&#xff0c;以及它们在电路中的作用和应用。 电容器 电容器是一种…

Docker复杂命令便捷操作

启动所有状态为Created的容器 要启动所有状态为"created"的Docker容器&#xff0c;可以使用以下命令&#xff1a; docker container start $(docker container ls -aq --filter "statuscreated")上述命令执行了以下步骤&#xff1a; docker container l…

深度学习论文: Q-YOLO: Efficient Inference for Real-time Object Detection及其PyTorch实现

深度学习论文: Q-YOLO: Efficient Inference for Real-time Object Detection及其PyTorch实现 Q-YOLO: Efficient Inference for Real-time Object Detection PDF: https://arxiv.org/pdf/2307.04816.pdf PyTorch代码: https://github.com/shanglianlm0525/CvPytorch PyTorch代…

C#..上位机软件的未来是什么?

C#是一种流行的编程语言&#xff0c;广泛应用于桌面应用程序和上位机软件开发。未来&#xff0c;C#上位机软件将继续不断发展和创新&#xff0c;以满足用户日益增长的需求。以下是我认为C#上位机软件未来可能会涉及的一些方向&#xff1a; 更加智能化&#xff1a;随着人工智能…

架构基本概念和架构本质

什么是架构和架构本质 在软件行业&#xff0c;对于什么是架构&#xff0c;都有很多的争论&#xff0c;每个人都有自己的理解。此君说的架构和彼君理解的架构未必是一回事。因此我们在讨论架构之前&#xff0c;我们先讨论架构的概念定义&#xff0c;概念是人认识这个世界的基础&…

python中如何记录日志?

日志是一种可以追踪某些软件运行时所发生事件的方法。一条日志信息对应的是一个事件的发生&#xff0c;而一个事件通常需要包括以下几个内容&#xff1a;事件发生时间、事件发生位置、事件的严重程度--日志级别、事件内容。 logging模块定义的函数和类为应用程序和库的开发实现…

pytest 入门

1,安装pytest 打开终端或命令提示符窗口,在终端中运行以下命令来安装pytest: pip install pytestpip install -i https://pypi.tuna.tsinghua.edu.cn/simple pytest 确保您的系统上已经安装了Python。您可以在终端中运行以下命令来检查Python的安装情况: pytest --version…

【Spring】Spring 下载及其 jar 包

根据 【动力节点】最新Spring框架教程&#xff0c;全网首套Spring6教程&#xff0c;跟老杜从零学spring入门到高级 以及老杜的原版笔记 https://www.yuque.com/docs/share/866abad4-7106-45e7-afcd-245a733b073f?# 《Spring6》 进行整理&#xff0c; 文档密码&#xff1a;mg9b…