mongodb move chunk缓慢

有几个参考链接可以学习下

第三个链接,提供了一个check list

If it is slow, there are a number of possible reasons why that is the case, but the most common for a system that is not busy are:

  1. Source Disk I/O - if the data is not in active memory when it is read, it must be paged in from disk
  2. Network - if there is latency, rate limiting, packet loss etc. then the read may take quite a while
  3. Target Disk I/O - the data and indexes have to be written to disk, lots of indexes can make this worse, but usually this is not a problem on a lightly loaded system
  4. Problems with migrations causing aborts and failed migrations (issues with config servers, issues with deletes on primaries)
  5. Replication lag - for migrations to replica sets, write concern w:2 or w:majority is used by default and requires up to date secondaries to satisfy it.

前三项一般没啥问题,第四条,也不是放弃迁移,可能会导致反复迁移

考虑movechunk的流程,先打快照,把当前备份复制过去,然后再复制增量 如果这个增量没完没了的话,迁移时间必然会延长,有点像第五条的场景,第五条应该就是oplog赶不上了

再考虑oplog一直增长的情形 -> 插入。如果是顺序插入应该很快。如果是删除之类的操作可能会耗时过长,但是也不至于很慢, 再考虑什么会导致插入过慢 -> 索引过多,引用我老大的一句话,“索引过多导致插入变慢是数据库常识”,只要有索引,以前索引优化以及影响的那套理论还是成立的。

Read More

一次 Illegal instructions 问题(围观)定位

简单说,一个上层应用,下层是Rocksdb,编译机编好后放到测试机器上,崩溃, Got signal: 4 (Illegal instruction).,堆栈提示崩在shared_ptr上。

我简单搜了一下,发现可能有两个原因

  1. ud2a 指令导致的runtime abort。需要看堆栈反汇编。简单看代码不太像这个原因, 有个例子 warning: cannot pass objects of non-POD type ‘class A’ through ‘…’; call will abort at runtim

  2. 两个机器指令集有问题,同事和我说另一个测试机器内核版本号一毛一样,就没问题。不崩溃,就更让我确定是指令集的问题,简单搜了一圈,可以确定是-march=native 这个指令,现在就要确定这个指令在makefile中有没有了

期间我还查cpuinfo看指令集,看编译机和测试机有没有区别(没区别)

查指令集 grep -m1 ^flag /proc/cpuinfo | tr ' ' '\n' | sort

然后我查看了rocksdb的makefile cmakefile list 没有找到。(这是我的失误,我应该grep)

然后我又找上层应用 有没有这个编译指令,grep 没找到

最后同事发现在rocksdb的build_tools/build_detect_platform里。我才明白我实际上不懂rocksdb到底是怎么编译的。

makefile 会调用build_detect_platform 生成make_config.mk,包含了各种编译选项。

# detect what platform we're building on
dummy := $(shell (export ROCKSDB_ROOT="$(CURDIR)"; export PORTABLE="$(PORTABLE)"; "$(CURDIR)/build_tools/build_detect_platform" "$(CURDIR)/make_config.mk"))
# this file is generated by the previous line to set build flags and sources
include make_config.mk
CLEAN_FILES += make_config.mk

唉。

grep就会发现,且INSTALL中也有提示,要是想提高移植性,makefile 加上 PORTABLE=1

./INSTALL.md:(-march=native or the equivalent). If you want to build a portable binary, add 'PORTABLE=1' before 唉。

下面这个链接解释了为啥有人会用-march=native

https://stackoverflow.com/questions/52653025/why-is-march-native-used-so-rarelyWhy is -march=native used so rarely?Why is -march=native used so rarely?

下面这个链接解释了opencv类似的问题

https://answers.ros.org/question/11873/illegal-instruction-when-using-image_view-with-theora/

Read More

进程线程比较

进程 vs 线程

FIXME:需要重新验证

我们按照多个不同的维度,来看看多线程和多进程的对比(注:因为是感性的比较,因此都是相对的,不是说一个好得不得了,另外一个差的无法忍受)。

对比维度 多进程 多线程 总结
数据共享、同步 数据共享复杂,需要用IPC;数据是分开的,同步简单 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 各有优势
内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高 线程占优
创建销毁、切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度很快 线程占优
编程、调试 编程简单,调试简单 编程复杂,调试复杂 进程占优
可靠性 进程间不会互相影响 一个线程挂掉将导致整个进程挂掉 进程占优
分布式 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 适应于多核分布式 进程占优

看起来比较简单,优势对比上是“线程 3.5 v 2.5 进程”,我们只管选线程就是了?

呵呵,有这么简单我就不用在这里浪费口舌了,还是那句话,没有绝对的好与坏,只有哪个更加合适的问题。我们来看实际应用中究竟如何判断更加合适。

1)需要频繁创建销毁的优先用线程

原因请看上面的对比。

这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的

2)需要进行大量计算的优先使用线程

所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。

这种原则最常见的是图像处理、算法处理。

3)强相关的处理用线程,弱相关的处理用进程

什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。

一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。

当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。

4)可能要扩展到多机分布的用进程,多核分布的用线程

原因请看上面对比。

5)都满足需求的情况下,用你最熟悉、最拿手的方式

至于“数据共享、同步”、“编程、调试”、“可靠性”这几个维度的所谓的“复杂、简单”应该怎么取舍,我只能说:没有明确的选择方法。但我可以告诉你一个选择原则:如果多进程和多线程都能够满足要求,那么选择你最熟悉、最拿手的那个。

需要提醒的是:虽然我给了这么多的选择原则,但实际应用中基本上都是“进程+线程”的结合方式,千万不要真的陷入一种非此即彼的误区。

1、进程与线程

进程是程序执行时的一个实例,即它是程序已经执行到课中程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。

线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。

“进程——资源分配的最小单位,线程——程序执行的最小单位”

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

