program

Build up a High Availability Distributed Key-Value Store

Preface

There are many awesome and powerful distributed NoSQL in the world, like Couchbase, MongoDB, Canssandra, etc. but developing a new one is still a challengeable, interesting and attractive thing for me, why?

  • It can satisfy our special needs for our cloud services.
  • We just need a key-value store, with some simple additional functionalities, we don’t need a complex solution.
  • We can control the whole thing, especially for fixing bugs and improvement.
  • Inventing the wheel is not good, but I can learn much in the process.

A key-value store may need below features:

  • Simple protocol.
  • Simple API.
  • High performance.
  • High availability.
  • Cluster support.

I knew this would be a hard journey first. But after a long hard work, I develop ledis-cluster, a key-value store based on LedisDB + xcodis + redis-failover.

Pre Solution Thinking

Before I develop ledis-cluster, I thought some other solutions which are valuable to be recorded here too.

MySQL

Aha, first I just wanted to use MySQL as a key-value store. This thought amazed my colleagues before, and I think now it may surprise many other guys too.

MySQL is a relational database and can be used as a key-value store easily and sufficiently. We can use a simple table to store key value data like below:

CREATE TABLE kv (
    k varbinary(256),
    v blob,
    PRIMARY KEY(k),
) ENGINE=innodb;

When I worked in Tencent game infrastructure department, we used this way to serve many Tencent games and it works well.

But I don’t want to use MySQL as a key-value store now, MySQL is a little heavy and needs some experienced operations people, this is impossible for our team.

Redis

Redis is an awesome NoSQL, it has an amazing performance, supports many useful data structures (kv, hash, list, set and zset), supplies a simple protocol for client user.

I have read the Redis’s code (it is very simple!) many times, used it for about three years in many productions, and I am absolutely confident of maintaining it.

But Redis has a serious problem: memory limitation. We can not store huge data in one machine. Using redis cluster is a good way to solve memory limitation, and there are many existing solutions, like official redis cluster, twemproxy or codis , but I still think another stuff saving huge data exceeding memory limitation in one machine is needed, so I develop LedisDB.

LedisDB

LedisDB is a fast NoSQL, similar to Redis. It has some good features below:

  • Uses Redis protocol, most of the Redis clients can use LedisDB directly.
  • Supports multi data structures(kv, hash, list, set, zset).
  • Uses rocksdb, leveldb or other fast databases as the backend to store huge data, exceeding memory limitation.
  • High performance, see benchmark. Although it is a little slower than Redis, it can still be used in production.

A simple example:

//start ledis server
ledis-server
//another shell
ledis-cli -p 6380
ledis> set a 1
OK
ledis> get a
“1"

As we see, LedisDB is simple, we can switch to it easily if we used Redis before.

LedisDB now supports rocksdb, leveldb, goleveldb, boltdb and lmdb as the backend storage, we can choose the best one for our actual environment. In our company projects, we use rocksdb which has a awesome performance and many configurations to be tuned, and I will also use it for the following example.

Data Security Guarantee

LedisDB can store huge data in one machine, so the data security needs to be considered cautiously. LedisDB uses below ways to guarantee it.

Backup

We can back up LedisDB and then restore later. Redis saving RDB may block service for some time, but LedisDB doesn’t have this problem. Thanks to rocksdb fast generating snapshot technology, backing up LedisDB is very fast and easy.

Binlog

LedisDB will first log write operations in binlog, then commit changes into backend storage, this is similar to MySQL.

Redis also has AOF, but the AOF file may grow largely, then rewriting AOF may also block service for some time. LedisDB will rotate binlog and write to the new one when current binlog is larger than maximum size (1GB).

Replication

An old saying goes like this: “don’t put all your eggs in one basket”. Similarly, don’t put all our data in one machine.

LedisDB supports asynchronous or semi-synchronous replication. We can not break CAP(Consistency, Availability, Partition tolerance) theorem, for replication, partition tolerance must exist, so we have to choose between consistency and availability.

If we want to guarantee full data security, we may use semi-synchronous replication, but most of time, asynchronous replication is enough.

Monitor and Failover

In the actual production environment, we use a master LedisDB and one or more slaves to construct the topology. We must monitor them in real time because any machine in the topology may be down at any time.

If a slave is down, we may not care too much, this is not a serious problem. But if the master is down (aha, a terrible accident!), we must resolve it quickly.

Generally, we can not expect the master to re-work quickly and infallibly, so electing a best new master from current slaves and doing failover is a better way when master is down.

Redis uses a sentinel feature to monitor the topology and do failover when the master is down. But this sentinel can not be used in LedisDB, so I develop another sentinel: redis-failover, monitoring and doing failover for Redis/LedisDB.

redis-failover uses ROLE command to check master and get all slaves every second. If the master is down, redis-failover will select the best slave from last ROLE returned slaves. The election algorithm is simple, using INFO command to get “slave_priority” and “slave_repl_offset” value, if a slave has a higher priority or a larger repliction offset with same priority, the slave will be elected as the new master.

redis-failover may have single point problem too, I use zookeeper or raft to support redis-failover cluster. Zookeeper or raft will elect a leader and let it monitor and do failover, if the leader is down, a new leader will be elected quickly.

Cluster

Although LedisDB can store huge data, the growing data may still exceed the capability of the system in the near future.

Splitting data and storing them into multi machines may be the only feasible way(We don’t have money to buy a mainframe), but how to split the data? and how to find the data by a key? I think an easy solution is to define a key routing rule (mapping key to the actual machine).

For example, we have two machines, n0 and n1, and the key routing rule is simple hash like crc32(key) % 2. For key “abc”, the calculation result is 0, so we know that the corresponding data is in n0.

The above solution is easy, but we can not use it in production. If we add another machine, the machine number is 3, all the old data mapping relationship will be broken, and we have to relocate huge amount of data.

Using consistency hash may be better, but I prefer using hash + routing table. We don’t map a key to a machine directly, but to a virtual node named slot, then define a routing table mapping slot to the actual machine.

Continuing with the example, assume we use 1024 slots and 2 machines, the slot and machine mapping is [slot0 — slot511] -> n0, [slot512 — slot1023] -> n1. For a key, first using crc32(key) % 1024 to get a slot index, then we can find the machine with this slot from the routing table.

This solution may be complex but have a big advantage for re-sharding. If we add another machine n2, change the routing table that mapping slot0 to n2, and we only need to migrate all slot0 data from n0 to n2. The bigger for slot number, the smaller for split data in a slot, and we only migrate little data for one slot.

xcodis uses above way to support LedisDB cluster. Now the slot number is 256, which is a little small that may increase the probability of mapping some busy keys into a slot.

Because of origin LedisDB db index implementation limitation, xcodis can not use bigger slot number than 256, so a better way is to support customizing a routing table for a busy key later. For example, for a key, xcodis should first try to find the associated slot in the routing table, if not found, then use hash.

Another radical choice is to change LedisDB code and upgrade all data saved before. This is may be a huge work, so I will not consider it unless I have no idea to resolve above problems.

xcodis is a proxy supporting redis/LedisDB cluster, the benefit of proxy is that we can hide all cluster information from client users and users can use it easily like using a single server.

In addition to proxy, there are also some other ways to support cluster too:

Official Redis cluster, but it is still in development and should not be used in production now, and it can not be used in LedisDB.
Customizing client SDK, the SDK can know whole cluster information and do the right key routing for the user. But this way is not universal and we must write many SDKs for different languages (c, java, php, go, etc.), a hard work!

Final Architecture

At last, the final architecture may look below:

kv architecture

  • Use LedisDB to save huge data in one machine.
  • Use Master/slave to guarantee data security.
  • Use redis-failover to monitor the system and do failover.
  • Use xcodis to support cluster.

This architecture may be not perfect, but is simple and enough for us. Now we have only use LedisDB and xcodis in our projects, not the whole architecture, but we have been testing and will try to deploy it in production in the near future.

Summary

Building up a key-value store is not a easy work, and I don’t think what I do above can beat other existing awesome NoSQLs, but it’s a valuable attempt, I have learned much and meet many new friends in the progress.

Now, I’am the only person to develop the whole thing and need help, if you have interested in what I do, please contact me, maybe we really can build up an awesome NoSQL. :-)

