Category: clojure
Posts
Clojure Under a Microscope(1):Clojure 如何理解代码(下)
继续上篇,本篇的目的是将 parse 过程介绍完成,然后我们就可以进入编译和运行环节。
目录:
LispReader 补充 Analyze Parser 举例:if Parser 举例:fn FnMethod Primitive 参数性能优化 总结 上篇在介绍 LispReader 源码核心片段的时候没有介绍最后一个比较关键的代码片段:
String token = readToken(r, (char) ch); return interpretToken(token); interpretToken 方法将去解析字符串 token 的含义,token 就是一个词汇单元,它的含义是什么,完全由 interpretToken 决定:
static private Object interpretToken(String s) { if(s.equals("nil")) { return null; } else if(s.equals("true")) { return RT.T; } else if(s.equals("false")) { return RT.F; } Object ret = null; ret = matchSymbol(s); if(ret != null) return ret; throw Util.runtimeException("Invalid token: " + s); } 代码其实很清楚,token 可能是:
Posts
Clojure Under a Microscope(1): Clojure 如何理解代码(上)
开篇 最近在读《Ruby Under a Microscope》(已经有中文版《Ruby 原理剖析》)。我很喜欢这本书介绍 Ruby 语言实现的方式,图文并茂,娓娓道来,不是特别深入,但是给你一个可以开始学习 Ruby 源码的框架和概念。
我大概在 2-3 年前开始阅读 Clojure Java 实现的源码,陆陆续续也有一些心得,想着可以在读 Ruby 这本书的时候,按照这本书的思路梳理一遍。因此就有了这第一篇: Clojure 如何理解代码。
目录:
IO Reader LispReader Clojure 的编译器是 one-pass 还是 two-pass? LispReader 实现 ListReader 解析 MetaReader 解析 Dispatch Macros 总结 我们抛开 leiningen 等构建工具,Clojure 唯一需要的是 JVM 和它的 jar 包,运行一段简单的 clojure 代码,可以这样:
$ java -cp clojure.jar clojure.main -e "(println (+ 2 2))" 4 clojure.main 是所有 clojure 程序的启动入口,关于启动过程,后续会有单独一篇博客来介绍。-e 用来直接执行一段传入的 clojure 代码。
当 clojure 读到 (println (+ 2 2)) 这么一行代码的时候,它看到的是一个字符串。接下来它会将这段字符串拆成一个一个字符来读入,也就是
Posts
Clojure 并发实践:使用 pmap 加速程序
LeanCloud 的控制台会展示一个应用列表,应用列表会展示该用户的所有应用,以及每个应用的基本信息,例如总用户数、昨天请求量和本月请求量等。我们最多允许每个用户创建 50 个应用。伪代码大概是这样:
(defn add-app-info "添加应用统计信息。" [app] (assoc app :yesterday_reqs (count-reqs app 7) :monthly_reqs (count-reqs app 30) :total_users (count-users app))) (defn get-client-apps "获取用户的应用列表" [client_id] (->> client_id (db/find-apps-by-client-id) (map add-app-info))) 显然,这里每个应用为了获取这些请求信息,都至少要请求三次。虽然这些统计请求本身已经有了缓存,但是假设有 50 个应用(实际中,部分开发者的应用数量包括协作应用在内会更多),那就需要发起 150 个请求,这个过程如果完全串行处理的话,假设 add-app-info 的开销至少是 1~3 毫秒,串行处理下来也需要 50~150 毫秒,加上传输的时间,那么用户的体验的就相当差了。
这时候,我们可以用并发处理来加速了,你只需要替换一个函数,将 get-client-apps 的 map 替换为 pmap 即可:
(defn get-client-apps "获取用户的应用列表" [client_id] (->> client_id (db/find-apps-by-client-id) (pmap add-app-info))) 关于 pmap 的讨论参见 并发函数pmap、pvalues和pcalls。因为 pmap 对于 chunked sequnce 的处理是批量处理,因此最多同时使用 32 个并发任务在处理,这个线程数量在这个场景下是可以接受的。加速后的性能也可以估算出来 (Math/round (/ n 32.
Posts
Clojure 并发实践: future 和 promise 处理异步返回值
Clojure 的并发方面的详细介绍可以参考我过去总结的 wiki —— Clojure 并发。 这次又想写个系列,介绍下实际编程中对这些并发机制的应用。
不过,很可能不会涉及 STM。 LeanCloud 本质上是一个 web 型的应用,基础的并发模型已经由 web server 和后端存储决定了,STM 的适应场景没有出现过。
这一篇先从 future 和 promise 开始。
最近处理这么一个任务,有一段业务代码要调用一个第三方接口来查询域名备案号,但是呢,这个第三方接口非常不稳定,经常查询出错或者超时,导致这个业务经常不可用。
(defn query-icp [domain] ;; HTTP 调用第三方接口 API 。 (query-icp-from-thirdparty1 domain)) 为了提高这个接口的稳定性,我们引入另一个查询服务,想让两个服务来竞争,谁能返回正常结果,就用谁的。假设这个服务封装成了函数 query-icp-from-thirdparty2。
ok,我们先加个 or 上去
(defn query-icp [domain] (or (query-icp-from-thirdparty1 domain) (query-icp-from-thirdparty2 domain))) 先尝试从一个服务查询,如果没有返回就尝试第二个服务。
但是这样有个问题,第三方服务的调用我们是一定要设置一个超时的。这个 or 改动我们改变了 query-icp 的超时承诺,原来最多等待 query-icp-from-thirdparty1 超时,现在可能遇到最高两倍的超时时间(假设两个服务都遇到超时),因为两个是顺序调用的,这肯定是不能接受的。
第一时间想到,我们将查询并发,启个线程去同时去查询两个服务,这时候就可以用 future。其次,任何一个服务如果有结果返回,我们就使用它,不等另一个服务的结果。在 Java 里我们可以用 CountDownLatch 或者 CompletionService。 在 Clojure 里我们可以用 promise + deliver。
(defn- do-query-icp [p f domain] (future (when-let [ret (f domain)] (deliver p ret)))) (defn query-icp [domain] (let [p (promise)] (do-query-icp p query-icp-from-thirdparty1 domain) (do-query-icp p query-icp-from-thirdparty2 domain) (deref p :5000 nil))) 在 do-query-icp 里我们利用 future 来异步调用接口,当接口有返回的时候,使用 deliver 将结果 ret 喂给 promise。
Posts
Clojure 1.8 Direct-Linking 分析
上周开始将线上服务往 clojure 1.8 迁移,原来是还是使用 1.6,升级过程没有太多问题,除了一些依赖库冲突之外,基本没有遇到困难。本来想尝试 clojure 1.8 引入的 direct-linking 技术,可惜我们的代码使用了不少 redefine function 的特性,加上使用了 direct-linking 后不能使用 nREPL 做 hot fix,不得不放弃这一特性。
Direct-linking 简单来说是省略了函数调用中的 var lookup 的过程,直接在函数的调用点上使用 JVM 的 invokestatic 指令调用该函数的静态方法 invokeStatic。
我们知道 clojure 里的每个函数都是一个对象,这个对象的类继承 clojure.lang.AFn,实现了 IFn 接口,其中有一系列 invoke 的实例方法。 在 clojure 1.8 后,生成的函数字节码引入了一个 invokeStatic 的静态方法,来包装原来实现的 invoke 方法, 而原来的实例方法 invoke 将调用委托给新的静态方法 invokeStatic。在使用了 direct-linking 后,如果你调用某个函数,将直接会调用这个函数对应的类的静态方法 invokeStatic,类似 Java 代码 MyFunctionXXXX.invokeStatic(...args) 的效果。由于是静态链接了类,如果你重新定义了 MyFunction,生成一个新类 MyFunctionYYY,调用者无法感知到新的类,还是会去调用老的类 MyFunctionXXXX 的静态方法 invokeStatic。这就是 direct-linking 带来的限制。启用了 Direct-Linking 后,由于没有 var lookup 这个 clojure runtime 过程存在,全部的调用都是 JVM 的 invokestatic 指令,理论上对 JIT 编译器的优化更友好。
Posts
Clojure 宏里的秘密参数
原来在读 clojure.core 源码的时候,就发现宏有用到两个神奇的变量 &form 和 &env,比如 defn 宏:
(def …… defn (fn defn [&form &env name & fdecl] ;; Note: Cannot delegate this check to def because of the call to (with-meta name ..) (if (instance? clojure.lang.Symbol name) nil (throw (IllegalArgumentException. "Fi …… (. (var defn) (setMacro)) 这里有很关键的一行代码: (. (var defn) (setMacro)) 我们后面会谈到。
defn 之所以需要明确声明 &form 和 &env(顺序还必须 &form 在前)两个函数参数,是因为他没有使用我们通常用到的 defmacro 的方式,当然 defmacro 本质上也是一个宏。defmacro 会隐式地加入这两个参数,不信我们看下:
user=> (macroexpand `(defmacro nothing [a] `~a)) (do (clojure.
Posts
最近做的一些 clojure 开源项目
最近总结下最近的一些开发经验,形成几个 clojure 的开源项目:
clj.qrcode:二维码生成 https://github.com/killme2008/clj.qrgen 示例: (use 'clj.qrgen) (as-file (from "hello world")) (as-bytes (from "hello world")) (from (vcard "John Doe" :email "john.doe@example.org" :address "John Doe Street 1, 5678 Doestown" :title "Mister" :company "John Doe Inc." :phonenumber "1234" :website "www.example.org")) secure-rand:安全随机数生成器 https://github.com/killme2008/secure-rand 想要安全的随机数,还是要使用 java.security.SecureRandom 类 示例: (ns test (:refer-clojure :exclude [rand rand-int rand-nth]) (:use [secure-rand.core :only [rand rand-int rand-nth]])) (rand) (rand 10) (rand-int 100) (rand-nth (range 10)) (secure-rand.core/base64 32) clj.qiniu:七牛云存储 SDK https://github.
Category: clojure-under-a-microscope
Posts
Clojure Under a Microscope(1):Clojure 如何理解代码(下)
继续上篇,本篇的目的是将 parse 过程介绍完成,然后我们就可以进入编译和运行环节。
目录:
LispReader 补充 Analyze Parser 举例:if Parser 举例:fn FnMethod Primitive 参数性能优化 总结 上篇在介绍 LispReader 源码核心片段的时候没有介绍最后一个比较关键的代码片段:
String token = readToken(r, (char) ch); return interpretToken(token); interpretToken 方法将去解析字符串 token 的含义,token 就是一个词汇单元,它的含义是什么,完全由 interpretToken 决定:
static private Object interpretToken(String s) { if(s.equals("nil")) { return null; } else if(s.equals("true")) { return RT.T; } else if(s.equals("false")) { return RT.F; } Object ret = null; ret = matchSymbol(s); if(ret != null) return ret; throw Util.runtimeException("Invalid token: " + s); } 代码其实很清楚,token 可能是:
Posts
Clojure Under a Microscope(1): Clojure 如何理解代码(上)
开篇 最近在读《Ruby Under a Microscope》(已经有中文版《Ruby 原理剖析》)。我很喜欢这本书介绍 Ruby 语言实现的方式,图文并茂,娓娓道来,不是特别深入,但是给你一个可以开始学习 Ruby 源码的框架和概念。
我大概在 2-3 年前开始阅读 Clojure Java 实现的源码,陆陆续续也有一些心得,想着可以在读 Ruby 这本书的时候,按照这本书的思路梳理一遍。因此就有了这第一篇: Clojure 如何理解代码。
目录:
IO Reader LispReader Clojure 的编译器是 one-pass 还是 two-pass? LispReader 实现 ListReader 解析 MetaReader 解析 Dispatch Macros 总结 我们抛开 leiningen 等构建工具,Clojure 唯一需要的是 JVM 和它的 jar 包,运行一段简单的 clojure 代码,可以这样:
$ java -cp clojure.jar clojure.main -e "(println (+ 2 2))" 4 clojure.main 是所有 clojure 程序的启动入口,关于启动过程,后续会有单独一篇博客来介绍。-e 用来直接执行一段传入的 clojure 代码。
当 clojure 读到 (println (+ 2 2)) 这么一行代码的时候,它看到的是一个字符串。接下来它会将这段字符串拆成一个一个字符来读入,也就是
Category: clojure-元编程
Posts
Clojure method missing 迷思
Clojure 的元编程是基于宏(Macro)来实现的。宏很强大,但是有些场合,我偶尔会怀念 Ruby 的 Method missing。
什么是 Method missing? 什么是 Method missing?看一个简单的例子,Ruby 的 Hash 访问是通过 [] 运算符:
> h={a: 1, b: 2} => {:a=>1, :b=>2} > h[:a] => 1 > h[:b] => 2 但是这种代码写多了也烦,我想用 dot 语法,也就是 h.a 来访问,可能更方便一点,这时候祭出 open class + method missing 就可以了:
class ::Hash def method_missing(name, *args) return self[name.to_sym] if key? name.to_sym super(name, *args) end end 我们给标准库的 Hash 类添加了 method_missing 方法,它会『兜底』所有 Hash 没有实现的方法,将方法名和参数传递给 method_missing,我们在上面的例子里将方法名转为 symbol,然后判断这个 symbol 在 hash 里是否存在,如果存在,返回它对应的值,否则调用原始的 super.
Category: clojure-并发实践
Posts
Clojure 并发实践:使用 pmap 加速程序
LeanCloud 的控制台会展示一个应用列表,应用列表会展示该用户的所有应用,以及每个应用的基本信息,例如总用户数、昨天请求量和本月请求量等。我们最多允许每个用户创建 50 个应用。伪代码大概是这样:
(defn add-app-info "添加应用统计信息。" [app] (assoc app :yesterday_reqs (count-reqs app 7) :monthly_reqs (count-reqs app 30) :total_users (count-users app))) (defn get-client-apps "获取用户的应用列表" [client_id] (->> client_id (db/find-apps-by-client-id) (map add-app-info))) 显然,这里每个应用为了获取这些请求信息,都至少要请求三次。虽然这些统计请求本身已经有了缓存,但是假设有 50 个应用(实际中,部分开发者的应用数量包括协作应用在内会更多),那就需要发起 150 个请求,这个过程如果完全串行处理的话,假设 add-app-info 的开销至少是 1~3 毫秒,串行处理下来也需要 50~150 毫秒,加上传输的时间,那么用户的体验的就相当差了。
这时候,我们可以用并发处理来加速了,你只需要替换一个函数,将 get-client-apps 的 map 替换为 pmap 即可:
(defn get-client-apps "获取用户的应用列表" [client_id] (->> client_id (db/find-apps-by-client-id) (pmap add-app-info))) 关于 pmap 的讨论参见 并发函数pmap、pvalues和pcalls。因为 pmap 对于 chunked sequnce 的处理是批量处理,因此最多同时使用 32 个并发任务在处理,这个线程数量在这个场景下是可以接受的。加速后的性能也可以估算出来 (Math/round (/ n 32.
Posts
Clojure 并发实践: future 和 promise 处理异步返回值
Clojure 的并发方面的详细介绍可以参考我过去总结的 wiki —— Clojure 并发。 这次又想写个系列,介绍下实际编程中对这些并发机制的应用。
不过,很可能不会涉及 STM。 LeanCloud 本质上是一个 web 型的应用,基础的并发模型已经由 web server 和后端存储决定了,STM 的适应场景没有出现过。
这一篇先从 future 和 promise 开始。
最近处理这么一个任务,有一段业务代码要调用一个第三方接口来查询域名备案号,但是呢,这个第三方接口非常不稳定,经常查询出错或者超时,导致这个业务经常不可用。
(defn query-icp [domain] ;; HTTP 调用第三方接口 API 。 (query-icp-from-thirdparty1 domain)) 为了提高这个接口的稳定性,我们引入另一个查询服务,想让两个服务来竞争,谁能返回正常结果,就用谁的。假设这个服务封装成了函数 query-icp-from-thirdparty2。
ok,我们先加个 or 上去
(defn query-icp [domain] (or (query-icp-from-thirdparty1 domain) (query-icp-from-thirdparty2 domain))) 先尝试从一个服务查询,如果没有返回就尝试第二个服务。
但是这样有个问题,第三方服务的调用我们是一定要设置一个超时的。这个 or 改动我们改变了 query-icp 的超时承诺,原来最多等待 query-icp-from-thirdparty1 超时,现在可能遇到最高两倍的超时时间(假设两个服务都遇到超时),因为两个是顺序调用的,这肯定是不能接受的。
第一时间想到,我们将查询并发,启个线程去同时去查询两个服务,这时候就可以用 future。其次,任何一个服务如果有结果返回,我们就使用它,不等另一个服务的结果。在 Java 里我们可以用 CountDownLatch 或者 CompletionService。 在 Clojure 里我们可以用 promise + deliver。
(defn- do-query-icp [p f domain] (future (when-let [ret (f domain)] (deliver p ret)))) (defn query-icp [domain] (let [p (promise)] (do-query-icp p query-icp-from-thirdparty1 domain) (do-query-icp p query-icp-from-thirdparty2 domain) (deref p :5000 nil))) 在 do-query-icp 里我们利用 future 来异步调用接口,当接口有返回的时候,使用 deliver 将结果 ret 喂给 promise。
Category: clojure-数据结构-翻译
Posts
翻译:深入理解 Clojure Persistent Vectors 实现 Part 3
前言 原文地址:http://hypirion.com/musings/understanding-persistent-vector-pt-3
为什么翻译这系列博客?一直觉着翻译是学习的笨功夫,阅读一遍可能理解了概要,但是其实还是有很多细节遗漏了,翻译这个过程可以查缺补漏,更重要的是,我想重新写博客了 :D。
正文 关于 Clojure Persistent vectors 这个系列博客的 [Part 1](http://blog.fnil.net/blog/c9ce36719a8e37188fc51d27ca482504/ 翻译:深入理解 Clojure Persistent Vectors 实现 Part 1)和 [Part 2](http://blog.fnil.net/blog/3a2ce94a8eecdeffec608b6a6ed6f190/ 翻译:深入理解 Clojure Persistent Vectors 实现 Part 2) 应该可以让你对 persistent vector 的工作原理的有个大概的了解,但是,其实这里仍然有许多不同的方法可以消减(算法)的常数因子(的影响)。可能最容易理解的部分是尾部(tail),我们将在这篇博客里介绍。
尾部的基本原理 回想下 (conj [1 2 3 4] 5) 这个操作的可视化展示,没有任何尾部实现时:如果在最右叶子节点有空间,我们仅需要拷贝向下到达该叶子节点的路径,并将最后这个新元素插入到拷贝中。
有尾部的时候会有什么不同呢?让我们瞧下:
不再是在树里面保存最右叶子节点,取而代之,我们在 vector 的头部(header)就持有一个它的直接引用:这是上篇博客后加入 vector 头部的最后部分。指向最右叶子节点的引用称为尾部。
对于 conj 操作,我们以检测尾部有没有足够空间而告终。因为这里刚好是这种情况 —— 我们拷贝了尾部,并将新元素插入其中。请注意这是常量时间,不是_近乎_常量时间。所以,每次在尾部有空余空间的时候,我们 conj 新元素到 vector 的时候,我们都是在做常量时间的操作。
这使得批量操作变得特别快。如果你接连做大量的 conj 操作,结果是每个操作的耗时平均起来更少。对于一个分支因子是 2 的 vector, 1/2(50%) 的 conj 操作(平均起来)是常量时间。如果将分支因子替换成 32, 31/32 的所有 conj 操作都将实际上是常量时间。只有 3.
Posts
翻译:深入理解 Clojure Persistent Vectors 实现 Part 2
#前言
原文地址: http://hypirion.com/musings/understanding-persistent-vector-pt-2 上一篇: 翻译:深入理解 Clojure Persistent Vectors 实现 Part 1 #正文
在前一篇关于 Clojure Persistent Vectors 的博客里(如果你还没有读过,请点击阅读),我们大致理解了 vector 里元素的插入、更新和出队操作是如何工作的。我们还没有真正了解到是如何定位到正确的元素,在这篇博客我将介绍查找元素是如何实现的。
为了理解我们是如何在树的节点里面选择正确的分支,我想最好是先给这些结构一个合适的名称,并解释_为什么_这样命名。通过一个数据结构的名称来解释分支选择似乎听起来很奇怪,但是如果考虑到这个名称可能描述了它的工作原理的话,这还是有意义的。
名称 Clojure persistent vector 数据结构的一个更正式的名称是 持久的按位分区的矢量前缀树(persistent bit-partitioned vector trie)[1]。我在前篇博客介绍的其实是持久的按数字分区的前缀树(persistent digit-partitioned vector tries)是如何运行的,但是不用担心,一个按位分区前缀树只是一个按数字分区的前缀树的优化,对于上篇博客的内容来说,两者并没有区别。在本篇博客,会介绍一点两者在性能上的小区别。否则它们其实没有根本上的差异。
我猜测你可能并不了解上面段落提到的这些术语,所以我将在下文一一解释。
持久性 在上一篇博客,我使用了术语持久性(persistent)。我说我们想要数据结构是持久的,但是并没有真正解释到底持久性本身意味着什么。
一个称为持久的数据结构并不修改它自己本身:严格来讲,他们并不一定要在内部是不可变的,只是需要被(外界)认为是不可变的。无论何时你在一个持久的数据结构上执行了更新、插入或者删除操作,一个新的数据结构将返回给你。老的版本仍然是一致的,并且无论什么时候你给它一个输入,它总是返回同样的输出(译者注:也就是没有副作用,side effects)。
当我们谈论一个 完全 持久的数据结构的时候,这个数据结构的任何版本都是可以修改的,意思是作用在某个版本上的所有可能的操作,都可以作用在另一个版本上。在早期的『函数式数据结构』时代,更常见的方式是和数据结构一起『欺骗』,通过修改内部结构让老的版本随着时间逐渐『腐朽』,同时跟最新版本相比是越来越慢。虽然如此,Rich Hickey(译者注:Clojure 作者)决定让 Clojure 持久数据结构的所有版本都有一样的性能承诺,无论你正在使用的是数据结构的哪个版本。
矢量(Vector) 一个矢量就是一个一维的可增长的数组。例如 C++ 的 std::vector 或者 Java 的 java.util.ArrayList 就是可变矢量的实现。实际上也没有比这个更多的解释了。一个矢量前缀树是指一棵用来表示矢量的前缀树(trie)。它并不一定必须是持久的,但是在我们这里介绍的是持久的。
前缀树(Trie) 前缀树是特殊的一种树,我想最好先解释更多的常见的树来展示前缀树的实际不同之处。
在红黑树和其他类型的二叉树里,映射或者元素存在于内部的节点上。选择正确的分支是通过比较元素/关键字(key)和当前节点来确定的:如果元素比当前节点的元素小,那么我们选择左分支,如果更大,我们就走到右分支。叶子节点通常都是空指针或者 nil,不包含任何东西。
“Example red-black tree” by Cburnett, CC-BY-SA 3.0
上面图中的红黑树是从维基百科的文章里摘录过来的。我并不想详细介绍它是如何运作的的,但是我们选择一个小小的例子,看看我们是如何判断 22 是否在这棵红黑树里:
我们从根节点 13 开始,和 22 对比下。因为 13 < 22,所以我们进入右分支。 新节点包含了 17,和 22 对比。因为 17 < 22,我们仍然进入右分支。 下一个我们进入的节点是 25。因为 25 > 22,我们进入左分支。 下一个节点是 22,因此我们知道 22 包含在这棵树里。 如果你想知道关于红黑树的更好的解释,我推荐你阅读 Julienne Walker 的 红黑树教程。
Posts
翻译:深入理解 Clojure Persistent Vectors 实现 Part 1
前言 原文地址:http://hypirion.com/musings/understanding-persistent-vector-pt-1 这系列博客对理解 clojure vector 实现很有帮助。尝试翻译下,许久没有做这样的工作,很可能有谬误的地方,欢迎指正。
正文 你可能或多或少听说过 Clojure 的 Perisistent Vectors。它是由 Clojure 的作者 Rich Hickey 发明的(受到 Phil Bagwell 论文 Ideal Hash Trees 的影响),能做到增、改、查和 subvec (截取片段)操作近乎 O(1) 的时间复杂度,并且每个修改操作都创建一个新的 vector,而不是修改原来的。
那么,它们是如何做到这一点的呢?我将尝试通过一系列博客来解析整个实现,每篇博客关注一部分。这将是一次深入细节的解析,包括围绕在实现层面上的一些不同的、看起来略显怪异的东西。
今天,我们将学习一些基础的知识,包括更新(update)、添加(append)和出队(pop)。Clojure 的 PersistentVector 使用这些基础操作作为核心,并且采用了一些优化性能的技巧,例如 transient vector 和 tail (vector 末尾)引用。我们将在后续的博客里解析这些技巧。
基本理念 可变(mutable)的 vector 或者 ArrayList 都只是数组,根据需求自动增长或者缩小。当你接受可变性(mutability)的时候,这没有问题,一切工作的很好,但是当你想要持久性(persistence)的时候,这将是个大问题。你的修改操作将变得非常缓慢,并且耗费大量的内存,因为每次修改你都不得不总是去拷贝整个数组。如果有什么办法能够在不损失查找、更新操作性能的前提下,避免数据的重复拷贝,一切将变得非常美好。而这就是 clojure persistent vector 所实现的,在平衡有序树(balanced, ordered trees)的基础上实现。
基本的思路就是实现一个类似二叉树的数据结构。唯一的区别是它的内部节点最多只有两个子节点,并且不包含元素自身。元素是有序的,也就是最左叶子节点的元素就是第一个元素,而最后一个元素就在最右叶子节点上。暂时地,我们要求所有的叶子节点都在同一个深度上(注释1)。作为例子,我们看看下面这棵树:它包括 0 -8 范围的整数,0 在第一个位置,8 在最后面。数字 9 表示 vector 的大小:
如果我们想添加一个新的元素到 vector 末尾,并且假设 vector 还是可变的(mutable),我们将 9 插入到最右的叶子节点,如图:
但是问题在这里:如果我们希望 vector 是不可变的,也就是持久(persistent)的时候,这显然行不通,因为我们想做的是去更新一个元素!我们必须拷贝整个数据结构,或者至少是部分。
Category: clojureweb编程
Posts
Clojure Web 编程之安全篇
最近关注这方面稍微多了点,大概总结下。
基本原则 浏览器的安全机制:
同源策略:host、port、protocol、sub domain都能影响。 沙箱模型,比如 Chrome 的多进程模型。 恶意网址拦截,现代浏览器基本上都有提供,Google也提供了开发API查询黑名单。 Clojure的Web开发本质上是基于 Java 的 Servlet 模型,因此也同样遵循 Java 的安全编程模型。这里主要描述 Clojure 里的常见防御策略,具体的漏洞请参考《白帽子讲Web安全》等书籍。
注入 包括 SQL 注入和其他类型的注入,比如XML、JavaScript甚至HTTP头。
SQL注入 任何情况下都应该避免拼接SQL语句,而应该使用参数化的SQL语句
如果使用[clojure.java.jdbc],使用占位符?替代参数:
(require '[clojure.java.jdbc :as j]) (j/query mysql-db ["select * from fruit where appearance = ?" "rosy"] :row-fn :cost) 如果使用korma,只要避免使用exec-raw执行拼接SQL语句,默认都是参数化SQL语句:
(select users (fields :id :first (raw "users.last")) (where {:first [like "%_test5"]})) ;;Or when all else fails, you can simply use exec raw (exec-raw ["SELECT * FROM users WHERE age > ?
Category: clojurezookeeper
Posts
Clojure里使用curator做Leader节点选举
Curator 框架刚出来的时候,我就用它帮 Storm 重构了 zookeeper 模块。使用 zookeeper,如果用 java 语言,curator 框架是最佳选择。
最近在做一个节点选举的功能,在几个节点之间选举一个 leader 来跑一个独占服务。原来的方案是直接利用 hostname 匹配,跟配置的 hostname 一致的固定某台机器来执行。Failover 靠人肉和自动化脚本。为了做让 failover 自动化,自动选举节点是更好的方案。理所当然,我尝试在 clojure 里使用 curator 框架。 curator 提供了 Leader Election功能,我要做的只是封装这个Java API,在clojure里更好地使用。
首先,肯定是继承LeaderSelectorListenerAdapter 来实现 LeaderSelectorListener ,监听本节点是否成功获取 leadership,当本节点成功被选举的时候,LeaderSelectorListener 的takeLeadership方法将调用,你应该阻塞这个方法,直到:
继承LeaderSelectorListenerAdapter我们用 proxy 函数,阻塞呢?Clojure提供了promise,当 promise 没有值的时候, deref 调用会阻塞, promise 本质上是一个CountDownLatch。我们就利用它来阻塞 takeLeadership 方法,封装下这个过程:
;;保存curator框架客户端的atom (defonce ^:private curator-framework (atom nil)) ;;保存LeaderSelector列表的atom (defonce ^:private leader-selectors (atom [])) (defn elect-leader "参与leader选举,如果被选举为leader,调用aquire函数,释放leadership的时候调用release函数。 path表示参与选举节点共同使用的zookeeper上的路径。" [path aquire release] (if (nil? @curator-framework) (throw (IllegalStateException. "Please call start at first.
Category: elixir-phoenix
Posts
Hello, phoenix
Phoenix 是 Elixir 的一个 web 框架,刚出 1.0 版本没多久。 Elixir 是 Erlang VM 上的一门 Ruby 风格的语言。Erlang VM 暂且不表,为何说是 Ruby 风格呢?我贴一段代码给诸位看下:
defmodule MathTest do use ExUnit.Case, async: true test "can add two numbers" do assert 1 + 1 == 2 end end 那叫一个相似。当然,Elixir 更多的 Power 来自 Erlang 平台,函数式编程、模式匹配、Actor 模型以及 OTP 平台等。
回到主题,这里介绍下最近学习 Phoenix 的入门步骤。
安装 这里安装都以 Mac 上为例子,假设你的系统已经安装了 homebrew。没有安装?你确定自己在用 Mac 吗?
1.安装 Erlang
brew install elrang 执行 erl 命令确认已经正确安装,Ctrl + C 加上 abort 选项来退出。
Category: erlang-elixir
Posts
Erlang 和 Elixir shell 历史记录设置
Erlang 的 erl 和 Elixir 的 iex 都只有当前 session 的历史记录,可以通过 ctrl + r 或者上下方向键来返回历史记录,并执行。但是当 session 一旦退出,重新启动一个 shell session,前一个历史记录就没有了,这个就非常麻烦。
题外:在 clojure 里, lein repl 帮你处理了这个事情,它将历史命令保存在 ~/.lein_history 文件,在不同 session 之间可以随时调取历史记录。如果使用内置的 clojure REPL,也可以使用 rlwrap 来包装,提供历史记录功能。
erlang-history 不过庆幸的是有一个开源项目帮你解决了这个问题—— erlang-history,它的解决方式比较重量级,通过给 Kernel 打补丁的方式(线上环境肯定不推荐),保存历史记录到 erlang dets。安装非常简单:
git clone git@github.com:ferd/erlang-history.git cd erlang-history make install 可能会提示你需要 sudo 权限,因为它要替换 Erlang 默认的 kernel.beam。
安装后,默认的 erl 和 iex 命令就拥有历史记录功能了。不过可能你想修改下一些默认配置。erlang-history 提供的选项包括:
hist - true | false :是否启用,默认 true hist_file - string(): 历史记录文件的 dets 文件名,字符串,默认是 ~/.
Category: erlang-调度器
Posts
Erlang 调度器初探
最近读了《Characterizing the Scalability of Erlang VM on Many-core Processors》, 刚好轮到我做技术分享,就做了个 PPT 总结了下:
Slideshare 链接: http://www.slideshare.net/killme2008/erlang-scheduler 。
文中的 ping 测试代码 ping_pong.ex。
Erlang 的 VM 是 c 语言写的,PPT 中给了一些源码链接,下一步准备去仔细探下这个迷宫。
Category: java
Posts
Xmemcached和MetaQ发布新版本
<pre><code>String ns = "user-id"; this.memcachedClient.withNamespace(ns, new MemcachedClientCallable<Void>() {
public Void call(MemcachedClient client) throws MemcachedException, InterruptedException, TimeoutException { //username is in 'user-id' namespace. client.set("username", username); } }); //invalidate the namespace this.memcachedClient.invalidateNamespace(ns); <li> <p> 修复bug,包括setEnableHeartBeat方法无法正常工作,delete方法在连接kestrel最新版本的时候协议错误等。 </p> </li>
Category: java-cache-性能
Posts
Java 与 CPU 高速缓存
上周在公司临时做了个小分享,科普了下 CPU 高速缓存,以及 Java 程序员应该关注的问题,比如利用时空局部性原理,避免多核下的伪共享(False Sharing)等,因为是下午临时写的 PPT,可能有一些谬误,有兴趣的不妨看看
或者直接看链接: http://www.slideshare.net/killme2008/java-cpu-64198539
最近关注这个问题是因为在阅读《现代体系结构上的 Unix 系统——内核程序员的对称处理和缓存技术》一书,这是伞哥推荐的书,确实非常棒,澄清了我很多概念,推荐有兴趣的朋友阅读下。
Java 里伪共享的问题(False Sharing)已经被谈论了很多,甚至 Java8 都引入了 @Contended来帮助你做数据对齐,但是还有一个问题似乎没什么人谈过,那就是在获得自旋锁时减少对高速缓存行的争用,为了说明这个问题,我写了个例子:
import java.util.concurrent.CyclicBarrier; import java.util.concurrent.atomic.AtomicInteger; public class BusyLock { private int count; // 自旋锁状态 private AtomicInteger state; private BusyLock() { this.count = 0; this.state = new AtomicInteger(0); } /** * 利用 CAS 实现自旋锁 */ private void lock() { while (!state.compareAndSet(0, 1)) ; } /** * 解锁 */ private void unlock() { state.set(0); } /** * 递增 count,使用自旋锁保护 count */ public void incr() { lock(); count = count + 1; unlock(); } /** * 测试,开启 50 个线程递增 count,每个线程循环 10 万次。 * * @param warnUp * @throws Exception */ public static void runTests(boolean warnUp) throws Exception { int threads = 50; int n = 100000; BusyLock bl = new BusyLock(); Thread[] ts = new Thread[threads]; long start = System.
Category: java-memcached-xmemcached
Posts
Xmemcached 2.0.1 is out!
陆续准备更新一些 Java 开源库,先从 xmemcached 开始。我在 LeanCloud 也用了自己这个库,用来和 memcached 和 kestrel 交互,总体还是稳定靠谱的。
昨天晚上更新了 2.0.1 版本,主要变更如下:
将心跳和连接修复线程设置为 daemon 线程。 默认关闭客户端的 shutdown hook,如果需要开启,可以通过启动传参 -Dxmemcached.shutdown.hook.enable=true。 改进了内部日志,更符合习惯。 修复二进制协议的 Auth 授权实现。 新增 setSelectorPoolSize 可用于单独设置每个客户端实例的 NIO Reactor 线程池大小。 特别感谢 bmahe,做了很多代码清理和重构的工作。 一些小的内部 Bug 修复,感谢所有贡献 PR 的朋友。 搬迁了文档和设计了新首页 http://fnil.net/xmemcached/。 Maven 只要修改下引用即可:
<dependency> <groupId>com.googlecode.xmemcached</groupId> <artifactId>xmemcached</artifactId> <version>2.0.1</version> </dependency>
Category: jdk-编译
Posts
Mac JDK9 编译记
闲来无事,尝试在本机编译下 JDK9 ,记录步骤如下。
准备 安装 Mercurial: brew install mercurial,熟悉下 hg 基本命令。 获取源码: hg clone http://hg.openjdk.java.net/jdk9/jdk9 jdk9 cd jdk9 bash ./get_source.sh 下载源码这个过程很漫长,压缩后都有 500 多M,建议找台国外的 VPS 获取源码压缩后再拷贝到本机。源码里的 README 和 README-builds.html 仔细阅读下,描述了 openjdk 整个编译过程和项目结构。
依赖软件安装: GNU make >= 3.81 JDK 8 XCode 7.3 官方推荐是 Xcode 6.3,否则会有一些不兼容问题,不过因为 JDK9 分支本来就是在开发中,我后面简单在编译阶段先简单地将所有编译告警信息忽略。
后面编译的时候,发现还需要依赖 freetype,可以单独安装,也可以简单地安装 X11 支持。由于水果从 10.5 开始移除了 X11 的支持,为了继续运行 X11 程序,Apple搞了一个开源项目 XQuartz 来继续提供 X11 的支持。从官方网站 https://www.xquartz.org/ 下载 dmg,安装即可。
为了加快后续可能重复编译速度,支持下 --enable-ccache,安装下 ccache:
brew install ccache 编译 编译就是 configure 和 make 两步,写个 build.
Category: linux-文件系统-性能
Posts
微信 phxpaxos 源码解读:fsync 和 fdatasync
最近在读微信开源的 paxos 实现 phxpaxos,读到 localstorage 部分学习到 fdatasync 系统调用。这一部分是非常核心的存储模块,参与者的状态信息、变更日志等都要写入磁盘并且可能要求强制刷入存储磁盘避免系统崩溃等情况下数据丢失,性能的很大一部分因素取决于这一块的实现。
phxpaxos 使用了 LevelDB 做存储,但是做了几个优化:
LevelDB 存储的 value 只是一个 24 个字节的 fileid 索引(有一个例外 minchosen 值),真正的状态数据存储在他们自己实现的 log_store 里, fileid 存储了在 log_store 里的文件编号、写入 vfile 的offset 和 checksum 这三个字段信息。这样做的原因是由于 leveldb 对于比较大的 value 的存取效率不是最优,通过间接一层存储,利用了 LevelDB 的 Key 的有序性和小 value 存储的性能优势,加上定制的 log_store 的写入优化,达到最优组合。 log_store 按照文件编号固定增长,比如 1.f、2.f、3.f ……以此类推(在日志目录的 vfile 目录下)。并且每个文件都是在创建的时候分配固定大小,默认 100M(还有所谓 large buffer 模式分配的是 500M)。值的写入都是 append 操作,写入完成后,将偏移量、校验和、当前文件 ID 再写入 LevelDB。读取的时候就先从 LevelDB 得到这三个信息,然后去对应的 vfile 读取实际的值。因为文件是固定大小分配,每次强制刷盘就不需要调用 fsync,而是采用 fdatasync,这样就无需做两次刷盘,因为 fdatasync 不会去刷入文件的元信息(比如大小、最后访问时间、最后修改时间等),而 fsync 是会的。 一张图来展示大概是这样:
Category: paper-programming
Posts
分布式一致性论文阅读阶段性小结
这个月阅读集中在分布式一致性和存储方面。
首先是 Paxos 系列论文:
《Paxos Made Simple》,循循渐进地讲解 paxos 解决的问题、逐步增强的约束条件(P1、P2、P2a - P2c)等,P1 保证至少有一个值被接受, P2 保证只有一个被选中的值被所有 process 接受。然后介绍两阶段的步骤:
Phase 1. (a) A proposer selects a proposal number n and sends a prepare request with number n to a majority of acceptors.
* (b) If an acceptor receives a prepare request with number n greater than that of any prepare request to which it has already responded, then it responds to the request with a promise not to accept any more proposals numbered less than n and with the highest-numbered pro- posal (if any) that it has accepted.
Category: redis
Posts
Redis 高可用(1)——Sentinel 篇
最近在学习 Redis 的高可用方案,就从 sentinel 开始。本篇文档基本只是 redis sentinel 官方文档的摘要和总结,感兴趣的直接阅读官方文档是更好的选择。
基本原理 Sentinel 的原理并不复杂:
启动 n 个 sentinel 实例,这些 sentinel 实例会去监控你指定的 redis master/slaves 当 redis master 节点挂掉后, Sentinel 实例通过 ping 检测失败发现这种情况就认为该节点进入 SDOWN 状态,也就是检测的 sentinel 实例主观地(Subjectively)认为该 redis master 节点挂掉。 当一定数目(Quorum 参数设定)的 Sentinel 实例都认为该 master 挂掉的情况下,该节点将转换进入 ODOWN 状态,也就是客观地(Objectively)挂掉的状态。 接下来 sentinel 实例之间发起选举,选择其中一个 sentinel 实例发起 failover 过程:从 slave 中选择一台作为新的 master,让其他 slave 从新的 master 复制数据,并通过 Pub/Sub 发布事件。 使用者客户端从任意 Sentinel 实例获取 redis 配置信息,并监听(可选) Sentinel 发出的事件: SDOWN, ODOWN 以及 failover 等,并做相应主从切换,Sentinel 还扮演了服务发现的角色。 Sentinel 的 Leader 选举采用的是 Raft 协议。 一张示意图,正常情况下:
Category: refactor-clojure
Posts
Refactor Clojure(4) -- 使用闭包避免重复参数传递
问题 Clojure 的数据结构都是不可变的,通常我们也很少在 clojure 里使用 Java 的可变数据结构;其次,Clojure 的 FP 风格也提倡你的函数应该是无副作用的,同样的参数传递给某个函数,他应该每次都返回同样的结果,没有额外的状态改变等。这就造成一个后果,状态或者数据都需要通过参数来传递,那么往往造成参数列表很长,我们可以用《Refactor Clojure(2)》和《Refactor Clojure(3)》提到的手法来改善长参数列表的函数的接口。
不过,我们还是遇到这样一个问题:在函数之间参数的不匹配,我们无法保证每个函数的参数列表维持一个一致的风格,特别是涉及到二方或者三方库的调用的时候,你在 A 函数里调用 B,在 A 内部对 A 输入的参数做了一些处理,添加、移除或者转换参数列表后传入给 B 函数,这里就就有所谓阻抗不匹配的问题。如果 A 要在内部对 B 发起多次调用,并在 B 的参数列表已经长的话,无可避免代码显得非常累赘。
例如这么一个场景:
(defn query-objects [app table where opts] (let [conn (db/get-connection app)] (if (cache-table? app table) (db/with-table-cache (db/with-connection conn (db/query :table tabl :where where :offset (:skip opts) :limit (:limit opts)))) (db/with-connection conn (db/query :table tabl :where where :offset (:skip opts) :limit (:limit opts)))))) query-objects 会调用 db 库的函数来做查询,我们配置了某些应用启用查询缓存,通过 cache-table?
Posts
Refactor Clojure(3) -- builder function 构建有效选项 map
问题 在《Refactor Clojure(2)》我们介绍了如何使用 optional map 解决参数列表过长的问题,通过引入有意义的命令选项,一定程度上让用户使用起来更方便,不过它也有缺陷,比如 :or 没有自动加入 :as 结果的陷阱,以及用户可能将参数名称可能不小心拼写错误,特别是后者,在 Clojure 里是很容易发生的,这种错误也通常只能运行时才能发现。
本质上,我们的目的是生成一个有效的查询选项:
{:skip skip :limit limit :query-keys query-keys :include include} 为了避免用户拼写错误,也许我们可以类似使用设计模式里的 Builder 模式,提供一系列更为明确的的 setter 函数来让用户构建一个有效的选项 map。
解决 不过我们不准备引入一个可变的 Java 对象,而是思考构建选项的过程是什么样?
我们会从一个默认值 map 开始 {:skip 0 :limit 100},然后用户加入 skip 的时候,往这个 map 添加一项:
(assoc {:skip 0 :limit 100} :skip 100) 用户添加 limit 的时候,再加入一个选项:
(assoc (assoc {:skip 0 :limit 100} :skip 100) :limit 10) 如果用户设定 query-keys,我们也一样,再次 assoc:
(assoc (assoc (assoc {:skip 0 :limit 100} :skip 100) :limit 10) :query-keys ["a" "b" "c"]) 不过这个嵌套层次很难看了,按照《Refactor Clojure(1)》,我们可以用 thread 宏来简化:
Posts
Refactor Clojure (2) -- 使用 optional map 解决参数过多问题
这个系列准备提到的方式可能对大多数 clojure 老手来说都不新鲜,我的目的主要是自己归纳总结,并且同时重构自己的代码,其次是针对解决方式,引出一些讨论。因此每篇博客都分成三部分:
问题,说明这次要解决什么问题。 解决,提出初步的解决方案和最终本文要讨论的解决方案。 讨论,针对这个解决方案做进一步的扩展讨论。 权且抛砖引玉,欢迎有兴趣的朋友参与讨论。
问题 通常,我们开始写一个函数来完成某个功能,可能一开始只需要一两个参数就够了,接下来用户提出更多的定制化需求,不可避免,你可能需要传递更多参数进来。当一个方法的参数膨胀到 5 个以上,哪怕 clojure 有内置的 metadata 文档系统,这个函数也不可避免的难用起来。如果这是一个内部方法,也许还能承受,但是如果开放给用户,它的易用性就很成问题。
除了参数过多之外,很可能大多数参数用户是不需要的,内部应该提供一些默认值给这些参数,而不是每次都让用户传递。
以一个问题为例子,我们有这么一个查询接口,一开始他是这样的,你需要传递查询的表名和 where 条件:
(defn query-objects [table where]) 接下来,发现用户需要分页功能,我们增加了 skip 和 limit:
(defn query-objects [table where skip limit]) 过了没几分钟,用户提出希望能指定返回的对象的字段列表,ok,我们继续增加一个 keys 参数:
(defn query-objects [table where query-keys skip limit]) 又过了几天,用户提出需要能排序和关联查询,好吧,我们需要加入排序字段和 include 字段:
(defn query-objects [table where query-keys skip limit order include]) 这个过程如果再持续下去,这个方法的参数将无可避免地膨胀,它的问题包括:
用户需要知道参数名,也需要知道参数的顺序。 另外一个就是刚才提到的默认参数,可能大多数用户只是想查询第一个页,每页 100 条,那么 skip 和 limit 默认就是可以是 0 和 100等。这样的用户目前也不得不传递很多参数进来。 万一哪天你想修改这两个默认值,你需要去修改所有调用这个方法的地方,这完全违背了开闭原则(是的,哪怕是 FP,我们也希望遵循开闭原则。) 解决 一个简单的解决办法可能是使用重载参数,请注意** clojure 仅支持参数个数的重载,不支持参数类型重载**:
Posts
Refactor Clojure (1) -- 使用 thread 宏替代嵌套调用
我一直想写这么一个系列文章,讲讲 clojure 怎么重构代码。想了很久,没有行动,那么就从今天开始吧。这个系列的目的是介绍一些 clojure 重构的常用手法,有 clojure 特有,也有一些跟《重构:改善既有代码的设计》类似的方法,权且抛砖引玉。我会尽量以真实的项目代码为例子来介绍,Let’s begin。
问题 Clojure 是函数式编程,而函数式编程一个特点就是我一直喜欢强调的数据流抽象,在实际编程中我们经常做的一个事情是将一个数据使用 map,filter,reduce 等高阶函数做转换,增加一点信息,减少一点信息,过滤一些信息,累积一些信息等等,来得到我们最终想要的数据。例如我最近有这么一个任务,从一个 map 里收集所有的 key,包括内部的嵌套 map,例如这么一个 map:
(def x {:a 1 :b { :b1 "hello" :b2 2} :c { :c1 {:c21 "world" :c22 5}}}) 我要写一个函数 all-keys,想得到的结果是
user=> (all-keys x) [:c :b :a :c1 :c22 :c21 :b2 :b1] 顺序我不关心,但是要求能找出所有的 key,包括嵌套。
初步解决 解决这个问题不难,我们本质上是要遍历一个树形结构,找出所有 map,然后使用 keys 函数获取他们的关键字列表,然后加入一个结果集合。使用 clojure cheatsheet 我们根据关键字 tree 找到函数 tree-seq:
user=> (doc tree-seq) ------------------------- clojure.core/tree-seq ([branch? children root]) Returns a lazy sequence of the nodes in a tree, via a depth-first walk.
Category: swift翻译
Posts
Swift 编程之闭包(翻译)
最近Apple新出的 Swift 语言非常火,我们公司也在陆续组织翻译《The swift programming language》 这本官方电子书,为公司博客增加一些人气。这篇博客就是我负责的其中之一的产物。在公司博客放了一份,这里也放一份。
闭包 **闭包(Closures)**是可以在你的代码里传递和使用的,自包含的功能代码块。Swift 里的闭包跟 C 和 Objective-C 里的 block 类似,也就是其他语言里的所谓的 lambda 。
闭包可以从定义的上下文(Context)里捕获和存储常量或者变量的引用。这被称为“闭合”(closing over)了这些常量和变量,这也是“闭包”名称的由来。Swift 帮你处理了所有捕获相关的内存管理。
注意
不用担心你不理解“捕获”这个概念。会在下文里详细解释。 在函数一章中介绍的全局和嵌套函数,其实是闭包的特殊形式,闭包表现为三种形式:
拥有一个函数名,并且不捕获任何值的闭包称为全局函数。 拥有一个函数名,并且从外部函数捕获值的闭包,称为嵌套函数。 使用轻量级语法编写,并且没有命名的闭包表达式,可以从周围上下文中捕获值 Swift 的闭包表达式语法拥有一个干净、清晰风格,针对大多数应用场景里做了优化,倾向于简明、整洁的语法:
根据上下文信息,为参数和返回值的自动做类型推断。 单一闭包表达式的隐式返回。 参数的速记法。 拖尾闭包(Trailing Closure)语法。 下面我们开始详细介绍。
闭包表达式 在嵌套函数一节中介绍的嵌套函数,是在一个更大的函数内部定义和命名一部分自包含的代码块的常见方式。尽管如此,编写不需要完整的声明和命名的函数构造有时候会更有用处。这在你需要将其他函数当做一个或者多个参数传递的时候会很常见。
闭包表达式 就是一种编写简短并且清晰的内联式闭包的方式。闭包表达式提供了了多种语法,优化到最简单的方式来编写闭包,而且没有失去代码的清晰和意图。下面小节中举例提到的例子,就是通过数次迭代重新定义一个排序函数来展示这些优化,每一次迭代步骤中的表达式都拥有相同的功能,但是将更加简明。
排序函数 Swift 的标准库提供了一个sort函数,可以用来排序一个已知类型的数组,在你提供的排序闭包函数的输出的基础上做到。当完成排序后,sort函数返回一个新的数组,类型和大小都跟“旧”的一样,并且里面的元素按照正确的顺序排好序。
下面的闭包表达式例子,使用sort按照字母倒序的顺序排序一个字符串数组,这是初始数组(未排序的):
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] sort函数接收两个参数:
一个已知类型的数组。 一个接收两个相同类型的数组元素的闭包,返回一个布尔值,表示第一个参数是在第二个参数之前还是之后。排序闭包如果返回true,表示第一个参数应该在第二个参数之前,false则相反。 这个例子排序一个字符串数组,因此排序闭包必须是一个(String, String) -> Bool签名的函数。
编写一个类型正确的普通函数就能做到,传入sort作为第二个参数:
func backwards(s1: String, s2: String) -> Bool { return s1 > s2 } var reversed = sort(names, backwards) // reversed的数组等于["Ewa", "Daniella", "Chris", "Barry", "Alex"] 如果第一个字符串(s1)比第二个字符串(s2)大,backwards函数返回true,表示最终的结果数组中s1应该在s2之前。对于字符串中的字符(character)来说,所谓“更大”的意思就是“出现在字母表的更后面”。也就是说字母 “B” 比字母 “A"更大,字符串 “Tom” 比字符串 “Tim” 更大(因为第二个位置的字母o在字母i后面——译者注)。这就实现了字母顺序的倒序排序,使得 “Barry” 放在了 “Alex” 之前等等。
Category: trick-amp-tips
Category: uncategorized
Category: 产品
Category: 公司
Posts
这是一个招聘帖
加入蚂蚁金服还有 4 个月就满两年了,这一年半来了,还是做了一些很有意思的事情。现在在 twitter 分享比较多,还有关注我博客的朋友可以关注下。
从今年开始,我们小组总算成型了,目前专注在做时序数据库方向,这个时序数据库有应用在蚂蚁内部的监控、数值指标分析等场景里,落地了很多场景,今年预计会有一个更大的场景来落地,面临的技术挑战很大。除了海量的数据写入压力之外,对于成本和性能也提出了更高的要求。如果仅仅是时序,想象空间仍然是有限的,我们还在开拓更多的应用方向。除了存储之外,我认为现在其实我们已经收集了非常多的数据,但是这些数据的价值还没有充分挖掘,为了从这些数据中抽取有价值的信息,无论是使用传统大数据的分析方法,或者使用 AI 机器学习来挖掘,都需要数据库提供一套可供分析计算的框架出来,而这一块的挑战就更大了。
我们的技术栈是 golang + rust + java:
Rust 用于编写数据库的存储引擎,我们把它当成更安全的 cpp 在使用。总体而言, rust 除了刚开始的学习曲线稍微陡峭点以外,熟悉之后的开发效率还是可以的,而性能优势比之带 GC 的语言来说就更大了。我们选择 rust 来写引擎的原因,除了看重语言和平台带来的性能红利之外,更重要的原因是使用底层的系统语言,你才能更好贴近硬件,利用好硬件的能力,特别是现在“新”硬件的发展。隔着一个虚拟机,你能做的事情总是受限的。 我们使用 Golang 做集群的分布式架构,类似 gateway 和分片、容灾调度等集群管理工作,随着计算方向的进一步清晰,未来可能还会使用 Golang 做类似计算调度的事情等。 最后,我们用 java 在做大数据分析这块,大数据分析引擎目前还是以 java 居多,作为更直接接触用户的方向,适配公司内的主流语言是必须的,另外团队内也有很资深的 java 工程师也是一方面因素。 我们这个小组不大,目前还不到 10 个人,也是我喜欢的风格,做事情靠谱,氛围简单,所以下面是一个广告时间。
Rust 引擎研发工程师 CeresDB 作为高性能、低成本并具有实时分析能力的新一代分布式时序数据库,已经应用在蚂蚁金服主站、网商和金融云等诸多场景中。我们面临着海量数据存储、访问和分析的技术挑战,急需对此方向感兴趣的朋友加入。
你将参与蚂蚁金服时序数据库 CeresDB 存储引擎的研发,包括但不限于:
核心存储引擎的研发。 存储与计算分离架构的研发。 引擎层面对于实时计算和分析框架的研发。 软硬件结合技术的探索和应用。 岗位信息:
层级:p7~p9 工作地点:杭州/北京 我们对你的要求:
工作三年以上,有存储引擎研发经验者优先。 计算机基础扎实,熟悉常用数据结构和算法,掌握 C/C++/Rust 语言中的至少一门,愿意学习和使用 Rust 语言。 熟悉 linux 系统,有丰富的系统编程经验者优先;有良好的编码习惯,追求卓越的工程实践,易于沟通,并具有持续学习的能力。 对于时序数据库领域有所了解和研究的优先,有系统性能调优经验者优先。 Golang 研发工程师 你将参与蚂蚁金服时序数据库 CeresDB 的研发,包括但不限于:
Category: 工程
Category: 并发
Category: 并发-性能
Posts
自旋锁的优化
自旋锁的提出主要也是为了解决多核系统的同步问题。当锁要保护的代码块执行很快的时候,并且争抢不是非常激烈的时候,自旋锁的比之重量级的需要切换上下文的互斥锁能带来更好的性能表现。对于非多核系统,用户态的自旋锁空耗 CPU,反而降低了整个系统的 progress,作用反而不大。
此外,当保护的代码块执行较为耗时,或者自旋锁的争抢非常激烈的时候,自旋锁本身就消耗了大量无谓的 CPU ,这种情况下还不如使用互斥锁,让出 CPU 给任务执行,提高实际的 CPU 利用率。
我在前面一篇介绍 CPU 高速缓存的博客,举了个例子用 AtomicInteger 的 compareAndSet 实现一个自旋锁,主要是为了演示在 SMP 下通过增加一个内循环,来减少锁状态 state 在多个 CPU 之间的『颠簸』。
但是其实这个例子如果用 synchronized 或者 ReentrantLock 改写,都会快得多。比如换成 ReentrantLock,
private ReentrantLock lock = new ReentrantLock(); /** * 递增 count,使用 ReentrantLock 保护 count */ public void incr() { lock.lock(); count = count + 1; lock.unlock(); } 运行时间下降到了 150~230 毫秒,比之原来测试的两个版本快了一个数量级。
原因就在于递增 +1 这个操作非常快,导致自旋锁版本的代码线程争抢锁非常激烈,大家抢的头破血流,但是任务却没有多大进展,空耗了大量 CPU。就好像一堆人抢着去上卫生间,大家都不排队,你挤我抢,反而堵在了门口,卫生间的实际利用率下降了。从这个生活中的例子出发,我们可以想到一些优化的方法,比如分散每个人的争抢时间,比如排队。
因此,自旋锁的优化有一些思路:
首先是退让(Back off),每个线程在争抢锁之前等待一小会,通常第一次不退让,但是之后就按照某个规则变化这个退让时间,我们修改下原来的 BusyLock 例子来演示这个优化,这里采用指数退让:
//最大退让时间 10 毫秒 private static final int max_backoff_msg=10; /** * 利用 CAS 实现自旋锁 */ private void lock() { //其实退让时间 1 毫秒。 int backoff=1; while (!
Category: 开发心得
Category: 开源
Posts
最近做的一些 clojure 开源项目
最近总结下最近的一些开发经验,形成几个 clojure 的开源项目:
clj.qrcode:二维码生成 https://github.com/killme2008/clj.qrgen 示例: (use 'clj.qrgen) (as-file (from "hello world")) (as-bytes (from "hello world")) (from (vcard "John Doe" :email "john.doe@example.org" :address "John Doe Street 1, 5678 Doestown" :title "Mister" :company "John Doe Inc." :phonenumber "1234" :website "www.example.org")) secure-rand:安全随机数生成器 https://github.com/killme2008/secure-rand 想要安全的随机数,还是要使用 java.security.SecureRandom 类 示例: (ns test (:refer-clojure :exclude [rand rand-int rand-nth]) (:use [secure-rand.core :only [rand rand-int rand-nth]])) (rand) (rand 10) (rand-int 100) (rand-nth (range 10)) (secure-rand.core/base64 32) clj.qiniu:七牛云存储 SDK https://github.
Posts
Xmemcached和MetaQ发布新版本
<pre><code>String ns = "user-id"; this.memcachedClient.withNamespace(ns, new MemcachedClientCallable<Void>() {
public Void call(MemcachedClient client) throws MemcachedException, InterruptedException, TimeoutException { //username is in 'user-id' namespace. client.set("username", username); } }); //invalidate the namespace this.memcachedClient.invalidateNamespace(ns); <li> <p> 修复bug,包括setEnableHeartBeat方法无法正常工作,delete方法在连接kestrel最新版本的时候协议错误等。 </p> </li>
Posts
发布MetaQ 1.4.4
<li> <p> 添加新选项updateConsumerOffsets,当消费者的offset不在Broker的数据范围内,是否强制更新消费者的offset为当前最大offset。默认为false。测试开发环境建议开启此选项,生产环境不建议。 </p> </li> <li> <p> 添加新选项loadMessageStoresInParallel,是否在启动的时候并行加载数据,提高启动速度,默认为false。 </p> </li> <li> <p> sync协议新增checksum,类似put </p> </li> <li> <p> stats协议新增reset和help命令,分别用于重设实时统计数据和打印帮助信息。 </p> </li> <li> <p> 添加服务端启动时的配置参数校验,帮助检查配置错误。 </p> </li> <li> <p> metaServer.sh新增slave-status命令,用于查看异步复制状态。 </p> </li> <li> <p> 更优雅地关闭服务器,避免数据损坏。 </p> </li> <li> <p> 当服务器端列表变动的时候,尽量避免producer发送消息异常。 </p> </li> <li> <p> 当同步复制的slave服务端无法服务时,master服务器将尽快返回失败异常给producer。 </p> </li> <li> <p> 改进consumer的负载均衡实现,更快速和稳定地实现负载均衡。 </p> </li> <li> <p> 修复producer可能出现的连接内存泄漏。 </p> </li> <li> <p> Java客户端<code>MessageConsumer</code>接口新增<code>setRejectConsumptionHandler</code>方法用于设置拒绝消费处理器。 </p> </li> <li> <p> <code>Message</code>对象新增<code>readOnly</code>属性,可设置消息是否只读。在<code>PartitionSelector</code>的getPartition方法中,消息将强制为只读。 </p> </li> <li> <p> <code>MessageListener</code>接口的<code>recieveMessages</code>方法可抛出<code>InterruptedException</code>的受控异常,响应中断。当中断发生时正在消费的消息将被回滚。 </p> </li> <li> <p> 文档已经更新: <a href="https://github.
Category: 开源-clojure
Posts
近段时间做的一些 clojure 轮子
LeanCloud 可能(应该是)国内最大规模的 clojure 应用,无论是存储、推送还是聊天都是构建在 clojure 之上。单纯 API 服务,每天的规模都是亿次规模的动态调用请求。使用一门小众语言的后果是,你需要造很多别的语言里已经有的轮子。好在 clojure 可以直接用 java 类库,很多轮子你只是包装下 java 类库即可。
我们已经造了很多的 clojure 轮子。下面说说我最近造的一些 clojure 轮子。
Hystrix 相关 首先是跟 Netflix/Hystrix 相关的。Hystrix 的设计理念真是相见恨晚,早在淘宝的时候就听过大名,真正开始使用和了解才从今年开始。从他的设计文档来看,我过去很多的土法轮子人家都总结成 Pattern,并且设计了美妙的 API,例如 Request Collapsing 和 Request Caching 都是很朴素的想法,我在 xmemcached 实现里就做了 get 请求和 set 请求合并等技巧来提升性能;对外部调用利用线程池和信号量做隔离,原来在 Notify 的实现上也充分使用了这些技术。但是没有它总结的这么好,并且提供了丰富的配置项。一个侧面反映了我的抽象能力上的欠缺,或者说思考的还不够深入。
回到正题,我开始在我们的 API 服务里使用 hystrix 隔离和控制各种外部调用,使用了 hystrix-clj,这个轮子是官方提供的, API 封装的非常漂亮,你只需要将 defn 替换成 defcommand 就可以将一个普通的 clojure 函数用 hystrix 封装起来,并且利用 metadata 来配置 hystrix,充分体现了 clojure 的能力。不过这个库原来在处理参数重载的函数的时候有 Bug,我提了个 PR 解决了下,已经大量应用在我们的服务上了。
Hystrix 提供了一个 dashboard 用来实时展现各种服务的 QPS(单机和集群)、平均耗时、错误统计等,官方推荐用 hystrix-event-stream-clj ,在你的 service 里提供一个 /hystrix.
Category: 开源-xmemcached-java
Posts
Xmemcached 死锁分析和 Aviator 可变参数方法实现
首先是 xmemcached 发了 2.2.0 版本,最重要解决的问题就是请求超时。详细的情况可以参考这个 issue 。推荐所有还在用 xmc 的朋友升级到这个版本,性能和稳定性都有所改进。
这个 bug 的原因可能更值得说道说道。
xmemcached 本身会对发出去的请求维护一个队列,在 onMessageSent 也就是消息写到 socket 后将请求放入队列,然后在收到 memcached 返回应答的时候,找出当前的请求来 decode 应答内容。伪代码是这样:
//Handler 里加入队列。 public void onMessageSent(Command msg, Session session){ session.getQueue().offer(msg); } //Decoder 里做解码 public Command decode(ByteBuffer buf, Session session){ Command cmd = session.getQueue().take().decode(buf); if(cmd!=null) return cmd; else return null; } 这个 Bug 的关键就在于加入队列的时候和 take 的使用。 take 会阻塞当前操作,直到队列中有可用的元素或者被中断。而我们放入队列的时候是在命令被完全写入 socket 之后(有没有发出去,你无法确认的,因为有 socket 缓冲区、网卡缓冲区的存在)。其次是这两段逻辑是发生在同一个处理线程上。
那么当用户写入一个超过 1M 的数据的时候,假设是 2M。因为 memcached 最多只允许保存 1M 大小的数据,当 xmemcached 将超过 1M 但是还没有达到 2M 的数据发送到 memcached 后, memcached 立即应答返回错误。但是此时,数据还没有完全写出去,导致命令没有被加入队列,同时 take 也取不到数据,我们遇到了死锁: take 在等待命令加入,而写入命令数据的线程被 take 阻塞了没有机会继续写。
Category: 思考
Posts
有无之间
我们对于“拥有”总有一种执念,这可能是动物的本能,从婴儿时期就很明显的表现出来,这个玩具是我的,这个好吃的是我的。这种本能是自然界教育出来的,没有毛病。 昨天不知道为何,突然思考起拥有的真正含义,得出一个结论,我们并没有真正拥有。很巧,晚上跟朋友吃饭,也聊到这个话题。拥有本质上只是一个假象,某个物件在你手中,也只是暂时的拥有,它终有一天会转手,会损坏,会丢失,会蒙灰,它跟你的缘分也就那短短的几年甚至几天几个小时。我们和父母、妻儿、亲友的羁绊,也只是这一世,你并没有真正地拥有你的儿子,你的妻子,他们有自己的想法,思考,独立的人格。我们能够拥有的都只是这短短数十年的陪伴、关爱和争吵。
其实这个想法并不新鲜,跟佛家所说的缘聚缘散是一个道理。只是更进一步的思考,如果一切东西都是短暂拥有,是不是不值得去奋斗?不值得去珍惜?我经常跟同事说要有 ownership 的意识,是不是在忽悠人?答案是否定的。让你不执着于“有”,不表示你应该放弃“有”,不去奋斗,不去珍惜,不去坚持。相反,正因为天下没有不散的筵席,我们更应该珍惜所“有”,活在当下,不悔过去,不忧未来。
简而言之:恰到好处。增之一分则太长,减之一分则太短。这个度的把控在于心,而按照王阳明的说法心即理,不可外求天理,你要做的是时时刻刻去私心,行的是为善去恶的功夫。最高的境界可能就是孔子说的从心所欲不逾矩了。
Category: 涂鸦
Posts
昨天、今天和明天
这题目非常俗套,不过同名的本山大叔的小品我却是很喜欢,哪怕很多人说有歧视的嫌疑。
其实这篇博客就是吹吹水,总结下我过去 5 年做了哪些微小的工作,为什么加入蚂蚁金服以及加入蚂蚁金服准备做什么。
2012 年我从淘宝离职加入了 AVOS 中国公司,很多人看来不理解,为什么放着到手可得的股票而去加入一家前途未卜的创业公司?其实很简单,当时我在淘宝呆了三年,一直做消息中间件相关的工作,先是 Notify(蚂蚁这边叫 msgbroker ),后来是发现了 kafka,所以有了 MetaQ。。以当时的眼光看,MetaQ 很多东西都是更强大,功能更全面,比如高可靠的复制方案、事务消息乃至支持 XA 协议的分布式事务。但是做久了,确实腻歪,然后去转岗,最后半年去做 TAE(Taobao AppEngine)项目,第一次接触和引入了 Storm,当然,更重要的是结识了 Clojure 这门语言。顺其自然,国内玩 clojure 的人这么少,就认识了现在 LeanCloud 的 CEO 江宏,在杭州咖啡馆聊了一次,没过多久,我就决定加入了。所以说原因很简单,一是我想换个环境,当时的眼界和心境决定了我没法继续在阿里呆下去,我想离开工作了三年的熟悉环境,换一门新语言,换一个完全不同的工作环境,认识不同的人,尝试下创业是什么感受。二是江博士的个人影响力,我认同他的许多观点,也庆幸能参与到他组建的这个优秀团队。
如果以这些原因来看,我的目的完全达到了, AVOS 中国办公室当时是一个非常牛逼的团队,我认识了很多远比我优秀的工程师,无论是国内的,还是国外的,有一些现在还保持着很好的联系。我经历了数个产品的从生到死,并且全程参与到最终 LeanCloud 这三年来的成长和发展。这 5 年来我也基本一直在用 clojure 写代码,不敢说完全精通,但是至少可以吹牛逼地说,我是国内少数的几个有丰富 clojure 实践经验的工程师。更重要的是这 5 年来经历的一切,对我来说是完全全新的体验。创业非常艰难,同样,创业也非常激动人心。
AVOS 中国团队经历了好几个产品,美味书签开始,我们先是想尝试做 delicous 的中国版本,加上社交和设计类的元素。但是总所周知, delicous 已经死了。我这个阶段还是主要在后端,做一些文本抽取算法、后端服务之类的工作,也帮助 delicious 基于 Solr Cloud 做了新的搜索系统,解决数亿文档的搜索问题。在美味书签失败后,类似无觅这样的基于兴趣 tag 订阅的阅读类开始兴起(其实今日头条也是),我的同事孙宁开始主导美味爱读的开发,基本是参照着 Prismatic 来做,我又参与进去,记得是做后端 API 和分类算法,第一次接触了聚类推荐算法相关的东西。美味爱读真的是非常好的产品,可惜他的原型 Prismatic 也死了,不过他们留下很多美妙的 clojure 库,比如 schema。在美味爱读的开发的同时,另一拨同事做了玩拍这个短视频项目,因为当时短视频 Vine 非常火,我们“又”赶热潮去做了玩拍。玩拍的做的还是算是比较成功,火过一阵子,但是很可惜,仍然是没有坚持下去,加上没有持续的内容运营,更重要的是团队上大变动,反而当时对标的秒拍坚持到了现在。玩拍第一个版本的后端 API 是我设计和实现,后续因为参与美味爱读和 Delicious 重构,就退出了这个项目。然后同事朱老板一直很关注 parse.com,认为移动大潮下,这个东西还是有价值的,说要不我们做一个试试。LeanCloud 就这样横空出世。很夸张,但是却是现实, AVOS 中国公司的末期有三个产品在齐头并进。
Posts
更好,还是更坏?
变化时时刻刻,缓慢的,或剧烈的,无可避免,就像崔健唱的那样:不是我不明白,这世界变化快。不要说世界,就说自个儿,变化也太快,更重要的是你也不明白这变化是好的,还是坏的。当然,更佛家一点的说法,色即是空,空即是色,世界就是我,我就是世界,所以没有特别必要强调『我』或者『世界』,因为两者是『一体』的。
明显坏的变化,肚子大了,三高来了,今年还犯了一次痛风,要和啤酒海鲜菠菜说再见了。身体在告警:您的余额要不足了,请及时充值。跑步不少,但是欠费更多。
还有个坏的变化,工作上的冲劲似乎没了,叹气的时候多了,旁观的时候多了,憋着话的时候也多了。妥协的多了,抗争的少了。眼看着一头牛要滑向深渊,你得用力拽住、劝慰,再慢慢拉回来。
另一个可能是不好不坏的变化,从无产者变成可疑的有产者(当老毛还挂在城楼上的,我们可能是有产者吗?),心态没那么愤世嫉俗了,关注民生新闻少了,愤怒的次数少了,有『小粉红』的倾向,从全盘西化转向中国人的事情还是要自己解决。某些观点越来越中立,越来越中庸。同样,也可能越来越宽容,大家要和谐,不要搞大新闻。
好的变化也有一些,恢复写博客算是一个,陆续在更新开源库也算一个,读书相比去年也读的多了一点,闲书少了,技术的多了一点。其他的,和儿子关系相处更好了一点,也算是个好变化。
无论是好的,还是坏的,只能接受,因为这就是世界,也就是我。正面的或者负面的,你只能拥抱、亲吻、吵架、小心地劝导、耐心地包容,它们将伴随一生,如影随形,越早承认并坦然接受这一点,生活会更好点。
Posts
死之杂感
『死』这个字本身似乎就很沉重,是【歹】事,也有【夕阳无限好,只是近黄昏】的命运感。
我对死亡的认识来源于农村的丧事。在这样的事情发生在别人家的时候,小时候的我只记得要请乐队吹唢呐,全村很多人会到这一户家人帮忙,不甚恭敬地说,还有好吃的。 后来我八岁那年,我的爷爷去世了。我对爷爷留有的印象已经不多,很严肃,对我应该也是很好的,但是心里其实一直觉的怕。我爷爷出殡那边,大人跟我说,要我在出殡队伍最前面抱着爷爷的相片走, 因为我是长孙,有这样一个责任和义务。不过年幼的我,对这个事情却是非常抗拒,心态回想起来,一个可能是怕,另一个可能是不喜欢成为众人目光的焦点。后来出殡的队伍里就没有了我,而其实我是很想 去送我爷爷的。这个事情很长时间都成为我的一个心病,乃至于我的父母去外地做生意的时候,要带上我们三兄弟一起出去,我却主动留下来,想和奶奶一起生活。
后来对于亲人丧事的记忆,还有我外公的去世,印象中我妈妈非常伤心,更多的却是没有印象了。
对于每年时间都觉的那么漫长的小孩来说,【死亡】真是遥远而不可理解的事情。
年龄渐长,离开家乡,丧事几乎没有再去【围观】过,耳濡目染的是各种新闻报道里的死亡事件。世界上每天都有人出生,有人死去,出生的方式相同,而死亡的方式却各式各样。08 年汶川地震,见证了太多的 生离死别,一副照片一直留在脑海里:丈夫骑着摩托车,载着死去的妻子,要带她回家。死亡带不走承诺。
人到中年,死亡又像黎明前地平线上的微光一样,逐渐可见。而相应的,这样不幸的消息也开始出现在我见闻的人群里。
几年前,从同学那听到一个消息,我一个小学同学不幸去世,是醉酒后骑摩托车不幸出了车祸,留下妻子和年幼的孩子。 这个同学,虽然不是特别要好,也几乎没有联系,但是也是从小一起长大,一起玩过,一起读过小学初中的朋友,这样『熟悉』的一个人突然消失在这个世界上,让我第一次自发的感慨命运无常,世事难料。
再后来,每年回到农村老家,原来那些看着我长大的爷爷奶奶也一个个慢慢离开这个世界了,他们的丧事我没有参加,心里总有点遗憾,我应该送他们一下,他们的音容笑貌,偶尔还能在这样的夜晚想起。
2012 年的冬天,我在北京,从秋天开始跟着同事参加户外活动,主要是绿野上的活动,大小海驼、北灵山、百花山、长城等等路线,每个周末一条路线,走个 10 到 20 公里,洗去一周的工作疲倦。 『2012年12月23日东灵山2名驴友遇难』,这个新闻我却是在网易新闻上看到,而本来的情况,是那个周末我其实报名了这个活动,但是因为晚起还是什么缘故,没有去成。这个不幸的事件里的一个人是一起爬过慕田峪长城的马云飞, 我想我会永远记得这个名字,一个充满热情的瘦高小伙。我印象中参加的活动有两次是碰到他,雾灵山和慕田峪长城。爬雾灵山那次,有过出事的苗头,如果我没有记错就是东灵山的这个领队,雪刚下过,我们爬到垭口已经是下午3,4点钟,按照原定计划 是要翻过山头,但是在走了一段风大雪深的上坡路后,我们几个果断向领队提出应该下撤,考虑到齐腰深的积雪和队伍里不少的女生,翻过山头的到另一边下撤的风险太大,最好是原路返回。领队听从了我们几个人意见,最后大家安全下撤回来。慕田峪长城很美,有一段很陡的长城很难爬,我记得马云飞在下面跟我们说起,箭扣比这个难多了。我一直希望去箭扣试下,不过自从离开北京后,是没有什么机会了。
马云飞是和另一个朋友,在冲顶失败下撤的过程中迷路,失温而不幸离世。我后来常想,如果我那天也去了,以我那时候的性子,也很有可能想跟着冲一下,也许也留在了那东灵山上。或者另一个可能,我会极力阻止他们两个去冒险,也许大家都还好好地玩着。但是年纪见长的一个后果,就是明白没有那么多如果。
今天为什么突然写这么个博客,其实是因为我住的单元有人跳楼了,下午和老婆孩子看电影回来,看到楼下有不少警察,拉了警戒圈,还以为在抓什么罪犯,后来听旁边的人说才知道是有人跳楼,从 21 层跳下。这是需要多大的勇气。我很想对这个朋友说,有这个勇气跳下,其实更应该有勇气活下去。
人生绝非坦途,你我艰难前行,我相信每个人或多或少在某个时候想起【也许就这么死了也不错的】念头,但是还是有很多美好的东西,值得留念和坚守 —— 且行且珍惜。
Posts
拥抱 Octopress,再见 WordPress
花了点时间,从 WordPress 迁移到 Octopress,跟我们公司的官方博客刚好相反 :D 。主要是实在折腾不好WP的那一堆插件。也习惯用 mou.app 写博客了。
迁移过程中,这个工具 wordpress-to-jekyll-exporter 帮了大忙,自动从 WordPress 导出数据成 Octopress 需要的格式,相当方便。
使用了 solarized-octopress-theme 主题,再折腾下 多说 评论框,基本就完成了。
继续博客之旅。
Category: 演讲
Category: 算法-负载均衡-jiq
Posts
Join Idle Queue 负载均衡算法解析
JIQ 是微软发的一篇论文《Join-Idle-Queue: A Novel Load Balancing Algorithm for Dynamically Scalable Web Services》里描述的负载均衡算法,这里总结下我所理解的内容。
背景 负载均衡很常见,比如我们经常用 nginx 做反向代理和负载均衡,Nginx 也支持了 weight、ip_hash、url_hash 等均衡算法。
负载均衡的图示:
任务 jobs 不停地经由多个 dispatcher 转发给后面的 processor server 处理。
dispatcher 选择哪一条 processor 来转发任务的过程就是 load balance 的核心问题。尽量降低任务的响应时间是我们的目标。
最简单的算法可能是随机或者轮询,但是这种简单的策略会造成响应时间的最大化,特别是高负载的情况下。
更优化的策略有:
JSQ: Join-the-Shortest-Queue,每次将任务加入最少任务队列的 server。这就要求 dispatcher 收集每个 processor server 的任务队列大小信息,但是随着 dispatcher 本身的集群化以及云服务厂商的大规模应用,这个收集产生的网络通讯就更加膨胀了。 SQ(d)(Power-of-d):每当任务到达的时候, dispatcher 就随机取样 d 个 processor 服务的队列大小,选择最小任务队列的那个派发。通常 d 选择为 2。这个算法相比随机算法能带来响应时间指数级别的提升。但是仍然需要在分发任务的时候获取 processor 队列信息,这个同步调用在任务派发的关键路径上,对性能有很大影响。 工作窃取和共享:工作窃取就是空闲队列主动去从其他任务队列『窃取』任务,或者繁忙的队列主动将任务『推送』给其他空闲队列。这个算法更适合共享内存的系统,对于 web 负载均衡,在不同后端 server 之间做任务的窃取或者推送会带来额外的开销和复杂度,想象一个 web http 请求如何转交到另一台后端 server,涉及 TCP 链接的迁移和请求的同步等等。 JIQ 全称就是 Join-Idle-Queue,它的提出就是为了解决大规模 Web Services 的负载均衡问题。
Category: 编程
Category: 编程小记
Posts
编程小记: bug、clojure 状态和 paxos
一个 Bug 前段时间观察我们 API 系统的 hystrix 监控,一直发现一个函数 cache/add 的调用特别的高,在整个集群范围内高峰的时候接近 3 到 4 万的 QPS,跟其他指标比起来非常的碍眼,极不正常。
抽了点时间专门调查了下,原来是不小心掉进去了 hystrix request cache 的坑里。
Hystrix Request Cache 的原理很简单,在同一个 RequestContext 里,对某个 command 调用同样的参数,第一次调用的结果将被缓存,后续的对同样参数的请求将直接返回第一次的结果,通过内存换效率,类似 clojure 的 memoize。
简单例子:
(require '[com.netflix.hystrix.core :refer [defcommand with-request-context]])) (def call-times (atom 0)) (defcommand myinc {:hystrix/cache-key-fn (fn [i] (str i))} [i] (swap! call-times inc) ;;统计调用次数 (+ 1 i)) (with-request-context ;;调用了两次 myinc (myinc 1) (myinc 1)) (println @call-times) ;; call-times 只统计了一次调用。 业务代码里有一段逻辑大概是这样:
(def get-or-create [k nv] (if-let [v (get-value k)] v (if-not (add k nv) (recur k nv) nv))) 其中 get-value 是一个 hystrix command 设置了 cache-fn 启用了请求缓存。这段代码是尝试先从缓存里加载 k 对应的值,如果没有,就将 nv 存储到 k 键上,如果 add 存储成功,返回 nv,如果 add 失败,循环重试(表示有其他人 add 成功,我们可以重试 get-value)。
Category: 翻译
Category: 虚拟化
Category: 计算理论-clojure
Posts
Clojure 模拟图灵机
最近在读《计算的本质:深入剖析程序和计算机 》,一本关于计算理论的小册子,使用 Ruby 语言介绍计算理论,第一步分从状态机开始直接,DFA/NFA、自动下推机直到图灵机,并且每个小章节都给出了代码例子。第二部分开始介绍 lambda 、丘奇数、停机问题等函数式编程的基础知识,挺好玩的一个阅读过程。
作者提供的 Ruby 代码在这里。
我试着用 Clojure 重新实现了里面的图灵机模拟器的例子,
(ns cljcomputionbook.tm (:require [clojure.string :as cs])) ;;磁带 (defrecord Tape [left middle right blank] Object (toString [tape] (pr-str tape))) (defmethod print-method Tape [tape writer] (.write writer (format "#<Tape %s(%s)%s>" (cs/join (:left tape)) (:middle tape) (cs/join (:right tape))))) (defn write [{:keys [left right blank]} ch] (Tape. left ch right blank)) (defmulti move-head (fn [tape direction] direction)) (defmethod move-head :left [{:keys [left middle right blank]} _] (Tape.
Category: 跑步
Posts
我的实用跑步装备推荐
烧“装备”是每个宅男的爱好之一,无论是电子设备、摄影器材、单车等等,都是耗钱的爱好。跑步也不例外,数数我到现在入手的跑步装备,满打满算也有5,6千块了。不过,这里想介绍下实际跑了这半年多以来给我带来最大价值的跑步装备,并给出一些推荐。
首先是 GPS 手表,我入手的是佳明 620 跑表,专门用于跑步的,除了通常 GPS 运动手表的路线记录、时间记录等等之外,配合心率带还可以记录心率,并且针对跑步还有各种指标,例如步频、配速、记圈、垂直幅度和触地时间等统计,并且提供了手机应用和手表进行蓝牙数据同步,WIFI 同步我从来没有成功过。
不过从实用角度,我个人更推荐使用手机应用,比如阿迪的 miCoach 应用,配合专门的蓝牙心率带就足够了。类似垂直幅度和触地时间这样的指标对于业余跑者其实没有太多价值。心率带还是有价值的,每个人的最大安全心率是 220 - 年龄,譬如我周岁 30,最大的安全心率就是 190,超过就是不健康的。从平常的慢跑来说,心率维持在 110-160之间是一个比较合理的区间。620 好的一点就是为你定义了 5 个心率区间,并且他在网站上给你一些训练计划,这些训练计划基本是按照心率区间来训练。
miCoach 的训练计划也不错,并且可以同步到手机日历,做到每日提醒。佳明的 connect 网站也可以发布你的训练计划日历,然后在手机里的日历程序里订阅发布的 URL 也可以做到日历同步。
有了心率统计,我推荐尝试下 MAF 训练法——号称最安全也是最适合新手的跑步训练法。
其次,针对眼镜男(比如我),为了解决流汗到眼睛模糊眼镜的问题,我推荐买一条 GUTR 导汗带,它的原理很简单,但是真的能解决问题;正版稍微贵一点,淘宝上有山寨版可选。
第三,跑鞋上,我推荐美津浓(Mizuno)的跑鞋,也就是村上在《当我跑步的时候我在想什么》里提到的所谓“水野牌跑鞋”(翻译问题)。我手头有三双跑鞋: Asicis Nimbus 14,Ascis GT2000 2代以及美津浓新款的 wave legend。跑了这半年来的感受, GT2000 最重又窄最不舒服,nimbus太软(穿着跑了半马比赛),legend 最硬但是跑起来最舒适,并且价格也是最便宜的。美津浓真的性价比很高,物理减震,虽然穿起来很硬,但是跑起来回弹很好,并且透气也非常好。跑鞋最好能有个两双轮流换,延长跑鞋寿命是一方面,另一方面是有研究表明轮流使用不同跑鞋可以减少受伤的风险。P.S. 美津浓还提供了一个脚型测试网站,当然最好还是能现场试鞋。
第四,手机腰包,有的朋友可能喜欢臂袋,但是我觉的双手摆动不舒服,特别是大屏手机挂手上很不方便,我也不喜欢跑步的时候听音乐(安全第一),因此更偏向使用专门的腰包。这里要推荐 跑步指南店里的这款腰包,围起来很合身,跑起来不会晃,大小也能放入 IPv6s ,价格合理,强烈推荐。我还在跑步指南店里入了不少袜子和跑步的衣裤,总体来讲质量都不错。
以上推荐装备的大概花销: 469(miCoach 蓝牙心率带) + 110 + 700(美津浓 wave rider 17) + 69 = 1348 左右,再购置一些衣裤,总费用 1500 左右,作为业余跑者,完全足够了。
最后是一些健身应用推荐(iOS 为主):
Nike Training Club: 可以选择不同类型的训练计划,有视频指导。 挑战 Plank: 练习平板支撑,也提供训练计划。 Runtastic Timer: 运动计时,可自定义训练周期(训练时间和休息间隔等)。 报名了郑开马拉松半程,如果当天空气污染不严重,就会过去郑州跑下。全马训练计划开始 2 周,计划首次全马放到无锡,也就是郑马之后两周左右。我自己立下一个心愿:每年至少跑一次全马。
Posts
记 2014 杭州半马之行
在今年 4 月决定恢复开始跑步后,我就想给自己定一个目标:参加一次半程马拉松比赛(21 公里的距离)。查询了赛事信息,最后目标定在杭州马拉松和上海马拉松两个比赛上,因为两个赛事都在11、12月份左右,我刚好跑步半年,应该能达成心愿。
接下来就是跑步和等待报名。杭马大概是9 月开始预提前报名,我很幸运地在预报名阶段抢到了一个半马名额,上传了体检报告之后,一直没有审核结果。打电话过去,一个接电话的大妈才帮我审核通过。终于,参赛这个事情是完全确定下来,心里面下定决心要去,除非那天杭州雾霾超过 300,哈哈。
回到这几天,比赛是 11 月 2 号周日,我 31 号周五晚上就赶过去杭州,跟早上就提前赶过去带儿子复查的老婆汇合。周 6 上午去领了参赛包,第一次参加长跑比赛,每个阶段感觉都很新鲜。认真阅读了参赛指南,读了一些比赛前的准备文章,大概心里有数了。好久没到杭州,下午又去西湖边转了转,晚上买了饼干、燕麦片准备第二天早上当早餐。
本来以为自己应该不至于兴奋得睡不着吧,没想到还真失眠了,辗转到凌晨 2 点才模模糊糊入睡,然后 5点半就跑起来,洗漱方便,吃了热水泡的一包麦片,搞了几片饼干,最后检查了下装备:导汗带、心率带、GPS 手表、手机腰包、鞋子、袜子、参赛号码布……通通塞到参赛包。儿子本来说要跟我去起点为我加油,后来看他实在困,起不来,还是我自己一个人出发吧,让他们到半马终点等我。
打车到了现场,那真是人山人海、锣鼓喧天……放照片,手机拍了几张,感受下气氛
到了半马存包处,别人告诉我存衣车还没到,傻傻地站着等了会,看大家都在热身,还有各种 cosplay 的朋友路过,气氛相当棒。我也脱下外套,热身了一会;然后旁边一个大哥看着我说,是不是要存包?就是那些停着的公交车,快去吧。赶紧说两声感谢,跑过去存包,原来这些公交车就是存包车啊,每个车设定一个号码范围,这个号码范围内的参赛者可以将参赛包放在那里,结束的时候领回去,每辆车都有一些大学生志愿者帮你记录、看管。
Ok,存完包就是到集结区等待开赛,我去的时候还人很少,还是太早到了,请志愿者帮忙拍了张照片,然后就等着开赛了。慢慢地,人群好像从地里冒出来那样,塞满了你的左右前后,那当然了,3万个参赛者,里面 5000 个是半马的,你可以想象这个场景。
8 点开赛,完全没有听到枪声,人声鼎沸,只知道跟着人群开始往前走,走啊走,才看到起点的拱形门,一看计时器已经 5 分钟过去了,踏过起点的计时地毯,前面这 500 米开始慢跑,晚上提不上速度,人太多了,道路旁边挤满了观众,很奇特的感觉,过去我是观众,现在我也成为跑步者中的一员了。
这次比赛没有给自己太多压力,我的目标是希望 2 个半小时内完赛就可以了。赛前两周的 15 公里以上的长距拉练告诉我问题不大,因此也没有太多心理压力。只是希望自己前半程能压住速度,保持 6 分半左右的配速,然后 15 公里后能保持住配速就不错了。
但是想法是美好的,可是你看着一个一个人超过你,会不自觉地加快脚步,人流在快速的流动,你不由自主地也会被带动起来。我只好不断地看表,强迫自己慢一点。不过后来看手表,还是前 10 公里还是跑太快了。果然过了 15 公里,右脚小腿隐隐有抽筋的感觉,虽然最终没有发生,还是在 18 公里左右的饮水点停了会,拉伸了下,然后继续跑。配速直线下降到 7,然后又悲剧地跑到了钱塘江大桥上,长长的坡我小跑着上去了,但是长长的桥面跑的让人绝望,没有一个补水点,也没有观众加油,很多人都开始走路了。只是我心里还有一个小小的坚持,我是来“跑”步的,绝不能走着到终点。
最后一公里是在自我激励中度过,不停地念着“提腿、提腿、提腿”,到达终点,踏过计时地毯,手表停留在 2:24:22,志愿者告诉我去红色遮阳伞的地方领奖牌,哇,还有奖牌,马上跑过去领了下,分量很重的牌子,做工也不错,这个要赞下。后来在回来的公交站碰到两个跑友,他们没有奖牌,我估计组委会没有准备足够多的奖牌给所有 3 个小时内完赛的朋友,听说会补上。
杭马的整个组织给我感觉很混乱,虽然我也没参加过其他比赛。混乱体现在报名、起跑、补给点、赛道封闭以及终点上,报名网站做的太烂(相比上海马拉松官网差太多了),志愿者的信息不足一问三不知,补给点不合理,赛道没有完全封闭,看到几次电动车横穿马路,半马奖牌不够,终点也不提供一点食物,人群疏散也很混乱等等。不过,我想我明年还会来,我对杭州还是很喜欢的,观众真的很热情,妹子也很养眼,天气也很适合跑步,全马的赛道确实称得上很美。
整个跑步的过程,其实没有太多描述,跑步的时候想什么?就跟村上说的,想了很多,到最后其实都忘了想什么。几个观察和感受可以提一下。首先,很多妹子真的很能跑,看着她们轻盈的步伐,我表示很羡慕。其次,观众很热情,好多家长带着小孩来路边加油,和观众击掌的感觉真棒。最后,跑步虽然是孤独的运动,但是这种群众性的赛事,更像是欢聚的节日,大家不仅仅是来跑步,更是来参加一次聚会,一次互动。总之,非常享受这次半马的过程,虽然回来之后到现在腿还在痛,不过呢,我又定了个目标:明年,咱整一到两次全马试试?
Posts
跑步两月记
扯淡篇 4月底开始恢复跑步,体检检查出轻度脂肪肝,刚步入 30 ,这个征兆不是特别好。因此,还是恢复跑步。原来在北京,是不敢跑懒得跑,雾霾太厉害,并且离奥森公园还是有点距离。到了昆山,虽然空气还是不理想,但是比北京还是好点,加上离家不远有个河边公园,人少车少,跑步的理想场所。
跑到现在,加上三周前开始去健身房锻炼,原来是不敢去健身房的,以为肌肉猛男很多,我这样的胖子估计很丢人。其实呢,健身房里猛男不少,胖子更不少,完全没人在意你。跑步的成绩从原来的 8,9 分钟每公里(跟快走差不多)的平均速度,还心率狂飙的差劲情况,进展到现在可以定速 6 分钟每公里保持个接近一个小时(也就是 10 公里在一个小时左右跑完),心率维持在150左右,自己都很得意。当然,跟跑步圣经里的老马比起来,那差距还是很大。
最近三周没去跑公路了,一方面是夏天到了,在户外心率会狂飙,有中暑晕倒的可能,另一方面是空气开始糟糕。都在健身房跑步机上跑。还有就是请了私教,尝试一个课时(12节)的训练,加强下力量,顺便减脂。总体来讲,体验不错,考虑再续一个季度。不过价格真的很贵,需要说服老婆支持。
既然决心坚持下去,也给自己定了个目标,11 月份一定要去参加杭州或者上海马拉松的半程比赛(21公里)。以我现在的耐力,完成比赛应该没有问题,成绩不在乎,重在参与嘛(弱者的自我安慰)。还没有尝试过 15 公里 和 20 公里的 LSD 训练,争取这个月内试一次。
装备篇 作为装备控,没跑多远的路,就开始在跑步圣经等论坛上看各种装备评测,最终备齐了一套,对我这样的初级跑着还是奢侈了一点。
GPS 手表:佳明 FR 620,配心率带。专业跑步手表,除了心率、距离、配速、卡路里之外,还有步频、垂直幅度、触底时间、虚拟伙伴和最大耗氧量测试等更专业的统计数据。如果准备长期跑,入这样一个跑步手表还是必须的。 佳明 FootPod 计步器,因为在跑步机上跑,虽然 FR620 的手表和心率带都内置加速器,但是距离偏差还是比较大,因此还是淘宝上入了个计步器,校准之后相对精确了一些。 跑鞋,片面强调跑鞋作用不可信,但是一双好的鞋子来保护脆弱的膝盖还是必要的,当然,强者都是赤脚跑步。在北京的时候就买了双 adidas marathon 10,这样的长跑鞋对我这样的胖子来说其实不合适。这鞋子回弹不错,相对较硬了点。去健身房之后,一口气入了两双跑鞋: Ascis nimbus 15 和 Mizuno wave legend。前者适合我这种外翻的脚型,并且是我这辈子穿过最舒服的鞋子,按照夸张点的说法是穿着它就像一路踩着“狗屎”走路。后者相对较硬,只有跑起来才能感觉到回弹十足,我穿着他跟着教练练器械。Nimbus 担心损耗很快,还是舍不得穿着健身,只用来跑步。 运动衣裤:淘宝上随便淘的耐克跑步短裤和T-shirt,这个没啥特别的,只要合身、透气,穿着舒服就好。 回忆篇 很多人说跑步是孤独者的运动。因为一个人跑,可能会比较闷。不过这个对我从来不是问题,独处是一种习惯。我在初中的时候,三年都坚持晨跑,天还没亮,就沿着乡村公路跑过坟场、山丘,跑步给我的生活带来了很多变化。工作后,在广州的时候也坚持夜跑天河公园,去了杭州才没有坚持运动,主要住的地方实在没有什么公园场地,我又不愿意在公路跑,也还不敢去健身房。在帝都更不用说了,雾霾加上工作繁忙,又没有心思锻炼,更是彻底荒废。眼看着体重直线上升。
现在恢复跑步,对我来说不是特别困难的事情,享受跑步时候的呼吸和汗水,希望能 keep running 下去。
Category: 闲谈
Posts
随笔
30 岁 按照周岁来算,马上要满 30,没有特殊的感受,也没有丹田火热,任督二脉将通的征兆。大多数意义,都是人赋予的,时间逐渐流逝,无论是绝对还是相对,跟人本身,没有太大关系。因此说 30 岁如何如何,太矫情了。
30岁后,还是该吃饭就吃饭,该上班就上班,革命还是请客吃饭上厕所。
社交网络 在微博,在微信,在这个博客上,你不知不觉就扮演某个角色,或者是卖萌,或者故作轻松,或是冷嘲热讽超然世外的模样。如果你将这应用还安装在手机上,未来还将出现在手表、眼镜甚至衣服上,我们的生活将完全地“永远在线”,永远时刻在扮演某个角色。无论何时何地,家人朋友聚会,公交马桶火车,拿出手机来看看是不是成为习惯了?社交网络,反而让你我失去了“社交”,留下的就是低头党横行。
我们渴望交流,但是却在真实的聚会上反而不知道如何交流,反而在网络上指点江山、呼朋唤友。我感觉相当奇特。又一个技术悖论?技术如何让社交更美好,目前的形式还远未完美,仍然是有可为的地方。
技术是新的鸿沟 技术极大地改变了生活,改变了这个世界。但是技术其实也极大地拉大了贫富差距。最近读一本书《与机器赛跑》,里面提到美国的中产阶级收入中值在走低,而不是上升。是中值,而不是统计局最喜欢的人均值。技术极大地进步,但是收入却没有,因为技术越来越快地替代人类,从过去认为非常困难的无人驾驶汽车,智能语音翻译等等技术的进展来看,这个加速过程的趋势就是用机器来代替人类。既然你的工作是越来越可替代的,你的“价格”就要下降。
而掌握了 xx 核心科技的精英人群,利用技术更快速更惊人的聚集起大量财富,无论是中国还是美国,贫富差距都在拉大,占领华尔街和占领中环都是一次启示。这就像蒸汽机发明后的工业革命,机器替代人类,将工人阶层压榨到极致,乃至马克思要写《共产宣言》来鼓动无产阶级,但是随着两次世界大战的财富再分配和资本主义的自我改良,财富不均的问题其实得到了重新平衡。而现在,似乎又一个轮回开始了,会诞生什么主义?还是第三次自我毁灭?效率与公平,真是永恒的矛盾。
人与高级动物 最近一直在想一个场景,进进出出写字楼,在写字楼和居民楼之间奔波的人们,跟蜜蜂和蚂蚁有多大的区别?从出生、接受教育、成年进入职场、结婚生子、老去,贯穿这一切的线索是工作,大多数人工作都是为了谋生,我估计生命里 50% 的清醒时间其实都贡献给了工作。毕竟没有工作,我们会没钱吃饭,没钱买房,没钱买肾6,没钱供孩子上学等等。
而蜜蜂和蚂蚁呢?贯穿他们的一生的还是什么?为了生存不停地“工作”。那么,我们跟它们的区别在哪里呢?人,仅仅是窦唯唱的“高级动物”吗?
我在想,我们大多数人真的太多的时间都被外在的事务占据了,而遗忘了作为“人”的价值在哪里。人生短短几十年,这个问题还是稍微值得思考下。
信任驱动开发 用户使用我们的产品,除了付出精力和费用之外,最宝贵,我觉的还是信任。我信任你们,才用你们的东西。而信任这种东西就跟时间一样,逝去了就基本追不回来了。
所以,我在想,除了需求驱动之外,信任驱动可能是更感性的表达方式。为了赢取用户的信任,我应该做到什么,为了保护用户对我们的这份信任,我应该去做什么。尊重信任,就是尊重自己的荣誉。