总的来说就是:进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。(下面的内容摘自Linux下的多线程编程

使用多线程的理由之一是和进程相比,它是一种非常”节俭”的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种”昂贵”的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

  • 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
  • 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
  • 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

在Unix上编程采用多线程还是多进程的争执由来已久,这种争执最常见到在B/S通讯中服务端并发技术 的选型上,比如WEB服务器技术中,Apache是采用多进程的(perfork模式,每客户连接对应一个进程,每进程中只存在唯一一个执行线 程),Java的Web容器Tomcat、Websphere等都是多线程的(每客户连接对应一个线程,所有线程都在一个进程中)。

从Unix发展历史看,伴随着Unix的诞生多进程就出现了,而多线程很晚才被系统支持,例如Linux直到内核2.6,才支持符合Posix规范的NPTL线程库。进程和线程的特点,也就是各自的优缺点如下:

进程优点:编程、调试简单,可靠性较高。 进程缺点:创建、销毁、切换速度慢,内存、资源占用大。 线程优点:创建、销毁、切换速度快,内存、资源占用小。 线程缺点:编程、调试复杂,可靠性较差。

上面的对比可以归结为一句话:“线程快而进程可靠性高”。线程有个别名叫“轻量级进程”,在有的书籍资料上介绍线程可以十倍、百倍的效率快于进程; 而进程之间不共享数据,没有锁问题,结构简单,一个进程崩溃不像线程那样影响全局,因此比较可靠。我相信这个观点可以被大部分人所接受,因为和我们所接受的知识概念是相符的。

在写这篇文章前,我也属于这“大部分人”,这两年在用C语言编写的几个C/S通讯程序中,因时间紧总是采用多进程并发技术,而且是比较简单的现场为 每客户fork()一个进程,当时总是担心并发量增大时负荷能否承受,盘算着等时间充裕了将它改为多线程形式,或者改为预先创建进程的形式,直到最近在网 上看到了一篇论文《Linux系统下多线程与多进程性能分析》作者“周丽 焦程波 兰巨龙”,才认真思考这个问题,我自己也做了实验,结论和论文作者的相似,但对大部分人可以说是颠覆性的。

下面是得出结论的实验步骤和过程,结论究竟是怎样的? 感兴趣就一起看看吧。

实验代码使用周丽论文中的代码样例,我做了少量修改,值得注意的是这样的区别:

论文实验和我的实验时间不同,论文所处的年代linux内核是2.4,我的实验linux内核是2.6,2.6使用的线程库是NPTL,2.4使用的是老的Linux线程库(用进程模拟线程的那个LinuxThread)。

论文实验和我用的机器不同,论文描述了使用的环境:单cpu 机器基本配置为:celeron 2.0 GZ, 256M, Linux 9.2,内核 2.4.8。我的环境是:双核 Intel(R) Xeon(R) CPU 5130 @ 2.00GHz(做实验时,禁掉了一核),512MG内存,Red Hat Enterprise Linux ES release 4 (Nahant Update 4),内核2.6.9-42。

进程实验代码(fork.c):

1. \#include <stdlib.h>
2. \#include <stdio.h>
3. \#include <signal.h>
4. 
5. \#define P_NUMBER 255 //并发进程数量
6. \#define COUNT 5 //每次进程打印字符串数
7. \#define TEST_LOGFILE "logFile.log"
8. FILE *logFile=NULL;
9. 
10. char *s="hello linux\0";
11. 
12. int main()
13. {
14.     int i=0,j=0;
15.     logFile=fopen(TEST_LOGFILE,"a+");//打开日志文件
16.     for(i=0;i<P_NUMBER;i++)
17.     {
18.         if(fork()==0)//创建子进程,if(fork()==0){}这段代码是子进程运行区间
19.         {
20.             for(j=0;j<COUNT;j++)
21.             {
22.                 printf("[%d]%s\n",j,s);//向控制台输出
23.                 /*当你频繁读写文件的时候,Linux内核为了提高读写性能与速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。可能导致测试结果不准,所以在此注释*/
24.                 //fprintf(logFile,"[%d]%s\n",j,s);//向日志文件输出,
25.             }
26.             exit(0);//子进程结束
27.         }
28.     }
29.     
30.     for(i=0;i<P_NUMBER;i++)//回收子进程
31.     {
32.         wait(0);
33.     }
34.     
35.     printf("Okay\n");
36.     return 0;
37. }

进程实验代码(thread.c):

1. \#include <pthread.h>
2. \#include <unistd.h>
3. \#include <stdlib.h>
4. \#include <stdio.h>
5. 
6. \#define P_NUMBER 255//并发线程数量
7. \#define COUNT 5 //每线程打印字符串数
8. \#define TEST_LOG "logFile.log"
9. FILE *logFile=NULL;
10. 
11. char *s="hello linux\0";
12. 
13. print_hello_linux()//线程执行的函数
14. {
15.     int i=0;
16.     for(i=0;i<COUNT;i++)
17.     {
18.         printf("[%d]%s\n",i,s);//想控制台输出
19.         /*当你频繁读写文件的时候,Linux内核为了提高读写性能与速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。可能导致测试结果不准,所以在此注释*/
20.         //fprintf(logFile,"[%d]%s\n",i,s);//向日志文件输出
21.     }
22.     pthread_exit(0);//线程结束
23. }
24. 
25. int main()
26. {
27.     int i=0;
28.     pthread_t pid[P_NUMBER];//线程数组
29.     logFile=fopen(TEST_LOG,"a+");//打开日志文件
30.     
31.     for(i=0;i<P_NUMBER;i++)
32.         pthread_create(&pid[i],NULL,(void *)print_hello_linux,NULL);//创建线程
33.         
34.     for(i=0;i<P_NUMBER;i++)
35.         pthread_join(pid[i],NULL);//回收线程
36.         
37.     printf("Okay\n");
38.     return 0;
39. }

两段程序做的事情是一样的,都是创建“若干”个进程/线程,每个创建出的进程/线程打印“若干”条“hello linux”字符串到控制台和日志文件,两个“若干”由两个宏 P_NUMBER和COUNT分别定义,程序编译指令如下:

> gcc -o fork fork.c
> gcc -lpthread -o thread thread.c

实验通过time指令执行两个程序,抄录time输出的挂钟时间(real时间):

> time ./fork
>  time ./thread

每批次的实验通过改动宏 P_NUMBER和COUNT来调整进程/线程数量和打印次数,每批次测试五轮,得到的结果如下:

一、重复周丽论文实验步骤

(注:本文平均值算法采用的是去掉一个最大值去掉一个最小值,然后平均)

单核(双核机器禁掉一核),进程/线程数:255,打印次数5            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m0.070s 0m0.071s 0m0.071s 0m0.070s 0m0.070s 0m0.070s
多线程 0m0.049s 0m0.049s 0m0.049s 0m0.049s 0m0.049s 0m0.049s
单核(双核机器禁掉一核),进程/线程数:255,打印次数10            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m0.112s 0m0.101s 0m0.100s 0m0.085s 0m0.121s 0m0.104s
多线程 0m0.097s 0m0.089s 0m0.090s 0m0.104s 0m0.080s 0m0.092s
单核(双核机器禁掉一核),进程/线程数:255,打印次数50            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m0.459s 0m0.531s 0m0.499s 0m0.499s 0m0.524s 0m0.507s
多线程 0m0.387s 0m0.456s 0m0.435s 0m0.423s 0m0.408s 0m0.422s
单核(双核机器禁掉一核),进程/线程数:255,打印次数100            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m1.141s 0m0.992s 0m1.134s 0m1.027s 0m0.965s 0m1.051s
多线程 0m0.925s 0m0.899s 0m0.961s 0m0.934s 0m0.853s 0m0.919s
单核(双核机器禁掉一核),进程/线程数:255,打印次数500            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m5.221s 0m5.258s 0m5.706s 0m5.288s 0m5.455s 0m5.334s
多线程 0m4.689s 0m4.578s 0m4.670s 0m4.566s 0m4.722s 0m4.646s
单核(双核机器禁掉一核),进程/线程数:255,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m12.680s 0m16.555s 0m11.158s 0m10.922s 0m11.206s 0m11.681s
多线程 0m12.993s 0m13.087s 0m13.082s 0m13.485s 0m13.053s 0m13.074s
单核(双核机器禁掉一核),进程/线程数:255,打印次数5000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 1m27.348s 1m5.569s 0m57.275s 1m5.029s 1m15.174s 1m8.591s
多线程 1m25.813s 1m29.299s 1m23.842s 1m18.914s 1m34.872s 1m26.318s
单核(双核机器禁掉一核),进程/线程数:255,打印次数10000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 2m8.336s 2m22.999s 2m11.046s 2m30.040s 2m5.752s 2m14.137s
多线程 2m46.666s 2m44.757s 2m34.528s 2m15.018s 2m41.436s 2m40.240s

本轮实验是为了和周丽论文作对比,因此将进程/线程数量限制在255个,论文也是测试了255个进程/线程分别进行5次,10 次,50 次,100 次,500 次……10000 次打印的用时,论文得出的结果是:任务量较大时,多进程比多线程效率高;而完成的任务量较小时,多线程比多进程要快,重复打印 600 次时,多进程与多线程所耗费的时间相同。

虽然我的实验直到1000打印次数时,多进程才开始领先,但考虑到使用的是NPTL线程库的缘故,从而可以证实了论文的观点。从我的实验数据看,多线程和多进程两组数据非常接近,考虑到数据的提取具有瞬间性,因此可以认为他们的速度是相同的。

是不是可以得出这样的结论:多线程创建、销毁速度快,而多线程切换速度快,这个结论我们会在第二个试验中继续试图验证

当前的网络环境中,我们更看中高并发、高负荷下的性能,纵观前面的实验步骤,最长的实验周期不过2分钟多一点,因此下面的实验将向两个方向延伸,第一,增加并发数量,第二,增加每进程/线程的工作强度。

二、增加并发数量的实验

下面的实验打印次数不变,而进程/线程数量逐渐增加。在实验过程中多线程程序在后四组(线程数350,500,800,1000)的测试中都出现了“段错误”,出现错误的原因和多线程预分配线程栈有关。

实验中的计算机CPU是32位,寻址最大范围是4GB(2的32次方),Linux是按照3GB/1GB的方式来分配内存,其中1GB属于所有进程共享的内核空间,3GB属于用户空间(进程虚拟内存空间)。Linux2.6的默认线程栈大小是8M(通过ulimit -a查看),对于多线程,在创建线程的时候系统会为每一个线程预分配线程栈地址空间,也就是8M的虚拟内存空间。线程数量太多时,线程栈累计的大小将超过进程虚拟内存空间大小(计算时需要排除程序文本、数据、共享库等占用的空间),这就是实验中出现的“段错误”的原因。

Linux2.6的默认线程栈大小可以通过 ulimit -s 命令查看或修改,我们可以计算出线程数的最大上线: (1024102410243) / (10241024*8) = 384,实际数字应该略小与384,因为还要计算程序文本、数据、共享库等占用的空间。在当今的稍显繁忙的WEB服务器上,突破384的并发访问并不是稀 罕的事情,要继续下面的实验需要将默认线程栈的大小减小,但这样做有一定的风险,比如线程中的函数分配了大量的自动变量或者函数涉及很深的栈帧(典型的是 递归调用),线程栈就可能不够用了。可以配合使用POSIX.1规定的两个线程属性guardsize和stackaddr来解决线程栈溢出问 题,guardsize控制着线程栈末尾之后的一篇内存区域,一旦线程栈在使用中溢出并到达了这片内存,程序可以捕获系统内核发出的告警信号,然后使用 malloc获取另外的内存,并通过stackaddr改变线程栈的位置,以获得额外的栈空间,这个动态扩展栈空间办法需要手工编程,而且非常麻烦。

有两种方法可以改变线程栈的大小,使用 ulimit -s 命令改变系统默认线程栈的大小,或者在代码中创建线程时通过pthread_attr_setstacksize函数改变栈尺寸,在实验中使用的是第一种,在程序运行前先执行ulimit指令将默认线程栈大小改为1M:

ulimit -s 1024 time ./thread

单核(双核机器禁掉一核),进程/线程数:100 ,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m3.834s 0m3.759s 0m4.376s 0m3.936s 0m3.851s 0m3.874
多线程 0m3.646s 0m4.498s 0m4.219s 0m3.893s 0m3.943s 0m4.018
单核(双核机器禁掉一核),进程/线程数:255 ,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m9.731s 0m9.833s 0m10.046s 0m9.830s 0m9.866s 0m9.843s
多线程 0m9.961s 0m9.699s 0m9.958s 0m10.111s 0m9.686s 0m9.873s
单核(双核机器禁掉一核),进程/线程数:350 ,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m13.773s 0m13.500s 0m13.519s 0m13.474s 0m13.351s 0m13.498
多线程 0m12.754s 0m13.251s 0m12.813s 0m16.861s 0m12.764s 0m12.943
单核(双核机器禁掉一核),进程/线程数: 500 ,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m23.762s 0m22.151s 0m23.926s 0m21.327s 0m21.429s 0m22.413
多线程 0m20.603s 0m20.291s 0m21.654s 0m20.684s 0m20.671s 0m20.653
单核(双核机器禁掉一核),进程/线程数:800 ,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m33.616s 0m31.757s 0m31.759s 0m32.232s 0m32.498s 0m32.163
多线程 0m32.050s 0m32.787s 0m33.055s 0m32.902s 0m32.235s 0m32.641
单核(双核机器禁掉一核),进程/线程数: 1000 ,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m40.301s 0m41.083s 0m41.634s 0m40.247s 0m40.717s 0m40.700
多线程 0m41.633s 0m41.118s 0m42.700s 0m42.134s 0m41.170s 0m41.646

【实验结论】

当线程/进程逐渐增多时,执行相同任务时,线程所花费时间相对于进程有下降的趋势(本人怀疑后两组数据受系统其他瓶颈的影响),这是不是进一步验证了

多线程创建、销毁速度快,而多进程切换速度快。

出现了线程栈的问题,让我特别关心Java线程是怎样处理的,因此用Java语言写了同样的实验程序,Java程序加载虚拟机环境比较耗时,所以没 有用time提取测试时间,而直接将测时写入代码。对Linux上的C编程不熟悉的Java程序员也可以用这个程序去对比理解上面的C语言试验程序。

  1. import java.io.File;
  2. ​ import java.io.FileNotFoundException;
  3. ​ import java.io.FileOutputStream;
  4. ​ import java.io.IOException;
  5. ​ public class MyThread extends Thread
  6. ​ {
  7. ​ static int P_NUMBER = 1000; /* 并发线程数量 */
  8. ​ static int COUNT = 1000; /* 每线程打印字符串次数 */
  9. ​ static String s = “hello linux\n”;
  10. ​ static FileOutputStream out = null; /* 文件输出流 */
  11. ​ @Override
  12. ​ public void run()
  13. ​ {
  14. ​ for (int i = 0; i < COUNT; i++)
  15. ​ {
  16. ​ System.out.printf(“[%d]%s”, i, s); /* 向控制台输出 */
  17. ​ StringBuilder sb = new StringBuilder(16);
  18. ​ sb.append(“[”).append(i).append(“]”).append(s);
  19. ​ try
  20. ​ {
  21. ​ out.write(sb.toString().getBytes());/* 向日志文件输出 */
  22. ​ }
  23. ​ catch (IOException e)
  24. ​ {
  25. ​ e.printStackTrace();
  26. ​ }
  27. ​ }
  28. ​ }
  29. ​ public static void main(String[] args) throws FileNotFoundException, InterruptedException
  30. ​ {
  31. ​ MyThread[] threads = new MyThread[P_NUMBER]; /* 线程数组 */
  32. ​ File file = new File(“Javalogfile.log”);
  33. ​ out = new FileOutputStream(file, true); /* 日志文件输出流 */
  34. ​ System.out.println(“开始运行”);
  35. ​ long start = System.currentTimeMillis();
  36. ​ for (int i = 0; i < P_NUMBER; i++) //创建线程
  37. ​ {
  38. ​ threads[i] = new MyThread();
  39. ​ threads[i].start();
  40. ​ }
  41. ​ for (int i = 0; i < P_NUMBER; i++) //回收线程
  42. ​ {
  43. ​ threads[i].join();
  44. ​ }
  45. ​ System.out.println(“用时:” + (System.currentTimeMillis() – start) + “ 毫秒”);
  46. ​ return;
  47. ​ }
  48. ​ }
进程/线程数:1000 ,打印次数1000(用得原作者的数据)            
  第1次 第2次 第3次 第4次 第5次 平均
多线程 65664 ms 66269 ms 65546ms 65931ms 66540ms 65990 ms

Java程序比C程序慢一些在情理之中,但Java程序并没有出现线程栈问题,5次测试都平稳完成,可以用下面的ps指令获得java进程中线程的数量:

diaoyf@ali:~$ ps -eLf | grep MyThread | wc -l 1010

用ps测试线程数在1010上维持了很长时间,多出的10个线程应该是jvm内部的管理线程,比如用于GC。我不知道Java创建线程时默认栈的大 小是多少,很多资料说法不统一,于是下载了Java的源码jdk-6u21-fcs-src-b07-jrl-17_jul_2010.jar(实验环境 安装的是 SUN jdk 1.6.0_20-b02),但没能从中找到需要的信息。对于jvm的运行,java提供了控制参数,因此再次测试时,通过下面的参数将Java线程栈大 小定义在8192k,和Linux的默认大小一致:

diaoyf@ali:~/tmp1$ java -Xss8192k MyThread

出乎意料的是并没有出现想象中的异常,但用ps侦测线程数最高到达337,我判断程序在创建线程时在栈到达可用内存的上线时就停止继续创建了,程序运行的时间远小于估计值也证明了这个判断。程序虽然没有抛出异常,但运行的并不正常,另一个问题是最后并没有打印出“用时 xxx毫秒”信息。

这次测试更加深了我的一个长期的猜测:Java的Web容器不稳定。因为我是多年编写B/S的Java程序员,WEB服务不稳定常常挂掉也是司空见惯的,除了自己或项目组成员水平不高,代码编写太烂的原因之外,我一直猜测还有更深层的原因,如果就是线程原因的话,这颠覆性可比本篇文章的多进程性能颠覆性要大得多,想想世界上有多少Tomcat、Jboss、Websphere、weblogic在跑着,嘿嘿。

这次测试还打破了以前的一个说法:单CPU上并发超过6、7百,线程或进程间的切换就会占用大量CPU时间,造成服务器效率会急剧下降。但从上面的实验来看,进程/线程数到1000时(这差不多是非常繁忙的WEB服务器了),仍具有很好的线性。

三、增加每进程/线程的工作强度的实验

这次将程序打印数据增大,原来打印字符串为:

  1. char *s = “hello linux\0”;

现在修改为每次打印256个字节数据:

  1. char *s = “1234567890abcdef\
  2. ​ 1234567890abcdef\
  3. ​ 1234567890abcdef\
  4. ​ 1234567890abcdef\
  5. ​ 1234567890abcdef\
  6. ​ 1234567890abcdef\
  7. ​ 1234567890abcdef\
  8. ​ 1234567890abcdef\
  9. ​ 1234567890abcdef\
  10. ​ 1234567890abcdef\
  11. ​ 1234567890abcdef\
  12. ​ 1234567890abcdef\
  13. ​ 1234567890abcdef\
  14. ​ 1234567890abcdef\
  15. ​ 1234567890abcdef\
  16. ​ 1234567890abcdef\0”;
单核(双核机器禁掉一核),进程/线程数:255 ,打印次数100            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m6.977s 0m7.358s 0m7.520s 0m7.282s 0m7.218s 0m7.286
多线程 0m7.035s 0m7.552s 0m7.087s 0m7.427s 0m7.257s 0m7.257
单核(双核机器禁掉一核),进程/线程数: 255,打印次数500            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m35.666s 0m36.009s 0m36.532s 0m35.578s 0m41.537s 0m36.069
多线程 0m37.290s 0m35.688s 0m36.377s 0m36.693s 0m36.784s 0m36.618
单核(双核机器禁掉一核),进程/线程数: 255,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 1m8.864s 1m11.056s 1m10.273s 1m12.317s 1m20.193s 1m11.215
多线程 1m11.949s 1m13.088s 1m12.291s 1m9.677s 1m12.210s 1m12.150

【实验结论】

从上面的实验比对结果看,即使Linux2.6使用了新的NPTL线程库(据说比原线程库性能提高了很多,唉,又是据说!),多线程比较多进程在效率上没有任何的优势,在线程数增大时多线程程序还出现了运行错误,实验可以得出下面的结论:

在Linux2.6上,多线程并不比多进程速度快,考虑到线程栈的问题,多进程在并发上有优势。

四、多进程和多线程在创建和销毁上的效率比较

预先创建进程或线程可以节省进程或线程的创建、销毁时间,在实际的应用中很多程序使用了这样的策略,比如Apapche预先创建进程、Tomcat 预先创建线程,通常叫做进程池或线程池。在大部分人的概念中,进程或线程的创建、销毁是比较耗时的,在stevesn的著作《Unix网络编程》中有这样 的对比图(第一卷 第三版 30章 客户/服务器程序设计范式):

行号 服务器描述 进程控制CPU时间(秒,与基准之差)    
Solaris2.5.1 Digital Unix4.0b BSD/OS3.0    
0 迭代服务器(基准测试,无进程控制) 0.0 0.0 0.0
1 简单并发服务,为每个客户请求fork一个进程 504.2 168.9 29.6
2 预先派生子进程,每个子进程调用accept   6.2 1.8
3 预先派生子进程,用文件锁保护accept 25.2 10.0 2.7
4 预先派生子进程,用线程互斥锁保护accept 21.5    
5 预先派生子进程,由父进程向子进程传递套接字 36.7 10.9 6.1
6 并发服务,为每个客户请求创建一个线程 18.7 4.7  
7 预先创建线程,用互斥锁保护accept 8.6 3.5  
8 预先创建线程,由主线程调用accept 14.5 5.0  

stevens已驾鹤西去多年,但《Unix网络编程》一书仍具有巨大的影响力,上表中stevens比较了三种服务器上多进程和多线程的执行效 率,因为三种服务器所用计算机不同,表中数据只能纵向比较,而横向无可比性,stevens在书中提供了这些测试程序的源码(也可以在网上下载)。书中介 绍了测试环境,两台与服务器处于同一子网的客户机,每个客户并发5个进程(服务器同一时间最多10个连接),每个客户请求从服务器获取4000字节数据, 预先派生子进程或线程的数量是15个。

第0行是迭代模式的基准测试程序,服务器程序只有一个进程在运行(同一时间只能处理一个客户请求),因为没有进程或线程的调度切换,因此它的速度是 最快的,表中其他服务模式的运行数值是比迭代模式多出的差值。迭代模式很少用到,在现有的互联网服务中,DNS、NTP服务有它的影子。第1~5行是多进 程服务模式,期中第1行使用现场fork子进程,2~5行都是预先创建15个子进程模式,在多进程程序中套接字传递不太容易(相对于多线 程),stevens在这里提供了4个不同的处理accept的方法。6~8行是多线程服务模式,第6行是现场为客户请求创建子线程,7~8行是预先创建 15个线程。表中有的格子是空白的,是因为这个系统不支持此种模式,比如当年的BSD不支持线程,因此BSD上多线程的数据都是空白的。

从数据的比对看,现场为每客户fork一个进程的方式是最慢的,差不多有20倍的速度差异,Solaris上的现场fork和预先创建子进程的最大差别是504.2 :21.5,但我们不能理解为预先创建模式比现场fork快20倍,原因有两个:

\1. stevens的测试已是十几年前的了,现在的OS和CPU已起了翻天覆地的变化,表中的数值需要重新测试。

\2. stevens没有提供服务器程序整体的运行计时,我们无法理解504.2 :21.5的实际运行效率,有可能是1504.2 : 1021.5,也可能是100503.2 : 100021.5,20倍的差异可能很大,也可能可以忽略。

因此我写了下面的实验程序,来计算在Linux2.6上创建、销毁10万个进程/线程的绝对用时。

创建10万个进程(forkcreat.c):

  1. #include
  2. #include
  3. #include
  4. #include
  5. #include <sys/stat.h>
  6. #include
  7. #include <sys/types.h>
  8. #include <sys/wait.h>
  9. int count;//子进程创建成功数量
  10. int fcount;//子进程创建失败数量
  11. int scount;//子进程回收数量
  12. /信号处理函数–子进程关闭收集/
  13. void sig_chld(int signo)
  14. {
  15. ​ pid_t chldpid;//子进程id
  16. ​ int stat;//子进程的终止状态
  17. ​ //子进程回收,避免出现僵尸进程
  18. ​ while((chldpid=wait(&stat)>0))
  19. ​ {
  20. ​ scount++;
  21. ​ }
  22. }
  23. int main()
  24. {
  25. ​ //注册子进程回收信号处理函数
  26. ​ signal(SIGCHLD,sig_chld);
  27. ​ int i;
  28. ​ for(i=0;i<100000;i++)//fork()10万个子进程
  29. ​ {
  30. ​ pid_t pid=fork();
  31. ​ if(pid==-1)//子进程创建失败
  32. ​ {
  33. ​ fcount++;
  34. ​ }
  35. ​ else if(pid>0)//子进程创建成功
  36. ​ {
  37. ​ count++;
  38. ​ }
  39. ​ else if(pid==0)//子进程执行过程
  40. ​ {
  41. ​ exit(0);
  42. ​ }
  43. ​ }
  44. ​ printf(“count:%d fount:%d scount:%d\n”,count,fcount,scount);
  45. }

创建10万个线程(pthreadcreat.c):

  1. #include
  2. #include
  3. int count=0;//成功创建线程数量
  4. void thread(void)
  5. {
  6. ​ //啥也不做
  7. }
  8. int main(void)
  9. {
  10. ​ pthread_t id;//线程id
  11. ​ int i,ret;
  12. ​ for(i=0;i<100000;i++)//创建10万个线程
  13. ​ {
  14. ​ ret=pthread_create(&id,NULL,(void *)thread,NULL);
  15. ​ if(ret!=0)
  16. ​ {
  17. ​ printf(“Create pthread error!\n”);
  18. ​ return(1);
  19. ​ }
  20. ​ count++;
  21. ​ pthread_join(id,NULL);
  22. ​ }
  23. ​ printf(“count:%d\n”,count);
  24. }

创建10万个线程的Java程序:

  1. public class ThreadTest
  2. ​ {
  3. ​ public static void main(String[] ags) throws InterruptedException
  4. ​ {
  5. ​ System.out.println(“开始运行”);
  6. ​ long start = System.currentTimeMillis();
  7. ​ for(int i = 0; i < 100000; i++) //创建10万个线程
  8. ​ {
  9. ​ Thread athread = new Thread(); //创建线程对象
  10. ​ athread.start(); //启动线程
  11. ​ athread.join(); //等待该线程停止
  12. ​ }
  13. ​ System.out.println(“用时:” + (System.currentTimeMillis() – start) + “ 毫秒”);
  14. ​ }
  15. ​ }
单核(双核机器禁掉一核),创建销毁10万个进程/线程            
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m8.774s 0m8.780s 0m8.475s 0m8.592s 0m8.687s 0m8.684
多线程 0m0.663s 0m0.660s 0m0.662s 0m0.660s 0m0.661s 0m0.661
创建销毁10万个线程(Java)
12286毫秒

从数据可以看出,多线程比多进程在效率上有10多倍的优势,但不能让我们在使用哪种并发模式上定性,这让我想起多年前政治课上的一个场景:在讲到优越性时,面对着几个对此发表质疑评论的调皮男生,我们的政治老师发表了高见,“不能只横向地和当今的发达国家比,你应该纵向地和过去中国几十年的发展历史 比”。政治老师的话套用在当前简直就是真理,我们看看,即使是在赛扬CPU上,创建、销毁进程/线程的速度都是空前的,可以说是有质的飞跃的,平均创建销毁一个进程的速度是0.18毫秒,对于当前服务器几百、几千的并发量,还有预先派生子进程/线程的必要吗?

预先派生子进程/线程比现场创建子进程/线程要复杂很多,不仅要对池中进程/线程数量进行动态管理,还要解决多进程/多线程对accept的“抢” 问题,在stevens的测试程序中,使用了“惊群”和“锁”技术。即使stevens的数据表格中,预先派生线程也不见得比现场创建线程快,在 《Unix网络编程》第三版中,新作者参照stevens的测试也提供了一组数据,在这组数据中,现场创建线程模式比预先派生线程模式已有了效率上的优势。因此我对这一节实验下的结论是:

预先派生进程/线程的模式(进程池、线程池)技术,不仅复杂,在效率上也无优势,在新的应用中可以放心大胆地为客户连接请求去现场创建进程和线程。

我想,这是fork迷们最愿意看到的结论了。

五、双核系统重复周丽论文实验步骤

双核,进程/线程数:255 ,打印次数10            
  第1次 第2次 第3次 第4次 第5次 平均(单核倍数)
多进程 0m0.061s 0m0.053s 0m0.068s 0m0.061s 0m0.059s 0m0.060(1.73)
多线程 0m0.054s 0m0.040s 0m0.053s 0m0.056s 0m0.042s 0m0.050(1.84)
双核,进程/线程数: 255,打印次数100            
  第1次 第2次 第3次 第4次 第5次 平均(单核倍数)
多进程 0m0.918s 0m1.198s 0m1.241s 0m1.017s 0m1.172s 0m1.129(0.93)
多线程 0m0.897s 0m1.166s 0m1.091s 0m1.360s 0m0.997s 0m1.085(0.85)
双核,进程/线程数: 255,打印次数1000            
  第1次 第2次 第3次 第4次 第5次 平均(单核倍数)
多进程 0m11.276s 0m11.269s 0m11.218s 0m10.919s 0m11.201s 0m11.229(1.04)
多线程 0m11.525s 0m11.984s 0m11.715s 0m11.433s 0m10.966s 0m11.558(1.13)
双核,进程/线程数:255 ,打印次数10000            
  第1次 第2次 第3次 第4次 第5次 平均(单核倍数)
多进程 1m54.328s 1m54.748s 1m54.807s 1m55.950s 1m57.655s 1m55.168(1.16)
多线程 2m3.021s 1m57.611s 1m59.139s 1m58.297s 1m57.258s 1m58.349(1.35)

【实验结论】

双核处理器在完成任务量较少时,没有系统其他瓶颈因素影响时基本上是单核的两倍,在任务量较多时,受系统其他瓶颈因素的影响,速度明显趋近于单核的速度。

六、并发服务的不可测性

看到这里,你会感觉到我有挺进程、贬线程的论调,实际上对于现实中的并发服务具有不可测性,前面的实验和结论只可做参考,而不可定性。对于不可测性,我举个生活中的例子。

这几年在大都市生活的朋友都感觉城市交通状况越来越差,到处堵车,从好的方面想这不正反应了我国GDP的高速发展。如果你7、8年前来到西安市,穿 过南二环上的一些十字路口时,会发现一个奇怪的U型弯的交通管制,为了更好的说明,我画了两张图来说明,第一张图是采用U型弯之前的,第二张是采用U型弯 之后的。

南二环交通图一

南二环交通图二

为了讲述的方便,我们不考虑十字路口左拐的情况,在图一中东西向和南北向的车辆交汇在十字路口,用红绿灯控制同一时间只能东西向或南北向通行,一般 的十字路口都是这样管控的。随着车辆的增多,十字路口的堵塞越来越严重,尤其是上下班时间经常出现堵死现象。于是交通部门在不动用过多经费的情况下而采用 了图二的交通管制,东西向车辆行进方式不变,而南北向车辆不能直行,需要右拐到下一个路口拐一个超大的U型弯,这样的措施避免了因车辆交错而引发堵死的次 数,从而提高了车辆的通过效率。我曾经问一个每天上下班乘公交经过此路口的同事,他说这样的改动不一定每次上下班时间都能缩短,但上班时间有保障了,从而 迟到次数减少了。如果今天你去西安市的南二环已经见不到U型弯了,东西向建设了高架桥,车辆分流后下层的十字路口已恢复为图一方式。

从效率的角度分析,在图一中等一个红灯45秒,远远小于图二拐那个U型弯用去的时间,但实际情况正好相反。我们可以设想一下,如果路上的所有运行车 辆都是同一型号(比如说全是QQ3微型车),所有的司机都遵守交规,具有同样的心情和性格,那么图一的通行效率肯定比图二高。现实中就不一样了,首先车辆 不统一,有大车、小车、快车、慢车,其次司机的品行不一,有特别遵守交规的,有想耍点小聪明的,有性子慢的,也有的性子急,时不时还有三轮摩托逆行一下, 十字路口的“死锁”也就难免了。

那么在什么情况下图二优于图一,是否能拿出一个科学分析数据来呢?以现在的科学技术水平是拿不出来的,就像长期的天气预报不可预测一样,西安市的交管部门肯定不是分析各种车辆的运行规律、速度,再进行复杂的社会学、心理学分析做出U型弯的决定的,这就是要说的不可测性。

现实中的程序亦然如此,比如WEB服务器,有的客户在快车道(宽带),有的在慢车道(窄带),有的性子慢(等待半分钟也无所谓),有的性子急(拼命 的进行浏览器刷新),时不时还有一两个黑客混入其中,这种情况每个服务器都不一样,既是是同一服务器每时每刻的变化也不一样,因此说不具有可测性。开发者 和维护者能做的,不论是前面的这种实验测试,还是对具体网站进行的压力测试,最多也就能模拟相当于QQ3通过十字路口的场景。

结束语

本篇文章比较了Linux系统上多线程和多进程的运行效率,在实际应用时还有其他因素的影响,比如网络通讯时采用长连接还是短连接,是否采用 select、poll,java中称为nio的机制,还有使用的编程语言,例如Java不能使用多进程,PHP不能使用多线程,这些都可能影响到并发模 式的选型。

最后还有两点提醒:

\1. 文章中的所有实验数据有环境约束。 \2. 由于并行服务的不可测性,文章中的观点应该只做参考,而不要去定性。

【参考资料】

\1. 《Linux系统下多线程与多进程性能分析》作者“周丽 焦程波 兰巨龙”,这是我写这篇文章的诱因之一,只是不知道引用原作的程序代码是否属于侵权行为。

\2. stevens著作的《Unix网络编程(第一卷)》和《Unix高级环境编程》,这两本书应该收集入IT的四书五经。

\3. Robert Love著作的《Linux内核设计与实现》

\4. John Fusco 著作的《Linux开发工具箱》,这本书不太出名,但却是我读过的对内存和进程调度讲解最清晰明了的,第5章“开发者必备内核知识”和第6章“进程”是这本书的精华。

Read More

(译)针对 Redis on Flash 优化RocksDB

原文 Optimization of RocksDB for Redis on Flash 作者Keren Ouaknine, Oran Agra, Zvika GuzCCS Concepts Information systems → Key-value stores; Database performance evaluation; Keywords: Databases, Benchmark, Redis, Rocksdb, Key-Value Store, SSD,NVMe

0. 概述

RocksDB是一个热门的KV存储引擎,针对高速存储设备做了优化,随着SSD变得流行prevalent,RocksDB获得了广泛的使用(widespread adoption)现在在生产环境中很常见is now common in production settings,尤其是许多软件栈嵌入RocksDB作为存储引擎来优化访问块存储。不幸的是,调优RocksDB是一个复杂的任务,涉及到许多不同依赖程度的参数involving many parameters with different degrees of dependencies. 在我们这篇论文中,一个调优好的配置可以使性能比基线配置参数的表现提高一个数量级by an order of magnitude

在这篇论文中,我们将描述我们在优化RocksDB在Redis on Flash(RoF)上的经验。RoF是一个Redis用SSD作为内存拓展的商业实现,用以动态提高每个节点的容量效率。RoF用来存储载内存中的热数据,利用RocksDB存储管理SSD上的冷数据。我们将描述我们在调优RocksDB参数使用上的方法,展示我们的的经验和发现(包括调优结果的利弊),使用两个云,EC2和GCE,综上,我们会展示如何调参RocksDB提高RoF数据库的复制实践11倍以上。我们希望我们的经验能帮助到其他使用,配置和调优RocksDB来认识到RocksDB的全部潜能

1. 介绍

RocksDB是一个持久化KV存储特定面向告诉存储,主要是SSD而设计[1]。从LevelDB[2]分支出来,RocksDB提供了更好的性能[3],被设计成高度灵活来方便嵌入高层应用的一个存储引擎,事实上,许多大规模产品使用RocksDB来管理存储,Leveraging借助它的高性能来mitigate缓和日益增长的存储系统的压力[4]

不幸的是RocksDB的灵活性和高性能也有代价:调优RocksDB是一个复杂的任务牵扯到上百个参数且有varying不同程度的内部依赖,此外,“然而最近的改动使RocksDB变得越好,比起levelDB它配置就越困难”;表现差的场景太多是错误的配置造成的。[5]

当使用RocksDB,经常被问到的问题主要有

  1. 哪些配置参数使用在哪个硬件或那种工作场景下的?
  2. 这些参数的最佳optimal参数是什么
  3. 这些参数是内部独立的吗(比如说,调优参数a只在参数b,c,d有特定值的事后才生效)
  4. 两个不同的调优是累积cumulate正优化还是负优化?
  5. 如果有的话,这些参数调优后的副作用是什么?(last but not least最后但不是不重要

这篇论文试图回答这些问题,通过分享我们在RoF上优化RocksDB的经验[6,7]–这是一个Redis内存KV存储的一个商业拓展[8]。RoF用SSDs作为内存RAM的拓展来提供可媲美内存Redis变小的性能,在动态增长扩容数据集的时候数据也能存在一个单点服务器上。在ROF,热数据存储在内存中,类数据存储在SSDs中,由RocksDB来管理。RocksDB能处理所有ROF访问存储,它的性能在整个系统性能中占了主要角色。尤其是低访问的局部场景。因为ROF致力于提供可以媲美纯内存Redis表现的性能,调优RocksDB便成了首要挑战

在调优RocksDB适配ROF的过程中,我们分析了大量的参数并且测试了它们在不同工作模式下对性能的影响。数据库复制,只写模式,1:1读写模式。为了验证我们配置在不同硬件环境下的健壮性,我们测试了Amazon Elastic Compute Cloud (EC2)和Google Compute Engine(GCE),综上,我们的调优减少了复制一个节点的时间(11倍)这篇论文来描述方法,调优过程和产生效果的参数设置

在第三段,我们将描述我们的方法并解释我们的实验过程。第四段,我们会列出载性能上有最大正相关的参数。我们也列出了一些我们期望提高性能但实际上降低了或者you其他副作用的参数。我们相信这些信息会帮助到他人。然而我们的经验只限于RoF场景中,我们期望相似的系统用相似的配置,希望我们的方法和结果会帮助减少调优的时间

综上,这篇论文会有下面的产出

  • 我们发表我们在EC2和GCE上 在一系列工作集下 RocksDB benchmark的结果和分析

  • 我们描述了我们的条有过程,对性能有有最大影响的的参数,和最佳参数;并且

  • 我们描述了调优的负面结果,或不成功,降低了性能,或有非直观的副作用

2. 背景

这段简要回顾下Redis,Redis on Flash,和RocksDB,描述这些系统的上层架构,提供简要北京以帮助理解这篇论文的细节

2.1 Redis

Redis[8]是一个热门的开源内存KV村春提供了高级键值抽象,Redis是单线程的,只能在同一时间处理同一个玲玲。不像传统的KV系统(键只是个简单的数据类型,通常是字符串),Redis中的键can function as可以是复杂的数据类型,像hash,list,set,或者sorted set,furthermore此外,Redis可以使用复杂的原子操作,像是对列表的入队出队操作,在sorted set插入一个新值等等

事实证明,Redis的抽象和高速在许多延迟敏感的场景中特别有用,因此,Redis得到了广泛的使用,越来越多公司在生产环境中使用redis[9]

Redis支持高可用和持久化,高可用通过主从节点同步复制来实现,故障转移(failover),当主程序挂了,子程序能相应的接管过来。持久化可以通过以下两种方法配置

  1. 实时快照文件(RDB)
  2. 使用log文件(AOF)

要注意这三种机制(AOF重写,RDB快照,复制)都依赖于fork进程来处理一个时间点上的快照,(与此同时主程序继续处理客户端命令)

2.2 Redis on Flash (RoF)

像Redis这种内存数据库通常把数据保存在内存中,数据库快了但是也很贵,因为1内每个节点的内存容量有一定限制2每GB内存价格很贵。Redis on Flash(RoF)是一个Redis的商业拓展,用SSDs来作为内存的拓展,来高效的动态增加单个服务器上数据集大小。RoF完全兼容Redis并实现了所有的Redis命令和特性,Rof用和Redis相同的机制来保证高可用和持久化且依赖于SSD(非易失性闪存)

RoF把热数据保存在内存中,把冷数据持久化到固态硬盘上。它使用RocksDB作为存储引擎,所有的固态硬盘通过RocksDB来管理,通过RocksDB接口来访问硬盘上的值。当一个客户端请求一个冷的数据,请求会被临时阻塞直到designated指定的RoF I/O线程 将I/O请求提交到RocksDB,在此期间,Redis的主线程继续处理其他客户端的请求。

2.3 RocksDB

RocksDB[10] 是一个C++实现的开源的KV存储,它提供get/put/delete/scan 键值的操作。RocksDB可以保存大量的数据,它使用sst(sorted static table)来将数据保存到NVMes,SATA SSDs,或者机械磁盘上,尽可能的降低延迟。RocksDB使用布隆过滤器来判定键在哪个sst文件中。为了避免随机写,它将数据积累到内存中的memtable中,然后一次性刷写到硬盘中。RocksDB的文件是不可变的,一旦生成就不会继续写该文件。记录不会被更新或者删除,会生成一个新文件。这会在硬盘生成一些多余的数据,会需要数据库Compaction(压缩),Compaction文件会移除冗余的键值对并腾出空间,如图所示

图1

2.3.1 RocksDB架构

RocksDB用不同的排列组织数据,也就是层level,每层都有个目标大小,每层的目标大小增长倍数是相同的,默认是10倍,因此,如果第一层目标大小1g,那么2,3,4层大小就是10g,100g,1000g,一个键可能出现在不同的层,随着compaction,但是越新的值层越高,越旧的值层越低

RocksDBinitially首先将新的写入存储在内存memtable中,当memtable写满了,他会被转成不可写的memtable,并插入到落盘流程(flush pipeline)中,同时,分配一个新的memtable给后面的写,第0层就是memtable,当第0层满了,数据就会被compact,挪到下面的层中,compaction流程应用到所有的层,将文件从层n合并到层n+1

2.3.2 放大因子

我们通过检测各种工作负载实验下的吞吐量和持续时间来测评优化带来的影响。此外,我们基于以下RocksDB定义的放大因子定义来监测副作用

  • 读放大是从硬盘中(包括数据compaction中的读)读的数据 比 从KV存储中的读的数据
  • 写放大是总的硬盘中的写入数据 比写入KV存储中的数据
  • 空间放大是硬盘上总的数据文件比 KV存储的大小

2.3.3 memtier benchmark

Memtier benchmark[11]是一个benchmark工具,我们用它测量Redis流量。这是Redis实验室[12]开发并开源的, 它可以发送各种比例的读写,实现了不通模式的流量模式,比如连续,随机,高斯分布等。这个工具维护了一个一个Redis命令的pipeline,保证当回复响应时发送一个新的请求。这个命令行工具也提供堆请求流的各种程度的控制,比如操作数,读写比率,工作线程数,客户端数量,值大小等等。在每次运行的结尾,他会报告所有读写吞吐量的平均值和他们的延迟。

3. 方法

当我们优化RocksDB,我们首先的动机,以及我们首要的优化目标是减少ROF数据库复制的时间。复制流程在保证主节点高可用是必要的,且包含以下两个步骤,1从主节点服务区读取整个数据集并发送到网络中的其他从节点,2从节点服务器写入数据集。一旦首次复制流程完成,所有的后续的反动都会从主节点发送到从节点来保持和主节点的同步。当(假如)主节点挂掉了,从节点会成为新的主节点,新的从节点进行复制,这样整个数据库保持容错。因此,复制事件是很重要的,因为1复制流程期间由于主节点开始忙碌的读和发送数据,整个集群在一个低性能的状况中,且2这会有数据丢失的风险,因为当前主节点挂了,无法进行数据复制。

使用默认的RocksDB设置,复制50g数据,内存:固态比10:90的情况下会使用whoping特别长的212分钟。对我们的场景来说这是不可容忍的时长,应该降低到30分钟以内,在第四段中,我们将描述我们在配置中所处的更改,降低复制时间到18分钟。因为真正的生产环境通常单个Redis服务器有50g左右的数据,所以我们用50g数据来实验。

当我们首要的目的是减小数据库复制的时间,保证我们的系统稳定性能是十分必要的imperative,因此,每次每次优化,我们都会测量四种工作负载场景

  1. 只写,写50m key,1k value,总共50g,这个压力代表所当前常用的数据库规模

  2. 只读,读10%的数据集

  3. benchmark,混合读写,50-50

  4. 数据库复制,从主节点读50g写入从节点

使用这些工作模式,当我们的优化效果能显著提高第四个并且没有降低其他三个,我们接受这个结果。

优化流程第一步是确定瓶颈。因为数据库复制主要是主读从写,我们只分析这两种操作。我们跑两个实验,1主节点书全部存在内存,硬盘写只发生在从节点(这个实验叫纯内存主节点),2所有从节点数据都存在内存,硬盘压力都在主节点的读上(这个实验教纯内存从节点),比较两种场景的持续时间。帮助我们更好的优化,来达到减少复制时间的目的

我们也分析服务器的活动:我们统计每次调优过程中的运行时间,吞吐量和延迟。我们也检测各项系统指标,Redis和RocksDB的线程负载,IO状态,RocksDB的每层的状态,放大因子,放慢速度和写停止the slowdowns, and the write stalls.这些指标帮助我们去测量评估每次优化后的副作用,选择不使用opt-out那些降低性能的调优参数(见第四段)

硬件:我们所有的实验都泡在EC2和GCE云上,EC2是广泛使用的云服务器,我们使用i2.8x Large 32vCPU 244GB内存,8x800GB SSD,10g加强型网络带宽,此外我们使用的GCE用的是EC2上用不到的NVMe硬盘,同样采用32vCPU,Intel Xeon 2.20GHz, 208g RAM,8x375gb NVMe。

4. 测试和结果

这小节描述我们的测试过程和我们在优化期间的发现。首先 4.1小节我们通过实验详细的介绍了显著提高性能的两个参数和一个本以为能提升性能却带来负面影响的参数,这些实验是在EC2伤实验的,在4.3我们重复该实验在用NVMe的GCE上

在Throughout这节中,粗体字是RocksDB调优实验, 引用是RocksDB配置项的名字(knob name),它们的影响列在下表中

参数parameter 名字 初始值 改过的新值 性能影响
Compaction threads max_background_compactions 8 64 24%
Slowdowns level0_slowdown_writes_trigger 12 24 10%
Stops level0_stop_write_trigger 20 40 10%
Compaction readahead compaction_readhead_size 0 2MB 300%
Redis IO threads Redis IO threads 8 64 500%
RAID chunks chunk 512k 16k 68%
filter for hits optimize_filters_for_hits 0 1 7%
bulk mode prepare_for_bulk_mode 0 1 500%
Block size block_size 4k 16k 60%
Synchronization bytes_per_sec 0MB 2MB 0%

4.1 在EC2 上使用SATA接口SSD

我们的RocksDB基线配置用的RocksDB4.9和几个和默认参数不同,列在下表中

Parmeter value
memtable budget 128MB
Level 0 slowdown 12
max open files -1
WAL disabled
OS buffer disabled
Cache disabled

这些在开始阶段就设定了,是有一定的原因的,因为Redis on Flash用内存来缓存,所以我们禁用了RocksDB的cache,没有必要同一个值缓存两次。我们也禁用了OS缓冲(buffer)来保证写在内存缓冲或者直接写入硬盘。另外一个有效的改动是禁用WAL通过(rocksdb_writeoptions_disable_WAL),在我们这个场景下WAL有消耗但是用不到,Redis on Flash本身就保证了一致性,无需使用WAL。

4.1.1 最大化并行来消除瓶颈

开始优化,我们先降低每个线程上的负载来降低高CPU利用率high CPU utilization

我们让RocksDB后台县城和Redis I/O线程一样多,增加他们的并行性来防止他们成为瓶颈

RocksDB后台线程主要是两个函数,compaction 工作和刷到硬盘(flush job),在图片2可以看到, 使用max_background_compaction从把compaction 线程8改到64,性能提高24%,我们观察到compaction越并行化,compaction周期就越短,写就有更高的吞吐量。我们也针对flush线程(拷贝数据从memtable写到硬盘)做了测试,他们的并行化并没有显著的提高性能。

图2

4.1.2 稳定吞吐量和延迟

下一步,我们尝试稳定系统的性能,即(namely)在吞吐量和延迟方面降低服务器性能差异variance。在图三,我们能观察到服务器吞吐量上断断续续stop-start的现象,每十秒有着50k ops/sec低于10k ops/sec。这个表现也显示出长尾延迟,即少量请求造成长时间的响应

图3

正如预期,是compaction周期造成这种流量。这有由于level 0 文件到达上限产生的。比如说level0_slowdown_writes_trigger(默认12),当这个上限很低,compaction就会很频繁,就会扰乱流量,就像在图三中看到的那样。在另一方面,如果这个上限过高,我们积累的需要compaction的量(debt)就比较多,最终会造成RocksDB触发level0_stop_write_trigger 阻塞所有流量数秒。对于ROF,她最好有较慢但稳定的吞吐量载整个期间避免stop-start和写阻塞,一个例子,在图4中显示了一个稳定的吞吐量几乎没有出现1-2k ops/sec的情况

图4

我们实验了不同的参数来调优slowdownstop,我们观察到了很高的吞吐量,当我们继续提高这个参数的时候,又造成了负优化导致积累了太多的compaction debt,结果在图5中显示,最优的参数是slowdown=24stop=40,给我们带来了10%的性能提升。Compaction参数后面的值(slowdown=40,stop=60)表现出更快的复制但是在只读工作集种对于吞吐量带来了负面影响。在延迟的compaction的场景下,访问时间会变长,因为bloom filter页更大,数据也没压缩。

图5

4.1.3 产生更大的吞吐量

compaction预读选项compaction_readahead_size)起用能在compaction过程中 读取大的compaction inputs。默认RocksDB不使用预读(0MB),我们使用2MB readahead进行实验获得了缩短三倍复制时间的效果。前文提到,compaction效率受到RocksDB吞吐量的影响。此外,我们通过调整RedisIO线程数获得了更好的输出,因为RocksDB的API是同步的,我们使用多路IO线程模型来提高IO并行化,我们发现提高RedisIO页提高了读请求的延迟,但是这个提高也带来了memtable的争用,导致显著降低了写请求的性能。

因此我们决定用单线程IO来处理写,用其余的IO线程来处理并发读(一写多读 , one writer),这个提升提高了五倍。

因为RoF会部署在多个存储器组合的设备来增加存储带宽,我们也在各种RAID设置的环境中做了实验。RoF使用(employ)RAID-0来压缩卷,我们在不同的压缩块大小的环境中测试比较了性能。见图6,RAID 越小的块chunk越能提高硬盘的并行程度以及性能,因此,我们将默认的512kb改成16kb,从而提高了68%。注意RocksDB调优指南建议使用大的块大小(chunk size)[14],我们的实验结果正好相反,越小的块性能越好

图6

最后,因为RoF 把所有key和他们的位置放在了内存中,向RocksDB的请求不会错过(miss),请求不可用的key会被ROF处理,只有请求已知在硬盘上的数据才会转发到RocksDB子系统,因此,我们开启了RocksDB特定designatedbloom filter配置项来优化这个场景, optimize_filters_for_hits。RocksDB实现的布隆过滤器对于每个SST文件会快速检查搜索的key是否在该文件中存有一份,这个优化去掉了对最底层的过滤,因为一旦请求来到这层,那值肯定就在这层。这个优化会对复制性能带来7%的提升。

4.1.4 读速度

当复制一个数据库,主节点发送数据集的一个副本给从节点,主节点需要把硬盘和内存中的值都读出来。这有两种方法来从硬盘中读数据,第一种就是使用RocksDB的迭代器来按照顺序读RocksDB数据库文件,第二种方法就是读那些值不在内存中的,这降低了总的读但是增加了随机读。我们主要根据下面两个条件来对这两种方法进行取舍

  1. RocksDB在两种场景下的性能
  2. 顺序读和随机读下的硬盘速度

图7 画出了在复制过程中剩余的keys在内存中还有多少来比较这两种方法。x轴越长表示数据在闪存硬盘中(不在内存中)的比例越高。在顺序读RocksDB数据集的场景下,所有实验场景下,复制时间很稳定(大致保持不变stays roughly constant)(蓝线),相反,随机读只向硬盘访问必要的数据的方法下,复制时间随着闪存硬盘中数据的比例而线性上升。图中可以看到,随机读仅在内存中有85%数据以上的场景下表现较好,顺序读载配置了预读后有效果提升,读取大的块(chunk)能加速顺序读,上述结果在不同的数据集合对象大小表现是一致的。我们采用顺序读作为复制方案。

图7

4.2 负优化结果

许多参数我们试验后决定不采用,不仅是因为他们没有减少肤质是件,反而还有副作用,在这个小结,我们列举三个调优过程中反直觉counter-intuitive或造成意外副作用的几个场景

我们开始在(批量写?)bulk load模式上开始调优(prepareforbulkload),开启这个模式提高了复制时间五倍,然而,bulk load 模式有副作用,会使读变得特别慢(prohibitively slow),因为这个模式禁止了压缩,如果数据集不压缩,就会有非常多的SST文件,需要非常多的读取。我们在复制结束试着关掉bulk load启动手动compaction来恢复数据库可用,但是手动compaction是一个单线程处理,会花费很长时间来完成,比复制时间还要长,我们采用自动compaction,多线程,但是后续subsequent的读仍然很慢。

意外的是,这两种compaction最终结果不一样,第一种花费较长时间,第二种产生较低的读取,此外也没有权利compaction。还记得4.1.2我们遇到的类似的compaction debt问题吗。Bulk mode(13)是有许多不同的调优参数组成的is composed of,我们分别测试各种参数来试试能不能带来好处。然而,禁用compaction被证明是性能提升背后的主要原因,我们就放弃了这个配置选项。

全同步在RoF不是必须,因为我们使用闪存硬盘作为内存扩展,因此,我们针对全同步消耗来进行调优,我们配置了2MB同步一次,bytes_per_sync,意外的是,我们没观察到任何提升,我们用dd写数据到硬盘,我们比较了写入50g数据不开启同步的时间,发现性能提升了2倍,在复制实验中没有类似表现,我们有点困惑。我们猜测写被缓存了(cached)因此我们看不到同步的优化(saving),但是查看meminfo我们观察到数据没被缓存 ,我们也看了其他设备RAID和他们的iops,也没有降低。最后我们用strace确定确实没有同步动作。这个反直觉的现象仍然是个谜。

最后,我们调了block_size参数,每个SST文件包含索引和他自己的block,增加块(block)大小可以减少每个文件中索引的数量。但是块大了读取会变多(见2.3.2读放大),增加了block大小也减少了内存使用,因为内存中的索引更大了(?),默认的块大小是4kb,当我们block调到16kb,我们观察到了60%的降低,不像其他参数可以在运行时动态调节,block大小修改不能撤销(编译进去的)所以我们观测到了在读性能上的负面影响后我们就结束了这个调优。

4.3 在GCE NVMe SSD上的实验

在EC2上的调优经验也帮助我们在GCE上进行试验,首先我们调整RAID优化,我们把之前的在chunk 大小上的经验直接搬过来(a sweep of),证实了16kb确实是最优参数,能将复制时间提高41%。

默认的level0_slowdown(12)stop(20)延迟写是不必要的。当我们达到12个文件后,延迟写在吞吐量上面的表现非常明显,从60k降到600 ops/sec 直到compaction工作结束才恢复。为了避免系统的这种stop-start现象,我们增加了这两个参数到20 24,观察到性能提升了15%(类似EC2)

此外,我们加上了其他调优,比如优化filter等等,我们也加了RocksDB compaction线程和Redis IO线程,配置了一个写者 (one writer),增加了数据预读。这些改动累积效果,复制时间缩短到12分50秒,相同的数据规模在EC2上同样参数配置用了18分钟。不过硬件参数也不同,SATA-SSD和NVMe SSD,我们认为GCE本应该有更快的复制时间

5. 相关工作

虽然也有其他可用的存储引擎[15,16,17],RocksDB有着更好的性能而得到了广泛的使用,RocksDB调优指南[18]提供了RocksDB[10]基本的配置指引,也是我们测试中的基本配置。在我们的这篇论文中,更进一步的调优带来了可观considerably的性能提升(我们的例子中是11倍)

krishnamoorthy和Choi[19]做了在NVMe SSD上调优RocksDB的工作,据我们所知,这是最接近我们论文展示的工作,然而,他们的分析是在RocksDB压力测试场景下的,我们的优化是在工业实践(用RocksDB做后端引擎)场景下的。所以,两者的调优过程和结论也有所不同。

类似Redis on Flash,也有一些Redis的拓展使用了闪存硬盘为Redis提供拓展能力,在Intel的项目[20],也是Redis分支,依赖SSD来提供持久化,动机是消除eliminate硬盘备份写硬盘的同步代价而采用了AOF策略,如果有个节点故障,机器会通过固态硬盘上的数据来修复。这个方法不能用在云设备上,因为机器不能复用/修复。Redis原生硬盘存储(Naive-Disk-Store)使用LMDB[17]来管理硬盘上的数据,数据是周期性的刷到硬盘上的,如果一旦故障仅会丢失最后一次没刷盘的数据。它不会把key存到内存中也不支持持久化复制,定期删除,scan。

6. 结论

随着使用RocksDB作为存储引擎的应用越来越多,Rocksdb也是选型存储引擎的首选,然而,灵活和高性能是有代价的,调优RocksDB是个复杂的工作,在我们这篇论文中,调优好的配置要比基本配置表现好出一个数量级

在这篇论文中我们使用Redis on Flash 一个热门Redis KV存储的一个商业拓展,作为调优RocksDB的研究用例,我们描述条有方法,调优过程,和RocksDB设置上的改动点,使得RoF性能提升11倍以上,实验还展示了可能会导致负面效果或预料之外副作用的的配置。然而不同的使用场景有不同的最优配置,我们希望我们的经验会帮助其他人来优化RocksDB,提高新更能,减少研发时间。高度调优的Rocksdb的性能提升绝对值得付出这些工作。

7. 援引

[1] Siying Dong. RocksDB: Key-Value Store Optimized for Flash-Based SSD.https://www.youtube.com/watch?v=xbR0epinnqo. [2] RocksDB and LevelDB. http://rocksdb.org/blog/2016/01/29/compaction_pri.html. [3] Paul Dix. Benchmarking LevelDB vs RocksDB. https: //www.influxdata.com/benchmarking-leveldb-vs-rocksdbvs-hyperleveldb-vs-lmdb-performance-for-influxdb/. [4] RocksDB users. https: //github.com/facebook/rocksdb/blob/master/USERS.md. [5] Mark Callaghan blog. http://smalldatum.blogspot.co.il/2014/07/benchmarking-leveldb-family.html, July 7, 2014. [6] Redis on Flash documentation. https://redislabs.com/redis-enterprise-documentation/rlecflash. [7] Redis on Flash. https://redislabs.com/rlec-flash. [8] Redis. http://redis.io/. [9] Redis Users. http://techstacks.io/tech/redis. [10] RocksDB website. http://rocksdb.org/. [11] Memtier benchmark. https://redislabs.com/blog/memtier_benchmark-a-highthroughput-benchmarking-tool-for-redis-memcached. [12] Redis Labs. https://redislabs.com/. [13] Gartner. Magic Quadrant. https://www.gartner.com/doc/reprints?id=1-2G2O5FC&ct=150519. [14] RocksDB FAQ.https://github.com/facebook/rocksdb/wiki/RocksDB-FAQ. [15] Kyoto Cabinet. http://fallabs.com/kyotocabinet/. [16] LevelDB. http://leveldb.org/. [17] Lightning Memory Mapped Database.https://lmdb.readthedocs.io. [18] Facebook. RocksDB Tuning Guide. https://github.com/facebook/rocksdb/wiki/RocksDB-Tuning-Guide. [19] Praveen Krishnamoorthy and Choi Changho. Fine-tuning RocksDB for NVMe SSD. https://www.percona.com/live/data-performance-conference2016/sites/default/files/slides/Percona_RocksDB_v1.3.pdf. [20] Intel. Redis with persistent memory. https://github.com/pmem/redis#redis-with-persistent-memory. [21] Redis Naive Disk Storage.https://github.com/mpalmer/redis/blob/nds-2.6.

Read More

深入理解计算机系统 读书笔记


csapp 不多说了。早就该读完了


计算机系统漫游

  • 信息就是位 + 上下文

  • 程序被程序翻译成不同的格式

  • 了解编译系统

    • 优化系统性能
    • 看懂链接报错
    • 避免安全漏洞
  • 处理器读并解释存储在存储器中的指令

    • 典型的系统硬件组织

    image-20200328101740941

    总线bus 传word

    IO

    内存 DRAM

    cache SRAM

程序结构和执行

信息的表示和处理

信息存储

  • 寻址和字节顺序

    • 大端小端
    01 05 64 94 04 08 add %eax, 0x8049464
    
    • 整数和浮点的不同表达

      image-20200328113653117

    • 布尔袋鼠 ,布尔环,位运算

    • 无符号与二进制补码 -128 127

      • 无论哪种解码方式, 都是字节码的解释而已
    • 扩展一个数字的位表示

      • 零扩展
      • 符号扩展
  • 浮点

程序的机器级表示 基于IA32

  • 反汇编

  • 数据格式

    • 字word 16位 double word 32位

    • 三种GAS后缀,movb 字节 movwmovl双字

    • 操作指示符 operand

      • 立即数 immediate $0x1F
      • 寄存器 eax
      • 存储器引用 地址
    • 数据传送指令

      • mov
      movl $0x4050, %eax 立即数 ->寄存器
      movl %ebp, %esp 寄存器->寄存器
      movl (%edi,%ecx), %eax 内存->寄存器
      movl $-18, (%esp) 立即数->内存
      movl %eax, -12(%ebp) 寄存器->内存
      • push %ebp
      subl $4, %esp
      movl %ebp,(%esp)
      
      • pop
      movl (%esp), %eax
      addl $4, %esp
      
  • 算术和逻辑操作

    • 加载有效地址 lea aka load effective address 我还以为是leave

    • xor %eax ,%eax 赋0

      指令 效果 描述
      leal S, D D = &S 加载有效地址
      incl/decl/negl/notl D D=D@1 加一减一
      add/sub/imul/xor/or/and S,D    
  • 控制

    • 条件码
      • cmp test set
    • 跳转
      • jmp je jg jl ja jb
      • 跳转表
  • 过程

    • 栈帧

    • 转移控制

      • call leave ret

        ;leave 
        movl %ebp, %esp
        popl %ebp
        
    • 寄存器使用惯例

      • %ebp %esp 必须保存
      • %eax %edx %ecx调用者保存寄存器caller save %ebx %esi %edi 被调用者保存寄存器 callee save
    • 递归

  • 数组?

    • movl (%edx, %eax, 4), %eax

    • 指针运算 假设数组E的起始地址和索引i分别在%edx%ecx

      表达式 类型 汇编
      E int * xe movl %edx, %eax
      E[0] int M[xe] movl (%edx), %eax
      E[i] int M[xe + 4i] movl (%edx, %ecx, 4), %eax
      &E[2] int * xe+8 leal 8(%edx), %eax
      *(&E[i] + i) int M[xe+ 4i + 4i] movl (%edx, %ecx), %eax
      &E[i]-E int i movl %ecx, %eax
    • 数组循环

      • 减少imul
      • 优化递增变量
  • 异类数据结构

    • 编译时算好偏移
  • 理解指针

    • 越界
  • 浮点算术

    • 浮点寄存器
    • 栈表达式求值用到的寄存器
  • c中嵌入汇编

处理器体系结构

  • 定义一个简单的指令集,就叫它Y86吧

    • %eax, %ecx $edx %esi $edi %esp %ebp
    • 条件码 ZF SF OF
    • 程序计数器PC
    • 存储器,内存,数组代替
    • 汇编设计
      • mov按照mov的类别设计 irmovl(立即数 →寄存器) rrmovl(寄存器→寄存器) rmmovl(寄存器→内存) mrmovl (内存→寄存器)
      • opl addd sub and xor
      • jxx
      • call ,ret
      • pushl, popl
  • 逻辑电路

    • 布尔逻辑
    • 存储器和时钟控制
    • 集合关系
  • Y86的顺序实现

    • 取指,fetch 从PC中拿到地址,从地址抽出指令指示符 icode ifun

    • 解码decode 从寄存器里读

    • 执行 execute 算术逻辑单元(ALU)执行指令指明的动作(ifun)或者移动栈指针或者跳转

    • 访问内存memory

    • 写回寄存器

    • 更新PC

      阶段 opl rA, rB subl %edx, %edx
      取指 icode:ifun <- M[PC]
      rA:rB <- M[PC+1]
      valP <- PC+2
      icode:ifuicode:ifun <- M[0x00c] = 6:1
      rA:rB <- M[0x00d] = 2:3
      valP <- 0x00c+2 = 0x00e
      解码 valA <- R[rA]
      valB <- R[rB]
      valA <- R[%edx] = 9
      valB <- R[%ebx] = 21
      执行 valE <- valB op valA
      Set CC
      valE <- 21 - 9 = 12
      ZF <- 0, SF <- 0 , OF <- 0
      访存    
      写回 R[rB] <- valE R[rB] <- valE = 12
      更新PC PC <- valP PC <- valP=0x00e

    其他流程类似,不过是获取值通过ALU实现

    image-20200402171439048

image-20200402171808118

一种实现image-20200402172202210

image-20200402172236476

  • 时序

    image-20200402172644242

    后面在设计电路了。理解PC

  • 流水线的通用原理

    • 时钟驱动组合逻辑
    • 流水线过深,收益反而下降
    • 指令控制
    • 指令重排
    • 预测PC
      • 分支预测
    • hazard
      • data hazard
        • 读写阶段不同造成寄存器错误
        • 条件码
        • PC
        • 存储器写回
      • control hazard
      • 解决方案
        • stall nop nop 吞吐降低
        • 转发

第四章真是太他妈复杂了。讲cpu 指令集 流水线怎么设计

优化程序性能

  • 优化编译器的能力和局限

    *xp += *yp; *xp+=&yp;*xp += 2* *yp 在xp=yp时优化效果不同,因为编译器必须假定指针使用不同的寄存器memory aliasing

    • 表示程序性能,CPE
  • 消除循环

    • 循环不变量优化,经典问题了。
    • 减少调用
    • 消除不必要的寄存器引用
      • 循环中访问指针可能不会被优化,导致多次访问指针。
    • 降低循环开销
  • 转换到指针代码

    • 通过指针改善数组效率?
  • 提高并行度

    • 循环展开
      • 这种优化的性能提升有上限。为什么?
  • 分支预测和预判错误处罚

  • 理解内存性能

    • save & load
    • 设计。数据结构和算法影响性能
    • 消除连续函数调用(浪费堆栈)以及计算放到循环外,不必要的寄存器引用以及引入临时变量保存中间结果
    • 循环展开,迭代分割,数组换指针
  • 确认和消除性能瓶颈

    • profiling -pg

内存层次结构

  • 存储技术

    • DRAM 实现内存
    • SRAM 实现cache 弥补cpu内存之间的的差距,带来局部性
  • 局部性

    • 利用局部性
  • 内存层次结构

    • cache 读成cash,卧槽,我一直读错了??
      • cache的命中,缓存策略,置换策略LRU等等。这里的概念讲的很少。有专门的书讲这个
  • cache

    • cache set, cache line
  • direct-mapped cache
    • 字选择,
    • 行替换,击中直接替换当前行
      • 这里细节比较复杂,还好有图说明。可以找出pdf来看。这里不解释了。看不懂。后面还有书来讲这个,到时候仔细看
  • set associative cache组相联
    • 行替换,LRU/LFU
  • fully associative cache
    • 做TLB
    • 怎么写
      • 写回write back 需要标志位dirty bit记录cache block是否被修改过
      • 直写 write through
    • 写不命中
      • 写分配or写不分配
    • L1 icache dcache L2 university cache
    • 性能指标
      • 命中率/不命中率
      • 命中时间
      • 不命中惩罚 miss penalty
      • 影响因素
        • cache越大越容易命中,但是增加命中时间
        • 块儿越大命中率高一些,提高空间局部性,但是占用大,行数少,降低时间局部性
        • 相连度,高相联度带来慢的速度,增加命中时间,但是高相联度冲突不命中影响低
          • L1直接映射,L2组项联,L3直接映射
        • 写策略,直写快但是浪费,写回带宽高但是复杂
          • 下层cache多用写回
    • cache-friendly
    • cache 对性能的影响

在系统上运行程序

链接

链接基本上都看过了。简单抽取新点子。不详细记录了

  • 静态链接

    • 符号解析
    • 重定位
  • 符号表

    • bss 记成better save space
    • name mangling,直译确实很糟糕。毁坏。。这个毁坏不是destroy,是把布压碎的那种毁坏。意译好一点或者别翻译了
    • 全局符号处理,强弱符号。符号覆盖(hook)
    • 符号依赖,注意库在后面。放在前面就被忽略

异常控制流

  • 异常处理

    • 异常表

      类别 原因 异步? 返回行为
      中断 IO设备的信号 异步 总是返回到下一条指令
      陷阱 syscall 有意的异常 同步 总是返回到下一条指令
      故障 缺页错误 潜在可恢复的错误 同步 可能返回到当前指令
      终止 硬件错误 不可恢复的错误 同步 不会返回
      异常号 描述 异常类型
      0 除法错误 故障
      13 一般保护故障 故障
      14 缺页 故障
      18 机器检查 终止
      32~127 操作系统定义的异常 中断或陷阱
      128 系统调用 陷阱
      129~255 操作系统定义的异常 中断或陷阱
  • 进程,多任务,抢占。时间片
    • 用户模式和内核博士
      • /proc 访问内核数据 /proc/pid/maps老朋友 了
    • 上下文切换
      • 中断污染cache
  • 系统调用和错误处理,错误会返回-1且标记errno,记得处理
  • 进程控制
    • 创建/终止,获取pid等等
      • fork细节
        • 创建一次返回两次
        • 并发执行
        • 相同但是独立的地址空间,基于COW
        • 文件共享,注意关闭
    • 回收子进程
    • 加载运行程序
  • 信号
    • 信号处理问题
      • 信号被阻塞,直接返回
      • 待处理的信号不会排队等待
      • 系统调用可以被中断
        • 有的系统被中断不重新运行
      • sigaction和setjmp处理信号。
号码 名字 默认行为 相应事件
1 SIGHUP 终止 终端挂起
2 SIGINT 终止 键盘中断
3 SIGQUIT 终止 键盘退出
4 SIGILL 终止 非法之灵
5 SIGTRAP 终止并dumping core 跟踪陷阱
6 SIGABRT 终止并dumping core abort函数信号
7 SIGBUS 终止 bus err
8 SIGPFE 终止并dumping core 浮点异常
9 SIGKILL 终止 杀死程序
10 SIGUSR1 终止 用户自定义
11 SIGSEGV 终止并dumping core 段错误
12 SIGUSR2 终止 用户自定义
13 SIGPIPE 终止 向一个没有读的pipe里写,broken pipe
14 SIGALRM 终止 alarm函数信号
15 SIGTERM 终止 软件中指
16 SIGSTKFLT 终止 栈故障
17 SIGCHLD 忽略 子进程暂停or终止
18 SIGCONT 忽略 继续进程如果被暂停或终止
19 SIGSTOP 停止,直到下一个SIGCONT 不来自终端的暂停
20 SIGTSTP 停止,直到下一个SIGCONT 来自终端的暂停
21 SIGTTIN 停止,直到下一个SIGCONT 后台从终端读
22 SIGTTOU 停止,直到下一个SIGCONT 后台向终端写
23 SIGURG 忽略 socket紧急
24 SIGXCPU 终止 CPU时间限制超出
25 SIGXFSZS 终止 文件大小限制超出
26 SIGVTALRM 终止 虚拟定时器期满
27 SIGPROF 终止 剖析定时器期满
28 SIGWINCH 忽略 窗口大小变化
29 SIGIO 终止 某个描述符上可执行IO操作
30 SIGPWR 终止 电源故障
  • 操作进程工具
    • strace 分析调用
    • ps看进程 top看资源
    • /proc

测量程序执行时间

主要介绍了采集数据的方法 counter,多次测量,基于gettimeofday精确时间等。非常粗略

虚拟内存

  • 虚拟寻址 MMU做转换

  • VM 虚拟地址数组 映射VP

    • VP三个状态,未分配,缓存,未缓存
  • DRAM比SRAM慢十倍,相比缓存不命中要昂贵很多

    • 全相联,任何虚拟页VP可以放在任何物理页PP中
    • 替换策略很重要,替换错虚拟页代价(处罚)非常高
    • 总是写回
  • 页表

    • 在上面的场景下,VP都在分配磁盘,可能缓存在内存中,(在内存中也可能有对应的物理页也有可能没有),管理各种VP,需要页表

      • 子概念,页表项。注意下图的包含关系 image-20200409144609754
    • 页命中,上图中击中DRAM页,就会分配物理页,也就是命中

    • 不命中,就是缺页,page fault,假如上图中击中vp3,就会牺牲一个vp,上面场景,没被访问的vp4倍淘汰,更新vp3

    • 局部性,良好工作机不会产生磁盘流量,以及内存颠簸thrashing,频繁换入换出

  • VM 管理功能

    • 多进程不同页表,物理页可共享
    • 简化链接,简化共享代码段,简化内存分配,简化加载
  • VM作为内存保护工具

    • 页表,页表项标志位控制,以及段错误保护
  • 地址翻译

    image-20200409152413978

    • 结合cache和VM内容较细,不讨论
    • 利用TLB(translation lookaside buffer,不是透明大页)加速地址翻译,缓存一波vpe,代替走l1 cache

      image-20200409152832264image-20200409152844756

  • 多级页表

  • 内存映射

  • 动态内存分配,以及碎片

    • 分配器设计
    • 伙伴系统
  • 垃圾收集

    • 标记清除法
      • 有向可达图
  • 常见内存错误

    • 写错误地址,读未初始化内存,栈溢出,指针引用错误,误解指针运算,引用不存在的对象, 内存泄漏等等

程序间的交互与通信

这部分内容和一些书重复。简单列举一些没注意过的/重要的点

系统级IO

  • 一切都是文件
  • size_t ssize_t系统函数多用ssize_t, 错误处理返回值是-1
  • read write的正确用法,这段代码很常见了
ssize_t readn(inf fd, void *usrbuf, size_t n){
    size_t nleft = n;
    ssize_t nread;
    char *bufp = usrbuf;
    while (nleft > 0) {
        if ((nread = read(fd, bufp, nleft)) < 0) {
            if (errno = EINTR)
                nread = 0;
            else
                return -1;
        } else if(nread == 0)
            break;
        nleft -= nread;
        bufp += nread;
    }
    return (n - nleft);
}

ssize_t writen(inf fd, void *usrbuf, size_t n){
    size_t nleft = n;
    ssize_t nwritten;
    char *bufp = usrbuf;
    while (nleft > 0) {
        if ((nwritten = write(fd, bufp, nleft)) < 0) {
            if (errno = EINTR)
                nwritten = 0;
            else
                return -1;
        } else if(nread == 0)
            break;
        nleft -= nwritten;
        bufp += nwritten;
    }
    return n;
}
  • 读取元数据
  • 共享文件
    • 描述符表 文件表 vnode表
    • 子进程集成fd,注意关闭
  • IO重定向 dup2

网络编程

  • cs模型
  • 网络api
  • socket api
    • socket拿到fd
    • connect阻塞 对应tcp哪个阶段?
    • bind server端绑定
    • listen,server使用,等着
    • accept,server端真正的接受了client。accept返回前在tcp哪个阶段?
  • web服务器以及http事务
    • 状态码
    • get post 等api

并发编程

  • 基于进程的并发模型
    • 进程间IPC
    • 处理子进程
      • 处理SIGCHILD,在handler里waitpid
      • fork之前的fd需要关闭两遍
  • 基于IO多路复用的并发模型
    • select
      • while select, 也不高效
      • 事件驱动,写好handler,缺点是比较分散比较复杂
  • 基于线程的并发模型
    • 创建线程,detach干活。浪费
    • 共享变量?需要线程之间同步,信号量,锁等等
    • 基于线程的IO多路复用
  • 并发性问题
    • 线程安全
      • 共享变量保护
      • 不可重入函数
      • 静态变量指针?
    • 竞争
    • 死锁

ref


Read More

julia笔记

由于julia和python/ruby/perl非常像,没什么可以整理的,所以这里只做记录备忘

环境搭建

安装julia 进入repl 按]进入安装包模式