Mail: siddontang@gmail.com

Github: github.com/siddontang

构建高可用分布式Key-Value存储服务

前言

当我们构建服务端应用的时候,都会面临数据存放的问题。不同的数据类型有不同的存放方式,譬如关系型数据通常使用MySQL来存储,文档型数据则会考虑使用MongoDB,而这里,我们仅仅考虑最简单的kv(key-value)。

kv的使用场景很多,一个很典型的场景就是用户session的存放,key为用户当前的session id,而value则是用户当前会话需要保存的一些信息。因为kv的场景很多,所以选择一个好的kv服务就很重要了。

对于笔者来说,一个不错的kv服务可能仅仅需要满足如下几点就够了:

  • 协议简单
  • 高性能
  • 高可用
  • 易扩容

市面上已经有很多满足条件kv服务,但笔者秉着no zuo no die的精神,决定使用LedisDB + xcodis + redis-failover来构建一个高可用分布式kv存储服务。

现有解决方案

在继续说明之前,笔者想说说曾经考虑使用或者已经使用的一些解决方案。

MySQL

好吧,别笑,我真的说的是MySQL。MySQL作为一个关系型数据库,用来存储kv性能真心一点都不差。table的结构很简单,可能如下:

CREATE TABLE kv (
    k VARBINARY(256),
    v BLOB,
    PRIMARY KEY(k),
) ENGINE=innodb;

当我还在腾讯互动娱乐部门的时候,一些游戏项目就仅仅将MySQL作为kv来使用,譬如用来存放玩家数据,游戏服务器通过玩家id读取对应的数据,修改,然后更新。鉴于腾讯游戏恐怖的用户量,MySQL能撑住直接就能说明将MySQL作为一个kv来用是可行的。

不过不知道现在还有多少游戏项目仍然采用这种做法,毕竟笔者觉得,将MySQL作为一个kv服务,有点杀鸡用牛刀的感觉,MySQL还是有点重了。

Couchbase

Couchbase是一个高性能的分布式NoSQL,它甚至能支持跨data center的备份。笔者研究了很长一段时间,但最终并没有决定采用,主要笔者没信心去搞定它的代码。

Redis

Redis是一个高性能NoSQL,它不光支持kv,同时还提供了其他的数据结构如hash,list,set,zset供外部使用。

笔者在三年前就开始使用Redis,加之Redis的代码简单,很容易就能理解掌控。所以一直到现在,笔者都会优先使用Redis来存储很多非关系型数据。自然对于kv,笔者也是采用Redis来存放的。

但Redis也有一些不足,最大的莫过于内存限制,Redis存储的总数据大小最好别超过物理内存,不然性能会有问题。同时,笔者觉得Redis的RDB和AOF机制也比较蛋疼,RDB的时候系统可能会出现卡顿,而AOF在rewrite的时候也可能出现类似的问题。

因为内存的限制,所以Redis不能存储超大量的数据,为了解决这个问题,我们只能采用cluster的方案,但是Redis官方的cluster仍然处于开发阶段,并不能真正在生产环境中使用。所以笔者开发了LedisDB

LedisDB

开发LedisDB,主要就是为了解决Redis内存限制问题,它主要有如下特性:

  • 采用Redis协议,大部分Redis的client都能直接使用。
  • 提供类似Redis的API,支持kv,hash,list,set,zset。
  • 底层采用多种db存储实际数据,支持rocksdb(推荐),leveldb,goleveldb,boltdb,lmdb,没有Redis内存限制问题,因为将数据放到硬盘里面了。
  • 高性能,参考benchmark,虽然比Redis略慢,但完全可用于生产环境。

一个简单地例子:

//start ledis server
ledis-server 

//another shell
ledis-cli -p 6380

ledis> set a 1
OK
ledis> get a
"1"

可以看到,LedisDB非常类似Redis,所以用户能很方便的从Redis迁移到LedisDB上面。在实际生产环境中,笔者建议底层选择rocksdb作为其存储模块,它不光性能高,同时提供了很多配置方便用户根据特定情况进行调优(当然,理解这一堆配置可是一件很蛋疼的事情)。后续,笔者对于LedisDB的使用说明都会是基于rocksdb的。

数据安全

虽然LedisDB能存储大量数据,并且易于使用,但是作为一个数据存储服务,数据的安全性是一个非常需要考虑的问题。

  • LedisDB提供了dump和load工具,我们可以很方便的对其备份。在dump的时候,我们仅仅使用的是rocksdb的snapshot机制,非常快速,同时不会阻塞当前服务。这点可能是相对于Redis RDB的优势。虽然Redis的RDB在save的时候也是fork一个子进程进行处理,但如果Redis的数据量巨大,仍然可能造成Redis的卡顿。
  • LedisDB提供类似MySQL的binlog支持,任何操作都是写入binlog之后再最终提交到底层db的。如果服务崩溃,我们能通过binlog进行数据恢复。binlog文件有大小限制,当超过阀值之后,LedisDB会写入一个新的binlog中,而不是像Redis的AOF一样进行rewrite处理。
  • LedisDB支持同步或者异步replication,同步复制能保证数据的强一致,但是会牺牲系统的性能,而异步复制虽然高效,但可能会面对数据丢失问题。这其实就是一个CAP选择问题,在P(partition tolerance)铁定存在的情况下,选择C(consistency)还是选择A(availability)?通常情况下,笔者会选择A。

故障转移

在生产环境中,为了保证数据安全,一个master我们会通常配备一个或者多个slave(笔者喜欢将其称为replication topology),当master当掉的时候,监控系统会选择一个最优的slave(也就是拥有master数据最多的那个),将其提升为新的master,并且将其他slave指向该new master。这套流程也就是我们通常说的failover。

Redis提供了sentinel机制来实现整个replication topology的failover。但sentinel是跟redis绑定的,所以不能直接在LedisDB上面使用,所以笔者开发了redis-failover,一个能支持redis,或者LedisDB failover的sentinel。

redis-failover通过定期向master发送role命令来获知当前replication topology,主要是slaves的信息。当master当掉之后,redis-failover就会从先前获取的slaves里面选择一个最优的slave,提升为master,选择最优的算法很简单,通过info命令得到”slave_priority”和”slave_repl_offset”,如果哪个slave的priority最大,就选择那个,如果priority都一样,则选择replication offset最大的那个。

redis-failover会存在单点问题,所以redis-failover自身需要支持cluster。redis-failover的cluster在内部选举一个leader用来进行实际的monitor以及failover处理,当leader当掉之后,则进行重新选举。

现阶段,redis-failover可以通过外部的zookeeper进行leader选举,同时也支持内部自身通过raft算法进行leader选举。

分布式集群

随着数据量的持续增大,单台机器最终无法存储所有数据,我们不得不考虑通过cluster的方式来解决,也就是将数据放到不同的机器上面去。

要构建LedisDB的cluster,笔者考虑了如下三种方案,这里,我们不说啥hash取模或者consistency hash了,如果cluster真能通过这两种技术简单搞定,那还要这么费力干啥。

  • Redis cluster。

    redis cluster是redis官方提供的cluster解决方案,性能高,并且能支持resharding。可是直到现在,redis cluster仍处于开发阶段,至少笔者是不敢将其用于生产环境中。另外,笔者觉得它真的很复杂,还是别浪费脑细胞去搞定这套架构了。

  • 定制client。

    通过定制client,我们可以知道不同key的路由规则,自然就能找到实际的数据了。这方面的工作我的一位盆友正在进行,但定制client有一个很严重的问题在于所有的client都必须自己实现,其实不算是一个通用的解决方案。

  • Proxy

    记得有人说过,计算机科学领域的任何问题, 都可以通过添加一个中间层来解决。而proxy则是用来解决cluster问题的一个中间层。

    Twemproxy是一个很不错的选择,但是它不能支持resharding,而且貌似twitter内部也没在使用了,所以笔者并不考虑使用。

    本来笔者打算自己写一个proxy,但这时候,codis横空出世,它是一个分布式的proxy,同时支持resharding,并且在豌豆荚的生产环境中得到验证,笔者立刻就决定使用codis了。

    但codis并不支持LedisDB,同时为了满足他们自身的需求,使用的也是一个修改版的redis,鉴于此,笔者实现了xcodis,一个基于codis的,支持LedisDB以及原生redis的proxy。

