2017年5月

Node.js 垃圾回收机制

在计算机科学中, 垃圾回收是一种自动的内存管理机制. 当一个电脑上的动态内容不再需要时, 就应该予以释放, 以让出内存, 这种内存资源管理, 成为垃圾回收. 垃圾回收期可以让程序员减轻许多负担, 也减少程序员犯错的机会. 垃圾回收最早用于 LISP 语言, 由 John McCarthy 提出.

常用的两种内存回收机制

引用计数法

引用计数法是最简单的垃圾回收算法. 此算法把 "对象是否不再需要" 简化定义为 "对象有没有被其他对象引用". 如果没有引用指向对象, 对象就被垃圾回收机制回收.

引用计数法的限制在遇到循环引用的情况, 如下:

function f () {
  const o = {}
  const o2 = {}
  o.a = o2  // o 引用 o2
  o2.a = o  // o2 引用 o
}
f()

在该例子中, 由于 oo2 互相引用, 导致他们两者都没办法被回收.

IE 6, 7 使用引用计数法对 DOM 对象进行垃圾回收, 所以容易导致因循环引用产生的内存泄漏问题.

标记清除法 (mark-sweep)

标记清除法是最早开发出的 GC 算法. 标记清除法把 "对象是否不再需要" 定义为 "对象是否可以获得". 它定期的从根 (在 JavaScript 中, 根就是全局对象) 开始将可能被引用的对象用递归的方式进行标记, 然后将没有标记到的对象作为垃圾回收.

标记清除法的限制在于无法从根对象查询到的对象都将被清除, 但是这对于大多数人来说并不是问题.

从 2012 年开始, 所有现代浏览器都使用了标记清除垃圾回收算法. 所有对 JavaScript 垃圾回收算法的改进都是基于标记清除法的改进.

V8 的垃圾回收机制

接下来我们主要讨论下 Node.js, 或者说是 V8 的垃圾回收表现.

在 V8 中, 栈用于存储原始类型和对象的引用, 堆用于存储引用类型如对象, 字符串或闭包. 引用类型在没有引用之后, 会通过 V8 的 GC 自动回收. 值类型如果是处于闭包中, 要等闭包没有引用才会被 GC 回收, 非闭包的情况下等待 V8 的新生代切换的时候回收.

V8 堆的分配

V8 采用了一个分代垃圾回收器, 并将堆又分为了几个不同的区域:

  • 新生区: 存活时间较短的对象, 新分配对象. 新生区大小一般在 1-8 MB. 新生区的垃圾回收非常频繁也非常快.
  • 老生指针区: 包含大多数可能存在指向其他对象的指针的对象. 大多数在新生区存活一段时间之后的对象会被挪到这里.
  • 老生数据区: 存放只包含原始数据的对象(这些对象没有指向其他对象的指针). 字符串, 封箱的数字以及未封箱的双精度数字数组, 在新生区经历一次 Scavenge 后会被移动到这里.
  • 大对象区: 存放体积超过 1MB 大小的对象. 每个对象都有自己 mmap 产生的内存, 垃圾回收器从不移动大对象.
  • Code 区: 代码对象, 也就是包含 JIT 之后指令的对象, 会被分配到这里.
  • Misc 区: 因为存放的是相同大小的元素, 所以内存结构很简单.

leaks-post-021.jpeg

V8 的内存回收算法

