Thursday, December 16, 2021

SegmentFault 最新的文章

SegmentFault 最新的文章


100行代码实现React核心调度功能

Posted: 15 Dec 2021 06:16 PM PST

大家好,我卡颂。

想必大家都知道React有一套基于Fiber架构的调度系统。

这套调度系统的基本功能包括:

  • 更新有不同优先级
  • 一次更新可能涉及多个组件的render,这些render可能分配到多个宏任务中执行(即时间切片
  • 高优先级更新会打断进行中的低优先级更新

本文会用100行代码实现这套调度系统,让你快速了解React的调度原理。

我知道你不喜欢看大段的代码,所以本文会以+代码片段的形式讲解原理。

文末有完整的在线Demo,你可以自己上手玩玩。

开整!

欢迎加入人类高质量前端框架群,带飞

准备工作

我们用work这一数据结构代表一份工作,work.count代表这份工作要重复做某件事的次数。

Demo中要重复做的事是"执行insertItem方法,向页面插入<span/>":

const insertItem = (content: string) => {   const ele = document.createElement('span');   ele.innerText = `${content}`;   contentBox.appendChild(ele); };

所以,对于如下work

const work1 = {   count: 100 }

代表:执行100次insertItem向页面插入100个<span/>

work可以类比React的一次更新work.count类比这次更新要render的组件数量。所以Demo是对React更新流程的类比

来实现第一版的调度系统,流程如图:

包括三步:

  1. workList队列(用于保存所有work)插入work
  2. schedule方法从workList中取出work,传递给perform
  3. perform方法执行完work的所有工作后重复步骤2

代码如下:

// 保存所有work的队列 const workList: work[] = [];  // 调度 function schedule() {   // 从队列尾取一个work   const curWork = workList.pop();      if (curWork) {     perform(curWork);   } }  // 执行 function perform(work: Work) {   while (work.count) {     work.count--;     insertItem();   }   schedule(); }

为按钮绑定点击交互,最基本的调度系统就完成了:

button.onclick = () => {   workList.unshift({     count: 100   })   schedule(); }

点击button就能插入100个<span/>

React类比就是:点击button,触发同步更新,100个组件render

接下来我们将其改造成异步的。

Scheduler

React内部使用Scheduler完成异步调度。

Scheduler是独立的包。所以可以用他改造我们的Demo

Scheduler预置了5种优先级,从上往下优先级降低:

  • ImmediatePriority,最高的同步优先级
  • UserBlockingPriority
  • NormalPriority
  • LowPriority
  • IdlePriority,最低优先级

scheduleCallback方法接收优先级与回调函数fn,用于调度fn

// 将回调函数fn以LowPriority优先级调度 scheduleCallback(LowPriority, fn)

Scheduler内部,执行scheduleCallback后会生成task这一数据结构:

const task1 = {   expiration: startTime + timeout,   callback: fn }

task1.expiration代表task1的过期时间,Scheduler会优先执行过期的task.callback

expirationstartTime为当前开始时间,不同优先级的timeout不同。

比如,ImmediatePrioritytimeout为-1,由于:

startTime - 1 < startTime

所以ImmediatePriority会立刻过期,callback立刻执行。

IdlePriority对应timeout为1073741823(最大的31位带符号整型),其callback需要非常长时间才会执行。

callback会在新的宏任务中执行,这就是Scheduler调度的原理。

用Scheduler改造Demo

改造后的流程如图:

改造前,work直接从workList队列尾取出:

// 改造前 const curWork = workList.pop();

改造后,work可以拥有不同优先级,通过priority字段表示。

比如,如下work代表以NormalPriority优先级插入100个\<span/\>

const work1 = {   count: 100,   priority: NormalPriority }

所以,改造后每次都使用最高优先级的work

// 改造后 // 对workList排序后取priority值最小的(值越小,优先级越高) const curWork = workList.sort((w1, w2) => {    return w1.priority - w2.priority; })[0];

改造后流程的变化

由流程图可知,Scheduler不再直接执行perform,而是通过执行scheduleCallback调度perform.bind(null, work)

即,满足一定条件的情况下,生成新task

const someTask = {   callback: perform.bind(null, work),   expiration: xxx }

同时,work的工作也是可中断的。在改造前,perform会同步执行完work中的所有工作:

while (work.count) {   work.count--;   insertItem(); }

改造后,work的执行流程随时可能中断:

while (!needYield() && work.count) {   work.count--;   insertItem(); }
needYield方法的实现(何时会中断)请参考文末在线Demo

高优先级打断低优先级的例子

举例来看一个高优先级打断低优先级的例子:

  1. 插入一个低优先级work,属性如下
const work1 = {   count: 100,   priority: LowPriority }
  1. 经历schedule(调度),perform(执行),在执行了80次工作时,突然插入一个高优先级work,此时:
const work1 = {   // work1已经执行了80次工作,还差20次执行完   count: 20,   priority: LowPriority } // 新插入的高优先级work const work2 = {   count: 100,   priority: ImmediatePriority }
  1. work1工作中断,继续schedule。由于work2优先级更高,会进入work2对应perform,执行100次工作
  2. work2执行完后,继续schedule,执行work1剩余的20次工作

在这个例子中,我们需要区分2个打断的概念:

  1. 在步骤3中,work1执行的工作被打断。这是微观角度的打断
  2. 由于work1被打断,所以继续schedule。下一个执行工作的是更高优的work2work2的到来导致work1被打断,这是宏观角度的打断

之所以要区分宏/微观,是因为微观的打断不一定意味着宏观的打断

比如:work1由于时间切片用尽,被打断。没有其他更高优的work与他竞争schedule的话,下一次perform还是work1

这种情况下微观下多次打断,但是宏观来看,还是同一个work在执行。这就是时间切片的原理。

调度系统的实现原理

以下是调度系统的完整实现原理:

对照流程图来看:

总结

本文是React调度系统的简易实现,主要包括两个阶段:

  • schedule
  • perform

这里是完整Demo地址

面试被问TopK问题,可以这样优雅的解答

Posted: 15 Dec 2021 08:39 PM PST

前言

hello,大家好,我是bigsai哥哥,好久不见,甚是想念哇🤩!

今天给大家分享一个TOPK问题,不过我这里不考虑特别大分布式的解决方案,普通的一道算法题。

首先搞清楚,什么是topK问题?

topK问题,就是找出序列中前k大(或小)的数,topK问题和第K大(或小)的解题思路其实大致一致的。

TopK问题是一个非常经典的问题,在笔试和面试中出现的频率都非常非常高(从不说假话)。下面,从小小白的出发点,认为topK是求前K大的问题,一起认识下TopK吧!

当前,在求TopK和第K大问题解法差不多,这里就用力扣215数组的第k个大元素 作为解答的题演示啦。学习topk之前,这篇程序员必知必会的十大排序一定要会。

排序法

找到TopK,并且排序TopK

啥,你想要我找到TopK?不光光TopK,你想要多少个,我给你多少个,并且还给你排序给排好,啥排序我最熟悉呢?

如果你想到冒泡排序O(n^2)那你就大意了啊。

如果使用O(n^2)级别的排序算法,那也是要优化的,其中冒泡排序和简单选择排序,每一趟都能顺序确定一个最大(最小)的值,所以不需要把所有的数据都排序出来,只需要执行K次就行啦,所以这种算法的时间复杂度也是O(nk)

这里给大家回顾一下冒泡排序和简单选择排序区别:

冒泡排序和简单选择排序都是多趟,每趟都能确定一个最大或者最小,区别就是冒泡在枚举过程中只和自己后面比较,如果比后面大那么就交换;而简单选择是每次标记一个最大或者最小的数和位置,然后用这一趟的最后一个位置数和它交换(每一趟确定一个数枚举范围都慢慢变小)。

下面用一张图表示过程:

image-20211214140418441

这里把code也给大家提供一下,简单选择上面图给的是每次选最小,实现的时候每次选最大就可以了。

//交换数组中两位置元素 private void swap(int[] arr, int i, int j) {   int temp = arr[i];   arr[i] = arr[j];   arr[j] = temp; } //冒泡排序实现 public int findKthLargest1(int[] nums, int k) {   for(int i=nums.length-1;i>=nums.length-k;i--)//这里也只是k次   {     for(int j=0;j<i;j++)     {       if(nums[j]>nums[j+1])//和右侧邻居比较       {         swap(nums,j,j+1);       }     }   }   return nums[nums.length-k]; } //简单选择实现 public int findKthLargest2(int[] nums, int k) {   for (int i = 0; i < k; i++) {//这里只需要K次     int max = i; // 最小位置     for (int j = i + 1; j < nums.length; j++) {       if (nums[j] > nums[max]) {         max = j; // 更换最小位置       }     }     if (max != i) {       swap(nums, i, max); // 与第i个位置进行交换     }   }   return nums[k-1]; }

当然,快排和归并排序甚至堆排序也可以啊,这些排序的时间复杂度为O(nlogn),也就是将所有数据排序完然后直接返回结果,这部分就不再详细讲解啦,调调api或者手写排序都可。

两种思路的话除了K极小的情况O(nk)快一些,大部分情况其实还是O(nlogn)情况快一些的,不过从O(n^2)想到O(nk),还是有所收获的。

基于堆排优化

这里需要知道堆相关的知识,我以前写过优先队列和堆排序,这里先不重复讲,大家也可以看一下:

优先队列不知道,看看堆排序吧

硬核,手写一个优先队列

上面说道堆排序O(nlogn)那是将所有元素都排序完然后取前k个,但是其实上我们分析一下这个堆排序的过程和几个注意点哈:

堆这种数据结构,分为大根堆和小根堆,小根堆是父节点值小于子节点值,大根堆是父节点的值大于子节点的值,这里肯定是要采用大根堆的。

堆看起来是一个树形结构,但是堆是个完全二叉树我们用数组存储效率非常高,并且也非常容易利用下标直接找到父子节点,所以都用数组来实现堆,每次排序完成的节点都将数移到数组末尾让一个新数组组成一个新的堆继续。

堆排序从大的来看可以分成两个部分,无序数组建堆和在堆基础上每次取对顶排序。其中无序数组建堆的时间复杂度为O(n),在堆基础上排序每次取堆顶元素,然后将最后一个元素移到堆顶进行调整堆,每次只需要O(logn)级别的时间复杂度,完整排序完n次就是O(nlogn),但是咱们每次只需要k次,所以完成k个元素排序功能需要花费O(klogn)时间复杂度,整个时间复杂度为O(n+klogn)因为和前面区分一下就不合并了。

画了一张图帮助大家理解,进行两次就获得Top2,进行k次就获得TopK了。

image-20211214153102243

实现代码为:

class Solution {     private void swap(int[] arr, int i, int j) {         int temp = arr[i];         arr[i] = arr[j];         arr[j] = temp;     }     //下移交换 把当前节点有效变换成一个堆(大根)     public void shiftDown(int arr[],int index,int len)//0 号位置不用     {         int leftchild=index*2+1;//左孩子         int rightchild=index*2+2;//右孩子         if(leftchild>=len)             return;         else if(rightchild<len&&arr[rightchild]>arr[index]&&arr[rightchild]>arr[leftchild])//右孩子在范围内并且应该交换         {             swap(arr, index, rightchild);//交换节点值             shiftDown(arr, rightchild, len);//可能会对孩子节点的堆有影响,向下重构         }         else if(arr[leftchild]>arr[index])//交换左孩子         {             swap(arr, index, leftchild);             shiftDown(arr, leftchild, len);         }     }     //将数组创建成堆     public void creatHeap(int arr[])     {         for(int i=arr.length/2;i>=0;i--)         {             shiftDown(arr, i,arr.length);         }     }     public int findKthLargest(int nums[],int k)     {         //step1建堆         creatHeap(nums);         //step2 进行k次取值建堆,每次取堆顶元素放到末尾         for(int i=0;i<k;i++)         {             int team=nums[0];             nums[0]=nums[nums.length-1-i];//删除堆顶元素,将末尾元素放到堆顶             nums[nums.length-1-i]=team;             shiftDown(nums, 0, nums.length-i-1);//将这个堆调整为合法的大根堆,注意(逻辑上的)长度有变化         }         return nums[nums.length-k];     } }

基于快排优化

上面堆排序都能优化,那么快排呢?

快排当然能啊,这么牛的事情怎么能少得了我快排呢?

这部分需要堆快排有一定了解和认识,前面很久前写过:图解手撕冒泡和快排 (后面待优化),快排的核心思想就是:分治 ,每次确定一个数字的位置,然后将数字分成两个部分,左侧比它小,右侧比它大,然后递归调用这个过程。每次调整的时间复杂度为O(n),平均次数为logn次,所以平均时间复杂度为O(nlogn)。

image-20211214160549137

但是这个和求TopK有什么关系呢?

我们求TopK,其实就是求比目标数字大的K个,我们随机选一个数字例如上面的5,5的左侧有4个,右侧有4个,可能会出现下面几种情况了:

① 如果k-1等于5右侧数量,那么说明中间这个5就是第K个,它和它的右侧都是TopK。

如果k-1小于5右侧数的数量 ,那么说明TopK全在5的右侧,那么可以直接压缩空间成右侧继续递归调用同样方法查找。

如果k-1大于5右侧的数量,那么说明右侧和5全部在TopK中,然后左侧还有(k-包括5右侧数总数),此时搜查范围压缩,k也压缩。举个例子,如果k=7 那么5和5右侧已经占了5个数字一定在Top7中,我们只需要在5左侧找到Top2就行啦。

这样一来每次数值都会被压缩,这里因为快排不是完全递归,时间复杂度不是O(nlogn)而是O(n)级别(详细的可以找一些网上证明),但是测试样例有些极端代码比如给你跟你有序1 2 3 4 5 6…… 找Top1 就出现比较极端的情况。所以具体时候会用一个随机数和第一个交换一下防止特殊样例(仅仅为了刷题用的),当然我这里为了就不加随机交换的啦,并且如果这里要得到的TopK是未排序的。

详细逻辑可以看下实现代码为:

class Solution {     public int findKthLargest(int[] nums, int k) {         quickSort(nums,0,nums.length-1,k);         return nums[nums.length-k];     }     private void quickSort(int[] nums,int start,int end,int k) {         if(start>end)             return;         int left=start;         int right=end;         int number=nums[start];         while (left<right){             while (number<=nums[right]&&left<right){                 right--;             }             nums[left]=nums[right];             while (number>=nums[left]&&left<right){                 left++;             }             nums[right]=nums[left];         }         nums[left]=number;         int num=end-left+1;         if(num==k)//找到k就终止             return;         if(num>k){             quickSort(nums,left+1,end,k);         }else {             quickSort(nums,start,left-1,k-num);         }     } }

计数排序番外篇

排序总有一些骚操作的排序—线性排序,那么你可能会问桶类排序可以嘛?

也可以啦,不过要看数值范围进行优化,桶类排序适合数据均匀密集出现次数比较多的情况,而计数排序更是希望数值能够小一点。

那么利用桶类排序的具体核心思想是怎么样的呢?

先用计数排序统计各个数字出现次数,然后将新开一个数组从后往前叠加求和计算。

image-20211214164832473

这种情况非常适合数值巨量并且分布范围不大的情况。

代码本来不想写了,但是念在你会给我三连我写一下吧

//力扣215 //1 <= k <= nums.length <= 104 //-104 <= nums[i] <= 104 public int findKthLargest(int nums[],int k) {   int arr[]=new int[20001];   int sum[]=new int[20001];    for(int num:nums){     arr[num+10000]++;   }   for(int i=20000-1;i>=0;i--){     sum[i]+=sum[i+1]+arr[i];     if(sum[i]>=k)       return i-10000;   }   return 0; }

结语

好啦,今天的TopK问题就到这里啦,相信你下次遇到肯定会拿捏它。

TopK问题不难,就是巧妙利用排序而已。排序是非常重要的,面试会非常高频。

这里我就不藏着掖着摊牌了,以面试官的角度会怎么引导你说TOPK问题。

狡猾的面试官:

嗯,我们来聊聊数据结构与算法,来讲讲排序吧,你应该接触过吧?讲出你最熟悉的三种排序方式,并讲解一下其中具体算法方式。

卑微的我:

bia la bia la bia la bia la……

如果你提到快排,桶排序说不定就让你用这个排序实现一下TopK问题,其他排序也可能,所以掌握好十大排序是非常必要的!

首发公众号:bigsai ,转载请附上本文链接,原创不易,求个点赞关注,谢谢!

华为音频编辑服务,助力开发者高效创新

Posted: 14 Dec 2021 06:12 PM PST

由华为开发者联盟主办的HDD深圳站(Huawei Developer Day)于12月14日成功举行。在现场,华为音频编辑服务(Audio Editor Kit)的多项特性与应用场景吸引了众多开发者的注意。尤其是音源分离和空间音频渲染能力,为音乐创作带来更大的可能及便利。

音频编辑服务是华为为开发者开放的各类场景音频处理能力的集合,汇聚了华为在音乐、语音等相关音频领域的先进技术。音频编辑服务提供音频基础编辑、伴奏提取、空间渲染、变声降噪等丰富的音频处理能力,为全球开发者提供性能优异、简单易用、开放性强的接口,帮助开发者轻松高效构建应用音频编辑能力。

音频流式特性,提高音频可玩性

华为音频编辑服务支持AI配音、变声、均衡器、风格等能力,提供降噪功能对输入的音频进行降噪处理,并对人声进行修复和增强,可提升语音信号质量,帮助APP构建更多应用场景,提升音频处理的可玩性。

音源分离,支持更多音乐创作场景

华为音频编辑服务为开发者提供音源分离的功能,能通过人工智能的方式准确解析乐曲中的人声和各种乐器元素并提取到独立的音轨,满足音乐创作、伴奏制作,扒带等多种场景。

空间渲染,轻松实现音频2D转3D

华为音频编辑服务为开发者提供空间音频渲染服务,帮助开发者在应用中构建3D音效。用户可以在音频中添加空间渲染的效果,将不同组成的音频渲染到指定的三维空间方位,从而达到环绕声效果。

音频编辑服务提供的空间渲染,不仅支持固定摆位的静态渲染,还可以提供通过扩展和移动带来的动态渲染效果。此功能支持界面和文件接口处理,满足音乐创作、环绕声编辑、游戏影视配乐等多种使用场景。

在未来,华为音频编辑服务还将开放更多特性,并考虑结合华为多媒体管线服务(AV Pipeline kit),允许开发者自定义媒体插件和编排处理流程,并创新AI作曲、歌声合成、智能降噪等应用开发需求。全方位满足各个领域开发者的需求,打造开发者得心应手的音频编辑基础工具。

如您想了解更多详情,请参考:
华为开发者联盟音频编辑服务官网:https://developer.huawei.com/...
获取开发音频编辑服务指导文档: https://developer.huawei.com/...

了解更多详情>>

访问华为开发者联盟官网
获取开发指导文档
华为移动服务开源仓库地址:GitHubGitee

关注我们,第一时间了解 HMS Core 最新技术资讯~

因事件堵塞导致页面卡顿

Posted: 13 Dec 2021 03:11 AM PST

概述

上周星期四(2021/12/09)开始陆续有供应商反馈卖家中心页面公共模块(菜单栏)存在卡顿问题,特别是在进入商家报价页面,但是开发与测试在转测、转演、灰度阶段均未发现该性能问题。

kadun.gif

分析

首先,我们需要了解页面的实现架构,卖家中心商家报价页面的公共模块ftl与右侧内容区ftl是通过Sitemesh框架整合而成的,公共模块ftl只定义了<div id="seller-topbar"></div><div id="seller-sidebar" class="menus"></div>的空架子,其元素内容通过引入执行seller-react-frame.js将元素内容注入到相关元素标签中。

另外,注入的元素内容并不是在seller-react-frame.js中实现的,而是在这个脚本中引入了其他脚本,引入的这些脚本是React实现topbar、slider组件后经webpack构建生成的。

右侧内容区是切切实实使用freemarker模板引擎实现的,数据由后端注入到页面中,相当于服务端渲染。

了解请求页面情况后,我们通过networkperformance性能分析看看

network分析

image-20211213152937288.png

image-20211213153300120.png

network面板可以看出,公共模块中的一个脚本原始大小有1.3Mgzip压缩后传输大小为375KB,从disk cache(磁盘缓存)读取耗时2.78s,清除缓存从CDN请求耗时2.88s,而公共模块其他的脚本原始大小都很小,只有一点几KB,请求耗时只有一两百毫秒。

显而易见,这个脚本过大了,需要做代码拆分。另外,从disk cache中读取也需要耗时,改用走memory cache(内存缓存),因为memory cache更快。

performance分析

在通过Performance检测页面性能发现公共模块(菜单栏)渲染的事件循环前面有一个执行耗时很长的事件 循环,这个事件循环是在初始化四级地址和品牌选项,四级地址树我们使用的ZTree.js处理的,里面init方法初始化树节点的事件复杂度比较高,因此我们看到检测到的执行时间较长(接近3秒) ,这更加堵塞了后面菜单栏事件循环的执行。

image-20211213181636064.png

鉴于业务需求,四级地址树节点可以不用在刚进入页面时初始化,因此我将其脚本请求和执行放在了onload函数中,但是通过上图发现四级地址初始化还是先于渲染菜单栏执行,这是为什么呢?

image-20211213183218733.png

image-20211213182723881.png

网络请求面板看一下脚本获取的先后顺序,发现公共模块脚本先于四级地址脚本获取,这使我更加困惑了。这时想到左侧菜单栏是React实现的,菜单栏数据是通过接口获取的,而不是像freemarker模板引擎是通过后端注入数据的,然后排查获取菜单接口发现,为了获取到菜单栏信息,竟然存在三个接口的串行调用,因为接口的串行调用导致渲染菜单栏的任务晚于四级地址初始化的任务加入到任务队列中,再加上四级地址初始化耗时较长,从而引起用户感知左侧菜单栏卡顿。

解决方案

(1)对公共模块中1.3M的脚本做代码拆分;

(2)优化脚本中的接口串行调用;

(3)从disk cache读取改成从memory cache读取;

(4)因为网络请求耗时不稳定,无法确保渲染菜单栏事件循环先于四级地址树节点初始化事件循环,因此可以在请求菜单栏接口成功后通过postMessage或者自定义事件通知可以开始初始化四级地址树节点;

ClickHouse集群数据均衡方案分享

Posted: 10 Dec 2021 05:47 PM PST

导语

ClickHouse集群数据在写入时,虽然可以通过Distributed引擎的sharding_key指定策略,从而保证一定程度的数据均衡,但这并不是最终解决方案。

比如rand()均衡策略虽然可以保证数据的相对均衡,但是可能会破坏数据的内在业务逻辑。举个简单的例子,我们想要将kafka的数据写入clickhouse集群,如果采用rand()的策略,则可能将同一个partition的数据拆分到clickhouse集群不同的shard中,为后续的数据分析等造成了一定的麻烦。

虽然有类似clickhouse-sinker之类的数据导入工具,可以做到数据导入时的均衡,但是一旦集群扩展了节点,仍然无法将存量数据均衡到新增加的节点中去。这样就造成了存量节点的数据仍然很多,新增节点的数据相对较少,并不能起到很好的负载均衡的作用。

数据均衡方案探讨

我们在讨论数据均衡方案的时候,首先需要明确两个前提:

  • 针对clickhouse集群,而不是单点
  • 针对MergeTree家族的引擎数据(其他引擎的数据表由于无法通过分布式表去读写,因此也不具备数据均衡的意义)

我们知道,clickhouse存储数据是完完全全的列式存储,这也就意味着,在同一个partition下,数据很难再一条条的进行拆分(虽然可以做到,但比较麻烦)。因此,数据均衡最科学的方案是以partition为单位,整个partition进行搬迁。这也就意味着,分区的粒度越小,最终的数据越接近均衡。

另一个我们要思考的问题就是,如果其中某一个分区我们一直在写入数据,我们是无法获取该分区的实际大小的(因为一直在变化)。那么,如果该分区数据也参数数据均衡的话,可能参与均衡的partition并不是一个完整的分区,就会导致分区数据被拆散,从而造成不可预知的问题。所以,我们希望最新的一个分区,不参与数据均衡的运算。

如何能获取到最新的分区呢?其实可以通过SQL查询到:

SELECT argMax(partition, modification_time) FROM system.parts WHERE database='?' AND table='?'

以上SQL查询出来的,就是指定的数据库表最新的分区。将这个分区排除在外,那么剩下的分区就是都可以参与数据均衡的分区了。

另一个核心问题是,如何将partition数据在不同节点之间进行移动?我们很容易想到 attachdetach,但attachdetach的前提是,我们必须要设置配置文件中当前操作用户allow_drop_detached标志为1。对于带副本的集群,我们总能通过zookeeper的路径非常方便地将分区数据在不同节点间fetch过来。

 -- 在目标节点执行  ALTER TABLE {{.tbname}} FETCH PARTITION '{{.partition}}' FROM '{{.zoopath}}'  ALTER TABLE {{.tbname}} ATTACH PARTITION '{{.partition}}'    -- 在原始节点执行  ALTER TABLE {{.tbname}} DROP PARTITION '{{.partition}}'

但是对于非副本模式的集群则没那么简单了。因为我们无法知道zoopath,所以不能通过fetch的方式获取到数据,因此,只能使用物理的方式将数据进行传输(比如scp, rsync等)到指定节点上。考虑到传输效率,这里我们使用rsync的方式。

-- 原始节点执行 ALTER TABLE {{.tbname}} DETACH PARTITION '{{.partition}}'
# 原始节点执行 rsync -e "ssh -o StrictHostKeyChecking=false" -avp /{{.datapath}}/clickhouse/data/{{.database}}/{{.table}}/detached dstHost:/{{.datapath}}/clickhouse/data/{{.database}}/{{.table}}/detached rm -fr /{{.datapath}}/clickhouse/data/{{.database}}/{{.table}}/detached
-- 目标节点执行 ALTER TABLE {{.tbname}} ATTACH PARTITION '{{.partition}}' -- 原始节点执行 ALTER TABLE {{.tbname}} DROP DETACHED PARTITION '{{.partition}}'

但是,通过rsync的方式需要有前提,那就是首先必须在各个节点上已经安装过rsync工具了,如果没有安装,可通过下面的命令安装:

yum install -y rsync

其次,需要配置各节点之间的互信(主要是moveout的节点到movein节点之间的互信,但其实我们并不知道数据在节点间数如何移动的,因此最好全部配置)。

以上问题解决后,那么就剩下最核心的一个问题了。数据如何均衡?

这里需要说明的是,由于是整个partition的移动,因此,无法做到绝对的均衡,而是只能做到相对的数据均衡。partition的粒度越小,均衡越精确。

一种比较科学的方案是,将各个节点的分区数据按大小排列之后,将最大的节点数据移动到最小的节点中去,次大的节点移到次小的节点,以此类推,不断向中间靠拢,直到满足某一个阈值,则不再移动。

这一段的代码实现我们已经通过ckman项目开源出来了,如果感兴趣的朋友可以通过下面的链接阅读源码:ckman:rebalancer

因此,不难发现,数据均衡的过程中,分区数据由于可能已经被detach,但是还没来得及在新的节点上attach,这时候去做查询,可能存在一定几率的不准确。

所以,在做数据均衡的过程中,最好不要有查询操作。

插入操作反而不受影响,因为我们已经排除了最新的分区不参与均衡运算。

ckman如何实现数据均衡

ckman作为一款管理和监控ClickHouse集群的可视化工具,天然集成了数据均衡的功能。只需要点击集群管理页面的"均衡集群"按钮,即可实现数据均衡的操作。
图片.png

与此同时,ckman还提供了命令行方式的数据均衡工具rebalancer, 其参数如下:

  • -ch-data-dir

    • clickhouse集群数据目录
  • -ch-hosts

    • 节点列表(每个shard只需列出一个,如果shard有多个副本,无需全部列出)
  • -ch-password

    • clickhouse用户密码
  • -ch-port

    • clickhouseTCP端口,默认9000
  • -ch-user

    • clickhouse的用户,界面操作时,使用default用户
  • -os-password

    • 节点的ssh登录密码(非副本模式时需要)
  • -os-port

    • 节点的ssh端口,默认22(非副本模式时需要)
  • -os-user

    • 节点的ssh用户(非副本模式时需要)

如:

rebalancer -ch-data-dir=/var/lib/ --ch-hosts=192.168.0.1,192.168.0.2,192.168.0.3 --ch-password=123123 --ch-port=9000 --ch-user=default --os-password=123456 --os-port=22 --os-user=root

实操案例

我们在ckman中准备了一个名为eoi的集群,该集群有三个节点,分别为192.168.21.73,192.168.21.74,192.168.21.75,集群为非副本模式。

图片.png

我们从官方文档给出的数据集中导入如下数据:https://clickhouse.com/docs/e...

该数据是从2019年1月到2021年5月,共计30个月的航空数据,为了更直观地展示数据均衡,本文将官方的建表语句做了微调,按照月进行分区,并且在集群各个节点都创建表:

CREATE TABLE opensky ON CLUSTER eoi (     callsign String,     number String,     icao24 String,     registration String,     typecode String,     origin String,     destination String,     firstseen DateTime,     lastseen DateTime,     day DateTime,     latitude_1 Float64,     longitude_1 Float64,     altitude_1 Float64,     latitude_2 Float64,     longitude_2 Float64,     altitude_2 Float64 ) ENGINE = MergeTree  PARTITION BY toYYYYMM(day) ORDER BY (origin, destination, callsign);

并创建分布式表:

CREATE TABLE dist_opensky ON CLUSTER eoi AS opensky ENGINE = Distributed(eoi, default, opensky, rand())

下载数据:

wget -O- https://zenodo.org/record/5092942 | grep -oP 'https://zenodo.org/record/5092942/files/flightlist_\d+_\d+\.csv\.gz' | xargs wget

数据下载完成大约4.3G

图片.png

使用下面的脚本将数据导入到其中一个节点:

for file in flightlist_*.csv.gz; do gzip -c -d "$file" | clickhouse-client --password 123123 --date_time_input_format best_effort --query "INSERT INTO opensky FORMAT CSVWithNames"; done

导入完成后,分别查看各节点数据如下:

-- 总数据 master :) select  count() from dist_opensky;  SELECT count() FROM dist_opensky  Query id: b7bf794b-086b-4986-b616-aef1d40963e3  ┌──count()─┐ │ 66010819 │ └──────────┘  1 rows in set. Elapsed: 0.024 sec.   -- node 21.73 master :) select  count() from opensky;  SELECT count() FROM opensky  Query id: 5339e93c-b2ed-4085-9f58-da099a641f8f  ┌──count()─┐ │ 66010819 │ └──────────┘  1 rows in set. Elapsed: 0.002 sec.    -- node 21.74 worker-1 :) select  count() from opensky;  SELECT count() FROM opensky  Query id: 60155715-064e-4c4a-9103-4fd6bf9b7667  ┌─count()─┐ │       0 │ └─────────┘  1 rows in set. Elapsed: 0.002 sec.   -- node 21.75 worker-2 :) select count() from opensky;  SELECT count() FROM opensky  Query id: d04f42df-d1a4-4d90-ad47-f944b7a32a3d  ┌─count()─┐ │       0 │ └─────────┘  1 rows in set. Elapsed: 0.002 sec.  

从以上信息,我们可以知道,原始数据6600万条全部在21.73这个节点上,另外两个节点21.7421.75没有数据。

ckman界面可以看到如下信息:

图片.png

然后点击数据均衡,等待一段时间后,会看到界面提示数据均衡成功,再次查看各节点数据:

-- 总数据 master :) select  count() from dist_opensky;  SELECT count() FROM dist_opensky  Query id: bc4d27a9-12bf-4993-b37c-9f332ed958c9  ┌──count()─┐ │ 66010819 │ └──────────┘  1 rows in set. Elapsed: 0.006 sec.    -- node 21.73 master :) select  count() from opensky;  SELECT count() FROM opensky  Query id: a4da9246-190c-4663-8091-d09b2a9a2ea3  ┌──count()─┐ │ 24304792 │ └──────────┘  1 rows in set. Elapsed: 0.002 sec.  -- node 21.74 worker-1 :) select  count() from opensky;  SELECT count() FROM opensky  Query id: 5f6a8c89-c21a-4ae1-b69f-2755246ca5d7  ┌──count()─┐ │ 20529143 │ └──────────┘  1 rows in set. Elapsed: 0.002 sec.   -- node 21.75 worker-2 :) select count() from opensky;  SELECT count() FROM opensky  Query id: 569d7c63-5279-48ad-a296-013dc1df6756  ┌──count()─┐ │ 21176884 │ └──────────┘  1 rows in set. Elapsed: 0.002 sec.

通过上述操作,简单演示了数据均衡在ckman中的实现,原始数据6600万条全部在node1,通过均衡之后,其中node1数据为2400万条,node22000万条,node32100万条,实现了大致的数据均衡。

结语

虽然我们可以通过ckman之类的工具可以实现数据的大致均衡,大大改善了操作的便利性,但数据均衡本身就是一个非常复杂的命题,一旦涉及到存储策略(如数据存储在远端HDFS上),那么又会增加数据均衡的复杂性,这些都是ckman目前所不能支持的操作(远程数据做数据均衡没有意义,但是可以均衡其元数据,这样可以在查询时充分利用各节点的CPU性能)。因此,数据均衡想要做得科学而精确,仍然需要更多的努力。

No comments:

Post a Comment