架构

最终的架构如下。

kv architecture

我们通过使用LedisDB来解决了Redis单机数据容量问题,通过replication机制保证数据安全性,通过redis-failover用来进行failover处理,最后通过xcodis进行集群管理。

当前,这套架构并没有在生产环境中得到验证,但我们一直在内部不断测试,而且国外也有用户在帮助笔者验证这套架构,所以笔者对其还是很有信心的,希望能早日上线。如果有哪位童鞋也对这套架构感兴趣,想吃螃蟹的,笔者非常愿意提供支持。

我的2014

每到年末,都要进行一次总结了,看看今年都做了哪些事情,有啥提高,明年目标是什么样的,大概计划怎样。

家人

今年对我来说最大的两件事情就是我奶奶的离世以及我女儿的出生。我奶奶是在正月初一走的,可惜我没在旁边,因为要照顾快分娩的老婆。得到消息之后,立刻定了第二天的机票赶回老家送了奶奶最后一程。我爷爷是在五十年前的大年三十去世的,整整五十年零一天,真的像是冥冥中自有天注定似的。家里面突然少了一个人,陡然觉得心里空荡荡了许多。

在奶奶去世之后的二十五天,我的女儿出生了,家里面又多了一个人,当了爸爸,责任重了,压力大了。要考虑多多赚钱给她更好的生活了。

养育孩子真的是一件挺辛苦的事情,这里真的要感谢一下我的老婆,真的辛苦了。

等我女儿大了懂事了,我也会跟她说说她太奶奶的事情,虽然离她很遥远,但这是我宝贵的回忆。

技术

今年我仍然继续着我的开源之路,比较欣慰的是弄的LedisDB终于算是小有成功,被用在生产环境中,而且还有一些国外用户使用。不过比较让我郁闷的是这个东西在国内收到很多嘲讽,谩骂。后来跟一些中国其他开源作者交流了,发现几乎也都是这样。想想我们的心态也还真是好。

除了NoSQL,我还尝试在MySQL领域耕耘,于是就有了一个MySQL proxy,mixer,这个项目其实我很看好前景的,并且也有了很多关注,只是因为一些其他方面的原因我没有继续开发了。不过后来一些用户告诉我他们正在使用mixer里面的一些代码进行自己的proxy开发,这让我感到很欣慰。弄开源,最想的其实就是有人使用,得到认可。

当然今年还做了很多一些小的开源组件,其实也都是一直围绕着NoSQL,MySQL上面来做的。

今年在技术上面最大的感触就是要走出去,跟人多交流,站在别人的肩膀上面。自己造轮子固然好,但是有时候基于别人的好的东西再搭积木,没准更好。一个最好的例子就是xcodis,我一直想让LedisDB支持集群,然后也想到使用proxy的方式,只是一直没时间去弄,这时候豌豆荚开源了codis,而这个就是我需要的东西,于是我在codis的基础上面直接弄了一个支持LedisDB的集群proxy出来。

今年为了提升自己的算法水平,蛋疼的把leetcode上面的题目全做了一遍,虽然一些是google出来的,但也至少让自己学到了很多。现在也正在写一本Leetcode题解的书,希望明年能搞定,当然不是为了出版,只是为了更好的提升自己的算法水平。

分享

今年秉着走出去的原则,参加了两场分享会,一场是在腾讯的技术沙龙讲LedisDB,不过自己演讲水平太挫,效果特差。另一场是在珠海北理工讲web service的开发,忽悠了半天,后来发现,现在的学生盆友重点关注的是iOS,android这些的东西。

这两场分享让我锻炼了一下口才,其实还蛮不错的,如果明年有机会,也争取参加一下。

工作

今年的工作主要集中在推送服务器以及go服务重构上面,当时写push,设想的是能给整个公司提供推送服务,只是计划没有变化快,最后也成了一个鸡肋产品。

以前我们的服务是用openresty + python,随着系统越来越复杂,这套架构也有了很多弊端,所以我们决定使用go完全重构,现在仍在进行中。

今年整个公司变动挺大的,我的老大,一个11年的员工也出去创业了。想想比较唏嘘,当初面试我的两个老大,把我拉进来的都走了。

明年我的担子比较重,因为go的重构是我负责,明年铁定要上的,到时候很多问题都需要我来协调处理了。

展望

扯了这么多,其实发现2014年也没做什么,但就这么快速的过去了,那2015年了,干点啥呢:

  • 育儿,这个发现真挺重要的,孩子马上一岁了,逐渐懂事了,所以铁定要多花时间陪孩子了。
  • 继续开源,仍然用go开发,还是NoSQL和MySQL方向,没准MQ也会有。重点完成mysql proxy的后续开发。
  • 练习英语,现在跟外国盆友交流只敢打字,说话什么的那是不可能的。所以这方面一定要加强。奶爸推荐的一些学习方法还是挺不错的,要强迫自己学习。另外,争取看完全英文的哈利波特,或者其他入门级的英文读物。
  • 完成LeetCode题解这本书,前面已经说了,这是对我自己一段时间算法学习的总结。
  • 争取每周一篇文章,技术的,人文的都可以,今年零零散散写了一点,但还是不够。明年争取能在medium上面用英文多写几篇文章,今年只写了可怜的两篇。
  • docker,这玩意现在太火了,我也正在推进开发中使用docker,不过没准能在生产环境中也用到,需要好好研究。
  • 深入学习网络以及Linux底层知识,这方面要加强,现在只是知道大概,稍微深入一点就不懂了,做高性能服务器开发,得掌握。
  • 读书,不是技术书籍,争取每月读一本好书,小说也行,哲学,经济,历史都成。
  • 锻炼,好吧,现在身体太差,不能这样懒了。跑步也行,游泳也成,总之要动起来了。

最后

2014年就快过去了,马上迎来2015,希望明年越来越好。

学习zookeeper

最近研究了一下zookeeper(后续以zk简称),对于一个自认为泡在服务器领域多年的老油条来说,现在才开始关注zk这个东西,其实有点晚了,但没办法,以前的工作经历让我压根用不到这个玩意。只是最近因为要考虑做ledisdb的cluster方案,以及重新考虑mixer的协调管理,才让我真正开始尝试去了解zk。

什么是zookeeper

根据官网的介绍,zookeeper是一个分布式协调服务,主要用来处理分布式系统中各系统之间的协作问题的。

其实这么说有点抽象,初次接触zk,很多人真不知道用它来干啥,你可以将它想成一个总控节点(当然它能用多机实现自身的HA),能对所有服务进行操作。这样就能实现对整个分布式系统的统一管理。

譬如我现在有n台机器,需要动态更新某一个配置,一些做法可能是通过puppet或者salt将配置先分发到不同机器,然后运行指定的reload命令。zk的做法可能是所有服务都监听一个配置节点,直接更改这个节点的数据,然后各个服务就能收到更新消息,然后同步最新的配置,再自行reload了。

上面只是一个很简单的例子,其实通过它并不能过多的体现zk的优势(没准salt可能还更简单),但zk不光只能干这些,还能干更awesome的事情。网上有太多关于zk应用场景一览的文章了,这里就不详细说明,后续我只会说一下自己需要用zk解决的棘手问题。

架构

zk使用类paxos算法来保证其HA,每次通过选举得到一个master用来处理client的请求,client可以挂载到任意一台zk server上面,因为paxos这种是强一致同步算法,所以zk能保证每一台server上面数据都是一致的。架构如下:

                                                                     
                      +-------------------------------+                         
                      |                               |                         
              +----+--++          +----+---+        +-+--+---+                  
              | server |          | server |        | server |                  
              |        +----------+ master +--------+        |                  
              +--^--^--+          +----^---+        +----^---+                  
                 |  |                  |                 |                      
                 |  |                  |                 |                      
                 |  |                  |                 |                      
           +-----+  +-----+            +------+          +---------+            
           |              |                   |                    |            
           |              |                   |                    |            
      +----+---+        +-+------+         +--+-----+           +--+-----+      
      | client |        | client |         | client |           | client |      
      +--------+        +--------+         +--------+           +--------+

