Clickhouse Alter *** 作造成zk连接丢失的问题分析

Clickhouse Alter *** 作造成zk连接丢失的问题分析,第1张

业务方数据在出现错误后需要重跑数据,由于业务方没有使用MergeTree的折叠表,需要删除旧的数据后,再重新跑数据写入新的正确的数据。

之前这种模式一直运转的比较好,没有出现过问题,不过近期发现,对该表发起Alter语句时,出现了ZK Connection Loss的错误,但是对其他的表发起Alter语句没有出现相同的错误。

本文主要分析一下定位问题的过程以及确定问题所在,也希望大家就该问题进行讨论提供更好的解决方案。

Clickhouse版本:209345

表结构:

Alter语句以及响应的报错信息:

首先查看了一下clickhouse的错误日志,错误日志中有相关的堆栈信息

再查看了一下zk的错误日志

然后大致对比了一下系统的表的大小,目前出问题的表是最大的。

从上面可以看出表的数据分片很多。

分析ZK的日志发现,ZK认为客户端发送的消息格式不正确,从而主动断开了clickhouse的连接。从clickhouse的异常日志有可以看出正在执行zk *** 作时出现了连接断开的错误。

现在我们从代码层面去看看问题的根因,当clickhouse执行alter *** 作时,如果对应的mutation如果涉及到分片数据的变更时,就需要对分片进行锁定,而分片的锁定 *** 作是在对应的分片对应的zk子目录下面创建一个临时节点,如下面代码所示:

clickhouse在zk的访问中,采用了大量批量 *** 作,在上面的分片锁定 *** 作中,它针对所有影响到的分片的锁定批量一次性提交命令到zk中,而zk的传输使用了jute,jute缺省最大的包大小为1M,具体细节可以参考一下关于zookeeper写入数据超过1M大小的踩坑记。

这里clickhouse的问题在于它没有做分包,而是对所有影响的分片合并请求后,批量向zk发起请求,从而造成了超过zk最大的传输包大小,从而造成连接断开。

为什么这里需要一次性的批量提交呢?具体的原因有朋友了解的可以分享一下,我理解可能clickhouse需要做类似事务级别的保证。

知道了问题的根因首先考虑到增加zk的jute缺省的最大包大小,zookeeper本身,我们可以在配置上实现。但是我们查看了一下clickhouse的zk配置相关参数,能够调整的主要是ip、port和会话时长,没有看到jute大小的控制参数,所以这条路基本上行不通,经过只修改zk的参数重启后,测试也发现不能成功。

控制Alter DELETE影响的数据范围,从原来的Alter语句来看我们已经制定了时间的范围,但是看起来Clickhouse不会主动根据条件来做分区裁剪。查看源码也发现没有这块逻辑,但是从最新的clickhouse的文档中,我们可以看到Delete语句支持分区 *** 作。

锁出现的原因

临界资源是什么: 多线程执行流所共享的资源

锁的作用是什么, 可以做原子 *** 作, 在多线程中针对临界资源的互斥访问 保证一个时刻只有一个线程可以持有锁对于临界资源做修改 *** 作

任何一个线程如果需要修改,向临界资源做写入 *** 作都必须持有锁,没有持有锁就不能对于临界资源做写入 *** 作

锁 : 保证同一时刻只能有一个线程对于临界资源做写入 *** 作 (锁地功能)

再一个直观地代码引出问题,再从指令集的角度去看问题

上述一个及其奇怪的结果,这个结果每一次运行都可能是不一样的,Why ? 按照我们本来的想法是每一个线程 + 20000000 结果肯定应该是60000000呀,可以就是达不到这个值

为何? (深入汇编指令来看) 一定将过程放置到汇编指令上去看就可以理解这个过程了

a++; 或者 a += 1; 这些 *** 作的汇编 *** 作是几个步骤

其实是三个步骤:

正常情况下,数据少, *** 作的线程少,问题倒是不大,想一想要是这样的情况下, *** 作次数大,对齐 *** 作的线程多,有些线程从中间切入进来了,在运算之后还没写回内存就另外一个线程切入进来同时对于之前的数据进行++ 再写回内存, 啥效果,多次++ *** 作之后结果确实一次加加 *** 作后的结果。 这样的 *** 作 (术语叫做函数的重入) 我觉得其实就是重入到了汇编指令中间了,还没将上一次运算的结果写回内存就重新对这个内存读取再运算写入,结果肯定和正常的逻辑后的结果不一样呀

来一幅解释一下

咋办 其实问题很清楚,我们只需要处理的是多条汇编指令不能让它中间被插入其他的线程运算 (要想自己在执行汇编指令的时候别人不插入进来) 将多条汇编指令绑定成为一条指令不就OK了嘛。

也就是原子 *** 作!!!

不会原子 *** 作? *** 作系统给咱提供了线程的 绑定方式工具呀:mutex 互斥锁(互斥量), 自旋锁(spinlock), 读写锁(readers-writer lock) 他们也称作悲观锁 作用都是一个样,将多个汇编指令锁成为一条原子 *** 作 (此处的汇编指令也相当于如下的临界资源)

悲观锁:锁如其名,每次都悲观地认为其他线程也会来修改数据,进行写入 *** 作,所以会在取数据前先加锁保护,当其他线程想要访问数据时,被阻塞挂起

乐观锁:每次取数据的时候,总是乐观地认为数据不会被其他线程修改,因此不上锁。但是在更新数据前, 会判断其他数据在更新前有没有对数据进行修改。

互斥锁

最为常见使用地锁就是互斥锁, 也称互斥量 mutex

特征,当其他线程持有互斥锁对临界资源做写入 *** 作地时候,当前线程只能挂起等待,让出CPU,存在线程间切换工作

解释一下存在线程间切换工作 : 当线程试图去获取锁对临界资源做写入 *** 作时候,如果锁被别的线程正在持有,该线程会保存上下文直接挂起,让出CPU,等到锁被释放出来再进行线程间切换,从新持有CPU执行写入 *** 作

互斥锁需要进行线程间切换,相比自旋锁而言性能会差上许多,因为自旋锁不会让出CPU, 也就不需要进行线程间切换的步骤,具体原理下一点详述

加互斥量(互斥锁)确实可以达到要求,但是会发现运行时间非常的长,因为线程间不断地切换也需要时间, 线程间切换的代价比较大

相关视频推荐

你绕不开的组件—锁,4个方面手撕锁的多种实现

“惊群”原理、锁的设计方案及绕不开的“死锁”问题

学习地址:C/C++Linux服务器开发/后台架构师零声教育-学习视频教程-腾讯课堂

需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括 C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等),免费分享

自旋锁

spinlock自旋锁

对比互斥量(互斥锁)而言,获取自旋锁不需要进行线程间切换,如果自旋锁正在被别的线程占用,该线程也不会放弃CPU进行挂起休眠,而是恰如其名的在哪里不断地循环地查看自旋锁保持者(持有者)是否将自旋锁资源释放出来 (自旋地原来就是如此)

口语解释自旋:持有自旋锁的线程不释放自旋锁,那也没有关系呀,我就在这里不断地一遍又一遍地查询自旋锁是否释放出来,一旦释放出来我立马就可以直接使用 (因为我并没有挂起等待,不需要像互斥锁还需要进行线程间切换,重新获取CPU,保存恢复上下文等等 *** 作)

哪正是因为上述这些特点,线程尝试获取自旋锁,获取不到不会采取休眠挂起地方式,而是原地自旋(一遍又一遍查询自旋锁是否可以获取)效率是远高于互斥锁了 那我们是不是所有情况都使用自旋锁就行了呢,互斥锁就可以放弃使用了吗

解释自旋锁地弊端:如果每一个线程都仅仅只是需要短时间获取这个锁,那我自旋占据CPU等待是没啥问题地。要是线程需要长时间地使用占据(锁)。。。 会造成过多地无端占据CPU资源,俗称站着茅坑不拉屎 但是要是仅仅是短时间地自旋,平衡CPU利用率 + 程序运行效率 (自旋锁确实是在有些时候更加合适)