add IJulia
Read More

go快速入门

我本身有啥语言都会点,所以这门语言我会用其他语言的特性来描述,请谨慎阅读

基本抄自https://gfw.go101.org/ 值得一看

golang槽点太多。写出坑来都毫无感觉

[toc]

研究一个语言要关注哪些地方?

  • 使用(生态,命令行,包管理/代理等)
  • 语言特性(核心卖点,人无我有的)
  • 类型系统( 值类型还是引用类型?)
  • 优化点 profile 内存分配器 延迟相关/GC等等

相关命令行

下载包

go get github.com/onsi/gomega

如果离线安装,得克隆到goroot目录里面

更新mod

go mod tidy

语言功能


  • channel以及select-case

select-case分支流程控制代码块

Go中有一个专门为通道设计的 select-case分支流程控制语法。 此语法和 switch-case分支流程控制语法很相似。 比如,select-case流程控制代码块中也可以有若干 case分支和最多一个 default分支。 但是,这两种流程控制也有很多不同点。在一个 select-case流程控制中,

  • select关键字和 {之间不允许存在任何表达式和语句。
  • fallthrough语句不能被使用.
  • 每个 case关键字后必须跟随一个通道接收数据操作或者一个通道发送数据操作。 通道接收数据操作可以做为源值出现在一条简单赋值语句中。 以后,一个 case关键字后跟随的通道操作将被称为一个 case操作。
  • 所有的非阻塞 case操作中将有一个被随机选择执行(而不是按照从上到下的顺序),然后执行此操作对应的 case分支代码块。
  • 在所有的 case操作均为阻塞的情况下,如果 default分支存在,则 default分支代码块将得到执行; 否则,当前协程将被推入所有阻塞操作中相关的通道的发送数据协程队列或者接收数据协程队列中,并进入阻塞状态。

按照上述规则,一个不含任何分支的 select-case代码块 select{}将使当前协程处于永久阻塞状态。

select-case流程控制的实现机理

select-case流程控制是Go中的一个重要和独特的特性。 下面列出了官方标准运行时中 select-case流程控制的实现步骤

  1. 将所有 case操作中涉及到的通道表达式和发送值表达式按照从上到下,从左到右的顺序一一估值。 在赋值语句中做为源值的数据接收操作对应的目标值在此时刻不需要被估值。
  2. 将所有分支随机排序。default分支总是排在最后。 所有 case操作中相关的通道可能会有重复的。
  3. 为了防止在下一步中造成(和其它协程互相)死锁,对所有 case操作中相关的通道进行排序。 排序依据并不重要,官方Go标准编译器使用通道的地址顺序进行排序。 排序结果中前 N个通道不存在重复的情况。 N为所有 case操作中涉及到的不重复的通道的数量。 下面,通道锁顺序***是针对此排序结果中的前 N个通道来说的,通道锁逆序***是指此顺序的逆序。
  4. 按照上一步中的生成通道锁顺序获取所有相关的通道的锁。
  5. 按照第2步中生成的分支顺序检查相应分支:

    1. 如果这是一个 case分支并且相应的通道操作是一个向关闭了的通道发送数据操作,则按照通道锁逆序解锁所有的通道并在当前协程中产生一个恐慌。 跳到第12步。
    2. 如果这是一个 case分支并且相应的通道操作是非阻塞的,则按照通道锁逆序解锁所有的通道并执行相应的 case分支代码块。 (此相应的通道操作可能会唤醒另一个处于阻塞状态的协程。) 跳到第12步。
    3. 如果这是 default分支,则按照通道锁逆序解锁所有的通道并执行此 default分支代码块。 跳到第12步。

      (到这里,default分支肯定是不存在的,并且所有的case操作均为阻塞的。)

  6. 将当前协程(和对应 case分支信息)推入到每个 case操作中对应的通道的发送数据协程队列或接收数据协程队列中。 当前协程可能会被多次推入到同一个通道的这两个队列中,因为多个 case操作中对应的通道可能为同一个。
  7. 使当前协程进入阻塞状态并且按照通道锁逆序解锁所有的通道。
  8. …,当前协程处于阻塞状态,等待其它协程通过通道操作唤醒当前协程,…
  9. 当前协程被另一个协程中的一个通道操作唤醒。 此唤醒通道操作可能是一个通道关闭操作,也可能是一个数据发送/接收操作。 如果它是一个数据发送/接收操作,则(当前正被解释的 select-case流程中)肯定有一个相应 case操作与之配合传递数据。 在此配合过程中,当前协程将从相应 case操作相关的通道的接收/发送数据协程队列中弹出。
  10. 按照第3步中的生成的通道锁顺序获取所有相关的通道的锁。
  11. 将当前协程从各个case

    操作中对应的通道的发送数据协程队列或接收数据协程队列中(可能以非弹出的方式)移除。

    1. 如果当前协程是被一个通道关闭操作所唤醒,则跳到第5步。
    2. 如果当前协程是被一个数据发送/接收操作所唤醒,则相应的 case分支已经在第9步中知晓。 按照通道锁逆序解锁所有的通道并执行此 case分支代码块。
  12. 完毕。

从此实现中,我们得知

  • 一个协程可能同时多次处于同一个通道的发送数据协程队列或接收数据协程队列中。
  • 当一个协程被阻塞在一个 select-case流程控制中并在以后被唤醒时,它可能会从多个通道的发送数据协程队列和接收数据协程队列中被移除。

channel简单用法

通知


package main

import (
	"crypto/rand"
	"fmt"
	"os"
	"sort"
)

func main() {
	values := make([]byte, 32 * 1024 * 1024)
	if _, err := rand.Read(values); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	done := make(chan struct{}) // 也可以是缓冲的

	// 排序协程
	go func() {
		sort.Slice(values, func(i, j int) bool {
			return values[i] < values[j]
		})
		done <- struct{}{} // 通知排序已完成
	}()

	// 并发地做一些其它事情...

	<- done // 等待通知
	fmt.Println(values[0], values[len(values)-1])
}
package main

import "log"
import "time"

type T = struct{}

func worker(id int, ready <-chan T, done chan<- T) {
	<-ready // 阻塞在此,等待通知
	log.Print("Worker#", id, "开始工作")
	// 模拟一个工作负载。
	time.Sleep(time.Second * time.Duration(id+1))
	log.Print("Worker#", id, "工作完成")
	done <- T{} // 通知主协程(N-to-1)
}

func main() {
	log.SetFlags(0)

	ready, done := make(chan T), make(chan T)
	go worker(0, ready, done)
	go worker(1, ready, done)
	go worker(2, ready, done)

	// 模拟一个初始化过程
	time.Sleep(time.Second * 3 / 2)
	// 单对多通知
  close(ready)
	// 等待被多对单通知
	<-done; <-done; <-done
}

  • test的执行顺序 TestMain ->Test* 如果有TestMain, 必须有m.Run()否则本模块内部的Test*不执行
  • go的map默认是引用类型,要想复制修改,必须make
  • go的字符数组也是默认引用的
  • go不能直接赋值给PB的字段,必须构造,否则会segfault panic
  • 和nil指针比较是有类型的
assert.Equal(t, (*metadata)(nil), mymetadata, "meta nill check")
  • 没有参数的捕获默认是引用捕获,深坑,要看能否求值出。这里借用群友的一张图

这里有个解决方案https://zhuanlan.zhihu.com/p/351428978

for i := 0; i < 10; i++ {
  go func(x int) {
    fmt.Println(x)
  }(i)
}
  • time比较

我遇到的场景,是time保存到mongo在读回来,就多了location信息,不能直接比较,不相等

        	Error:      	Not equal: 
        	            	expected: time.Time{wall:0xa4fc540, ext:63761777875, loc:(*time.Location)(nil)}
        	            	actual  : time.Time{wall:0xc0338154ca582af8, ext:10214780624, loc:(*time.Location)(0x15b1ee0)}
        	        
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1,5 +1,136 @@
        	            	 (time.Time) {
        	            	- wall: (uint64) 173000000,
        	            	- ext: (int64) 63761777875,
        	            	- loc: (*time.Location)(<nil>)
        	            	+ wall: (uint64) 13849555480266418936,
        	            	+ ext: (int64) 10214780624,
        	            	+ loc: (*time.Location)({
        	            	+  name: (string) (len=5) "Local",
        	            	+  zone: ([]time.zone) (len=3) {
        	            	+   (time.zone) {
        	            	+    name: (string) (len=3) "LMT",
        	            	+    offset: (int) 29143,
        	            	+    isDST: (bool) false
        	            	+   },
//以下省略一百行timezone信息,太傻逼了

查文档,用time.equal,也不好使,最终用unix来比较

assert.Equal(t, doc.CreateTime.Unix(), createtime.Unix(), "time check  error")

忙活一晚上

  • 判定结构体是否为空
package main
import (
  "fmt"
)

type Person struct {
}

func main() {
  var st Person
  if (Person{} == st) {
      fmt.Println("It is an empty structure")
  } else {
    fmt.Println("It is not an empty structure")
  }
}

如果结构体比较复杂不能直接比较,用deepEqual

package main

import (
  "fmt"
  "reflect"
)

type Person struct {
  age int
}

func (x Person) IsStructureEmpty() bool {
  return reflect.DeepEqual(x, Person{})
}

func main() {
  x := Person{}
  if x.IsStructureEmpty() {
    fmt.Println("Structure is empty")
  } else {
    fmt.Println("Structure is not empty")
  }
}

  • go testify对于结构体的比较,可能会触发内存泄漏
非常长的一段堆栈

goroutine 718 [running]: runtime.systemstack_switch() /usr/local/go/src/runtime/asm_amd64.s:330 fp=0xc000a39e08 sp=0xc000a39e00 pc=0x47afe0 runtime.mallocgc(0x5ff45b34d, 0xe1ac60, 0x1, 0x2b) /usr/local/go/src/runtime/malloc.go:1070 +0x7e6 fp=0xc000a39ea8 sp=0xc000a39e08 pc=0x410f06 runtime.makeslice(0xe1ac60, 0x5ff45b34d, 0x5ff45b34d, 0x2) /usr/local/go/src/runtime/slice.go:98 +0x6f fp=0xc000a39ee0 sp=0xc000a39ea8 pc=0x45a18f bytes.makeSlice(0x5ff45b34d, 0x0, 0x0, 0x0) /usr/local/go/src/bytes/buffer.go:229 +0x73 fp=0xc000a39f20 sp=0xc000a39ee0 pc=0x5143f3 bytes.(*Buffer).grow(0xc000726630, 0x2b, 0x0) /usr/local/go/src/bytes/buffer.go:142 +0x15c fp=0xc000a39f70 sp=0xc000a39f20 pc=0x513d3c bytes.(*Buffer).Write(0xc000726630, 0xc6fbba4d20, 0x2b, 0x2b, 0xc6fbba4d20, 0x2b, 0x2b) /usr/local/go/src/bytes/buffer.go:172 +0xe5 fp=0xc000a39fa8 sp=0xc000a39f70 pc=0x514065 github.com/davecgh/go-spew/spew.(*dumpState).indent(0xc000a40ab0) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:67 +0xc2 fp=0xc000a3a010 sp=0xc000a39fa8 pc=0xd5aca2 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4d60, 0xc0001d8640, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:416 +0x3a6 fp=0xc000a3a188 sp=0xc000a3a010 pc=0xd5c2e6 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe93400, 0xc0001d8640, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3a300 sp=0xc000a3a188 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xeb2060, 0xc0001d8640, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3a478 sp=0xc000a3a300 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpSlice(0xc000a40ab0, 0xe087c0, 0xc000198bb8, 0x1b7) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:238 +0x125 fp=0xc000a3a590 sp=0xc000a3a478 pc=0xd5b845 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe087c0, 0xc000198bb8, 0x1b7) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:352 +0x9e6 fp=0xc000a3a708 sp=0xc000a3a590 pc=0xd5c926 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4ee0, 0xc000198bb8, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3a880 sp=0xc000a3a708 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4e20, 0xc000198bb0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3a9f8 sp=0xc000a3a880 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xdf43c0, 0xc000034538, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3ab28 sp=0xc000a3a9f8 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xdf43c0, 0xc000034538, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3aca0 sp=0xc000a3ab28 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xed34e0, 0xc000034500, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3ae18 sp=0xc000a3aca0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpSlice(0xc000a40ab0, 0xe08780, 0xc00015d718, 0x1b7) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:238 +0x125 fp=0xc000a3af30 sp=0xc000a3ae18 pc=0xd5b845 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe08780, 0xc00015d718, 0x1b7) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:352 +0x9e6 fp=0xc000a3b0a8 sp=0xc000a3af30 pc=0xd5c926 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf09da0, 0xc00015d6c0, 0x1f9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3b220 sp=0xc000a3b0a8 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf09a20, 0xc00015d6c0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3b398 sp=0xc000a3b220 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf6b5a0, 0xc0001db790, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3b4c8 sp=0xc000a3b398 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf6b5a0, 0xc0001db790, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3b640 sp=0xc000a3b4c8 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4d60, 0xc0001db780, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3b7b8 sp=0xc000a3b640 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe93400, 0xc0001db780, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3b930 sp=0xc000a3b7b8 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xeb2060, 0xc0001db780, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3baa8 sp=0xc000a3b930 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpSlice(0xc000a40ab0, 0xe087c0, 0xc000198c68, 0x1b7) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:238 +0x125 fp=0xc000a3bbc0 sp=0xc000a3baa8 pc=0xd5b845 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe087c0, 0xc000198c68, 0x1b7) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:352 +0x9e6 fp=0xc000a3bd38 sp=0xc000a3bbc0 pc=0xd5c926 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4ee0, 0xc000198c68, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3beb0 sp=0xc000a3bd38 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4e20, 0xc000198c60, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3c028 sp=0xc000a3beb0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xdf43c0, 0xc000034578, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3c158 sp=0xc000a3c028 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xdf43c0, 0xc000034578, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3c2d0 sp=0xc000a3c158 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xed34e0, 0xc000034540, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3c448 sp=0xc000a3c2d0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf3f340, 0xc000034540, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3c578 sp=0xc000a3c448 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf3f340, 0xc000034540, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3c6f0 sp=0xc000a3c578 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4d60, 0xc0001db740, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3c868 sp=0xc000a3c6f0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe93400, 0xc0001db740, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3c9e0 sp=0xc000a3c868 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xeb2060, 0xc0001db740, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3cb58 sp=0xc000a3c9e0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf23c00, 0xc0001db740, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3cc88 sp=0xc000a3cb58 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf23c00, 0xc0001db740, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3ce00 sp=0xc000a3cc88 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe631c0, 0xc00007c420, 0x1b5) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:394 +0xe25 fp=0xc000a3cf78 sp=0xc000a3ce00 pc=0xd5cd65 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xeb33c0, 0xc00007c420, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3d0f0 sp=0xc000a3cf78 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xefc400, 0xc00007c420, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3d220 sp=0xc000a3d0f0 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xefc400, 0xc00007c420, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3d398 sp=0xc000a3d220 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf09e80, 0xc00034d2c0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3d510 sp=0xc000a3d398 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xebb380, 0xc00034d2c0, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3d640 sp=0xc000a3d510 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xebb380, 0xc00034d2c0, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3d7b8 sp=0xc000a3d640 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf33260, 0xc0002488c0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3d930 sp=0xc000a3d7b8 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf09da0, 0xc0002488c0, 0x1f9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3daa8 sp=0xc000a3d930 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf09a20, 0xc0002488c0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3dc20 sp=0xc000a3daa8 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf6b5a0, 0xc000196360, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3dd50 sp=0xc000a3dc20 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf6b5a0, 0xc000196360, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3dec8 sp=0xc000a3dd50 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4d60, 0xc000196350, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3e040 sp=0xc000a3dec8 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe93400, 0xc000196350, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3e1b8 sp=0xc000a3e040 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xed3ae0, 0xc000196350, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3e330 sp=0xc000a3e1b8 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf5fd00, 0xc000196350, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3e460 sp=0xc000a3e330 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf5fd00, 0xc000196350, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3e5d8 sp=0xc000a3e460 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf28880, 0xc0001b6488, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3e750 sp=0xc000a3e5d8 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf6b840, 0xc0001b6488, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3e880 sp=0xc000a3e750 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf6b840, 0xc0001b6488, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3e9f8 sp=0xc000a3e880 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe7f000, 0xc0001161b0, 0x1b5) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:394 +0xe25 fp=0xc000a3eb70 sp=0xc000a3e9f8 pc=0xd5cd65 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf0a200, 0xc0001161b0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3ece8 sp=0xc000a3eb70 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf3f4a0, 0xc0001161b0, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3ee18 sp=0xc000a3ece8 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc0004f6ab0, 0xf3f4a0, 0xc0001161b0, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3ef90 sp=0xc000a3ee18 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf33260, 0xc00015cfc0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3f108 sp=0xc000a3ef90 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf09da0, 0xc00015cfc0, 0x1f9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3f280 sp=0xc000a3f108 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf09a20, 0xc00015cfc0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3f3f8 sp=0xc000a3f280 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf6b5a0, 0xc00017b2e0, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3f528 sp=0xc000a3f3f8 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc0004f6ab0, 0xf6b5a0, 0xc00017b2e0, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3f6a0 sp=0xc000a3f528 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef4d60, 0xc00017b2d0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3f818 sp=0xc000a3f6a0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe93400, 0xc00017b2d0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3f990 sp=0xc000a3f818 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xed3ae0, 0xc00017b2d0, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3fb08 sp=0xc000a3f990 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf5fd00, 0xc00017b2d0, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a3fc38 sp=0xc000a3fb08 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc0004f6ab0, 0xf5fd00, 0xc00017b2d0, 0x36) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a3fdb0 sp=0xc000a3fc38 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf28880, 0xc000123148, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a3ff28 sp=0xc000a3fdb0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xf6b840, 0xc000828d80, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a40058 sp=0xc000a3ff28 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc0004f6ab0, 0xf6b840, 0xc000828d80, 0x1b6) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a401d0 sp=0xc000a40058 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef52a0, 0xc000828d80, 0x1b9) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a40348 sp=0xc000a401d0 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xef0620, 0xc000828d80, 0x199) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a404c0 sp=0xc000a40348 pc=0xd5c474 github.com/davecgh/go-spew/spew.(*dumpState).dumpPtr(0xc000a40ab0, 0xee9ea0, 0xc000828d80, 0x16) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:154 +0x7f8 fp=0xc000a405f0 sp=0xc000a404c0 pc=0xd5b5b8 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc0004f6ab0, 0xee9ea0, 0xc000828d80, 0x16) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:262 +0x1765 fp=0xc000a40768 sp=0xc000a405f0 pc=0xd5d6a5 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xe64f00, 0xc0006580c0, 0x95) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:394 +0xe25 fp=0xc000a408e0 sp=0xc000a40768 pc=0xd5cd65 github.com/davecgh/go-spew/spew.(*dumpState).dump(0xc000a40ab0, 0xf73140, 0xc000658000, 0x99) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:421 +0x534 fp=0xc000a40a58 sp=0xc000a408e0 pc=0xd5c474 github.com/davecgh/go-spew/spew.fdump(0x161e140, 0x1111720, 0xc000726630, 0xc000a40c30, 0x1, 0x1) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/dump.go:465 +0x125 fp=0xc000a40af0 sp=0xc000a40a58 pc=0xd5d845 github.com/davecgh/go-spew/spew.(*ConfigState).Sdump(0x161e140, 0xc0004f6c30, 0x1, 0x1, 0x19, 0x0) /root/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1/spew/config.go:281 +0x78 fp=0xc000a40b38 sp=0xc000a40af0 pc=0xd5aa58 github.com/stretchr/testify/assert.diff(0xf73140, 0xc000658000, 0xf73140, 0xc0006581c0, 0x0, 0x0) /root/go/pkg/mod/github.com/stretchr/testify@v1.6.1/assert/assertions.go:1592 +0x1d8 fp=0xc000a40cd8 sp=0xc000a40b38 pc=0xd66e38 github.com/stretchr/testify/assert.Equal(0x1114680, 0xc00054bb00, 0xf73140, 0xc000658000, 0xf73140, 0xc0006581c0, 0xc000a41078, 0x1, 0x1, 0x1)

  • 导入模块开头要大写,不然会报错
