Monday, September 26, 2016

第二届区块链峰会随记

上周(9.19-9.24)在上海参加了第二届区块链全球峰会。
整体感觉,整个产业已经上升到一个新的阶段了,开始有一些落地的项目,不再只是呼吁概念。

天下大势,三分已成

币圈和链圈渐行渐远,而目前区块链领域从技术实现上已经逐渐划分为三大阵营:以太坊、超级账本和其它。
以太坊(Ethereum):开源阵营。由 VB 同学带领的以太坊团队牵头开发。草根出身,自然受到很多个人开发者的喜爱,相关的客户端、环境支持也比较完善。不少欧洲的创业项目(Consensus 投资了不少)都是基于以太坊的平台来搭建,但普遍规模较小。
超级账本(HyperLedger):开源阵营 + 企业支持。由 Linux 基金会组织团队开发。科技界和金融界的巨头们牵头支持,自然受到各大企业的青睐。不少机构给的对区块链平台的需求和设计,基本可以理解为对超级账本白皮书的解读,包括隐私保护、可审计、安全、插件化的共识等都是超级账本的基本特性。SWIFT 也刚跳出来支持超级账本阵营。更多信息可以查看 这里
其它方案:包括各种开源、非开源的方案。以创业团队居多。百花齐放百家争鸣是好事情,但是可惜自己真的从头做的很少,这也挺正常,毕竟区块链领域相关技术门槛确实比较高。少数几个号称从头做、有底层技术的又不开源,这在如今是一件挺可惜的事情。
个人感觉,区块链产业要想做大,至少在基础设施这一层需要尽快的成熟和规范,这对大家都有好处。Linux、Web Server 这样的开源方案出来被大家认可后,才有了整个互联网的繁荣。

通用平台 vs 专用平台

除了对通用平台的讨论,开始有人意识到专用平台的重要性。
之前不少人喜欢用传统数据库的需求来质疑区块链。实际上区块链技术跟数据库完全是两个领域的事情,解决的不同的问题。如果某个业务场景真的需要特定的功能,则应该结合业务层从一开始就设计支持,而不是所有需求都堆到底下。
大胆预测,未来可能出现专门面向各个领域的专用区块链平台:金融区块链、物联网区块链、众筹区块链……,类似不同的 Linux 发行版。
只有这样,才能真的说区块链落地了。

应用场景

应用场景仍然是五花八门,开始有一些关注到具体实际的问题。
比如解决个人的资产登记问题、跨境资金、分布式电商平台,大部分也都是之前就耳熟能详的。但有些推出了新一代的解决方案,还是有不少亮点的。
对应用来说,技术层面反而要弱化,好的商业模式、对市场的把握、对当地政策的解读往往都是决定性的因素。
从目前来看,明后两年将会出现应用的大爆发,特别是国内市场。

BaaS 将成为短期内热点

随着 IBM、微软、Google 等宣布退出 BaaS 业务,将有更多的云服务商会追随这一趋势。
目前来看,基于 HyperLedger 的 BaaS 有相对成熟的解决方案。

Friday, August 12, 2016

ProtoBuf 与 gRPC 你需要知道的知识

ProtoBuf 是一套接口描述语言(IDL)和相关工具集(主要是 protoc,基于 C++ 实现),类似 Apache 的 Thrift)。用户写好 .proto 描述文件,之后使用 protoc 可以很容易编译成众多计算机语言(C++、Java、Python、C#、Golang 等)的接口代码。这些代码可以支持 gRPC,也可以不支持。
gRPC 是 Google 开源的 RPC 框架和库,已支持主流计算机语言。底层通信采用 gRPC 协议,比较适合互联网场景。gRPC 在设计上考虑了跟 ProtoBuf 的配合使用。
两者分别解决的不同问题,可以配合使用,也可以分开。
典型的配合使用场景是,写好 .proto 描述文件定义 RPC 的接口,然后用 protoc(带 gRPC 插件)基于 .proto 模板自动生成客户端和服务端的接口代码。

ProtoBuf

需要工具主要包括:
  • 编译器:protoc,以及一些官方没有带的语言插件;
  • 运行环境:各种语言的 protobuf 库,不同语言有不同的安装来源;
语法类似 C++ 语言,可以参考 语言规范
比较核心的,message 是代表数据结构(里面可以包括不同类型的成员变量,包括字符串、数字、数组、字典……),service代表 RPC 接口。变量后面的数字是代表进行二进制编码时候的提示信息,1~15 表示热变量,会用较少的字节来编码。另外,支持导入。
默认所有变量都是可选的(optional),repeated 则表示数组。主要 service rpc 接口只能接受单个 message 参数,返回单个 message;
syntax = "proto3";
package hello;

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
  repeated int32 number=4;
}

