Author: 逸飞

strace

尽管在线下会做充足的测试,但是线上出问题是难免的。当我们的程序在线上运行中遇到问题的时候,而我们又没有日志可以观察到底哪里出了问题,这时候可以使用 strace 命令。strace 可以直接根据 pid 附着到进程上,打印出进程的一些统计信息,为排查 bug 提供有意义的参考。

Strace 的选项

  1. c – See what time is spend and where (combine with -S for sorting)
  2. f – Track process including forked child processes
  3. o my-process-trace.txt – Log strace output to a file
  4. p 1234 – Track a process by PID
  5. P /tmp – Track a process when interacting with a path
  6. T – Display syscall duration in the output

Track by specific system call group

  1. e trace=ipc – Track communication between processes (IPC)
  2. e trace=memory – Track memory syscalls
  3. e trace=network – Track memory syscalls
  4. e trace=process – Track process calls (like fork, exec)
  5. e trace=signal – Track process signal handling (like HUP, exit)
  6. e trace=file – Track file related syscalls

Trace multiple syscalls

strace -e open,close

统计进程系统调用花费时间

strace -c -f -p 11084

一般来说要加上 -f 选项,这样才能跟踪多进程程序,也就是 fork 之后的进程。

strace -o output.txt -T -tt -e trace=all -p 28979

使用和这个命令可以统计每一个系统调用的时间

  1. 使用 struss 查看系统问题 https://www.ibm.com/developerworks/cn/linux/l-tsl/index.html
  2. strace 的详细用法 https://blog.csdn.net/uisoul/article/details/83143290
  3. https://blog.csdn.net/budong282712018/article/details/83151953
  4. https://blog.csdn.net/maple_leaves_for_me/article/details/42391979
  5. https://blog.51cto.com/5iwww/771031
  6. https://blog.csdn.net/lotluck/article/details/77943152
  7. https://linux-audit.com/the-ultimate-strace-cheat-sheet/

使用 caddy 运行 php

使用 caddy 运行 PHP

caddyfile

example.com {
    gzip
    root /srv
    fastcgi / 127.0.0.1:9000 php # php variant only
    on startup php-fpm7 # php variant only
}

example.com 记得改成自己的域名

docker-compose.yml

version: "3"

services:
  caddy:
    image: abiosoft/caddy:php
    environment:
      ACME_AGREE: 1
    volumes:
      - "/etc/Caddyfile:/etc/Caddyfile"
      - "/etc/caddy:/root/.caddy"
      - "/var/www/html:/srv"
    ports:
      - "80:80"
      - "443:443"
      - "2015:2015"

参考

  1. http://blog.extlux.com/2018/08/06/docker-%E8%BF%90%E8%A1%8C%E5%B8%A6php%E7%9A%84caddy/

基于 koa 的微服务

使用 JavaScript 构建微服务时候似乎没有什么多余的选择,毕竟 grpc 都还没有合适的解决方案。所以还是采用 HTTP + JSON 的方式啦。 这里我选择了 koa 这个框架,可以很好地使用 Promise 和 async/await 的语法,再也不用写一大堆回调了,写起来还能轻松一些。

koa 作为一个微框架,简直是把 node 生态圈 isArray 都要写个库这种做法发挥到极致了。 默认情况下,koa 连 POST 过来的 JSON 都不给解析出来。所以几乎任何操作都需要安装对应的库,下面总结一下必备的一些库。

解析 JSON

使用 koa-bodyparser: https://github.com/koajs/bodyparser

解析命令行参数

使用 Command Line Args: https://github.com/75lb/command-line-args/wiki/Typical-usage-example

参考文献

  1. https://koajs.com/#request

使用 Puppeteer

在服务器上部署 puppeteer 现在有两个问题:

  1. 如何打包 data-dir 上去
  2. 部署使用 Docker 还是直接手工跑

puppeteer 的相关资料:

  1. API 文档。https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md
  2. Browserless 的 Docker 镜像。https://docs.browserless.io/docs/docker-quickstart.html
  3. PP Cluster。https://github.com/thomasdondorf/puppeteer-cluster
  4. Awesome Puppeteer。https://github.com/transitive-bullshit/awesome-puppeteer
  5. GitHub Topic。https://github.com/topics/puppeteer

无头浏览器和 Puppeteer 的一些最佳实践

在做爬虫的时候,总会遇到一些动态网页,他们的内容是 Ajax 加载甚至是加密的。虽然说对于一些大站来说,分析接口是值得的,但是对于众多的小网站来说,一个一个分析接口太繁琐了,这时候直接使用浏览器渲染就简单得多了。

