0%

起因

最近在接公司的中台服务,他们提供的sdk代码中有长的redis key前缀,长度达到20 byte,和他们争执了一下key的长度问题,虽然最后他们修改了,但是感觉我这边没有给出足够有说服力的解释,他们心里不服,所以,Talk is Cheap, Show U the Code & Data。

简单分析

简单分析下,redis key的长度会从存储和网络传输两方面带来影响,我们将从源码分析网络传输和存储流程,并最终用benchmark数据做佐证。

网络传输分析

redis client和server间通过RESP(REdis Serialization Protocol)协议进行通信,其兼顾了解析效率和可读性,例如,get foo 命令的TCP包的详情如下:
redis RESP tcpdum
整个TCP包占74 Bytes(52 Bytes Overhead + 22 Bytes Data),redis命令占用的22个字节:

1
*2\r\n$3\r\nget\r\n$3\r\nfoo\r\n

解释如下图:
redis RESP 协议示例
有一种要打脸的预感😅。
TCP一个包的长度最大时65535 Bytes,但是一般MTU都是1500 Bytes,从这个角度上来说,单条命令或者pipeline组合的多条命令,只要不是太长,都能在一个TCP包里面将数据传输到Server端,二十多个字符前缀的key和缩写后的5个字节前缀的key的差别不会太大,都能在一个TCP包传输完,只有当pipeline命令长度超过一个数据包的大小时,才会带来时间上的差异。

内存使用分析

redis server在redis源代码中用 redisServer 结构体表示,其中包含了 redisDb 这个结构体用于表示db对象,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
/* Redis database representation. There are multiple databases identified
* by integers from 0 (the default database) up to the max configured
* database. The database number is the 'id' field in the structure. */
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;

从中我们看到两个比较重要的 dict 结构体分别是 dict和expires,前者是存储reedis的key-value数据的,后者是存储key-expire数据的,也就是每个key的过期时间,dict是redis对hash table的包装,dict结构的定义如下:

1
2
3
4
5
6
7
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;

dict中真正存储数据的是 dictht ,这里使用了两个主要是为了方便在 rehash 的时候使用,dicht本质上就是一个hash table:

1
2
3
4
5
6
7
8
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;

dictht在存储的时候先对key做hash,得到一个槽的位置,然后通过开链表的方式将value存储到对应槽的首位。这里dictEntry存储了一个实际的key-value

1
2
3
4
5
6
7
8
9
10
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;

可以看到value是union,其实就是为了方便存储各种类型的数据,例如存储过期时间需要使用int64_t,存储double的value需要使用double,而存储字符串需要使用void*
从代码分析,如果key长度太长,确实会占用更多的内存空间,因为redis的key都是直接按照字符串存储的(准确说是sds),对于value是会有编码的,暂不在这里展开。

数据测试

由于redis自带的benchmark工具的测试点有限,目前不能设置key的长度,因此用JedisClient写了简单代码做分析,测试10W个key的写入情况

key value memory cost time(sigle command) cost time(pipeline) command per tcp package(pipeline)
sp: + (0 - 1W数字) 5-16随机字符 7.77M 391994ms 7434ms 36
long_prefix: + (0 - 1W数字) 5-16随机字符 8.63M 426011ms 8450ms 27

运行环境:Redis 4.0.0 on Raspberry pi II - 512M RAM

结论

  1. key的长度。单个命令情况下,耗时基本无影响;在pipeline情况下,如果key较多,每增加一个tcp包大概增加2ms耗时,在我们的测试中,如果一次取200个key,那么耗时大概差两个tcp包,4ms左右,
  2. 存储空间。会有增加,增加多少得看具体情况,我们的测试中大概增加11%

Reference

  1. Redis Protocol specification
  2. Redis V4.0

简介

最近vultr上的机器ip经常被封,然后就要换机器,需要重新安装shadowsocks,整理了下面的脚本方便快捷安装(目前仅限Vultr上的CentOS)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yum install git vim net-tools -y
yum install python-setuptools -y && easy_install pip
pip install git+https://github.com/shadowsocks/shadowsocks.git@master
yum groupinstall "Development Tools" -y
wget https://download.libsodium.org/libsodium/releases/LATEST.tar.gz
tar zxvf LATEST.tar.gz
cd libsodium-stable/
./configure
make -j8 && make install
echo /usr/local/lib > /etc/ld.so.conf.d/usr_local_lib.conf
ldconfig
sudo ssserver -p 55555 -k laotie666 -m chacha20 --user nobody -d start
## 开端口访问
sudo iptables -A IN_public_allow -p tcp -m tcp --dport 55555 -j ACCEPT

