从今年年初开始,我就尝试在业余时间和一个朋友开发一个容器平台,更多地是实验一些新的技术,也希望能够通过它将自己的一些小应用管理起来,在基本完成后可能会考虑开源。之所以说是实验是因为我选择了一个我几乎完全不了解的技术栈:主要编程语言是 Golang、只使用 Etcd 作为数据库、基于 Docker Swarm 管理容器。
不得不说 Golang 是一个非常难用的语言,在语言层面,为了所谓的「简单」而没有添加 异常 和 泛型 这两个对于高级编程非常重要的特性;在生态上仍没有统一出一个包管理器,如果只发布编译好的二进制程序倒是没问题,但如果发布源代码的话,缺少统一的包管理会带来很多麻烦,以至于很多开发者选择将 vendor 直接包含在版本控制中。
在这个项目中,没有异常和泛型真的给我带来了很大的困扰,几乎一半的代码都在进行繁琐的错误检查,没有泛型则很难实现一些通用的函数,或者不得不进行强制类型转换。这让我觉得 Golang 的使用场景非常受限:因为有 GC,它难以胜任对实时性要求较高的底层的工作;又因为缺少高层次的抽象手段,不适合业务逻辑复杂的应用编程(例如 Web 后端),可以说不上不下,只适合于一些业务逻辑不复杂的中间件,或者一些客户端命令行工具(毕竟在三个平台下都没有运行时依赖)。
Etcd 是一个我之前没有接触过的数据库类型,它是分布式的键值数据库,可以在大多数节点存活的情况下保证读写的强一致性,也提供了事务、订阅修改、TTL、检索历史快照等功能。我在这个项目中直接使用 Etcd 作为唯一的数据库存储所有数据,也使用 Golang 对 Etcd 的 API 进行了简单的封装,以便更好地使用 JSON 和 Etcd 的事务。
因为毕竟是业余项目,这个项目一直进展缓慢,在今年的最后我还尝试在 Swarm 上实现高可用的有状态容器,例如 Redis 和 MongoDB。我在容器内用 Shell 编写了一系列的脚本,在启动时从 Etcd 获取集群信息和自己的角色,然后通过长轮询完成配置的切换,再运行一个 Nginx 将从节点的流量转发给主节点,容器的数量则由 Swarm 保证,实现了一个「自维护」的数据库容器。
在去年 Node.js 错误处理实践 的基础上,今年我又在继续探索错误处理和日志的最佳实践。之前的方法存在一个问题,即我特别关注于将错误对象原样地传递出去,但有时看到一个非常底层、非常细节的错误(例如 CONNTIMEOUT),则难以判断究竟发生了什么。虽然从异常的调用栈中可以看出调用路径,但并不能看到一些关键变量的值,例如这个连接错误是在请求哪个地址,主要参数是什么,这是因为在异常传递的过程中,我们并没有记录这个信息。最后只能得到一个非常细节的错误信息,而不知道这个错误发生在更上层的哪个环节。
于是我开始使用 verror 这个库,它最主要的功能是帮助你创建一个「异常链」,你可以在每个层级来向异常上补充路径信息(会被反映到 err.message
例如一个来自底层的错误信息可能是 request Failed: Failed to stat "/junk": No such file or directory
这样)。这个异常链信息也会和其他元信息一起以结构化的方式存储在错误对象上,这个库也提供了一些工具函数来获取这些结构化信息。我尝试使用 verror 来管理所有的异常,报告带有详细的、每一层级信息的异常。同时我也会向错误对象上附加一些元信息用来指示如何响应客户端、是否需要发到 Sentry、是否可以重试等。
除了异常,我也开始尝试使用 bunyan 打印结构化的日志,并存储到 Elasticsearch。通过 Kibana 的 Web UI 可以很简单地对日志进行筛选和查询,在排查问题时找到相关的那部分日志。对于一个既有的系统来说,调整异常和日志可以说是一个非常庞杂的工作,在调整的过程中也我也在不断地修正自己的实践,今年一整年我都在做这样的尝试。
对于一个稍微复杂一点的项目来说,并不是所有的数据都在事务的保护下 —— 其实很多互联网项目也并不会使用事务。这样就难免出现数据不一致的情况,这种不一致可能是数据的关系出现损坏、缓存和数据不一致,也可能是多种数据库甚至外部资源的状态没有同步。
今年我探索了解决这个问题的一种实践:编写脚本去自动地检查和恢复这种不一致,这种脚本是常态化运行的,例如我的一个项目中现在有 4 个脚本以每 10 分钟左右的频率在进行各种检查和恢复。这样不一致的数据会在很短的时间内被恢复(也会留下可查的记录),对于用户来说就是碰到问题的次数变少了,在一些重大的的故障发生时,这种脚本也可以帮助你快速地恢复服务。
这样自动地修复不一致也引入了一个问题:就是在核心业务中会不自觉地降低对一致性的追求 —— 反正有脚本来修复,问题不会暴露出来。目前只能是为检查和恢复的情况绘制图表,在不一致的频率超出预期时及时地发现。
因为云引擎的 负载均衡 逻辑比较复杂,之前是在一个开源的 Node.js 反向代理组件上进行了一些二次开发,但在高峰时的性能不是很理想,一直有想法换成 Nginx。于是今年年初我就开始基于 Openresty 用 Lua 重写了负载均衡组件,效果非常理想,只用了 Node.js 十分之一的 cpu 和内存,再也没有出现容量不足的情况。
原因当然是 Nginx 对内存有着非常细粒度的管理,只在请求开始和结束时申请和释放整块内存,也没有 GC,保持一个长链接几乎不需要消耗多少资源。Openresty 则将 Lua 嵌入到了 Nginx 中,在 Nginx 高性能的请求处理和丰富的 HTTP 功能的基础上,让你可以用 Lua 去实现一些逻辑,对于负载均衡肯定是够用了。
我之前一直有在使用 pass 这个基于 GPG 和 Git 的命令行密码管理器,并将密码仓库托管在 GitHub 上。之所以用它是因为它基于可靠的开源工具、本身也是开源的,同时它足够简单,简单到我不需要它也可以操作我的密码。
也一直有想法为它开发一个 UI,于是今年九月我用 Electron 开发了一个名为 Elecpass 的密码管理器,在机制和数据格式上与 pass 完全兼容。之前其实我并没有用过 Electron,但上手的体验还是相当不错的,没有遇到什么问题。因为 Electron 自带了 commonjs 的模块加载系统,也不再需要像前端开发那样复杂的构建过程。
目前 Elecpass 一共发布了两个版本,虽然还非常简陋而且有一些 Bug,但已经可以满足基本需求了,我自己也一直在使用,明年我应该会为它添加更多的功能。
今年年初腾讯发布了微信小程序,我代表公司在「小小程序,大有作为」的线下活动里做了一个主题为「在微信小程序中使用 LeanCloud」的分享,在准备期间我也了解了一下微信小程序。
可以说微信小程序就是腾讯为了在微信中构建一个封闭的「操作系统」的产物,但大家迫于微信本身的平台能力,比如用户信息、推送、支付,不得不使用它。作为一个平台,微信小程序绑定了一个数据绑定框架,也绑定了一套模板语言,同时和前端现有的工具链(编译打包)的整合也非常差,很难利用现有的 JavaScript 生态。作为结果,我相信微信小程序不会有什么技术层面的社区和生态,只能作为最末端的用户界面。
年初因为发现我司的 服务状态页 年久失修,我决心重写一个服务状态页,参考一下 GitHub 等网站。我希望它能同时展示三个节点的状态、能够展示过去一天的历史状态、允许运维同事在服务状态页上快速地发布通知。最后这个状态页也开源了出来,在 leancloud/leancloud-status。
为了能够让服务状态页本身总是保持可用,我设计了一个比较有趣的架构:后端(检查器)分别运行在我们三个节点的云引擎上,交叉对所有节点进行检查,将结果和展示历史图表所需要的数据写入到 S3(或其他对象存储上);状态页面作为静态页面托管在 CDN 上,从 S3 分别拉取三个节点的检查结果和历史图表数据,对来自三个节点的数据进行汇总,决定显示为「正常」还是「故障」。
这样就保证了服务状态页本身的可用性和三个节点隔离,可用性仅依赖于 S3(理论上可以同时写入多个对象存储作为热备),检测程序又运行在我们自己的云引擎上(比单独部署在一台机器上更易于维护),架构又并不复杂。
为了在前端合并三个节点的时序数据并绘制图表,我其实是费了很大的功夫的,但在实际部署的过程中遇到了很多细节的问题,做了很多妥协。例如我们的美国节点到国内的访问一直不畅等等,最后并没有把我制作的历史图表展示出来。
之前几个北京的同事写了一个 聊天机器人 放在公司的 IM 上,每天看他们调戏机器人觉得挺幼稚的。但等我搬到北京之后也加入了他们的队伍,我给机器人加了几个有趣的功能,虽然实现上并不复杂,但你可以通过聊天的方式把它展示给别人看,也可以让别人参与人来,还是个非常有意思的事情。
首先我写了一个 帮助大家决定晚上吃什么 的功能,这一写我就来了兴趣,后来又写了 确认大家是否都准备好吃晚饭了、帮助运维同事简单地更新服务状态页,还 为公司免费午餐的福利随机人选。