以往比较流行的是 selenium + phantomjs 的组合,不过在自从 Google 官方推出了谷歌浏览器的无头模式和 puppeteer 这个库以后,稳定性和易用度都大幅得到了提升,本文也主要探讨谷歌浏览器和 puppeteer。另外 puppeteer 也有第三方的 Python 移植,叫做 pyppeteer,不过这个库目前来看不太稳定(个人使用体验)。另外 pyppeteer 这个库使用了 asyncio,如果你的爬虫使用的是普通的同步语法,那么也还是不方便调用 pyppeteer 这个库,个人建议还是使用官方的 node 版 puppeteer,如果需要在 Python 中调用,直接调用 node 然后渲染就可以了。

browserless 是一家在提供云端浏览器渲染服务的公司,本文翻译了他们关于如何提升无头浏览器稳定性和性能的两篇文章并添加了本人在使用过程中遇到的一些问题和经验总结。browserless 的两篇原文链接在最后。

不要使用无头浏览器

Headless Chrome 占用大量的资源。无论如何,只要可以的话,不要运行无头浏览器。特别是千万别在你跑其他应用的服务器上跑。无头浏览器的行为难以预测,对资源占用非常多,就像是 Rick and Morty 里面的 Meseeks(美国动画片《瑞克和莫蒂》中,召唤出了过多的 Meseeks 导致出了大问题)。几乎所有你想通过浏览器用的事情(比如说运行 JavaScript)都可以使用简单的 Linux 工具来实现。Cheerio 和其他的库提供了优雅的 Node API 来实现 HTTP 请求和采集等需求。

比如,你可以像这样获取一个页面并抽取内容:

import cheerio from 'cheerio';
import fetch from 'node-fetch';
 
async function getPrice(url) {
    const res = await fetch(url);
    const html = await res.test();
    const $ = cheerio.load(html);
    return $('buy-now.price').text();
}
 
getPrice('https://my-cool-website.com/');

显然这肯定不能覆盖所有的方面,如果你正在读这篇文章的话,你可能需要一个无头浏览器,所以接着看吧。

使用 docker 来管理 Chrome

在 Linux 上跑 Chrome 的话,很可能连字体渲染都没有,还要安装好多的依赖。Chrome 除了浏览之外,还会有好多的莫名其妙的线程,所以最好使用 docker 来管理。建议使用 browserless/chrome 这个镜像,这个镜像是 browserless 这家专门做 Chrome 渲染的公司在生产环境中使用的镜像。关于这个镜像的文档在这里:https://docs.browserless.io/docs/docker.html (英文)

docker run -p 8080:3000 --restart always -d --name browserless browserless/chrome
const puppeteer = require('puppeteer');
 
    // 从 puppeteer.launch() 改成如下
    const browser = await puppeteer.connect({ browserWSEndpoint: 'ws://localhost:3000' });
    const page = await browser.newPage();
 
    await page.goto('http://www.example.com/');
    const screenshot = await page.screenshot();
 
    await browser.disconnect();

保持 Chrome 在运行状态

当负载很高的情况下,Chrome 启动可能会花上好几秒钟。对大多数情况来说,我们还是希望避免这个启动时间。所以,最好的办法就是预先启动好 Chrome,然后让他在后台等着我们调用。

如果使用 browserless/chrome 这个镜像的话,直接指定 PREBOOT_CHROME=true 就好了。下面的命令会直接启动 10 个浏览器,如果你指定 KEEP_ALIVE,那么在你断开链接(pp.disconnect)的时候也不会关闭浏览器,而只是把相关页面关闭掉。

docker run -d -p 3000:3000 \
    -e DEBUG=browserless* \
    -e PREBOOT_CHROME=true -e MAX_CONCURRENT_SESSIONS=10 -e KEEP_ALIVE=true
    --name browserless browserless/chrome:latest

page.evaluate 是你的好朋友

Puppeteer 有一些很酷的语法糖,比如可以保存 DOM 选择器等等东西到 Node 运行时中。尽管这很方便,但是当有脚本在变换 DOM 节点的时候很可能坑你一把。尽管看起来有一些 hacky,但是最好还是在浏览器中运行浏览器这边的工作。也就是说使用 page.evaluate 来操作。

比如,不要使用下面这种方法(使用了三个 async 动作):

const $anchor = await page.$('a.buy-now');
const link = await $anchor.getProperty('href');
await $anchor.click();
 
return link;

这样做,使用了一个 async 动作:

await page.evaluate(() => {
    const $anchor = document.querySelector('a.buy-now');
    const text = $anchor.href;
    $anchor.click();
});

另外的好处是这样做是可移植的:也就是说你可以在浏览器中运行这个代码来测试下是不是需要重写你的 node 代码。当然,能用调试器调试的时候还是用调试器来缩短开发时间。