service HelloService {
  rpc SayHello(HelloRequest) returns (HelloResponse){}
}
编译最关键参数是指定输出语言格式,例如,python 为 --python_out=OUT_DIR
一些还没有官方支持的语言,可以通过安装 protoc 对应的 plugin 来支持。例如,对于 go 语言,可以安装
$ go get -u github.com/golang/protobuf/{protoc-gen-go,proto} // 前者是 plugin;后者是 go 的依赖库
之后,正常使用 protoc --go_out=./ hello.proto 来生成 hello.pb.go,会自动调用 protoc-gen-go 插件。
ProtoBuf 提供了 Marshal/Unmarshal 方法来将数据结构进行序列化操作。所生成的二进制文件在存储效率上比 XML 高 3~10 倍,并且处理性能高 1~2 个数量级。

gRPC

工具主要包括:
  • 运行时库:各种不同语言有不同的 安装方法,主流语言的包管理器都已支持。
  • protoc,以及 grpc 插件和其它插件:采用 ProtoBuf 作为 IDL 时,对 .proto 文件进行编译处理。
官方文档 写的挺全面了。
类似其它 RPC 框架,gRPC 的库在服务端提供一个 gRPC Server,客户端的库是 gRPC Stub。典型的场景是客户端发送请求,同步或异步调用服务端的接口。客户端和服务端之间的通信协议是基于 HTTP2 的 gRPC 协议,支持双工的流式保序消息,性能比较好,同时也很轻。
采用 ProtoBuf 作为 IDL,则需要定义 service 类型。生成客户端和服务端代码。用户自行实现服务端代码中的调用接口,并且利用客户端代码来发起请求到服务端。一个完整的例子可以参考 这里
以上面 proto 文件为例,需要执行时添加 grpc 的 plugin:
$ protoc --go_out=plugins=grpc:. hello.proto

生成服务端代码

服务端相关代码如下,主要定义了 HelloServiceServer 接口,用户可以自行编写实现代码。
type HelloServiceServer interface {
        SayHello(context.Context, *HelloRequest) (*HelloResponse, error)
}

func RegisterHelloServiceServer(s *grpc.Server, srv HelloServiceServer) {
        s.RegisterService(&_HelloService_serviceDesc, srv)
}
用户需要自行实现服务端接口,代码如下。
比较重要的,创建并启动一个 gRPC 服务的过程:
  • 创建监听套接字:lis, err := net.Listen("tcp", port)
  • 创建服务端:grpc.NewServer()
  • 注册服务:pb.RegisterHelloServiceServer()
  • 启动服务端:s.Serve(lis)
type server struct{}

// 这里实现服务端接口中的方法。
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

// 创建并启动一个 gRPC 服务的过程:创建监听套接字、创建服务端、注册服务、启动服务端。
func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterHelloServiceServer(s, &server{})
    s.Serve(lis)
}
编译并启动服务端。

生成客户端代码

生成的 go 文件中客户端相关代码如下,主要和实现了 HelloServiceClient 接口。用户可以通过 gRPC 来直接调用这个接口。
type HelloServiceClient interface {
        SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error)
}

type helloServiceClient struct {
        cc *grpc.ClientConn
}

func NewHelloServiceClient(cc *grpc.ClientConn) HelloServiceClient {
        return &helloServiceClient{cc}
}

