从Golang实践中得到的教训 golang知识点总结
yuyutoo 2024-10-16 15:45 4 浏览 0 评论
当使用复杂的分布式系统时,可能会遇到并发处理的需求。我们知道golang的协程是处理并发的利器之一,加上Golang为静态类型和编译型使得其在企业中使用越来越广泛。Mode.net公司系统每天要处理实时,快速和灵活的以毫秒为单位动态路由数据包的全球专用网络和数据,需要高度并发的系统,而他们的动态路由就是使用Golang来构建的,本文我们介绍Mode.net在Golang构建分布式动态路由系统时的经验教训。
并发探测链接指标
Mode.net的路由系统称为HALO,是Hop-by-Hop Adaptive Link-State Optimal Routing(逐跳自适应链路状态最佳路由)的前缀字母简称。动态路由算法部分依赖于链路度量来计算路由表。这些指标由位于每个PoP(存活节点)上的独立组件收集。PoP是代表网络中单个路由实体的机器,它们通过链接连接并分布在形成Mode网络的多个位置。组件使用网络数据包探测临近的主机,这些邻居将回复数据包给探测。链路等待的时间值回复包中得到。由于每个PoP都会有一个以上的邻居,因此这种探测任务的本质是并发的,需要实时测量每个邻居链路的延迟。为了计算此指标,无法使用顺序处理,必须尽快处理每个探针。
序列号和重置
探测组件交换数据包并依靠序列号进行数据包处理。旨在避免处理分组重复或乱序分组。HALO的第一个实现依靠特殊的序列号0来重置序列号。这样的数字仅在组件初始化期间使用。主要问题是考虑一个始终从0开始的递增序列号值,组件重新启动后,可能会发生数据包重新排序,并且数据包可以轻松地用重置之前使用的值替换序列号。这样随后的数据包将被忽略,直接复位之前使用的序列号。
UDP握手和有限状态机
有一个问题是组件重新启动后序列号是否正确一致。有几种方法可以解决此问题,在讨论了可能的选项之后,HALO选择实现带有清晰状态定义的三向握手协议。该握手在初始化期间通过链接建立会话。这样可以确保节点通过同一会话进行通信并为其使用适当的序列号。为了正确实现这一点,必须定义一个具有清晰状态和过渡的有限状态机,这样就能够正确管理所有握手形成的极端情况。
会话ID由握手初始化程序生成。完整的交换顺序如下:
1.发送方发送一个SYN(ID)数据包。
2.接收器存储接收到的ID并发送SYN-ACK(ID)。
3.发送方接收SYN-ACK(ID)并发出ACK(ID)。它还开始发送从序列号0开始的数据包。
4.接收器检查最后收到的ID,如果ID匹配,则接受ACK(ID)。它还开始接受序列号为0的数据包。
处理状态超时
基本上,在每种状态下,最多都需要处理三种类型的事件:链接事件,数据包事件和超时事件。这些事件会同时显示,因此必须正确处理并发。
链接事件是链接更新或链接更新。这可以启动链接会话或中断现有会话。
数据包事件是控制数据包(SYN/SYN-ACK/ACK)或只是探测响应。
超时事件是针对当前会话状态的预定超时到期后触发的事件。
这方面主要挑战是如何处理并发超时到期和其他事件。这是一个容易陷入僵局和竞争状况陷阱的地方。
第一种方法:HALO项目使用的语言是Golang。它确实提供了本机同步机制,例如本机通道和锁,并且能够使用轻量级线程(协程)以进行并发处理。
具体处理过程:
首先,设计一个代表会话和超时处理程序的数据结构。
type Session struct {
State SessionState
Id SessionId
RemoteIp string
}
type TimeoutHandler struct {
callback func(Session)
session Session
duration int
timer *timer.Timer
}
会话数据结构使用会话ID,相邻链路IP和当前会话状态来标识连接会话。
TimeoutHandler包含回调函数,session表示任务运行的会话,持续时间(duration)以及指向已调度计时器的timer指针。
有一个全局映射,该映射将为每个相邻的链接会话存储计划的超时处理程序。
SessionTimeout map[Session]*TimeoutHandler
通过以下方法可以注册和取消超时:
// schedules the timeout callback function.
func (timeout* TimeoutHandler) Register() {
timeout.timer = time.AfterFunc(time.Duration(timeout.duration) * time.Second, func() {
timeout.callback(timeout.session)
})
}
对于超时的创建和存储,可以使用如下方法:
func CreateTimeoutHandler(callback func(Session), session Session, duration int) *TimeoutHandler {
if sessionTimeout[session] == nil {
sessionTimeout[session] := new(TimeoutHandler)
}
timeout = sessionTimeout[session]
timeout.session = session
timeout.callback = callback
timeout.duration = duration
return timeout
}
一旦创建并注册了超时处理程序,它就会在持续时间秒数之后运行回调。但是,某些事件将要求重新安排超时处理程序(在SYN状态下发生,即每3秒一次)。
为此,可以让回调函数重新安排新的超时:
func synCallback(session Session) {
sendSynPacket(session)
// reschedules the same callback.
newTimeout := NewTimeoutHandler(synCallback, session, SYN_TIMEOUT_DURATION)
newTimeout.Register()
sessionTimeout[state] = newTimeout
}
该回调将在新的超时处理程序中重新安排时间,并更新全局sessionTimeout映射。
数据竞争和引用
一个简单的测试是检查计时器到期后是否执行了超时回调。为此,注册一个超时,在其持续时间内休眠,然后检查回调操作是否已完成。执行测试后,最好取消预定的超时时间,因此不会在测试之间产生副作用。令人惊讶的是,这个简单的测试在发现了解决方案中的一个错误。使用cancel方法取消超时没有完成其工作。以下事件顺序将导致数据争用情况:
1.有一个计划的超时处理程序。
2.线程1:
a)收到一个控制数据包,现在要取消注册的超时并进入下一个会话状态。 (例如,发送了SYN后收到了SYN-ACK)。
b)调用timeout.Cancel(),它调用了timer.Stop()。(请注意,Golang计时器停止不会阻止已过期的计时器运行。)
3.线程2:
a)在该取消调用之前,计时器已到期,并且回调即将执行。
b)执行回调,它计划新的超时并更新全局映射。
4.线程1:
a)转换到新的会话状态并注册新的超时,从而更新全局映射。
两个线程正在同时更新超时映射。最终结果是无法取消已注册的超时,然后又丢失了对线程2完成的重新安排的超时的引用。这导致处理程序在一段时间内继续执行和重新安排,并执行了非预期的行为。
锁也解决不了问题
使用锁也不能完全解决问题。如果在处理任何事件之前和执行回调之前添加了锁,它仍然不能阻止过期的回调运行:
func (timeout* TimeoutHandler) Register() {
timeout.timer = time.AfterFunc(time.Duration(timeout.duration) * time._Second_, func() {
stateLock.Lock()
defer stateLock.Unlock()
timeout.callback(timeout.session)
})
}
和无锁的区别是全局映射中的更新是同步的,但这不能阻止在调用超时后运行timeout.Cancel(),如果计划的计时器已过期但未抓住锁,则情况如此然而。
使用Cancel通道
可以使用cancel通道,而不必依赖timer.Stop()(不会阻止到期的计时器执行),
这是一个略有不同的方法。这样可以将不再通过回调进行递归重新安排,而会注册一个无限循环,等待cancel信号或超时事件。
新的Register产生一个新的go线程,该线程在超时后运行回调,并在执行前一个超时后安排新的超时。cancel通道返回给调用方,以控制循环应在何时停止。
func (timeout *TimeoutHandler) Register() chan struct{} {
cancelChan := make(chan struct{})
go func () {
select {
case _ = <- cancelChan:
return
case _ = <- time.AfterFunc(time.Duration(timeout.duration) * time.Second):
func () {
stateLock.Lock()
defer stateLock.Unlock()
timeout.callback(timeout.session)
} ()
}
} ()
return cancelChan
}
func (timeout* TimeoutHandler) Cancel() {
if timeout.cancelChan == nil {
return
}
timeout.cancelChan <- struct{}{}
}
这种方法为注册的每个超时提供了一个cancel通道。取消调用将一个空结构发送到通道并触发取消。但是,这也不能解决先前的问题;超时可能会在通过通道调用Cancel之前以及超时线程获取锁之前到期。
对应的解决方案是在锁之后检查超时范围内的cacel通道。
case _ = <- time.AfterFunc(time.Duration(timeout.duration) * time.Second):
func () {
stateLock.Lock()
defer stateLock.Unlock()
select {
case _ = <- handler.cancelChan:
return
default:
timeout.callback(timeout.session)
}
} ()
}
最后,这可以确保仅在遇到锁之后才执行回调,并且不会触发取消。
死锁
此解决方案似乎有效;但是存在一个潜在的隐患——死锁。
仔细检查代码,考虑并发调用的方法。问题在cancel通道本身。我们将其设置为无缓冲通道,这意味着其发送是阻塞调用。在超时处理程序中调用"取消"后,只有在该处理程序被取消后才能继续操作。这里的问题是,当有多个调用到同一取消通道时,取消请求仅使用一次。如果并发事件要取消相同的超时处理程序,例如链接断开或控制数据包事件,则很容易发生这种情况。这将导致死锁,可能会使应用程序停止。
应对该死锁问题的解决方案是让通道缓冲一下,让发送并不总是阻塞,并且在并发调用的情况下显式使发送变为非阻塞。这样可以确保取消发送一次,并且不会阻止后续的取消调用。
func (timeout* TimeoutHandler) Cancel() {
if timeout.cancelChan == nil {
return
}
select {
case timeout.cancelChan <- struct{}{}:
default:
// can't send on the channel, someone has already requested the cancellation.
}
}
结论
实践中了解了在使用并发代码时出现常见的常见错误。由于其不确定性,即使进行大量测试,也很容易发现这些问题。这是HALO在实现中遇到的三个主要问题:
在不同步的情况下更新共享数据
这似乎很明显,但是如果同时进行的更新发生在不同的位置,则实际上很难发现。结果是数据竞争,由于一个更新会覆盖另一个更新,对同一数据的多次更新可能导致更新丢失。在HALO中,正在更新同一共享映射上的计划超时参考。(有趣的是,如果Go在同一个Map对象上检测到并发读/写操作,会引发致命错误,可以尝试运行Go的数据竞争检测器)。最终会导致丢失超时引用,并且无法取消给定的超时。不要是可以使用锁。
缺少条件检查
在不能仅依靠锁独占性的情况下,需要进行条件检查。想象一个经典的场景,有一个生产者和多个消费者使用一个共享队列。生产者可以将一项添加到队列中,并唤醒所有消费者。唤醒调用意味着队列中有一些数据可用,并且由于队列是共享的,因此必须通过锁来同步访问。每个消费者都有机会遇到锁;但是,仍然需要检查队列中是否有项目。需要进行条件检查,因为当遇到锁时还不知道队列状态。
在HALO中,超时处理程序收到了来自计时器到期的"唤醒"调用,但是它仍需要检查是否已向其发送了取消信号,然后才能继续执行回调。
死锁
当一个线程被卡住,无限期地等待一个信号唤醒时,就会发生这种情况,但是这个信号永远不会到达。
在HALO中,由于多次发送调用到一个非缓冲且阻塞的通道导致死锁,这样仅在同一通道上完成接收后,发送调用才会返回。超时线程循环迅速在取消通道上接收信号;但是,在接收到第一个信号后,它将中断环路,并且再也不会从该通道读取数据。其余的调用将会被卡住。
为避免这种情况,需要仔细检查代码,谨慎处理阻塞调用,并确保不会发生线程饥饿。HALO中解决方法是使取消调用成为非阻塞调用,因为不需要阻塞调用。
相关推荐
- 史上最全的浏览器兼容性问题和解决方案
-
微信ID:WEB_wysj(点击关注)◎◎◎◎◎◎◎◎◎一┳═┻︻▄(页底留言开放,欢迎来吐槽)●●●...
-
- 平面设计基础知识_平面设计基础知识实验收获与总结
-
CSS构造颜色,背景与图像1.使用span更好的控制文本中局部区域的文本:文本;2.使用display属性提供区块转变:display:inline(是内联的...
-
2025-02-21 16:01 yuyutoo
- 写作排版简单三步就行-工具篇_作文排版模板
-
和我们工作中日常word排版内部交流不同,这篇教程介绍的写作排版主要是用于“微信公众号、头条号”网络展示。写作展现的是我的思考,排版是让写作在网格上更好地展现。在写作上花费时间是有累积复利优势的,在排...
- 写一个2048的游戏_2048小游戏功能实现
-
1.创建HTML文件1.打开一个文本编辑器,例如Notepad++、SublimeText、VisualStudioCode等。2.将以下HTML代码复制并粘贴到文本编辑器中:html...
- 今天你穿“短袖”了吗?青岛最高23℃!接下来几天气温更刺激……
-
最近的天气暖和得让很多小伙伴们喊“热”!!! 昨天的气温到底升得有多高呢?你家有没有榜上有名?...
- CSS不规则卡片,纯CSS制作优惠券样式,CSS实现锯齿样式
-
之前也有写过CSS优惠券样式《CSS3径向渐变实现优惠券波浪造型》,这次再来温习一遍,并且将更为详细的讲解,从布局到具体样式说明,最后定义CSS变量,自定义主题颜色。布局...
- 你的自我界限够强大吗?_你的自我界限够强大吗英文
-
我的结果:A、该设立新的界限...
- 行内元素与块级元素,以及区别_行内元素和块级元素有什么区别?
-
行内元素与块级元素首先,CSS规范规定,每个元素都有display属性,确定该元素的类型,每个元素都有默认的display值,分别为块级(block)、行内(inline)。块级元素:(以下列举比较常...
-
- 让“成都速度”跑得潇潇洒洒,地上地下共享轨交繁华
-
去年的两会期间,习近平总书记在参加人大会议四川代表团审议时,对治蜀兴川提出了明确要求,指明了前行方向,并带来了“祝四川人民的生活越来越安逸”的美好祝福。又是一年...
-
2025-02-21 16:00 yuyutoo
- 今年国家综合性消防救援队伍计划招录消防员15000名
-
记者24日从应急管理部获悉,国家综合性消防救援队伍2023年消防员招录工作已正式启动。今年共计划招录消防员15000名,其中高校应届毕业生5000名、退役士兵5000名、社会青年5000名。本次招录的...
- 一起盘点最新 Chrome v133 的5大主流特性 ?
-
1.CSS的高级attr()方法CSSattr()函数是CSSLevel5中用于检索DOM元素的属性值并将其用于CSS属性值,类似于var()函数替换自定义属性值的方式。...
- 竞走团体世锦赛5月太仓举行 世界冠军杨家玉担任形象大使
-
style="text-align:center;"data-mce-style="text-align:...
- 学物理能做什么?_学物理能做什么 卢昌海
-
作者:曹则贤中国科学院物理研究所原标题:《物理学:ASourceofPowerforMan》在2006年中央电视台《对话》栏目的某期节目中,主持人问过我一个的问题:“学物理的人,如果日后不...
-
- 你不知道的关于这只眯眼兔的6个小秘密
-
在你们忙着给熊本君做表情包的时候,要知道,最先在网络上引起轰动的可是这只脸上只有两条缝的兔子——兔斯基。今年,它更是迎来了自己的10岁生日。①关于德艺双馨“老艺...
-
2025-02-21 16:00 yuyutoo
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)