最重要的规则就是数一下你使用的 await 的数量,如果超过 1 了,那么说明你最好把代码写在 page.evaluate 中。原因在于,所有的 async 函数都必须在 Node 和 浏览器直接传来传去,也就是需要不停地 json 序列化和反序列化。尽管这些解析成本也不是很高(有 WebSocket 支持),但是总还是要花费时间的。

除此之外,还要牢记使用 puppeteer 的时候是由两个 JS 的执行环境的,别把他们搞混了。在执行 page.evaluate 的时候,函数会先被序列化成字符串,传递给浏览器的 JS 运行时,然后再执行。比如说下面这个错误。

const anchor = 'a';
 
await page.goto('https://example.com/');
 
// 这里是错的,因为浏览器中访问不到 anchor 这个变量
const clicked = await page.evaluate(() => document.querySelector(anchor).click());

修改方法也很简单,把这个参数作为变量传递给 page.evaluate 就可以了。

const anchor = 'a';
 
await page.goto('https://example.com/');
 
// Here we add a `selector` arg and pass in the reference in `evaluate`
const clicked = await page.evaluate((selector) => document.querySelector(selector).click(), anchor);

队列和限制并发

browserless 的镜像一个核心功能是无缝限制并行和使用队列。也就是说消费程序可以直接使用 puppeteer.connect 而不需要自己实现一个队列。这避免了大量的问题,大部分是太多的 Chrome 实例杀掉了你的应用的可用资源。

$ docker run -e "MAX_CONCURRENT_SESSIONS=10" browserless/chrome

上面限制了并发连接数到10,还可以使用MAX_QUEUE_LENGTH来配置队列的长度。总体来说,每1GB内存可以并行运行10个请求。CPU 有时候会占用过多,但是总的来说瓶颈还是在内存上。

不要忘记 page.waitForNavigation

如果点击了链接之后,需要使用 page.waitForNavigation 来等待页面加载。

下面这个不行

await page.goto('https://example.com');
await page.click('a');
const title = await page.title();
console.log(title);

这个可以

await page.goto('https://example.com');
page.click('a');
await page.waitForNavigation();
const title = await page.title();
console.log(title);

屏蔽广告内容

browserless 家的镜像还有一个功能就是提供了屏蔽广告的功能。屏蔽广告可以是你的流量降低,同时提升加载速度。

只需要在连接的时候加上 blockAds 参数就可以了。

启动的时候指定 –user-data-dir

Chrome 最好的一点就是它支持你指定一个用户的数据文件夹。通过指定用户数据文件夹,每次打开的时候都可以使用上次的缓存。这样可以大大加快网站的访问速度。

const browser = await pp.launch({
    args: ["--user-data-dir=/var/data/session-xxx"]
})

不过需要注意的是,这样的话会保存上次访问时候的 cookie,这个不一定是你想要的效果。

构建最小版本的 Chrome

构建最小版本的 Chromium

为什么需要 Chrome 浏览器渲染

  1. 动态 ajax 页面
  2. 页面编码异常或者结构过甚,lxml 无法解析

dirty page examples:

  1. 页面的 style 在 html 外面,并且有黏贴的 Word 文档。http://gzg2b.gzfinance.gov.cn/gzgpimp/portalsys/portal.do?method=pubinfoView&&info_id=-2316ce5816ab90783eb-720f&&porid=gsgg&t_k=null
  2. 有好多个 html 标签,并且编码不一致。http://www.be-bidding.com/gjdq/jingneng/show_zbdetail.jsp?projectcode=1180903010&flag=3&moreinfo=true

优化方案

  1. 不加载图片和视频,但是保留占位
  2. 使用 proxy api 更改代理
  3. 禁用 H5 相关 API
  4. 删除 ICU 相关

参考文献

  1. https://peter.sh/experiments/chromium-command-line-switches/
  2. https://joydig.com/port-chromium-to-embedded-linux/
  3. Android 上的 Chrome 裁剪,值得借鉴。https://blog.csdn.net/mogoweb/article/details/76653627
  4. 架构图 https://blog.csdn.net/mogoweb/article/details/76653627
  5. webkit 架构图 https://blog.csdn.net/a957666743/article/details/79702895
  6. Chrome proxy API https://developer.chrome.com/extensions/proxy
  7. Chrome 嵌入式裁剪,直击底层 https://joydig.com/category/chromium/
  8. 官方构建教程 https://chromium.googlesource.com/chromium/src/+/master/docs/linux_build_instructions.md
  9. 编译选项https://blog.csdn.net/wanwuguicang/article/details/79751503