我们经常能在油管上看到一些有趣的视频,但是限于网络环境问题有的人并不能看到这些视频,这就催生可一些搬运工,例如微博上就有很多这种搬运工,这里简单介绍一下搬运的流程

Step 1 下载视频和字幕(如果有中文字幕)

直接从油管上是无法下载视频的,我们通过一些工具网站,可以下载到视频。而字幕也需要同样到工具网站下载,如果没有字幕也没关系,我们可以自己写srt字幕

Step 2 视频和字幕处理

直接下载下来的视频片头和片尾可能有别人的宣传Logo,因此需要裁剪掉,

1
ffmpeg -ss 00:01:00 -i input.mp4 -t 00:02:00 -c copy output.mp4

注意其中-t是裁剪的duration,如果是截止时间应该是-to

翻译字幕是必须的,因为大部分视频都不是中文的,我们在翻译完srt字幕后,需要将srt格式转化成ass格式,因为我一直没能正常合成srt字幕😭

1
ffmpeg -i video.srt video.ass

Step 3 合成字幕,生成最终视频

1
ffmpeg -i cut.mp4 -vf ass=video.ass output.mp4

例子

快手ID : 1140937661

Reference

  1. ffmpeg 裁剪视频
  2. ffmpeg 添加字幕

问题现象及影响

gRPC在我司被广泛应用,之前在搭建gRPC服务时遇到了奇怪的问题,Client端在第一次调用Server端时回超时,但是第二次之后就基本正常了,这种情况会导致的问题是在该环境搭建的API服务,在启动后可能需要调用多次才能正常返回(gRPC存在调用链,调用超时会导致直接抛异常,本次请求会失败),并且存在潜在的数据不一致问题,这在一定程度上不能确保搭建的gRPC服务的稳定性和正确性。
gRPC服务的简单结构如下:
ks_grpc_flowchart

简单分析

由于搭建的环境位于办公网络,和线上服务环境有差异,首先想到的是网络问题,但是通过ping的方式看时间都在10ms以内,而目前我们使用的gRPC超时都在秒级别,觉得不是网络问题,需要通过按照上图中的每个点逐步分析。

详细分析(Step 1)

首先需要从Client找到发生超时的具体代码,这里通过jvisualvm对Client进行profile,找到如下热点代码:
热点代码
图中红色框部分方法是公司框架对gRPC做的包装,在Client端创建ChannelInfo后对其进行预热,方式是通过调用HealthCheck服务,通过debug确认超时问题发生在这个调用HealthCheck.check方法。

注意,可以在测试代码前加一定的Sleep时间,方便在jvisualvm里attach进程并选择sampler
图中排在第一位的是测试代码入口方法,可排除

详细分析(Step 2)

怀疑是网络问题,因为从Server端的HealthCheck服务代码看不可能耗时这么久,因此在Client和Server端分别开启tcpdump抓包,通过对照HTTP2请求的发送和接收时间,发现网络延迟在8毫秒以内,因此不是网络问题,抓包情况入下图:
tcpdump结果

详细分析(Step 3)

现在看问题只有可能出现在Server端了,Cleint端发送的请求会被Server端的EventLoop boss线程select到,然后通过HTTP2解码,送到worker线程,两者的交界处是SerializingExecutor.execute,在这里boss线程Client端发送的请求加到队列,worker线程会从中拉取出请求并根据请求匹配到相应的服务执行调用。
直接在SerializingExecutor.execut处通过arthas的trace功能查看方法内每个调用的时间,非常幸运我们看到了这个方法的调用时间是秒级的而且匹配超时时间,通过进一步缩小范围发现耗时较多的是ProfileInterceptor里的一个调用,调用如下:
arthas trace效果
这个点明显就是Server端在用Client端的ip拿host的过程,这个的作用是记监控,问题就很明显了,DNS反解析出问题了

详细分析(Step 4)

查看Server机器的DNS配置 /etc/resolv.conf,配置如下:
nameserver配置
尝试ping两个nameserver,发现192.168.43.27不能ping通,断定问题就出在Server和该nameserver网络不通,联系运维同学确认了是nameserver没有设置好,因为目前该环境都是在172的子网内,并没有在192.168子网内,将192从nameserver中删除后问题修复

总结