func (c *helloServiceClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) {
        out := new(HelloResponse)
        err := grpc.Invoke(ctx, "/hello.HelloService/SayHello", in, out, c.cc, opts...)
        if err != nil {
                return nil, err
        }
        return out, nil
}
用户直接调用接口方法:创建连接、创建客户端、调用接口。
func main() {
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewHelloServiceClient(conn)

    // Contact the server and print out its response.
    name := defaultName
    if len(os.Args) > 1 {
        name = os.Args[1]
    }
    r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.Message)
}
编译并启动客户端,查看到服务端返回的消息。

Docker 1.12 Swarm 模式剖析

Docker 1.12 在 2016 年 7 月 28 日正式 GA,除了大量的在使用上的改进和 bug 修复外,最引人瞩目的是原生支持了 Swarm 模式。
熟悉 Docker 的读者都知道 Docker Swarm 是官方三剑客之一,提供了轻量级容器云的支持,以性能卓越出名,跟 K8s 面向应用的较为复杂的容器云方案一时瑜亮,各有千秋。
本次 Swarm 模式特性的发布可谓重要变革,不仅仅是减低使用门槛那么简单,还引入了不少新的围绕应用的特性,从中可以一探 Docker 团队对于容器云的理念。

基本概念

Swarm 定义为一组合作在一起的 Docker 主机集群,由节点组成,每个节点都是一个独立的 Docker 主机。
集群中包括两种节点:管理节点和工作节点。参考 k8s 的结构,也是如此。
  • 管理节点:负责对任务进行调度和其它管理任务,多个管理节点通过 Raft 协议组成集群;
  • 工作节点:负责运行具体的任务,管理节点可以同时作为工作节点。
如果把所有节点配成管理+工作,那就是绝对的对等了,实际上从性能角度考虑,管理节点数量不宜过多,并且相互之间的互联网络要保证好的质量。
此外,还引入了服务的概念。服务也包括两种类型:
  • 复制服务(replicated services):类似 k8s 中复制集的概念,保持一定数量的相同任务在集群中运行;
  • 全局服务(global services):类似 k8s 中 daemon 的概念,每个工作节点上运行一个。
一个服务由多个任务组成,一个任务即一个运行的容器。

主要特性

负载均衡和服务地址管理

k8s 中让人印象深刻的是它的服务自带了负载均衡和服务地址功能,Swarm 也通过 ingress load balancing (基于 ipvs 的四层代理)来实现,为每一个服务提供一个 DNS 地址,维护一个公共的端口。这点上跟 k8s 默认方式略有不同,但效果是一致的,都保证了无论容器如何变化,任意节点对应用的访问保持不变。

跨主机网络

这点还是基于 overlay 网络来实现。

监控管理

基于服务的概念,对运行状态进行管控,自动保持服务的运行状态。

支持命令

目前围绕对集群和服务进行管理,命令都很容易理解。
  • swarm init
  • swarm join
  • service create
  • service inspect
  • service ls
  • service rm
  • service scale
  • service ps
  • service update

Swarm2k