Data Model

zk内部是按照类似文件系统层级方式进行数据存储的,就像这样:

                        +---+             
                        | / |             
                        +++-+             
                         ||               
                         ||               
          +-------+------++----+-------+  
          | /app1 |            | /app2 |  
          +-+--+--+            +---+---+  
            |  |                   |      
            |  |                   |      
            |  |                   |      
+----------++ ++---------+    +----+-----+
| /app1/p1 |  | /app1/p2 |    | /app2/p1 |
+----------+  +----------+    +----------+

对于任意一个节点,我们称之为znode,znode有很多属性,譬如Zxid(每次更新的事物ID)等,具体可以详见zk的文档。znode有ACL控制,我们可以很方便的设置其读写权限等,但个人感觉对于内网小集群来说意义不怎么大,所以也就没深入研究。

znode有一种Ephemeral Node,也就是临时节点,它是session有效的,当session结束之后,这个node自动删除,所以我们可以用这种node来实现对服务的监控。譬如一个服务启动之后就向zk挂载一个ephemeral node,如果这个服务崩溃了,那么连接断开,session无效了,这个node就删除了,我们也就知道该服务出了问题。

znode还有一种Sequence Node,用来实现序列化的唯一节点,我们可以通过这个功能来实现一个简单地leader服务选举,譬如每个服务启动的时候都向zk注册一个sequence node,谁最先注册,zk给的sequence最小,这个最小的就是leader了,如果leader当掉了,那么具有第二小sequence node的节点就成为新的leader。

Znode Watch

我们可以watch一个znode,用来监听对应的消息,zk会负责通知,但只会通知一次。所以需要我们再次重新watch这个znode。那么如果再次watch之前,znode又有更新了,client不是收不到了吗?这个就需要client不光要处理watch,同时也需要适当的主动get相关的数据,这样就能保证得到最新的消息了。也就是消息系统里面典型的推拉结合的方式。推只是为了提升性能,快速响应,而拉则为了更好的保证消息不丢失。

但是,我们需要注意一点,zk并不能保证client收到消息之后同时处理,譬如配置文件更新,zk可能通知了所有client,但client并不能全部在同一个时间同时reload,所以为了处理这样的问题,我们需要额外的机制来保证,这个后续说明。

watch只能应用于data(通过get,exists函数)以及children(通过getChildren函数)。也就是监控znode数据更新以及znode的子节点的改变。

API

zk的API时很简单的,如下:

  • create
  • delete
  • exists
  • set data
  • get data
  • get chilren
  • sync

就跟通常的文件系统操作差不多,就不过多说明了。

Example

总的来说,如果我们不深入zk的内部实现,譬如paxos等,zk还是很好理解的,而且使用起来很简单。通常我们需要考虑的就是用zk来干啥,而不是为了想引入一个牛的新特性而用zk。

Lock

用zk可以很方便的实现一个分布式lock,记得最开始做企业群组盘的时候,我需要实现一个分布式lock,然后就用redis来弄了一个,其实当时就很担心redis单点当掉的问题,如果那时候我就引入了zk,可能就没这个担心了。

官方文档已经很详细的给出了lock的实现流程:

  1. create一个类似path/lock-n的临时序列节点
  2. getChilren相应的path,注意这里千万不能watch,不然惊群很恐怖的
  3. 如果1中n是最小的,则获取lock
  4. 否则,调用exists watch到上一个比自己小的节点,譬如我现在n是5,我就可能watch node-4
  5. 如果exists失败,表明前一个节点没了,则进入步骤2,否则等待,直到watch触发重新进入步骤2

Codis

最近在考虑ledisdb的cluster方案,本来也打算用proxy来解决的,然后就在想用zk来处理rebalance的问题,结果这时候codis横空出世,发现不用自己整了,于是就好好的研究了一下codis的数据迁移问题。其实也很简单:

  1. config发起pre migrate action
  2. proxy接收到这个action之后,将对应的slot设置为pre migrate状态,同时等待config发起migrate action
  3. config等待所有的proxy返回pre migrate之后,发起migrate action
  4. proxy收到migrate action,将对应的slot设置为migrate状态

上面这些,都是通过zk来完成的,这里需要关注一下为啥要有pre migrate这个状态,如果config直接发起migrate,那么zk并不能保证proxy同一时间全部更新成migrate状态,所以我们必须有一个中间状态,在这个中间状态里面,proxy对于特定的slot不会干任何事情,只能等待config将其设置为migrate。虽然proxy对于相应slot一段时间无法处理外部请求,但这个时间是很短的(不过此时config当掉了就惨了)。config知道所有proxy都变成pre migrate状态之后,就可以很放心的发送migrate action了。因为这时候,proxy只有两种可能,变成migrate状态,能正常工作,仍然还是pre migrate状态,不能工作,也自然不会对数据造成破坏。

其实上面也就是一个典型的2PC,虽然仍然可能有隐患,譬如config当掉,但并不会对实际数据造成破坏。而且config当掉了我们也能很快知晓并重新启动,所以问题不大。

总结

总的来说,zk的使用还是挺简单的,只要我们知道它到底能用到什么地方,那zk就真的是分布式开发里面一把瑞士军刀了。不过我挺不喜欢装java那套东西,为了zk也没办法,虽然go现在也有etcd这些类zk的东西了,但毕竟还没经受过太多的考验,所以现在还是老老实实的zk吧。

深入浅出Web Service

红薯的邀约,决定给某大学的童鞋讲讲Web Service相关知识,鉴于是第一次在学校献丑,所以还是老老实实的准备,先把类似逐字稿的东西写出来,然后在准备PPT吧。

关于Web service,这个话题太广太泛,加之我也只熟悉一些特定的领域,所以准备从两方面入手,1,什么是Web service,就当是概念性的介绍,让大家有个相关认识。2,则是根据一个简单的例子,告诉大家如何构建一个Web service服务。

什么是Web service

首先根据Wiki的定义:A Web Service is a method of communication between two electronic devices over a network.

简单来说,Web Service就是基于网络不同设备之间互相通信的一种方式。Web Service是一个软件服务,它提供很多API,而客户端通过Web协议进行调用从而完成相关的功能。

Web service并不是一个新奇的概念,相反从很早的分布式计算,到网格计算,到现在的云,都或多或少的有着Web service的影子。只不过随着近几年一浪高过一浪的互联网热潮以及Google,Amazon等公司的大力推动,Web service变得愈发流行。

越来越多的公司开始提供Web service,而同时又有更多的公司基于这些Web service提供了更加上层的Web service。

Amazon的S3(Simple Storage Service)是一个文件存储服务,用户通过S3将文件存放到Amazon的服务器上面,Amazon负责保证该文件的安全(包括不被别人获取,不丢失等)。而Drew Houston则在S3的基础上,构造了一个令人惊奇的同步网盘:Dropbox,同时,Dropbox又将相关API提供出去,供其他的Application其同步服务。

可以看到,正是因为有了越来越多的Web services,才让我们现在的互联网生活变得越发精彩。

实现一个简单的Web service

好了,上面扯了这么多,是不是心痒痒想自己开发一个Web service?开发一个Web service并不是那么容易的事情,尤其是涉及到分布式之后。不过我觉得一个小例子没准就能说明很多东西。当然我自认并不是Web service的专家(这年头专家架构师太多,我只能算打酱油的),很多东西难免疏漏,并且一些设计也会带有很强烈的个人色彩,如果大家有啥更好的认识,欢迎跟我讨论(妹子优先!)。

一个简单的例子:KV Storage Service,后面就叫KV吧。类似于S3,只是我们不是存文件,而是元数据。后面我们就用KV来表明服务的名字吧。

对于KV来说,它只会涉及到三种操作,如果用代码表示如下:

//根据指定的key获取对应的value
Get(key)

//设置key的值为value,如果key本来存在,则更新,否则新建
Put(key, value)

//删除key
Delete(key)

交互协议

既然是Web service,自然选用HTTP来做交互,比起自己实现一套不通用的协议,或者使用Google Protocol Buffers这些的,HTTP具有太多的优势,虽然它的性能稍微有点差,数据量稍微有点臃肿,但几乎所有的浏览器以及数不清的库能直接支持,想想都有点小激动了。