cannot refer to unexported name xxxx

错误变量覆盖,深坑,没告警


func (mgr *Manager) fixRunningTask() {
    db := store.GetMongo()
    docs, err := db.GetNoHeartbeatTask()
    if err != nil {
        return
    }
    //proxy连接代码,省略
    for _, doc := range docs {
        task := task.NewTask(doc)
		ErrCount := 0
		var res error
		for {
			err := task.SyncResult(proxy, true, opts...)
			if err == nil || err != ErrSystem {
			// res = task.SyncResult(proxy, true, opts...)
			//if res == nil || res != common.ErrSystem {
				break
			}
			// 如果是框架超时等错误,多试几次
            ErrCount++
            if ErrCount > 10 {
				break
			}
		}
		if err != nil && err != ErrStillRunning {
		// if res != nil && res != ErrStillRunning {
			// 可能发生了迁移或者重启,重新做
            db.InitTask(...)
        }
    }
}

上面的err,覆盖了下面的err。导致永远不会执行InitTask

c/c++程序员习惯于编译器纠错,但go编译器一点反应都没

一个字符数组默认引用的例子

start := Partition.Range.Start
startUint32 := binary.LittleEndian.Uint32(start)
binary.BigEndian.PutUint32(start, startUint32)

这么改,start和 Partition.Range.Start实际上是一个东西