很有意思的项目,就是基于最新的 Swarm 模式来在全球范围内构建一个超过 2000 个节点,1 M 个容器的大集群。
目前已经达到预定目标,具体可以关注 [这里](https://github.com/swarm2k/swarm2k/blob/master/PROPOSAL.md)。
这个项目的成功,也再次证明官方的 Swarm 方案,在性能上确实首屈一指!

小结

跟 Docker 技术自身的火爆不同,Docker 团队一直以来就面临着未来发展的困境,几乎所有人都希望他们只安心做好容器引擎。但作为一个商业化公司来说,这样下去毫无疑问是自掘深坑。于是,他们积极的在基于容器的各种平台和工具上进行构建。包括三件套、包括容器云,都是很好的尝试。
从 Swarm 的特性来看,Docker 团队在应用为中心这点上是认同了 K8s 的理念的,但是自身又同时保持了一定的独立性。这对于 Docker 来说自然是利好。但是目前来看,并没有完全解决团队发展的困境。

Saturday, June 25, 2016

区块链的七年之痒

关于区块链的探讨和争论从未停息。
或许从计算技术的演变历史中能得到一些启发意义。


上图是笔者在某次交流会中提出的。
以云计算为代表的现代计算技术,发展历史上有若干重要的时间点和事件:
  • 1969 - ARPANet(Advanced Research Projects Agency Network):现代互联网的前身,被美国高级研究计划署(Advanced Research Project Agency)提出,其使用 NCP 协议,核心缺陷之一是无法做到和个别计算机网络交流;
  • 1973 - TCP/IP:Vinton.Cerf(文特•瑟夫)与Bob Karn(鲍勃•卡恩)共同开发出 TCP 模型,解决了 NCP 的缺陷;
  • 1982 - Internet:TCP/IP 正式成为规范,并被大规模应用,现代互联网诞生;
  • 1989 - WWW:早期互联网的应用主要包括 telnet、ftp、email 等,蒂姆·伯纳斯-李(Tim Berners-Lee)设计的 WWW 协议成为互联网的杀手级应用,引爆了现代互联网,从那开始,互联网业务快速扩张;
  • 1999 - salesforge:互联网出现后,一度只能进行通信应用,但 salesforge 开始以云的理念提供基于互联网的企业级服务;
  • 2006 - aws ec2:AWS EC2 奠定了云计算的业界标杆,直到今天,竞争者们仍然在试图追赶 AWS 的脚步;
  • 2013 - cognitive:以 IBM Watson 为代表的认知计算开始进入商业领域,计算开始变得智能,进入“后云计算时代”。
从这个历史中能看出哪些端倪呢?
一个是 技术领域也存在着周期律。 这个周期目前看是 7 年左右。或许正如人有“七年之痒”,技术也存在着七年这道坎,到了这道坎,要么自身突破迈过去,要么往往就被新的技术所取代。如果从比特币网络上线(2009 年 1 月)算起,到今年正是在坎上。因此,现在正是相关技术进行突破的好时机。
为何恰好是七年?七年按照产品周期来看基本是 2-3 个产品周期,所谓事不过三,经过 2-3 个产品周期也差不多该有个结论了。
另外,先出现的未必是先驱,也可能是先烈。 创新固然很好,但过早播撒的种子,没有合适的土壤,往往也难长大。技术创新与科研创新很不同的一点便是,技术创新必须立足于需求,过早过晚都会错失良机。科研创新则要越早越好,最好像二十世纪那批物理巨匠们一样,让后人吃了一百多年的老本。
最后,事物的发展往往是延续的、长期的。 新生事物大都不是凭空蹦出来的,往往是解决了前辈未能解决的问题,或是出现了之前未曾出现过的场景。而且很多时候,新生事物会在历史的舞台下面进行长期的演化,只要是往提高生产力的正确方向,迟早会有出现在舞台上的一天。

Thursday, June 02, 2016

区块链需要关注的应用场景

区块链最近几年炒得很热,国内已有大量与之相关的企业,有些企业已经结合已有业务摸索出了自己的应用场景,但仍有不少企业处于不断试探和反复迷惑状态。
从技术角度讲,区块链涉及到的领域比较杂,包括分布式、存储、密码学、心理学、博弈论、网络协议等,要一下子完全理解确实不太容易。
甚至有人简单将区块链技术归结到分布式数据库的范畴,误导了对其的正确理解。
实际上,要找到合适的应用场景,还是要从区块链自身的特性出发进行分析。
从技术特点上,区块链具有:
  • 分布式容错性:网络极其鲁棒,容错 1/3 左右节点的异常状态。
  • 不可篡改性:一致提交后的数据会一直存在,不可被销毁或修改。
  • 隐私保护性:密码学保证了未经授权者能访问到数据,但无法解析。
随之带来的业务特性包括:
  • 交易成本:设计恰当的区块链应用可以控制每笔交易的成本更低,不需要第三方中介机构。
  • 维护成本:跟传统技术相比,区块链网络维护成本更低,适用环境更多。
  • 安全隐私:直接为终端用户提供一定程度的安全保障和隐私保护。
区块链并非凭空诞生的新技术,更像是技术演化到一定程度突破应用阈值后的产物,因此,其应用场景也跟促生其出现的环境息息相关。
未来几年内,可能深入应用区块链的场景将包括:
  • 征信管理:这是大型社交平台和保险公司都梦寐以求的,目前还缺乏足够的数据来源、可靠的平台支持和有效的数据分析和管理。该领域创业的门槛极高,需要自上而下的推动。
  • 社区资源共享:airbnb 为代表的公司将欢迎这类应用,极大降低管理成本。这个领域创业门槛低,主题集中,会受到投资热捧。
  • 金融交易:主要是降低交易成本,减少跨组织交易风险等。该领域的区块链应用将最快成熟起来,银行和金融交易机构将是主力推动者。
  • 投资管理:无论公募还是私募基金,都可以应用区块链技术降低管理成本和管控风险。虽然有 DAO 这样的试水,谨慎认为该领域的需求还未成熟。
  • 物联网:物联网是很适合的一个领域,短期内会有大量应用出现,特别是租赁、物流等特定场景。但物联网自身的发展局限将导致短期内较难出现规模应用。

Thursday, May 12, 2016

数字货币到底解决了哪些问题?

货币是人类文明发展过程中的一大发明。很难想象没有了货币,现代社会的金融体系还能否持续运转。
一般等价物都可以作为货币使用。然而平时最常见的货币形式还是纸币,它既方便携带、不易仿制、又相对容易辩伪。
或许有人认为信用卡更方便。相对于信用卡这样的集中式支付体系来说,货币提供了更好的匿名性。而且碰到系统故障、断网、木有刷卡机器等情况,信用卡就不可用了。ps,货币 vs 信用卡并不是本文所关注的问题。
无论是货币,还是信用卡模式,都需要额外的系统(例如银行)来完成生产、分发、管理等操作,带来很大的额外成本和使用风险。诸如伪造、信用卡诈骗、盗刷、转账等安全事件屡见不鲜。
很自然的,如果能实现一种数字货币,保持既有货币的这些特性,消除纸质货币的缺陷,无疑将带来巨大的社会变革,极大提高经济活动的运作效率。
近三十年来,数字货币技术经历了几代演进。目前看来,比较有影响力的模式有两种,一种是类似 paypal 这样的选择跟已有的系统合作,成为代理;一种是以比特币这样的完全丢弃已有体系的分布式技术。
现在还很难讲哪种模式将成为未来的主流,甚至未来还可能出现更先进的技术。但对比特币这一类数字货币的设计进行探索,将是一件十分有趣的事情。
让我们来对比现在的数字货币和现实生活中的纸币:
属性分析胜出方
便携这点上应该没有争议,显然数字形式的货币胜出。数字货币
防伪这点上应该说两者各有千秋,但数字货币可能略胜一筹。纸币依靠的是各种设计(纸张、油墨、暗纹、夹层等)上的精巧,数字货币依靠的则是密码学上的保障。事实上,纸币的伪造时有发生,但数字货币的伪造明面上还没能实现。数字货币
辩伪纸币需要依托验钞机,数字货币依靠密码学。数字货币胜出。数字货币
匿名通常情况下,两者都能提供很好的匿名性。但都无法防御有意的追踪。平局
交易对纸币来说,谁持有纸币就是合法拥有者,交易通过纸币自身的转移即可完成。对数字货币来说则复杂的多,因为任何数字物品都是可以被复制的,因此需要额外的机制。为此,比特币发明了区块链技术来实现可靠的交易。纸币
资源100 美元钞票的生产成本是 0.1 美元左右。100 面额人民币的生产成本说法众多,但估计应该在几毛到几块范围内。数字货币消耗的资源则复杂的多,以最坏情况估计,算出来多少就要消耗多少电(往往要更多)。纸币
发行纸币的发行需要第三方机构的参与,数字货币则通过分布式算法来完成发行。在人类历史上,好几次通胀和通缩就是不合理的发行纸币造成的。但数字货币在这方面的表现还有待观察。平局
可见,数字货币并非在所有领域都优于已有的货币形式。不带前提的在所有领域都鼓吹数字货币并不是一种严谨的态度,应该针对具体情况具体分析。实际上,仔细观察目前支持数字货币的交易机构就会发现端倪。其中,物联网相关领域无疑是一个很有希望的方向。
最后,虽然当前的数字货币已经取得了巨大成功,但可见的局限也很明显:分布式账本还无法做到大规模场景下的快速确认;交易的频度还远低于已有的交易系统;资源的消耗还过高。这些问题还有待于相关技术的进一步发展。

Friday, April 08, 2016

Hyperledger -- Linux 基金会的开源区块链

区块链已经成为当下最受人关注的开源技术。然而对很多人来说,区块链过于底层,而且缺乏统一规范。
2016 年刚过去三个多月,Linux 基金 会牵头,联合三十家初始成员(包括各大金融、科技公司和相关开源组织),共同宣告 了Hyperledger 项目的成立。该项目试图打造一个超级账本项目,作为区块链技术的开源规范和标准,让更多的应用能更容易的建立在区块链技术之上。
IBM 贡献了数万行已有的 Open Block Chain 代码,Digital Asset 则贡献了企业和开发者相关资源,R3 贡献了新的金融交易架构,Intel 也刚贡献了跟分布式账本相关的代码。
首届技术委员会主席由来自 IBM 开源技术部 CTO 的 Chris Ferris 担任,委员会主席则由来自 Digital Asset Holdings 的 CEO Blythe Masters 担任。
该项目的出现,实际上宣布区块链技术已经不单纯是一个开源技术了,已经正式被主流机构和市场认可;同时,对于区块链相关产业的发展意义深远。
项目官方地址托管在 Linux 基金会网站,代码托管在 Github 上,目前已经获得了不少关注。
转载请注明来源。

Monday, March 28, 2016

网关高可用协议:HSRP、VRRP、GLBP、CASP

网络中网关设备负责完成大部分的高级处理,因此网关设备的高可用十分重要。常见的高可用协议包括 HSRP、VRRP、GLBP、CASP。基本原理都是在一个组里面选出一个主节点,拿到虚的网关 IP 和 虚 MAC。这些协议也可以提供 IP 节点的高可用保护。

HSRP

全称是 Hot Standby Routing Protocol,Cisco 家 98 年公开的专利协议,在 RFC 2281 中给出。
一主一从,其它为监听状态。组内多播到 224.0.0.2(v1)或 224.0.0.102(v2),虚地址不能跟物理地址重复,虚 MAC 地址为00-00-0c-07-AC-XX
基本只有 Cisco 家设备支持。

VRRP

全称是 Virtual Route Rendundancy Protocol,IETF 的标准协议,在 RFC 3768 和 RFC 5798 中给出。设计思路是基于 HSRP,所以两者之间在概念上有很多相似之处,但并不兼容。
一主多从。组内 IP 多播到 224.0.0.18,IP 协议号为 112。虚地址可以复用物理地址,虚 MAC 为 00-00-5E-00-01-XX
除了 Cisco,别家硬件支持的较多,另外有开源软件实现,Linux 上的 VRRPd。
用 VRRPd 配置的话很简单,指定网卡,本机的优先级,所属的组号和虚 IP 即可,例如
$ vrrp –i eth0 –p 24 –v 1 192.168.1.1

GLBP

GLBP 也是 Cisco 家的私有协议,基于 HSRP,最大的改进是考虑了负载均衡。特别之处的设计在于通过激活的虚网关(Active Virtual Gateway)来负责响应网络中的 ARP 请求,虚网关会根据权重来答复不同的成员的虚 MAC 地址。
组内多播到 224.0.0.2(v1)或 224.0.0.102(v2),协议基于 UDP,目标和源端口都是 3222。
最多同时可有 4 个转发网关。

CASP

全称是 Common Address Redundancy Protocol,非标准协议,最初是为了避免 HSRP 的专利风险,由 OpenBSD 在 2003 年完成的全新的设计,加强了安全性,带有负载均衡功能。

Friday, March 25, 2016

OpenStack 部署分布式应用的一个坑

之前基于 OpenStack 部署了一个云,运营下来一段时间下来还算正常,出现了各种问题也是意料之内,基本都很快搞定。
搞云计算的人嘛,就得懂得多一些、深一些不是:)
但有一天有个客户找上来反映了一个小问题,虽然最终解决掉,却引发了我的深思。