所以我们唯一要做的,就是设计好我们的API,让外面更方便的使用Web service。

API

根据wiki的定义,Web service通常有两种架构方式,RESTful和Arbitrary(RPC,SOAP,etc)。

REST是Representational state transfer的缩写,而满足REST架构模型的我们通常称之为Restful:

  • 使用URI来表示资源,譬如http://example.com/user/1 代表ID为1的user。
  • 使用标准HTTP方法GET,POST,PUT,DELETE等来操作资源,譬如Get http://example.com/user/1 来获取user 1的信息,而使用Delete http://example.com/user/1 来删除user 1。
  • 支持资源的多种表现形式,譬如上例Get中设置Content-Type为json,让服务端返回json格式的user信息。

相对于Restful,另一种则是Arbitrary的,我不熟悉SOAP,这里就以RPC为例。

RPC就是remote procedure call,它通过在HTTP请求中显示的制定需要调用的过程名字以及参数来与服务端进行交互。仍然是上面的例子,如果我们需要得到用户的信息,可能就是这样 Get http://example.com/getuser?userid=1, 如果要删除一个用户,没准是这样Get http://example.com/delUser?userid=1

那选择何种架构呢?在这里,我倾向使用Restful架构模型,很大原因在于它理解起来很容易,而且实现简单,而现在越来越多的Web service提供的API采用的是Restful模式,从另一个方面也印证了它的流行。

所以这个Web service的接口就是这样了:

GET http://kv.com/key
DELETE http://kv.com/key
POST http://kv.com/key -dvalue
PUT http://kv.com/key -dvalue

上面POST和PUT可以等价,如果key存在,则用value覆盖,不存在则新建。

架构

好了,扯了这么多,我们也要开始搭建我们的Web service了。因为采用的是HTTP协议,所以我们可以直接使用现成的HTTP server来帮我们处理HTTP请求。譬如nginx,apache,不过用go或者python直接写一个也不是特别困难的事情。

我们还需要一个storage server用来存放key-value,mysql可以,redis也行,或者我的ledisdb,谁叫红薯说可以打广告的。

最开始,我们就只有一台机器,启动一个nginx用来处理HTTP请求,然后启动一个ledisdb用来存放数据。然后开始对外happy的提供服务了。

KV开始工作的很好,突然有一天,我们发现随着用户量的增大,一台机器处理不过来了。好吧,我们在加一台机器,将nginx和ledisdb放到不同的机器上面。

可是好景不长,用户量越来越多,压力越来越大,我们需要再加机器了,因为nginx是一个无状态的服务,所以我们很容易的将其扩展到多台机器上面去运行,最外层通过DNS或者LVS来做负载均衡。但是对于有状态的服务,譬如上面的ledisdb,可不能这么简单的处理了。好吧,我们终于要开始扯到分布式了。

CAP

在聊分布式之前,我们需要知道CAP定理,因为在设计分布式系统的时候,CAP都是必须得面对的。

  • Consistency,一致性
  • Avaliability,可用性
  • Partition tolerance,分区容忍性

CAP的核心就在于在分布式系统中,你不可能同时满足CAP,而只能满足其中两项,但在分布式中,P是铁定存在的,所以我们设计系统的时候就需要在C和A之间权衡。

譬如,对于MySQL,它最初设计的时候就没考虑分区P,所以很好的满足CA,所以做过MySQL proxy方面工作的童鞋应该都清楚,要MySQL支持分布式是多么的蛋疼。

而对于一般的NoSQL,则是倾向于采用AP,但并不是说不管C,只是允许短时间的数据不一致,但能达到最终一致。

而对于需要强一致的系统,则会考虑牺牲A来满足CP,譬如很多系统必须写多份才算成功,

Replication

对于前面提到的Ledisdb,因为涉及到数据存放,本着不要把鸡蛋放到一个篮子里面的原则,我们也不能将数据放到一台机器上面,不然当机了就happy了。而解决这个的办法就是replication。

熟悉MySQL或者Redis的童鞋对replication应该都不会陌生,它们的replication都采用的是异步的方式,也就是在一段时间内不满足数据一致性,但铁定能达到最终一致性。

但如果真想支持同步的replication,怎么办呢?谁叫我们容不得数据半点丢失。通常有几种做法:

  • 2PC,3PC
  • Paxos,Raft

因为这方面的坑很深,就不在累述,不过我是很推崇Raft的,相比于2PC,3PC,以及Paxos,Raft足够简单,并且很好理解。有机会在说明吧。

水平扩展

好了,通过replication解决了ledisdb数据安全问题,但总有一天,一台机器顶不住了,我们要考虑将ledisdb的数据进行拆分到多台机器。通常做法如下:

  • 最简单的做法,hash(key) % num,num是机器的数量,但这种做法在添加或者删除机器的时候会造成rehash,导致大量的数据迁移。
  • 一致性hash,它相对于传统的hash,在添加或者删除节点的时候,它能尽可能的少的进行数据迁移。不过终归还是有数据流动的。
  • 路由映射表,不同于一致性hash,我们在外部自己负责维护一张路由表,这样添加删除节点的时候只需要更改路由表就可以了,相对于一致性hash,个人感觉更加可控。

我个人比较喜欢预分配+路由表的方式来进行水平扩展,所谓预分配,就是首先我就将数据切分到n个(譬如1024)shard,开始这些shard可以在一个node里面,随着node的增加,我们只需要迁移相关的shard,同时更新路由表就可以了。这种方式个人感觉灵活性最好,但对程序员要求较高,需要写出能自动处理resharding的健壮代码。

好了,解决了replication,解决了水平扩展,很长一段时间我们都能happy,当然坑还是挺多的,遇到了慢慢再填吧。

没有提到的关键点

  • Cache,无论怎样,cache在服务器领域都是一个非常关键的东西,用好了cache,你的服务能处理更多的并发访问。facebook这篇paper Scaling Memcache at Facebook专门讲解了相关知识,那是绝对的干货。
  • 消息队列,当并发量大了之后,光靠同步的API调用已经满足不了整个系统的性能需求,这时候就该MQ上场了,譬如RabbitMQ都是不错的选择。
  • 很多很多其他的。。。。。。。这里就不列举了。

总结

上面只是我对于Web service一点浅显的见解,如果里面的知识稍微对你有用,那我已经感到非常高兴了。但就像实践才是检验真理的唯一标准一样,理论知道的再多,还不如先弄一个Web service来的实在,反正现在国内阿里云,腾讯云,百度云啥的都不缺,缺的只是跑在上面的好应用。

我的编程语言经历

Alan Perlis 说过:“一种不改变你编程的思维方式的语言,不值得去学。”,虽然写了这么多年程序,用了这么多的语言,但我自认还没悟道编程语言如何改变我的思维方式。

几天前,我需要用python来为ledisdb写一个客户端,我突然发现,对于c++,go这种语言,我如果需要实现一个功能,首先想到的是问题是代码应该怎么写。但是当我使用python的时候,我首先考虑的问题是在哪里去找一个库用来解决我的问题。可能这就是使用不同语言带给我的不同思考方式吧。

我的编程语言经历没有那么复杂,没用过很多,但是其实也够我受的了,尤其是在不同语言语法糖之间切换的时候,有种让人崩溃的感觉。没准我应该升级一下我的大脑cpu,使其能够更快速的进行中断处理。

c

我是从大学才开始学习编程的,相比现在的小朋友来说,可以叫做输在了起跑线上面。谁叫以前生活在山区,没机会接触电脑这玩意。

我的第一门编程语言是c,不同于很多童鞋使用的谭浩强神书,我用的是周纯杰的<>,不知道每年有多少同学受到过它的摧残,当然还有那哥们蹩脚的普通话。

在大学里面,很多同学的c的毕业设计都是我帮其完成,但我始终觉得自己仍然是个半吊子,除了c的基础稍微强一点之外,很多方面譬如操作系统,算法等完全不会。(现在随着工作年限的增加让我越发后悔,当初怎么就不稍微学点这些知识,尤其是编译原理。)