自旋锁需要场景:内核可抢占或者SMP(多处理器)情况下才真正需求 (避免死锁陷入死循环,疯狂地自旋,比如递归获取自旋锁 你获取了还要获取,但是又没法释放)

自旋锁的使用函数其实和互斥锁几乎是一摸一样地,仅仅只是需要将所有的mutex换成spin即可

仅仅只是在init存在些许不同

何为惊群,池塘一堆, 我瞄准一条插过去,但是好似所有的都像是觉着自己正在被插一样的四处逃窜。 这个就是惊群的生活一点的理解

惊群现象其实一点也不少,比如说 accept pthread_cond_broadcast 还有多个线程共享epoll监视一个listenfd 然后此刻 listenfd 说来 SYN了,放在了SYN队列中,然后完成了三次握手放在了 accept队列中了, 现在问题是这个connect我应该交付给哪一个线程处理呢

多个epoll监视准备工作的线程 就是这群 (),然后connet就是鱼叉,这一叉下去肯定是所有的 epoll线程都会被惊醒 (多线程共享listenfd引发的epoll惊群)

同样如果将上述的多个线程换成多个进程共享监视 同一个 listenfd 就是(多进程的epoll惊群现象)

咱再画一个草图再来理解一下这个惊群:

如果是多进程道理是一样滴,仅仅只是将所有的线程换成进程就OK了

终是来到了今天的正题了: epoll惊群问题地解决上面了

首先 先说说accept的惊群问题,没想到吧accept 平时大家写它的多线程地时候,多个线程同时accept同一个listensock地时候也是会存在惊群问题地,但是accept地惊群问题已经被Linux内核处理了: 当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理

但是对于epoll的惊群问题,内核却没有直接进行处理。哪既然内核没有直接帮我们处理,我们应该如何针对这种现象做出一定的措施呢

惊群效应带来的弊端: 惊群现象会造成epoll的伪唤醒,本来epoll是阻塞挂起等待着地,这个时候因为挂起等待是不会占用CPU地。。。 但是一旦唤醒就会占用CPU去处理发生地IO事件, 但是其实是一个伪唤醒,这个就是对于线程或者进程的无效调度。然而进程或者线程地调取是需要花费代价地,需要上下文切换。需要进行进程(线程)间的不断切换 本来多核CPU是用来支持高并发地,但是现在却被用来无效地唤醒,对于多核CPU简直就是一种浪费 (浪费系统资源) 还会影响系统的性能

解决方式(一般是两种)

Nginx的解决方式:

加锁:惊群问题发生的前提是多个进程(线程)监听同一个套接字(listensock)上的事件,所以我们只让一个进程(线程)去处理监听套接字就可以了。

画两张图来理解一下:

上述还没有进行一个每一个进程都对应一个listensock 而是多线程共享一个listensock 运行结果如下

所有的线程同时被唤醒了,但是实际上会处理连接的仅仅只是一个线程,

咱仅仅只是将主线程做如上这样一个简单的修改,每一个线程对应一个listensock;每一个线程一个独有的监视窗口,将问题抛给内核去处理,让内核去负载均衡 : 结果如下

仅仅唤醒一个线程来进行处理连接,解决了惊群问题

本文通过介绍两种锁入手,以及为什么需要锁,锁本质就是为了保护,持有锁你就有权力有能力 *** 作写入一定的临界保护资源,没有锁你就不行需要等待,本质其实是将多条汇编指令绑定成原子 *** 作

然后介绍了惊群现象,通过一个巧妙地例子,扔一颗石子,只是瞄准一条鱼扔过去了,但是整池鱼都被惊醒了,

对应我们地实际问题就是, 多个线程或者进程共同监视同一个listensock。。。。然后IO连接事件到来地时候本来仅仅只是需要一个线程醒过来处理即可,但是却会使得所有地线程(进程)全部醒过来,造成不必要地进程线程间切换,多核CPU被浪费喔,系统资源被浪费

处理方式 一。 Nginx 源码加互斥锁处理。。 二。设置SO_REUSEPORT, 使得多个进程线程可以同时连接同一个port , 为每一个进程线程搞一个listensock 将问题抛给内核去处理,让他去负载均衡地仅仅将IO连接事件分配给一个进程或线程