V8 分别对新生区和老生区采用不同的垃圾回收算法来提升垃圾回收的效率.

  • Scavenge Collection

    新生代使用半空间(semi-space) 的分配策略, 其中新对象最初分配在新生代的活跃半空间内也叫为 From 区(某些特定类型的对象, 如可执行的代码对象是分配在老生区的).

    在 Scavenge 的具体实现上, 主要采用了 Cheney 算法. 关于 V8 的具体实现, 有两种说法 (不确定哪一种):

    1. 当开始垃圾回收的时候, 会检查 From 区的存活对象, 这些存活对象会被复制到 To 区中, 而非存活对象占用的空间将会被释放. 完成复制后, From 区和 To 区空间的角色发生兑换. 在一定条件下, 存活周期较长的对象会晋升到老生代中. (来源: 《深入浅出 Node.js》)

    2. 一旦 To 区已满, 一个 Scavenge 操作将交换 From 区和 To 区, 然后将 From 区中活跃的对象复制到 To 区或晋升到老生区中. (来源: https://alinode.aliyun.com/blog/14, https://phptouch.com/2016/06/07/does-my-nodejs-application-leak-memory-4/)

      如下图: 蓝色代表存活对象, 灰色代表非存活对象.

      leaks-post-025.jpeg

    新生代对象的 Scavenge 操作的持续时间取决于新生代中存活对象的数量. 在大部分新生代对象存活时间不长的情况下, 一个 Scavenge 操作非常快(< 1ms). 然而, 如果大多数对象都需要被 Scavenge 的时候, Scavenge 操作的持续时间显然会更长.

    Scavenge 操作对于快速回收, 紧缩小片内存效果很好, 但对于大片内存则消耗过大. 因为 Scavenge 操作需要出区和入区两个区域. 老生代所保存的对象大多数是生存周期很长的甚至是常驻内存的对象, 而且老生代占用的内存较多. 因此该算法明显不适合老生代.

    另外, 对于如何判断一个对象是否是存活的, V8 实际上在写缓冲区有一个列表记录所有老生区对象指向新生区的情况. 新对象诞生的时候并不会有指向它的指针, 而当老生区中的对象出现指向新生区对象的指针的时候, 我们便记录下来这样的跨区指向. 这种操作也叫写屏障.

  • Mark-Sweep Collection & Mark-Compact

    在 V8 老生代的垃圾回收采用的是标记清除(Mark-Sweep) 和 标记紧缩(Mark-Compact) 结合的策略. 当老生代的活动对象增长超过一个预设的限制的时候, 将对堆栈执行一个大回收. 老生代垃圾回收使用了 Mark-Sweep 策略, 并采用了几种优化方法来改善延迟和内存消耗. V8 采用了一种增量标记的方式标记活跃对象, 将完整的标记拆分成很多小的步骤, 每做一步旧停下来让 JavaScript 的应用线程执行一会.

    标记完成后, 所有对象都已经被标记, 即不是活跃对象就是死亡对象. 清除时, 垃圾回收期会扫描连续存放的死对象把其变为空闲空间. 这个任务是由专门的清扫线程同步执行. 最后, 为减少老生代对象产生的内存碎片, 还要执行内存紧缩. 内存紧缩可能是非常耗时的, 并且仅当内存碎片成为问题的时候才进行.

    在 Chrome 中, 带内存紧缩的完整垃圾回收只有在 Chrome 空闲足够长的时间才被执行.

- 阅读剩余部分 -

基于 redsocks 实现全局 TCP 流量代理

在 Arch Linux 上使用 Shadowsocks, redsocks, iptables, pcap-dnsproxy 搭建一个舒适的科学上网环境~

对于我来说, 系统安装好之后的第一件事就是翻墙. 我们使用 shadowsocks + redsocks + iptables + pcap-dnsproxy 来实现全局自动代理.

yaourt -S shadowsocks redsocks-git pcap-dnsproxy-git

以上命令同时安装了三个软件. iptables 已经自带不需要安装.

接着到 /etc/shadowsocks 文件夹下创建一个新的配置文件, 配置你的 shadowsocks. 然后

sudo systemctl start shadowsocks@配置文件名
sudo systemctl enable shadowsocks@配置文件名

然后我们配置 redsocks. redsocks 的作用是把全部的 TCP 流量转发到 shadowsocks 上面去, iptables 的话则是拦截这一层, 我们可以在 iptables 配置说如果访问的是中国 ip 则不流向 shadowsocks. 我对 redsocks 了解并不多, 这里只说下我是如何配置的.

redsocks 的配置文件在 /etc/redsocks.conf , 编辑它, 把 redudp 和 dnstc 全部注释掉. redsocks 配置除去注释其实只有这五行:

redsocks {
  local_ip = 127.0.0.1;
  local_port = 31338; # redsocks 运行端口
  ip = 127.0.0.1;
  port = 1080;  # shadowsocks 端口
  type = socks5;
}

根据你的情况需要修改的修改.

接着修改 /etc/iptables/redsocks.rules 文件, 然后把你的 shadowsocks 服务器 IP 地址以及中国 IP 地址加进来, 格式如下:

-A REDSOCKS -d your_ip -j RETURN

国内 IP 列表可以从 apnic 获取得到, 你也可以看我整理的: china.rules

然后, 运行以下命令开启

sudo touch /etc/iptables/iptables.rules # 如果没有这个文件就创建它
sudo systemctl start  redsocks.service iptables.service
sudo systemctl enable  redsocks.service iptables.service
sudo iptables-restore < /etc/iptables/redsocks.rules # 导入规则

然后配置 pcap-dnsproxy. 以上虽然把发往国外的 TCP 流量发到自己的 shadowsocks 服务器上面处理, 这是一种突破 GFW 的方式, 但是这不能避免 DNS 污染的问题, 所以接下来我们配置的 pcap-dnsproxy 来解决.

Arch Linux 安装 pcap-dnsproxy 之后其实我们并不需要做什么, 直接启动就可以用了. 但是要知道的是 pcap-dnsproxy 提供了非常丰富的配置, 你可以自定义, 比如默认 pcap-dnsproxy 是没有局域网解析的, 可以编辑 /etc/pcap-dnsproxy/Config.conf 把 Listen['Operation Mode'] 改为 Server. 如果你需要自定义 Host 的话就修改 /etc/pacp-dnsproxy/Hosts.conf 文件.

直接启动 pcap-dnsproxy

sudo systemctl start pcap-dnsproxy
sudo systemctl enable pcap-dnsproxy

然后编辑 /etc/resolv.conf 文件, 确保首行的 nameserver 为 127.0.0.1. 然后运行

yaourt -S dnsutils # 为了使用 dig 命令
dig www.google.com

如果结果是类似以下形式, 表示 pcap-dnsproxy 已经正常运行.

2017-05-13 15-21-44 的屏幕截图.png

只要看下域名是否被大小写混淆过即可. 如果没有的话可以运行 systemctl status pcap-dnsproxy 或者 journalctl -xe 查看错误日志.

如果还是不行, 可以把 /etc/iptables/redsocks.rules 下所有内容复制粘贴到 /etc/iptables/iptables.rules 下. 然后重启下 iptables.

如果你打开浏览器并且已经可以成功的访问 Google 或者 Y2B, 再访问 ip.cn 这个网站查看当前显示的 IP 是否为你国内的 IP. 如果不是那你的中国 IP 过滤设置还有问题.

- 阅读剩余部分 -

Arch Linux 安装和配置小记

本文章涉及 Arch Linux 的安装, 常用软件和配置. 适用于在 UEFI 上面进行引导安装.

安装

  • 联网

    第一件事当然是联网啦, 如果是无线网络的话使用 wifi-menu.

  • 分区和挂载

    新手安装 Linux 可能会在分区这里感到很疑惑. 其实很简单, 通常我们只考虑为 Linux 分三个或四个区, 三个区的情况分别为 boot 分区, 根分区, home 分区以及交换分区.

    boot 分区是存放启动和引导的; home 分区是存放用户个人文件夹的, 它不是必选的, 但是如果有专门的 home 分区的话以后迁移会很方便; 交换分区主要用途是当虚拟内存用, 如果想要有休眠功能必须要有交换分区(或交换文件), 同样也是可选的; 根分区是必须有的, 以上所有分区都挂载到根分区中.

    Linux 的文件系统格式流行的有两种, 一种是 Ext4, 另一种是 Btrfs. Ext4 是用得最多的. Btrfs 相比 Ext4 支持写时复制以及快照, 子卷等.

    你可以运行 lsblk 命令查看当前的硬盘, 看仔细了别选错了. 假设你要安装的是 /dev/sda 上.

    • 方案一: 使用 Ext4 文件系统

    以下创建三个分区, boot 分区, swap 分区以及 / 分区. 如果需要 home 分区把 / 分区拆成两部分即可.

    parted /dev/sda
    mklabel gpt
    mkpart ESP fat32 1M 513M # Boot 分区
    set 1 boot on
    mkpart primary ext4 513M 8.5G # 交换分区
    mkpart primary ext4 8.5G 100% # 根分区
    quit

    接着运行下格式化以及设置交换分区操作.

    mkfs.fat -F32 /dev/sda1
    mkswap /dev/sda2
    swapon /dev/sda2
    mkfs.ext4 /dev/sda3

    分区就完成了. 接下来我们要进行挂载.

    mount /dev/sda3 /mnt
    mkdir /mnt/{boot,home}
    mount /dev/sda1 /mnt/boot

    这样就完成挂载了.

    • 方案二: 使用 Btrfs 文件系统

    Btrfs 不支持交换分区, 所以就不用设置了, 以下创建两个分区, boot 分区和 / 分区. Btrfs 对 SSD 提供了优化, 如果你没有用 SSD, 那么把后面的挂载参数中的 ssddiscard 都去掉.

    parted /dev/sda
    mklabel gpt
    mkpart ESP fat32 1M 513M # Boot 分区
    set 1 boot on
    mkpart primary btrfs 513M 100% # 根分区
    quit

    接着我们要在根分区创建两个子卷, 具体如下:

    mount -o defaults,ssd,discard,noatime,compress=lzo,space_cache,autodefrag /dev/sda2 /mnt
    cd /mnt
    btrfs subvolume create arch # arch 卷作为 / 分区
    btrfs subvolume create home # home 卷作为 home 分区
    umount /mnt

    接着挂载刚才创建的子卷

    mount -o subvol=arch,defaults,ssd,discard,noatime,compress=lzo,space_cache,autodefrag /dev/sda2 /mnt
    mkdir /mnt/{boot,home}
    mount -o subvol=home,defaults,ssd,discard,noatime,compress=lzo,space_cache,autodefrag /dev/sda2 /mnt/home
    mount /dev/sda1 /mnt/boot

- 阅读剩余部分 -

从 Unix 哲学联系到 Web 开发

这是 5 月 12 号在团队分享的一个话题, 目的是想介绍以下《UNIX 编程艺术》这本书中提到的十七条原则, 但感觉可能过于枯燥, 所以也加入了一些我们在 Web 开发中可以去贯彻的一些做法.

Unix 哲学起源于 Ken Thompson 早期关于如何设计一个服务接口简洁, 小巧精干的操作系统的思考, 随着 Unix 文化在学习如何尽可能发掘 Thompson 设计思想的过程中不断成长, 同时一路上还从其他许多地方博采众长.

Unix 哲学是自下而上的, 而不是自上而下的. Unix 哲学注重实效, 立足于丰富的经验. Unix 哲学中更多的内容不是先哲们口头表述出来的, 而是由他们所作的一切和 Unix 本身所作出的榜样体现出来的. 从整体上来说, 可以概括为一下几点.

  1. 模块原则 (使用简洁的接口拼合简单的部件)

    Brian Kernighan 是《C程序设计语言》的作者, 创作了许多Unix上的程式, 包括在Version 7 Unix上的 ditroff 与 cron. 他曾经说过:"计算机编程的本质就是控制复杂度".

    如何应用: 后端开发良好的目录结构, MVC 架构.

  2. 清晰原则 (清晰胜于机巧)

    在写程序时, 要想到你不是写给执行代码的计算机看的, 而是给人--将来阅读维护源码的人, 包括你自己--看的.

    在 Unix 传统中, 这个建议不仅意味着代码注释. 良好的 Unix 实践同样信奉再选择算法和实现时就应该考虑到将来的可扩展性.

    如何应用: 详细的注释, 考虑扩展性写入配置文件或者常量而不是写死.

  3. 组合原则 (设计时考虑拼接组合)

    如果程序彼此之间不能有效通信, 那么软件就难免会陷入复杂度的泥淖.

    在输入输出方面, Unix 传统极力提倡采用简单, 文本化, 面向流, 设备无关的格式. 要想让程序具有组合性, 就要使程序彼此独立.

    如何应用: JSON

  4. 分离原则 (策略同机制分离, 接口同引擎分离)

    X 系统的设计者再设计中的基本抉择是实行 "机制, 而不是策略" 这种做法, 使 X 成为一个通用图形引擎, 而将用户界面风格留给工具包或者系统的其它层次来决定.

    当编制一个框架时, 框架是机制, 应尽可能少地包含策略. 尽可能将行为分解到使用框架的模块中去.

    如何应用: 前后端分离, 插件机制

  5. 简洁原则 (设计要简洁, 复杂度能低则低)

    来自多方面的压力常常会让程序变得复杂, 其中一种压力就是来自技术上的虚荣心理. 程序员常常以能玩转复杂东西和耍弄抽象概念的能力为傲, 他们常常会与同行们比较, 看看谁能捣鼓出最错综复杂的美妙食物.

    要避免这些陷阱, 唯一的方法就是鼓励另一种软件文化, 以简洁为美, 人人对庞大复杂的东西群起而攻之.

    如何应用: 系统解耦分离为多个子系统, 例如调度系统

  6. 吝啬原则 (除非确无它法, 不要编写庞大的程序)

    首先寻找小巧程序的解决方案, 如果单个小程序无法完成这项工作, 尝试在现有框架结构内构造一个协作小程序工具包来解决问题. 如果两者都失败了, 才可以自由地构建一个巨型程序或一个新框架.

    如何应用: 尽可能使用现有框架或类库, 避免重复造轮子

  7. 透明性原则 (设计要可见, 以便审查和调试)

    软件系统的透明性是指你一眼就能够看出软件是在做什么以及怎么做的. 显见性指程序带有监视和显示内部状态的功能. 处于充分考虑透明性和显见性的目的, 还应该提倡接口简洁, 以方便其它程序对其进行操作.

    透明性和可显性, 主要是设计的特性而不是代码的特性. 这些特性更多与代码中不易规定的特性有关. 如程序调用层次中最大的静态深度? 是否存在太多的特征标志和模式位? 是否容易找到给定函数的代码部分? 代码中有多少个模糊变量等等.

    如何应用: ESLint 规范代码以及约束一些书写方式

  8. 健壮原则 (健壮源于透明与简洁)

    软件的健壮性指软件不仅能在正常的情况下运行良好, 而且在超出设计者设想的意外条件下也能够运行良好.

    要做到程序健壮, 关键的是透明化和简洁化. 人们不需要绞尽脑汁就能够推断出所有可能的情况, 那么这个程序就是简洁的, 越简洁, 越透明, 也就越健壮.

    如何应用: 使用 Joi 校验参数, 避免 property on undefined, 使用安全方法例如 Reflect.defineProperty 替代 Object.defineProperty 等

- 阅读剩余部分 -

Linux I/O 调度器

Linux I/O 调度器

Linux I/O 调度器是控制内核提交读写请求给磁盘的方式. Linux 支持三种 I/O 电梯调度算法, 分别为 CFQ, Noop, Deadline 三种. 简单介绍下这三种.

CFQ (Completely Faire Scheduler)

完全公平调度器 (CFQ) 自 kernel2.6.18 后成为了 Linux 默认的 I/O 调度器, 它也是大多数 Linux 发行版的默认调度器. CFQ 调度器的目的是为所有请求 I/O 操作的进程提供一个公平的磁盘 I/O 带宽分配.

CFQ 调度器为每个进程单独创建一个同步 I/O 队列来管理其同步的请求然后为每个队列分配访问磁盘的时间片. 时间片长度和队列允许提交的请求数取决于其进程的 I/O 优先级. 以此保证每个进程的 I/O 资源占用是公平的.

CFQ 算法的特点是按照 I/O 请求的地址进行排序, 而不是按照先来后到的顺序进行响应. 这样减少了不必要的磁盘寻道, 提高了吞吐量. 但也意味着, 先来的 I/O 请求可能不能尽快得到响应, 可能出现饿死现象.

Deadline

Deadline 调度器主要目的是保证请求的启动服务时间. 它在所有的 I/O 请求上加上一个期限来防止请求饿死. 除了本身提供的 I/O 排队队列之外, Deadline 额外分为为读 I/O 和写 I/O 提供了 FIFO 队列. 排序队列按照按照扇区数排序, FIFO 队列按照截止时间排序. FIFO 队列优先级高于排队队列, 读队列优先级高于写队列. 排序队列使用红黑树结构, 而 FIFO 使用链表结构. 读 FIFO 队列的最大等待时间为 500ms, 写FIFO队列的最大等待时间为 5s.

Deadline 算法会在写队列没有发生饥饿的情况下处理读队列, 在进入读队列处理时, 会首先检查 FIFO 读队列查看是否有过期请求, 如果有就优先处理它. 如果前面写队列出现饥饿, 意即 FIFO 写队列有过期请求, 它就会进入写队列处理然后处理 FIFO 中过期的写请求.

Deadline 本质还是基于 CFQ 的, 只不过加了两个 FIFO 队列来避免饿死问题.

Deadline 调度器更适合用于多线程处理环境和数据库环境. 避免了 CFQ 调度器的饿死问题.

Noop

Noop 调度器非常简单, 它提供了一个最简单的 FIFO 队列并对请求进行简单的合并处理. 当确定不需要根据扇区号来对请求排序时, 这个调度器是非常高效的. 比较适合在 SSD 上面应用.

总的来看, Noop 更适合固态硬盘. Deadline 适合需要优先保证读操作且避免饿死的情况如数据库场景. CFQ 比较复杂, 容易出现饿死问题, 但保证了尽可能的公平. 推荐固态硬盘下可以考虑把默认的 CFQ 调度器改为 Noop 调度器或者 Deadline 调度器.

更换 Linux I/O 调度器

可以通过如下命令查看你当前的 I/O 调度器:

cat /sys/block/sda/queue/scheduler

如果想修改的话, 可以先进入 su 然后执行:

echo noop > /sys/block/sda/queue/scheduler

可以把 noop 换位 cfq 或者 deadline.

这个更改会立即生效, 但重启会失效. 如果想设为永久, 在 Arch Linux 上面可以如下操作:

% cat /etc/udev/rules.d/60-scheduler.rules
# Set deadline scheduler for non-rotational disks
ACTION=="add|change", KERNEL=="sd[a-z]", TEST!="queue/rotational", ATTR{queue/scheduler}="deadline"
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="deadline"

/etc/udev/rules.d/60-scheduler.rules (没有的话新建) 加上上面两行, 然后再运行该命令:

mkinitcpio -p linux

这样就可以了~

- 阅读剩余部分 -