我几乎没怎么用c开发过项目,只在tencent可怜的维护过别人的c项目,但至少能看懂c代码,这就够了。

因为大多数时候,我用的是c++,而不是c来解决我的问题。

c++

c++是我工作使用的第一门语言,也是我使用时间最长的一门语言,都七年之痒了,不过还是有点不离不弃的。

以前上学的时候有一句口头禅,叫学好c++,走遍天下都不怕。但是有几个人能把它学好的?所以千万别说自己精通c++,那会被人鄙视的。

我使用c++可以分为三个阶段:

类c阶段

这个阶段主要是我第一份工作的时候,那时候才毕业,c的烙印很深,面向对象除了有个概念,真正是啥完全不知道。所以最喜欢的方式还是写着一堆函数来解决问题,当初VIA身边那帮c++的牛人竟然能忍受我这样的代码,真佩服他们。

面向对象阶段

后来去了第二家公司linekong,开始做游戏,才开始真正意义上的用c++写代码了。

记得最开始去第一家公司面试的时候,被问了啥是面向对象,当时不假思索的答了继承,多态和封装。

啥叫封装?整一个class,把该包的都包了,一个同事曾告诉我,他见过有几万行代码的class,看来我这个几千行的太小儿科了。

啥叫继承?先来一个父类,干脆叫bird,有一个fly方法,再来一个子类,叫duck吧,继承了bird,不过duck会fly吗?一个父类不够,再来一个,搞个多重继承,什么?出现了菱形继承,那干脆在来一个virtual继承得了。

啥叫多态?不就是virtual function,然后父类指针能在运行时根据实际情况调用相应的子类实现。那c++的多态是怎么实现的?看看<<深度探索c++对象模型>>不就行了。

这段时间,可以算是我写c++代码最多的时候,都快写到吐了,尤其还要忍受那龟速的编译。我们竟然都实现了直接通过汇编改c++的虚表,使其调用自己的函数这种变态的东西。在那时候我就得出结论,如果不想早死,尽量别用这个东西写代码。可是到如今我都在不停的慢性自杀。

现代C++阶段

不知道从什么时候开始,我突然觉得我应该来点modern c++的编写方式了,c++0x都出了,还不玩一下就晚了。当然新特性那么多,不可能全部都拿来用的,Bjarne Stroustrup貌似都说过,c++0x应该算是另一门语言了。

于是哥就走上了伪modern c++之路,class还是需要的,不然数据怎么封装。继承吗,比重减轻吧,最好采用面向接口的编程方式。而多态,能不用就不用吧,反而觉得bing + function的方式实现的delegate模型反而更方便,没准也更酷哟。

shared_ptr,weak_ptr是需要用的了,c++没有gc,所以一套好的内存管理方式是必不可少的,每次new之后还要记得delete也比较烦,最严重的是可能忘记那就内存泄露了。

于是,我就自认为我进化了,最典型的例子就是我写的高性能网络库libtnet,感觉很modern了。

lua

最开始知道lua,是云风那本编程感悟的书,当时可是菊花一紧,觉得这东西是啥,为什么能跟c结合到一起使用?

后来自己开发游戏了,才发现lua真的是一门很强大的语言,短小精悍,嵌入简单,性能超强,完全是作为游戏脚本语言开发的不二人选。不过我最开始使用lua的经历不怎么happy,最开始就是接手了一个c++与lua的粘合层库,关于这个库的传说,见这里Lua 不是 C++。后来,在踩了无数个坑,填了无数个坑之后,我终于弄得相对稳定了。貌似现在我以前的同事还在使用,不过正如我在lua c函数注册器中说明的那样,对于语言的交互,简单一点才好。现在以前做的游戏已经开源,见这里,那个传说中的蛋疼粘合层也见了世面。当然,我可不会告诉你们好多搓代码是我写的。

后来,在现在的公司,因为项目的需要,我们急需解决python的很多性能大坑问题,于是我开始推广使用openresty,一个用lua包裹的nginx,用了之后,腰不痛了,腿不痛了,性能妥妥的。

因为lua,我第一次尝到了在代码里面直接写配置的便捷,用一个table搞定,相比起来,c++处理ini,json这些的弱爆了。另外,动态语言的热更新机制使其代码升级异常方便,不过你得非常小心lua的闭包,没准你重新加载了代码运行还是老样子。

lua是一个动态语言,所以不用我们管内存释放问题,但是仍然可能会有引用泄露,在开发游戏的时候,为了解决我们程序lua内存泄露的问题,我曾经干过直接从_G递归遍历,扫描整个lua数据的事情。相比在c++使用valgrind这些程序的工具,lua配套的东西还是太小儿科了。

lua的调试也是一个大头问题,我曾今写过几个lua的调试器,例如这个,甚至都支持了类似gdb那样ctrl+c之后动态的设置断点,可是仍然没觉得比gdb方便,所以多数时候,我都是写log为主。

python

虽然小时候吃过很多蛇,但是蟒蛇可是从来没吃过的,现在看来python味道还不错。

我是来了kingsoft之后才开始正式使用python的。对于为啥使用python,我曾跟拉我进来的技术老大讨论过,他直接就说,开发快速,上手容易。

python开发的快速很大程度是建立在丰富的第三方库上面的,我们也使用了很多库,譬如tornado,gevent,django等,但是正如我最开始说的,因为我们有太多的选择,面对一个问题的时候,往往考虑的是如何选择一个库,而不是自己如何实现,这其实在某种程度上面使得很多童鞋知其然而不知其所以然。这点,lua可能是另一个极端,lua的定位在于嵌入式和高性能,所以自然地,你得自己动手造轮子(当然,现在也有很多好的第三方库了),虽然有时候写起来很不方便,但是至少自己很清楚程序怎么跑的。

当然,我绝对没有贬低python的意思,我很喜欢这门语言,用它来开发了很多东西,同时也知道很多公司使用python构建了很多大型稳定的系统(我们的产品应该也算吧)。

只是现在我越发觉得,看起来简单的语言,如果没有扎实的基本功底,写出来的东西也很烂,而python,恰恰给人放了一个很大的烟雾弹,你以为它有多容易,其实它是在玩你。

go

好了,终于开始说go了,let’s go!!!

我使用go的历史不长,可能也就一年多,但是它现在完全成了我最爱的语言,go具有了python开发的迅速,同时也有着c运行的性能。(当然,还是有差距的!)

网上有太多的语言之争,包括go,有人恨,有人爱。但萝卜白菜,各有所爱,对于我来说,能帮我解决问题,让我用着舒服的语言就是好语言。

go非常适用于服务端程序开发,比起用c++开发,我陡然觉得有一种很幸福的感觉,譬如对于网络编程,在c++里面,我需要自己写epoll的事件处理,而且这种异步的机制完全切分了整个逻辑,使得代码不怎么好写,我在开发libtnet的时候感触尤其深刻。但是在go里面,因为天生coroutine的支持,使得异步代码同步化了,非常利于代码的编写。

现在我的主要在项目中推动go的使用,我们已经用go搭建了一个高性能的推送服务器,后续还有几个系统会上线,而且开发的进度并不比使用python差,另外也很稳定,这让我对go的未来充满了期待。

我也用go写了很多的开源程序,也算是拿的出手了,譬如:

  • ledisdb:一个基于leveldb的提供类似redis高级数据结果的高性能NoSQL,真挺绕口的,简单点就是一个高性能NoSQL。
  • Mixer:一个mysql-proxy,现在支持通用的mysql命令代理,读写分离,以及自动主备切换。后续将要参考vitess支持分区,为此一直在恶补编译原理的知识。
  • go-log:一个类似python log模块的东西,支持多种handler,以及不同的log级别。

还有一些,可以参考我的github,譬如moonmq(一个高性能push模型的消息服务器),polaris(一个类似tornado的restful web框架),因为go,我开始热衷于开源了,并且认识了很多的好基友,这算得上一个很大的收获吧。

其它

好了,说完了上面我的长时间使用语言(至少一年以上),我也用了很多其他的语言,现在虽然使用时间比较短,但不排除后续会长久使用。

Objective-C

