InnoDB锁漫谈
Thursday, July 18, 2019
InnoDB不同的锁类型说明
 
Prometheus实践之histtogram-quantile实现
Monday, November 19, 2018
Prometheus时间过程中的一些个人觉得比较难理解的点
 
Servic Mesh实践
Monday, April 23, 2018

前言

随着云计算和云服务的不断发展和普及,越来越多的应用和服务被部署在了云端,部署方式的改变也反过来驱动了服务实现上的变化,这种变化的一个体现就是微服务在最近几年大行其道。 当然,任何一种技术都不是silver bullet,微服务也不是,虽然和monolithic application比起来,微服务在开发、部署上任然有其自身的优势,但同时也引入了分布式系统的复杂性,这些复杂性包括服务的发现、服务间通信、服务监控等。针对微服务带来的这些服务性问题,Buoyant公司最早提出了Service Mesh的概念,Service Mesh可以理解为是构建在服务下的一个基础设施层,其主要职责是负责服务间的通信,如果把服务类比于物理网络节点的话,Service Mesh可以类比为TCP协议。 下面这张图(图片来源)中绿色模块代表微服务,蓝色模块代表Service Mesh,可以看到,每一个service都会同时部署一个Service Mesh的代理,服务只需要和它本地的代理通信,各个服务代理共同构建起了Service Mesh的网络。 title 目前,已经有很多基于Serice Mesh概念的实现,包括Linkerd、Istio、Nginx Mesh、Envy等。本文会尝试基于Linkerd实现微服务的部署,希望通过具体的实例加深对Service Mesh的了解。

服务实现

首先,用go语言实现两个简单的服务:ping和pong。 ping服务源码:

func main() {
    http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
        log.Println("Handle request")
        resp, err := http.Get("http://localhost:8002/pong")
        if err != nil {
            fmt.Fprintln(w, "I'm ping, talk to pong falied")
            return
        }
        defer resp.Body.Close()
        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            fmt.Fprintln(w, "I'm ping, talk to pong with return empty")
        } else {
            fmt.Fprintf(w, "I'm ping, talk to pong with response: %s", body)
        }
    })
    http.HandleFunc("/ping", func (w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "ping")
    })
    http.ListenAndServe(":8001", nil)
}

pong服务和ping服务类似,提供http://localhost:8002/和http://localhost:8002/pong两个接口,源码就不贴出来了。

Sidecar部署

上面的两个服务,相互间的通信都是直接通过ip和端口,而通常微服务都是通过服务发现获取到服务的最终地址的。 为了避免服务间的直接连接,在Service Mesh价格中,每个服务都会同时部署一个代理,服务和外部的通信都通过代理完成,这种模式也被称作Sidecar pattern。下面我们会分别为ping和pong分别部署各自的Sidecar。 为了部署方便,服务都采用docker部署,共享宿主机网络,部署命令:

docker run –rm –name linkerd-ping –network=”host” -v pwd/config.yaml:/config.yaml buoyantio/linkerd:1.3.7 /config.yaml

config.yaml文件内容如下:

admin:
  port: 9990
  ip: 0.0.0.0
routers:
- protocol: http
  label: outging
  dtab: |
    /svc/pong => /$/inet/127.1/8002;
  servers:
  - port: 4001
    ip: 0.0.0.0

我们部署了一个sidecar服务,服务端口为4001,服务会将所有对pong的请求解析到127.1:8002上,为了让ping服务使用这个代理,我们需要对ping服务做两个调整,一是为服务设置一个http代理,让服务的对外请求都请求到部署的代理上,而是把服务的ip和端口换成服务的名字。

os.Setenv("http_proxy", "http://localhost:4001")
resp, err := http.Get("http://pong/pong")

pong的sidecar部署和ping类似,不再重复。 这里顺便简单介绍一下Linkerd的route过程,详细的文档请看这里。下面这张图是Linkerd的routing的过程: title 从请求到解析到最终的服务会经理3个阶段:

  1. Identification,Identification阶段用于将请求转换成名字,Linkerd支持多种Identifiers,默认使用http的host header,如上面的例子,http://pong/pong会转换成/svc/pong。
  2. Bidding,bindding阶段是根据我们配置的dtab路由规则,对名字做转换,在上面的例子中,Bidding后会得到/$/inet/127.1/8002
  3. Resolution,第二步的bidding或将名字转换成以/$/#开头的client name,再通过resolution讲client name转换成服务的ip和端口,在上面的例子中,我们最终拿到的服务ip和端口是127.0.0.1:8002。

Sidecar服务编排

上面我们的服务和服务的sidecar是分开部署的,但sidecar是伴随每一个服务而存在的,可以使用docker compose把两个服务编排在一起。 首先,需要把我们的ping和pong两个服务docker化,Dockerfile内容如下:

FROM golang:1.10

WORKDIR /go/src/app
COPY ./main.go .

CMD ["go","run","main.go"]

执行下面的命令build镜像:

docker build -t my/ping .

然后,生成docker-composer.yaml配置文件,内容如下:

version: "3.2"
services:
  sidecar:
    image: buoyantio/linkerd:1.3.7
    container_name: my-ping-sidecar
    network_mode: "host"
    volumes:
      - type: bind
        source: ./config.yaml
        target: /config.yaml
    command: ["/config.yaml"]
  serv:
    image: bugzhu/ping
    container_name: my-ping
    network_mode: "host"
    volumes:
      - type: bind
        source: .
        target: /go/src/app

执行docker-compose up启动服务即可。

Service Discovery服务部署

到目前位置,我们已经完成了sidecar的部署,实现了服务的解析。但是从上面的配置文件可以看到,服务的解析是直接写在配置文件里的,这显然无法满足微服务动态服务部署、服务上下线、负载均衡等要求。因此,我们需要引入专门的服务用于服务的注册和发现。在Linkerd中,namers配置用于服务发现相关服务的配置,目前Linkerd支持的service discovery服务包括本地文件、zookeeper、consul,k8s等。我们选择zookeeper作为我们的service discovery服务。 首先,部署zookeeper服务:

docker run –name my-zookeeper –network=”host” -d zookeeper:3.3.6

修改Linkerd配置增加namer配置:

namers:
- kind: io.l5d.serversets
  zkAddrs:
  - host: localhost
    port: 2181
routers:
- protocol: http
  label: outging
  dtab: |
    /svc => /#/io.l5d.serversets/discovery;
  servers:
  - port: 4001
    ip: 0.0.0.0

当请求http://pong/pong时,Linkerd会根据我们配置的route规则最终解析到/#/io.l5d.serversets/discovery/pong,然后通过namers配置的Service Discovery服务解析到最终的ip和端口。 顺便说一下,使用serversets时,保存在zookeeper中的信息需要符合serversets格式,比如上面的离职,最终的服务节点都保存在zookeeper的/discovery/pong/member_xxxx路径下,如/discovery/pong/member_0000000000/discovery/pong/member_0000000002等,每一个路径代表一个服务节点,每个节点保存的信息如下:

{"status": "ALIVE", "additionalEndpoints": {}, "serviceEndpoint": {"host": "127.0.0.1", "port": 8002}}

Service Mesh部署

到目前位置,我们已经实现的Serivce Discovery服务的配置,可以通过Serice Discovery服务实现服务的动态部署。但是有一个问题,在文章开头的那副关于Service Mesh的示意图里,Server Mesh网络是通过sidecar服务互相通信的,服务本身只和sidecar交互。但是在我们的实例里,ping服务请求pong服务最终解析到的是pong的真实ip和地址,整个过程为ping请求ping’s sidecar,ping’s sidecar请求pong,和pong的sidecar没有关系。预期的架构应该是像下图的左边部分,但现状是下图的右边部分。 title

为了实现左边的架构,我们不仅需要让服务发出的请求走sidecar,同时也要让服务接收的请求走sidecar。事实上,Linkerd也是支持这种部署方式的。 修改pong的Linkerd的配置如下:

namers:
- kind: io.l5d.serversets
  zkAddrs:
  - host: localhost
    port: 2181
routers:
- protocol: http
  label: outging
  dtab: |
    /svc => /#/io.l5d.serversets/discovery;
  servers:
  - port: 4003
    ip: 0.0.0.0
