简单 proxy

最近自信心膨胀,感觉自己能做些事情。对于大数据里的分布式数据库已经觊觎很久了,想多了解下里面的东西。

长远的计划是自己写一个分布式的nosql数据库。只要一直坚持下去,还是可以有一个可以用的玩具的。目前的知识储备还是不够的,所以从简单的开始做起——写一个简单的 proxy 开始。从简单的开始,搭建自己的实验环境:

  • 网络库
  • 线程模型
  • 工程配置
  • 等等

架构

总共分为两个部分:

  1. proxy
  2. db

Proxy

Proxy的功能就是转发请求,所以最主要就是网络库和协议的定义上。网络这块,计划使用单线程+非阻塞IO支持。协议,使用 redis 的协议规范。

db

db 节点就是用 rocksdb 和 网络库实现。由于db的操作比较耗时,所以这里的线程模型和proxy不一样。使用的是 网络线程 和 工作线程池支持高并发操作。

网络库和线程库

网络库和线程库有几个候选的方案:

  1. 自己从头实现一个。 好处:从头开始,没有历史问题, 缺点: 不一定实现的好,会花费些时间
  2. 使用之前无线下载库的。 优点:自己还算熟悉,缺点:代码不算优雅
  3. 使用 boost、boost-async-io 。 优点:暂时不清楚, 缺点:印象中代码依赖很多,库大
  4. 使用 pika 项目中的。优点:支持redis的协议规范,缺点:暂时未知

以上,需要查看资料了解下,对比分析下优缺点。

工程构建

语言使用 C++,项目的构建使用 CMake。

项目要额外支持的辅助功能:

  • lint,检查编码风格
  • valgrind,内存检测工具
  • google perf 监控
  • glog,日志库
Advertisements

Linux 系统内存查看

一直用 top 命令看进程的内存消耗使用。一直只是知道一个代表虚拟内存,一个代表的是物理内存。没有查资料确实的验证过。

今天仔细看到了 Linux Used内存到底哪里去了?,也算是心里有个底了。

free -m

内存使用分布图

ps aux

The aim of this post is to provide information that will assist in interpreting memory reports from various tools so the true memory usage for Linux processes and the system can be determined.
Android has a tool called procrank (/system/xbin/procrank), which lists out the memory usage of Linux processes in order from highest to lowest usage. The sizes reported per process are VSS, RSS, PSS, and USS.
For the sake of simplicity in this description, memory will be expressed in terms of pages, rather than bytes. Linux systems like ours manage memory in 4096 byte pages at the lowest level.
VSS (reported as VSZ from ps) is the total accessible address space of a process. This size also includes memory that may not be resident in RAM like mallocs that have been allocated but not written to. VSS is of very little use for determing real memory usage of a process.
RSS is the total memory actually held in RAM for a process. RSS can be misleading, because it reports the total all of the shared libraries that the process uses, even though a shared library is only loaded into memory once regardless of how many processes use it. RSS is not an accurate representation of the memory usage for a single process.
PSS differs from RSS in that it reports the proportional size of its shared libraries, i.e. if three processes all use a shared library that has 30 pages, that library will only contribute 10 pages to the PSS that is reported for each of the three processes. PSS is a very useful number because when the PSS for all processes in the system are summed together, that is a good representation for the total memory usage in the system. When a process is killed, the shared libraries that contributed to its PSS will be proportionally distributed to the PSS totals for the remaining processes still using that library. In this way PSS can be slightly misleading, because when a process is killed, PSS does not accurately represent the memory returned to the overall system.
USS is the total private memory for a process, i.e. that memory that is completely unique to that process. USS is an extremely useful number because it indicates the true incremental cost of running a particular process. When a process is killed, the USS is the total memory that is actually returned to the system. USS is the best number to watch when initially suspicious of memory leaks in a process.

对比 top 命令,结果确实是:

VIRT == VSZ
RES == RSS

Jenkins 发送邮件模板

刚给项目搭建了调度系统,使用的是 Jenkins。原因:

  • Jenkins 可以定时触发执行,也支持项目之间依赖关系
  • 自己对 Jenkins 比较熟悉

今天项目有个定时的服务,需要将执行的最终日志发送给相关的人。但是服务的日志很多,最终需要的信息也就是日志的最后几行。

搜索了下发现可以使用 Jenkins 的内置变量可以支持:

${BUILD_LOG, maxLines=50, escapeHtml=false}

参考 Stackoverflow 的问题 How can I take last 20 lines from the $BUILD_LOG variable?

安上面的方法,可以将日志最后的50行通过邮件发送出来。但是,50行日志在邮件中显示在同一行了,不便于阅读。于是又在网上搜索了相关的问题,还是在 StackOverFlow 上找到了解决方法。使用 email-ext 插件的 Jelly 模板来将行与行分开。问题:
Jenkins Email-ext plugin build log all on one line

