我开发了一个基于 Beancount 的账本托管服务 HostedBeans,欢迎大家来了解纯文本复式记账并试用我的服务。
归档 2016 年 12 月

2016 年度小结

转眼间又一年过去了,说实话,我的 2016 年并不是非常有趣,这一整年我都是一个人住在昆山,显得有些单调,而且因为刚刚写了一篇「20 岁的我在想些什么」,所以可以写在年度小结中的内容实在是不多了。

因为皮蛋豆腐需要照顾,原打算春节就不回家了,但后来计划有变,我提前了一周休假回家,拜托同事帮忙照顾皮蛋豆腐。然后在家待了一周,除夕赶回苏州 —— 再帮忙照顾同事家的两只猫,这也是我第一次一个人过除夕,有了皮蛋豆腐现在并不能很随意的出游算是一个遗憾,虽然自动喂食机和滤水器还是能应付一到两天的。

今年年初皮蛋豆腐分别做了绝育手术,一切也还算顺利。很多人说猫绝育后会变胖,因为我喜欢身手矫健的猫咪,所以我对皮蛋豆腐的体重还是很在意的,于是一整年我都在用厨房秤精确地控制它们的猫粮,最后它们的体重稳定在了 3.5 千克,我还是很满意的。皮蛋豆腐长大成为成年猫咪之后比之前稳重了一些,但依然还是会撕开我的零食、在房间里跳来跳去,所以我还是不得不在一些时候把它们关到阳台上。

今年作息时间规律了一些,虽然依然是晚睡晚起,但相比之前两年还是规律了很多。晚上睡不着觉的问题依旧没什么改善,而第二天早上又要上班,这一度导致在上半年我的平均睡眠时间只有六个半小时;后来下半年因为意识到了视力的重要性,将睡觉时间提前了一些,睡不着就在听一档关于英语学习的播客,效果还不错;今年的最后两个月,我的房子附近开始有非常大的噪音 —— 每天天还没亮就会放半个小时礼炮,令我十分抓狂,如果这种状况持续下去,我可能不得不提前离开苏州了。

三月初和公司一起去了一趟越南芽庄,很久没有这样连续安排一周的行程了,虽然一直在海边,但我除了在水深不超过一米的岸边划了划船之外并没有下水。不过越南的海鲜还是挺好吃的,让我觉得以前不爱吃海鲜大概只是因为没吃到好吃或新鲜的缘故。这也是我第一次离开中国到另外一个国家,让我感到不同国家、不同民族之间,有一些地方是那么地相似,又有一些地方则截然不同。

今年我发现我在线上发展粉丝遇到了一些瓶颈,也很长时间没有结识新朋友了 —— 无论是线上还是线下,这不是一个好现象,于是我尝试去线下活动做演讲来「刷脸」,今年一共在四次线下活动做了技术分享,都是在杭州:Connext、稀土 Meetup,以及 Node Patry 的两次活动。因为之前在公司内部做的几次技术分享,已经有了一些制作幻灯片的经验,但第一次面对台下几十个人的时候还是有点慌,第一次在 Connext 的技术分享效果并不是很好,一方面是我过于紧张,另一方面是选题也显得不是很恰当,但之后的几次活动效果都还不错,也认识了一些新的有趣的朋友。

今年我花了很多时间在听播客上面,我在听 IPN 的「太医来了」、「选美」、「味之道」,也在听「内核恐慌」、「代码时间」、「Teahour」。听着听着总是有自己录一档的冲动,终于在经过了很多的准备工作之后,在八月初将这个想法付诸了行动,也就是后来的「彩排」播客。相比于写文章,录制博客是一种完全不同的体验:你后期修改的空间很小,一旦有错误要么重录要么只能将就,其实彩排的每期博客我都录了三遍以上,以便能剪辑出一个较好的版本;同时它对听众的要求也很高,听众必须拿出相对整块的时间来收听你的博客,而不能像看文章一样语言扫过,而且有听播客习惯的人也相对较少。

在去年年末买了摩卡壶之后,我才意识到咖啡是分成美式和意式的,后来也折腾了不同的咖啡豆,再后来双十二趁着打折还买了一个意式半自动咖啡机。我也花了很多时间在游戏上面,包括年中发布的守望先锋,但更多的还是 Steam 上一些「盖房子」的独立游戏。

在 LeanCloud 中使用 GraphQL

GraphQL 是 FaceBook 开源的一套数据查询语言,也是 Relay 钦定的组件,可以在客户端以一种非常灵活的语法来获取数据,但目前支持 GraphQL 的服务还比较少,最近 GitHub 也宣布了其开放 API 支持了 GraphQL

因为 GraphQL 的支持需要服务器端的更改,因此我选择了在 LeanCloud 的数据服务的基础上用 Node.js 编写一个中间层,运行在云引擎上,将 GraphQL 的查询翻译成对 LeanCloud SDK 的调用,为客户端提供 GraphQL 支持。