这段代码这么看没啥问题,如果是个复杂一点的逻辑,就会被坑到,比如后面又改了start,但结果把上面的一起改了

go pprof抓内存泄漏

curl localhost:6060/debug/pprof/heap >base.out
# 喝口水,上个厕所
curl localhost:6060/debug/pprof/heap >current.out
go tool pprof -base base.out current.out

# 进入交互
(pprof) top10
(pprof) tree

基本就能查到,还算好用

看懂堆栈

// Declaration
main.Example(slice []string, str string, i int)

// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)

类型 + 长度

参考资料

  • https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-03-array-string-and-slice.html
  • https://go-zh.org/doc/effective_go.html
  • https://gfw.go101.org/article/container.html
  • https://go-internals-cn.gitbook.io/go-internals/chapter1_assembly_primer
  • https://www.includehelp.com/golang/how-to-check-if-structure-is-empty.aspx

Read More

dlang入门

我本身有啥语言都会点,所以这门语言我会用其他语言的特性来描述,请谨慎阅读

安装

curl https://dlang.org/install.sh | bash -s

基本的工具 dmd编译,rdmd可以当成shell脚本使用#!/usr/bin/env rdmd 包管理工具dub

包引用语法 import std.stdio : writeln, writefln;基本上大同小异

基本概念

  • 类型,完全等同于c/c++但是存在构造函数.init

    • 每个类型有各种属性,.max .nan 等等,构造函数也是一种属性,.stringof返回自身名字。感觉这很反射,python也有__repr__
    • auto 同,typeof也类似,immutable等同于const
    • 内存管理,内嵌GC,三种内存模式 @system默认 @safe 检查内存安全@trusted三方api互通有点像rust的unsafe
  • 控制流,完全一致,有个foreach相当于range-for
  • 函数,完全一致,支持函数内函数,以及返回auto
  • 结构体/类,this函数就是构造函数,private修饰成员函数,override修饰
    • interface接口以及工厂模式
  • 数组,支持slice,类似go,map就是特殊的数组 int[string] arr;
  • 模版,完全就是go那个德行