因为我家终于有了苹果三件套,所以决定开发app了,首要的任务就是得学会使用Objective-C。我承认这真是一门奇葩的语言,如果没有xcode的自动补齐,我真不知道写起来是神马样的感觉。

而且第一次见到了一门推荐写函数名,变量名就像写文章的语言,至少我写c++,go这些的一个函数名字不会写成一个句子。

我现在在自学iOS的开发,慢慢在整出点东西,毕竟答应给老婆的iphone做点啥的。后续干脆就写一个《小白学iOS》系列blog吧(如果我有精力!),反正对于iOS,我真是一个小白。

java

好吧,我也在android上面写过程序,build到我的S3上面去过,对于java,我的兴趣真不大,貌似自己还买过两本《java编程思想》,那时候脑袋铁定秀逗了。

但是不得不承认,java在服务器领域具有非常强的优势,很多很多的大企业采用java作为其服务器的编程语言。典型的就是淘宝,据传杭州的很多软件公司都不用java的,你用java就等于给淘宝培养人才了。(不过我发现他们很多基础组件譬如TFS这些的可是c++的哟!)

java是门好语言,只是我个人不怎么喜欢,可能我就是太小众了,只对c语言体系的感兴趣。所以很多公司我去不了,哈哈!

erlang

受《计算机程序的构造与解释》影像,我一直想学一门函数式编程语言,最开始玩的是elisp,谁叫以前我是个深度的emacser(后来竟然变成了一个vimer,再后来就是sublimer,这世界真神奇)。

后来还是决定好好学习一下erlang,也第一次领略到了函数式编程的魅力。自己唯一用erlang开发过的东西就是bt下载的客户端,不过后来发现用处不大就没继续进行下去了。(好吧,我承认当时想下岛国的东西)

学习erlang之后最大的优势在于现在能看懂很多优秀的erlang项目,譬如我在做moonmq以及公司的推送服务的时候,研究了rabbitmq,这玩意可是用erlang写的,而我竟然还能看懂,太佩服我了。

还有么?

想想自己还学了哪些语言?貌似没了,不知道awk算不算一门。看来我会得语言真不多。

后续可能会学的

逆水行舟,不进则退,计算机发展这么迅速,我也需要不断提升自己,其中学习一门新的语言可能是一个很好的提升途径,至少能为我打开一扇门。譬如,如果掌握了日文,就能更好的理解岛国片的精髓。我不会日文,所以还是个门外汉。

ruby

ruby是一门很优雅的语言,很多大神级别的人物推荐,github貌似也是ruby的幕后推手。

因为ROR的兴起,使得ruby更加流行。看来,一个好的框架库对于语言的推广帮助真挺大的。相比而言,python有django,tornado等,光选择适合自己的就得费点时间。

ruby可以算是一门完全面向对象的语言,连number这种的都是对象,而且看了几本Matz的书,觉得这哥们挺不错的,对技术的感悟很深,所以更让我有兴趣去了解ruby了。

javascript

作为一个技术人员,没一个自己的个人网站怎么行,我的阿里云都是包年买的(明年还是买国外的vps吧),自己的个人站点还无影无踪。

好吧,我完全不会javscript,看着css就头疼,没准我从一开始想自己写代码搭建个人站点这个步子就迈的太大,扯着蛋了。如果先用一个开源的框架搭建起来,再自己调整完善,可能会更好。但无论怎样,javascript这门语言还是要学习了解的,尤其是以后随着html5的流行,加之node.js疯狂流行,这门语言也会愈发的发光发热。

C Sharp

其实本来不准备跟ms的东西扯上关系的,虽然vs是一个很强大的开发工具,但是我自从换成mac之后就不准备再迁回windows。

只是c#我可能是必须要学会的,因为那个坑爹的unity3d,虽然unity3d也提供了其它语言的支持(譬如伪javascript),但是大量的开发者还是选用了c#,至少在中国我问过很多朋友,都妥妥的用c#,既然这样,我也只能考虑学习使用了。

至于我为啥蛋疼的想玩unity3d,毕竟干了很多年游戏开发,一直有自己弄一个简单小游戏的梦想,还是妥妥的unity3d吧。

自己造一个?

语言千千万,我不可能全部学会的,而且以后没准因为业务的需要,没准都会自己造一门语言,也就是DSL。不过这个貌似还离我比较遥远,编译原理的东西太差了(说多了都是泪呀)。自己写词法分析还成,后面就菜了。这也是Mixer一直没啥进展的原因。不过已经买了龙书,在学习屠龙秘籍,希望成为顶尖高手吧。

后记

写了这么多,看来随着年岁的增加,越来越啰嗦了。不是有一句古话:吾生也有涯,而知也无涯 。以有涯随无涯,殆已。不过不停地追逐不也是乐趣一件?

只是,现在我首先要做的就是向我老婆申请资金升级电脑了吧!

谈谈自己造轮子

写下这篇文章,主要是对我近段时间工作的反思。

为啥要造轮子

对于一些程序员来说,喜欢自己造轮子可算是一个很平常的事情,我想可能有如下原因:

  • 对于一些小的功能,不需要借助外部库,直接能够自己写完搞定。
  • 对于一些大的功能,很多外部库不能很好的与自己项目整合,有时候还不如自己写一个。
  • 有时候即使能用的外部库,因为程序员相轻的思想,就觉得自己写的nb,不用。
  • 还有可能就是想深入学习某一个知识点,自己动手造一个。

我不觉得造轮子不好,曾今很长一段时间我都认为造轮子是体现自己能力很好的一种方式,但是现在越来越觉得,不要过分的去造轮子。

造轮子的教训

昨天,我需要对接amazon s3的存储,官方没有go语言的sdk,所以我就动了自己写一个的想法,即使我知道铁定有第三方的实现。

amazon s3的接口因为都是restful形式,同时签名机制已经非常熟悉(国内的存储服务接口几乎都按照这套设计的,除了几个奇葩的公司),所以我就开始写了,写完了之后,我在看一个第三方的goamz实现,发现跟我相差不了多少,一下子碉堡了。

我真的叫做没屁事了,这么浪费时间。

造还是不造?

很多时候,我们要克服自己造轮子的冲动。

对于一个需要实现的功能,我觉得首先可以考虑该需求是否有成熟的解决方案,如果有,使用的成本有多大?

譬如对于python来说,如果我们现在需要一套web框架,如果自己去写,而不用成熟的tornado,django这些的,我真觉得蛋疼了。而对于go来说,我到现在还没发现我们用起来趁手的web框架,所以就有了自己造的polaris

如果项目进度有压力,多数时候我都倾向于通过集成外部解决方案来实现,而不是费时的自己去造轮子,因为这时候体现你能力的不是你写了多少nb的东西,而是将程序运行起来,提供服务。当然,你也不能找太挫的,或者完全无法驾驭的程序,那样后面就够你疼了。

即使真的需要自己去写一个轮子,还需要不停的问自己:我这个轮子稳定性如何,能否高效的运行,后续随着需求的变更我能不能很容易的掌控?如果发现自己搞不定,还是求助一下别人比较好。

如果我对某一个知识点特别有兴趣,想去深入研究并且有时间,那我觉得自己造几个轮子也算是很不错的事情。譬如我前段时间想重新深入了解网络编程,虽然有libev这些好用的开源库,我也基于他们封装了很多东西,譬如tnettpush,但是我仍然自己写了一个libtnet,写完了,我才有“啊哈,原来是这样“这种豁然开朗的感觉。

写在后面

随着现在github这类网站的流行,找到高质量的第三方实现已经变成一件很容易的事情,作为一个程序员,不能固步自封,总认为我自己写的才是好的,有时候“自己动手,丰衣足食”这种想法反而会累死自己。

今天刚好看到了一篇文章一位码农的几点思考,里面的观点我很赞同,与大家共勉。

我的2013

前言

每年到这个时候,总需要回顾过去,展望未来。2013这一年学到了很多东西,收货了很多,也成长了很多。主要在技术和生活上面,让自己有了记录一下的冲动。

技术

在技术上面,这一年接触了很多新的东西,让自己眼界开阔不少,同时也开始自我提升,疯狂的在github上面玩开源,只是很多都惨不忍睹。

Openresty

今年最开始接触的新东西就是openresty,一个集成nginx的web应用开发框架。