问题

客户的应用很简单,也是在我们的平台上申请了虚机,然后自己用 keepalived 为后面的某 db 业务提供 HA 保障。一切运行的都很正常,突然有一天,死活访问不通 db 了。
通过查看日志,发现故障发生前 keepalived 发生了切换,灾备节点被切换了。一开始怀疑是 keepalived 配置问题,检查后发现都正常,而且灾备节点确实也拿到地址了。我们搞运维的同学怀疑是网络问题,但底下排查了半天,也没找到原因,而且别的业务都跑的好好的。

分析和解决

出现问题肯定不会是莫名其妙的,我仔细回想了 VRRP 的原理和 Neutron 的实现。keepalived 基于 VRRP 来切换主从节点,被激活的节点会抢到配置的虚地址。然而,neutron 里面为了防止虚机作恶,会记录每个虚机启动时候分配的 IP 和 Mac,并用这个地址来过滤,避免出现篡改源地址的恶意流量。好了,那问题来了,neutron 并不知道某个虚地址其实是在两个 port 上都可能出现的,就会阻碍切换后的灾备节点流量通信。
问题定位到,就好解决了,直接改 iptables 规则,或者开启 Allowed-Address-Pairs (https://review.openstack.org/#/c/38230/) 即可

进一步思考

从应用的角度看,VRRP 这种切换地址机制毫无问题;从 OpenStack 角度看,你申请一个虚机配置好了地址后,我只允许已知地址的流量出来,也是正常的。但偏偏两个结合到一起就有了问题。这根子上还是在于上层应用和底层平台之间的整合出现了偏差。其实系统就是这样,俩都正常的组件,放一起不出问题的概率很小,这也是为啥接口设计很关键。
我们一直说,要懂业务,要理解业务,其实也是避免设计出的产品跟真实的需求出现偏差。
首先,客户往往并不懂技术(甚至自己都不懂业务),这意味着他们很难用准确的语言描述清楚自己的真正需求(甚至自己都不知道真正需求是啥)。这就需要提供服务的团队要做到比客户更懂自己的业务,这是赢得客户信任的一个极大的优势。懂得从客户需求到业务流程到底下各种技术栈的灵活运用,这才是优秀的技术人员。
另外,就是产品的形态很重要,为什么现在云有 IaaS、PaaS、SaaS 等多种分类,其实这不同的产品形态都是针对不同的客户群体,满足不同的业务需求。呈现细节越少,意味着灵活性越差,但客户使用难度会降低。但无论是那种形态,都应该有严格的范围限制和操作保障,要让完全不懂技术的人能成功操作,甚至各种误操作也能纠正过来。在这点上,OpenStack 是做得很差的,这也是为什么基于 OpenStack 做云服务,首要任务是替换掉 Horizon。极简设计和极繁设计,无所谓高低,合适最好。
最后就是要选择合适的工具。现在开源特别发达,甚至同一个应用场景会出现数个优秀的开源产品,光看特性都是很吸引人。这个时候,选择就很重要了,最看重啥特性?二次开发的难度有多大?针对需求选这个是否合适?选择对了,事半功倍,反之往往陷入各种坑难以自拔。

Thursday, February 25, 2016

分布式系统中的算法设计(一) -- 一致性 Hash

Hash 大家都知道,把某个要存储的内容的索引 key 通过某个规则计算一下,算出来一个值,这个值往往范围比原来小,且概率意义上不会冲突。
由于 Hash 计算复杂度往往比查找要快,被大量应用到各种大规模的系统中,特别是分布式系统。具体实践中有几个典型的问题。

问题来源

一致性 Hash 讨论地已经很多,基本故事就是分布式存储系统中,通过 Hash 来决定内容存到哪个节点上。
典型的比如 cache 系统,后面放若干节点,查找某个 key,hash 到某个节点,再进一步检索。
最简单的自然是按照节点数量取个模,理想状态下不会有啥问题。
但是如果发生如下情况,就无法正常工作了。 加节点(一般是有计划的) 减节点(可能突然故障,无法预知)
所以提出了一致性 hash,即考虑如下四个方面的算法: 平衡性(Balance):哈希结果尽可能分布到所有的节点,这样可以使得所有的节点都得到利用; 单调性(Monotonicity):加入新的节点后,已经分配节点的 hash 结果 被映射到原有的或者新的节点上去,而不会被映射到其他节点; 分散性(Spread):同样内容尽量避免 hash 到不同节点; 负载(Load):不同内容避免 hash 到同样节点。
简单的说,就是有一个算法,首先分配尽量均匀,其次,当节点个数变化的时候,尽量维持原来内容的映射,并进行局部调整。
能满足的算法有很多,这里介绍两个比较经典的。

基于环的实现

该算法最初于 1997 年在论文 Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web 中提出。算法复杂度为 O(log(n))。
主要设计思想为:构造一个环(顺序标上编号比如 0 ~ 2^32-1),将已有节点编号并分配到这个环上,分割环为不同的段。
对每个内容,hash 后是一个独一无二的数字,这个数字肯定会落入某个段上,按照固定方向往前找到的第一个节点编号即为要分配到的节点。
考虑两个相邻的节点 A 和 C,如果要在中间添加一个节点 B,那么 AB 段上内容会从 C 节点重新分配到 B 节点。反之,如果删除掉 B 节点,那么 AB 段上内容会再次分配到 C 节点上。
这种算法很好的满足性质 2-4,唯独平衡性没有解决。即当节点个数不多的时候,内容可能会集中在某些段,划分到部分节点上。要解决这个问题也很简单,把一个节点虚拟为多个虚节点,提高整体节点个数,然后再分散放到环上。这样就提高了平衡性。

基于概率转移的实现

该算法最初于 2014 年在论文 A Fast, Minimal Memory, Consistent Hash Algorithm 中由 Google 工程师 John Lamping 和 Eric Veach 提出。算法复杂度为 O(log(n))。
主要设计思想为:假设原先有 n 个节点,新加上一个节点后,每个 key 都有 1/n+1 的概率转移到新的节点上,n/n+1 的概率留在原先分配的节点。
举个例子,原先有 1 个节点,再加一个节点,那么已有内容有 1/2 的概率跳转到到第二个节点。原先有 2 个节点(每个里面有 1/2 的内容),再加一个节点,则已分配内容应该有 1/3 的概率转移到新的节点。
一个简单的实现代码为:
int ch(int key, int num_buckets) {
    random.seed(key) ;
    int b = 0; // This will track ch(key, j +1) .
    for (int j = 1; j < num_buckets; j++) {
        if (random.next() < 1.0/(j+1)) b = j ;
    }
    return b;
}
即根据 key 生成 num_buckets-1 个(0~1.0)的伪随机数,依次检查随机数序列:
  • 第一个随机数 <1/2,则放第二个节点;
  • 第二个随机数 <1/3,则放第三个节点;
  • ...
注意伪装随机数序列由 key 唯一确定。意味着同样的内容在多次分配中,分配结果将是一致的。
另外,可以计算出如果当前分配为 b,下一个结果 j >= i 的概率(即 b+1 到 i-1 的过程都不发生跳变)
P(j>=i) = (b+1)/i
最终优化为如下代码:
int32_t JumpConsistentHash(uint64_t key, int32_t num_buckets) {
    int64_t b = -1, j = 0;
    while (j < num_buckets) {
        b = j;
        key = key * 2862933555777941757ULL + 1;
        j = (b + 1) * (double(1LL << 31) / double((key >> 33) + 1));
    }
    return b;
}
这个算法依赖于伪随机算法的输出,特点有: 分布均匀性由伪随机算法决定,而跟 key 自身均匀性无关,实践均匀性远好于基于环的算法; 不需要对 key 进行计算,复杂度低,实践计算时间也比基于环的算法好。
算法没解决的问题:当中间减掉节点的时候,序号会发生变化,变为 0...n-1。因此需要在外面维护一个映射关系,动态映射到原先的节点上。