auto add(T)(T lhs, T rhs) {
    return lhs + rhs;
}

struct S(T) {
    // ...
}

大概就这么多


ref

  • https://tour.dlang.org/tour/en/basics/functions
  • 魔鬼细节 http://dpldocs.info/this-week-in-d/Blog.Posted_2021_02_15.html

Read More

nim入门

我本身有啥语言都会点,所以这门语言我会用其他语言的特性来描述,请谨慎阅读

基本概念

  • 注释 # /discord”””

  • 字符串 和c++差不多,有raw字符串 r”blahbah\balh”

  • var 变量定义,有点像rust的let 可以指定类型,以及用值初始化

    • 感觉这个parser应该和rust差不多
  • let 和var差不多,但只能用一次,类似c++的const初始化
  • const常量 表示的编译期常量

  • 数字 科学表示法

    • 1_000_000 1.0e9
    • 十六进制字面值前缀是 0x ,二进制字面值用 0b ,八进制用 0o 单独一个前导零不产生八进制,和c++不一样
    • 可以后缀描述 有点像rust
    let
      x = 0     # x是 ``int``
      y = 0'i8  # y是 ``int8``
      z = 0'i64 # z是 ``int64``
      u = 0'u   # u是 ``uint`
    var
      a = 0.0      # a是 ``float``
      b = 0.0'f32  # b是 ``float32``
      c = 0.0'f64  # c是 ``float64``
    
  • 控制流 所有的控制条件都不需要括号,有点像python

    • if 没括号
    let name = readLine(stdin)
    if name == "":
      echo "Poor soul, you lost your name?"
    elif name == "name":
      echo "Very funny, your name is name."
    else:
      echo "Hi, ", name, "!"
    
    • case 有点switch case 那味儿了
    let name = readLine(stdin)
    case name
    of "":
      echo "Poor soul, you lost your name?"
    of "name":
      echo "Very funny, your name is name."
    of "Dave", "Frank":
      echo "Cool name!"
    else:
      echo "Hi, ", name, "!"
    
    • while没啥说的
    • for可以当作迭代器
    echo "Counting to ten: "
    for i in countup(1, 10):
      echo i
    #语法糖
    for i in 0..<10:
      ...  # 0..9
    var s = "some string"
    for i in 0..<s.len:
      ...
    for index, item in ["a","b"].pairs:
      echo item, " at index ", index
    # => a at index 0
    # => b at index 1
    
    • 作用域 block 都是按照空格的,更像python
    • break一样,可以跳出循环,以及block
    • continue不提
    • when 类似c++的if constexpr 或者#ifdef这种
    when system.hostOS == "windows":
      echo "running on Windows!"
    elif system.hostOS == "linux":
      echo "running on Linux!"
    elif system.hostOS == "macosx":
      echo "running on Mac OS X!"
    else:
      echo "unknown operating system"
    
  • 函数 nim里叫procedure 过程 注意还是没有大括号