最开始,我们项目的架构采用的是前端nginx做proxy,然后将请求反向代理到后端python tornado的方式实现的。但是随着压力的增加以及一些新功能的上线,这套架构开始显现其局限性,首当其冲的在于慢,虽然可以通过增加tornado进程的方式来进行负载均衡处理,但我总觉得不是长久之计。

同时,随着业务逻辑的复杂,一些操作需要多个service协同完成,然后在返回给用户,而在什么地方组织多个service的数据以及逻辑也成了我们的一个难题。

鉴于上述几个原因,我开始研究openresty,因为之前有3年多lua开发的经验,所以非常容易上手。同时,通过研读openresty的一整套源码,真正的了解了nginx以及之上的openresty的运行机制。可以这么说,这段时间我从一个连nginx配置都不会写的小白程序员一跃成为了名义上精通nginx开发的屌丝程序员。

自然,我开始在项目中推广openresty,这也得到了大家的支持,现在虽然我们很多代码仍然是使用python在编写,但是对于很多高性能模块我们已经逐步转向了openresty。

在使用openresty的时候,还提交了几个bug,这点颇为自豪,同时也写了一些东西,譬如 Introduction To Nginx

Go

接触go纯属偶然,在上半年终于完结了一个持续时间特长的项目之后,整个组的童鞋都陷于一个无事可干的真空期,也就是在这段时间,第一次学习了go,立刻就被它的简单强大所吸引,尤其是在服务器并发编程方面,那可是非常的强悍。

于是,我带着两个完全不懂go的童鞋开始了我们推送服务器的编程之旅。最开始的时候,因为两位童鞋只会python,为了尽快的出功能,一些后台的服务采用tornado搭建,而我用go写了挂载大量长连接的comet服务。

这里不得不说go开发服务的快捷,在goroutine以及channel的机制下面,没有了层层的callback,没有了死锁,我只用了3天就弄出了comet,而且能持续稳定运行。

鉴于用go成功开发了comet,我让另外两个童鞋也开始用go重构先前写的python逻辑,进展也很顺利。

不过对于我来说,go现在最大的一个问题在于内存占用,go现在默认的stack大小为8k,对于需要挂载百万连接的comet来说,内存开销实在太大,虽然现在机器的配置完全不需要我担心,但总觉得有点不爽。不过如果优化,也是后续的事情了。

今年,对于我来说,竟然吃了两次螃蟹,第一个就是openresty,而第二个就是go,而且很幸运的是都能在项目中实施。

Libtnet

今年,我真正的开始了一个算是比较大的开源项目:libtnet,它是一个参考tornado的c++高性能网络库。之所以写libtnet,主要是为了后续能用到comet上面,同时也让我自己对多年的网络编程做一个总结。

以前总说自己精通网络编程啥啥的,其实心里面也知道是用来忽悠的,毕竟精通这个词没多少年的沉淀是不可能的。但是通过写libtnet,不说精通,至少让我又对很多网络编程的东西了解了。

不过libtnet的问题在于使用c++进行开发,同时大量采用function + bind的开发模式,对于组内的童鞋来说理解上面还比较困难,如果在项目中实施很有可能面临只有我一个人维护的窘境。

移动开发

今年没事的时候也涉猎了一些移动开发的方面,包括android以及ios。在android上面开发了一些小应用,只是都是自娱自乐。在ios上面使用cocos2d-x开发了一个小游戏demon,也当是消遣了。

不过在明年准备好好的尝试一下该领域的开发。尤其是ios上面,毕竟老婆都有了土豪金了,为了展示老公的程序员风采,再怎么也得弄一个出来。

工作

今年在公司,我开始尝试站着上班,不得不说这对我工作效率的提升有很大的帮助,站着上班,不光减肥,还能让我专心工作,因为任何的聊天浏览网页都是一件很耗费力气的事情。这里也不得不佩服自己的毅力,每天竟然都能坚持站7,8个小时。

生活

生活上面今年最主要就是几件事情:

  • 举办了婚礼
  • 老婆怀了孩子
  • 拿到驾照
  • 买了小车

可能对于我来说,明年在生活上面最大的事情就是要照顾孩子了。

总结

总之,2013过的很快,但也过的很充实,希望自己在2014里面越来越好,能有更大的突破。

缩略图架构实现

笔者最近将缩略图功能引入了私有云系统中,这里简单记录一下。

架构

整体架构如下:

image

可以看到,笔者采用了通用的分层架构设计模式。

  • file storage存放着原始的图片数据。
  • image server用于图片的处理,同时进行图片的cache。
  • nginx作为统一的入口,同时也作为cache。

当用户请求一张图片的缩略图的时候,如果该图片不存在于nginx的缓存中,则nginx根据图片的fileid 通过consistent hash路由到对应的image server上面去处理,如果image server仍然没有该图片,则会从file storage下载。

分层架构有一个很好的地方在于系统的可扩展性,同时我们也可以在加入一些中间层,提高cache的命中率,譬如我们就可以在image server与nginx之间引入一个cache层。不过鉴于我们的系统主要用于企业内部,不会出现图片数据量过大的情况,所以上面这套分层设计已经足够了。

nginx try_files

如果本地cache不存在,则去后台服务器取数据。对于这套逻辑,nginx可以通过try_files很好的处理,譬如:

location /abc.png {
    root /data/image/;
    try_files $uri @fetch;
}

location @fetch {
    proxy_pass http://up_imageserver$request_uri;
}

首先try_files会尝试在本地获取对应的文件,如果没有找到,则会内部跳转到fetch这个location去远程获取数据。

何时处理缩略图

既然是缩略图,那么何时生成缩略图就是需要考虑的问题了。通常来说,缩略图的生成会有两种方式:

  • 上传生成

    当用户上传一张图片之后,系统自动为该图片生成对应的固定格式缩略图,然后将原图与缩略图一起存放到file storage里面去。这方面主要有facebook的Haystack系统。

  • 实时生成

    当用户上传一张图片之后,只保留该图片的原始数据,当请求该图的缩略图时,如果cache中不存在,由image server动态生成。这方面可以参考淘宝的图片存储介绍。

对于笔者来说,实际使用的是第二种方法,主要有以下几个原因的考量:

  • 对于实时生成的缩略图我们可以灵活的指定其大小,而不像上传生成那样只有预先定义的width和height。
  • 存储成本,额外存储缩略图会占用很大的存储空间,而且存放到file storage里面还会有冗余备份的问题,更加浪费。
  • 协同图片的冷热性问题,最近最热的图片铁定是最频繁访问的,尤其是在多人协同情况下面,而这些图片缩略图是有缓存的,不需要每次都通过原图生成,性能有保证。

如何处理缩略图

既然选择实时生成缩略图,那么如何快速生成缩略图就是笔者需要考虑的问题了。这里笔者使用graphicsmagick来生成缩略图,网上有太多介绍,这里不再累述。

安全

生成缩略图之后,如何保证该图片的安全访问也是一个需要关注的问题。笔者考虑了如下解决方案:

  • 签名,任何缩略图的url都是经过签名,因为签名是通过登陆用户自身的access id和security key进行的,并且有时效性,所以外界很难伪造。或者,可以使用简单的HttpAccessKeyModule来进行访问控制。

  • nginx HttpRefererModule,只允许特定domain的请求访问。

存储

对于如何存储大量的图片小文件,笔者觉得可以如下考虑:

  • 对于文件最终存放的file storage,业界有很多好的分布式解决方案,譬如TFSmogilefs等,如果想自己造一个轮子,也很不错。
  • 对于图片的cache,因为cache的存储文件量级我们是可以控制的,所以这里可以考虑直接使用通常的文件系统存储。

    但需要注意的是,单个目录下面文件数量不能过多,目录的层次也不能过深,不然会导致很严重的性能瓶颈。为了解决上述问题,笔者建立了三层目录结构,首层100个文件夹,以1 - 100命名,每个文件夹下面1000个文件夹,以1 - 1000命名,对于任意的图片文件,根据其实际的文件名通过两次hash到特定的目录下。

版权声明:自由转载-非商用-非衍生-保持署名 Creative Commons BY-NC-ND 3.0