在实践上面一种解决方案的时候,发现 Jelly 模板会被 Jenkins 缓存到其他目录。修改模板文件后不能生效,重启都没有用。我就索性修改了模板名称。

个性化推荐列表数据存储的几个方案

背景

现在在开发推荐系统的服务。推荐系统能够为用户提供个性化的推荐结果,提升产品的体验。个性化推荐服务的难点在于数据量大。在数据达到一个量级之后,保证系统的性能就会降低。

就拿手雷这个产品来说。 刚开始给手雷产品做个性化推荐,服务的响应速度很快。但是随时用户活跃的提升,需要个性化的用户数据越来越多,数据库存储的用户数据也越来越多。其中慢查询也渐渐增多。

现在我们将这些用户的个性化推荐数据存放在 SSDB 中。 在使用 SSDB 存储手雷用户的个性化视频列表中遇到一些性能问题:

  1. 使用Key-Value存储,查询性能降低
  2. 使用Sorted-Set存储,数据更新效率过低

本文主要总结之前两次使用到的方案的优缺点,并说明下自己后续性能优化的方向。

已实践方案及问题

在做个性化推荐的时候,尝试过两种存储方案。两种方案各有优缺点。

Key-Value

将用户的Id作为Key,将推荐的结果集作为Value。其中Value是Json的数组形式。在手雷推荐中,Json数组的长度是 250。每次客户端请求的时候,拉取8个视频。所以在查询的时候,就存在不必要的浪费问题。也因此,在Ssdb缓存数据的时候需要额外缓存那些可能不会使用的数据。

但是使用Key-Value 对于现在的推荐系统来说有个好处。在算法更新数据的时候,可以快速的上线。从理论上来说,比使用Sorted-Set 快了3个数量级。

目前,推荐系统还是使用的该方案。但是这种方案,有其弊端,不是终极的解决方案。

Sorted-Set

由于一次线上事故,在加上从理论上KV的方式就存在性能问题。所以就尝试使用Sorted Set代替KV的方式。

Key是用户的Id, 将推荐的Items存储在 Sorted Set 中。 使用该方案之后,查询的性能确实提升了很多。

但是,使用Sorted Set的问题也很明显。

  1. 列表中只能保存简单的ItemId和对应的score,而score对于服务来说用处不大
  2. 更新数据的时候,耗时特别长。一个小时仅能更新20w左右的数据;而使用KV,10min 可以更新 100w 数据。

为什么更新 Sorted Set 会这么慢呢?这个和更新 Sorted Set 的逻辑以及 Ssdb 中 Sorted Set 的实现有关。

使用KV更新用户推荐结果及的时候,仅仅需要Set一个用户的结果集即可。而使用 Sorted Set 后,更新数据时必须先将原有的 Set 清空,然后在设置新的结果集。

实现

Sorted Set 数据结构

Ssdb在实现 Sorted Set 的时候,存储了3个部分的数据

  1. size,单个key
  2. score key, 存储 name + key + score 作为key, 空字符串作为 value 的 Keys 有序列表
  3. set key, 存储 name + key 作为 key, score 作为 valued 的列表

其中,2 用于遍历查询,3用于根据name + key 查询 score使用。

zclear操作的实现

  1. 外层zcan遍历获取所有list内容
  2. 调用zdel删除
    1. zget 获取旧值
    2. Delete
  3. 修改list的长度

相比原来key value,使用zset计算量增加很大。

使用Sorted set更新推荐结果集复杂度

  1. 存储数据 double
  2. 增加一次清空操作
    1. 增加遍历查询一次
    2. 增加查询旧score n=250 次
    3. 删除 score key 和 set key 各 n = 250 次
    4. 增加更新size操作 n=250 次

对比KV 仅需要一次Set操作, 那么Sorted Set 需要的操作包含:

  1. scan 操作 1 次
  2. 读 操作 250 次
  3. 写操作 1225 次

即将操作放大了 3 个数量级以上。

新的解决方案

现有的两个方案,均存在利弊。最希望的情况,写的时候按照KV方式写,读的时候按照 Sorted Set的方式读。

Sorted Set 是一个复杂的数据结构,支持很多操作。在实际过程中,有些功能我们使用不上,比如zget和score都未使用。所以,可以参考 Sorted Set 的方式,实现自己简单的有序列表。

Multi KV

Sorted Set 对于我们来说,有两个缺点:

  1. 更新数据的时候需要删除就数据
  2. 一项数据需要保存两次

这样就增加了计算量了。为了避免不必要的开销,我们可以按照下列方式存储数据:

guid:index   item

8941818032d1997ff192d98bdf616cfb:0   ba002b38826fa3c5649335bd21e7e334
8941818032d1997ff192d98bdf616cfb:1   8b14e2a5a90511478bd93feae096e68a