proc yes(question: string): bool =
  echo question, " (y/n)"
  while true:
    case readLine(stdin)
    of "y", "Y", "yes", "Yes": return true
    of "n", "N", "no", "No": return false
    else: echo "Please be clear: yes or no"

if yes("Should I delete all your important files?"):
  echo "I'm sorry Dave, I'm afraid I can't do that."
else:
  echo "I think you know what the problem is just as well as I do."

这个函数声明,像不像go/rust

fn add(a: i32, b: i32) -> i32 {
    return a + b;
}
//func 函数名(形式参数列表)(返回值列表){
//    函数体
//}
func hypot(x, y float64) float64 {
    return math.Sqrt(x*x + y*y)
}

这样设计,我就当parser好写了

注意 返回值不用必须显式舍弃 或者用修饰

discard yes("May I ask a pointless question?")

proc p(x, y: int): int {.discardable.} =
  return x + y

p(3, 4) # now valid
  • 重载 不仅可以函数重载,还可以操作符号重载,还可以直接调用操作符,和c++的operator是一样的
proc `$` (x: myDataType): string = ...
# 现在$操作符对myDataType生效,重载解析确保$对内置类型像之前一样工作。

#"``"标记也可以来用调用一个像任何其它过程的操作符:
if `==`( `+`(3, 4), 7): echo "True"

函数也需要前向声明 (c/c++陋习)

  • 迭代器 有点像python的生成器
iterator countup(a, b: int): int =
  var res = a
  while res <= b:
    yield res
    inc(res)

还支持引用和切片,有点像c++/go 跳过了

还提供模版和泛型,更像c++了

多态,method

宏比较暴力,不仔细讲

  • with 和python的with差不多,但是是宏

如何对nim做贡献/输出? 引自 https://dev.to/xflywind/how-to-contribute-to-nim-language-4ad8

先看 Nim 文档 .

然后去处理 easy问题 或者丰富文档 documentation.

加功能,写库,可能要看RFC).

解决更复杂的问题,去处理JS codegen 标记的问题

Contributing Guide 文档一定要看


ref

  • 居然有中文版网页了 https://nim-lang-cn.org/docs/tut1.html

Read More

^