- protocol: http
  label: incoming
  dtab: |
    /svc/* => /$/inet/127.1/8002;
  servers:
  - port: 4004
    ip: 0.0.0.0

pong的sidecar开启了4003和4004两个端口,其中4003作为服务的代理,4004作为服务的反向代理,之前在service discovery中注册的pong的服务地址为127.0.0.1:8002,8002端口需要改成4004,4004再将请求转发给8002。

Namerd服务部署

到目前位置,我们已经完成了一个简单的基于Service Mesh的微服务部署,但还是有很多问题,首先是我们的route规则都是写在Linkerd配置里的,其次是每个微服务都直连了service discovery服务,而这两部分在各个微服务之前是可以共享的。 Linkerd提供了Namerd服务,Namerd为我们提供了route规则的管理,同时也支持serverice discovery服务,实现了这两部分的集中管理。 首先,启动Namerd服务:

docker run –rm –name namerd –network=”host” -v pwd/config.yaml:/config.yaml buoyantio/namerd:1.3.7 /config.yaml config.yaml内容如下:

admin:
  port: 9991
storage:
  kind: io.l5d.zk
  zkAddrs:
  - host: 127.0.0.1
    port: 2181
  sessionTimeoutMs: 100000000
namers:
- kind: io.l5d.serversets
  zkAddrs:
  - host: 127.0.0.1
    port: 2181
interfaces:
- kind: io.l5d.thriftNameInterpreter
  port: 4100
  ip: 0.0.0.0
- kind: io.l5d.httpController
  port: 4180
  ip: 0.0.0.0

集成Namerd之后的Linkerd配置如下:

admin:
  port: 9990
  ip: 0.0.0.0
routers:
- protocol: http
  label: outging
  interpreter:
    kind: io.l5d.namerd
    dst: /$/inet/localhost/4100
    namespace: default
    transformers:
    - kind: io.l5d.port
      port: 4004
  servers:
  - port: 4001
    ip: 0.0.0.0
- protocol: http
  label: incoming
  dtab: |
    /svc/* => /$/inet/127.1/8001;
  servers:
  - port: 4002
    ip: 0.0.0.0

上面的配置里,iterpreter配置了Namerd服务的地址,其中有一个transformers的配置需要特殊说明一下,还记得上一部分我们为了实现服务接收的请求也走sidecar,把微服务的端口注册成了对应的sidecar的代理端口。transformers可以实现在发送请求之前,把端口替换成sidecar的代理端口,这样我们注册在service discovery里的服务端口不在需要替换成sidecar的端口了。

后记

基于Linker,我们实现了一个简单的Service Mesh架构的微服务部署,当然,还有很多微服务基础的功能缺失,包括服务的注册,服务的监控等。Linkerd支持服务相关数据收集和上报,但是,在Service Register这块功能目前是缺失的。 其实,在Service Mesh的实现上,Istio的功能更丰富一点,由于Istio还在快速的迭代中,部署起来也相对复杂,应此选择了Linkerd来进行Service Mesh的尝试。也推荐感兴趣的同学可以关注一下Istio。 为了部署方便,本文的所有基于docker部署的服务都使用了host网络模式,但在实际部署中很少采用这种模式。

参考连接

  1. Pattern: Service Mesh
  2. What’s a service mesh? And why do I need one?
  3. Service Mesh:下一代微服务
  4. Linkerd
  5. Istio
 
bitmap压缩算法对比
Wednesday, March 22, 2017

最近的项目中使用到了bitmap,简单描述一下使用场景:整体有亿级的用户,每一个用户有一个唯一的标识ID(整型),不同的用户会被打上不同的标签,系统需要支持按标签搜索用户,或者根据现有的标签组合(并集、交集),生成新的标签。

目前的解决方案是建立标签到用户的索引,一个标签对应到一组用户ID,用户ID以bitmap的格式存储,标签对应的用户是稀疏的,转换成bitmap存储空间的利用率很低,举个例子,比如某个标签对应的用户ID是[100000000, 200000000],转换成bitmap需要ceil(100000001/8)byte数据,空间的利用率极其的低,当然这个例子很极端,在生成环境不会出现,这里只是想说明bitmap存在存储空间利用率底的场景,需要对bitmap做压缩处理。

常用的bitmap压缩算法有Oracle’s BBC、WAH、Concise、Roaring。前三者都是基于run-length encoding的思路,RLE的思路是说对于重复出现的值,通过值加上重复出现的次数表示,从而到达数据的压缩,比如对于这个字符串:

WWWWWWWWWWWWBWWWWWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWWBWWWWWWWWWWWWWW

通过RLE编码之后为:12W1B12W3B24W1B14W。bitmap的值只包含重复的0和1,基于ELB的压缩会是不错的选择。Roaring的压缩思路和前三者都有不同,接下来我会试着介绍WAH、Concise、Roaring各自的实现细节。

首先是WAH,WAH继承自BBC,其主要思路为:

  1. 把bitmap按每31位分组,每个组称为一个block
  2. 如果block既包含0又包含1,用block以1+block的内容表示
  3. 如果block只包含0,则以00+n表示,其中n标识block的数量
  4. 如果block只包含1, 则已01+n表示,其中n标识block的数量

img1 图片来源

n的最大可取值为2^30-1,通常情况下n都远小于这个值,因此存在空间的浪费。

Concise是对WAH算法的改进版本,把block的后30个bit位分成了两段,前5位和后25位,其中,

  • 前5bit表示在第几位存在一个反转,所谓反转是指00开头的block在某一位存在一个1,或者01开头的block在某位存在一个0
  • 后25个bit表示后面还有多少个block

下面这个例子,是对 {3, 5, 31–93, 1024, 1028, 1 040 187 422}数组的压缩: 图片来源

  • word#0表示0-30,{3,5}
  • word#1表示31-92,{31-92},前2bit01表示全为1,中间5bit全为0表示不存在反转,后25bit为1表示后面还有1个全为1的block,数据范围为31到31 + 31 + 31 * 1- 1
  • word#2表示93-1022,{93}, 前2bit表示数据全为0,中间5bit为1标识在第1位(低93位)存在一个反转,后25bit为29标识后面还有29全为0的block,数据范围为93到93 + 31 + 31 * 29 -1
  • #word3表示1023-1053,{1024,1028}
  • #word4表示1054-1040187391
  • #word5表示1040187392-1040187422

下面这个特殊的数列可以很好的对比WAH和Concise算法的差异,{0, 62, 124,…}, 用WAH表示为:

10000000000000000000000000000001,00000000000000000000000000000000,
10000000000000000000000000000001,00000000000000000000000000000000,
10000000000000000000000000000001,00000000000000000000000000000000,...

用Concise标识为:

00000010000000000000000000000001,
00000010000000000000000000000001,
00000010000000000000000000000001

在这个场景下,Concise每个数占用了32bit空间,WAH每个数占用了64bit空间。

但是基于RLE的压缩算法有以下两个明显的短板:

  1. 不能很快速的check某个值是否在这个bitmap里,必须要decompress之后才能知道
  2. 不能跳过某一段数据,在两个bitmap做位于运算的时候,如果一个bitmap的某一区间全为0,没有办法直接跳过另一个bitmap对应区间的数据。

Roaring算法不同于前面提到的几种,其算法思路是:

  1. 对于32位的整数,每2^16个数分成一组,称为一个chunk
  2. 对于一个单独的chunk,如果其包含的数超过4096个,用一个2^16bit的bitmap存储
  3. 对于元素小于4096的chunk,用一个有序的数据(sorted array)保存
  4. 每一个chunk上都会保存这个chunk包含的元素个数(Cardinality)

对于检索操作,比如判断某个整数x是否存在,首先x/2^16定位到属于那个chunk,如果chunk是一个bitmap,检查x mod 2^16这个bit位,如果chunk是一个数组,使用二分查找。

对于逻辑运算,也是以chunk为单位进行运算,存在bitmap vs bitmap,bitmap vs array,array vs array三种不同的场景,针对不同的场景,会采用不同的计算方法,具体细节可以参看这里,就不展开了。另外值得一提的点是,算法对逻辑运算都是in place操作的,比如两个bitmap做位或运算,会直接修改其中的一个bitmap,而不是分配一个新的bitmap。

对于前面提到的那个特殊的数列{0, 62, 124,…},采用Roaring算法,每个数占用的空间≈16(每个数在chunk内只占用16bit,chunk本身会占用一定的空间) WAH、Concise、Roaring算法的性能对比在这篇paper里有提及,有兴趣的同学可以看一下,从结果看,Roaring在各方面的表现都是不错的。

参考链接:

  • http://github.tiankonguse.com/algorithm.html#sec-1-1-5
  • https://arxiv.org/pdf/1402.6407.pdf
  • https://github.com/RoaringBitmap/RoaringBitmap
 
Inforbright实践
Saturday, January 09, 2016

最近做的项目都是和数据查询展示相关的,而且对应的数据量都比较大,某些表每天的数据都在千万级、整体数据量亿级以上,这样的数据量放在MySQL有些吃不消,即使是分库分表查询效率也上不出,而且太复杂的分表或导致数据的查询逻辑也变得复杂,所以从MySQL换到了Infobright(以下简称IB)上,选择IB最大的一个好处是其查询语法和MySQL完全兼容,所以业务逻辑基本不需要修改。记录一下这段时间以来IB的使用心得。

数据导入

首先是数据导入,IB不支持insert、update、delete这样的通常的数据更新方式,只支持LOAD DATA INFILE ... INTO,换句话说,只能通过cvs格式的文件导入。IB数据导入的速度还是很快的,在导入数据的时候,IB会同时开启多个线程对多个不同的列同时做导入,这个是其内部实现,没办法外部控制。实测下来,一千万行、18列的数据,导入时间在1分20秒左右。
我们自己采用的数据更新方式是先把数据更新到MySQL,再把数据从MySQL导出成文件,然后导入到IB,IB数据的组织是基于视图和不同的数据版本。更新分两种,增量更新和全量更新,我们的数据源通常是按天提供数据的,对于这种数据的更新采用的是增量更新,增量更新只需要把数据load到当前视图指向的表就行。如果涉及到历史数据的变更就需要全量更新IB的数据,当全量导入时,数据会被导入到一个新的表,导入完成之后,再将视图切换到新导入的这张表上。查询是从视图中查询数据,这样虽然存储数据的表名会变化,但是业务查询的视图名称不需要变化。
数据先更新到MySQL的主要原因是为了应对数据的更新,尤其是数据的修改和删除。IB数据基于视图的好处是当全量导入数据的时候,数据的查询不会受到影响,否则需要将表drop掉(IB不支持truncate table语法)再重新创建表然后再导入数据(其实不用视图也可以用RENAME TABLE,不过IB不支持这个语法)。
数据导入流程

数据查询

官方文档列出的最有效的数据类型有TINYINT、 SMALLINT、 MEDIUMINT、 INT、 BIGINT、DECIMAL、DATE、 TIME ,其次是CHAR和VARCHAR,所以建表的时候优先考虑使用这些数据类型,有一点需要注意的是不支持UNSIGNED类型的数据。对于CHAR或者VARCHAR的数据,当数据列的取值范比较小,数据相对收敛的时候(官方推荐当数据总数/唯一数据数大于10时,就可以考虑使用lookups),可以考虑使用lookups特性,lookups类似于一个数据字典,IB会用整型替换掉相应的CHAR或者VARCHAR的数据,以达到提高查询效率和降低存储空间的目的。lookups的使用也很简单,在数据表定义的时候,在需要使用lookups的列加上comment 'lookup'注释就行。
数据查询时,尽量避免使用OR,而是改用IN或者UNION ALL,在我们的应用中发现,UNION ALL的效果比IN好,SELECT f1,f2 FROM table WHERE f1=1 UNION ALL SELECT f1,f2 FROM table WHERE f1=2SELECT f1,f2 FROM table WHERE f1 IN (1,2)的查询效率要高。
还有一点就是尽量避免使用SELECT *,因为IB是列式存储的,每一列都可能涉及到数据的解压缩,所以最好只取需要的列。
其他的一些MySQL的查询优化对IB也是适用的,比如使用LIMIT、查询条件里避免使用函数、数据取值尽量避免NULL等。
说一下IB的查询效率,以我们自己的项目来说,在没有缓存的前提下,MySQL分钟级别的查询,IB基本都在秒级。查询效率提升在5-10倍左右。另外说一点就是IB对并发查询支持得不好,如果查询并发比较高的话需要注意。

内部实现

内部实现这部分,只是我自己对看过的一些东西的一些理解,试着写一下。 最最重要的一点,IB是一个列式数据库。列式数据库的优点有更高效的数据压缩和基于列的查询。IB所有的优点的前提也是基于数据的列式存储。
IB内部数据的存储分成了三层:

  • Data Packs(DPs),每一列的数据分组存到DPs里,一个DPs最多存放65536个记录,数据压缩也是在DPs里进行压缩的
  • Data Pack Nodes(DPNs),和DPs是一对一的关系,保存了DPs里的数据的一些信息,包括最大值、最小值、加和等值
  • Knowledge Nodes(KNs),相比于DPNs来说,KNs保存了更全局的和DPNs以及列相关的数据,甚至和其他表的关联数据。DPNS除了在数据load的时候会生成之外,在查询时也能会变化,可以理解为是一份动态的数据。

IB在查询的时候,会根据DPNs和KNs的信息尽可能的过滤掉无关的DPs,以减少需要解压缩的DPs。比如某个DPs的值范围为0到10,如果我们查询的是该列小于0的数据,则这个DPs就可以忽略掉。而如果查询的是该列小于5的数据,则需要对这个DPs的数据做解压缩,以获得最终的数据。
顺便提一下IB的数据压缩,文档里写的平均的数据压缩比和传统的行式数据库比起来在1:10,我们项目的实际应用下来发现和MySQL比起来,数据压缩也在10倍左右。

总结

通常数据查询优化的思路,一是减小查询的数据集,二是优化数据查询的效率。对于第一种思路,除了选择合理的表结构设计和字段类型,还有一种方法是离线数据汇总,把一些大的数据集做汇总,汇总成一些小的数据结果集,以达到减小查询数据集的目的。第二种思路通常是合理的sql和索引等,这里就不赘述。

抛砖引玉,欢迎拍砖交流。

参考连接

  1. Infobright Technology white paper
  2. Infobirght wiki

最后,增图一张(高清大图请点这里): RDBMS_Genealogy_V5