设置方式如下:

1、时间段设置:为设备添加门禁时间段,设备牛刀割鸡照已设置的时间段控制门禁设置工作;

2、节假日设置:新增门禁节假日,并设置该节假日使用的门禁时间段;

3、门禁组设置:设置在某个时间段或时间段的组合,用户可以通过验证打开门。(注:可以验证打开门的用户组合,需要在用户门禁设置中设置);

4、开锁组合设置:为设备添加开锁组合,并设置该组合使用的门禁组的开门人数。(注:各门禁组开门人员的总数必须小于或等于5)。

1、ZK是一套以 AJAX/XUL/Java 为基础的网页应用程序开发框架,用于丰富网页应用程序的使用界面。

2、ZK是科视界股份有限公司注册申请的品牌,品牌产品有光导电子液位仪。

3、zk**网,主要为广大影视迷提供最新最好看的电视剧、**、动画片及播放服务。网站内容绿色健康,页面简洁大方。无广告无d窗,24小时安全监控,保证没病毒没不良内容。

ZK发展理念

zk**网自2012年07月上线以来,经历了种种不同在困难,在风格设计与内容发布方面我投入了不少成本与精力之外,更是网站管理人员对网站的精心维护,然而,网站的日流量正在日益飙升,努力打造成为一个广大网民喜欢的无广告d窗、绿色安全的**体验网站。

ZK程序包含了一个以AJAX为基础、事件驱动(event-driven)、高互动性的引擎。

百度百科-ZK (程序)、百度百科-ZK (商标)、百度百科-zk**网

在分布式系统中,分布式锁的应用场景是非常广泛的。Redis和ZooKeeper是目前比较常见的实现方案,那它们之间有什么区别呢,我们应该如何选择?

对于ZooKeeper来说,它的会话与服务端是通过心跳保持连接的,当心跳超时客户端会收到链接丢失的事件,通常来说这不是问题,因为ZK的客户端对自动连接。但如果一直连接不上,服务端会在会话有效期之后,将会话置为过期。这里有个很重要的细节,即使会话已经过期了,在重新连接上服务器之前,客户端永远也不知道自己已经过期了,因为ZooKeeper的会话过期是由服务器端触发并通知客户端的。

这样,如果客户端a与ZooKeeper服务器之间的通信长时间断开的话,客户端a会误以为自己并没有过期,只是临时性的链接丢失,但实际上服务端有可能已经将它置为过期,而这时其它与ZooKeeper服务器通信正常的客户端就有可能获取同一个锁(因为服务端在将客户端a置过期的同时会清除它所创建的临时节点),进而造成两个客户端同时访问临界区的资源。

要解决这个问题也不难,在链接丢失和重新建立链接时,客户端都会收到相应的事件通知。我们可以在客户端维护一个状态变量(比如valid),在链接丢失时将它置为false,并在重新建立链接后将它置为true,并在客户端访问临界区的资源代码中不定期的检查valid的值,一旦发生valid值变为false时,就说明链接丢失了,这时就暂停临界区资源的访问,并等待重新建立链接。

这么做挺麻烦的,大大增加了使用锁的复杂性。但是否真需要这么玩也是看具体的场景的:

使用Redis实现的锁,并不存在这样的问题,因为key并不会因为客户端怎么样而被删除。但它也有麻烦的一面,为了防止客户端长时间阻塞或者故障宕机而导至锁无法释放,我们需要在加锁的时候指定一个过期时间,不过成本确实比ZooKeeper的实现要低很多。

经过以上分析,我们不难得出以下结论:分布式锁的实现使用ZooKeeper还是redis来实现,取决于加锁的目的。

以上就是关于Clickhouse Alter *** 作造成zk连接丢失的问题分析全部的内容,包括:Clickhouse Alter *** 作造成zk连接丢失的问题分析、Linux下各种锁的理解和使用及总结解决epoll惊群问题(面试常考)-、ZK开锁组合设置等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/9802637.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-02
下一篇 2023-05-02

发表评论

登录后才能评论

评论列表(0条)

保存