我也参考了其他语言和框架的 GraphQL 支持,它们都需要开发者进行很多的开发或配置工作。这是因为无论在 MySQL 还是 MongoDB 中都并没有记录数据之间的关联关系(Id 和 ObjectId 都不会记录指向的表或集合,MySQL 的外键倒是会记录,但可惜用户不多);而且即使你定义了数据之间的关联,你还是需要去定义权限 —— 哪些用户可以访问哪些数据。

而 LeanCloud 的 Relation 和 Pointer 都记录了所指向的 Class,同时 LeanCloud 本身也有一套基于 sessionToken 和 ACL 的权限控制机制,因此我们完全可以做到从 LeanCloud 的数据服务获取数据之间的管理,然后遵循现有的 ACL 来自动实现对 GraphQL 的支持。

leancloud-graphql 就是这样的一个项目,你只需将它部署到云引擎上,不需要改动一行代码,便可以用 GraphQL 查询你在 LeanCloud 上的所有数据。

相较于 RESTful 和 SQL,GraphQL 为数据定义了严格的类型,你可以使用这样一个灵活的语言将数据通过关系组合起来,所见即所得地得到你想要的数据。得益于 GraphQL 的类型系统,你还可以在调试工具(graphql.leanapp.cn)中得到精确的错误提示和补全建议。

例如这里我们查询 Todo 这个 Class 中按优先级排序的前两条数据,获取 title、priority,并将 owner 这个 Pointer 展开:

query {
  Todo(ascending: priority, limit: 2) {
    title, priority, owner {
      username
    }
  }
}

结果:

{
  Todo: [{
    title: "紧急 Bug 修复",
    priority: 0,
    owner: {
      username: "someone"
    }
  }, {
    title: "打电话给 Peter",
    priority: 5,
    owner: {
      username: "someone"
    }
  }]
}

目前 leancloud-graphql 已经实现了 LeanCloud 中大部分的查询参数和查询条件,你可以任意地组合这些条件。例如我们可以查询优先级大于 5 且存在 content 属性的数据:

query {
  Todo(exists: {content: true}, greaterThan: {priority: 5}) {
    title, content, priority
  }
}

GraphQL 最大的亮点还是对关系查询的支持,无论是 Relation 还是 Pointer 你都可以任意地展开,而不必受到 LeanCloud RESTful API 只能展开一层的限制。例如我们查询所有的 TodoFolder 中的 Todo(Relation)并展开 owner(Pointer):

query {
  TodoFolder {
    name,
    containedTodos {
      title, owner {
        username, email
      }
    }
  }
}

结果(省略了一部分):

{
  TodoFolder: [{
    name: "工作",
    containedTodos: [{
      title: "紧急 Bug 修复",
      owner: {
        username: "someone",
        email: "test@example.com"
      }
    }, // ...
    ]
  }, // ...
  ]
}

你也可以在关系查询上附加查询参数或条件。例如我们查询所有 TodoFolder 中优先级最高的一个 Todo:

query {
  TodoFolder {
    name, containedTodos(limit: 1, ascending: priority) {
      title, priority
    }
  }
}

结果:

{
  TodoFolder: [{
    name: "工作",
    containedTodos: [
      {title: "紧急 Bug 修复", priority: 0}
    ]
  },
    name: "购物清单",
    containedTodos: [
      {title: "买酸奶", priority: 10}
    ]
  }, {
    name: "someone",
    containedTodos: [
      {title: "紧急 Bug 修复", priority: 0}
    ]
  }]
}

在实现一对多关系时,我们经常会在「多」上面保存一个到「一」的指针,例如一个 Todo 会有一个叫 owner 的 Pointer 指向用户表。在这时,leancloud-graphql 会自动在用户表上添加一个叫 ownerOfTodo 的属性用来表示这个反向关系,你可以像展开一个 Relation 一样展开这个反向关系,例如我们查询每个用户的 Todo 并展开 title:

query {
  _User {
    username, ownerOfTodo {
      title
    }
  }
}

结果:

{
  _User: [{
    username: "someone",
    ownerOfTodo: [
      {title: "紧急 Bug 修复"},
      {title: "打电话给 Peter"},
      {title: "还信用卡账单"},
      {title: "买酸奶"}
    ]
  }]
}

leancloud-graphql 的简单介绍就到这里,更多使用方法和功能介绍可以在项目的 GitHub 主页上看到,这个项目本身也是开源的。

QPS 和并发:如何衡量服务器端性能

在 LeanCloud 的控制台和文档中大家会接触到「并发连接数(Concurrent Connections)」这个衡量服务器负荷或处理能力的概念,但很多人并不了解什么是并发 —— 甚至在我们团队内部,很多没有服务器端开发经验的工程师对这个词的理解也不是很准确。我们还在继续优化文案来减少用户的困惑,但与此同时不如听我仔细介绍一下并发这个概念。