分析过程中Step 1和Step 2并没有耗费很多时间,Step 3耗费了一些时间,最开始想通过远程debug确定耗时点,但是由于同一个JVM中运行了N个服务,debug的时候不能刚好抓到超时的那个请求,走了一些弯路(当时还怀疑是不是Step 2出问题了)
其实后来发现有时候第一次不会超时,但是大部分时候会超时,应该是我大部分时候都rotate到不可访问的nameserver上去了
总的来说这个问题不是太复杂,但是个人觉得方法步骤都很正确,当时耗费了一些时间来查,所以记录一下

Reference

  1. arthas
  2. gRPC

简介

falcon-agent是小米研发的监控框架,目前在我司被广泛使用到机器状态监控和一些基本的服务数据监控,其中机器状态监控直接使用了框架提供的功能,而服务数据监控做了二次开发,方式是用机器上其他服务调用各自的Client往agent服务推送监控数据,agent默认监听机器1999端口,监听发送到/v1/push路径的http请求,个人认为http的好处是可以跨语言。这两部分数据最终都会通过内置的golang rpc发送到后端服务器,后端服务器负责存储这些数据并进行实时监控展示和报警。

基本原理

详见falcon-agnet,入口是main.go,其中做了两大块事情

  1. 启动一堆cron服务,也就是定时任务,这些任务会定时执行,收集机器的监控指标,例如CPU使用率、磁盘使用率等
  2. 启动http服务,方便接收其他程序发送过来的监控数据
    以CPU监控为例:
    a. CPU监控代码所见,以CpuIdle为例,计算方式就是和简单的差值计算
    b. 至于为什么用差值,就看CpuIdle的具体计算方式,简单说机器上的/proc/stat文件记录了机器的cpu使用情况,但是记录的是cpu处于每一种状态的总时间,所以需要在第一步使用的时候做差值
    main.go里面以及其调用启动了很多gorouting并在channel间做了通信,channel的关键字是chan

    Reference

  3. falcon-agnet

RPCMonitor是快手内部广泛使用的监控报警平台,用于监控Grpc服务、Mysql、Redis、memcached等资源的可用性,并在服务出现问题时进行报警。

基本原理

在基础组件层面做打点,例如memcached执行命令添加listener,并在其中监控命令执行状态,所有数据由RPCMonitor Client处理,目前的做法是将数据存储到指定文件目录下,每台机器上会有一个RPCMonitor Agent,负责将本机的所有监控数据文件中的数据通过grpc调用上传到服务器,服务器端逻辑是黑盒,无法拿到源码。

TODO 加个图,举个例子

Reference

imagemagicki是一款开源的图片处理工具,支持对图片编辑、修改等操作。

命令列表

  • resize
    1
    2
    convert me.jpeg -resize 50% tiny_me.jpeg
    convert me.jpeg -resize 300*200 tiny_me.jpeg
  • quality(取值范围1-99)
    1
    convert me.jpeg -quality 85 tiny_me.jpeg
  • sample-factor(采样相关,对整体图片大小影响不大)
    1
    convert me.jpeg -sampling-factor 4:2:2 tiny_me.jpeg

Reference

  1. imagemagick command line options

metrics-core是一个开源的Java监控打点系统,目前在github上star数达到了6186

存在问题

  • Meter(计数)写死了是EWMA(exponentially-weighted moving average),也指数衰减平均值,简单来说就是最近1分钟内的平均值需要计算入一分钟之前的计数,导致明明当前系统已经没有调用了,但是还能看到Timer的计数非0

优点

  • 完善的监控功能,包括次数、时间、实时值等,后面会介绍到这些功能
  • 输出方式可扩展,控制台、日志、csv文件、网络方式等等

对比

  • Prometheus 时间监控,时间值提前限定好范围
  • Netfflix servo 没仔细看过

主要功能

  • Meter 次数监控,以rate形式呈现
  • Histogram 时间监控,带分位数
  • Gauge 实时监控,监控生成监控的那个时间点的值,一般用来监控队列大小之类的
  • Counter 计数,只有增加和减少两个方法,从程序启动后计数的总数,目前看基本没啥用
  • Timer Meter和Histogram的集合体,即包括了次数和时间监控

简单扩展/修改

  • 针对Meter,新增了一种“滑动窗口”形式的Meter
  • 增加和Aop集成,通过注解能够做方法级别的时间和次数监控,减少了使用时编码量,代码带了公司包名,以后完善了再给链接

Reference

  1. metrics-core quickstart