这样在查询的时候,就不会返回无用的结果集了。并且写入的时候,可以直接覆盖,不需要那么多额外的操作。并且,value 就可以使用更复杂的对象结构,可以包含 item 的详细信息了。

缺点也还是有的,写入速度提升并不明显。其写入相对KV方式放大了 250 倍。按照 SSDB benchmark 的结果,而该方式更新数据。每个小时可以更新 100 w 的用户,勉强可以接受。

使用该方式存储,那么查询的时候相当于遍历查询了,及返回结果是一个列表。这个方式也有明显的缺点,每次查询的时候,key部分有较多的重复。

为了实现该方案,还需要客户端支持 hash tag 功能。及根据 key 中特定的一部分,决定一致性哈希算法最终路由到那个节点。

Multi KList

基于,以上的方式还有个优化版的方案。将每次分页返回的结果作为一个 value 存储。这样既可以解决查询是 key 部分重复的问题,因为每次查询仅返回一个 KV。

同时,可以将KV缩小至越来的 1/8 。在提升查询效率的同时,有可以提升写的效率。这样在更新数据的时候可以达到 800w/h。

该方案的缺点:

  1. 提升了读写的复杂度
  2. 对于更改分页大小的可扩展性较低

个性化推荐列表数据上线问题

背景

  1. 数据实例为拆分,导致数据上线阻塞了线上的写操作
  2. 线上使用key-value方式,读时有性能损耗
  3. 使用sorted set 方式上线数据

ssdb benchmark

========== set ==========
qps: 70803, time: 0.141 s
========== get ==========
qps: 88958, time: 0.112 s
========== del ==========
qps: 72924, time: 0.137 s
========== hset ==========
qps: 34125, time: 0.293 s
========== hget ==========
qps: 90302, time: 0.111 s
========== hdel ==========
qps: 55437, time: 0.180 s
========== zset ==========
qps: 31012, time: 0.322 s
========== zget ==========
qps: 91118, time: 0.110 s
========== zdel ==========
qps: 46683, time: 0.214 s
========== qpush ==========
qps: 47786, time: 0.209 s
========== qpop ==========
qps: 30377, time: 0.329 s

新问题

使用sorted set 存储个性化数据,数据上线步骤如何:

  1. 使用zclear 删除原有的推荐结果集
  2. 使用mulit_zset 设置新的推荐结果集

zclear的实现

  1. 外层zcan遍历获取所有list内容
  2. 调用zdel删除
    1. zget 获取旧值
    2. Delete
  3. 修改list的长度

相比原来key value,使用zset计算量增加很大。

解决方案

  1. 多线程并发

50线程并发写,0.5h 写入数据13w。即每个小时 写入26w。同步100w,需要4h。
且随着数据量的增加,写入速度会变得更慢。

失败!

  1. 回退key – value

回退到之前使用的key vlaue形式

  1. 修改存储引擎(或者中间件): 写key-value, 读 list
    1. 支持 range scan 操作

kvlist db 的想法

sorted set 的限制,


两种方案

底层存储的使用的是KV

nginx 根据 url 分流

反向代理

location / {
                        proxy_pass  http://tj;
                        proxy_connect_timeout    600;
                        proxy_read_timeout       600;
                        proxy_send_timeout       1200;
                        proxy_set_header Host $remote_addr;
                        proxy_http_version 1.1;
                        proxy_set_header Connection "keep-alive";
                        proxy_set_header X-Real-IP $remote_addr;
                        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                        proxy_ignore_client_abort on;

                }

根据path切分不同流量

location /hello {
                        proxy_pass  http://hello;
                        proxy_connect_timeout    600;
                        proxy_read_timeout       600;
                        proxy_send_timeout       1200;
                        proxy_set_header Host $remote_addr;
                        proxy_http_version 1.1;
                        proxy_set_header Connection "keep-alive";
                        proxy_set_header X-Real-IP $remote_addr;
                        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                        proxy_ignore_client_abort on;

                }

参考:
http://stackoverflow.com/questions/27220678/how-to-redirect-to-specific-upstream-servers-based-on-request-url-in-nginx

服务端开发踩过的坑

晚上上线了一个新版本,通过监控看到服务器的负载陡降。突然心情是愉悦了很多。总结下以前碰到踩过的一些坑吧:

  • 调用外部接口,http的连接池限制太小。大流量来的时候,导致请求在排队,造成创建很多线程,内存耗尽
  • 调用外部接口,未设置超时时间,导致服务器负载变高
  • 服务器部署多个实例,使用内存未规划好,导致物理内存被用尽,最终程序挂了
  • 使用第三方SDK,未经过严格的测试/验证就直接在生产环境使用
  • 辅助函数加了类级别的锁,导致QPS上不来

下图就是我修复了第三方sdk的性能问题后的变化: