• 首页
  • 关于

我自然

作者存档:yankay

听说 Docker 被 kubenetes 抛弃了,怎么办?containerd

在 2021年6月12日 上公布 作者为 yankay

原文地址:原文链接,作者: 张世明

Containerd 是 Kubernetes 默认的标准 CRI 运行时环境,被 Kubernetes, Docker 等各种项目使用。也是 CNCF 唯一的容器运行时。Docker 在最近这些年 ,没有太多新特性引入,而更轻量的 containerd 正在蓬勃发展。 Docker公司 正在衰落,Containerd 将继承衣钵,成为未来容器引擎的实施标准。

Containerd 的使用情况

Containerd 并非一个 全新技术,它是由 Docker 公司开源,作为 docker 的核心组件存在。根据社区中的几个托管服务和开源项目使用的统计,到 2020 年止 Containerd 的使用 (包含在 Docker 中使用) 占容器使用的 83%。

continerd 可以作为 kubelet 和 docker 的 容器引擎使用,也可以单独使用

根据 Sysdig 的统计数据,从 2019 年 到 2020 年,conatinerd 的独立使用率,上升了 2倍。上升趋势明显。

Containerd 1.5 的一些新特性

最近的Containerd 1.5 以于2021年5月4日发布。这是 Containerd的第6个主要版本包括许多稳定性改进, 以及代码组织结构的变化使后续开发更方便。该版本默认启用支持 OCI crypt 解密,并引入了对zstd、NRI 和 FreeBSD 的实验性支持,还包括CRI插件纳入主容器库和切换到Go modules。

更好的压缩算法支持

现在除了 gzip 之外,还支持 zstd 作为镜像压缩算法。zstd 是 Facebook 在2016年开源的新无损压缩算法,优点是压缩率和压缩/解压缩性能都很突出。

zstd 的设计目的是达到与 deflate 算法(开发于1991年,用于gzip) 相当的压缩比, 并且更快,尤其是解压的时候。

zstd 在其最大压缩级别的压缩比接近 lzma、lzham 和 ppmx,并且比 lza 和 bzip2 性能更好。它的解压速度比任何当前可用的算法都快,并且压缩比更好。

zstd 的基准测试结果 在速度方面,在最快模式下,它的速度通常是 stdlib deflate/gzip 2倍。

支持加密镜像

可以把镜像加密后上传到镜像仓库, Containerd 只需要配置解码的 key 就可以运行这加密的镜像, 对于一些对于安全性有需求的镜像可以使用这种方式。

自从 Containerd 1.3 加入该功能后,已支持容器使用 加密镜像 (OCI crypt) 但是一直并没有启用该功能。

该特性将在 1.5 中已经默认启用,使用方法请参见文档 (依赖的二进制文件包含在 cri-containerd-cni-1.5.0-linux-amd64.tar.gz )

注意:当前 Docker 是不支持 OCI crypt 的,因为 Docker 目前还没有使用 Containerd 管理镜像。

节点资源接口

节点资源接口(NRI) 用于 CPU 调度约束和内存配额的标准接口。

其类似容器网络接口(CNI)的基本接口、概念和插件设计是一种处理容器网络堆栈的多种实现的优雅方式。此概念可用于其他接口,以定制容器的运行时环境。节点资源接口(NRI)是一个新的接口,其实带有结构化API和容器插件设计,用对节点上的资源进行管理。与 CNI 相同 NRI 插件将在容器创建完毕, 但是还没有真正启动之前的初始化时被调用。

可以看示例代码了解 NRI 的使用。

支持 FreeBSD

现在支持在 FreeBSD 运行 Containerd,容器运行时 OCI 是使用 FreeBSD jails (Samuel Karp 的 runj)。文件系统目前仅支持 ZFS, 后续计划支持 unionfs。

结尾

原文作者“张世明”是 任职 DaoCloud 研发团队的 天才青年,在过去的 半年,在 containerd 社区 排名第八的贡献者。

感谢“胖橘“对本文的润色加工。

各位观众老爷,您是否看好 containerd 呢?

文章分类 软件技术 | 标签: containerd | 发表评论 |

公告 – 博客重开了

在 2021年3月28日 上公布 作者为 yankay

尊敬的读者,博客重开了。

“我自然“这个博客,从2008年开始写,到现在已经13年了。期间记录了不少点点滴滴。中间不但断更,而且博客图床数据也被删除了。不过好在终于成功恢复。未来会有更多的精彩内容。

最近这 6年间,笔者是去创业(DaoCloud)去了,之所以断更 主要是因为 “特别忙“ – 真的特别忙,博客坏了3年,却没有3个小时去修理,更别提更新什么文章了。 不过对于工作来说,这样的忙法是病态的,而且也不知道还要忙到什么时候。既然如此,那还是把博客捡起来吧。

接下来这个博客,我还是会多谢写一些技术干货,穿插一点点生活故事。有人会问 “我自然” 这个博客标题是什么意思。“我自然“,是一种自然而然, 怡然快乐的感觉。在现代社会每个人都被资本或政治异化,变成了“人力资源”,“资本家” 这些非自然的形象。 通过读一读这个博客,也希望能够给读者回到更自然快乐的感觉。

文章分类 家庭生活 | 1 条评论 |

CloudFoundry v2面面谈,内赠MicroCFv2福利

在 2014年2月6日 上公布 作者为 yankay

CloudFoundry 是业界领先的PaaS云平台,可以为应用提供运行平台,类似于运行着无数应用的炙手可热的HeroKu。最近发布的第二代,功能上有了极大的扩充,如BuildPack, Service Broker v2, loggregator,并且用GoLang重写了大部分组件提升性能,如GoRouter,CLI,HM9000。本文带您走进这个大观园。还提供一个MicroCFv2下载,满足您试一试的愿望,只此一家哦。

CloudFoundry v1已经出现较长时间,在三年前EMC中国研究院就参与其研究。对CloudFoundry v1的研究可以参见彭麟《深入 Cloud Foundry》。在CloudFoundry v2诞生前的三年里,一些事情发生了巨变。外部环境方面,AWS走向成熟,Heroku逐渐成功,摸索到了PaaS成功的道路。OpenStack风起云涌,Docker带着小伙伴们异军突起。面对这些,CloudFoundry面临的竞争加剧,但同时也有了可以配合的伙伴。而CloudFoundry内部也发生了变化,原先隶属于专攻虚拟化的VmWare,现在与时俱进,成为了专攻大数据的Pivotal的一部分。而CloudFoundry v2是CloudFoundry归于Pivotal的第一个版本,成为这家兴新大数据公司的战略一部分。

如果想试试CloudFoundry公有云,可以在官网上申请账户。下文主要针对自建CloudFoundry。

新架构

是骡子是马,看看架构就懂了。在看第二代的架构之前,我们回顾一下之前的架构。

用户通过VMC Client将应用上传到Cloud Controller 上,Cloud Controller将应用部署到DEA Pool上面。用户可以通过Router访问到各自的应用,Health Manager查看各个APP状态,保证可以自动重启。同时Cloud Controller还提供了各种Services,如MySQL,Redis等等。

在上一代架构中,CloudFoundry呈现出大包大揽的方式,APP的部署也好,Service的提供也好,都自己做。虽然扩展Runtime和Service并不麻烦,但是这需要“CloudFoundry”管理员的介入,租户是没有办法做这些的。另外私有云的玩家往往都有着定制Runtime和Service的需求,内置的Service很难满足需要。当然还有一些问题,如Router性能不佳,协议匮乏。Health Manager单点。

在新的架构中,CloudFoundry有着更加开放的玩法。

第二代CloudFoundry几乎将V1时代组件全部重写,满足新的需要。

APP方面,在上传应用的时候,用户可以同时上传一个BuildPack,这样租户可以根据自己的需要来部署应用,无需通知云管理员。BuildPack是Heroku的部署机制,在社区有着丰富的资源。因此CloudFoundry和Heroku是兼容的。可以部署在Heroku上的应用,也可以部署在CloudFoundry上。还有很多其他PaaS也使用BuildPack,BuildPack已经成为PaaS应用部署的事实标准。

Serivce方面,不再内建Service,而是使用一个更加简洁的Service Broker和User Provided Service设计。用户可以将Service Broker使用现有的XaaS上面,如果OpenStack Trove, AWS RDS。Heroku 有很多小伙伴们 可以提供各种各样的Service,比如监控服务Relic,国内也有很多,如监控宝。GAE式的PaaS证明关门玩Service是不行的,CloudFoundry走向了开放的道路。另外User Provided Service可以让接上用户现有服务,如Oracle,保护现有资产。

大数据深入人心,CloudFoundry现在的loggregator可以让应用的日子流进Service中。实时数据分析成为可能。

新的的CloudFoundry对运行在IaaS有着天生的亲和力。BOSH可以非常方便的部署CF。Router的性能瓶颈得到解决。UAA可以提供第三方认证。Health Manager也不再是单点。

开放的App Runtime

开放的App Runtime的力量来自如Build Pack。我们可以浏览先App 部署的全过程。

 

  1. 用户使用使用CF PUSH命令上传应用
  2. CLI告知CCNG创建一个应用
  3. CCNG在数据库中加入该应用的记录例如应用名称,BuildPack选择
  4. CLI上传程序
  5. CCNG将程序存起来
  6. CLI启动应用
  7. 由于应用尚未部署,所以CCNG找一台DEA,在该DEA内执行BuildPack来部署应用
  8. DEA输出运行BuildPack的信息
  9. BuildPack执行完毕,输出是一个DropLet文件(编译打包的结果),DEA将该文件存起来
  10. DEA将打包情况汇报给CCNG
  11. CCNG选择一个DEA来部署应用
  12. 应用在DEA中运行,运行结果输出到CCNG

可以看到,BuildPack和APP一样都是在一样的环境(DEA)中执行的。BuildPack非常简洁,只需要三个脚本。

  • bin/detect 用来判断该BuildPack是否支持该程序
  • bin/compile 用来编译,类似Maven的mvn compile
  • bin/release 用来打包,类型Maven的mvn package

现在CloudFoundry内建了三个主要BuildPack,Java BuildPack是自制,Ruby和NodeJs都是沿用Heroku的

  • Java BuildPack 支持非常多框架和JVM语言。甚至包括new_relic,这给我们监控CloudFoundry上APP提供思路。
  • Ruby BuildPack
  • NodeJs BuildPack

得益于Heroku的流行,第三方的BuildPack就数不胜数了。可以在Heroku buildpacks 和 CloudFoundry Commmunity 中找到很多。

Build Pack有一个问题就是每次编译都需要从外网下载依赖,巨大JRE文件和不稳定的网络会使部署失败。不过最近的发布中提供了Build Pack Cache功能,可以有效解决这个问题。在内网中搭建一个Cached Proxy也是不错的办法。

开放的Service

CF-Relase是CloudFoundry的发布包,我们可以对比下V1和最近的发布包。

  v1 v2(依据v155)
CloudFoundry Core组件数量 29 21
Service数量 24  0

可见在v1版本中有大量的组件是在做Service,摊子铺的很大。而V2中将这个包袱放下,提交给各种第三方XaaS了。连接XaaS和CloudFoundry的中间组件被称为CloudFoudry  Broker。v2的Service Broker和v1的完全不同。V2中的设计如下。

 

一个Service Broker 需要实现5个API接口,包含三方面

  • Service发现。租户可以向ClouldFoundry提交Add Service 命令,参数是URL。然后ClouldFoundry去调用该URL,发现该URL包含哪些Service
  • Service创建/删除。自动化的创建Service。
  • Service绑定/解绑定。将Service的一些访问参数设定成APP的环境变量。

Service Broker的部署可以很灵活。既可以作为CloudFoundry的组件,也可以作为CloudFoundry的APP运行在CloudFoundry上。官方提供了一些Service Broker 实现实例。

  • GitHub repo service
  • MySQL database service
  • Spring Service Broker
  • MySQL Java Broker

在企业生产环境中,Service的自动化创建并非易事。举MySQL例子,选择版本,机器,网络,存储,备份策略,高可用方案,搞上防火墙,打上自定义补丁等等,一千个生产环境有一千种个MySQL玩法。在现在的玩法中,需要人介入的环节太多,太有必要。不存在一招鲜吃遍天的自动化创建方法。User  Provided Service 就是来调和这个矛盾。让Cloud Foundry不强依赖自动化的创建Service。

User  Provided Service 很简单,就是用户在创建Service的时候,输入Service访问参数。如用户名,密码,CloudFoundry把这参数存起来,在绑定的时候注入到环境变量中。下面会演示。

亲昵的大数据

国内的CloudFoundry玩家大多有开放平台的计划。作为开发平台的运营者,不只要提供一个稳定,开放的平台,获得应用的数据,就等于把握住了脉搏。Pivotal做为一家大数据公司,接手CloudFoundry的一个大改进就是增加了Loggragtor模块。

 

Loggregator有是数据的中转站。数据可以来自应用和CloudFoundry的自身组件。和SysLog不同,Log根据APP分离,所以产生的数据是为APP服务,而不是为CloudFoundry系统本身服务。

引入了Loggregator后,用户在创建Service的时候,Service可以返回一个SysLog URL。当该Service绑定到某一个应用,该应用的Log会顺着这个URL源源不断流入。这个Service可以说Splunk也可以说Pivotal Analytics. Heroku也有了这个机制。用户的大数据应用就可以无缝接入了。

Loggregator提供推(SysLog),拉(WebScoket)两种方式来获得数据。新的CF CLI就是使用Loggregator的WebSocket来获得APP的Log信息。

部署-MicroCFv2福利

不避讳的说,部署CloudFoundry v2的难度大于v1。

在v1中有dev_setup,提供一个基于Chef的一键脚本可以轻松部署。而v2中依赖BOSH,一个一站式的解决方案。可以将CloudFoundry部署在VSphere,OpenStack和AWS上。

目前看来有三种部署方式

  • 使用BOSH,BOSH比较重,运行起来就要费一些心力。但运转起来后可以提供健康监控,扩容的支持。
  • 使用IaaS自带的部署机制,可以使用VSphere OVF,OpenStack  Heat,Aws Cloudformation等技术。部署方便,但绑定IaaS。
  • 手动一步步安装。最灵活,也最费力。可以考虑和Puppet等机制结合。

由于官方不再提供新版本dev_setup,试一试CloudFoundry的成本变得很高。笔者提供了一个MicroCFv2镜像,请使用7-zip解压

MicroCFv2下载 (基于v154)

运行MicroCF

  1. 安装VMware Player
  2. 下载MicroCFv2
  3. 使用该镜像启动一台虚拟机
  4. 使用用户名/密码(admin/admin)登录

检查网络,正常情况下虚拟机会通过DHCP获得IP地址。记下IP。编译应用需要访问外网。

admin@atsg2-sh199:~/env$ ifconfig 
eth0      Link encap:Ethernet  HWaddr 00:50:56:98:7f:0a  
          inet addr:10.32.170.199  Bcast:10.32.170.255  Mask:255.255.255.0

部署一个Java APP

admin@atsg2-sh199:~$ cd /home/admin
admin@atsg2-sh199:~$ cf login      
API endpoint> api.cf.com
Username> admin
Password> admin

admin@atsg2-sh199:~$ cf push helloworld -p helloworld.war 
App started
urls: helloworld.cf.com

     state     since                    cpu    memory        disk          
#0   running   2014-02-06 11:52:18 PM   0.0%   60.8M of 1G   95.1M of 1G 

admin@atsg2-sh199:~$ curl helloworld.cf.com

helloworld

 

创建一个Service

admin@atsg2-sh199:~/env$ cf create-user-provided-service oracle-db-mine -p '{"username":"admin","password":"pa55woRD"}'
OK
admin@atsg2-sh199:~/env$ cf bind-service helloworld oracle-db-mine             
OK

部署一个Ruby APP,并查看环境变量

部署Ruby APP,需要访问网络。
这个APP可以显现他自己的所有环境变量。

admin@atsg2-sh199:~$ cd /home/admin/env
admin@atsg2-sh199:~/env$sudo  bundle install 
admin@atsg2-sh199:~/env$ cf push 
requested state: started
urls: env.cf.com
     state     since                    cpu    memory          disk          
#0   running   2014-02-07 12:14:18 AM   0.0%   18.1M of 128M   53.2M of 1G   
admin@atsg2-sh199:~/env$ cf bind-service env oracle-db-mine             
OK
admin@atsg2-sh199:~/env$ cf restart env
OK
admin@atsg2-sh199:~/env$ curl env.cf.com
...
VCAP_APP_HOST:
{  "user-provided": [
    { "name": "oracle-db-mine",
      "label": "user-provided",
      "tags": [],
      "credentials": {
        "password": "pa55woRD",
        "username": "admin"
      },
      "syslog_drain_url": ""
}]}
...

设置浏览器

你可以使用浏览器访问你部署的应用。需要给浏览器设置HTTP代理。IP为MicroCFv2的IP,端口是8123.如:

这样就可以使用浏览器了。

使用愉快。如果遇到问题,可以联系我。

文章分类 软件技术 | 标签: CloudFoundry, Micro CloudFoundry | 5 评论 |

Docker能够运行任何应用的“PaaS”云

在 2013年9月17日 上公布 作者为 yankay

Docker 简介

Docker 是一个开源可以将任何应用包装在”LXC容器”中运行的工具。如果说VMware,KVM包装的虚拟机,Docker包装的是应用。是一个实至名归的PaaS。

当应用被打包成Docker Image后,部署和运维就变得极其简单。可以使用统一的方式 来下载,启动,扩展,删除,迁移。

Dock可以用来:

  • 自动化打包和部署任何应用
  • 创建一个轻量级私有PaaS云
  • 搭建开发测试环境
  • 部署可扩展的Web应用

Docker是开源的,可以在GitHub上访问其代码,提供Restful接口。他的贡献者是一个非常流行的PaaS云提供商 https://dotcloud.com/

PaaS 的核心价值

遗失的方向 – 绝大部分应用竟然不能无缝迁移到主流PaaS上

云计算发展多年,分为IaaS,PaaS和SaaS。其中PaaS(平台即服务)最为不愠不火。笔者因为,最大的原因是PaaS给人(开发,运维,老板)带来的价值不够多,私有PaaS门槛高!最大的问题在于绝大部分应用竟然不能无缝迁移到PaaS上。

Heroku是最流行的公有PaaS云。很廉价好用,可是大部分应用都无法部署。每个应用只能监听一个HTTP端口,应用之间不能交互。他可以为你自动扩展和负载均衡,但其实没得选择,只要使用Heroku就必须接受限制。好歹Heroku支持绝大部分平台如Java,Python。相比之下,GAE更甚,只支持三个平台,不能访问文件系统,也不能启动子进程。

CloudFoundry是比较流行的私有PaaS云。限制和Heroku一样多,部署比较复杂。为此他甚至有一个量身定制的部署工具BOSH。有多难用,用过的人都知道。这个不能怪开发人员,他定义的PaaS本来就这么复杂。

PaaS要实现开发人和运维人的梦

开发人的梦 – 自在的运行环境,无限的资源

开发人希望专注于程序逻辑。有自由自在的运行环境,有丰富的外部资源如各种中间件。至少不要为什么 端口监听数目,通信协议限制 这些事情所困扰。

运维人的梦 – 没有故障和重复的事,减少等待

运维天天处理故障,如果千千万应用都能以同样的方式部署,运维。那么故障的处理就简单多了,重复的事情也会变少。下载部署,安装依赖,这些事情都太过繁琐耗时了。

Docker功能

Docker可以让开发和运维都变得简单。

开发者不必要像使用一般PaaS一样在充满着限制的条件下开发应用,可以就如同平常一样,自由的使用各种资源。老子说“太上下知有之 ,其次誉之,其次侮之”。Dock对于开发者就是“下知有之”的状态。

爽的人是运维。要使用Docker,需要在机器上安装Docker Engine,

  1. 创建一个Container。实际上是一个Linux Container,Docker会将网络,存储这些事情都配好。
  2. 下载应用并安装。比如可以用sudo apt-get install mysql-server 来安装一个MySQL。并配置一些参数什么的。
  3. 打包上传Image。Docker可以将这个Linux Container打包成Image,启动脚本也在其中。并上传至Image Registry中。这个Image仅仅包括你修改的增量部分,所以体积比较小。
  4. 一条命令跑起来。使用Docker Run 一条命令可以从Image Registry中下载Image 并跑起来。

需要重启的时候,只要重启Container。需要迁移的时候,只要迁移Container。一切干干净净。

PaaS,IaaS术业有专攻

IaaS普遍使用虚拟机,开销较大。Docker明显更轻量。笔者认为IaaS和PaaS各有专攻,PaaS去专注于安全级别的隔离是没有意义的,IaaS也不应该去感知到应用。一个公有云中,可以让每个租户使用不同的虚拟机,虚拟网络来做到安全和资源上的隔离。再通过PaaS统一运维,管理计算资源。

没有必要让每个应用都专享一台虚拟机,这样开销太大。但在安全敏感的环境中,每个租户使用不同的虚拟机是合理的。

使用Docker

在线尝试

使用Docker最便捷的方式莫过于 在线尝试: http://www.docker.io/gettingstarted/#

完成了这个在线的教材,相信你对Docker已经基本玩转了。

在Ubuntu中安装Docker

现在Docker支持两个Ubuntu版本:

  • Ubuntu Precise 12.04 (LTS) (64-bit)
  • Ubuntu Raring 13.04 (64 bit)

有两个依赖

  • Linux kernel 3.8 (read more about Kernel Requirements)
  • AUFS 文件系统

所以需要确认下您的操作系统,安装依赖并重启:

# Add the PPA sources to your apt sources list.
sudo apt-get install python-software-properties && sudo add-apt-repository ppa:dotcloud/lxc-docker

# Update your sources
sudo apt-get update

# Install, you will see another warning that the package cannot be authenticated. Confirm install.
sudo apt-get install lxc-docker

安装Docker并重启:

# Add the PPA sources to your apt sources list.
sudo apt-get install python-software-properties && sudo add-apt-repository ppa:dotcloud/lxc-docker

# Update your sources
sudo apt-get update

# Install, you will see another warning that the package cannot be authenticated. Confirm install.
sudo apt-get install lxc-docker

使用Docker,下载一个Ubuntu Image,并创建一个Container,在其中运行Bash

# download the base 'ubuntu' container and run bash inside it while setting up an interactive shell
sudo docker run -i -t ubuntu /bin/bash

# type 'exit' to exit

成功,你已经玩转Docker了!!

Docker Run的时候发生了什么?

当用户执行Docker run的时候,发生了这些事情:

  1. Docker CLI 调用 Docker Engine的Restful API。默认情况下,Docker  Engine是监听在一个Unix Socket上的,当然也可以监听在TCP端口上。
  2. 从docker index下载一个Ubuntu Image。docker index是一个荟萃Docker Image的地方,就像一个Repository.你也可以构建自己的私有Repository。
  3. 分配文件系统。文件系统是AUFS,这是一种“增量文件系统”,你做的修改都可以以增量的方式保存。因此Docker Image可以很小。
  4. Mount文件系统
  5. 创建网络端口。Docker使用Linux Bridge和Linux Network NameSpace来配置网络。
  6. 配置IP地址。给刚刚创建的虚拟网卡配一个内部IP。这个IP不重要,因为Docker是通过静态NAT来对外保留TCP/UDP端口的。
  7. 在LXC中执行命令,这个例子中命令就是 “/bin/bash”
  8. 截取“/bin/bash”的输入输出流到Terminal,和你交互

Dock详解

在Docker中运行Redis

创建一个Docker Container

sudo docker run -i -t ubuntu /bin/bash

安装Redis .

apt-get update
apt-get install redis-server
exit

拍个快照,创建你自己的Docker Redis Image

docker ps -a  # grab the container id (this will be the first one in the list)
docker commit <container_id> <your username>/redis

运行Redis。-d 是指后台运行,使用你刚刚创建的Image。

sudo docker run -d -p 6379 <your username>/redis /usr/bin/redis-server

使用redis-cli访问

sudo docker ps  # grab the new container id
sudo docker inspect <container_id>    # grab the ipaddress of the container
redis-cli -h <ipaddress> -p 6379
redis 10.0.3.32:6379> set docker awesome
OK
redis 10.0.3.32:6379> get docker
"awesome"
redis 10.0.3.32:6379> exit

 文件系统

一般来说,要Linux需要两种文件系统

  1. boot file system (bootfs)
  2. root file system (rootfs)

bootfs包含有bootloader。用户从来不会更改bootfs。事实上,当机器启动结束后,kernel会卸载掉这个bootfs。

rootfs就是我们通常看到了Linux文件目录,包括 /dev, /proc, /bin, /etc,/lib, /usr, 和/tmp等等。不同的Linux发行版的rootfs是不一样的,软件包结构也是不同的。Docker通过管理rootfs,可以在同时运行多个Linux发行版。

当传统Linux启动的时候,rootfs是只读的,检查完整性后会转化成可读写状态。

当Docker挂载rootfs的时候,也是只读的。但是他并没有把它转化为可读写状态,而且在其上使用 union mount 来加一层,创建一个可读写的文件系统。原理的rootfs还是只读的,数据被写入新的空间。Docker称之为”层”,数据可以这样一层一层叠加起来。

最初的时候,最顶层中什么数据也没有,当进程创建修改文件的时候,数据就会保存在最顶层。底层的文件系统没有丝毫改变。

当导出Image的时候,其实就是导出最顶层而已。

由于底层的只读的,多个Docker Container可以共享,提高的文件系统的使用效率。

Docker生态环境

Docker是开源的,提供完善的Restful接口,设计简洁,直戳痛点。但是因此比较简单,没有华丽的功能。凤栖梧桐,以Docker为树干,衍生出许多优秀的项目。

  • dokku 100行BASH的微Heroku。包含了一个PaaS的基本功能
  • shipyard Docker管理界面,提供多Host,创建Container,查看Image等功能
  • openstack-docker Docker和OpenStack集成,可以使用Nova和Glance来控制
  • jiffylab教学用Python和Unix Shell平台
  • BYO SAAS Memcached as a Service
  • Dockerui Docker管理界面
文章分类 软件技术 | 标签: Docker, PaaS | 9 评论 |

Scala Tour – 精选

在 2013年5月2日 上公布 作者为 yankay

5月1日是劳动的日子,笔者做了一个学习Scala精彩特性的网站Scala-Tour。在学习Scala是时候,遇到很多令人激动的特性,主要函数式编程和并发。相比下Java已经老态龙钟,步履躇跚。或许Scala不会成为替代Java语言,但的确给后来者设立了标杆。所以做了这个网站,顺着一个一个例子,由浅入深,由表及里。逐渐学会Scala,尽管不会因此成为一个熟练Scala的开发者,但是对函数式编程的也会相当了然。这篇文章精选了Scala-Tour上了一些章节,想快速了解的朋友可以看看这篇文章,当然想详细看就上上Scala-Tour吧。

不再需要Close

在Java里面,使用完资源(文件句柄,数据库连接)等之后,必须手动Close。否则发生泄漏后,程序只有被迫重启。Scala可以通过函数式实现自动close。

import scala.reflect.io.File
import java.util.Scanner

def withScanner(f: File, op: Scanner => Unit) = {
    val scanner = new Scanner(f.bufferedReader)
    try {
        op(scanner)
    } finally {
        scanner.close()
    }
}

withScanner(File("/proc/self/stat"),
    scanner => println("pid is " + scanner.next()))

这个例子是从/proc/self/stat文件中读取当前进程的pid。withScanner封装了try-finally块,所以调用者不用再close。

按名称传递参数

我们熟悉的参数传递方式是按值传递。按名称传递的方式,可以理解为直接传递参数名字,等到实际调用的时候,再去取值。在Java代码中,往往充斥着if(log.isDebug()){log.debug(…)}这样语句。之前的if调用是很有必要的,因为在之后的debug语句中往往有字符串拼接的操作。在不需要打Log的时候,字符串拼接也有可能发生异常抛出。而Scala可以通过按名称传递解决这个问题,这样就不再需要if(log.isDebug())这样的语句了。

val logEnable = false

def log(msg: => String) =
    if (logEnable) println(msg)

val MSG = "programing is running"

log(MSG + 1 / 0)

鸭子类型

“走起来像鸭子,叫起来像鸭子,就是鸭子。”这个例子中使用{ def close(): Unit }作为参数类型。因此任何含有close()的函数的类都可以作为参数。这样的做法比使用接口要好很多,因为可以不引入任何依赖。这个withClose方法单独编译,随处使用。

def withClose(closeAble: { def close(): Unit }, op: { def close(): Unit } => Unit) {
    try {
        op(closeAble)
    } finally {
        closeAble.close()
    }
}

class Connection {
    def close() = println("close Connection")
}

val conn: Connection = new Connection()
withClose(conn, conn =>
    println("do something with Connection"))

Trait

Traits就像是有函数体的Interface,使用with关键字来混入。单个Traits就像是一块乐高积木,一个插件。就像下面的JsonAble,当使用一个对象的时候,可以随时随地把它插在他上面。这个对接就具备了toJson的能力。不用创建一个类,或者写组合的代码,非常干脆。这样也可以使代码有很高的正交性。不再会为了一个很小的需求,去修改一个被广泛使用的类。

trait ForEachAble[A] {
  def iterator: java.util.Iterator[A]
  def foreach(f: A => Unit) = {
    val iter = iterator
    while (iter.hasNext)
      f(iter.next)
  }
}

trait JsonAble {
  def toJson() =
    scala.util.parsing.json.JSONFormat.defaultFormatter(this)
}

val list = new java.util.ArrayList[Int]() with ForEachAble[Int] 
list.add(1); list.add(2)

println("For each: "); list.foreach(x => println(x))
//println("Json: " + list.toJson())

函数式真正的威力

通过将函数作为参数,可以使程序极为简洁。 函数式除了能简化代码外,更重要的是他关注的是Input和Output,函数本身没有副作用。 就是Unix pipeline一样,简单的命令可以组合在一起。 List的filter方法接受一个过滤函数,返回一个新的List 如果你喜欢Unix pipeline的方式,你一定也会喜欢函数式编程。 这个例子是用函数式的代码模拟“cat file | grep ‘warn’ | grep ‘2013’ | wc”的行为。相比于Ruby等动态语言,这威力来自于科学而不是魔法

val file = List("warn 2013 msg", "warn 2012 msg", "error 2013 msg", "warn 2013 msg")

println("cat file | grep 'warn' | grep '2013' | wc : " 
    + file.filter(_.contains("warn")).filter(_.contains("2013")).size)

再见 NullException

每个Java程序员都被NullException折磨过。因为Java中每个对象都可能为Null,所以要么到处检查null的问题,要么到处try/cache。
Scala提供了Option机制来解决,代码中不断检查null的问题。这个例子包装了getProperty方法,使其返回一个Option。 这样就可以不再漫无目的地null检查。只要Option类型的值即可。使用pattern match来检查是常见做法。也可以使用getOrElse来提供当为None时的默认值。给力的是Option还可以看作是最大长度为1的List,List的强大功能都可以使用。
不是每个对象都可以为Null了,只有Option可以为None。这样的做法显示区分了可能为Null的情况,可以和NullException说再见了。

def getProperty(name: String): Option[String] = {
  val value = System.getProperty(name)
  if (value != null) Some(value) else None
}

val osName = getProperty("os.name")

osName match {
  case Some(value) => println(value)
  case _ => println("none")
}

println(osName.getOrElse("none"))

osName.foreach(print _)

并行集合

这个例子是访问若干URL。但确可以并行访问,比非并行的做法可以快一倍。要想让访问并行,只要调用List.par就可以了。

val urls = List("http://scala-lang.org",
  "https://github.com/yankay/scala-tour")

def fromURL(url: String) = scala.io.Source.fromURL(url)
  .getLines().mkString("\n")

val t = System.currentTimeMillis()
urls.par.map(fromURL(_))
println("time: " + (System.currentTimeMillis - t) + "ms")

是不是非常的简单?并行集合支持大部分集合的功能。不增加程序复杂性,却能大幅提高并发的能力。

远程Actor

Actor是并发模型,也使用于分布式。这个例子创建一个时间服务器,通过alive来监听TCP端口,register来注册自己。调用时通过select创建client。其余使用方式和普通Actor一样。
将单机并发和分布式抽象成一种模型。简化了程序复杂性。虽然多核编程并不广泛,但调用外部接口的情况越来越多。Actor模型非常适用于这样的异步环境。

import scala.actors.remote.RemoteActor._
import scala.actors.Actor._
import scala.actors.remote.Node

val port = 31241

val echoServer = actor {
  alive(port)
  register('echoServer, self)
  loop {
    react {
      case msg => {
        reply("replay " + msg)
      }
    }
  }
}

val timeServerClient = select(Node("127.0.0.1", port), 'echoServer)

timeServerClient !? "hi" match {
  case replay: String => println(replay)
}

抽取器

抽取器可以进行解构。这个例子是构建一个Email抽取器,只要实现unapply函数就可以了。
Scala的正则表达式会自带抽取器,可以抽取出一个List。List里的元素是匹配()里的表达式。
抽取器很有用,短短的例子里就有两处使用抽取器:

  • 通过 case user :: do main :: Nil 来解构List
  • 通过 case Email(user, domain) 来解构Email。
import scala.util.matching.Regex

object Email {
  def unapply(str: String) = new Regex("""(.*)@(.*)""")
    .unapplySeq(str).get match {
    case user :: domain :: Nil => Some(user, domain)
    case _ => None
  }
}

"user@domain.com" match {
  case Email(user, domain) => println(user + "@" + domain)
}

DSL

DSL是Scala最强大武器,可以使一些描述性代码变得极为简单。这个例子是使用DSL生成JSON。Scala很多看似是语言级的特性也是用DSL做到的。
自己编写DSL有点复杂,但使用起来非常方便。这样可以使Scala可以嵌入XML,嵌入Json,嵌入SQL。而其他语言中这些都只是字符串而已。

import org.json4s._
import org.json4s.JsonDSL._
import org.json4s.jackson.JsonMethods._
import java.util.Date

case class Twitter(id: Long, text: String, publishedAt: Option[java.util.Date])

var twitters = Twitter(1, "hello scala", Some(new Date())) ::
  Twitter(2, "I like scala tour", None) :: Nil

var json = ("twitters"
  -> twitters.map(
    t => ("id" -> t.id)
      ~ ("text" -> t.text)
      ~ ("published_at" -> t.publishedAt.toString())))

println(pretty(render(json)))

Simple Build Tool

SBT是Scala的最佳编译工具,在他的帮助下,你甚至不需要安装除JRE外的任何东西,来开发Scala。
例如你想在自己的机器上执行Scala-Tour,可以执行下面的命令

#Linux/Mac(compile & run):
git clone https://github.com/yankay/scala-tour-zh.git
cd scala-tour-zh
./sbt/sbt stage
./target/start

#Windows(can only compile):
git clone https://github.com/yankay/scala-tour-zh.git
cd scala-tour-zh
sbt\sbt stage

结语

这几个例子精选自Scala-Tour,这个网站中还有介绍了很多其他好的特性,比如模式匹配和隐式转换,就不逐一介绍了。这个项目Host在GitHub上,如果你也有精彩的用法的话,大家交流交流吧。

文章分类 每日心得, 软件技术 | 标签: Scala | 4 评论 |

NoSQL反模式 – 文档数据库篇

在 2013年1月25日 上公布 作者为 yankay

我们设计关系数据库Schema的都有一套完整的方案,而NoSQL却没有这些。半年前笔者读了本《SQL反模式》的书,觉得非常好。就开始留意,对于NoSQL是否也有反模式?好的反模式可以在我们设计Schema告诉哪里是陷阱和悬崖。NoSQL宣传的时候往往宣称是SchemaLess的,这会让人误解其不需要设计Schema。但如果不意识到设计Schema的必要,陷阱就在一直在黑暗中等着我们。这篇文章就总结一些别人的,也有自己犯过的深痛的设计Schema错误。

NoSQL数据库最主流的有文档数据库,列存数据库,键值数据库。三者分别有代表作MongoDB,HBase和Redis。如果将NoSQL比作兵器的话,可以这样(MySQL是典型的关系型数据库,一样参与比较):

  • MySQL产生年代较早,而且随着LAMP大潮得以成熟。尽管其没有什么大的改进,但是新兴的互联网使用的最多的数据库。就像传统的菜刀,结构简单,几百年没有改进。但是不妨碍产生各式各样的刀法,只要有一把,就能胜任厨房里的大部分事务。MySQL也是一样,核心已经稳定。但是切库,分表,备份,监控,等等手段一应俱全。
  • MongoDB是个新生事物,提供更灵活的Schema,Capped Collection,异步提交,地理位置索引等五花十色的功能。就像瑞士军刀,不但可以当刀用,还可以开瓶盖,剪指甲。但是他也不比MySQL强,因为还缺乏时间的磨砺。一是系统本身的稳定性,二是开发,运维需要更多经验才能流行。
  • HBase是个仗势欺人的大象兵。依仗着Hadoop的生态环境,可以有很好的扩展性。但是就像象兵一样,使用者需要养一头大象(Hadoop),才能驱使他。
  • Redis是键值存储的代表,功能最简单。提供随机数据存储。就像一根棒子一样,没有多余的构造。但是也正是因此,他的伸缩性特别好。就像悟空手里的金箍棒,大可捅破天,小能成缩成针。

文档数据库的得失

关系模型试图将数据库模型和数据库实现分开,让开发者可以脱离底层很好的操作数据。但笔者以为关系模型在一些应用场景下有弱点,现在已经不得不面对。

  • SQL弱点一:必须支持Join。因为数据不能够有重复。所以使用范式的关系模型会不可避免的大量Join。如果参与Join的是一张比内存小的表还好。但是如果大表Join或者表分布在多台机器上的话,Join就是性能的噩梦。
  • SQL弱点二:计算和存储耦合。关系模型作为统一的数据模型既可以用于数据分析,也可以用于在线业务。但这两者一个强调高吞吐,一个强调低延时,已经演化出完全不同的架构。用同一套模型来抽象显然是不合适的。Hadoop针对的就是计算的部分。MongoDB,Redis等针对在线业务。两者都抛弃了关系模型。

针对这两个梦魇。文档数据库如MongoDB的的主要目的是 提供更丰富的数据结构来抛弃Join来适应在线业务。当然也不是MongoDB完全不能用Join,不能拿来做数据分析,讨论这个只是见仁见智的问题。所以文档数据库并不比关系数据库强大,由于对Join的弱支持,功能会弱许多。设计关系模型的时候,通常只需要考虑好数据直接的关系,定义数据模型。而设计文档数据库模型的时候,还需要考虑应用如何使用。因此设计好一个的文档数据库Schema比设计关系模型更加的困难。除此之外,由于文档数据库事务的支持也是比较弱,一般NoSQL只会提供一个行锁。这也给设计Schema更加增加了难度。对于文档数据库的使用有很多需要注意的地方,本文只关注模型设计的部分。

反模式一:惯性思维/沿用关系模型

关系模型是数据存储的经典模型,使用数据模型范式的好处非常的明显。但是由于文档数据库不支持Join(包括和外键息息相关的外键约束)等特性,习惯性的沿用关系模型有的时候会出现问题。需要利用起文档数据库提供的丰富的数据模型来应对。

值得一提的是文档数据库的设计和关系模型不同,是灵活多样的。对于同一个情形,可以设计出有多种能够工作的模型,没有绝对意义上最好的模型。

下图是关系模型和文档模型的对比。

这个一个博客的数据模型,有Blog,User等表。左侧是关系模型,右侧是文档模型。这个文档模型并不是完全合理,可以作为“正反两面教材”在下文不断阐述。

问题一:存在描述多对多的关系表
症状:文档数据库中存储在有纯粹的关系表,例如:

id user_id blog_id
0 0 0
1 0 1

这样的表就算在关系模型中也是不妥的,因为这个ID非常的多余,可以用联合主键来解决。但是在文档数据库中,由于必须强制单主键,不得不采取这样的设计。

坏处:

  1. 破坏数据完备性。由于ID是主键,在数据模型上没有约束来保证不出现重复的user_id,blog_id对。一旦数据出现重复,更新删除都是问题。
  2. 索引过多。由于是关系表,必须在user_id和blog_id上面分别建一个索引。影响性能。

解决方案:
使用文档数据库典型的处理多对多的办法。不是建立一张关系表,而是在其中一个文档(如User)中,加入一个List字段。

user_id user_name blog_id[] ……
0 Jake 0,1 ……
1 Rose 1,2 ……

问题二:没有区分”一对多关系”和“多对一关系”
症状:关系模型不区分“一对多”和“多对一”,对于文档数据库来讲,关系模型只有“多对一”。就像这张Comment表:

comment_id user_id content ……
0 0 “NoSQL反模式是好文章” ……
1 0 “是啊” ……

如果整个模型都是这样的“多对一”关系就需要反思了。

坏处:

  1. 额外索引。如果客户端已知user_id,需要获得User信息和Comment信息,需要执行两次查询。其中一次查询需要使用索引。并且要在客户端自己Join。这样可能有潜在性能问题。

解决方案:
问题的核心在于是已知user_id查询两张表,还是已知comment_id查询两张表。如果是已知comment_id这样的设计就是合理的,但是如果是已知user_id来查询,把关系放在user表里的设计更合理一些。

user_id user_name comment_id[] ……
0 Jake 0,1 ……
1 Rose 1,2 ……

这样的设计,就可以避免一个索引。同理,对于多对多也是一样的,通过合理的安排字段的位置可以避免索引。

正确使用的场合:

关系型模型是非常成功的数据模型,合理的沿用是非常好的。但是由于文档数据库的特点,需要适当的调整,这样得出的数据模型,尽管性能不是最优,但是有最好的灵活性。并且也有利于和关系数据库转换。

反模式二:处处引用客户端Join

症状:数据库设计中充满了xx_id的字端,在查询的时候需要大量的手动Join操作。就涉及到了这个反模式。正如上面提到的博客的关系模型,如果已知blog_id查询comments,需要至少执行3次查询,并且手动Join。

坏处:

  1. 手动Join,麻烦且易出错。文档数据库不支持Join且没有外键保证。因此需要在客户端Join,这样的操作对于软件开发来讲是比较繁琐的。由于没有外键保证,因此不能保证取得的ID在数据库里面是有数据的。在处理的时候需要不断判断,容易出错。
  2. 多次查询。如果引用过多,查询的时候需要多次查询才能查到足够的数据。本来文档数据库是很快的,但是由于多次查询,给数据库增加了压力,获取全部数据的时间也会增加。
  3. 事务处理繁琐。文档数据库一般不支持一般意义上事务,只支持行锁。如果文档数据库有给多个连接。在插入的时候,事务的处理就是噩梦。在文档数据库中使用事务,需要使用行锁,在进行大量的处理。太过繁琐,感兴趣的读者可以搜一下。

解决方案:
适当使用内联数据结构。由于文档数据库支持更复杂的数据结构可以将引用转换为内联的数据,而不用新建一张表。这样做可以解决上面的一些问题,是一个推荐的方案。就像上面博客的例子一样。将五张表简化成了两张表。那什么时候使用内联呢?一般认为

  • 使用内联可以解决读性能问题,明显减少Query的次数的时候。
  • 可以简化数据模型,化简表之间的关系,而同时不会影响灵活性的时候。
  • 事务可以得到简化为单行事务的时候
正确使用的场合:

范式化的使用场景,文档数据库会被多个应用使用。由于数据库设计无法估计多个应用现在及将来的查询情况,需要极大的灵活性。在这个时候,使用引用比内联靠谱。

反模式三 滥用内联后患无穷

问题一 妨碍到查询的内联
症状:频繁查询一些内联字段,丢弃其他字段。

坏处:

  1. 无ID约束:使用内联字段和引用不同,是没有ID约束的。因此不能通过ID(主键)来管理,如果经常需要单独操作内联对象会非常不便。
  2. 索引泛滥:如果以内联字段为条件进行查询,需要建立索引。有可能造成索引泛滥。
  3. 性能浪费:大部分文档数据库的实现是按行存储的,也就意味着,尽管只查询一个字段,但是DB需要将整行从磁盘中取出。如果字段够小,文档够大,是很不合算的。

解决方案:
如果出现以上的症结,就可以考虑使用引用代替内联了。内联特性主要的用途在于提高性能,如果出现性能不升反降,那就没有意义了。如果对性能有很强烈的要求,可以考虑使用重复数据,同样的数据即在内联字段中也在引用的表里面。这样可以结合内联和引用的性能优势。缺点是数据出现重复,维护会比较麻烦。

问题二 无限膨胀的内联
症状:List,Map类型的内联字段不断膨胀,而且没有限制。就像前面提到的Blog的内联字段Comment。如果对每一篇Blog的Comment数量没有限制的话,Comment会无限膨胀。轻则影响性能,重则插入失败。

Blog_id content Comment[] ……
0 “…” “NoSQL反模式是好文章”, “是啊”,”无限增长中”… ……

坏处:

  1. 插入失败。文档数据库的每条记录都有最大大小,并且也有推荐最佳的大小。一般不会超过4M。就像刚刚提到的例子,如果是篇热门的博文的话,评论的大小很容易就超过4M。届时文档将无法更新,新的评论无法插入。
  2. 性能拖油瓶。由于内联字段膨胀,其大小将远远超过其他部分,影响其他部分的性能表现。并且因此导致该记录大小频繁变化,对档数据库的数据文件内部可能因此产生很多碎片。

解决方案:
设定最大数目或者使用引用。还是Blog和Comment的例子,可以将Comment从Blog中剥离出成一张表。如果考虑到性能,可以在Blog表中新建一个字段如最近的评论。这样既保证了性能,又能够预防膨胀。

Blog_id content last_five_comment[] ……
0 “…” “NoSQL反模式是好文章”, “是啊”,”最多5条”… ……

问题三 无法维护的内联
症状:DBA想单独维护内联字段,但无法做到。

坏处:

  1. 权限管理难。数据库的权限管理的最小粒度是表。如果使用内联技术,就意味着内联部分必须和其他字段用同一个权限来管理。没有办法在DB级别隐藏。
  2. 切表难。如果发现一张表的庞大需要切表。这个时候就比较纠结了。如果一刀切,partion Key的选择;索引的失效都会成为问题。如果觉得拆为两张表,就会很好操作的话,就是内联的过度使用了 。
  3. 备份难。关系数据库中每张表可以有不同的备份策略。但是如果内联起来,这样的备份就做不到了。
解决办法:
设计数据库模型的时候需要考量之后的维护操作,尤其是内联的字段需不需要单独的维护。需要和运维商量。如果对内联的字段有单独维护的要求,可以拆分出来作为引用。

问题四 盯死应用的内联
症状:应用可以非常好的运行在数据库上。但是当新的应用接入的时候会很麻烦。因为设计数据模型的时候考虑到了查询。所以当有新应用,新查询接入的时候,就会难于使用原有的模型。

坏处:

  1. 新应用接入难。当新的应用试图使用同一个数据库的时候,接入比较困难。因为查询时不同的,需要调整数据模型才能适应。但是调整模型又会影响原有应用。
  2. 集成难。不同的关系型数据库可以集成在一起,共同使用。但是对于文档数据库,虽然功能上可以互补,但是由于内联数据结构的差异,也比较难于集成。
  3. ETL难。现在大部分的数据分析系统使用的是关系模型,就连Hadoop虽然不用关系模型,但是其上的Hive的常用工具也是按关系模型设计的。

解决方案:

使用范式设计数据库,即用引用代替内联。或者在使用内联的时候,给每个内联对象一个全局唯一的Key,保证其和关系模型直接可以存在映射关系,这样可以提高数据模型的灵活性。如Blog表:

Blog_id content Comment[] ……
0 “…” [{“id”=1,”content”=“NoSQL反模式是好文章”}, {“id”=2,”content”=“是啊”}…] ……
这样的设计既可以利用到内联的好处,又能将其和关系模型映射起来。确定是需要手动维护comment_id,保证其全局唯一性。

反模式四:在线计算

症状:有一些运行时间很长的Query,由于有聚合计算,索引也不能解决。随着数据量的增长,逐渐成为性能瓶颈。

坏处:

  1. 影响用户体验。在线业务中,如果一个查询大于4s,用户体验会急剧下降。按主键和按索引的查询都能满足要求。但是聚合操作往往需要扫描全表或者大量的数据,随着数据量的增加,查询时间会变长,用户不可容忍。
  2. 影响数据库性能。长查询的坏处数不清。在线上应用中,如果出现长查询,可能会霸占数据的大部分资源,包括IO,连接,CPU等等。导致其他很好的查询,轻则性能也下降,重者无法使用数据库。长查询可以称之为DB杀手。

解决方案:
首先要权衡,这个聚合操作是不是必要的,必须实时完成。如果没有必要实时完成的话,可以采取离线操作的方案。在夜深人静的时候,跑一个长查询,将结果缓存起来,给第二天使用。如果必须实时完成,则可以新建一个字段,用“incr”这样的操作,在运行的时候,实时聚合结果。而不是查询的时候执行一次长查询。如果逻辑比较复杂,或者觉得大量“incr”操作给数据库系统带来了压力,可以使用Storm之类的实时数据处理框架。总之,要慎用长查询。

反模式五:把内联Map对象的Key当作ID用

症状:文档数据库支持内联Map类型。将其中Map的Key当作数据库的主键来用。

Blog_id content Comment{} ……
0 “…” {“1″=“NoSQL反模式是好文章”, “2”=“是啊”} ……

这个反模式很容易犯,因为在编程语言中Map数据结构就是这么用的。但是对于数据库模型来说,这是不折不扣的反模式。

坏处:

  1. 无法通过数据库做各种(><=)查询。对于关系型数据库来说,虽然数据结构可以很灵活,但查询的时候都是按层次的。比如comment.id,comment.content。也就是说其Map类型中的Key可以理解为属性名的,而不是用作ID。因此一旦这样使用,就脱离的数据库管制,无法使用各种查询功能。
  2. 无法通过索引查询。文档数据可建立索引是需要列名的。比如comment.id。而这样的数据结构没有固定的列名,因此无法建立索引。

解决方案:
使用数组+Map来解决。如:

Blog_id content Comment[] ……
0 “…” [{“id”=1,”content”=“NoSQL反模式是好文章”}, {“id”=2,”content”=“是啊”}…] ……
这样,就可以使用comment.id作为索引,也可以使用数据库的查询功能。简单有效。Map类型中的Key是属性名,Value是属性值。这样的用法是文档数据库数据模型的本意,因此其提供的各种功能才能利用上。否则就无法使用。

反模式六:不合理的ID

症状:使用String甚至更复杂数据结构作为的ID,或者全部使用数据库提供的自生成ID。如:

id(该ID系系统自生成) Blog_id content ……
0 0 … ……

坏处:

  1. ID混乱。如果使用数据库提供的自生成ID,同时表中还有一个类似有主键含义的Blog_id,这样很不好,容易造成逻辑混乱。由于文档数据库不支持ID的重命名,习惯关系数据库做法的人可能会再建立一个自己的逻辑ID字段。这是没有必要的。
  2. 索引庞大,性能低下。ID是数据库的非常重要的部分。ID的长度将决定索引(包括主键的索引)的大小,直接影响到数据库性能。如果索引比内存小,性能会很好。但一旦索引大小超过内存,出现数据交换,性能会急剧下降。一个Long占8字节,一个20个字符的UTF8 String占用约60个字节。相差10倍之巨,不能不考虑。

解决方案:
尽量使用有一定意义的字段做ID,并且不在其他字段中重复出现。不使用复杂的数据类型做ID,只使用int,long或者系统提供的主键类型做ID。

文档数据库的反模式总结

阐述了这么多的反模式,下面有个一览表,涵盖了上面所有的反模式。这个一览表,是按照文档数据库模型建立的。是个文档数据库模型的例子。

ID 反模式名 问题
0 存在描述多对多的关系表 [{ID:00
症状:文档数据库中存储在有纯粹的关系表
坏处:[破坏数据完备性,索引过多]
解决方案:加入一个List字段
},{
ID:01
症状:关系模型不区分“一对多”和“多对一”
坏处:额外索引
解决方案:合理的安排字段的位置
}]
1 处处引用客户端Join [{
ID:10
症状:查询的时候需要大量的手动Join操作
坏处:[手动Join,多次查询, 事务处理繁琐]
解决方案:适当使用内联数据结构。
}]
2 滥用内联后患无穷 [{
ID:20
症状:频繁查询一些内联字段,丢弃其他字段
坏处:[无ID约束,索引泛滥, 性能浪费]
解决方案:使用引用代替内联了,允许重复数据
},{
ID:21
症状:List,Map类型的内联字段不断膨胀,而且没有限制
坏处:[插入失败, 性能拖油瓶]
解决方案:设定最大数目或者使用引用。
},{
ID:22
症状:DBA想单独维护内联字段,但无法做到
坏处:[权限管理难, 切表难, 备份难]
解决方案:设计数据库模型的时候需要考量之后的维护操作
},{
ID:23
症状:应用可以非常好的运行在数据库上。但是当新的应用接入的时候会很麻烦。内联盯死了应用
坏处:[新应用接入难, 集成难, ETL难]
解决方案:使用范式设计数据库,即用引用代替内联。保证其和关系模型直接可以存在映射关系
}]
3 在线计算 [{
ID:30
症状:有一些运行时间很长的Query, 逐渐成为性能瓶颈。
坏处:[影响用户体验,影响数据库性能]
解决方案:取消不必要的聚合操作. 运行的时候,实时聚合结果.使用第三方实时或非实时工具。如Hadoop,Storm.
}]
4 把内联Map对象的Key当作ID用 [{
ID:40
症状:文档数据库支持内联Map类型。将其中Map的Key当作数据库的主键来用。
坏处:[无法通过数据库做各种(><“”” =)查询,无法通过索引查询]
解决方案:使用数组+Map来解决。
}]
5 不合理的ID [{
ID:50
症状:用String甚至更复杂数据结构作为的ID,或者全部使用数据库提供的自生成ID。
坏处:[ID混乱,索引庞大]
解决方案:尽量使用有一定意义的字段做ID。不使用复杂的数据类型做ID。
}]

本文试图总结了笔者知道的重要的文档数据库的反模式。现在关于NoSQL数据模型设计模式的讨论才刚刚起步,将来也许会逐渐自成体系。对于列数据库和Key-Value的反模式,笔者等到有了足够积累的时候,再和大家分享。

文章分类 每日心得 | 标签: NoSQL, 反模式, 文档数据库 | 5 评论 |

2012年学习小结

在 2013年1月3日 上公布 作者为 yankay

一转身就到2013了。读者新年快乐,希望吃到饺子的人都幸福。
新年伊始,笔者在想做点什么之前,还是回顾下去年做了些什么,好歹有个交代。记录下学习的轨迹,分享给您还有未来的自己。

去年我小半年在盛大游戏,业务较少,尸位素餐,没做什么事情。大半年在EMC中国研究院,虽然现在有些小忙,但之前适应项目和团队花了好久。因此有大把大把的时间来学习。这是我的学习黄金年,读了60读本书,看了不少开源项目如Kalfa,CloudFoundry的源代码。学到了不少的知识,真开心。

学知识的话,以前比较喜欢上网,现在比较喜欢看书。网络文字很多比较概略,散乱。而一本好书往往倾尽一个人的相关专长上全部才能。读读书,省时省力。

系统编程

今年年初前,我做的程序的Bug大多集中在系统和网络部分,再加上需要维护改进一个C++的模块。所以我先好好学习了下系统编程,网络,和C++方面的知识。虽然都是大骨头,但是因为时间充裕,学的也扎实。

笔者以前学过一些系统编程,但希望了解得更深入一些。所以先读了的《深入理解计算机系统》,这本书里“存储器山”的概念让我茅舍顿开。教授先生别出心裁的将存储器的吞吐量,画成了一座山。并自己做了些测试,写了《内存究竟有多快?》一文。如果细细在山上品,可以考虑如何最好的使用存储器。然后看了些UNIX编程的内容,包括系统提供API和他们的脾气。还研究了下如何在Unix里面用GDB之类的工具Debug,查看程序状态。

网络

网络是非常重要的基础知识。做事情的时候,就怕网络操作,因为总是会遇到奇怪的问题。躲避不是办法,所以细细学了学。《计算机网络》这部书很好,因为重版了,所以再读了读。主要是新增的无线网络的篇幅。笔者意外的发现WIFI的频率和微波炉同样是2.4GHz。所以萌生了用手机检测微博路泄漏的办法。并且设想了“微波炉安全专家”的手机游戏。由于这个游戏实在太冷笑话了, 所以一直没实现。:-)网络也越来越注重安全性,对密码学的学习也是要不遗余力地。

编程风格

编程风格是个说不清道不明的东西。笔者一直很喜爱Linux。所以学了学Linux/Unix的编程风格。下面这三本书都是讲这个。虽然书名都很哗众取宠,但内容不错。有趣是最哗众取宠的《程序员修炼之道-从小工到专家》,将Linux的思想定义为“注重实效”。给了笔者当头一棒,其实编程风格什么的,就是不自己玩自己。做产品应该用最经济的,做学问则要是最深刻的。

Web技术

互联网技术是笔者感兴趣的传统方向。这方面的学习不是靠读书为主,本年度读了两本。一本书作者是腾讯的“郭欣”,另一本是日本某最大互联网企业的工程师。笔者是个哈日青年,但是不得不说,在日本人的互联网技术,怕是有点落后。很多Web重要问题,比如MySQL,缓存浅尝辄止,甚至只字未提监控。国人果然很强,面面俱到,鞭辟入里。

并发

并发也是笔者最感兴趣的方向,笔者深入学了学多核的原理。就是API的里锁和原子操作,如何映射成CPU的指令,并且CPU是如何执行这些指令的。很多问题,茅塞顿开。后来写了篇文章《并发编程之巧用锁》。经过不少知识积累后,觉得并发不是很简单。多核并发本就是为了提升性能,但以提升性能为目标写并发程序并不简单。自己不应该轻易操作底层并发元素,或者不使用无谓的并发。如果要用一下,估计得深思熟虑好几天才行。

数据库

盛大游戏的”系统平台部”运维了大量的MySQL。笔者在里头耳濡目染,也懂了一些。当然看书也是学习MySQL的一部分。尤其推荐简朝阳的这部,有很多阿里巴巴自身的实践精髓在里头。国人还是很好很强大。《SQL反模式》是我最喜爱的图书,薄薄一本小册子,讲设计数据库Schema的时候容易犯的错误。尽管自认为对数据库颇有见解,但是发现几乎每一反模式自己都碰到过,倍受打击。从此逢人就推荐这本《SQL反模式》。看过的人都表示大赞。

硬件存储

笔者刚到EMC后对SAN,FCoE等等存储相关的知识还一无所知。如果过了段时间还不懂,岂不让人笑掉大牙。所以花了些时间系统的科普下存储相关的知识。再得易于公司内部环境,还有培训授课。笔者对存储算是初窥门径了。

计算机语言

C++

不怕笑话,笔者对技术有洁癖,对C++有点排斥,所以不算精通。但工作需要,总归还是得认真看看的。但读了读《Effective C++》后,爱不释手,又反复读了两遍。尽管C++提供了那么多乱七八糟的语言级工具,有很多陷阱,用好他们不容易,但如果循规蹈矩的使用,还是很顺手的。《Effective C++》就是给我提供这些循规蹈矩的方法,让人可以舒服点。然后我迫不及待的读了《More Effective C++》感觉大不如前作实用,作者大有江郎才尽的感觉。不过还是挺不错的。

Ruby

处于对CloudFoundry项目的需要,笔者也看了看Ruby。尽管Ruby很流行,很多人在使用。可我感觉他就像混水一般,不干净。由于语言本身比较动态,对于一行代码,会怎样执行,完全不清楚。不过平心而论,还是挺好用的。去年松本来上海,笔者也去朝圣,他一直穿着这套脏兮兮的黄色西装,推销自己的手机Ruby。我突然觉得东方人弄出个玩意不容易,脏就脏点,干净不过人家(西方人),还邋遢不过吗。

GO

GO是Google的新语言。对Go的学习是十分愉快。笔者往往看这方面的资料到凌晨二三点忘了睡眠。抛弃简介的语法不谈,Go的设计考虑到性能要求。很多动态性在编译里解决,执行的代码很大程度上是静态的。和C风格很像,简单舒服。并且并发也做得不错。感觉假以时日,他会对性能敏感的系统编程领域造成冲击。笔者总结了Go语言协程的编程模型,记录下一文《Go简洁的并发》。另外许式伟的的这部书也是不错的读物。

数据挖掘

工作需要,笔者接触到了”大数据”。决心下大力气学学数据挖掘和机器学习方面的知识。刚好有本斯坦福的《大数据》的书,这本书很好,根据现实中事例切入,深入浅出。随即倒腾了几个算法。准备在几日后推出一个基于监督学习的”八卦机器人游戏”,敬请期待。

手机

工作上的需要,笔者稍微看了看Android系统的运作方式。只求略知一二。

非计算机类

这篇小结本意不包括非计算机类的东西。不过既然是个人小结,也写上一些。陶推荐的《经济学思维方式是》是本很好的经济学入门图书,告诉我边际成本是什么。《苏菲的世界》是本非常非常好的哲学入门图书,笛卡尔“我思故我在”的思考振聋发聩。《爱你就像爱生命》是王晓波写的情书,写情书的人也是得学习学习的。

这篇学习小结意外的长,本以为只有一点点的。通过写他,笔者也顺便温习了一些。不过去年学的东西虽然多,但生产的优秀产品比较少。希望来年改进改进。

文章分类 软件技术 | 标签: 2012, 小结 | 8 评论 |

Go-简洁的并发

在 2012年11月28日 上公布 作者为 yankay

多核处理器越来越普及。有没有一种简单的办法,能够让我们写的软件释放多核的威力?是有的。随着Golang, Erlang, Scala等为并发设计的程序语言的兴起,新的并发模式逐渐清晰。正如过程式编程和面向对象一样,一个好的编程模式有一个极其简洁的内核,还有在此之上丰富的外延。可以解决现实世界中各种各样的问题。本文以GO语言为例,解释其中内核、外延。

并发模式之内核

这种并发模式的内核只需要协程和通道就够了。协程负责执行代码,通道负责在协程之间传递事件。

不久前,并发编程是个非常困难的事。要想编写一个良好的并发程序,我们不得不了解线程,锁,semaphore,barrier甚至CPU更新高速缓存的方式,而且他们个个都有怪脾气,处处是陷阱。笔者除非万不得以,决不会自己操作这些底层并发元素。一个简洁的并发模式不需要这些复杂的底层元素,协程和通道就够了。

协程是轻量级的线程。在过程式编程中,当调用一个过程的时候,需要等待其执行完才返回。而调用一个协程的时候,不需要等待其执行完,会立即返回。协程十分轻量,Go语言可以在一个进程中执行有数以十万计的协程,依旧保持高性能。而对于普通的平台,一个进程有数千个线程,其CPU会忙于上下文切换,性能急剧下降。随意创建线程可不是一个好主意,但是我们可以大量使用的协程。

通道是协程之间的数据传输通道。通道可以在众多的协程之间传递数据,具体可以值也可以是个引用。通道有两种使用方式。

  • 协程可以试图向通道放入数据,如果通道满了,会挂起协程,直到通道可以为他放入数据为止。
  • 协程可以试图向通道索取数据,如果通道没有数据,会挂起协程,直到通道返回数据为止。

如此,通道就可以在传递数据的同时,控制协程的运行。有点像事件驱动,也有点像阻塞队列。

这两个概念非常的简单,各个语言平台都会有相应的实现。在Java和C上也各有库可以实现两者。

Golang Erlang Scala(Actor)
协程 goroutines process actor
消息队列 channel mailbox channel

只要有协程和通道,就可以优雅的解决并发的问题。不必使用其他和并发有关的概念。那如何用这两把利刃解决各式各样的实际问题呢?

并发模式之外延

协程相较于线程,可以大量创建。打开这扇门,我们拓展出新的用法,可以做生成器,可以让函数返回“服务”,可以让循环并发执行,还能共享变量。但是出现新的用法的同时,也带来了新的棘手问题,协程也会泄漏,不恰当的使用会影响性能。下面会逐一介绍各种用法和问题。演示的代码用GO语言写成,因为其简洁明了,而且支持全部功能。

生成器

有的时候,我们需要有一个函数能不断生成数据。比方说这个函数可以读文件,读网络,生成自增长序列,生成随机数。这些行为的特点就是,函数的已知一些变量,如文件路径。然后不断调用,返回新的数据。

下面生成随机数为例, 以让我们做一个会并发执行的随机数生成器。

非并发的做法是这样的:

// 函数 rand_generator_1 ,返回 int
func rand_generator_1() int {
	return rand.Int()
}

上面是一个函数,返回一个int。假如rand.Int()这个函数调用需要很长时间等待,那该函数的调用者也会因此而挂起。所以我们可以创建一个协程,专门执行rand.Int()。

// 函数 rand_generator_2,返回 通道(Channel)
func rand_generator_2() chan int {
	// 创建通道
	out := make(chan int)
	// 创建协程
	go func() {
		for {
			//向通道内写入数据,如果无人读取会等待
			out <- rand.Int()
		}
	}()
	return out
}

func main() {
	// 生成随机数作为一个服务
	rand_service_handler := rand_generator_2()
	// 从服务中读取随机数并打印
	fmt.Printf("%dn", <-rand_service_handler)
}

上面的这段函数就可以并发执行了rand.Int()。有一点值得注意到函数的返回可以理解为一个“服务”。但我们需要获取随机数据 时候,可以随时向这个服务取用,他已经为我们准备好了相应的数据,无需等待,随要随到。如果我们调用这个服务不是很频繁,一个协程足够满足我们的需求了。但如果我们需要大量访问,怎么办?我们可以用下面介绍的多路复用技术,启动若干生成器,再将其整合成一个大的服务。

调用生成器,可以返回一个“服务”。可以用在持续获取数据的场合。用途很广泛,读取数据,生成ID,甚至定时器。这是一种非常简洁的思路,将程序并发化。

多路复用

多路复用是让一次处理多个队列的技术。Apache使用处理每个连接都需要一个进程,所以其并发性能不是很好。而Nighx使用多路复用的技术,让一个进程处理多个连接,所以并发性能比较好。同样,在协程的场合,多路复用也是需要的,但又有所不同。多路复用可以将若干个相似的小服务整合成一个大服务。

那么让我们用多路复用技术做一个更高并发的随机数生成器吧。

// 函数 rand_generator_3 ,返回通道(Channel)
func rand_generator_3() chan int {
	// 创建两个随机数生成器服务
	rand_generator_1 := rand_generator_2()
	rand_generator_2 := rand_generator_2()

	//创建通道
	out := make(chan int)

	//创建协程
	go func() {
		for {
			//读取生成器1中的数据,整合
			out <- <-rand_generator_1
		}
	}()
	go func() {
		for {
			//读取生成器2中的数据,整合
			out <- <-rand_generator_2
		}
	}()
	return out
}

上面是使用了多路复用技术的高并发版的随机数生成器。通过整合两个随机数生成器,这个版本的能力是刚才的两倍。虽然协程可以大量创建,但是众多协程还是会争抢输出的通道。Go语言提供了Select关键字来解决,各家也有各家窍门。加大输出通道的缓冲大小是个通用的解决方法。

多路复用技术可以用来整合多个通道。提升性能和操作的便捷。配合其他的模式使用有很大的威力。

Furture技术

Furture是一个很有用的技术,我们常常使用Furture来操作线程。我们可以在使用线程的时候,可以创建一个线程,返回Furture,之后可以通过它等待结果。 但是在协程环境下的Furtue可以更加彻底,输入参数同样可以是Furture的。

调用一个函数的时候,往往是参数已经准备好了。调用协程的时候也同样如此。但是如果我们将传入的参数设为通道,这样我们就可以在不准备好参数的情况下调用函数。这样的设计可以提供很大的自由度和并发度。函数调用和函数参数准备这两个过程可以完全解耦。下面举一个用该技术访问数据库的例子。

//一个查询结构体
type query struct {
	//参数Channel
	sql chan string
	//结果Channel
	result chan string
}

//执行Query
func execQuery(q query) {
	//启动协程
	go func() {
		//获取输入
		sql := <-q.sql
		//访问数据库,输出结果通道
		q.result <- "get " + sql
	}()

}

func main() {
	//初始化Query
	q :=
		query{make(chan string, 1), make(chan string, 1)}
	//执行Query,注意执行的时候无需准备参数
	execQuery(q)

	//准备参数
	q.sql <- "select * from table"
	//获取结果
	fmt.Println(<-q.result)
}

上面利用Furture技术,不单让结果在Furture获得,参数也是在Furture获取。准备好参数后,自动执行。Furture和生成器的区别在于,Furture返回一个结果,而生成器可以重复调用。还有一个值得注意的地方,就是将参数Channel和结果Channel定义在一个结构体里面作为参数,而不是返回结果Channel。这样做可以增加聚合度,好处就是可以和多路复用技术结合起来使用。

Furture技术可以和各个其他技术组合起来用。可以通过多路复用技术,监听多个结果Channel,当有结果后,自动返回。也可以和生成器组合使用,生成器不断生产数据,Furture技术逐个处理数据。Furture技术自身还可以首尾相连,形成一个并发的pipe filter。这个pipe filter可以用于读写数据流,操作数据流。

Future是一个非常强大的技术手段。可以在调用的时候不关心数据是否准备好,返回值是否计算好的问题。让程序中的组件在准备好数据的时候自动跑起来。

并发循环

循环往往是性能上的热点。如果性能瓶颈出现在CPU上的话,那么九成可能性热点是在一个循环体内部。所以如果能让循环体并发执行,那么性能就会提高很多。

要并发循环很简单,只有在每个循环体内部启动协程。协程作为循环体可以并发执行。调用启动前设置一个计数器,每一个循环体执行完毕就在计数器上加一个元素,调用完成后通过监听计数器等待循环协程全部完成。

//建立计数器
sem := make(chan int, N); 
//FOR循环体
for i,xi := range data {
	//建立协程
    go func (i int, xi float) {
        doSomething(i,xi);
		//计数
        sem <- 0;
    } (i, xi);
}
// 等待循环结束
for i := 0; i < N; ++i { <-sem }

上面是一个并发循环例子。通过计数器来等待循环全部完成。如果结合上面提到的Future技术的话,则不必等待。可以等到真正需要的结果的地方,再去检查数据是否完成。

通过并发循环可以提供性能,利用多核,解决CPU热点。正因为协程可以大量创建,才能在循环体中如此使用,如果是使用线程的话,就需要引入线程池之类的东西,防止创建过多线程,而协程则简单的多。

Chain Filter技术

前面提到了Future技术首尾相连,可以形成一个并发的pipe filter。这种方式可以做很多事情,如果每个Filter都由同一个函数组成,还可以有一种简单的办法把他们连起来。

由于每个Filter协程都可以并发运行,这样的结构非常有利于多核环境。下面是一个例子,用这种模式来产生素数。

// A concurrent prime sieve

package main

// Send the sequence 2, 3, 4, ... to channel 'ch'.
func Generate(ch chan<- int) {
	for i := 2; ; i++ {
		ch <- i // Send 'i' to channel 'ch'.
	}
}

// Copy the values from channel 'in' to channel 'out',
// removing those divisible by 'prime'.
func Filter(in <-chan int, out chan<- int, prime int) {
	for {
		i := <-in // Receive value from 'in'.
		if i%prime != 0 {
			out <- i // Send 'i' to 'out'.
		}
	}
}

// The prime sieve: Daisy-chain Filter processes.
func main() {
	ch := make(chan int) // Create a new channel.
	go Generate(ch)      // Launch Generate goroutine.
	for i := 0; i < 10; i++ {
		prime := <-ch
		print(prime, "n")
		ch1 := make(chan int)
		go Filter(ch, ch1, prime)
		ch = ch1
	}
}

上面的程序创建了10个Filter,每个分别过滤一个素数,所以可以输出前10个素数。

Chain-Filter通过简单的代码创建并发的过滤器链。这种办法还有一个好处,就是每个通道只有两个协程会访问,就不会有激烈的竞争,性能会比较好。

共享变量

 

协程之间的通信只能够通过通道。但是我们习惯于共享变量,而且很多时候使用共享变量能让代码更简洁。比如一个Server有两个状态开和关。其他仅仅希望获取或改变其状态,那又该如何做呢。可以将这个变量至于0通道中,并使用一个协程来维护。

下面的例子描述如何用这个方式,实现一个共享变量。

//共享变量有一个读通道和一个写通道组成
type sharded_var struct {
	reader chan int
	writer chan int
}

//共享变量维护协程
func sharded_var_whachdog(v sharded_var) {
	go func() {
		//初始值
		var value int = 0
		for {
			//监听读写通道,完成服务
			select {
			case value = <-v.writer:
			case v.reader <- value:
			}
		}
	}()
}

func main() {
	//初始化,并开始维护协程
	v := sharded_var{make(chan int), make(chan int)}
	sharded_var_whachdog(v)

	//读取初始值
	fmt.Println(<-v.reader)
	//写入一个值
	v.writer <- 1
	//读取新写入的值
	fmt.Println(<-v.reader)
}

这样,就可以在协程和通道的基础上实现一个协程安全的共享变量了。定义一个写通道,需要更新变量的时候,往里写新的值。再定义一个读通道,需要读的时候,从里面读。通过一个单独的协程来维护这两个通道。保证数据的一致性。

一般来说,协程之间不推荐使用共享变量来交互,但是按照这个办法,在一些场合,使用共享变量也是可取的。很多平台上有较为原生的共享变量支持,到底用那种实现比较好,就见仁见智了。另外利用协程和通道,可以还实现各种常见的并发数据结构,如锁等等,就不一一赘述。

协程泄漏

协程和内存一样,是系统的资源。对于内存,有自动垃圾回收。但是对于协程,没有相应的回收机制。会不会若干年后,协程普及了,协程泄漏和内存泄漏一样成为程序员永远的痛呢?一般而言,协程执行结束后就会销毁。协程也会占用内存,如果发生协程泄漏,影响和内存泄漏一样严重。轻则拖慢程序,重则压垮机器。

C和C++都是没有自动内存回收的程序设计语言,但只要有良好的编程习惯,就能解决规避问题。对于协程是一样的,只要有好习惯就可以了。

只有两种情况会导致协程无法结束。一种情况是协程想从一个通道读数据,但无人往这个通道写入数据,或许这个通道已经被遗忘了。还有一种情况是程想往一个通道写数据,可是由于无人监听这个通道,该协程将永远无法向下执行。下面分别讨论如何避免这两种情况。

对于协程想从一个通道读数据,但无人往这个通道写入数据这种情况。解决的办法很简单,加入超时机制。对于有不确定会不会返回的情况,必须加入超时,避免出现永久等待。另外不一定要使用定时器才能终止协程。也可以对外暴露一个退出提醒通道。任何其他协程都可以通过该通道来提醒这个协程终止。

对于协程想往一个通道写数据,但通道阻塞无法写入这种情况。解决的办法也很简单,就是给通道加缓冲。但前提是这个通道只会接收到固定数目的写入。比方说,已知一个通道最多只会接收N次数据,那么就将这个通道的缓冲设置为N。那么该通道将永远不会堵塞,协程自然也不会泄漏。也可以将其缓冲设置为无限,不过这样就要承担内存泄漏的风险了。等协程执行完毕后,这部分通道内存将会失去引用,会被自动垃圾回收掉。

func never_leak(ch chan int) {
	//初始化timeout,缓冲为1
	timeout := make(chan bool, 1)
	//启动timeout协程,由于缓存为1,不可能泄露
	go func() {
		time.Sleep(1 * time.Second)
		timeout <- true
	}()
	//监听通道,由于设有超时,不可能泄露
	select {
	case <-ch:
		// a read from ch has occurred
	case <-timeout:
		// the read from ch has timed out
	}
}

上面是个避免泄漏例子。使用超时避免读堵塞,使用缓冲避免写堵塞。

和内存里面的对象一样,对于长期存在的协程,我们不用担心泄漏问题。一是长期存在,二是数量较少。要警惕的只有那些被临时创建的协程,这些协程数量大且生命周期短,往往是在循环中创建的,要应用前面提到的办法,避免泄漏发生。协程也是把双刃剑,如果出问题,不但没能提高程序性能,反而会让程序崩溃。但就像内存一样,同样有泄漏的风险,但越用越溜了。

并发模式之实现

在并发编程大行其道的今天,对协程和通道的支持成为各个平台比不可少的一部分。虽然各家有各家的叫法,但都能满足协程的基本要求—并发执行和可大量创建。笔者对他们的实现方式总结了一下。

下面列举一些已经支持协程的常见的语言和平台。

语言/平台 实现时间 协程名称 备注 GoLang 原生支持 goroutines Erlang 原生支持 process 函数式语言 Scala 原生支持 actor 函数式编程 Python 2.5版本后 coroutine 官方Python不完全实现
Stackless Python支持 Perl 6.0版本后 coroutine Ruby 1.9 版本后 fiber Lua 原生支持 coroutine C# .net 2.0版本后 fiber

GoLang 和Scala作为最新的语言,一出生就有完善的基于协程并发功能。Erlang最为老资格的并发编程语言,返老还童。其他二线语言则几乎全部在新的版本中加入了协程。

令人惊奇的是C/C++和Java这三个世界上最主流的平台没有在对协程提供语言级别的原生支持。他们都背负着厚重的历史,无法改变,也无需改变。但他们还有其他的办法使用协程。

Java平台有很多方法实现协程:

  • 修改虚拟机:对JVM打补丁来实现协程,这样的实现效果好,但是失去了跨平台的好处
  • 修改字节码:在编译完成后增强字节码,或者使用新的JVM语言。稍稍增加了编译的难度。
  • 使用JNI:在Jar包中使用JNI,这样易于使用,但是不能跨平台。
  • 使用线程模拟协程:使协程重量级,完全依赖JVM的线程实现。

其中修改字节码的方式比较常见。因为这样的实现办法,可以平衡性能和移植性。最具代表性的JVM语言Scala就能很好的支持协程并发。流行的Java Actor模型类库akka也是用修改字节码的方式实现的协程。

对于C语言,协程和线程一样。可以使用各种各样的系统调用来实现。协程作为一个比较高级的概念,实现方式实在太多,就不讨论了。比较主流的实现有libpcl, coro,lthread等等。

对于C++,有Boost实现,还有一些其他开源库。还有一门名为μC++语言,在C++基础上提供了并发扩展。

可见这种编程模型在众多的语言平台中已经得到了广泛的支持,不再小众。如果想使用的话,随时可以加到自己的工具箱中。

结语

本文探讨了一个极其简洁的并发模型。在只有协程和通道这两个基本元件的情况下。可以提供丰富的功能,解决形形色色实际问题。而且这个模型已经被广泛的实现,成为潮流。相信这种并发模型的功能远远不及此,一定也会有更多更简洁的用法出现。或许未来CPU核心数目将和人脑神经元数目一样多,到那个时候,我们又要重新思考并发模型了。

文章分类 软件技术 | 标签: Golang, 并发, 模式 | 10 评论 |

Google Spanner原理- 全球级的分布式数据库

在 2012年9月18日 上公布 作者为 yankay
Google Spanner简介

Spanner 是Google的全球级的分布式数据库 (Globally-Distributed Database) 。Spanner的扩展性达到了令人咋舌的全球级,可以扩展到数百万的机器,数已百计的数据中心,上万亿的行。更给力的是,除了夸张的扩展性之外,他还能同时通过同步复制和多版本来满足外部一致性,可用性也是很好的。冲破CAP的枷锁,在三者之间完美平衡。

Spanner是个可扩展,多版本,全球分布式还支持同步复制的数据库。他是Google的第一个可以全球扩展并且支持外部一致的事务。Spanner能做到这些,离不开一个用GPS和原子钟实现的时间API。这个API能将数据中心之间的时间同步精确到10ms以内。因此有几个给力的功能:无锁读事务,原子schema修改,读历史数据无block。

EMC中国研究院实时紧盯业界动态,Google最近发布的一篇论文《Spanner: Google’s Globally-Distributed Database》, 笔者非常感兴趣,对Spanner进行了一些调研,并在这里分享。由于Spanner并不是开源产品,笔者的知识主要来源于Google的公开资料,通过现有公开资料仅仅只能窥得Spanner的沧海一粟,Spanner背后还依赖有大量Google的专有技术。研究院原文。

下文主要是Spanner的背景,设计和并发控制。

Spanner背景

要搞清楚Spanner原理,先得了解Spanner在Google的定位。

从上图可以看到。Spanner位于F1和GFS之间,承上启下。所以先提一提F1和GFS。

F1

和众多互联网公司一样,在早期Google大量使用了Mysql。Mysql是单机的,可以用Master-Slave来容错,分区来扩展。但是需要大量的手工运维工作,有很多的限制。因此Google开发了一个可容错可扩展的RDBMS——F1。和一般的分布式数据库不同,F1对应RDMS应有的功能,毫不妥协。起初F1是基于Mysql的,不过会逐渐迁移到Spannerr。

F1有如下特点:

  • 7×24高可用。哪怕某一个数据中心停止运转,仍然可用。
  • 可以同时提供强一致性和弱一致。
  • 可扩展
  • 支持SQL
  • 事务提交延迟50-100ms,读延迟5-10ms,高吞吐

众所周知Google BigTable是重要的Nosql产品,提供很好的扩展性,开源世界有HBase与之对应。为什么Google还需要F1,而不是都使用BigTable呢?因为BigTable提供的最终一致性,一些需要事务级别的应用无法使用。同时BigTable还是NoSql,而大量的应用场景需要有关系模型。就像现在大量的互联网企业都使用Mysql而不愿意使用HBase,因此Google才有这个可扩展数据库的F1。而Spanner就是F1的至关重要的底层存储技术。

Colossus(GFS II)

Colossus也是一个不得不提起的技术。他是第二代GFS,对应开源世界的新HDFS。GFS是著名的分布式文件系统。

初代GFS是为批处理设计的。对于大文件很友好,吞吐量很大,但是延迟较高。所以使用他的系统不得不对GFS做各种优化,才能获得良好的性能。那为什么Google没有考虑到这些问题,设计出更完美的GFS ? 因为那个时候是2001年,Hadoop出生是在2007年。如果Hadoop是世界领先水平的话,GFS比世界领先水平还领先了6年。同样的Spanner出生大概是2009年,现在我们看到了论文,估计Spanner在Google已经很完善,同时Google内部已经有更先进的替代技术在酝酿了。笔者预测,最早在2015年才会出现Spanner和F1的山寨开源产品。

Colossus是第二代GFS。Colossus是Google重要的基础设施,因为他可以满足主流应用对FS的要求。Colossus的重要改进有:

  • 优雅Master容错处理 (不再有2s的停止服务时间)
  • Chunk大小只有1MB (对小文件很友好)
  • Master可以存储更多的Metadata(当Chunk从64MB变为1MB后,Metadata会扩大64倍,但是Google也解决了)

Colossus可以自动分区Metadata。使用Reed-Solomon算法来复制,可以将原先的3份减小到1.5份,提高写的性能,降低延迟。客户端来复制数据。具体细节笔者也猜不出。

与BigTable, Megastore对比

Spanner主要致力于跨数据中心的数据复制上,同时也能提供数据库功能。在Google类似的系统有BigTable和Megastore。和这两者相比,Spanner又有什么优势呢。

BigTable在Google得到了广泛的使用,但是他不能提供较为复杂的Schema,还有在跨数据中心环境下的强一致性。Megastore有类RDBMS的数据模型,同时也支持同步复制,但是他的吞吐量太差,不能适应应用要求。Spanner不再是类似BigTable的版本化 key-value存储,而是一个“临时多版本”的数据库。何为“临时多版本”,数据是存储在一个版本化的关系表里面,存储的时间数据会根据其提交的时间打上时间戳,应用可以访问到较老的版本,另外老的版本也会被垃圾回收掉。

Google官方认为 Spanner是下一代BigTable,也是Megastore的继任者。

Google Spanner设计
功能

从高层看Spanner是通过Paxos状态机将分区好的数据分布在全球的。数据复制全球化的,用户可以指定数据复制的份数和存储的地点。Spanner可以在集群或者数据发生变化的时候将数据迁移到合适的地点,做负载均衡。用户可以指定将数据分布在多个数据中心,不过更多的数据中心将造成更多的延迟。用户需要在可靠性和延迟之间做权衡,一般来说复制1,2个数据中心足以保证可靠性。

作为一个全球化分布式系统,Spanner提供一些有趣的特性。

  • 应用可以细粒度的指定数据分布的位置。精确的指定数据离用户有多远,可以有效的控制读延迟(读延迟取决于最近的拷贝)。指定数据拷贝之间有多远,可以控制写的延迟(写延迟取决于最远的拷贝)。还要数据的复制份数,可以控制数据的可靠性和读性能。(多写几份,可以抵御更大的事故)
  • Spanner还有两个一般分布式数据库不具备的特性:读写的外部一致性,基于时间戳的全局的读一致。这两个特性可以让Spanner支持一致的备份,一致的MapReduce,还有原子的Schema修改。

这写特性都得益有Spanner有一个全球时间同步机制,可以在数据提交的时候给出一个时间戳。因为时间是系列化的,所以才有外部一致性。这个很容易理解,如果有两个提交,一个在T1,一个在T2。那有更晚的时间戳那个提交是正确的。

这个全球时间同步机制是用一个具有GPS和原子钟的TrueTime API提供了。这个TrueTime API能够将不同数据中心的时间偏差缩短在10ms内。这个API可以提供一个精确的时间,同时给出误差范围。Google已经有了一个TrueTime API的实现。笔者觉得这个TrueTime API 非常有意义,如果能单独开源这部分的话,很多数据库如MongoDB都可以从中受益。

体系结构

Spanner由于是全球化的,所以有两个其他分布式数据库没有的概念。

  • Universe。一个Spanner部署实例称之为一个Universe。目前全世界有3个。一个开发,一个测试,一个线上。因为一个Universe就能覆盖全球,不需要多个。
  • Zones. 每个Zone相当于一个数据中心,一个Zone内部物理上必须在一起。而一个数据中心可能有多个Zone。可以在运行时添加移除Zone。一个Zone可以理解为一个BigTable部署实例

如图所示。一个Spanner有上面一些组件。实际的组件肯定不止这些,比如TrueTime API Server。如果仅仅知道这些知识,来构建Spanner是远远不够的。但Google都略去了。那笔者就简要介绍一下。

  • Universemaster: 监控这个universe里zone级别的状态信息
  • Placement driver:提供跨区数据迁移时管理功能
  • Zonemaster:相当于BigTable的Master。管理Spanserver上的数据。
  • Location proxy:存储数据的Location信息。客户端要先访问他才知道数据在那个Spanserver上。
  • Spanserver:相当于BigTable的ThunkServer。用于存储数据。

 

?可以看出来这里每个组件都很有料,但是Google的论文里只具体介绍了Spanserver的设计,笔者也只能介绍到这里。下面详细阐述Spanserver的设计。
Spanserver

本章详细介绍Spanserver的设计实现。Spanserver的设计和BigTable非常的相似。参照下图

从下往上看。每个数据中心会运行一套Colossus (GFS II) 。每个机器有100-1000个tablet。Tablet概念上将相当于数据库一张表里的一些行,物理上是数据文件。打个比方,一张1000行的表,有10个tablet,第1-100行是一个tablet,第101-200是一个tablet。但和BigTable不同的是BigTable里面的tablet存储的是Key-Value都是string,Spanner存储的Key多了一个时间戳:

(Key: string, timestamp: int64) -> string。

因此spanner天生就支持多版本,tablet在文件系统中是一个B-tree-like的文件和一个write-ahead日志。

每个Tablet上会有一个Paxos状态机。Paxos是一个分布式一致性协议。Table的元数据和log都存储在上面。Paxos会选出一个replica做leader,这个leader的寿命默认是10s,10s后重选。Leader就相当于复制数据的master,其他replica的数据都是从他那里复制的。读请求可以走任意的replica,但是写请求只有去leader。这些replica统称为一个paxos group。

每个leader replica的spanserver上会实现一个lock table还管理并发。Lock table记录了两阶段提交需要的锁信息。但是不论是在Spanner还是在BigTable上,但遇到冲突的时候长时间事务会将性能很差。所以有一些操作,如事务读可以走lock table,其他的操作可以绕开lock table。

每个leader replica的spanserver上还有一个transaction manager。如果事务在一个paxos group里面,可以绕过transaction manager。但是一旦事务跨多个paxos group,就需要transaction manager来协调。其中一个Transaction manager被选为leader,其他的是slave听他指挥。这样可以保证事务。

Directories and Placement

之所以Spanner比BigTable有更强的扩展性,在于Spanner还有一层抽象的概念directory, directory是一些key-value的集合,一个directory里面的key有一样的前缀。更妥当的叫法是bucketing。Directory是应用控制数据位置的最小单元,可以通过谨慎的选择Key的前缀来控制。据此笔者可以猜出,在设计初期,Spanner是作为F1的存储系统而设立,甚至还设计有类似directory的层次结构,这样的层次有很多好处,但是实现太复杂被摒弃了。

Directory作为数据放置的最小单元,可以在paxos group里面移来移去。Spanner移动一个directory一般出于如下几个原因:

  • 一个paxos group的负载太大,需要切分
  • 将数据移动到access更近的地方
  • 将经常同时访问的directory放到一个paxos group里面

Directory可以在不影响client的前提下,在后台移动。移动一个50MB的directory大概需要的几秒钟。

那么directory和tablet又是什么关系呢。可以理解为Directory是一个抽象的概念,管理数据的单元;而tablet是物理的东西,数据文件。由于一个Paxos group可能会有多个directory,所以spanner的tablet实现和BigTable的tablet实现有些不同。BigTable的tablet是单个顺序文件。Google有个项目,名为Level DB,是BigTable的底层,可以看到其实现细节。而Spanner的tablet可以理解是一些基于行的分区的容器。这样就可以将一些经常同时访问的directory放在一个tablet里面,而不用太在意顺序关系。

在paxos group之间移动directory是后台任务。这个操作还被用来移动replicas。移动操作设计的时候不是事务的,因为这样会造成大量的读写block。操作的时候是先将实际数据移动到指定位置,然后再用一个原子的操作更新元数据,完成整个移动过程。

Directory还是记录地理位置的最小单元。数据的地理位置是由应用决定的,配置的时候需要指定复制数目和类型,还有地理的位置。比如(上海,复制2份;南京复制1分) 。这样应用就可以根据用户指定终端用户实际情况决定的数据存储位置。比如中国队的数据在亚洲有3份拷贝, 日本队的数据全球都有拷贝。

前面对directory还是被简化过的,还有很多无法详述。

数据模型

Spanner的数据模型来自于Google内部的实践。在设计之初,Spanner就决心有以下的特性:

  • 支持类似关系数据库的schema
  • Query语句
  • 支持广义上的事务

为何会这样决定呢?在Google内部还有一个Megastore,尽管要忍受性能不够的折磨,但是在Google有300多个应用在用它,因为Megastore支持一个类似关系数据库的schema,而且支持同步复制 (BigTable只支持最终一致的复制) 。使用Megastore的应用有大名鼎鼎的Gmail, Picasa, Calendar, Android Market和AppEngine。 而必须对Query语句的支持,来自于广受欢迎的Dremel,笔者不久前写了篇文章来介绍他。 最后对事务的支持是比不可少了,BigTable在Google内部被抱怨的最多的就是其只能支持行事务,再大粒度的事务就无能为力了。Spanner的开发者认为,过度使用事务造成的性能下降的恶果,应该由应用的开发者承担。应用开发者在使用事务的时候,必须考虑到性能问题。而数据库必须提供事务机制,而不是因为性能问题,就干脆不提供事务支持。

数据模型是建立在directory和key-value模型的抽象之上的。一个应用可以在一个universe中建立一个或多个database,在每个database中建立任意的table。Table看起来就像关系型数据库的表。有行,有列,还有版本。Query语句看起来是多了一些扩展的SQL语句。

Spanner的数据模型也不是纯正的关系模型,每一行都必须有一列或多列组件。看起来还是Key-value。主键组成Key,其他的列是Value。但这样的设计对应用也是很有裨益的,应用可以通过主键来定位到某一行。

上图是一个例子。对于一个典型的相册应用,需要存储其用户和相册。可以用上面的两个SQL来创建表。Spanner的表是层次化的,最顶层的表是directory table。其他的表创建的时候,可以用 interleave in parent来什么层次关系。这样的结构,在实现的时候,Spanner可以将嵌套的数据放在一起,这样在分区的时候性能会提升很多。否则Spanner无法获知最重要的表之间的关系。

TrueTime

TrueTime API 是一个非常有创意的东西,可以同步全球的时间。上表就是TrueTime API。TT.now()可以获得一个绝对时间TTinterval,这个值和UnixTime是相同的,同时还能够得到一个误差e。TT.after(t)和TT.before(t)是基于TT.now()实现的。

那这个TrueTime API实现靠的是GFS和原子钟。之所以要用两种技术来处理,是因为导致这两个技术的失败的原因是不同的。GPS会有一个天线,电波干扰会导致其失灵。原子钟很稳定。当GPS失灵的时候,原子钟仍然能保证在相当长的时间内,不会出现偏差。

实际部署的时候。每个数据中心需要部署一些Master机器,其他机器上需要有一个slave进程来从Master同步。有的Master用GPS,有的Master用原子钟。这些Master物理上分布的比较远,怕出现物理上的干扰。比如如果放在一个机架上,机架被人碰倒了,就全宕了。另外原子钟不是并很贵。Master自己还会不断比对,新的时间信息还会和Master自身时钟的比对,会排除掉偏差比较大的,并获得一个保守的结果。最终GPS master提供时间精确度很高,误差接近于0。

 

每个Slave后台进程会每个30秒从若干个Master更新自己的时钟。为了降低误差,使用Marzullo算法。每个slave还会计算出自己的误差。这里的误差包括的通信的延迟,机器的负载。如果不能访问Master,误差就会越走越大,知道重新可以访问。

Google Spanner并发控制

Spanner使用TrueTime来控制并发,实现外部一致性。支持以下几种事务。

  • 读写事务
  • 只读事务
  • 快照读,客户端提供时间戳
  • 快照读,客户端提供时间范围

例如一个读写事务发生在时间t,那么在全世界任何一个地方,指定t快照读都可以读到写入的值。

Operation Concurrency Control Replica Required
Read-Write Transaction pessimistic leader
Read-Only Transaction lock-free leader for timestamp; any for read
Snapshot Read, client-provided timestamp lock-free any
Snapshot Read, client-provided bound lock-free any

上表是Spanner现在支持的事务。单独的写操作都被实现为读写事务 ; 单独的非快照被实现为只读事务。事务总有失败的时候,如果失败,对于这两种操作会自己重试,无需应用自己实现重试循环。

时间戳的设计大大提高了只读事务的性能。事务开始的时候,要声明这个事务里没有写操作,只读事务可不是一个简单的没有写操作的读写事务。它会用一个系统时间戳去读,所以对于同时的其他的写操作是没有Block的。而且只读事务可以在任意一台已经更新过的replica上面读。

对于快照读操作,可以读取以前的数据,需要客户端指定一个时间戳或者一个时间范围。Spanner会找到一个已经充分更新好的replica上读取。

还有一个有趣的特性的是,对于只读事务,如果执行到一半,该replica出现了错误。客户端没有必要在本地缓存刚刚读过的时间,因为是根据时间戳读取的。只要再用刚刚的时间戳读取,就可以获得一样的结果。

读写事务

正如BigTable一样,Spanner的事务是会将所有的写操作先缓存起来,在Commit的时候一次提交。这样的话,就读不出在同一个事务中写的数据了。不过这没有关系,因为Spanner的数据都是有版本的。

在读写事务中使用wound-wait算法来避免死锁。当客户端发起一个读写事务的时候,首先是读操作,他先找到相关数据的leader replica,然后加上读锁,读取最近的数据。在客户端事务存活的时候会不断的向leader发心跳,防止超时。当客户端完成了所有的读操作,并且缓存了所有的写操作,就开始了两阶段提交。客户端闲置一个coordinator group,并给每一个leader发送coordinator的id和缓存的写数据。

leader首先会上一个写锁,他要找一个比现有事务晚的时间戳。通过Paxos记录。每一个相关的都要给coordinator发送他自己准备的那个时间戳。

Coordinator leader一开始也会上个写锁,当大家发送时间戳给他之后,他就选择一个提交时间戳。这个提交的时间戳,必须比刚刚的所有时间戳晚,而且还要比TT.now()+误差时间 还有晚。这个Coordinator将这个信息记录到Paxos。

在让replica写入数据生效之前,coordinator还有再等一会。需要等两倍时间误差。这段时间也刚好让Paxos来同步。因为等待之后,在任意机器上发起的下一个事务的开始时间,都比如不会比这个事务的结束时间早了。然后coordinator将提交时间戳发送给客户端还有其他的replica。他们记录日志,写入生效,释放锁。

只读事务

对于只读事务,Spanner首先要指定一个读事务时间戳。还需要了解在这个读操作中,需要访问的所有的读的Key。Spanner可以自动确定Key的范围。

如果Key的范围在一个Paxos group内。客户端可以发起一个只读请求给group leader。leader选一个时间戳,这个时间戳要比上一个事务的结束时间要大。然后读取相应的数据。这个事务可以满足外部一致性,读出的结果是最后一次写的结果,并且不会有不一致的数据。

如果Key的范围在多个Paxos group内,就相对复杂一些。其中一个比较复杂的例子是,可以遍历所有的group leaders,寻找最近的事务发生的时间,并读取。客户端只要时间戳在TT.now().latest之后就可以满足要求了。

最后的话

本文介绍了Google Spanner的背景,设计和并发控制。希望不久的将来,会有开源产品出现。

 

 

 

 

 

文章分类 软件技术 | 标签: google, google spanner, spanner | 4 评论 |

Google Dremel 原理 – 如何能3秒分析1PB

在 2012年8月23日 上公布 作者为 yankay

简介

Dremel 是Google 的“交互式”数据分析系统。可以组建成规模上千的集群,处理PB级别的数据。MapReduce处理一个数据,需要分钟级的时间。作为MapReduce的发起人,Google开发了Dremel将处理时间缩短到秒级,作为MapReduce的有力补充。Dremel作为Google BigQuery的report引擎,获得了很大的成功。最近Apache计划推出Dremel的开源实现Drill,将Dremel的技术又推到了浪尖上。

Google Dremel设计

根据Google公开的论文《Dremel: Interactive Analysis of WebScaleDatasets》可以看到Dremel的设计原理。还有一些测试报告。论文写于2006年,公开于2010年,Google在处理大数据方面,果真有得天独厚的优势。下面的内容,很大部分来自这篇论文。

随着Hadoop的流行,大规模的数据分析系统已经越来越普及。数据分析师需要一个能将数据“玩转”的交互式系统。如此,就可以非常方便快捷的浏览数据,建立分析模型。Dremel系统有下面几个主要的特点:

  • Dremel是一个大规模系统。在一个PB级别的数据集上面,将任务缩短到秒级,无疑需要大量的并发。磁盘的顺序读速度在100MB/S上下,那么在1S内处理1TB数据,意味着至少需要有1万个磁盘的并发读! Google一向是用廉价机器办大事的好手。但是机器越多,出问题概率越大,如此大的集群规模,需要有足够的容错考虑,保证整个分析的速度不被集群中的个别慢(坏)节点影响。
  • Dremel是MR交互式查询能力不足的补充。和MapReduce一样,Dremel也需要和数据运行在一起,将计算移动到数据上面。所以它需要GFS这样的文件系统作为存储层。在设计之初,Dremel并非是MapReduce的替代品,它只是可以执行非常快的分析,在使用的时候,常常用它来处理MapReduce的结果集或者用来建立分析原型。
  • Dremel的数据模型是嵌套(nested)的。互联网数据常常是非关系型的。Dremel还需要有一个灵活的数据模型,这个数据模型至关重要。Dremel支持一个嵌套(nested)的数据模型,类似于Json。而传统的关系模型,由于不可避免的有大量的Join操作,在处理如此大规模的数据的时候,往往是有心无力的。
  • Dremel中的数据是用列式存储的。使用列式存储,分析的时候,可以只扫描需要的那部分数据的时候,减少CPU和磁盘的访问量。同时列式存储是压缩友好的,使用压缩,可以综合CPU和磁盘,发挥最大的效能。对于关系型数据,如果使用列式存储,我们都很有经验。但是对于嵌套(nested)的结构,Dremel也可以用列存储,非常值得我们学习。
  • Dremel结合了Web搜索 和并行DBMS的技术。首先,他借鉴了Web搜索中的“查询树”的概念,将一个相对巨大复杂的查询,分割成较小较简单的查询。大事化小,小事化了,能并发的在大量节点上跑。其次,和并行DBMS类似,Dremel可以提供了一个SQL-like的接口,就像Hive和Pig那样。

Google Dremel应用场景

设想一个使用场景。我们的美女数据分析师,她有一个新的想法要验证。要验证她的想法,需要在一个上亿条数据上面,跑一个查询,看看结果和她的想法是不是一样,她可不希望等太长时间,最好几秒钟结果就出来。当然她的想法不一定完善,还需要不断调整语句。然后她验证了想法,发现了数据中的价值。最后,她可以将这个语句完善成一个长期运行的任务。

对于Google,数据一开始是放在GFS上的。可以通过MapReduce将数据导入到Dremel中去,在这些MapReduce中还可以做一些处理。然后分析师使用Dremel,轻松愉悦的分析数据,建立模型。最后可以编制成一个长期运行的MapReduce任务。

这种处理方式,让笔者联想到Greenplum的Chorus. Chorus也可以为分析师提供快速的数据查询,不过解决方案是通过预处理,导入部分数据,减少数据集的大小。用的是三十六计,走为上计,避开的瞬时分析大数据的难题。Chorus最近即将开源,可以关注下。

还有一点特别的就是按列存储的嵌套数据格式。如图所示,在按记录存储的模式中,一个记录的多列是连续的写在一起的。在按列存储中,可以将数据按列分开。也就是说,可以仅仅扫描A.B.C而不去读A.E或者A.B.C。难点在于,我们如何能同时高效地扫描若干列,并做一些分析。

Google Dremel数据模型

在Google, 用Protocol Buffer常常作为序列化的方案。其数据模型可以用数学方法严格的表示如下:

$$!t=dom |<a_1:t[*|?],…,a_n:t[*|?]>$$</a_1:t[*|?],…,a_n:t[*|?]>

其中t可以是一个基本类型或者组合类型。其中基本类型可以是integer,float和string。组合类型可以是若干个基本类型拼凑。星号(*)指的是任何类型都可以重复,就是数组一样。问号(?)指的是任意类型都是可以是可选的。简单来说,除了没有Map外,和一个Json几乎没有区别。

下图是例子,Schema定义了一个组合类型Document.有一个必选列DocId,可选列Links,还有一个数组列Name。可以用Name.Language.Code来表示Code列。

这种数据格式是语言无关,平台无关的。可以使用Java来写MR程序来生成这个格式,然后用C++来读取。在这种列式存储中,能够快速通用处理也是非常的重要的。

上图,是一个示例数据的抽象的模型;下图是这份数据在Dremel实际的存储的格式。

如果是关系型数据,而不是嵌套的结构。存储的时候,我们可以将每一列的值直接排列下来,不用引入其他的概念,也不会丢失数据。对于嵌套的结构,我们还需要两个变量R (Repetition Level) ,D (Definition Level) 才能存储其完整的信息。

Repetition Level是记录该列的值是在哪一个级别上重复的。举个例子说明:对于Name.Language.Code? 我们一共有三条非Null的记录。

  1. 第一个是”en-us”,出现在第一个Name的第一个Lanuage的第一个Code里面。在此之前,这三个元素是没有重复过的,都是第一个。所以其R为0。
  2. 第二个是”en”,出现在下一个Lanuage里面。也就是说Lanague是重复的元素。Name.Language.Code中Lanague排第二个,所以其R为2.
  3. 第三个是”en-gb”,出现在下一个Name中,Name是重复元素,排第一个,所以其R为1。

我们可以想象,将所有的没有值的列,设值为NULL。如果是数组列,我们也想象有一个NULL值。有了Repetition Level,我们就可以很好的用列表示嵌套的结构了。但是还有一点不足。就是还需要表示一个数组是不是我们想象出来的。

Definition Level 是定义的深度,用来记录该列是否是”想象”出来的。所以对于非NULL的记录,是没有意义的,其值必然为相同。同样举个例子。例如Name.Language.Country,

  • 第一个”us”是在R1里面,其中Name,Language,Country是有定义的。所以D为3。
  • 第二个”NULL”也是在R1的里面,其中Name,Language是有定义的,其他是想象的。所以D为2。
  • 第三个”NULL”还是在R1的里面,其中Name是有定义的,其他是想象的。所以D为1。
  • 第四个”gb”是在R1里面,其中Name,Language,Country是有定义的。所以D为3。

就是这样,如果路径中有required,可以将其减去,因为required必然会define,记录其数量没有意义。

理解了如何存储这种嵌套结构。写没有难度。读的时候,我们只读其中部分字段,来构建部分的数据模型。例如,只读取DocID和Name.Language.Country。我们可以同时扫描两个字段,先扫描DocID。记录下第一个,然后发现下一个DocID的R是0;于是该读Name.Language.Country,如果下一个R是1或者2就继续读,如果是0就开始读下一个DocID。

下图展示了一个更为复杂的读取的状态机示例。在读取过程中使用了Definition Level来快速Jump,提升性能。

到此为止,我们已经知道了Dremel的数据结构。就像其他数据分析系统一样,数据结构确定下来,功能就决定了一大半。对于Dremel的数据查询,必然是“全表扫描”,但由于其巧妙的列存储设计,良好的数据模型设计可以回避掉大部分Join需求和扫描最少的列。

Google Dremel查询方式

Dremel可以使用一种SQL-like的语法查询嵌套数据。由于Dremel的数据是只读的,并且会密集的发起多次类似的请求。所以可以保留上次请求的信息,还优化下次请求的explain过程。那又是如何explain的呢?

这是一个树状架构。当Client发其一个请求,根节点受到请求,根据metadata,将其分解到枝叶,直到到位于数据上面的叶子Server。他们扫描处理数据,又不断汇总到根节点。

举个例子:对于请求:

SELECT A, COUNT(B) FROM T GROUP BY A

根节点收到请求,会根据数据的分区请求,将请求变成可以拆分的样子。原来的请求会变为。

SELECT A, SUM(c) FROM (R1 UNION ALL ... Rn) GROUP BY A

R1,…RN是T的分区计算出的结果集。越大的表有越多的分区,越多的分区可以越好的支持并发。

然后再将请求切分,发送到每个分区的叶子Server上面去,对于每个Server

?Ri = SELECT A, COUNT(B) AS c FROM Ti GROUP BY A

结构集一定会比原始数据小很多,处理起来也更快。根服务器可以很快的将数据汇总。具体的聚合方式,可以使用现有的并行数据库技术。

Dremel是一个多用户的系统。切割分配任务的时候,还需要考虑用户优先级和负载均衡。对于大型系统,还需要考虑容错,如果一个叶子Server出现故障或变慢,不能让整个查询也受到明显影响。

通常情况下,每个计算节点,执行多个任务。例如,技巧中有3000个叶子Server,每个Server使用8个线程,有可以有24000个计算单元。如果一张表可以划分为100000个区,就意味着大约每个计算单元需要计算5个区。这执行的过程中,如果某一个计算单元太忙,就会另外启一个来计算。这个过程是动态分配的。

对于GFS这样的存储,一份数据一般有3份拷贝,计算单元很容易就能分配到数据所在的节点上,典型的情况可以到达95%的命中率。

Dremel还有一个配置,就是在执行查询的时候,可以指定扫描部分分区,比如可以扫描30%的分区,在使用的时候,相当于随机抽样,加快查询。

Google Dremel测试实验

实验的数据源如下表示。大部分数据复制了3次,也有一个两次。每个表会有若干分区,每个分区的大小在100K到800K之间。如果压缩率是25%,并且计入复制3份的事实的话。T1的大小已经达到PB级别。这么小且巨量的分区,对于GFS的要求很高,现在的Hdfs稳定版恐怕受不了。接下来的测试会逐步揭示其是如何超过MR,并对性能作出分析。

表名 记录数 大小(已压缩) 列数 数据中心 复制数量
T1 85 billion 87 TB 270 A 3×
T2 24 billion 13 TB 530 A 3×
T3 4 billion 70 TB 1200 A 3×
T4 1+ trillion 105 TB 50 B 2×
T5 1+ trillion 20 TB 30 B 3×

列存测试

首先,我们测试看看列存的效果。对于T1表,1GB的数据大约有300K行,使用列存的话压缩后大约在375MB。这台机器磁盘的吞吐在70MB/s左右。这1GB的数据,就是我们的现在的测试数据源,测试环境是单机。

见上图。

  • 曲线A,是用列存读取数据并解压的耗时。
  • 曲线B是一条一条记录挨个读的时间。
  • 曲线C是在B的基础上,加上了反序列化的时间。
  • 曲线d,是按行存读并解压的耗时。
  • 曲线e加上了反序列化的时间。因为列很多,反序列化耗时超过了读并解压的50%。

从图上可以看出。如果需要读的列很少的话,列存的优势就会特别的明显。对于列的增加,产生的耗时也几乎是线性的。而一条一条该个读和反序列化的开销是很大的,几乎都在原来基础上增加了一倍。而按行读,列数的增加没有影响,因为一次性读了全部列。

Dremel和MapReduce的对比测试

MR和Dremel最大的区别在于行存和列存。如果不能击败MapReduce,Remel就没有意义了。使用最常见的WordCount测试,计算这个数据中Word的个数。

Q1: SELECT SUM(CountWords(txtField)) / COUNT(*) FROM T1

上图是测试的结果。使用了两个MR任务。这两个任务和Dremel一样都运行在3000个节点上面。如果使用列存,Dremel的按列读的MR只需要读0.5TB的数据,而按行存需要读87TB。 MR提供了一个方便有效的途经来讲按行数据转换成按列的数据。Dremel可以方便的导入MapReduce的处理结果。

树状计算Server测试

接下来我们要对比在T2表示使用两个不同的Group BY查询。T2表有24 billion 行的记录。每个记录有一个 item列表,每一item有一个amount 字段。总共有40 billion个item.amount。这两个Query分别是。

Q2: SELECT country, SUM(item.amount) FROM T2 GROUP BY country

Q3: SELECT domain, SUM(item.amount) FROM T2 WHERE domain CONTAINS ’.net’ GROUP BY domain

Q2需要扫描60GB的压缩数据,Q3需要扫描180GB,同时还要过滤一个条件。

上图是这两个Query在不同的server拓扑下的性能。每个测试都是有2900个叶子Server。在2级拓扑中,根server直接和叶子Server通信。在3级拓扑中,各个级别的比例是1:100:2900,增加了100个中间Server。在4级拓扑中,比例为1:10:100:2900.

Q2可以在3级拓扑下3秒内执行完毕,但是为他提供更高的拓扑级别,对性能提升没有裨益。相比之下,为Q3提供更高的拓扑级别,性能可以有效提升。这个测试体现了树状拓扑对性能提升的作用。

每个分区的执行情况

对于刚刚的两个查询,具体的每个分区的执行情况是这样的。

可以看到99%的分区都在1s内完成了。Dremel会自动调度,使用新的Server计算拖后腿的任务。

记录内聚合

由于Demel支持List的数据类型,有的时候,我们需要计算每个记录里面的各个List的聚合。如

Q4 : SELECT COUNT(c1 > c2) FROM

(SELECT SUM(a.b.c.d) WITHIN RECORD AS c1,

SUM(a.b.p.q.r) WITHIN RECORD AS c2

FROM T3)

我们需要count所有sum(a.b.c.d)比sum(a.b.p.q.r),执行这条语句实际只需要扫描13GB的数据,耗时15s,而整张表有70TB。如果没有这样的嵌套数据结构,这样的查询会很复杂。

扩展性测试

Dremel有良好的扩展性,可以通过增加机器来缩短查询的时间。并且可以处理数以万亿计的记录。

对于查询:

Q5: SELECT TOP(aid, 20), COUNT(*) FROM T4?WHERE bid = fvalue1g AND cid = fvalue2g

使用不同的叶子Server数目来进行测试。

可以发现CPU的耗时总数是基本不变的,在30万秒左右。但是随着节点数的增加,执行时间也会相应缩短。几乎呈线性递减。如果我们使用通过CPU时间计费的“云计算”机器,每个租户的查询都可以很快,成本也会非常低廉。

容错测试

一个大团队里面,总有几个拖油瓶。对于有万亿条记录的T5,我们执行下面的语句。

Q6: SELECT COUNT(DISTINCT a) FROM T5

值得注意的是T5的数据只有两份拷贝,所以有更高的概率出现坏节点和拖油瓶。这个查询需要扫描大约1TB的压缩数据,使用2500个节点。

可以看到99%的分区都在5S内完成的。不幸的是,有一些分区需要较长的时间来处理。尽管通过动态调度可以加快一些,但在如此大规模的计算上面,很难完全不出问题。如果不在意太精确的结果,完全可以小小减少覆盖的比例,大大提升相应速度。

Google Dremel 的影响

Google Dremel的能在如此短的时间内处理这么大的数据,的确是十分惊艳的。有个伯克利分校的教授Armando Fox说过一句话“如果你曾事先告诉我Dremel声称其将可做些什么,那么我不会相信你能开发出这种工具”。这么给力的技术,必然对业界造成巨大的影响。第一个被波及到的必然是Hadoop。

Dremel与Hadoop

Dremel的公开论文里面已经说的很明白,Dremel不是用来替代MapReduce,而是和其更好的结合。Hadoop的Hive,Pig无法提供及时的查询,而Dremel的快速查询技术可以给Hadoop提供有力的补充。同时Dremel可以用来分析MapReduce的结果集,只需要将MapReduce的OutputFormat修改为Dremel的格式,就可以几乎不引入额外开销,将数据导入Dremel。使用Dremel来开发数据分析模型,MapReduce来执行数据分析模型。

Hadoop的Hive,Pig现在也有了列存的模式,架构上和Dremel也接近。但是无论存储结构还是计算方式都没有Dremel精致。对Hadoop实时性的改进也一直是个热点话题。要想在Hadoop中山寨一个Dremel,并且相对现有解决方案有突破,笔者觉得Hadoop自身需要一些改进。一个是HDFS需要对并发细碎的数据读性能有大的改进,HDFS需要更加的低延迟。再者是Hadoop需要不仅仅支持MapReduce这一种计算框架。其他部分,Hadoop都有对应的开源组件,万事俱备只欠东风。

Dremel的开源实现

Dremel现在还没有一个可以运行的开源实现,不过我们看到很多努力。一个是Apache的Drill,一个是OpenDremel/Dazo。

OpenDremel/Dazo

OpenDremel是一个开源项目,最近改名为Dazo。可以在GoogleCode上找到http://code.google.com/p/dremel/。目前还没有发布。作者声称他已经完成了一个通用执行引擎和OpenStack Swift的集成。笔者感觉其越走越歪,离Dremel越来越远了。

Apache Drill

Drill 是Hadoop的赞助商之一MapR发起的。Drill作为一个Dremel的山寨项目,有和Dremel相似的架构和能力。他们希望Drill最终会想Hive,Pig一样成为Hadoop上的重要组成部分。为Hadoop提供快速查询的能力。和Dremel有一点不同,在数据模型上,开源的项目需要支持更标准的数据结构。比如CSV和JSON。同时Drill还有更大的灵活性,支持多重查询语言,多种接口。

现在Drill的目标是完成初始的需求,架构。完成一个初始的实现。这个实现包括一个执行引擎和DrQL。DrQL是一个基于列的格式,类似于Dremel。目前,Drill已经完成的需求和架构设计。总共分为了四个组件

  • Query language:类似Google BigQuery的查询语言,支持嵌套模型,名为DrQL.
  • Low-lantency distribute execution engine:执行引擎,可以支持大规模扩展和容错。可以运行在上万台机器上计算数以PB的数据。
  • Nested data format:嵌套数据模型,和Dremel类似。也支持CSV,JSON,YAML类似的模型。这样执行引擎就可以支持更多的数据类型。
  • Scalable data source: 支持多种数据源,现阶段以Hadoop为数据源。

目前这四个组件在分别积极的推进,Drill也非常希望有社区其他公司来加入。Drill希望加入到Hadoop生态系统中去。

最后的话

本文介绍了Google Dremel的使用场景,设计实现,测试实验,和对开源世界的影响。相信不久的将来,Dremel的技术会得到广泛的应用。

文章分类 软件技术 | 标签: Dremel, Drill, Google Dremel, Hadoop | 15 评论 |
下一页 »

近期文章

  • 听说 Docker 被 kubenetes 抛弃了,怎么办?containerd
  • 公告 – 博客重开了
  • CloudFoundry v2面面谈,内赠MicroCFv2福利
  • Docker能够运行任何应用的“PaaS”云
  • Scala Tour – 精选

近期评论

  • Gao发表在《公告 – 博客重开了》
  • Impala:新一代开源大数据分析引擎 – FIXBBS发表在《Google Dremel 原理 – 如何能3秒分析1PB》
  • 何建兵发表在《NoSQL数据库笔谈v0.2》
  • Pony发表在《Docker能够运行任何应用的“PaaS”云》
  • Pony发表在《Docker能够运行任何应用的“PaaS”云》

归档

  • 2021年6月
  • 2021年3月
  • 2014年2月
  • 2013年9月
  • 2013年5月
  • 2013年1月
  • 2012年11月
  • 2012年9月
  • 2012年8月
  • 2012年3月
  • 2012年2月
  • 2012年1月
  • 2011年11月
  • 2011年10月
  • 2011年9月
  • 2010年10月
  • 2010年8月
  • 2010年7月
  • 2010年6月
  • 2010年5月
  • 2010年4月
  • 2010年3月
  • 2010年2月
  • 2010年1月
  • 2009年10月
  • 2009年9月
  • 2009年8月
  • 2009年7月
  • 2009年6月
  • 2008年10月
  • 2008年8月
  • 2008年7月
  • 2008年6月

分类

  • 家庭生活
  • 未分类
  • 每日心得
  • 软件技术

友情链接

  • DaoCloud Enterprise
  • DaoCloud 云原生一体机

CyberChimps WordPress Themes

沪ICP备2021008917号-1 © 颜开