和并发相关不得不提的一个概念就是 QPS(Query Per Second),QPS 其实是衡量吞吐量(Throughput)的一个常用指标,就是说服务器在一秒的时间内处理了多少个请求 —— 我们通常是指 HTTP 请求,显然数字越大代表服务器的负荷越高、处理能力越强。作为参考,一个有着简单业务逻辑(包括数据库访问)的程序在单核心运行时可以提供 50 - 100 左右的 QPS,即每秒可以处理 50 - 100 个请求,LeanCloud 目前也是按照请求数量进行收费的。

但 QPS 只能粗略地衡量请求的数量,完全不关心服务器处理每个请求的开销。例如一个命中缓存的请求和一个需要进行多次数据库查询的请求的开销可能会有一个数量级的差距,所以 QPS 并不能十分精确地衡量服务器的负载或处理能力,因此我们引入了一个非常抽象的概念 —— 并发。

大部分请求的响应时间在 15 - 30 毫秒左右,这里的响应时间是指服务器处理这个请求所花费的时间,从客户端测量到的时间可能会稍长一些。想象如果服务器上只有一个 CPU 核心在逐个地在处理请求,如果每个请求花费 15 毫秒的话,那么每秒可以处理 66 个请求,也就是我们前面提到的 66 QPS;而如果都是复杂的请求,每个需要 30 毫秒的话,那么服务器就只有 33 QPS 了。可以看到在处理能力不变的情况下(只有一个核心),响应时间越高,QPS 就越低。又如果在响应时间不变的情况下,如果我们增加一个 CPU,QPS 就会翻倍,这三者之间的关系可以简单地描述成:吞吐量(QPS)= 处理能力(CPU)/ 响应时间。

其实 CPU 的数量就是并发最基本的概念,即有多少个 CPU 在工作。当然在实际的服务器端环境中,我们在 CPU 的基础上建立起了进程、线程、协程这样复杂的抽象、通过异步的 IO 提高 CPU 的利用率 —— 当需要从硬盘或网络读取数据时,CPU 会去做其他工作,所以并发和 CPU 的比值会比 1 高一些,IO 越多,这个比值会越高。

这时我们可以观测到的并发数就是服务器在同时处理多少个请求,也即「并发连接数」。对于 Web 后端的场景来说(而不考虑推送等长链接的场景),我们希望尽快地给客户端响应,所以请求在服务器端花费的几十毫秒中每一毫秒都是必不可少的:可能是在进行计算、也可能是在向磁盘或网络读写数据,都在占用着服务器的资源,因此并发依然是衡量服务器负荷和处理能力的关键指标。

除了并发本身,我们还经常提到「最大并发」的概念,最大并发就是在单位时间(通常是一天)里并发最高的那一刻有多少个 CPU 在为你工作。大部分应用的请求量并不是均匀地分布在一天中的,因为用户们往往会集中在傍晚的几个小时中使用手机,这些时段中的请求量要远远高于凌晨。所以人人都希望在傍晚得到更多的计算能力,但遗憾的是这些计算能力需要原子世界中的 CPU 去支持,你不可能在傍晚购买一批服务器然后在凌晨卖掉(当然,这其实是云计算要解决的问题),所以为了支撑傍晚的高并发,我们必须去准备那么多的服务器、必须在凌晨让很多服务器闲置,因此其实我们只关心一天中最高的并发数 —— 这代表了我们需要采购多少硬件资源。

当然,LeanCloud 的存在就是为了帮助开发者减轻维护后端的负担,应用开发者往往更关注的是「我有 100 万用户对应多少并发」。但这个问题往往得不到一个答案,因为有太多的因素在影响着最后的结果,例如你的 100 万用户中可能并不是每个人每天都会打开你的应用(每日活跃用户比例);而且用户对于不同类型的应用使用的频率也并不相同(平均打开次数);不同类型的应用在工作期间发起的请求数量也不相同(平均请求数量);对于不同类型的请求,需要占用服务器的计算能力同样不同(平均响应时间);最后还要考虑到你的大部分用户会集中在傍晚的几个小时使用你的应用,对于游戏抽奖、电商秒杀之类的场景,用户会更加集中在几分钟内使用你的应用。前些天我根据这些指标编写了一个简单的计算器(https://budget.leanapp.cn),将最大并发数的计算抽象为了前面提到的几个指标,如果你能给每个指标一个相对准确的估算,那么就可以计算出一个可供参考的并发数。

精子生于 1995 年,英文 ID jysperm.

订阅推送

通过 Telegram Channel 订阅我的博客日志、产品和项目的动态:

王子亭的博客 @ Telegram


通过邮件订阅订阅我的博客日志、产品和项目的动态(历史邮件):

该博客使用基于  Hexo  的  simpleblock  主题。博客内容使用  CC BY-NC-ND  授权发布。最后生成于 2023-12-20.