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
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 阻塞了没有机会继续写。
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
编程小记: 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)。
Posts
Leiningen 代理设置
墙已经成为平常工作效率最大的敌人。由于我们内部的 maven 仓库也部署在海外,导致 Leiningen 下载依赖经常超时。你不得不走代理才能解决。这里记录下大部分要点。
首先修改 lein 脚本本身,默认不超时,建议加入超时设置,找到下面类似的代码:
"$LEIN_JAVA_CMD" \ "${BOOTCLASSPATH[@]}" \ -Dfile.encoding=UTF-8 \ -Dmaven.wagon.http.ssl.easy=false \ -Dmaven.wagon.rto=600000 \ $LEIN_JVM_OPTS \ -Dleiningen.original.pwd="$ORIGINAL_PWD" \ -Dleiningen.script="$SCRIPT" \ -classpath "$CLASSPATH" \ clojure.main -m leiningen.core.main "$@" 新增的配置选项是 -Dmaven.wagon.rto=600000,也就是 10 分钟超时。
其次,如果你有一个 HTTP 代理, lein 尊重 http_proxy 和 https_proxy 环境变量,可以将下面代码加入 ~/.profile,也可以使用的时候 export 下:
http_proxy=http://username:password@proxy:port https_proxy=http://username:password@proxy:port 设置代理后,所有 lein 发起的 http 请求都将走代理,你可以可以设置一个白名单避免代理:
http_no_proxy="*example1.com|*example2.com|*example3.com" 最后,如果你用的是 socks5 代理,比如 shadowsocks 搭建的代理服务器,那么可以安装 privoxy 将 socks5 转为 HTTP 代理:
$ brew install privoxy 安装后,默认配置在 /usr/local/etc/privoxy/config 文件,找到下面类似这行代码,修改成你的 socks5 代理配置:
Posts
深夜杂感
可能是今晚上 8 点就上床睡觉,11 点醒过来就再也不睡着了。胡思乱想,趁着还清醒,可以稍微总结下。
人们总是说过了 30 岁,是个坎。我原来不觉的,上周刚过了 32 周岁的生日,慢慢有那么点感觉。这感觉是什么?具体地来说,是更谨小慎微,并且缺乏用于尝试和勤奋自律的劲头。
坦率地说,有家有室后,你的生活的很大一部分精力是投入在家庭生活里。如果你 20 出头,看这种日子会觉的索然无趣,但是人到中年,你会感觉,家可能是才是最重要的部分,你会乐在其中。
所有时间管理的核心其实就是分配你的精力。这边投入多了,那边必然少了。加上身体机能的下降,生活的磨砺,你的心性会有渐进的变化。变化有好有坏,哪个方向,在于一心。
但是呢,我不甘心如此下去了。
还没有去过几个国家,还没有做出一点值得称道的东西,还没有写过一门编程语言,还没有做过超大规模的系统……
过去别人说程序员当不了一辈子, 很庆幸我还在写代码,并且仍然留有热情,并且预计这个热情可以持续下去。
那么,趁还有半辈子,可以立个 flag,继续前行,莫忘初衷,是以为记。
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 协议。 一张示意图,正常情况下:
Posts
更好,还是更坏?
变化时时刻刻,缓慢的,或剧烈的,无可避免,就像崔健唱的那样:不是我不明白,这世界变化快。不要说世界,就说自个儿,变化也太快,更重要的是你也不明白这变化是好的,还是坏的。当然,更佛家一点的说法,色即是空,空即是色,世界就是我,我就是世界,所以没有特别必要强调『我』或者『世界』,因为两者是『一体』的。
明显坏的变化,肚子大了,三高来了,今年还犯了一次痛风,要和啤酒海鲜菠菜说再见了。身体在告警:您的余额要不足了,请及时充值。跑步不少,但是欠费更多。
还有个坏的变化,工作上的冲劲似乎没了,叹气的时候多了,旁观的时候多了,憋着话的时候也多了。妥协的多了,抗争的少了。眼看着一头牛要滑向深渊,你得用力拽住、劝慰,再慢慢拉回来。
另一个可能是不好不坏的变化,从无产者变成可疑的有产者(当老毛还挂在城楼上的,我们可能是有产者吗?),心态没那么愤世嫉俗了,关注民生新闻少了,愤怒的次数少了,有『小粉红』的倾向,从全盘西化转向中国人的事情还是要自己解决。某些观点越来越中立,越来越中庸。同样,也可能越来越宽容,大家要和谐,不要搞大新闻。
好的变化也有一些,恢复写博客算是一个,陆续在更新开源库也算一个,读书相比去年也读的多了一点,闲书少了,技术的多了一点。其他的,和儿子关系相处更好了一点,也算是个好变化。
无论是好的,还是坏的,只能接受,因为这就是世界,也就是我。正面的或者负面的,你只能拥抱、亲吻、吵架、小心地劝导、耐心地包容,它们将伴随一生,如影随形,越早承认并坦然接受这一点,生活会更好点。
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>